- // ==UserScript==
- // @name Web Highlighter
- // @author Damodar Rajbhandari
- // @namespace physicslog.com.web-highlighter
- // @version 1.33
- // @description Highlight selected text, saves locally, and edit or delete highlights
- // @license GNU GPL v3
- // @match *://*.wikipedia.org/*
- // @grant none
- // @noframes
- // ==/UserScript==
-
- // @note: Please read any news or bugs at https://github.com/physicslog/web-highlighter.user.js
-
- (function() {
- 'use strict';
-
- const colors = ['#E5AE26', '#B895FF', '#54D171', '#D02848'];
- const colors_title = ['Introduction / General / Well-known', 'Important / Spectacle / Interesting', 'Answer / Hint / Idea', 'Question / Critical / Hard / Sceptical'];
- let selectedColor = colors[0];
-
- // Load highlights from local storage
- const highlights = JSON.parse(localStorage.getItem('highlights') || '[]');
-
- // Adds a yellow dot at the top right corner of the webpage if highlights is present.
- if (highlights.length !== 0) {
- const dot = document.createElement('div');
- dot.title = 'Highlights exists! To view: do cmd+shift+v in the Mac';
- dot.style.width = '10px';
- dot.style.height = '10px';
- dot.style.backgroundColor = '#E5AE26';
- dot.style.borderRadius = '50%';
- dot.style.position = 'fixed';
- dot.style.top = '5px';
- dot.style.right = '5px';
- dot.style.zIndex = '1000';
- document.body.appendChild(dot);
- }
-
- console.log("Loaded highlights:", highlights);
- highlights.forEach(hl => {
- if (hl.url === window.location.href) {
- console.log("Restoring highlight:", hl);
- restoreHighlight(hl);
- }
- });
-
- // Save highlights to local storage
- function saveHighlights() {
- const serialized = Array.from(document.querySelectorAll('.highlighted')).map(el => ({
- text: el.innerText,
- color: el.style.backgroundColor,
- parentPath: getElementXPath(el.parentElement),
- url: window.location.href,
- timestamp: new Date().toISOString()
- }));
- console.log("Saving highlights to local storage:", serialized);
- localStorage.setItem('highlights', JSON.stringify(serialized));
- }
-
- // Restore a highlight
- function restoreHighlight({ text, color, parentPath }) {
- const parentElement = getElementByXPath(parentPath);
- if (parentElement) {
- const nodes = Array.from(parentElement.childNodes);
- nodes.forEach(node => {
- if (node.nodeType === Node.TEXT_NODE && node.nodeValue.includes(text)) {
- const range = document.createRange();
- range.setStart(node, node.nodeValue.indexOf(text));
- range.setEnd(node, node.nodeValue.indexOf(text) + text.length);
- wrapHighlight(range, color);
- console.log("Highlight restored for text:", text);
- }
- });
- } else {
- console.error("Parent element not found for XPath:", parentPath);
- }
- }
-
- // Highlight selected text
- function wrapHighlight(range, color) {
- const span = document.createElement('span');
- span.style.backgroundColor = color;
- span.classList.add('highlighted');
- range.surroundContents(span);
- }
-
- // Get an XPath to an element
- function getElementXPath(element) {
- const paths = [];
- while (element && element.nodeType === Node.ELEMENT_NODE) {
- let index = 0;
- let sibling = element.previousSibling;
- while (sibling) {
- if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
- index++;
- }
- sibling = sibling.previousSibling;
- }
- const tagName = element.nodeName.toLowerCase();
- const pathIndex = index ? `[${index + 1}]` : '';
- paths.unshift(`${tagName}${pathIndex}`);
- element = element.parentNode;
- }
- return paths.length ? `/${paths.join('/')}` : null;
- }
-
- // Retrieve an element by its XPath
- function getElementByXPath(xpath) {
- try {
- return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
- } catch (e) {
- console.error("Error evaluating XPath:", xpath, e);
- return null;
- }
- }
-
- // Event listener for text selection and highlighting
- document.addEventListener('mouseup', () => {
- const selection = window.getSelection();
- if (!event.target.closest('#displayHighlightsPopUp')) { // ignore if that is a popup to list all the highlights
- if (selection.rangeCount > 0) {
- const range = selection.getRangeAt(0);
- const selectedText = selection.toString().trim();
- if (selectedText.length > 0) {
- wrapHighlight(range, selectedColor);
- saveHighlights();
- selection.removeAllRanges();
- }
- }
- }
- });
-
- // Event listener for clicking on existing highlights
- document.addEventListener('click', (event) => {
- if (event.target.classList.contains('highlighted')) {
- createPopup(event.target);
- }
- });
-
- // Popup for editing or deleting highlights
- function createPopup(element) {
- const popup = document.createElement('div');
- popup.style.position = 'absolute';
- popup.style.background = 'rgba(255, 255, 255, 0.9)';
- popup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
- popup.style.border = '1px solid #ccc';
- popup.style.borderRadius = '8px';
- popup.style.padding = '5px';
- popup.style.zIndex = '1001';
- popup.style.transition = 'all 0.3s ease';
-
- // Color selection buttons to popup
- const buttonContainer = document.createElement('div');
- buttonContainer.style.display = 'flex';
- colors.forEach((color, index) => {
- const colorButton = document.createElement('button');
- colorButton.style.backgroundColor = color;
- colorButton.style.width = '20px';
- colorButton.style.height = '20px';
- colorButton.style.margin = '2px';
- colorButton.style.borderRadius = '50%';
- colorButton.style.border = 'none';
- colorButton.style.cursor = 'pointer';
- colorButton.title = colors_title[index];
- colorButton.onclick = () => {
- element.style.backgroundColor = color;
- saveHighlights();
- };
- buttonContainer.appendChild(colorButton);
- });
-
- // Add "X" button to popup
- const closeButton = document.createElement('button');
- closeButton.title = 'Delete';
- closeButton.innerText = '\u00D7';
- closeButton.style.width = '20px';
- closeButton.style.height = '20px';
- closeButton.style.margin = '2px';
- closeButton.style.lineHeight = '15px';
- closeButton.style.textAlign = 'center';
- closeButton.style.border = 'none';
- closeButton.style.backgroundColor = 'gray';
- closeButton.style.color = 'white';
- closeButton.style.fontWeight = 'bold';
- closeButton.style.borderRadius = '50%';
- closeButton.style.cursor = 'pointer';
- closeButton.onclick = () => {
- const parent = element.parentNode;
- parent.replaceChild(document.createTextNode(element.innerText), element);
- saveHighlights();
- document.body.removeChild(popup);
- };
-
- buttonContainer.appendChild(closeButton);
- popup.appendChild(buttonContainer);
- document.body.appendChild(popup);
-
- // Position the popup near the element
- const rect = element.getBoundingClientRect();
- popup.style.left = `${rect.left}px`;
- popup.style.top = `${rect.bottom + window.scrollY + 10}px`;
-
- // Remove popup when clicking outside
- const outsideClickListener = (event) => {
- if (!popup.contains(event.target)) {
- document.body.removeChild(popup);
- document.removeEventListener('click', outsideClickListener);
- }
- };
- document.addEventListener('click', outsideClickListener);
- }
-
- // Another popup to show all the highlighted text on the present webpage
- // Get the XPath of the element
- function getElementByXPath(xpath) {
- return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
- }
-
- // Highlight the target element
- function flashingElement(element, color) {
- const originalBackgroundColor = element.style.backgroundColor;
- element.style.backgroundColor = color; // Flashing color
- setTimeout(() => {
- element.style.backgroundColor = originalBackgroundColor; // Restore original background color
- }, 1000); // flashing color duration
- }
-
- // Display highlights in a popup window
- function displayHighlightsPopup() {
- const highlights = JSON.parse(localStorage.getItem('highlights') || '[]');
- const popup = document.createElement('div');
- popup.id = 'displayHighlightsPopUp';
- popup.style.position = 'fixed';
- popup.style.top = '50%';
- popup.style.left = '50%';
- popup.style.transform = 'translate(-50%, -50%)';
- popup.style.width = '600px';
- popup.style.height = '400px';
- popup.style.backgroundColor = 'black';
- popup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
- popup.style.border = '1px solid #ccc';
- popup.style.borderRadius = '8px';
- popup.style.padding = '10px';
- popup.style.zIndex = '1002';
- popup.style.overflowY = 'scroll';
- popup.style.userSelect = 'none'; // Disable text selection
-
- popup.innerHTML = '<h3>All Saved Highlights of this webpage</h3><ul></ul>';
- const list = popup.querySelector('ul');
- highlights.forEach((hl, index) => {
- const listItem = document.createElement('li');
- 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>`;
- listItem.style.cursor = 'pointer';
- listItem.onclick = (event) => {
- if (event.target.classList.contains('delete-btn')) {
- event.stopPropagation(); // Prevent event propagation to the popup
- const index = event.target.getAttribute('data-index');
- deleteHighlight(index);
- } else {
- console.log(`XPath: ${hl.parentPath}`); // Debug log
- const parentElement = getElementByXPath(hl.parentPath);
- if (parentElement) {
- parentElement.scrollIntoView({ behavior: 'smooth' });
- flashingElement(parentElement, hl.color); // Highlight the target element
- } else {
- console.error('Element not found for XPath:', hl.parentPath); // Debug error
- }
- }
- };
- list.appendChild(listItem);
- });
-
- document.body.appendChild(popup);
-
- // Remove popup when clicking outside
- const outsideClickListener = (event) => {
- if (!popup.contains(event.target)) {
- document.body.removeChild(popup);
- document.removeEventListener('click', outsideClickListener);
- }
- };
- document.addEventListener('click', outsideClickListener);
- }
-
- // Delete highlight function
- function deleteHighlight(index) {
- let highlights = JSON.parse(localStorage.getItem('highlights') || '[]');
- if (index >= 0 && index < highlights.length) {
- // Remove highlight from local storage
- const [deletedHighlight] = highlights.splice(index, 1);
- localStorage.setItem('highlights', JSON.stringify(highlights));
-
- // Remove highlight from the DOM
- removeHighlight(deletedHighlight.parentPath, deletedHighlight.text);
-
- // Update indices
- document.getElementById('displayHighlightsPopUp').remove();
- displayHighlightsPopup();
- }
- }
-
- // Remove highlight from the DOM
- function removeHighlight(parentPath, text) {
- const parentElement = getElementByXPath(parentPath);
- if (parentElement) {
- const highlights = Array.from(parentElement.querySelectorAll('.highlighted'));
- const span = highlights.find(span => span.textContent.includes(text));
- if (span) {
- while (span.firstChild) {
- parentElement.insertBefore(span.firstChild, span);
- }
- parentElement.removeChild(span);
- }
- }
- }
-
- // Listen for Command + V to display highlights popup
- document.addEventListener('keydown', (event) => {
- if (event.key === 'v' && (event.metaKey || event.ctrlKey)) {
- event.preventDefault();
- displayHighlightsPopup();
- }
- });
-
- })();