/** * Content script Memento — sélection live, surlignage, communication avec le side panel. * Injecté automatiquement sur http(s) ; ré-injecté à la demande si l'onglet était déjà ouvert. * * IMPORTANT : la bannière "Surlignez le texte à clipper" s'affiche dès * l'injection du script, sans dépendre de SET_PICK_MODE. C'est volontaire : * en MV3, le routage des messages entre side panel et content script * peut être flaky (race conditions entre injection et listeners), donc * on rend la bannière visible dès qu'on est là. L'utilisateur peut la * fermer via le bouton ✕. */ ;(function initMementoClipperContent() { if (globalThis.__mementoClipperContent) return globalThis.__mementoClipperContent = true const HIGHLIGHT_ID = 'memento-clipper-highlight-root' const BANNER_ID = 'memento-clipper-banner-root' const STYLE_ID = 'memento-clipper-styles' let pickMode = false let bannerDismissed = false let debounceTimer = null function isRestrictedUrl(url) { return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools|file):/i.test(url) } function getSelectionText() { return window.getSelection()?.toString().trim() || '' } function getPageMeta() { const dir = document.documentElement.getAttribute('dir') || document.body?.getAttribute('dir') || '' const lang = ( document.documentElement.getAttribute('lang') || document.body?.getAttribute('lang') || '' ).split('-')[0] return { text: getSelectionText(), dir, lang, url: location.href, title: document.title, } } function broadcastSelection() { clearTimeout(debounceTimer) debounceTimer = setTimeout(() => { const payload = { type: 'SELECTION_CHANGED', ...getPageMeta() } try { chrome.runtime.sendMessage(payload).catch(() => {}) } catch { /* ignore */ } if (pickMode) paintHighlight() }, 80) } function removeHighlight() { document.getElementById(HIGHLIGHT_ID)?.remove() } function paintHighlight() { removeHighlight() const sel = window.getSelection() if (!sel || sel.isCollapsed || !sel.rangeCount) return let range try { range = sel.getRangeAt(0) } catch { return } const host = document.createElement('div') host.id = HIGHLIGHT_ID host.setAttribute('aria-hidden', 'true') host.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483644;overflow:hidden;' for (const rect of range.getClientRects()) { if (rect.width < 2 || rect.height < 2) continue const box = document.createElement('div') box.style.cssText = [ 'position:fixed', `left:${rect.left - 2}px`, `top:${rect.top - 1}px`, `width:${rect.width + 4}px`, `height:${rect.height + 2}px`, 'background:rgba(164,113,72,0.28)', 'border-radius:3px', 'box-shadow:0 0 0 1px rgba(164,113,72,0.35)', 'transition:opacity 0.15s ease', ].join(';') host.appendChild(box) } if (host.childNodes.length) document.documentElement.appendChild(host) } function ensureStyles() { if (document.getElementById(STYLE_ID)) return const style = document.createElement('style') style.id = STYLE_ID style.textContent = ` html.memento-clipper-pick ::selection { background: rgba(164, 113, 72, 0.45) !important; color: inherit !important; } html.memento-clipper-pick { scroll-behavior: auto; } ` document.documentElement.appendChild(style) } function removeBanner() { document.getElementById(BANNER_ID)?.remove() } function ensureBanner() { if (document.getElementById(BANNER_ID)) return if (!shouldShowBanner()) return const bannerText = (typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) || 'Highlight the text on the page, or clip the whole page.' const dismissLabel = (typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerDismiss')) || 'Dismiss' const host = document.createElement('div') host.id = BANNER_ID // Le host est positionné en fixed (hors flux) au top-center. host.style.cssText = 'position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;font-family:Inter,system-ui,-apple-system,sans-serif;' const shadow = host.attachShadow({ mode: 'open' }) shadow.innerHTML = `