From 4e2898144395a45201651d5a02d4f31157afa4fa Mon Sep 17 00:00:00 2001 From: KimlikDAO-bot Date: Thu, 20 Feb 2025 00:49:31 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=9D=EF=B8=8F=20Add=20KeyedSwitch=20=20?= =?UTF-8?q?-=20KeyedSwitch=20displays=20exactly=201=20child=20at=20a=20tim?= =?UTF-8?q?e=20=20=20=20specified=20by=20a=20string=20key.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kastro/KeyedSwitch.jsx | 57 ++++++++++++++++++++++++++++++++++++++++ kastro/Router.jsx | 19 ++++++++++++++ kastro/Switch.jsx | 12 ++++----- kastro/kastro.js | 3 +++ kastro/transpiler/jsx.js | 48 ++++++++++++++++++++++++++++++--- util/dom.js | 1 + 6 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 kastro/KeyedSwitch.jsx create mode 100644 kastro/Router.jsx diff --git a/kastro/KeyedSwitch.jsx b/kastro/KeyedSwitch.jsx new file mode 100644 index 0000000..0bfd25b --- /dev/null +++ b/kastro/KeyedSwitch.jsx @@ -0,0 +1,57 @@ +import dom from "../util/dom"; + +/** + * @constructor + * @param {{ + * id: string, + * initialPane: (string | undefined), + * children: !Array, + * keyToIndex: !Object, + * }} props + */ +const KeyedSwitch = function ({ id, initialPane, children, keyToIndex }) { + /** @type {number} */ + this.selectedPane = dom.GEN ? 0 : initialPane ? keyToIndex[initialPane] : 0; + /** @const {!Array} */ + this.initializers = children; + /** @const {!Object} */ + this.keyToIndex = keyToIndex; + /** @const {!HTMLDivElement} */ + const Root = dom.div(id); + /** @const {!HTMLDivElement} */ + this.root = Root; + + return ( + + {children.modify((c, i) => { + c.nodisplay = initialPane ? c.key != initialPane : i != this.selectedPane; + delete c.key; + })} + + ); +} + +/** + * Shows the child with the given key and hides the currently shown child. + * If the child is being shown for the first time, initializes it. + * + * @param {string} key Key of the child to show + */ +KeyedSwitch.prototype.showPane = function (key) { + /** @const {number} */ + const idx = this.keyToIndex[key]; + /** @const {number} */ + const old = this.selectedPane; + if (idx == old) return; + /** @const {?function():void} */ + const f = this.initializers[idx]; + if (f) { + f(); + this.initializers[idx] = null; + } + this.selectedPane = idx; + dom.show(this.root.children[idx]); + dom.hide(this.root.children[old]); +} + +export default KeyedSwitch; diff --git a/kastro/Router.jsx b/kastro/Router.jsx new file mode 100644 index 0000000..2f5c36b --- /dev/null +++ b/kastro/Router.jsx @@ -0,0 +1,19 @@ +import dom from "../util/dom"; + +/** + * @param {{ + * routeHandler: function(string):void + * }} props + */ +const Router = ({ routeHandler }) => { + const onHashChange = () => routeHandler(window.location.hash.slice(1)); + window.onhashchange = onHashChange; + dom.schedule(onHashChange, 0); +} + +/** + * @param {string} route + */ +Router.navigate = (route) => window.location.hash = route; + +export default Router; diff --git a/kastro/Switch.jsx b/kastro/Switch.jsx index b006b6a..59717ba 100644 --- a/kastro/Switch.jsx +++ b/kastro/Switch.jsx @@ -4,13 +4,13 @@ import dom from "../util/dom"; * @constructor * @param {{ * id: string, - * initialSelected: number, + * initialPane: number, * children: !Array, * }} props */ -const Switch = function ({ id, initialSelected, children }) { +const Switch = function ({ id, initialPane = 0, children }) { /** @type {number} */ - this.selectedChild = initialSelected; + this.selectedPane = initialPane; /** @const {!Array} */ this.initializers = children; /** @const {!HTMLDivElement} */ @@ -20,7 +20,7 @@ const Switch = function ({ id, initialSelected, children }) { return ( - {children.modify((c, i) => c.nodisplay = i != initialSelected)} + {children.modify((c, i) => c.nodisplay = i != initialPane)} ); } @@ -33,7 +33,7 @@ const Switch = function ({ id, initialSelected, children }) { */ Switch.prototype.showPane = function (idx) { /** @const {number} */ - const old = this.selectedChild; + const old = this.selectedPane; if (idx == old) return; /** @const {?function():void} */ const f = this.initializers[idx]; @@ -41,7 +41,7 @@ Switch.prototype.showPane = function (idx) { f(); this.initializers[idx] = null; } - this.selectedChild = idx; + this.selectedPane = idx; dom.show(this.root.children[idx]); dom.hide(this.root.children[old]); } diff --git a/kastro/kastro.js b/kastro/kastro.js index c75ac1d..734e53d 100644 --- a/kastro/kastro.js +++ b/kastro/kastro.js @@ -111,6 +111,9 @@ const setupKastro = () => { ethereum: { isRabby: false }, + location: { + hash: "", + }, addEventListener(name, handler) { }, dispatchEvent(event) { } }; diff --git a/kastro/transpiler/jsx.js b/kastro/transpiler/jsx.js index 1cab911..b0834ce 100644 --- a/kastro/transpiler/jsx.js +++ b/kastro/transpiler/jsx.js @@ -71,11 +71,11 @@ const transpile = (isEntry, file, content, domIdMapper, globals) => { const length = node.children.length; if (!length) return; /** @const {number} */ - const selected = props.initialSelected.expression.value; + const selectedPane = props.initialPane ? props.initialPane.expression.value : 0; const fnExprs = Array(length); for (let i = 0; i < length; ++i) { const statements = []; - if (i != selected) + if (i != selectedPane) processJsxElement(node.children[i], node, i, statements); const fnExpr = statements.length ? statements.length == 1 @@ -85,7 +85,45 @@ const transpile = (isEntry, file, content, domIdMapper, globals) => { fnExprs[i] = fnExpr; } props.children = `[${fnExprs.join(", ")}]`; - node.children[0] = node.children[selected]; + node.children[0] = node.children[selectedPane]; + node.children.length = 1; + }, + + KeyedSwitch: (node, props) => { + node.children = node.children.filter((c) => c.type == "JSXElement"); + const length = node.children.length; + if (!length) return; + // Build keyToIndex map and collect keys + const keyToIndex = {}; + node.children.forEach((child, i) => { + const keyProp = child.openingElement.attributes + .find(attr => attr.name.name === "key"); + if (!keyProp) + throw new Error("KeyedSwitch children must have key prop"); + const key = keyProp.value.value; + keyToIndex[key] = i; + }); + + /** @const {string} */ + const initialPane = props.initialPane ? props.initialPane.expression.value : ""; + /** @const {number} */ + const selectedPane = initialPane ? keyToIndex[initialPane] : 0; + + const fnExprs = Array(length); + for (let i = 0; i < length; ++i) { + const statements = []; + if (i != selectedPane) + processJsxElement(node.children[i], node, i, statements); + const fnExpr = statements.length + ? statements.length == 1 + ? `() => ${statements[0]}` + : `() => {\n ${statements.join(";\n ")}\n}` + : "null"; + fnExprs[i] = fnExpr; + } + props.keyToIndex = `${JSON.stringify(keyToIndex)}`; + props.children = `[${fnExprs.join(", ")}]`; + node.children[0] = node.children[selectedPane]; node.children.length = 1; } } @@ -146,6 +184,8 @@ const transpile = (isEntry, file, content, domIdMapper, globals) => { } else if (name == "instance") { keepImport = true; instance = content.slice(attr.value.start + 1, attr.value.end - 1); + } else if (parent.type == "JSXElement" && parent.openingElement.name.name == "KeyedSwitch" && name == "key") { + // Ignore key prop for KeyedSwitch } else if (!name.endsWith("$")) props[name] = attr.value; else @@ -169,7 +209,7 @@ const transpile = (isEntry, file, content, domIdMapper, globals) => { ? `{\n ${Object.entries(props).map(([k, v]) => `${k}: ${serialize(v)}`).join(",\n ")}\n }` : ""; const call = `${tagName}(${callParams})`; - statements.push(instance ? `/** @const {${tagName}} */\n ${instance} = new ${call}` : call); + statements.push(instance ? `/** @const {!${tagName}} */\n ${instance} = new ${call}` : call); } if (keepImport && info) info.state = SpecifierState.Keep; diff --git a/util/dom.js b/util/dom.js index 274119e..fd55c22 100644 --- a/util/dom.js +++ b/util/dom.js @@ -255,6 +255,7 @@ const schedule = (f, ms) => (GEN && globalThis["GEN"]) ? {} : setTimeout(f, ms); const run = (f) => (GEN && globalThis["GEN"]) ? {} : f(); export default { + GEN, Lang, // Elements a,