/*! trentpower.fr · authored source */ /* trentpower.fr copy-to-clipboard Role: one shared copy-to-clipboard behaviour for the whole site. a single delegated click listener on document handles every copy trigger, so the cite overlay, the verify pages and the source reader no longer each carry their own clipboard code. Source: edited here as copy.template.js; compiled to copy.js by generate_site.py (minified, no substitution). Contract: a copy trigger is any element matching [data-copy-target] or [data-copy-text]. data-copy-target id of the element whose textContent is copied data-copy-text a literal string to copy (wins over -target) data-copy-collapse "whitespace" → collapse whitespace runs + trim data-copy-feedback success button text (default: localised "Copied") data-copy-announce id of an aria-live region for the success message data-copy-mode "cite" → on success the trigger's label becomes its own confirmation in place ("Cited · Edition YYYY-MM-DD") for ~1 s, then restores. on failure the label becomes "Copy failed" / "Échec de la copie" briefly. the row is briefly stamped by language; no floating layer, no overlay child, no absolute positioning. default (no mode) → legacy textContent swap to data-copy-feedback ("Copy fingerprint" → "Copied") for verb-labelled buttons. while the feedback window is open the trigger carries data-state="copied" (success) or data-state="failed" (failure) as a css hook. also exposes window.TP_COPY.copy(text) → Promise for callers that build their payload dynamically (the source reader's selection copy). Constraints: - no fetch, no inline handlers, no third-party scripts - enhancement only: every copy target is plain readable text in the page, so the page means the same with this script absent */ (function () { 'use strict'; var REVERT_MS = 1500; var CITED_MS = 1000; function lang() { return document.documentElement.lang === 'fr' ? 'fr' : 'en'; } function copiedLabel() { return lang() === 'fr' ? 'Copié' : 'Copied'; } // in-place citation confirmation labels. the trigger row's own text // becomes "Cited · Edition YYYY-MM-DD" for ~1 s on a successful // citation copy, then restores. no floating layer. function citedLabel() { return lang() === 'fr' ? 'Citée' : 'Cited'; } function editionWord() { return lang() === 'fr' ? 'Édition' : 'Edition'; } function copyFailedLabel() { return lang() === 'fr' ? 'Échec de la copie' : 'Copy failed'; } function copyFailedAnnounce() { return lang() === 'fr' ? 'Échec de la copie de citation' : 'Citation copy failed'; } // edition source priority: body[data-edition] (substituted at build // time on every page) → . both are // swept to the canonical edition by generate_site.py; reading either // means the inscription always matches the page the visitor is on. function editionDate() { var b = document.body && document.body.dataset && document.body.dataset.edition; if (b) return b; var m = document.querySelector('meta[name="document-edition"]'); return m ? (m.getAttribute('content') || '') : ''; } // hidden-textarea + execCommand fallback for non-secure contexts and // older engines where the async clipboard api is absent or rejects. function execCommandCopy(text) { return new Promise(function (resolve, reject) { try { var ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); // phase 96 · class, not inline style (csp style-src-attr 'none'). ta.className = 'tp-copy-fallback'; document.body.appendChild(ta); ta.select(); var ok = document.execCommand && document.execCommand('copy'); document.body.removeChild(ta); if (ok) resolve(); else reject(); } catch (_) { reject(); } }); } // two-tier clipboard write: the async clipboard api first, the // textarea fallback when it is absent OR rejects (older safari, a // denied permission, a non-secure context). resolves on success. function writeClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text).catch(function () { return execCommandCopy(text); }); } return execCommandCopy(text); } function payloadFor(trigger) { if (trigger.hasAttribute('data-copy-text')) { return trigger.getAttribute('data-copy-text') || ''; } var targetId = trigger.getAttribute('data-copy-target'); var target = targetId ? document.getElementById(targetId) : null; var raw = target ? (target.textContent || '') : ''; if (trigger.getAttribute('data-copy-collapse') === 'whitespace') { return raw.replace(/\s+/g, ' ').trim(); } return raw.replace(/\s+$/, ''); } // ─── aria-live region helper ─────────────────────────────────────────── // resolves the live region keyed by data-copy-announce, empties it, // then writes the message on the next tick so assistive tech reliably // notices the change. returns the region (or null) so callers can // clear it again when the visible feedback window closes. function announce(trigger, msg) { var announceId = trigger.getAttribute('data-copy-announce'); var region = announceId ? document.getElementById(announceId) : null; if (!region) return null; region.textContent = ''; setTimeout(function () { region.textContent = msg; }, 0); return region; } // ─── feedback (success) ──────────────────────────────────────────────── function feedback(trigger) { if (trigger.getAttribute('data-state') === 'copied' || trigger.getAttribute('data-state') === 'failed') return; var done = trigger.getAttribute('data-copy-feedback') || copiedLabel(); var region = announce(trigger, done); // mode A · in-place citation confirmation. the trigger's visible // label becomes its own confirmation ("Cited · Edition YYYY-MM-DD") // for ~1 s, then restores. the row is briefly stamped by language; // no floating layer, no overlay child, no absolute positioning. if (trigger.getAttribute('data-copy-mode') === 'cite') { var resting = trigger.textContent; var ed = editionDate(); trigger.textContent = ed ? citedLabel() + ' · ' + editionWord() + ' ' + ed : citedLabel(); trigger.setAttribute('data-state', 'copied'); // store the resting label so rapid repeat clicks restore the // original rather than the cited inscription. trigger._restingLabel = resting; if (trigger._restoreTimer) window.clearTimeout(trigger._restoreTimer); trigger._restoreTimer = window.setTimeout(function () { trigger.textContent = trigger._restingLabel || resting; trigger.removeAttribute('data-state'); trigger._restingLabel = null; trigger._restoreTimer = null; if (region) region.textContent = ''; }, CITED_MS); // micro-int #10 · also signal the citation source text so it // can flicker italic→roman→italic. micro-interactions.js listens // for this event and toggles .is-acknowledging on .tp-citation. // the in-place "Cited · Edition …" label above remains the // primary confirmation (per the no-floating-feedback rule); // the source flash is a second quiet beat. try { document.dispatchEvent(new CustomEvent('tp:citation-copied')); } catch (_) {} return; } // mode B · textContent swap. legacy/default for buttons whose // resting label is a verb ("Copy fingerprint" → "Copied"). var resting2 = trigger.textContent; trigger.textContent = done; trigger.setAttribute('data-state', 'copied'); setTimeout(function () { trigger.textContent = resting2; trigger.removeAttribute('data-state'); if (region) region.textContent = ''; }, REVERT_MS); } // ─── failure (cite mode only) ────────────────────────────────────────── // clipboard write rejected — denied permission, non-secure context, // engine without execCommand fallback. only the cite-mode trigger // surfaces this; the legacy verb-button swap stays silent on failure // as it always has. function failure(trigger) { if (trigger.getAttribute('data-copy-mode') !== 'cite') return; if (trigger.getAttribute('data-state') === 'copied' || trigger.getAttribute('data-state') === 'failed') return; var resting = trigger.textContent; trigger.textContent = copyFailedLabel(); trigger.setAttribute('data-state', 'failed'); var region = announce(trigger, copyFailedAnnounce()); trigger._restingLabel = resting; if (trigger._restoreTimer) window.clearTimeout(trigger._restoreTimer); trigger._restoreTimer = window.setTimeout(function () { trigger.textContent = trigger._restingLabel || resting; trigger.removeAttribute('data-state'); trigger._restingLabel = null; trigger._restoreTimer = null; if (region) region.textContent = ''; }, CITED_MS); } document.addEventListener('click', function (e) { var trigger = e.target && e.target.closest ? e.target.closest('[data-copy-target], [data-copy-text]') : null; if (!trigger) return; var payload = payloadFor(trigger); if (!payload) return; e.preventDefault(); writeClipboard(payload).then( function () { feedback(trigger); }, function () { failure(trigger); } ); }); // programmatic hook for callers that assemble their payload at click // time (e.g. the source reader copying the current line selection). window.TP_COPY = { copy: function (text) { return writeClipboard(String(text == null ? '' : text)); } }; })();