/* figures: pointer
*
* Two widgets that enhance static markup (the markup alone is a complete, readable figure):
* - [data-nj-pointer-walk] in features/json_pointer.md: a tree of the example document plus a pointer bar whose
* segments select a prefix of the pointer; the path down to that level is highlighted and its value is shown.
* - [data-nj-patch] in features/json_patch.md: a stepper through six JSON Patch operations; the static table of the
* operations is hidden once the stepper is built.
* Initialised through Material's document$ (navigation.instant), idempotent per element.
*/
(function () {
"use strict";
function el(tag, cls, html) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (html !== undefined) e.innerHTML = html;
return e;
}
function esc(s) {
return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
/* ---------- JSON Pointer walker ---------- */
var PRESETS = ["", "/array", "/array/0", "/array/1", "/array/2", "/nested", "/nested/one", "/nested/two",
"/nested/three", "/nested/three/0", "/nested/three/1", "/"];
function tokens(ptr) {
if (ptr === "") return [];
return ptr.slice(1).split("/").map(function (t) { return t.replace(/~1/g, "/").replace(/~0/g, "~"); });
}
function joinTokens(toks) {
return toks.map(function (t) { return "/" + t.replace(/~/g, "~0").replace(/\//g, "~1"); }).join("");
}
// RFC 6901 resolution as json::at() does it; returns {ok, value, depth, error}
function resolve(doc, toks) {
var v = doc;
for (var i = 0; i < toks.length; ++i) {
var t = toks[i];
if (Array.isArray(v)) {
if (!/^(0|[1-9][0-9]*)$/.test(t) || Number(t) >= v.length) {
return { ok: false, depth: i, error: "at() throws out_of_range: array index '" + t + "' is out of range" };
}
v = v[Number(t)];
} else if (v !== null && typeof v === "object") {
if (!Object.prototype.hasOwnProperty.call(v, t)) {
return { ok: false, depth: i, error: "at() throws out_of_range.403: key '" + t + "' not found" };
}
v = v[t];
} else {
return { ok: false, depth: i, error: "a primitive value has no children" };
}
}
return { ok: true, value: v, depth: toks.length };
}
function ptrLabel(p) { return p === "" ? '""' : p; }
function initWalk(root) {
if (root.dataset.njReady) return;
var doc;
try { doc = JSON.parse(root.getAttribute("data-doc")); } catch (e) { return; }
root.dataset.njReady = "1";
root.classList.add("nj-pointer-walk--js");
var tree = root.querySelector(".nj-pointer-tree");
var items = Array.prototype.slice.call(tree.querySelectorAll("li[data-p]"));
var state = { ptr: "/nested/three/1", level: 3 };
// tree labels become buttons that select their own pointer
items.forEach(function (li) {
var span = li.querySelector(":scope > .nj-pointer-n");
var b = el("button", "nj-pointer-n", span.innerHTML);
b.type = "button";
var p = li.getAttribute("data-p");
b.setAttribute("aria-label", "Select the pointer " + ptrLabel(p));
b.addEventListener("click", function () { set(p, tokens(p).length); });
span.replaceWith(b);
});
var panel = el("div", "nj-pointer-panel");
panel.appendChild(el("div", "nj-pointer-lbl", "Pointer (select a segment)"));
var bar = el("div", "nj-pointer-bar");
bar.setAttribute("role", "group");
bar.setAttribute("aria-label", "Pointer segments");
panel.appendChild(bar);
var out = el("div", "nj-pointer-out");
out.setAttribute("aria-live", "polite");
panel.appendChild(out);
panel.appendChild(el("div", "nj-pointer-lbl", "Examples"));
var presets = el("div", "nj-pointer-presets");
presets.setAttribute("role", "group");
presets.setAttribute("aria-label", "Example pointers");
PRESETS.forEach(function (p) {
var b = el("button", "nj-pointer-chip", "" + esc(ptrLabel(p)) + "");
b.type = "button";
b.setAttribute("data-p", p);
b.setAttribute("aria-label", p === "" ? "Pointer \"\" (the whole document)" : "Pointer " + p);
b.addEventListener("click", function () { set(p, tokens(p).length); });
presets.appendChild(b);
});
panel.appendChild(presets);
var stat = root.querySelector(".nj-pointer-static");
if (stat) stat.hidden = true;
root.insertBefore(panel, root.firstChild);
function liFor(p) {
for (var i = 0; i < items.length; ++i) if (items[i].getAttribute("data-p") === p) return items[i];
return null;
}
function render() {
var toks = tokens(state.ptr);
var sel = toks.slice(0, state.level);
var r = resolve(doc, sel);
// bar: root chip plus one button per reference token
bar.innerHTML = "";
var parts = [{ label: '""', text: "root", n: 0 }];
toks.forEach(function (t, i) { parts.push({ label: joinTokens([t]), n: i + 1 }); });
parts.forEach(function (part) {
var b = el("button", "nj-pointer-seg" + (part.n === 0 ? " nj-pointer-seg--root" : ""),
part.n === 0 ? "\"\"" : "" + esc(part.label) + "");
b.type = "button";
b.setAttribute("aria-pressed", part.n === state.level ? "true" : "false");
if (part.n > state.level) b.classList.add("is-after");
var prefix = joinTokens(toks.slice(0, part.n));
b.setAttribute("aria-label", "Go to level " + part.n + ": " + ptrLabel(prefix) + (part.n === 0 ? " (the whole document)" : ""));
b.addEventListener("click", function () { state.level = part.n; render(); focusSeg(part.n); });
bar.appendChild(b);
});
// tree highlight
items.forEach(function (li) { li.classList.remove("is-on", "is-target", "is-pass", "is-miss"); });
var depth = r.ok ? sel.length : r.depth;
for (var d = 0; d <= depth; ++d) {
var li = liFor(joinTokens(sel.slice(0, d)));
if (!li) break;
li.classList.add("is-on");
if (d > 0) {
for (var s = li.previousElementSibling; s; s = s.previousElementSibling) s.classList.add("is-pass");
}
if (r.ok && d === depth) li.classList.add("is-target");
}
if (!r.ok) {
var miss = liFor(joinTokens(sel.slice(0, r.depth)));
if (miss) miss.classList.add("is-miss");
}
var shown = joinTokens(sel);
out.innerHTML = r.ok
? "" + esc(ptrLabel(shown)) + " resolves to " + esc(JSON.stringify(r.value)) + "" +
(shown === "" ? " (the whole document)" : "")
: "" + esc(ptrLabel(shown)) + " identifies no value: " + esc(r.error) +
". The token after / is the empty key, and the document has none.";
Array.prototype.forEach.call(presets.children, function (b) {
b.setAttribute("aria-pressed", b.getAttribute("data-p") === shown && state.level === tokens(state.ptr).length ? "true" : "false");
});
}
function focusSeg(n) {
var b = bar.children[n];
if (b) b.focus();
}
function set(p, level) {
state.ptr = p;
state.level = level;
render();
}
render();
}
/* ---------- JSON Patch stepper ---------- */
function k(s) { return '"' + s + '"'; }
function s(v) { return '"' + v + '"'; }
function ins(x) { return '' + x + ""; }
function src(x) { return '' + x + ""; }
var I = " ";
var OPS = [
{ op: "replace", path: "/baz", extra: ', "value": "boo"' },
{ op: "add", path: "/hello", extra: ', "value": ["world"]' },
{ op: "remove", path: "/foo", extra: "" },
{ op: "move", from: "/baz", path: "/hello/1", extra: "" },
{ op: "copy", from: "/hello/0", path: "/greeting", extra: "" },
{ op: "test", path: "/greeting", extra: ', "value": "world"' }
];
// each frame: document lines [html, line class, note], optional connector {from, to}, status text
var FRAMES = [
{ lines: [["{"], [I + k("baz") + ": " + s("qux") + ","], [I + k("foo") + ": " + s("bar")], ["}"]],
status: "Start: the original document of the example below. Press Next to apply the first operation." },
{ lines: [["{"], [I + k("baz") + ": " + s("qux") + ",", "del"], [I + k("baz") + ": " + s("boo") + ",", "add", "replaced"],
[I + k("foo") + ": " + s("bar")], ["}"]],
status: "replace /baz: the value \"qux\" is replaced by \"boo\"." },
{ lines: [["{"], [I + k("baz") + ": " + s("boo") + ","], [I + k("foo") + ": " + s("bar") + ","],
[I + k("hello") + ": [" + s("world") + "]", "add", "added"], ["}"]],
status: "add /hello: the new member \"hello\" with the value [\"world\"] is added." },
{ lines: [["{"], [I + k("baz") + ": " + s("boo") + ","], [I + k("foo") + ": " + s("bar") + ",", "del", "removed"],
[I + k("hello") + ": [" + s("world") + "]"], ["}"]],
status: "remove /foo: the member \"foo\" is removed. This is the result of the example below." },
{ lines: [["{"], [I + k("baz") + ": " + s("boo") + ",", "del", "moved"],
[I + k("hello") + ": [" + s("world") + ", " + ins(s("boo")) + "]", "mod", "from /baz"], ["}"]],
arc: { from: 1, to: 2 },
status: "move from /baz to /hello/1: \"boo\" is removed from /baz and inserted into the array at index 1." },
{ lines: [["{"], [I + k("greeting") + ": " + s("world") + ",", "add", "copy of /hello/0"],
[I + k("hello") + ": [" + src(s("world")) + ", " + s("boo") + "]"], ["}"]],
arc: { from: 2, to: 1 },
status: "copy from /hello/0 to /greeting: the value \"world\" is copied to the new member \"greeting\"." },
{ lines: [["{"], [I + k("greeting") + ": " + s("world") + ",", "test", "test passed"],
[I + k("hello") + ": [" + s("world") + ", " + s("boo") + "]"], ["}"]],
status: "test /greeting: the value equals \"world\", so the document is unchanged. A failed test would make patch() throw other_error 501." }
];
function opHtml(o) {
var h = '{"op": "' + o.op + '"';
if (o.from) h += ', "from": "' + o.from + '"';
h += ', "path": "' + o.path + '"' + esc(o.extra) + "}";
return h;
}
function initPatch(root) {
if (root.dataset.njReady) return;
root.dataset.njReady = "1";
root.classList.add("nj-patch--js");
var step = 0;
var stage = el("div", "nj-patch-stage");
var cols = el("div", "nj-patch-cols");
var left = el("div", "nj-patch-col");
left.appendChild(el("div", "nj-patch-h", "Document"));
var docBox = el("div", "nj-patch-doc");
left.appendChild(docBox);
var right = el("div", "nj-patch-col");
right.appendChild(el("div", "nj-patch-h", "Patch"));
var list = el("ol", "nj-patch-ops");
OPS.forEach(function (o) { list.appendChild(el("li", "", "" + opHtml(o) + "")); });
right.appendChild(list);
cols.appendChild(left);
cols.appendChild(right);
stage.appendChild(cols);
// controls and status sit under the document, so on a phone both stay in view together
var ctrl = el("div", "nj-patch-ctrl");
function btn(label, aria, fn) {
var b = el("button", "nj-patch-btn", label);
b.type = "button";
b.setAttribute("aria-label", aria);
b.addEventListener("click", fn);
ctrl.appendChild(b);
return b;
}
var bReset = btn("Reset", "Back to the original document", function () { step = 0; render(true); });
var bPrev = btn("Back", "Previous operation", function () { if (step > 0) { step--; render(true); } });
var bNext = btn("Next", "Apply the next operation", function () { if (step < OPS.length) { step++; render(true); } });
bNext.classList.add("nj-patch-btn--primary");
var count = el("span", "nj-patch-count");
ctrl.appendChild(count);
left.appendChild(ctrl);
var status = el("p", "nj-patch-status");
status.setAttribute("aria-live", "polite");
left.appendChild(status);
root.insertBefore(stage, root.firstChild);
function render(animate) {
var f = FRAMES[step];
docBox.innerHTML = "";
f.lines.forEach(function (ln) {
var row = el("div", "nj-patch-line" + (ln[1] ? " nj-patch-line--" + ln[1] : ""));
row.appendChild(el("span", "nj-patch-code", ln[0]));
if (ln[2]) row.appendChild(el("span", "nj-patch-note", esc(ln[2])));
docBox.appendChild(row);
});
if (f.arc) {
var a = el("div", "nj-patch-arc " + (f.arc.to > f.arc.from ? "nj-patch-arc--down" : "nj-patch-arc--up"));
var top = Math.min(f.arc.from, f.arc.to), n = Math.abs(f.arc.to - f.arc.from);
a.style.top = "calc(var(--nj-patch-pad) + " + (top + 0.5) + " * var(--nj-patch-lh))";
a.style.height = "calc(" + n + " * var(--nj-patch-lh))";
a.setAttribute("aria-hidden", "true");
docBox.appendChild(a);
}
docBox.classList.toggle("nj-patch-doc--anim", !!animate);
Array.prototype.forEach.call(list.children, function (li, i) {
li.className = i + 1 === step ? "is-cur" : (i + 1 < step ? "is-done" : "");
if (i + 1 === step) li.setAttribute("aria-current", "step"); else li.removeAttribute("aria-current");
});
bPrev.disabled = step === 0;
bReset.disabled = step === 0;
bNext.disabled = step === OPS.length;
count.textContent = "Step " + step + " of " + OPS.length;
status.textContent = f.status;
}
render(false);
}
function init() {
Array.prototype.forEach.call(document.querySelectorAll("[data-nj-pointer-walk]"), initWalk);
Array.prototype.forEach.call(document.querySelectorAll("[data-nj-patch]"), initPatch);
}
if (typeof window.document$ !== "undefined" && window.document$.subscribe) {
window.document$.subscribe(init);
} else if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();