Files

164 lines
5.3 KiB
JavaScript

/* math_copy.js — click-to-copy LaTeX source for IHUFT display equations.
*
* Every display equation produced by hooks/math_prerender.py is wrapped in
*
* <div class="math math-display" data-latex="…raw LaTeX source…">
* <span class="katex"> … pre-rendered KaTeX HTML …</span>
* <button class="math-copy" type="button" data-tooltip="Copy LaTeX">
* <svg class="math-copy__icon"> … clipboard glyph …</svg>
* <svg class="math-copy__check"> … checkmark glyph …</svg>
* </button>
* </div>
*
* This script attaches a single delegated click/keyboard handler to the
* document and, when the user activates a .math-copy button, reads the
* `data-latex` attribute from the surrounding .math-display, copies it to
* the system clipboard, and toggles the .is-copied class on the button
* for ~1.2 s so the CSS (see docs/stylesheets/extra.css) can swap the
* clipboard glyph for a check mark and the "Copy LaTeX" tooltip for
* "Copied!".
*
* Graceful degradation:
* • Without JS at all → math still renders perfectly; the button is
* simply inert; LaTeX source remains recoverable from the MathML
* <annotation> element and the data-latex attribute via DevTools.
* • Without navigator.clipboard (older browsers / non-secure context) →
* falls back to the legacy document.execCommand('copy') path using a
* hidden <textarea>.
*/
(function () {
"use strict";
var COPIED_CLASS = "is-copied";
var COPIED_DURATION_MS = 1200;
function copyViaClipboardAPI(text) {
if (
typeof navigator !== "undefined" &&
navigator.clipboard &&
typeof navigator.clipboard.writeText === "function"
) {
return navigator.clipboard.writeText(text);
}
return null;
}
function copyViaFallback(text) {
// Hidden textarea + execCommand('copy') is the universally supported
// path for browsers that lack the async Clipboard API or that
// refuse to use it outside a secure context.
var ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.top = "0";
ta.style.left = "0";
ta.style.width = "1px";
ta.style.height = "1px";
ta.style.padding = "0";
ta.style.border = "0";
ta.style.outline = "0";
ta.style.boxShadow = "none";
ta.style.background = "transparent";
ta.style.opacity = "0";
document.body.appendChild(ta);
var selection = document.getSelection();
var previousRange =
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
ta.select();
var ok = false;
try {
ok = document.execCommand("copy");
} catch (e) {
ok = false;
}
document.body.removeChild(ta);
if (previousRange && selection) {
selection.removeAllRanges();
selection.addRange(previousRange);
}
return ok
? Promise.resolve()
: Promise.reject(new Error("execCommand('copy') failed"));
}
function flashCopied(button) {
button.classList.add(COPIED_CLASS);
// Tear-off any previously scheduled cleanup so rapid double-clicks
// restart the timer rather than letting the first one cut the second
// off prematurely.
if (button.__copyTimer) {
clearTimeout(button.__copyTimer);
}
button.__copyTimer = setTimeout(function () {
button.classList.remove(COPIED_CLASS);
button.__copyTimer = null;
}, COPIED_DURATION_MS);
}
function handleActivation(button) {
var container = button.closest(".math-display");
if (!container) return;
var tex = container.getAttribute("data-latex");
if (typeof tex !== "string" || tex.length === 0) return;
// Decode HTML entities that may have crept in via _html_escape in the
// Python hook (it escapes &, <, >, ", '), so the clipboard receives
// the raw LaTeX source the author wrote rather than the HTML-safe
// representation.
var decoded = decodeEntities(tex);
var p = copyViaClipboardAPI(decoded);
if (p && typeof p.then === "function") {
p.then(
function () {
flashCopied(button);
},
function () {
copyViaFallback(decoded).then(
function () {
flashCopied(button);
},
function () {
/* swallow — user can still copy via DevTools or selection */
}
);
}
);
} else {
copyViaFallback(decoded).then(
function () {
flashCopied(button);
},
function () {
/* swallow */
}
);
}
}
// Browser-decodes &amp; / &lt; / &gt; / &quot; / &#39; back to their raw
// characters by parsing through a textarea. Cheap, dependency-free,
// and exactly what we want for HTML attribute round-tripping.
function decodeEntities(s) {
var ta = document.createElement("textarea");
ta.innerHTML = s;
return ta.value;
}
document.addEventListener("click", function (e) {
var btn = e.target.closest && e.target.closest(".math-copy");
if (!btn) return;
e.preventDefault();
handleActivation(btn);
});
document.addEventListener("keydown", function (e) {
if (e.key !== "Enter" && e.key !== " ") return;
var btn =
e.target.closest && e.target.closest(".math-copy");
if (!btn) return;
e.preventDefault();
handleActivation(btn);
});
})();