@loadbare/app 0.25.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.
- package/dist/build/assemble.d.ts +1 -2
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +11 -53
- package/dist/build/assemble.js.map +1 -1
- package/dist/build/cli.d.ts +4 -10
- package/dist/build/cli.d.ts.map +1 -1
- package/dist/build/cli.js +11 -33
- package/dist/build/cli.js.map +1 -1
- package/dist/build/declared.js +1 -1
- package/dist/build/declared.js.map +1 -1
- package/dist/core/lb-constants.d.ts +8 -18
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +32 -72
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +12 -11
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +7 -0
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +10 -37
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +139 -522
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +115 -126
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/hub/lb-lifecycle.d.ts +2 -2
- package/dist/hub/lb-lifecycle.d.ts.map +1 -1
- package/dist/hub/lb-lifecycle.js +9 -17
- package/dist/hub/lb-lifecycle.js.map +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +3 -3
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +35 -19
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +141 -88
- package/dist/server/lb-server.js.map +1 -1
- package/docs/binding-cases.md +150 -0
- package/docs/comparison.md +18 -29
- package/docs/miswarts.md +2 -2
- package/docs/reference/css.md +2 -15
- package/docs/reference/custom-elements.md +6 -8
- package/docs/reference/data-binding.md +37 -180
- package/docs/reference/names.md +10 -26
- package/docs/reference/page-files.md +1 -12
- package/docs/reference/server.md +0 -1
- package/docs/testing.md +2 -5
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +52 -54
- package/skills/loadbare-app/references/css.md +2 -15
- package/skills/loadbare-app/references/custom-elements.md +6 -8
- package/skills/loadbare-app/references/data-binding.md +37 -180
- package/skills/loadbare-app/references/miswarts.md +2 -2
- package/skills/loadbare-app/references/names.md +10 -26
- package/skills/loadbare-app/references/page-files.md +1 -12
- package/skills/loadbare-app/references/server.md +0 -1
package/dist/hub/lb-apply.js
CHANGED
|
@@ -1,45 +1,10 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Landing
|
|
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 {
|
|
42
|
-
import { compareRows,
|
|
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
|
|
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
|
|
255
|
+
function below(root, selector) {
|
|
296
256
|
const found = [];
|
|
297
257
|
for (const el of root.querySelectorAll(selector)) {
|
|
298
|
-
if (inScope(root, el
|
|
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
|
|
303
|
-
found.push(...below(parked.content, selector
|
|
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
|
|
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
|
|
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
|
|
522
|
-
const places = search(root, self
|
|
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,107 +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
584
|
/**
|
|
649
|
-
* The
|
|
650
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
694
|
-
rowTemplate: at,
|
|
594
|
+
rowTemplate: template,
|
|
695
595
|
byKey: new Map(),
|
|
596
|
+
rows: [],
|
|
696
597
|
unentered: [],
|
|
697
|
-
root: {
|
|
698
|
-
id: "",
|
|
699
|
-
nodes: [],
|
|
700
|
-
slot: template,
|
|
701
|
-
children: [],
|
|
702
|
-
rows: [],
|
|
703
|
-
heads: [],
|
|
704
|
-
aggregates: aggregatesIn(el, false),
|
|
705
|
-
hidden: false,
|
|
706
|
-
},
|
|
707
598
|
};
|
|
708
599
|
// Live rows already in the markup, as a page rendered ahead of its data
|
|
709
600
|
// ships them, are the list's from the start, in the order they stand.
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
console.error(`lb-hub: <${el.localName}> shows two rows with the key '${key}'`);
|
|
717
|
-
}
|
|
718
|
-
const entry = {
|
|
719
|
-
key,
|
|
720
|
-
el: row,
|
|
721
|
-
places: search(row, true),
|
|
722
|
-
data: {},
|
|
723
|
-
placed: {},
|
|
724
|
-
hidden: false,
|
|
725
|
-
unplaced: false,
|
|
726
|
-
};
|
|
727
|
-
list.byKey.set(key, entry);
|
|
728
|
-
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}'`);
|
|
729
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);
|
|
730
619
|
}
|
|
731
620
|
lists.set(el, list);
|
|
732
621
|
return list;
|
|
733
622
|
}
|
|
734
|
-
/** The live rows of a group and everything under it, in order. */
|
|
735
|
-
function rowsUnder(group) {
|
|
736
|
-
return group.children.length > 0
|
|
737
|
-
? group.children.flatMap(rowsUnder)
|
|
738
|
-
: group.rows;
|
|
739
|
-
}
|
|
740
623
|
/**
|
|
741
624
|
* A new live row for a key, copied from the row template: upgraded, stamped
|
|
742
625
|
* with its key, its places found by the template's paths, and no values yet.
|
|
@@ -769,11 +652,11 @@ function create(list, doc, key) {
|
|
|
769
652
|
}
|
|
770
653
|
/**
|
|
771
654
|
* Put the rows not yet entered after every row: immediately after the list's
|
|
772
|
-
*
|
|
773
|
-
*
|
|
655
|
+
* row template, which the rows land in front of, so placing them never moves
|
|
656
|
+
* these.
|
|
774
657
|
*/
|
|
775
658
|
function standLast(list) {
|
|
776
|
-
let at = list.
|
|
659
|
+
let at = list.top;
|
|
777
660
|
for (const row of list.unentered) {
|
|
778
661
|
if (at.nextElementSibling !== row)
|
|
779
662
|
at.after(row);
|
|
@@ -782,7 +665,7 @@ function standLast(list) {
|
|
|
782
665
|
}
|
|
783
666
|
/** Stamp a list's count: the rows it shows, and its rows not yet entered. */
|
|
784
667
|
function recount(el, list) {
|
|
785
|
-
const showing =
|
|
668
|
+
const showing = list.rows.filter((entry) => !entry.hidden);
|
|
786
669
|
setStamp(el, ATTR_QUERY_ROW_COUNT, String(showing.length + list.unentered.length));
|
|
787
670
|
}
|
|
788
671
|
/**
|
|
@@ -847,115 +730,6 @@ function nameRadios(row, key) {
|
|
|
847
730
|
el.setAttribute("name", `${name}-${key}`);
|
|
848
731
|
}
|
|
849
732
|
}
|
|
850
|
-
/** A group's identity among its siblings: its column and its value there. */
|
|
851
|
-
function groupId(column, value) {
|
|
852
|
-
return `${column}\u0000${JSON.stringify(value ?? null)}`;
|
|
853
|
-
}
|
|
854
|
-
/** A new group, cloned from the group template for its level and stamped. */
|
|
855
|
-
function makeGroup(doc, template, id, term, value) {
|
|
856
|
-
// Imported rather than cloned, for the reason a live row is.
|
|
857
|
-
const fragment = doc.importNode(template.content, true);
|
|
858
|
-
upgradeAbsent(fragment, doc);
|
|
859
|
-
const slot = slotIn(fragment);
|
|
860
|
-
const nodes = [...fragment.childNodes];
|
|
861
|
-
const heads = [];
|
|
862
|
-
const aggregates = [];
|
|
863
|
-
for (const node of nodes) {
|
|
864
|
-
if (node.nodeType !== ELEMENT_NODE || node === slot)
|
|
865
|
-
continue;
|
|
866
|
-
const el = node;
|
|
867
|
-
placed(el);
|
|
868
|
-
el.setAttribute(ATTR_GROUP_LIVE, "");
|
|
869
|
-
if (term)
|
|
870
|
-
el.setAttribute(ATTR_GROUP_COLUMN, term.column);
|
|
871
|
-
if (value === null || value === undefined) {
|
|
872
|
-
el.removeAttribute(ATTR_GROUP_VALUE);
|
|
873
|
-
}
|
|
874
|
-
else {
|
|
875
|
-
el.setAttribute(ATTR_GROUP_VALUE, String(value));
|
|
876
|
-
}
|
|
877
|
-
// A heading's own column is filled, as a live row's is, unless it holds
|
|
878
|
-
// the group's contents, which filling it would replace.
|
|
879
|
-
heads.push(search(el, !el.contains(slot), SCAFFOLD_STOP));
|
|
880
|
-
aggregates.push(...aggregatesIn(el, true));
|
|
881
|
-
}
|
|
882
|
-
return {
|
|
883
|
-
id,
|
|
884
|
-
nodes,
|
|
885
|
-
slot,
|
|
886
|
-
children: [],
|
|
887
|
-
rows: [],
|
|
888
|
-
heads,
|
|
889
|
-
aggregates,
|
|
890
|
-
hidden: false,
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
/**
|
|
894
|
-
* Put a sequence of items in the document in the order given, in front of
|
|
895
|
-
* `end`, where `old` is the order they stood in. The run they share at the
|
|
896
|
-
* start and the run they share at the end stay where they are, and what lies
|
|
897
|
-
* between goes in as one fragment. An item not in `old` is new, and one in
|
|
898
|
-
* `old` that is not given has gone somewhere else, or is leaving.
|
|
899
|
-
*/
|
|
900
|
-
function placeRun(old, next, end, nodesOf) {
|
|
901
|
-
const most = Math.min(old.length, next.length);
|
|
902
|
-
let start = 0;
|
|
903
|
-
while (start < most && old[start] === next[start])
|
|
904
|
-
start++;
|
|
905
|
-
let tail = 0;
|
|
906
|
-
while (tail < most - start &&
|
|
907
|
-
old[old.length - 1 - tail] === next[next.length - 1 - tail]) {
|
|
908
|
-
tail++;
|
|
909
|
-
}
|
|
910
|
-
const middle = next.slice(start, next.length - tail);
|
|
911
|
-
if (middle.length === 0)
|
|
912
|
-
return;
|
|
913
|
-
const ref = tail > 0 ? nodesOf(next[next.length - tail])[0] : end;
|
|
914
|
-
const fragment = ref.ownerDocument.createDocumentFragment();
|
|
915
|
-
for (const item of middle)
|
|
916
|
-
fragment.append(...nodesOf(item));
|
|
917
|
-
ref.parentNode.insertBefore(fragment, ref);
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Place a list's live rows in the order given, with groups made where the
|
|
921
|
-
* order breaks, kept while their run goes on, and gone where it no longer
|
|
922
|
-
* does. What leaves is returned, to leave once everything is placed.
|
|
923
|
-
*/
|
|
924
|
-
function arrange(list, doc, order, terms) {
|
|
925
|
-
const retired = [];
|
|
926
|
-
const visit = (parent, level, entries) => {
|
|
927
|
-
if (level === list.levels.length) {
|
|
928
|
-
placeRun(parent.rows, entries, parent.slot, (entry) => [entry.el]);
|
|
929
|
-
parent.rows = entries;
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
const term = terms[level];
|
|
933
|
-
const runs = [];
|
|
934
|
-
for (const entry of entries) {
|
|
935
|
-
const value = term ? entry.placed[term.column] : null;
|
|
936
|
-
const id = groupId(term?.column ?? "", value);
|
|
937
|
-
const last = runs.at(-1);
|
|
938
|
-
if (last?.id === id)
|
|
939
|
-
last.entries.push(entry);
|
|
940
|
-
else
|
|
941
|
-
runs.push({ id, value, entries: [entry] });
|
|
942
|
-
}
|
|
943
|
-
// Each group already here goes on with the first run that has its id.
|
|
944
|
-
const pool = new Map();
|
|
945
|
-
for (const child of parent.children) {
|
|
946
|
-
pool.set(child.id, [...(pool.get(child.id) ?? []), child]);
|
|
947
|
-
}
|
|
948
|
-
const next = runs.map((run) => pool.get(run.id)?.shift() ??
|
|
949
|
-
makeGroup(doc, list.levels[level], run.id, term, run.value));
|
|
950
|
-
for (const left of pool.values())
|
|
951
|
-
retired.push(...left);
|
|
952
|
-
placeRun(parent.children, next, parent.slot, (group) => group.nodes);
|
|
953
|
-
parent.children = next;
|
|
954
|
-
runs.forEach((run, i) => visit(next[i], level + 1, run.entries));
|
|
955
|
-
};
|
|
956
|
-
visit(list.root, 0, order);
|
|
957
|
-
return retired;
|
|
958
|
-
}
|
|
959
733
|
/**
|
|
960
734
|
* The rows that moved: rows whose values changed, and which now stand
|
|
961
735
|
* somewhere else among the rows whose values did not. A neighbour a moved
|
|
@@ -995,6 +769,35 @@ function movedRows(before, after, changed) {
|
|
|
995
769
|
}
|
|
996
770
|
return moved;
|
|
997
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
|
+
}
|
|
998
801
|
/** Mark an element as leaving: no longer live, and taking no input. */
|
|
999
802
|
function leaving(el, live, stamp) {
|
|
1000
803
|
el.removeAttribute(live);
|
|
@@ -1002,17 +805,6 @@ function leaving(el, live, stamp) {
|
|
|
1002
805
|
el.setAttribute(stamp, "");
|
|
1003
806
|
el.setAttribute("inert", "");
|
|
1004
807
|
}
|
|
1005
|
-
/** A group whose rows are all gone, and every group inside it, leave. */
|
|
1006
|
-
function leaveGroup(group, nodes) {
|
|
1007
|
-
for (const child of group.children)
|
|
1008
|
-
leaveGroup(child, nodes);
|
|
1009
|
-
for (const node of group.nodes) {
|
|
1010
|
-
if (node.nodeType === ELEMENT_NODE) {
|
|
1011
|
-
leaving(node, ATTR_GROUP_LIVE, ATTR_GROUP_LEAVING);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
nodes.push(...group.nodes);
|
|
1015
|
-
}
|
|
1016
808
|
/**
|
|
1017
809
|
* Take nodes out of the document once whatever their stylesheet started on
|
|
1018
810
|
* them has finished: at once when nothing is running. Asking for the running
|
|
@@ -1063,117 +855,11 @@ class Stamps {
|
|
|
1063
855
|
el.setAttribute(name, "");
|
|
1064
856
|
}
|
|
1065
857
|
}
|
|
1066
|
-
/** A plain decimal number, as JSON or Postgres writes one. */
|
|
1067
|
-
const DECIMAL = /^[+-]?\d+(\.\d+)?$/;
|
|
1068
|
-
function decimalOf(value) {
|
|
1069
|
-
const text = typeof value === "number" ? String(value) : value;
|
|
1070
|
-
if (typeof text !== "string" || !DECIMAL.test(text.trim()))
|
|
1071
|
-
return null;
|
|
1072
|
-
const [whole, fraction = ""] = text.trim().split(".");
|
|
1073
|
-
return { digits: BigInt(whole + fraction), scale: fraction.length };
|
|
1074
|
-
}
|
|
1075
|
-
function rescale(d, scale) {
|
|
1076
|
-
return d.digits * 10n ** BigInt(scale - d.scale);
|
|
1077
|
-
}
|
|
1078
|
-
function decimalText(digits, scale) {
|
|
1079
|
-
const negative = digits < 0n;
|
|
1080
|
-
const text = (negative ? -digits : digits)
|
|
1081
|
-
.toString()
|
|
1082
|
-
.padStart(scale + 1, "0");
|
|
1083
|
-
const whole = text.slice(0, text.length - scale);
|
|
1084
|
-
const fraction = scale > 0 ? `.${text.slice(text.length - scale)}` : "";
|
|
1085
|
-
return `${negative ? "-" : ""}${whole}${fraction}`;
|
|
1086
|
-
}
|
|
1087
|
-
/** Aggregates already warned about, so a bad value is reported once. */
|
|
1088
|
-
const reported = new Set();
|
|
1089
|
-
/**
|
|
1090
|
-
* One aggregate over rows. A sum and an average are exact, carried as
|
|
1091
|
-
* decimals with as many places as the most precise value, and an average
|
|
1092
|
-
* keeps up to two places more. Null and empty values are left out, as SQL
|
|
1093
|
-
* leaves them out; a value that is not a number is left out and reported.
|
|
1094
|
-
*/
|
|
1095
|
-
function aggregateOf(name, column, rows, type) {
|
|
1096
|
-
if (name === ATTR_COUNT)
|
|
1097
|
-
return rows.length;
|
|
1098
|
-
const values = rows
|
|
1099
|
-
.map((row) => row[column])
|
|
1100
|
-
.filter((v) => v !== null && v !== undefined && v !== "");
|
|
1101
|
-
if (values.length === 0)
|
|
1102
|
-
return null;
|
|
1103
|
-
if (name === ATTR_MIN || name === ATTR_MAX) {
|
|
1104
|
-
const sign = name === ATTR_MIN ? 1 : -1;
|
|
1105
|
-
return values.reduce((a, b) => sign * compareValues(readAs(b, type), readAs(a, type)) < 0 ? b : a);
|
|
1106
|
-
}
|
|
1107
|
-
const decimals = [];
|
|
1108
|
-
for (const value of values) {
|
|
1109
|
-
const d = decimalOf(value);
|
|
1110
|
-
if (d)
|
|
1111
|
-
decimals.push(d);
|
|
1112
|
-
else if (!reported.has(`${name}:${column}`)) {
|
|
1113
|
-
reported.add(`${name}:${column}`);
|
|
1114
|
-
console.warn(`lb-hub: ${name}="${column}" left out ${JSON.stringify(value)}, ` +
|
|
1115
|
-
`which is not a number`);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
if (decimals.length === 0)
|
|
1119
|
-
return null;
|
|
1120
|
-
const scale = Math.max(...decimals.map((d) => d.scale));
|
|
1121
|
-
const sum = decimals.reduce((total, d) => total + rescale(d, scale), 0n);
|
|
1122
|
-
if (name === ATTR_SUM)
|
|
1123
|
-
return decimalText(sum, scale);
|
|
1124
|
-
// An average, rounded half away from zero at two places past the values',
|
|
1125
|
-
// which are then dropped while they are zeros.
|
|
1126
|
-
const scaled = sum * 100n;
|
|
1127
|
-
const n = BigInt(decimals.length);
|
|
1128
|
-
const sign = scaled < 0n ? -1n : 1n;
|
|
1129
|
-
let text = decimalText((2n * scaled + sign * n) / (2n * n), scale + 2);
|
|
1130
|
-
for (let i = 0; i < 2 && text.endsWith("0"); i++)
|
|
1131
|
-
text = text.slice(0, -1);
|
|
1132
|
-
return text.endsWith(".") ? text.slice(0, -1) : text;
|
|
1133
|
-
}
|
|
1134
|
-
/** Set each aggregate element from rows, each column read as its type. */
|
|
1135
|
-
function aggregate(elements, rows, types) {
|
|
1136
|
-
for (const el of elements) {
|
|
1137
|
-
const name = AGGREGATES.find((a) => el.hasAttribute(a));
|
|
1138
|
-
const column = el.getAttribute(name) ?? "";
|
|
1139
|
-
land(el, aggregateOf(name, column, rows, types[column]));
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
858
|
/** Show or hide an element for a filter, saying so only when it changes. */
|
|
1143
859
|
function hide(el, hidden) {
|
|
1144
860
|
if (el.hasAttribute("hidden") !== hidden)
|
|
1145
861
|
el.toggleAttribute("hidden", hidden);
|
|
1146
862
|
}
|
|
1147
|
-
/**
|
|
1148
|
-
* Finish a group and everything under it, from the rows it shows: each
|
|
1149
|
-
* heading filled and linked from the first, as it was when last placed by
|
|
1150
|
-
* the order,
|
|
1151
|
-
* each aggregate set from all of them, and a group hidden when a filter hides
|
|
1152
|
-
* every row in it. The rows it shows are returned.
|
|
1153
|
-
*/
|
|
1154
|
-
function finish(group, types) {
|
|
1155
|
-
const shown = group.children.length > 0
|
|
1156
|
-
? group.children.flatMap((child) => finish(child, types))
|
|
1157
|
-
: group.rows.filter((entry) => !entry.hidden);
|
|
1158
|
-
const rows = shown.map((entry) => entry.data);
|
|
1159
|
-
const hidden = rows.length === 0 && rowsUnder(group).length > 0;
|
|
1160
|
-
if (hidden !== group.hidden) {
|
|
1161
|
-
group.hidden = hidden;
|
|
1162
|
-
for (const node of group.nodes) {
|
|
1163
|
-
if (node.nodeType === ELEMENT_NODE && node !== group.slot) {
|
|
1164
|
-
hide(node, hidden);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
if (shown[0]) {
|
|
1169
|
-
for (const head of group.heads) {
|
|
1170
|
-
fill(head, shown[0].placed);
|
|
1171
|
-
link(head, shown[0].placed);
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
aggregate(group.aggregates, rows, types);
|
|
1175
|
-
return shown;
|
|
1176
|
-
}
|
|
1177
863
|
/** The order to place rows by, or none when it cannot be read. */
|
|
1178
864
|
function termsOf(el, order) {
|
|
1179
865
|
try {
|
|
@@ -1205,73 +891,46 @@ function filtered(el) {
|
|
|
1205
891
|
.filter((column) => column !== "");
|
|
1206
892
|
}
|
|
1207
893
|
/**
|
|
1208
|
-
* The order an element's rows are placed by:
|
|
1209
|
-
* `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`.
|
|
1210
895
|
*/
|
|
1211
|
-
function
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1214
|
-
return
|
|
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);
|
|
1215
904
|
}
|
|
1216
905
|
/**
|
|
1217
906
|
* Whether a row passes every filter the user chose for the element: for each
|
|
1218
|
-
* column its `lb-filter` names, the filter in the
|
|
1219
|
-
* the same name.
|
|
907
|
+
* column its `lb-filter` names, the filter in the query's row of
|
|
908
|
+
* `lb-filters` of the same name.
|
|
1220
909
|
*/
|
|
1221
|
-
function filterFor(el,
|
|
1222
|
-
const
|
|
910
|
+
function filterFor(el, filters, types) {
|
|
911
|
+
const terms = [];
|
|
1223
912
|
for (const column of filtered(el)) {
|
|
1224
|
-
const text = choiceIn(
|
|
1225
|
-
const
|
|
1226
|
-
if (
|
|
1227
|
-
|
|
913
|
+
const text = choiceIn(filters, column);
|
|
914
|
+
const parsed = text === undefined ? [] : parseFilter(text);
|
|
915
|
+
if (parsed.length > 0)
|
|
916
|
+
terms.push([column, parsed]);
|
|
1228
917
|
}
|
|
1229
|
-
return (row) =>
|
|
918
|
+
return (row) => terms.every(([column, parsed]) => passes(row[column], parsed, types[column]));
|
|
1230
919
|
}
|
|
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
920
|
function filterRow(entry, hidden) {
|
|
1236
921
|
if (entry.hidden === hidden)
|
|
1237
922
|
return;
|
|
1238
923
|
entry.hidden = hidden;
|
|
1239
924
|
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
925
|
}
|
|
1247
926
|
/** Whether a row's columns as landed differ from what they were. */
|
|
1248
927
|
function differs(before, after) {
|
|
1249
928
|
return Object.entries(after).some(([column, value]) => !same(before[column], value));
|
|
1250
929
|
}
|
|
1251
930
|
/**
|
|
1252
|
-
* Land rows through a row template.
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
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.
|
|
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.
|
|
1275
934
|
*/
|
|
1276
935
|
export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
1277
936
|
const list = listFor(el, template);
|
|
@@ -1280,12 +939,11 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
1280
939
|
const doc = options.doc ?? el.ownerDocument;
|
|
1281
940
|
const host = el;
|
|
1282
941
|
const whole = !isPatch(result);
|
|
1283
|
-
const shown =
|
|
942
|
+
const shown = list.rows;
|
|
1284
943
|
const focused = focusedRow(list, doc);
|
|
1285
944
|
// What a request did is said by this landing alone.
|
|
1286
945
|
for (const entry of shown)
|
|
1287
946
|
entry.el.removeAttribute(ATTR_ROW_REQUESTED);
|
|
1288
|
-
// The rows arriving, by key.
|
|
1289
947
|
const arriving = new Map();
|
|
1290
948
|
for (const data of whole ? result : (result.rows ?? [])) {
|
|
1291
949
|
if (data[keyColumn] === undefined) {
|
|
@@ -1310,7 +968,6 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
1310
968
|
const gone = whole
|
|
1311
969
|
? shown.filter((entry) => !arriving.has(entry.key))
|
|
1312
970
|
: dropped.flatMap((key) => list.byKey.get(key) ?? []);
|
|
1313
|
-
// The creation loop.
|
|
1314
971
|
const created = new Set();
|
|
1315
972
|
for (const key of arriving.keys()) {
|
|
1316
973
|
if (!list.byKey.has(key))
|
|
@@ -1341,13 +998,7 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
1341
998
|
// already showing with what is new after it. A sort keeps rows that
|
|
1342
999
|
// compare equal in this order.
|
|
1343
1000
|
const stayed = shown.filter((entry) => list.byKey.get(entry.key) === entry);
|
|
1344
|
-
const terms = termsOf(el,
|
|
1345
|
-
if (list.levels.length > terms.length && !warned.has(el)) {
|
|
1346
|
-
warned.add(el);
|
|
1347
|
-
console.warn(`lb-hub: <${el.localName}> has ${list.levels.length} group ` +
|
|
1348
|
-
`template(s) and an order of ${terms.length} term(s); a level with ` +
|
|
1349
|
-
`no term shows its rows as one group`);
|
|
1350
|
-
}
|
|
1001
|
+
const terms = termsOf(el, options.order);
|
|
1351
1002
|
const sort = (entries) => terms.length === 0
|
|
1352
1003
|
? entries
|
|
1353
1004
|
: entries
|
|
@@ -1355,7 +1006,7 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
1355
1006
|
.sort((a, b) => compareRows(a.entry.data, b.entry.data, terms, options.types) ||
|
|
1356
1007
|
a.i - b.i)
|
|
1357
1008
|
.map(({ entry }) => entry);
|
|
1358
|
-
const passing = filterFor(el, options.
|
|
1009
|
+
const passing = filterFor(el, options.filters, options.types ?? {});
|
|
1359
1010
|
const manual = options.requested ? manualOf(el) : NO_MANUAL;
|
|
1360
1011
|
const touched = new Set([...changed, ...created]);
|
|
1361
1012
|
// A row left out of place stays touched until the rows are placed again.
|
|
@@ -1365,11 +1016,10 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
1365
1016
|
let order;
|
|
1366
1017
|
let out = new Set();
|
|
1367
1018
|
if (manual.order) {
|
|
1368
|
-
// What already shows stays where it stands, and what is new goes last
|
|
1369
|
-
// in the group it belongs to.
|
|
1019
|
+
// What already shows stays where it stands, and what is new goes last.
|
|
1370
1020
|
for (const entry of created)
|
|
1371
1021
|
entry.placed = entry.data;
|
|
1372
|
-
order =
|
|
1022
|
+
order = [...stayed, ...created];
|
|
1373
1023
|
out = movedRows(order, sort(order), touched);
|
|
1374
1024
|
}
|
|
1375
1025
|
else {
|
|
@@ -1383,15 +1033,12 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
1383
1033
|
const kept = manual.filter && touched.has(entry) && !entry.hidden && fails;
|
|
1384
1034
|
const hides = fails && !kept;
|
|
1385
1035
|
filterRow(entry, hides);
|
|
1386
|
-
|
|
1387
|
-
const moves = manual.order && regrouped(entry, list, terms);
|
|
1388
|
-
if (!moves)
|
|
1389
|
-
entry.placed = entry.data;
|
|
1036
|
+
entry.placed = entry.data;
|
|
1390
1037
|
// A row nobody sees stands nowhere the user would notice.
|
|
1391
|
-
entry.unplaced = !hides && (out.has(entry) || kept
|
|
1038
|
+
entry.unplaced = !hides && (out.has(entry) || kept);
|
|
1392
1039
|
}
|
|
1393
1040
|
const moved = movedRows(stayed, order, changed);
|
|
1394
|
-
|
|
1041
|
+
placeRows(list, order);
|
|
1395
1042
|
refocus(list, focused);
|
|
1396
1043
|
const stamps = new Stamps();
|
|
1397
1044
|
for (const key of arriving.keys()) {
|
|
@@ -1421,7 +1068,7 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
1421
1068
|
if (entry.unplaced)
|
|
1422
1069
|
unplaced++;
|
|
1423
1070
|
}
|
|
1424
|
-
const rows =
|
|
1071
|
+
const rows = list.rows.filter((entry) => !entry.hidden);
|
|
1425
1072
|
// Counted from what the hub holds, which is what the rows show.
|
|
1426
1073
|
setStamp(el, ATTR_QUERY_ROW_COUNT, String(rows.length + list.unentered.length));
|
|
1427
1074
|
standLast(list);
|
|
@@ -1431,8 +1078,6 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
|
|
|
1431
1078
|
else
|
|
1432
1079
|
setStamp(el, ATTR_QUERY_ORDER, orderText(terms));
|
|
1433
1080
|
const departing = gone.map((entry) => entry.el);
|
|
1434
|
-
for (const group of retired)
|
|
1435
|
-
leaveGroup(group, departing);
|
|
1436
1081
|
if (departing.length > 0)
|
|
1437
1082
|
depart(departing);
|
|
1438
1083
|
host.lbRowsLanded?.();
|
|
@@ -1445,39 +1090,6 @@ function manualOf(el) {
|
|
|
1445
1090
|
filter: sides.includes(MANUAL_FILTER),
|
|
1446
1091
|
};
|
|
1447
1092
|
}
|
|
1448
|
-
/** The order's terms a list's groups break on, outermost first. */
|
|
1449
|
-
function groupTerms(list, terms) {
|
|
1450
|
-
return terms.slice(0, list.levels.length);
|
|
1451
|
-
}
|
|
1452
|
-
/** The group a row's columns put it in, as one string. */
|
|
1453
|
-
function groupOf(row, terms) {
|
|
1454
|
-
return terms.map((term) => groupId(term.column, row[term.column])).join("\u0001");
|
|
1455
|
-
}
|
|
1456
|
-
/** Whether a row's columns now put it in another group than it stands in. */
|
|
1457
|
-
function regrouped(entry, list, terms) {
|
|
1458
|
-
const by = groupTerms(list, terms);
|
|
1459
|
-
return groupOf(entry.data, by) !== groupOf(entry.placed, by);
|
|
1460
|
-
}
|
|
1461
|
-
/**
|
|
1462
|
-
* The rows of a list the user is working in: those showing where they
|
|
1463
|
-
* stand, and each new one after the last row of its group, or last of all
|
|
1464
|
-
* when its group shows no row.
|
|
1465
|
-
*/
|
|
1466
|
-
function heldOrder(stayed, created, by) {
|
|
1467
|
-
const order = [...stayed];
|
|
1468
|
-
for (const entry of created) {
|
|
1469
|
-
const group = groupOf(entry.placed, by);
|
|
1470
|
-
let at = order.length;
|
|
1471
|
-
if (by.length > 0) {
|
|
1472
|
-
while (at > 0 && groupOf(order[at - 1].placed, by) !== group)
|
|
1473
|
-
at--;
|
|
1474
|
-
if (at === 0)
|
|
1475
|
-
at = order.length;
|
|
1476
|
-
}
|
|
1477
|
-
order.splice(at, 0, entry);
|
|
1478
|
-
}
|
|
1479
|
-
return order;
|
|
1480
|
-
}
|
|
1481
1093
|
function focusedRow(list, doc) {
|
|
1482
1094
|
const active = doc.activeElement;
|
|
1483
1095
|
const row = active?.closest(LIVE_ROW);
|
|
@@ -1503,7 +1115,7 @@ function refocus(list, focused) {
|
|
|
1503
1115
|
active.scrollIntoView?.({ block: "nearest" });
|
|
1504
1116
|
return;
|
|
1505
1117
|
}
|
|
1506
|
-
const first =
|
|
1118
|
+
const first = list.rows.find((row) => !row.hidden);
|
|
1507
1119
|
if (first)
|
|
1508
1120
|
focusIn(first.el);
|
|
1509
1121
|
}
|
|
@@ -1511,7 +1123,7 @@ function refocus(list, focused) {
|
|
|
1511
1123
|
function landItem(el, item, options = {}) {
|
|
1512
1124
|
const template = lists.get(el)?.top ?? templateIn(el);
|
|
1513
1125
|
if (item.kind === KIND_ROW) {
|
|
1514
|
-
const row = item
|
|
1126
|
+
const row = oneRow(item);
|
|
1515
1127
|
if (template) {
|
|
1516
1128
|
applyRows(el, template, item.key, row ? [row] : [], options);
|
|
1517
1129
|
return;
|
|
@@ -1523,8 +1135,8 @@ function landItem(el, item, options = {}) {
|
|
|
1523
1135
|
el.removeAttribute(ATTR_KEY_VALUE);
|
|
1524
1136
|
return;
|
|
1525
1137
|
}
|
|
1526
|
-
applyRow(el, row);
|
|
1527
1138
|
const key = row[item.key];
|
|
1139
|
+
applyRow(el, row);
|
|
1528
1140
|
if (key !== undefined && key !== null) {
|
|
1529
1141
|
setStamp(el, ATTR_KEY_VALUE, String(key));
|
|
1530
1142
|
}
|
|
@@ -1563,9 +1175,9 @@ export function keptAnswer(root, query) {
|
|
|
1563
1175
|
function rowsOptions(state, item) {
|
|
1564
1176
|
const declared = state.answers.get(item.query) ?? item;
|
|
1565
1177
|
return {
|
|
1566
|
-
order: declared.order,
|
|
1178
|
+
order: sortOf(state, item.query) ?? declared.order,
|
|
1567
1179
|
types: declared.types,
|
|
1568
|
-
|
|
1180
|
+
filters: filtersOf(state, item.query),
|
|
1569
1181
|
doc: state.doc,
|
|
1570
1182
|
};
|
|
1571
1183
|
}
|
|
@@ -1619,38 +1231,41 @@ function keep(answers, item) {
|
|
|
1619
1231
|
rows: patched(before.rows ?? [], item.key, item.patch),
|
|
1620
1232
|
});
|
|
1621
1233
|
}
|
|
1622
|
-
/** The
|
|
1623
|
-
function
|
|
1624
|
-
const
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
+
}
|
|
1630
1252
|
}
|
|
1631
|
-
return
|
|
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)));
|
|
1253
|
+
return queries;
|
|
1638
1254
|
}
|
|
1639
1255
|
/**
|
|
1640
|
-
* Place again the rows of every
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
1643
|
-
* 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.
|
|
1644
1259
|
*/
|
|
1645
|
-
function rechoose(root, state,
|
|
1646
|
-
for (const
|
|
1647
|
-
|
|
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)
|
|
1648
1264
|
continue;
|
|
1649
1265
|
for (const el of everywhere(root, `[${ATTR_QUERY}="${query}"]`)) {
|
|
1650
1266
|
if (!state.landed.has(el) || el.closest(LEAVING))
|
|
1651
1267
|
continue;
|
|
1652
|
-
|
|
1653
|
-
landItem(el, item, rowsOptions(state, item));
|
|
1268
|
+
landItem(el, item, rowsOptions(state, item));
|
|
1654
1269
|
}
|
|
1655
1270
|
}
|
|
1656
1271
|
}
|
|
@@ -1677,14 +1292,16 @@ function fillArrivals(root, state) {
|
|
|
1677
1292
|
/**
|
|
1678
1293
|
* Land response items. Every answer arrives through here, and is kept for
|
|
1679
1294
|
* `root`, so that what arrives after it is filled from it — see forget().
|
|
1680
|
-
* An answer to `lb-
|
|
1681
|
-
* everything has landed, each custom element placed since the last
|
|
1682
|
-
* 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.
|
|
1683
1298
|
*/
|
|
1684
1299
|
export function applyResponse(root, items, options = {}) {
|
|
1685
1300
|
const state = keptFor(root);
|
|
1686
1301
|
for (const item of items) {
|
|
1687
|
-
const
|
|
1302
|
+
const changed = item.query === SORT_QUERY || item.query === FILTERS_QUERY
|
|
1303
|
+
? changedQueries(state, item)
|
|
1304
|
+
: new Set();
|
|
1688
1305
|
keep(state.answers, item);
|
|
1689
1306
|
const targets = everywhere(root, `[${ATTR_QUERY}="${item.query}"]`).filter((el) => !el.closest(LEAVING));
|
|
1690
1307
|
if (targets.length === 0 &&
|
|
@@ -1699,8 +1316,8 @@ export function applyResponse(root, items, options = {}) {
|
|
|
1699
1316
|
requested: options.requested,
|
|
1700
1317
|
});
|
|
1701
1318
|
}
|
|
1702
|
-
if (
|
|
1703
|
-
rechoose(root, state,
|
|
1319
|
+
if (changed.size > 0)
|
|
1320
|
+
rechoose(root, state, changed);
|
|
1704
1321
|
}
|
|
1705
1322
|
fillArrivals(root, state);
|
|
1706
1323
|
attachPlaced();
|