@mosaicajs/components 0.3.0 → 0.4.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 (52) hide show
  1. package/README.md +2 -0
  2. package/dist/aggregate.d.ts +4 -10
  3. package/dist/aggregate.js +7 -13
  4. package/dist/artifact-card.d.ts +21 -7
  5. package/dist/artifact-card.js +136 -79
  6. package/dist/base.d.ts +44 -5
  7. package/dist/base.js +54 -5
  8. package/dist/cluster-filter.d.ts +29 -25
  9. package/dist/cluster-filter.js +123 -106
  10. package/dist/colour-editor.d.ts +15 -4
  11. package/dist/colour-editor.js +107 -34
  12. package/dist/colour-picker.d.ts +12 -5
  13. package/dist/colour-picker.js +119 -32
  14. package/dist/display.js +77 -9
  15. package/dist/events.d.ts +21 -7
  16. package/dist/explorer.d.ts +116 -37
  17. package/dist/explorer.js +476 -210
  18. package/dist/field-card.d.ts +304 -48
  19. package/dist/field-card.js +1364 -244
  20. package/dist/filter-panel.d.ts +54 -10
  21. package/dist/filter-panel.js +242 -42
  22. package/dist/filter.d.ts +22 -4
  23. package/dist/filter.js +113 -34
  24. package/dist/hierarchy.d.ts +68 -45
  25. package/dist/hierarchy.js +320 -243
  26. package/dist/item-card.d.ts +47 -16
  27. package/dist/item-card.js +241 -108
  28. package/dist/key-picker.d.ts +2 -1
  29. package/dist/key-picker.js +4 -3
  30. package/dist/layer-picker.d.ts +4 -2
  31. package/dist/layer-picker.js +16 -7
  32. package/dist/lineage.d.ts +132 -0
  33. package/dist/lineage.js +307 -0
  34. package/dist/map.d.ts +135 -24
  35. package/dist/map.js +387 -117
  36. package/dist/mosaica-components.js +1837 -833
  37. package/dist/mosaica-components.js.sri +1 -1
  38. package/dist/mosaica-components.json +2 -2
  39. package/dist/parts.d.ts +4 -4
  40. package/dist/parts.js +18 -4
  41. package/dist/paths.d.ts +34 -12
  42. package/dist/paths.js +84 -46
  43. package/dist/selection.d.ts +22 -3
  44. package/dist/selection.js +125 -59
  45. package/dist/states.d.ts +72 -0
  46. package/dist/states.js +135 -26
  47. package/dist/status.d.ts +63 -5
  48. package/dist/status.js +215 -36
  49. package/dist/tokens.d.ts +8 -4
  50. package/dist/tokens.js +144 -14
  51. package/dist/view-picker.js +5 -0
  52. package/package.json +4 -3
@@ -7,7 +7,8 @@ import { chrome, tokens } from './tokens.js';
7
7
  /**
8
8
  * Put the layer picker beside a map when the bundle has annotation layers that viewers should be
9
9
  * able to turn on and off. A layer that others depend on, such as a clustering and its labels, has
10
- * one checkbox, so that the whole group is turned on together.
10
+ * one checkbox, so that the whole group is turned on together. The row's `title` names the layers
11
+ * it turns on, and a screen reader hears them as the checkbox's description.
11
12
  *
12
13
  * Filter layers draw nothing, so the picker names them in a note instead of giving them checkboxes.
13
14
  * Viewers filter by a filter layer from its card in `<mosaica-filter-panel>`, or browse it in
@@ -23,7 +24,8 @@ import { chrome, tokens } from './tokens.js';
23
24
  * @csspart refusal - The words "View refused", with `data-code`, in the refused state.
24
25
  * @csspart reason - The server's reason for a refusal, under "View refused".
25
26
  * @csspart entry - One layer that draws, with `data-layer`.
26
- * @csspart name - A layer's title, or its name if it has no title.
27
+ * @csspart name - A layer's title, or its name if it has no title, on one line unless
28
+ * `--mosaica-long-text` is `normal`.
27
29
  * @csspart note - The note naming the filter layers, such as "Venues can be used as a filter
28
30
  * only.", with `data-layers` listing their names.
29
31
  */
@@ -50,7 +52,8 @@ export class MosaicaLayerPicker extends MosaicaElement {
50
52
  [part='name'] {
51
53
  overflow: hidden;
52
54
  text-overflow: ellipsis;
53
- white-space: nowrap;
55
+ white-space: var(--_mosaica-long-text);
56
+ overflow-wrap: anywhere;
54
57
  }
55
58
  `
56
59
  ];
@@ -84,13 +87,15 @@ export class MosaicaLayerPicker extends MosaicaElement {
84
87
  if (drawn.length + filters.length === 0)
85
88
  return html `<div class="panel">${heading}<span part="state" data-state="empty">No layers</span></div>`;
86
89
  const on = new Set(s.get('artifacts').layers);
90
+ const titled = (name) => meta.layers.find((l) => l.name === name)?.title || name;
87
91
  return html `<div class="panel">${heading}
88
92
  <span part="state" data-state="shown"></span>
89
93
  <div class="col">
90
- ${drawn.map((e) => html `<label part="entry" class="check" data-layer=${e.root.name} title=${e.closure.length > 1 ? e.closure.join(' + ') : nothing}>
91
- <input type="checkbox" .checked=${on.has(e.root.name)} @change=${(ev) => this.toggle(e, ev.target.checked)} />
92
- <span part="name">${e.root.title || e.root.name}</span>
93
- </label>`)}
94
+ ${drawn.map((e, i) => html `<label part="entry" class="check" data-layer=${e.root.name} title=${[e.root.name, ...linkedTo(e)].map(titled).join(' + ')}>
95
+ <input type="checkbox" aria-describedby=${e.closure.length > 1 ? `linked-${i}` : nothing} .checked=${on.has(e.root.name)} @change=${(ev) => this.toggle(e, ev.target.checked)} />
96
+ <span part="name">${e.root.title || e.root.name}</span>
97
+ </label>
98
+ ${e.closure.length > 1 ? html `<span id=${`linked-${i}`} class="sr-only" data-layers=${linkedTo(e).join(' ')}>with ${linkedTo(e).map(titled).join(', ')}</span>` : nothing}`)}
94
99
  </div>
95
100
  ${filters.length > 0
96
101
  ? html `<p part="note" data-layers=${filters.map((e) => e.root.name).join(' ')}>${filterNote(filters.map((e) => e.root.title || e.root.name))}</p>`
@@ -98,6 +103,10 @@ export class MosaicaLayerPicker extends MosaicaElement {
98
103
  </div>`;
99
104
  }
100
105
  }
106
+ /** The layers an entry draws with its own, which its row's `title` and the text its checkbox is described by name. */
107
+ function linkedTo(entry) {
108
+ return entry.closure.filter((name) => name !== entry.root.name);
109
+ }
101
110
  /** "Venues can be used as a filter only.", or for several, "Venues and Countries can be used as filters only." */
102
111
  function filterNote(titles) {
103
112
  if (titles.length === 1)
@@ -0,0 +1,132 @@
1
+ import type { LitElement } from 'lit';
2
+ import { type BrowseRow, type Layer, type Refusal, type Store } from '@mosaicajs/client';
3
+ import { type Called } from './base.js';
4
+ /** One node of the walk: a row, its children once fetched, and where its paging got to. */
5
+ export type Node = {
6
+ row: BrowseRow;
7
+ /** The path that reached it; a `dag` node under two parents is two nodes with different paths. */
8
+ path: string;
9
+ children: Node[] | null;
10
+ next: string | null;
11
+ loading: boolean;
12
+ refusal: Refusal | null;
13
+ };
14
+ /** What a {@link Lineage} reads from the element that holds it. */
15
+ export type LineageHost = {
16
+ store(): Store | null;
17
+ /** The layer to browse; `null` asks for nothing. */
18
+ layer(): Layer | null;
19
+ /** Rows per page, which the server's `maxBrowseRows` caps. */
20
+ limit(): number;
21
+ /** Whether the row at `path` is open, so a refetch fetches its children again. */
22
+ isOpen(path: string): boolean;
23
+ /** Called after each change to what the walk holds. */
24
+ changed(): void;
25
+ };
26
+ /**
27
+ * The served rows of one layer from the browse route, under the store's filters, a page at a time:
28
+ * the children of {@link top}, or the roots where it is `null`, and the children of each node
29
+ * expanded under them. Counts are taken under the filters the store sends, so a change of filters
30
+ * or of {@link top} is a new question, and the rows are fetched again: as many pages of the top
31
+ * rows and of each open row as More had loaded, since a page's cursor is good only for the filters
32
+ * it was served under. Every row met is kept for its name, and a `dag` row's other parents that no
33
+ * page has served are asked for by name.
34
+ */
35
+ export declare class Lineage {
36
+ private readonly host;
37
+ /** The top rows, once fetched. */
38
+ roots: Node[] | null;
39
+ loading: boolean;
40
+ refusal: Refusal | null;
41
+ /** The artifact whose children are the top rows; `null` for the layer's roots. */
42
+ top: bigint | null;
43
+ /** The palette size each row's slot is chosen for; `null` asks for no slots. */
44
+ paletteSize: number | null;
45
+ /** The names and parents of every row met. */
46
+ private readonly paths;
47
+ /** The cursor of the top rows' next page, with the question it was served under. */
48
+ private next;
49
+ /** The question the top rows were fetched under; see {@link question}. */
50
+ private fetchedUnder;
51
+ /** How many pages More has loaded under each path, and under the top's path for the top rows, so a refetch loads as many. */
52
+ private readonly pages;
53
+ /** How many of a refetch's requests are in flight, and those waiting for a place. */
54
+ private asking;
55
+ private waiting;
56
+ /** The top the rows held were fetched under. */
57
+ private fetchedTop;
58
+ /** Moved by {@link reset}, so a page asked for before it is dropped. */
59
+ private epoch;
60
+ constructor(host: LineageHost);
61
+ /** Drop everything fetched and every name met, as a change of viewer does. */
62
+ reset(): void;
63
+ /** Drop the rows and their paging, as a change of layer does; the names met are kept. */
64
+ clear(): void;
65
+ /** Whether the top rows have a next page under the question being asked, which {@link loadMore} loads. */
66
+ get hasMore(): boolean;
67
+ /** Whether every page of the top rows held has landed. */
68
+ get complete(): boolean;
69
+ /** Whether the top rows held were fetched for {@link top} and nothing is on its way. */
70
+ get settled(): boolean;
71
+ /** The path of the top rows' parent: `''` for the roots. */
72
+ private get topPath();
73
+ /**
74
+ * The question the rows answer, as a string: the layer, the top, the palette size and the
75
+ * store's filters. A change refetches the top rows and the children of the open rows.
76
+ */
77
+ private question;
78
+ /**
79
+ * Fetch the top rows where the question they were fetched under is no longer the one being
80
+ * asked. Rows of another top are dropped at once, so they are not shown as its children.
81
+ */
82
+ loadIfStale(): void;
83
+ /** One page of the layer, under the store's filters. */
84
+ private page;
85
+ /** The rows whose names hold `q`, under the store's filters, as nodes of their own; `null` with no layer to search. */
86
+ search(q: string): Promise<Node[]> | null;
87
+ /** Run `ask` in one of a refetch's {@link REFETCH_AT_ONCE} places, once one is free. */
88
+ private placed;
89
+ /**
90
+ * The rows under `parent`, or the top rows: the page at `cursor`, or from the first page as many
91
+ * as More had loaded there. The store aborts no request, so a fetch no longer wanted asks for
92
+ * nothing more and returns `null`.
93
+ */
94
+ private rowsUnder;
95
+ /** Load the top rows' next page under the question being asked, and return the rows it added. */
96
+ loadMore(): Promise<Node[]>;
97
+ /**
98
+ * The top rows, with the children of every row among them that is open. They replace the rows,
99
+ * or the page at `cursor` joins them, once those have landed, so a walk under one question is
100
+ * drawn whole under the next. Rows asked for under a question since replaced are dropped.
101
+ * Returns the rows it added.
102
+ */
103
+ private loadRoots;
104
+ /** Fetch the children of each open row among `nodes` and under them that has none yet. */
105
+ private reopen;
106
+ /** A node for `row` under the node at `parentPath`, the row kept for its name. */
107
+ private node;
108
+ /**
109
+ * Ask by name for the other parents of `nodes` that no page has served, through the children
110
+ * form of the first node under each, which names all of that node's parents.
111
+ */
112
+ private askNames;
113
+ /** What an artifact is called from the row a page served, or `null` where none has met it. */
114
+ knownName(id: bigint): Called | null;
115
+ /** What to call an artifact: as a page served it, else Unnamed. */
116
+ nameOf(id: bigint): Called;
117
+ /** What the served parents of a node's row other than the one it was reached under are called, which a `dag` row names. */
118
+ alsoUnder(node: Node): Called[];
119
+ /**
120
+ * Expand a node: its children, or with `more` their next page. A node keeps what it fetched
121
+ * while {@link question} is unchanged, so reopening it fetches nothing. Returns the rows it added.
122
+ */
123
+ expand(node: Node, more?: boolean): Promise<Node[]>;
124
+ private expandUnder;
125
+ }
126
+ /** A browse row's counts as a screen reader is given them: matching over every item where a filter is set, else every item. */
127
+ export declare function spokenCounts(row: BrowseRow): string;
128
+ /**
129
+ * Once More's page has landed, move focus from More to `first`, the first row it added, unless
130
+ * focus has gone elsewhere meanwhile. More may be gone from the page by then.
131
+ */
132
+ export declare function focusAdded(host: LitElement, more: HTMLElement, first: () => HTMLElement | null | undefined): Promise<void>;
@@ -0,0 +1,307 @@
1
+ import { refusalOf } from '@mosaicajs/client/internal';
2
+ import { called, countText, focusLost } from './base.js';
3
+ import { ClusterPaths } from './paths.js';
4
+ /** How many requests a refetch of the rows has in flight at once. */
5
+ const REFETCH_AT_ONCE = 4;
6
+ /** A row's press, which waits for nothing. */
7
+ const PRESSED = { wanted: () => true, refetch: false };
8
+ /**
9
+ * The served rows of one layer from the browse route, under the store's filters, a page at a time:
10
+ * the children of {@link top}, or the roots where it is `null`, and the children of each node
11
+ * expanded under them. Counts are taken under the filters the store sends, so a change of filters
12
+ * or of {@link top} is a new question, and the rows are fetched again: as many pages of the top
13
+ * rows and of each open row as More had loaded, since a page's cursor is good only for the filters
14
+ * it was served under. Every row met is kept for its name, and a `dag` row's other parents that no
15
+ * page has served are asked for by name.
16
+ */
17
+ export class Lineage {
18
+ host;
19
+ /** The top rows, once fetched. */
20
+ roots = null;
21
+ loading = false;
22
+ refusal = null;
23
+ /** The artifact whose children are the top rows; `null` for the layer's roots. */
24
+ top = null;
25
+ /** The palette size each row's slot is chosen for; `null` asks for no slots. */
26
+ paletteSize = null;
27
+ /** The names and parents of every row met. */
28
+ paths;
29
+ /** The cursor of the top rows' next page, with the question it was served under. */
30
+ next = null;
31
+ /** The question the top rows were fetched under; see {@link question}. */
32
+ fetchedUnder = '';
33
+ /** How many pages More has loaded under each path, and under the top's path for the top rows, so a refetch loads as many. */
34
+ pages = new Map();
35
+ /** How many of a refetch's requests are in flight, and those waiting for a place. */
36
+ asking = 0;
37
+ waiting = [];
38
+ /** The top the rows held were fetched under. */
39
+ fetchedTop = null;
40
+ /** Moved by {@link reset}, so a page asked for before it is dropped. */
41
+ epoch = 0;
42
+ constructor(host) {
43
+ this.host = host;
44
+ this.paths = new ClusterPaths(() => host.changed());
45
+ }
46
+ /** Drop everything fetched and every name met, as a change of viewer does. */
47
+ reset() {
48
+ this.epoch += 1;
49
+ this.roots = null;
50
+ this.next = null;
51
+ this.loading = false;
52
+ this.refusal = null;
53
+ this.pages.clear();
54
+ this.paths.reset();
55
+ this.fetchedUnder = '';
56
+ this.fetchedTop = null;
57
+ }
58
+ /** Drop the rows and their paging, as a change of layer does; the names met are kept. */
59
+ clear() {
60
+ this.roots = null;
61
+ this.next = null;
62
+ this.pages.clear();
63
+ }
64
+ /** Whether the top rows have a next page under the question being asked, which {@link loadMore} loads. */
65
+ get hasMore() {
66
+ return this.next !== null && this.next.under === this.fetchedUnder;
67
+ }
68
+ /** Whether every page of the top rows held has landed. */
69
+ get complete() {
70
+ return this.roots !== null && this.next === null;
71
+ }
72
+ /** Whether the top rows held were fetched for {@link top} and nothing is on its way. */
73
+ get settled() {
74
+ return this.roots !== null && this.fetchedTop === this.top && !this.loading;
75
+ }
76
+ /** The path of the top rows' parent: `''` for the roots. */
77
+ get topPath() {
78
+ return this.top === null ? '' : `/${this.top}`;
79
+ }
80
+ /**
81
+ * The question the rows answer, as a string: the layer, the top, the palette size and the
82
+ * store's filters. A change refetches the top rows and the children of the open rows.
83
+ */
84
+ question() {
85
+ // Hashed from `requestFilters()`, what `store.browse` sends, which includes the drawn region.
86
+ // A `member_of` leaf holds its artifact as a decimal string, so it serialises.
87
+ return `${this.host.layer()?.name ?? ''}|${this.top ?? ''}|${this.paletteSize ?? ''}|${JSON.stringify(this.host.store()?.requestFilters() ?? null)}`;
88
+ }
89
+ /**
90
+ * Fetch the top rows where the question they were fetched under is no longer the one being
91
+ * asked. Rows of another top are dropped at once, so they are not shown as its children.
92
+ */
93
+ loadIfStale() {
94
+ const q = this.question();
95
+ if (q === this.fetchedUnder || !this.host.store() || !this.host.layer())
96
+ return;
97
+ this.fetchedUnder = q;
98
+ if (this.top !== this.fetchedTop) {
99
+ this.fetchedTop = this.top;
100
+ this.roots = null;
101
+ this.next = null;
102
+ this.refusal = null;
103
+ }
104
+ void this.loadRoots();
105
+ }
106
+ /** One page of the layer, under the store's filters. */
107
+ page(req) {
108
+ const s = this.host.store();
109
+ const layer = this.host.layer();
110
+ if (!s || !layer)
111
+ return null;
112
+ const ceiling = s.get('meta')?.selection.maxBrowseRows ?? this.host.limit();
113
+ return s.browse({ ...req, layer: layer.name, limit: Math.max(1, Math.min(this.host.limit(), ceiling)), ...(this.paletteSize === null ? {} : { paletteSize: this.paletteSize }) });
114
+ }
115
+ /** The rows whose names hold `q`, under the store's filters, as nodes of their own; `null` with no layer to search. */
116
+ search(q) {
117
+ const request = this.page({ q });
118
+ if (!request)
119
+ return null;
120
+ const epoch = this.epoch;
121
+ return request.then((page) => {
122
+ const nodes = page.artifacts.map((row) => this.node(row, 'q'));
123
+ if (epoch === this.epoch)
124
+ this.askNames(nodes);
125
+ return nodes;
126
+ });
127
+ }
128
+ /** Run `ask` in one of a refetch's {@link REFETCH_AT_ONCE} places, once one is free. */
129
+ async placed(ask) {
130
+ if (this.asking < REFETCH_AT_ONCE)
131
+ this.asking++;
132
+ else
133
+ await new Promise((free) => this.waiting.push(free));
134
+ try {
135
+ return await ask();
136
+ }
137
+ finally {
138
+ // A waiting request takes the place over; with none, the place is free.
139
+ const next = this.waiting.shift();
140
+ if (next)
141
+ next();
142
+ else
143
+ this.asking--;
144
+ }
145
+ }
146
+ /**
147
+ * The rows under `parent`, or the top rows: the page at `cursor`, or from the first page as many
148
+ * as More had loaded there. The store aborts no request, so a fetch no longer wanted asks for
149
+ * nothing more and returns `null`.
150
+ */
151
+ async rowsUnder(parent, cursor, fetch) {
152
+ const path = parent?.path ?? this.topPath;
153
+ const id = parent?.row.mosaicaId ?? this.top;
154
+ const pages = cursor === undefined ? (this.pages.get(path) ?? 1) : 1;
155
+ const nodes = [];
156
+ let next = cursor ?? null;
157
+ for (let i = 0; i < pages && (i === 0 || next !== null); i++) {
158
+ const ask = async () => (fetch.wanted() ? this.page({ ...(id === null ? {} : { parent: id }), ...(next === null ? {} : { cursor: next }) }) : null);
159
+ const page = await (fetch.refetch ? this.placed(ask) : ask());
160
+ if (!page)
161
+ return null;
162
+ nodes.push(...page.artifacts.map((row) => this.node(row, path)));
163
+ next = page.next;
164
+ }
165
+ if (cursor !== undefined)
166
+ this.pages.set(path, (this.pages.get(path) ?? 1) + 1);
167
+ return { nodes, next };
168
+ }
169
+ /** Load the top rows' next page under the question being asked, and return the rows it added. */
170
+ loadMore() {
171
+ return this.hasMore ? this.loadRoots(this.next.cursor) : Promise.resolve([]);
172
+ }
173
+ /**
174
+ * The top rows, with the children of every row among them that is open. They replace the rows,
175
+ * or the page at `cursor` joins them, once those have landed, so a walk under one question is
176
+ * drawn whole under the next. Rows asked for under a question since replaced are dropped.
177
+ * Returns the rows it added.
178
+ */
179
+ async loadRoots(cursor) {
180
+ if (!this.host.store() || !this.host.layer())
181
+ return [];
182
+ const epoch = this.epoch;
183
+ const under = this.fetchedUnder;
184
+ const wanted = () => epoch === this.epoch && under === this.fetchedUnder;
185
+ const fetch = { wanted, refetch: cursor === undefined };
186
+ this.loading = true;
187
+ this.refusal = null;
188
+ this.host.changed();
189
+ try {
190
+ const got = await this.rowsUnder(null, cursor, fetch);
191
+ if (!got || !wanted())
192
+ return [];
193
+ await this.reopen(got.nodes, fetch);
194
+ if (!wanted())
195
+ return [];
196
+ this.roots = cursor === undefined ? got.nodes : [...(this.roots ?? []), ...got.nodes];
197
+ this.next = got.next === null ? null : { cursor: got.next, under };
198
+ this.askNames(got.nodes);
199
+ // A row pressed open while these were on their way has no children yet.
200
+ void this.reopen(got.nodes, fetch);
201
+ return got.nodes;
202
+ }
203
+ catch (error) {
204
+ if (!wanted())
205
+ return [];
206
+ this.refusal = refusalOf(error);
207
+ this.roots = this.roots ?? [];
208
+ return [];
209
+ }
210
+ finally {
211
+ if (wanted())
212
+ this.loading = false;
213
+ this.host.changed();
214
+ }
215
+ }
216
+ /** Fetch the children of each open row among `nodes` and under them that has none yet. */
217
+ async reopen(nodes, fetch) {
218
+ await Promise.all(nodes
219
+ .filter((n) => this.host.isOpen(n.path) && n.row.childCount > 0)
220
+ .map((n) => (n.children !== null ? this.reopen(n.children, fetch) : n.loading ? undefined : this.expandUnder(n, false, fetch))));
221
+ }
222
+ /** A node for `row` under the node at `parentPath`, the row kept for its name. */
223
+ node(row, parentPath) {
224
+ this.paths.meet([row]);
225
+ return { row, path: `${parentPath}/${row.mosaicaId}`, children: null, next: null, loading: false, refusal: null };
226
+ }
227
+ /**
228
+ * Ask by name for the other parents of `nodes` that no page has served, through the children
229
+ * form of the first node under each, which names all of that node's parents.
230
+ */
231
+ askNames(nodes) {
232
+ const s = this.host.store();
233
+ const layer = this.host.layer();
234
+ const through = new Map();
235
+ for (const n of nodes)
236
+ for (const p of otherParents(n))
237
+ if (this.paths.nameOf(p) === null && !through.has(p))
238
+ through.set(p, n.row.mosaicaId);
239
+ if (s && layer && through.size > 0)
240
+ this.paths.ask(s, layer, [...new Set(through.values())], 1);
241
+ }
242
+ /** What an artifact is called from the row a page served, or `null` where none has met it. */
243
+ knownName(id) {
244
+ return this.paths.nameOf(id);
245
+ }
246
+ /** What to call an artifact: as a page served it, else Unnamed. */
247
+ nameOf(id) {
248
+ return this.paths.nameOf(id) ?? called(null, null);
249
+ }
250
+ /** What the served parents of a node's row other than the one it was reached under are called, which a `dag` row names. */
251
+ alsoUnder(node) {
252
+ return otherParents(node).map((p) => this.nameOf(p));
253
+ }
254
+ /**
255
+ * Expand a node: its children, or with `more` their next page. A node keeps what it fetched
256
+ * while {@link question} is unchanged, so reopening it fetches nothing. Returns the rows it added.
257
+ */
258
+ expand(node, more = false) {
259
+ return this.expandUnder(node, more, PRESSED);
260
+ }
261
+ async expandUnder(node, more, fetch) {
262
+ if (!this.host.store() || !this.host.layer())
263
+ return [];
264
+ const epoch = this.epoch;
265
+ node.loading = true;
266
+ node.refusal = null;
267
+ this.host.changed();
268
+ try {
269
+ const got = await this.rowsUnder(node, more ? (node.next ?? undefined) : undefined, fetch);
270
+ if (!got || epoch !== this.epoch)
271
+ return [];
272
+ await this.reopen(got.nodes, fetch);
273
+ node.children = more ? [...(node.children ?? []), ...got.nodes] : got.nodes;
274
+ node.next = got.next;
275
+ this.askNames(got.nodes);
276
+ return got.nodes;
277
+ }
278
+ catch (error) {
279
+ node.refusal = refusalOf(error);
280
+ node.children = node.children ?? [];
281
+ return [];
282
+ }
283
+ finally {
284
+ node.loading = false;
285
+ this.host.changed();
286
+ }
287
+ }
288
+ }
289
+ /** The served parents of a node's row other than the one it was reached under. */
290
+ function otherParents(node) {
291
+ return node.row.parentIds.filter((p) => String(p) !== node.path.split('/').at(-2));
292
+ }
293
+ /** A browse row's counts as a screen reader is given them: matching over every item where a filter is set, else every item. */
294
+ export function spokenCounts(row) {
295
+ const masked = countText(Number(row.maskedCount));
296
+ return row.matchedCount === null ? `${masked} items` : `${countText(Number(row.matchedCount))} matching, ${masked} in all`;
297
+ }
298
+ /**
299
+ * Once More's page has landed, move focus from More to `first`, the first row it added, unless
300
+ * focus has gone elsewhere meanwhile. More may be gone from the page by then.
301
+ */
302
+ export async function focusAdded(host, more, first) {
303
+ await host.updateComplete;
304
+ const onMore = host.shadowRoot?.activeElement === more || (!more.isConnected && focusLost());
305
+ if (onMore)
306
+ first()?.focus();
307
+ }