const [bar, editChip, editName] = [$(".bar"), $(".edit"), $(".bar-name")];
const [earlier, later, placeLine] = [$(".earlier"), $(".later"), $(".place")];
const [upButton, downButton] = [$(".parent"), $(".child")];
const EDIT_STYLES = ["touch-action"];
const EDIT_BLOCKED = ["pointerdown", "mousedown", "dblclick", "submit", "click", "dragstart"];
let editing = null;
let dragging = false;
let trail = [];
let fingered = false;
const keepInline = (node, props) =>
props.map((prop) => [prop, node.style.getPropertyValue(prop), node.style.getPropertyPriority(prop)]);
const putInline = (node, kept) => {
for (const [prop, value, priority] of kept) {
if (value) node.style.setProperty(prop, value, priority);
else node.style.removeProperty(prop);
}
};
const editGuard = (event) => {
if (ours(event)) return;
swallow(event);
const { target, type } = event;
if (type === "pointerdown") fingered = event.pointerType === "touch";
const own = editing.node.contains(target);
const press = type === "pointerdown" && !event.button;
if (own && press) {
grabNode(editing.node, event, whileDragged);
} else if (!own && target !== document.body && target !== document.documentElement
&& (press ? !fingered : type === "click" && fingered)) {
trail = [];
switchTo(target, event);
}
};
const STEPS = { ArrowUp: -1, ArrowLeft: -1, ArrowDown: 1, ArrowRight: 1 };
const editKey = (event) => {
const by = event.altKey && STEPS[event.key];
if (event.key !== "Escape" && !by) return;
event.preventDefault();
event.stopPropagation();
if (by) step(by);
else cancelEdit();
};
function placeBar() {
const box = editing.node.getBoundingClientRect();
const first = box.top + box.height / 2 > innerHeight / 2;
for (const top of [first, !first]) {
bar.classList.toggle("top", top);
const gap = () => {
const [box, cover] = [editing.node.getBoundingClientRect(), bar.getBoundingClientRect()];
return top ? box.top - cover.bottom : cover.top - box.bottom;
};
const room = gap();
if (room < 0) scrollBy(0, top ? room - 12 : 12 - room);
if (gap() >= 0) return;
}
}
function showPlace(say) {
const { at, of } = placeOf(editing.node);
placeLine.textContent = `Place ${at} of ${of}`;
earlier.disabled = at <= 1;
later.disabled = at >= of;
if (root.activeElement?.disabled) (earlier.disabled ? later : earlier).focus();
if (say) announce(`Place ${at} of ${of}`);
}
const step = (by) => {
moveTo(editing.node, placeOf(editing.node).at + by);
showPlace(true);
};
function whileDragged(done) {
if (!dragging) clearHover();
dragging = !done;
bar.classList.toggle("faded", !done);
showPlace(done);
if (!done) return;
showFamily();
placeBar();
}
function showFamily() {
const up = editing.node.parentElement;
upButton.disabled = !up || up === document.body || up === document.documentElement;
downButton.disabled = !trail.length || !editing.node.contains(trail.at(-1));
}
function listen(on) {
const method = on ? "addEventListener" : "removeEventListener";
for (const type of EDIT_BLOCKED) window[method](type, editGuard, true);
window[method]("keydown", editKey, true);
window[method]("pointermove", hover, true);
bar.hidden = !on;
}
function startEdit(node, event) {
const old = points.find((point) => point.node === node && point.edit);
if (old) takePoint(old);
editing = old ? { ...old.edit.session } : {
node,
home: { node, parent: node.parentNode, next: node.nextSibling },
from: placeOf(node),
info: describe(node),
bare: !node.getAttribute("style"),
kept: keepInline(node, [...EDIT_STYLES, ...KNOB_PROPS]),
};
editing.resume = old && { point: old, style: node.getAttribute("style"),
at: { node, parent: node.parentNode, next: node.nextSibling } };
editing.unmark = outline(node, `3px solid ${PINK}`);
node.style.setProperty("touch-action", "none", "important");
editName.textContent = pointLabel(editing.info);
loadKnobs(node, old?.edit.session.knobs);
showPlace(false);
showFamily();
const opening = bar.hidden;
if (opening) listen(true);
placeBar();
if (opening || root.activeElement?.disabled) bar.querySelector("button:enabled, input").focus({ preventScroll: true });
if (!opening) announce(`Editing ${pointLabel(editing.info)}`);
if (event?.type === "pointerdown") grabNode(node, event, whileDragged);
}
function release(keep) {
const { node, home, kept, unmark, bare } = editing;
editing = null;
const tidy = () => {
if (bare && !node.style.length) node.removeAttribute("style");
};
const undo = () => {
putInline(node, kept);
tidy();
putBack(home);
};
unmark();
putInline(node, kept.filter(([prop]) => EDIT_STYLES.includes(prop)));
if (keep) tidy();
else undo();
return undo;
}
function closeBar() {
clearHover();
listen(false);
trail = [];
}
const editChanges = () => {
const { node, from, home } = editing;
const { at, of } = placeOf(node);
const moved = node.parentNode !== home.parent
? [`Moved out of its block to place ${at} of ${of} in \`${selector(node.parentElement)}\` (was place ${from.at} of ${from.of})`]
: at === from.at ? [] : [`Moved from place ${from.at} to place ${at} of ${of}`];
return [...moved, ...knobChanges()];
};
function leave(always) {
const { node, info } = editing;
const changes = editChanges();
const session = { ...editing, knobs: knobState() };
const undo = release(changes.length > 0);
if (!changes.length && !always) return null;
return addPoint(node, info, changes.length ? { changes, undo, session } : null);
}
function switchTo(node, event) {
clearHover();
leave(false);
startEdit(node, event);
}
const doneEdit = () => {
const point = leave(true);
closeBar();
openPanel(phone ? editChip : point.field);
if (wantShot) retake();
};
function cancelEdit() {
const { node, resume, unmark } = editing;
if (resume) {
editing = null;
unmark();
if (resume.style === null) node.removeAttribute("style");
else node.setAttribute("style", resume.style);
putBack(resume.at);
addPoint(node, resume.point.info, resume.point.edit);
} else {
release(false);
}
closeBar();
openPanel(editChip);
}
function adjustPoint(point) {
closePanel(false);
showFlags(true);
startEdit(point.node);
}
editChip.addEventListener("click", () => startPicking(startEdit, editChip, true));
earlier.addEventListener("click", () => step(-1));
later.addEventListener("click", () => step(1));
upButton.addEventListener("click", () => {
trail.push(editing.node);
switchTo(editing.node.parentElement);
});
downButton.addEventListener("click", () => switchTo(trail.pop()));
$(".done").addEventListener("click", doneEdit);
$(".cancel-edit").addEventListener("click", cancelEdit);