79 lines
3.2 KiB
JavaScript
79 lines
3.2 KiB
JavaScript
const pop = {anchor: null, marked: null, closed: null};
|
|||
|
|
const HITS = 8;
|
||
|
|
const popupDialog = () => document.getElementById("popup");
|
||
|
|
const popupBox = () => popupDialog().querySelector(".popup-box");
|
||
|
|
const near = (selector) => () => document.querySelector(selector);
|
||
|
|
// Trip-wire: artifact ids hold NUL, which CSS.escape turns into U+FFFD, so an attribute selector never matches them.
|
||
|
|
const pickOf = (id) => () => [...document.querySelectorAll("#main [data-pick]")].find((node) => node.dataset.pick === id) || null;
|
||
|
|
const popupHead = (lead, title, ...sub) => h("div", {class: "popup-head"}, lead, h("div", {}, h("h2", {class: "panel-title"}, title), sub), h("span", {class: "spacer"}), iconBtn("bi-x-lg", "Close", "btn small ghost", closePopup));
|
||
|
|
|
||
|
|
function openPopup(label, body, anchor, closed) {
|
||
|
|
closePopup();
|
||
|
|
pop.anchor = anchor || null;
|
||
|
|
pop.closed = closed || null;
|
||
|
|
popupDialog().setAttribute("aria-label", label);
|
||
|
|
popupBox().replaceChildren(...body);
|
||
|
|
popupDialog().show();
|
||
|
|
placePopup();
|
||
|
|
}
|
||
|
|
|
||
|
|
function closePopup() {
|
||
|
|
if (!popupDialog().open) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
popupDialog().close();
|
||
|
|
markPick(null);
|
||
|
|
const closed = pop.closed;
|
||
|
|
pop.anchor = null;
|
||
|
|
pop.closed = null;
|
||
|
|
if (closed) {
|
||
|
|
closed();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function markPick(target) {
|
||
|
|
if (pop.marked && pop.marked !== target) {
|
||
|
|
pop.marked.classList.remove("picked");
|
||
|
|
}
|
||
|
|
pop.marked = target;
|
||
|
|
if (target) {
|
||
|
|
target.classList.add("picked");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function placePopup() {
|
||
|
|
const dialog = popupDialog();
|
||
|
|
if (!dialog.open) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const found = pop.anchor ? pop.anchor() : null;
|
||
|
|
const target = found instanceof Element && found.isConnected ? found : null;
|
||
|
|
markPick(target);
|
||
|
|
const shape = target ? target.querySelector("circle") || target : null;
|
||
|
|
const box = popupBox();
|
||
|
|
box.style.maxHeight = "";
|
||
|
|
const at = Popup.place(shape ? shape.getBoundingClientRect() : found instanceof Element ? null : found, {width: box.offsetWidth, height: box.offsetHeight}, {width: dialog.clientWidth, height: dialog.clientHeight});
|
||
|
|
box.style.left = `${at.left}px`;
|
||
|
|
box.style.top = `${at.top}px`;
|
||
|
|
box.style.maxHeight = at.height === null ? "" : `${at.height}px`;
|
||
|
|
const line = dialog.querySelector(".leader line");
|
||
|
|
line.setAttribute("display", at.line ? "inline" : "none");
|
||
|
|
for (const [key, value] of Object.entries(at.line || {})) {
|
||
|
|
line.setAttribute(key, value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function nodeSearch(nodes, pick) {
|
||
|
|
const list = h("div", {class: "chips hits", role: "group", "aria-label": "Search hits"});
|
||
|
|
const field = h("input", {type: "search", class: "field", placeholder: "Find a node …", "aria-label": "Find a node", on: {
|
||
|
|
input: () => list.replaceChildren(...Popup.hits(nodes(), field.value, HITS).map((node) => h("button", {type: "button", class: "chip", title: node.label, "data-hit": node.id, on: {click: () => pick(node)}}, node.label))),
|
||
|
|
keydown: (event) => {
|
||
|
|
const [first] = event.key === "Enter" ? Popup.hits(nodes(), field.value, 1) : [];
|
||
|
|
if (first) {
|
||
|
|
event.preventDefault();
|
||
|
|
pick(first);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
}});
|
||
|
|
return h("div", {class: "node-search"}, field, list);
|
||
|
|
}
|