@loadbare/app 0.24.0 → 0.26.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 +61 -53
  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 +8 -18
  12. package/dist/core/lb-constants.d.ts.map +1 -1
  13. package/dist/core/lb-constants.js +32 -72
  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 +23 -37
  20. package/dist/hub/lb-apply.d.ts.map +1 -1
  21. package/dist/hub/lb-apply.js +205 -518
  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 +159 -132
  25. package/dist/hub/lb-hub.browser.js.map +1 -1
  26. package/dist/hub/lb-lifecycle.d.ts +2 -2
  27. package/dist/hub/lb-lifecycle.d.ts.map +1 -1
  28. package/dist/hub/lb-lifecycle.js +9 -17
  29. package/dist/hub/lb-lifecycle.js.map +1 -1
  30. package/dist/server/lb-express.d.ts.map +1 -1
  31. package/dist/server/lb-express.js +3 -3
  32. package/dist/server/lb-express.js.map +1 -1
  33. package/dist/server/lb-server.d.ts +35 -19
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +141 -88
  36. package/dist/server/lb-server.js.map +1 -1
  37. package/docs/binding-cases.md +150 -0
  38. package/docs/comparison.md +20 -29
  39. package/docs/miswarts.md +2 -2
  40. package/docs/reference/css.md +2 -15
  41. package/docs/reference/custom-elements.md +6 -8
  42. package/docs/reference/data-binding.md +100 -185
  43. package/docs/reference/names.md +11 -27
  44. package/docs/reference/page-files.md +4 -14
  45. package/docs/reference/server.md +0 -1
  46. package/docs/testing.md +2 -5
  47. package/package.json +1 -1
  48. package/skills/loadbare-app/SKILL.md +66 -56
  49. package/skills/loadbare-app/references/css.md +2 -15
  50. package/skills/loadbare-app/references/custom-elements.md +6 -8
  51. package/skills/loadbare-app/references/data-binding.md +100 -185
  52. package/skills/loadbare-app/references/miswarts.md +2 -2
  53. package/skills/loadbare-app/references/names.md +11 -27
  54. package/skills/loadbare-app/references/page-files.md +4 -14
  55. package/skills/loadbare-app/references/server.md +0 -1
  56. package/docs/temp-proposals.md +0 -251
@@ -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 { ATTR_COLUMN, ATTR_FILTER, FILTERS_COLUMN_QUERY, FILTERS_QUERY, SORT_COLUMN_ORDER, SORT_COLUMN_QUERY, SORT_QUERY, LB_RESERVED_PREFIX, ATTR_COLUMN_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_ROW, ORDER_DESCENDING, REQUESTED_CHANGED, REQUESTED_CREATED, REQUESTED_MOVED, SHOW_NOT, } from "../core/lb-constants.js";
7
+ import { compareRows, isPatch, parseFilter, parseOrder, passes, 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;
@@ -239,7 +204,6 @@ function setText(el, text) {
239
204
  if (!same)
240
205
  el.textContent = text;
241
206
  }
242
- /** Set an attribute, unless it already has that value. */
243
207
  function setStamp(el, name, value) {
244
208
  if (el.getAttribute(name) !== value)
245
209
  el.setAttribute(name, value);
@@ -258,14 +222,10 @@ function setStamp(el, name, value) {
258
222
  * up from an element inside it stops at the top of that content, which has
259
223
  * no parent, and the template it stands for was in scope already.
260
224
  */
261
- function inScope(root, el, stop) {
262
- if (stop && el !== root && el.matches(stop))
263
- return false;
225
+ function inScope(root, el) {
264
226
  for (let node = el.parentElement; node && node !== root;) {
265
227
  if (node.hasAttribute(ATTR_QUERY))
266
228
  return false;
267
- if (stop && node.matches(stop))
268
- return false;
269
229
  node = node.parentElement;
270
230
  }
271
231
  return true;
@@ -292,15 +252,15 @@ export function within(root, selector) {
292
252
  * stale or with its rows empty. A template holding one is in root's query by
293
253
  * the ordinary rule, and its content is searched as if it stood there.
294
254
  */
295
- function below(root, selector, stop) {
255
+ function below(root, selector) {
296
256
  const found = [];
297
257
  for (const el of root.querySelectorAll(selector)) {
298
- if (inScope(root, el, stop))
258
+ if (inScope(root, el))
299
259
  found.push(el);
300
260
  }
301
261
  for (const parked of root.querySelectorAll(PARKED)) {
302
- if (inScope(root, parked, stop)) {
303
- found.push(...below(parked.content, selector, stop));
262
+ if (inScope(root, parked)) {
263
+ found.push(...below(parked.content, selector));
304
264
  }
305
265
  }
306
266
  return found;
@@ -317,14 +277,6 @@ function everywhere(root, selector) {
317
277
  }
318
278
  return found;
319
279
  }
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
280
  /**
329
281
  * Put an element in the document or take it out, as its `lb-show` column
330
282
  * says.
@@ -347,12 +299,6 @@ function present(el, on) {
347
299
  }
348
300
  if (on || el.parentNode?.nodeType === FRAGMENT_NODE)
349
301
  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
302
  const parked = el.ownerDocument.createElement("template");
357
303
  parked.setAttribute(ATTR_SHOW, el.getAttribute(ATTR_SHOW));
358
304
  el.replaceWith(parked);
@@ -386,8 +332,9 @@ function addPlace(map, name, el) {
386
332
  */
387
333
  function note(places, el) {
388
334
  const column = el.getAttribute(ATTR_COLUMN);
389
- if (column !== null)
335
+ if (column !== null) {
390
336
  addPlace(places.columns, column, el);
337
+ }
391
338
  if (el.hasAttribute(ATTR_URL_PARM))
392
339
  places.links.push(el);
393
340
  const show = el.getAttribute(ATTR_SHOW);
@@ -422,11 +369,11 @@ function noteRoot(places, root) {
422
369
  * when `self`, and every one below it short of `stop`, absent branches
423
370
  * included.
424
371
  */
425
- function search(root, self, stop) {
372
+ function search(root, self) {
426
373
  const places = noPlaces();
427
374
  if (self)
428
375
  noteRoot(places, root);
429
- for (const el of below(root, PLACE, stop))
376
+ for (const el of below(root, PLACE))
430
377
  note(places, el);
431
378
  return places;
432
379
  }
@@ -515,11 +462,9 @@ function fill(places, row) {
515
462
  * it displays. An element a `row` lands on is not its own ancestor, so its
516
463
  * own `lb-column` reads from the row around it and is left alone here. Root's
517
464
  * own `lb-show` never counts.
518
- *
519
- * `stop` passes over everything matching it and inside it.
520
465
  */
521
- export function applyRow(root, row, self = false, stop) {
522
- const places = search(root, self, stop);
466
+ export function applyRow(root, row, self = false) {
467
+ const places = search(root, self);
523
468
  fill(places, row);
524
469
  link(places, row);
525
470
  }
@@ -636,106 +581,45 @@ export function focusIn(root) {
636
581
  }
637
582
  }
638
583
  const lists = new WeakMap();
639
- /** Elements already told their group templates outrun their order. */
640
- const warned = new WeakSet();
641
- /**
642
- * What a group's heading and aggregates stop at: the rows and groups inside
643
- * it, live or leaving, which are filled from rows of their own.
644
- */
645
- const SCAFFOLD_STOP = `${LIVE_ROW}, ${LIVE_GROUP}, ${LEAVING}`;
646
- /** An element carrying any aggregate. */
647
- const AGGREGATE = AGGREGATES.map((name) => `[${name}]`).join(", ");
648
- /**
649
- * The template a group's contents go in front of: the first `<template>` in
650
- * the content outside any nested `lb-query`, and not one holding an absent
651
- * branch.
652
- */
653
- function slotIn(root) {
654
- for (const t of root.querySelectorAll(`template:not([${ATTR_SHOW}])`)) {
655
- if (inScope(root, t))
656
- return t;
657
- }
658
- return null;
659
- }
660
- /**
661
- * The aggregates in root's scaffolding: root itself when `self`, and every
662
- * one below it that is not inside a row or a group of its own.
663
- */
664
- function aggregatesIn(root, self) {
665
- const found = below(root, AGGREGATE, SCAFFOLD_STOP);
666
- if (self && root.matches(AGGREGATE))
667
- found.unshift(root);
668
- return found;
669
- }
670
584
  /**
671
- * The list an element holds, found from its first template the first time
672
- * rows land on it, and kept after that: once groups exist, their slots are
673
- * templates too, and they come first in the document.
585
+ * The list an element holds, found from its row template the first time
586
+ * rows land on it, and kept after that.
674
587
  */
675
588
  function listFor(el, template) {
676
589
  const found = lists.get(el);
677
590
  if (found)
678
591
  return found;
679
- const levels = [];
680
- let at = template;
681
- while (at.hasAttribute(ATTR_GROUP)) {
682
- levels.push(at);
683
- const next = slotIn(at.content);
684
- if (!next) {
685
- console.error(`lb-hub: a group template in <${el.localName}> holds no nested ` +
686
- `template for its contents`);
687
- return null;
688
- }
689
- at = next;
690
- }
691
592
  const list = {
692
593
  top: template,
693
- levels,
694
- rowTemplate: at,
594
+ rowTemplate: template,
695
595
  byKey: new Map(),
696
- root: {
697
- id: "",
698
- nodes: [],
699
- slot: template,
700
- children: [],
701
- rows: [],
702
- heads: [],
703
- aggregates: aggregatesIn(el, false),
704
- hidden: false,
705
- },
596
+ rows: [],
597
+ unentered: [],
706
598
  };
707
599
  // Live rows already in the markup, as a page rendered ahead of its data
708
600
  // ships them, are the list's from the start, in the order they stand.
709
- if (levels.length === 0) {
710
- for (const row of el.querySelectorAll(LIVE_ROW)) {
711
- if (!inScope(el, row))
712
- continue;
713
- const key = row.getAttribute(ATTR_KEY_VALUE);
714
- if (list.byKey.has(key)) {
715
- console.error(`lb-hub: <${el.localName}> shows two rows with the key '${key}'`);
716
- }
717
- const entry = {
718
- key,
719
- el: row,
720
- places: search(row, true),
721
- data: {},
722
- placed: {},
723
- hidden: false,
724
- unplaced: false,
725
- };
726
- list.byKey.set(key, entry);
727
- list.root.rows.push(entry);
601
+ for (const row of el.querySelectorAll(LIVE_ROW)) {
602
+ if (!inScope(el, row))
603
+ continue;
604
+ const key = row.getAttribute(ATTR_KEY_VALUE);
605
+ if (list.byKey.has(key)) {
606
+ console.error(`lb-hub: <${el.localName}> shows two rows with the key '${key}'`);
728
607
  }
608
+ const entry = {
609
+ key,
610
+ el: row,
611
+ places: search(row, true),
612
+ data: {},
613
+ placed: {},
614
+ hidden: false,
615
+ unplaced: false,
616
+ };
617
+ list.byKey.set(key, entry);
618
+ list.rows.push(entry);
729
619
  }
730
620
  lists.set(el, list);
731
621
  return list;
732
622
  }
733
- /** The live rows of a group and everything under it, in order. */
734
- function rowsUnder(group) {
735
- return group.children.length > 0
736
- ? group.children.flatMap(rowsUnder)
737
- : group.rows;
738
- }
739
623
  /**
740
624
  * A new live row for a key, copied from the row template: upgraded, stamped
741
625
  * with its key, its places found by the template's paths, and no values yet.
@@ -767,125 +651,84 @@ function create(list, doc, key) {
767
651
  return entry;
768
652
  }
769
653
  /**
770
- * Give each radio button of a new live row a group of its own. The browser
771
- * groups radio buttons by `name`, and a row template repeats one `name` in
772
- * every live row, so the hub follows the `name` written, or the column when
773
- * none is, with the row's key.
654
+ * Put the rows not yet entered after every row: immediately after the list's
655
+ * row template, which the rows land in front of, so placing them never moves
656
+ * these.
774
657
  */
775
- function nameRadios(row, key) {
776
- for (const el of below(row, `input[type=radio][${ATTR_COLUMN}]`)) {
777
- const name = el.getAttribute("name") || el.getAttribute(ATTR_COLUMN);
778
- el.setAttribute("name", `${name}-${key}`);
658
+ function standLast(list) {
659
+ let at = list.top;
660
+ for (const row of list.unentered) {
661
+ if (at.nextElementSibling !== row)
662
+ at.after(row);
663
+ at = row;
779
664
  }
780
665
  }
781
- /** A group's identity among its siblings: its column and its value there. */
782
- function groupId(column, value) {
783
- return `${column}\u0000${JSON.stringify(value ?? null)}`;
666
+ /** Stamp a list's count: the rows it shows, and its rows not yet entered. */
667
+ function recount(el, list) {
668
+ const showing = list.rows.filter((entry) => !entry.hidden);
669
+ setStamp(el, ATTR_QUERY_ROW_COUNT, String(showing.length + list.unentered.length));
784
670
  }
785
- /** A new group, cloned from the group template for its level and stamped. */
786
- function makeGroup(doc, template, id, term, value) {
671
+ /**
672
+ * Put a row not yet entered into a list: a live row copied from its row
673
+ * template, keyed by the empty key, its controls at their HTML defaults and
674
+ * its `lb-show` branches as for a row whose columns are all null. It sends
675
+ * nothing; the submit of the form around the list carries it. False when the
676
+ * element holds no row template.
677
+ */
678
+ export function addUnentered(el) {
679
+ const template = lists.get(el)?.top ?? templateIn(el);
680
+ if (!template)
681
+ return false;
682
+ const list = listFor(el, template);
683
+ if (!list)
684
+ return false;
685
+ const doc = el.ownerDocument;
686
+ const source = list.rowTemplate.content.firstElementChild;
687
+ list.paths ??= compile(source);
787
688
  // Imported rather than cloned, for the reason a live row is.
788
- const fragment = doc.importNode(template.content, true);
789
- upgradeAbsent(fragment, doc);
790
- const slot = slotIn(fragment);
791
- const nodes = [...fragment.childNodes];
792
- const heads = [];
793
- const aggregates = [];
794
- for (const node of nodes) {
795
- if (node.nodeType !== ELEMENT_NODE || node === slot)
796
- continue;
797
- const el = node;
798
- placed(el);
799
- el.setAttribute(ATTR_GROUP_LIVE, "");
800
- if (term)
801
- el.setAttribute(ATTR_GROUP_COLUMN, term.column);
802
- if (value === null || value === undefined) {
803
- el.removeAttribute(ATTR_GROUP_VALUE);
804
- }
805
- else {
806
- el.setAttribute(ATTR_GROUP_VALUE, String(value));
807
- }
808
- // A heading's own column is filled, as a live row's is, unless it holds
809
- // the group's contents, which filling it would replace.
810
- heads.push(search(el, !el.contains(slot), SCAFFOLD_STOP));
811
- aggregates.push(...aggregatesIn(el, true));
812
- }
813
- return {
814
- id,
815
- nodes,
816
- slot,
817
- children: [],
818
- rows: [],
819
- heads,
820
- aggregates,
821
- hidden: false,
822
- };
689
+ const row = doc.importNode(source, true);
690
+ upgradeAbsent(row, doc);
691
+ placed(row);
692
+ row.setAttribute(ATTR_ROW_LIVE, "");
693
+ row.setAttribute(ATTR_KEY_VALUE, "");
694
+ nameRadios(row, "");
695
+ for (const [show, els] of placesBy(row, list.paths).shows) {
696
+ for (const shown of els)
697
+ presentPlaced(shown, show.startsWith(SHOW_NOT));
698
+ }
699
+ list.unentered.push(row);
700
+ standLast(list);
701
+ recount(el, list);
702
+ attachPlaced();
703
+ return true;
823
704
  }
824
705
  /**
825
- * Put a sequence of items in the document in the order given, in front of
826
- * `end`, where `old` is the order they stood in. The run they share at the
827
- * start and the run they share at the end stay where they are, and what lies
828
- * between goes in as one fragment. An item not in `old` is new, and one in
829
- * `old` that is not given has gone somewhere else, or is leaving.
706
+ * Take a row not yet entered out of its list, as a row leaves. False when
707
+ * the row is not one `--lb-new` put in a list.
830
708
  */
831
- function placeRun(old, next, end, nodesOf) {
832
- const most = Math.min(old.length, next.length);
833
- let start = 0;
834
- while (start < most && old[start] === next[start])
835
- start++;
836
- let tail = 0;
837
- while (tail < most - start &&
838
- old[old.length - 1 - tail] === next[next.length - 1 - tail]) {
839
- tail++;
840
- }
841
- const middle = next.slice(start, next.length - tail);
842
- if (middle.length === 0)
843
- return;
844
- const ref = tail > 0 ? nodesOf(next[next.length - tail])[0] : end;
845
- const fragment = ref.ownerDocument.createDocumentFragment();
846
- for (const item of middle)
847
- fragment.append(...nodesOf(item));
848
- ref.parentNode.insertBefore(fragment, ref);
709
+ export function discardUnentered(row) {
710
+ const el = row.parentElement?.closest(`[${ATTR_QUERY}]`);
711
+ const list = el ? lists.get(el) : undefined;
712
+ const at = list?.unentered.indexOf(row) ?? -1;
713
+ if (!el || !list || at < 0)
714
+ return false;
715
+ list.unentered.splice(at, 1);
716
+ leaving(row, ATTR_ROW_LIVE, ATTR_ROW_LEAVING);
717
+ depart([row]);
718
+ recount(el, list);
719
+ return true;
849
720
  }
850
721
  /**
851
- * Place a list's live rows in the order given, with groups made where the
852
- * order breaks, kept while their run goes on, and gone where it no longer
853
- * does. What leaves is returned, to leave once everything is placed.
722
+ * Give each radio button of a new live row a group of its own. The browser
723
+ * groups radio buttons by `name`, and a row template repeats one `name` in
724
+ * every live row, so the hub follows the `name` written, or the column when
725
+ * none is, with the row's key.
854
726
  */
855
- function arrange(list, doc, order, terms) {
856
- const retired = [];
857
- const visit = (parent, level, entries) => {
858
- if (level === list.levels.length) {
859
- placeRun(parent.rows, entries, parent.slot, (entry) => [entry.el]);
860
- parent.rows = entries;
861
- return;
862
- }
863
- const term = terms[level];
864
- const runs = [];
865
- for (const entry of entries) {
866
- const value = term ? entry.placed[term.column] : null;
867
- const id = groupId(term?.column ?? "", value);
868
- const last = runs.at(-1);
869
- if (last?.id === id)
870
- last.entries.push(entry);
871
- else
872
- runs.push({ id, value, entries: [entry] });
873
- }
874
- // Each group already here goes on with the first run that has its id.
875
- const pool = new Map();
876
- for (const child of parent.children) {
877
- pool.set(child.id, [...(pool.get(child.id) ?? []), child]);
878
- }
879
- const next = runs.map((run) => pool.get(run.id)?.shift() ??
880
- makeGroup(doc, list.levels[level], run.id, term, run.value));
881
- for (const left of pool.values())
882
- retired.push(...left);
883
- placeRun(parent.children, next, parent.slot, (group) => group.nodes);
884
- parent.children = next;
885
- runs.forEach((run, i) => visit(next[i], level + 1, run.entries));
886
- };
887
- visit(list.root, 0, order);
888
- return retired;
727
+ function nameRadios(row, key) {
728
+ for (const el of below(row, `input[type=radio][${ATTR_COLUMN}]`)) {
729
+ const name = el.getAttribute("name") || el.getAttribute(ATTR_COLUMN);
730
+ el.setAttribute("name", `${name}-${key}`);
731
+ }
889
732
  }
890
733
  /**
891
734
  * The rows that moved: rows whose values changed, and which now stand
@@ -926,6 +769,35 @@ function movedRows(before, after, changed) {
926
769
  }
927
770
  return moved;
928
771
  }
772
+ /**
773
+ * Put a list's live rows in the order given, in front of the row template.
774
+ * The run they share at the start and the run they share at the end stay
775
+ * where they are, and what lies between goes in as one fragment.
776
+ */
777
+ function placeRows(list, next) {
778
+ const old = list.rows;
779
+ const end = list.top;
780
+ const most = Math.min(old.length, next.length);
781
+ let start = 0;
782
+ while (start < most && old[start] === next[start])
783
+ start++;
784
+ let tail = 0;
785
+ while (tail < most - start &&
786
+ old[old.length - 1 - tail] === next[next.length - 1 - tail]) {
787
+ tail++;
788
+ }
789
+ const middle = next.slice(start, next.length - tail);
790
+ if (middle.length === 0) {
791
+ list.rows = next;
792
+ return;
793
+ }
794
+ const ref = tail > 0 ? next[next.length - tail].el : end;
795
+ const fragment = ref.ownerDocument.createDocumentFragment();
796
+ for (const item of middle)
797
+ fragment.append(item.el);
798
+ ref.parentNode.insertBefore(fragment, ref);
799
+ list.rows = next;
800
+ }
929
801
  /** Mark an element as leaving: no longer live, and taking no input. */
930
802
  function leaving(el, live, stamp) {
931
803
  el.removeAttribute(live);
@@ -933,17 +805,6 @@ function leaving(el, live, stamp) {
933
805
  el.setAttribute(stamp, "");
934
806
  el.setAttribute("inert", "");
935
807
  }
936
- /** A group whose rows are all gone, and every group inside it, leave. */
937
- function leaveGroup(group, nodes) {
938
- for (const child of group.children)
939
- leaveGroup(child, nodes);
940
- for (const node of group.nodes) {
941
- if (node.nodeType === ELEMENT_NODE) {
942
- leaving(node, ATTR_GROUP_LIVE, ATTR_GROUP_LEAVING);
943
- }
944
- }
945
- nodes.push(...group.nodes);
946
- }
947
808
  /**
948
809
  * Take nodes out of the document once whatever their stylesheet started on
949
810
  * them has finished: at once when nothing is running. Asking for the running
@@ -994,117 +855,11 @@ class Stamps {
994
855
  el.setAttribute(name, "");
995
856
  }
996
857
  }
997
- /** A plain decimal number, as JSON or Postgres writes one. */
998
- const DECIMAL = /^[+-]?\d+(\.\d+)?$/;
999
- function decimalOf(value) {
1000
- const text = typeof value === "number" ? String(value) : value;
1001
- if (typeof text !== "string" || !DECIMAL.test(text.trim()))
1002
- return null;
1003
- const [whole, fraction = ""] = text.trim().split(".");
1004
- return { digits: BigInt(whole + fraction), scale: fraction.length };
1005
- }
1006
- function rescale(d, scale) {
1007
- return d.digits * 10n ** BigInt(scale - d.scale);
1008
- }
1009
- function decimalText(digits, scale) {
1010
- const negative = digits < 0n;
1011
- const text = (negative ? -digits : digits)
1012
- .toString()
1013
- .padStart(scale + 1, "0");
1014
- const whole = text.slice(0, text.length - scale);
1015
- const fraction = scale > 0 ? `.${text.slice(text.length - scale)}` : "";
1016
- return `${negative ? "-" : ""}${whole}${fraction}`;
1017
- }
1018
- /** Aggregates already warned about, so a bad value is reported once. */
1019
- const reported = new Set();
1020
- /**
1021
- * One aggregate over rows. A sum and an average are exact, carried as
1022
- * decimals with as many places as the most precise value, and an average
1023
- * keeps up to two places more. Null and empty values are left out, as SQL
1024
- * leaves them out; a value that is not a number is left out and reported.
1025
- */
1026
- function aggregateOf(name, column, rows, type) {
1027
- if (name === ATTR_COUNT)
1028
- return rows.length;
1029
- const values = rows
1030
- .map((row) => row[column])
1031
- .filter((v) => v !== null && v !== undefined && v !== "");
1032
- if (values.length === 0)
1033
- return null;
1034
- if (name === ATTR_MIN || name === ATTR_MAX) {
1035
- const sign = name === ATTR_MIN ? 1 : -1;
1036
- return values.reduce((a, b) => sign * compareValues(readAs(b, type), readAs(a, type)) < 0 ? b : a);
1037
- }
1038
- const decimals = [];
1039
- for (const value of values) {
1040
- const d = decimalOf(value);
1041
- if (d)
1042
- decimals.push(d);
1043
- else if (!reported.has(`${name}:${column}`)) {
1044
- reported.add(`${name}:${column}`);
1045
- console.warn(`lb-hub: ${name}="${column}" left out ${JSON.stringify(value)}, ` +
1046
- `which is not a number`);
1047
- }
1048
- }
1049
- if (decimals.length === 0)
1050
- return null;
1051
- const scale = Math.max(...decimals.map((d) => d.scale));
1052
- const sum = decimals.reduce((total, d) => total + rescale(d, scale), 0n);
1053
- if (name === ATTR_SUM)
1054
- return decimalText(sum, scale);
1055
- // An average, rounded half away from zero at two places past the values',
1056
- // which are then dropped while they are zeros.
1057
- const scaled = sum * 100n;
1058
- const n = BigInt(decimals.length);
1059
- const sign = scaled < 0n ? -1n : 1n;
1060
- let text = decimalText((2n * scaled + sign * n) / (2n * n), scale + 2);
1061
- for (let i = 0; i < 2 && text.endsWith("0"); i++)
1062
- text = text.slice(0, -1);
1063
- return text.endsWith(".") ? text.slice(0, -1) : text;
1064
- }
1065
- /** Set each aggregate element from rows, each column read as its type. */
1066
- function aggregate(elements, rows, types) {
1067
- for (const el of elements) {
1068
- const name = AGGREGATES.find((a) => el.hasAttribute(a));
1069
- const column = el.getAttribute(name) ?? "";
1070
- land(el, aggregateOf(name, column, rows, types[column]));
1071
- }
1072
- }
1073
858
  /** Show or hide an element for a filter, saying so only when it changes. */
1074
859
  function hide(el, hidden) {
1075
860
  if (el.hasAttribute("hidden") !== hidden)
1076
861
  el.toggleAttribute("hidden", hidden);
1077
862
  }
1078
- /**
1079
- * Finish a group and everything under it, from the rows it shows: each
1080
- * heading filled and linked from the first, as it was when last placed by
1081
- * the order,
1082
- * each aggregate set from all of them, and a group hidden when a filter hides
1083
- * every row in it. The rows it shows are returned.
1084
- */
1085
- function finish(group, types) {
1086
- const shown = group.children.length > 0
1087
- ? group.children.flatMap((child) => finish(child, types))
1088
- : group.rows.filter((entry) => !entry.hidden);
1089
- const rows = shown.map((entry) => entry.data);
1090
- const hidden = rows.length === 0 && rowsUnder(group).length > 0;
1091
- if (hidden !== group.hidden) {
1092
- group.hidden = hidden;
1093
- for (const node of group.nodes) {
1094
- if (node.nodeType === ELEMENT_NODE && node !== group.slot) {
1095
- hide(node, hidden);
1096
- }
1097
- }
1098
- }
1099
- if (shown[0]) {
1100
- for (const head of group.heads) {
1101
- fill(head, shown[0].placed);
1102
- link(head, shown[0].placed);
1103
- }
1104
- }
1105
- aggregate(group.aggregates, rows, types);
1106
- return shown;
1107
- }
1108
863
  /** The order to place rows by, or none when it cannot be read. */
1109
864
  function termsOf(el, order) {
1110
865
  try {
@@ -1136,73 +891,46 @@ function filtered(el) {
1136
891
  .filter((column) => column !== "");
1137
892
  }
1138
893
  /**
1139
- * The order an element's rows are placed by: the user's, from the column of
1140
- * `lb-choices` its `lb-order` names, else the one the server declared.
894
+ * The order an element's rows are placed by: its query's row of `lb-sort`.
1141
895
  */
1142
- function orderFor(el, options) {
1143
- const column = el.getAttribute(ATTR_ORDER);
1144
- const chosen = column ? choiceIn(options.choices, column) : undefined;
1145
- return chosen ?? options.order;
896
+ function sortOf(state, query) {
897
+ const row = state.answers.get(SORT_QUERY)?.rows?.find((r) => r[SORT_COLUMN_QUERY] === query);
898
+ const order = row?.[SORT_COLUMN_ORDER];
899
+ return typeof order === "string" && order !== "" ? order : undefined;
900
+ }
901
+ /** The query's row of `lb-filters`, when it has one. */
902
+ function filtersOf(state, query) {
903
+ return state.answers.get(FILTERS_QUERY)?.rows?.find((r) => r[FILTERS_COLUMN_QUERY] === query);
1146
904
  }
1147
905
  /**
1148
906
  * Whether a row passes every filter the user chose for the element: for each
1149
- * column its `lb-filter` names, the filter in the column of `lb-choices` of
1150
- * the same name.
907
+ * column its `lb-filter` names, the filter in the query's row of
908
+ * `lb-filters` of the same name.
1151
909
  */
1152
- function filterFor(el, choices, types) {
1153
- const filters = [];
910
+ function filterFor(el, filters, types) {
911
+ const terms = [];
1154
912
  for (const column of filtered(el)) {
1155
- const text = choiceIn(choices, column);
1156
- const terms = text === undefined ? [] : parseFilter(text);
1157
- if (terms.length > 0)
1158
- filters.push([column, terms]);
913
+ const text = choiceIn(filters, column);
914
+ const parsed = text === undefined ? [] : parseFilter(text);
915
+ if (parsed.length > 0)
916
+ terms.push([column, parsed]);
1159
917
  }
1160
- return (row) => filters.every(([column, terms]) => passes(row[column], terms, types[column]));
918
+ return (row) => terms.every(([column, parsed]) => passes(row[column], parsed, types[column]));
1161
919
  }
1162
- /**
1163
- * Hide or show a row for a filter. A selection hidden is a selection
1164
- * abandoned, as it is for `lb-show`.
1165
- */
1166
920
  function filterRow(entry, hidden) {
1167
921
  if (entry.hidden === hidden)
1168
922
  return;
1169
923
  entry.hidden = hidden;
1170
924
  hide(entry.el, hidden);
1171
- if (!hidden)
1172
- return;
1173
- for (const box of [entry.el, ...entry.el.querySelectorAll(`[${ATTR_SELECT}]`)]) {
1174
- if (box.hasAttribute(ATTR_SELECT))
1175
- select(box, false);
1176
- }
1177
925
  }
1178
926
  /** Whether a row's columns as landed differ from what they were. */
1179
927
  function differs(before, after) {
1180
928
  return Object.entries(after).some(([column, value]) => !same(before[column], value));
1181
929
  }
1182
930
  /**
1183
- * Land rows through a row template.
1184
- *
1185
- * Two loops do the work, on rows in no document. The creation loop makes a
1186
- * live row for each key not yet shown, with its key and no values. The
1187
- * landing loop lands each arriving row's values on its live row, as a `row`
1188
- * lands, passing over a column that already shows its value. Then every live
1189
- * row is placed by the order in one fragment, and the browser styles once.
1190
- *
1191
- * All rows decide membership: a row whose key did not arrive leaves. A patch
1192
- * disturbs only what it names — a row it did not mention keeps its contents,
1193
- * and keeps its place unless a row the patch named now sorts around it.
1194
- *
1195
- * With no order, all rows show in the order they arrived, and a patch's new
1196
- * rows go last. A row that stands where it stood is not touched, so landing
1197
- * the same rows again moves nothing. A row a filter the user chose does not
1198
- * pass is `hidden`.
1199
- *
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.
931
+ * Land rows through a row template: a live row per key, placed by the order.
932
+ * A patch disturbs only what it names — rows it leaves out keep their
933
+ * contents and their place.
1206
934
  */
1207
935
  export function applyRows(el, template, keyColumn, result, options = {}) {
1208
936
  const list = listFor(el, template);
@@ -1211,12 +939,11 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1211
939
  const doc = options.doc ?? el.ownerDocument;
1212
940
  const host = el;
1213
941
  const whole = !isPatch(result);
1214
- const shown = rowsUnder(list.root);
942
+ const shown = list.rows;
1215
943
  const focused = focusedRow(list, doc);
1216
944
  // What a request did is said by this landing alone.
1217
945
  for (const entry of shown)
1218
946
  entry.el.removeAttribute(ATTR_ROW_REQUESTED);
1219
- // The rows arriving, by key.
1220
947
  const arriving = new Map();
1221
948
  for (const data of whole ? result : (result.rows ?? [])) {
1222
949
  if (data[keyColumn] === undefined) {
@@ -1241,7 +968,6 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1241
968
  const gone = whole
1242
969
  ? shown.filter((entry) => !arriving.has(entry.key))
1243
970
  : dropped.flatMap((key) => list.byKey.get(key) ?? []);
1244
- // The creation loop.
1245
971
  const created = new Set();
1246
972
  for (const key of arriving.keys()) {
1247
973
  if (!list.byKey.has(key))
@@ -1272,13 +998,7 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1272
998
  // already showing with what is new after it. A sort keeps rows that
1273
999
  // compare equal in this order.
1274
1000
  const stayed = shown.filter((entry) => list.byKey.get(entry.key) === entry);
1275
- const terms = termsOf(el, orderFor(el, options));
1276
- if (list.levels.length > terms.length && !warned.has(el)) {
1277
- warned.add(el);
1278
- console.warn(`lb-hub: <${el.localName}> has ${list.levels.length} group ` +
1279
- `template(s) and an order of ${terms.length} term(s); a level with ` +
1280
- `no term shows its rows as one group`);
1281
- }
1001
+ const terms = termsOf(el, options.order);
1282
1002
  const sort = (entries) => terms.length === 0
1283
1003
  ? entries
1284
1004
  : entries
@@ -1286,7 +1006,7 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1286
1006
  .sort((a, b) => compareRows(a.entry.data, b.entry.data, terms, options.types) ||
1287
1007
  a.i - b.i)
1288
1008
  .map(({ entry }) => entry);
1289
- const passing = filterFor(el, options.choices, options.types ?? {});
1009
+ const passing = filterFor(el, options.filters, options.types ?? {});
1290
1010
  const manual = options.requested ? manualOf(el) : NO_MANUAL;
1291
1011
  const touched = new Set([...changed, ...created]);
1292
1012
  // A row left out of place stays touched until the rows are placed again.
@@ -1296,11 +1016,10 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1296
1016
  let order;
1297
1017
  let out = new Set();
1298
1018
  if (manual.order) {
1299
- // What already shows stays where it stands, and what is new goes last
1300
- // in the group it belongs to.
1019
+ // What already shows stays where it stands, and what is new goes last.
1301
1020
  for (const entry of created)
1302
1021
  entry.placed = entry.data;
1303
- order = heldOrder(stayed, created, groupTerms(list, terms));
1022
+ order = [...stayed, ...created];
1304
1023
  out = movedRows(order, sort(order), touched);
1305
1024
  }
1306
1025
  else {
@@ -1314,15 +1033,12 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1314
1033
  const kept = manual.filter && touched.has(entry) && !entry.hidden && fails;
1315
1034
  const hides = fails && !kept;
1316
1035
  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)
1320
- entry.placed = entry.data;
1036
+ entry.placed = entry.data;
1321
1037
  // A row nobody sees stands nowhere the user would notice.
1322
- entry.unplaced = !hides && (out.has(entry) || kept || moves);
1038
+ entry.unplaced = !hides && (out.has(entry) || kept);
1323
1039
  }
1324
1040
  const moved = movedRows(stayed, order, changed);
1325
- const retired = arrange(list, doc, order, terms);
1041
+ placeRows(list, order);
1326
1042
  refocus(list, focused);
1327
1043
  const stamps = new Stamps();
1328
1044
  for (const key of arriving.keys()) {
@@ -1352,17 +1068,16 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1352
1068
  if (entry.unplaced)
1353
1069
  unplaced++;
1354
1070
  }
1355
- const rows = finish(list.root, options.types ?? {});
1071
+ const rows = list.rows.filter((entry) => !entry.hidden);
1356
1072
  // Counted from what the hub holds, which is what the rows show.
1357
- setStamp(el, ATTR_QUERY_ROW_COUNT, String(rows.length));
1073
+ setStamp(el, ATTR_QUERY_ROW_COUNT, String(rows.length + list.unentered.length));
1074
+ standLast(list);
1358
1075
  setStamp(el, ATTR_QUERY_UNPLACED_COUNT, String(unplaced));
1359
1076
  if (terms.length === 0)
1360
1077
  el.removeAttribute(ATTR_QUERY_ORDER);
1361
1078
  else
1362
1079
  setStamp(el, ATTR_QUERY_ORDER, orderText(terms));
1363
1080
  const departing = gone.map((entry) => entry.el);
1364
- for (const group of retired)
1365
- leaveGroup(group, departing);
1366
1081
  if (departing.length > 0)
1367
1082
  depart(departing);
1368
1083
  host.lbRowsLanded?.();
@@ -1375,39 +1090,6 @@ function manualOf(el) {
1375
1090
  filter: sides.includes(MANUAL_FILTER),
1376
1091
  };
1377
1092
  }
1378
- /** The order's terms a list's groups break on, outermost first. */
1379
- function groupTerms(list, terms) {
1380
- return terms.slice(0, list.levels.length);
1381
- }
1382
- /** The group a row's columns put it in, as one string. */
1383
- function groupOf(row, terms) {
1384
- return terms.map((term) => groupId(term.column, row[term.column])).join("\u0001");
1385
- }
1386
- /** Whether a row's columns now put it in another group than it stands in. */
1387
- function regrouped(entry, list, terms) {
1388
- const by = groupTerms(list, terms);
1389
- return groupOf(entry.data, by) !== groupOf(entry.placed, by);
1390
- }
1391
- /**
1392
- * The rows of a list the user is working in: those showing where they
1393
- * stand, and each new one after the last row of its group, or last of all
1394
- * when its group shows no row.
1395
- */
1396
- function heldOrder(stayed, created, by) {
1397
- const order = [...stayed];
1398
- for (const entry of created) {
1399
- const group = groupOf(entry.placed, by);
1400
- let at = order.length;
1401
- if (by.length > 0) {
1402
- while (at > 0 && groupOf(order[at - 1].placed, by) !== group)
1403
- at--;
1404
- if (at === 0)
1405
- at = order.length;
1406
- }
1407
- order.splice(at, 0, entry);
1408
- }
1409
- return order;
1410
- }
1411
1093
  function focusedRow(list, doc) {
1412
1094
  const active = doc.activeElement;
1413
1095
  const row = active?.closest(LIVE_ROW);
@@ -1433,7 +1115,7 @@ function refocus(list, focused) {
1433
1115
  active.scrollIntoView?.({ block: "nearest" });
1434
1116
  return;
1435
1117
  }
1436
- const first = rowsUnder(list.root).find((row) => !row.hidden);
1118
+ const first = list.rows.find((row) => !row.hidden);
1437
1119
  if (first)
1438
1120
  focusIn(first.el);
1439
1121
  }
@@ -1441,7 +1123,7 @@ function refocus(list, focused) {
1441
1123
  function landItem(el, item, options = {}) {
1442
1124
  const template = lists.get(el)?.top ?? templateIn(el);
1443
1125
  if (item.kind === KIND_ROW) {
1444
- const row = item.row;
1126
+ const row = oneRow(item);
1445
1127
  if (template) {
1446
1128
  applyRows(el, template, item.key, row ? [row] : [], options);
1447
1129
  return;
@@ -1453,8 +1135,8 @@ function landItem(el, item, options = {}) {
1453
1135
  el.removeAttribute(ATTR_KEY_VALUE);
1454
1136
  return;
1455
1137
  }
1456
- applyRow(el, row);
1457
1138
  const key = row[item.key];
1139
+ applyRow(el, row);
1458
1140
  if (key !== undefined && key !== null) {
1459
1141
  setStamp(el, ATTR_KEY_VALUE, String(key));
1460
1142
  }
@@ -1493,9 +1175,9 @@ export function keptAnswer(root, query) {
1493
1175
  function rowsOptions(state, item) {
1494
1176
  const declared = state.answers.get(item.query) ?? item;
1495
1177
  return {
1496
- order: declared.order,
1178
+ order: sortOf(state, item.query) ?? declared.order,
1497
1179
  types: declared.types,
1498
- choices: state.answers.get(CHOICES_QUERY)?.row ?? undefined,
1180
+ filters: filtersOf(state, item.query),
1499
1181
  doc: state.doc,
1500
1182
  };
1501
1183
  }
@@ -1549,38 +1231,41 @@ function keep(answers, item) {
1549
1231
  rows: patched(before.rows ?? [], item.key, item.patch),
1550
1232
  });
1551
1233
  }
1552
- /** The columns of `lb-choices` an answer to it changes. */
1553
- function chosen(state, item) {
1554
- const before = state.answers.get(CHOICES_QUERY)?.row ?? {};
1555
- const after = item.row ?? {};
1556
- const columns = new Set();
1557
- for (const column of new Set([...Object.keys(before), ...Object.keys(after)])) {
1558
- if (choiceIn(before, column) !== choiceIn(after, column))
1559
- columns.add(column);
1234
+ /** The queries whose sort or filters an answer changes. */
1235
+ function changedQueries(state, item) {
1236
+ const rowsOf = (answer) => {
1237
+ const map = new Map();
1238
+ for (const row of answer?.rows ?? []) {
1239
+ const query = row[SORT_COLUMN_QUERY];
1240
+ if (typeof query === "string")
1241
+ map.set(query, row);
1242
+ }
1243
+ return map;
1244
+ };
1245
+ const before = rowsOf(state.answers.get(item.query));
1246
+ const after = rowsOf(item);
1247
+ const queries = new Set();
1248
+ for (const query of new Set([...before.keys(), ...after.keys()])) {
1249
+ if (JSON.stringify(before.get(query)) !== JSON.stringify(after.get(query))) {
1250
+ queries.add(query);
1251
+ }
1560
1252
  }
1561
- return columns;
1562
- }
1563
- /** Whether an element's order or filter reads any of the columns. */
1564
- function reads(el, columns) {
1565
- const order = el.getAttribute(ATTR_ORDER);
1566
- return ((order !== null && columns.has(order)) ||
1567
- filtered(el).some((column) => columns.has(column)));
1253
+ return queries;
1568
1254
  }
1569
1255
  /**
1570
- * Place again the rows of every element whose order or filter reads a choice
1571
- * that changed. Each lands its query's kept answer again, whole, so it shows
1572
- * what a round trip carrying the choice would have, and the server is asked
1573
- * for nothing.
1256
+ * Place again the rows of every query whose sort or filters changed. Each
1257
+ * lands its query's kept answer again, whole, so it shows what a round trip
1258
+ * carrying the change would have, and the server is asked for nothing.
1574
1259
  */
1575
- function rechoose(root, state, columns) {
1576
- for (const [query, item] of state.answers) {
1577
- if (item.kind === KIND_ROW)
1260
+ function rechoose(root, state, queries) {
1261
+ for (const query of queries) {
1262
+ const item = state.answers.get(query);
1263
+ if (!item || item.kind === KIND_ROW)
1578
1264
  continue;
1579
1265
  for (const el of everywhere(root, `[${ATTR_QUERY}="${query}"]`)) {
1580
1266
  if (!state.landed.has(el) || el.closest(LEAVING))
1581
1267
  continue;
1582
- if (reads(el, columns))
1583
- landItem(el, item, rowsOptions(state, item));
1268
+ landItem(el, item, rowsOptions(state, item));
1584
1269
  }
1585
1270
  }
1586
1271
  }
@@ -1607,14 +1292,16 @@ function fillArrivals(root, state) {
1607
1292
  /**
1608
1293
  * Land response items. Every answer arrives through here, and is kept for
1609
1294
  * `root`, so that what arrives after it is filled from it — see forget().
1610
- * An answer to `lb-choices` places again the rows its choices change. Once
1611
- * everything has landed, each custom element placed since the last landing
1612
- * is attached.
1295
+ * An answer to `lb-sort` or `lb-filters` places again the rows it changes.
1296
+ * Once everything has landed, each custom element placed since the last
1297
+ * landing is attached.
1613
1298
  */
1614
1299
  export function applyResponse(root, items, options = {}) {
1615
1300
  const state = keptFor(root);
1616
1301
  for (const item of items) {
1617
- const choices = item.query === CHOICES_QUERY ? chosen(state, item) : new Set();
1302
+ const changed = item.query === SORT_QUERY || item.query === FILTERS_QUERY
1303
+ ? changedQueries(state, item)
1304
+ : new Set();
1618
1305
  keep(state.answers, item);
1619
1306
  const targets = everywhere(root, `[${ATTR_QUERY}="${item.query}"]`).filter((el) => !el.closest(LEAVING));
1620
1307
  if (targets.length === 0 &&
@@ -1629,8 +1316,8 @@ export function applyResponse(root, items, options = {}) {
1629
1316
  requested: options.requested,
1630
1317
  });
1631
1318
  }
1632
- if (choices.size > 0)
1633
- rechoose(root, state, choices);
1319
+ if (changed.size > 0)
1320
+ rechoose(root, state, changed);
1634
1321
  }
1635
1322
  fillArrivals(root, state);
1636
1323
  attachPlaced();