@loadbare/app 0.23.0 → 0.24.0

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.
Files changed (38) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +28 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/core/lb-constants.d.ts +8 -5
  5. package/dist/core/lb-constants.d.ts.map +1 -1
  6. package/dist/core/lb-constants.js +20 -11
  7. package/dist/core/lb-constants.js.map +1 -1
  8. package/dist/core/lb-types.d.ts +19 -3
  9. package/dist/core/lb-types.d.ts.map +1 -1
  10. package/dist/core/lb-types.js +35 -5
  11. package/dist/core/lb-types.js.map +1 -1
  12. package/dist/hub/lb-apply.d.ts +18 -22
  13. package/dist/hub/lb-apply.d.ts.map +1 -1
  14. package/dist/hub/lb-apply.js +115 -51
  15. package/dist/hub/lb-apply.js.map +1 -1
  16. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  17. package/dist/hub/lb-hub.browser.js +15 -22
  18. package/dist/hub/lb-hub.browser.js.map +1 -1
  19. package/dist/server/lb-server.d.ts +2 -2
  20. package/dist/server/lb-server.d.ts.map +1 -1
  21. package/dist/server/lb-server.js +29 -9
  22. package/dist/server/lb-server.js.map +1 -1
  23. package/docs/comparison.md +11 -11
  24. package/docs/miswarts.md +4 -3
  25. package/docs/reference/chrome.md +42 -0
  26. package/docs/reference/custom-elements.md +9 -4
  27. package/docs/reference/data-binding.md +79 -61
  28. package/docs/reference/names.md +10 -7
  29. package/docs/reference/page-files.md +8 -4
  30. package/docs/testing.md +3 -3
  31. package/package.json +1 -1
  32. package/skills/loadbare-app/SKILL.md +23 -12
  33. package/skills/loadbare-app/references/chrome.md +42 -0
  34. package/skills/loadbare-app/references/custom-elements.md +9 -4
  35. package/skills/loadbare-app/references/data-binding.md +79 -61
  36. package/skills/loadbare-app/references/miswarts.md +4 -3
  37. package/skills/loadbare-app/references/names.md +10 -7
  38. package/skills/loadbare-app/references/page-files.md +8 -4
@@ -4,6 +4,18 @@ import { FILTER_OPERATORS, LB_RESERVED_PREFIX, LB_ROW_REQUESTS, ORDER_DESCENDING
4
4
  export function isPatch(result) {
5
5
  return !Array.isArray(result);
6
6
  }
7
+ /**
8
+ * The query parms of a URL as a row of `lb-url`: a parm the URL carries
9
+ * once is a string, and one it repeats is every value it carries, in order.
10
+ */
11
+ export function parmsRow(parms) {
12
+ const row = {};
13
+ for (const name of new Set(parms.keys())) {
14
+ const all = parms.getAll(name);
15
+ row[name] = all.length === 1 ? all[0] : all;
16
+ }
17
+ return row;
18
+ }
7
19
  /** Whether a name is one of the requests Loadbare provides. */
8
20
  export function isRowRequest(name) {
9
21
  return LB_ROW_REQUESTS.includes(name);
@@ -21,10 +33,27 @@ export function isRowRequest(name) {
21
33
  export function withQueryParms(search, parms) {
22
34
  const next = new URLSearchParams(search);
23
35
  for (const [name, value] of Object.entries(parms)) {
24
- if (value === "")
25
- next.delete(name);
26
- else
27
- next.set(name, value);
36
+ if (!Array.isArray(value)) {
37
+ if (value === "")
38
+ next.delete(name);
39
+ else
40
+ next.set(name, value);
41
+ continue;
42
+ }
43
+ // Several values repeat the parm, as an HTML form sends them. Values
44
+ // already there are left where they stand, so the URL is unchanged.
45
+ const kept = value.filter((v) => v !== "");
46
+ const had = next.getAll(name);
47
+ if (kept.length === had.length && kept.every((v, i) => v === had[i])) {
48
+ continue;
49
+ }
50
+ if (kept.length === 1) {
51
+ next.set(name, kept[0]);
52
+ continue;
53
+ }
54
+ next.delete(name);
55
+ for (const v of kept)
56
+ next.append(name, v);
28
57
  }
29
58
  return next;
30
59
  }
@@ -224,10 +253,11 @@ export function passes(value, terms, type) {
224
253
  */
225
254
  export function nextLocation(page, parms, row) {
226
255
  const named = {};
256
+ const text = (value) => value === null || value === undefined ? "" : String(value);
227
257
  for (const [name, value] of Object.entries(row)) {
228
258
  if (name.startsWith(LB_RESERVED_PREFIX))
229
259
  continue;
230
- named[name] = value === null || value === undefined ? "" : String(value);
260
+ named[name] = Array.isArray(value) ? value.map(text) : text(value);
231
261
  }
232
262
  const path = row[URL_COLUMN_PATH];
233
263
  const next = typeof path === "string" ? pageNameFor(path) : page;
@@ -1 +1 @@
1
- {"version":3,"file":"lb-types.js","sourceRoot":"","sources":["../../core/lb-types.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\n// The wire vocabulary. Shared by browser and server.\n\nimport {\n COLUMN_TYPES,\n KIND_ROW,\n FILTER_OPERATORS,\n KIND_ROWS,\n LB_RESERVED_PREFIX,\n LB_ROW_REQUESTS,\n ORDER_DESCENDING,\n URL_COLUMN_PATH,\n} from \"./lb-constants.js\";\n\n/**\n * One row: its columns, by name.\n *\n * A value is whatever the server serialized. The hub knows nothing about its\n * type and enforces nothing: it hands each value to the browser untouched,\n * and the browser renders it as it sees fit. What a number, a date or a null\n * should look like is the application's decision, made in the query.\n */\nexport type Row = Record<string, unknown>;\n\n/**\n * Part of a `rows` query's answer — see docs/reference/server.md, \"Queries\".\n *\n * Rows named here are added or updated, keys in `drop` are removed, and\n * anything unnamed is left alone. The hub places what it names by the\n * query's order. Add and remove are one result rather than two because the\n * interesting cases are both at once — a row whose sort key changed has to\n * move, a swap is one out and one in — and two messages would paint the\n * intermediate state.\n */\nexport interface Patch {\n rows?: Row[];\n drop?: unknown[];\n}\n\n/** A type a `rows` query may declare for a column — see COLUMN_TYPES. */\nexport type ColumnType = (typeof COLUMN_TYPES)[number];\n\n/** The types a `rows` query declares for its columns, by column. */\nexport type ColumnTypes = Record<string, ColumnType>;\n\n/** A query's kind, declared by the server and carried by every answer. */\nexport type Kind = typeof KIND_ROW | typeof KIND_ROWS;\n\n/**\n * What a `rows` query answers with: all rows, or a patch. `Array.isArray`\n * tells the two apart. All rows arrive in the order the server chose, which\n * is the order they show in when the query declares none.\n */\nexport type RowsResult = Row[] | Patch;\n\nexport function isPatch(result: RowsResult): result is Patch {\n return !Array.isArray(result);\n}\n\n/**\n * What any one query answers with, as the application writes it. A `row`\n * query that finds no row answers `null`.\n */\nexport type HubResult = Row | RowsResult | null;\n\n/**\n * What application code hands the engine: answers by query name. The engine\n * turns each into a response item, adding the kind and key the query\n * declared.\n */\nexport type HubData = Record<string, HubResult>;\n\n/**\n * One query's answer on the wire: its name, its kind, its key's column name,\n * and exactly one of a row, all rows, or a patch. A patch answers a `rows`\n * query only.\n *\n * A `rows` query also carries the order and column types it declared, once,\n * on the answer that loads the page. The hub keeps them with the answer for\n * every answer after it.\n */\nexport interface ResponseItem {\n query: string;\n kind: Kind;\n key: string;\n row?: Row | null;\n rows?: Row[];\n patch?: Patch;\n order?: string;\n types?: ColumnTypes;\n}\n\n/** Every response the server sends: a page load and a request alike. */\nexport type HubResponse = ResponseItem[];\n\n/**\n * Every request has one shape. The name picks the handler; the rest is\n * present when the hub found it.\n */\nexport interface HubRequest {\n name: string;\n query?: string;\n key?: string;\n /**\n * The rows a request acts on together, by key: those the user selected,\n * for a request that names no one row. Never sent beside `key`.\n */\n keys?: string[];\n values?: Record<string, string>;\n /**\n * The rows of the queries nested in a form, by query name, each with the\n * values of its own controls: a set of rows gathered beside the form's\n * own values.\n */\n rows?: Record<string, GatheredRow[]>;\n}\n\n/** One nested row a form gathered: its key, and its controls' values. */\nexport interface GatheredRow {\n key: string;\n values: Record<string, string>;\n}\n\n/**\n * The detail of the `lb-request-done` event: the request as the hub\n * completed and sent it, and what came of it. `items` is every response\n * item that landed because of it, in the order it landed. `error` is set\n * when the request's own round trip failed, and then nothing landed.\n */\nexport interface HubRequestDone {\n request: HubRequest;\n items: ResponseItem[];\n error?: Error;\n /** Set beside `error` when the handler refused the request. */\n refusal?: HubRefusal;\n}\n\n/**\n * Why a handler refused a request, in words written for the user: a\n * message for the request as a whole, a message per column the request\n * gathered, or both. Sent with status 422; any other failure sends nothing.\n */\nexport interface HubRefusal {\n message?: string;\n columns?: Record<string, string>;\n}\n\n/** The fields a handler receives: the request, less its name. */\nexport type RequestFields = Omit<HubRequest, \"name\">;\n\n/** Whether a name is one of the requests Loadbare provides. */\nexport function isRowRequest(name: string): boolean {\n return (LB_ROW_REQUESTS as readonly string[]).includes(name);\n}\n\n/**\n * A query string with some parms set, and the rest as they were. An empty\n * value takes its parm out, since empty is every parm's default and a\n * default is left out of the URL.\n *\n * One rule on both sides of the wire: the hub applies it to the address bar,\n * and the server to the query string it loads the page at after a request\n * returned `lb-url`. The two must agree, or what the page shows would differ\n * from what the address bar says.\n */\nexport function withQueryParms(\n search: URLSearchParams,\n parms: Record<string, string>,\n): URLSearchParams {\n const next = new URLSearchParams(search);\n for (const [name, value] of Object.entries(parms)) {\n if (value === \"\") next.delete(name);\n else next.set(name, value);\n }\n return next;\n}\n\n/**\n * A path names a page by its stub, and nothing finer. The bare path resolves\n * to `index`, the same convention a static file server follows.\n */\nexport function pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/** One term of an order: a column, and whether it sorts descending. */\nexport interface OrderTerm {\n column: string;\n descending: boolean;\n}\n\n/**\n * An order's terms, from its text: columns separated by commas, each\n * descending when it begins with `-`. Throws on a term that names no column.\n * An empty text is no order.\n */\nexport function parseOrder(text: string): OrderTerm[] {\n if (text.trim() === \"\") return [];\n return text.split(\",\").map((raw) => {\n const term = raw.trim();\n const descending = term.startsWith(ORDER_DESCENDING);\n const column = descending ? term.slice(ORDER_DESCENDING.length) : term;\n if (column === \"\" || column.startsWith(ORDER_DESCENDING)) {\n throw new Error(`the order '${text}' has a term naming no column`);\n }\n return { column, descending };\n });\n}\n\n/** Strings compare as the user's language orders them. */\nconst collator = new Intl.Collator();\n\n/** How a value sorts among values of other types: by its JSON type. */\nfunction rank(value: unknown): number {\n switch (typeof value) {\n case \"boolean\":\n return 0;\n case \"number\":\n return 1;\n case \"string\":\n return 2;\n default:\n return 3;\n }\n}\n\n/**\n * Compare two column values, ascending.\n *\n * A value compares by its JSON type, and nothing is guessed: numbers\n * numerically, booleans false before true, strings as the user's language\n * orders them. A number that arrives as a string sorts as a string, so a\n * numeric sort column is sent as a JSON number. Null and a missing value\n * sort after everything, as Postgres sorts them ascending, and so first\n * descending.\n */\nexport function compareValues(a: unknown, b: unknown): number {\n const missingA = a === null || a === undefined;\n const missingB = b === null || b === undefined;\n if (missingA || missingB) return missingA === missingB ? 0 : missingA ? 1 : -1;\n const ra = rank(a);\n const rb = rank(b);\n if (ra !== rb) return ra - rb;\n if (typeof a === \"number\") return a - (b as number);\n if (typeof a === \"boolean\") return Number(a) - Number(b);\n if (typeof a === \"string\") return collator.compare(a, b as string);\n return collator.compare(JSON.stringify(a), JSON.stringify(b));\n}\n\n/**\n * A value read as a declared column type: a number for `number`, a boolean\n * for `boolean`, text for `text`. A value that is no value of its type reads\n * as missing, and compares as a null does. A column with no declared type\n * reads as it arrived.\n */\nexport function readAs(value: unknown, type: ColumnType | undefined): unknown {\n if (type === undefined || value === null || value === undefined) return value;\n switch (type) {\n case \"number\": {\n if (typeof value === \"number\") return value;\n if (typeof value !== \"string\" || value.trim() === \"\") return undefined;\n const n = Number(value);\n return Number.isNaN(n) ? undefined : n;\n }\n case \"boolean\":\n if (typeof value === \"boolean\") return value;\n return value === \"true\" ? true : value === \"false\" ? false : undefined;\n default:\n return typeof value === \"object\" ? JSON.stringify(value) : String(value);\n }\n}\n\n/** Compare two rows by an order's terms, each column read as its type. */\nexport function compareRows(\n a: Row,\n b: Row,\n terms: OrderTerm[],\n types: ColumnTypes = {},\n): number {\n for (const { column, descending } of terms) {\n const c = compareValues(\n readAs(a[column], types[column]),\n readAs(b[column], types[column]),\n );\n if (c !== 0) return descending ? -c : c;\n }\n return 0;\n}\n\n/** A filter's operator — see FILTER_OPERATORS. */\nexport type FilterOperator = (typeof FILTER_OPERATORS)[number];\n\n/** One term of a filter: an operator, and the value it compares with. */\nexport interface FilterTerm {\n operator: FilterOperator;\n value: string;\n}\n\n/** Where a filter's next term begins: whitespace before an operator. */\nconst NEXT_TERM = new RegExp(\n `\\\\s+(?=${FILTER_OPERATORS.map((op) => op.replace(/[$^*]/g, \"\\\\$&\")).join(\"|\")})`,\n);\n\n/**\n * A filter's terms, from its text: each an operator and the value after it,\n * a new term beginning wherever whitespace comes before an operator, so a\n * value may hold spaces. Text with no operator before it is `*=`. A term\n * with no value is not yet a term, and is left out, so a filter being typed\n * filters nothing until it says something. Nothing is evaluated.\n */\nexport function parseFilter(text: string): FilterTerm[] {\n const terms: FilterTerm[] = [];\n for (const raw of text.trim().split(NEXT_TERM)) {\n const operator =\n FILTER_OPERATORS.find((op) => raw.startsWith(op)) ?? \"*=\";\n const value = raw.slice(raw.startsWith(operator) ? operator.length : 0);\n if (value.trim() !== \"\") terms.push({ operator, value: value.trim() });\n }\n return terms;\n}\n\n/**\n * The filter's value as the column's own type, so a comparison is the one\n * the order makes: a number for a number, a boolean for a boolean, text for\n * anything else. Undefined when the text is no value of that type.\n */\nfunction asTypeOf(text: string, like: unknown): unknown {\n if (typeof like === \"number\") {\n const n = Number(text);\n return text.trim() === \"\" || Number.isNaN(n) ? undefined : n;\n }\n if (typeof like === \"boolean\") {\n return text === \"true\" ? true : text === \"false\" ? false : undefined;\n }\n return text;\n}\n\n/**\n * Whether a column's value passes one term.\n *\n * `^=`, `$=` and `*=` read the value as text, ignoring case: it begins with,\n * ends with or contains the term's value. The rest compare as the order\n * compares, with the column read as its declared type, if it has one, and\n * the term's value read as the column's type. A missing value, and one that\n * is no value of its declared type, passes only `!=`.\n */\nexport function passesTerm(\n value: unknown,\n { operator, value: text }: FilterTerm,\n type?: ColumnType,\n): boolean {\n if (value === null || value === undefined) return operator === \"!=\";\n if (operator === \"^=\" || operator === \"$=\" || operator === \"*=\") {\n const have = String(value).toLocaleLowerCase();\n const want = text.toLocaleLowerCase();\n if (operator === \"^=\") return have.startsWith(want);\n if (operator === \"$=\") return have.endsWith(want);\n return have.includes(want);\n }\n const read = readAs(value, type);\n const typed = read === undefined ? undefined : asTypeOf(text, read);\n if (typed === undefined) return operator === \"!=\";\n const c = compareValues(read, typed);\n switch (operator) {\n case \"=\":\n return c === 0;\n case \"!=\":\n return c !== 0;\n case \"<\":\n return c < 0;\n case \"<=\":\n return c <= 0;\n case \">\":\n return c > 0;\n default:\n return c >= 0;\n }\n}\n\n/** Whether a column's value passes every term of a filter. */\nexport function passes(\n value: unknown,\n terms: FilterTerm[],\n type?: ColumnType,\n): boolean {\n return terms.every((term) => passesTerm(value, term, type));\n}\n\n/**\n * Where a row for `lb-url` leads, from a page and its query parms.\n *\n * A column whose name begins with `lb-` is Loadbare's, and never a query\n * parm; every other column is one. An `lb-path` naming another page enters\n * it with only the parms the row names. Otherwise the named parms are set,\n * or taken out when empty, and the rest are kept.\n *\n * One rule on both sides of the wire: the server loads the page it names,\n * and the hub writes the URL it names. The two must agree, or what the page\n * shows would differ from what the address bar says.\n */\nexport function nextLocation(\n page: string,\n parms: URLSearchParams,\n row: Row,\n): { page: string; parms: URLSearchParams; moved: boolean } {\n const named: Record<string, string> = {};\n for (const [name, value] of Object.entries(row)) {\n if (name.startsWith(LB_RESERVED_PREFIX)) continue;\n named[name] = value === null || value === undefined ? \"\" : String(value);\n }\n const path = row[URL_COLUMN_PATH];\n const next = typeof path === \"string\" ? pageNameFor(path) : page;\n const moved = next !== page;\n return {\n page: next,\n parms: withQueryParms(moved ? new URLSearchParams() : parms, named),\n moved,\n };\n}\n\n/**\n * What the hub tells a custom element it did, in its own terms rather than\n * the browser's: a custom element implements any of these in place of\n * `connectedCallback` and its kin, which the builder refuses.\n *\n * Each is a method on an element the hub does not own, so it carries the\n * `lb` prefix Loadbare reserves for exactly that — see\n * docs/reference/custom-elements.md.\n */\nexport interface HubLifecycle {\n /**\n * Called once, after the hub has placed the element and every column has\n * landed on it. An element under an `lb-show` that is off is placed inside\n * its template, so it may not be rendered.\n */\n lbAttached?(): void;\n /**\n * Called once after a result has landed on an element holding rows, with\n * every row in place and every stamp set. The hub reconciles and places\n * every row itself, by the query's order, so a custom element reacts to\n * rows and never places them.\n */\n lbRowsLanded?(): void;\n /**\n * Called once a request committed by this element, or by one inside it,\n * has finished, innermost first and before `lb-request-done` is\n * dispatched. `origin` is the element that committed.\n */\n lbRequestDone?(detail: HubRequestDone, origin: Element): void;\n /** Called once, after the hub has removed the element. */\n lbDetached?(): void;\n}\n"]}
1
+ {"version":3,"file":"lb-types.js","sourceRoot":"","sources":["../../core/lb-types.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\n// The wire vocabulary. Shared by browser and server.\n\nimport {\n COLUMN_TYPES,\n KIND_ROW,\n FILTER_OPERATORS,\n KIND_ROWS,\n LB_RESERVED_PREFIX,\n LB_ROW_REQUESTS,\n ORDER_DESCENDING,\n URL_COLUMN_PATH,\n} from \"./lb-constants.js\";\n\n/**\n * One row: its columns, by name.\n *\n * A value is whatever the server serialized. The hub knows nothing about its\n * type and enforces nothing: it hands each value to the browser untouched,\n * and the browser renders it as it sees fit. What a number, a date or a null\n * should look like is the application's decision, made in the query.\n */\nexport type Row = Record<string, unknown>;\n\n/**\n * Part of a `rows` query's answer — see docs/reference/server.md, \"Queries\".\n *\n * Rows named here are added or updated, keys in `drop` are removed, and\n * anything unnamed is left alone. The hub places what it names by the\n * query's order. Add and remove are one result rather than two because the\n * interesting cases are both at once — a row whose sort key changed has to\n * move, a swap is one out and one in — and two messages would paint the\n * intermediate state.\n */\nexport interface Patch {\n rows?: Row[];\n drop?: unknown[];\n}\n\n/** A type a `rows` query may declare for a column — see COLUMN_TYPES. */\nexport type ColumnType = (typeof COLUMN_TYPES)[number];\n\n/** The types a `rows` query declares for its columns, by column. */\nexport type ColumnTypes = Record<string, ColumnType>;\n\n/** A query's kind, declared by the server and carried by every answer. */\nexport type Kind = typeof KIND_ROW | typeof KIND_ROWS;\n\n/**\n * What a `rows` query answers with: all rows, or a patch. `Array.isArray`\n * tells the two apart. All rows arrive in the order the server chose, which\n * is the order they show in when the query declares none.\n */\nexport type RowsResult = Row[] | Patch;\n\nexport function isPatch(result: RowsResult): result is Patch {\n return !Array.isArray(result);\n}\n\n/**\n * What any one query answers with, as the application writes it. A `row`\n * query that finds no row answers `null`.\n */\nexport type HubResult = Row | RowsResult | null;\n\n/**\n * What application code hands the engine: answers by query name. The engine\n * turns each into a response item, adding the kind and key the query\n * declared.\n */\nexport type HubData = Record<string, HubResult>;\n\n/**\n * One query's answer on the wire: its name, its kind, its key's column name,\n * and exactly one of a row, all rows, or a patch. A patch answers a `rows`\n * query only.\n *\n * A `rows` query also carries the order and column types it declared, once,\n * on the answer that loads the page. The hub keeps them with the answer for\n * every answer after it.\n */\nexport interface ResponseItem {\n query: string;\n kind: Kind;\n key: string;\n row?: Row | null;\n rows?: Row[];\n patch?: Patch;\n order?: string;\n types?: ColumnTypes;\n}\n\n/** Every response the server sends: a page load and a request alike. */\nexport type HubResponse = ResponseItem[];\n\n/**\n * Every request has one shape. The name picks the handler; the rest is\n * present when the hub found it.\n */\nexport interface HubRequest {\n name: string;\n query?: string;\n key?: string;\n /**\n * The rows a request acts on together, by key: those the user selected,\n * for a request that names no one row. Never sent beside `key`.\n */\n keys?: string[];\n /**\n * The gathered controls' values, by column. A column of `lb-url` may hold\n * several, as a URL repeats a parm; every other column holds one.\n */\n values?: Record<string, ParmValue>;\n /**\n * The rows of the queries nested in a form, by query name, each with the\n * values of its own controls: a set of rows gathered beside the form's\n * own values.\n */\n rows?: Record<string, GatheredRow[]>;\n}\n\n/** One nested row a form gathered: its key, and its controls' values. */\nexport interface GatheredRow {\n key: string;\n values: Record<string, string>;\n}\n\n/**\n * What a control gathers: one value, or several from a control that holds\n * several, which only a column of `lb-url` takes.\n */\nexport type ParmValue = string | string[];\n\n/**\n * The query parms of a URL as a row of `lb-url`: a parm the URL carries\n * once is a string, and one it repeats is every value it carries, in order.\n */\nexport function parmsRow(parms: URLSearchParams): Row {\n const row: Row = {};\n for (const name of new Set(parms.keys())) {\n const all = parms.getAll(name);\n row[name] = all.length === 1 ? all[0] : all;\n }\n return row;\n}\n\n/**\n * The detail of the `lb-request-done` event: the request as the hub\n * completed and sent it, and what came of it. `items` is every response\n * item that landed because of it, in the order it landed. `error` is set\n * when the request's own round trip failed, and then nothing landed.\n */\nexport interface HubRequestDone {\n request: HubRequest;\n items: ResponseItem[];\n error?: Error;\n /** Set beside `error` when the handler refused the request. */\n refusal?: HubRefusal;\n}\n\n/**\n * Why a handler refused a request, in words written for the user: a\n * message for the request as a whole, a message per column the request\n * gathered, or both. Sent with status 422; any other failure sends nothing.\n */\nexport interface HubRefusal {\n message?: string;\n columns?: Record<string, string>;\n}\n\n/** The fields a handler receives: the request, less its name. */\nexport type RequestFields = Omit<HubRequest, \"name\" | \"values\"> & {\n values?: Record<string, string>;\n};\n\n/** Whether a name is one of the requests Loadbare provides. */\nexport function isRowRequest(name: string): boolean {\n return (LB_ROW_REQUESTS as readonly string[]).includes(name);\n}\n\n/**\n * A query string with some parms set, and the rest as they were. An empty\n * value takes its parm out, since empty is every parm's default and a\n * default is left out of the URL.\n *\n * One rule on both sides of the wire: the hub applies it to the address bar,\n * and the server to the query string it loads the page at after a request\n * returned `lb-url`. The two must agree, or what the page shows would differ\n * from what the address bar says.\n */\nexport function withQueryParms(\n search: URLSearchParams,\n parms: Record<string, ParmValue>,\n): URLSearchParams {\n const next = new URLSearchParams(search);\n for (const [name, value] of Object.entries(parms)) {\n if (!Array.isArray(value)) {\n if (value === \"\") next.delete(name);\n else next.set(name, value);\n continue;\n }\n // Several values repeat the parm, as an HTML form sends them. Values\n // already there are left where they stand, so the URL is unchanged.\n const kept = value.filter((v) => v !== \"\");\n const had = next.getAll(name);\n if (kept.length === had.length && kept.every((v, i) => v === had[i])) {\n continue;\n }\n if (kept.length === 1) {\n next.set(name, kept[0]!);\n continue;\n }\n next.delete(name);\n for (const v of kept) next.append(name, v);\n }\n return next;\n}\n\n/**\n * A path names a page by its stub, and nothing finer. The bare path resolves\n * to `index`, the same convention a static file server follows.\n */\nexport function pageNameFor(path: string): string {\n const name = path.replace(/^\\/+|\\/+$/g, \"\");\n return name === \"\" ? \"index\" : name;\n}\n\n/** One term of an order: a column, and whether it sorts descending. */\nexport interface OrderTerm {\n column: string;\n descending: boolean;\n}\n\n/**\n * An order's terms, from its text: columns separated by commas, each\n * descending when it begins with `-`. Throws on a term that names no column.\n * An empty text is no order.\n */\nexport function parseOrder(text: string): OrderTerm[] {\n if (text.trim() === \"\") return [];\n return text.split(\",\").map((raw) => {\n const term = raw.trim();\n const descending = term.startsWith(ORDER_DESCENDING);\n const column = descending ? term.slice(ORDER_DESCENDING.length) : term;\n if (column === \"\" || column.startsWith(ORDER_DESCENDING)) {\n throw new Error(`the order '${text}' has a term naming no column`);\n }\n return { column, descending };\n });\n}\n\n/** Strings compare as the user's language orders them. */\nconst collator = new Intl.Collator();\n\n/** How a value sorts among values of other types: by its JSON type. */\nfunction rank(value: unknown): number {\n switch (typeof value) {\n case \"boolean\":\n return 0;\n case \"number\":\n return 1;\n case \"string\":\n return 2;\n default:\n return 3;\n }\n}\n\n/**\n * Compare two column values, ascending.\n *\n * A value compares by its JSON type, and nothing is guessed: numbers\n * numerically, booleans false before true, strings as the user's language\n * orders them. A number that arrives as a string sorts as a string, so a\n * numeric sort column is sent as a JSON number. Null and a missing value\n * sort after everything, as Postgres sorts them ascending, and so first\n * descending.\n */\nexport function compareValues(a: unknown, b: unknown): number {\n const missingA = a === null || a === undefined;\n const missingB = b === null || b === undefined;\n if (missingA || missingB) return missingA === missingB ? 0 : missingA ? 1 : -1;\n const ra = rank(a);\n const rb = rank(b);\n if (ra !== rb) return ra - rb;\n if (typeof a === \"number\") return a - (b as number);\n if (typeof a === \"boolean\") return Number(a) - Number(b);\n if (typeof a === \"string\") return collator.compare(a, b as string);\n return collator.compare(JSON.stringify(a), JSON.stringify(b));\n}\n\n/**\n * A value read as a declared column type: a number for `number`, a boolean\n * for `boolean`, text for `text`. A value that is no value of its type reads\n * as missing, and compares as a null does. A column with no declared type\n * reads as it arrived.\n */\nexport function readAs(value: unknown, type: ColumnType | undefined): unknown {\n if (type === undefined || value === null || value === undefined) return value;\n switch (type) {\n case \"number\": {\n if (typeof value === \"number\") return value;\n if (typeof value !== \"string\" || value.trim() === \"\") return undefined;\n const n = Number(value);\n return Number.isNaN(n) ? undefined : n;\n }\n case \"boolean\":\n if (typeof value === \"boolean\") return value;\n return value === \"true\" ? true : value === \"false\" ? false : undefined;\n default:\n return typeof value === \"object\" ? JSON.stringify(value) : String(value);\n }\n}\n\n/** Compare two rows by an order's terms, each column read as its type. */\nexport function compareRows(\n a: Row,\n b: Row,\n terms: OrderTerm[],\n types: ColumnTypes = {},\n): number {\n for (const { column, descending } of terms) {\n const c = compareValues(\n readAs(a[column], types[column]),\n readAs(b[column], types[column]),\n );\n if (c !== 0) return descending ? -c : c;\n }\n return 0;\n}\n\n/** A filter's operator — see FILTER_OPERATORS. */\nexport type FilterOperator = (typeof FILTER_OPERATORS)[number];\n\n/** One term of a filter: an operator, and the value it compares with. */\nexport interface FilterTerm {\n operator: FilterOperator;\n value: string;\n}\n\n/** Where a filter's next term begins: whitespace before an operator. */\nconst NEXT_TERM = new RegExp(\n `\\\\s+(?=${FILTER_OPERATORS.map((op) => op.replace(/[$^*]/g, \"\\\\$&\")).join(\"|\")})`,\n);\n\n/**\n * A filter's terms, from its text: each an operator and the value after it,\n * a new term beginning wherever whitespace comes before an operator, so a\n * value may hold spaces. Text with no operator before it is `*=`. A term\n * with no value is not yet a term, and is left out, so a filter being typed\n * filters nothing until it says something. Nothing is evaluated.\n */\nexport function parseFilter(text: string): FilterTerm[] {\n const terms: FilterTerm[] = [];\n for (const raw of text.trim().split(NEXT_TERM)) {\n const operator =\n FILTER_OPERATORS.find((op) => raw.startsWith(op)) ?? \"*=\";\n const value = raw.slice(raw.startsWith(operator) ? operator.length : 0);\n if (value.trim() !== \"\") terms.push({ operator, value: value.trim() });\n }\n return terms;\n}\n\n/**\n * The filter's value as the column's own type, so a comparison is the one\n * the order makes: a number for a number, a boolean for a boolean, text for\n * anything else. Undefined when the text is no value of that type.\n */\nfunction asTypeOf(text: string, like: unknown): unknown {\n if (typeof like === \"number\") {\n const n = Number(text);\n return text.trim() === \"\" || Number.isNaN(n) ? undefined : n;\n }\n if (typeof like === \"boolean\") {\n return text === \"true\" ? true : text === \"false\" ? false : undefined;\n }\n return text;\n}\n\n/**\n * Whether a column's value passes one term.\n *\n * `^=`, `$=` and `*=` read the value as text, ignoring case: it begins with,\n * ends with or contains the term's value. The rest compare as the order\n * compares, with the column read as its declared type, if it has one, and\n * the term's value read as the column's type. A missing value, and one that\n * is no value of its declared type, passes only `!=`.\n */\nexport function passesTerm(\n value: unknown,\n { operator, value: text }: FilterTerm,\n type?: ColumnType,\n): boolean {\n if (value === null || value === undefined) return operator === \"!=\";\n if (operator === \"^=\" || operator === \"$=\" || operator === \"*=\") {\n const have = String(value).toLocaleLowerCase();\n const want = text.toLocaleLowerCase();\n if (operator === \"^=\") return have.startsWith(want);\n if (operator === \"$=\") return have.endsWith(want);\n return have.includes(want);\n }\n const read = readAs(value, type);\n const typed = read === undefined ? undefined : asTypeOf(text, read);\n if (typed === undefined) return operator === \"!=\";\n const c = compareValues(read, typed);\n switch (operator) {\n case \"=\":\n return c === 0;\n case \"!=\":\n return c !== 0;\n case \"<\":\n return c < 0;\n case \"<=\":\n return c <= 0;\n case \">\":\n return c > 0;\n default:\n return c >= 0;\n }\n}\n\n/** Whether a column's value passes every term of a filter. */\nexport function passes(\n value: unknown,\n terms: FilterTerm[],\n type?: ColumnType,\n): boolean {\n return terms.every((term) => passesTerm(value, term, type));\n}\n\n/**\n * Where a row for `lb-url` leads, from a page and its query parms.\n *\n * A column whose name begins with `lb-` is Loadbare's, and never a query\n * parm; every other column is one. An `lb-path` naming another page enters\n * it with only the parms the row names. Otherwise the named parms are set,\n * or taken out when empty, and the rest are kept.\n *\n * One rule on both sides of the wire: the server loads the page it names,\n * and the hub writes the URL it names. The two must agree, or what the page\n * shows would differ from what the address bar says.\n */\nexport function nextLocation(\n page: string,\n parms: URLSearchParams,\n row: Row,\n): { page: string; parms: URLSearchParams; moved: boolean } {\n const named: Record<string, ParmValue> = {};\n const text = (value: unknown): string =>\n value === null || value === undefined ? \"\" : String(value);\n for (const [name, value] of Object.entries(row)) {\n if (name.startsWith(LB_RESERVED_PREFIX)) continue;\n named[name] = Array.isArray(value) ? value.map(text) : text(value);\n }\n const path = row[URL_COLUMN_PATH];\n const next = typeof path === \"string\" ? pageNameFor(path) : page;\n const moved = next !== page;\n return {\n page: next,\n parms: withQueryParms(moved ? new URLSearchParams() : parms, named),\n moved,\n };\n}\n\n/**\n * What the hub tells a custom element it did, in its own terms rather than\n * the browser's: a custom element implements any of these in place of\n * `connectedCallback` and its kin, which the builder refuses.\n *\n * Each is a method on an element the hub does not own, so it carries the\n * `lb` prefix Loadbare reserves for exactly that — see\n * docs/reference/custom-elements.md.\n */\nexport interface HubLifecycle {\n /**\n * Called once, after the hub has placed the element and every column has\n * landed on it. An element under an `lb-show` that is off is placed inside\n * its template, so it may not be rendered.\n */\n lbAttached?(): void;\n /**\n * Called once after a result has landed on an element holding rows, with\n * every row in place and every stamp set. The hub reconciles and places\n * every row itself, by the query's order, so a custom element reacts to\n * rows and never places them.\n */\n lbRowsLanded?(): void;\n /**\n * Called once a request committed by this element, or by one inside it,\n * has finished, innermost first and before `lb-request-done` is\n * dispatched. `origin` is the element that committed.\n */\n lbRequestDone?(detail: HubRequestDone, origin: Element): void;\n /** Called once, after the hub has removed the element. */\n lbDetached?(): void;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type ColumnTypes, type ResponseItem, type Row, type RowsResult } from "../core/lb-types.js";
1
+ import { type ColumnTypes, type ParmValue, type ResponseItem, type Row, type RowsResult } from "../core/lb-types.js";
2
2
  /** A control's value, whether it is native or a custom element. */
3
3
  export interface Control extends Element {
4
4
  value: string;
@@ -25,9 +25,10 @@ export declare function isControl(el: Element): el is Control;
25
25
  /**
26
26
  * Gather a control into `values` under its column. Of the radio buttons
27
27
  * naming one column, the one checked gives the value, and `""` stands when
28
- * none is.
28
+ * none is. Of the checkboxes in a set naming one column, the checked ones
29
+ * give their values, and none checked gives none.
29
30
  */
30
- export declare function gatherInto(values: Record<string, string>, column: string, el: Control): void;
31
+ export declare function gatherInto(values: Record<string, ParmValue>, column: string, el: Control): void;
31
32
  /**
32
33
  * Root if it matches, then every descendant in root's query that does.
33
34
  * Gathering finds columns through here. It does not reach into an absent
@@ -80,13 +81,11 @@ export interface RowsOptions {
80
81
  types?: ColumnTypes;
81
82
  /** The user's choices: the row of `lb-choices`. */
82
83
  choices?: Row;
83
- /** The landing answers a request from this page. */
84
- requested?: boolean;
85
84
  /**
86
- * The request it answers was sent from inside this list, where the user
87
- * is working, so the rows already showing stay where they stand.
85
+ * The landing answers a request from this page, so a side the list
86
+ * places by hand leaves its rows where they stand.
88
87
  */
89
- held?: boolean;
88
+ requested?: boolean;
90
89
  /**
91
90
  * Fill every row, changed or not: the element has not been filled for
92
91
  * real before, so what its rows show cannot be trusted.
@@ -118,10 +117,12 @@ export interface RowsOptions {
118
117
  * the same rows again moves nothing. A row a filter the user chose does not
119
118
  * pass is `hidden`.
120
119
  *
121
- * A landing `held` for the user working in the list moves nothing already
122
- * showing, whole or a patch. A row it changed or created that the order
123
- * would put elsewhere, or that the filter would hide, stays and is stamped
124
- * unsorted until the rows are next placed by the order.
120
+ * A landing that answers a request leaves alone each side `lb-manual` names
121
+ * on the element, whole or a patch. Under `order`, nothing already showing
122
+ * moves, and a new row goes last in its group. Under `filter`, a row the
123
+ * answer changed or created stays showing. A row left where the order or
124
+ * the filter would not leave it is stamped unplaced until the rows are next
125
+ * placed.
125
126
  */
126
127
  export declare function applyRows(el: Element, template: HTMLTemplateElement, keyColumn: string, result: RowsResult, options?: RowsOptions): void;
127
128
  /** The answer kept for a query, as all of its rows. */
@@ -142,12 +143,6 @@ export interface LandOptions {
142
143
  quiet?: readonly string[];
143
144
  /** The items answer a request from this page. */
144
145
  requested?: boolean;
145
- /**
146
- * The element that sent the request, or the live row a form that sent it
147
- * is pointed at. The list it was sent from inside keeps its rows where
148
- * they stand.
149
- */
150
- origin?: Element;
151
146
  }
152
147
  /**
153
148
  * Land response items. Every answer arrives through here, and is kept for
@@ -158,9 +153,10 @@ export interface LandOptions {
158
153
  */
159
154
  export declare function applyResponse(root: ParentNode, items: ResponseItem[], options?: LandOptions): void;
160
155
  /**
161
- * Place an element's rows by the order again, from the answer kept for its
162
- * query, with no round trip: what the user asks for once an edit has left
163
- * rows out of order. False when the element shows no rows the hub holds.
156
+ * Place an element's rows by the order and the filter again, from the answer
157
+ * kept for its query, with no round trip: what the user asks for once an
158
+ * answer has left rows out of place. False when the element shows no rows
159
+ * the hub holds.
164
160
  */
165
- export declare function resort(root: ParentNode, el: Element): boolean;
161
+ export declare function placeAgain(root: ParentNode, el: Element): boolean;
166
162
  //# sourceMappingURL=lb-apply.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lb-apply.d.ts","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lb-apply.d.ts","sourceRoot":"","sources":["../../hub/lb-apply.ts"],"names":[],"mappings":"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"}
@@ -38,7 +38,7 @@
38
38
  * landing reaches in there as it reaches everywhere else, so a branch that
39
39
  * returns is already current. Nothing but landing reaches in.
40
40
  */
41
- import { AGGREGATES, ATTR_COLUMN, ATTR_FILTER, ATTR_ORDER, CHOICES_QUERY, LB_RESERVED_PREFIX, ATTR_COLUMN_VALUE, ATTR_COUNT, ATTR_GROUP, ATTR_GROUP_COLUMN, ATTR_GROUP_LEAVING, ATTR_GROUP_LIVE, ATTR_GROUP_VALUE, ATTR_KEY_VALUE, ATTR_MAX, ATTR_MIN, ATTR_QUERY, ATTR_QUERY_ORDER, ATTR_QUERY_ROW_COUNT, ATTR_QUERY_UNSORTED_COUNT, ATTR_ROW_CHANGED, ATTR_ROW_CREATED, ATTR_ROW_LEAVING, ATTR_ROW_LIVE, ATTR_ROW_MOVED, ATTR_ROW_REQUESTED, ATTR_ROW_UNSORTED, ATTR_SHOW, ATTR_SUM, ATTR_URL_PARM, KIND_ROW, LIVE_GROUP, LIVE_ROW, ORDER_DESCENDING, REQUESTED_CHANGED, REQUESTED_CREATED, REQUESTED_MOVED, SHOW_NOT, ATTR_SELECT, ATTR_ROW_SELECTED, } from "../core/lb-constants.js";
41
+ import { AGGREGATES, ATTR_COLUMN, ATTR_FILTER, ATTR_ORDER, CHOICES_QUERY, LB_RESERVED_PREFIX, ATTR_COLUMN_VALUE, ATTR_COUNT, ATTR_GROUP, ATTR_GROUP_COLUMN, ATTR_GROUP_LEAVING, ATTR_GROUP_LIVE, ATTR_GROUP_VALUE, ATTR_KEY_VALUE, ATTR_MAX, ATTR_MIN, ATTR_QUERY, ATTR_QUERY_ORDER, ATTR_QUERY_ROW_COUNT, ATTR_MANUAL, MANUAL_FILTER, MANUAL_ORDER, ATTR_QUERY_UNPLACED_COUNT, ATTR_ROW_CHANGED, ATTR_ROW_CREATED, ATTR_ROW_LEAVING, ATTR_ROW_LIVE, ATTR_ROW_MOVED, ATTR_ROW_REQUESTED, ATTR_ROW_UNPLACED, ATTR_SHOW, ATTR_SUM, ATTR_URL_PARM, KIND_ROW, LIVE_GROUP, LIVE_ROW, ORDER_DESCENDING, REQUESTED_CHANGED, REQUESTED_CREATED, REQUESTED_MOVED, SHOW_NOT, ATTR_SELECT, ATTR_ROW_SELECTED, } from "../core/lb-constants.js";
42
42
  import { compareRows, compareValues, isPatch, parseFilter, parseOrder, passes, readAs, } from "../core/lb-types.js";
43
43
  import { attachPlaced, detachRemoved, LEAVING, PARKED, placed, upgradeAbsent, } from "./lb-lifecycle.js";
44
44
  /** A DocumentFragment's nodeType. Tier 3 has no `Node` global to read it from. */
@@ -100,23 +100,58 @@ export function isFormAssociated(el) {
100
100
  export function isControl(el) {
101
101
  return (isNativeControl(el) || isCheckbox(el) || isRadio(el) || isFormAssociated(el));
102
102
  }
103
+ /**
104
+ * A control that holds several values: a `<select multiple>`, or a checkbox
105
+ * carrying a `value`, one of a set naming one column. A checkbox with no
106
+ * `value` is a flag.
107
+ */
108
+ function holdsSeveral(el) {
109
+ if (el.localName === "select")
110
+ return el.multiple;
111
+ return isCheckbox(el) && el.hasAttribute("value");
112
+ }
113
+ /** A landed value as the list of strings a control holding several shows. */
114
+ function listOf(value) {
115
+ if (Array.isArray(value))
116
+ return value.map((v) => String(v));
117
+ if (value === null || value === undefined || value === "")
118
+ return [];
119
+ return [String(value)];
120
+ }
103
121
  /**
104
122
  * What a control gathers: a checkbox `"true"` or `"false"`, so unchecked
105
- * still says so, and any other control its `value`.
123
+ * still says so, a `<select multiple>` the values of its selected options,
124
+ * and any other control its `value`, which a custom element may give as
125
+ * several.
106
126
  */
107
127
  function controlValue(el) {
108
- return isCheckbox(el) ? String(el.checked) : el.value;
128
+ if (isCheckbox(el))
129
+ return String(el.checked);
130
+ if (el.localName === "select" && el.multiple) {
131
+ return [...el.selectedOptions].map((o) => o.value);
132
+ }
133
+ const value = el.value;
134
+ return Array.isArray(value) ? value.map((v) => String(v)) : el.value;
109
135
  }
110
136
  /**
111
137
  * Gather a control into `values` under its column. Of the radio buttons
112
138
  * naming one column, the one checked gives the value, and `""` stands when
113
- * none is.
139
+ * none is. Of the checkboxes in a set naming one column, the checked ones
140
+ * give their values, and none checked gives none.
114
141
  */
115
142
  export function gatherInto(values, column, el) {
116
143
  if (isRadio(el) && !el.checked) {
117
144
  values[column] ??= "";
118
145
  return;
119
146
  }
147
+ if (isCheckbox(el) && holdsSeveral(el)) {
148
+ const had = values[column];
149
+ const set = Array.isArray(had) ? had : [];
150
+ values[column] = set;
151
+ if (el.checked)
152
+ set.push(el.value);
153
+ return;
154
+ }
120
155
  values[column] = controlValue(el);
121
156
  }
122
157
  /**
@@ -126,6 +161,12 @@ export function gatherInto(values, column, el) {
126
161
  * text content is set. Every one carries the value as `lb-column-value`.
127
162
  */
128
163
  function land(el, value) {
164
+ if (holdsSeveral(el))
165
+ return landSeveral(el, value);
166
+ // A custom element control takes several values as they are. Anything
167
+ // else holds one, and shows the first, as a URL's parm reads.
168
+ if (Array.isArray(value) && !isFormAssociated(el))
169
+ value = value[0];
129
170
  // Untouched, whatever the server sent: the browser decides what a
130
171
  // non-string looks like. See Row in core/lb-types.ts.
131
172
  if (value === null || value === undefined) {
@@ -163,6 +204,26 @@ function land(el, value) {
163
204
  }
164
205
  setStamp(el, ATTR_COLUMN_VALUE, String(value));
165
206
  }
207
+ /**
208
+ * Set a control that holds several values: a checkbox in a set is checked
209
+ * when its `value` is among them, and a `<select multiple>` selects each
210
+ * option whose `value` is.
211
+ */
212
+ function landSeveral(el, value) {
213
+ const list = listOf(value);
214
+ if (isCheckbox(el)) {
215
+ el.checked = list.includes(el.value);
216
+ }
217
+ else {
218
+ for (const option of el.options) {
219
+ option.selected = list.includes(option.value);
220
+ }
221
+ }
222
+ if (list.length === 0)
223
+ el.removeAttribute(ATTR_COLUMN_VALUE);
224
+ else
225
+ setStamp(el, ATTR_COLUMN_VALUE, JSON.stringify(list));
226
+ }
166
227
  /**
167
228
  * Set an element's text, unless it already holds exactly that text. Writing
168
229
  * the same text again replaces the text node, which is two mutations and a
@@ -660,7 +721,7 @@ function listFor(el, template) {
660
721
  data: {},
661
722
  placed: {},
662
723
  hidden: false,
663
- unsorted: false,
724
+ unplaced: false,
664
725
  };
665
726
  list.byKey.set(key, entry);
666
727
  list.root.rows.push(entry);
@@ -700,7 +761,7 @@ function create(list, doc, key) {
700
761
  data: {},
701
762
  placed: {},
702
763
  hidden: false,
703
- unsorted: false,
764
+ unplaced: false,
704
765
  };
705
766
  list.byKey.set(key, entry);
706
767
  return entry;
@@ -1136,10 +1197,12 @@ function differs(before, after) {
1136
1197
  * the same rows again moves nothing. A row a filter the user chose does not
1137
1198
  * pass is `hidden`.
1138
1199
  *
1139
- * A landing `held` for the user working in the list moves nothing already
1140
- * showing, whole or a patch. A row it changed or created that the order
1141
- * would put elsewhere, or that the filter would hide, stays and is stamped
1142
- * unsorted until the rows are next placed by the order.
1200
+ * A landing that answers a request leaves alone each side `lb-manual` names
1201
+ * on the element, whole or a patch. Under `order`, nothing already showing
1202
+ * moves, and a new row goes last in its group. Under `filter`, a row the
1203
+ * answer changed or created stays showing. A row left where the order or
1204
+ * the filter would not leave it is stamped unplaced until the rows are next
1205
+ * placed.
1143
1206
  */
1144
1207
  export function applyRows(el, template, keyColumn, result, options = {}) {
1145
1208
  const list = listFor(el, template);
@@ -1224,39 +1287,39 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1224
1287
  a.i - b.i)
1225
1288
  .map(({ entry }) => entry);
1226
1289
  const passing = filterFor(el, options.choices, options.types ?? {});
1290
+ const manual = options.requested ? manualOf(el) : NO_MANUAL;
1291
+ const touched = new Set([...changed, ...created]);
1292
+ // A row left out of place stays touched until the rows are placed again.
1293
+ for (const entry of shown)
1294
+ if (entry.unplaced)
1295
+ touched.add(entry);
1227
1296
  let order;
1228
- if (options.held) {
1229
- // The user is working in this list: what already shows stays where it
1230
- // stands, and what is new goes last in the group it belongs to.
1297
+ let out = new Set();
1298
+ if (manual.order) {
1299
+ // What already shows stays where it stands, and what is new goes last
1300
+ // in the group it belongs to.
1231
1301
  for (const entry of created)
1232
1302
  entry.placed = entry.data;
1233
1303
  order = heldOrder(stayed, created, groupTerms(list, terms));
1234
- const touched = new Set([...changed, ...created]);
1235
- for (const entry of order)
1236
- if (entry.unsorted)
1237
- touched.add(entry);
1238
- const out = movedRows(order, sort(order), touched);
1239
- for (const entry of order) {
1240
- const fails = !passing(entry.data);
1241
- // A row the user just changed does not vanish from under them.
1242
- const kept = touched.has(entry) && !entry.hidden;
1243
- filterRow(entry, fails && !kept);
1244
- // A row stands in the group it was placed in until it is placed again.
1245
- const moves = regrouped(entry, list, terms);
1246
- if (!moves)
1247
- entry.placed = entry.data;
1248
- entry.unsorted = out.has(entry) || (kept && fails) || moves;
1249
- }
1304
+ out = movedRows(order, sort(order), touched);
1250
1305
  }
1251
1306
  else {
1252
1307
  order = sort(whole
1253
1308
  ? [...arriving.keys()].map((key) => list.byKey.get(key))
1254
1309
  : [...stayed, ...created]);
1255
- for (const entry of order) {
1310
+ }
1311
+ for (const entry of order) {
1312
+ const fails = !passing(entry.data);
1313
+ // A row the answer touched does not vanish from under the user.
1314
+ const kept = manual.filter && touched.has(entry) && !entry.hidden && fails;
1315
+ const hides = fails && !kept;
1316
+ filterRow(entry, hides);
1317
+ // A row stands in the group it was placed in until it is placed again.
1318
+ const moves = manual.order && regrouped(entry, list, terms);
1319
+ if (!moves)
1256
1320
  entry.placed = entry.data;
1257
- entry.unsorted = false;
1258
- filterRow(entry, !passing(entry.data));
1259
- }
1321
+ // A row nobody sees stands nowhere the user would notice.
1322
+ entry.unplaced = !hides && (out.has(entry) || kept || moves);
1260
1323
  }
1261
1324
  const moved = movedRows(stayed, order, changed);
1262
1325
  const retired = arrange(list, doc, order, terms);
@@ -1281,18 +1344,18 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1281
1344
  }
1282
1345
  }
1283
1346
  stamps.done(doc);
1284
- let unsorted = 0;
1347
+ let unplaced = 0;
1285
1348
  for (const entry of order) {
1286
- if (entry.el.hasAttribute(ATTR_ROW_UNSORTED) !== entry.unsorted) {
1287
- entry.el.toggleAttribute(ATTR_ROW_UNSORTED, entry.unsorted);
1349
+ if (entry.el.hasAttribute(ATTR_ROW_UNPLACED) !== entry.unplaced) {
1350
+ entry.el.toggleAttribute(ATTR_ROW_UNPLACED, entry.unplaced);
1288
1351
  }
1289
- if (entry.unsorted)
1290
- unsorted++;
1352
+ if (entry.unplaced)
1353
+ unplaced++;
1291
1354
  }
1292
1355
  const rows = finish(list.root, options.types ?? {});
1293
1356
  // Counted from what the hub holds, which is what the rows show.
1294
1357
  setStamp(el, ATTR_QUERY_ROW_COUNT, String(rows.length));
1295
- setStamp(el, ATTR_QUERY_UNSORTED_COUNT, String(unsorted));
1358
+ setStamp(el, ATTR_QUERY_UNPLACED_COUNT, String(unplaced));
1296
1359
  if (terms.length === 0)
1297
1360
  el.removeAttribute(ATTR_QUERY_ORDER);
1298
1361
  else
@@ -1304,6 +1367,14 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1304
1367
  depart(departing);
1305
1368
  host.lbRowsLanded?.();
1306
1369
  }
1370
+ const NO_MANUAL = { order: false, filter: false };
1371
+ function manualOf(el) {
1372
+ const sides = (el.getAttribute(ATTR_MANUAL) ?? "").split(/\s+/);
1373
+ return {
1374
+ order: sides.includes(MANUAL_ORDER),
1375
+ filter: sides.includes(MANUAL_FILTER),
1376
+ };
1377
+ }
1307
1378
  /** The order's terms a list's groups break on, outermost first. */
1308
1379
  function groupTerms(list, terms) {
1309
1380
  return terms.slice(0, list.levels.length);
@@ -1533,13 +1604,6 @@ function fillArrivals(root, state) {
1533
1604
  }
1534
1605
  }
1535
1606
  }
1536
- /**
1537
- * Whether a request from `origin` was sent from inside `el`'s list: `el` is
1538
- * the nearest element carrying `lb-query` around it.
1539
- */
1540
- function sentFrom(el, origin) {
1541
- return origin?.parentElement?.closest(`[${ATTR_QUERY}]`) === el;
1542
- }
1543
1607
  /**
1544
1608
  * Land response items. Every answer arrives through here, and is kept for
1545
1609
  * `root`, so that what arrives after it is filled from it — see forget().
@@ -1563,7 +1627,6 @@ export function applyResponse(root, items, options = {}) {
1563
1627
  landItem(el, item, {
1564
1628
  ...rowsOptions(state, item),
1565
1629
  requested: options.requested,
1566
- held: sentFrom(el, options.origin),
1567
1630
  });
1568
1631
  }
1569
1632
  if (choices.size > 0)
@@ -1573,11 +1636,12 @@ export function applyResponse(root, items, options = {}) {
1573
1636
  attachPlaced();
1574
1637
  }
1575
1638
  /**
1576
- * Place an element's rows by the order again, from the answer kept for its
1577
- * query, with no round trip: what the user asks for once an edit has left
1578
- * rows out of order. False when the element shows no rows the hub holds.
1639
+ * Place an element's rows by the order and the filter again, from the answer
1640
+ * kept for its query, with no round trip: what the user asks for once an
1641
+ * answer has left rows out of place. False when the element shows no rows
1642
+ * the hub holds.
1579
1643
  */
1580
- export function resort(root, el) {
1644
+ export function placeAgain(root, el) {
1581
1645
  const state = kept.get(root);
1582
1646
  const item = state?.answers.get(el.getAttribute(ATTR_QUERY) ?? "");
1583
1647
  if (!state || !item || item.kind === KIND_ROW || !state.landed.has(el)) {