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 23, 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})();