@loadbare/app 0.7.2 → 0.7.4
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.
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +67 -6
- package/dist/build/assemble.js.map +1 -1
- package/dist/core/lb-constants.d.ts +1 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +15 -2
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +6 -0
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +112 -15
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.js +46 -26
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/docs/TECHREF-1.0.md +138 -98
- package/docs/analysis-closed-set.md +203 -0
- package/docs/comparison.md +1123 -0
- package/docs/prior-art.md +216 -0
- package/docs/reference/custom-elements.md +18 -3
- package/docs/reference/data-binding.md +111 -41
- package/docs/testing.md +10 -0
- package/docs/theory.md +737 -399
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../build/assemble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../build/assemble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAqBH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAgM/C,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,8EAA8E;IAC9E,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,wBAAsB,QAAQ,CAAC,EAC7B,UAAU,EACV,KAAK,EACL,KAAK,GACN,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CA8CtC"}
|
package/dist/build/assemble.js
CHANGED
|
@@ -20,7 +20,7 @@ import { readFile } from "node:fs/promises";
|
|
|
20
20
|
import { JSDOM } from "jsdom";
|
|
21
21
|
import { addDefinitions, customElementTags, expand, } from "./expand.js";
|
|
22
22
|
import { formatHtml } from "./format.js";
|
|
23
|
-
import { ATTR_CELL, ATTR_KEY, ATTR_LIST, ATTR_PAGE, ATTR_ROW, ATTR_UNKNOWN_PAGE, HUB_TAG_NAME, } from "../core/lb-constants.js";
|
|
23
|
+
import { ATTR_CELL, ATTR_KEY, ATTR_LIST, ATTR_PAGE, ATTR_ROW, ATTR_SHOW, ATTR_UNKNOWN_PAGE, HUB_TAG_NAME, } from "../core/lb-constants.js";
|
|
24
24
|
/**
|
|
25
25
|
* The hub calls showModal() on whatever carries lb-unknown-page
|
|
26
26
|
* (hub/lb-hub.browser.ts), so a chrome that puts the attribute on anything
|
|
@@ -62,6 +62,11 @@ function checkUnknownPageDialog(chromeFile, expandedBody) {
|
|
|
62
62
|
*
|
|
63
63
|
* A `<template>`'s children are walked in the scope that encloses the
|
|
64
64
|
* template, because a row template's clones land in the list around it.
|
|
65
|
+
*
|
|
66
|
+
* `lb-show` is judged by the scope around the element, not one the element
|
|
67
|
+
* carries, because its condition is a cell of the row around it. It needs a
|
|
68
|
+
* row: in a list scope but outside the row template, nothing ever lands on
|
|
69
|
+
* it, and the branch would never show.
|
|
65
70
|
*/
|
|
66
71
|
function walkBinding(node, scope, problems) {
|
|
67
72
|
for (const el of node.children) {
|
|
@@ -74,9 +79,9 @@ function walkBinding(node, scope, problems) {
|
|
|
74
79
|
`one or the other, and a name answers with one shape`);
|
|
75
80
|
}
|
|
76
81
|
if (listName !== null)
|
|
77
|
-
here = { kind: "list", name: listName };
|
|
82
|
+
here = { kind: "list", name: listName, row: false };
|
|
78
83
|
else if (rowName !== null)
|
|
79
|
-
here = { kind: "row", name: rowName };
|
|
84
|
+
here = { kind: "row", name: rowName, row: true };
|
|
80
85
|
if (el.hasAttribute(ATTR_CELL) && here === null) {
|
|
81
86
|
problems.push(`<${tag}> carries ${ATTR_CELL}="${el.getAttribute(ATTR_CELL)}" with ` +
|
|
82
87
|
`no ${ATTR_LIST} or ${ATTR_ROW} scope around it`);
|
|
@@ -91,14 +96,69 @@ function walkBinding(node, scope, problems) {
|
|
|
91
96
|
`only a list has rows to identify`);
|
|
92
97
|
}
|
|
93
98
|
}
|
|
99
|
+
const show = el.getAttribute(ATTR_SHOW);
|
|
100
|
+
if (show !== null) {
|
|
101
|
+
if (tag === "template") {
|
|
102
|
+
problems.push(`a <template> carries ${ATTR_SHOW}="${show}"; write it on the ` +
|
|
103
|
+
`element it shows, and Loadbare supplies the template`);
|
|
104
|
+
}
|
|
105
|
+
else if (scope === null || !scope.row) {
|
|
106
|
+
problems.push(`<${tag}> carries ${ATTR_SHOW}="${show}" with no row around it ` +
|
|
107
|
+
`to take the value from`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
94
110
|
walkBinding(el, here, problems);
|
|
95
111
|
// By tag name rather than by instanceof: JSDOM.fragment() parses into a
|
|
96
112
|
// document with no window, so the constructor is not reachable from the
|
|
97
113
|
// node.
|
|
98
114
|
if (tag === "template") {
|
|
99
|
-
|
|
115
|
+
const content = el.content;
|
|
116
|
+
const isRowTemplate = el.hasAttribute(ATTR_KEY) && here?.kind === "list";
|
|
117
|
+
if (isRowTemplate) {
|
|
118
|
+
for (const root of content.children) {
|
|
119
|
+
if (root.hasAttribute(ATTR_SHOW)) {
|
|
120
|
+
problems.push(`<${root.localName}> is a row template's root and carries ` +
|
|
121
|
+
`${ATTR_SHOW}; a row that should not show is left out by ` +
|
|
122
|
+
`the query`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
walkBinding(content, isRowTemplate ? { ...here, row: true } : here, problems);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Ship every `lb-show` element already inside the template the hub keeps an
|
|
132
|
+
* absent branch in, so nothing conditional shows until its value says so.
|
|
133
|
+
* A page is in the document before its data arrives, and a value that has not
|
|
134
|
+
* arrived is no value.
|
|
135
|
+
*
|
|
136
|
+
* Parsed as template content, which accepts a `<tr>` or an `<option>` at the
|
|
137
|
+
* top level where a `<div>` would drop it. Elements are collected before any
|
|
138
|
+
* is moved, and a wrapped element is still reached once its outer one has
|
|
139
|
+
* gone into a template, because the references are held.
|
|
140
|
+
*/
|
|
141
|
+
function wrapShown(html) {
|
|
142
|
+
const doc = new JSDOM("").window.document;
|
|
143
|
+
const holder = doc.createElement("template");
|
|
144
|
+
holder.innerHTML = html;
|
|
145
|
+
const shown = [];
|
|
146
|
+
const collect = (root) => {
|
|
147
|
+
shown.push(...root.querySelectorAll(`[${ATTR_SHOW}]`));
|
|
148
|
+
for (const t of root.querySelectorAll("template")) {
|
|
149
|
+
collect(t.content);
|
|
100
150
|
}
|
|
151
|
+
};
|
|
152
|
+
collect(holder.content);
|
|
153
|
+
if (shown.length === 0)
|
|
154
|
+
return html;
|
|
155
|
+
for (const el of shown) {
|
|
156
|
+
const parked = doc.createElement("template");
|
|
157
|
+
parked.setAttribute(ATTR_SHOW, el.getAttribute(ATTR_SHOW));
|
|
158
|
+
el.replaceWith(parked);
|
|
159
|
+
parked.content.append(el);
|
|
101
160
|
}
|
|
161
|
+
return holder.innerHTML;
|
|
102
162
|
}
|
|
103
163
|
/** Check one expanded fragment, and name the file if anything is wrong. */
|
|
104
164
|
function checkBinding(file, html) {
|
|
@@ -121,7 +181,7 @@ export async function assemble({ chromeFile, pages, scans, }) {
|
|
|
121
181
|
const authored = await readFile(file, "utf-8");
|
|
122
182
|
const host = expand(authored, defs);
|
|
123
183
|
checkBinding(file, host);
|
|
124
|
-
templates.push(`<template ${ATTR_PAGE}="${name}">${host}</template>`);
|
|
184
|
+
templates.push(`<template ${ATTR_PAGE}="${name}">${wrapShown(host)}</template>`);
|
|
125
185
|
}
|
|
126
186
|
let chromeSource;
|
|
127
187
|
try {
|
|
@@ -138,7 +198,8 @@ export async function assemble({ chromeFile, pages, scans, }) {
|
|
|
138
198
|
const expandedBody = expand(bodyContent, defs);
|
|
139
199
|
checkUnknownPageDialog(chromeFile, expandedBody);
|
|
140
200
|
checkBinding(chromeFile, expandedBody);
|
|
141
|
-
const
|
|
201
|
+
const shippedBody = wrapShown(expandedBody);
|
|
202
|
+
const withPages = chromeSource.replace(whole, () => `<body${bodyAttrs}>${shippedBody}\n${templates.join("\n")}\n</body>`);
|
|
142
203
|
return {
|
|
143
204
|
html: await formatHtml(withPages),
|
|
144
205
|
usedTags: customElementTags(withPages),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../build/assemble.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Assembling one document out of a chrome file and a set of pages.\n *\n * Every `.html` file in play — the chrome and each page — goes through the\n * same `expand()` (see build/expand.ts) against the same definitions: the\n * built-in widgets and whatever definitions were found in the caller's own\n * source tree. `expand()` only ever sees a fragment, so the chrome's own is\n * the text between its `<body>` tags; a page's is the whole file. A page's\n * expanded host is wrapped in `<template lb-page=\"<name>\">` and, together\n * with the chrome's own expanded body content, spliced back into the\n * chrome's `<body>`. The result is the one document a server has to serve\n * for every route — see docs/tutorials/000-getting-started.md and\n * docs/reference/builder.md.\n *\n * Every path here is a parameter, resolved once by build/locations.ts — this\n * module never reconstructs one of its own, and never reads a directory\n * looking for pages or widgets; it is handed exactly which files those are.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { JSDOM } from \"jsdom\";\nimport {\n addDefinitions,\n customElementTags,\n expand,\n type Definitions,\n} from \"./expand.js\";\nimport { formatHtml } from \"./format.js\";\nimport {\n ATTR_CELL,\n ATTR_KEY,\n ATTR_LIST,\n ATTR_PAGE,\n ATTR_ROW,\n ATTR_UNKNOWN_PAGE,\n HUB_TAG_NAME,\n} from \"../core/lb-constants.js\";\nimport type { PageFile } from \"./locations.js\";\nimport type { OriginScan } from \"./origins.js\";\n\n/**\n * The hub calls showModal() on whatever carries lb-unknown-page\n * (hub/lb-hub.browser.ts), so a chrome that puts the attribute on anything\n * else would fail at runtime, on whichever page happens to 404 first, rather\n * than at build time. The attribute is optional; a wrong element on it, or a\n * right one out of the hub's reach, is an error.\n *\n * Out of reach because the hub looks for the dialog inside itself, as it\n * does for everything else it acts on. A dialog elsewhere in the body would\n * never open and would say nothing about why.\n */\nfunction checkUnknownPageDialog(\n chromeFile: string,\n expandedBody: string,\n): void {\n const fragment = JSDOM.fragment(expandedBody);\n const el = fragment.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!el) return;\n if (el.tagName !== \"DIALOG\") {\n throw new Error(\n `assemble: '${chromeFile}' declares ${ATTR_UNKNOWN_PAGE} on ` +\n `<${el.tagName.toLowerCase()}>, must be a <dialog>`,\n );\n }\n if (!el.closest(HUB_TAG_NAME)) {\n throw new Error(\n `assemble: '${chromeFile}' declares ${ATTR_UNKNOWN_PAGE} outside ` +\n `<${HUB_TAG_NAME}>, where the hub cannot reach it`,\n );\n }\n}\n\n/**\n * One scope while the binding check walks a document: which rung it is, the\n * name it carries, and whether a list has found its row template yet.\n */\ninterface Scope {\n kind: \"list\" | \"row\";\n name: string;\n}\n\n/**\n * The binding vocabulary, checked once against the finished document rather\n * than discovered one console error at a time in a browser.\n *\n * Everything here is a fact the markup states about itself, so none of it\n * needs the server: a key is named on a row template, a row template belongs\n * to a list, and a cell belongs to a scope. What the markup and the server\n * say about cardinality cannot be compared here, because they are separate\n * artifacts — createHub holds that check. See docs/reference/data-binding.md.\n *\n * A list scope with no row template is not an error. It is bound to the list\n * without displaying it, which is what an insert form naming the list it adds\n * to already is.\n *\n * A `<template>`'s children are walked in the scope that encloses the\n * template, because a row template's clones land in the list around it.\n */\nfunction walkBinding(\n node: ParentNode,\n scope: Scope | null,\n problems: string[],\n): void {\n for (const el of node.children) {\n const listName = el.getAttribute(ATTR_LIST);\n const rowName = el.getAttribute(ATTR_ROW);\n const tag = el.localName;\n let here = scope;\n\n if (listName !== null && rowName !== null) {\n problems.push(\n `<${tag}> carries both ${ATTR_LIST} and ${ATTR_ROW}; a scope is ` +\n `one or the other, and a name answers with one shape`,\n );\n }\n if (listName !== null) here = { kind: \"list\", name: listName };\n else if (rowName !== null) here = { kind: \"row\", name: rowName };\n\n if (el.hasAttribute(ATTR_CELL) && here === null) {\n problems.push(\n `<${tag}> carries ${ATTR_CELL}=\"${el.getAttribute(ATTR_CELL)}\" with ` +\n `no ${ATTR_LIST} or ${ATTR_ROW} scope around it`,\n );\n }\n\n if (el.hasAttribute(ATTR_KEY)) {\n if (tag !== \"template\") {\n problems.push(\n `<${tag}> carries ${ATTR_KEY}; it names the column that identifies ` +\n `a row and belongs on the row <template>`,\n );\n } else if (here === null || here.kind !== \"list\") {\n problems.push(\n `a <template> carries ${ATTR_KEY} outside any ${ATTR_LIST} scope; ` +\n `only a list has rows to identify`,\n );\n }\n }\n\n walkBinding(el, here, problems);\n // By tag name rather than by instanceof: JSDOM.fragment() parses into a\n // document with no window, so the constructor is not reachable from the\n // node.\n if (tag === \"template\") {\n walkBinding((el as HTMLTemplateElement).content, here, problems);\n }\n }\n}\n\n/** Check one expanded fragment, and name the file if anything is wrong. */\nfunction checkBinding(file: string, html: string): void {\n const problems: string[] = [];\n walkBinding(JSDOM.fragment(html), null, problems);\n if (problems.length > 0) {\n throw new Error(`assemble: '${file}' binding:\\n ` + problems.join(`\\n `));\n }\n}\n\nexport interface AssembleOptions {\n chromeFile: string;\n pages: PageFile[];\n /** Every origin, already scanned, in cascade order — see build/origins.ts. */\n scans: OriginScan[];\n}\n\nexport interface Assembled {\n html: string;\n /** Every custom element tag left in the document once expansion is done. */\n usedTags: Set<string>;\n}\n\nexport async function assemble({\n chromeFile,\n pages,\n scans,\n}: AssembleOptions): Promise<Assembled> {\n // One layer per origin, in order, so a later origin's definition overrides\n // an earlier one's for the same tag. Each layer is checked for cycles as it\n // lands, against everything merged so far.\n let defs: Definitions = {};\n for (const scan of scans) {\n defs = await addDefinitions(defs, scan.definitions);\n }\n\n const templates: string[] = [];\n for (const { name, file } of pages) {\n const authored = await readFile(file, \"utf-8\");\n const host = expand(authored, defs);\n checkBinding(file, host);\n templates.push(`<template ${ATTR_PAGE}=\"${name}\">${host}</template>`);\n }\n\n let chromeSource: string;\n try {\n chromeSource = await readFile(chromeFile, \"utf-8\");\n } catch {\n throw new Error(`assemble: no chrome file at '${chromeFile}'`);\n }\n\n const body = /<body([^>]*)>([\\s\\S]*)<\\/body>/i.exec(chromeSource);\n if (!body) {\n throw new Error(\n `assemble: '${chromeFile}' has no <body>...</body> to expand`,\n );\n }\n const [whole, bodyAttrs, bodyContent] = body;\n const expandedBody = expand(bodyContent!, defs);\n checkUnknownPageDialog(chromeFile, expandedBody);\n checkBinding(chromeFile, expandedBody);\n const withPages = chromeSource.replace(\n whole!,\n () => `<body${bodyAttrs}>${expandedBody}\\n${templates.join(\"\\n\")}\\n</body>`,\n );\n\n return {\n html: await formatHtml(withPages),\n usedTags: customElementTags(withPages),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../build/assemble.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Assembling one document out of a chrome file and a set of pages.\n *\n * Every `.html` file in play — the chrome and each page — goes through the\n * same `expand()` (see build/expand.ts) against the same definitions: the\n * built-in widgets and whatever definitions were found in the caller's own\n * source tree. `expand()` only ever sees a fragment, so the chrome's own is\n * the text between its `<body>` tags; a page's is the whole file. A page's\n * expanded host is wrapped in `<template lb-page=\"<name>\">` and, together\n * with the chrome's own expanded body content, spliced back into the\n * chrome's `<body>`. The result is the one document a server has to serve\n * for every route — see docs/tutorials/000-getting-started.md and\n * docs/reference/builder.md.\n *\n * Every path here is a parameter, resolved once by build/locations.ts — this\n * module never reconstructs one of its own, and never reads a directory\n * looking for pages or widgets; it is handed exactly which files those are.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { JSDOM } from \"jsdom\";\nimport {\n addDefinitions,\n customElementTags,\n expand,\n type Definitions,\n} from \"./expand.js\";\nimport { formatHtml } from \"./format.js\";\nimport {\n ATTR_CELL,\n ATTR_KEY,\n ATTR_LIST,\n ATTR_PAGE,\n ATTR_ROW,\n ATTR_SHOW,\n ATTR_UNKNOWN_PAGE,\n HUB_TAG_NAME,\n} from \"../core/lb-constants.js\";\nimport type { PageFile } from \"./locations.js\";\nimport type { OriginScan } from \"./origins.js\";\n\n/**\n * The hub calls showModal() on whatever carries lb-unknown-page\n * (hub/lb-hub.browser.ts), so a chrome that puts the attribute on anything\n * else would fail at runtime, on whichever page happens to 404 first, rather\n * than at build time. The attribute is optional; a wrong element on it, or a\n * right one out of the hub's reach, is an error.\n *\n * Out of reach because the hub looks for the dialog inside itself, as it\n * does for everything else it acts on. A dialog elsewhere in the body would\n * never open and would say nothing about why.\n */\nfunction checkUnknownPageDialog(\n chromeFile: string,\n expandedBody: string,\n): void {\n const fragment = JSDOM.fragment(expandedBody);\n const el = fragment.querySelector(`[${ATTR_UNKNOWN_PAGE}]`);\n if (!el) return;\n if (el.tagName !== \"DIALOG\") {\n throw new Error(\n `assemble: '${chromeFile}' declares ${ATTR_UNKNOWN_PAGE} on ` +\n `<${el.tagName.toLowerCase()}>, must be a <dialog>`,\n );\n }\n if (!el.closest(HUB_TAG_NAME)) {\n throw new Error(\n `assemble: '${chromeFile}' declares ${ATTR_UNKNOWN_PAGE} outside ` +\n `<${HUB_TAG_NAME}>, where the hub cannot reach it`,\n );\n }\n}\n\n/**\n * One scope while the binding check walks a document: which rung it is, the\n * name it carries, and whether there is a row here to take a value from. A\n * row scope always has one. A list has one only inside its row template.\n */\ninterface Scope {\n kind: \"list\" | \"row\";\n name: string;\n row: boolean;\n}\n\n/**\n * The binding vocabulary, checked once against the finished document rather\n * than discovered one console error at a time in a browser.\n *\n * Everything here is a fact the markup states about itself, so none of it\n * needs the server: a key is named on a row template, a row template belongs\n * to a list, and a cell belongs to a scope. What the markup and the server\n * say about cardinality cannot be compared here, because they are separate\n * artifacts — createHub holds that check. See docs/reference/data-binding.md.\n *\n * A list scope with no row template is not an error. It is bound to the list\n * without displaying it, which is what an insert form naming the list it adds\n * to already is.\n *\n * A `<template>`'s children are walked in the scope that encloses the\n * template, because a row template's clones land in the list around it.\n *\n * `lb-show` is judged by the scope around the element, not one the element\n * carries, because its condition is a cell of the row around it. It needs a\n * row: in a list scope but outside the row template, nothing ever lands on\n * it, and the branch would never show.\n */\nfunction walkBinding(\n node: ParentNode,\n scope: Scope | null,\n problems: string[],\n): void {\n for (const el of node.children) {\n const listName = el.getAttribute(ATTR_LIST);\n const rowName = el.getAttribute(ATTR_ROW);\n const tag = el.localName;\n let here = scope;\n\n if (listName !== null && rowName !== null) {\n problems.push(\n `<${tag}> carries both ${ATTR_LIST} and ${ATTR_ROW}; a scope is ` +\n `one or the other, and a name answers with one shape`,\n );\n }\n if (listName !== null) here = { kind: \"list\", name: listName, row: false };\n else if (rowName !== null) here = { kind: \"row\", name: rowName, row: true };\n\n if (el.hasAttribute(ATTR_CELL) && here === null) {\n problems.push(\n `<${tag}> carries ${ATTR_CELL}=\"${el.getAttribute(ATTR_CELL)}\" with ` +\n `no ${ATTR_LIST} or ${ATTR_ROW} scope around it`,\n );\n }\n\n if (el.hasAttribute(ATTR_KEY)) {\n if (tag !== \"template\") {\n problems.push(\n `<${tag}> carries ${ATTR_KEY}; it names the column that identifies ` +\n `a row and belongs on the row <template>`,\n );\n } else if (here === null || here.kind !== \"list\") {\n problems.push(\n `a <template> carries ${ATTR_KEY} outside any ${ATTR_LIST} scope; ` +\n `only a list has rows to identify`,\n );\n }\n }\n\n const show = el.getAttribute(ATTR_SHOW);\n if (show !== null) {\n if (tag === \"template\") {\n problems.push(\n `a <template> carries ${ATTR_SHOW}=\"${show}\"; write it on the ` +\n `element it shows, and Loadbare supplies the template`,\n );\n } else if (scope === null || !scope.row) {\n problems.push(\n `<${tag}> carries ${ATTR_SHOW}=\"${show}\" with no row around it ` +\n `to take the value from`,\n );\n }\n }\n\n walkBinding(el, here, problems);\n // By tag name rather than by instanceof: JSDOM.fragment() parses into a\n // document with no window, so the constructor is not reachable from the\n // node.\n if (tag === \"template\") {\n const content = (el as HTMLTemplateElement).content;\n const isRowTemplate = el.hasAttribute(ATTR_KEY) && here?.kind === \"list\";\n if (isRowTemplate) {\n for (const root of content.children) {\n if (root.hasAttribute(ATTR_SHOW)) {\n problems.push(\n `<${root.localName}> is a row template's root and carries ` +\n `${ATTR_SHOW}; a row that should not show is left out by ` +\n `the query`,\n );\n }\n }\n }\n walkBinding(\n content,\n isRowTemplate ? { ...here!, row: true } : here,\n problems,\n );\n }\n }\n}\n\n/**\n * Ship every `lb-show` element already inside the template the hub keeps an\n * absent branch in, so nothing conditional shows until its value says so.\n * A page is in the document before its data arrives, and a value that has not\n * arrived is no value.\n *\n * Parsed as template content, which accepts a `<tr>` or an `<option>` at the\n * top level where a `<div>` would drop it. Elements are collected before any\n * is moved, and a wrapped element is still reached once its outer one has\n * gone into a template, because the references are held.\n */\nfunction wrapShown(html: string): string {\n const doc = new JSDOM(\"\").window.document;\n const holder = doc.createElement(\"template\");\n holder.innerHTML = html;\n const shown: Element[] = [];\n const collect = (root: ParentNode): void => {\n shown.push(...root.querySelectorAll(`[${ATTR_SHOW}]`));\n for (const t of root.querySelectorAll(\"template\")) {\n collect((t as HTMLTemplateElement).content);\n }\n };\n collect(holder.content);\n if (shown.length === 0) return html;\n for (const el of shown) {\n const parked = doc.createElement(\"template\");\n parked.setAttribute(ATTR_SHOW, el.getAttribute(ATTR_SHOW)!);\n el.replaceWith(parked);\n parked.content.append(el);\n }\n return holder.innerHTML;\n}\n\n/** Check one expanded fragment, and name the file if anything is wrong. */\nfunction checkBinding(file: string, html: string): void {\n const problems: string[] = [];\n walkBinding(JSDOM.fragment(html), null, problems);\n if (problems.length > 0) {\n throw new Error(`assemble: '${file}' binding:\\n ` + problems.join(`\\n `));\n }\n}\n\nexport interface AssembleOptions {\n chromeFile: string;\n pages: PageFile[];\n /** Every origin, already scanned, in cascade order — see build/origins.ts. */\n scans: OriginScan[];\n}\n\nexport interface Assembled {\n html: string;\n /** Every custom element tag left in the document once expansion is done. */\n usedTags: Set<string>;\n}\n\nexport async function assemble({\n chromeFile,\n pages,\n scans,\n}: AssembleOptions): Promise<Assembled> {\n // One layer per origin, in order, so a later origin's definition overrides\n // an earlier one's for the same tag. Each layer is checked for cycles as it\n // lands, against everything merged so far.\n let defs: Definitions = {};\n for (const scan of scans) {\n defs = await addDefinitions(defs, scan.definitions);\n }\n\n const templates: string[] = [];\n for (const { name, file } of pages) {\n const authored = await readFile(file, \"utf-8\");\n const host = expand(authored, defs);\n checkBinding(file, host);\n templates.push(\n `<template ${ATTR_PAGE}=\"${name}\">${wrapShown(host)}</template>`,\n );\n }\n\n let chromeSource: string;\n try {\n chromeSource = await readFile(chromeFile, \"utf-8\");\n } catch {\n throw new Error(`assemble: no chrome file at '${chromeFile}'`);\n }\n\n const body = /<body([^>]*)>([\\s\\S]*)<\\/body>/i.exec(chromeSource);\n if (!body) {\n throw new Error(\n `assemble: '${chromeFile}' has no <body>...</body> to expand`,\n );\n }\n const [whole, bodyAttrs, bodyContent] = body;\n const expandedBody = expand(bodyContent!, defs);\n checkUnknownPageDialog(chromeFile, expandedBody);\n checkBinding(chromeFile, expandedBody);\n const shippedBody = wrapShown(expandedBody);\n const withPages = chromeSource.replace(\n whole!,\n () => `<body${bodyAttrs}>${shippedBody}\\n${templates.join(\"\\n\")}\\n</body>`,\n );\n\n return {\n html: await formatHtml(withPages),\n usedTags: customElementTags(withPages),\n };\n}\n"]}
|
|
@@ -3,6 +3,7 @@ export declare const ATTR_ROW = "lb-row";
|
|
|
3
3
|
export declare const ATTR_KEY = "lb-key";
|
|
4
4
|
export declare const ATTR_KEY_VALUE = "lb-key-value";
|
|
5
5
|
export declare const ATTR_CELL = "lb-cell";
|
|
6
|
+
export declare const ATTR_SHOW = "lb-show";
|
|
6
7
|
export declare const ATTR_VALUE = "lb-value";
|
|
7
8
|
export declare const ATTR_ROW_COUNT = "lb-row-count";
|
|
8
9
|
export declare const LB_EVENT_NAME = "lb-request";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-constants.d.ts","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,SAAS,YAAY,CAAC;AAQnC,eAAO,MAAM,QAAQ,WAAW,CAAC;AAMjC,eAAO,MAAM,QAAQ,WAAW,CAAC;AAMjC,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAQ7C,eAAO,MAAM,SAAS,YAAY,CAAC;AAEnC,eAAO,MAAM,UAAU,aAAa,CAAC;AAOrC,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAK7C,eAAO,MAAM,aAAa,eAAe,CAAC;AAS1C,eAAO,MAAM,WAAW,cAAc,CAAC;AAYvC,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAMxC,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAKjD,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAOjD,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAMjD,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAInD,eAAO,MAAM,UAAU,gFAKb,CAAC;AAMX,eAAO,MAAM,SAAS,YAAY,CAAC;AAYnC,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAM3C,eAAO,MAAM,SAAS,YAAY,CAAC;AAQnC,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAKvC,eAAO,MAAM,cAAc,eAAe,CAAC;AAC3C,eAAO,MAAM,YAAY,aAAa,CAAC;AAUvC,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAQnD,eAAO,MAAM,YAAY,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"lb-constants.d.ts","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,SAAS,YAAY,CAAC;AAQnC,eAAO,MAAM,QAAQ,WAAW,CAAC;AAMjC,eAAO,MAAM,QAAQ,WAAW,CAAC;AAMjC,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAQ7C,eAAO,MAAM,SAAS,YAAY,CAAC;AAanC,eAAO,MAAM,SAAS,YAAY,CAAC;AAEnC,eAAO,MAAM,UAAU,aAAa,CAAC;AAOrC,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAK7C,eAAO,MAAM,aAAa,eAAe,CAAC;AAS1C,eAAO,MAAM,WAAW,cAAc,CAAC;AAYvC,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAMxC,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAKjD,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAOjD,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAMjD,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAInD,eAAO,MAAM,UAAU,gFAKb,CAAC;AAMX,eAAO,MAAM,SAAS,YAAY,CAAC;AAYnC,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAM3C,eAAO,MAAM,SAAS,YAAY,CAAC;AAQnC,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAKvC,eAAO,MAAM,cAAc,eAAe,CAAC;AAC3C,eAAO,MAAM,YAAY,aAAa,CAAC;AAUvC,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAQnD,eAAO,MAAM,YAAY,WAAW,CAAC;AASrC,eAAO,MAAM,YAAY,eAAe,CAAC;AAOzC,eAAO,MAAM,UAAU,aAAa,CAAC;AAMrC,eAAO,MAAM,WAAW,QAAQ,CAAC;AAIjC,eAAO,MAAM,qBAAqB,QAAS,CAAC"}
|
|
@@ -31,6 +31,18 @@ export const ATTR_KEY_VALUE = "lb-key-value";
|
|
|
31
31
|
// from scope, so the column is all that is left to say. ATTR_KEY holds a
|
|
32
32
|
// column name for the same reason.
|
|
33
33
|
export const ATTR_CELL = "lb-cell";
|
|
34
|
+
// The column that decides whether an element is present: `null` and `false`
|
|
35
|
+
// are off, and every other value is on. It binds by the same ancestor rule as
|
|
36
|
+
// ATTR_CELL, and names a column for the same reason.
|
|
37
|
+
//
|
|
38
|
+
// An element that is off is moved into a `<template>` carrying this attribute
|
|
39
|
+
// and the same column, standing where the element stood, and moved back out
|
|
40
|
+
// when the column turns on. The builder ships every such element already
|
|
41
|
+
// inside its template. A developer writes this attribute on an element and
|
|
42
|
+
// never on a template: the template form is Loadbare's, and holds the absent
|
|
43
|
+
// branch in the document so the hub holds nothing. Landing reaches inside it,
|
|
44
|
+
// so a branch that returns is already current; nothing else does.
|
|
45
|
+
export const ATTR_SHOW = "lb-show";
|
|
34
46
|
export const ATTR_VALUE = "lb-value";
|
|
35
47
|
// Stamped on a list scope by the shared row machinery: how many rows it is
|
|
36
48
|
// showing. Written by the hub and never bound to — it is derived from rows
|
|
@@ -146,8 +158,9 @@ export const HUB_TAG_NAME = "lb-hub";
|
|
|
146
158
|
// Stamped on the element that dispatched a request — the button, form, or
|
|
147
159
|
// widget itself — for the duration of its round trip. It carries no
|
|
148
160
|
// meaning beyond "in flight": a stylesheet dims the element, or a custom
|
|
149
|
-
// button widget watches it to disable itself
|
|
150
|
-
// and
|
|
161
|
+
// button widget watches it to disable itself. The hub sets and clears it,
|
|
162
|
+
// alongside aria-busy, and reads it in one place: a native action (a button
|
|
163
|
+
// or form, not a widget) still pending ignores being performed again.
|
|
151
164
|
export const ATTR_PENDING = "lb-pending";
|
|
152
165
|
// Stamped on the same element if that round trip failed — a non-2xx
|
|
153
166
|
// response, a thrown application error, or a timeout, all alike. Cleared at
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-constants.js","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"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","sourcesContent":["// Every hub attribute name lives here. Nothing writes one as a string\n// literal — not a widget, not a page host, not a test. See\n// docs/reference/data-binding.md.\n\n// --- Binding (docs/reference/data-binding.md) ---------------------------\n// One containment ladder: a list holds rows, a row holds cells. Both scope\n// attributes take a name the server declares, and a nested one of either\n// kind begins a new scope.\n\n// Scopes DOM children to a named set of rows.\nexport const ATTR_LIST = \"lb-list\";\n\n// Scopes DOM children to one named row.\n//\n// A single-row scope is read-only. Every write operation needs a key, and a\n// key exists only on a live row the hub stamped inside a list, so a declared\n// action is the only thing such a scope can send. An application that wants\n// a writable single row declares a list that answers with one row.\nexport const ATTR_ROW = \"lb-row\";\n\n// Named on a row template by the developer: the column that identifies a row\n// of the list. A property of the list rather than of the template — a list\n// without a fixed key column is meaningless — but written where the rows\n// land, because that is the only place the page says anything about them.\nexport const ATTR_KEY = \"lb-key\";\n\n// Stamped on a live row by the row machinery: that row's value of the column\n// ATTR_KEY names. Written by the hub, never by a developer, and read back by\n// whatever needs to know which row it is inside — a delete button, an update\n// form, a widget building its own request, or a stylesheet.\nexport const ATTR_KEY_VALUE = \"lb-key-value\";\n\n// The column an element displays.\n//\n// Its value is a column name and never a cell name. A cell has no name of\n// its own: it is identified by its row and its column, and the row arrives\n// from scope, so the column is all that is left to say. ATTR_KEY holds a\n// column name for the same reason.\nexport const ATTR_CELL = \"lb-cell\";\n\nexport const ATTR_VALUE = \"lb-value\";\n\n// Stamped on a list scope by the shared row machinery: how many rows it is\n// showing. Written by the hub and never bound to — it is derived from rows\n// the hub already knows, exactly as the section heading in lb-table is. A\n// list widget watches it, and it exists so that \"no members yet\" is a\n// stylesheet rule rather than a conditional in every list widget.\nexport const ATTR_ROW_COUNT = \"lb-row-count\";\n\n// --- Requests (docs/reference/data-binding.md) --------------------------\n// One event name, used by a widget reaching the server and by a widget\n// reaching an ancestor widget.\nexport const LB_EVENT_NAME = \"lb-request\";\n\n// Names what an interaction asks for.\n//\n// One attribute name, one wire field, one set of values: the request field\n// `action` carries this attribute's value verbatim, and nothing is\n// translated in between. On a native element the hub turns the element's own\n// event into the request — a form submits, anything else clicks; on a\n// widget, the widget builds it from whatever interaction it owns.\nexport const ATTR_ACTION = \"lb-action\";\n\n// --- Reserved names ----------------------------------------------------\n// Loadbare owns every value beginning with this prefix, in every lb-*\n// attribute: a name so prefixed is the hub's own (NAV_ROW), and an\n// lb-action so prefixed is an operation (below). The server refuses a page\n// that declares a query or an action beginning with it, so a new reserved\n// name can never collide with one an application already uses.\n//\n// It is also the whole of the wire discriminant. A request whose `action`\n// begins with it is one of the four operations; anything else is a name the\n// page declared under its requests. Nothing else tells them apart.\nexport const LB_RESERVED_PREFIX = \"lb-\";\n\n// On a <form> in a list scope: builds a new row on submit. The hub gathers\n// every lb-cell inside the form into a values map — no key, because there is\n// no row yet — and the form's own lb-list says which list it is inserting\n// into.\nexport const ACTION_ROW_INSERT = \"lb-row-insert\";\n\n// Deletes the row the element is inside. Needs no declared name: it takes\n// only the row's own lb-list/lb-key-value binding, which is all the server\n// needs to know which row and whether the operation is permitted on it.\nexport const ACTION_ROW_DELETE = \"lb-row-delete\";\n\n// On a <form> inside a live row: commits several cells of that row at once\n// on submit — the batch counterpart to a cell change. The hub gathers the\n// same values map, plus the row's own lb-key-value: a form that is a list's\n// row template root, or sits inside one, has both from its ancestors exactly\n// as a delete button does.\nexport const ACTION_ROW_UPDATE = \"lb-row-update\";\n\n// On a widget wrapping one control: commits that one cell when the control\n// changes. The hub never sends this itself — a native element has no widget\n// to decide what a change is — so a widget that carries it builds the\n// operation from its own lb-list/lb-key-value/lb-cell binding.\nexport const ACTION_CELL_CHANGE = \"lb-cell-change\";\n\n// Every reserved lb-action, in one place, so the builder can refuse an\n// unknown one and the hub can refuse to send one it does not implement.\nexport const LB_ACTIONS = [\n ACTION_ROW_INSERT,\n ACTION_ROW_DELETE,\n ACTION_ROW_UPDATE,\n ACTION_CELL_CHANGE,\n] as const;\n\n// --- Expansion (docs/reference/custom-elements.md) ---------------------\n// Marks the element in a definition whose children receive the authored\n// inner content. An attribute rather than an element because HTML content\n// models discard foreign elements inside <select> and <table>.\nexport const ATTR_SLOT = \"lb-slot\";\n\n// One name in two positions. In a definition it marks a destination; on an\n// authored <template> it names the destination that template is for, and the\n// template is unwrapped into it.\n//\n// It exists because the parser, not the content model, is the obstacle. A\n// <thead> written inside <lb-table> on a page is not in a table, so the\n// tokenizer drops the tag and keeps the text. A <template> survives anywhere\n// and its contents parse as if they were in a table, so wrapping the markup\n// is what gets it to expansion intact — and unwrapping it is what puts real\n// elements in the shipped document.\nexport const ATTR_TEMPLATE = \"lb-template\";\n\n// --- Host channel (docs/reference/chrome.md) ---------------------------\n// Only anchors carrying this are intercepted, so external links are never\n// hijacked.\nexport const ATTR_NAV_LINK = \"lb-nav-link\";\n\n// A page host ships as <template lb-page=\"<name>\">. An attribute rather than\n// an id: an id lives in the application's own namespace, where a page named\n// for something the chrome also ids would collide, and getElementById()\n// answers a collision by silently picking one.\nexport const ATTR_PAGE = \"lb-page\";\n\n// The hub's own row. Landed on every navigation, on any subtree inside the\n// hub that names it, through the same applyData() a server answer goes\n// through — so a chrome displays where the page is the way it displays\n// anything else. One row rather than a list, so a subtree binds it with\n// lb-row. A reserved name (LB_RESERVED_PREFIX): the server refuses to\n// declare a query so named.\nexport const NAV_ROW = \"lb-navigation\";\n\n// Its two columns. The label is the nav's: the text of the lb-nav-link\n// anchor whose href names the current page, and empty when no anchor does.\n// The URI is the path as the browser has it.\nexport const NAV_CELL_LABEL = \"page-label\";\nexport const NAV_CELL_URI = \"page-uri\";\n\n// Marks the chrome's own <dialog> for a page name that resolves to no\n// template — a routing miss discovered client-side, not an HTTP 404 (every\n// route gets the same 200 response; see docs/tutorials/010-pages-and-navigation.md).\n// The hub only opens it; what it says comes from NAV_ROW, which the dialog\n// consumes like any other subtree, by naming it in lb-row. Optional: an\n// application that declares none gets today's console.error and nothing\n// more. The builder requires the element it's on to be a <dialog>\n// (build/assemble.ts), since the hub calls showModal() on it.\nexport const ATTR_UNKNOWN_PAGE = \"lb-unknown-page\";\n\n// The hub's own tag. It is exempt from the rule every other LB-* tag\n// follows: expansion does not require it to have a definition (build/expand.ts),\n// and the client build does not require it to have a script of its own\n// (build/elements.ts) — it is the framework's live element, always present,\n// registered once by the package's main entry point, not a widget an\n// application discovers or declares.\nexport const HUB_TAG_NAME = \"lb-hub\";\n\n// --- Request lifecycle --------------------------------------------------\n// Stamped on the element that dispatched a request — the button, form, or\n// widget itself — for the duration of its round trip. It carries no\n// meaning beyond \"in flight\": a stylesheet dims the element, or a custom\n// button widget watches it to disable itself, but the hub itself only sets\n// and clears it.\nexport const ATTR_PENDING = \"lb-pending\";\n\n// Stamped on the same element if that round trip failed — a non-2xx\n// response, a thrown application error, or a timeout, all alike. Cleared at\n// the start of that element's next request. What (if anything) an\n// application shows for it is undecided; this is only the fact that it\n// happened.\nexport const ATTR_ERROR = \"lb-error\";\n\n// --- Wire --------------------------------------------------------------\n// One path, one method: everything the hub asks of the server is a POST to\n// this path. Navigation is therefore the only GET an application serves,\n// which is what leaves the page name space unrestricted.\nexport const LB_ENDPOINT = \"/lb\";\n\n// A client-side deadline for one round trip. fetch() has no deadline of its\n// own; past this the request is aborted and treated as a failure.\nexport const LB_REQUEST_TIMEOUT_MS = 10_000;\n"]}
|
|
1
|
+
{"version":3,"file":"lb-constants.js","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,2DAA2D;AAC3D,kCAAkC;AAElC,2EAA2E;AAC3E,2EAA2E;AAC3E,yEAAyE;AACzE,2BAA2B;AAE3B,8CAA8C;AAC9C,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAC;AAEnC,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,mEAAmE;AACnE,MAAM,CAAC,MAAM,QAAQ,GAAG,QAAQ,CAAC;AAEjC,6EAA6E;AAC7E,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,MAAM,CAAC,MAAM,QAAQ,GAAG,QAAQ,CAAC;AAEjC,6EAA6E;AAC7E,6EAA6E;AAC7E,6EAA6E;AAC7E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,cAAc,GAAG,cAAc,CAAC;AAE7C,kCAAkC;AAClC,EAAE;AACF,0EAA0E;AAC1E,2EAA2E;AAC3E,yEAAyE;AACzE,mCAAmC;AACnC,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAC;AAEnC,4EAA4E;AAC5E,8EAA8E;AAC9E,qDAAqD;AACrD,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,6EAA6E;AAC7E,8EAA8E;AAC9E,kEAAkE;AAClE,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAC;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAC;AAErC,2EAA2E;AAC3E,2EAA2E;AAC3E,0EAA0E;AAC1E,sEAAsE;AACtE,kEAAkE;AAClE,MAAM,CAAC,MAAM,cAAc,GAAG,cAAc,CAAC;AAE7C,2EAA2E;AAC3E,uEAAuE;AACvE,+BAA+B;AAC/B,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC;AAE1C,sCAAsC;AACtC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,6EAA6E;AAC7E,sEAAsE;AACtE,kEAAkE;AAClE,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC;AAEvC,0EAA0E;AAC1E,sEAAsE;AACtE,mEAAmE;AACnE,2EAA2E;AAC3E,0EAA0E;AAC1E,+DAA+D;AAC/D,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,mEAAmE;AACnE,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAExC,2EAA2E;AAC3E,6EAA6E;AAC7E,0EAA0E;AAC1E,QAAQ;AACR,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEjD,0EAA0E;AAC1E,2EAA2E;AAC3E,wEAAwE;AACxE,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEjD,2EAA2E;AAC3E,0EAA0E;AAC1E,4EAA4E;AAC5E,6EAA6E;AAC7E,2BAA2B;AAC3B,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEjD,2EAA2E;AAC3E,4EAA4E;AAC5E,sEAAsE;AACtE,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,gBAAgB,CAAC;AAEnD,uEAAuE;AACvE,wEAAwE;AACxE,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,iBAAiB;IACjB,iBAAiB;IACjB,iBAAiB;IACjB,kBAAkB;CACV,CAAC;AAEX,0EAA0E;AAC1E,wEAAwE;AACxE,0EAA0E;AAC1E,+DAA+D;AAC/D,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAC;AAEnC,2EAA2E;AAC3E,6EAA6E;AAC7E,iCAAiC;AACjC,EAAE;AACF,0EAA0E;AAC1E,wEAAwE;AACxE,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,oCAAoC;AACpC,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAE3C,0EAA0E;AAC1E,0EAA0E;AAC1E,YAAY;AACZ,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAE3C,6EAA6E;AAC7E,4EAA4E;AAC5E,wEAAwE;AACxE,+CAA+C;AAC/C,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAC;AAEnC,2EAA2E;AAC3E,uEAAuE;AACvE,uEAAuE;AACvE,wEAAwE;AACxE,sEAAsE;AACtE,4BAA4B;AAC5B,MAAM,CAAC,MAAM,OAAO,GAAG,eAAe,CAAC;AAEvC,uEAAuE;AACvE,2EAA2E;AAC3E,6CAA6C;AAC7C,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAC;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;AAEvC,sEAAsE;AACtE,2EAA2E;AAC3E,qFAAqF;AACrF,2EAA2E;AAC3E,wEAAwE;AACxE,wEAAwE;AACxE,kEAAkE;AAClE,8DAA8D;AAC9D,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;AAEnD,qEAAqE;AACrE,iFAAiF;AACjF,uEAAuE;AACvE,4EAA4E;AAC5E,qEAAqE;AACrE,qCAAqC;AACrC,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC;AAErC,2EAA2E;AAC3E,0EAA0E;AAC1E,oEAAoE;AACpE,yEAAyE;AACzE,0EAA0E;AAC1E,4EAA4E;AAC5E,sEAAsE;AACtE,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC;AAEzC,oEAAoE;AACpE,4EAA4E;AAC5E,kEAAkE;AAClE,uEAAuE;AACvE,YAAY;AACZ,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAC;AAErC,0EAA0E;AAC1E,2EAA2E;AAC3E,yEAAyE;AACzE,yDAAyD;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC;AAEjC,4EAA4E;AAC5E,kEAAkE;AAClE,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC","sourcesContent":["// Every hub attribute name lives here. Nothing writes one as a string\n// literal — not a widget, not a page host, not a test. See\n// docs/reference/data-binding.md.\n\n// --- Binding (docs/reference/data-binding.md) ---------------------------\n// One containment ladder: a list holds rows, a row holds cells. Both scope\n// attributes take a name the server declares, and a nested one of either\n// kind begins a new scope.\n\n// Scopes DOM children to a named set of rows.\nexport const ATTR_LIST = \"lb-list\";\n\n// Scopes DOM children to one named row.\n//\n// A single-row scope is read-only. Every write operation needs a key, and a\n// key exists only on a live row the hub stamped inside a list, so a declared\n// action is the only thing such a scope can send. An application that wants\n// a writable single row declares a list that answers with one row.\nexport const ATTR_ROW = \"lb-row\";\n\n// Named on a row template by the developer: the column that identifies a row\n// of the list. A property of the list rather than of the template — a list\n// without a fixed key column is meaningless — but written where the rows\n// land, because that is the only place the page says anything about them.\nexport const ATTR_KEY = \"lb-key\";\n\n// Stamped on a live row by the row machinery: that row's value of the column\n// ATTR_KEY names. Written by the hub, never by a developer, and read back by\n// whatever needs to know which row it is inside — a delete button, an update\n// form, a widget building its own request, or a stylesheet.\nexport const ATTR_KEY_VALUE = \"lb-key-value\";\n\n// The column an element displays.\n//\n// Its value is a column name and never a cell name. A cell has no name of\n// its own: it is identified by its row and its column, and the row arrives\n// from scope, so the column is all that is left to say. ATTR_KEY holds a\n// column name for the same reason.\nexport const ATTR_CELL = \"lb-cell\";\n\n// The column that decides whether an element is present: `null` and `false`\n// are off, and every other value is on. It binds by the same ancestor rule as\n// ATTR_CELL, and names a column for the same reason.\n//\n// An element that is off is moved into a `<template>` carrying this attribute\n// and the same column, standing where the element stood, and moved back out\n// when the column turns on. The builder ships every such element already\n// inside its template. A developer writes this attribute on an element and\n// never on a template: the template form is Loadbare's, and holds the absent\n// branch in the document so the hub holds nothing. Landing reaches inside it,\n// so a branch that returns is already current; nothing else does.\nexport const ATTR_SHOW = \"lb-show\";\n\nexport const ATTR_VALUE = \"lb-value\";\n\n// Stamped on a list scope by the shared row machinery: how many rows it is\n// showing. Written by the hub and never bound to — it is derived from rows\n// the hub already knows, exactly as the section heading in lb-table is. A\n// list widget watches it, and it exists so that \"no members yet\" is a\n// stylesheet rule rather than a conditional in every list widget.\nexport const ATTR_ROW_COUNT = \"lb-row-count\";\n\n// --- Requests (docs/reference/data-binding.md) --------------------------\n// One event name, used by a widget reaching the server and by a widget\n// reaching an ancestor widget.\nexport const LB_EVENT_NAME = \"lb-request\";\n\n// Names what an interaction asks for.\n//\n// One attribute name, one wire field, one set of values: the request field\n// `action` carries this attribute's value verbatim, and nothing is\n// translated in between. On a native element the hub turns the element's own\n// event into the request — a form submits, anything else clicks; on a\n// widget, the widget builds it from whatever interaction it owns.\nexport const ATTR_ACTION = \"lb-action\";\n\n// --- Reserved names ----------------------------------------------------\n// Loadbare owns every value beginning with this prefix, in every lb-*\n// attribute: a name so prefixed is the hub's own (NAV_ROW), and an\n// lb-action so prefixed is an operation (below). The server refuses a page\n// that declares a query or an action beginning with it, so a new reserved\n// name can never collide with one an application already uses.\n//\n// It is also the whole of the wire discriminant. A request whose `action`\n// begins with it is one of the four operations; anything else is a name the\n// page declared under its requests. Nothing else tells them apart.\nexport const LB_RESERVED_PREFIX = \"lb-\";\n\n// On a <form> in a list scope: builds a new row on submit. The hub gathers\n// every lb-cell inside the form into a values map — no key, because there is\n// no row yet — and the form's own lb-list says which list it is inserting\n// into.\nexport const ACTION_ROW_INSERT = \"lb-row-insert\";\n\n// Deletes the row the element is inside. Needs no declared name: it takes\n// only the row's own lb-list/lb-key-value binding, which is all the server\n// needs to know which row and whether the operation is permitted on it.\nexport const ACTION_ROW_DELETE = \"lb-row-delete\";\n\n// On a <form> inside a live row: commits several cells of that row at once\n// on submit — the batch counterpart to a cell change. The hub gathers the\n// same values map, plus the row's own lb-key-value: a form that is a list's\n// row template root, or sits inside one, has both from its ancestors exactly\n// as a delete button does.\nexport const ACTION_ROW_UPDATE = \"lb-row-update\";\n\n// On a widget wrapping one control: commits that one cell when the control\n// changes. The hub never sends this itself — a native element has no widget\n// to decide what a change is — so a widget that carries it builds the\n// operation from its own lb-list/lb-key-value/lb-cell binding.\nexport const ACTION_CELL_CHANGE = \"lb-cell-change\";\n\n// Every reserved lb-action, in one place, so the builder can refuse an\n// unknown one and the hub can refuse to send one it does not implement.\nexport const LB_ACTIONS = [\n ACTION_ROW_INSERT,\n ACTION_ROW_DELETE,\n ACTION_ROW_UPDATE,\n ACTION_CELL_CHANGE,\n] as const;\n\n// --- Expansion (docs/reference/custom-elements.md) ---------------------\n// Marks the element in a definition whose children receive the authored\n// inner content. An attribute rather than an element because HTML content\n// models discard foreign elements inside <select> and <table>.\nexport const ATTR_SLOT = \"lb-slot\";\n\n// One name in two positions. In a definition it marks a destination; on an\n// authored <template> it names the destination that template is for, and the\n// template is unwrapped into it.\n//\n// It exists because the parser, not the content model, is the obstacle. A\n// <thead> written inside <lb-table> on a page is not in a table, so the\n// tokenizer drops the tag and keeps the text. A <template> survives anywhere\n// and its contents parse as if they were in a table, so wrapping the markup\n// is what gets it to expansion intact — and unwrapping it is what puts real\n// elements in the shipped document.\nexport const ATTR_TEMPLATE = \"lb-template\";\n\n// --- Host channel (docs/reference/chrome.md) ---------------------------\n// Only anchors carrying this are intercepted, so external links are never\n// hijacked.\nexport const ATTR_NAV_LINK = \"lb-nav-link\";\n\n// A page host ships as <template lb-page=\"<name>\">. An attribute rather than\n// an id: an id lives in the application's own namespace, where a page named\n// for something the chrome also ids would collide, and getElementById()\n// answers a collision by silently picking one.\nexport const ATTR_PAGE = \"lb-page\";\n\n// The hub's own row. Landed on every navigation, on any subtree inside the\n// hub that names it, through the same applyData() a server answer goes\n// through — so a chrome displays where the page is the way it displays\n// anything else. One row rather than a list, so a subtree binds it with\n// lb-row. A reserved name (LB_RESERVED_PREFIX): the server refuses to\n// declare a query so named.\nexport const NAV_ROW = \"lb-navigation\";\n\n// Its two columns. The label is the nav's: the text of the lb-nav-link\n// anchor whose href names the current page, and empty when no anchor does.\n// The URI is the path as the browser has it.\nexport const NAV_CELL_LABEL = \"page-label\";\nexport const NAV_CELL_URI = \"page-uri\";\n\n// Marks the chrome's own <dialog> for a page name that resolves to no\n// template — a routing miss discovered client-side, not an HTTP 404 (every\n// route gets the same 200 response; see docs/tutorials/010-pages-and-navigation.md).\n// The hub only opens it; what it says comes from NAV_ROW, which the dialog\n// consumes like any other subtree, by naming it in lb-row. Optional: an\n// application that declares none gets today's console.error and nothing\n// more. The builder requires the element it's on to be a <dialog>\n// (build/assemble.ts), since the hub calls showModal() on it.\nexport const ATTR_UNKNOWN_PAGE = \"lb-unknown-page\";\n\n// The hub's own tag. It is exempt from the rule every other LB-* tag\n// follows: expansion does not require it to have a definition (build/expand.ts),\n// and the client build does not require it to have a script of its own\n// (build/elements.ts) — it is the framework's live element, always present,\n// registered once by the package's main entry point, not a widget an\n// application discovers or declares.\nexport const HUB_TAG_NAME = \"lb-hub\";\n\n// --- Request lifecycle --------------------------------------------------\n// Stamped on the element that dispatched a request — the button, form, or\n// widget itself — for the duration of its round trip. It carries no\n// meaning beyond \"in flight\": a stylesheet dims the element, or a custom\n// button widget watches it to disable itself. The hub sets and clears it,\n// alongside aria-busy, and reads it in one place: a native action (a button\n// or form, not a widget) still pending ignores being performed again.\nexport const ATTR_PENDING = \"lb-pending\";\n\n// Stamped on the same element if that round trip failed — a non-2xx\n// response, a thrown application error, or a timeout, all alike. Cleared at\n// the start of that element's next request. What (if anything) an\n// application shows for it is undecided; this is only the fact that it\n// happened.\nexport const ATTR_ERROR = \"lb-error\";\n\n// --- Wire --------------------------------------------------------------\n// One path, one method: everything the hub asks of the server is a POST to\n// this path. Navigation is therefore the only GET an application serves,\n// which is what leaves the page name space unrestricted.\nexport const LB_ENDPOINT = \"/lb\";\n\n// A client-side deadline for one round trip. fetch() has no deadline of its\n// own; past this the request is aborted and treated as a failure.\nexport const LB_REQUEST_TIMEOUT_MS = 10_000;\n"]}
|
package/dist/hub/lb-apply.d.ts
CHANGED
|
@@ -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
|
+
* Gathering finds cells through here. It does not reach into an absent
|
|
15
|
+
* branch, so a form reads back from the places a row lands that are showing.
|
|
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":"
|
|
1
|
+
{"version":3,"file":"lb-apply.d.ts","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"AAkCA,OAAO,EAEL,KAAK,OAAO,EAEZ,KAAK,UAAU,EACf,KAAK,GAAG,EACT,MAAM,qBAAqB,CAAC;AAW7B;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,OAAO,GACV,EAAE,IAAI,gBAAgB,GAAG,iBAAiB,GAAG,mBAAmB,CAMlE;AA8DD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAMjE;AAqED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAetD;AA6CD;;;;;;;;;;;;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"}
|
package/dist/hub/lb-apply.js
CHANGED
|
@@ -14,9 +14,18 @@
|
|
|
14
14
|
* never sees a whole result. Cloning the template, matching a row to the
|
|
15
15
|
* element already showing it, and filling that element are the same in every
|
|
16
16
|
* list, so no widget can get them wrong.
|
|
17
|
+
*
|
|
18
|
+
* A condition is the one other thing landing moves. An element whose
|
|
19
|
+
* `lb-show` column is off stands in the document inside a template, and
|
|
20
|
+
* landing reaches in there as it reaches everywhere else, so a branch that
|
|
21
|
+
* returns is already current. Nothing but landing reaches in.
|
|
17
22
|
*/
|
|
18
|
-
import { ATTR_CELL, ATTR_KEY, ATTR_KEY_VALUE, ATTR_LIST, ATTR_ROW, ATTR_ROW_COUNT, ATTR_VALUE, } from "../core/lb-constants.js";
|
|
23
|
+
import { ATTR_CELL, ATTR_KEY, ATTR_KEY_VALUE, ATTR_LIST, ATTR_ROW, ATTR_ROW_COUNT, ATTR_SHOW, ATTR_VALUE, } from "../core/lb-constants.js";
|
|
19
24
|
import { isPatch, } from "../core/lb-types.js";
|
|
25
|
+
/** The template holding an absent branch — see ATTR_SHOW. */
|
|
26
|
+
const PARKED = `template[${ATTR_SHOW}]`;
|
|
27
|
+
/** A DocumentFragment's nodeType. Tier 3 has no `Node` global to read it from. */
|
|
28
|
+
const FRAGMENT_NODE = 11;
|
|
20
29
|
/** The input types whose state is not their `value`, or cannot be set. */
|
|
21
30
|
const UNLANDED_INPUTS = ["checkbox", "radio", "file"];
|
|
22
31
|
/**
|
|
@@ -72,12 +81,19 @@ function land(el, value) {
|
|
|
72
81
|
*
|
|
73
82
|
* Both scope attributes scope their DOM children, and a nested one of either
|
|
74
83
|
* 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
|
|
76
|
-
*
|
|
77
|
-
*
|
|
84
|
+
* is in root's scope unless something between it and root carries one. The
|
|
85
|
+
* element's own attribute does not count: it names what the element
|
|
86
|
+
* displays, and where it belongs is its ancestors', so a picker carrying
|
|
87
|
+
* `lb-list` and `lb-cell` is a cell of the row around it. Root's own
|
|
88
|
+
* attribute does not count either: root is the scope being filled, whatever
|
|
89
|
+
* it carries.
|
|
90
|
+
*
|
|
91
|
+
* Root may be the content of a template holding an absent branch. The walk
|
|
92
|
+
* up from an element inside it stops at the top of that content, which has
|
|
93
|
+
* no parent, and the template it stands for was in scope already.
|
|
78
94
|
*/
|
|
79
95
|
function inScope(root, el) {
|
|
80
|
-
for (let node = el; node && node !== root;) {
|
|
96
|
+
for (let node = el.parentElement; node && node !== root;) {
|
|
81
97
|
if (node.hasAttribute(ATTR_LIST) || node.hasAttribute(ATTR_ROW)) {
|
|
82
98
|
return false;
|
|
83
99
|
}
|
|
@@ -85,8 +101,12 @@ function inScope(root, el) {
|
|
|
85
101
|
}
|
|
86
102
|
return true;
|
|
87
103
|
}
|
|
88
|
-
/**
|
|
89
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Root if it matches, then every descendant in root's scope that does.
|
|
106
|
+
* Gathering finds cells through here. It does not reach into an absent
|
|
107
|
+
* branch, so a form reads back from the places a row lands that are showing.
|
|
108
|
+
*/
|
|
109
|
+
export function within(root, selector) {
|
|
90
110
|
const found = root.matches(selector) ? [root] : [];
|
|
91
111
|
for (const el of root.querySelectorAll(selector)) {
|
|
92
112
|
if (inScope(root, el))
|
|
@@ -94,6 +114,72 @@ function within(root, selector) {
|
|
|
94
114
|
}
|
|
95
115
|
return found;
|
|
96
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Every descendant in root's scope that matches, absent branches included.
|
|
119
|
+
*
|
|
120
|
+
* `querySelectorAll` does not enter template content, so an element that is
|
|
121
|
+
* off would otherwise miss everything landed while it was away, and return
|
|
122
|
+
* stale or with its lists empty. A template holding one is in root's scope by
|
|
123
|
+
* the ordinary rule, and its content is searched as if it stood there.
|
|
124
|
+
*/
|
|
125
|
+
function below(root, selector) {
|
|
126
|
+
const found = [];
|
|
127
|
+
for (const el of root.querySelectorAll(selector)) {
|
|
128
|
+
if (inScope(root, el))
|
|
129
|
+
found.push(el);
|
|
130
|
+
}
|
|
131
|
+
for (const parked of root.querySelectorAll(PARKED)) {
|
|
132
|
+
if (inScope(root, parked)) {
|
|
133
|
+
found.push(...below(parked.content, selector));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
138
|
+
/** What `within` is for gathering, `reach` is for landing. */
|
|
139
|
+
function reach(root, selector) {
|
|
140
|
+
const found = below(root, selector);
|
|
141
|
+
return root.matches(selector) ? [root, ...found] : found;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Every element in a whole response's root that matches, absent branches
|
|
145
|
+
* included. A name is looked up this way before any scope is known, so
|
|
146
|
+
* nothing is filtered out by scope.
|
|
147
|
+
*/
|
|
148
|
+
function everywhere(root, selector) {
|
|
149
|
+
const found = [...root.querySelectorAll(selector)];
|
|
150
|
+
for (const parked of root.querySelectorAll(PARKED)) {
|
|
151
|
+
const content = parked.content;
|
|
152
|
+
found.push(...everywhere(content, selector));
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Put an element in the document or take it out, as its `lb-show` column
|
|
158
|
+
* says.
|
|
159
|
+
*
|
|
160
|
+
* Off is inside a template that stands where the element stood, carrying the
|
|
161
|
+
* same `lb-show`. The element is moved rather than rebuilt, so a widget keeps
|
|
162
|
+
* its instance and a control keeps what was typed into it. On moves it back
|
|
163
|
+
* and the template goes.
|
|
164
|
+
*
|
|
165
|
+
* Landing reaches into that template, so both it and the element it holds
|
|
166
|
+
* arrive here. The template decides; the element it holds, whose parent is
|
|
167
|
+
* the template's content, is left to it.
|
|
168
|
+
*/
|
|
169
|
+
function present(el, on) {
|
|
170
|
+
if (el.localName === "template") {
|
|
171
|
+
const branch = el.content.firstElementChild;
|
|
172
|
+
if (on && branch)
|
|
173
|
+
el.replaceWith(branch);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (on || el.parentNode?.nodeType === FRAGMENT_NODE)
|
|
177
|
+
return;
|
|
178
|
+
const parked = el.ownerDocument.createElement("template");
|
|
179
|
+
parked.setAttribute(ATTR_SHOW, el.getAttribute(ATTR_SHOW));
|
|
180
|
+
el.replaceWith(parked);
|
|
181
|
+
parked.content.append(el);
|
|
182
|
+
}
|
|
97
183
|
/**
|
|
98
184
|
* Fill one scope from one row.
|
|
99
185
|
*
|
|
@@ -104,22 +190,33 @@ function within(root, selector) {
|
|
|
104
190
|
*/
|
|
105
191
|
export function applyRow(root, row) {
|
|
106
192
|
for (const [column, value] of Object.entries(row)) {
|
|
107
|
-
for (const el of
|
|
193
|
+
for (const el of reach(root, `[${ATTR_CELL}="${column}"]`)) {
|
|
108
194
|
land(el, value);
|
|
109
195
|
}
|
|
196
|
+
// Only null and false are off. A string is never read, so "false" is on,
|
|
197
|
+
// and the query spells a condition as a boolean or a null.
|
|
198
|
+
//
|
|
199
|
+
// Never the root's own: a scope's condition is a cell of the row around
|
|
200
|
+
// it, the way its lb-cell is, and a row template's root carries none.
|
|
201
|
+
const on = value !== null && value !== false;
|
|
202
|
+
for (const el of below(root, `[${ATTR_SHOW}="${column}"]`)) {
|
|
203
|
+
present(el, on);
|
|
204
|
+
}
|
|
110
205
|
}
|
|
111
206
|
}
|
|
112
207
|
/**
|
|
113
208
|
* The row template: the one the developer wrote inside this list scope, or
|
|
114
209
|
* null when this scope shows nothing. A template inside a nested scope is
|
|
115
|
-
* that scope's
|
|
210
|
+
* that scope's, and a template holding an absent branch is not a row
|
|
211
|
+
* template: rows land before the row template, so one inside the first live
|
|
212
|
+
* row comes first in document order.
|
|
116
213
|
*
|
|
117
214
|
* A scope with no template is bound to the list without displaying it.
|
|
118
215
|
* That is not a mistake, so it is silent. A template that does not name its
|
|
119
216
|
* key column is a mistake, and says so.
|
|
120
217
|
*/
|
|
121
218
|
function templateIn(scope) {
|
|
122
|
-
const template = within(scope,
|
|
219
|
+
const template = within(scope, `template:not([${ATTR_SHOW}])`)[0];
|
|
123
220
|
if (!template)
|
|
124
221
|
return null;
|
|
125
222
|
if (!template.getAttribute(ATTR_KEY)) {
|
|
@@ -134,13 +231,13 @@ function templateIn(scope) {
|
|
|
134
231
|
* carries one: the template names the key column under a different name.
|
|
135
232
|
*
|
|
136
233
|
* 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,
|
|
138
|
-
*
|
|
234
|
+
* names what it displays, not which list it is a row of, which is the same
|
|
235
|
+
* test a cell gets.
|
|
139
236
|
*/
|
|
140
237
|
function showing(scope) {
|
|
141
238
|
const rows = new Map();
|
|
142
239
|
for (const el of scope.querySelectorAll(`[${ATTR_KEY_VALUE}]`)) {
|
|
143
|
-
if (inScope(scope, el
|
|
240
|
+
if (inScope(scope, el)) {
|
|
144
241
|
rows.set(el.getAttribute(ATTR_KEY_VALUE), el);
|
|
145
242
|
}
|
|
146
243
|
}
|
|
@@ -230,8 +327,8 @@ export function applyList(scope, result) {
|
|
|
230
327
|
/** Land a whole response. Every result arrives through here. */
|
|
231
328
|
export function applyData(root, data) {
|
|
232
329
|
for (const [name, result] of Object.entries(data)) {
|
|
233
|
-
const lists = root
|
|
234
|
-
const rows = root
|
|
330
|
+
const lists = everywhere(root, `[${ATTR_LIST}="${name}"]`);
|
|
331
|
+
const rows = everywhere(root, `[${ATTR_ROW}="${name}"]`);
|
|
235
332
|
if (lists.length === 0 && rows.length === 0) {
|
|
236
333
|
console.warn(`lb-hub: no scope for '${name}', skipping`);
|
|
237
334
|
continue;
|