UpToDate Text Highlighter (Option/Alt Only)

Floating toolbar to highlight and save text selections with colored markers (stable restore + Option/Alt triggers highlight)

Size

53.5 KB

Version

1.4.1

Created

Jan 22, 2026

Updated

11 days ago

1// ==UserScript==
2// @name		UpToDate Text Highlighter (Option/Alt Only)
3// @description		Floating toolbar to highlight and save text selections with colored markers (stable restore + Option/Alt triggers highlight)
4// @version		1.4.1
5// @match		https://www.uptodate.com/*
6// @match		https://www-uptodate-com.ezproxy.is.cuni.cz/*
7// @icon		https://www-uptodate-com.ezproxy.is.cuni.cz/app/utd-menu/utd-icon-redesign.png
8// @grant		none
9// ==/UserScript==
10(function () {
11    'use strict';
12
13    const COLORS = {
14        yellow: '#FFFF00',
15        green: '#90EE90',
16        pink: '#FFB6C1',
17        blue: '#ADD8E6',
18        orange: '#FFD580'
19    };
20
21    const STORAGE_KEY_PREFIX = 'uptodate_highlights_v2_';
22    const CONTEXT_LEN = 60;
23
24    let selectedColor = 'yellow';
25    
26    // Timer state
27    let timerInterval = null;
28    let startTime = null;
29    let elapsedTime = 0;
30    let isTimerRunning = false;
31    
32    // Laser pointer state
33    let laserEnabled = false;
34    let laserPointer = null;
35    
36    // Toolbar collapse state
37    let isCollapsed = false;
38
39    function getPageKey() {
40        return STORAGE_KEY_PREFIX + location.pathname + location.search;
41    }
42
43    function safeClosest(el, selector) {
44        if (!el || el.nodeType !== 1) return null;
45        return el.closest(selector);
46    }
47
48    function generateId() {
49        return 'hl_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
50    }
51
52    function debounce(fn, wait) {
53        let t;
54        return (...args) => {
55            clearTimeout(t);
56            t = setTimeout(() => fn(...args), wait);
57        };
58    }
59
60    function normalizeText(s) {
61        return (s || '').replace(/\s+/g, ' ').trim();
62    }
63
64    function getTextNodes() {
65        const walker = document.createTreeWalker(
66            document.body,
67            NodeFilter.SHOW_TEXT,
68            {
69                acceptNode(node) {
70                    const p = node.parentElement;
71                    if (!p) return NodeFilter.FILTER_REJECT;
72                    if (safeClosest(p, '#highlighter-toolbar')) return NodeFilter.FILTER_REJECT;
73                    if (p.classList && p.classList.contains('text-highlight')) return NodeFilter.FILTER_REJECT;
74                    const tag = p.tagName ? p.tagName.toLowerCase() : '';
75                    if (tag === 'script' || tag === 'style' || tag === 'noscript') return NodeFilter.FILTER_REJECT;
76                    if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
77                    return NodeFilter.FILTER_ACCEPT;
78                },
79            },
80            false
81        );
82
83        const nodes = [];
84        let n;
85        while ((n = walker.nextNode())) nodes.push(n);
86        return nodes;
87    }
88
89    function buildSelectorFromRange(range) {
90        const exact = normalizeText(range.toString());
91        if (!exact) return null;
92
93        let startNode = range.startContainer;
94        let prefix = '';
95        let suffix = '';
96
97        if (startNode && startNode.nodeType === Node.TEXT_NODE) {
98            const full = startNode.nodeValue || '';
99            const start = range.startOffset;
100            const preRaw = full.slice(Math.max(0, start - CONTEXT_LEN), start);
101            prefix = normalizeText(preRaw).slice(-CONTEXT_LEN);
102
103            if (range.endContainer === startNode) {
104                const end = range.endOffset;
105                const sufRaw = full.slice(end, Math.min(full.length, end + CONTEXT_LEN));
106                suffix = normalizeText(sufRaw).slice(0, CONTEXT_LEN);
107            }
108        }
109
110        return { exact, prefix, suffix };
111    }
112
113    function getXPath(node) {
114        if (node.id) {
115            return `//*[@id="${node.id}"]`;
116        }
117        
118        const parts = [];
119        let n = node;
120        while (n && n.nodeType === Node.ELEMENT_NODE) {
121            let index = 0;
122            let sibling = n.previousSibling;
123            while (sibling) {
124                if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === n.nodeName) {
125                    index++;
126                }
127                sibling = sibling.previousSibling;
128            }
129            
130            const tagName = n.nodeName.toLowerCase();
131            const pathIndex = index > 0 ? `[${index + 1}]` : '';
132            parts.unshift(tagName + pathIndex);
133            n = n.parentNode;
134        }
135        
136        return parts.length ? '/' + parts.join('/') : '';
137    }
138
139    function getTextNodeXPath(textNode) {
140        const parent = textNode.parentElement;
141        if (!parent) return null;
142        
143        const parentXPath = getXPath(parent);
144        
145        // Find text node index among siblings
146        let textIndex = 0;
147        for (let i = 0; i < parent.childNodes.length; i++) {
148            const child = parent.childNodes[i];
149            if (child === textNode) {
150                textIndex = i;
151                break;
152            }
153        }
154        
155        return { parentXPath, textIndex };
156    }
157
158    function resolveXPath(xpath) {
159        try {
160            const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
161            return result.singleNodeValue;
162        } catch (e) {
163            console.error('XPath resolution failed:', e);
164            return null;
165        }
166    }
167
168    function scoreMatch(nodeText, idx, exact, prefix, suffix) {
169        const before = normalizeText(nodeText.slice(Math.max(0, idx - CONTEXT_LEN), idx));
170        const after = normalizeText(
171            nodeText.slice(idx + exact.length, Math.min(nodeText.length, idx + exact.length + CONTEXT_LEN))
172        );
173
174        let score = 0;
175        if (prefix) {
176            const tail = prefix.slice(-Math.min(30, prefix.length));
177            if (tail && before.includes(tail)) score += 3;
178        }
179        if (suffix) {
180            const head = suffix.slice(0, Math.min(30, suffix.length));
181            if (head && after.includes(head)) score += 3;
182        }
183        score += Math.min(4, Math.floor(exact.length / 30));
184        return score;
185    }
186
187    function findBestMatch(exact, prefix, suffix) {
188        const nodes = getTextNodes();
189        const normalizedExact = normalizeText(exact);
190        if (!normalizedExact) return null;
191
192        let best = null;
193
194        for (const node of nodes) {
195            const raw = node.nodeValue || '';
196            const normRaw = normalizeText(raw);
197            
198            // Try to find the normalized text in the normalized node
199            const probe = normalizedExact.slice(0, Math.min(12, normalizedExact.length));
200            if (!normRaw.includes(probe)) continue;
201
202            let from = 0;
203            while (from < normRaw.length) {
204                const idx = normRaw.indexOf(normalizedExact, from);
205                if (idx === -1) break;
206
207                // Map normalized index back to raw index
208                let rawIdx = 0;
209                let normIdx = 0;
210                while (normIdx < idx && rawIdx < raw.length) {
211                    if (/\s/.test(raw[rawIdx])) {
212                        // Skip whitespace in raw
213                        while (rawIdx < raw.length && /\s/.test(raw[rawIdx])) rawIdx++;
214                        if (normIdx < normRaw.length && normRaw[normIdx] === ' ') normIdx++;
215                    } else {
216                        rawIdx++;
217                        normIdx++;
218                    }
219                }
220
221                const sc = scoreMatch(raw, rawIdx, normalizedExact, prefix, suffix);
222                if (!best || sc > best.score) {
223                    best = { node, index: rawIdx, score: sc };
224                }
225
226                from = idx + normalizedExact.length;
227            }
228        }
229
230        return best;
231    }
232
233    function addStyles() {
234        const style = document.createElement('style');
235        style.textContent = `
236#highlighter-toolbar{
237  position:fixed; top:50%; right:10px; transform:translateY(-50%);
238  background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
239  border-radius:10px; padding:8px;
240  box-shadow:0 4px 20px rgba(0,0,0,.3);
241  z-index:999999;
242  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
243  user-select:none; min-width:50px;
244  backdrop-filter:blur(10px);
245  transition:all .3s ease;
246}
247#highlighter-toolbar.collapsed{
248  padding:6px;
249}
250#highlighter-toolbar.collapsed .toolbar-extra{
251  display:none !important;
252}
253#highlighter-toolbar.collapsed .toolbar-collapse-btn{
254  transform:rotate(180deg);
255}
256#highlighter-toolbar .toolbar-header{
257  display:flex; justify-content:space-between; align-items:center;
258  margin-bottom:6px; padding-bottom:4px;
259  border-bottom:1px solid rgba(255,255,255,.2);
260}
261#highlighter-toolbar.collapsed .toolbar-header{
262  margin-bottom:0; padding-bottom:0; border-bottom:none;
263}
264#highlighter-toolbar .toolbar-title{
265  color:#fff; font-size:9px; font-weight:700;
266  letter-spacing:.5px;
267}
268#highlighter-toolbar .toolbar-collapse-btn{
269  background:none; border:none; color:rgba(255,255,255,.9);
270  cursor:pointer; font-size:12px; padding:2px;
271  transition:transform .3s ease;
272}
273#highlighter-toolbar .timer-section{
274  background:rgba(255,255,255,.15); border-radius:6px;
275  padding:6px; margin-bottom:6px; text-align:center;
276}
277#highlighter-toolbar .timer-display{
278  color:#fff; font-size:13px; font-weight:700;
279  font-family:'Courier New',monospace; margin-bottom:4px;
280  letter-spacing:.5px;
281}
282#highlighter-toolbar .timer-controls{
283  display:flex; gap:3px; justify-content:center;
284}
285#highlighter-toolbar .timer-btn{
286  width:24px; height:24px; border:none; border-radius:50%;
287  cursor:pointer; background:rgba(255,255,255,.9);
288  font-size:10px; display:flex; align-items:center;
289  justify-content:center; transition:transform .15s ease;
290}
291#highlighter-toolbar .timer-btn:hover{
292  transform:scale(1.1); background:#fff;
293}
294#highlighter-toolbar .timer-btn:active{
295  transform:scale(.97);
296}
297#highlighter-toolbar .highlight-btn, #highlighter-toolbar .clear-btn, #highlighter-toolbar .stats-btn, #highlighter-toolbar .export-btn, #highlighter-toolbar .import-btn, #highlighter-toolbar .laser-btn{
298  width:28px; height:28px; border:none; border-radius:50%;
299  cursor:pointer; transition:transform .15s ease, box-shadow .15s ease;
300  display:flex; align-items:center; justify-content:center;
301  background:#fff; box-shadow:0 2px 8px rgba(0,0,0,.15);
302  font-size:13px;
303}
304#highlighter-toolbar .highlight-btn:hover, #highlighter-toolbar .clear-btn:hover, #highlighter-toolbar .stats-btn:hover, #highlighter-toolbar .export-btn:hover, #highlighter-toolbar .import-btn:hover, #highlighter-toolbar .laser-btn:hover{
305  transform:scale(1.08); box-shadow:0 4px 12px rgba(0,0,0,.25);
306}
307#highlighter-toolbar .highlight-btn:active, #highlighter-toolbar .clear-btn:active{
308  transform:scale(.97);
309}
310#highlighter-toolbar .highlight-btn.active{
311  box-shadow:0 0 0 2px rgba(102,126,234,.55);
312}
313#highlighter-toolbar .color-circle{
314  width:18px; height:18px; border-radius:50%;
315  border:2px solid rgba(0,0,0,.1);
316}
317#highlighter-toolbar .clear-btn{ background:#ff4757; }
318
319.text-highlight{ padding:2px 0; border-radius:2px; cursor:pointer; }
320.text-highlight:hover{ opacity:.85; }
321.text-highlight.yellow{ background:${COLORS.yellow}; }
322.text-highlight.green{ background:${COLORS.green}; }
323.text-highlight.pink{ background:${COLORS.pink}; }
324.text-highlight.blue{ background:${COLORS.blue}; }
325.text-highlight.orange{ background:${COLORS.orange}; }
326
327#stats-modal{
328  position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);
329  background:#fff; border-radius:16px; padding:24px;
330  box-shadow:0 8px 32px rgba(0,0,0,.3);
331  z-index:1000000; min-width:400px; max-width:600px;
332  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
333}
334#stats-modal .modal-header{
335  display:flex; justify-content:space-between; align-items:center;
336  margin-bottom:20px; padding-bottom:12px;
337  border-bottom:2px solid #f0f0f0;
338}
339#stats-modal .modal-title{
340  font-size:20px; font-weight:700; color:#333;
341}
342#stats-modal .modal-close{
343  background:none; border:none; font-size:24px;
344  cursor:pointer; color:#999; padding:0;
345}
346#stats-modal .stat-item{
347  display:flex; justify-content:space-between; align-items:center;
348  padding:12px; margin:8px 0; border-radius:8px;
349  background:#f8f9fa;
350}
351#stats-modal .stat-label{
352  font-size:14px; color:#666; display:flex; align-items:center; gap:8px;
353}
354#stats-modal .stat-value{
355  font-size:18px; font-weight:700; color:#333;
356}
357#stats-modal .color-indicator{
358  width:16px; height:16px; border-radius:50%;
359  display:inline-block; border:2px solid rgba(0,0,0,.1);
360}
361#stats-modal .progress-bar{
362  width:100%; height:8px; background:#e0e0e0;
363  border-radius:4px; overflow:hidden; margin-top:8px;
364}
365#stats-modal .progress-fill{
366  height:100%; background:linear-gradient(90deg,#667eea,#764ba2);
367  transition:width .3s ease;
368}
369.modal-overlay{
370  position:fixed; top:0; left:0; right:0; bottom:0;
371  background:rgba(0,0,0,.5); z-index:999999;
372}
373
374.highlight-tooltip{
375  position:fixed; background:#fff; border-radius:8px;
376  box-shadow:0 4px 16px rgba(0,0,0,.2);
377  padding:12px; z-index:1000001;
378  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
379  max-width:300px;
380}
381.highlight-tooltip .tooltip-text{
382  font-size:13px; color:#333; margin-bottom:8px;
383  line-height:1.4;
384}
385.highlight-tooltip .tooltip-actions{
386  display:flex; gap:8px; justify-content:flex-end;
387}
388.highlight-tooltip .tooltip-btn{
389  background:#f0f0f0; border:none; border-radius:4px;
390  padding:6px 12px; cursor:pointer; font-size:14px;
391  transition:background .2s;
392}
393.highlight-tooltip .tooltip-btn:hover{
394  background:#e0e0e0;
395}
396
397.laser-pointer{
398  position:fixed; width:20px; height:20px;
399  border-radius:50%; background:radial-gradient(circle, rgba(255,0,0,0.8) 0%, rgba(255,0,0,0.3) 50%, transparent 100%);
400  pointer-events:none; z-index:999998;
401  box-shadow:0 0 20px rgba(255,0,0,0.8), 0 0 40px rgba(255,0,0,0.5);
402  animation:pulse 1s infinite;
403}
404@keyframes pulse{
405  0%, 100%{ transform:scale(1); opacity:1; }
406  50%{ transform:scale(1.2); opacity:0.8; }
407}
408.laser-trail{
409  position:fixed; width:4px; height:4px;
410  border-radius:50%; background:rgba(255,0,0,0.6);
411  pointer-events:none; z-index:999997;
412  animation:fadeOut 0.5s forwards;
413}
414@keyframes fadeOut{
415  to{ opacity:0; transform:scale(0.5); }
416}
417.laser-btn.active{
418  background:#ff4757 !important;
419  box-shadow:0 0 0 3px rgba(255,71,87,0.5) !important;
420}
421`;
422        document.head.appendChild(style);
423    }
424
425    function createToolbar() {
426        const toolbar = document.createElement('div');
427        toolbar.id = 'highlighter-toolbar';
428        toolbar.innerHTML = `
429  <div class="toolbar-header">
430    <span class="toolbar-title">HL</span>
431    <button class="toolbar-collapse-btn" title="Collapse/Expand"></button>
432  </div>
433  <div class="timer-section">
434    <div class="timer-display">00:00</div>
435    <div class="timer-controls">
436      <button class="timer-btn start-btn" title="Start">▶️</button>
437      <button class="timer-btn pause-btn" title="Pause" style="display:none;">⏸️</button>
438      <button class="timer-btn reset-btn" title="Reset">🔄</button>
439    </div>
440  </div>
441  <div class="toolbar-colors">
442    <div class="color-row">
443      <button class="highlight-btn active" data-color="yellow" title="Important"><div class="color-circle" style="background:${COLORS.yellow}"></div></button>
444      <button class="highlight-btn" data-color="green" title="Definitions"><div class="color-circle" style="background:${COLORS.green}"></div></button>
445    </div>
446    <div class="color-row">
447      <button class="highlight-btn" data-color="pink" title="Key Points"><div class="color-circle" style="background:${COLORS.pink}"></div></button>
448      <button class="highlight-btn" data-color="blue" title="Examples"><div class="color-circle" style="background:${COLORS.blue}"></div></button>
449    </div>
450    <div class="color-row">
451      <button class="highlight-btn" data-color="orange" title="Questions"><div class="color-circle" style="background:${COLORS.orange}"></div></button>
452    </div>
453  </div>
454  <div class="toolbar-extra collapsed">
455    <button class="stats-btn" title="Statistics">📊</button>
456    <button class="export-btn" title="Export">💾</button>
457    <button class="import-btn" title="Import">📥</button>
458    <button class="laser-btn" title="Laser">🔴</button>
459    <button class="clear-btn" title="Clear"></button>
460  </div>
461`;
462        document.body.appendChild(toolbar);
463        return toolbar;
464    }
465
466    function makeDraggable(toolbar) {
467        let isDragging = false;
468        let startX = 0, startY = 0;
469        let startLeft = 0, startTop = 0;
470
471        toolbar.addEventListener('mousedown', (e) => {
472            if (e.button !== 0) return;
473            if (e.target.closest('.highlight-btn') || e.target.closest('.clear-btn') || 
474                e.target.closest('.timer-btn') || e.target.closest('.toolbar-collapse-btn')) return;
475
476            const rect = toolbar.getBoundingClientRect();
477            startX = e.clientX;
478            startY = e.clientY;
479            startLeft = rect.left;
480            startTop = rect.top;
481            isDragging = true;
482
483            toolbar.style.right = 'auto';
484            toolbar.style.transform = 'none';
485            toolbar.style.left = rect.left + 'px';
486            toolbar.style.top = rect.top + 'px';
487            e.preventDefault();
488        });
489
490        document.addEventListener('mousemove', (e) => {
491            if (!isDragging) return;
492            const dx = e.clientX - startX;
493            const dy = e.clientY - startY;
494            toolbar.style.left = startLeft + dx + 'px';
495            toolbar.style.top = startTop + dy + 'px';
496        });
497
498        document.addEventListener('mouseup', () => {
499            isDragging = false;
500        });
501    }
502
503    function saveHighlights() {
504        const els = document.querySelectorAll('.text-highlight[data-highlight-id]');
505        const items = [];
506
507        els.forEach((el) => {
508            const id = el.getAttribute('data-highlight-id');
509            const color = el.classList.contains('yellow') ? 'yellow' :
510                el.classList.contains('green') ? 'green' :
511                    el.classList.contains('pink') ? 'pink' :
512                        el.classList.contains('blue') ? 'blue' :
513                            el.classList.contains('orange') ? 'orange' : 'yellow';
514
515            const exact = normalizeText(el.textContent);
516            const prefix = el.getAttribute('data-prefix') || '';
517            const suffix = el.getAttribute('data-suffix') || '';
518            const xpath = el.getAttribute('data-xpath') || '';
519            const textIndex = el.getAttribute('data-text-index') || '';
520
521            items.push({ 
522                id, 
523                color, 
524                exact, 
525                prefix, 
526                suffix, 
527                xpath,
528                textIndex,
529                url: location.href, 
530                ts: Date.now() 
531            });
532        });
533
534        try {
535            localStorage.setItem(getPageKey(), JSON.stringify(items));
536            console.log('💾 Saved', items.length, 'highlights to localStorage');
537        } catch (e) {
538            console.error('saveHighlights failed', e);
539        }
540    }
541
542    function loadHighlights() {
543        try {
544            const raw = localStorage.getItem(getPageKey());
545            if (!raw) return [];
546            const arr = JSON.parse(raw);
547            return Array.isArray(arr) ? arr : [];
548        } catch (e) {
549            console.error('loadHighlights failed', e);
550            return [];
551        }
552    }
553
554    function wrapRange(range, color, id, prefix, suffix) {
555        const span = document.createElement('span');
556        span.className = `text-highlight ${color}`;
557        span.setAttribute('data-highlight-id', id);
558        span.setAttribute('data-prefix', prefix || '');
559        span.setAttribute('data-suffix', suffix || '');
560
561        try {
562            range.surroundContents(span);
563        } catch {
564            // Alternative method using replaceChild
565            const startNode = range.startContainer;
566            const endNode = range.endContainer;
567            
568            if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) {
569                const parent = startNode.parentNode;
570                const text = startNode.nodeValue;
571                const before = text.slice(0, range.startOffset);
572                const selected = text.slice(range.startOffset, range.endOffset);
573                const after = text.slice(range.endOffset);
574                
575                const beforeNode = before ? document.createTextNode(before) : null;
576                const afterNode = after ? document.createTextNode(after) : null;
577                
578                span.textContent = selected;
579                
580                parent.insertBefore(span, startNode);
581                if (beforeNode) parent.insertBefore(beforeNode, span);
582                if (afterNode) parent.insertBefore(afterNode, span.nextSibling);
583                parent.removeChild(startNode);
584            } else {
585                const frag = range.extractContents();
586                span.appendChild(frag);
587                range.insertNode(span);
588            }
589        }
590    }
591
592    function highlightSelection(color) {
593        const sel = window.getSelection();
594        if (!sel || sel.rangeCount === 0) return;
595
596        const range = sel.getRangeAt(0);
597        if (range.collapsed) return;
598
599        const common = range.commonAncestorContainer;
600        const commonEl = common.nodeType === Node.ELEMENT_NODE ? common : common.parentElement;
601
602        if (safeClosest(commonEl, '#highlighter-toolbar')) return;
603        if (safeClosest(commonEl, '.text-highlight')) return;
604
605        const selector = buildSelectorFromRange(range);
606        if (!selector) return;
607
608        const id = generateId();
609        
610        // Save XPath data for better restoration
611        let xpathData = null;
612        if (range.startContainer.nodeType === Node.TEXT_NODE) {
613            xpathData = getTextNodeXPath(range.startContainer);
614        }
615
616        try {
617            wrapRange(range, color, id, selector.prefix, selector.suffix);
618            sel.removeAllRanges();
619            
620            // Save with XPath data
621            if (xpathData) {
622                const span = document.querySelector(`[data-highlight-id="${id}"]`);
623                if (span) {
624                    span.setAttribute('data-xpath', xpathData.parentXPath);
625                    span.setAttribute('data-text-index', xpathData.textIndex);
626                }
627            }
628            
629            saveHighlights();
630            console.log('✓ Highlight saved:', selector.exact.substring(0, 30) + '...');
631        } catch (e) {
632            console.error('highlightSelection failed', e);
633            alert('Could not highlight this selection. Try selecting text within a simpler block.');
634        }
635    }
636
637    function removeHighlightSpan(span) {
638        const parent = span.parentNode;
639        if (!parent) return;
640        while (span.firstChild) parent.insertBefore(span.firstChild, span);
641        parent.removeChild(span);
642    }
643
644    function clearAllHighlights() {
645        const spans = document.querySelectorAll('.text-highlight[data-highlight-id]');
646        if (spans.length === 0) return;
647
648        if (!confirm(`Clear all ${spans.length} highlights on this page?`)) return;
649
650        spans.forEach(removeHighlightSpan);
651        localStorage.removeItem(getPageKey());
652    }
653
654    function setupPerHighlightRemoval() {
655        // Alt+Click deletes one highlight
656        document.addEventListener('click', (e) => {
657            const span = e.target && e.target.closest ? e.target.closest('.text-highlight[data-highlight-id]') : null;
658            if (!span) return;
659            if (!e.altKey) return;
660
661            e.preventDefault();
662            removeHighlightSpan(span);
663            saveHighlights();
664        });
665        
666        // Click on highlight to show note tooltip
667        document.addEventListener('click', (e) => {
668            const span = e.target && e.target.closest ? e.target.closest('.text-highlight[data-highlight-id]') : null;
669            if (!span || e.altKey) return;
670            
671            showHighlightTooltip(span, e);
672        });
673    }
674
675    // Timer functions
676    function formatTime(seconds) {
677        const hrs = Math.floor(seconds / 3600);
678        const mins = Math.floor((seconds % 3600) / 60);
679        const secs = seconds % 60;
680        
681        // Show hours only if > 0
682        if (hrs > 0) {
683            return `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
684        }
685        return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
686    }
687
688    function updateTimerDisplay() {
689        const display = document.querySelector('.timer-display');
690        if (display) {
691            display.textContent = formatTime(elapsedTime);
692        }
693    }
694
695    function startTimer() {
696        if (isTimerRunning) return;
697        
698        isTimerRunning = true;
699        startTime = Date.now() - (elapsedTime * 1000);
700        
701        timerInterval = setInterval(() => {
702            elapsedTime = Math.floor((Date.now() - startTime) / 1000);
703            updateTimerDisplay();
704            
705            // Save timer state
706            try {
707                localStorage.setItem(getPageKey() + '_timer', JSON.stringify({
708                    elapsed: elapsedTime,
709                    running: true,
710                    startTime: startTime
711                }));
712            } catch (e) {
713                console.error('Failed to save timer state:', e);
714            }
715        }, 1000);
716        
717        document.querySelector('.start-btn').style.display = 'none';
718        document.querySelector('.pause-btn').style.display = 'flex';
719        
720        console.log('Timer started');
721    }
722
723    function pauseTimer() {
724        if (!isTimerRunning) return;
725        
726        isTimerRunning = false;
727        clearInterval(timerInterval);
728        
729        document.querySelector('.start-btn').style.display = 'flex';
730        document.querySelector('.pause-btn').style.display = 'none';
731        
732        // Save timer state
733        try {
734            localStorage.setItem(getPageKey() + '_timer', JSON.stringify({
735                elapsed: elapsedTime,
736                running: false
737            }));
738        } catch (e) {
739            console.error('Failed to save timer state:', e);
740        }
741        
742        console.log('Timer paused at:', formatTime(elapsedTime));
743    }
744
745    function resetTimer() {
746        isTimerRunning = false;
747        clearInterval(timerInterval);
748        elapsedTime = 0;
749        startTime = null;
750        
751        updateTimerDisplay();
752        
753        document.querySelector('.start-btn').style.display = 'flex';
754        document.querySelector('.pause-btn').style.display = 'none';
755        
756        // Clear timer state
757        try {
758            localStorage.removeItem(getPageKey() + '_timer');
759        } catch (e) {
760            console.error('Failed to clear timer state:', e);
761        }
762        
763        console.log('Timer reset');
764    }
765
766    function restoreTimerState() {
767        try {
768            const timerData = localStorage.getItem(getPageKey() + '_timer');
769            if (!timerData) return;
770            
771            const data = JSON.parse(timerData);
772            elapsedTime = data.elapsed || 0;
773            
774            updateTimerDisplay();
775            
776            if (data.running && data.startTime) {
777                // Continue timer from where it left off
778                const timeSinceStart = Math.floor((Date.now() - data.startTime) / 1000);
779                elapsedTime = timeSinceStart;
780                startTimer();
781            }
782            
783            console.log('Timer state restored:', formatTime(elapsedTime));
784        } catch (e) {
785            console.error('Failed to restore timer state:', e);
786        }
787    }
788
789    // Laser pointer functions
790    function createLaserPointer() {
791        const laser = document.createElement('div');
792        laser.className = 'laser-pointer';
793        laser.style.display = 'none';
794        document.body.appendChild(laser);
795        return laser;
796    }
797
798    function toggleLaser() {
799        laserEnabled = !laserEnabled;
800        const laserBtn = document.querySelector('.laser-btn');
801        
802        if (laserEnabled) {
803            laserBtn.classList.add('active');
804            if (!laserPointer) {
805                laserPointer = createLaserPointer();
806            }
807            laserPointer.style.display = 'block';
808            document.body.style.cursor = 'none';
809            console.log('Laser pointer enabled');
810        } else {
811            laserBtn.classList.remove('active');
812            if (laserPointer) {
813                laserPointer.style.display = 'none';
814            }
815            document.body.style.cursor = 'auto';
816            console.log('Laser pointer disabled');
817        }
818    }
819
820    function updateLaserPosition(e) {
821        if (!laserEnabled || !laserPointer) return;
822        
823        laserPointer.style.left = (e.clientX - 10) + 'px';
824        laserPointer.style.top = (e.clientY - 10) + 'px';
825        
826        // Create trail effect
827        const trail = document.createElement('div');
828        trail.className = 'laser-trail';
829        trail.style.left = (e.clientX - 2) + 'px';
830        trail.style.top = (e.clientY - 2) + 'px';
831        document.body.appendChild(trail);
832        
833        setTimeout(() => trail.remove(), 500);
834    }
835
836    // Toggle toolbar collapse
837    function toggleCollapse() {
838        isCollapsed = !isCollapsed;
839        const toolbar = document.querySelector('#highlighter-toolbar');
840        const collapseBtn = document.querySelector('.toolbar-collapse-btn');
841        const extraSection = document.querySelector('.toolbar-extra');
842        
843        if (isCollapsed) {
844            toolbar.classList.add('collapsed');
845            collapseBtn.textContent = '▶';
846            if (extraSection) extraSection.classList.add('collapsed');
847            console.log('Toolbar collapsed');
848        } else {
849            toolbar.classList.remove('collapsed');
850            collapseBtn.textContent = '◀';
851            if (extraSection) extraSection.classList.remove('collapsed');
852            console.log('Toolbar expanded');
853        }
854        
855        // Save collapse state
856        try {
857            localStorage.setItem('highlighter_collapsed', isCollapsed);
858        } catch (e) {
859            console.error('Failed to save collapse state:', e);
860        }
861    }
862
863    function restoreCollapseState() {
864        try {
865            const collapsed = localStorage.getItem('highlighter_collapsed');
866            if (collapsed === 'true') {
867                isCollapsed = true;
868                document.querySelector('#highlighter-toolbar').classList.add('collapsed');
869                document.querySelector('.toolbar-collapse-btn').textContent = '▶';
870                const extraSection = document.querySelector('.toolbar-extra');
871                if (extraSection) extraSection.classList.add('collapsed');
872            }
873        } catch (e) {
874            console.error('Failed to restore collapse state:', e);
875        }
876    }
877
878    // Show tooltip with highlight info
879    function showHighlightTooltip(span, event) {
880        const existingTooltip = document.querySelector('.highlight-tooltip');
881        if (existingTooltip) existingTooltip.remove();
882        
883        const tooltip = document.createElement('div');
884        tooltip.className = 'highlight-tooltip';
885        tooltip.innerHTML = `
886            <div class="tooltip-content">
887                <div class="tooltip-text">"${span.textContent.substring(0, 100)}${span.textContent.length > 100 ? '...' : ''}"</div>
888                <div class="tooltip-actions">
889                    <button class="tooltip-btn delete-btn" title="Delete (Alt+Click)">🗑️</button>
890                    <button class="tooltip-btn copy-btn" title="Copy Text">📋</button>
891                </div>
892            </div>
893        `;
894        
895        document.body.appendChild(tooltip);
896        
897        const rect = span.getBoundingClientRect();
898        tooltip.style.left = rect.left + 'px';
899        tooltip.style.top = (rect.bottom + 10) + 'px';
900        
901        // Delete button
902        tooltip.querySelector('.delete-btn').addEventListener('click', () => {
903            removeHighlightSpan(span);
904            saveHighlights();
905            tooltip.remove();
906        });
907        
908        // Copy button
909        tooltip.querySelector('.copy-btn').addEventListener('click', () => {
910            navigator.clipboard.writeText(span.textContent);
911            const btn = tooltip.querySelector('.copy-btn');
912            btn.textContent = '✓';
913            setTimeout(() => btn.textContent = '📋', 1000);
914        });
915        
916        // Close on click outside
917        setTimeout(() => {
918            document.addEventListener('click', function closeTooltip(e) {
919                if (!tooltip.contains(e.target) && !span.contains(e.target)) {
920                    tooltip.remove();
921                    document.removeEventListener('click', closeTooltip);
922                }
923            });
924        }, 100);
925    }
926
927    // Show statistics modal
928    function showStatistics() {
929        const highlights = loadHighlights();
930        
931        const colorCounts = {
932            yellow: 0,
933            green: 0,
934            pink: 0,
935            blue: 0,
936            orange: 0
937        };
938        
939        highlights.forEach(h => {
940            if (colorCounts[h.color] !== undefined) {
941                colorCounts[h.color]++;
942            }
943        });
944        
945        const total = highlights.length;
946        const totalWords = highlights.reduce((sum, h) => sum + (h.exact || h.text || '').split(' ').length, 0);
947        const avgWordsPerHighlight = total > 0 ? Math.round(totalWords / total) : 0;
948        
949        const overlay = document.createElement('div');
950        overlay.className = 'modal-overlay';
951        
952        const modal = document.createElement('div');
953        modal.id = 'stats-modal';
954        modal.innerHTML = `
955            <div class="modal-header">
956                <div class="modal-title">📊 Highlight Statistics</div>
957                <button class="modal-close">×</button>
958            </div>
959            <div class="modal-body">
960                <div class="stat-item">
961                    <div class="stat-label">⏱️ Study Time</div>
962                    <div class="stat-value">${formatTime(elapsedTime)}</div>
963                </div>
964                <div class="stat-item">
965                    <div class="stat-label">📝 Total Highlights</div>
966                    <div class="stat-value">${total}</div>
967                </div>
968                <div class="stat-item">
969                    <div class="stat-label">📖 Total Words Highlighted</div>
970                    <div class="stat-value">${totalWords}</div>
971                </div>
972                <div class="stat-item">
973                    <div class="stat-label">📏 Avg Words per Highlight</div>
974                    <div class="stat-value">${avgWordsPerHighlight}</div>
975                </div>
976                <div class="stat-item">
977                    <div class="stat-label">
978                        <span class="color-indicator" style="background:${COLORS.yellow}"></span>
979                        Important (Yellow)
980                    </div>
981                    <div class="stat-value">${colorCounts.yellow}</div>
982                </div>
983                <div class="stat-item">
984                    <div class="stat-label">
985                        <span class="color-indicator" style="background:${COLORS.green}"></span>
986                        Definitions (Green)
987                    </div>
988                    <div class="stat-value">${colorCounts.green}</div>
989                </div>
990                <div class="stat-item">
991                    <div class="stat-label">
992                        <span class="color-indicator" style="background:${COLORS.pink}"></span>
993                        Key Points (Pink)
994                    </div>
995                    <div class="stat-value">${colorCounts.pink}</div>
996                </div>
997                <div class="stat-item">
998                    <div class="stat-label">
999                        <span class="color-indicator" style="background:${COLORS.blue}"></span>
1000                        Examples (Blue)
1001                    </div>
1002                    <div class="stat-value">${colorCounts.blue}</div>
1003                </div>
1004                <div class="stat-item">
1005                    <div class="stat-label">
1006                        <span class="color-indicator" style="background:${COLORS.orange}"></span>
1007                        Questions (Orange)
1008                    </div>
1009                    <div class="stat-value">${colorCounts.orange}</div>
1010                </div>
1011                <div class="stat-item">
1012                    <div class="stat-label">🎯 Study Progress</div>
1013                    <div class="stat-value">${Math.round((total / 50) * 100)}%</div>
1014                </div>
1015                <div class="progress-bar">
1016                    <div class="progress-fill" style="width:${Math.min(100, (total / 50) * 100)}%"></div>
1017                </div>
1018                <div class="stat-item" style="margin-top:16px; background:#e8f5e9;">
1019                    <div class="stat-label">💪 Highlights per Minute</div>
1020                    <div class="stat-value">${elapsedTime > 0 ? ((total / elapsedTime) * 60).toFixed(1) : '0.0'}</div>
1021                </div>
1022            </div>
1023        `;
1024        
1025        document.body.appendChild(overlay);
1026        document.body.appendChild(modal);
1027        
1028        modal.querySelector('.modal-close').addEventListener('click', () => {
1029            modal.remove();
1030            overlay.remove();
1031        });
1032        
1033        overlay.addEventListener('click', () => {
1034            modal.remove();
1035            overlay.remove();
1036        });
1037    }
1038
1039    // Export highlights to text file
1040    function exportHighlights() {
1041        const highlights = loadHighlights();
1042        
1043        if (highlights.length === 0) {
1044            alert('No highlights to export!');
1045            return;
1046        }
1047        
1048        let exportText = 'UpToDate Highlights Export\n';
1049        exportText += `Page: ${location.href}\n`;
1050        exportText += `Date: ${new Date().toLocaleString()}\n`;
1051        exportText += `Total Highlights: ${highlights.length}\n`;
1052        exportText += `Study Time: ${formatTime(elapsedTime)}\n\n`;
1053        exportText += `${'='.repeat(60)}\n\n`;
1054        
1055        const colorLabels = {
1056            yellow: '⭐ IMPORTANT',
1057            green: '📗 DEFINITION',
1058            pink: '🔑 KEY POINT',
1059            blue: '💡 EXAMPLE',
1060            orange: '❓ QUESTION'
1061        };
1062        
1063        highlights.forEach((h, idx) => {
1064            const label = colorLabels[h.color] || h.color.toUpperCase();
1065            exportText += `${idx + 1}. [${label}]\n`;
1066            exportText += `"${h.exact || h.text}"\n\n`;
1067        });
1068        
1069        const blob = new Blob([exportText], { type: 'text/plain' });
1070        const url = URL.createObjectURL(blob);
1071        const a = document.createElement('a');
1072        a.href = url;
1073        a.download = `uptodate-highlights-${Date.now()}.txt`;
1074        a.click();
1075        URL.revokeObjectURL(url);
1076        
1077        console.log('Highlights exported successfully!');
1078    }
1079
1080    // Import highlights from file
1081    function importHighlights() {
1082        const input = document.createElement('input');
1083        input.type = 'file';
1084        input.accept = '.json';
1085        
1086        input.addEventListener('change', (e) => {
1087            const file = e.target.files[0];
1088            if (!file) return;
1089            
1090            const reader = new FileReader();
1091            reader.onload = (event) => {
1092                try {
1093                    const imported = JSON.parse(event.target.result);
1094                    
1095                    if (!Array.isArray(imported)) {
1096                        alert('Invalid file format!');
1097                        return;
1098                    }
1099                    
1100                    // Merge with existing highlights
1101                    const existing = loadHighlights();
1102                    const merged = [...existing, ...imported];
1103                    
1104                    localStorage.setItem(getPageKey(), JSON.stringify(merged));
1105                    
1106                    // Restore the imported highlights
1107                    restoreHighlights();
1108                    
1109                    alert(`Imported ${imported.length} highlights successfully!`);
1110                    console.log('Highlights imported:', imported.length);
1111                } catch (err) {
1112                    console.error('Failed to import highlights:', err);
1113                    alert('Failed to import file. Make sure it\'s a valid JSON file.');
1114                }
1115            };
1116            
1117            reader.readAsText(file);
1118        });
1119        
1120        input.click();
1121    }
1122
1123    // Restore highlights from localStorage
1124    function restoreHighlights() {
1125        try {
1126            const pageKey = getPageKey();
1127            const saved = localStorage.getItem(pageKey);
1128            
1129            if (!saved) {
1130                console.log('No saved highlights found for this page');
1131                return;
1132            }
1133
1134            const highlights = JSON.parse(saved);
1135            console.log('🔄 Trying to restore', highlights.length, 'highlights...');
1136
1137            let restoredCount = 0;
1138            
1139            highlights.forEach((highlight, idx) => {
1140                try {
1141                    // Check if already restored
1142                    if (document.querySelector(`[data-highlight-id="${highlight.id}"]`)) {
1143                        restoredCount++;
1144                        return;
1145                    }
1146                    
1147                    // Use exact field (not text field)
1148                    const textToFind = highlight.exact || highlight.text;
1149                    if (!textToFind) {
1150                        console.warn('No text to restore for highlight', idx + 1);
1151                        return;
1152                    }
1153                    
1154                    console.log('Restoring highlight', idx + 1, ':', textToFind.substring(0, 30) + '...');
1155                    
1156                    // Try XPath method first if available
1157                    if (highlight.xpath && highlight.textIndex !== undefined && highlight.textIndex !== '') {
1158                        console.log('Trying XPath method for highlight', idx + 1);
1159                        const parentEl = resolveXPath(highlight.xpath);
1160                        if (parentEl && parentEl.childNodes[highlight.textIndex]) {
1161                            const textNode = parentEl.childNodes[highlight.textIndex];
1162                            if (textNode.nodeType === Node.TEXT_NODE) {
1163                                const raw = textNode.nodeValue;
1164                                const normRaw = normalizeText(raw);
1165                                const normSearch = normalizeText(textToFind);
1166                                const normIdx = normRaw.indexOf(normSearch);
1167                                
1168                                if (normIdx !== -1) {
1169                                    // Map back to raw index
1170                                    let rawIdx = 0;
1171                                    let normIdx2 = 0;
1172                                    
1173                                    while (normIdx2 < normIdx && rawIdx < raw.length) {
1174                                        if (/\s/.test(raw[rawIdx])) {
1175                                            while (rawIdx < raw.length && /\s/.test(raw[rawIdx])) rawIdx++;
1176                                            if (normIdx2 < normRaw.length && normRaw[normIdx2] === ' ') normIdx2++;
1177                                        } else {
1178                                            rawIdx++;
1179                                            normIdx2++;
1180                                        }
1181                                    }
1182                                    
1183                                    const span = document.createElement('span');
1184                                    span.className = `text-highlight ${highlight.color}`;
1185                                    span.setAttribute('data-highlight-id', highlight.id);
1186                                    span.setAttribute('data-prefix', highlight.prefix || '');
1187                                    span.setAttribute('data-suffix', highlight.suffix || '');
1188                                    span.setAttribute('data-xpath', highlight.xpath);
1189                                    span.setAttribute('data-text-index', highlight.textIndex);
1190                                    
1191                                    // Use replaceChild method for reliability
1192                                    const parent = textNode.parentNode;
1193                                    const before = raw.slice(0, rawIdx);
1194                                    const selected = raw.slice(rawIdx, rawIdx + textToFind.length);
1195                                    const after = raw.slice(rawIdx + textToFind.length);
1196                                    
1197                                    span.textContent = selected;
1198                                    
1199                                    if (before) parent.insertBefore(document.createTextNode(before), textNode);
1200                                    parent.insertBefore(span, textNode);
1201                                    if (after) parent.insertBefore(document.createTextNode(after), textNode);
1202                                    parent.removeChild(textNode);
1203                                    
1204                                    restoredCount++;
1205                                    console.log('✓ XPath restore successful for highlight', idx + 1);
1206                                    return;
1207                                } else {
1208                                    console.log('Text not found in XPath node for highlight', idx + 1);
1209                                }
1210                            } else {
1211                                console.log('XPath node is not a text node for highlight', idx + 1);
1212                            }
1213                        } else {
1214                            console.log('XPath resolution failed for highlight', idx + 1);
1215                        }
1216                    }
1217                    
1218                    // Fallback to text search with context
1219                    console.log('Trying text search method for highlight', idx + 1);
1220                    const match = findBestMatch(textToFind, highlight.prefix || '', highlight.suffix || '');
1221                    
1222                    if (match) {
1223                        const span = document.createElement('span');
1224                        span.className = `text-highlight ${highlight.color}`;
1225                        span.setAttribute('data-highlight-id', highlight.id);
1226                        span.setAttribute('data-prefix', highlight.prefix || '');
1227                        span.setAttribute('data-suffix', highlight.suffix || '');
1228                        
1229                        // Use replaceChild method
1230                        const parent = match.node.parentNode;
1231                        const raw = match.node.nodeValue;
1232                        const before = raw.slice(0, match.index);
1233                        const selected = raw.slice(match.index, match.index + textToFind.length);
1234                        const after = raw.slice(match.index + textToFind.length);
1235                        
1236                        span.textContent = selected;
1237                        
1238                        if (before) parent.insertBefore(document.createTextNode(before), match.node);
1239                        parent.insertBefore(span, match.node);
1240                        if (after) parent.insertBefore(document.createTextNode(after), match.node);
1241                        parent.removeChild(match.node);
1242                        
1243                        restoredCount++;
1244                        console.log('✓ Text search successful for highlight', idx + 1);
1245                    } else {
1246                        console.warn('✗ Could not find text for highlight', idx + 1, ':', textToFind.substring(0, 30) + '...');
1247                    }
1248                } catch (e) {
1249                    console.error('Failed to restore highlight', idx + 1, ':', e);
1250                }
1251            });
1252            
1253            console.log('=== Restored:', restoredCount, '/', highlights.length, '===');
1254        } catch (e) {
1255            console.error('Failed to restore highlights:', e);
1256        }
1257    }
1258
1259    // Setup MutationObserver to watch for dynamic content
1260    function setupContentObserver() {
1261        console.log('Setting up MutationObserver');
1262        
1263        let isRestoring = false;
1264        
1265        const debouncedRestore = debounce(() => {
1266            if (isRestoring) return;
1267            isRestoring = true;
1268            console.log('Content changed, restoring...');
1269            restoreHighlights();
1270            setTimeout(() => {
1271                isRestoring = false;
1272            }, 1000);
1273        }, 2000);
1274
1275        const observer = new MutationObserver((mutations) => {
1276            let shouldRestore = false;
1277            
1278            for (const mutation of mutations) {
1279                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
1280                    for (const node of mutation.addedNodes) {
1281                        if (node.nodeType === Node.ELEMENT_NODE) {
1282                            if (node.textContent && node.textContent.length > 100) {
1283                                shouldRestore = true;
1284                                break;
1285                            }
1286                        }
1287                    }
1288                }
1289                if (shouldRestore) break;
1290            }
1291            
1292            if (shouldRestore) {
1293                debouncedRestore();
1294            }
1295        });
1296
1297        const contentArea = document.body;
1298        if (contentArea) {
1299            observer.observe(contentArea, {
1300                childList: true,
1301                subtree: true
1302            });
1303            console.log('MutationObserver started');
1304        }
1305    }
1306
1307    // Handle keyboard shortcuts
1308    function handleKeyDown(e) {
1309        if (e.altKey && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
1310            const selection = window.getSelection();
1311            
1312            if (selection && !selection.isCollapsed && selection.toString().trim().length > 0) {
1313                try {
1314                    const range = selection.getRangeAt(0);
1315                    const container = range.startContainer;
1316                    const parentElement = container.nodeType === Node.TEXT_NODE ? container.parentElement : container;
1317                    
1318                    if (!parentElement) return;
1319                    
1320                    if (parentElement.closest('#highlighter-toolbar') || parentElement.classList.contains('text-highlight')) {
1321                        return;
1322                    }
1323                    
1324                    if (e.key === 'Alt') {
1325                        e.preventDefault();
1326                        highlightSelection(selectedColor);
1327                        console.log('Text highlighted with Option/Alt key');
1328                    }
1329                } catch (err) {
1330                    console.error('Error in keyboard handler:', err);
1331                }
1332            }
1333        }
1334    }
1335
1336    function init() {
1337        console.log('🚀 Initializing UpToDate Highlighter...');
1338        
1339        addStyles();
1340        const toolbar = createToolbar();
1341        makeDraggable(toolbar);
1342
1343        const highlightButtons = toolbar.querySelectorAll('.highlight-btn');
1344        highlightButtons.forEach((btn) => {
1345            btn.addEventListener('click', () => {
1346                const color = btn.getAttribute('data-color');
1347                selectedColor = color;
1348
1349                highlightButtons.forEach((b) => b.classList.remove('active'));
1350                btn.classList.add('active');
1351
1352                const sel = window.getSelection();
1353                if (sel && !sel.isCollapsed) highlightSelection(color);
1354            });
1355        });
1356
1357        toolbar.querySelector('.clear-btn').addEventListener('click', clearAllHighlights);
1358
1359        setupPerHighlightRemoval();
1360        document.addEventListener('keydown', handleKeyDown);
1361        setupContentObserver();
1362        
1363        // Stats button
1364        toolbar.querySelector('.stats-btn').addEventListener('click', showStatistics);
1365        
1366        // Export button
1367        toolbar.querySelector('.export-btn').addEventListener('click', exportHighlights);
1368        
1369        // Import button
1370        toolbar.querySelector('.import-btn').addEventListener('click', importHighlights);
1371        
1372        // Timer buttons
1373        toolbar.querySelector('.start-btn').addEventListener('click', startTimer);
1374        toolbar.querySelector('.pause-btn').addEventListener('click', pauseTimer);
1375        toolbar.querySelector('.reset-btn').addEventListener('click', resetTimer);
1376        
1377        // Restore timer state
1378        restoreTimerState();
1379        
1380        // Laser pointer button
1381        toolbar.querySelector('.laser-btn').addEventListener('click', toggleLaser);
1382        
1383        // Laser pointer mouse tracking
1384        document.addEventListener('mousemove', updateLaserPosition);
1385        
1386        // Collapse button
1387        toolbar.querySelector('.toolbar-collapse-btn').addEventListener('click', toggleCollapse);
1388        
1389        // Restore collapse state
1390        restoreCollapseState();
1391
1392        // Restore highlights with multiple attempts - more aggressive timing
1393        console.log('Starting highlight restoration attempts...');
1394        restoreHighlights();
1395        setTimeout(() => {
1396            console.log('Restoration attempt 2/6');
1397            restoreHighlights();
1398        }, 1000);
1399        setTimeout(() => {
1400            console.log('Restoration attempt 3/6');
1401            restoreHighlights();
1402        }, 2500);
1403        setTimeout(() => {
1404            console.log('Restoration attempt 4/6');
1405            restoreHighlights();
1406        }, 5000);
1407        setTimeout(() => {
1408            console.log('Restoration attempt 5/6');
1409            restoreHighlights();
1410        }, 8000);
1411        setTimeout(() => {
1412            console.log('Restoration attempt 6/6 (final)');
1413            restoreHighlights();
1414        }, 12000);
1415        
1416        console.log('✅ Highlighter initialized successfully!');
1417    }
1418
1419    if (document.readyState === 'loading') {
1420        document.addEventListener('DOMContentLoaded', init);
1421    } else {
1422        init();
1423    }
1424})();
UpToDate Text Highlighter (Option/Alt Only) | Robomonkey