flexdesk 0.17.0 → 0.17.1

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.
@@ -1,610 +0,0 @@
1
- // src/ui/components/autocomplete_field.js
2
- var AutocompleteField = class {
3
- constructor({
4
- fieldId,
5
- field = {},
6
- provider = null,
7
- onChange = null,
8
- logger = null,
9
- namespace = null,
10
- namespaceOptions = [],
11
- showNamespaceSelect = false,
12
- variant = null,
13
- showNamespaceChip = false,
14
- allowScopeToggle = false,
15
- namespaceLabel = null,
16
- scopeMode = "current",
17
- skipNamespacePrefix = false,
18
- // Material symbol per completion type, merged over the generic set. The
19
- // framework's map used to include `sector: 'domain'` — an EcoAgent
20
- // completion type — so the widget knew one application's ontology.
21
- typeIcons = {}
22
- } = {}) {
23
- if (!fieldId) {
24
- throw new Error("AutocompleteField requires a fieldId");
25
- }
26
- this.fieldId = fieldId;
27
- this.field = field;
28
- this.provider = typeof provider === "function" ? provider : () => [];
29
- this.onChange = typeof onChange === "function" ? onChange : null;
30
- this.logger = logger;
31
- this.typeIcons = typeIcons ?? {};
32
- this.namespace = namespace;
33
- this.namespaceOptions = Array.isArray(namespaceOptions) ? [...namespaceOptions] : [];
34
- if (this.namespace && !this.namespaceOptions.some((opt) => opt.value === this.namespace)) {
35
- const fallbackLabel = this.field.namespaceLabel || "This Tab";
36
- this.namespaceOptions.push({ value: this.namespace, label: fallbackLabel, token: fallbackLabel });
37
- }
38
- this.showNamespaceSelect = Boolean(showNamespaceSelect);
39
- this.variant = variant;
40
- this.showNamespaceChip = Boolean(showNamespaceChip);
41
- this.allowScopeToggle = Boolean(allowScopeToggle);
42
- this.namespaceLabel = namespaceLabel;
43
- this.scope = scopeMode === "all" ? "all" : "current";
44
- this.skipNamespacePrefix = Boolean(skipNamespacePrefix);
45
- this.disabled = Boolean(field.disabled);
46
- const parsed = this.#parseInitialNamespace(field.value ?? "");
47
- this.selectedNamespace = parsed.initialNamespace;
48
- this.initialFragment = parsed.initialFragment;
49
- this._lastDropdownRect = null;
50
- this.root = null;
51
- this.input = null;
52
- this.dropdown = null;
53
- this.helperEl = null;
54
- this.scopeControl = null;
55
- this.namespaceSelect = null;
56
- this.ghost = null;
57
- this._items = [];
58
- this._activeIndex = -1;
59
- this._isOpen = false;
60
- this._disposers = [];
61
- this._suggestSeq = 0;
62
- }
63
- getValue() {
64
- return this.#composeValue();
65
- }
66
- setComposedValue(value) {
67
- if (!this.input) return;
68
- const safeValue = typeof value === "string" ? value : "";
69
- if (this.skipNamespacePrefix) {
70
- this.input.value = safeValue;
71
- this.initialFragment = safeValue;
72
- this.#renderGhost();
73
- this.#close();
74
- return;
75
- }
76
- const parsed = this.#parseInitialNamespace(safeValue);
77
- this.selectedNamespace = parsed.initialNamespace;
78
- this.initialFragment = parsed.initialFragment;
79
- if (this.namespaceSelect) {
80
- this.namespaceSelect.value = this.selectedNamespace;
81
- }
82
- this.#updateNamespaceChip();
83
- this.input.value = parsed.initialFragment;
84
- this.#renderGhost();
85
- this.#close();
86
- }
87
- #parseInitialNamespace(rawValue) {
88
- const value = typeof rawValue === "string" ? rawValue.trim() : "";
89
- const options = this.namespaceOptions;
90
- const defaultNs = this.namespace ? options.find((opt) => opt.value === this.namespace)?.value ?? this.namespace : options[0]?.value ?? "__ALL__";
91
- if (!options.length) {
92
- return { initialNamespace: defaultNs, initialFragment: value };
93
- }
94
- if (!value) {
95
- return { initialNamespace: defaultNs, initialFragment: "" };
96
- }
97
- const dotIndex = value.indexOf(".");
98
- if (dotIndex > 0) {
99
- const nsCandidate = value.slice(0, dotIndex);
100
- const match = options.find(
101
- (opt) => (opt.value || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.label || "").toLowerCase() === nsCandidate.toLowerCase()
102
- );
103
- if (match) {
104
- return { initialNamespace: match.value, initialFragment: value.slice(dotIndex + 1) };
105
- }
106
- }
107
- return { initialNamespace: defaultNs, initialFragment: value };
108
- }
109
- #resolveNamespaceLabel() {
110
- if (this.scope === "all") {
111
- return "All Namespaces";
112
- }
113
- if (typeof this.namespaceLabel === "string" && this.namespaceLabel.trim()) {
114
- return this.namespaceLabel.trim();
115
- }
116
- if (typeof this.field.namespaceLabel === "string" && this.field.namespaceLabel.trim()) {
117
- return this.field.namespaceLabel.trim();
118
- }
119
- if (typeof this.namespace === "string" && this.namespace.trim()) {
120
- return this.namespace.trim();
121
- }
122
- return "This Tab";
123
- }
124
- #toggleScope() {
125
- if (this.disabled) return;
126
- this.scope = this.scope === "all" ? "current" : "all";
127
- this.root?.classList?.toggle("twm-autocomplete-field--scope-all", this.scope === "all");
128
- this.#updateNamespaceChip();
129
- this.#updateSuggestions();
130
- }
131
- #updateNamespaceChip() {
132
- if (!this.scopeControl) return;
133
- this.scopeControl.textContent = this.#resolveNamespaceLabel();
134
- this.scopeControl.dataset.scope = this.scope;
135
- this.scopeControl.title = this.scope === "all" ? "Searching across all namespaces (click to limit to this tab)" : "Searching this tab (click to search all namespaces)";
136
- }
137
- render() {
138
- if (this.root) {
139
- return this.root;
140
- }
141
- this.root = document.createElement("div");
142
- this.root.className = "twm-autocomplete-field";
143
- if (this.variant) {
144
- this.root.classList.add(`twm-autocomplete-field--${this.variant}`);
145
- }
146
- this.root.classList.toggle("twm-autocomplete-field--scope-all", this.scope === "all");
147
- const inputWrapper = document.createElement("div");
148
- inputWrapper.className = "twm-autocomplete-field__input-wrapper";
149
- if (this.showNamespaceSelect && this.namespaceOptions.length) {
150
- const select = document.createElement("select");
151
- select.className = "twm-autocomplete-field__namespace-select";
152
- this.namespaceOptions.forEach((opt) => {
153
- const option = document.createElement("option");
154
- option.value = opt.value;
155
- option.textContent = opt.label ?? opt.value;
156
- if (opt.value === this.selectedNamespace) {
157
- option.selected = true;
158
- }
159
- select.appendChild(option);
160
- });
161
- this.namespaceSelect = select;
162
- inputWrapper.appendChild(select);
163
- }
164
- if (this.showNamespaceChip) {
165
- const nsChip = document.createElement(this.allowScopeToggle ? "button" : "span");
166
- nsChip.className = "twm-autocomplete-field__namespace";
167
- if (this.allowScopeToggle) {
168
- nsChip.setAttribute("type", "button");
169
- }
170
- if (this.allowScopeToggle) {
171
- nsChip.classList.add("twm-autocomplete-field__namespace--toggle");
172
- nsChip.addEventListener("click", () => this.#toggleScope());
173
- }
174
- this.scopeControl = nsChip;
175
- this.#updateNamespaceChip();
176
- inputWrapper.appendChild(nsChip);
177
- }
178
- this.input = document.createElement("input");
179
- this.input.type = "text";
180
- this.input.className = "twm-autocomplete-field__input";
181
- this.input.id = this.fieldId;
182
- this.input.autocomplete = "off";
183
- this.input.spellcheck = false;
184
- if (this.field.placeholder) {
185
- this.input.placeholder = this.field.placeholder;
186
- }
187
- if (this.field.value != null) {
188
- this.input.value = this.initialFragment;
189
- }
190
- if (this.disabled) {
191
- this.input.disabled = true;
192
- this.root.classList.add("twm-autocomplete-field--disabled");
193
- }
194
- inputWrapper.appendChild(this.input);
195
- this.ghost = null;
196
- this.dropdown = document.createElement("div");
197
- this.dropdown.className = "twm-autocomplete-field__dropdown";
198
- this.dropdown.setAttribute("role", "listbox");
199
- document.body.appendChild(this.dropdown);
200
- this.helperEl = document.createElement("div");
201
- this.helperEl.className = "twm-autocomplete-field__helper twm-config-hint";
202
- if (this.field.helperText) {
203
- this.helperEl.textContent = this.field.helperText;
204
- }
205
- this.root.appendChild(inputWrapper);
206
- this.root.appendChild(this.helperEl);
207
- this.#wireEvents();
208
- this.#renderGhost();
209
- return this.root;
210
- }
211
- #wireEvents() {
212
- const onInput = () => {
213
- if (this.disabled) return;
214
- this.#syncNamespaceFromInput();
215
- this.#renderGhost();
216
- this.#updateSuggestions();
217
- };
218
- const onChange = () => {
219
- this.onChange?.(this.#composeValue(), { immediate: false, namespace: this.selectedNamespace });
220
- };
221
- const onPaste = () => {
222
- };
223
- const onKeyDown = (e) => {
224
- if (this.disabled) return;
225
- this.#handleKeyDown(e);
226
- };
227
- const onFocus = () => {
228
- if (this.disabled) return;
229
- this.#updateSuggestions();
230
- };
231
- const onBlur = (e) => {
232
- setTimeout(() => {
233
- if (!this.root?.contains(document.activeElement)) {
234
- this.#close();
235
- this.onChange?.(this.#composeValue(), { immediate: false, namespace: this.selectedNamespace });
236
- }
237
- }, 150);
238
- };
239
- const onDropdownMouseDown = (e) => {
240
- e.preventDefault();
241
- };
242
- const onDropdownClick = (e) => {
243
- const item = e.target.closest(".twm-autocomplete-field__item");
244
- if (item) {
245
- const index = parseInt(item.dataset.index, 10);
246
- if (!isNaN(index)) {
247
- this.#selectItem(index);
248
- }
249
- }
250
- };
251
- this.input.addEventListener("input", onInput);
252
- this.input.addEventListener("change", onChange);
253
- this.input.addEventListener("paste", onPaste);
254
- this.input.addEventListener("keydown", onKeyDown);
255
- this.input.addEventListener("focus", onFocus);
256
- this.input.addEventListener("blur", onBlur);
257
- this.dropdown.addEventListener("mousedown", onDropdownMouseDown);
258
- this.dropdown.addEventListener("click", onDropdownClick);
259
- if (this.namespaceSelect) {
260
- const onSelectChange = () => {
261
- this.selectedNamespace = this.namespaceSelect.value;
262
- this.#updateSuggestions();
263
- this.onChange?.(this.#composeValue(), {
264
- immediate: false,
265
- namespace: this.selectedNamespace,
266
- viewNamespace: this.selectedNamespace
267
- });
268
- };
269
- this.namespaceSelect.addEventListener("change", onSelectChange);
270
- this._disposers.push(() => this.namespaceSelect.removeEventListener("change", onSelectChange));
271
- }
272
- const onWindowResize = () => {
273
- if (this._isOpen) {
274
- this.#positionDropdown();
275
- }
276
- };
277
- window.addEventListener("resize", onWindowResize);
278
- const onScroll = (e) => {
279
- if (!this._isOpen) return;
280
- if (this.dropdown?.contains(e.target)) return;
281
- this.#close();
282
- };
283
- document.addEventListener("scroll", onScroll, true);
284
- this._disposers.push(() => {
285
- document.removeEventListener("scroll", onScroll, true);
286
- });
287
- const onDocumentClick = (e) => {
288
- if (!this._isOpen) return;
289
- const clickedInRoot = this.root?.contains(e.target);
290
- const clickedInDropdown = this.dropdown?.contains(e.target);
291
- if (!clickedInRoot && !clickedInDropdown) {
292
- this.#close();
293
- }
294
- };
295
- document.addEventListener("click", onDocumentClick, true);
296
- this._disposers.push(() => {
297
- window.removeEventListener("resize", onWindowResize);
298
- });
299
- this._disposers.push(() => {
300
- document.removeEventListener("click", onDocumentClick, true);
301
- });
302
- this._disposers.push(() => {
303
- this.input.removeEventListener("input", onInput);
304
- this.input.removeEventListener("change", onChange);
305
- this.input.removeEventListener("paste", onPaste);
306
- this.input.removeEventListener("keydown", onKeyDown);
307
- this.input.removeEventListener("focus", onFocus);
308
- this.input.removeEventListener("blur", onBlur);
309
- this.dropdown.removeEventListener("mousedown", onDropdownMouseDown);
310
- this.dropdown.removeEventListener("click", onDropdownClick);
311
- });
312
- }
313
- #handleKeyDown(e) {
314
- if (!this._isOpen) {
315
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
316
- this.#updateSuggestions();
317
- e.preventDefault();
318
- }
319
- return;
320
- }
321
- switch (e.key) {
322
- case "ArrowDown":
323
- e.preventDefault();
324
- this.#moveSelection(1);
325
- break;
326
- case "ArrowUp":
327
- e.preventDefault();
328
- this.#moveSelection(-1);
329
- break;
330
- case "Enter":
331
- e.preventDefault();
332
- if (this._activeIndex >= 0) {
333
- this.#selectItem(this._activeIndex);
334
- }
335
- this.input?.blur();
336
- break;
337
- case "Tab":
338
- if (this._activeIndex >= 0) {
339
- e.preventDefault();
340
- this.#selectItem(this._activeIndex);
341
- }
342
- break;
343
- case "Escape":
344
- e.preventDefault();
345
- this.#close();
346
- break;
347
- }
348
- }
349
- #updateSuggestions() {
350
- const fragment = this.#getInputFragment();
351
- const request = ++this._suggestSeq;
352
- let produced;
353
- try {
354
- produced = this.provider({
355
- value: fragment,
356
- scope: this.scope,
357
- namespace: this.selectedNamespace
358
- });
359
- } catch (err) {
360
- this.logger?.warn?.("autocomplete", "Provider error", { err });
361
- this._items = [];
362
- this.#close();
363
- return;
364
- }
365
- if (produced && typeof produced.then === "function") {
366
- produced.then(
367
- (items) => {
368
- if (request !== this._suggestSeq) return;
369
- this.#applySuggestions(items);
370
- },
371
- (err) => {
372
- this.logger?.warn?.("autocomplete", "Provider error", { err });
373
- if (request !== this._suggestSeq) return;
374
- this._items = [];
375
- this.#close();
376
- }
377
- );
378
- return;
379
- }
380
- this.#applySuggestions(produced);
381
- }
382
- /** Draw whatever the provider produced. Split out of `#updateSuggestions`
383
- * so the synchronous and the awaited paths cannot drift apart. */
384
- #applySuggestions(produced) {
385
- const items = Array.isArray(produced) ? produced : [];
386
- this._items = items;
387
- this._activeIndex = items.length > 0 ? 0 : -1;
388
- this.#renderDropdown();
389
- if (items.length > 0) {
390
- this.#open();
391
- } else {
392
- this.#close();
393
- }
394
- }
395
- #renderDropdown() {
396
- this.dropdown.innerHTML = "";
397
- const showingAll = this.selectedNamespace === "__ALL__";
398
- this._items.forEach((item, index) => {
399
- const el = document.createElement("div");
400
- el.className = "twm-autocomplete-field__item";
401
- el.setAttribute("role", "option");
402
- el.dataset.index = index;
403
- el.dataset.type = item.type || item.iconType || "variable";
404
- if (index === this._activeIndex) {
405
- el.classList.add("active");
406
- }
407
- const icon = document.createElement("span");
408
- icon.className = "twm-autocomplete-field__icon material-symbols-outlined";
409
- icon.textContent = this.#getIconForType(item.type || item.iconType);
410
- el.appendChild(icon);
411
- const label = document.createElement("span");
412
- label.className = "twm-autocomplete-field__label";
413
- label.textContent = item.label || item.insertText || "";
414
- el.appendChild(label);
415
- if (showingAll && item.metadata?.namespaceId) {
416
- const nsLabel = this.#resolveNamespaceToken(item.metadata.namespaceId);
417
- if (nsLabel) {
418
- const nsIndicator = document.createElement("span");
419
- nsIndicator.className = "twm-autocomplete-field__namespace-indicator";
420
- nsIndicator.textContent = nsLabel;
421
- el.appendChild(nsIndicator);
422
- }
423
- }
424
- if (item.type && !showingAll) {
425
- const badge = document.createElement("span");
426
- badge.className = "twm-autocomplete-field__badge";
427
- badge.textContent = item.type;
428
- el.appendChild(badge);
429
- }
430
- this.dropdown.appendChild(el);
431
- });
432
- if (this._isOpen) {
433
- this.#positionDropdown();
434
- }
435
- }
436
- #resolveNamespaceToken(namespaceId) {
437
- if (!namespaceId) return null;
438
- const opt = this.namespaceOptions.find((o) => o.value === namespaceId);
439
- return opt?.token || opt?.label || null;
440
- }
441
- #getIconForType(type) {
442
- const icons = {
443
- variable: "functions",
444
- "variable-qualified": "link",
445
- stock: "account_balance",
446
- account: "savings",
447
- function: "code",
448
- constant: "numbers",
449
- operator: "calculate",
450
- snippet: "content_paste",
451
- ...this.typeIcons
452
- };
453
- return icons[type] || "label";
454
- }
455
- #moveSelection(delta) {
456
- if (this._items.length === 0) return;
457
- let newIndex = this._activeIndex + delta;
458
- if (newIndex < 0) newIndex = this._items.length - 1;
459
- if (newIndex >= this._items.length) newIndex = 0;
460
- this._activeIndex = newIndex;
461
- this.#renderDropdown();
462
- this.#scrollToActive();
463
- }
464
- #scrollToActive() {
465
- const activeEl = this.dropdown.querySelector(".twm-autocomplete-field__item.active");
466
- if (activeEl) {
467
- activeEl.scrollIntoView({ block: "nearest", behavior: "smooth" });
468
- }
469
- }
470
- #selectItem(index) {
471
- const item = this._items[index];
472
- if (!item) return;
473
- const itemNamespace = item.metadata?.namespaceId || null;
474
- const insertText = item.insertText || item.label || "";
475
- this.input.value = insertText;
476
- const viewNamespace = this.selectedNamespace;
477
- const resolvedNamespace = viewNamespace === "__ALL__" && itemNamespace ? itemNamespace : viewNamespace;
478
- this.#close();
479
- this.input.focus();
480
- this.onChange?.(this.#composeValue(), {
481
- immediate: true,
482
- namespace: resolvedNamespace,
483
- viewNamespace,
484
- metadata: item.metadata || null
485
- });
486
- }
487
- #open() {
488
- if (this._isOpen) {
489
- return;
490
- }
491
- this._isOpen = true;
492
- this.#positionDropdown();
493
- this.dropdown.classList.add("visible");
494
- }
495
- #close() {
496
- if (!this._isOpen) return;
497
- this._isOpen = false;
498
- this.dropdown.classList.remove("visible");
499
- this._activeIndex = -1;
500
- }
501
- #syncNamespaceFromInput() {
502
- if (!this.namespaceOptions.length) return;
503
- const value = this.input?.value || "";
504
- const dotIndex = value.indexOf(".");
505
- if (dotIndex > 0) {
506
- const nsCandidate = value.slice(0, dotIndex);
507
- const match = this.namespaceOptions.find(
508
- (opt) => opt.value !== "__ALL__" && ((opt.value || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.label || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.token || "").toLowerCase() === nsCandidate.toLowerCase())
509
- );
510
- if (match && this.namespaceSelect && this.namespaceSelect.value !== match.value) {
511
- this.namespaceSelect.value = match.value;
512
- this.selectedNamespace = match.value;
513
- }
514
- }
515
- }
516
- #getInputFragment() {
517
- const value = this.input?.value || "";
518
- const dotIndex = value.indexOf(".");
519
- if (dotIndex > 0) {
520
- const nsCandidate = value.slice(0, dotIndex);
521
- const match = this.namespaceOptions.find(
522
- (opt) => opt.value !== "__ALL__" && ((opt.value || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.label || "").toLowerCase() === nsCandidate.toLowerCase())
523
- );
524
- if (match) {
525
- return value.slice(dotIndex + 1);
526
- }
527
- }
528
- return value;
529
- }
530
- #composeValue() {
531
- const rawInput = this.input?.value || "";
532
- if (!rawInput) return "";
533
- if (this.skipNamespacePrefix) {
534
- return rawInput;
535
- }
536
- const dotIndex = rawInput.indexOf(".");
537
- if (dotIndex > 0) {
538
- const nsCandidate = rawInput.slice(0, dotIndex);
539
- const match = this.namespaceOptions.find(
540
- (opt) => opt.value !== "__ALL__" && ((opt.value || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.label || "").toLowerCase() === nsCandidate.toLowerCase())
541
- );
542
- if (match) {
543
- return rawInput;
544
- }
545
- }
546
- const ns = this.selectedNamespace;
547
- if (!ns || ns === "__ALL__") {
548
- return rawInput;
549
- }
550
- const nsOption = this.namespaceOptions.find(
551
- (opt) => opt.value === ns || (opt.value || "").toLowerCase() === (ns || "").toLowerCase()
552
- );
553
- const nsToken = nsOption?.token || nsOption?.label || "";
554
- const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
555
- if (ns === "__ALL__" || (nsToken || "").toUpperCase() === "__ALL__") {
556
- return rawInput;
557
- }
558
- if (!nsToken || uuidPattern.test(nsToken)) {
559
- return rawInput;
560
- }
561
- return `${nsToken}.${rawInput}`;
562
- }
563
- #renderGhost() {
564
- if (this.ghost) {
565
- this.ghost.style.display = "none";
566
- }
567
- }
568
- #positionDropdown() {
569
- if (!this.dropdown || !this.root) return;
570
- const wrapper = this.root.querySelector(".twm-autocomplete-field__input-wrapper");
571
- if (!wrapper) return;
572
- const rect = wrapper.getBoundingClientRect();
573
- this._lastDropdownRect = rect;
574
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
575
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
576
- const width = Math.min(rect.width, viewportWidth - rect.left - 8);
577
- const maxHeight = Math.max(120, viewportHeight - rect.bottom - 12);
578
- Object.assign(this.dropdown.style, {
579
- position: "fixed",
580
- top: `${rect.bottom}px`,
581
- left: `${rect.left}px`,
582
- width: `${width}px`,
583
- maxWidth: `${width}px`,
584
- maxHeight: `${maxHeight}px`
585
- });
586
- }
587
- dispose() {
588
- this._suggestSeq += 1;
589
- this.#close();
590
- this._disposers.forEach((fn) => {
591
- try {
592
- fn();
593
- } catch (e) {
594
- }
595
- });
596
- this._disposers = [];
597
- if (this.dropdown && this.dropdown.parentNode) {
598
- this.dropdown.parentNode.removeChild(this.dropdown);
599
- }
600
- this.root = null;
601
- this.input = null;
602
- this.dropdown = null;
603
- this.helperEl = null;
604
- }
605
- };
606
-
607
- export {
608
- AutocompleteField
609
- };
610
- //# sourceMappingURL=chunk-QWQJUDPD.js.map