/* view.js: the preview widget's half of the gallery's script, served joined after gallery.js as one file, and no part of the design system. Two jobs: the frames, and the Copy button on every code block. It is Rastrillo's frame-painting and Copy code; the scheme itself is fiesta.js's, from the scheme menu in the header, and this file only carries what fiesta.js decided into every preview frame. The frames. Every example on the page is a document of its own inside an iframe, and an iframe does not inherit the reader's choice: a colour scheme is not propagated into an embedded document that declares one, and every preview links a theme that does. The frames are same-origin, so the fix is the attribute fiesta.js wrote on this page's , written again on each of them: once each document is parsed and styled, and on every rst-scheme event after. frame.js, first in each frame's head, does the same for itself before its first paint, so a frame loaded after the choice starts in it; a document a navigation inside the frame loads from elsewhere has no such script, and is what the hiding below is for. A frame is hidden by view.css until this file marks it painted (data-gallery-painted), which is why the joined script is a blocking script in
: the marker that turns the hiding rule on (data-gallery-js) has to be there before the first frame can paint in the wrong scheme. With scripts off the marker is never set, nothing is hidden, and the frames show as they come, in the OS's scheme, which is what System means anyway. */ (function () { "use strict"; var root = document.documentElement; root.setAttribute("data-gallery-js", "on"); // The scheme in force on this page: fiesta.js keeps it on , // and no attribute is System. function scheme() { return root.getAttribute("data-theme"); } function readable(frame) { try { var doc = frame.contentDocument; return doc && doc.documentElement && doc.URL !== "about:blank" ? doc : null; } catch (e) { return null; } } function paint(frame, s) { var doc = readable(frame); if (!doc) return; if (s === "light" || s === "dark") doc.documentElement.setAttribute("data-theme", s); else doc.documentElement.removeAttribute("data-theme"); } function frames() { return document.querySelectorAll(".ds-view__frame"); } function all(s) { var f = frames(); for (var i = 0; i < f.length; i++) paint(f[i], s); } // Phase two, once the body exists. This file is in and not // deferred, so readyState is always "loading" here; the branch is // for a copy of it moved to the foot of the page. function ready(fn) { if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn); else fn(); } // Shown even if it could not be painted: a frame in the wrong scheme // beats one that never appears. function shown(frame) { paint(frame, scheme()); frame.setAttribute("data-gallery-painted", ""); } // DOMContentLoaded does not wait for stylesheets, and a frame shown // then shows its document without them. Each sheet not yet arrived // is waited for, loaded or failed; one that failed before this // looked never says so, and the frame's load shows it instead. function styled(frame, doc) { var links = doc.querySelectorAll('link[rel~="stylesheet"]'), left = 1; function one() { if (--left === 0 && readable(frame) === doc) shown(frame); } for (var i = 0; i < links.length; i++) { if (links[i].sheet) continue; left++; links[i].addEventListener("load", one); links[i].addEventListener("error", one); } one(); } // Every document a frame loads goes through arm(), the first and each // one a sample's form or link loads in it after, and in this order: // the hook that hides the frame again as the document goes, then the // reveal. Shown first, a document that navigated before its own load // left the mark in place, and the next one painted in the OS's // scheme, unhidden. Not cleared when the whole gallery page enters // the back/forward cache (persisted), which unloads nothing and // restores as it was. // // Parsed and styled is soon enough to show it: waiting for load kept // a parsed preview an empty box while an image was slow, or for as // long as the network took to give up on one that hung. function arm(frame, armed) { var doc; try { doc = frame.contentDocument; } catch (e) { return false; } if (!doc || doc === armed.doc || doc.URL === "about:blank") return false; armed.doc = doc; watchRest(frame, doc); doc.defaultView.addEventListener("pagehide", function (event) { if (event.persisted) return; frame.removeAttribute("data-gallery-painted"); seek(frame, armed); }); if (doc.readyState === "loading") { doc.addEventListener("DOMContentLoaded", function () { if (readable(frame) === doc) styled(frame, doc); }); } else { styled(frame, doc); } return true; } // The parent hears nothing when a navigation inside a frame commits // its next document, so after pagehide it looks until the frame holds // one, and arms it. The look ends there, or at the frame's load, // which arms whatever is there; a document from another origin cannot // be read, and load shows the frame unpainted. // // It also ends when the frame leaves the page, and after SEEK_MS // whatever happens. Without those, a frame removed mid-look, or sent // to another origin whose load hangs, kept a 16ms timer running for // the life of the page, holding the frame and its old document, and // the second kept the frame hidden for good. Giving up shows the // frame unpainted, as load would have. var SEEK_MS = 5000; function seek(frame, armed) { clearTimeout(armed.timer); var end = Date.now() + SEEK_MS; (function look() { armed.timer = 0; if (!frame.isConnected || arm(frame, armed)) return; if (Date.now() >= end) shown(frame); else armed.timer = setTimeout(look, 16); })(); } // A frame's first document reuses the window of the about:blank it // replaces (same origin), so a listener on that window hears the // preview's DOMContentLoaded and arms it while it is still parsing; // the frames are lazy, so nothing looks for them before then. load // stays the fallback, for an engine that does not reuse the window. function watch(frame) { var armed = { doc: null, timer: 0 }, view = restView(frame); // Nothing is open in a frame that has not loaded. if (view) view.setAttribute("data-ds-rest", ""); try { frame.contentWindow.addEventListener("DOMContentLoaded", function () { arm(frame, armed); }); } catch (e) { /* no window yet, or not readable; load will arm it */ } frame.addEventListener("load", function () { clearTimeout(armed.timer); arm(frame, armed); shown(frame); }); // A document already parsed before this ran (a frame served from // cache) has had its DOMContentLoaded. arm(frame, armed); } // ── Rest heights ──────────────────────────────────────────────────── // // A frame whose menus open far below its page (the column's footer // row) has two heights, the open one in --ds-h and --ds-hm and the // rest one in --ds-hr and --ds-hmr. Sized for its menus open, its box // was mostly empty at rest. While no menu in the frame is open its // widget is marked data-ds-rest, and view.css gives the box the rest // heights; unmarked, as with scripts off, the box keeps the open // height, so a menu a reader opens is in view either way. // // A click on a closed menu's summary unmarks the widget at once, // before the menu opens, so the frame is already tall when the panel // is placed: placed in the short frame, an anchored panel opens up // over the page, and stays there once the frame grows. toggle then // settles the mark from what is really open, which also covers a menu // opened from the keyboard or by a script, one closed by its sibling // opening, and a click whose menu did not open after all. var MENU_OPEN = "details[rst-dropdown][open], details.rst-dropdown[open]"; function restView(frame) { var view = frame.closest(".ds-view"); return view && view.style.getPropertyValue("--ds-hr") ? view : null; } function settleRest(view, doc) { view.toggleAttribute("data-ds-rest", !doc.querySelector(MENU_OPEN)); } function watchRest(frame, doc) { var view = restView(frame); if (!view) return; settleRest(view, doc); doc.addEventListener("toggle", function () { settleRest(view, doc); }, true); doc.addEventListener("click", function (event) { var summary = event.target.closest && event.target.closest("summary"), menu = summary && summary.parentElement; if (!menu || menu.open || !menu.matches("details[rst-dropdown], details.rst-dropdown")) return; view.removeAttribute("data-ds-rest"); setTimeout(function () { settleRest(view, doc); }, 100); }, true); } // fiesta.js dispatches rst-scheme from the form after it has written // the attribute, so the detail and the root agree; the detail is // read all the same, so this file never depends on the order. document.addEventListener("rst-scheme", function (event) { var s = event.detail && event.detail.scheme; all(s === "light" || s === "dark" ? s : null); }); // A page back from the back/forward cache is reactivated, not // re-run, and fiesta.js may have restored a stored choice since. addEventListener("pageshow", function (event) { if (event.persisted) all(scheme()); }); ready(function () { var f = frames(); for (var i = 0; i < f.length; i++) watch(f[i]); }); // ── Copy ──────────────────────────────────────────────────────────── // // A button on every source block, drawn only where the clipboard API // exists. On a plain-HTTP origin (a tailnet address) it does not, and // a button that cannot work is worse than a a reader can
// select. The words are the page's, read off the live region it
// renders in its own language; a button's name is built from text
// already on screen, so the server writes nothing per block.
ready(function () {
var region = document.querySelector("[data-ds-copy-status]");
if (!region || !navigator.clipboard || !navigator.clipboard.writeText) return;
var d = region.dataset, pres = document.querySelectorAll("pre.ds-src:not([data-ds-nocopy])");
// The name after the label: the tab the block is under, the
// section (its anchored heading, or the last heading before it),
// and the nearest state label above. On an example page the
// anchored section's heading is the state's own label, so the two
// read the same and the second is dropped; a grouped widget's rows
// have the page's heading and then each row's label.
function name(pre) {
var sec = pre.closest("[data-ds-anchor]"), h = sec && sec.querySelector("h1, h2, h3, h4");
var all = document.querySelectorAll("main :is(h1, h2, h3, h4)"), sample = pre.closest(".ds-sample");
if (!h) for (var i = 0; i < all.length && all[i].compareDocumentPosition(pre) & 4; i++) h = all[i];
for (var s = pre.previousElementSibling; s && !s.matches(".ds-state"); s = s.previousElementSibling);
s = s || sample && sample.querySelector(":scope > .ds-state");
var panel = pre.closest(".ds-view__code"), tab = panel && panel.parentNode.querySelector(".ds-view__tab--h");
var parts = [tab, h, s].filter(Boolean).map(function (e) {
return e.textContent.trim();
});
return parts.filter(function (text, i) {
return text && text !== parts[i - 1];
}).join(d.join);
}
// Emptied and written a beat later, so Copied twice in a row is
// still announced.
function say(text) {
region.textContent = "";
setTimeout(function () { region.textContent = text; }, 50);
}
for (var i = 0; i < pres.length; i++) (function (pre) {
var code = pre.querySelector("code") || pre, b = document.createElement("button");
var label = document.createTextNode(d.copy), suffix = document.createElement("span"), timer;
b.type = "button";
b.className = "ds-copy";
suffix.className = "rst-sr-only";
suffix.textContent = " " + name(pre);
b.append(label, suffix);
pre.before(b);
// Copied is said only once the write resolves. A refusal (a
// permission, lost focus, a policy) or a throw is announced as a
// failure and the block selected, so Ctrl or Cmd+C works at once.
b.addEventListener("click", function () {
new Promise(function (ok) { ok(navigator.clipboard.writeText(code.textContent)); }).then(function () {
say(d.copied);
label.data = d.copied;
clearTimeout(timer);
timer = setTimeout(function () { label.data = d.copy; }, 2000);
}, function () {
say(d.failed);
getSelection().selectAllChildren(code);
});
});
})(pres[i]);
});
})();