Web Highlighter

Highlight selected text, saves locally, and edit or delete highlights

目前為 2024-12-02 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Web Highlighter
  3. // @author Damodar Rajbhandari
  4. // @namespace physicslog.com.web-highlighter
  5. // @version 1.32
  6. // @description Highlight selected text, saves locally, and edit or delete highlights
  7. // @match *://*.wikipedia.org/*
  8. // @grant none
  9. // @noframes
  10. // ==/UserScript==
  11.  
  12. // @note: Please read any news or bugs at https://github.com/physicslog/web-highlighter.user.js
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const colors = ['#E5AE26', '#B895FF', '#54D171', '#D02848'];
  18. const colors_title = ['Introduction / General / Well-known', 'Important / Spectacle / Interesting', 'Answer / Hint / Idea', 'Question / Critical / Hard / Sceptical'];
  19. let selectedColor = colors[0];
  20.  
  21. // Load highlights from local storage
  22. const highlights = JSON.parse(localStorage.getItem('highlights') || '[]');
  23. // Adds a yellow dot at the top right corner of the webpage if highlights is present.
  24. if (highlights.length !== 0) {
  25. const dot = document.createElement('div');
  26. dot.title = 'Highlights exists! To view: do cmd+shift+v in the Mac';
  27. dot.style.width = '10px';
  28. dot.style.height = '10px';
  29. dot.style.backgroundColor = '#E5AE26';
  30. dot.style.borderRadius = '50%';
  31. dot.style.position = 'fixed';
  32. dot.style.top = '5px';
  33. dot.style.right = '5px';
  34. dot.style.zIndex = '1000';
  35. document.body.appendChild(dot);
  36. }
  37.  
  38. console.log("Loaded highlights:", highlights);
  39. highlights.forEach(hl => {
  40. if (hl.url === window.location.href) {
  41. console.log("Restoring highlight:", hl);
  42. restoreHighlight(hl);
  43. }
  44. });
  45.  
  46. // Save highlights to local storage
  47. function saveHighlights() {
  48. const serialized = Array.from(document.querySelectorAll('.highlighted')).map(el => ({
  49. text: el.innerText,
  50. color: el.style.backgroundColor,
  51. parentPath: getElementXPath(el.parentElement),
  52. url: window.location.href,
  53. timestamp: new Date().toISOString()
  54. }));
  55. console.log("Saving highlights to local storage:", serialized);
  56. localStorage.setItem('highlights', JSON.stringify(serialized));
  57. }
  58.  
  59. // Restore a highlight
  60. function restoreHighlight({ text, color, parentPath }) {
  61. const parentElement = getElementByXPath(parentPath);
  62. if (parentElement) {
  63. const nodes = Array.from(parentElement.childNodes);
  64. nodes.forEach(node => {
  65. if (node.nodeType === Node.TEXT_NODE && node.nodeValue.includes(text)) {
  66. const range = document.createRange();
  67. range.setStart(node, node.nodeValue.indexOf(text));
  68. range.setEnd(node, node.nodeValue.indexOf(text) + text.length);
  69. wrapHighlight(range, color);
  70. console.log("Highlight restored for text:", text);
  71. }
  72. });
  73. } else {
  74. console.error("Parent element not found for XPath:", parentPath);
  75. }
  76. }
  77.  
  78. // Highlight selected text
  79. function wrapHighlight(range, color) {
  80. const span = document.createElement('span');
  81. span.style.backgroundColor = color;
  82. span.classList.add('highlighted');
  83. range.surroundContents(span);
  84. }
  85.  
  86. // Get an XPath to an element
  87. function getElementXPath(element) {
  88. const paths = [];
  89. while (element && element.nodeType === Node.ELEMENT_NODE) {
  90. let index = 0;
  91. let sibling = element.previousSibling;
  92. while (sibling) {
  93. if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
  94. index++;
  95. }
  96. sibling = sibling.previousSibling;
  97. }
  98. const tagName = element.nodeName.toLowerCase();
  99. const pathIndex = index ? `[${index + 1}]` : '';
  100. paths.unshift(`${tagName}${pathIndex}`);
  101. element = element.parentNode;
  102. }
  103. return paths.length ? `/${paths.join('/')}` : null;
  104. }
  105.  
  106. // Retrieve an element by its XPath
  107. function getElementByXPath(xpath) {
  108. try {
  109. return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  110. } catch (e) {
  111. console.error("Error evaluating XPath:", xpath, e);
  112. return null;
  113. }
  114. }
  115.  
  116. // Event listener for text selection and highlighting
  117. document.addEventListener('mouseup', () => {
  118. const selection = window.getSelection();
  119. if (!event.target.closest('#displayHighlightsPopUp')) { // ignore if that is a popup to list all the highlights
  120. if (selection.rangeCount > 0) {
  121. const range = selection.getRangeAt(0);
  122. const selectedText = selection.toString().trim();
  123. if (selectedText.length > 0) {
  124. wrapHighlight(range, selectedColor);
  125. saveHighlights();
  126. selection.removeAllRanges();
  127. }
  128. }
  129. }
  130. });
  131.  
  132. // Event listener for clicking on existing highlights
  133. document.addEventListener('click', (event) => {
  134. if (event.target.classList.contains('highlighted')) {
  135. createPopup(event.target);
  136. }
  137. });
  138.  
  139. // Popup for editing or deleting highlights
  140. function createPopup(element) {
  141. const popup = document.createElement('div');
  142. popup.style.position = 'absolute';
  143. popup.style.background = 'rgba(255, 255, 255, 0.9)';
  144. popup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
  145. popup.style.border = '1px solid #ccc';
  146. popup.style.borderRadius = '8px';
  147. popup.style.padding = '5px';
  148. popup.style.zIndex = '1001';
  149. popup.style.transition = 'all 0.3s ease';
  150.  
  151. // Color selection buttons to popup
  152. const buttonContainer = document.createElement('div');
  153. buttonContainer.style.display = 'flex';
  154. colors.forEach((color, index) => {
  155. const colorButton = document.createElement('button');
  156. colorButton.style.backgroundColor = color;
  157. colorButton.style.width = '20px';
  158. colorButton.style.height = '20px';
  159. colorButton.style.margin = '2px';
  160. colorButton.style.borderRadius = '50%';
  161. colorButton.style.border = 'none';
  162. colorButton.style.cursor = 'pointer';
  163. colorButton.title = colors_title[index];
  164. colorButton.onclick = () => {
  165. element.style.backgroundColor = color;
  166. saveHighlights();
  167. };
  168. buttonContainer.appendChild(colorButton);
  169. });
  170.  
  171. // Add "X" button to popup
  172. const closeButton = document.createElement('button');
  173. closeButton.title = 'Delete';
  174. closeButton.innerText = '\u00D7';
  175. closeButton.style.width = '20px';
  176. closeButton.style.height = '20px';
  177. closeButton.style.margin = '2px';
  178. closeButton.style.lineHeight = '15px';
  179. closeButton.style.textAlign = 'center';
  180. closeButton.style.border = 'none';
  181. closeButton.style.backgroundColor = 'gray';
  182. closeButton.style.color = 'white';
  183. closeButton.style.fontWeight = 'bold';
  184. closeButton.style.borderRadius = '50%';
  185. closeButton.style.cursor = 'pointer';
  186. closeButton.onclick = () => {
  187. const parent = element.parentNode;
  188. parent.replaceChild(document.createTextNode(element.innerText), element);
  189. saveHighlights();
  190. document.body.removeChild(popup);
  191. };
  192.  
  193. buttonContainer.appendChild(closeButton);
  194. popup.appendChild(buttonContainer);
  195. document.body.appendChild(popup);
  196.  
  197. // Position the popup near the element
  198. const rect = element.getBoundingClientRect();
  199. popup.style.left = `${rect.left}px`;
  200. popup.style.top = `${rect.bottom + window.scrollY + 10}px`;
  201.  
  202. // Remove popup when clicking outside
  203. const outsideClickListener = (event) => {
  204. if (!popup.contains(event.target)) {
  205. document.body.removeChild(popup);
  206. document.removeEventListener('click', outsideClickListener);
  207. }
  208. };
  209. document.addEventListener('click', outsideClickListener);
  210. }
  211. // Another popup to show all the highlighted text on the present webpage
  212. // Get the XPath of the element
  213. function getElementByXPath(xpath) {
  214. return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  215. }
  216.  
  217. // Highlight the target element
  218. function flashingElement(element, color) {
  219. const originalBackgroundColor = element.style.backgroundColor;
  220. element.style.backgroundColor = color; // Flashing color
  221. setTimeout(() => {
  222. element.style.backgroundColor = originalBackgroundColor; // Restore original background color
  223. }, 1000); // flashing color duration
  224. }
  225.  
  226. // Display highlights in a popup window
  227. function displayHighlightsPopup() {
  228. const highlights = JSON.parse(localStorage.getItem('highlights') || '[]');
  229. const popup = document.createElement('div');
  230. popup.id = 'displayHighlightsPopUp';
  231. popup.style.position = 'fixed';
  232. popup.style.top = '50%';
  233. popup.style.left = '50%';
  234. popup.style.transform = 'translate(-50%, -50%)';
  235. popup.style.width = '600px';
  236. popup.style.height = '400px';
  237. popup.style.backgroundColor = 'black';
  238. popup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
  239. popup.style.border = '1px solid #ccc';
  240. popup.style.borderRadius = '8px';
  241. popup.style.padding = '10px';
  242. popup.style.zIndex = '1002';
  243. popup.style.overflowY = 'scroll';
  244. popup.style.userSelect = 'none'; // Disable text selection
  245.  
  246. popup.innerHTML = '<h3>All Saved Highlights of this webpage</h3><ul></ul>';
  247. const list = popup.querySelector('ul');
  248. highlights.forEach((hl, index) => {
  249. const listItem = document.createElement('li');
  250. listItem.innerHTML = `<b>${new Date(hl.timestamp).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true })}:</b> <span style="color:${hl.color}">${hl.text}</span><button style="margin-left:10px;" class="delete-btn" data-index="${index}">X</button>`;
  251. listItem.style.cursor = 'pointer';
  252. listItem.onclick = (event) => {
  253. if (event.target.classList.contains('delete-btn')) {
  254. event.stopPropagation(); // Prevent event propagation to the popup
  255. const index = event.target.getAttribute('data-index');
  256. deleteHighlight(index);
  257. } else {
  258. console.log(`XPath: ${hl.parentPath}`); // Debug log
  259. const parentElement = getElementByXPath(hl.parentPath);
  260. if (parentElement) {
  261. parentElement.scrollIntoView({ behavior: 'smooth' });
  262. flashingElement(parentElement, hl.color); // Highlight the target element
  263. } else {
  264. console.error('Element not found for XPath:', hl.parentPath); // Debug error
  265. }
  266. }
  267. };
  268. list.appendChild(listItem);
  269. });
  270.  
  271. document.body.appendChild(popup);
  272.  
  273. // Remove popup when clicking outside
  274. const outsideClickListener = (event) => {
  275. if (!popup.contains(event.target)) {
  276. document.body.removeChild(popup);
  277. document.removeEventListener('click', outsideClickListener);
  278. }
  279. };
  280. document.addEventListener('click', outsideClickListener);
  281. }
  282.  
  283. // Delete highlight function
  284. function deleteHighlight(index) {
  285. let highlights = JSON.parse(localStorage.getItem('highlights') || '[]');
  286. if (index >= 0 && index < highlights.length) {
  287. // Remove highlight from local storage
  288. const [deletedHighlight] = highlights.splice(index, 1);
  289. localStorage.setItem('highlights', JSON.stringify(highlights));
  290.  
  291. // Remove highlight from the DOM
  292. removeHighlight(deletedHighlight.parentPath, deletedHighlight.text);
  293.  
  294. // Update indices
  295. document.getElementById('displayHighlightsPopUp').remove();
  296. displayHighlightsPopup();
  297. }
  298. }
  299.  
  300. // Remove highlight from the DOM
  301. function removeHighlight(parentPath, text) {
  302. const parentElement = getElementByXPath(parentPath);
  303. if (parentElement) {
  304. const highlights = Array.from(parentElement.querySelectorAll('.highlighted'));
  305. const span = highlights.find(span => span.textContent.includes(text));
  306. if (span) {
  307. while (span.firstChild) {
  308. parentElement.insertBefore(span.firstChild, span);
  309. }
  310. parentElement.removeChild(span);
  311. }
  312. }
  313. }
  314.  
  315. // Listen for Command + V to display highlights popup
  316. document.addEventListener('keydown', (event) => {
  317. if (event.key === 'v' && (event.metaKey || event.ctrlKey)) {
  318. event.preventDefault();
  319. displayHighlightsPopup();
  320. }
  321. });
  322.  
  323. })();