/** * 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 = `
${bannerText.replace(/
` document.documentElement.appendChild(host) // Bind le bouton ✕ — ferme la bannière pour cette session d'onglet. const closeBtn = shadow.querySelector('.close') if (closeBtn) { closeBtn.addEventListener('click', (e) => { e.preventDefault() e.stopPropagation() bannerDismissed = true removeBanner() }) } } function setPickMode(enabled) { pickMode = !!enabled ensureStyles() if (pickMode) { document.documentElement.classList.add('memento-clipper-pick') ensureBanner() paintHighlight() } else { document.documentElement.classList.remove('memento-clipper-pick') removeBanner() removeHighlight() } } function onScrollOrResize() { if (pickMode) paintHighlight() } // Ne rien faire sur les pages restreintes (chrome://, file://, etc.) if (isRestrictedUrl(location.href)) { // On écoute quand même les messages au cas où le side panel veut // quand même parler (ex: pour info). } else { document.addEventListener('selectionchange', broadcastSelection) document.addEventListener('mouseup', broadcastSelection) document.addEventListener('keyup', broadcastSelection) window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true }) window.addEventListener('resize', onScrollOrResize, { passive: true }) } chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.type === 'PING') { sendResponse({ ok: true, pickMode }) return true } if (message?.type === 'GET_CONTEXT') { sendResponse({ html: document.documentElement.outerHTML, ...getPageMeta(), }) return true } if (message?.type === 'SET_PICK_MODE') { setPickMode(!!message.enabled) sendResponse({ ok: true, pickMode }) return true } if (message?.type === 'DISMISS_BANNER') { bannerDismissed = true removeBanner() sendResponse({ ok: true }) return true } return false }) // === AFFICHAGE IMMÉDIAT DE LA BANNIÈRE === // On n'attend pas SET_PICK_MODE — le side panel peut être lent à nous // parler (race conditions MV3). La bannière est le seul signal visuel // pour l'utilisateur que l'extension est active et qu'il peut clipper. // L'utilisateur peut la fermer via le bouton ✕. if (!isRestrictedUrl(location.href)) { setPickMode(true) } // Notifie le side panel qu'on est là (peut aider à amorcer). broadcastSelection() })()