164 lines
5.3 KiB
JavaScript
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 & / < / > / " / ' 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);
|
|
});
|
|
})();
|