@loadbare/app 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,12 @@ import { type HubData, type ListResult, type Row } from "../core/lb-types.js";
9
9
  * element from any document.
10
10
  */
11
11
  export declare function isValueControl(el: Element): el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
12
+ /**
13
+ * Root if it matches, then every descendant in root's scope that does.
14
+ * Landing and gathering both find cells through here, so a form reads back
15
+ * from exactly the places a row lands.
16
+ */
17
+ export declare function within(root: Element, selector: string): Element[];
12
18
  /**
13
19
  * Fill one scope from one row.
14
20
  *
@@ -1 +1 @@
1
- {"version":3,"file":"lb-apply.d.ts","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"AA4BA,OAAO,EAEL,KAAK,OAAO,EAEZ,KAAK,UAAU,EACf,KAAK,GAAG,EACT,MAAM,qBAAqB,CAAC;AAK7B;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,OAAO,GACV,EAAE,IAAI,gBAAgB,GAAG,iBAAiB,GAAG,mBAAmB,CAMlE;AAgED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAMtD;AA2CD;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAgElE;AAED,gEAAgE;AAChE,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAkC/D"}
1
+ {"version":3,"file":"lb-apply.d.ts","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"AA4BA,OAAO,EAEL,KAAK,OAAO,EAEZ,KAAK,UAAU,EACf,KAAK,GAAG,EACT,MAAM,qBAAqB,CAAC;AAK7B;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,OAAO,GACV,EAAE,IAAI,gBAAgB,GAAG,iBAAiB,GAAG,mBAAmB,CAMlE;AA0DD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAMjE;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAMtD;AA2CD;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAgElE;AAED,gEAAgE;AAChE,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAkC/D"}
@@ -72,12 +72,15 @@ function land(el, value) {
72
72
  *
73
73
  * Both scope attributes scope their DOM children, and a nested one of either
74
74
  * kind begins a new scope (docs/reference/data-binding.md). So a descendant
75
- * is in root's scope unless something between it and root — the element
76
- * itself included — carries one. Root's own attribute does not count: root is
77
- * the scope being filled, whatever it carries.
75
+ * is in root's scope unless something between it and root carries one. The
76
+ * element's own attribute does not count: it names what the element
77
+ * displays, and where it belongs is its ancestors', so a picker carrying
78
+ * `lb-list` and `lb-cell` is a cell of the row around it. Root's own
79
+ * attribute does not count either: root is the scope being filled, whatever
80
+ * it carries.
78
81
  */
79
82
  function inScope(root, el) {
80
- for (let node = el; node && node !== root;) {
83
+ for (let node = el.parentElement; node && node !== root;) {
81
84
  if (node.hasAttribute(ATTR_LIST) || node.hasAttribute(ATTR_ROW)) {
82
85
  return false;
83
86
  }
@@ -85,8 +88,12 @@ function inScope(root, el) {
85
88
  }
86
89
  return true;
87
90
  }
88
- /** Root if it matches, then every descendant in root's scope that does. */
89
- function within(root, selector) {
91
+ /**
92
+ * Root if it matches, then every descendant in root's scope that does.
93
+ * Landing and gathering both find cells through here, so a form reads back
94
+ * from exactly the places a row lands.
95
+ */
96
+ export function within(root, selector) {
90
97
  const found = root.matches(selector) ? [root] : [];
91
98
  for (const el of root.querySelectorAll(selector)) {
92
99
  if (inScope(root, el))
@@ -134,13 +141,13 @@ function templateIn(scope) {
134
141
  * carries one: the template names the key column under a different name.
135
142
  *
136
143
  * A row of a nested scope is that scope's. A row's own `lb-list` or `lb-row`
137
- * names what it displays, not which list it is a row of, so the walk starts
138
- * at its parent.
144
+ * names what it displays, not which list it is a row of, which is the same
145
+ * test a cell gets.
139
146
  */
140
147
  function showing(scope) {
141
148
  const rows = new Map();
142
149
  for (const el of scope.querySelectorAll(`[${ATTR_KEY_VALUE}]`)) {
143
- if (inScope(scope, el.parentElement)) {
150
+ if (inScope(scope, el)) {
144
151
  rows.set(el.getAttribute(ATTR_KEY_VALUE), el);
145
152
  }
146
153
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lb-apply.js","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\n/**\n * Landing data on a live host — see docs/reference/data-binding.md.\n *\n * One containment ladder, and one function per rung. `applyData` lands a\n * whole response, `applyList` lands one list result, `applyRow` fills one\n * scope from one row. Filling a live row and filling a single-row scope are\n * the same operation, and the only difference is how much of the page the\n * root covers.\n *\n * The row machinery used to live in its own file because every list widget\n * imported and called it. The hub reconciles a list scope itself now, so a\n * widget supplies placement and scaffolding through two optional hooks and\n * never sees a whole result. Cloning the template, matching a row to the\n * element already showing it, and filling that element are the same in every\n * list, so no widget can get them wrong.\n */\n\nimport {\n ATTR_CELL,\n ATTR_KEY,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_ROW,\n ATTR_ROW_COUNT,\n ATTR_VALUE,\n} from \"../core/lb-constants.js\";\nimport {\n isPatch,\n type HubData,\n type ListHost,\n type ListResult,\n type Row,\n} from \"../core/lb-types.js\";\n\n/** The input types whose state is not their `value`, or cannot be set. */\nconst UNLANDED_INPUTS = [\"checkbox\", \"radio\", \"file\"];\n\n/**\n * A form control whose state is its `value`: a `<select>`, a `<textarea>`,\n * or an `<input>` of any type but checkbox, radio and file. Landing and\n * gathering draw the same line, so a value read back from a form is the one\n * that landed there.\n *\n * Told apart by tag name and type rather than by class, so it holds for an\n * element from any document.\n */\nexport function isValueControl(\n el: Element,\n): el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement {\n if (el.localName === \"select\" || el.localName === \"textarea\") return true;\n return (\n el.localName === \"input\" &&\n !UNLANDED_INPUTS.includes((el as HTMLInputElement).type)\n );\n}\n\n/**\n * A cell lands one of three ways. A custom element owns whatever control it\n * wraps, so it receives the value as an attribute and renders it itself.\n * Because the browser runs `attributeChangedCallback` for attributes already\n * present when a widget upgrades, that is the same operation whether the host\n * was inserted a microsecond ago or an hour ago.\n *\n * A form control shows its state as its `value`, so that is where the value\n * goes; its text is its options, for a select. Any other native element has\n * no behavior of its own, so its value is its text. Either one also carries\n * the value as `lb-value`, so a stylesheet can select on what landed.\n * Checkboxes and radio buttons are not implemented, and receive nothing — see\n * docs/TECHREF-1.0.md, \"Blockers\".\n */\nfunction land(el: Element, value: unknown): void {\n // Untouched, whatever the server sent: the browser decides what a\n // non-string looks like. See Row in core/lb-types.ts.\n if (el.localName.includes(\"-\")) {\n el.setAttribute(ATTR_VALUE, value as string);\n } else if (isValueControl(el)) {\n el.value = value as string;\n el.setAttribute(ATTR_VALUE, value as string);\n } else if (el.localName === \"input\") {\n console.warn(\n `lb-hub: a value does not land on <input type=\"${(el as HTMLInputElement).type}\">, ignoring`,\n el,\n );\n } else {\n el.textContent = value as string;\n el.setAttribute(ATTR_VALUE, value as string);\n }\n}\n\n/**\n * Whether an element under `root` belongs to root's scope, rather than to a\n * scope nested inside it.\n *\n * Both scope attributes scope their DOM children, and a nested one of either\n * kind begins a new scope (docs/reference/data-binding.md). So a descendant\n * is in root's scope unless something between it and root — the element\n * itself included — carries one. Root's own attribute does not count: root is\n * the scope being filled, whatever it carries.\n */\nfunction inScope(root: Element, el: Element): boolean {\n for (let node: Element | null = el; node && node !== root;) {\n if (node.hasAttribute(ATTR_LIST) || node.hasAttribute(ATTR_ROW)) {\n return false;\n }\n node = node.parentElement;\n }\n return true;\n}\n\n/** Root if it matches, then every descendant in root's scope that does. */\nfunction within(root: Element, selector: string): Element[] {\n const found: Element[] = root.matches(selector) ? [root] : [];\n for (const el of root.querySelectorAll(selector)) {\n if (inScope(root, el)) found.push(el);\n }\n return found;\n}\n\n/**\n * Fill one scope from one row.\n *\n * The root counts as a cell if it carries one. A `<tr>` holds its cells in\n * `<td>` children, but `<option>`'s content model is text, so an option row\n * has to be the cell it displays. Requiring a wrapper there would require an\n * element HTML does not allow.\n */\nexport function applyRow(root: Element, row: Row): void {\n for (const [column, value] of Object.entries(row)) {\n for (const el of within(root, `[${ATTR_CELL}=\"${column}\"]`)) {\n land(el, value);\n }\n }\n}\n\n/**\n * The row template: the one the developer wrote inside this list scope, or\n * null when this scope shows nothing. A template inside a nested scope is\n * that scope's.\n *\n * A scope with no template is bound to the list without displaying it.\n * That is not a mistake, so it is silent. A template that does not name its\n * key column is a mistake, and says so.\n */\nfunction templateIn(scope: Element): HTMLTemplateElement | null {\n const template = within(scope, \"template\")[0] as\n HTMLTemplateElement | undefined;\n if (!template) return null;\n if (!template.getAttribute(ATTR_KEY)) {\n console.error(\n `lb-hub: the <template> in <${scope.localName}> has no ${ATTR_KEY} ` +\n `naming the column that identifies a row`,\n );\n return null;\n }\n return template;\n}\n\n/**\n * The rows already showing, by the key value each carries. Only a live row\n * carries one: the template names the key column under a different name.\n *\n * A row of a nested scope is that scope's. A row's own `lb-list` or `lb-row`\n * names what it displays, not which list it is a row of, so the walk starts\n * at its parent.\n */\nfunction showing(scope: Element): Map<string, Element> {\n const rows = new Map<string, Element>();\n for (const el of scope.querySelectorAll(`[${ATTR_KEY_VALUE}]`)) {\n if (inScope(scope, el.parentElement!)) {\n rows.set(el.getAttribute(ATTR_KEY_VALUE)!, el);\n }\n }\n return rows;\n}\n\n/**\n * Land a list result in a list scope.\n *\n * An array is the whole set, so it decides membership and order: every row is\n * placed in the order given, and a row whose key did not arrive is gone. A\n * patch disturbs only what it names — a row it did not mention keeps its\n * contents and its position.\n *\n * A widget that carries `lbPlaceRow` decides where a row goes, because only\n * it knows whether it sorts or groups. Without it a row lands immediately\n * before the template, so rows accumulate in the order they arrive and the\n * template stays put as the insertion marker.\n */\nexport function applyList(scope: Element, result: ListResult): void {\n const template = templateIn(scope);\n if (!template) return;\n const keyColumn = template.getAttribute(ATTR_KEY)!;\n const shown = showing(scope);\n const host = scope as Element & ListHost;\n const whole = !isPatch(result);\n\n const place = (el: Element, row: Row): void => {\n if (host.lbPlaceRow) host.lbPlaceRow(el, row, template);\n else template.parentElement!.insertBefore(el, template);\n };\n\n const upsert = (row: Row): string | null => {\n if (row[keyColumn] === undefined) {\n console.error(\n `lb-hub: a row for <${scope.localName}> has no '${keyColumn}' column`,\n );\n return null;\n }\n // The key is stored on the row as an attribute and read back from\n // there, so it is compared as the string the attribute holds.\n const key = String(row[keyColumn]);\n let el = shown.get(key);\n const fresh = el === undefined;\n if (!el) {\n el = template.content.firstElementChild!.cloneNode(true) as Element;\n el.setAttribute(ATTR_KEY_VALUE, key);\n shown.set(key, el);\n }\n // Fill before insertion. The attributes are already there when the row\n // upgrades, which is the same thing that makes hydration and refresh one\n // operation everywhere else.\n applyRow(el, row);\n if (fresh || whole) place(el, row);\n return key;\n };\n\n if (Array.isArray(result)) {\n const arrived = new Set<string>();\n for (const row of result) {\n const key = upsert(row);\n if (key !== null) arrived.add(key);\n }\n for (const [key, el] of shown) if (!arrived.has(key)) el.remove();\n } else {\n for (const row of result.rows ?? []) upsert(row);\n for (const key of result.drop ?? []) shown.get(String(key))?.remove();\n }\n\n // How many rows are showing, counted from the DOM rather than from either\n // branch above, so a whole set and a patch report the same fact the same\n // way.\n //\n // It is stamped here because only this function knows the count: it is the\n // one conditional a page cannot be sent, since the server answers with rows\n // and says nothing about how many survived reconciliation. A page says what\n // an empty list looks like in a stylesheet, and no list widget carries code\n // for it. See docs/reference/data-binding.md.\n scope.setAttribute(ATTR_ROW_COUNT, String(showing(scope).size));\n\n // Derived scaffolding — a section heading, an <optgroup> — goes when its\n // last row does, and only the widget knows it exists.\n host.lbRowsLanded?.();\n}\n\n/** Land a whole response. Every result arrives through here. */\nexport function applyData(root: ParentNode, data: HubData): void {\n for (const [name, result] of Object.entries(data)) {\n const lists = root.querySelectorAll(`[${ATTR_LIST}=\"${name}\"]`);\n const rows = root.querySelectorAll(`[${ATTR_ROW}=\"${name}\"]`);\n if (lists.length === 0 && rows.length === 0) {\n console.warn(`lb-hub: no scope for '${name}', skipping`);\n continue;\n }\n // Cardinality is a property of the name, so one name is a list or a row\n // and never both. Two spellings of it on one page is a mistake in the\n // markup rather than a case to reconcile.\n if (lists.length > 0 && rows.length > 0) {\n console.error(\n `lb-hub: '${name}' is bound as a list in one place and a row in ` +\n `another; a name answers with one shape`,\n );\n continue;\n }\n if (lists.length > 0) {\n for (const scope of lists) applyList(scope, result as ListResult);\n continue;\n }\n // The only disagreement visible from here: a patch and a row are both\n // objects, so an array arriving at a row scope is the one case the\n // browser can name. createHub holds the stronger check, because it knows\n // what the query declared.\n if (Array.isArray(result)) {\n console.error(\n `lb-hub: '${name}' is bound with ${ATTR_ROW} but answered with rows`,\n );\n continue;\n }\n for (const scope of rows) applyRow(scope, result as Row);\n }\n}\n"]}
1
+ {"version":3,"file":"lb-apply.js","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\n/**\n * Landing data on a live host — see docs/reference/data-binding.md.\n *\n * One containment ladder, and one function per rung. `applyData` lands a\n * whole response, `applyList` lands one list result, `applyRow` fills one\n * scope from one row. Filling a live row and filling a single-row scope are\n * the same operation, and the only difference is how much of the page the\n * root covers.\n *\n * The row machinery used to live in its own file because every list widget\n * imported and called it. The hub reconciles a list scope itself now, so a\n * widget supplies placement and scaffolding through two optional hooks and\n * never sees a whole result. Cloning the template, matching a row to the\n * element already showing it, and filling that element are the same in every\n * list, so no widget can get them wrong.\n */\n\nimport {\n ATTR_CELL,\n ATTR_KEY,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_ROW,\n ATTR_ROW_COUNT,\n ATTR_VALUE,\n} from \"../core/lb-constants.js\";\nimport {\n isPatch,\n type HubData,\n type ListHost,\n type ListResult,\n type Row,\n} from \"../core/lb-types.js\";\n\n/** The input types whose state is not their `value`, or cannot be set. */\nconst UNLANDED_INPUTS = [\"checkbox\", \"radio\", \"file\"];\n\n/**\n * A form control whose state is its `value`: a `<select>`, a `<textarea>`,\n * or an `<input>` of any type but checkbox, radio and file. Landing and\n * gathering draw the same line, so a value read back from a form is the one\n * that landed there.\n *\n * Told apart by tag name and type rather than by class, so it holds for an\n * element from any document.\n */\nexport function isValueControl(\n el: Element,\n): el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement {\n if (el.localName === \"select\" || el.localName === \"textarea\") return true;\n return (\n el.localName === \"input\" &&\n !UNLANDED_INPUTS.includes((el as HTMLInputElement).type)\n );\n}\n\n/**\n * A cell lands one of three ways. A custom element owns whatever control it\n * wraps, so it receives the value as an attribute and renders it itself.\n * Because the browser runs `attributeChangedCallback` for attributes already\n * present when a widget upgrades, that is the same operation whether the host\n * was inserted a microsecond ago or an hour ago.\n *\n * A form control shows its state as its `value`, so that is where the value\n * goes; its text is its options, for a select. Any other native element has\n * no behavior of its own, so its value is its text. Either one also carries\n * the value as `lb-value`, so a stylesheet can select on what landed.\n * Checkboxes and radio buttons are not implemented, and receive nothing — see\n * docs/TECHREF-1.0.md, \"Blockers\".\n */\nfunction land(el: Element, value: unknown): void {\n // Untouched, whatever the server sent: the browser decides what a\n // non-string looks like. See Row in core/lb-types.ts.\n if (el.localName.includes(\"-\")) {\n el.setAttribute(ATTR_VALUE, value as string);\n } else if (isValueControl(el)) {\n el.value = value as string;\n el.setAttribute(ATTR_VALUE, value as string);\n } else if (el.localName === \"input\") {\n console.warn(\n `lb-hub: a value does not land on <input type=\"${(el as HTMLInputElement).type}\">, ignoring`,\n el,\n );\n } else {\n el.textContent = value as string;\n el.setAttribute(ATTR_VALUE, value as string);\n }\n}\n\n/**\n * Whether an element under `root` belongs to root's scope, rather than to a\n * scope nested inside it.\n *\n * Both scope attributes scope their DOM children, and a nested one of either\n * kind begins a new scope (docs/reference/data-binding.md). So a descendant\n * is in root's scope unless something between it and root carries one. The\n * element's own attribute does not count: it names what the element\n * displays, and where it belongs is its ancestors', so a picker carrying\n * `lb-list` and `lb-cell` is a cell of the row around it. Root's own\n * attribute does not count either: root is the scope being filled, whatever\n * it carries.\n */\nfunction inScope(root: Element, el: Element): boolean {\n for (let node = el.parentElement; node && node !== root;) {\n if (node.hasAttribute(ATTR_LIST) || node.hasAttribute(ATTR_ROW)) {\n return false;\n }\n node = node.parentElement;\n }\n return true;\n}\n\n/**\n * Root if it matches, then every descendant in root's scope that does.\n * Landing and gathering both find cells through here, so a form reads back\n * from exactly the places a row lands.\n */\nexport function within(root: Element, selector: string): Element[] {\n const found: Element[] = root.matches(selector) ? [root] : [];\n for (const el of root.querySelectorAll(selector)) {\n if (inScope(root, el)) found.push(el);\n }\n return found;\n}\n\n/**\n * Fill one scope from one row.\n *\n * The root counts as a cell if it carries one. A `<tr>` holds its cells in\n * `<td>` children, but `<option>`'s content model is text, so an option row\n * has to be the cell it displays. Requiring a wrapper there would require an\n * element HTML does not allow.\n */\nexport function applyRow(root: Element, row: Row): void {\n for (const [column, value] of Object.entries(row)) {\n for (const el of within(root, `[${ATTR_CELL}=\"${column}\"]`)) {\n land(el, value);\n }\n }\n}\n\n/**\n * The row template: the one the developer wrote inside this list scope, or\n * null when this scope shows nothing. A template inside a nested scope is\n * that scope's.\n *\n * A scope with no template is bound to the list without displaying it.\n * That is not a mistake, so it is silent. A template that does not name its\n * key column is a mistake, and says so.\n */\nfunction templateIn(scope: Element): HTMLTemplateElement | null {\n const template = within(scope, \"template\")[0] as\n HTMLTemplateElement | undefined;\n if (!template) return null;\n if (!template.getAttribute(ATTR_KEY)) {\n console.error(\n `lb-hub: the <template> in <${scope.localName}> has no ${ATTR_KEY} ` +\n `naming the column that identifies a row`,\n );\n return null;\n }\n return template;\n}\n\n/**\n * The rows already showing, by the key value each carries. Only a live row\n * carries one: the template names the key column under a different name.\n *\n * A row of a nested scope is that scope's. A row's own `lb-list` or `lb-row`\n * names what it displays, not which list it is a row of, which is the same\n * test a cell gets.\n */\nfunction showing(scope: Element): Map<string, Element> {\n const rows = new Map<string, Element>();\n for (const el of scope.querySelectorAll(`[${ATTR_KEY_VALUE}]`)) {\n if (inScope(scope, el)) {\n rows.set(el.getAttribute(ATTR_KEY_VALUE)!, el);\n }\n }\n return rows;\n}\n\n/**\n * Land a list result in a list scope.\n *\n * An array is the whole set, so it decides membership and order: every row is\n * placed in the order given, and a row whose key did not arrive is gone. A\n * patch disturbs only what it names — a row it did not mention keeps its\n * contents and its position.\n *\n * A widget that carries `lbPlaceRow` decides where a row goes, because only\n * it knows whether it sorts or groups. Without it a row lands immediately\n * before the template, so rows accumulate in the order they arrive and the\n * template stays put as the insertion marker.\n */\nexport function applyList(scope: Element, result: ListResult): void {\n const template = templateIn(scope);\n if (!template) return;\n const keyColumn = template.getAttribute(ATTR_KEY)!;\n const shown = showing(scope);\n const host = scope as Element & ListHost;\n const whole = !isPatch(result);\n\n const place = (el: Element, row: Row): void => {\n if (host.lbPlaceRow) host.lbPlaceRow(el, row, template);\n else template.parentElement!.insertBefore(el, template);\n };\n\n const upsert = (row: Row): string | null => {\n if (row[keyColumn] === undefined) {\n console.error(\n `lb-hub: a row for <${scope.localName}> has no '${keyColumn}' column`,\n );\n return null;\n }\n // The key is stored on the row as an attribute and read back from\n // there, so it is compared as the string the attribute holds.\n const key = String(row[keyColumn]);\n let el = shown.get(key);\n const fresh = el === undefined;\n if (!el) {\n el = template.content.firstElementChild!.cloneNode(true) as Element;\n el.setAttribute(ATTR_KEY_VALUE, key);\n shown.set(key, el);\n }\n // Fill before insertion. The attributes are already there when the row\n // upgrades, which is the same thing that makes hydration and refresh one\n // operation everywhere else.\n applyRow(el, row);\n if (fresh || whole) place(el, row);\n return key;\n };\n\n if (Array.isArray(result)) {\n const arrived = new Set<string>();\n for (const row of result) {\n const key = upsert(row);\n if (key !== null) arrived.add(key);\n }\n for (const [key, el] of shown) if (!arrived.has(key)) el.remove();\n } else {\n for (const row of result.rows ?? []) upsert(row);\n for (const key of result.drop ?? []) shown.get(String(key))?.remove();\n }\n\n // How many rows are showing, counted from the DOM rather than from either\n // branch above, so a whole set and a patch report the same fact the same\n // way.\n //\n // It is stamped here because only this function knows the count: it is the\n // one conditional a page cannot be sent, since the server answers with rows\n // and says nothing about how many survived reconciliation. A page says what\n // an empty list looks like in a stylesheet, and no list widget carries code\n // for it. See docs/reference/data-binding.md.\n scope.setAttribute(ATTR_ROW_COUNT, String(showing(scope).size));\n\n // Derived scaffolding — a section heading, an <optgroup> — goes when its\n // last row does, and only the widget knows it exists.\n host.lbRowsLanded?.();\n}\n\n/** Land a whole response. Every result arrives through here. */\nexport function applyData(root: ParentNode, data: HubData): void {\n for (const [name, result] of Object.entries(data)) {\n const lists = root.querySelectorAll(`[${ATTR_LIST}=\"${name}\"]`);\n const rows = root.querySelectorAll(`[${ATTR_ROW}=\"${name}\"]`);\n if (lists.length === 0 && rows.length === 0) {\n console.warn(`lb-hub: no scope for '${name}', skipping`);\n continue;\n }\n // Cardinality is a property of the name, so one name is a list or a row\n // and never both. Two spellings of it on one page is a mistake in the\n // markup rather than a case to reconcile.\n if (lists.length > 0 && rows.length > 0) {\n console.error(\n `lb-hub: '${name}' is bound as a list in one place and a row in ` +\n `another; a name answers with one shape`,\n );\n continue;\n }\n if (lists.length > 0) {\n for (const scope of lists) applyList(scope, result as ListResult);\n continue;\n }\n // The only disagreement visible from here: a patch and a row are both\n // objects, so an array arriving at a row scope is the one case the\n // browser can name. createHub holds the stronger check, because it knows\n // what the query declared.\n if (Array.isArray(result)) {\n console.error(\n `lb-hub: '${name}' is bound with ${ATTR_ROW} but answered with rows`,\n );\n continue;\n }\n for (const scope of rows) applyRow(scope, result as Row);\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"lb-hub.browser.d.ts","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="dom" />
2
- import { ACTION_CELL_CHANGE, ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, ATTR_ACTION, ATTR_CELL, ATTR_ERROR, ATTR_KEY_VALUE, ATTR_LIST, ATTR_NAV_LINK, ATTR_PAGE, ATTR_PENDING, ATTR_ROW, ATTR_UNKNOWN_PAGE, LB_ACTIONS, LB_RESERVED_PREFIX, HUB_TAG_NAME, NAV_CELL_LABEL, NAV_CELL_URI, NAV_ROW, LB_EVENT_NAME, LB_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants.js";
3
- import { applyData, isValueControl } from "./lb-apply.js";
2
+ import { ACTION_CELL_CHANGE, ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, ATTR_ACTION, ATTR_CELL, ATTR_ERROR, ATTR_KEY_VALUE, ATTR_LIST, ATTR_NAV_LINK, ATTR_PAGE, ATTR_PENDING, ATTR_ROW, ATTR_UNKNOWN_PAGE, ATTR_VALUE, LB_ACTIONS, LB_RESERVED_PREFIX, HUB_TAG_NAME, NAV_CELL_LABEL, NAV_CELL_URI, NAV_ROW, LB_EVENT_NAME, LB_ENDPOINT, LB_REQUEST_TIMEOUT_MS, } from "../core/lb-constants.js";
3
+ import { applyData, isValueControl, within } from "./lb-apply.js";
4
4
  export { applyData, applyRow } from "./lb-apply.js";
5
5
  /** What each operation takes from scope, and what it cannot be sent without. */
6
6
  const OPERATIONS = {
@@ -78,19 +78,31 @@ function scope(request, el) {
78
78
  }
79
79
  for (const name of operation.fills)
80
80
  fill(name);
81
- // Gathered from the nearest element holding a cell to read, starting at
82
- // whatever dispatched: a form holds its own, and a button in a new row's
83
- // <tr> reads the row around it. Never past the live row the element sits
84
- // in, nor out into the scope, whose cells are other rows'. Nothing to read
85
- // is nothing to send, so values stays missing.
81
+ // Gathered from the record the element belongs to, the way a button
82
+ // submits its form owner: the nearest form, table row or live row around
83
+ // it, the element itself included. A <tr> counts because it is the one
84
+ // place a form cannot go around a row's controls. What else sits beside
85
+ // the element never decides it. The record is the scope's, so it is never
86
+ // the scope itself nor outside it; with none, there is nothing to say which
87
+ // cells are meant. Outside every scope the list is what is missing, and
88
+ // that is what gets reported.
86
89
  if (fields.values === undefined && operation.requires.includes("values")) {
87
- const bound = row && enclosing?.contains(row) ? row : null;
88
- let holder = el;
89
- while (holder && holder !== enclosing && !hasReadableCell(holder)) {
90
- holder = holder === bound ? null : holder.parentElement;
90
+ const owner = el.closest(`form, tr, [${ATTR_KEY_VALUE}]`);
91
+ const ours = !enclosing || (owner !== enclosing && enclosing.contains(owner));
92
+ if (owner && ours) {
93
+ const { values, read } = gatherValues(owner);
94
+ if (read.length > 0) {
95
+ fields.values = values;
96
+ if (action === ACTION_ROW_INSERT)
97
+ gathered.set(request, read);
98
+ }
99
+ }
100
+ else if (enclosing) {
101
+ console.error(`lb-hub: '${action}' from <${el.localName}> is in no form, table ` +
102
+ `row or live row to gather values from, not sent; put its cells ` +
103
+ `in a <form>`, el);
104
+ return false;
91
105
  }
92
- if (holder && holder !== enclosing)
93
- fields.values = gatherValues(holder);
94
106
  }
95
107
  const missing = operation.requires.filter((f) => fields[f] === undefined);
96
108
  if (missing.length > 0) {
@@ -106,7 +118,12 @@ function dispatchAction(el) {
106
118
  el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
107
119
  }
108
120
  /**
109
- * The value a cell is currently showing, for gathering rather than landing.
121
+ * What each insert gathered, until its round trip succeeds. Keyed by the
122
+ * request itself, so a request an ancestor stops leaves nothing behind.
123
+ */
124
+ const gathered = new WeakMap();
125
+ /**
126
+ * The control a cell reads from, for gathering rather than landing.
110
127
  *
111
128
  * A cell is always either a native form control itself (a bare
112
129
  * `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)
@@ -114,34 +131,64 @@ function dispatchAction(el) {
114
131
  * one rule instead of one per widget type. The controls are the ones a value
115
132
  * lands on, so what is read back is what landed.
116
133
  */
117
- function controlValue(el) {
118
- const control = isValueControl(el)
134
+ function controlOf(el) {
135
+ return isValueControl(el)
119
136
  ? el
120
137
  : [...el.querySelectorAll("input, select, textarea")].find(isValueControl);
121
- return control?.value;
122
- }
123
- /** Whether gathering from an element would read anything, reporting nothing. */
124
- function hasReadableCell(el) {
125
- return [...el.querySelectorAll(`[${ATTR_CELL}]`)].some((cell) => controlValue(cell) !== undefined);
126
138
  }
127
139
  /**
128
- * Every lb-cell inside an element, gathered into one values map. Shared by
129
- * lb-row-insert and lb-row-update — both submit a batch of cells, and differ
130
- * only in whether a key comes with them.
140
+ * Every lb-cell of an element's scope, gathered into one values map. Shared
141
+ * by lb-row-insert and lb-row-update — both submit a batch of cells, and
142
+ * differ only in whether a key comes with them. What was read comes back
143
+ * beside the map, so an insert can reset it once it succeeds.
144
+ *
145
+ * Found the way a row lands, so a cell inside a nested scope is that scope's
146
+ * and is not read: the options of a picker in the row are the picker's rows,
147
+ * while the picker's own cell is the row's.
131
148
  */
132
149
  function gatherValues(el) {
133
150
  const values = {};
134
- for (const cell of el.querySelectorAll(`[${ATTR_CELL}]`)) {
151
+ const read = [];
152
+ for (const cell of within(el, `[${ATTR_CELL}]`)) {
135
153
  const name = cell.getAttribute(ATTR_CELL);
136
- const value = controlValue(cell);
137
- if (value === undefined) {
154
+ const control = controlOf(cell);
155
+ if (control === undefined) {
138
156
  console.warn(`lb-hub: lb-cell '${name}' has no input, select or textarea to ` +
139
157
  `read, ignoring it`, cell);
140
158
  continue;
141
159
  }
142
- values[name] = value;
160
+ values[name] = control.value;
161
+ read.push({ cell, control, value: control.value });
162
+ }
163
+ return { values, read };
164
+ }
165
+ /**
166
+ * After an insert succeeds, what it gathered goes back to its default, the
167
+ * way `form.reset()` resets a form: the row those cells held now lives in the
168
+ * list. A control showing something other than what was read holds an edit
169
+ * made during the round trip, which was not sent, and is left alone.
170
+ *
171
+ * Where a value landed, `lb-value` comes off the cell first, so a widget that
172
+ * renders from it sees the attribute go and returns to its default. The
173
+ * control is reset after, so it ends at its default whatever the widget did.
174
+ * Nothing is dispatched, as `form.reset()` fires no `change`.
175
+ */
176
+ function resetGathered(read) {
177
+ for (const { cell, control, value } of read) {
178
+ if (control.value !== value)
179
+ continue;
180
+ cell.removeAttribute(ATTR_VALUE);
181
+ control.removeAttribute(ATTR_VALUE);
182
+ if (control.localName === "select") {
183
+ for (const option of control.options) {
184
+ option.selected = option.defaultSelected;
185
+ }
186
+ }
187
+ else {
188
+ const text = control;
189
+ text.value = text.defaultValue;
190
+ }
143
191
  }
144
- return values;
145
192
  }
146
193
  /**
147
194
  * A path names a page host, and nothing finer. The bare path resolves to
@@ -236,7 +283,10 @@ class LbHub extends HTMLElement {
236
283
  origin?.removeAttribute(ATTR_ERROR);
237
284
  origin?.setAttribute(ATTR_PENDING, "");
238
285
  void fetchData(this.page, request)
239
- .then((data) => applyData(this, data))
286
+ .then((data) => {
287
+ applyData(this, data);
288
+ resetGathered(gathered.get(request) ?? []);
289
+ })
240
290
  .catch((err) => {
241
291
  console.error(`lb-hub: request failed`, err);
242
292
  origin?.setAttribute(ATTR_ERROR, "");
@@ -1 +1 @@
1
- {"version":3,"file":"lb-hub.browser.js","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ACTION_CELL_CHANGE,\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n ATTR_ACTION,\n ATTR_CELL,\n ATTR_ERROR,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_NAV_LINK,\n ATTR_PAGE,\n ATTR_PENDING,\n ATTR_ROW,\n ATTR_UNKNOWN_PAGE,\n LB_ACTIONS,\n LB_RESERVED_PREFIX,\n HUB_TAG_NAME,\n NAV_CELL_LABEL,\n NAV_CELL_URI,\n NAV_ROW,\n LB_EVENT_NAME,\n LB_ENDPOINT,\n LB_REQUEST_TIMEOUT_MS,\n} from \"../core/lb-constants.js\";\nimport type { HubData, HubRequest } from \"../core/lb-types.js\";\nimport { applyData, isValueControl } from \"./lb-apply.js\";\n\nexport { applyData, applyRow } from \"./lb-apply.js\";\n\n/** What each operation takes from scope, and what it cannot be sent without. */\nconst OPERATIONS: Record<\n string,\n { fills: readonly string[]; requires: readonly string[] }\n> = {\n [ACTION_CELL_CHANGE]: {\n fills: [\"list\", \"key\", \"cell\"],\n requires: [\"list\", \"key\", \"cell\", \"value\"],\n },\n [ACTION_ROW_INSERT]: { fills: [\"list\"], requires: [\"list\", \"values\"] },\n [ACTION_ROW_DELETE]: { fills: [\"list\", \"key\"], requires: [\"list\", \"key\"] },\n [ACTION_ROW_UPDATE]: {\n fills: [\"list\", \"key\"],\n requires: [\"list\", \"key\", \"values\"],\n },\n};\n\n/**\n * Complete a request from where its element sits — see docs/TECHREF-1.0.md,\n * \"Requests\". Every request passes through here, whoever built it, so a\n * widget states what it did and never where it is.\n *\n * The scope field is `list` or `row`, whichever attribute scoped the nearest\n * ancestor, so a declared action fired inside a single-row scope says so and\n * the wire never needs a noun for the addressable thing. A field the request\n * already carries is the dispatcher's to decide and is left as it is.\n *\n * False when the request cannot be sent, having said why.\n */\nfunction scope(request: HubRequest | undefined, el: Element): boolean {\n if (!request?.action) {\n console.warn(`lb-hub: event without an action, ignoring`, request);\n return false;\n }\n const { action } = request;\n const reserved = action.startsWith(LB_RESERVED_PREFIX);\n // The reserved prefix is the whole of the discriminant, so a reserved\n // name that is not one of the four operations is a typo rather than\n // something to forward. Refused here, where the widget can see why: a\n // server would only report that it declares no such action.\n if (reserved && !(LB_ACTIONS as readonly string[]).includes(action)) {\n console.error(\n `lb-hub: '${action}' begins with '${LB_RESERVED_PREFIX}' ` +\n `but names no operation, ignoring`,\n );\n return false;\n }\n\n const fields = request as unknown as Record<string, unknown>;\n // The element's own lb-list or lb-row is what it displays. Where it sits\n // is its ancestors', the way a control belongs to the form around it.\n const parent = el.parentElement;\n const enclosing = parent?.closest(`[${ATTR_LIST}], [${ATTR_ROW}]`);\n const row = parent?.closest(`[${ATTR_KEY_VALUE}]`);\n const inScope: Record<string, string | null | undefined> = {\n list: enclosing?.getAttribute(ATTR_LIST),\n row: enclosing?.hasAttribute(ATTR_LIST)\n ? null\n : enclosing?.getAttribute(ATTR_ROW),\n // Only a row of that scope: a key from outside it names a row of some\n // other list.\n key:\n row && enclosing?.contains(row) ? row.getAttribute(ATTR_KEY_VALUE) : null,\n cell: el.getAttribute(ATTR_CELL),\n };\n const fill = (name: string) => {\n const value = inScope[name];\n if (fields[name] === undefined && value !== null && value !== undefined) {\n fields[name] = value;\n }\n };\n\n const operation = OPERATIONS[action];\n if (!operation) {\n // A declared action names one scope or the other, never both.\n if (fields.list === undefined && fields.row === undefined) {\n fill(\"list\");\n fill(\"row\");\n }\n fill(\"key\");\n fill(\"cell\");\n return true;\n }\n\n for (const name of operation.fills) fill(name);\n // Gathered from the nearest element holding a cell to read, starting at\n // whatever dispatched: a form holds its own, and a button in a new row's\n // <tr> reads the row around it. Never past the live row the element sits\n // in, nor out into the scope, whose cells are other rows'. Nothing to read\n // is nothing to send, so values stays missing.\n if (fields.values === undefined && operation.requires.includes(\"values\")) {\n const bound = row && enclosing?.contains(row) ? row : null;\n let holder: Element | null = el;\n while (holder && holder !== enclosing && !hasReadableCell(holder)) {\n holder = holder === bound ? null : holder.parentElement;\n }\n if (holder && holder !== enclosing) fields.values = gatherValues(holder);\n }\n const missing = operation.requires.filter((f) => fields[f] === undefined);\n if (missing.length > 0) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> has no ` +\n `${missing.join(\", \")} in scope, not sent`,\n el,\n );\n return false;\n }\n return true;\n}\n\n/** Dispatch the bare action an element carries, for the hub to scope. */\nfunction dispatchAction(el: Element): void {\n const detail = { action: el.getAttribute(ATTR_ACTION)! } as HubRequest;\n el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));\n}\n\n/**\n * The value a cell is currently showing, for gathering rather than landing.\n *\n * A cell is always either a native form control itself (a bare\n * `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)\n * — that wrapping is structural, not incidental, which is what lets this be\n * one rule instead of one per widget type. The controls are the ones a value\n * lands on, so what is read back is what landed.\n */\nfunction controlValue(el: Element): string | undefined {\n const control = isValueControl(el)\n ? el\n : [...el.querySelectorAll(\"input, select, textarea\")].find(isValueControl);\n return control?.value;\n}\n\n/** Whether gathering from an element would read anything, reporting nothing. */\nfunction hasReadableCell(el: Element): boolean {\n return [...el.querySelectorAll(`[${ATTR_CELL}]`)].some(\n (cell) => controlValue(cell) !== undefined,\n );\n}\n\n/**\n * Every lb-cell inside an element, gathered into one values map. Shared by\n * lb-row-insert and lb-row-update — both submit a batch of cells, and differ\n * only in whether a key comes with them.\n */\nfunction gatherValues(el: Element): Record<string, string> {\n const values: Record<string, string> = {};\n for (const cell of el.querySelectorAll(`[${ATTR_CELL}]`)) {\n const name = cell.getAttribute(ATTR_CELL)!;\n const value = controlValue(cell);\n if (value === undefined) {\n console.warn(\n `lb-hub: lb-cell '${name}' has no input, select or textarea to ` +\n `read, ignoring it`,\n cell,\n );\n continue;\n }\n values[name] = value;\n }\n return values;\n}\n\n/**\n * A path names a page host, and nothing finer. The bare path resolves to\n * `index`, the same convention a static file server follows — an\n * application names its landing page that way rather than the hub singling\n * one out.\n */\nfunction pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/**\n * The `<template>` a page's markup ships in. The builder guarantees one per\n * page name (build/locations.ts refuses two page files sharing a stub), so\n * a second match here means the document was assembled by something else,\n * and is worth saying out loud rather than resolving by document order.\n */\nfunction pageHostFor(page: string): HTMLTemplateElement | null {\n const hosts: HTMLTemplateElement[] = [];\n for (const el of document.querySelectorAll(`template[${ATTR_PAGE}]`)) {\n if (el.getAttribute(ATTR_PAGE) === page) {\n hosts.push(el as HTMLTemplateElement);\n }\n }\n if (hosts.length === 0) {\n console.error(`lb-hub: no page host for '${page}'`);\n return null;\n }\n if (hosts.length > 1) {\n console.error(\n `lb-hub: ${hosts.length} page hosts carry ${ATTR_PAGE}=\"${page}\", ` +\n `using the first`,\n );\n }\n return hosts[0]!;\n}\n\n/**\n * Fetch one round trip against the client-side deadline. Throws — rather\n * than swallowing — on a non-ok response, a network failure, or a timeout,\n * so a caller that needs to know the request failed (as opposed to one that\n * only ever wants the freshest data, like navigate()) can tell the two\n * apart from a genuinely empty result.\n */\nasync function fetchData(page: string, request?: HubRequest): Promise<HubData> {\n const url = `${LB_ENDPOINT}?page=${encodeURIComponent(page)}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);\n try {\n // An empty body is how the hub asks for the page's whole query set.\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request ?? {}),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`lb-hub: server responded ${res.status} for ${url}`);\n }\n return (await res.json()) as HubData;\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * The hub is a singleton outside <main>. It survives every page change,\n * so it needs no id to disambiguate it.\n */\nclass LbHub extends HTMLElement {\n private main!: HTMLElement;\n\n /**\n * The page currently in <main>. A page declares its own requests, so a request\n * has to say which page it came from. The page rides on the URL rather\n * than in the request, leaving the operation set closed.\n */\n private page = \"\";\n\n connectedCallback() {\n this.main = this.querySelector(\"main\")!;\n\n // Scoped in the capture phase, on the way down, so every ancestor that\n // sees the request on its way back up sees it whole. A request that\n // cannot be sent is stopped here, before anything sees it at all.\n this.addEventListener(\n LB_EVENT_NAME,\n (e: Event) => {\n const origin = e.target as Element;\n const request = (e as CustomEvent<HubRequest>).detail;\n if (!scope(request, origin)) e.stopPropagation();\n },\n { capture: true },\n );\n\n this.addEventListener(LB_EVENT_NAME, (e: Event) => {\n const request = (e as CustomEvent<HubRequest>).detail;\n // The element that dispatched the request, not e.currentTarget (the\n // hub itself, since the event bubbles) — pending/error state belongs\n // at the origin so a widget can watch its own attributes.\n const origin = e.target as Element | null;\n origin?.removeAttribute(ATTR_ERROR);\n origin?.setAttribute(ATTR_PENDING, \"\");\n void fetchData(this.page, request)\n .then((data) => applyData(this, data))\n .catch((err) => {\n console.error(`lb-hub: request failed`, err);\n origin?.setAttribute(ATTR_ERROR, \"\");\n })\n .finally(() => origin?.removeAttribute(ATTR_PENDING));\n });\n\n /**\n * A native element carrying an action. The hub dispatches the bare action\n * from that element as the ordinary bubbling event, rather than sending\n * it directly, so it is scoped exactly as a widget's is and an ancestor\n * widget can still stop it and confirm. A native action button and a\n * hand-written widget therefore produce identical events.\n *\n * A widget carrying an action is left alone — it owns its own interaction\n * and decides what counts as performing it, which for a <select> is a\n * change rather than a click. The test is the same lexical one applyData\n * uses: a hyphen in the tag name.\n */\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const el = (e.target as Element | null)?.closest(`[${ATTR_ACTION}]`);\n if (!el || el.localName.includes(\"-\")) return;\n // A form performs its action on submit, whatever was clicked in it.\n if (el instanceof HTMLFormElement) return;\n // A button beside the cells is pressed once they are done. An element\n // holding them has no such moment: a click into one of its own inputs\n // would send the row.\n const action = el.getAttribute(ATTR_ACTION);\n if (\n (action === ACTION_ROW_INSERT || action === ACTION_ROW_UPDATE) &&\n el.querySelector(`[${ATTR_CELL}]`)\n ) {\n console.error(\n `lb-hub: '${action}' on <${el.localName}> holds its own cells, ` +\n `so a click inside it is not sent; put it on a <form> or a button`,\n el,\n );\n return;\n }\n e.preventDefault();\n dispatchAction(el);\n });\n\n // The same delegation for the event a form owns.\n this.addEventListener(\"submit\", (e: SubmitEvent) => {\n const form = e.target as Element | null;\n if (!form || !form.hasAttribute(ATTR_ACTION)) return;\n e.preventDefault();\n dispatchAction(form);\n });\n\n // Only anchors that opted in. Everything else is a real link.\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const link = (e.target as Element | null)?.closest(`a[${ATTR_NAV_LINK}]`);\n if (!link) return;\n e.preventDefault();\n const path = new URL((link as HTMLAnchorElement).href).pathname;\n history.pushState(null, \"\", path);\n void this.navigate(path);\n });\n\n addEventListener(\"popstate\", () => void this.navigate(location.pathname));\n\n void this.navigate(location.pathname);\n }\n\n /**\n * Navigation is the one operation that replaces host DOM. The host is\n * already in the document, so selecting a page is getElementById() and\n * there is no fetching mechanism on the host channel.\n */\n private async navigate(path: string): Promise<void> {\n const page = pageNameFor(path);\n this.landNavigation(path, page);\n const template = pageHostFor(page);\n if (!template) {\n this.reportUnknownPage();\n this.reveal();\n return;\n }\n this.page = page;\n // Insert the page shell before fetching its data, not after: the\n // template is already in the document, so holding it back behind a\n // network round trip only leaves <main> empty for no reason.\n this.main.replaceChildren(template.content.cloneNode(true));\n this.reveal();\n let data: HubData = {};\n try {\n data = await fetchData(page);\n } catch (err) {\n console.error(`lb-hub: failed to load data for '${page}'`, err);\n }\n applyData(this, data);\n }\n\n /**\n * An app that ships `<body hidden>` (see docs/reference/chrome.md) is\n * asking to stay invisible until there is something coherent to show,\n * rather than flash the chrome before `<main>` has real content. This is\n * a no-op for an app that doesn't use that convention.\n */\n private reveal(): void {\n document.body.hidden = false;\n }\n\n /**\n * The hub's own row: where the page is, in the vocabulary a server\n * answer arrives in, so a chrome displays it the way it displays anything\n * — `lb-row=\"lb-navigation\"` on a subtree, `lb-cell` on what shows a\n * value. Landed before the page host is looked up, so a miss has it too;\n * the unknown-page dialog is one consumer among whatever others the\n * chrome writes.\n *\n * The label is the nav's: the text of the first lb-nav-link anchor whose\n * href names this page. Taken from the nav rather than from a click, so a\n * click, a reload and the back button all land the same label — and a\n * path no anchor names lands an empty one, explicitly, so a consumer is\n * never left showing the previous page's.\n *\n * Landed only where a subtree names it. applyData() warns about a query\n * with no scope, and a chrome that displays no navigation has nothing to\n * be warned about.\n */\n private landNavigation(path: string, page: string): void {\n if (!this.querySelector(`[${ATTR_ROW}=\"${NAV_ROW}\"]`)) return;\n let label = \"\";\n for (const a of this.querySelectorAll<HTMLAnchorElement>(\n `a[${ATTR_NAV_LINK}]`,\n )) {\n if (pageNameFor(new URL(a.href).pathname) !== page) continue;\n label = a.textContent?.trim() ?? \"\";\n break;\n }\n applyData(this, {\n [NAV_ROW]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },\n });\n }\n\n /**\n * A path that resolves to no page host is discovered client-side, after a\n * successful 200 — every route gets the same document, so there is no\n * HTTP 404 to report (see docs/tutorials/010-pages-and-navigation.md).\n * lb-unknown-page is optional: an application that declares none gets\n * nothing beyond navigate()'s console.error. The builder guarantees the\n * element it's on is a <dialog> (build/assemble.ts), so this can call\n * showModal() without checking the tag here.\n *\n * Only opens it. What the dialog says it gets from landNavigation(),\n * already run, by naming the hub's row in lb-row like any subtree.\n *\n * Scoped to the hub, like every other lookup here, so that one rule holds\n * without exception: what the hub acts on is inside the hub.\n */\n private reportUnknownPage(): void {\n const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!dialog) return;\n const modal = dialog as HTMLDialogElement;\n if (!modal.open) modal.showModal();\n }\n}\n\ncustomElements.define(HUB_TAG_NAME, LbHub);\n"]}
1
+ {"version":3,"file":"lb-hub.browser.js","sourceRoot":"","sources":["../../hub/lb-hub.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,UAAU,EACV,cAAc,EACd,SAAS,EACT,aAAa,EACb,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,OAAO,EACP,aAAa,EACb,WAAW,EACX,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEpD,gFAAgF;AAChF,MAAM,UAAU,GAGZ;IACF,CAAC,kBAAkB,CAAC,EAAE;QACpB,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC;QAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC;KAC3C;IACD,CAAC,iBAAiB,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;IACtE,CAAC,iBAAiB,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;IAC1E,CAAC,iBAAiB,CAAC,EAAE;QACnB,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC;QACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC;KACpC;CACF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,SAAS,KAAK,CAAC,OAA+B,EAAE,EAAW;IACzD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,2CAA2C,EAAE,OAAO,CAAC,CAAC;QACnE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3B,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;IACvD,sEAAsE;IACtE,oEAAoE;IACpE,sEAAsE;IACtE,4DAA4D;IAC5D,IAAI,QAAQ,IAAI,CAAE,UAAgC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACpE,OAAO,CAAC,KAAK,CACX,YAAY,MAAM,kBAAkB,kBAAkB,IAAI;YACxD,kCAAkC,CACrC,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,MAAM,GAAG,OAA6C,CAAC;IAC7D,yEAAyE;IACzE,sEAAsE;IACtE,MAAM,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC;IAChC,MAAM,SAAS,GAAG,MAAM,EAAE,OAAO,CAAC,IAAI,SAAS,OAAO,QAAQ,GAAG,CAAC,CAAC;IACnE,MAAM,GAAG,GAAG,MAAM,EAAE,OAAO,CAAC,IAAI,cAAc,GAAG,CAAC,CAAC;IACnD,MAAM,OAAO,GAA8C;QACzD,IAAI,EAAE,SAAS,EAAE,YAAY,CAAC,SAAS,CAAC;QACxC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,SAAS,CAAC;YACrC,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,QAAQ,CAAC;QACrC,sEAAsE;QACtE,cAAc;QACd,GAAG,EACD,GAAG,IAAI,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI;QAC3E,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC;KACjC,CAAC;IACF,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IACrC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,8DAA8D;QAC9D,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;YAC1D,IAAI,CAAC,MAAM,CAAC,CAAC;YACb,IAAI,CAAC,KAAK,CAAC,CAAC;QACd,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,CAAC;QACZ,IAAI,CAAC,MAAM,CAAC,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/C,oEAAoE;IACpE,yEAAyE;IACzE,uEAAuE;IACvE,wEAAwE;IACxE,0EAA0E;IAC1E,4EAA4E;IAC5E,wEAAwE;IACxE,8BAA8B;IAC9B,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzE,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,cAAc,cAAc,GAAG,CAAC,CAAC;QAC1D,MAAM,IAAI,GACR,CAAC,SAAS,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QACnE,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;YAClB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;YAC7C,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;gBACvB,IAAI,MAAM,KAAK,iBAAiB;oBAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAChE,CAAC;QACH,CAAC;aAAM,IAAI,SAAS,EAAE,CAAC;YACrB,OAAO,CAAC,KAAK,CACX,YAAY,MAAM,WAAW,EAAE,CAAC,SAAS,yBAAyB;gBAChE,iEAAiE;gBACjE,aAAa,EACf,EAAE,CACH,CAAC;YACF,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;IAC1E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CACX,YAAY,MAAM,WAAW,EAAE,CAAC,SAAS,WAAW;YAClD,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAC5C,EAAE,CACH,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,yEAAyE;AACzE,SAAS,cAAc,CAAC,EAAW;IACjC,MAAM,MAAM,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,YAAY,CAAC,WAAW,CAAE,EAAgB,CAAC;IACvE,EAAE,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;AAC9E,CAAC;AAYD;;;GAGG;AACH,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAsB,CAAC;AAEnD;;;;;;;;GAQG;AACH,SAAS,SAAS,CAAC,EAAW;IAC5B,OAAO,cAAc,CAAC,EAAE,CAAC;QACvB,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,YAAY,CAAC,EAAW;IAI/B,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,IAAI,GAAW,EAAE,CAAC;IACxB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,EAAE,EAAE,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,SAAS,CAAE,CAAC;QAC3C,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,IAAI,CACV,oBAAoB,IAAI,wCAAwC;gBAC9D,mBAAmB,EACrB,IAAI,CACL,CAAC;YACF,SAAS;QACX,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,aAAa,CAAC,IAAY;IACjC,KAAK,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC;QAC5C,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK;YAAE,SAAS;QACtC,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;QACjC,OAAO,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;QACpC,IAAI,OAAO,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YACnC,KAAK,MAAM,MAAM,IAAK,OAA6B,CAAC,OAAO,EAAE,CAAC;gBAC5D,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,eAAe,CAAC;YAC3C,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,OAAiD,CAAC;YAC/D,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC;QACjC,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAC5C,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACtC,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,KAAK,GAA0B,EAAE,CAAC;IACxC,KAAK,MAAM,EAAE,IAAI,QAAQ,CAAC,gBAAgB,CAAC,YAAY,SAAS,GAAG,CAAC,EAAE,CAAC;QACrE,IAAI,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,EAAyB,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CAAC,6BAA6B,IAAI,GAAG,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,KAAK,CACX,WAAW,KAAK,CAAC,MAAM,qBAAqB,SAAS,KAAK,IAAI,KAAK;YACjE,iBAAiB,CACpB,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,CAAE,CAAC;AACnB,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,OAAoB;IACzD,MAAM,GAAG,GAAG,GAAG,WAAW,SAAS,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;IAC9D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,qBAAqB,CAAC,CAAC;IAC5E,IAAI,CAAC;QACH,oEAAoE;QACpE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,CAAC;YACnC,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,4BAA4B,GAAG,CAAC,MAAM,QAAQ,GAAG,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAY,CAAC;IACvC,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,KAAM,SAAQ,WAAW;IACrB,IAAI,CAAe;IAE3B;;;;OAIG;IACK,IAAI,GAAG,EAAE,CAAC;IAElB,iBAAiB;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAE,CAAC;QAExC,uEAAuE;QACvE,oEAAoE;QACpE,kEAAkE;QAClE,IAAI,CAAC,gBAAgB,CACnB,aAAa,EACb,CAAC,CAAQ,EAAE,EAAE;YACX,MAAM,MAAM,GAAG,CAAC,CAAC,MAAiB,CAAC;YACnC,MAAM,OAAO,GAAI,CAA6B,CAAC,MAAM,CAAC;YACtD,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC;gBAAE,CAAC,CAAC,eAAe,EAAE,CAAC;QACnD,CAAC,EACD,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAC;QAEF,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,CAAC,CAAQ,EAAE,EAAE;YAChD,MAAM,OAAO,GAAI,CAA6B,CAAC,MAAM,CAAC;YACtD,oEAAoE;YACpE,qEAAqE;YACrE,0DAA0D;YAC1D,MAAM,MAAM,GAAG,CAAC,CAAC,MAAwB,CAAC;YAC1C,MAAM,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;YACpC,MAAM,EAAE,YAAY,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;YACvC,KAAK,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC;iBAC/B,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;gBACb,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBACtB,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YAC7C,CAAC,CAAC;iBACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACb,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;gBAC7C,MAAM,EAAE,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YACvC,CAAC,CAAC;iBACD,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;QAEH;;;;;;;;;;;WAWG;QACH,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAa,EAAE,EAAE;YAC/C,MAAM,EAAE,GAAI,CAAC,CAAC,MAAyB,EAAE,OAAO,CAAC,IAAI,WAAW,GAAG,CAAC,CAAC;YACrE,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,OAAO;YAC9C,oEAAoE;YACpE,IAAI,EAAE,YAAY,eAAe;gBAAE,OAAO;YAC1C,sEAAsE;YACtE,sEAAsE;YACtE,sBAAsB;YACtB,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;YAC5C,IACE,CAAC,MAAM,KAAK,iBAAiB,IAAI,MAAM,KAAK,iBAAiB,CAAC;gBAC9D,EAAE,CAAC,aAAa,CAAC,IAAI,SAAS,GAAG,CAAC,EAClC,CAAC;gBACD,OAAO,CAAC,KAAK,CACX,YAAY,MAAM,SAAS,EAAE,CAAC,SAAS,yBAAyB;oBAC9D,kEAAkE,EACpE,EAAE,CACH,CAAC;gBACF,OAAO;YACT,CAAC;YACD,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,cAAc,CAAC,EAAE,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;QAEH,iDAAiD;QACjD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAc,EAAE,EAAE;YACjD,MAAM,IAAI,GAAG,CAAC,CAAC,MAAwB,CAAC;YACxC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC;gBAAE,OAAO;YACrD,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,cAAc,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,8DAA8D;QAC9D,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAa,EAAE,EAAE;YAC/C,MAAM,IAAI,GAAI,CAAC,CAAC,MAAyB,EAAE,OAAO,CAAC,KAAK,aAAa,GAAG,CAAC,CAAC;YAC1E,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,IAAI,GAAG,CAAE,IAA0B,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;YAChE,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;YAClC,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;QAEH,gBAAgB,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;QAE1E,KAAK,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,QAAQ,CAAC,IAAY;QACjC,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAChC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,iEAAiE;QACjE,mEAAmE;QACnE,6DAA6D;QAC7D,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5D,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,IAAI,GAAY,EAAE,CAAC;QACvB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,oCAAoC,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC;QAClE,CAAC;QACD,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACxB,CAAC;IAED;;;;;OAKG;IACK,MAAM;QACZ,QAAQ,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IAC/B,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACK,cAAc,CAAC,IAAY,EAAE,IAAY;QAC/C,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,QAAQ,KAAK,OAAO,IAAI,CAAC;YAAE,OAAO;QAC9D,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,gBAAgB,CACnC,KAAK,aAAa,GAAG,CACtB,EAAE,CAAC;YACF,IAAI,WAAW,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,IAAI;gBAAE,SAAS;YAC7D,KAAK,GAAG,CAAC,CAAC,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YACpC,MAAM;QACR,CAAC;QACD,SAAS,CAAC,IAAI,EAAE;YACd,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE;SAC7D,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACK,iBAAiB;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,iBAAiB,GAAG,CAAC,CAAC;QAC5D,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,KAAK,GAAG,MAA2B,CAAC;QAC1C,IAAI,CAAC,KAAK,CAAC,IAAI;YAAE,KAAK,CAAC,SAAS,EAAE,CAAC;IACrC,CAAC;CACF;AAED,cAAc,CAAC,MAAM,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ACTION_CELL_CHANGE,\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n ATTR_ACTION,\n ATTR_CELL,\n ATTR_ERROR,\n ATTR_KEY_VALUE,\n ATTR_LIST,\n ATTR_NAV_LINK,\n ATTR_PAGE,\n ATTR_PENDING,\n ATTR_ROW,\n ATTR_UNKNOWN_PAGE,\n ATTR_VALUE,\n LB_ACTIONS,\n LB_RESERVED_PREFIX,\n HUB_TAG_NAME,\n NAV_CELL_LABEL,\n NAV_CELL_URI,\n NAV_ROW,\n LB_EVENT_NAME,\n LB_ENDPOINT,\n LB_REQUEST_TIMEOUT_MS,\n} from \"../core/lb-constants.js\";\nimport type { HubData, HubRequest } from \"../core/lb-types.js\";\nimport { applyData, isValueControl, within } from \"./lb-apply.js\";\n\nexport { applyData, applyRow } from \"./lb-apply.js\";\n\n/** What each operation takes from scope, and what it cannot be sent without. */\nconst OPERATIONS: Record<\n string,\n { fills: readonly string[]; requires: readonly string[] }\n> = {\n [ACTION_CELL_CHANGE]: {\n fills: [\"list\", \"key\", \"cell\"],\n requires: [\"list\", \"key\", \"cell\", \"value\"],\n },\n [ACTION_ROW_INSERT]: { fills: [\"list\"], requires: [\"list\", \"values\"] },\n [ACTION_ROW_DELETE]: { fills: [\"list\", \"key\"], requires: [\"list\", \"key\"] },\n [ACTION_ROW_UPDATE]: {\n fills: [\"list\", \"key\"],\n requires: [\"list\", \"key\", \"values\"],\n },\n};\n\n/**\n * Complete a request from where its element sits — see docs/TECHREF-1.0.md,\n * \"Requests\". Every request passes through here, whoever built it, so a\n * widget states what it did and never where it is.\n *\n * The scope field is `list` or `row`, whichever attribute scoped the nearest\n * ancestor, so a declared action fired inside a single-row scope says so and\n * the wire never needs a noun for the addressable thing. A field the request\n * already carries is the dispatcher's to decide and is left as it is.\n *\n * False when the request cannot be sent, having said why.\n */\nfunction scope(request: HubRequest | undefined, el: Element): boolean {\n if (!request?.action) {\n console.warn(`lb-hub: event without an action, ignoring`, request);\n return false;\n }\n const { action } = request;\n const reserved = action.startsWith(LB_RESERVED_PREFIX);\n // The reserved prefix is the whole of the discriminant, so a reserved\n // name that is not one of the four operations is a typo rather than\n // something to forward. Refused here, where the widget can see why: a\n // server would only report that it declares no such action.\n if (reserved && !(LB_ACTIONS as readonly string[]).includes(action)) {\n console.error(\n `lb-hub: '${action}' begins with '${LB_RESERVED_PREFIX}' ` +\n `but names no operation, ignoring`,\n );\n return false;\n }\n\n const fields = request as unknown as Record<string, unknown>;\n // The element's own lb-list or lb-row is what it displays. Where it sits\n // is its ancestors', the way a control belongs to the form around it.\n const parent = el.parentElement;\n const enclosing = parent?.closest(`[${ATTR_LIST}], [${ATTR_ROW}]`);\n const row = parent?.closest(`[${ATTR_KEY_VALUE}]`);\n const inScope: Record<string, string | null | undefined> = {\n list: enclosing?.getAttribute(ATTR_LIST),\n row: enclosing?.hasAttribute(ATTR_LIST)\n ? null\n : enclosing?.getAttribute(ATTR_ROW),\n // Only a row of that scope: a key from outside it names a row of some\n // other list.\n key:\n row && enclosing?.contains(row) ? row.getAttribute(ATTR_KEY_VALUE) : null,\n cell: el.getAttribute(ATTR_CELL),\n };\n const fill = (name: string) => {\n const value = inScope[name];\n if (fields[name] === undefined && value !== null && value !== undefined) {\n fields[name] = value;\n }\n };\n\n const operation = OPERATIONS[action];\n if (!operation) {\n // A declared action names one scope or the other, never both.\n if (fields.list === undefined && fields.row === undefined) {\n fill(\"list\");\n fill(\"row\");\n }\n fill(\"key\");\n fill(\"cell\");\n return true;\n }\n\n for (const name of operation.fills) fill(name);\n // Gathered from the record the element belongs to, the way a button\n // submits its form owner: the nearest form, table row or live row around\n // it, the element itself included. A <tr> counts because it is the one\n // place a form cannot go around a row's controls. What else sits beside\n // the element never decides it. The record is the scope's, so it is never\n // the scope itself nor outside it; with none, there is nothing to say which\n // cells are meant. Outside every scope the list is what is missing, and\n // that is what gets reported.\n if (fields.values === undefined && operation.requires.includes(\"values\")) {\n const owner = el.closest(`form, tr, [${ATTR_KEY_VALUE}]`);\n const ours =\n !enclosing || (owner !== enclosing && enclosing.contains(owner));\n if (owner && ours) {\n const { values, read } = gatherValues(owner);\n if (read.length > 0) {\n fields.values = values;\n if (action === ACTION_ROW_INSERT) gathered.set(request, read);\n }\n } else if (enclosing) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> is in no form, table ` +\n `row or live row to gather values from, not sent; put its cells ` +\n `in a <form>`,\n el,\n );\n return false;\n }\n }\n const missing = operation.requires.filter((f) => fields[f] === undefined);\n if (missing.length > 0) {\n console.error(\n `lb-hub: '${action}' from <${el.localName}> has no ` +\n `${missing.join(\", \")} in scope, not sent`,\n el,\n );\n return false;\n }\n return true;\n}\n\n/** Dispatch the bare action an element carries, for the hub to scope. */\nfunction dispatchAction(el: Element): void {\n const detail = { action: el.getAttribute(ATTR_ACTION)! } as HubRequest;\n el.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));\n}\n\n/** A form control whose state is its `value`. */\ntype ValueControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/** One cell a request read, the control it read, and the value it read. */\ninterface Read {\n cell: Element;\n control: ValueControl;\n value: string;\n}\n\n/**\n * What each insert gathered, until its round trip succeeds. Keyed by the\n * request itself, so a request an ancestor stops leaves nothing behind.\n */\nconst gathered = new WeakMap<HubRequest, Read[]>();\n\n/**\n * The control a cell reads from, for gathering rather than landing.\n *\n * A cell is always either a native form control itself (a bare\n * `<input lb-cell>`) or a widget that wraps one (`<lb-input>`, `<lb-select>`)\n * — that wrapping is structural, not incidental, which is what lets this be\n * one rule instead of one per widget type. The controls are the ones a value\n * lands on, so what is read back is what landed.\n */\nfunction controlOf(el: Element): ValueControl | undefined {\n return isValueControl(el)\n ? el\n : [...el.querySelectorAll(\"input, select, textarea\")].find(isValueControl);\n}\n\n/**\n * Every lb-cell of an element's scope, gathered into one values map. Shared\n * by lb-row-insert and lb-row-update — both submit a batch of cells, and\n * differ only in whether a key comes with them. What was read comes back\n * beside the map, so an insert can reset it once it succeeds.\n *\n * Found the way a row lands, so a cell inside a nested scope is that scope's\n * and is not read: the options of a picker in the row are the picker's rows,\n * while the picker's own cell is the row's.\n */\nfunction gatherValues(el: Element): {\n values: Record<string, string>;\n read: Read[];\n} {\n const values: Record<string, string> = {};\n const read: Read[] = [];\n for (const cell of within(el, `[${ATTR_CELL}]`)) {\n const name = cell.getAttribute(ATTR_CELL)!;\n const control = controlOf(cell);\n if (control === undefined) {\n console.warn(\n `lb-hub: lb-cell '${name}' has no input, select or textarea to ` +\n `read, ignoring it`,\n cell,\n );\n continue;\n }\n values[name] = control.value;\n read.push({ cell, control, value: control.value });\n }\n return { values, read };\n}\n\n/**\n * After an insert succeeds, what it gathered goes back to its default, the\n * way `form.reset()` resets a form: the row those cells held now lives in the\n * list. A control showing something other than what was read holds an edit\n * made during the round trip, which was not sent, and is left alone.\n *\n * Where a value landed, `lb-value` comes off the cell first, so a widget that\n * renders from it sees the attribute go and returns to its default. The\n * control is reset after, so it ends at its default whatever the widget did.\n * Nothing is dispatched, as `form.reset()` fires no `change`.\n */\nfunction resetGathered(read: Read[]): void {\n for (const { cell, control, value } of read) {\n if (control.value !== value) continue;\n cell.removeAttribute(ATTR_VALUE);\n control.removeAttribute(ATTR_VALUE);\n if (control.localName === \"select\") {\n for (const option of (control as HTMLSelectElement).options) {\n option.selected = option.defaultSelected;\n }\n } else {\n const text = control as HTMLInputElement | HTMLTextAreaElement;\n text.value = text.defaultValue;\n }\n }\n}\n\n/**\n * A path names a page host, and nothing finer. The bare path resolves to\n * `index`, the same convention a static file server follows — an\n * application names its landing page that way rather than the hub singling\n * one out.\n */\nfunction pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/**\n * The `<template>` a page's markup ships in. The builder guarantees one per\n * page name (build/locations.ts refuses two page files sharing a stub), so\n * a second match here means the document was assembled by something else,\n * and is worth saying out loud rather than resolving by document order.\n */\nfunction pageHostFor(page: string): HTMLTemplateElement | null {\n const hosts: HTMLTemplateElement[] = [];\n for (const el of document.querySelectorAll(`template[${ATTR_PAGE}]`)) {\n if (el.getAttribute(ATTR_PAGE) === page) {\n hosts.push(el as HTMLTemplateElement);\n }\n }\n if (hosts.length === 0) {\n console.error(`lb-hub: no page host for '${page}'`);\n return null;\n }\n if (hosts.length > 1) {\n console.error(\n `lb-hub: ${hosts.length} page hosts carry ${ATTR_PAGE}=\"${page}\", ` +\n `using the first`,\n );\n }\n return hosts[0]!;\n}\n\n/**\n * Fetch one round trip against the client-side deadline. Throws — rather\n * than swallowing — on a non-ok response, a network failure, or a timeout,\n * so a caller that needs to know the request failed (as opposed to one that\n * only ever wants the freshest data, like navigate()) can tell the two\n * apart from a genuinely empty result.\n */\nasync function fetchData(page: string, request?: HubRequest): Promise<HubData> {\n const url = `${LB_ENDPOINT}?page=${encodeURIComponent(page)}`;\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);\n try {\n // An empty body is how the hub asks for the page's whole query set.\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request ?? {}),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`lb-hub: server responded ${res.status} for ${url}`);\n }\n return (await res.json()) as HubData;\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * The hub is a singleton outside <main>. It survives every page change,\n * so it needs no id to disambiguate it.\n */\nclass LbHub extends HTMLElement {\n private main!: HTMLElement;\n\n /**\n * The page currently in <main>. A page declares its own requests, so a request\n * has to say which page it came from. The page rides on the URL rather\n * than in the request, leaving the operation set closed.\n */\n private page = \"\";\n\n connectedCallback() {\n this.main = this.querySelector(\"main\")!;\n\n // Scoped in the capture phase, on the way down, so every ancestor that\n // sees the request on its way back up sees it whole. A request that\n // cannot be sent is stopped here, before anything sees it at all.\n this.addEventListener(\n LB_EVENT_NAME,\n (e: Event) => {\n const origin = e.target as Element;\n const request = (e as CustomEvent<HubRequest>).detail;\n if (!scope(request, origin)) e.stopPropagation();\n },\n { capture: true },\n );\n\n this.addEventListener(LB_EVENT_NAME, (e: Event) => {\n const request = (e as CustomEvent<HubRequest>).detail;\n // The element that dispatched the request, not e.currentTarget (the\n // hub itself, since the event bubbles) — pending/error state belongs\n // at the origin so a widget can watch its own attributes.\n const origin = e.target as Element | null;\n origin?.removeAttribute(ATTR_ERROR);\n origin?.setAttribute(ATTR_PENDING, \"\");\n void fetchData(this.page, request)\n .then((data) => {\n applyData(this, data);\n resetGathered(gathered.get(request) ?? []);\n })\n .catch((err) => {\n console.error(`lb-hub: request failed`, err);\n origin?.setAttribute(ATTR_ERROR, \"\");\n })\n .finally(() => origin?.removeAttribute(ATTR_PENDING));\n });\n\n /**\n * A native element carrying an action. The hub dispatches the bare action\n * from that element as the ordinary bubbling event, rather than sending\n * it directly, so it is scoped exactly as a widget's is and an ancestor\n * widget can still stop it and confirm. A native action button and a\n * hand-written widget therefore produce identical events.\n *\n * A widget carrying an action is left alone — it owns its own interaction\n * and decides what counts as performing it, which for a <select> is a\n * change rather than a click. The test is the same lexical one applyData\n * uses: a hyphen in the tag name.\n */\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const el = (e.target as Element | null)?.closest(`[${ATTR_ACTION}]`);\n if (!el || el.localName.includes(\"-\")) return;\n // A form performs its action on submit, whatever was clicked in it.\n if (el instanceof HTMLFormElement) return;\n // A button beside the cells is pressed once they are done. An element\n // holding them has no such moment: a click into one of its own inputs\n // would send the row.\n const action = el.getAttribute(ATTR_ACTION);\n if (\n (action === ACTION_ROW_INSERT || action === ACTION_ROW_UPDATE) &&\n el.querySelector(`[${ATTR_CELL}]`)\n ) {\n console.error(\n `lb-hub: '${action}' on <${el.localName}> holds its own cells, ` +\n `so a click inside it is not sent; put it on a <form> or a button`,\n el,\n );\n return;\n }\n e.preventDefault();\n dispatchAction(el);\n });\n\n // The same delegation for the event a form owns.\n this.addEventListener(\"submit\", (e: SubmitEvent) => {\n const form = e.target as Element | null;\n if (!form || !form.hasAttribute(ATTR_ACTION)) return;\n e.preventDefault();\n dispatchAction(form);\n });\n\n // Only anchors that opted in. Everything else is a real link.\n this.addEventListener(\"click\", (e: MouseEvent) => {\n const link = (e.target as Element | null)?.closest(`a[${ATTR_NAV_LINK}]`);\n if (!link) return;\n e.preventDefault();\n const path = new URL((link as HTMLAnchorElement).href).pathname;\n history.pushState(null, \"\", path);\n void this.navigate(path);\n });\n\n addEventListener(\"popstate\", () => void this.navigate(location.pathname));\n\n void this.navigate(location.pathname);\n }\n\n /**\n * Navigation is the one operation that replaces host DOM. The host is\n * already in the document, so selecting a page is getElementById() and\n * there is no fetching mechanism on the host channel.\n */\n private async navigate(path: string): Promise<void> {\n const page = pageNameFor(path);\n this.landNavigation(path, page);\n const template = pageHostFor(page);\n if (!template) {\n this.reportUnknownPage();\n this.reveal();\n return;\n }\n this.page = page;\n // Insert the page shell before fetching its data, not after: the\n // template is already in the document, so holding it back behind a\n // network round trip only leaves <main> empty for no reason.\n this.main.replaceChildren(template.content.cloneNode(true));\n this.reveal();\n let data: HubData = {};\n try {\n data = await fetchData(page);\n } catch (err) {\n console.error(`lb-hub: failed to load data for '${page}'`, err);\n }\n applyData(this, data);\n }\n\n /**\n * An app that ships `<body hidden>` (see docs/reference/chrome.md) is\n * asking to stay invisible until there is something coherent to show,\n * rather than flash the chrome before `<main>` has real content. This is\n * a no-op for an app that doesn't use that convention.\n */\n private reveal(): void {\n document.body.hidden = false;\n }\n\n /**\n * The hub's own row: where the page is, in the vocabulary a server\n * answer arrives in, so a chrome displays it the way it displays anything\n * — `lb-row=\"lb-navigation\"` on a subtree, `lb-cell` on what shows a\n * value. Landed before the page host is looked up, so a miss has it too;\n * the unknown-page dialog is one consumer among whatever others the\n * chrome writes.\n *\n * The label is the nav's: the text of the first lb-nav-link anchor whose\n * href names this page. Taken from the nav rather than from a click, so a\n * click, a reload and the back button all land the same label — and a\n * path no anchor names lands an empty one, explicitly, so a consumer is\n * never left showing the previous page's.\n *\n * Landed only where a subtree names it. applyData() warns about a query\n * with no scope, and a chrome that displays no navigation has nothing to\n * be warned about.\n */\n private landNavigation(path: string, page: string): void {\n if (!this.querySelector(`[${ATTR_ROW}=\"${NAV_ROW}\"]`)) return;\n let label = \"\";\n for (const a of this.querySelectorAll<HTMLAnchorElement>(\n `a[${ATTR_NAV_LINK}]`,\n )) {\n if (pageNameFor(new URL(a.href).pathname) !== page) continue;\n label = a.textContent?.trim() ?? \"\";\n break;\n }\n applyData(this, {\n [NAV_ROW]: { [NAV_CELL_LABEL]: label, [NAV_CELL_URI]: path },\n });\n }\n\n /**\n * A path that resolves to no page host is discovered client-side, after a\n * successful 200 — every route gets the same document, so there is no\n * HTTP 404 to report (see docs/tutorials/010-pages-and-navigation.md).\n * lb-unknown-page is optional: an application that declares none gets\n * nothing beyond navigate()'s console.error. The builder guarantees the\n * element it's on is a <dialog> (build/assemble.ts), so this can call\n * showModal() without checking the tag here.\n *\n * Only opens it. What the dialog says it gets from landNavigation(),\n * already run, by naming the hub's row in lb-row like any subtree.\n *\n * Scoped to the hub, like every other lookup here, so that one rule holds\n * without exception: what the hub acts on is inside the hub.\n */\n private reportUnknownPage(): void {\n const dialog = this.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!dialog) return;\n const modal = dialog as HTMLDialogElement;\n if (!modal.open) modal.showModal();\n }\n}\n\ncustomElements.define(HUB_TAG_NAME, LbHub);\n"]}
@@ -417,6 +417,12 @@ that the hub leaves a widget's own clicks alone — see [Requests](#requests).
417
417
  Nothing an application writes ever sets `lb-value`. Loadbare writes it, and
418
418
  a widget or a stylesheet reads it.
419
419
 
420
+ Loadbare also removes it, when a successful insert resets the cells it
421
+ gathered — see [The round trip](#the-round-trip). A widget reads a removed
422
+ `lb-value` as "nothing landed here" and returns its control to its default,
423
+ the way a form control with no `value` attribute shows its default. A blank
424
+ `lb-value` is a value like any other.
425
+
420
426
  The value arrives as the query produced it, with no conversion, so the
421
427
  browser decides what a non-string looks like.
422
428
 
@@ -499,9 +505,9 @@ name, the same test that decides how a value lands — see
499
505
 
500
506
  | lb-action | Written on |
501
507
  |----------------|---------------------------------------------------|
502
- | lb-row-insert | a `<form>`, or a button by its cells, in a list |
508
+ | lb-row-insert | a `<form>`, or a button in a `<tr>`, in a list |
503
509
  | lb-row-delete | anything inside a live row |
504
- | lb-row-update | a `<form>`, or a button by its cells, in a row |
510
+ | lb-row-update | a `<form>`, or a button in a live row |
505
511
  | lb-cell-change | a widget wrapping one control |
506
512
  | anything else | must be a named routine in the page's server code |
507
513
 
@@ -528,14 +534,21 @@ way a control belongs to the form around it. `list` or `row` comes from the
528
534
  nearest ancestor scope, `key` from the nearest live row inside that scope,
529
535
  and `cell` from the dispatching element's own `lb-cell`.
530
536
  A request missing a required field is not sent. The hub never fills
531
- `value`; for `lb-row-insert` and `lb-row-update`, `values` is gathered from
532
- the nearest element holding an `lb-cell` with a control to read, starting at
533
- the dispatching element and walking up. A `<form>` holds its own cells; a
534
- button in a `<tr>` for a new row, which cannot be a form, reads the row
535
- around it. The walk stops at the live row the element sits in and never
536
- reaches the scope, whose other cells belong to other rows. A request that
537
- already carries `values` keeps them, and one with no cell to read is not
538
- sent. A click inside a native element that carries either operation and
537
+ `value`. An `lb-row-insert` or `lb-row-update` gathers the row it belongs
538
+ to: `values` holds every `lb-cell` with a control to read in the nearest
539
+ `<form>`, `<tr>` or live row around the dispatching element, itself
540
+ included, inside its scope. The cells are found the way a row lands, so a
541
+ cell inside a scope nested in the row is that scope's and is not gathered,
542
+ while an element carrying a scope and `lb-cell` both is the row's cell.
543
+ This is a button's form owner: what else sits
544
+ beside the element never changes what is sent. A `<tr>` counts because a
545
+ form cannot go around a table row's controls, so a new row in a table is its
546
+ own form; a live row counts because it is the row the key names. The scope
547
+ itself is never the row, since its cells belong to other rows. A request
548
+ from an element in no form or row is not sent, and the hub reports that its
549
+ cells belong in a `<form>`. A request that already carries `values` keeps
550
+ them, and one whose row has no cell to read is not sent. A click inside a
551
+ native element that carries either operation and
539
552
  holds cells itself is not sent, since clicking into one of its controls
540
553
  would send the row: put the action on a form or on a button.
541
554
 
@@ -576,6 +589,26 @@ A round trip fails on a server error, on a network failure, or on that
576
589
  deadline, and all three set `lb-error` on the element that dispatched the
577
590
  request — see [Request state](#request-state).
578
591
 
592
+ After a write succeeds, the cells it gathered show what the server holds.
593
+ An `lb-row-update` does this by landing the row on them. An
594
+ `lb-row-insert` does it by resetting them, the way `form.reset()` resets a
595
+ form, because the row they held now lives in the list. The hub resets
596
+ after it lands the response, and only what it gathered:
597
+
598
+ - Each control returns to its default: an `<input>` or `<textarea>` to its
599
+ `defaultValue`, a `<select>` to the options marked `selected`. A page
600
+ that wants a prefilled insert writes a `value` attribute, as in plain
601
+ HTML.
602
+ - A control showing something other than the value the hub read holds an
603
+ edit made during the round trip. That edit was not sent, and is left.
604
+ - `lb-value` comes off each reset cell, and off its control, before the
605
+ control resets — see [How a value lands](#how-a-value-lands).
606
+ - Values a widget supplied in the request were not gathered, and are not
607
+ reset.
608
+ - Nothing is dispatched, as `form.reset()` fires no `change`.
609
+
610
+ A failed insert resets nothing, so the entry can be corrected.
611
+
579
612
  A navigation that fails to load its data sets nothing. No element
580
613
  dispatched it, so there is nothing to stamp, and the hub reports it to the
581
614
  console.
@@ -255,6 +255,11 @@ class VisitCount extends HTMLElement {
255
255
  customElements.define("visit-count", VisitCount);
256
256
  ```
257
257
 
258
+ `value` is `null` when the attribute is removed, which the hub does to the
259
+ cells a successful insert resets — see
260
+ [TECHREF-1.0](../TECHREF-1.0.md#the-round-trip). Read it as nothing landed,
261
+ and show the widget's default. A blank string is a value that landed.
262
+
258
263
  List `ATTR_VALUE` in `observedAttributes`, or `attributeChangedCallback`
259
264
  never fires. The browser calls it for an attribute already present when an
260
265
  element upgrades, not only for one that changes afterward, so a widget's
@@ -282,9 +287,14 @@ See [TECHREF-1.0](../TECHREF-1.0.md#requests) for what each operation is
282
287
  filled with.
283
288
 
284
289
  A widget sending `lb-row-insert` or `lb-row-update` doesn't read its own
285
- controls. It dispatches the bare action from the element holding the cells,
286
- a `<tr>` as readily as a `<form>`, or from anything inside it, and the hub
287
- gathers `values` from the nearest element holding a readable `lb-cell`:
290
+ controls. It dispatches the bare action from the row, or from anything
291
+ inside it, and the hub gathers `values` from the row the event came from:
292
+ the nearest `<form>`, `<tr>` or live row around the dispatching element,
293
+ itself included, inside its scope. Every readable `lb-cell` in that row is
294
+ gathered, wherever in the row it sits, except the cells of a scope nested in
295
+ it: a picker carrying `lb-list` and `lb-cell` gives its own value, not its
296
+ options'. A request from an element in no such
297
+ row is not sent, and the console says to use a `<form>`.
288
298
 
289
299
  ```ts
290
300
  row.dispatchEvent(
@@ -298,6 +308,12 @@ row.dispatchEvent(
298
308
  Values the widget supplies itself are kept, and nothing is gathered over
299
309
  them.
300
310
 
311
+ When an insert succeeds, the hub resets the controls it gathered to their
312
+ defaults, so a blank row for entering a new one is blank again without the
313
+ widget watching the request settle. A hidden input the widget added keeps
314
+ its value, since a hidden input's default is its value. Values the widget
315
+ supplied in the request were not gathered, and are not reset.
316
+
301
317
  Let the event bubble, so an ancestor widget can intercept and stop it before
302
318
  the hub sees it. A hand-written widget and a native element carrying
303
319
  `lb-action` produce the same event.
@@ -70,6 +70,13 @@ arrives from scope. The scope's own root counts as a cell if it carries one,
70
70
  which is how an `<option>` — whose content model is text — displays the value
71
71
  it is.
72
72
 
73
+ An element that carries a scope and `lb-cell` both is a cell of the scope
74
+ around it. Its own `lb-list` or `lb-row` names what it displays, and its
75
+ ancestors name where it belongs, so a `<select lb-list="accounts"
76
+ lb-cell="account">` in a row displays the accounts and holds that row's
77
+ `account`. The value lands on it and is gathered from it; the cells inside
78
+ it are the accounts', and neither.
79
+
73
80
  Name the same query on more than one subtree to display it in more than one
74
81
  place. Every subtree gets the result.
75
82
 
@@ -160,7 +167,7 @@ anything else is a name the page declared.
160
167
 
161
168
  The hub sends the first four from a native element on the element's own
162
169
  event: a form on submit, anything else on click. An insert or update clicked
163
- from a button reads the cells around the button (see [Forms](#forms)). A
170
+ from a button reads the form or row the button is in (see [Forms](#forms)). A
164
171
  cell change needs a
165
172
  widget to say what a change is, so only a widget sends it: the shipped
166
173
  `<lb-input>` does when it carries `lb-action="lb-cell-change"`, and stays
@@ -207,8 +214,10 @@ The hub sends it from a native element. A widget sends its own request.
207
214
  ### Forms
208
215
 
209
216
  A `<form>` performs its `lb-action` on submit. `lb-row-insert` and `lb-row-update`
210
- both gather every `lb-cell` inside the form into one values map, read from
211
- the control each cell is or wraps. They differ in one thing: `lb-row-update`
217
+ both gather every `lb-cell` of the form into one values map, read from
218
+ the control each cell is or wraps. A cell inside a scope nested in the form
219
+ is that scope's and is not gathered, the same way a value landing on the
220
+ form's row does not reach it. They differ in one thing: `lb-row-update`
212
221
  also carries the key of the row it is inside, and `lb-row-insert` carries none,
213
222
  because there is no row yet. A declared name on a form sends that action on
214
223
  submit, carrying the binding and no values.
@@ -227,31 +236,42 @@ from the same ancestor a delete button reads:
227
236
  </template>
228
237
  ```
229
238
 
239
+ When an `lb-row-insert` succeeds, the hub resets every control it gathered
240
+ from to its default, as `form.reset()` would, so the form is ready for the
241
+ next entry. A failed insert leaves the entry for the user to correct, and a
242
+ control the user changed while the request was in flight keeps the change.
243
+ An `lb-row-update` resets nothing: the row it sent lands back on its cells.
244
+
230
245
  Give every `lb-cell` in a form a control to read. A cell that is neither an
231
246
  `<input>`, `<select>`, or `<textarea>` nor wraps one is left out of the
232
247
  values map, and a form with no cell to read sends nothing.
233
248
 
234
- Where the cells cannot sit in a form, as in a table row, put the action on a
235
- button beside them. The hub gathers from the nearest element around the
236
- button that holds a cell to read, so a button in a row for a new entry
237
- inserts that row's cells:
249
+ An insert or update gathers the row it belongs to: the nearest `<form>`,
250
+ `<tr>` or live row around the element that sends it, inside its scope. A
251
+ button submits its form the same way, whatever else sits beside it.
252
+
253
+ A form cannot go around a table row's controls, so there the row is the
254
+ form. Put the action on a button in the row, and it inserts every cell in
255
+ the row, wherever in the row the button and the cells are:
238
256
 
239
257
  ```html
240
258
  <table lb-list="roster">
241
259
  <tbody>
242
260
  <tr>
243
261
  <td><input lb-cell="name" /></td>
244
- <td><input lb-cell="role" /></td>
245
- <td><button lb-action="lb-row-insert">Add</button></td>
262
+ <td>
263
+ <input lb-cell="role" />
264
+ <button lb-action="lb-row-insert">Add</button>
265
+ </td>
246
266
  </tr>
247
267
  </tbody>
248
268
  </table>
249
269
  ```
250
270
 
251
- The search stops at the live row the button is in, so an `lb-row-update`
252
- button in a row saves only that row, and it never takes in the list scope
253
- itself. Keep the button's own cell apart from the others: a button sharing
254
- a `<td>` with one input gathers only that input.
271
+ An `lb-row-update` button in a live row saves that row and no other, and a
272
+ form nested in a row gathers only the form. Anywhere else, write the form:
273
+ cells and a button in a `<div>` belong to no row, so the hub refuses the
274
+ request and says to put them in a `<form>`.
255
275
 
256
276
  Put these two actions on a form or a button, not on an element that holds
257
277
  the cells. A click into one of its inputs would send the row, so the hub
package/docs/testing.md CHANGED
@@ -136,6 +136,8 @@ here.
136
136
  - a native element receives its value as text, a hyphenated one as `lb-value`
137
137
  - the root of a scope counts as a cell if it carries one, which is what makes
138
138
  an `<option>` row possible
139
+ - an element carrying both a scope and `lb-cell` is a cell of the row around
140
+ it, and the cells inside it are its own scope's
139
141
  - a query with no scope is reported and skipped; several scopes for one query
140
142
  are all filled; a set of rows landing on a scope bound with `lb-row`
141
143
  is reported rather than thrown
@@ -182,6 +184,12 @@ What the hub is tested for here:
182
184
  - a click finds the nearest `lb-action`, and skips a hyphenated tag and a
183
185
  form, both of which own the interaction themselves
184
186
  - a submit gathers the form's cells, and reports a cell with no control
187
+ - an insert or update gathers the nearest form, `<tr>` or live row inside
188
+ its scope, whatever shares the button's cell: one ghost row among its
189
+ neighbours, a whole live row, a form nested in a row, a button's form
190
+ owner; and refuses a `<div>` holding cells, pointing to `<form>`
191
+ - gathering skips the cells of a nested scope, so a picker in a row sends
192
+ its own cell and nothing about its options
185
193
  - a request arriving with no action, or with a reserved name that is not one
186
194
  of the four, is refused before it reaches the wire
187
195
  - `lb-pending` lands on the element that dispatched, `lb-error` replaces it
package/docs/theory.md CHANGED
@@ -422,6 +422,15 @@ destroy each other.
422
422
  | Row and template lookups stop at a nested scope | Cells respected nesting while rows and templates did not |
423
423
  | Master-detail is a row and a list | An open question born of document-shaped data |
424
424
  | `lb-value` on every cell | Only a widget's landed value was visible to a stylesheet |
425
+ | An insert or update gathers its form or row \* | What sat beside a button decided what was sent |
426
+ | Gathering finds cells the way landing does \* | Gathering read into nested scopes, and an element that was both a scope and a cell was gathered but never landed on |
427
+
428
+ \* LLM-authored change, not yet reviewed by the author. Added by Claude on
429
+ 2026-09-13: `lb-row-insert` and `lb-row-update` gather the nearest `<form>`,
430
+ `<tr>` or live row inside their scope, rather than the nearest element
431
+ holding a cell. Gathering and landing then share one test for which cells
432
+ belong to a scope, judged from the element's ancestors and never its own
433
+ attributes, following the first rule.
425
434
 
426
435
  Two signs suggest these are rules rather than patches. Fixing where a
427
436
  request's scope comes from also fixed an unrelated console error about insert
@@ -83,7 +83,8 @@ export function openDb() {
83
83
  npm run dev
84
84
  ```
85
85
 
86
- Open the About page. Type a note, click Add — it appears in the list.
86
+ Open the About page. Type a note, click Add — it appears in the list, and
87
+ the input is empty again for the next one, as a submitted form is.
87
88
  Reload — it's still there.
88
89
 
89
90
  ---
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loadbare/app",
3
3
  "description": "High performance web app framework for server-bound applications",
4
- "version": "0.7.1",
4
+ "version": "0.7.3",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",