@ixfx/components 0.7.4 → 0.8.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/bundle/index.d.ts +787 -190
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2770 -592
- package/bundle/index.js.map +1 -1
- package/dist/ac-token.js +1 -1
- package/dist/crumbs.d.ts +5 -1
- package/dist/crumbs.d.ts.map +1 -1
- package/dist/crumbs.js +10 -0
- package/dist/crumbs.js.map +1 -1
- package/dist/{data-grid-BddUjAti.d.ts → data-grid-D-z7bx5-.d.ts} +26 -1
- package/dist/data-grid-D-z7bx5-.d.ts.map +1 -0
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +88 -7
- package/dist/data-grid.js.map +1 -1
- package/dist/{detail-list-Br0WAnry.d.ts → detail-list-_ZJHYEBr.d.ts} +20 -12
- package/dist/detail-list-_ZJHYEBr.d.ts.map +1 -0
- package/dist/detail-list.d.ts +2 -2
- package/dist/detail-list.js +51 -135
- package/dist/detail-list.js.map +1 -1
- package/dist/grid-list-nRUQOcEL.js +1537 -0
- package/dist/grid-list-nRUQOcEL.js.map +1 -0
- package/dist/{grid-list-B_orPsot.d.ts → grid-list-u33vmKzb.d.ts} +128 -10
- package/dist/grid-list-u33vmKzb.d.ts.map +1 -0
- package/dist/grid-list.d.ts +1 -1
- package/dist/grid-list.js +1 -932
- package/dist/incr-search-C05WjbZh.js +999 -0
- package/dist/incr-search-C05WjbZh.js.map +1 -0
- package/dist/incr-search.d.ts +2 -2
- package/dist/incr-search.js +2 -3
- package/dist/index-h6gxtfHH.d.ts +397 -0
- package/dist/index-h6gxtfHH.d.ts.map +1 -0
- package/dist/index.d.ts +62 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +343 -17
- package/dist/index.js.map +1 -1
- package/dist/led.d.ts +33 -1
- package/dist/led.d.ts.map +1 -1
- package/dist/led.js +157 -13
- package/dist/led.js.map +1 -1
- package/dist/{list-selection-controller-CUo-Hhcm.js → list-selection-controller-DTFI40li.js} +2 -76
- package/dist/list-selection-controller-DTFI40li.js.map +1 -0
- package/dist/miller.d.ts +81 -3
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +327 -68
- package/dist/miller.js.map +1 -1
- package/dist/narrowed-text.js +1 -2
- package/dist/narrowed-text.js.map +1 -1
- package/dist/panel.d.ts +3 -1
- package/dist/panel.d.ts.map +1 -1
- package/dist/panel.js +31 -1
- package/dist/panel.js.map +1 -1
- package/dist/plots.d.ts +2 -2
- package/dist/plots.js +1 -1
- package/dist/{titlebar-Bj5pfdw_.d.ts → titlebar-YQ-jVQoS.d.ts} +2 -2
- package/dist/{titlebar-Bj5pfdw_.d.ts.map → titlebar-YQ-jVQoS.d.ts.map} +1 -1
- package/dist/titlebar.d.ts +1 -1
- package/dist/titlebar.js +83 -1
- package/dist/titlebar.js.map +1 -1
- package/dist/{tree-B74zdI60.js → tree-Chptss7I.js} +77 -15
- package/dist/tree-Chptss7I.js.map +1 -0
- package/dist/{tree-component-BvV4muxx.d.ts → tree-component-F6qZ0Gdv.d.ts} +9 -1
- package/dist/{tree-component-BvV4muxx.d.ts.map → tree-component-F6qZ0Gdv.d.ts.map} +1 -1
- package/dist/tree.d.ts +22 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +1 -1
- package/dist/{vertical-list-CQSEVeOT.d.ts → vertical-list-CFuFDygi.d.ts} +17 -14
- package/dist/vertical-list-CFuFDygi.d.ts.map +1 -0
- package/dist/vertical-list.d.ts +1 -1
- package/dist/vertical-list.js +59 -127
- package/dist/vertical-list.js.map +1 -1
- package/dist/{xy-axis-5KhVTNoX.d.ts → xy-axis-BfP9hKLj.d.ts} +25 -2
- package/dist/{xy-axis-5KhVTNoX.d.ts.map → xy-axis-BfP9hKLj.d.ts.map} +1 -1
- package/dist/{xy-axis-VrX8dmvS.js → xy-axis-DzM1brBZ.js} +141 -8
- package/dist/xy-axis-DzM1brBZ.js.map +1 -0
- package/docs-user/data-grid.md +19 -0
- package/docs-user/detail-list.md +1 -1
- package/docs-user/grid-list.md +120 -11
- package/docs-user/incr-search.md +12 -2
- package/docs-user/index.json +3 -3
- package/docs-user/led.md +37 -7
- package/docs-user/llms.txt +1 -1
- package/docs-user/panel.md +68 -13
- package/docs-user/titlebar.md +3 -4
- package/docs-user/tree.md +22 -1
- package/docs-user/vertical-list.md +7 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/dist/data-grid-BddUjAti.d.ts.map +0 -1
- package/dist/detail-list-Br0WAnry.d.ts.map +0 -1
- package/dist/element-search-Y8SRfPGM.js +0 -185
- package/dist/element-search-Y8SRfPGM.js.map +0 -1
- package/dist/grid-list-B_orPsot.d.ts.map +0 -1
- package/dist/grid-list.js.map +0 -1
- package/dist/incr-search-vCzuao-2.js +0 -137
- package/dist/incr-search-vCzuao-2.js.map +0 -1
- package/dist/index-Cu5i8zZY.d.ts +0 -160
- package/dist/index-Cu5i8zZY.d.ts.map +0 -1
- package/dist/list-selection-controller-CUo-Hhcm.js.map +0 -1
- package/dist/tree-B74zdI60.js.map +0 -1
- package/dist/vertical-list-CQSEVeOT.d.ts.map +0 -1
- package/dist/xy-axis-VrX8dmvS.js.map +0 -1
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
import { n as buildHighlightTargets, r as clearHighlights, t as applyHighlights } from "./highlight-B-eQrhhb.js";
|
|
2
|
+
import { css, html, nothing } from "lit";
|
|
3
|
+
import { Fzf } from "fzf";
|
|
4
|
+
//#region src/incr-search/fuzzy.ts
|
|
5
|
+
function defaultSelector(item) {
|
|
6
|
+
if (typeof item === `string`) return item;
|
|
7
|
+
const r = item;
|
|
8
|
+
if (typeof r.label === `string`) return r.label;
|
|
9
|
+
if (typeof r.key === `string`) return r.key;
|
|
10
|
+
if (typeof r.value === `string`) return r.value;
|
|
11
|
+
return String(item);
|
|
12
|
+
}
|
|
13
|
+
function createFuzzySearch(items, options = {}) {
|
|
14
|
+
const selector = options.selector ?? defaultSelector;
|
|
15
|
+
const fzf = new Fzf(items, {
|
|
16
|
+
selector,
|
|
17
|
+
...options.fzf ?? {}
|
|
18
|
+
});
|
|
19
|
+
const find = (query) => {
|
|
20
|
+
if (!query.trim()) return items.map((item) => ({
|
|
21
|
+
item,
|
|
22
|
+
positions: /* @__PURE__ */ new Set(),
|
|
23
|
+
score: 0
|
|
24
|
+
}));
|
|
25
|
+
return fzf.find(query).map((r) => ({
|
|
26
|
+
item: r.item,
|
|
27
|
+
positions: r.positions,
|
|
28
|
+
score: r.score
|
|
29
|
+
}));
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
find,
|
|
33
|
+
items
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/incr-search/ac-adapter.ts
|
|
38
|
+
/**
|
|
39
|
+
* Creates an ixfx-ac-text-compatible `completionProvider` function from a
|
|
40
|
+
* static item list. Uses synchronous fuzzy search wrapped in a Promise so it
|
|
41
|
+
* fits the async provider signature.
|
|
42
|
+
*
|
|
43
|
+
* Each returned item has `positions` populated with matched character indices,
|
|
44
|
+
* which `ixfx-ac-text` uses to render CSS Custom Highlights on the label.
|
|
45
|
+
*
|
|
46
|
+
* Empty query returns all items in original order (no positions).
|
|
47
|
+
* A cancelled signal causes the returned promise to reject with an AbortError.
|
|
48
|
+
*/
|
|
49
|
+
function createAcCompletionProvider(items, options = {}) {
|
|
50
|
+
const fuzzy = createFuzzySearch(items, { selector: options.selector ?? ((item) => item.label) });
|
|
51
|
+
return (query, signal) => new Promise((resolve, reject) => {
|
|
52
|
+
if (signal.aborted) {
|
|
53
|
+
reject(new DOMException(`Aborted`, `AbortError`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const results = fuzzy.find(query).map((r) => ({
|
|
57
|
+
...r.item,
|
|
58
|
+
positions: r.positions
|
|
59
|
+
}));
|
|
60
|
+
if (signal.aborted) {
|
|
61
|
+
reject(new DOMException(`Aborted`, `AbortError`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve(results);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/incr-search/incr-search.ts
|
|
69
|
+
function buildState(results, query) {
|
|
70
|
+
const positionsByItem = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const r of results) positionsByItem.set(r.item, r.positions);
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
query,
|
|
74
|
+
results: Object.freeze([...results]),
|
|
75
|
+
positionsByItem
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function createIncrSearch(options = {}) {
|
|
79
|
+
let items = options.items ?? [];
|
|
80
|
+
let query = ``;
|
|
81
|
+
let fuzzy = createFuzzySearch(items, {
|
|
82
|
+
selector: options.selector,
|
|
83
|
+
fzf: options.fzf
|
|
84
|
+
});
|
|
85
|
+
let state = buildState(fuzzy.find(query), query);
|
|
86
|
+
let disposed = false;
|
|
87
|
+
const observers = /* @__PURE__ */ new Set();
|
|
88
|
+
if (options.onChange) observers.add(options.onChange);
|
|
89
|
+
const notify = (s) => {
|
|
90
|
+
for (const cb of observers) cb(s);
|
|
91
|
+
};
|
|
92
|
+
const run = async (q, signal) => {
|
|
93
|
+
await Promise.resolve();
|
|
94
|
+
if (signal?.aborted || disposed) return state;
|
|
95
|
+
const results = fuzzy.find(q);
|
|
96
|
+
if (signal?.aborted || disposed) return state;
|
|
97
|
+
state = buildState(results, q);
|
|
98
|
+
notify(state);
|
|
99
|
+
return state;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
get state() {
|
|
103
|
+
return state;
|
|
104
|
+
},
|
|
105
|
+
async setItems(newItems, signal) {
|
|
106
|
+
items = newItems;
|
|
107
|
+
fuzzy = createFuzzySearch(items, {
|
|
108
|
+
selector: options.selector,
|
|
109
|
+
fzf: options.fzf
|
|
110
|
+
});
|
|
111
|
+
await run(query, signal);
|
|
112
|
+
},
|
|
113
|
+
async setQuery(q, signal) {
|
|
114
|
+
query = q;
|
|
115
|
+
return run(q, signal);
|
|
116
|
+
},
|
|
117
|
+
async clearQuery() {
|
|
118
|
+
query = ``;
|
|
119
|
+
await run(``, void 0);
|
|
120
|
+
},
|
|
121
|
+
onChange(cb) {
|
|
122
|
+
observers.add(cb);
|
|
123
|
+
cb(state);
|
|
124
|
+
return () => {
|
|
125
|
+
observers.delete(cb);
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
dispose() {
|
|
129
|
+
disposed = true;
|
|
130
|
+
observers.clear();
|
|
131
|
+
items = [];
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/incr-search/controller-tree.ts
|
|
137
|
+
const DEFAULT_HIGHLIGHT_KEY$1 = `incr-search-highlight`;
|
|
138
|
+
/**
|
|
139
|
+
* External-input filter-search controller for `TreeSearchHost` components.
|
|
140
|
+
*
|
|
141
|
+
* @deprecated `ixfx-tree-list` / `ixfx-miller-list` now have a built-in
|
|
142
|
+
* incremental-search popover with navigate / select / filter modes — invoke
|
|
143
|
+
* `el.registry.invoke('list.search.filter')` (see `docs/infra-incremental.md`)
|
|
144
|
+
* instead of wiring an external `<input>`. This class remains for existing
|
|
145
|
+
* consumers and covers filter mode only.
|
|
146
|
+
*
|
|
147
|
+
* @example Using with ixfx-tree-list:
|
|
148
|
+
* ```typescript
|
|
149
|
+
* const ctrl = new IncrSearchTreeController(myTreeList, {
|
|
150
|
+
* selector: item => item.node.item.label,
|
|
151
|
+
* });
|
|
152
|
+
* ctrl.connect();
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @example Using with ixfx-miller-list (different label selector):
|
|
156
|
+
* ```typescript
|
|
157
|
+
* const ctrl = new IncrSearchTreeController(myMillerList, {
|
|
158
|
+
* selector: item => item.node.item.label,
|
|
159
|
+
* labelSelector: '.label',
|
|
160
|
+
* });
|
|
161
|
+
* ctrl.connect();
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
var IncrSearchTreeController = class {
|
|
165
|
+
#el;
|
|
166
|
+
#opts;
|
|
167
|
+
#search;
|
|
168
|
+
#unsub;
|
|
169
|
+
#labelMap = /* @__PURE__ */ new Map();
|
|
170
|
+
constructor(el, opts) {
|
|
171
|
+
this.#el = el;
|
|
172
|
+
this.#opts = opts;
|
|
173
|
+
this.#search = createIncrSearch({
|
|
174
|
+
selector: opts.selector,
|
|
175
|
+
fzf: opts.fzf
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
connect() {
|
|
179
|
+
this.disconnect();
|
|
180
|
+
const items = this.#el.getNodes();
|
|
181
|
+
for (const item of items) this.#labelMap.set(this.#opts.selector(item), item);
|
|
182
|
+
this.#search.setItems(items);
|
|
183
|
+
if (this.#opts.input) {
|
|
184
|
+
this.#opts.input.addEventListener(`input`, this.#onInput);
|
|
185
|
+
this.#opts.input.addEventListener(`keydown`, this.#onKeydown);
|
|
186
|
+
}
|
|
187
|
+
this.#unsub = this.#search.onChange((state) => this.#onStateChange(state));
|
|
188
|
+
}
|
|
189
|
+
disconnect() {
|
|
190
|
+
if (this.#unsub) {
|
|
191
|
+
this.#unsub();
|
|
192
|
+
this.#unsub = void 0;
|
|
193
|
+
}
|
|
194
|
+
if (this.#opts.input) {
|
|
195
|
+
this.#opts.input.removeEventListener(`input`, this.#onInput);
|
|
196
|
+
this.#opts.input.removeEventListener(`keydown`, this.#onKeydown);
|
|
197
|
+
}
|
|
198
|
+
this.#el.filterPredicate = void 0;
|
|
199
|
+
clearHighlights({ key: this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY$1 });
|
|
200
|
+
this.#labelMap.clear();
|
|
201
|
+
}
|
|
202
|
+
async setQuery(query) {
|
|
203
|
+
await this.#search.setQuery(query);
|
|
204
|
+
}
|
|
205
|
+
async clearQuery() {
|
|
206
|
+
await this.#search.clearQuery();
|
|
207
|
+
}
|
|
208
|
+
#onInput = () => {
|
|
209
|
+
const value = this.#opts.input?.value ?? ``;
|
|
210
|
+
this.setQuery(value);
|
|
211
|
+
};
|
|
212
|
+
#onKeydown = (e) => {
|
|
213
|
+
if (e.key === `Escape` && this.#opts.input) {
|
|
214
|
+
this.#opts.input.value = ``;
|
|
215
|
+
this.clearQuery();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
#onStateChange(state) {
|
|
219
|
+
const matched = new Set(state.results.map((r) => r.item.node.item.key));
|
|
220
|
+
const hasQuery = state.query.trim().length > 0;
|
|
221
|
+
this.#el.filterPredicate = hasQuery && matched.size > 0 ? (node, _depth) => matched.has(node.item.key) : void 0;
|
|
222
|
+
const key = this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY$1;
|
|
223
|
+
if (!this.#el.shadowRoot) return;
|
|
224
|
+
if (!state.query.trim()) {
|
|
225
|
+
clearHighlights({ key });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const labelSelector = this.#opts.labelSelector ?? `.tree-label`;
|
|
229
|
+
const targets = buildHighlightTargets(this.#el.shadowRoot, labelSelector, state.positionsByItem, (labelEl) => {
|
|
230
|
+
const text = labelEl.textContent ?? ``;
|
|
231
|
+
return this.#labelMap.get(text);
|
|
232
|
+
}, (labelEl) => labelEl.textContent ?? ``);
|
|
233
|
+
applyHighlights(targets, { key });
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/incr-search/data-list-search-adapter.ts
|
|
238
|
+
/**
|
|
239
|
+
* A {@link ListSearchAdapter} over a data-backed list. Unlike the element
|
|
240
|
+
* adapter it never materialises an element per item — it reads keys and text
|
|
241
|
+
* straight from the source by index. Highlights are painted on whatever
|
|
242
|
+
* captions are currently rendered in the shadow window; offscreen matches paint
|
|
243
|
+
* when scrolled into view. See `docs/infra-incremental.md`.
|
|
244
|
+
*/
|
|
245
|
+
function createDataListSearchAdapter(host, options) {
|
|
246
|
+
return {
|
|
247
|
+
getItems: () => Array.from({ length: options.rawCount() }, (_, i) => ({
|
|
248
|
+
key: options.rawKeyAt(i),
|
|
249
|
+
index: i
|
|
250
|
+
})),
|
|
251
|
+
getLabel: (item) => options.rawSearchTextAt(item.index),
|
|
252
|
+
getKey: (item) => item.key,
|
|
253
|
+
focusItem: (item) => {
|
|
254
|
+
if (item) host.focusKey(item.key);
|
|
255
|
+
},
|
|
256
|
+
activateItem: (item) => {
|
|
257
|
+
if (options.onActivate) options.onActivate(item.key);
|
|
258
|
+
else host.focusKey(item.key);
|
|
259
|
+
},
|
|
260
|
+
selectItems: (items) => {
|
|
261
|
+
host.selectKeys(items.map((i) => i.key));
|
|
262
|
+
},
|
|
263
|
+
applyFilter: (matched) => {
|
|
264
|
+
options.onFilterChange?.(matched);
|
|
265
|
+
},
|
|
266
|
+
highlightRoot: options.getHighlightRoot,
|
|
267
|
+
highlightSelector: `.cell .caption`,
|
|
268
|
+
highlightKeyOf: (el) => el.closest(`.cell`)?.getAttribute(`data-key`) ?? void 0,
|
|
269
|
+
highlightText: (el) => el.textContent ?? ``,
|
|
270
|
+
highlightKey: `grid-list-search`
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/interaction/keyed-list.ts
|
|
275
|
+
/**
|
|
276
|
+
* Helpers for key-based selection on the Element-flavoured list components
|
|
277
|
+
* (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-grid-list`).
|
|
278
|
+
*
|
|
279
|
+
* A "key" is a stable string the host puts on each item element — `data-key`
|
|
280
|
+
* (preferred) or `data-value`. It survives the host rebuilding the item set
|
|
281
|
+
* (re-fetch, re-sort, filter) where raw `Element` references do not.
|
|
282
|
+
*/
|
|
283
|
+
/**
|
|
284
|
+
* The stable key for an item element: `data-key`, then `data-value`, else
|
|
285
|
+
* `undefined`.
|
|
286
|
+
*/
|
|
287
|
+
function keyForElement(el) {
|
|
288
|
+
return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? void 0;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A lazily-built key → element index over a container. Rebuilt on first use
|
|
292
|
+
* and after any child-list mutation under `root`; `invalidate()` forces a
|
|
293
|
+
* rebuild on the next `get()`.
|
|
294
|
+
*
|
|
295
|
+
* ```ts
|
|
296
|
+
* #keyIndex = new KeyedElementIndex(this);
|
|
297
|
+
* // ...
|
|
298
|
+
* const el = this.#keyIndex.get(`row-42`);
|
|
299
|
+
* // on disconnect:
|
|
300
|
+
* this.#keyIndex.dispose();
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
var KeyedElementIndex = class {
|
|
304
|
+
#root;
|
|
305
|
+
#selector;
|
|
306
|
+
#map = /* @__PURE__ */ new Map();
|
|
307
|
+
#dirty = true;
|
|
308
|
+
#observer;
|
|
309
|
+
/**
|
|
310
|
+
* @param root Container whose descendants are indexed.
|
|
311
|
+
* @param itemSelector Which descendants count as items (default: any `[data-key]`).
|
|
312
|
+
*/
|
|
313
|
+
constructor(root, itemSelector = `[data-key]`) {
|
|
314
|
+
this.#root = root;
|
|
315
|
+
this.#selector = itemSelector;
|
|
316
|
+
this.#observer = new MutationObserver(() => {
|
|
317
|
+
this.#dirty = true;
|
|
318
|
+
});
|
|
319
|
+
this.#observer.observe(root, {
|
|
320
|
+
childList: true,
|
|
321
|
+
subtree: true
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/** The element with this key, or `undefined`. First match wins on duplicates. */
|
|
325
|
+
get(key) {
|
|
326
|
+
if (this.#dirty) this.#rebuild();
|
|
327
|
+
return this.#map.get(key);
|
|
328
|
+
}
|
|
329
|
+
/** Force a rebuild on the next `get()` — e.g. after a `data-key` attribute change. */
|
|
330
|
+
invalidate() {
|
|
331
|
+
this.#dirty = true;
|
|
332
|
+
}
|
|
333
|
+
/** Stop observing and drop the cache. */
|
|
334
|
+
dispose() {
|
|
335
|
+
this.#observer.disconnect();
|
|
336
|
+
this.#map.clear();
|
|
337
|
+
}
|
|
338
|
+
#rebuild() {
|
|
339
|
+
this.#map.clear();
|
|
340
|
+
for (const el of this.#root.querySelectorAll(this.#selector)) {
|
|
341
|
+
const key = keyForElement(el);
|
|
342
|
+
if (key !== void 0 && !this.#map.has(key)) this.#map.set(key, el);
|
|
343
|
+
}
|
|
344
|
+
this.#dirty = false;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/incr-search/element-list-search-adapter.ts
|
|
349
|
+
function defaultLabel(el) {
|
|
350
|
+
return el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* A {@link ListSearchAdapter} over the light-DOM item elements of a list
|
|
354
|
+
* component. Filter mode toggles `hidden` (only on elements it hid itself, so a
|
|
355
|
+
* consumer's own `hidden` items are left alone). Highlights are painted on the
|
|
356
|
+
* item elements in the light DOM — the consuming page styles
|
|
357
|
+
* `::highlight(<highlightKey>)`. See `docs/infra-incremental.md`.
|
|
358
|
+
*/
|
|
359
|
+
function createElementListSearchAdapter(host, options = {}) {
|
|
360
|
+
const itemSelector = options.itemSelector ?? `:scope > *`;
|
|
361
|
+
const getLabel = options.getLabel ?? defaultLabel;
|
|
362
|
+
const getKey = options.getKey ?? ((el) => keyForElement(el) ?? defaultLabel(el));
|
|
363
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
364
|
+
const items = () => Array.from(host.querySelectorAll(itemSelector));
|
|
365
|
+
return {
|
|
366
|
+
getItems: items,
|
|
367
|
+
getLabel,
|
|
368
|
+
getKey,
|
|
369
|
+
focusItem: (el) => {
|
|
370
|
+
if (el) host.focusKey(getKey(el));
|
|
371
|
+
},
|
|
372
|
+
activateItem: (el) => {
|
|
373
|
+
el.dispatchEvent(new MouseEvent(`click`, {
|
|
374
|
+
bubbles: true,
|
|
375
|
+
composed: true
|
|
376
|
+
}));
|
|
377
|
+
},
|
|
378
|
+
selectItems: (els) => {
|
|
379
|
+
host.selectKeys(els.map(getKey));
|
|
380
|
+
},
|
|
381
|
+
applyFilter: (matched) => {
|
|
382
|
+
if (matched === void 0) {
|
|
383
|
+
for (const el of hidden) el.hidden = false;
|
|
384
|
+
hidden.clear();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
for (const el of items()) if (matched.has(getKey(el))) {
|
|
388
|
+
if (hidden.delete(el)) el.hidden = false;
|
|
389
|
+
} else {
|
|
390
|
+
el.hidden = true;
|
|
391
|
+
hidden.add(el);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
highlightRoot: () => host,
|
|
395
|
+
highlightSelector: itemSelector,
|
|
396
|
+
highlightKeyOf: (el) => getKey(el),
|
|
397
|
+
highlightText: getLabel,
|
|
398
|
+
highlightKey: options.highlightKey,
|
|
399
|
+
container: () => host
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/incr-search/element-search.ts
|
|
404
|
+
const defaultGetLabel = (el) => el.textContent?.trim() ?? ``;
|
|
405
|
+
function defaultGetKey(el) {
|
|
406
|
+
return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? el.textContent?.trim() ?? ``;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* DOM filter-search over a container's item elements (sets `hidden` on
|
|
410
|
+
* non-matches, applies highlights).
|
|
411
|
+
*
|
|
412
|
+
* @deprecated The list components (`ixfx-grid-list`, `ixfx-vertical-list`,
|
|
413
|
+
* `ixfx-detail-list`, …) now have a built-in navigate / select / filter search
|
|
414
|
+
* popover — see `docs/infra-incremental.md` and `ListSearchController`. Kept for
|
|
415
|
+
* standalone / non-component use.
|
|
416
|
+
*/
|
|
417
|
+
function createElementIncrSearch(opts) {
|
|
418
|
+
const { container, itemSelector, getLabel = defaultGetLabel, getKey = defaultGetKey, labelSelector, highlightKey } = opts;
|
|
419
|
+
let elementByKey = /* @__PURE__ */ new Map();
|
|
420
|
+
const search = createIncrSearch({
|
|
421
|
+
selector: (item) => item.label,
|
|
422
|
+
fzf: opts.fzf
|
|
423
|
+
});
|
|
424
|
+
const updateVisibility = (state) => {
|
|
425
|
+
const matchedKeys = new Set(state.results.map((r) => r.item.key));
|
|
426
|
+
const hasQuery = state.query.trim().length > 0;
|
|
427
|
+
for (const [key, el] of elementByKey) el.hidden = hasQuery ? !matchedKeys.has(key) : false;
|
|
428
|
+
};
|
|
429
|
+
const updateHighlights = (state) => {
|
|
430
|
+
if (!state.query.trim()) {
|
|
431
|
+
clearHighlights({ key: highlightKey });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const targets = buildHighlightTargets(container, labelSelector ?? itemSelector, state.positionsByItem, (el) => {
|
|
435
|
+
const key = getKey(el);
|
|
436
|
+
return state.results.find((r) => r.item.key === key)?.item;
|
|
437
|
+
}, (el) => getLabel(el));
|
|
438
|
+
applyHighlights(targets, { key: highlightKey });
|
|
439
|
+
};
|
|
440
|
+
const unsubscribeState = search.onChange((state) => {
|
|
441
|
+
updateVisibility(state);
|
|
442
|
+
updateHighlights(state);
|
|
443
|
+
opts.onChange?.(state);
|
|
444
|
+
});
|
|
445
|
+
let refreshPending = false;
|
|
446
|
+
const refresh = () => {
|
|
447
|
+
if (refreshPending) return;
|
|
448
|
+
refreshPending = true;
|
|
449
|
+
Promise.resolve().then(() => {
|
|
450
|
+
refreshPending = false;
|
|
451
|
+
elementByKey = /* @__PURE__ */ new Map();
|
|
452
|
+
const els = container.querySelectorAll(itemSelector);
|
|
453
|
+
const items = [];
|
|
454
|
+
for (const el of els) {
|
|
455
|
+
const key = getKey(el);
|
|
456
|
+
elementByKey.set(key, el);
|
|
457
|
+
items.push({
|
|
458
|
+
key,
|
|
459
|
+
label: getLabel(el)
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
search.setItems(items);
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
const observer = new MutationObserver(() => refresh());
|
|
466
|
+
observer.observe(container, {
|
|
467
|
+
childList: true,
|
|
468
|
+
subtree: false
|
|
469
|
+
});
|
|
470
|
+
refresh();
|
|
471
|
+
return {
|
|
472
|
+
async setQuery(query, signal) {
|
|
473
|
+
await search.setQuery(query, signal);
|
|
474
|
+
},
|
|
475
|
+
async clearQuery() {
|
|
476
|
+
await search.clearQuery();
|
|
477
|
+
},
|
|
478
|
+
refresh,
|
|
479
|
+
onChange(cb) {
|
|
480
|
+
return search.onChange(cb);
|
|
481
|
+
},
|
|
482
|
+
dispose() {
|
|
483
|
+
observer.disconnect();
|
|
484
|
+
unsubscribeState();
|
|
485
|
+
search.dispose();
|
|
486
|
+
clearHighlights({ key: highlightKey });
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/incr-search/list-search-commands.ts
|
|
492
|
+
/** Command ids registered by {@link registerListSearchCommands}, keyed by mode. */
|
|
493
|
+
const LIST_SEARCH_COMMAND_IDS = {
|
|
494
|
+
navigate: `list.search.navigate`,
|
|
495
|
+
select: `list.search.select`,
|
|
496
|
+
filter: `list.search.filter`
|
|
497
|
+
};
|
|
498
|
+
const LABELS = {
|
|
499
|
+
navigate: {
|
|
500
|
+
label: `Find`,
|
|
501
|
+
description: `Type to move the cursor to the best match`
|
|
502
|
+
},
|
|
503
|
+
select: {
|
|
504
|
+
label: `Find & Select`,
|
|
505
|
+
description: `Type to select every matching item`
|
|
506
|
+
},
|
|
507
|
+
filter: {
|
|
508
|
+
label: `Filter`,
|
|
509
|
+
description: `Type to hide items that don't match`
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* Register one command per mode the controller offers (see
|
|
514
|
+
* `controller.registeredModes`). Each command opens the search popover in its
|
|
515
|
+
* mode. Returns a function that unregisters them again — call it from
|
|
516
|
+
* `disconnectedCallback`. See `docs/infra-incremental.md`.
|
|
517
|
+
*/
|
|
518
|
+
function registerListSearchCommands(registry, controller, options = {}) {
|
|
519
|
+
const id = (mode) => options.idPrefix ? `${options.idPrefix}.search.${mode}` : LIST_SEARCH_COMMAND_IDS[mode];
|
|
520
|
+
const registered = [];
|
|
521
|
+
for (const mode of controller.registeredModes) {
|
|
522
|
+
const commandId = id(mode);
|
|
523
|
+
if (registry.has(commandId)) continue;
|
|
524
|
+
registry.register({
|
|
525
|
+
id: commandId,
|
|
526
|
+
label: LABELS[mode].label,
|
|
527
|
+
description: LABELS[mode].description,
|
|
528
|
+
execute: () => controller.open(mode)
|
|
529
|
+
});
|
|
530
|
+
registered.push(commandId);
|
|
531
|
+
}
|
|
532
|
+
return () => {
|
|
533
|
+
for (const commandId of registered) registry.unregister(commandId);
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/incr-search/list-search-controller.ts
|
|
538
|
+
/** Canonical order — used for the switch-row and to normalise a requested set. */
|
|
539
|
+
const LIST_SEARCH_MODES = [
|
|
540
|
+
`navigate`,
|
|
541
|
+
`select`,
|
|
542
|
+
`filter`
|
|
543
|
+
];
|
|
544
|
+
const DEFAULT_HIGHLIGHT_KEY = `incr-search-highlight`;
|
|
545
|
+
const INPUT_CLASS = `list-search-input`;
|
|
546
|
+
/**
|
|
547
|
+
* Shared brain for the three-mode incremental-search popover. A Lit
|
|
548
|
+
* `ReactiveController`: the host adds `listSearchStyles` to `static styles`,
|
|
549
|
+
* drops `${controller.renderField()}` into `render()`, and registers the
|
|
550
|
+
* commands with {@link registerListSearchCommands}. See `docs/infra-incremental.md`.
|
|
551
|
+
*/
|
|
552
|
+
var ListSearchController = class {
|
|
553
|
+
#host;
|
|
554
|
+
#adapter;
|
|
555
|
+
#modes;
|
|
556
|
+
#placeholder;
|
|
557
|
+
#search;
|
|
558
|
+
#unsub;
|
|
559
|
+
#observer;
|
|
560
|
+
#itemByKey = /* @__PURE__ */ new Map();
|
|
561
|
+
#open = false;
|
|
562
|
+
#mode = `filter`;
|
|
563
|
+
#query = ``;
|
|
564
|
+
#matchIndex = 0;
|
|
565
|
+
#matches = [];
|
|
566
|
+
/** Whether a filter has been pushed to the adapter and still needs undoing. */
|
|
567
|
+
#filterActive = false;
|
|
568
|
+
constructor(host, options) {
|
|
569
|
+
this.#host = host;
|
|
570
|
+
this.#adapter = options.adapter;
|
|
571
|
+
const requested = new Set(options.modes ?? LIST_SEARCH_MODES);
|
|
572
|
+
this.#modes = LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
573
|
+
this.#mode = this.#modes[0] ?? `filter`;
|
|
574
|
+
this.#placeholder = options.placeholder ?? `Search…`;
|
|
575
|
+
this.#search = createIncrSearch({
|
|
576
|
+
selector: (i) => i.label,
|
|
577
|
+
fzf: options.fzf
|
|
578
|
+
});
|
|
579
|
+
this.#unsub = this.#search.onChange((state) => this.#onState(state));
|
|
580
|
+
host.addController(this);
|
|
581
|
+
}
|
|
582
|
+
/** `false` when no modes are available — the host should register no commands and render no field. */
|
|
583
|
+
get enabled() {
|
|
584
|
+
return this.#modes.length > 0;
|
|
585
|
+
}
|
|
586
|
+
/** The modes actually offered (component capability ∩ consumer request). */
|
|
587
|
+
get registeredModes() {
|
|
588
|
+
return this.#modes;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Replace the offered mode set (normalised to {@link LIST_SEARCH_MODES}
|
|
592
|
+
* order). If the current mode is dropped the popover closes and falls back to
|
|
593
|
+
* the first remaining mode. Callers should re-run
|
|
594
|
+
* {@link registerListSearchCommands} afterwards.
|
|
595
|
+
*/
|
|
596
|
+
setModes(modes) {
|
|
597
|
+
const set = new Set(modes);
|
|
598
|
+
this.#modes = LIST_SEARCH_MODES.filter((m) => set.has(m));
|
|
599
|
+
if (!this.#modes.includes(this.#mode)) {
|
|
600
|
+
if (this.#open) this.close();
|
|
601
|
+
this.#mode = this.#modes[0] ?? `filter`;
|
|
602
|
+
}
|
|
603
|
+
this.#host.requestUpdate();
|
|
604
|
+
}
|
|
605
|
+
get isOpen() {
|
|
606
|
+
return this.#open;
|
|
607
|
+
}
|
|
608
|
+
get mode() {
|
|
609
|
+
return this.#mode;
|
|
610
|
+
}
|
|
611
|
+
get query() {
|
|
612
|
+
return this.#query;
|
|
613
|
+
}
|
|
614
|
+
get matchCount() {
|
|
615
|
+
return this.#matches.length;
|
|
616
|
+
}
|
|
617
|
+
get matchIndex() {
|
|
618
|
+
return this.#matchIndex;
|
|
619
|
+
}
|
|
620
|
+
hostConnected() {
|
|
621
|
+
const container = this.#adapter.container?.();
|
|
622
|
+
if (container) {
|
|
623
|
+
this.#observer = new MutationObserver(() => this.refresh());
|
|
624
|
+
this.#observer.observe(container, {
|
|
625
|
+
childList: true,
|
|
626
|
+
subtree: false
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
this.refresh();
|
|
630
|
+
}
|
|
631
|
+
hostDisconnected() {
|
|
632
|
+
this.#observer?.disconnect();
|
|
633
|
+
this.#observer = void 0;
|
|
634
|
+
if (this.#open) this.#teardownEffects();
|
|
635
|
+
this.#open = false;
|
|
636
|
+
this.#unsub?.();
|
|
637
|
+
this.#unsub = void 0;
|
|
638
|
+
this.#search.dispose();
|
|
639
|
+
}
|
|
640
|
+
/** Open the popover in `mode` (clamped to the available set). */
|
|
641
|
+
open(mode) {
|
|
642
|
+
if (!this.enabled) return;
|
|
643
|
+
this.#mode = this.#modes.includes(mode) ? mode : this.#modes[0];
|
|
644
|
+
this.#open = true;
|
|
645
|
+
this.#query = ``;
|
|
646
|
+
this.#matchIndex = 0;
|
|
647
|
+
this.#matches = [];
|
|
648
|
+
this.refresh();
|
|
649
|
+
this.#search.setQuery(``);
|
|
650
|
+
this.#host.requestUpdate();
|
|
651
|
+
this.#host.updateComplete.then(() => this.#focusInput());
|
|
652
|
+
}
|
|
653
|
+
/** Close the popover, restoring any live filter. Selection / cursor stay put. */
|
|
654
|
+
close() {
|
|
655
|
+
if (!this.#open) return;
|
|
656
|
+
this.#open = false;
|
|
657
|
+
this.#teardownEffects();
|
|
658
|
+
this.#query = ``;
|
|
659
|
+
this.#matches = [];
|
|
660
|
+
this.#search.clearQuery();
|
|
661
|
+
this.#host.requestUpdate();
|
|
662
|
+
this.#host.focus();
|
|
663
|
+
}
|
|
664
|
+
setQuery(query) {
|
|
665
|
+
this.#query = query;
|
|
666
|
+
this.#matchIndex = 0;
|
|
667
|
+
this.#search.setQuery(query);
|
|
668
|
+
}
|
|
669
|
+
/** Re-interpret the current query under a new mode (undoing the old mode's effect). */
|
|
670
|
+
switchMode(next) {
|
|
671
|
+
if (!this.#modes.includes(next) || next === this.#mode) return;
|
|
672
|
+
if (this.#mode === `filter` && this.#filterActive) {
|
|
673
|
+
this.#adapter.applyFilter(void 0);
|
|
674
|
+
this.#filterActive = false;
|
|
675
|
+
}
|
|
676
|
+
this.#mode = next;
|
|
677
|
+
this.#applyMode();
|
|
678
|
+
this.#host.requestUpdate();
|
|
679
|
+
this.#host.updateComplete.then(() => this.#focusInput());
|
|
680
|
+
}
|
|
681
|
+
/** One-shot: turn the current match set into the selection, staying in the current mode. */
|
|
682
|
+
selectMatches() {
|
|
683
|
+
if (!this.#modes.includes(`select`)) return;
|
|
684
|
+
this.#adapter.selectItems(this.#currentItems());
|
|
685
|
+
this.#host.requestUpdate();
|
|
686
|
+
}
|
|
687
|
+
nextMatch() {
|
|
688
|
+
this.#cycle(1);
|
|
689
|
+
}
|
|
690
|
+
prevMatch() {
|
|
691
|
+
this.#cycle(-1);
|
|
692
|
+
}
|
|
693
|
+
/** Enter: in `navigate` activate the current match; then close. */
|
|
694
|
+
commit() {
|
|
695
|
+
if (this.#mode === `navigate`) {
|
|
696
|
+
const item = this.#itemFor(this.#matches[this.#matchIndex]);
|
|
697
|
+
if (item) {
|
|
698
|
+
if (this.#adapter.activateItem) this.#adapter.activateItem(item);
|
|
699
|
+
else this.#adapter.focusItem(item);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
this.close();
|
|
703
|
+
}
|
|
704
|
+
/** Rebuild the searchable item set from the adapter. Called on open and on container mutation. */
|
|
705
|
+
refresh() {
|
|
706
|
+
this.#itemByKey.clear();
|
|
707
|
+
const items = [];
|
|
708
|
+
for (const it of this.#adapter.getItems()) {
|
|
709
|
+
const key = this.#adapter.getKey(it);
|
|
710
|
+
this.#itemByKey.set(key, it);
|
|
711
|
+
items.push({
|
|
712
|
+
key,
|
|
713
|
+
label: this.#adapter.getLabel(it)
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
this.#search.setItems(items);
|
|
717
|
+
}
|
|
718
|
+
/** Popover markup. Pair with `listSearchStyles` in the host's `static styles`. */
|
|
719
|
+
renderField() {
|
|
720
|
+
if (!this.enabled) return nothing;
|
|
721
|
+
const hasQuery = this.#query.trim().length > 0;
|
|
722
|
+
const others = this.#modes.filter((m) => m !== this.#mode);
|
|
723
|
+
const showSelectMatches = this.#modes.includes(`select`) && this.#mode !== `select`;
|
|
724
|
+
return html`
|
|
725
|
+
<div class="list-search" part="search" ?hidden=${!this.#open}>
|
|
726
|
+
<div class="list-search-row">
|
|
727
|
+
<span class="list-search-mode">${this.#mode}</span>
|
|
728
|
+
<input
|
|
729
|
+
class=${INPUT_CLASS}
|
|
730
|
+
type="text"
|
|
731
|
+
part="search-input"
|
|
732
|
+
placeholder=${this.#placeholder}
|
|
733
|
+
.value=${this.#query}
|
|
734
|
+
@input=${this.#onInput}
|
|
735
|
+
@keydown=${this.#onKeydown}
|
|
736
|
+
/>
|
|
737
|
+
${hasQuery ? html`<span class="list-search-count">${this.#matches.length === 0 ? `0` : this.#matchIndex + 1} / ${this.#matches.length}</span>` : nothing}
|
|
738
|
+
${this.#mode === `navigate` && this.#matches.length > 1 ? html`
|
|
739
|
+
<button class="list-search-icon-btn" title="Previous match (↑)" @click=${() => this.prevMatch()}>‹</button>
|
|
740
|
+
<button class="list-search-icon-btn" title="Next match (↓)" @click=${() => this.nextMatch()}>›</button>
|
|
741
|
+
` : nothing}
|
|
742
|
+
<button class="list-search-icon-btn" title="Close (Esc)" @click=${() => this.close()}>✕</button>
|
|
743
|
+
</div>
|
|
744
|
+
${others.length > 0 || showSelectMatches ? html`
|
|
745
|
+
<div class="list-search-switch">
|
|
746
|
+
${others.map((m) => html`<button class="list-search-btn" @click=${() => this.switchMode(m)}>${m}</button>`)}
|
|
747
|
+
${showSelectMatches ? html`<button class="list-search-btn" @click=${() => this.selectMatches()}>Select matches</button>` : nothing}
|
|
748
|
+
</div>
|
|
749
|
+
` : nothing}
|
|
750
|
+
</div>
|
|
751
|
+
`;
|
|
752
|
+
}
|
|
753
|
+
#focusInput() {
|
|
754
|
+
this.#host.renderRoot?.querySelector(`.${INPUT_CLASS}`)?.focus();
|
|
755
|
+
}
|
|
756
|
+
#onInput = (e) => {
|
|
757
|
+
this.setQuery(e.target.value);
|
|
758
|
+
};
|
|
759
|
+
#onKeydown = (e) => {
|
|
760
|
+
switch (e.key) {
|
|
761
|
+
case `Escape`:
|
|
762
|
+
e.preventDefault();
|
|
763
|
+
e.stopPropagation();
|
|
764
|
+
this.close();
|
|
765
|
+
break;
|
|
766
|
+
case `Enter`:
|
|
767
|
+
e.preventDefault();
|
|
768
|
+
e.stopPropagation();
|
|
769
|
+
this.commit();
|
|
770
|
+
break;
|
|
771
|
+
case `ArrowDown`:
|
|
772
|
+
if (this.#mode === `navigate`) {
|
|
773
|
+
e.preventDefault();
|
|
774
|
+
this.nextMatch();
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
case `ArrowUp`: if (this.#mode === `navigate`) {
|
|
778
|
+
e.preventDefault();
|
|
779
|
+
this.prevMatch();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
#cycle(delta) {
|
|
784
|
+
if (this.#matches.length === 0) return;
|
|
785
|
+
const n = this.#matches.length;
|
|
786
|
+
this.#matchIndex = (this.#matchIndex + delta + n) % n;
|
|
787
|
+
if (this.#mode === `navigate`) this.#adapter.focusItem(this.#itemFor(this.#matches[this.#matchIndex]));
|
|
788
|
+
this.#host.requestUpdate();
|
|
789
|
+
}
|
|
790
|
+
#itemFor(match) {
|
|
791
|
+
return match ? this.#itemByKey.get(match.item.key) : void 0;
|
|
792
|
+
}
|
|
793
|
+
#currentItems() {
|
|
794
|
+
return this.#matches.map((m) => this.#itemFor(m)).filter((x) => x !== void 0);
|
|
795
|
+
}
|
|
796
|
+
get #highlightKey() {
|
|
797
|
+
return this.#adapter.highlightKey ?? DEFAULT_HIGHLIGHT_KEY;
|
|
798
|
+
}
|
|
799
|
+
#onState(state) {
|
|
800
|
+
if (!this.#open) return;
|
|
801
|
+
const hasQuery = state.query.trim().length > 0;
|
|
802
|
+
this.#matches = hasQuery ? [...state.results] : [];
|
|
803
|
+
if (this.#matchIndex >= this.#matches.length) this.#matchIndex = 0;
|
|
804
|
+
this.#applyMode();
|
|
805
|
+
this.#host.requestUpdate();
|
|
806
|
+
}
|
|
807
|
+
#applyMode() {
|
|
808
|
+
if (!(this.#query.trim().length > 0)) {
|
|
809
|
+
if (this.#filterActive) {
|
|
810
|
+
this.#adapter.applyFilter(void 0);
|
|
811
|
+
this.#filterActive = false;
|
|
812
|
+
}
|
|
813
|
+
this.#applyHighlights();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
switch (this.#mode) {
|
|
817
|
+
case `navigate`:
|
|
818
|
+
this.#adapter.focusItem(this.#itemFor(this.#matches[this.#matchIndex]));
|
|
819
|
+
break;
|
|
820
|
+
case `select`:
|
|
821
|
+
this.#adapter.selectItems(this.#currentItems());
|
|
822
|
+
break;
|
|
823
|
+
case `filter`:
|
|
824
|
+
this.#adapter.applyFilter(new Set(this.#matches.map((m) => m.item.key)));
|
|
825
|
+
this.#filterActive = true;
|
|
826
|
+
}
|
|
827
|
+
this.#applyHighlights();
|
|
828
|
+
}
|
|
829
|
+
#applyHighlights() {
|
|
830
|
+
const root = this.#adapter.highlightRoot?.();
|
|
831
|
+
const selector = this.#adapter.highlightSelector;
|
|
832
|
+
const keyOf = this.#adapter.highlightKeyOf;
|
|
833
|
+
if (!root || !selector || !keyOf) return;
|
|
834
|
+
if (this.#query.trim().length === 0) {
|
|
835
|
+
clearHighlights({ key: this.#highlightKey });
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const state = this.#search.state;
|
|
839
|
+
const targets = buildHighlightTargets(root, selector, state.positionsByItem, (el) => {
|
|
840
|
+
const key = keyOf(el);
|
|
841
|
+
return key === void 0 ? void 0 : state.results.find((r) => r.item.key === key)?.item;
|
|
842
|
+
}, this.#adapter.highlightText ?? ((el) => el.textContent ?? ``));
|
|
843
|
+
try {
|
|
844
|
+
applyHighlights(targets, { key: this.#highlightKey });
|
|
845
|
+
} catch {
|
|
846
|
+
clearHighlights({ key: this.#highlightKey });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
#teardownEffects() {
|
|
850
|
+
if (this.#filterActive) {
|
|
851
|
+
this.#adapter.applyFilter(void 0);
|
|
852
|
+
this.#filterActive = false;
|
|
853
|
+
}
|
|
854
|
+
clearHighlights({ key: this.#highlightKey });
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
//#endregion
|
|
858
|
+
//#region src/incr-search/list-search-styles.ts
|
|
859
|
+
/**
|
|
860
|
+
* Styles for the incremental-search popover rendered by
|
|
861
|
+
* `ListSearchController.renderField()`. Add to a component's `static styles`;
|
|
862
|
+
* the class names are the contract between this fragment and `renderField()`.
|
|
863
|
+
*
|
|
864
|
+
* The host must give the popover a positioned ancestor (a wrapper with
|
|
865
|
+
* `position: relative`, or `:host { position: relative }`). Tunables:
|
|
866
|
+
* `--list-search-inset` (default `8px`) offsets it from the top-right corner.
|
|
867
|
+
*
|
|
868
|
+
* See `docs/infra-incremental.md`.
|
|
869
|
+
*/
|
|
870
|
+
const listSearchStyles = css`
|
|
871
|
+
.list-search {
|
|
872
|
+
position: absolute;
|
|
873
|
+
top: var(--list-search-inset, 8px);
|
|
874
|
+
right: var(--list-search-inset, 8px);
|
|
875
|
+
z-index: 20;
|
|
876
|
+
display: flex;
|
|
877
|
+
flex-direction: column;
|
|
878
|
+
gap: 4px;
|
|
879
|
+
padding: 6px;
|
|
880
|
+
box-sizing: border-box;
|
|
881
|
+
background: var(--surface-4, #2a2a2a);
|
|
882
|
+
color: var(--surface-3-text, inherit);
|
|
883
|
+
border: 1px solid var(--border, #444);
|
|
884
|
+
border-radius: var(--radius-m, 6px);
|
|
885
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
|
886
|
+
font-size: var(--text-s, 0.85em);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.list-search[hidden] {
|
|
890
|
+
display: none;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.list-search-row {
|
|
894
|
+
display: flex;
|
|
895
|
+
align-items: center;
|
|
896
|
+
gap: 4px;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.list-search-mode {
|
|
900
|
+
flex: none;
|
|
901
|
+
padding: 1px 7px;
|
|
902
|
+
border-radius: 999px;
|
|
903
|
+
font-weight: 600;
|
|
904
|
+
text-transform: capitalize;
|
|
905
|
+
background: var(--accent, #60a5fa);
|
|
906
|
+
color: var(--accent-text, #000);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.list-search-input {
|
|
910
|
+
flex: 1;
|
|
911
|
+
min-width: 140px;
|
|
912
|
+
background: transparent;
|
|
913
|
+
border: none;
|
|
914
|
+
outline: none;
|
|
915
|
+
color: inherit;
|
|
916
|
+
font: inherit;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.list-search-count {
|
|
920
|
+
flex: none;
|
|
921
|
+
opacity: 0.6;
|
|
922
|
+
font-variant-numeric: tabular-nums;
|
|
923
|
+
white-space: nowrap;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.list-search-switch {
|
|
927
|
+
display: flex;
|
|
928
|
+
flex-wrap: wrap;
|
|
929
|
+
gap: 4px;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.list-search-btn {
|
|
933
|
+
padding: 1px 7px;
|
|
934
|
+
background: var(--surface-3, #333);
|
|
935
|
+
color: inherit;
|
|
936
|
+
border: 1px solid var(--border, #444);
|
|
937
|
+
border-radius: var(--radius-s, 4px);
|
|
938
|
+
cursor: pointer;
|
|
939
|
+
font: inherit;
|
|
940
|
+
text-transform: capitalize;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.list-search-btn:hover {
|
|
944
|
+
background: var(--item-bg-tickled, #444);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.list-search-icon-btn {
|
|
948
|
+
flex: none;
|
|
949
|
+
padding: 0 2px;
|
|
950
|
+
background: transparent;
|
|
951
|
+
border: none;
|
|
952
|
+
color: var(--surface-muted-text, #888);
|
|
953
|
+
cursor: pointer;
|
|
954
|
+
font-size: 1.1em;
|
|
955
|
+
line-height: 1;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
.list-search-icon-btn:hover {
|
|
959
|
+
color: inherit;
|
|
960
|
+
}
|
|
961
|
+
`;
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region src/incr-search/tree-list-search-adapter.ts
|
|
964
|
+
/**
|
|
965
|
+
* A {@link ListSearchAdapter} over any `TreeSearchHost` (`ixfx-tree-list`,
|
|
966
|
+
* `ixfx-miller-list`, `ixfx-crumb-navigation`). Navigate reveals the match
|
|
967
|
+
* (expands its ancestors, moves the cursor); select drives `selectMany`; filter
|
|
968
|
+
* sets `filterPredicate`. The item set is the whole tree — `getNodes()` returns
|
|
969
|
+
* every node regardless of expansion or an active filter — so refining a query
|
|
970
|
+
* can always re-surface a node. See `docs/infra-incremental.md`.
|
|
971
|
+
*/
|
|
972
|
+
function createTreeListSearchAdapter(host, options = {}) {
|
|
973
|
+
return {
|
|
974
|
+
getItems: () => host.getNodes(),
|
|
975
|
+
getLabel: options.getLabel ?? ((item) => item.node.item.label),
|
|
976
|
+
getKey: (item) => item.node.item.key,
|
|
977
|
+
focusItem: (item) => {
|
|
978
|
+
if (item) host.revealKey(item.node.item.key);
|
|
979
|
+
},
|
|
980
|
+
activateItem: (item) => {
|
|
981
|
+
host.revealKey(item.node.item.key);
|
|
982
|
+
host.select(item.node);
|
|
983
|
+
},
|
|
984
|
+
selectItems: (items) => {
|
|
985
|
+
host.selectMany(items.map((i) => i.node));
|
|
986
|
+
},
|
|
987
|
+
applyFilter: (matched) => {
|
|
988
|
+
host.filterPredicate = matched === void 0 ? void 0 : (node) => matched.has(node.item.key);
|
|
989
|
+
},
|
|
990
|
+
highlightRoot: () => host.shadowRoot,
|
|
991
|
+
highlightSelector: options.labelSelector ?? `.tree-label`,
|
|
992
|
+
highlightKeyOf: (el) => el.closest(`[data-key]`)?.dataset.key,
|
|
993
|
+
highlightKey: options.highlightKey
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
export { LIST_SEARCH_COMMAND_IDS as a, createElementListSearchAdapter as c, createDataListSearchAdapter as d, IncrSearchTreeController as f, createFuzzySearch as h, ListSearchController as i, KeyedElementIndex as l, createAcCompletionProvider as m, listSearchStyles as n, registerListSearchCommands as o, createIncrSearch as p, LIST_SEARCH_MODES as r, createElementIncrSearch as s, createTreeListSearchAdapter as t, keyForElement as u };
|
|
998
|
+
|
|
999
|
+
//# sourceMappingURL=incr-search-C05WjbZh.js.map
|