@loadbare/app 0.25.0 → 0.27.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 (56) hide show
  1. package/dist/build/assemble.d.ts +1 -2
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +31 -37
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/cli.d.ts +4 -10
  6. package/dist/build/cli.d.ts.map +1 -1
  7. package/dist/build/cli.js +11 -33
  8. package/dist/build/cli.js.map +1 -1
  9. package/dist/build/declared.js +1 -1
  10. package/dist/build/declared.js.map +1 -1
  11. package/dist/core/lb-constants.d.ts +15 -13
  12. package/dist/core/lb-constants.d.ts.map +1 -1
  13. package/dist/core/lb-constants.js +47 -51
  14. package/dist/core/lb-constants.js.map +1 -1
  15. package/dist/core/lb-types.d.ts +12 -11
  16. package/dist/core/lb-types.d.ts.map +1 -1
  17. package/dist/core/lb-types.js +7 -0
  18. package/dist/core/lb-types.js.map +1 -1
  19. package/dist/hub/lb-apply.d.ts +10 -34
  20. package/dist/hub/lb-apply.d.ts.map +1 -1
  21. package/dist/hub/lb-apply.js +92 -151
  22. package/dist/hub/lb-apply.js.map +1 -1
  23. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  24. package/dist/hub/lb-hub.browser.js +115 -127
  25. package/dist/hub/lb-hub.browser.js.map +1 -1
  26. package/dist/hub/lb-lifecycle.d.ts.map +1 -1
  27. package/dist/hub/lb-lifecycle.js +4 -11
  28. package/dist/hub/lb-lifecycle.js.map +1 -1
  29. package/dist/server/lb-express.d.ts.map +1 -1
  30. package/dist/server/lb-express.js +3 -3
  31. package/dist/server/lb-express.js.map +1 -1
  32. package/dist/server/lb-server.d.ts +35 -19
  33. package/dist/server/lb-server.d.ts.map +1 -1
  34. package/dist/server/lb-server.js +141 -88
  35. package/dist/server/lb-server.js.map +1 -1
  36. package/docs/binding-cases.md +150 -0
  37. package/docs/comparison.md +34 -38
  38. package/docs/opinions.md +2 -2
  39. package/docs/reference/chrome.md +8 -10
  40. package/docs/reference/css.md +9 -10
  41. package/docs/reference/custom-elements.md +11 -8
  42. package/docs/reference/data-binding.md +93 -134
  43. package/docs/reference/names.md +21 -22
  44. package/docs/reference/page-files.md +24 -26
  45. package/docs/reference/server.md +3 -4
  46. package/docs/roadmap.md +2 -2
  47. package/docs/testing.md +5 -6
  48. package/package.json +1 -1
  49. package/skills/loadbare-app/SKILL.md +55 -48
  50. package/skills/loadbare-app/references/chrome.md +8 -10
  51. package/skills/loadbare-app/references/css.md +9 -10
  52. package/skills/loadbare-app/references/custom-elements.md +11 -8
  53. package/skills/loadbare-app/references/data-binding.md +93 -134
  54. package/skills/loadbare-app/references/names.md +21 -22
  55. package/skills/loadbare-app/references/page-files.md +24 -26
  56. package/skills/loadbare-app/references/server.md +3 -4
@@ -36,11 +36,6 @@ export declare function gatherInto(values: Record<string, ParmValue>, column: st
36
36
  * showing.
37
37
  */
38
38
  export declare function within(root: Element, selector: string): Element[];
39
- /**
40
- * Tick or clear an `lb-select` checkbox, and stamp the live row it selects
41
- * to match.
42
- */
43
- export declare function select(box: Element, on: boolean): void;
44
39
  /**
45
40
  * Fill from one row: every `lb-column` and `lb-show` in root's query.
46
41
  *
@@ -88,12 +83,12 @@ export declare function addUnentered(el: Element): boolean;
88
83
  export declare function discardUnentered(row: Element): boolean;
89
84
  /** How a list of rows lands: in what order, and whether a request asked. */
90
85
  export interface RowsOptions {
91
- /** The order the server declared, as an order's text. */
86
+ /** The query's resolved order, from its row of `lb-sort`. */
92
87
  order?: string;
93
88
  /** The column types the server declared. */
94
89
  types?: ColumnTypes;
95
- /** The user's choices: the row of `lb-choices`. */
96
- choices?: Row;
90
+ /** The query's row of `lb-filters`. */
91
+ filters?: Row;
97
92
  /**
98
93
  * The landing answers a request from this page, so a side the list
99
94
  * places by hand leaves its rows where they stand.
@@ -113,29 +108,10 @@ export interface RowsOptions {
113
108
  doc?: Document;
114
109
  }
115
110
  /**
116
- * Land rows through a row template.
117
- *
118
- * Two loops do the work, on rows in no document. The creation loop makes a
119
- * live row for each key not yet shown, with its key and no values. The
120
- * landing loop lands each arriving row's values on its live row, as a `row`
121
- * lands, passing over a column that already shows its value. Then every live
122
- * row is placed by the order in one fragment, and the browser styles once.
123
- *
124
- * All rows decide membership: a row whose key did not arrive leaves. A patch
125
- * disturbs only what it names — a row it did not mention keeps its contents,
126
- * and keeps its place unless a row the patch named now sorts around it.
127
- *
128
- * With no order, all rows show in the order they arrived, and a patch's new
129
- * rows go last. A row that stands where it stood is not touched, so landing
130
- * the same rows again moves nothing. A row a filter the user chose does not
131
- * pass is `hidden`.
132
- *
133
- * A landing that answers a request leaves alone each side `lb-manual` names
134
- * on the element, whole or a patch. Under `order`, nothing already showing
135
- * moves, and a new row goes last in its group. Under `filter`, a row the
136
- * answer changed or created stays showing. A row left where the order or
137
- * the filter would not leave it is stamped unplaced until the rows are next
138
- * placed.
111
+ * Land rows through a row template: a live row per key, placed by the order,
112
+ * with groups built from the markup's group templates where the order breaks.
113
+ * A patch disturbs only what it names — rows it leaves out keep their
114
+ * contents and their place.
139
115
  */
140
116
  export declare function applyRows(el: Element, template: HTMLTemplateElement, keyColumn: string, result: RowsResult, options?: RowsOptions): void;
141
117
  /** The answer kept for a query, as all of its rows. */
@@ -160,9 +136,9 @@ export interface LandOptions {
160
136
  /**
161
137
  * Land response items. Every answer arrives through here, and is kept for
162
138
  * `root`, so that what arrives after it is filled from it — see forget().
163
- * An answer to `lb-choices` places again the rows its choices change. Once
164
- * everything has landed, each custom element placed since the last landing
165
- * is attached.
139
+ * An answer to `lb-sort` or `lb-filters` places again the rows it changes.
140
+ * Once everything has landed, each custom element placed since the last
141
+ * landing is attached.
166
142
  */
167
143
  export declare function applyResponse(root: ParentNode, items: ResponseItem[], options?: LandOptions): void;
168
144
  /**
@@ -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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"}
@@ -1,45 +1,10 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Landing — see docs/reference/data-binding.md.
4
- *
5
- * The hub lands a response item's rows on every element whose `lb-query`
6
- * names its query. The item says the query's kind and key, so the markup
7
- * says neither:
8
- *
9
- * kind row template the hub
10
- * row no lands the row on the element itself
11
- * row yes lands one live row
12
- * rows yes lands one live row per row
13
- * rows no lands nothing
14
- *
15
- * Filling a live row and filling an element a `row` lands on are the same
16
- * operation, and the only difference is how much of the page the root
17
- * covers.
18
- *
19
- * An answer is kept after it lands. An element that names a query and
20
- * arrives after that query's answer — a picker in a new live row, a ghost
21
- * row a custom element built — is filled from the kept answer at the end of
22
- * the next landing, so a query need not land again, and need not land after
23
- * the one whose rows hold it, just to fill what is new.
24
- *
25
- * Every custom element landing places is told so once the landing is done,
26
- * and every one it removes is told so once it is gone — see lb-lifecycle.
27
- *
28
- * A `rows` query is placed by its order: the user's, from the choice its
29
- * element's `lb-order` names, or the one the server declared, or, with
30
- * neither, the order its rows arrived in. Groups are built from the markup's
31
- * group templates as the order breaks, and every change to a list — a row
32
- * created, changed, moved or leaving, a group appearing or leaving — is one
33
- * a stylesheet can see. A row a filter the user chose does not pass is
34
- * hidden.
35
- *
36
- * A condition is the one other thing landing moves. An element whose
37
- * `lb-show` column is off stands in the document inside a template, and
38
- * landing reaches in there as it reaches everywhere else, so a branch that
39
- * returns is already current. Nothing but landing reaches in.
3
+ * Landing: rows from response items onto the elements that name their query.
4
+ * `applyRows` fills a row template; `applyRow` fills one element.
40
5
  */
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
- import { compareRows, compareValues, isPatch, parseFilter, parseOrder, passes, readAs, } from "../core/lb-types.js";
6
+ import { AGGREGATE_COUNT, AGGREGATE_MAX, AGGREGATE_MIN, AGGREGATE_SUM, ATTR_AGGREGATE, ATTR_COLUMN, ATTR_FILTER, FILTERS_COLUMN_QUERY, FILTERS_QUERY, SORT_COLUMN_ORDER, SORT_COLUMN_QUERY, SORT_QUERY, LB_RESERVED_PREFIX, ATTR_COLUMN_VALUE, ATTR_GROUP, ATTR_GROUP_COLUMN, ATTR_GROUP_LEAVING, ATTR_GROUP_LIVE, ATTR_GROUP_VALUE, ATTR_KEY_VALUE, 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_URL_PARM, KIND_ROW, LIVE_GROUP, LIVE_ROW, ORDER_DESCENDING, REQUESTED_CHANGED, REQUESTED_CREATED, REQUESTED_MOVED, SHOW_NOT, } from "../core/lb-constants.js";
7
+ import { compareRows, compareValues, isPatch, parseFilter, parseOrder, passes, readAs, oneRow, } from "../core/lb-types.js";
43
8
  import { attachPlaced, detachRemoved, LEAVING, PARKED, placed, upgradeAbsent, } from "./lb-lifecycle.js";
44
9
  /** A DocumentFragment's nodeType. Tier 3 has no `Node` global to read it from. */
45
10
  const FRAGMENT_NODE = 11;
@@ -317,14 +282,6 @@ function everywhere(root, selector) {
317
282
  }
318
283
  return found;
319
284
  }
320
- /**
321
- * Tick or clear an `lb-select` checkbox, and stamp the live row it selects
322
- * to match.
323
- */
324
- export function select(box, on) {
325
- box.checked = on;
326
- box.closest(LIVE_ROW)?.toggleAttribute(ATTR_ROW_SELECTED, on);
327
- }
328
285
  /**
329
286
  * Put an element in the document or take it out, as its `lb-show` column
330
287
  * says.
@@ -347,12 +304,6 @@ function present(el, on) {
347
304
  }
348
305
  if (on || el.parentNode?.nodeType === FRAGMENT_NODE)
349
306
  return;
350
- // A selection hidden is a selection abandoned: a tick kept out of sight
351
- // would come back stale, and nothing else could clear it.
352
- for (const box of [el, ...el.querySelectorAll(`[${ATTR_SELECT}]`)]) {
353
- if (box.hasAttribute(ATTR_SELECT))
354
- select(box, false);
355
- }
356
307
  const parked = el.ownerDocument.createElement("template");
357
308
  parked.setAttribute(ATTR_SHOW, el.getAttribute(ATTR_SHOW));
358
309
  el.replaceWith(parked);
@@ -386,8 +337,10 @@ function addPlace(map, name, el) {
386
337
  */
387
338
  function note(places, el) {
388
339
  const column = el.getAttribute(ATTR_COLUMN);
389
- if (column !== null)
340
+ // An aggregate's column is the one it totals, which no row lands on.
341
+ if (column !== null && !el.hasAttribute(ATTR_AGGREGATE)) {
390
342
  addPlace(places.columns, column, el);
343
+ }
391
344
  if (el.hasAttribute(ATTR_URL_PARM))
392
345
  places.links.push(el);
393
346
  const show = el.getAttribute(ATTR_SHOW);
@@ -411,7 +364,7 @@ const PLACE = `[${ATTR_COLUMN}], [${ATTR_SHOW}], [${ATTR_URL_PARM}]`;
411
364
  * count, as the row is its own. Its own `lb-show` never does.
412
365
  */
413
366
  function noteRoot(places, root) {
414
- if (root.hasAttribute(ATTR_COLUMN)) {
367
+ if (root.hasAttribute(ATTR_COLUMN) && !root.hasAttribute(ATTR_AGGREGATE)) {
415
368
  addPlace(places.columns, root.getAttribute(ATTR_COLUMN), root);
416
369
  }
417
370
  if (root.hasAttribute(ATTR_URL_PARM))
@@ -592,7 +545,7 @@ function link(places, row) {
592
545
  */
593
546
  export function blankRow(root) {
594
547
  const row = {};
595
- for (const el of below(root, `[${ATTR_COLUMN}]`)) {
548
+ for (const el of below(root, `[${ATTR_COLUMN}]:not([${ATTR_AGGREGATE}])`)) {
596
549
  row[el.getAttribute(ATTR_COLUMN)] = null;
597
550
  }
598
551
  for (const el of below(root, `[${ATTR_SHOW}]`)) {
@@ -644,7 +597,7 @@ const warned = new WeakSet();
644
597
  */
645
598
  const SCAFFOLD_STOP = `${LIVE_ROW}, ${LIVE_GROUP}, ${LEAVING}`;
646
599
  /** An element carrying any aggregate. */
647
- const AGGREGATE = AGGREGATES.map((name) => `[${name}]`).join(", ");
600
+ const AGGREGATE = `[${ATTR_AGGREGATE}]`;
648
601
  /**
649
602
  * The template a group's contents go in front of: the first `<template>` in
650
603
  * the content outside any nested `lb-query`, and not one holding an absent
@@ -737,6 +690,7 @@ function rowsUnder(group) {
737
690
  ? group.children.flatMap(rowsUnder)
738
691
  : group.rows;
739
692
  }
693
+ /**
740
694
  /**
741
695
  * A new live row for a key, copied from the row template: upgraded, stamped
742
696
  * with its key, its places found by the template's paths, and no values yet.
@@ -956,6 +910,12 @@ function arrange(list, doc, order, terms) {
956
910
  visit(list.root, 0, order);
957
911
  return retired;
958
912
  }
913
+ /**
914
+ * The rows that moved: rows whose values changed, and which now stand
915
+ * somewhere else among the rows whose values did not. A neighbour a moved
916
+ * row passed did not move, and neither did a row whose new values left it
917
+ * where it was.
918
+
959
919
  /**
960
920
  * The rows that moved: rows whose values changed, and which now stand
961
921
  * somewhere else among the rows whose values did not. A neighbour a moved
@@ -1093,15 +1053,18 @@ const reported = new Set();
1093
1053
  * leaves them out; a value that is not a number is left out and reported.
1094
1054
  */
1095
1055
  function aggregateOf(name, column, rows, type) {
1096
- if (name === ATTR_COUNT)
1056
+ if (name === AGGREGATE_COUNT && column === "")
1097
1057
  return rows.length;
1098
1058
  const values = rows
1099
1059
  .map((row) => row[column])
1100
1060
  .filter((v) => v !== null && v !== undefined && v !== "");
1061
+ // As SQL's count(column): the values that are not null.
1062
+ if (name === AGGREGATE_COUNT)
1063
+ return values.length;
1101
1064
  if (values.length === 0)
1102
1065
  return null;
1103
- if (name === ATTR_MIN || name === ATTR_MAX) {
1104
- const sign = name === ATTR_MIN ? 1 : -1;
1066
+ if (name === AGGREGATE_MIN || name === AGGREGATE_MAX) {
1067
+ const sign = name === AGGREGATE_MIN ? 1 : -1;
1105
1068
  return values.reduce((a, b) => sign * compareValues(readAs(b, type), readAs(a, type)) < 0 ? b : a);
1106
1069
  }
1107
1070
  const decimals = [];
@@ -1111,7 +1074,8 @@ function aggregateOf(name, column, rows, type) {
1111
1074
  decimals.push(d);
1112
1075
  else if (!reported.has(`${name}:${column}`)) {
1113
1076
  reported.add(`${name}:${column}`);
1114
- console.warn(`lb-hub: ${name}="${column}" left out ${JSON.stringify(value)}, ` +
1077
+ console.warn(`lb-hub: ${ATTR_AGGREGATE}="${name}" ${ATTR_COLUMN}="${column}" ` +
1078
+ `left out ${JSON.stringify(value)}, ` +
1115
1079
  `which is not a number`);
1116
1080
  }
1117
1081
  }
@@ -1119,7 +1083,7 @@ function aggregateOf(name, column, rows, type) {
1119
1083
  return null;
1120
1084
  const scale = Math.max(...decimals.map((d) => d.scale));
1121
1085
  const sum = decimals.reduce((total, d) => total + rescale(d, scale), 0n);
1122
- if (name === ATTR_SUM)
1086
+ if (name === AGGREGATE_SUM)
1123
1087
  return decimalText(sum, scale);
1124
1088
  // An average, rounded half away from zero at two places past the values',
1125
1089
  // which are then dropped while they are zeros.
@@ -1134,8 +1098,8 @@ function aggregateOf(name, column, rows, type) {
1134
1098
  /** Set each aggregate element from rows, each column read as its type. */
1135
1099
  function aggregate(elements, rows, types) {
1136
1100
  for (const el of elements) {
1137
- const name = AGGREGATES.find((a) => el.hasAttribute(a));
1138
- const column = el.getAttribute(name) ?? "";
1101
+ const name = el.getAttribute(ATTR_AGGREGATE) ?? "";
1102
+ const column = el.getAttribute(ATTR_COLUMN) ?? "";
1139
1103
  land(el, aggregateOf(name, column, rows, types[column]));
1140
1104
  }
1141
1105
  }
@@ -1190,7 +1154,7 @@ function orderText(terms) {
1190
1154
  .map((term) => (term.descending ? ORDER_DESCENDING : "") + term.column)
1191
1155
  .join(",");
1192
1156
  }
1193
- /** The user's choice in a column of `lb-choices`, when one is made. */
1157
+ /** The filter in a column of the query's `lb-filters` row, when one is made. */
1194
1158
  function choiceIn(choices, column) {
1195
1159
  const value = choices?.[column];
1196
1160
  return value === null || value === undefined || value === ""
@@ -1205,73 +1169,47 @@ function filtered(el) {
1205
1169
  .filter((column) => column !== "");
1206
1170
  }
1207
1171
  /**
1208
- * The order an element's rows are placed by: the user's, from the column of
1209
- * `lb-choices` its `lb-order` names, else the one the server declared.
1172
+ * The order an element's rows are placed by: its query's row of `lb-sort`.
1210
1173
  */
1211
- function orderFor(el, options) {
1212
- const column = el.getAttribute(ATTR_ORDER);
1213
- const chosen = column ? choiceIn(options.choices, column) : undefined;
1214
- return chosen ?? options.order;
1174
+ function sortOf(state, query) {
1175
+ const row = state.answers.get(SORT_QUERY)?.rows?.find((r) => r[SORT_COLUMN_QUERY] === query);
1176
+ const order = row?.[SORT_COLUMN_ORDER];
1177
+ return typeof order === "string" && order !== "" ? order : undefined;
1178
+ }
1179
+ /** The query's row of `lb-filters`, when it has one. */
1180
+ function filtersOf(state, query) {
1181
+ return state.answers.get(FILTERS_QUERY)?.rows?.find((r) => r[FILTERS_COLUMN_QUERY] === query);
1215
1182
  }
1216
1183
  /**
1217
1184
  * Whether a row passes every filter the user chose for the element: for each
1218
- * column its `lb-filter` names, the filter in the column of `lb-choices` of
1219
- * the same name.
1185
+ * column its `lb-filter` names, the filter in the query's row of
1186
+ * `lb-filters` of the same name.
1220
1187
  */
1221
- function filterFor(el, choices, types) {
1222
- const filters = [];
1188
+ function filterFor(el, filters, types) {
1189
+ const terms = [];
1223
1190
  for (const column of filtered(el)) {
1224
- const text = choiceIn(choices, column);
1225
- const terms = text === undefined ? [] : parseFilter(text);
1226
- if (terms.length > 0)
1227
- filters.push([column, terms]);
1191
+ const text = choiceIn(filters, column);
1192
+ const parsed = text === undefined ? [] : parseFilter(text);
1193
+ if (parsed.length > 0)
1194
+ terms.push([column, parsed]);
1228
1195
  }
1229
- return (row) => filters.every(([column, terms]) => passes(row[column], terms, types[column]));
1196
+ return (row) => terms.every(([column, parsed]) => passes(row[column], parsed, types[column]));
1230
1197
  }
1231
- /**
1232
- * Hide or show a row for a filter. A selection hidden is a selection
1233
- * abandoned, as it is for `lb-show`.
1234
- */
1235
1198
  function filterRow(entry, hidden) {
1236
1199
  if (entry.hidden === hidden)
1237
1200
  return;
1238
1201
  entry.hidden = hidden;
1239
1202
  hide(entry.el, hidden);
1240
- if (!hidden)
1241
- return;
1242
- for (const box of [entry.el, ...entry.el.querySelectorAll(`[${ATTR_SELECT}]`)]) {
1243
- if (box.hasAttribute(ATTR_SELECT))
1244
- select(box, false);
1245
- }
1246
1203
  }
1247
1204
  /** Whether a row's columns as landed differ from what they were. */
1248
1205
  function differs(before, after) {
1249
1206
  return Object.entries(after).some(([column, value]) => !same(before[column], value));
1250
1207
  }
1251
1208
  /**
1252
- * Land rows through a row template.
1253
- *
1254
- * Two loops do the work, on rows in no document. The creation loop makes a
1255
- * live row for each key not yet shown, with its key and no values. The
1256
- * landing loop lands each arriving row's values on its live row, as a `row`
1257
- * lands, passing over a column that already shows its value. Then every live
1258
- * row is placed by the order in one fragment, and the browser styles once.
1259
- *
1260
- * All rows decide membership: a row whose key did not arrive leaves. A patch
1261
- * disturbs only what it names — a row it did not mention keeps its contents,
1262
- * and keeps its place unless a row the patch named now sorts around it.
1263
- *
1264
- * With no order, all rows show in the order they arrived, and a patch's new
1265
- * rows go last. A row that stands where it stood is not touched, so landing
1266
- * the same rows again moves nothing. A row a filter the user chose does not
1267
- * pass is `hidden`.
1268
- *
1269
- * A landing that answers a request leaves alone each side `lb-manual` names
1270
- * on the element, whole or a patch. Under `order`, nothing already showing
1271
- * moves, and a new row goes last in its group. Under `filter`, a row the
1272
- * answer changed or created stays showing. A row left where the order or
1273
- * the filter would not leave it is stamped unplaced until the rows are next
1274
- * placed.
1209
+ * Land rows through a row template: a live row per key, placed by the order,
1210
+ * with groups built from the markup's group templates where the order breaks.
1211
+ * A patch disturbs only what it names — rows it leaves out keep their
1212
+ * contents and their place.
1275
1213
  */
1276
1214
  export function applyRows(el, template, keyColumn, result, options = {}) {
1277
1215
  const list = listFor(el, template);
@@ -1285,7 +1223,6 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1285
1223
  // What a request did is said by this landing alone.
1286
1224
  for (const entry of shown)
1287
1225
  entry.el.removeAttribute(ATTR_ROW_REQUESTED);
1288
- // The rows arriving, by key.
1289
1226
  const arriving = new Map();
1290
1227
  for (const data of whole ? result : (result.rows ?? [])) {
1291
1228
  if (data[keyColumn] === undefined) {
@@ -1310,7 +1247,6 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1310
1247
  const gone = whole
1311
1248
  ? shown.filter((entry) => !arriving.has(entry.key))
1312
1249
  : dropped.flatMap((key) => list.byKey.get(key) ?? []);
1313
- // The creation loop.
1314
1250
  const created = new Set();
1315
1251
  for (const key of arriving.keys()) {
1316
1252
  if (!list.byKey.has(key))
@@ -1341,7 +1277,7 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1341
1277
  // already showing with what is new after it. A sort keeps rows that
1342
1278
  // compare equal in this order.
1343
1279
  const stayed = shown.filter((entry) => list.byKey.get(entry.key) === entry);
1344
- const terms = termsOf(el, orderFor(el, options));
1280
+ const terms = termsOf(el, options.order);
1345
1281
  if (list.levels.length > terms.length && !warned.has(el)) {
1346
1282
  warned.add(el);
1347
1283
  console.warn(`lb-hub: <${el.localName}> has ${list.levels.length} group ` +
@@ -1355,7 +1291,7 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1355
1291
  .sort((a, b) => compareRows(a.entry.data, b.entry.data, terms, options.types) ||
1356
1292
  a.i - b.i)
1357
1293
  .map(({ entry }) => entry);
1358
- const passing = filterFor(el, options.choices, options.types ?? {});
1294
+ const passing = filterFor(el, options.filters, options.types ?? {});
1359
1295
  const manual = options.requested ? manualOf(el) : NO_MANUAL;
1360
1296
  const touched = new Set([...changed, ...created]);
1361
1297
  // A row left out of place stays touched until the rows are placed again.
@@ -1511,7 +1447,7 @@ function refocus(list, focused) {
1511
1447
  function landItem(el, item, options = {}) {
1512
1448
  const template = lists.get(el)?.top ?? templateIn(el);
1513
1449
  if (item.kind === KIND_ROW) {
1514
- const row = item.row;
1450
+ const row = oneRow(item);
1515
1451
  if (template) {
1516
1452
  applyRows(el, template, item.key, row ? [row] : [], options);
1517
1453
  return;
@@ -1523,8 +1459,8 @@ function landItem(el, item, options = {}) {
1523
1459
  el.removeAttribute(ATTR_KEY_VALUE);
1524
1460
  return;
1525
1461
  }
1526
- applyRow(el, row);
1527
1462
  const key = row[item.key];
1463
+ applyRow(el, row);
1528
1464
  if (key !== undefined && key !== null) {
1529
1465
  setStamp(el, ATTR_KEY_VALUE, String(key));
1530
1466
  }
@@ -1563,9 +1499,9 @@ export function keptAnswer(root, query) {
1563
1499
  function rowsOptions(state, item) {
1564
1500
  const declared = state.answers.get(item.query) ?? item;
1565
1501
  return {
1566
- order: declared.order,
1502
+ order: sortOf(state, item.query) ?? declared.order,
1567
1503
  types: declared.types,
1568
- choices: state.answers.get(CHOICES_QUERY)?.row ?? undefined,
1504
+ filters: filtersOf(state, item.query),
1569
1505
  doc: state.doc,
1570
1506
  };
1571
1507
  }
@@ -1619,38 +1555,41 @@ function keep(answers, item) {
1619
1555
  rows: patched(before.rows ?? [], item.key, item.patch),
1620
1556
  });
1621
1557
  }
1622
- /** The columns of `lb-choices` an answer to it changes. */
1623
- function chosen(state, item) {
1624
- const before = state.answers.get(CHOICES_QUERY)?.row ?? {};
1625
- const after = item.row ?? {};
1626
- const columns = new Set();
1627
- for (const column of new Set([...Object.keys(before), ...Object.keys(after)])) {
1628
- if (choiceIn(before, column) !== choiceIn(after, column))
1629
- columns.add(column);
1558
+ /** The queries whose sort or filters an answer changes. */
1559
+ function changedQueries(state, item) {
1560
+ const rowsOf = (answer) => {
1561
+ const map = new Map();
1562
+ for (const row of answer?.rows ?? []) {
1563
+ const query = row[SORT_COLUMN_QUERY];
1564
+ if (typeof query === "string")
1565
+ map.set(query, row);
1566
+ }
1567
+ return map;
1568
+ };
1569
+ const before = rowsOf(state.answers.get(item.query));
1570
+ const after = rowsOf(item);
1571
+ const queries = new Set();
1572
+ for (const query of new Set([...before.keys(), ...after.keys()])) {
1573
+ if (JSON.stringify(before.get(query)) !== JSON.stringify(after.get(query))) {
1574
+ queries.add(query);
1575
+ }
1630
1576
  }
1631
- return columns;
1632
- }
1633
- /** Whether an element's order or filter reads any of the columns. */
1634
- function reads(el, columns) {
1635
- const order = el.getAttribute(ATTR_ORDER);
1636
- return ((order !== null && columns.has(order)) ||
1637
- filtered(el).some((column) => columns.has(column)));
1577
+ return queries;
1638
1578
  }
1639
1579
  /**
1640
- * Place again the rows of every element whose order or filter reads a choice
1641
- * that changed. Each lands its query's kept answer again, whole, so it shows
1642
- * what a round trip carrying the choice would have, and the server is asked
1643
- * for nothing.
1580
+ * Place again the rows of every query whose sort or filters changed. Each
1581
+ * lands its query's kept answer again, whole, so it shows what a round trip
1582
+ * carrying the change would have, and the server is asked for nothing.
1644
1583
  */
1645
- function rechoose(root, state, columns) {
1646
- for (const [query, item] of state.answers) {
1647
- if (item.kind === KIND_ROW)
1584
+ function rechoose(root, state, queries) {
1585
+ for (const query of queries) {
1586
+ const item = state.answers.get(query);
1587
+ if (!item || item.kind === KIND_ROW)
1648
1588
  continue;
1649
1589
  for (const el of everywhere(root, `[${ATTR_QUERY}="${query}"]`)) {
1650
1590
  if (!state.landed.has(el) || el.closest(LEAVING))
1651
1591
  continue;
1652
- if (reads(el, columns))
1653
- landItem(el, item, rowsOptions(state, item));
1592
+ landItem(el, item, rowsOptions(state, item));
1654
1593
  }
1655
1594
  }
1656
1595
  }
@@ -1677,14 +1616,16 @@ function fillArrivals(root, state) {
1677
1616
  /**
1678
1617
  * Land response items. Every answer arrives through here, and is kept for
1679
1618
  * `root`, so that what arrives after it is filled from it — see forget().
1680
- * An answer to `lb-choices` places again the rows its choices change. Once
1681
- * everything has landed, each custom element placed since the last landing
1682
- * is attached.
1619
+ * An answer to `lb-sort` or `lb-filters` places again the rows it changes.
1620
+ * Once everything has landed, each custom element placed since the last
1621
+ * landing is attached.
1683
1622
  */
1684
1623
  export function applyResponse(root, items, options = {}) {
1685
1624
  const state = keptFor(root);
1686
1625
  for (const item of items) {
1687
- const choices = item.query === CHOICES_QUERY ? chosen(state, item) : new Set();
1626
+ const changed = item.query === SORT_QUERY || item.query === FILTERS_QUERY
1627
+ ? changedQueries(state, item)
1628
+ : new Set();
1688
1629
  keep(state.answers, item);
1689
1630
  const targets = everywhere(root, `[${ATTR_QUERY}="${item.query}"]`).filter((el) => !el.closest(LEAVING));
1690
1631
  if (targets.length === 0 &&
@@ -1699,8 +1640,8 @@ export function applyResponse(root, items, options = {}) {
1699
1640
  requested: options.requested,
1700
1641
  });
1701
1642
  }
1702
- if (choices.size > 0)
1703
- rechoose(root, state, choices);
1643
+ if (changed.size > 0)
1644
+ rechoose(root, state, changed);
1704
1645
  }
1705
1646
  fillArrivals(root, state);
1706
1647
  attachPlaced();