flexdesk 0.17.0 → 0.17.2

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 (45) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +399 -3
  3. package/canvas.js +18 -0
  4. package/charts.js +21 -0
  5. package/core.js +14 -0
  6. package/css/base.css +23 -1
  7. package/css/flexdesk.css +61 -1
  8. package/dist/charts.js +3 -5
  9. package/dist/charts.js.map +1 -1
  10. package/dist/{chunk-Q36EPT2R.js → chunk-LWV23YVE.js} +1614 -138
  11. package/dist/chunk-LWV23YVE.js.map +7 -0
  12. package/dist/{chunk-MCIXFNVF.js → chunk-RMTRWWEO.js} +2 -2
  13. package/dist/flexdesk.css +61 -1
  14. package/dist/flow.js +2 -2
  15. package/dist/flow.js.map +2 -2
  16. package/dist/tiles.js +2 -2
  17. package/dist/widgets.js +186 -55
  18. package/dist/widgets.js.map +2 -2
  19. package/dist/wm.js +90 -10
  20. package/dist/wm.js.map +2 -2
  21. package/editor.js +20 -0
  22. package/flow.js +21 -0
  23. package/host.js +14 -0
  24. package/package.json +10 -1
  25. package/src/flow/canvas_editor/editor.js +1 -1
  26. package/src/tiling/command_palette.js +46 -7
  27. package/src/tiling/content_registry.js +3 -0
  28. package/src/tiling/entity_sources.js +10 -0
  29. package/src/tiling/wm.js +65 -3
  30. package/src/ui/components/cell_editor_host.js +100 -18
  31. package/src/ui/components/cell_editors.js +480 -0
  32. package/src/ui/components/cell_grid.js +121 -8
  33. package/src/ui/components/cell_paste.js +243 -0
  34. package/src/ui/components/data_table.js +96 -10
  35. package/src/ui/components/settings_view.js +192 -51
  36. package/src/ui/components/staged_edits.js +19 -1
  37. package/src/ui/components/tsv.js +74 -0
  38. package/src/ui/notification_center.js +5 -5
  39. package/tiles.js +27 -0
  40. package/widgets.js +69 -0
  41. package/wm.js +28 -0
  42. package/dist/chunk-Q36EPT2R.js.map +0 -7
  43. package/dist/chunk-QWQJUDPD.js +0 -610
  44. package/dist/chunk-QWQJUDPD.js.map +0 -7
  45. /package/dist/{chunk-MCIXFNVF.js.map → chunk-RMTRWWEO.js.map} +0 -0
@@ -3,6 +3,612 @@ import {
3
3
  isRowControl
4
4
  } from "./chunk-CVMCZZX2.js";
5
5
 
6
+ // src/ui/components/autocomplete_field.js
7
+ var AutocompleteField = class {
8
+ constructor({
9
+ fieldId,
10
+ field = {},
11
+ provider = null,
12
+ onChange = null,
13
+ logger = null,
14
+ namespace = null,
15
+ namespaceOptions = [],
16
+ showNamespaceSelect = false,
17
+ variant = null,
18
+ showNamespaceChip = false,
19
+ allowScopeToggle = false,
20
+ namespaceLabel = null,
21
+ scopeMode = "current",
22
+ skipNamespacePrefix = false,
23
+ // Material symbol per completion type, merged over the generic set. The
24
+ // framework's map used to include `sector: 'domain'` — an EcoAgent
25
+ // completion type — so the widget knew one application's ontology.
26
+ typeIcons = {}
27
+ } = {}) {
28
+ if (!fieldId) {
29
+ throw new Error("AutocompleteField requires a fieldId");
30
+ }
31
+ this.fieldId = fieldId;
32
+ this.field = field;
33
+ this.provider = typeof provider === "function" ? provider : () => [];
34
+ this.onChange = typeof onChange === "function" ? onChange : null;
35
+ this.logger = logger;
36
+ this.typeIcons = typeIcons ?? {};
37
+ this.namespace = namespace;
38
+ this.namespaceOptions = Array.isArray(namespaceOptions) ? [...namespaceOptions] : [];
39
+ if (this.namespace && !this.namespaceOptions.some((opt) => opt.value === this.namespace)) {
40
+ const fallbackLabel = this.field.namespaceLabel || "This Tab";
41
+ this.namespaceOptions.push({ value: this.namespace, label: fallbackLabel, token: fallbackLabel });
42
+ }
43
+ this.showNamespaceSelect = Boolean(showNamespaceSelect);
44
+ this.variant = variant;
45
+ this.showNamespaceChip = Boolean(showNamespaceChip);
46
+ this.allowScopeToggle = Boolean(allowScopeToggle);
47
+ this.namespaceLabel = namespaceLabel;
48
+ this.scope = scopeMode === "all" ? "all" : "current";
49
+ this.skipNamespacePrefix = Boolean(skipNamespacePrefix);
50
+ this.disabled = Boolean(field.disabled);
51
+ const parsed = this.#parseInitialNamespace(field.value ?? "");
52
+ this.selectedNamespace = parsed.initialNamespace;
53
+ this.initialFragment = parsed.initialFragment;
54
+ this._lastDropdownRect = null;
55
+ this.root = null;
56
+ this.input = null;
57
+ this.dropdown = null;
58
+ this.helperEl = null;
59
+ this.scopeControl = null;
60
+ this.namespaceSelect = null;
61
+ this.ghost = null;
62
+ this._items = [];
63
+ this._activeIndex = -1;
64
+ this._isOpen = false;
65
+ this._disposers = [];
66
+ this._suggestSeq = 0;
67
+ }
68
+ getValue() {
69
+ return this.#composeValue();
70
+ }
71
+ setComposedValue(value) {
72
+ if (!this.input) return;
73
+ const safeValue = typeof value === "string" ? value : "";
74
+ if (this.skipNamespacePrefix) {
75
+ this.input.value = safeValue;
76
+ this.initialFragment = safeValue;
77
+ this.#renderGhost();
78
+ this.#close();
79
+ return;
80
+ }
81
+ const parsed = this.#parseInitialNamespace(safeValue);
82
+ this.selectedNamespace = parsed.initialNamespace;
83
+ this.initialFragment = parsed.initialFragment;
84
+ if (this.namespaceSelect) {
85
+ this.namespaceSelect.value = this.selectedNamespace;
86
+ }
87
+ this.#updateNamespaceChip();
88
+ this.input.value = parsed.initialFragment;
89
+ this.#renderGhost();
90
+ this.#close();
91
+ }
92
+ #parseInitialNamespace(rawValue) {
93
+ const value = typeof rawValue === "string" ? rawValue.trim() : "";
94
+ const options = this.namespaceOptions;
95
+ const defaultNs = this.namespace ? options.find((opt) => opt.value === this.namespace)?.value ?? this.namespace : options[0]?.value ?? "__ALL__";
96
+ if (!options.length) {
97
+ return { initialNamespace: defaultNs, initialFragment: value };
98
+ }
99
+ if (!value) {
100
+ return { initialNamespace: defaultNs, initialFragment: "" };
101
+ }
102
+ const dotIndex = value.indexOf(".");
103
+ if (dotIndex > 0) {
104
+ const nsCandidate = value.slice(0, dotIndex);
105
+ const match = options.find(
106
+ (opt) => (opt.value || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.label || "").toLowerCase() === nsCandidate.toLowerCase()
107
+ );
108
+ if (match) {
109
+ return { initialNamespace: match.value, initialFragment: value.slice(dotIndex + 1) };
110
+ }
111
+ }
112
+ return { initialNamespace: defaultNs, initialFragment: value };
113
+ }
114
+ #resolveNamespaceLabel() {
115
+ if (this.scope === "all") {
116
+ return "All Namespaces";
117
+ }
118
+ if (typeof this.namespaceLabel === "string" && this.namespaceLabel.trim()) {
119
+ return this.namespaceLabel.trim();
120
+ }
121
+ if (typeof this.field.namespaceLabel === "string" && this.field.namespaceLabel.trim()) {
122
+ return this.field.namespaceLabel.trim();
123
+ }
124
+ if (typeof this.namespace === "string" && this.namespace.trim()) {
125
+ return this.namespace.trim();
126
+ }
127
+ return "This Tab";
128
+ }
129
+ #toggleScope() {
130
+ if (this.disabled) return;
131
+ this.scope = this.scope === "all" ? "current" : "all";
132
+ this.root?.classList?.toggle("twm-autocomplete-field--scope-all", this.scope === "all");
133
+ this.#updateNamespaceChip();
134
+ this.#updateSuggestions();
135
+ }
136
+ #updateNamespaceChip() {
137
+ if (!this.scopeControl) return;
138
+ this.scopeControl.textContent = this.#resolveNamespaceLabel();
139
+ this.scopeControl.dataset.scope = this.scope;
140
+ this.scopeControl.title = this.scope === "all" ? "Searching across all namespaces (click to limit to this tab)" : "Searching this tab (click to search all namespaces)";
141
+ }
142
+ render() {
143
+ if (this.root) {
144
+ return this.root;
145
+ }
146
+ this.root = document.createElement("div");
147
+ this.root.className = "twm-autocomplete-field";
148
+ if (this.variant) {
149
+ this.root.classList.add(`twm-autocomplete-field--${this.variant}`);
150
+ }
151
+ this.root.classList.toggle("twm-autocomplete-field--scope-all", this.scope === "all");
152
+ const inputWrapper = document.createElement("div");
153
+ inputWrapper.className = "twm-autocomplete-field__input-wrapper";
154
+ if (this.showNamespaceSelect && this.namespaceOptions.length) {
155
+ const select = document.createElement("select");
156
+ select.className = "twm-autocomplete-field__namespace-select";
157
+ this.namespaceOptions.forEach((opt) => {
158
+ const option = document.createElement("option");
159
+ option.value = opt.value;
160
+ option.textContent = opt.label ?? opt.value;
161
+ if (opt.value === this.selectedNamespace) {
162
+ option.selected = true;
163
+ }
164
+ select.appendChild(option);
165
+ });
166
+ this.namespaceSelect = select;
167
+ inputWrapper.appendChild(select);
168
+ }
169
+ if (this.showNamespaceChip) {
170
+ const nsChip = document.createElement(this.allowScopeToggle ? "button" : "span");
171
+ nsChip.className = "twm-autocomplete-field__namespace";
172
+ if (this.allowScopeToggle) {
173
+ nsChip.setAttribute("type", "button");
174
+ }
175
+ if (this.allowScopeToggle) {
176
+ nsChip.classList.add("twm-autocomplete-field__namespace--toggle");
177
+ nsChip.addEventListener("click", () => this.#toggleScope());
178
+ }
179
+ this.scopeControl = nsChip;
180
+ this.#updateNamespaceChip();
181
+ inputWrapper.appendChild(nsChip);
182
+ }
183
+ this.input = document.createElement("input");
184
+ this.input.type = "text";
185
+ this.input.className = "twm-autocomplete-field__input";
186
+ this.input.id = this.fieldId;
187
+ this.input.autocomplete = "off";
188
+ this.input.spellcheck = false;
189
+ if (this.field.placeholder) {
190
+ this.input.placeholder = this.field.placeholder;
191
+ }
192
+ if (this.field.value != null) {
193
+ this.input.value = this.initialFragment;
194
+ }
195
+ if (this.disabled) {
196
+ this.input.disabled = true;
197
+ this.root.classList.add("twm-autocomplete-field--disabled");
198
+ }
199
+ inputWrapper.appendChild(this.input);
200
+ this.ghost = null;
201
+ this.dropdown = document.createElement("div");
202
+ this.dropdown.className = "twm-autocomplete-field__dropdown";
203
+ this.dropdown.setAttribute("role", "listbox");
204
+ document.body.appendChild(this.dropdown);
205
+ this.helperEl = document.createElement("div");
206
+ this.helperEl.className = "twm-autocomplete-field__helper twm-config-hint";
207
+ if (this.field.helperText) {
208
+ this.helperEl.textContent = this.field.helperText;
209
+ }
210
+ this.root.appendChild(inputWrapper);
211
+ this.root.appendChild(this.helperEl);
212
+ this.#wireEvents();
213
+ this.#renderGhost();
214
+ return this.root;
215
+ }
216
+ #wireEvents() {
217
+ const onInput = () => {
218
+ if (this.disabled) return;
219
+ this.#syncNamespaceFromInput();
220
+ this.#renderGhost();
221
+ this.#updateSuggestions();
222
+ };
223
+ const onChange = () => {
224
+ this.onChange?.(this.#composeValue(), { immediate: false, namespace: this.selectedNamespace });
225
+ };
226
+ const onPaste = () => {
227
+ };
228
+ const onKeyDown = (e) => {
229
+ if (this.disabled) return;
230
+ this.#handleKeyDown(e);
231
+ };
232
+ const onFocus = () => {
233
+ if (this.disabled) return;
234
+ this.#updateSuggestions();
235
+ };
236
+ const onBlur = (e) => {
237
+ setTimeout(() => {
238
+ if (!this.root?.contains(document.activeElement)) {
239
+ this.#close();
240
+ this.onChange?.(this.#composeValue(), { immediate: false, namespace: this.selectedNamespace });
241
+ }
242
+ }, 150);
243
+ };
244
+ const onDropdownMouseDown = (e) => {
245
+ e.preventDefault();
246
+ };
247
+ const onDropdownClick = (e) => {
248
+ const item = e.target.closest(".twm-autocomplete-field__item");
249
+ if (item) {
250
+ const index = parseInt(item.dataset.index, 10);
251
+ if (!isNaN(index)) {
252
+ this.#selectItem(index);
253
+ }
254
+ }
255
+ };
256
+ this.input.addEventListener("input", onInput);
257
+ this.input.addEventListener("change", onChange);
258
+ this.input.addEventListener("paste", onPaste);
259
+ this.input.addEventListener("keydown", onKeyDown);
260
+ this.input.addEventListener("focus", onFocus);
261
+ this.input.addEventListener("blur", onBlur);
262
+ this.dropdown.addEventListener("mousedown", onDropdownMouseDown);
263
+ this.dropdown.addEventListener("click", onDropdownClick);
264
+ if (this.namespaceSelect) {
265
+ const onSelectChange = () => {
266
+ this.selectedNamespace = this.namespaceSelect.value;
267
+ this.#updateSuggestions();
268
+ this.onChange?.(this.#composeValue(), {
269
+ immediate: false,
270
+ namespace: this.selectedNamespace,
271
+ viewNamespace: this.selectedNamespace
272
+ });
273
+ };
274
+ this.namespaceSelect.addEventListener("change", onSelectChange);
275
+ this._disposers.push(() => this.namespaceSelect.removeEventListener("change", onSelectChange));
276
+ }
277
+ const onWindowResize = () => {
278
+ if (this._isOpen) {
279
+ this.#positionDropdown();
280
+ }
281
+ };
282
+ window.addEventListener("resize", onWindowResize);
283
+ const onScroll = (e) => {
284
+ if (!this._isOpen) return;
285
+ if (this.dropdown?.contains(e.target)) return;
286
+ this.#close();
287
+ };
288
+ document.addEventListener("scroll", onScroll, true);
289
+ this._disposers.push(() => {
290
+ document.removeEventListener("scroll", onScroll, true);
291
+ });
292
+ const onDocumentClick = (e) => {
293
+ if (!this._isOpen) return;
294
+ const clickedInRoot = this.root?.contains(e.target);
295
+ const clickedInDropdown = this.dropdown?.contains(e.target);
296
+ if (!clickedInRoot && !clickedInDropdown) {
297
+ this.#close();
298
+ }
299
+ };
300
+ document.addEventListener("click", onDocumentClick, true);
301
+ this._disposers.push(() => {
302
+ window.removeEventListener("resize", onWindowResize);
303
+ });
304
+ this._disposers.push(() => {
305
+ document.removeEventListener("click", onDocumentClick, true);
306
+ });
307
+ this._disposers.push(() => {
308
+ this.input.removeEventListener("input", onInput);
309
+ this.input.removeEventListener("change", onChange);
310
+ this.input.removeEventListener("paste", onPaste);
311
+ this.input.removeEventListener("keydown", onKeyDown);
312
+ this.input.removeEventListener("focus", onFocus);
313
+ this.input.removeEventListener("blur", onBlur);
314
+ this.dropdown.removeEventListener("mousedown", onDropdownMouseDown);
315
+ this.dropdown.removeEventListener("click", onDropdownClick);
316
+ });
317
+ }
318
+ #handleKeyDown(e) {
319
+ if (!this._isOpen) {
320
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
321
+ this.#updateSuggestions();
322
+ e.preventDefault();
323
+ }
324
+ return;
325
+ }
326
+ switch (e.key) {
327
+ case "ArrowDown":
328
+ e.preventDefault();
329
+ this.#moveSelection(1);
330
+ break;
331
+ case "ArrowUp":
332
+ e.preventDefault();
333
+ this.#moveSelection(-1);
334
+ break;
335
+ case "Enter":
336
+ e.preventDefault();
337
+ if (this._activeIndex >= 0) {
338
+ this.#selectItem(this._activeIndex);
339
+ }
340
+ this.input?.blur();
341
+ break;
342
+ case "Tab":
343
+ if (this._activeIndex >= 0) {
344
+ e.preventDefault();
345
+ this.#selectItem(this._activeIndex);
346
+ }
347
+ break;
348
+ case "Escape":
349
+ e.preventDefault();
350
+ this.#close();
351
+ break;
352
+ }
353
+ }
354
+ #updateSuggestions() {
355
+ const fragment = this.#getInputFragment();
356
+ const request = ++this._suggestSeq;
357
+ let produced;
358
+ try {
359
+ produced = this.provider({
360
+ value: fragment,
361
+ scope: this.scope,
362
+ namespace: this.selectedNamespace
363
+ });
364
+ } catch (err) {
365
+ this.logger?.warn?.("autocomplete", "Provider error", { err });
366
+ this._items = [];
367
+ this.#close();
368
+ return;
369
+ }
370
+ if (produced && typeof produced.then === "function") {
371
+ produced.then(
372
+ (items) => {
373
+ if (request !== this._suggestSeq) return;
374
+ this.#applySuggestions(items);
375
+ },
376
+ (err) => {
377
+ this.logger?.warn?.("autocomplete", "Provider error", { err });
378
+ if (request !== this._suggestSeq) return;
379
+ this._items = [];
380
+ this.#close();
381
+ }
382
+ );
383
+ return;
384
+ }
385
+ this.#applySuggestions(produced);
386
+ }
387
+ /** Draw whatever the provider produced. Split out of `#updateSuggestions`
388
+ * so the synchronous and the awaited paths cannot drift apart. */
389
+ #applySuggestions(produced) {
390
+ const items = Array.isArray(produced) ? produced : [];
391
+ this._items = items;
392
+ this._activeIndex = items.length > 0 ? 0 : -1;
393
+ this.#renderDropdown();
394
+ if (items.length > 0) {
395
+ this.#open();
396
+ } else {
397
+ this.#close();
398
+ }
399
+ }
400
+ #renderDropdown() {
401
+ this.dropdown.innerHTML = "";
402
+ const showingAll = this.selectedNamespace === "__ALL__";
403
+ this._items.forEach((item, index) => {
404
+ const el = document.createElement("div");
405
+ el.className = "twm-autocomplete-field__item";
406
+ el.setAttribute("role", "option");
407
+ el.dataset.index = index;
408
+ el.dataset.type = item.type || item.iconType || "variable";
409
+ if (index === this._activeIndex) {
410
+ el.classList.add("active");
411
+ }
412
+ const icon = document.createElement("span");
413
+ icon.className = "twm-autocomplete-field__icon material-symbols-outlined";
414
+ icon.textContent = this.#getIconForType(item.type || item.iconType);
415
+ el.appendChild(icon);
416
+ const label = document.createElement("span");
417
+ label.className = "twm-autocomplete-field__label";
418
+ label.textContent = item.label || item.insertText || "";
419
+ el.appendChild(label);
420
+ if (showingAll && item.metadata?.namespaceId) {
421
+ const nsLabel = this.#resolveNamespaceToken(item.metadata.namespaceId);
422
+ if (nsLabel) {
423
+ const nsIndicator = document.createElement("span");
424
+ nsIndicator.className = "twm-autocomplete-field__namespace-indicator";
425
+ nsIndicator.textContent = nsLabel;
426
+ el.appendChild(nsIndicator);
427
+ }
428
+ }
429
+ if (item.type && !showingAll) {
430
+ const badge = document.createElement("span");
431
+ badge.className = "twm-autocomplete-field__badge";
432
+ badge.textContent = item.type;
433
+ el.appendChild(badge);
434
+ }
435
+ this.dropdown.appendChild(el);
436
+ });
437
+ if (this._isOpen) {
438
+ this.#positionDropdown();
439
+ }
440
+ }
441
+ #resolveNamespaceToken(namespaceId) {
442
+ if (!namespaceId) return null;
443
+ const opt = this.namespaceOptions.find((o) => o.value === namespaceId);
444
+ return opt?.token || opt?.label || null;
445
+ }
446
+ #getIconForType(type) {
447
+ const icons = {
448
+ variable: "functions",
449
+ "variable-qualified": "link",
450
+ stock: "account_balance",
451
+ account: "savings",
452
+ function: "code",
453
+ constant: "numbers",
454
+ operator: "calculate",
455
+ snippet: "content_paste",
456
+ ...this.typeIcons
457
+ };
458
+ return icons[type] || "label";
459
+ }
460
+ #moveSelection(delta) {
461
+ if (this._items.length === 0) return;
462
+ let newIndex = this._activeIndex + delta;
463
+ if (newIndex < 0) newIndex = this._items.length - 1;
464
+ if (newIndex >= this._items.length) newIndex = 0;
465
+ this._activeIndex = newIndex;
466
+ this.#renderDropdown();
467
+ this.#scrollToActive();
468
+ }
469
+ #scrollToActive() {
470
+ const activeEl = this.dropdown.querySelector(".twm-autocomplete-field__item.active");
471
+ if (activeEl) {
472
+ activeEl.scrollIntoView({ block: "nearest", behavior: "smooth" });
473
+ }
474
+ }
475
+ #selectItem(index) {
476
+ const item = this._items[index];
477
+ if (!item) return;
478
+ const itemNamespace = item.metadata?.namespaceId || null;
479
+ const insertText = item.insertText || item.label || "";
480
+ this.input.value = insertText;
481
+ const viewNamespace = this.selectedNamespace;
482
+ const resolvedNamespace = viewNamespace === "__ALL__" && itemNamespace ? itemNamespace : viewNamespace;
483
+ this.#close();
484
+ this.input.focus();
485
+ this.onChange?.(this.#composeValue(), {
486
+ immediate: true,
487
+ namespace: resolvedNamespace,
488
+ viewNamespace,
489
+ metadata: item.metadata || null
490
+ });
491
+ }
492
+ #open() {
493
+ if (this._isOpen) {
494
+ return;
495
+ }
496
+ this._isOpen = true;
497
+ this.#positionDropdown();
498
+ this.dropdown.classList.add("visible");
499
+ }
500
+ #close() {
501
+ if (!this._isOpen) return;
502
+ this._isOpen = false;
503
+ this.dropdown.classList.remove("visible");
504
+ this._activeIndex = -1;
505
+ }
506
+ #syncNamespaceFromInput() {
507
+ if (!this.namespaceOptions.length) return;
508
+ const value = this.input?.value || "";
509
+ const dotIndex = value.indexOf(".");
510
+ if (dotIndex > 0) {
511
+ const nsCandidate = value.slice(0, dotIndex);
512
+ const match = this.namespaceOptions.find(
513
+ (opt) => opt.value !== "__ALL__" && ((opt.value || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.label || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.token || "").toLowerCase() === nsCandidate.toLowerCase())
514
+ );
515
+ if (match && this.namespaceSelect && this.namespaceSelect.value !== match.value) {
516
+ this.namespaceSelect.value = match.value;
517
+ this.selectedNamespace = match.value;
518
+ }
519
+ }
520
+ }
521
+ #getInputFragment() {
522
+ const value = this.input?.value || "";
523
+ const dotIndex = value.indexOf(".");
524
+ if (dotIndex > 0) {
525
+ const nsCandidate = value.slice(0, dotIndex);
526
+ const match = this.namespaceOptions.find(
527
+ (opt) => opt.value !== "__ALL__" && ((opt.value || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.label || "").toLowerCase() === nsCandidate.toLowerCase())
528
+ );
529
+ if (match) {
530
+ return value.slice(dotIndex + 1);
531
+ }
532
+ }
533
+ return value;
534
+ }
535
+ #composeValue() {
536
+ const rawInput = this.input?.value || "";
537
+ if (!rawInput) return "";
538
+ if (this.skipNamespacePrefix) {
539
+ return rawInput;
540
+ }
541
+ const dotIndex = rawInput.indexOf(".");
542
+ if (dotIndex > 0) {
543
+ const nsCandidate = rawInput.slice(0, dotIndex);
544
+ const match = this.namespaceOptions.find(
545
+ (opt) => opt.value !== "__ALL__" && ((opt.value || "").toLowerCase() === nsCandidate.toLowerCase() || (opt.label || "").toLowerCase() === nsCandidate.toLowerCase())
546
+ );
547
+ if (match) {
548
+ return rawInput;
549
+ }
550
+ }
551
+ const ns = this.selectedNamespace;
552
+ if (!ns || ns === "__ALL__") {
553
+ return rawInput;
554
+ }
555
+ const nsOption = this.namespaceOptions.find(
556
+ (opt) => opt.value === ns || (opt.value || "").toLowerCase() === (ns || "").toLowerCase()
557
+ );
558
+ const nsToken = nsOption?.token || nsOption?.label || "";
559
+ 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;
560
+ if (ns === "__ALL__" || (nsToken || "").toUpperCase() === "__ALL__") {
561
+ return rawInput;
562
+ }
563
+ if (!nsToken || uuidPattern.test(nsToken)) {
564
+ return rawInput;
565
+ }
566
+ return `${nsToken}.${rawInput}`;
567
+ }
568
+ #renderGhost() {
569
+ if (this.ghost) {
570
+ this.ghost.style.display = "none";
571
+ }
572
+ }
573
+ #positionDropdown() {
574
+ if (!this.dropdown || !this.root) return;
575
+ const wrapper = this.root.querySelector(".twm-autocomplete-field__input-wrapper");
576
+ if (!wrapper) return;
577
+ const rect = wrapper.getBoundingClientRect();
578
+ this._lastDropdownRect = rect;
579
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
580
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
581
+ const width = Math.min(rect.width, viewportWidth - rect.left - 8);
582
+ const maxHeight = Math.max(120, viewportHeight - rect.bottom - 12);
583
+ Object.assign(this.dropdown.style, {
584
+ position: "fixed",
585
+ top: `${rect.bottom}px`,
586
+ left: `${rect.left}px`,
587
+ width: `${width}px`,
588
+ maxWidth: `${width}px`,
589
+ maxHeight: `${maxHeight}px`
590
+ });
591
+ }
592
+ dispose() {
593
+ this._suggestSeq += 1;
594
+ this.#close();
595
+ this._disposers.forEach((fn) => {
596
+ try {
597
+ fn();
598
+ } catch (e) {
599
+ }
600
+ });
601
+ this._disposers = [];
602
+ if (this.dropdown && this.dropdown.parentNode) {
603
+ this.dropdown.parentNode.removeChild(this.dropdown);
604
+ }
605
+ this.root = null;
606
+ this.input = null;
607
+ this.dropdown = null;
608
+ this.helperEl = null;
609
+ }
610
+ };
611
+
6
612
  // src/ui/components/cell_editor_host.js
7
613
  function createTextEditor({ value, initialText = null }) {
8
614
  const input = document.createElement("input");
@@ -33,13 +639,29 @@ function normalise(made) {
33
639
  const own = isNode ? {} : made;
34
640
  let getValue = typeof own.getValue === "function" ? own.getValue : null;
35
641
  if (!getValue && el.matches?.("input, select, textarea")) getValue = () => el.value;
642
+ const parse = typeof own.parse === "function" ? own.parse : null;
643
+ let read = null;
644
+ if (parse) {
645
+ read = () => {
646
+ let r;
647
+ try {
648
+ r = parse();
649
+ } catch (err) {
650
+ r = { ok: false, error: err?.message || String(err) };
651
+ }
652
+ return r && r.ok === false ? { ok: false, error: String(r.error ?? "Not valid") } : { ok: true, value: r ? r.value : void 0 };
653
+ };
654
+ } else if (getValue) read = () => ({ ok: true, value: getValue() });
36
655
  return {
37
656
  el,
38
657
  getValue,
658
+ read,
659
+ multiline: !!own.multiline,
39
660
  focus: typeof own.focus === "function" ? own.focus : null,
40
661
  destroy: typeof own.destroy === "function" ? own.destroy : null
41
662
  };
42
663
  }
664
+ var MOVES = /* @__PURE__ */ new Set(["down", "up", "right", "left"]);
43
665
  var CellEditorHost = class {
44
666
  /**
45
667
  * @param {object} o
@@ -47,17 +669,22 @@ var CellEditorHost = class {
47
669
  * cell is drawn as it was again; `move` is 'down', 'up', 'right', 'left'
48
670
  * or null; `reason` is 'key', 'blur', 'editor' or 'scroll'.
49
671
  * @param {(session, reason) => void} o.onCancel
672
+ * @param {(session, error, text, reason) => void} [o.onRefuse] a value
673
+ * that did not parse, when the editor closed with it (the focus left,
674
+ * `commit()` from outside); without it such a close is a cancel.
50
675
  */
51
- constructor({ onCommit, onCancel }) {
676
+ constructor({ onCommit, onCancel, onRefuse = null }) {
52
677
  this._onCommit = onCommit;
53
678
  this._onCancel = onCancel;
679
+ this._onRefuse = onRefuse;
54
680
  this.session = null;
55
681
  }
56
682
  /**
57
- * Open an editor in `td`. Returns whether one opened (a factory that
683
+ * Open an editor in `td`; `args` go to the factory beside the cell's own
684
+ * (DataTable's `locale`, `utc`, `messages`). Returns whether one opened (a factory that
58
685
  * throws or returns nothing opens none, and the cell is untouched).
59
686
  */
60
- open({ td, factory, value, row, rowIdx, colIdx, column, initialText = null, ...extra }) {
687
+ open({ td, factory, value, row, rowIdx, colIdx, column, initialText = null, args = {}, ...extra }) {
61
688
  if (this.session) return false;
62
689
  const s = {
63
690
  ...extra,
@@ -76,13 +703,14 @@ var CellEditorHost = class {
76
703
  let made = null;
77
704
  try {
78
705
  made = factory({
706
+ ...args,
79
707
  value,
80
708
  row,
81
709
  rowIdx,
82
710
  colIdx,
83
711
  column,
84
712
  initialText,
85
- commit: (v) => this._finish(s, v, null, "editor"),
713
+ commit: (v, move = null) => this._finish(s, v, MOVES.has(move) ? move : null, "editor"),
86
714
  cancel: () => this._cancel(s, "editor")
87
715
  });
88
716
  } catch (err) {
@@ -94,6 +722,10 @@ var CellEditorHost = class {
94
722
  s.editor = editor;
95
723
  s.saved = [...td.childNodes];
96
724
  s.savedTitle = td.getAttribute("title");
725
+ s.savedError = {
726
+ on: td.classList.contains("twm-dt-cell--error"),
727
+ message: td.getAttribute("data-error")
728
+ };
97
729
  this.session = s;
98
730
  td.classList.add("twm-dt-cell--editing");
99
731
  td.replaceChildren(editor.el);
@@ -107,24 +739,25 @@ var CellEditorHost = class {
107
739
  if (ev.key === "Escape") {
108
740
  ev.preventDefault();
109
741
  this._cancel(s, "key");
110
- } else if (ev.key === "Enter" && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.getValue) {
742
+ } else if (ev.key === "Enter" && !ev.altKey && editor.read && (editor.multiline ? ev.ctrlKey || ev.metaKey : !(ev.ctrlKey || ev.metaKey))) {
111
743
  ev.preventDefault();
112
- this._finish(s, editor.getValue(), ev.shiftKey ? "up" : "down", "key");
113
- } else if (ev.key === "Tab" && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.getValue) {
744
+ this._tryKey(s, ev.shiftKey ? "up" : "down");
745
+ } else if (ev.key === "Tab" && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.read) {
114
746
  ev.preventDefault();
115
- this._finish(s, editor.getValue(), ev.shiftKey ? "left" : "right", "key");
747
+ this._tryKey(s, ev.shiftKey ? "left" : "right");
116
748
  }
117
749
  });
750
+ on("input", () => this._unmark(s));
118
751
  for (const type of ["click", "dblclick", "mousedown", "contextmenu"]) {
119
752
  on(type, (ev) => ev.stopPropagation());
120
753
  }
121
754
  on("focusout", (ev) => {
122
- if (s.closed || !editor.getValue) return;
755
+ if (s.closed || !editor.read) return;
123
756
  const to = ev.relatedTarget;
124
757
  if (to && editor.el.contains(to)) return;
125
758
  const doc = editor.el.ownerDocument;
126
759
  if (!to && doc && typeof doc.hasFocus === "function" && !doc.hasFocus()) return;
127
- this._finish(s, editor.getValue(), null, "blur");
760
+ this._settle(s, null, "blur");
128
761
  });
129
762
  try {
130
763
  if (editor.focus) editor.focus();
@@ -137,11 +770,51 @@ var CellEditorHost = class {
137
770
  commit(move = null, reason = "editor") {
138
771
  const s = this.session;
139
772
  if (!s) return false;
140
- if (!s.editor.getValue) {
773
+ if (!s.editor.read) {
141
774
  this._cancel(s, reason);
142
775
  return false;
143
776
  }
144
- return this._finish(s, s.editor.getValue(), move, reason);
777
+ return this._settle(s, move, reason);
778
+ }
779
+ /** Enter or Tab: commit what parses; a refusal keeps the editor open,
780
+ * the message on its cell. */
781
+ _tryKey(s, move) {
782
+ const r = s.editor.read();
783
+ if (r.ok) return this._finish(s, r.value, move, "key");
784
+ s.td.classList.add("twm-dt-cell--error");
785
+ s.td.setAttribute("data-error", r.error);
786
+ s.td.setAttribute("title", r.error);
787
+ s.marked = true;
788
+ return false;
789
+ }
790
+ _unmark(s) {
791
+ if (!s.marked) return;
792
+ s.marked = false;
793
+ this._restoreMark(s);
794
+ }
795
+ _restoreMark(s) {
796
+ const { on, message } = s.savedError;
797
+ s.td.classList.toggle("twm-dt-cell--error", on);
798
+ if (message == null) s.td.removeAttribute("data-error");
799
+ else s.td.setAttribute("data-error", message);
800
+ if (s.savedTitle == null) s.td.removeAttribute("title");
801
+ else s.td.setAttribute("title", s.savedTitle);
802
+ }
803
+ /** The editor closing with what it holds (the focus left, `commit()`):
804
+ * committed when it parses, else handed to `onRefuse`. */
805
+ _settle(s, move, reason) {
806
+ const r = s.editor.read();
807
+ if (r.ok) return this._finish(s, r.value, move, reason);
808
+ if (s.closed) return false;
809
+ let text = "";
810
+ try {
811
+ text = s.editor.getValue ? s.editor.getValue() : "";
812
+ } catch (_) {
813
+ }
814
+ this._close(s);
815
+ if (this._onRefuse) this._onRefuse(s, r.error, text, reason);
816
+ else this._onCancel?.(s, reason);
817
+ return true;
145
818
  }
146
819
  /** Cancel the open session: the cell is drawn as it was. */
147
820
  cancel(reason = "editor") {
@@ -167,8 +840,7 @@ var CellEditorHost = class {
167
840
  }
168
841
  s.td.classList.remove("twm-dt-cell--editing");
169
842
  s.td.replaceChildren(...s.saved);
170
- if (s.savedTitle == null) s.td.removeAttribute("title");
171
- else s.td.setAttribute("title", s.savedTitle);
843
+ this._restoreMark(s);
172
844
  }
173
845
  _finish(s, value, move, reason) {
174
846
  if (s.closed) return false;
@@ -184,6 +856,363 @@ var CellEditorHost = class {
184
856
  }
185
857
  };
186
858
 
859
+ // src/ui/components/cell_editors.js
860
+ var EDITOR_MESSAGES = Object.freeze({
861
+ number: "Not a number",
862
+ datetime: "Not a date",
863
+ bool: "Not true or false",
864
+ choice: "Not one of the options"
865
+ });
866
+ var ok = (value) => ({ ok: true, value });
867
+ var refuse = (ctx, type) => ({ ok: false, error: (ctx?.messages || {})[type] || EDITOR_MESSAGES[type] });
868
+ var isBlank = (text) => text == null || String(text).trim() === "";
869
+ var ALIASES = { num: "number", date: "datetime" };
870
+ function editorTypeOf(type) {
871
+ const t = type == null ? "text" : String(type);
872
+ return ALIASES[t] || t;
873
+ }
874
+ function safeLocale(locale) {
875
+ if (!locale) return void 0;
876
+ try {
877
+ return Intl.NumberFormat.supportedLocalesOf(locale).length ? locale : void 0;
878
+ } catch (_) {
879
+ return void 0;
880
+ }
881
+ }
882
+ function numberSeparators(locale) {
883
+ const parts = new Intl.NumberFormat(safeLocale(locale)).formatToParts(12345675e-1);
884
+ return {
885
+ group: parts.find((p) => p.type === "group")?.value ?? ",",
886
+ decimal: parts.find((p) => p.type === "decimal")?.value ?? "."
887
+ };
888
+ }
889
+ var SPACES = /[\s\u00a0\u202f]/g;
890
+ function parseNumber(text, ctx = {}) {
891
+ if (isBlank(text)) return ok(null);
892
+ const { group, decimal } = numberSeparators(ctx.locale);
893
+ let s = String(text).trim().replace(/\u2212/g, "-");
894
+ let sign = "";
895
+ if (s[0] === "-" || s[0] === "+") {
896
+ sign = s[0] === "-" ? "-" : "";
897
+ s = s.slice(1);
898
+ }
899
+ const halves = s.split(decimal);
900
+ if (halves.length > 2) return refuse(ctx, "number");
901
+ let [whole, frac = null] = halves;
902
+ let exp = "";
903
+ const tail = frac ?? whole;
904
+ const e = /[eE][+-]?\d+$/.exec(tail);
905
+ if (e) {
906
+ exp = e[0];
907
+ if (frac != null) frac = frac.slice(0, -exp.length);
908
+ else whole = whole.slice(0, -exp.length);
909
+ }
910
+ const spaceGroup = group !== "" && group.replace(SPACES, "") === "";
911
+ const g = spaceGroup ? " " : group;
912
+ if (spaceGroup) whole = whole.replace(SPACES, " ");
913
+ if (g && whole.includes(g)) {
914
+ const esc = g.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
915
+ if (!new RegExp(`^\\d{1,3}(${esc}\\d{3})+$`).test(whole)) return refuse(ctx, "number");
916
+ whole = whole.split(g).join("");
917
+ }
918
+ if (!/^\d*$/.test(whole) || frac != null && !/^\d*$/.test(frac)) return refuse(ctx, "number");
919
+ if (!whole && !frac) return refuse(ctx, "number");
920
+ const n = Number(`${sign}${whole || "0"}.${frac || "0"}${exp}`);
921
+ return Number.isFinite(n) ? ok(n) : refuse(ctx, "number");
922
+ }
923
+ function formatNumberForEdit(value, ctx = {}) {
924
+ if (value == null) return "";
925
+ if (typeof value !== "number" || !Number.isFinite(value)) return String(value);
926
+ const { decimal } = numberSeparators(ctx.locale);
927
+ return String(value).replace(".", decimal);
928
+ }
929
+ var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "on", "x", "\u2713", "\u2714"]);
930
+ var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "off", ""]);
931
+ function parseBool(text, ctx = {}) {
932
+ if (typeof text === "boolean") return ok(text);
933
+ const t = String(text ?? "").trim().toLowerCase();
934
+ if (TRUE_WORDS.has(t)) return ok(true);
935
+ if (FALSE_WORDS.has(t)) return ok(false);
936
+ return refuse(ctx, "bool");
937
+ }
938
+ function truthy(value) {
939
+ if (typeof value === "boolean") return value;
940
+ if (value == null) return false;
941
+ const r = parseBool(String(value));
942
+ return r.ok ? r.value : true;
943
+ }
944
+ function dateOrder(locale) {
945
+ let loc;
946
+ try {
947
+ loc = Intl.DateTimeFormat.supportedLocalesOf(locale || []).length ? locale : void 0;
948
+ } catch (_) {
949
+ loc = void 0;
950
+ }
951
+ return new Intl.DateTimeFormat(loc, { year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date(2026, 11, 31)).map((p) => p.type).filter((t) => t === "day" || t === "month" || t === "year");
952
+ }
953
+ function daysIn(y, m) {
954
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
955
+ }
956
+ function makeDate({ y, mo, d, h = 0, mi = 0, s = 0, ms = 0 }, utc, offsetMin = null) {
957
+ if (!(mo >= 1 && mo <= 12) || !(d >= 1 && d <= daysIn(y, mo))) return null;
958
+ if (h > 23 || mi > 59 || s > 59) return null;
959
+ if (offsetMin != null) return new Date(Date.UTC(y, mo - 1, d, h, mi, s, ms) - offsetMin * 6e4);
960
+ if (utc) return new Date(Date.UTC(y, mo - 1, d, h, mi, s, ms));
961
+ const date = new Date(y, mo - 1, d, h, mi, s, ms);
962
+ date.setFullYear(y);
963
+ return date;
964
+ }
965
+ var ISO = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,3})\d*)?)?\s*(Z|[+-]\d{2}:?\d{2})?)?$/i;
966
+ var LOCAL = /^(\d{1,4})[./-](\d{1,2})[./-](\d{1,4})\.?(?:[,\sT]+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*([ap])?\.?m?\.?)?$/i;
967
+ function parseDateTime(text, ctx = {}) {
968
+ if (isBlank(text)) return ok(null);
969
+ const t = String(text).trim();
970
+ const iso = ISO.exec(t);
971
+ if (iso) {
972
+ const [, y, mo, d, h2, mi2, s, ms, zone] = iso;
973
+ let offset = null;
974
+ if (zone) {
975
+ if (zone.toUpperCase() === "Z") offset = 0;
976
+ else {
977
+ const z = zone.replace(":", "");
978
+ offset = (z[0] === "-" ? -1 : 1) * (Number(z.slice(1, 3)) * 60 + Number(z.slice(3, 5)));
979
+ }
980
+ }
981
+ const date2 = makeDate({
982
+ y: +y,
983
+ mo: +mo,
984
+ d: +d,
985
+ h: +(h2 || 0),
986
+ mi: +(mi2 || 0),
987
+ s: +(s || 0),
988
+ ms: ms ? Number(ms.padEnd(3, "0")) : 0
989
+ }, !!ctx.utc, offset);
990
+ return date2 ? ok(date2) : refuse(ctx, "datetime");
991
+ }
992
+ const m = LOCAL.exec(t);
993
+ if (!m) return refuse(ctx, "datetime");
994
+ const [, a, b, c, hh, mi, ss, ap] = m;
995
+ let h = hh == null ? 0 : +hh;
996
+ if (ap) {
997
+ if (h < 1 || h > 12) return refuse(ctx, "datetime");
998
+ h = h % 12 + (ap.toLowerCase() === "p" ? 12 : 0);
999
+ }
1000
+ const time = { h, mi: +(mi || 0), s: +(ss || 0) };
1001
+ const order = dateOrder(ctx.locale);
1002
+ const nums = [a, b, c];
1003
+ const read = (ord) => {
1004
+ const at = {};
1005
+ ord.forEach((type, i) => {
1006
+ at[type] = nums[i];
1007
+ });
1008
+ if (!at.year || at.year.length !== 4) return null;
1009
+ return makeDate({ y: +at.year, mo: +at.month, d: +at.day, ...time }, !!ctx.utc);
1010
+ };
1011
+ let date = read(order);
1012
+ if (!date) {
1013
+ const swapped = order.map((p) => p === "day" ? "month" : p === "month" ? "day" : p);
1014
+ date = read(swapped);
1015
+ }
1016
+ return date ? ok(date) : refuse(ctx, "datetime");
1017
+ }
1018
+ var pad = (n, w = 2) => String(n).padStart(w, "0");
1019
+ function formatDateTimeForEdit(value, ctx = {}) {
1020
+ if (value == null || value === "") return "";
1021
+ if (!(value instanceof Date)) return String(value);
1022
+ if (Number.isNaN(value.getTime())) return "";
1023
+ const u = !!ctx.utc;
1024
+ const y = u ? value.getUTCFullYear() : value.getFullYear();
1025
+ const mo = (u ? value.getUTCMonth() : value.getMonth()) + 1;
1026
+ const d = u ? value.getUTCDate() : value.getDate();
1027
+ const h = u ? value.getUTCHours() : value.getHours();
1028
+ const mi = u ? value.getUTCMinutes() : value.getMinutes();
1029
+ const s = u ? value.getUTCSeconds() : value.getSeconds();
1030
+ const day = `${pad(y, 4)}-${pad(mo)}-${pad(d)}`;
1031
+ if (!h && !mi && !s) return day;
1032
+ return `${day} ${pad(h)}:${pad(mi)}${s ? `:${pad(s)}` : ""}`;
1033
+ }
1034
+ function normalizeOptions(options) {
1035
+ if (!Array.isArray(options)) return [];
1036
+ return options.map((o) => o && typeof o === "object" ? { value: o.value, label: String(o.label ?? o.value ?? "") } : { value: o, label: String(o ?? "") });
1037
+ }
1038
+ function parseChoice(text, ctx = {}) {
1039
+ if (isBlank(text)) return ok(null);
1040
+ const t = String(text).trim();
1041
+ const lower = t.toLowerCase();
1042
+ const opts = normalizeOptions(ctx.options);
1043
+ const hit = opts.find((o) => o.label.trim().toLowerCase() === lower) || opts.find((o) => String(o.value ?? "").trim().toLowerCase() === lower);
1044
+ if (hit) return ok(hit.value);
1045
+ return ctx.allowOther ? ok(t) : refuse(ctx, "choice");
1046
+ }
1047
+ function labelOf(value, ctx) {
1048
+ if (value == null) return "";
1049
+ const hit = normalizeOptions(ctx.options).find((o) => Object.is(o.value, value) || String(o.value) === String(value));
1050
+ return hit ? hit.label : String(value);
1051
+ }
1052
+ function contextOf(args) {
1053
+ const col = args.column || {};
1054
+ return {
1055
+ locale: args.locale,
1056
+ utc: !!args.utc,
1057
+ messages: args.messages,
1058
+ options: col.options,
1059
+ allowOther: !!col.allowOther
1060
+ };
1061
+ }
1062
+ function parsedField(el, args, start, parse) {
1063
+ const { value, initialText = null } = args;
1064
+ el.value = initialText != null ? String(initialText) : start;
1065
+ const ctx = contextOf(args);
1066
+ return {
1067
+ el,
1068
+ getValue: () => el.value,
1069
+ parse: () => initialText == null && el.value === start ? ok(value) : parse(el.value, ctx),
1070
+ focus: () => {
1071
+ el.focus({ preventScroll: true });
1072
+ const end = el.value.length;
1073
+ try {
1074
+ el.setSelectionRange(end, end);
1075
+ } catch (_) {
1076
+ }
1077
+ }
1078
+ };
1079
+ }
1080
+ function textInput(kind) {
1081
+ const input = document.createElement("input");
1082
+ input.type = "text";
1083
+ input.className = `twm-dt-editor twm-dt-editor--${kind}`;
1084
+ input.spellcheck = false;
1085
+ input.setAttribute("autocomplete", "off");
1086
+ return input;
1087
+ }
1088
+ function createNumberEditor(args) {
1089
+ const input = textInput("number");
1090
+ input.inputMode = "decimal";
1091
+ return parsedField(input, args, formatNumberForEdit(args.value, contextOf(args)), parseNumber);
1092
+ }
1093
+ function createDateTimeEditor(args) {
1094
+ const input = textInput("datetime");
1095
+ return parsedField(input, args, formatDateTimeForEdit(args.value, contextOf(args)), parseDateTime);
1096
+ }
1097
+ function createMultiTextEditor(args) {
1098
+ const area = document.createElement("textarea");
1099
+ area.className = "twm-dt-editor twm-dt-editor--multitext";
1100
+ area.spellcheck = false;
1101
+ area.rows = Number(args.column?.rows) > 0 ? Number(args.column.rows) : 3;
1102
+ const start = args.value == null ? "" : String(args.value);
1103
+ const field = parsedField(area, args, start, (text) => ok(text));
1104
+ return { ...field, multiline: true };
1105
+ }
1106
+ function createBoolEditor(args) {
1107
+ const { value, initialText = null, commit } = args;
1108
+ const box = document.createElement("input");
1109
+ box.type = "checkbox";
1110
+ box.className = "twm-dt-editor twm-dt-editor--bool";
1111
+ const start = truthy(value);
1112
+ box.checked = start;
1113
+ if (initialText === " ") box.checked = !start;
1114
+ else if (initialText != null) {
1115
+ const r = parseBool(initialText);
1116
+ if (r.ok) box.checked = r.value;
1117
+ }
1118
+ box.addEventListener("change", () => commit(box.checked));
1119
+ const read = () => box.checked === start && initialText == null ? ok(value) : ok(box.checked);
1120
+ return {
1121
+ el: box,
1122
+ getValue: () => box.checked,
1123
+ parse: read,
1124
+ focus: () => box.focus({ preventScroll: true })
1125
+ };
1126
+ }
1127
+ var uid = 0;
1128
+ function createChoiceEditor(args) {
1129
+ const { value, initialText = null, commit } = args;
1130
+ const ctx = contextOf(args);
1131
+ const opts = normalizeOptions(ctx.options);
1132
+ const start = labelOf(value, ctx);
1133
+ const shown = initialText != null ? String(initialText) : start;
1134
+ let picked = null;
1135
+ let keying = null;
1136
+ let done = false;
1137
+ const ac = new AutocompleteField({
1138
+ fieldId: `twm-dt-choice-${++uid}`,
1139
+ field: { value: shown },
1140
+ skipNamespacePrefix: true,
1141
+ typeIcons: { option: "list" },
1142
+ provider: ({ value: term }) => {
1143
+ const q = String(term ?? "").trim().toLowerCase();
1144
+ const all = !q || initialText == null && term === start;
1145
+ return opts.filter((o) => all || o.label.toLowerCase().includes(q)).map((o) => ({ label: o.label, insertText: o.label, iconType: "option", metadata: { value: o.value } }));
1146
+ },
1147
+ onChange: (text, meta) => {
1148
+ if (done || !meta?.immediate || !meta.metadata) return;
1149
+ picked = { label: String(text ?? ""), value: meta.metadata.value };
1150
+ const move = keying ? keying.key === "Tab" ? keying.shift ? "left" : "right" : keying.shift ? "up" : "down" : null;
1151
+ commit(picked.value, move);
1152
+ }
1153
+ });
1154
+ const root = ac.render();
1155
+ root.classList.add("twm-dt-editor--choice");
1156
+ const input = ac.input;
1157
+ input.classList.add("twm-dt-editor");
1158
+ let browsed = false;
1159
+ const onCapture = (ev) => {
1160
+ const commitKey = (ev.key === "Enter" || ev.key === "Tab") && !ev.ctrlKey && !ev.metaKey && !ev.altKey;
1161
+ keying = commitKey ? { key: ev.key, shift: ev.shiftKey } : null;
1162
+ if (ev.key === "ArrowDown" || ev.key === "ArrowUp") browsed = true;
1163
+ if (commitKey && !browsed && !String(input.value).trim() && !ev.isComposing) {
1164
+ ev.preventDefault();
1165
+ ev.stopPropagation();
1166
+ const move = ev.key === "Tab" ? ev.shiftKey ? "left" : "right" : ev.shiftKey ? "up" : "down";
1167
+ commit(null, move);
1168
+ }
1169
+ };
1170
+ const onInput = () => {
1171
+ browsed = false;
1172
+ };
1173
+ const onAfter = (ev) => {
1174
+ keying = null;
1175
+ if (ev.key === "Escape" && ev.defaultPrevented) ev.stopPropagation();
1176
+ };
1177
+ root.addEventListener("keydown", onCapture, true);
1178
+ input.addEventListener("keydown", onAfter);
1179
+ input.addEventListener("input", onInput);
1180
+ return {
1181
+ el: root,
1182
+ getValue: () => ac.input?.value ?? "",
1183
+ parse: () => {
1184
+ const text = ac.input?.value ?? "";
1185
+ if (initialText == null && text === start) return ok(value);
1186
+ if (picked && text === picked.label) return ok(picked.value);
1187
+ return parseChoice(text, ctx);
1188
+ },
1189
+ focus: () => {
1190
+ ac.input?.focus({ preventScroll: true });
1191
+ const end = ac.input?.value.length ?? 0;
1192
+ try {
1193
+ ac.input?.setSelectionRange(end, end);
1194
+ } catch (_) {
1195
+ }
1196
+ },
1197
+ destroy: () => {
1198
+ done = true;
1199
+ root.removeEventListener("keydown", onCapture, true);
1200
+ input.removeEventListener("keydown", onAfter);
1201
+ input.removeEventListener("input", onInput);
1202
+ ac.dispose();
1203
+ }
1204
+ };
1205
+ }
1206
+ var identity = (text) => ok(text == null ? "" : String(text));
1207
+ var CELL_EDITORS = Object.freeze({
1208
+ text: Object.freeze({ create: createTextEditor, parse: identity }),
1209
+ multitext: Object.freeze({ create: createMultiTextEditor, parse: identity }),
1210
+ number: Object.freeze({ create: createNumberEditor, parse: parseNumber }),
1211
+ bool: Object.freeze({ create: createBoolEditor, parse: parseBool }),
1212
+ choice: Object.freeze({ create: createChoiceEditor, parse: parseChoice }),
1213
+ datetime: Object.freeze({ create: createDateTimeEditor, parse: parseDateTime })
1214
+ });
1215
+
187
1216
  // src/ui/components/cell_selection.js
188
1217
  var CellSelection = class {
189
1218
  constructor() {
@@ -406,7 +1435,7 @@ var WEEKDAYS = [
406
1435
  var ISO_DATE = "YYYY-MM-DD";
407
1436
  var ISO_DATETIME = "YYYY-MM-DD HH:mm";
408
1437
  var ISO_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?)?\s*(Z|[+-]\d{2}(?::?\d{2})?)?$/i;
409
- var pad = (n, w = 2) => String(Math.trunc(Math.abs(n))).padStart(w, "0");
1438
+ var pad2 = (n, w = 2) => String(Math.trunc(Math.abs(n))).padStart(w, "0");
410
1439
  function daysInMonth(y, mo) {
411
1440
  if (mo !== 1) return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo];
412
1441
  return y % 4 === 0 && y % 100 !== 0 || y % 400 === 0 ? 29 : 28;
@@ -483,15 +1512,15 @@ function formatDate(value, pattern = ISO_DATE, { utc = false } = {}) {
483
1512
  if (literal !== void 0) return literal;
484
1513
  switch (tok) {
485
1514
  case "YYYY":
486
- return pad(get.y, 4);
1515
+ return pad2(get.y, 4);
487
1516
  case "YY":
488
- return pad(get.y % 100);
1517
+ return pad2(get.y % 100);
489
1518
  case "MMMM":
490
1519
  return MONTHS[get.mo];
491
1520
  case "MMM":
492
1521
  return MONTHS[get.mo].slice(0, 3);
493
1522
  case "MM":
494
- return pad(get.mo + 1);
1523
+ return pad2(get.mo + 1);
495
1524
  case "M":
496
1525
  return String(get.mo + 1);
497
1526
  case "dddd":
@@ -499,27 +1528,27 @@ function formatDate(value, pattern = ISO_DATE, { utc = false } = {}) {
499
1528
  case "ddd":
500
1529
  return WEEKDAYS[get.wd].slice(0, 3);
501
1530
  case "DD":
502
- return pad(get.d);
1531
+ return pad2(get.d);
503
1532
  case "D":
504
1533
  return String(get.d);
505
1534
  case "HH":
506
- return pad(get.h);
1535
+ return pad2(get.h);
507
1536
  case "H":
508
1537
  return String(get.h);
509
1538
  case "hh":
510
- return pad(h12);
1539
+ return pad2(h12);
511
1540
  case "h":
512
1541
  return String(h12);
513
1542
  case "mm":
514
- return pad(get.mi);
1543
+ return pad2(get.mi);
515
1544
  case "m":
516
1545
  return String(get.mi);
517
1546
  case "ss":
518
- return pad(get.s);
1547
+ return pad2(get.s);
519
1548
  case "s":
520
1549
  return String(get.s);
521
1550
  case "SSS":
522
- return pad(get.ms, 3);
1551
+ return pad2(get.ms, 3);
523
1552
  case "A":
524
1553
  return get.h < 12 ? "AM" : "PM";
525
1554
  case "a":
@@ -620,7 +1649,7 @@ function formatDuration(value, pattern = "auto") {
620
1649
  const h = Math.floor(total % 86400 / 3600);
621
1650
  const mi = Math.floor(total % 3600 / 60);
622
1651
  const s = total % 60;
623
- out = `${d ? `${d}.${pad(h)}` : h}:${pad(mi)}:${pad(s)}`;
1652
+ out = `${d ? `${d}.${pad2(h)}` : h}:${pad2(mi)}:${pad2(s)}`;
624
1653
  } else if (a < 1e3) {
625
1654
  out = `${Math.round(a)} ms`;
626
1655
  } else if (a < 59950) {
@@ -744,12 +1773,12 @@ function createChipsFilter(ctx = {}, spec = {}) {
744
1773
  };
745
1774
  const chosen = () => multiple ? value.length === 0 : value === null;
746
1775
  for (const opt of options) {
747
- const key3 = keyOf(opt.value);
748
- valueOf.set(key3, opt.value);
1776
+ const key4 = keyOf(opt.value);
1777
+ valueOf.set(key4, opt.value);
749
1778
  const btn = document.createElement("button");
750
1779
  btn.type = "button";
751
1780
  btn.className = "twm-dt-filter-chip";
752
- btn.dataset.value = key3;
1781
+ btn.dataset.value = key4;
753
1782
  if (opt.title) btn.title = String(opt.title);
754
1783
  if (opt.icon) {
755
1784
  const icon = document.createElement("span");
@@ -760,7 +1789,7 @@ function createChipsFilter(ctx = {}, spec = {}) {
760
1789
  }
761
1790
  const label = document.createElement("span");
762
1791
  label.className = "twm-dt-filter-chip__label";
763
- label.textContent = String(opt.label ?? key3);
1792
+ label.textContent = String(opt.label ?? key4);
764
1793
  btn.appendChild(label);
765
1794
  let count = null;
766
1795
  if (showCounts) {
@@ -771,21 +1800,21 @@ function createChipsFilter(ctx = {}, spec = {}) {
771
1800
  }
772
1801
  btn.addEventListener("click", (ev) => {
773
1802
  ev.stopPropagation();
774
- pick(key3);
1803
+ pick(key4);
775
1804
  });
776
- chips.set(key3, { btn, count });
1805
+ chips.set(key4, { btn, count });
777
1806
  el.appendChild(btn);
778
1807
  }
779
1808
  function outward() {
780
1809
  if (multiple) return value.length ? value.map((k) => valueOf.get(k)) : null;
781
1810
  return value === null ? null : valueOf.get(value);
782
1811
  }
783
- function pick(key3) {
784
- if (isAll(key3)) value = multiple ? [] : null;
1812
+ function pick(key4) {
1813
+ if (isAll(key4)) value = multiple ? [] : null;
785
1814
  else if (multiple) {
786
- value = value.includes(key3) ? value.filter((k) => k !== key3) : [...value, key3];
1815
+ value = value.includes(key4) ? value.filter((k) => k !== key4) : [...value, key4];
787
1816
  } else {
788
- value = value === key3 ? null : key3;
1817
+ value = value === key4 ? null : key4;
789
1818
  }
790
1819
  paint();
791
1820
  const out = outward();
@@ -797,8 +1826,8 @@ function createChipsFilter(ctx = {}, spec = {}) {
797
1826
  }
798
1827
  }
799
1828
  function paint() {
800
- for (const [key3, { btn }] of chips) {
801
- const on = isAll(key3) ? chosen() : multiple ? value.includes(key3) : value === key3;
1829
+ for (const [key4, { btn }] of chips) {
1830
+ const on = isAll(key4) ? chosen() : multiple ? value.includes(key4) : value === key4;
802
1831
  btn.classList.toggle("twm-dt-filter-chip--on", on);
803
1832
  btn.setAttribute("aria-pressed", on ? "true" : "false");
804
1833
  }
@@ -808,8 +1837,8 @@ function createChipsFilter(ctx = {}, spec = {}) {
808
1837
  paint();
809
1838
  }
810
1839
  function setCounts(counts) {
811
- for (const [key3, { btn, count }] of chips) {
812
- const n = counts == null ? void 0 : counts[key3];
1840
+ for (const [key4, { btn, count }] of chips) {
1841
+ const n = counts == null ? void 0 : counts[key4];
813
1842
  const known = typeof n === "number" && Number.isFinite(n);
814
1843
  if (count) count.textContent = known ? n.toLocaleString() : "\u2013";
815
1844
  btn.classList.toggle("twm-dt-filter-chip--empty", known && n === 0);
@@ -966,10 +1995,10 @@ var ISO_DATE2 = /^\d{4}-\d{2}-\d{2}$/;
966
1995
  var TRAILING_NUMBER = /^(.*?)(\d+)(\D*)$/;
967
1996
  var NUMERIC_TEXT = /^[-+]?(\d+\.?\d*|\.\d+)$/;
968
1997
  var DAY_MS = 864e5;
969
- var isBlank = (v) => v === null || v === void 0 || v === "";
1998
+ var isBlank2 = (v) => v === null || v === void 0 || v === "";
970
1999
  function detectSeries(values) {
971
2000
  if (!Array.isArray(values) || values.length === 0) return null;
972
- if (values.every(isBlank)) return null;
2001
+ if (values.every(isBlank2)) return null;
973
2002
  if (values.length === 1) return { kind: "copy", step: 0 };
974
2003
  const numbers = values.map(toNumber);
975
2004
  if (numbers.every((n) => n !== null)) {
@@ -1071,6 +2100,53 @@ function nearlyEqual(a, b) {
1071
2100
  return Math.abs(a - b) < 1e-9;
1072
2101
  }
1073
2102
 
2103
+ // src/ui/components/tsv.js
2104
+ function tsvCell(value) {
2105
+ const text = value === null || value === void 0 ? "" : String(value);
2106
+ if (/[\t\n\r"]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
2107
+ return text;
2108
+ }
2109
+ function toTSV(matrix) {
2110
+ if (!Array.isArray(matrix)) return "";
2111
+ return matrix.map((row) => (Array.isArray(row) ? row : [row]).map(tsvCell).join(" ")).join("\n");
2112
+ }
2113
+ function fromTSV(text) {
2114
+ if (text === null || text === void 0 || text === "") return [];
2115
+ const src = String(text);
2116
+ const rows = [];
2117
+ let row = [];
2118
+ let cell = "";
2119
+ let quoted = false;
2120
+ for (let i = 0; i < src.length; i++) {
2121
+ const ch = src[i];
2122
+ if (quoted) {
2123
+ if (ch === '"' && src[i + 1] === '"') {
2124
+ cell += '"';
2125
+ i++;
2126
+ } else if (ch === '"') quoted = false;
2127
+ else cell += ch;
2128
+ } else if (ch === '"' && cell === "") quoted = true;
2129
+ else if (ch === " ") {
2130
+ row.push(cell);
2131
+ cell = "";
2132
+ } else if (ch === "\r") {
2133
+ } else if (ch === "\n") {
2134
+ row.push(cell);
2135
+ rows.push(row);
2136
+ row = [];
2137
+ cell = "";
2138
+ } else cell += ch;
2139
+ }
2140
+ row.push(cell);
2141
+ rows.push(row);
2142
+ while (rows.length > 1) {
2143
+ const last = rows[rows.length - 1];
2144
+ if (last.length === 1 && last[0] === "") rows.pop();
2145
+ else break;
2146
+ }
2147
+ return rows;
2148
+ }
2149
+
1074
2150
  // src/ui/components/inline_renamer.js
1075
2151
  function assertLabel(label) {
1076
2152
  if (!label || typeof label.appendChild !== "function") {
@@ -1240,7 +2316,12 @@ var StagedEdits = class {
1240
2316
  * Returns whether anything changed.
1241
2317
  */
1242
2318
  stage(rowIdx, colIdx, value) {
1243
- if (this.committing || !this._stageOne(rowIdx, colIdx, value)) return false;
2319
+ if (this.committing) return false;
2320
+ const refusal = this._dropRefusal(rowIdx, colIdx);
2321
+ if (!this._stageOne(rowIdx, colIdx, value)) {
2322
+ if (refusal) this.grid._redraw(rowIdx, colIdx);
2323
+ return false;
2324
+ }
1244
2325
  this.grid._redraw(rowIdx, colIdx);
1245
2326
  this._changed();
1246
2327
  return true;
@@ -1257,10 +2338,11 @@ var StagedEdits = class {
1257
2338
  this.history.beginGroup(label);
1258
2339
  try {
1259
2340
  for (const { rowIdx, colIdx, value } of cells) {
2341
+ const refusal = this._dropRefusal(rowIdx, colIdx);
1260
2342
  if (this._stageOne(rowIdx, colIdx, value)) {
1261
2343
  changed++;
1262
2344
  rows.add(rowIdx);
1263
- }
2345
+ } else if (refusal) rows.add(rowIdx);
1264
2346
  }
1265
2347
  } finally {
1266
2348
  this.history.endGroup();
@@ -1269,6 +2351,16 @@ var StagedEdits = class {
1269
2351
  if (changed) this._changed();
1270
2352
  return changed;
1271
2353
  }
2354
+ /** STORY-0087. A text that did not parse stays in its cell, marked, in
2355
+ * the grid's own marks; anything staged into the cell ends that refusal,
2356
+ * the value its row holds included. Whether there was one. */
2357
+ _dropRefusal(rowIdx, colIdx) {
2358
+ const k = key(rowIdx, colIdx);
2359
+ if (!this.grid.errors.has(k)) return false;
2360
+ this.grid.errors.delete(k);
2361
+ this.grid.latest.delete(k);
2362
+ return true;
2363
+ }
1272
2364
  /** One cell's new state and its delta in the ring; whether it changed. */
1273
2365
  _stageOne(rowIdx, colIdx, value) {
1274
2366
  const k = key(rowIdx, colIdx);
@@ -1762,6 +2854,210 @@ var FillHandle = class {
1762
2854
  }
1763
2855
  };
1764
2856
 
2857
+ // src/ui/components/cell_paste.js
2858
+ var key3 = (rowIdx, colIdx) => `${rowIdx}:${colIdx}`;
2859
+ var CellPaste = class {
2860
+ /** @param {import('./cell_grid.js').CellGrid} grid */
2861
+ constructor(grid) {
2862
+ this.grid = grid;
2863
+ this.skipped = /* @__PURE__ */ new Set();
2864
+ this.busy = false;
2865
+ }
2866
+ // ── painting (called by the grid for every cell it paints) ───────────
2867
+ paintCell(td, col) {
2868
+ const rowIdx = td.parentNode?.__rowIndex;
2869
+ td.classList.toggle("twm-dt-cell--paste-skipped", this.skipped.has(key3(rowIdx, col)));
2870
+ }
2871
+ /** The selection moved, or new rows came: the skipped marks are over. */
2872
+ clearMarks() {
2873
+ this.skipped.clear();
2874
+ }
2875
+ // ── reading a value ──────────────────────────────────────────────────
2876
+ /**
2877
+ * The parse a column's pasted text is read with: an injected editor's
2878
+ * own (`editors[k].parse`, looked up as the editor is), else the built-in
2879
+ * editor's for the column's type, else the text as it is.
2880
+ */
2881
+ _parserFor(col) {
2882
+ const g = this.grid;
2883
+ const editors = g._cfg().editors;
2884
+ if (editors && typeof editors === "object") {
2885
+ for (const k of [col, g._rawType(col), g._typeOf(col)]) {
2886
+ const f = editors[k];
2887
+ if (typeof f === "function") {
2888
+ if (typeof f.parse === "function") return f.parse;
2889
+ break;
2890
+ }
2891
+ }
2892
+ }
2893
+ const type = g._typeOf(col);
2894
+ return Object.prototype.hasOwnProperty.call(CELL_EDITORS, type) ? CELL_EDITORS[type].parse : CELL_EDITORS.text.parse;
2895
+ }
2896
+ /** One column's reader: `(text) => {ok: true, value} | {ok: false, error}`. */
2897
+ _reader(col) {
2898
+ const g = this.grid;
2899
+ const cfg = g._cfg();
2900
+ const spec = g._editSpec(col) || {};
2901
+ const parse = this._parserFor(col);
2902
+ const ctx = {
2903
+ locale: cfg.locale ?? void 0,
2904
+ utc: g.table._utc(),
2905
+ messages: cfg.editorMessages ?? void 0,
2906
+ options: spec.options,
2907
+ allowOther: !!spec.allowOther
2908
+ };
2909
+ return (text) => {
2910
+ let r;
2911
+ try {
2912
+ r = parse(text, ctx);
2913
+ } catch (err) {
2914
+ return { ok: false, error: err?.message || String(err ?? "Not pasted") };
2915
+ }
2916
+ if (r && r.ok === true) return { ok: true, value: r.value };
2917
+ if (r && r.ok === false) return { ok: false, error: String(r.error ?? "Not pasted") };
2918
+ return { ok: false, error: "Not pasted" };
2919
+ };
2920
+ }
2921
+ /** The position of the last loaded row: the rows held in the order shown
2922
+ * (all of them filtered and sorted; a server's page, the rows it sent). */
2923
+ _lastLoaded() {
2924
+ const t = this.grid.table;
2925
+ if (t._isServerSide()) return this.grid.span()[1];
2926
+ return t._getProcessedRows().length - 1;
2927
+ }
2928
+ _warn(message) {
2929
+ console.warn(`[DataTable] paste: ${message}`);
2930
+ this.grid.table._notify("Paste", message, "warn");
2931
+ }
2932
+ // ── the paste ────────────────────────────────────────────────────────
2933
+ /**
2934
+ * Paste tab-separated `text` at the cursor. Resolves `{rows, cols, staged,
2935
+ * skipped, refused, droppedColumns, appended}` — `staged` the NUMBER of
2936
+ * cells that changed, `skipped` the cells `editable` refused and `refused`
2937
+ * the ones whose text did not parse (`{row, col, rowIdx}`, `refused` with
2938
+ * its `text` and `error`), `droppedColumns` the columns past the last one,
2939
+ * `appended` the rows handed to `onRowsAppend` — or null when nothing was
2940
+ * pasted: empty text, no cursor, an editor open, a commit or another paste
2941
+ * out, an overrun declined, or new rows while `onPasteOverrun` was asked.
2942
+ */
2943
+ async paste(text) {
2944
+ const g = this.grid;
2945
+ const matrix = fromTSV(text);
2946
+ if (!matrix.length || this.busy) return null;
2947
+ g._sync();
2948
+ const at = g.sel.focus ? { ...g.sel.focus } : null;
2949
+ if (!at || g.isEditing() || g.staged.committing) return null;
2950
+ const cfg = g._cfg();
2951
+ const cols = g.columns();
2952
+ const width = Math.max(...matrix.map((line) => line.length));
2953
+ const targetCols = cols.slice(cols.indexOf(at.col), cols.indexOf(at.col) + width);
2954
+ const droppedColumns = width - targetCols.length;
2955
+ const last = this._lastLoaded();
2956
+ const fit = Math.max(0, Math.min(matrix.length, last - at.row + 1));
2957
+ const extra = matrix.length - fit;
2958
+ const generation = g.generation;
2959
+ if (extra > 0) {
2960
+ const ask = cfg.onPasteOverrun;
2961
+ let answer = false;
2962
+ if (typeof ask === "function") {
2963
+ this.busy = true;
2964
+ try {
2965
+ answer = await ask({ rows: extra });
2966
+ } catch (err) {
2967
+ console.error("[DataTable] onPasteOverrun threw", err);
2968
+ answer = false;
2969
+ } finally {
2970
+ this.busy = false;
2971
+ }
2972
+ } else {
2973
+ console.warn(`[DataTable] paste: ${extra} row(s) past the last loaded row and no \`onPasteOverrun\`; nothing pasted.`);
2974
+ }
2975
+ if (answer !== true) return null;
2976
+ if (generation !== g.generation || g.isEditing() || g.staged.committing) return null;
2977
+ }
2978
+ const cells = [];
2979
+ const skipped = [];
2980
+ const refused = [];
2981
+ const readers = new Map(targetCols.map((col) => [col, this._reader(col)]));
2982
+ for (let r = 0; r < fit; r++) {
2983
+ const pos = at.row + r;
2984
+ const rowIdx = g.rowIndexAt(pos);
2985
+ targetCols.forEach((col, c) => {
2986
+ if (c >= matrix[r].length) return;
2987
+ if (!g.canEdit(pos, col)) {
2988
+ skipped.push({ row: pos, col, rowIdx });
2989
+ return;
2990
+ }
2991
+ const text2 = matrix[r][c];
2992
+ const read = readers.get(col)(text2);
2993
+ if (read.ok) cells.push({ rowIdx, colIdx: col, value: read.value });
2994
+ else refused.push({ row: pos, col, rowIdx, text: text2, error: read.error });
2995
+ });
2996
+ }
2997
+ let appended = 0;
2998
+ if (extra > 0) {
2999
+ const n = (cfg.headers || []).length;
3000
+ const rows = [];
3001
+ const errors = [];
3002
+ for (let r = fit; r < matrix.length; r++) {
3003
+ const row = new Array(n).fill(null);
3004
+ targetCols.forEach((col, c) => {
3005
+ if (c >= matrix[r].length) return;
3006
+ const text2 = matrix[r][c];
3007
+ const read = readers.get(col)(text2);
3008
+ if (read.ok) row[col] = read.value;
3009
+ else errors.push({ row: rows.length, colIdx: col, text: text2, error: read.error });
3010
+ });
3011
+ rows.push(row);
3012
+ }
3013
+ const append = cfg.onRowsAppend;
3014
+ if (typeof append === "function") {
3015
+ appended = rows.length;
3016
+ this.busy = true;
3017
+ try {
3018
+ await append({ rows, errors });
3019
+ } catch (err) {
3020
+ console.error("[DataTable] onRowsAppend threw", err);
3021
+ } finally {
3022
+ this.busy = false;
3023
+ }
3024
+ } else {
3025
+ this._warn(`${rows.length} row(s) past the last loaded row were not pasted (no \`onRowsAppend\`).`);
3026
+ }
3027
+ }
3028
+ const staged = g.staged.stageMany(cells, `paste ${cells.length} cell(s)`);
3029
+ const lastCol = targetCols[targetCols.length - 1] ?? at.col;
3030
+ const lastRow = at.row + Math.max(0, fit - 1);
3031
+ g.set({ row: at.row, col: at.col }, { row: lastRow, col: lastCol });
3032
+ for (const s of skipped) this.skipped.add(key3(s.rowIdx, s.col));
3033
+ const redraw = /* @__PURE__ */ new Set();
3034
+ for (const f of refused) {
3035
+ const k = key3(f.rowIdx, f.col);
3036
+ g.errors.set(k, { value: f.text, message: f.error });
3037
+ g.latest.set(k, {});
3038
+ g.pending.delete(k);
3039
+ redraw.add(f.rowIdx);
3040
+ }
3041
+ for (const rowIdx of redraw) g._redraw(rowIdx);
3042
+ if (skipped.length) g.paint();
3043
+ if (droppedColumns > 0) {
3044
+ this._warn(`${droppedColumns} column(s) past the last column were not pasted.`);
3045
+ }
3046
+ return {
3047
+ rows: matrix.length,
3048
+ cols: width,
3049
+ staged,
3050
+ skipped,
3051
+ refused,
3052
+ droppedColumns,
3053
+ appended
3054
+ };
3055
+ }
3056
+ dispose() {
3057
+ this.skipped.clear();
3058
+ }
3059
+ };
3060
+
1765
3061
  // src/ui/components/cell_grid.js
1766
3062
  function isFilledValue(value) {
1767
3063
  if (value == null) return false;
@@ -1778,6 +3074,16 @@ var ARROWS = {
1778
3074
  ArrowLeft: [0, -1],
1779
3075
  ArrowRight: [0, 1]
1780
3076
  };
3077
+ function copyText(value, { kind, utc }) {
3078
+ if (value === null || value === void 0) return "";
3079
+ const dated = kind === "date" || kind === "datetime";
3080
+ if (value instanceof Date || dated && typeof value === "number") {
3081
+ const t = value instanceof Date ? value.getTime() : value;
3082
+ if (!Number.isFinite(t)) return "";
3083
+ return kind === "date" ? formatDate(t, ISO_DATE, { utc }) : new Date(t).toISOString();
3084
+ }
3085
+ return String(value);
3086
+ }
1781
3087
  var CellGrid = class {
1782
3088
  /** @param {import('./data_table.js').DataTable} table */
1783
3089
  constructor(table) {
@@ -1791,7 +3097,8 @@ var CellGrid = class {
1791
3097
  this.generation = 0;
1792
3098
  this.editor = new CellEditorHost({
1793
3099
  onCommit: (s, value, move, reason) => this._committed(s, value, move, reason),
1794
- onCancel: (s, reason) => this._closed(s, reason)
3100
+ onCancel: (s, reason) => this._closed(s, reason),
3101
+ onRefuse: (s, error, text, reason) => this._refused(s, error, text, reason)
1795
3102
  });
1796
3103
  this.staged = null;
1797
3104
  if (this._cfg().staged) {
@@ -1803,6 +3110,11 @@ var CellGrid = class {
1803
3110
  if (this.staged) this.fill = new FillHandle(this);
1804
3111
  else console.warn("[DataTable] `fillHandle` needs `staged`; ignored.");
1805
3112
  }
3113
+ this.paste = null;
3114
+ if (this._cfg().pasteable) {
3115
+ if (this.staged) this.paste = new CellPaste(this);
3116
+ else console.warn("[DataTable] `pasteable` needs `staged`; ignored.");
3117
+ }
1806
3118
  }
1807
3119
  // ── what the table is, read through its own helpers ──────────────────
1808
3120
  _cfg() {
@@ -1847,11 +3159,11 @@ var CellGrid = class {
1847
3159
  }
1848
3160
  /** What a cell shows: a refused or pending commit's value, else the row's. */
1849
3161
  shownValue(rowIdx, col) {
1850
- const key3 = `${rowIdx}:${col}`;
1851
- const err = this.errors.get(key3);
3162
+ const key4 = `${rowIdx}:${col}`;
3163
+ const err = this.errors.get(key4);
1852
3164
  if (err) return err.value;
1853
- if (this.pending.has(key3)) return this.pending.get(key3);
1854
- if (this.staged?.has(key3)) return this.staged.value(key3);
3165
+ if (this.pending.has(key4)) return this.pending.get(key4);
3166
+ if (this.staged?.has(key4)) return this.staged.value(key4);
1855
3167
  const row = this.currentRow(rowIdx);
1856
3168
  return row ? row[col] : void 0;
1857
3169
  }
@@ -1940,6 +3252,28 @@ var CellGrid = class {
1940
3252
  this.sel.clear();
1941
3253
  this._changed();
1942
3254
  }
3255
+ /**
3256
+ * STORY-0085. What a copy of the selection puts on the clipboard: the
3257
+ * range's cells as text, one array per row in the order SHOWN (after the
3258
+ * sort and the filters), its shown columns left to right (a hidden column
3259
+ * inside it is not one of them). With no range beyond the cursor, the
3260
+ * cursor cell alone. A cell is its RAW value as text (`copyText`), not
3261
+ * the text it is drawn as: the value the cell holds now, a staged,
3262
+ * pending or refused one included. Null with nothing selected.
3263
+ */
3264
+ copyMatrix() {
3265
+ this._sync();
3266
+ const r = this.sel.range();
3267
+ if (!r) return null;
3268
+ const t = this.table;
3269
+ const utc = t._utc();
3270
+ const out = [];
3271
+ for (let p = r.top; p <= r.bottom; p++) {
3272
+ const rowIdx = this.rowIndexAt(p);
3273
+ out.push(r.columns.map((col) => copyText(this.shownValue(rowIdx, col), { kind: t._valueKind(col), utc })));
3274
+ }
3275
+ return out;
3276
+ }
1943
3277
  /** New rows (`setData({rows})`): the marks were about the old ones. */
1944
3278
  newRows() {
1945
3279
  this.generation++;
@@ -1949,6 +3283,7 @@ var CellGrid = class {
1949
3283
  this.rowsWaiting.clear();
1950
3284
  this.staged?.clear();
1951
3285
  this.fill?.clearMarks();
3286
+ this.paste?.clearMarks();
1952
3287
  }
1953
3288
  isEditing() {
1954
3289
  return !!this.editor.session;
@@ -1967,16 +3302,16 @@ var CellGrid = class {
1967
3302
  * (STORY-0088) its staged value and a refused batch's mark. */
1968
3303
  _decorateCell(td, pos, rowIdx, col) {
1969
3304
  this._paintCell(td, pos, col);
1970
- const key3 = `${rowIdx}:${col}`;
1971
- const err = this.errors.get(key3);
1972
- const waiting = this.pending.has(key3);
1973
- const staged = !!this.staged?.has(key3);
3305
+ const key4 = `${rowIdx}:${col}`;
3306
+ const err = this.errors.get(key4);
3307
+ const waiting = this.pending.has(key4);
3308
+ const staged = !!this.staged?.has(key4);
1974
3309
  if (err || waiting) {
1975
- td.textContent = this.table._formatValue(err ? err.value : this.pending.get(key3), col);
3310
+ td.textContent = this.table._formatValue(err ? err.value : this.pending.get(key4), col);
1976
3311
  } else if (staged) {
1977
- td.textContent = this.table._formatValue(this.staged.value(key3), col);
3312
+ td.textContent = this.table._formatValue(this.staged.value(key4), col);
1978
3313
  }
1979
- const message = err ? err.message : this.staged?.errors.get(key3);
3314
+ const message = err ? err.message : this.staged?.errors.get(key4);
1980
3315
  td.classList.toggle("twm-dt-cell--pending", waiting);
1981
3316
  td.classList.toggle("twm-dt-cell--staged", staged);
1982
3317
  td.classList.toggle("twm-dt-cell--error", message != null);
@@ -1993,6 +3328,7 @@ var CellGrid = class {
1993
3328
  if (cursor) td.setAttribute("aria-selected", "true");
1994
3329
  else td.removeAttribute("aria-selected");
1995
3330
  this.fill?.paintCell(td, pos, col);
3331
+ this.paste?.paintCell(td, col);
1996
3332
  }
1997
3333
  paint() {
1998
3334
  const tbody = this.table._tbodyEl;
@@ -2010,6 +3346,7 @@ var CellGrid = class {
2010
3346
  _changed({ reveal = false } = {}) {
2011
3347
  const t = this.table;
2012
3348
  this.fill?.clearMarks();
3349
+ this.paste?.clearMarks();
2013
3350
  if (reveal && this.sel.focus) t.scrollToRow(this.rowIndexAt(this.sel.focus.row));
2014
3351
  this.paint();
2015
3352
  const before = t.getSelection().join();
@@ -2082,6 +3419,16 @@ var CellGrid = class {
2082
3419
  ev.stopImmediatePropagation();
2083
3420
  }
2084
3421
  });
3422
+ if (this.paste) {
3423
+ on(table, "paste", (ev) => {
3424
+ if (inEditor(ev.target)) return;
3425
+ if (ev.target !== table && ev.target?.closest?.('input, select, textarea, [contenteditable="true"]')) return;
3426
+ const text = ev.clipboardData?.getData?.("text/plain");
3427
+ if (typeof text !== "string") return;
3428
+ ev.preventDefault();
3429
+ this.paste.paste(text);
3430
+ });
3431
+ }
2085
3432
  this.paint();
2086
3433
  this.staged?.paintFreeze();
2087
3434
  }
@@ -2159,20 +3506,37 @@ var CellGrid = class {
2159
3506
  }
2160
3507
  return false;
2161
3508
  }
2162
- /** The column's type, as `editors` is keyed by: the declared one, else the
2163
- * detected one. */
2164
- _typeOf(col) {
3509
+ /** STORY-0087. The column's `editTypes` entry, as `{type, ...}`, or null. */
3510
+ _editSpec(col) {
3511
+ const all = this._cfg().editTypes;
3512
+ let spec = all && typeof all === "object" ? all[col] : null;
3513
+ if (typeof spec === "string") spec = { type: spec };
3514
+ return spec && typeof spec === "object" ? spec : null;
3515
+ }
3516
+ /** The column's type as it is named: `editTypes`', else the declared one
3517
+ * (`columnTypes`), else the detected one. */
3518
+ _rawType(col) {
3519
+ const spec = this._editSpec(col);
3520
+ if (spec?.type) return String(spec.type);
2165
3521
  const declared = this._cfg().columnTypes?.[col];
2166
3522
  return declared || this.table._columnTypes?.[col] || "text";
2167
3523
  }
3524
+ /** The column's type as `editors` and the built-in editors know it:
3525
+ * 'num' is 'number', 'date' is 'datetime'. */
3526
+ _typeOf(col) {
3527
+ return editorTypeOf(this._rawType(col));
3528
+ }
3529
+ /** By column index, then by type (as named, then as the built-ins name
3530
+ * it), then the built-in editor for the type, else the text box. */
2168
3531
  _factoryFor(col) {
2169
3532
  const editors = this._cfg().editors;
2170
3533
  if (editors && typeof editors === "object") {
2171
- if (typeof editors[col] === "function") return editors[col];
2172
- const type = this._typeOf(col);
2173
- if (typeof editors[type] === "function") return editors[type];
3534
+ for (const k of [col, this._rawType(col), this._typeOf(col)]) {
3535
+ if (typeof editors[k] === "function") return editors[k];
3536
+ }
2174
3537
  }
2175
- return createTextEditor;
3538
+ const builtIn = Object.prototype.hasOwnProperty.call(CELL_EDITORS, this._typeOf(col)) ? CELL_EDITORS[this._typeOf(col)] : null;
3539
+ return builtIn ? builtIn.create : createTextEditor;
2176
3540
  }
2177
3541
  /**
2178
3542
  * Open the editor on a cell (position, data column). Brings the row into
@@ -2195,7 +3559,12 @@ var CellGrid = class {
2195
3559
  row,
2196
3560
  generation: this.generation,
2197
3561
  value: this.shownValue(rowIdx, col),
2198
- column: { index: col, header: headers[col], type: this._typeOf(col) },
3562
+ column: { ...this._editSpec(col), index: col, header: headers[col], type: this._typeOf(col) },
3563
+ args: {
3564
+ locale: this._cfg().locale ?? void 0,
3565
+ utc: this.table._utc(),
3566
+ messages: this._cfg().editorMessages ?? void 0
3567
+ },
2199
3568
  initialText
2200
3569
  });
2201
3570
  }
@@ -2208,6 +3577,7 @@ var CellGrid = class {
2208
3577
  }
2209
3578
  dispose() {
2210
3579
  this.fill?.dispose();
3580
+ this.paste?.dispose();
2211
3581
  this.editor.cancel("dispose");
2212
3582
  this.renderWaiting = false;
2213
3583
  this.rowsWaiting.clear();
@@ -2268,6 +3638,20 @@ var CellGrid = class {
2268
3638
  this.rowsWaiting.delete(rowIdx);
2269
3639
  t.updateRow(rowIdx, row);
2270
3640
  }
3641
+ /** STORY-0087. The editor closed holding text that does not parse (the
3642
+ * focus left it, or its row left the window): nothing is committed, and
3643
+ * the cell keeps the text, marked with the message, as a refused commit's
3644
+ * would be. */
3645
+ _refused(s, error, text, reason) {
3646
+ if (s.generation === this.generation) {
3647
+ const key4 = `${s.rowIdx}:${s.colIdx}`;
3648
+ this.errors.set(key4, { value: text, message: error });
3649
+ this.latest.set(key4, {});
3650
+ this.pending.delete(key4);
3651
+ this._redraw(s.rowIdx, s.colIdx);
3652
+ }
3653
+ this._closed(s, reason);
3654
+ }
2271
3655
  _committed(s, value, move, reason) {
2272
3656
  if (move) {
2273
3657
  this._sync();
@@ -2276,7 +3660,7 @@ var CellGrid = class {
2276
3660
  this._changed({ reveal: true });
2277
3661
  }
2278
3662
  const live = s.generation === this.generation;
2279
- const key3 = `${s.rowIdx}:${s.colIdx}`;
3663
+ const key4 = `${s.rowIdx}:${s.colIdx}`;
2280
3664
  if (Object.is(value, s.previous)) {
2281
3665
  this._closed(s, reason);
2282
3666
  return;
@@ -2288,29 +3672,29 @@ var CellGrid = class {
2288
3672
  }
2289
3673
  const held = live ? this.currentRow(s.rowIdx) : s.row;
2290
3674
  const stored = held ? held[s.colIdx] : void 0;
2291
- if (live && this.errors.has(key3) && !this.pending.has(key3) && Object.is(value, stored)) {
2292
- this.errors.delete(key3);
2293
- this.latest.delete(key3);
3675
+ if (live && this.errors.has(key4) && !this.pending.has(key4) && Object.is(value, stored)) {
3676
+ this.errors.delete(key4);
3677
+ this.latest.delete(key4);
2294
3678
  this._redraw(s.rowIdx, s.colIdx);
2295
3679
  this._closed(s, reason);
2296
3680
  return;
2297
3681
  }
2298
3682
  const fn = this._cfg().onCellCommit;
2299
3683
  const token = {};
2300
- if (live) this.latest.set(key3, token);
3684
+ if (live) this.latest.set(key4, token);
2301
3685
  const finish = (result2) => {
2302
3686
  if (!live || s.generation !== this.generation) return;
2303
- const newest = this.latest.get(key3) === token;
3687
+ const newest = this.latest.get(key4) === token;
2304
3688
  const good = !(result2 && result2.ok === false);
2305
3689
  if (good) {
2306
3690
  const row = this.currentRow(s.rowIdx);
2307
3691
  if (Array.isArray(row)) row[s.colIdx] = value;
2308
3692
  }
2309
3693
  if (newest) {
2310
- this.latest.delete(key3);
2311
- this.pending.delete(key3);
2312
- if (good) this.errors.delete(key3);
2313
- else this.errors.set(key3, { value, message: String(result2.error ?? "Not saved") });
3694
+ this.latest.delete(key4);
3695
+ this.pending.delete(key4);
3696
+ if (good) this.errors.delete(key4);
3697
+ else this.errors.set(key4, { value, message: String(result2.error ?? "Not saved") });
2314
3698
  }
2315
3699
  this._redraw(s.rowIdx, s.colIdx);
2316
3700
  };
@@ -2325,8 +3709,8 @@ var CellGrid = class {
2325
3709
  }
2326
3710
  if (result && typeof result.then === "function") {
2327
3711
  if (live) {
2328
- this.pending.set(key3, value);
2329
- this.errors.delete(key3);
3712
+ this.pending.set(key4, value);
3713
+ this.errors.delete(key4);
2330
3714
  this._redraw(s.rowIdx, s.colIdx);
2331
3715
  }
2332
3716
  this._closed(s, reason);
@@ -2545,8 +3929,8 @@ var ColumnGroups = class {
2545
3929
  const n = cfg.headers?.length || 0;
2546
3930
  const shown = this.table._shownColumns();
2547
3931
  const spec = Array.isArray(cfg.columnGroups) ? cfg.columnGroups : [];
2548
- const key3 = `${n}|${shown.join(",")}|${spec.map((g) => g ? `${g.key}${g.label}${(g.columns || []).join(",")}` : "").join("")}`;
2549
- if (key3 === this._memoKey) return this._memo;
3932
+ const key4 = `${n}|${shown.join(",")}|${spec.map((g) => g ? `${g.key}${g.label}${(g.columns || []).join(",")}` : "").join("")}`;
3933
+ if (key4 === this._memoKey) return this._memo;
2550
3934
  const groups = normalize(spec, n);
2551
3935
  const groupOf = new Array(n).fill(null);
2552
3936
  groups.forEach((g, gi) => {
@@ -2574,7 +3958,7 @@ var ColumnGroups = class {
2574
3958
  if (!s.placeholder) slotOfCol.set(s.col, i);
2575
3959
  });
2576
3960
  const placeholders = slots.filter((s) => s.placeholder).length;
2577
- this._memoKey = key3;
3961
+ this._memoKey = key4;
2578
3962
  this._memo = { groups, slots, slotOfCol, placeholders };
2579
3963
  return this._memo;
2580
3964
  }
@@ -3277,7 +4661,12 @@ function compareSortKeys(a, b, asc) {
3277
4661
  }
3278
4662
  var AUTOSIZE_MAX_PX = 520;
3279
4663
  var VIRTUAL_FALLBACK_VIEWPORT_PX = 1e3;
3280
- var COPY_MENU_HTML = `
4664
+ var COPY_CELLS_MENU_HTML = `
4665
+ <div class="twm-context-menu-item" data-action="copy-tsv">
4666
+ <span class="material-symbols-outlined">content_copy</span>
4667
+ <span class="label">Copy Cells</span>
4668
+ </div>`;
4669
+ var copyMenuHtml = (cells) => cells ? COPY_CELLS_MENU_HTML : `
3281
4670
  <div class="twm-context-menu-item" data-action="copy-tsv">
3282
4671
  <span class="material-symbols-outlined">content_copy</span>
3283
4672
  <span class="label">Copy Row(s)</span>
@@ -3424,6 +4813,9 @@ var DataTable = class {
3424
4813
  selectionMode: "rows",
3425
4814
  editable: false,
3426
4815
  editors: null,
4816
+ editTypes: null,
4817
+ locale: null,
4818
+ editorMessages: null,
3427
4819
  onCellCommit: null,
3428
4820
  onCellSelectionChange: null,
3429
4821
  headerMenu: null,
@@ -3448,6 +4840,10 @@ var DataTable = class {
3448
4840
  onRowDelete: null,
3449
4841
  // STORY-0089. Off — no fill handle — until set.
3450
4842
  fillHandle: false,
4843
+ // STORY-0090. Off — Ctrl+V is not the table's — until set.
4844
+ pasteable: false,
4845
+ onPasteOverrun: null,
4846
+ onRowsAppend: null,
3451
4847
  ...config
3452
4848
  };
3453
4849
  this._cellGrid = null;
@@ -3470,6 +4866,9 @@ var DataTable = class {
3470
4866
  if (this.config.fillHandle && !this._cellGrid) {
3471
4867
  console.warn("[DataTable] `fillHandle` needs `staged`; ignored.");
3472
4868
  }
4869
+ if (this.config.pasteable && !this._cellGrid) {
4870
+ console.warn("[DataTable] `pasteable` needs `staged`; ignored.");
4871
+ }
3473
4872
  if (Array.isArray(this.config.columnGroups) && this.config.columnGroups.length && this._crosstab()) {
3474
4873
  console.warn("[DataTable] `columnGroups` cannot be combined with `crosstab` (use crosstab.headerRows); ignored.");
3475
4874
  }
@@ -4101,9 +5500,9 @@ var DataTable = class {
4101
5500
  *
4102
5501
  * @param {*} key
4103
5502
  */
4104
- setActiveRow(key3) {
4105
- this._activeKey = key3 == null ? null : String(key3);
4106
- this.config.activeRow = key3 ?? null;
5503
+ setActiveRow(key4) {
5504
+ this._activeKey = key4 == null ? null : String(key4);
5505
+ this.config.activeRow = key4 ?? null;
4107
5506
  const tbody = this._tbodyEl;
4108
5507
  if (!tbody) return;
4109
5508
  for (const tr of tbody.children) {
@@ -4242,6 +5641,24 @@ var DataTable = class {
4242
5641
  fill(to, { forceCopy = false } = {}) {
4243
5642
  return this._cellGrid?.fill ? this._cellGrid.fill.fill(to, { forceCopy }) : null;
4244
5643
  }
5644
+ /**
5645
+ * STORY-0090. Paste tab-separated `text` at the cursor cell, as Ctrl+V
5646
+ * does: each value read by its column's parse, staged as ONE undo entry.
5647
+ * Resolves `{rows, cols, staged, skipped, refused, droppedColumns,
5648
+ * appended}` — `staged` the NUMBER of cells that changed, `skipped` the
5649
+ * cells `editable` refused (`{row, col, rowIdx}`), `refused` the values
5650
+ * that did not parse (`{row, col, rowIdx, text, error}`),
5651
+ * `droppedColumns` the columns past the last one, `appended` the rows
5652
+ * handed to `onRowsAppend` — or null when nothing was pasted (no
5653
+ * `pasteable`, empty text, no cursor, an editor open, a commit or another
5654
+ * paste out, an overrun `onPasteOverrun` did not accept).
5655
+ *
5656
+ * @param {string} text
5657
+ * @returns {Promise<object|null>}
5658
+ */
5659
+ paste(text) {
5660
+ return this._cellGrid?.paste ? this._cellGrid.paste.paste(text) : Promise.resolve(null);
5661
+ }
4245
5662
  /** Why sort, filters and pages are held — a staged edit — or null; with
4246
5663
  * a reason, `what` is refused out loud. */
4247
5664
  _stagedRefusal(what) {
@@ -4626,10 +6043,19 @@ var DataTable = class {
4626
6043
  }
4627
6044
  }
4628
6045
  /**
4629
- * Copy selected rows to clipboard
6046
+ * Copy selected rows to clipboard — in cell mode (STORY-0085) the cells
6047
+ * selected, or the cursor cell alone, as tab-separated raw values with no
6048
+ * header line; cell mode has no CSV copy
4630
6049
  * @param {'tsv'|'csv'} [format='tsv'] - Output format
4631
6050
  */
4632
6051
  async copyToClipboard(format = "tsv") {
6052
+ if (this._cellGrid) {
6053
+ if (format === "csv") {
6054
+ console.warn("[DataTable] copyToClipboard: cell mode has no CSV copy; nothing copied");
6055
+ return;
6056
+ }
6057
+ return this._copyCells();
6058
+ }
4633
6059
  const indices = this.getSelection();
4634
6060
  if (indices.length === 0) {
4635
6061
  this._notify("Copy", "No rows selected.", "warn");
@@ -4645,14 +6071,14 @@ var DataTable = class {
4645
6071
  }
4646
6072
  const separator = format === "csv" ? "," : " ";
4647
6073
  const text = format === "csv" ? matrix.map((row) => row.map((cell) => this._csvEscape(cell)).join(separator)).join("\n") : matrix.map((row) => row.join(separator)).join("\n");
4648
- let ok = false;
6074
+ let ok2 = false;
4649
6075
  try {
4650
6076
  await navigator.clipboard.writeText(text);
4651
- ok = true;
6077
+ ok2 = true;
4652
6078
  } catch (_) {
4653
- ok = this._fallbackCopy(text);
6079
+ ok2 = this._fallbackCopy(text);
4654
6080
  }
4655
- if (ok) {
6081
+ if (ok2) {
4656
6082
  const desc = indices.length === 1 ? "row" : "rows";
4657
6083
  const suffix = format === "csv" ? " as CSV" : "";
4658
6084
  this._notify("Copy", `Copied ${indices.length} ${desc}${suffix}.`, "info");
@@ -4661,6 +6087,35 @@ var DataTable = class {
4661
6087
  }
4662
6088
  this._hideContextMenu();
4663
6089
  }
6090
+ /**
6091
+ * STORY-0085. Cell mode's copy: the range (or the cursor cell alone), its
6092
+ * raw values as TSV — Excel's quoting, `tsv.js` — with no header line.
6093
+ */
6094
+ async _copyCells() {
6095
+ const matrix = this._cellGrid.copyMatrix();
6096
+ if (!matrix || !matrix.length) {
6097
+ this._notify("Copy", "No cells selected.", "warn");
6098
+ this._hideContextMenu();
6099
+ return;
6100
+ }
6101
+ const text = toTSV(matrix);
6102
+ let ok2 = false;
6103
+ try {
6104
+ await navigator.clipboard.writeText(text);
6105
+ ok2 = true;
6106
+ } catch (_) {
6107
+ ok2 = this._fallbackCopy(text);
6108
+ }
6109
+ if (ok2) {
6110
+ const n = matrix.length;
6111
+ const m = matrix[0].length;
6112
+ const what = n * m === 1 ? "1 cell" : `${n} \xD7 ${m} cells`;
6113
+ this._notify("Copy", `Copied ${what}.`, "info");
6114
+ } else {
6115
+ this._notify("Copy", "Clipboard unavailable.", "warn");
6116
+ }
6117
+ this._hideContextMenu();
6118
+ }
4664
6119
  /**
4665
6120
  * Download all data as CSV
4666
6121
  * @param {string} [filename='data.csv'] - Filename for download
@@ -6591,8 +8046,8 @@ var DataTable = class {
6591
8046
  td.insertBefore(gap, td.firstChild);
6592
8047
  return;
6593
8048
  }
6594
- const key3 = this._rowKey(row, rowIdx);
6595
- const open = key3 !== null && this._expanded.has(key3);
8049
+ const key4 = this._rowKey(row, rowIdx);
8050
+ const open = key4 !== null && this._expanded.has(key4);
6596
8051
  const btn = document.createElement("button");
6597
8052
  btn.type = "button";
6598
8053
  btn.className = "material-symbols-outlined twm-dt-detail-toggle";
@@ -6614,14 +8069,14 @@ var DataTable = class {
6614
8069
  const row = tr.__row;
6615
8070
  const idx = tr.__rowIndex;
6616
8071
  if (!this._rowHasDetail(row, idx)) return;
6617
- const key3 = this._rowKey(row, idx);
6618
- if (key3 === null || !this._expanded.has(key3)) return;
6619
- const pieces = this._buildDetailPieces(tr, key3);
8072
+ const key4 = this._rowKey(row, idx);
8073
+ if (key4 === null || !this._expanded.has(key4)) return;
8074
+ const pieces = this._buildDetailPieces(tr, key4);
6620
8075
  const parent = tr.parentNode;
6621
8076
  if (!parent) return;
6622
8077
  for (const p of pieces) parent.insertBefore(p, ref);
6623
8078
  }
6624
- _buildDetailPieces(tr, key3) {
8079
+ _buildDetailPieces(tr, key4) {
6625
8080
  const n = this._domColCount();
6626
8081
  const detailTr = document.createElement("tr");
6627
8082
  detailTr.className = "twm-dt-detail-row";
@@ -6640,16 +8095,16 @@ var DataTable = class {
6640
8095
  filler.setAttribute("aria-hidden", "true");
6641
8096
  detailTr.__detailOf = tr;
6642
8097
  tr.__detail = [detailTr, filler];
6643
- this._drawDetail(tr, host, key3);
8098
+ this._drawDetail(tr, host, key4);
6644
8099
  const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
6645
8100
  if (btn) btn.setAttribute("aria-controls", host.id);
6646
8101
  return tr.__detail;
6647
8102
  }
6648
- _drawDetail(tr, host, key3) {
8103
+ _drawDetail(tr, host, key4) {
6649
8104
  host.replaceChildren();
6650
8105
  let out;
6651
8106
  try {
6652
- out = this.config.rowDetail(tr.__row, tr.__rowIndex, { host, key: key3, table: this });
8107
+ out = this.config.rowDetail(tr.__row, tr.__rowIndex, { host, key: key4, table: this });
6653
8108
  } catch (err) {
6654
8109
  console.error("[DataTable] rowDetail threw", err);
6655
8110
  out = err?.message || String(err);
@@ -6668,8 +8123,8 @@ var DataTable = class {
6668
8123
  }
6669
8124
  /** `updateRow`'s half: the detail follows the row's new data. */
6670
8125
  _redrawDetail(tr) {
6671
- const key3 = this._rowKey(tr.__row, tr.__rowIndex);
6672
- const want = this._rowHasDetail(tr.__row, tr.__rowIndex) && key3 !== null && this._expanded.has(key3);
8126
+ const key4 = this._rowKey(tr.__row, tr.__rowIndex);
8127
+ const want = this._rowHasDetail(tr.__row, tr.__rowIndex) && key4 !== null && this._expanded.has(key4);
6673
8128
  const pieces = tr.__detail;
6674
8129
  if (!want) {
6675
8130
  if (pieces) this._removeDetailPieces(tr);
@@ -6678,7 +8133,7 @@ var DataTable = class {
6678
8133
  if (pieces) {
6679
8134
  const host = pieces[0].querySelector(".twm-dt-detail");
6680
8135
  if (host) {
6681
- this._drawDetail(tr, host, key3);
8136
+ this._drawDetail(tr, host, key4);
6682
8137
  const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
6683
8138
  if (btn) btn.setAttribute("aria-controls", host.id);
6684
8139
  }
@@ -6691,27 +8146,27 @@ var DataTable = class {
6691
8146
  _setRowElExpanded(tr, open, ev = null) {
6692
8147
  if (!this._detailOn() || tr.__rowIndex === void 0) return false;
6693
8148
  if (!this._rowHasDetail(tr.__row, tr.__rowIndex)) return false;
6694
- const key3 = this._rowKey(tr.__row, tr.__rowIndex);
6695
- if (key3 === null || this._expanded.has(key3) === !!open) return false;
6696
- if (open) this._expanded.add(key3);
6697
- else this._expanded.delete(key3);
6698
- this._syncDetailOnScreen(tr, key3);
8149
+ const key4 = this._rowKey(tr.__row, tr.__rowIndex);
8150
+ if (key4 === null || this._expanded.has(key4) === !!open) return false;
8151
+ if (open) this._expanded.add(key4);
8152
+ else this._expanded.delete(key4);
8153
+ this._syncDetailOnScreen(tr, key4);
6699
8154
  this._afterDetailChange();
6700
8155
  try {
6701
- this.config.onRowDetailToggle?.(tr.__rowIndex, tr.__row, !!open, key3, ev);
8156
+ this.config.onRowDetailToggle?.(tr.__rowIndex, tr.__row, !!open, key4, ev);
6702
8157
  } catch (err) {
6703
8158
  console.error("[DataTable] onRowDetailToggle threw", err);
6704
8159
  }
6705
8160
  return true;
6706
8161
  }
6707
8162
  /** Make the row on screen agree with `_expanded` for its key. */
6708
- _syncDetailOnScreen(tr, key3) {
6709
- const open = this._expanded.has(key3);
8163
+ _syncDetailOnScreen(tr, key4) {
8164
+ const open = this._expanded.has(key4);
6710
8165
  const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
6711
8166
  if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
6712
8167
  if (open && !tr.__detail && tr.parentNode) {
6713
8168
  const ref = tr.nextSibling;
6714
- for (const p of this._buildDetailPieces(tr, key3)) tr.parentNode.insertBefore(p, ref);
8169
+ for (const p of this._buildDetailPieces(tr, key4)) tr.parentNode.insertBefore(p, ref);
6715
8170
  } else if (!open && tr.__detail) {
6716
8171
  this._removeDetailPieces(tr);
6717
8172
  }
@@ -6725,9 +8180,9 @@ var DataTable = class {
6725
8180
  * measured height open, three rows until it has been measured. */
6726
8181
  _detailHeightOf(row, rowIdx, base) {
6727
8182
  if (!this._rowHasDetail(row, rowIdx)) return 0;
6728
- const key3 = this._rowKey(row, rowIdx);
6729
- if (key3 === null || !this._expanded.has(key3)) return 0;
6730
- const h = this._detailHeights.get(key3);
8183
+ const key4 = this._rowKey(row, rowIdx);
8184
+ if (key4 === null || !this._expanded.has(key4)) return 0;
8185
+ const h = this._detailHeights.get(key4);
6731
8186
  return h > 0 ? h : base * 3;
6732
8187
  }
6733
8188
  /** Measure the open details among `trs` (client pixels, the zoom divided
@@ -6750,9 +8205,9 @@ var DataTable = class {
6750
8205
  if (!d || !d.isConnected) continue;
6751
8206
  const h = d.getBoundingClientRect().height / zoom;
6752
8207
  if (!(h > 0)) continue;
6753
- const key3 = this._rowKey(tr.__row, tr.__rowIndex);
6754
- if (Math.abs((this._detailHeights.get(key3) || 0) - h) > 0.5) {
6755
- this._detailHeights.set(key3, h);
8208
+ const key4 = this._rowKey(tr.__row, tr.__rowIndex);
8209
+ if (Math.abs((this._detailHeights.get(key4) || 0) - h) > 0.5) {
8210
+ this._detailHeights.set(key4, h);
6756
8211
  changed = true;
6757
8212
  }
6758
8213
  }
@@ -6767,9 +8222,9 @@ var DataTable = class {
6767
8222
  * @param {boolean} [open=true]
6768
8223
  * @returns {boolean} whether anything changed
6769
8224
  */
6770
- setRowExpanded(key3, open = true) {
6771
- if (key3 == null) return false;
6772
- const k = String(key3);
8225
+ setRowExpanded(key4, open = true) {
8226
+ if (key4 == null) return false;
8227
+ const k = String(key4);
6773
8228
  if (this._expanded.has(k) === !!open) return false;
6774
8229
  if (open) this._expanded.add(k);
6775
8230
  else this._expanded.delete(k);
@@ -6790,8 +8245,8 @@ var DataTable = class {
6790
8245
  return true;
6791
8246
  }
6792
8247
  /** Whether the row whose key is `key` has its detail open. */
6793
- isRowExpanded(key3) {
6794
- return key3 != null && this._expanded.has(String(key3));
8248
+ isRowExpanded(key4) {
8249
+ return key4 != null && this._expanded.has(String(key4));
6795
8250
  }
6796
8251
  /** The keys of the rows whose detail is open, as text. */
6797
8252
  getExpandedRows() {
@@ -6802,12 +8257,12 @@ var DataTable = class {
6802
8257
  for (const k of [...this._expanded]) this.setRowExpanded(k, false);
6803
8258
  }
6804
8259
  /** The `<tr>` on screen for a key, or null. */
6805
- _rowElByKey(key3) {
8260
+ _rowElByKey(key4) {
6806
8261
  const tbody = this._tbodyEl;
6807
8262
  if (!tbody) return null;
6808
8263
  for (const tr of tbody.children) {
6809
8264
  if (tr.__rowIndex === void 0) continue;
6810
- if (this._rowKey(tr.__row, tr.__rowIndex) === key3) return tr;
8265
+ if (this._rowKey(tr.__row, tr.__rowIndex) === key4) return tr;
6811
8266
  }
6812
8267
  return null;
6813
8268
  }
@@ -7736,18 +9191,18 @@ var DataTable = class {
7736
9191
  /** A row's key, as text, or null: `getRowKey`, else its original index. */
7737
9192
  _rowKey(row, rowIdx) {
7738
9193
  const fn = this.config.getRowKey;
7739
- let key3;
9194
+ let key4;
7740
9195
  if (typeof fn === "function") {
7741
9196
  try {
7742
- key3 = fn(row, rowIdx);
9197
+ key4 = fn(row, rowIdx);
7743
9198
  } catch (err) {
7744
9199
  console.error("[DataTable] getRowKey threw", err);
7745
- key3 = null;
9200
+ key4 = null;
7746
9201
  }
7747
9202
  } else {
7748
- key3 = rowIdx;
9203
+ key4 = rowIdx;
7749
9204
  }
7750
- return key3 == null ? null : String(key3);
9205
+ return key4 == null ? null : String(key4);
7751
9206
  }
7752
9207
  /** The gestures `activateOn` names, as a set. */
7753
9208
  _activateOn() {
@@ -8058,8 +9513,8 @@ var DataTable = class {
8058
9513
  const el = th.firstElementChild;
8059
9514
  if (!el) continue;
8060
9515
  const cs = getComputedStyle(th);
8061
- const pad2 = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
8062
- floors[i] = Math.ceil(el.getBoundingClientRect().width / zoom + pad2);
9516
+ const pad3 = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
9517
+ floors[i] = Math.ceil(el.getBoundingClientRect().width / zoom + pad3);
8063
9518
  natural[i] = Math.max(natural[i], floors[i]);
8064
9519
  }
8065
9520
  }
@@ -8812,13 +10267,13 @@ var DataTable = class {
8812
10267
  const handleKeyDown = (event) => {
8813
10268
  const ctrlLike = event.ctrlKey || event.metaKey;
8814
10269
  if (ctrlLike && !event.altKey) {
8815
- const key3 = String(event.key || "").toLowerCase();
8816
- if (key3 === "a" && selectable === "single") {
10270
+ const key4 = String(event.key || "").toLowerCase();
10271
+ if (key4 === "a" && selectable === "single") {
8817
10272
  event.preventDefault();
8818
- } else if (key3 === "a" && selectable) {
10273
+ } else if (key4 === "a" && selectable) {
8819
10274
  event.preventDefault();
8820
10275
  this._selectAllShown();
8821
- } else if (key3 === "c") {
10276
+ } else if (key4 === "c" && !(event.shiftKey && this._cellGrid)) {
8822
10277
  event.preventDefault();
8823
10278
  this.copyToClipboard(event.shiftKey ? "csv" : "tsv");
8824
10279
  }
@@ -8956,7 +10411,7 @@ var DataTable = class {
8956
10411
  menu.style.display = "none";
8957
10412
  menu.style.position = "fixed";
8958
10413
  menu.style.zIndex = "10001";
8959
- menu.innerHTML = COPY_MENU_HTML;
10414
+ menu.innerHTML = copyMenuHtml(!!this._cellGrid);
8960
10415
  document.body.appendChild(menu);
8961
10416
  menu.addEventListener("click", (event) => {
8962
10417
  const item = event.target.closest(".twm-context-menu-item");
@@ -9114,7 +10569,7 @@ var DataTable = class {
9114
10569
  this._contextMenuCtx = custom ? custom.ctx : null;
9115
10570
  if (!custom) {
9116
10571
  if (this._contextMenuCustom) {
9117
- menu.innerHTML = COPY_MENU_HTML;
10572
+ menu.innerHTML = copyMenuHtml(!!this._cellGrid);
9118
10573
  this._contextMenuCustom = false;
9119
10574
  }
9120
10575
  return;
@@ -9158,7 +10613,7 @@ var DataTable = class {
9158
10613
  sep.className = "twm-context-menu__separator";
9159
10614
  menu.appendChild(sep);
9160
10615
  }
9161
- menu.insertAdjacentHTML("beforeend", COPY_MENU_HTML);
10616
+ menu.insertAdjacentHTML("beforeend", copyMenuHtml(!!this._cellGrid));
9162
10617
  }
9163
10618
  }
9164
10619
  _teardownContextMenu() {
@@ -9207,9 +10662,9 @@ var DataTable = class {
9207
10662
  textarea.style.left = "-9999px";
9208
10663
  document.body.appendChild(textarea);
9209
10664
  textarea.select();
9210
- const ok = document.execCommand("copy");
10665
+ const ok2 = document.execCommand("copy");
9211
10666
  document.body.removeChild(textarea);
9212
- return ok;
10667
+ return ok2;
9213
10668
  } catch (_) {
9214
10669
  return false;
9215
10670
  }
@@ -9220,8 +10675,26 @@ var DataTable = class {
9220
10675
  };
9221
10676
 
9222
10677
  export {
10678
+ AutocompleteField,
9223
10679
  createTextEditor,
9224
10680
  CellEditorHost,
10681
+ EDITOR_MESSAGES,
10682
+ editorTypeOf,
10683
+ numberSeparators,
10684
+ parseNumber,
10685
+ formatNumberForEdit,
10686
+ parseBool,
10687
+ dateOrder,
10688
+ parseDateTime,
10689
+ formatDateTimeForEdit,
10690
+ normalizeOptions,
10691
+ parseChoice,
10692
+ createNumberEditor,
10693
+ createDateTimeEditor,
10694
+ createMultiTextEditor,
10695
+ createBoolEditor,
10696
+ createChoiceEditor,
10697
+ CELL_EDITORS,
9225
10698
  CellSelection,
9226
10699
  createRafResizeObserver,
9227
10700
  ISO_DATE,
@@ -9239,10 +10712,13 @@ export {
9239
10712
  StagedHistory,
9240
10713
  detectSeries,
9241
10714
  projectSeries,
10715
+ tsvCell,
10716
+ toTSV,
10717
+ fromTSV,
9242
10718
  attachInlineRenamer,
9243
10719
  PAGING_HARD_LIMIT,
9244
10720
  PAGING_SOFT_LIMIT,
9245
10721
  resolvePaging,
9246
10722
  DataTable
9247
10723
  };
9248
- //# sourceMappingURL=chunk-Q36EPT2R.js.map
10724
+ //# sourceMappingURL=chunk-LWV23YVE.js.map