PNG explorer mostly done: interactivity complete
Some small tasks remain, but it largely works.
This commit is contained in:
parent
4ea0edcbae
commit
ff4eb82a39
|
@ -8,6 +8,6 @@
|
|||
"mock": "https://deno.land/std@0.196.0/testing/mock.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.ns", "dom"]
|
||||
"lib": ["deno.ns", "dom", "dom.iterable"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
* @returns {Generator<HTMLElement, void, unknown>}
|
||||
*/
|
||||
export default function* allChildrenOf(element) {
|
||||
yield element;
|
||||
for (const child of element.children ?? []) {
|
||||
yield* allChildrenOf(/** @type {HTMLElement} */ (child));
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import crel, { fragment } from "../common/crel.js";
|
||||
import formatBytes from "../common/formatBytes.js";
|
||||
import allChildrenOf from "../common/allChildrenOf.js";
|
||||
import * as nodePath from "./nodePath.js";
|
||||
/** @typedef {import("./nodePath.js").NodePath} NodePath */
|
||||
/** @typedef {import("../../types/png.d.ts").PngNode} PngNode */
|
||||
|
||||
|
@ -11,6 +13,12 @@ import formatBytes from "../common/formatBytes.js";
|
|||
* @returns {NodeUi}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} HoveringOver
|
||||
* @prop {"bytes" | "tree"} side
|
||||
* @prop {NodePath} path
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {PngNode} rootNode
|
||||
* @param {NodeUiFn} getNodeUi
|
||||
|
@ -22,6 +30,142 @@ export default (rootNode, getNodeUi) => {
|
|||
const outerBytesEl = crel("div", { class: "bytes" }, bytesRootEl);
|
||||
const outerTreeEl = crel("div", { class: "tree" }, treeRootEl);
|
||||
|
||||
/** @type {null | HoveringOver} */
|
||||
let hoveringOver = null;
|
||||
|
||||
const rerender = () => {
|
||||
// TODO: Remove this console.log
|
||||
console.log("rerendering", JSON.stringify(hoveringOver));
|
||||
|
||||
/**
|
||||
* @callback IsActivatedFn
|
||||
* @param {NodePath} path
|
||||
* @returns {boolean}
|
||||
*/
|
||||
/** @type {IsActivatedFn} */ let isByteElActivated;
|
||||
/** @type {IsActivatedFn} */ let isTreeElActivated;
|
||||
if (hoveringOver) {
|
||||
const hoveredPath = hoveringOver.path;
|
||||
switch (hoveringOver.side) {
|
||||
case "bytes":
|
||||
isByteElActivated = (path) => nodePath.isEqualTo(path, hoveredPath);
|
||||
isTreeElActivated = (path) =>
|
||||
nodePath.isSupersetOf(path, hoveredPath);
|
||||
break;
|
||||
case "tree":
|
||||
isByteElActivated = (path) =>
|
||||
nodePath.isSupersetOf(hoveredPath, path);
|
||||
isTreeElActivated = (path) => nodePath.isEqualTo(path, hoveredPath);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unexpected hovering over");
|
||||
}
|
||||
} else {
|
||||
isByteElActivated = isTreeElActivated = () => false;
|
||||
}
|
||||
|
||||
for (const bytesEl of allChildrenOf(bytesRootEl)) {
|
||||
const path = elPath(bytesEl);
|
||||
if (path) {
|
||||
bytesEl.classList.toggle("activated", isByteElActivated(path));
|
||||
} else {
|
||||
throw new Error("Expected a path");
|
||||
}
|
||||
}
|
||||
for (const treeEl of allChildrenOf(treeRootEl)) {
|
||||
const path = elPath(treeEl);
|
||||
if (!path) continue;
|
||||
treeEl.classList.toggle("activated", isTreeElActivated(path));
|
||||
}
|
||||
};
|
||||
|
||||
outerBytesEl.addEventListener("click", (event) => {
|
||||
if (!(event.target instanceof HTMLElement)) return;
|
||||
|
||||
const rawPath = event.target?.dataset?.path;
|
||||
if (!rawPath) return;
|
||||
|
||||
for (const explorerEl of allChildrenOf(outerTreeEl)) {
|
||||
if (explorerEl.dataset.path) explorerEl.removeAttribute("open");
|
||||
}
|
||||
|
||||
const leafEl = outerTreeEl.querySelector(
|
||||
`details[data-path="${rawPath}"]`,
|
||||
);
|
||||
let currentTreeEl = leafEl;
|
||||
while (currentTreeEl && (currentTreeEl !== outerTreeEl)) {
|
||||
currentTreeEl.setAttribute("open", "open");
|
||||
currentTreeEl = currentTreeEl.parentElement;
|
||||
}
|
||||
leafEl?.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
|
||||
outerBytesEl.addEventListener("mousemove", (event) => {
|
||||
if (!(event.target instanceof HTMLElement)) return;
|
||||
const path = elPath(event.target);
|
||||
if (!path) return;
|
||||
|
||||
hoveringOver = { side: "bytes", path };
|
||||
rerender();
|
||||
});
|
||||
|
||||
outerBytesEl.addEventListener("mouseout", (event) => {
|
||||
if (event.target !== outerBytesEl) return;
|
||||
|
||||
if (hoveringOver?.side === "bytes") {
|
||||
hoveringOver = null;
|
||||
rerender();
|
||||
}
|
||||
});
|
||||
|
||||
/** @param {UIEvent} event */
|
||||
const onFocusExplorerTree = (event) => {
|
||||
if (!(event.target instanceof HTMLElement)) return;
|
||||
|
||||
// We don't update the state in these situations to avoid UI jitter.
|
||||
if (event.target.classList.contains("children")) return;
|
||||
|
||||
/** @type {null | HTMLElement} */ let nodeToCheckForPath = event.target;
|
||||
while (nodeToCheckForPath && (nodeToCheckForPath !== outerTreeEl)) {
|
||||
if (nodeToCheckForPath.dataset.path) break;
|
||||
nodeToCheckForPath = nodeToCheckForPath.parentElement;
|
||||
}
|
||||
const path = nodeToCheckForPath && elPath(nodeToCheckForPath);
|
||||
if (!path) return;
|
||||
|
||||
hoveringOver = { side: "tree", path };
|
||||
rerender();
|
||||
};
|
||||
/** @param {UIEvent} event */
|
||||
const onBlurExplorerTree = (event) => {
|
||||
if (
|
||||
(event.target !== outerTreeEl) &&
|
||||
(event.target !== outerTreeEl.children[0]) &&
|
||||
(hoveringOver?.side !== "tree")
|
||||
) return;
|
||||
hoveringOver = null;
|
||||
rerender();
|
||||
};
|
||||
outerTreeEl.addEventListener("mousemove", onFocusExplorerTree);
|
||||
outerTreeEl.addEventListener("focusin", onFocusExplorerTree);
|
||||
outerTreeEl.addEventListener("mouseout", onBlurExplorerTree);
|
||||
outerTreeEl.addEventListener("focusout", onBlurExplorerTree);
|
||||
|
||||
outerTreeEl.addEventListener("click", (event) => {
|
||||
if (!(event.target instanceof HTMLElement)) return;
|
||||
|
||||
/** @type {null | HTMLElement} */ let nodeToCheckForPath = event.target;
|
||||
while (nodeToCheckForPath && (nodeToCheckForPath !== outerTreeEl)) {
|
||||
if (nodeToCheckForPath.dataset.path) break;
|
||||
nodeToCheckForPath = nodeToCheckForPath.parentElement;
|
||||
}
|
||||
const rawPath = nodeToCheckForPath?.dataset.path;
|
||||
if (!rawPath) return;
|
||||
|
||||
const bytesEl = outerBytesEl.querySelector(`[data-path="${rawPath}"]`);
|
||||
bytesEl?.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
|
||||
return fragment(outerBytesEl, outerTreeEl);
|
||||
};
|
||||
|
||||
|
@ -71,3 +215,9 @@ const traverse = (node, path, getNodeUi) => {
|
|||
|
||||
return [nodeBytesEl, nodeTreeEl];
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} el
|
||||
* @returns {null | NodePath}
|
||||
*/
|
||||
const elPath = (el) => nodePath.parse(el.dataset?.path);
|
||||
|
|
|
@ -26,5 +26,29 @@
|
|||
*/
|
||||
export const parse = (value) => {
|
||||
if (typeof value !== "string") return null;
|
||||
return value.split(".").map(Number);
|
||||
if (!value) return [];
|
||||
return value.split(",").map((str) => {
|
||||
const result = parseInt(str, 10);
|
||||
if (Number.isFinite(result)) return result;
|
||||
throw new Error(`Invalid node path: ${value}`);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {NodePath} a
|
||||
* @param {NodePath} b
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isEqualTo = (a, b) => a.length === b.length && isSupersetOf(a, b);
|
||||
|
||||
/**
|
||||
* @param {NodePath} smaller
|
||||
* @param {NodePath} bigger
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isSupersetOf = (smaller, bigger) => {
|
||||
for (let i = 0; i < smaller.length; i++) {
|
||||
if (smaller[i] !== bigger[i]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { assert, assertEquals } from "assert";
|
||||
import { isEqualTo, isSupersetOf, parse } from "../../public/png/nodePath.js";
|
||||
|
||||
Deno.test("parse", () => {
|
||||
assertEquals(parse(""), []);
|
||||
assertEquals(parse("1"), [1]);
|
||||
assertEquals(parse("1,2,3"), [1, 2, 3]);
|
||||
});
|
||||
|
||||
Deno.test("isEqualTo", () => {
|
||||
assert(isEqualTo([], []));
|
||||
assert(isEqualTo([1, 2, 3], [1, 2, 3]));
|
||||
assert(!isEqualTo([1, 2], [1, 2, 3]));
|
||||
});
|
||||
|
||||
Deno.test("isSupersetOf", () => {
|
||||
assert(isSupersetOf([1, 2, 3], [1, 2, 3]));
|
||||
assert(isSupersetOf([1, 2], [1, 2, 3]));
|
||||
assert(!isSupersetOf([1, 2, 3], [1, 2]));
|
||||
});
|
Loading…
Reference in New Issue