求助开发Chrome高亮扩展:实现选中文本持久高亮功能
Hey there! Let's build that persistent text highlighter extension you're aiming for. The key challenge here is moving beyond the temporary ::selection highlight—we need to capture selected text ranges, store them persistently, and render them permanently even after clicking away or reloading the page. Let's fix your existing code and add the core functionality step by step.
First, Fix Existing Code Issues
Let's start by cleaning up the bugs in your current files:
1. options.js - Fix Element Selection & Improve UI
Your code uses an invalid document.get('buttonDiv') method. Replace it with document.getElementById, plus add some basic styling for color buttons:
'use strict'; let page = document.getElementById('buttonDiv'); const kButtonColors = ['#3aa757', '#e8453c', '#f9bb2d', '#4688f1']; function constructOptions(kButtonColors) { for (let item of kButtonColors) { let button = document.createElement('button'); button.style.backgroundColor = item; button.style.width = '40px'; button.style.height = '40px'; button.style.border = 'none'; button.style.borderRadius = '50%'; button.style.margin = '0 5px'; button.style.cursor = 'pointer'; button.addEventListener('click', function() { chrome.storage.sync.set({color: item}, function() { console.log('Highlight color set to ' + item); }); }); page.appendChild(button); // Use the pre-defined page variable } } constructOptions(kButtonColors);
2. background.js - Show Page Action on All Pages
Your current rule only shows the page action on developer.chrome.com. Let's adjust it to work on all HTTP/HTTPS pages:
'use strict'; chrome.runtime.onInstalled.addListener(function() { chrome.storage.sync.set({color: '#3aa757'}, function() { console.log('Default highlight color set to green (#3aa757)'); }); chrome.declarativeContent.onPageChanged.removeRules(undefined, function() { chrome.declarativeContent.onPageChanged.addRules([{ conditions: [ new chrome.declarativeContent.PageStateMatcher({ pageUrl: {schemes: ['http', 'https']}, }) ], actions: [new chrome.declarativeContent.ShowPageAction()] }]); }); });
Core Functionality: Persistent Highlighting
The magic happens in the content script (script.js). We'll add logic to:
- Capture text selections when the user releases the mouse
- Wrap selected text in a highlighted span
- Store highlight data so it persists across page reloads
- Restore highlights when the page loads
script.js - Content Script
'use strict'; // Load saved highlights when the page loads document.addEventListener('DOMContentLoaded', restoreHighlights); // Listen for mouseup to detect text selection document.addEventListener('mouseup', handleTextSelection); // Listen for messages from popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'clearHighlights') { const highlights = document.querySelectorAll('.persistent-highlight'); highlights.forEach(highlight => { // Replace the span with its contents const parent = highlight.parentNode; parent.replaceChild(highlight.firstChild, highlight); parent.normalize(); // Merge adjacent text nodes }); sendResponse({status: 'success'}); } }); // Handle text selection and create highlight function handleTextSelection() { const selection = window.getSelection(); if (!selection || selection.isCollapsed) return; // Get the selected range const range = selection.getRangeAt(0); const selectedText = range.toString().trim(); if (!selectedText) return; // Get the saved highlight color from storage chrome.storage.sync.get('color', (data) => { const highlightColor = data.color || '#3aa757'; createHighlight(range, highlightColor); saveHighlight(range, highlightColor); // Clear the native selection selection.removeAllRanges(); }); } // Create a highlight span and insert it into the DOM function createHighlight(range, color) { const highlightSpan = document.createElement('span'); highlightSpan.style.backgroundColor = color; highlightSpan.style.color = getContrastColor(color); // Ensure text readability highlightSpan.classList.add('persistent-highlight'); // Surround the selected range with the span (handle cross-element ranges) try { range.surroundContents(highlightSpan); } catch (e) { const fragment = range.extractContents(); highlightSpan.appendChild(fragment); range.insertNode(highlightSpan); } } // Save highlight data to chrome storage function saveHighlight(range, color) { // Convert range to serializable data using XPath for node location const highlightData = { startXPath: getXPath(range.startContainer), startOffset: range.startOffset, endXPath: getXPath(range.endContainer), endOffset: range.endOffset, color: color, pageUrl: window.location.href }; // Get existing highlights and add the new one chrome.storage.sync.get('highlights', (data) => { const highlights = data.highlights || []; highlights.push(highlightData); chrome.storage.sync.set({highlights: highlights}); }); } // Restore highlights from storage when page loads function restoreHighlights() { chrome.storage.sync.get('highlights', (data) => { const highlights = data.highlights || []; // Filter highlights for the current page const pageHighlights = highlights.filter(h => h.pageUrl === window.location.href); pageHighlights.forEach(highlight => { try { const startNode = getNodeFromXPath(highlight.startXPath); const endNode = getNodeFromXPath(highlight.endXPath); if (!startNode || !endNode) return; const range = document.createRange(); range.setStart(startNode, highlight.startOffset); range.setEnd(endNode, highlight.endOffset); createHighlight(range, highlight.color); } catch (e) { console.error('Failed to restore highlight:', e); } }); }); } // Helper: Get XPath for a DOM node function getXPath(node) { if (node.id) return `//*[@id="${node.id}"]`; if (node === document.body) return '/html/body'; let count = 0; let sibling = node.previousSibling; while (sibling) { if (sibling.nodeType === 1 && sibling.tagName === node.tagName) count++; sibling = sibling.previousSibling; } return `${getXPath(node.parentNode)}/${node.tagName.toLowerCase()}[${count + 1}]`; } // Helper: Get DOM node from XPath function getNodeFromXPath(xpath) { return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } // Helper: Get contrasting text color for readability function getContrastColor(hexColor) { hexColor = hexColor.replace('#', ''); const r = parseInt(hexColor.substr(0, 2), 16); const g = parseInt(hexColor.substr(2, 2), 16); const b = parseInt(hexColor.substr(4, 2), 16); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.5 ? '#000000' : '#ffffff'; }
style.css - Content Script CSS
Add this to ensure highlights don't interfere with default styles:
.persistent-highlight { padding: 2px 0; border-radius: 3px; user-select: none; /* Prevent re-selecting the highlighted text */ }
Add a Clear Highlights Button to Popup
Update your popup.html and popup.js to let users clear highlights on the current page:
popup.html
<!DOCTYPE html> <html> <head> <style> body { width: 150px; padding: 10px; } button { width: 100%; padding: 8px; margin: 5px 0; cursor: pointer; border: none; border-radius: 4px; background: #4688f1; color: white; } </style> </head> <body> <button id="clearHighlights">Clear All Highlights</button> <script src="popup.js"></script> </body> </html>
popup.js
'use strict'; document.getElementById('clearHighlights').addEventListener('click', () => { // Send message to content script to clear highlights chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { chrome.tabs.sendMessage(tabs[0].id, {action: 'clearHighlights'}, () => { // Remove highlights from storage for this page chrome.storage.sync.get('highlights', (data) => { const highlights = data.highlights || []; const updatedHighlights = highlights.filter(h => h.pageUrl !== tabs[0].url); chrome.storage.sync.set({highlights: updatedHighlights}); window.close(); }); }); }); });
Final Notes
- Manifest Update: Ensure your
manifest.jsoncontent script matches all URLs by updatingmatchesto["<all_urls>"]. - Dynamic Pages: For single-page apps (SPAs), add a MutationObserver to detect DOM updates and re-apply highlights, since XPath references might break when the DOM changes.
- Performance: If you expect many highlights, consider adding pagination or a "clear all highlights across pages" option to avoid storage bloat.
内容的提问来源于stack exchange,提问作者rrrrrauno

