Greasy Fork 支持简体中文。

Web Highlighter

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

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