Files
MokoCassiopeia/src/media/js/template.js
Jonathan Miller cb4468af19
Some checks failed
Repo Health / Access control (push) Failing after 1s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Auto-Update SHA Hash / Update SHA-256 Hash in updates.xml (release) Failing after 40s
v03.09.16: brand-aside columns, offline page redesign, variable click-to-copy
- Brand-aside position now uses flex columns like top-a (card style, equal-width)
- Offline page: external offline.css with theme variables, 3-column centered card
  layout, Osaka font loading, full-screen on mobile
- CSS variable click-to-copy: scans text for --var patterns, wraps in clickable
  chips with toast notification on copy
- Search button border matches input border (--input-border-color)
- mod_stats override: converted from dl to table layout
- Patch bump 03.09.15 → 03.09.16

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:56:20 -05:00

827 lines
26 KiB
JavaScript

/* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
*/
(function (win, doc) {
"use strict";
// ========================================================================
// THEME INITIALIZATION (Early theme application)
// ========================================================================
var storageKey = "theme";
var mql = win.matchMedia("(prefers-color-scheme: dark)");
var root = doc.documentElement;
/**
* Apply theme to <html>, syncing both data-bs-theme and data-aria-theme.
* @param {"light"|"dark"} theme
*/
function applyTheme(theme) {
root.setAttribute("data-bs-theme", theme);
root.setAttribute("data-aria-theme", theme);
try { localStorage.setItem(storageKey, theme); } catch (e) {}
}
/**
* Clear stored preference so system preference is followed.
*/
function clearStored() {
try { localStorage.removeItem(storageKey); } catch (e) {}
}
/**
* Determine system theme.
*/
function systemTheme() {
return mql.matches ? "dark" : "light";
}
/**
* Get stored theme preference.
*/
function getStored() {
try { return localStorage.getItem(storageKey); } catch (e) { return null; }
}
// ========================================================================
// FLOATING THEME TOGGLE (FAB)
// ========================================================================
function posClassFromBody() {
var pos = (doc.body.getAttribute('data-theme-fab-pos') || 'br').toLowerCase();
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = 'br';
return 'pos-' + pos;
}
function buildThemeToggle() {
if (doc.getElementById('mokoThemeFab')) return;
var wrap = doc.createElement('div');
wrap.id = 'mokoThemeFab';
wrap.className = posClassFromBody();
// Sun/Moon toggle button
var switchWrap = doc.createElement('button');
switchWrap.id = 'mokoThemeSwitch';
switchWrap.type = 'button';
switchWrap.className = 'theme-icon-btn';
switchWrap.setAttribute('aria-label', 'Toggle dark mode');
var sunIcon = doc.createElement('i');
sunIcon.className = 'fa-solid fa-sun';
sunIcon.setAttribute('aria-hidden', 'true');
var moonIcon = doc.createElement('i');
moonIcon.className = 'fa-solid fa-moon';
moonIcon.setAttribute('aria-hidden', 'true');
switchWrap.appendChild(sunIcon);
switchWrap.appendChild(moonIcon);
function updateThemeIcon(theme) {
if (theme === 'dark') {
switchWrap.classList.add('is-dark');
switchWrap.classList.remove('is-light');
} else {
switchWrap.classList.add('is-light');
switchWrap.classList.remove('is-dark');
}
}
// Auto toggle (on/off switch style)
var autoWrap = doc.createElement('div');
autoWrap.className = 'auto-toggle-wrap';
var autoLabel = doc.createElement('span');
autoLabel.className = 'auto-label';
autoLabel.textContent = 'Auto';
var auto = doc.createElement('button');
auto.id = 'mokoThemeAuto';
auto.type = 'button';
auto.className = 'auto-switch';
auto.setAttribute('role', 'switch');
auto.setAttribute('aria-label', 'Automatic theme (follow system)');
auto.setAttribute('aria-checked', getStored() ? 'false' : 'true');
var autoTrack = doc.createElement('span');
autoTrack.className = 'auto-track';
var autoKnob = doc.createElement('span');
autoKnob.className = 'auto-knob';
autoTrack.appendChild(autoKnob);
auto.appendChild(autoTrack);
if (!getStored()) auto.classList.add('on');
autoWrap.appendChild(autoLabel);
autoWrap.appendChild(auto);
// Divider before a11y slot
var divider = doc.createElement('span');
divider.className = 'fab-divider';
// A11y slot — buildA11yToolbar will inject its toggle here
var a11ySlot = doc.createElement('span');
a11ySlot.id = 'mokoA11ySlot';
// Behavior
switchWrap.addEventListener('click', function () {
var current = (root.getAttribute('data-bs-theme') || 'light').toLowerCase();
var next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
updateThemeIcon(next);
// Turn off auto when manually switching
auto.classList.remove('on');
auto.setAttribute('aria-checked', 'false');
// Update meta theme color
var meta = doc.querySelector('meta[name="theme-color"]');
if (meta) {
meta.setAttribute('content', next === 'dark' ? '#0f1115' : '#ffffff');
}
});
auto.addEventListener('click', function () {
var isAuto = auto.classList.toggle('on');
auto.setAttribute('aria-checked', isAuto ? 'true' : 'false');
if (isAuto) {
clearStored();
var sys = systemTheme();
applyTheme(sys);
updateThemeIcon(sys);
}
});
// Respond to OS changes only when not user-forced
var onMql = function () {
if (!getStored()) {
var sys = systemTheme();
applyTheme(sys);
updateThemeIcon(sys);
}
};
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
else if (typeof mql.addListener === 'function') mql.addListener(onMql);
// Initial state
var initial = getStored() || systemTheme();
updateThemeIcon(initial);
// Mount
wrap.appendChild(switchWrap);
wrap.appendChild(autoWrap);
wrap.appendChild(divider);
wrap.appendChild(a11ySlot);
doc.body.appendChild(wrap);
// Debug helper
win.mokoThemeFabStatus = function () {
var el = doc.getElementById('mokoThemeFab');
if (!el) return { mounted: false };
var r = el.getBoundingClientRect();
return {
mounted: true,
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
zIndex: win.getComputedStyle(el).zIndex,
posClass: el.className
};
};
// Outline if invisible
setTimeout(function () {
var r = wrap.getBoundingClientRect();
if (r.width < 10 || r.height < 10) {
wrap.classList.add('debug-outline');
console.warn('[moko] Theme FAB mounted but appears too small — check CSS collisions.');
}
}, 50);
}
// ========================================================================
// ACCESSIBILITY TOOLBAR
// ========================================================================
var a11yStorageKey = "moko-a11y";
var fontSizeSteps = [85, 90, 100, 110, 120, 130];
var defaultStep = 2; // index of 100
function getA11yPrefs() {
try {
var raw = localStorage.getItem(a11yStorageKey);
if (raw) return JSON.parse(raw);
} catch (e) {}
return { fontStep: defaultStep, inverted: false, contrast: false, links: false, font: false, paused: false };
}
function saveA11yPrefs(prefs) {
try { localStorage.setItem(a11yStorageKey, JSON.stringify(prefs)); } catch (e) {}
}
function applyFontSize(step) {
root.style.fontSize = fontSizeSteps[step] + "%";
}
function applyInversion(on) {
if (on) {
root.classList.add("a11y-inverted");
} else {
root.classList.remove("a11y-inverted");
}
}
function applyContrast(on) {
root.classList.toggle("a11y-high-contrast", on);
// Lazy-load the high-contrast stylesheet
var hcId = "mokoA11yHcSheet";
var existing = doc.getElementById(hcId);
if (on && !existing) {
var link = doc.createElement("link");
link.id = hcId;
link.rel = "stylesheet";
link.href = (doc.querySelector('link[href*="mokocassiopeia/css/template"]') || {}).href
? (doc.querySelector('link[href*="mokocassiopeia/css/template"]').href.replace(/\/css\/template[^/]*$/, "/css/a11y-high-contrast.css"))
: "media/templates/site/mokocassiopeia/css/a11y-high-contrast.css";
doc.head.appendChild(link);
}
}
function applyLinks(on) {
root.classList.toggle("a11y-highlight-links", on);
}
function applyFont(on) {
root.classList.toggle("a11y-readable-font", on);
}
function applyPaused(on) {
root.classList.toggle("a11y-pause-animations", on);
}
/** Create a Font Awesome icon element (safe DOM, no innerHTML). */
function faIcon(classes) {
var i = doc.createElement("i");
i.className = classes;
i.setAttribute("aria-hidden", "true");
return i;
}
function buildA11yToolbar() {
if (doc.getElementById("mokoA11yToolbar")) return;
var body = doc.body;
var showResize = body.getAttribute("data-a11y-resize") === "1";
var showInvert = body.getAttribute("data-a11y-invert") === "1";
var showContrast = body.getAttribute("data-a11y-contrast") === "1";
var showLinks = body.getAttribute("data-a11y-links") === "1";
var showFont = body.getAttribute("data-a11y-font") === "1";
var showAnimations = body.getAttribute("data-a11y-animations") === "1";
var pos = (body.getAttribute("data-a11y-pos") || "tl").toLowerCase();
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = "tl";
var prefs = getA11yPrefs();
// Container
var toolbar = doc.createElement("div");
toolbar.id = "mokoA11yToolbar";
toolbar.className = "a11y-pos-" + pos;
toolbar.setAttribute("role", "toolbar");
toolbar.setAttribute("aria-label", "Accessibility options");
// Toggle button (accessibility icon)
var toggle = doc.createElement("button");
toggle.type = "button";
toggle.id = "mokoA11yToggle";
toggle.className = "a11y-toggle";
toggle.setAttribute("aria-label", "Accessibility options");
toggle.setAttribute("aria-expanded", "false");
var a11yIcon = faIcon("fa-solid fa-universal-access");
// Unicode fallback if FA7 glyph doesn't render (e.g. FA6/FA7 conflict)
setTimeout(function () {
var cs = win.getComputedStyle(a11yIcon, "::before");
if (!cs.content || cs.content === "none" || cs.content === '""' || cs.content === '"" / ""') {
a11yIcon.className = "";
a11yIcon.textContent = "\u267F";
a11yIcon.style.fontSize = "1.1rem";
}
}, 500);
toggle.appendChild(a11yIcon);
// Panel
var panel = doc.createElement("div");
panel.id = "mokoA11yPanel";
panel.className = "a11y-panel";
panel.hidden = true;
// --- Text resize controls ---
if (showResize) {
var resizeGroup = doc.createElement("div");
resizeGroup.className = "a11y-group";
resizeGroup.setAttribute("role", "group");
resizeGroup.setAttribute("aria-label", "Text size");
var sizeLabel = doc.createElement("span");
sizeLabel.className = "a11y-group-label";
sizeLabel.textContent = "Text size";
resizeGroup.appendChild(sizeLabel);
var btnRow = doc.createElement("div");
btnRow.className = "a11y-btn-row";
var btnMinus = doc.createElement("button");
btnMinus.type = "button";
btnMinus.className = "a11y-btn";
btnMinus.setAttribute("aria-label", "Decrease text size");
btnMinus.appendChild(faIcon("fa-solid fa-minus"));
var sizeDisplay = doc.createElement("span");
sizeDisplay.className = "a11y-size-display";
sizeDisplay.setAttribute("aria-live", "polite");
sizeDisplay.textContent = fontSizeSteps[prefs.fontStep] + "%";
var btnPlus = doc.createElement("button");
btnPlus.type = "button";
btnPlus.className = "a11y-btn";
btnPlus.setAttribute("aria-label", "Increase text size");
btnPlus.appendChild(faIcon("fa-solid fa-plus"));
var btnReset = doc.createElement("button");
btnReset.type = "button";
btnReset.className = "a11y-btn a11y-btn-reset";
btnReset.setAttribute("aria-label", "Reset text size");
btnReset.appendChild(faIcon("fa-solid fa-rotate-left"));
btnMinus.addEventListener("click", function () {
if (prefs.fontStep > 0) {
prefs.fontStep--;
applyFontSize(prefs.fontStep);
sizeDisplay.textContent = fontSizeSteps[prefs.fontStep] + "%";
saveA11yPrefs(prefs);
}
});
btnPlus.addEventListener("click", function () {
if (prefs.fontStep < fontSizeSteps.length - 1) {
prefs.fontStep++;
applyFontSize(prefs.fontStep);
sizeDisplay.textContent = fontSizeSteps[prefs.fontStep] + "%";
saveA11yPrefs(prefs);
}
});
btnReset.addEventListener("click", function () {
prefs.fontStep = defaultStep;
applyFontSize(prefs.fontStep);
sizeDisplay.textContent = fontSizeSteps[prefs.fontStep] + "%";
saveA11yPrefs(prefs);
});
btnRow.appendChild(btnMinus);
btnRow.appendChild(sizeDisplay);
btnRow.appendChild(btnPlus);
btnRow.appendChild(btnReset);
resizeGroup.appendChild(btnRow);
panel.appendChild(resizeGroup);
}
// --- Helper: build a switch-style toggle button ---
function addSwitchOption(show, prefKey, icon, label, applyFn) {
if (!show) return;
var group = doc.createElement("div");
group.className = "a11y-group";
var btn = doc.createElement("button");
btn.type = "button";
btn.className = "a11y-btn a11y-btn-wide";
btn.setAttribute("role", "switch");
btn.setAttribute("aria-checked", prefs[prefKey] ? "true" : "false");
btn.setAttribute("aria-label", label);
btn.appendChild(faIcon(icon));
btn.appendChild(doc.createTextNode(" " + label));
if (prefs[prefKey]) btn.classList.add("active");
btn.addEventListener("click", function () {
prefs[prefKey] = !prefs[prefKey];
applyFn(prefs[prefKey]);
btn.setAttribute("aria-checked", prefs[prefKey] ? "true" : "false");
btn.classList.toggle("active", prefs[prefKey]);
saveA11yPrefs(prefs);
});
group.appendChild(btn);
panel.appendChild(group);
}
// --- Toggle options ---
addSwitchOption(showInvert, "inverted", "fa-solid fa-circle-half-stroke", "Invert colors", applyInversion);
addSwitchOption(showContrast, "contrast", "fa-solid fa-adjust", "High contrast", applyContrast);
addSwitchOption(showLinks, "links", "fa-solid fa-link", "Highlight links", applyLinks);
addSwitchOption(showFont, "font", "fa-solid fa-font", "Readable font", applyFont);
addSwitchOption(showAnimations, "paused", "fa-solid fa-pause", "Pause animations", applyPaused);
// Apply saved preferences on load
if (prefs.fontStep !== defaultStep) applyFontSize(prefs.fontStep);
if (prefs.inverted) applyInversion(true);
if (prefs.contrast) applyContrast(true);
if (prefs.links) applyLinks(true);
if (prefs.font) applyFont(true);
if (prefs.paused) applyPaused(true);
// If theme FAB is present, mount a11y toggle inside it; otherwise standalone
var fabSlot = doc.getElementById("mokoA11ySlot");
if (fabSlot) {
toggle.className = "a11y-toggle a11y-toggle-inline";
fabSlot.appendChild(toggle);
toolbar.className = "a11y-toolbar-floating";
toolbar.appendChild(panel);
body.appendChild(toolbar);
// Position panel near the FAB
toggle.addEventListener("click", function () {
var isOpen = !panel.hidden;
panel.hidden = isOpen;
toggle.setAttribute("aria-expanded", isOpen ? "false" : "true");
toggle.classList.toggle("active", !isOpen);
if (!isOpen) {
var rect = toggle.getBoundingClientRect();
toolbar.style.position = "fixed";
toolbar.style.bottom = (win.innerHeight - rect.top + 8) + "px";
toolbar.style.right = (win.innerWidth - rect.right) + "px";
toolbar.style.zIndex = "1202";
}
});
// Close on outside click for inline mode
doc.addEventListener("click", function (e) {
if (!toggle.contains(e.target) && !toolbar.contains(e.target) && !panel.hidden) {
panel.hidden = true;
toggle.setAttribute("aria-expanded", "false");
toggle.classList.remove("active");
}
});
} else {
// Standalone mode — toggle and close handlers
toggle.addEventListener("click", function () {
var isOpen = !panel.hidden;
panel.hidden = isOpen;
toggle.setAttribute("aria-expanded", isOpen ? "false" : "true");
toggle.classList.toggle("active", !isOpen);
});
doc.addEventListener("click", function (e) {
if (!toolbar.contains(e.target) && !panel.hidden) {
panel.hidden = true;
toggle.setAttribute("aria-expanded", "false");
toggle.classList.remove("active");
}
});
toolbar.appendChild(toggle);
toolbar.appendChild(panel);
body.appendChild(toolbar);
}
}
// ========================================================================
// TEMPLATE UTILITIES
// ========================================================================
/**
* Utility: smooth scroll to top
*/
function backToTop() {
win.scrollTo({ top: 0, behavior: "smooth" });
}
/**
* Utility: toggle body class on scroll for sticky header styling
*/
function handleScroll() {
if (win.scrollY > 50) {
doc.body.classList.add("scrolled");
} else {
doc.body.classList.remove("scrolled");
}
}
/**
* Initialize offcanvas drawer buttons for left/right drawers.
* Bootstrap handles drawers automatically via data-bs-toggle="offcanvas"
* This function is kept for backwards compatibility but only runs if drawers exist.
*/
function initDrawers() {
// Check if any drawer buttons exist before initializing
var hasDrawers = doc.querySelector(".drawer-toggle-left") || doc.querySelector(".drawer-toggle-right");
if (!hasDrawers) {
return; // No drawers, skip initialization
}
// Bootstrap 5 handles offcanvas automatically via data-bs-toggle attribute
// No manual initialization needed if Bootstrap is loaded correctly
// The buttons already have data-bs-toggle="offcanvas" and data-bs-target="#drawer-*"
}
/**
* Initialize back-to-top link if present
*/
function initBackTop() {
var backTop = doc.getElementById("back-top");
if (backTop) {
backTop.addEventListener("click", function (e) {
e.preventDefault();
backToTop();
});
}
}
/**
* Initialize theme based on stored preference or system setting
*/
function initTheme() {
var stored = getStored();
var theme = stored ? stored : systemTheme();
applyTheme(theme);
// Listen for system changes only if Auto mode (no stored)
var onChange = function () {
if (!getStored()) {
applyTheme(systemTheme());
}
};
if (typeof mql.addEventListener === "function") {
mql.addEventListener("change", onChange);
} else if (typeof mql.addListener === "function") {
mql.addListener(onChange);
}
// Hook toggle UI if present (for inline switch, not FAB)
var switchEl = doc.getElementById("themeSwitch");
var autoBtn = doc.getElementById("themeAuto");
if (switchEl) {
switchEl.checked = (theme === "dark");
switchEl.addEventListener("change", function () {
var choice = switchEl.checked ? "dark" : "light";
applyTheme(choice);
});
}
if (autoBtn) {
autoBtn.addEventListener("click", function () {
clearStored();
applyTheme(systemTheme());
});
}
}
/**
* Check if theme FAB should be enabled based on body data attribute
*/
function shouldEnableThemeFab() {
return doc.body.getAttribute('data-theme-fab-enabled') === '1';
}
/**
* Convert sidebar card modules into accordion on mobile.
* On screens <= 991px each card collapses; on desktop they revert.
*/
function initSidebarAccordion() {
var BREAKPOINT = 992;
var sidebars = doc.querySelectorAll(".container-sidebar-left, .container-sidebar-right");
if (!sidebars.length) return;
// Build accordion structure once — works at all breakpoints
sidebars.forEach(function (sidebar, si) {
var accId = "sidebarAcc-" + si;
sidebar.setAttribute("id", accId);
sidebar.classList.add("accordion");
var isMobile = win.innerWidth < BREAKPOINT;
var cards = sidebar.querySelectorAll(":scope > .card");
cards.forEach(function (card, ci) {
var collapseId = accId + "-c" + ci;
card.classList.add("accordion-item");
var header = card.querySelector(".card-header");
var body = card.querySelector(".card-body");
if (!header || !body) return;
// Turn header into accordion button
header.classList.add("accordion-header");
var btn = doc.createElement("button");
btn.className = isMobile ? "accordion-button collapsed" : "accordion-button";
btn.type = "button";
btn.setAttribute("data-bs-toggle", "collapse");
btn.setAttribute("data-bs-target", "#" + collapseId);
btn.setAttribute("aria-expanded", isMobile ? "false" : "true");
btn.setAttribute("aria-controls", collapseId);
btn.textContent = header.textContent;
header.textContent = "";
header.appendChild(btn);
// Wrap body in collapse
var wrapper = doc.createElement("div");
wrapper.id = collapseId;
wrapper.className = isMobile ? "accordion-collapse collapse" : "accordion-collapse collapse show";
wrapper.setAttribute("data-bs-parent", "#" + accId);
card.insertBefore(wrapper, body);
wrapper.appendChild(body);
body.classList.add("accordion-body");
});
});
}
/**
* Toggle search on mobile via .show class
*/
function initSearchToggle() {
var btn = doc.querySelector(".search-toggler");
var target = doc.getElementById("headerSearchCollapse");
if (!btn || !target) return;
btn.addEventListener("click", function () {
var isOpen = target.classList.toggle("show");
btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
});
}
// ========================================================================
// CSS VARIABLE CLICK-TO-COPY
// ========================================================================
/**
* Inject toast + variable-chip styles once.
*/
function injectVarCopyStyles() {
if (doc.getElementById("moko-var-copy-styles")) return;
var style = doc.createElement("style");
style.id = "moko-var-copy-styles";
style.textContent =
".moko-var-chip{cursor:pointer;font-family:var(--font-monospace,monospace);font-size:.875em;" +
"background:var(--secondary-bg,#151b22);color:var(--link-color,#8ab4f8);" +
"border:1px solid var(--border-color,#2b323b);border-radius:.25rem;padding:.1em .4em;" +
"transition:background .15s,border-color .15s;white-space:nowrap;display:inline}" +
".moko-var-chip:hover{background:var(--color-primary,#112855);color:#fff;border-color:var(--color-primary,#112855)}" +
".moko-toast{position:fixed;bottom:1.5rem;left:50%;transform:translateX(-50%);z-index:10000;" +
"background:var(--color-primary,#112855);color:#fff;padding:.6rem 1.25rem;" +
"border-radius:.375rem;font-size:.875rem;box-shadow:0 4px 12px rgba(0,0,0,.25);" +
"opacity:0;transition:opacity .2s;pointer-events:none}" +
".moko-toast--show{opacity:1}";
doc.head.appendChild(style);
}
/**
* Show a brief "Copied to clipboard" toast.
* @param {string} text - The variable name that was copied
*/
function showCopyToast(text) {
var existing = doc.querySelector(".moko-toast");
if (existing) existing.remove();
var toast = doc.createElement("div");
toast.className = "moko-toast";
toast.textContent = "Copied to clipboard: " + text;
doc.body.appendChild(toast);
// Trigger reflow then show
void toast.offsetWidth;
toast.classList.add("moko-toast--show");
setTimeout(function () {
toast.classList.remove("moko-toast--show");
setTimeout(function () { toast.remove(); }, 200);
}, 2000);
}
/**
* Copy text to clipboard and show toast.
* @param {string} text
*/
function copyVariable(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
showCopyToast(text);
});
} else {
// Fallback for older browsers using deprecated API
var ta = doc.createElement("textarea");
ta.value = text;
ta.style.cssText = "position:fixed;left:-9999px";
doc.body.appendChild(ta);
ta.select();
try { doc.execCommand("copy"); } catch (e) { /* noop */ }
ta.remove();
showCopyToast(text);
}
}
/**
* Scan text nodes for CSS variable patterns (--variable-name) and wrap
* each match in a clickable chip that copies the variable to clipboard.
*/
function initVarCopy() {
injectVarCopyStyles();
// Pattern: --[a-zA-Z] followed by word/hyphen chars
var varPattern = /--[a-zA-Z][\w-]*/g;
// Elements to skip (inputs, scripts, styles, already-processed, code editors)
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, TEXTAREA: 1, INPUT: 1, SELECT: 1, NOSCRIPT: 1 };
var walker = doc.createTreeWalker(
doc.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
if (SKIP_TAGS[node.parentNode.tagName]) return NodeFilter.FILTER_REJECT;
if (node.parentNode.classList && node.parentNode.classList.contains("moko-var-chip")) return NodeFilter.FILTER_REJECT;
if (!varPattern.test(node.nodeValue)) return NodeFilter.FILTER_REJECT;
varPattern.lastIndex = 0;
return NodeFilter.FILTER_ACCEPT;
}
}
);
var textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);
textNodes.forEach(function (node) {
var text = node.nodeValue;
var frag = doc.createDocumentFragment();
var lastIndex = 0;
var match;
varPattern.lastIndex = 0;
while ((match = varPattern.exec(text)) !== null) {
// Text before the match
if (match.index > lastIndex) {
frag.appendChild(doc.createTextNode(text.slice(lastIndex, match.index)));
}
// Clickable chip
var chip = doc.createElement("span");
chip.className = "moko-var-chip";
chip.textContent = match[0];
chip.setAttribute("role", "button");
chip.setAttribute("tabindex", "0");
chip.setAttribute("title", "Click to copy " + match[0]);
chip.addEventListener("click", (function (varName) {
return function (e) {
e.preventDefault();
copyVariable(varName);
};
})(match[0]));
chip.addEventListener("keydown", (function (varName) {
return function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
copyVariable(varName);
}
};
})(match[0]));
frag.appendChild(chip);
lastIndex = match.index + match[0].length;
}
// Remaining text after last match
if (lastIndex < text.length) {
frag.appendChild(doc.createTextNode(text.slice(lastIndex)));
}
node.parentNode.replaceChild(frag, node);
});
}
/**
* Run all template JS initializations
*/
function init() {
// Initialize theme first
initTheme();
// Build floating theme toggle if enabled
if (shouldEnableThemeFab()) {
buildThemeToggle();
}
// Build accessibility toolbar if enabled
if (doc.body.getAttribute("data-a11y-toolbar") === "1") {
buildA11yToolbar();
}
// Sticky header behavior
handleScroll();
win.addEventListener("scroll", handleScroll);
// Init features
initDrawers();
initBackTop();
initSearchToggle();
initSidebarAccordion();
initVarCopy();
}
if (doc.readyState === "loading") {
doc.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})(window, document);