@toolbox-web/grid 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/all.d.ts +19 -19
  2. package/all.d.ts.map +1 -1
  3. package/all.js +1798 -1201
  4. package/all.js.map +1 -1
  5. package/index.js +2215 -2026
  6. package/index.js.map +1 -1
  7. package/lib/core/grid.d.ts +22 -12
  8. package/lib/core/grid.d.ts.map +1 -1
  9. package/lib/core/internal/columns.d.ts +0 -9
  10. package/lib/core/internal/columns.d.ts.map +1 -1
  11. package/lib/core/internal/config-manager.d.ts +236 -0
  12. package/lib/core/internal/config-manager.d.ts.map +1 -0
  13. package/lib/core/internal/editing.d.ts +3 -3
  14. package/lib/core/internal/editing.d.ts.map +1 -1
  15. package/lib/core/internal/event-delegation.d.ts.map +1 -1
  16. package/lib/core/internal/header.d.ts.map +1 -1
  17. package/lib/core/internal/keyboard.d.ts.map +1 -1
  18. package/lib/core/internal/render-scheduler.d.ts +123 -0
  19. package/lib/core/internal/render-scheduler.d.ts.map +1 -0
  20. package/lib/core/internal/rows.d.ts +8 -3
  21. package/lib/core/internal/rows.d.ts.map +1 -1
  22. package/lib/core/internal/sanitize.d.ts +2 -2
  23. package/lib/core/internal/sanitize.d.ts.map +1 -1
  24. package/lib/core/internal/shell.d.ts +40 -2
  25. package/lib/core/internal/shell.d.ts.map +1 -1
  26. package/lib/core/internal/validate-config.d.ts +25 -0
  27. package/lib/core/internal/validate-config.d.ts.map +1 -0
  28. package/lib/core/plugin/base-plugin.d.ts +118 -0
  29. package/lib/core/plugin/base-plugin.d.ts.map +1 -1
  30. package/lib/core/plugin/index.d.ts +1 -0
  31. package/lib/core/plugin/index.d.ts.map +1 -1
  32. package/lib/core/plugin/plugin-manager.d.ts +13 -2
  33. package/lib/core/plugin/plugin-manager.d.ts.map +1 -1
  34. package/lib/core/plugin/types.d.ts +17 -3
  35. package/lib/core/plugin/types.d.ts.map +1 -1
  36. package/lib/core/types.d.ts +112 -12
  37. package/lib/core/types.d.ts.map +1 -1
  38. package/lib/plugins/clipboard/ClipboardPlugin.d.ts +7 -1
  39. package/lib/plugins/clipboard/ClipboardPlugin.d.ts.map +1 -1
  40. package/lib/plugins/clipboard/index.js +78 -22
  41. package/lib/plugins/clipboard/index.js.map +1 -1
  42. package/lib/plugins/column-virtualization/index.js +72 -21
  43. package/lib/plugins/column-virtualization/index.js.map +1 -1
  44. package/lib/plugins/context-menu/index.js +53 -2
  45. package/lib/plugins/context-menu/index.js.map +1 -1
  46. package/lib/plugins/editing/EditingPlugin.d.ts +117 -0
  47. package/lib/plugins/editing/EditingPlugin.d.ts.map +1 -0
  48. package/lib/{core/internal → plugins/editing}/editors.d.ts +1 -1
  49. package/lib/plugins/editing/editors.d.ts.map +1 -0
  50. package/lib/plugins/editing/index.d.ts +8 -0
  51. package/lib/plugins/editing/index.d.ts.map +1 -0
  52. package/lib/plugins/editing/index.js +721 -0
  53. package/lib/plugins/editing/index.js.map +1 -0
  54. package/lib/plugins/editing/types.d.ts +45 -0
  55. package/lib/plugins/editing/types.d.ts.map +1 -0
  56. package/lib/plugins/export/ExportPlugin.d.ts.map +1 -1
  57. package/lib/plugins/export/index.js +68 -17
  58. package/lib/plugins/export/index.js.map +1 -1
  59. package/lib/plugins/filtering/FilteringPlugin.d.ts.map +1 -1
  60. package/lib/plugins/filtering/index.js +103 -50
  61. package/lib/plugins/filtering/index.js.map +1 -1
  62. package/lib/plugins/grouping-columns/grouping-columns.d.ts +4 -4
  63. package/lib/plugins/grouping-columns/grouping-columns.d.ts.map +1 -1
  64. package/lib/plugins/grouping-columns/index.js +80 -29
  65. package/lib/plugins/grouping-columns/index.js.map +1 -1
  66. package/lib/plugins/grouping-rows/GroupingRowsPlugin.d.ts.map +1 -1
  67. package/lib/plugins/grouping-rows/grouping-rows.d.ts.map +1 -1
  68. package/lib/plugins/grouping-rows/index.js +62 -11
  69. package/lib/plugins/grouping-rows/index.js.map +1 -1
  70. package/lib/plugins/master-detail/MasterDetailPlugin.d.ts +2 -2
  71. package/lib/plugins/master-detail/MasterDetailPlugin.d.ts.map +1 -1
  72. package/lib/plugins/master-detail/index.js +156 -102
  73. package/lib/plugins/master-detail/index.js.map +1 -1
  74. package/lib/plugins/master-detail/types.d.ts +12 -2
  75. package/lib/plugins/master-detail/types.d.ts.map +1 -1
  76. package/lib/plugins/multi-sort/MultiSortPlugin.d.ts.map +1 -1
  77. package/lib/plugins/multi-sort/index.js +75 -22
  78. package/lib/plugins/multi-sort/index.js.map +1 -1
  79. package/lib/plugins/pinned-columns/index.js +57 -6
  80. package/lib/plugins/pinned-columns/index.js.map +1 -1
  81. package/lib/plugins/pinned-rows/PinnedRowsPlugin.d.ts.map +1 -1
  82. package/lib/plugins/pinned-rows/index.js +61 -9
  83. package/lib/plugins/pinned-rows/index.js.map +1 -1
  84. package/lib/plugins/pivot/index.js +58 -7
  85. package/lib/plugins/pivot/index.js.map +1 -1
  86. package/lib/plugins/reorder/ReorderPlugin.d.ts.map +1 -1
  87. package/lib/plugins/reorder/index.js +75 -19
  88. package/lib/plugins/reorder/index.js.map +1 -1
  89. package/lib/plugins/selection/index.js +57 -6
  90. package/lib/plugins/selection/index.js.map +1 -1
  91. package/lib/plugins/server-side/index.js +60 -9
  92. package/lib/plugins/server-side/index.js.map +1 -1
  93. package/lib/plugins/tree/TreePlugin.d.ts +3 -3
  94. package/lib/plugins/tree/TreePlugin.d.ts.map +1 -1
  95. package/lib/plugins/tree/index.js +181 -126
  96. package/lib/plugins/tree/index.js.map +1 -1
  97. package/lib/plugins/tree/tree-data.d.ts +6 -6
  98. package/lib/plugins/tree/tree-data.d.ts.map +1 -1
  99. package/lib/plugins/tree/tree-detect.d.ts +5 -9
  100. package/lib/plugins/tree/tree-detect.d.ts.map +1 -1
  101. package/lib/plugins/tree/types.d.ts +16 -4
  102. package/lib/plugins/tree/types.d.ts.map +1 -1
  103. package/lib/plugins/undo-redo/UndoRedoPlugin.d.ts +7 -1
  104. package/lib/plugins/undo-redo/UndoRedoPlugin.d.ts.map +1 -1
  105. package/lib/plugins/undo-redo/index.js +65 -6
  106. package/lib/plugins/undo-redo/index.js.map +1 -1
  107. package/lib/plugins/visibility/VisibilityPlugin.d.ts +7 -1
  108. package/lib/plugins/visibility/VisibilityPlugin.d.ts.map +1 -1
  109. package/lib/plugins/visibility/index.js +61 -2
  110. package/lib/plugins/visibility/index.js.map +1 -1
  111. package/package.json +1 -1
  112. package/public.d.ts +104 -13
  113. package/public.d.ts.map +1 -1
  114. package/umd/grid.all.umd.js +37 -19
  115. package/umd/grid.all.umd.js.map +1 -1
  116. package/umd/grid.umd.js +25 -7
  117. package/umd/grid.umd.js.map +1 -1
  118. package/umd/plugins/clipboard.umd.js +2 -2
  119. package/umd/plugins/clipboard.umd.js.map +1 -1
  120. package/umd/plugins/editing.umd.js +2 -0
  121. package/umd/plugins/editing.umd.js.map +1 -0
  122. package/umd/plugins/export.umd.js +2 -2
  123. package/umd/plugins/export.umd.js.map +1 -1
  124. package/umd/plugins/filtering.umd.js +1 -1
  125. package/umd/plugins/filtering.umd.js.map +1 -1
  126. package/umd/plugins/grouping-columns.umd.js +1 -1
  127. package/umd/plugins/grouping-columns.umd.js.map +1 -1
  128. package/umd/plugins/grouping-rows.umd.js +1 -1
  129. package/umd/plugins/grouping-rows.umd.js.map +1 -1
  130. package/umd/plugins/master-detail.umd.js +1 -1
  131. package/umd/plugins/master-detail.umd.js.map +1 -1
  132. package/umd/plugins/multi-sort.umd.js +1 -1
  133. package/umd/plugins/multi-sort.umd.js.map +1 -1
  134. package/umd/plugins/pinned-rows.umd.js +1 -1
  135. package/umd/plugins/pinned-rows.umd.js.map +1 -1
  136. package/umd/plugins/reorder.umd.js +1 -1
  137. package/umd/plugins/reorder.umd.js.map +1 -1
  138. package/umd/plugins/tree.umd.js +1 -1
  139. package/umd/plugins/tree.umd.js.map +1 -1
  140. package/umd/plugins/undo-redo.umd.js +1 -1
  141. package/umd/plugins/undo-redo.umd.js.map +1 -1
  142. package/umd/plugins/visibility.umd.js +1 -1
  143. package/umd/plugins/visibility.umd.js.map +1 -1
  144. package/lib/core/internal/column-state.d.ts +0 -124
  145. package/lib/core/internal/column-state.d.ts.map +0 -1
  146. package/lib/core/internal/editors.d.ts.map +0 -1
  147. package/lib/core/internal/grid-internals.d.ts +0 -83
  148. package/lib/core/internal/grid-internals.d.ts.map +0 -1
@@ -0,0 +1,721 @@
1
+ const R = {
2
+ expand: "▶",
3
+ collapse: "▼",
4
+ sortAsc: "▲",
5
+ sortDesc: "▼",
6
+ sortNone: "⇅",
7
+ submenuArrow: "▶",
8
+ dragHandle: "⋮⋮",
9
+ toolPanel: "☰"
10
+ };
11
+ class _ {
12
+ /**
13
+ * Plugin dependencies - declare other plugins this one requires.
14
+ *
15
+ * Dependencies are validated when the plugin is attached.
16
+ * Required dependencies throw an error if missing.
17
+ * Optional dependencies log an info message if missing.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * static readonly dependencies: PluginDependency[] = [
22
+ * { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },
23
+ * { name: 'selection', required: false, reason: 'Enables selection-based undo' },
24
+ * ];
25
+ * ```
26
+ */
27
+ static dependencies;
28
+ /** Plugin version - override in subclass if needed */
29
+ version = "1.0.0";
30
+ /** CSS styles to inject into the grid's shadow DOM */
31
+ styles;
32
+ /** Custom cell renderers keyed by type name */
33
+ cellRenderers;
34
+ /** Custom header renderers keyed by type name */
35
+ headerRenderers;
36
+ /** Custom cell editors keyed by type name */
37
+ cellEditors;
38
+ /** The grid instance this plugin is attached to */
39
+ grid;
40
+ /** Plugin configuration - merged with defaults in attach() */
41
+ config;
42
+ /** User-provided configuration from constructor */
43
+ userConfig;
44
+ /**
45
+ * Plugin-level AbortController for event listener cleanup.
46
+ * Created fresh in attach(), aborted in detach().
47
+ * This ensures event listeners are properly cleaned up when plugins are re-attached.
48
+ */
49
+ #e;
50
+ /**
51
+ * Default configuration - subclasses should override this getter.
52
+ * Note: This must be a getter (not property initializer) for proper inheritance
53
+ * since property initializers run after parent constructor.
54
+ */
55
+ get defaultConfig() {
56
+ return {};
57
+ }
58
+ constructor(e = {}) {
59
+ this.userConfig = e;
60
+ }
61
+ /**
62
+ * Called when the plugin is attached to a grid.
63
+ * Override to set up event listeners, initialize state, etc.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * attach(grid: GridElement): void {
68
+ * super.attach(grid);
69
+ * // Set up document-level listeners with auto-cleanup
70
+ * document.addEventListener('keydown', this.handleEscape, {
71
+ * signal: this.disconnectSignal
72
+ * });
73
+ * }
74
+ * ```
75
+ */
76
+ attach(e) {
77
+ this.#e?.abort(), this.#e = new AbortController(), this.grid = e, this.config = { ...this.defaultConfig, ...this.userConfig };
78
+ }
79
+ /**
80
+ * Called when the plugin is detached from a grid.
81
+ * Override to clean up event listeners, timers, etc.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * detach(): void {
86
+ * // Clean up any state not handled by disconnectSignal
87
+ * this.selectedRows.clear();
88
+ * this.cache = null;
89
+ * }
90
+ * ```
91
+ */
92
+ detach() {
93
+ this.#e?.abort(), this.#e = void 0;
94
+ }
95
+ /**
96
+ * Get another plugin instance from the same grid.
97
+ * Use for inter-plugin communication.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const selection = this.getPlugin(SelectionPlugin);
102
+ * if (selection) {
103
+ * const selectedRows = selection.getSelectedRows();
104
+ * }
105
+ * ```
106
+ */
107
+ getPlugin(e) {
108
+ return this.grid?.getPlugin(e);
109
+ }
110
+ /**
111
+ * Emit a custom event from the grid.
112
+ */
113
+ emit(e, t) {
114
+ this.grid?.dispatchEvent?.(new CustomEvent(e, { detail: t, bubbles: !0 }));
115
+ }
116
+ /**
117
+ * Request a re-render of the grid.
118
+ */
119
+ requestRender() {
120
+ this.grid?.requestRender?.();
121
+ }
122
+ /**
123
+ * Request a lightweight style update without rebuilding DOM.
124
+ * Use this instead of requestRender() when only CSS classes need updating.
125
+ */
126
+ requestAfterRender() {
127
+ this.grid?.requestAfterRender?.();
128
+ }
129
+ /**
130
+ * Get the current rows from the grid.
131
+ */
132
+ get rows() {
133
+ return this.grid?.rows ?? [];
134
+ }
135
+ /**
136
+ * Get the original unfiltered/unprocessed rows from the grid.
137
+ * Use this when you need all source data regardless of active filters.
138
+ */
139
+ get sourceRows() {
140
+ return this.grid?.sourceRows ?? [];
141
+ }
142
+ /**
143
+ * Get the current columns from the grid.
144
+ */
145
+ get columns() {
146
+ return this.grid?.columns ?? [];
147
+ }
148
+ /**
149
+ * Get only visible columns from the grid (excludes hidden).
150
+ * Use this for rendering that needs to match the grid template.
151
+ */
152
+ get visibleColumns() {
153
+ return this.grid?._visibleColumns ?? [];
154
+ }
155
+ /**
156
+ * Get the shadow root of the grid.
157
+ */
158
+ get shadowRoot() {
159
+ return this.grid?.shadowRoot ?? null;
160
+ }
161
+ /**
162
+ * Get the disconnect signal for event listener cleanup.
163
+ * This signal is aborted when the grid disconnects from the DOM.
164
+ * Use this when adding event listeners that should be cleaned up automatically.
165
+ *
166
+ * Best for:
167
+ * - Document/window-level listeners added in attach()
168
+ * - Listeners on the grid element itself
169
+ * - Any listener that should persist across renders
170
+ *
171
+ * Not needed for:
172
+ * - Listeners on elements created in afterRender() (removed with element)
173
+ *
174
+ * @example
175
+ * element.addEventListener('click', handler, { signal: this.disconnectSignal });
176
+ * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });
177
+ */
178
+ get disconnectSignal() {
179
+ return this.#e?.signal ?? this.grid?.disconnectSignal;
180
+ }
181
+ /**
182
+ * Get the grid-level icons configuration.
183
+ * Returns merged icons (user config + defaults).
184
+ */
185
+ get gridIcons() {
186
+ const e = this.grid?.gridConfig?.icons ?? {};
187
+ return { ...R, ...e };
188
+ }
189
+ /**
190
+ * Resolve an icon value to string or HTMLElement.
191
+ * Checks plugin config first, then grid-level icons, then defaults.
192
+ *
193
+ * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')
194
+ * @param pluginOverride - Optional plugin-level override
195
+ * @returns The resolved icon value
196
+ */
197
+ resolveIcon(e, t) {
198
+ return t !== void 0 ? t : this.gridIcons[e];
199
+ }
200
+ /**
201
+ * Set an icon value on an element.
202
+ * Handles both string (text/HTML) and HTMLElement values.
203
+ *
204
+ * @param element - The element to set the icon on
205
+ * @param icon - The icon value (string or HTMLElement)
206
+ */
207
+ setIcon(e, t) {
208
+ typeof t == "string" ? e.innerHTML = t : t instanceof HTMLElement && (e.innerHTML = "", e.appendChild(t.cloneNode(!0)));
209
+ }
210
+ /**
211
+ * Log a warning message.
212
+ */
213
+ warn(e) {
214
+ console.warn(`[tbw-grid:${this.name}] ${e}`);
215
+ }
216
+ // #endregion
217
+ }
218
+ function k(f) {
219
+ switch (f.type) {
220
+ case "number":
221
+ return (e) => {
222
+ const t = document.createElement("input");
223
+ return t.type = "number", t.value = e.value != null ? String(e.value) : "", t.addEventListener("blur", () => e.commit(t.value === "" ? null : Number(t.value))), t.addEventListener("keydown", (i) => {
224
+ i.key === "Enter" && e.commit(t.value === "" ? null : Number(t.value)), i.key === "Escape" && e.cancel();
225
+ }), t;
226
+ };
227
+ case "boolean":
228
+ return (e) => {
229
+ const t = document.createElement("input");
230
+ return t.type = "checkbox", t.checked = !!e.value, t.addEventListener("change", () => e.commit(t.checked)), t;
231
+ };
232
+ case "date":
233
+ return (e) => {
234
+ const t = document.createElement("input");
235
+ return t.type = "date", e.value instanceof Date && (t.valueAsDate = e.value), t.addEventListener("change", () => e.commit(t.valueAsDate)), t.addEventListener("keydown", (i) => {
236
+ i.key === "Escape" && e.cancel();
237
+ }), t;
238
+ };
239
+ case "select":
240
+ case "typeahead":
241
+ return (e) => {
242
+ const t = document.createElement("select"), i = e.column;
243
+ i.multi && (t.multiple = !0);
244
+ const s = i.options;
245
+ (typeof s == "function" ? s() : s || []).forEach((r) => {
246
+ const c = document.createElement("option");
247
+ c.value = String(r.value), c.textContent = r.label, (i.multi && Array.isArray(e.value) && e.value.includes(r.value) || !i.multi && e.value === r.value) && (c.selected = !0), t.appendChild(c);
248
+ });
249
+ const l = () => {
250
+ if (i.multi) {
251
+ const r = [];
252
+ Array.from(t.selectedOptions).forEach((c) => {
253
+ r.push(c.value);
254
+ }), e.commit(r);
255
+ } else
256
+ e.commit(t.value);
257
+ };
258
+ return t.addEventListener("change", l), t.addEventListener("blur", l), t.addEventListener("keydown", (r) => {
259
+ r.key === "Escape" && e.cancel();
260
+ }), t;
261
+ };
262
+ default:
263
+ return (e) => {
264
+ const t = document.createElement("input");
265
+ return t.type = "text", t.value = e.value != null ? String(e.value) : "", t.addEventListener("blur", () => e.commit(t.value)), t.addEventListener("keydown", (i) => {
266
+ i.key === "Enter" && e.commit(t.value), i.key === "Escape" && e.cancel();
267
+ }), t;
268
+ };
269
+ }
270
+ }
271
+ const w = 'input,select,textarea,[contenteditable="true"],[contenteditable=""],[tabindex]:not([tabindex="-1"])';
272
+ function b(f) {
273
+ return !(typeof f != "string" || f === "__proto__" || f === "constructor" || f === "prototype");
274
+ }
275
+ function T(f) {
276
+ return (f.__editingCellCount ?? 0) > 0;
277
+ }
278
+ function L(f) {
279
+ const e = (f.__editingCellCount ?? 0) + 1;
280
+ f.__editingCellCount = e, f.setAttribute("data-has-editing", "");
281
+ }
282
+ function S(f) {
283
+ f.__editingCellCount = 0, f.removeAttribute("data-has-editing");
284
+ }
285
+ function A(f, e, t) {
286
+ const i = f.querySelector("input,textarea,select");
287
+ if (!i) return;
288
+ const s = () => i instanceof HTMLInputElement ? i.type === "checkbox" ? i.checked : i.type === "number" ? i.value === "" ? null : Number(i.value) : i.type === "date" ? i.valueAsDate : i.value : e.type === "number" && i.value !== "" ? Number(i.value) : i.value;
289
+ i.addEventListener("blur", () => {
290
+ t(s());
291
+ }), i instanceof HTMLInputElement && i.type === "checkbox" ? i.addEventListener("change", () => t(i.checked)) : i instanceof HTMLSelectElement && i.addEventListener("change", () => t(s()));
292
+ }
293
+ class q extends _ {
294
+ name = "editing";
295
+ version = "1.0.0";
296
+ get defaultConfig() {
297
+ return {
298
+ editOn: "click"
299
+ };
300
+ }
301
+ // #region Editing State (fully owned by plugin)
302
+ /** Currently active edit row index, or -1 if not editing */
303
+ #e = -1;
304
+ /** Currently active edit column index, or -1 if not editing */
305
+ #r = -1;
306
+ /** Snapshots of row data before editing started */
307
+ #s = /* @__PURE__ */ new Map();
308
+ /** Set of row indices that have been modified */
309
+ #t = /* @__PURE__ */ new Set();
310
+ /** Set of cells currently in edit mode: "rowIndex:colIndex" */
311
+ #n = /* @__PURE__ */ new Set();
312
+ /** Flag to restore focus after next render (used when exiting edit mode) */
313
+ #o = !1;
314
+ // #endregion
315
+ // #region Lifecycle
316
+ attach(e) {
317
+ super.attach(e);
318
+ const t = this.disconnectSignal, i = e;
319
+ i._activeEditRows = -1, i._rowEditSnapshots = /* @__PURE__ */ new Map(), i._changedRowIndices = /* @__PURE__ */ new Set(), Object.defineProperty(e, "changedRows", {
320
+ get: () => this.changedRows,
321
+ configurable: !0
322
+ }), Object.defineProperty(e, "changedRowIndices", {
323
+ get: () => this.changedRowIndices,
324
+ configurable: !0
325
+ }), e.resetChangedRows = (s) => this.resetChangedRows(s), e.beginBulkEdit = (s, n) => {
326
+ n && this.beginCellEdit(s, n);
327
+ }, document.addEventListener(
328
+ "keydown",
329
+ (s) => {
330
+ s.key === "Escape" && this.#e !== -1 && this.#i(this.#e, !0);
331
+ },
332
+ { capture: !0, signal: t }
333
+ ), document.addEventListener(
334
+ "mousedown",
335
+ (s) => {
336
+ if (this.#e === -1) return;
337
+ const n = i.findRenderedRowElement?.(this.#e);
338
+ !n || (s.composedPath && s.composedPath() || []).includes(n) || this.#i(this.#e, !1);
339
+ },
340
+ { signal: t }
341
+ );
342
+ }
343
+ detach() {
344
+ this.#e = -1, this.#r = -1, this.#s.clear(), this.#t.clear(), this.#n.clear(), super.detach();
345
+ }
346
+ // #endregion
347
+ // #region Config Augmentation (processColumns hook)
348
+ /**
349
+ * Augment columns with editing metadata.
350
+ * This enables the grid to recognize editable columns without core knowledge.
351
+ */
352
+ processColumns(e) {
353
+ return e;
354
+ }
355
+ // #endregion
356
+ // #region Event Handlers (event distribution)
357
+ /**
358
+ * Handle cell clicks - start editing if configured for click mode.
359
+ * Both click and dblclick events come through this handler.
360
+ * Starts row-based editing (all editable cells in the row get editors).
361
+ */
362
+ onCellClick(e) {
363
+ const t = this.grid, i = this.config.editOn ?? t.effectiveConfig?.editOn;
364
+ if (i === !1 || i === "manual" || i !== "click" && i !== "dblclick") return !1;
365
+ const s = e.originalEvent.type === "dblclick";
366
+ if (i === "click" && s || i === "dblclick" && !s) return !1;
367
+ const { rowIndex: n } = e;
368
+ return t._columns?.some((r) => r.editable) ? (e.originalEvent.stopPropagation(), this.beginBulkEdit(n), !0) : !1;
369
+ }
370
+ /**
371
+ * Handle keyboard events for edit lifecycle.
372
+ */
373
+ onKeyDown(e) {
374
+ const t = this.grid;
375
+ if (e.key === "Escape" && this.#e !== -1)
376
+ return this.#i(this.#e, !0), !0;
377
+ if (e.key === " " || e.key === "Spacebar") {
378
+ const i = t._focusRow, s = t._focusCol;
379
+ if (i >= 0 && s >= 0) {
380
+ const n = t._visibleColumns[s], l = t._rows[i];
381
+ if (n?.editable && n.type === "boolean" && l) {
382
+ const r = n.field;
383
+ if (b(r)) {
384
+ const u = !l[r];
385
+ return this.#c(i, n, u, l), e.preventDefault(), this.requestRender(), !0;
386
+ }
387
+ }
388
+ }
389
+ return !1;
390
+ }
391
+ if (e.key === "Enter" && !e.shiftKey) {
392
+ if (this.#e !== -1)
393
+ return !1;
394
+ const i = this.config.editOn ?? t.effectiveConfig?.editOn;
395
+ if (i === !1 || i === "manual") return !1;
396
+ const s = t._focusRow;
397
+ return s >= 0 && t._columns?.some((l) => l.editable) ? (this.beginBulkEdit(s), !0) : !1;
398
+ }
399
+ return !1;
400
+ }
401
+ // #endregion
402
+ // #region Render Hooks
403
+ /**
404
+ * After render, reapply editors to cells in edit mode.
405
+ * This handles virtualization - when a row scrolls back into view,
406
+ * we need to re-inject the editor.
407
+ */
408
+ afterRender() {
409
+ const e = this.grid;
410
+ if (this.#o && (this.#o = !1, this.#u(e)), this.#n.size !== 0)
411
+ for (const t of this.#n) {
412
+ const [i, s] = t.split(":"), n = parseInt(i, 10), l = parseInt(s, 10), r = e.findRenderedRowElement?.(n);
413
+ if (!r) continue;
414
+ const c = r.querySelector(`.cell[data-col="${l}"]`);
415
+ if (!c || c.classList.contains("editing")) continue;
416
+ const u = e._rows[n], a = e._visibleColumns[l];
417
+ u && a && this.#a(u, n, a, l, c, !0);
418
+ }
419
+ }
420
+ /**
421
+ * On scroll render, reapply editors to recycled cells.
422
+ */
423
+ onScrollRender() {
424
+ this.afterRender();
425
+ }
426
+ // #endregion
427
+ // #region Public API
428
+ /**
429
+ * Get all rows that have been modified.
430
+ */
431
+ get changedRows() {
432
+ const e = this.grid;
433
+ return Array.from(this.#t).map((t) => e._rows[t]);
434
+ }
435
+ /**
436
+ * Get indices of all modified rows.
437
+ */
438
+ get changedRowIndices() {
439
+ return Array.from(this.#t);
440
+ }
441
+ /**
442
+ * Get the currently active edit row index, or -1 if not editing.
443
+ */
444
+ get activeEditRow() {
445
+ return this.#e;
446
+ }
447
+ /**
448
+ * Get the currently active edit column index, or -1 if not editing.
449
+ */
450
+ get activeEditCol() {
451
+ return this.#r;
452
+ }
453
+ /**
454
+ * Check if a specific row is currently being edited.
455
+ */
456
+ isRowEditing(e) {
457
+ return this.#e === e;
458
+ }
459
+ /**
460
+ * Check if a specific cell is currently being edited.
461
+ */
462
+ isCellEditing(e, t) {
463
+ return this.#n.has(`${e}:${t}`);
464
+ }
465
+ /**
466
+ * Check if a specific row has been modified.
467
+ */
468
+ isRowChanged(e) {
469
+ return this.#t.has(e);
470
+ }
471
+ /**
472
+ * Reset all change tracking.
473
+ */
474
+ resetChangedRows(e) {
475
+ const t = this.changedRows, i = this.changedRowIndices;
476
+ this.#t.clear(), this.#l(), e || this.emit("changed-rows-reset", { rows: t, indices: i }), this.grid._rowPool?.forEach((n) => n.classList.remove("changed"));
477
+ }
478
+ /**
479
+ * Programmatically begin editing a cell.
480
+ */
481
+ beginCellEdit(e, t) {
482
+ const i = this.grid, s = i._visibleColumns.findIndex((c) => c.field === t);
483
+ if (s === -1 || !i._visibleColumns[s]?.editable) return;
484
+ const r = i.findRenderedRowElement?.(e)?.querySelector(`.cell[data-col="${s}"]`);
485
+ r && this.#f(e, s, r);
486
+ }
487
+ /**
488
+ * Programmatically begin editing all editable cells in a row.
489
+ */
490
+ beginBulkEdit(e) {
491
+ const t = this.grid;
492
+ if ((this.config.editOn ?? t.effectiveConfig?.editOn) === !1 || !t._columns?.some((r) => r.editable)) return;
493
+ const n = t.findRenderedRowElement?.(e);
494
+ if (!n) return;
495
+ const l = t._rows[e];
496
+ this.#d(e, l), Array.from(n.children).forEach((r, c) => {
497
+ const u = t._visibleColumns[c];
498
+ if (u?.editable) {
499
+ const a = r;
500
+ a.classList.contains("editing") || this.#a(l, e, u, c, a, !0);
501
+ }
502
+ }), setTimeout(() => {
503
+ let r = n.querySelector(`.cell[data-col="${t._focusCol}"]`);
504
+ if (r?.classList.contains("editing") || (r = n.querySelector(".cell.editing")), r?.classList.contains("editing")) {
505
+ const c = r.querySelector(w);
506
+ try {
507
+ c?.focus({ preventScroll: !0 });
508
+ } catch {
509
+ }
510
+ }
511
+ }, 0);
512
+ }
513
+ /**
514
+ * Commit the currently active row edit.
515
+ */
516
+ commitActiveRowEdit() {
517
+ this.#e !== -1 && this.#i(this.#e, !1);
518
+ }
519
+ /**
520
+ * Cancel the currently active row edit.
521
+ */
522
+ cancelActiveRowEdit() {
523
+ this.#e !== -1 && this.#i(this.#e, !0);
524
+ }
525
+ // #endregion
526
+ // #region Internal Methods
527
+ /**
528
+ * Begin editing a single cell.
529
+ */
530
+ #f(e, t, i) {
531
+ const s = this.grid, n = s._rows[e], l = s._visibleColumns[t];
532
+ !n || !l?.editable || i.classList.contains("editing") || (this.#e !== e && this.#d(e, n), this.#r = t, this.#a(n, e, l, t, i, !1));
533
+ }
534
+ /**
535
+ * Sync the internal grid state with the plugin's editing state.
536
+ */
537
+ #l() {
538
+ const e = this.grid;
539
+ e._activeEditRows = this.#e, e._rowEditSnapshots = this.#s, e._changedRowIndices = this.#t;
540
+ }
541
+ /**
542
+ * Snapshot original row data and mark as editing.
543
+ */
544
+ #d(e, t) {
545
+ this.#e !== e && (this.#s.set(e, { ...t }), this.#e = e, this.#l());
546
+ }
547
+ /**
548
+ * Exit editing for a row.
549
+ */
550
+ #i(e, t) {
551
+ if (this.#e !== e) return;
552
+ const i = this.grid, s = this.#s.get(e), n = i._rows[e], l = i.findRenderedRowElement?.(e);
553
+ if (!t && l && n && l.querySelectorAll(".cell.editing").forEach((c) => {
554
+ const u = Number(c.getAttribute("data-col"));
555
+ if (isNaN(u)) return;
556
+ const a = i._visibleColumns[u];
557
+ if (!a) return;
558
+ const g = c.querySelector("input,textarea,select");
559
+ if (g) {
560
+ let o;
561
+ g instanceof HTMLInputElement && g.type === "checkbox" ? o = g.checked : (o = g.value, a.type === "number" && o !== "" && (o = Number(o))), n[a.field] !== o && this.#c(e, a, o, n);
562
+ }
563
+ }), t && s && n)
564
+ Object.keys(s).forEach((r) => {
565
+ n[r] = s[r];
566
+ }), this.#t.delete(e);
567
+ else if (!t) {
568
+ const r = this.#t.has(e);
569
+ this.emit("row-commit", {
570
+ rowIndex: e,
571
+ row: n,
572
+ changed: r,
573
+ changedRows: this.changedRows,
574
+ changedRowIndices: this.changedRowIndices
575
+ });
576
+ }
577
+ this.#s.delete(e), this.#e = -1, this.#r = -1, this.#l();
578
+ for (const r of this.#n)
579
+ r.startsWith(`${e}:`) && this.#n.delete(r);
580
+ l && (l.querySelectorAll(".cell.editing").forEach((r) => {
581
+ r.classList.remove("editing"), S(r.parentElement);
582
+ }), this.requestRender()), this.#o = !0, l || (this.#u(i), this.#o = !1);
583
+ }
584
+ /**
585
+ * Commit a single cell value change.
586
+ */
587
+ #c(e, t, i, s) {
588
+ const n = t.field;
589
+ if (!b(n) || s[n] === i) return;
590
+ s[n] = i;
591
+ const r = !this.#t.has(e);
592
+ this.#t.add(e), this.#l();
593
+ const u = this.grid.findRenderedRowElement?.(e);
594
+ u && u.classList.add("changed"), this.emit("cell-commit", {
595
+ row: s,
596
+ field: n,
597
+ value: i,
598
+ rowIndex: e,
599
+ changedRows: this.changedRows,
600
+ changedRowIndices: this.changedRowIndices,
601
+ firstTimeForRow: r
602
+ });
603
+ }
604
+ /**
605
+ * Inject an editor into a cell.
606
+ */
607
+ #a(e, t, i, s, n, l) {
608
+ if (!i.editable || n.classList.contains("editing")) return;
609
+ const r = b(i.field) ? e[i.field] : void 0;
610
+ n.classList.add("editing"), this.#n.add(`${t}:${s}`);
611
+ const c = n.parentElement;
612
+ c && L(c);
613
+ let u = !1;
614
+ const a = (h) => {
615
+ u || this.#e === -1 || this.#c(t, i, h, e);
616
+ }, g = () => {
617
+ u = !0, b(i.field) && (e[i.field] = r);
618
+ }, o = document.createElement("div");
619
+ o.className = "tbw-editor-host", n.innerHTML = "", n.appendChild(o), o.addEventListener("keydown", (h) => {
620
+ h.key === "Enter" && (h.stopPropagation(), h.preventDefault(), u = !0, this.#i(t, !1)), h.key === "Escape" && (h.stopPropagation(), h.preventDefault(), g(), this.#i(t, !0));
621
+ });
622
+ const p = i, v = p.__editorTemplate, d = p.editor || (v ? "template" : k(i)), m = r;
623
+ if (d === "template" && v)
624
+ this.#h(o, p, e, r, a, g, l, t);
625
+ else if (typeof d == "string") {
626
+ const h = document.createElement(d);
627
+ h.value = m, h.addEventListener("change", () => a(h.value)), o.appendChild(h), l || queueMicrotask(() => {
628
+ o.querySelector(w)?.focus({ preventScroll: !0 });
629
+ });
630
+ } else if (typeof d == "function") {
631
+ const h = { row: e, value: m, field: i.field, column: i, commit: a, cancel: g }, E = d(h);
632
+ typeof E == "string" ? (o.innerHTML = E, A(o, i, a)) : E instanceof Node && o.appendChild(E), l || queueMicrotask(() => {
633
+ o.querySelector(w)?.focus({ preventScroll: !0 });
634
+ });
635
+ } else if (d && typeof d == "object") {
636
+ const h = this.grid, E = document.createElement("div");
637
+ E.setAttribute("data-external-editor", ""), E.setAttribute("data-field", i.field), o.appendChild(E);
638
+ const y = { row: e, value: m, field: i.field, column: i, commit: a, cancel: g };
639
+ if (d.mount)
640
+ try {
641
+ d.mount({ placeholder: E, context: y, spec: d });
642
+ } catch (C) {
643
+ console.warn(`[tbw-grid] External editor mount error for column '${i.field}':`, C);
644
+ }
645
+ else
646
+ h.dispatchEvent(
647
+ new CustomEvent("mount-external-editor", { detail: { placeholder: E, spec: d, context: y } })
648
+ );
649
+ }
650
+ }
651
+ /**
652
+ * Render a template-based editor.
653
+ */
654
+ #h(e, t, i, s, n, l, r, c) {
655
+ const u = t.__editorTemplate;
656
+ if (!u) return;
657
+ const a = u.cloneNode(!0), g = t.__compiledEditor;
658
+ g ? a.innerHTML = g({
659
+ row: i,
660
+ value: s,
661
+ field: t.field,
662
+ column: t,
663
+ commit: n,
664
+ cancel: l
665
+ }) : a.querySelectorAll("*").forEach((p) => {
666
+ p.childNodes.length === 1 && p.firstChild?.nodeType === Node.TEXT_NODE && (p.textContent = p.textContent?.replace(/{{\s*value\s*}}/g, s == null ? "" : String(s)).replace(/{{\s*row\.([a-zA-Z0-9_]+)\s*}}/g, (v, d) => {
667
+ if (!b(d)) return "";
668
+ const m = i[d];
669
+ return m == null ? "" : String(m);
670
+ }) || "");
671
+ });
672
+ const o = a.querySelector(
673
+ "input,textarea,select"
674
+ );
675
+ if (o) {
676
+ o instanceof HTMLInputElement && o.type === "checkbox" ? o.checked = !!s : o.value = String(s ?? "");
677
+ let p = !1;
678
+ o.addEventListener("blur", () => {
679
+ if (p) return;
680
+ const v = o instanceof HTMLInputElement && o.type === "checkbox" ? o.checked : o.value;
681
+ n(v);
682
+ }), o.addEventListener("keydown", (v) => {
683
+ const d = v;
684
+ if (d.key === "Enter") {
685
+ d.stopPropagation(), d.preventDefault(), p = !0;
686
+ const m = o instanceof HTMLInputElement && o.type === "checkbox" ? o.checked : o.value;
687
+ n(m), this.#i(c, !1);
688
+ }
689
+ d.key === "Escape" && (d.stopPropagation(), d.preventDefault(), l(), this.#i(c, !0));
690
+ }), o instanceof HTMLInputElement && o.type === "checkbox" && o.addEventListener("change", () => n(o.checked)), r || setTimeout(() => o.focus({ preventScroll: !0 }), 0);
691
+ }
692
+ e.appendChild(a);
693
+ }
694
+ /**
695
+ * Restore focus to cell after exiting edit mode.
696
+ */
697
+ #u(e) {
698
+ queueMicrotask(() => {
699
+ try {
700
+ const t = e._focusRow, i = e._focusCol, s = e.findRenderedRowElement?.(t);
701
+ if (s) {
702
+ Array.from(e._bodyEl.querySelectorAll(".cell-focus")).forEach(
703
+ (l) => l.classList.remove("cell-focus")
704
+ );
705
+ const n = s.querySelector(`.cell[data-row="${t}"][data-col="${i}"]`);
706
+ n && (n.classList.add("cell-focus"), n.setAttribute("aria-selected", "true"), n.hasAttribute("tabindex") || n.setAttribute("tabindex", "-1"), n.focus({ preventScroll: !0 }));
707
+ }
708
+ } catch {
709
+ }
710
+ });
711
+ }
712
+ // #endregion
713
+ }
714
+ export {
715
+ q as EditingPlugin,
716
+ w as FOCUSABLE_EDITOR_SELECTOR,
717
+ S as clearEditingState,
718
+ k as defaultEditorFor,
719
+ T as hasEditingCells
720
+ };
721
+ //# sourceMappingURL=index.js.map