@toolbox-web/grid 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +80 -22
  2. package/all.d.ts +1 -0
  3. package/all.d.ts.map +1 -1
  4. package/all.js +557 -365
  5. package/all.js.map +1 -1
  6. package/index.d.ts +1 -1
  7. package/index.d.ts.map +1 -1
  8. package/index.js +903 -769
  9. package/index.js.map +1 -1
  10. package/lib/core/grid.d.ts +102 -3
  11. package/lib/core/grid.d.ts.map +1 -1
  12. package/lib/core/internal/row-animation.d.ts +37 -0
  13. package/lib/core/internal/row-animation.d.ts.map +1 -0
  14. package/lib/core/internal/rows.d.ts.map +1 -1
  15. package/lib/core/internal/shell.d.ts.map +1 -1
  16. package/lib/core/plugin/base-plugin.d.ts +65 -3
  17. package/lib/core/plugin/base-plugin.d.ts.map +1 -1
  18. package/lib/core/plugin/index.d.ts +1 -1
  19. package/lib/core/plugin/index.d.ts.map +1 -1
  20. package/lib/core/plugin/plugin-manager.d.ts +25 -1
  21. package/lib/core/plugin/plugin-manager.d.ts.map +1 -1
  22. package/lib/core/plugin/types.d.ts +62 -0
  23. package/lib/core/plugin/types.d.ts.map +1 -1
  24. package/lib/core/types.d.ts +64 -1
  25. package/lib/core/types.d.ts.map +1 -1
  26. package/lib/plugins/clipboard/index.js +73 -69
  27. package/lib/plugins/clipboard/index.js.map +1 -1
  28. package/lib/plugins/clipboard/types.d.ts +1 -0
  29. package/lib/plugins/clipboard/types.d.ts.map +1 -1
  30. package/lib/plugins/column-virtualization/index.js.map +1 -1
  31. package/lib/plugins/context-menu/index.js.map +1 -1
  32. package/lib/plugins/editing/EditingPlugin.d.ts.map +1 -1
  33. package/lib/plugins/editing/index.js +69 -40
  34. package/lib/plugins/editing/index.js.map +1 -1
  35. package/lib/plugins/export/index.js.map +1 -1
  36. package/lib/plugins/filtering/index.js.map +1 -1
  37. package/lib/plugins/grouping-columns/index.js.map +1 -1
  38. package/lib/plugins/grouping-rows/index.js.map +1 -1
  39. package/lib/plugins/master-detail/MasterDetailPlugin.d.ts.map +1 -1
  40. package/lib/plugins/master-detail/index.js +14 -12
  41. package/lib/plugins/master-detail/index.js.map +1 -1
  42. package/lib/plugins/multi-sort/index.js.map +1 -1
  43. package/lib/plugins/pinned-columns/index.js.map +1 -1
  44. package/lib/plugins/pinned-rows/index.js.map +1 -1
  45. package/lib/plugins/pivot/index.js.map +1 -1
  46. package/lib/plugins/reorder/index.js.map +1 -1
  47. package/lib/plugins/responsive/index.js.map +1 -1
  48. package/lib/plugins/row-reorder/RowReorderPlugin.d.ts +155 -0
  49. package/lib/plugins/row-reorder/RowReorderPlugin.d.ts.map +1 -0
  50. package/lib/plugins/row-reorder/index.d.ts +9 -0
  51. package/lib/plugins/row-reorder/index.d.ts.map +1 -0
  52. package/lib/plugins/row-reorder/index.js +597 -0
  53. package/lib/plugins/row-reorder/index.js.map +1 -0
  54. package/lib/plugins/row-reorder/types.d.ts +80 -0
  55. package/lib/plugins/row-reorder/types.d.ts.map +1 -0
  56. package/lib/plugins/selection/SelectionPlugin.d.ts +13 -0
  57. package/lib/plugins/selection/SelectionPlugin.d.ts.map +1 -1
  58. package/lib/plugins/selection/index.d.ts +1 -1
  59. package/lib/plugins/selection/index.d.ts.map +1 -1
  60. package/lib/plugins/selection/index.js +95 -64
  61. package/lib/plugins/selection/index.js.map +1 -1
  62. package/lib/plugins/selection/types.d.ts +50 -6
  63. package/lib/plugins/selection/types.d.ts.map +1 -1
  64. package/lib/plugins/server-side/index.js.map +1 -1
  65. package/lib/plugins/tree/index.js.map +1 -1
  66. package/lib/plugins/undo-redo/index.js.map +1 -1
  67. package/lib/plugins/visibility/index.js.map +1 -1
  68. package/package.json +21 -4
  69. package/public.d.ts +15 -2
  70. package/public.d.ts.map +1 -1
  71. package/umd/grid.all.umd.js +23 -23
  72. package/umd/grid.all.umd.js.map +1 -1
  73. package/umd/grid.umd.js +15 -15
  74. package/umd/grid.umd.js.map +1 -1
  75. package/umd/plugins/clipboard.umd.js +5 -5
  76. package/umd/plugins/clipboard.umd.js.map +1 -1
  77. package/umd/plugins/editing.umd.js +1 -1
  78. package/umd/plugins/editing.umd.js.map +1 -1
  79. package/umd/plugins/master-detail.umd.js +1 -1
  80. package/umd/plugins/master-detail.umd.js.map +1 -1
  81. package/umd/plugins/row-reorder.umd.js +2 -0
  82. package/umd/plugins/row-reorder.umd.js.map +1 -0
  83. package/umd/plugins/selection.umd.js +2 -2
  84. package/umd/plugins/selection.umd.js.map +1 -1
@@ -0,0 +1,597 @@
1
+ function p(l) {
2
+ l && l.querySelectorAll(".cell-focus").forEach((e) => e.classList.remove("cell-focus"));
3
+ }
4
+ const v = 'input,select,textarea,[contenteditable="true"],[contenteditable=""],[tabindex]:not([tabindex="-1"])', R = document.createElement("template");
5
+ R.innerHTML = '<div class="cell" role="gridcell" part="cell"></div>';
6
+ const m = document.createElement("template");
7
+ m.innerHTML = '<div class="data-grid-row" role="row" part="row"></div>';
8
+ function h(l, e) {
9
+ if (l._virtualization?.enabled) {
10
+ const { rowHeight: o, container: s, viewportEl: a } = l._virtualization, c = s, d = a?.clientHeight ?? c?.clientHeight ?? 0;
11
+ if (c && d > 0) {
12
+ const u = l._focusRow * o;
13
+ u < c.scrollTop ? c.scrollTop = u : u + o > c.scrollTop + d && (c.scrollTop = u - d + o);
14
+ }
15
+ }
16
+ const r = l._activeEditRows !== void 0 && l._activeEditRows !== -1;
17
+ r || l.refreshVirtualWindow(!1), p(l._bodyEl), Array.from(l._bodyEl.querySelectorAll('[aria-selected="true"]')).forEach((o) => {
18
+ o.setAttribute("aria-selected", "false");
19
+ });
20
+ const t = l._focusRow, i = l._virtualization.start ?? 0, n = l._virtualization.end ?? l._rows.length;
21
+ if (t >= i && t < n) {
22
+ const o = l._bodyEl.querySelectorAll(".data-grid-row")[t - i];
23
+ let s = o?.children[l._focusCol];
24
+ if ((!s || !s.classList?.contains("cell")) && (s = o?.querySelector(`.cell[data-col="${l._focusCol}"]`) ?? o?.querySelector(".cell[data-col]")), s) {
25
+ s.classList.add("cell-focus"), s.setAttribute("aria-selected", "true");
26
+ const a = l.querySelector(".tbw-scroll-area");
27
+ if (a && s && !r) {
28
+ const c = l._getHorizontalScrollOffsets?.(o ?? void 0, s) ?? { left: 0, right: 0 };
29
+ if (!c.skipScroll) {
30
+ const d = s.getBoundingClientRect(), u = a.getBoundingClientRect(), g = d.left - u.left + a.scrollLeft, f = g + d.width, w = a.scrollLeft + c.left, b = a.scrollLeft + a.clientWidth - c.right;
31
+ g < w ? a.scrollLeft = g - c.left : f > b && (a.scrollLeft = f - a.clientWidth + c.right);
32
+ }
33
+ }
34
+ if (l._activeEditRows !== void 0 && l._activeEditRows !== -1 && s.classList.contains("editing")) {
35
+ const c = s.querySelector(v);
36
+ if (c && document.activeElement !== c)
37
+ try {
38
+ c.focus({ preventScroll: !0 });
39
+ } catch {
40
+ }
41
+ } else if (!s.contains(document.activeElement)) {
42
+ s.hasAttribute("tabindex") || s.setAttribute("tabindex", "-1");
43
+ try {
44
+ s.focus({ preventScroll: !0 });
45
+ } catch {
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ const _ = {
52
+ expand: "▶",
53
+ collapse: "▼",
54
+ sortAsc: "▲",
55
+ sortDesc: "▼",
56
+ sortNone: "⇅",
57
+ submenuArrow: "▶",
58
+ dragHandle: "⋮⋮",
59
+ toolPanel: "☰"
60
+ };
61
+ class y {
62
+ /**
63
+ * Plugin dependencies - declare other plugins this one requires.
64
+ *
65
+ * Dependencies are validated when the plugin is attached.
66
+ * Required dependencies throw an error if missing.
67
+ * Optional dependencies log an info message if missing.
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * static readonly dependencies: PluginDependency[] = [
72
+ * { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },
73
+ * { name: 'selection', required: false, reason: 'Enables selection-based undo' },
74
+ * ];
75
+ * ```
76
+ */
77
+ static dependencies;
78
+ /**
79
+ * Plugin manifest - declares owned properties, config rules, and hook priorities.
80
+ *
81
+ * This is read by the configuration validator to:
82
+ * - Validate that required plugins are loaded when their properties are used
83
+ * - Execute configRules to detect invalid/conflicting settings
84
+ * - Order hook execution based on priority
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * static override readonly manifest: PluginManifest<MyConfig> = {
89
+ * ownedProperties: [
90
+ * { property: 'myProp', level: 'column', description: 'the "myProp" column property' },
91
+ * ],
92
+ * configRules: [
93
+ * { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },
94
+ * ],
95
+ * };
96
+ * ```
97
+ */
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ static manifest;
100
+ /**
101
+ * Plugin version - defaults to grid version for built-in plugins.
102
+ * Third-party plugins can override with their own semver.
103
+ */
104
+ version = typeof __GRID_VERSION__ < "u" ? __GRID_VERSION__ : "dev";
105
+ /** CSS styles to inject into the grid's shadow DOM */
106
+ styles;
107
+ /** Custom cell renderers keyed by type name */
108
+ cellRenderers;
109
+ /** Custom header renderers keyed by type name */
110
+ headerRenderers;
111
+ /** Custom cell editors keyed by type name */
112
+ cellEditors;
113
+ /** The grid instance this plugin is attached to */
114
+ grid;
115
+ /** Plugin configuration - merged with defaults in attach() */
116
+ config;
117
+ /** User-provided configuration from constructor */
118
+ userConfig;
119
+ /**
120
+ * Plugin-level AbortController for event listener cleanup.
121
+ * Created fresh in attach(), aborted in detach().
122
+ * This ensures event listeners are properly cleaned up when plugins are re-attached.
123
+ */
124
+ #e;
125
+ /**
126
+ * Default configuration - subclasses should override this getter.
127
+ * Note: This must be a getter (not property initializer) for proper inheritance
128
+ * since property initializers run after parent constructor.
129
+ */
130
+ get defaultConfig() {
131
+ return {};
132
+ }
133
+ constructor(e = {}) {
134
+ this.userConfig = e;
135
+ }
136
+ /**
137
+ * Called when the plugin is attached to a grid.
138
+ * Override to set up event listeners, initialize state, etc.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * attach(grid: GridElement): void {
143
+ * super.attach(grid);
144
+ * // Set up document-level listeners with auto-cleanup
145
+ * document.addEventListener('keydown', this.handleEscape, {
146
+ * signal: this.disconnectSignal
147
+ * });
148
+ * }
149
+ * ```
150
+ */
151
+ attach(e) {
152
+ this.#e?.abort(), this.#e = new AbortController(), this.grid = e, this.config = { ...this.defaultConfig, ...this.userConfig };
153
+ }
154
+ /**
155
+ * Called when the plugin is detached from a grid.
156
+ * Override to clean up event listeners, timers, etc.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * detach(): void {
161
+ * // Clean up any state not handled by disconnectSignal
162
+ * this.selectedRows.clear();
163
+ * this.cache = null;
164
+ * }
165
+ * ```
166
+ */
167
+ detach() {
168
+ this.#e?.abort(), this.#e = void 0;
169
+ }
170
+ /**
171
+ * Get another plugin instance from the same grid.
172
+ * Use for inter-plugin communication.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * const selection = this.getPlugin(SelectionPlugin);
177
+ * if (selection) {
178
+ * const selectedRows = selection.getSelectedRows();
179
+ * }
180
+ * ```
181
+ */
182
+ getPlugin(e) {
183
+ return this.grid?.getPlugin(e);
184
+ }
185
+ /**
186
+ * Emit a custom event from the grid.
187
+ */
188
+ emit(e, r) {
189
+ this.grid?.dispatchEvent?.(new CustomEvent(e, { detail: r, bubbles: !0 }));
190
+ }
191
+ /**
192
+ * Emit a cancelable custom event from the grid.
193
+ * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise
194
+ */
195
+ emitCancelable(e, r) {
196
+ const t = new CustomEvent(e, { detail: r, bubbles: !0, cancelable: !0 });
197
+ return this.grid?.dispatchEvent?.(t), t.defaultPrevented;
198
+ }
199
+ /**
200
+ * Request a re-render of the grid.
201
+ */
202
+ requestRender() {
203
+ this.grid?.requestRender?.();
204
+ }
205
+ /**
206
+ * Request a re-render and restore focus styling afterward.
207
+ * Use this when a plugin action (like expand/collapse) triggers a render
208
+ * but needs to maintain keyboard navigation focus.
209
+ */
210
+ requestRenderWithFocus() {
211
+ this.grid?.requestRenderWithFocus?.();
212
+ }
213
+ /**
214
+ * Request a lightweight style update without rebuilding DOM.
215
+ * Use this instead of requestRender() when only CSS classes need updating.
216
+ */
217
+ requestAfterRender() {
218
+ this.grid?.requestAfterRender?.();
219
+ }
220
+ /**
221
+ * Get the current rows from the grid.
222
+ */
223
+ get rows() {
224
+ return this.grid?.rows ?? [];
225
+ }
226
+ /**
227
+ * Get the original unfiltered/unprocessed rows from the grid.
228
+ * Use this when you need all source data regardless of active filters.
229
+ */
230
+ get sourceRows() {
231
+ return this.grid?.sourceRows ?? [];
232
+ }
233
+ /**
234
+ * Get the current columns from the grid.
235
+ */
236
+ get columns() {
237
+ return this.grid?.columns ?? [];
238
+ }
239
+ /**
240
+ * Get only visible columns from the grid (excludes hidden).
241
+ * Use this for rendering that needs to match the grid template.
242
+ */
243
+ get visibleColumns() {
244
+ return this.grid?._visibleColumns ?? [];
245
+ }
246
+ /**
247
+ * Get the grid as an HTMLElement for direct DOM operations.
248
+ * Use sparingly - prefer the typed GridElementRef API when possible.
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * const width = this.gridElement.clientWidth;
253
+ * this.gridElement.classList.add('my-plugin-active');
254
+ * ```
255
+ */
256
+ get gridElement() {
257
+ return this.grid;
258
+ }
259
+ /**
260
+ * Get the disconnect signal for event listener cleanup.
261
+ * This signal is aborted when the grid disconnects from the DOM.
262
+ * Use this when adding event listeners that should be cleaned up automatically.
263
+ *
264
+ * Best for:
265
+ * - Document/window-level listeners added in attach()
266
+ * - Listeners on the grid element itself
267
+ * - Any listener that should persist across renders
268
+ *
269
+ * Not needed for:
270
+ * - Listeners on elements created in afterRender() (removed with element)
271
+ *
272
+ * @example
273
+ * element.addEventListener('click', handler, { signal: this.disconnectSignal });
274
+ * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });
275
+ */
276
+ get disconnectSignal() {
277
+ return this.#e?.signal ?? this.grid?.disconnectSignal;
278
+ }
279
+ /**
280
+ * Get the grid-level icons configuration.
281
+ * Returns merged icons (user config + defaults).
282
+ */
283
+ get gridIcons() {
284
+ const e = this.grid?.gridConfig?.icons ?? {};
285
+ return { ..._, ...e };
286
+ }
287
+ // #region Animation Helpers
288
+ /**
289
+ * Check if animations are enabled at the grid level.
290
+ * Respects gridConfig.animation.mode and the CSS variable set by the grid.
291
+ *
292
+ * Plugins should use this to skip animations when:
293
+ * - Animation mode is 'off' or `false`
294
+ * - User prefers reduced motion and mode is 'reduced-motion' (default)
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * private get animationStyle(): 'slide' | 'fade' | false {
299
+ * if (!this.isAnimationEnabled) return false;
300
+ * return this.config.animation ?? 'slide';
301
+ * }
302
+ * ```
303
+ */
304
+ get isAnimationEnabled() {
305
+ const e = this.grid?.effectiveConfig?.animation?.mode ?? "reduced-motion";
306
+ if (e === !1 || e === "off") return !1;
307
+ if (e === !0 || e === "on") return !0;
308
+ const r = this.gridElement;
309
+ return r ? getComputedStyle(r).getPropertyValue("--tbw-animation-enabled").trim() !== "0" : !0;
310
+ }
311
+ /**
312
+ * Get the animation duration in milliseconds from CSS variable.
313
+ * Falls back to 200ms if not set.
314
+ *
315
+ * Plugins can use this for their animation timing to stay consistent
316
+ * with the grid-level animation.duration setting.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * element.animate(keyframes, { duration: this.animationDuration });
321
+ * ```
322
+ */
323
+ get animationDuration() {
324
+ const e = this.gridElement;
325
+ if (e) {
326
+ const r = getComputedStyle(e).getPropertyValue("--tbw-animation-duration").trim(), t = parseInt(r, 10);
327
+ if (!isNaN(t)) return t;
328
+ }
329
+ return 200;
330
+ }
331
+ // #endregion
332
+ /**
333
+ * Resolve an icon value to string or HTMLElement.
334
+ * Checks plugin config first, then grid-level icons, then defaults.
335
+ *
336
+ * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')
337
+ * @param pluginOverride - Optional plugin-level override
338
+ * @returns The resolved icon value
339
+ */
340
+ resolveIcon(e, r) {
341
+ return r !== void 0 ? r : this.gridIcons[e];
342
+ }
343
+ /**
344
+ * Set an icon value on an element.
345
+ * Handles both string (text/HTML) and HTMLElement values.
346
+ *
347
+ * @param element - The element to set the icon on
348
+ * @param icon - The icon value (string or HTMLElement)
349
+ */
350
+ setIcon(e, r) {
351
+ typeof r == "string" ? e.innerHTML = r : r instanceof HTMLElement && (e.innerHTML = "", e.appendChild(r.cloneNode(!0)));
352
+ }
353
+ /**
354
+ * Log a warning message.
355
+ */
356
+ warn(e) {
357
+ console.warn(`[tbw-grid:${this.name}] ${e}`);
358
+ }
359
+ // #endregion
360
+ }
361
+ const E = '.dg-row-drag-handle{display:flex;align-items:center;justify-content:center;cursor:grab;-webkit-user-select:none;user-select:none;color:var(--tbw-color-fg-muted, #999);transition:color .15s ease;font-size:14px;letter-spacing:-2px}.dg-row-drag-handle:hover{color:var(--tbw-color-fg, #333)}.dg-row-drag-handle:active{cursor:grabbing}.data-grid-row.dragging{opacity:.6}.data-grid-row.drop-target{position:relative}.data-grid-row.drop-target.drop-before:before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background-color:var(--tbw-color-accent, #1976d2);z-index:10}.data-grid-row.drop-target.drop-after:after{content:"";position:absolute;bottom:0;left:0;right:0;height:2px;background-color:var(--tbw-color-accent, #1976d2);z-index:10}.data-grid-row.keyboard-moving{background-color:var(--tbw-color-bg-selected, #e3f2fd);box-shadow:0 0 0 1px var(--tbw-color-accent, #1976d2) inset}.data-grid-row.animate-flip{transition:transform var(--tbw-animation-duration, .2s) ease-out}', C = "__tbw_row_drag";
362
+ class D extends y {
363
+ /** @internal */
364
+ name = "rowReorder";
365
+ /** @internal */
366
+ styles = E;
367
+ /** @internal */
368
+ get defaultConfig() {
369
+ return {
370
+ enableKeyboard: !0,
371
+ showDragHandle: !0,
372
+ dragHandlePosition: "left",
373
+ dragHandleWidth: 40,
374
+ debounceMs: 150,
375
+ animation: "flip"
376
+ };
377
+ }
378
+ // #region Internal State
379
+ isDragging = !1;
380
+ draggedRowIndex = null;
381
+ dropRowIndex = null;
382
+ pendingMove = null;
383
+ debounceTimer = null;
384
+ /** Column index to use when flushing pending move */
385
+ lastFocusCol = 0;
386
+ // #endregion
387
+ // #region Lifecycle
388
+ /** @internal */
389
+ detach() {
390
+ this.clearDebounceTimer(), this.isDragging = !1, this.draggedRowIndex = null, this.dropRowIndex = null, this.pendingMove = null;
391
+ }
392
+ // #endregion
393
+ // #region Hooks
394
+ /** @internal */
395
+ processColumns(e) {
396
+ if (!this.config.showDragHandle)
397
+ return [...e];
398
+ const r = {
399
+ field: C,
400
+ header: "",
401
+ width: this.config.dragHandleWidth ?? 40,
402
+ resizable: !1,
403
+ sortable: !1,
404
+ filterable: !1,
405
+ meta: {
406
+ lockPosition: !0,
407
+ suppressMovable: !0,
408
+ utility: !0
409
+ },
410
+ viewRenderer: () => {
411
+ const t = document.createElement("div");
412
+ return t.className = "dg-row-drag-handle", t.setAttribute("aria-label", "Drag to reorder"), t.setAttribute("role", "button"), t.setAttribute("tabindex", "-1"), t.draggable = !0, this.setIcon(t, this.resolveIcon("dragHandle")), t;
413
+ }
414
+ };
415
+ return this.config.dragHandlePosition === "right" ? [...e, r] : [r, ...e];
416
+ }
417
+ /** @internal */
418
+ afterRender() {
419
+ if (!this.config.showDragHandle) return;
420
+ const e = this.gridElement;
421
+ if (!e) return;
422
+ e.querySelectorAll(".dg-row-drag-handle").forEach((i) => {
423
+ const n = i;
424
+ if (n.getAttribute("data-drag-bound")) return;
425
+ n.setAttribute("data-drag-bound", "true");
426
+ const o = n.closest(".data-grid-row");
427
+ o && this.setupHandleDragListeners(n, o);
428
+ }), e.querySelectorAll(".data-grid-row").forEach((i) => {
429
+ const n = i;
430
+ n.getAttribute("data-drop-bound") || (n.setAttribute("data-drop-bound", "true"), this.setupRowDropListeners(n));
431
+ });
432
+ }
433
+ /**
434
+ * Handle Ctrl+Arrow keyboard shortcuts for row reordering.
435
+ * @internal
436
+ */
437
+ onKeyDown(e) {
438
+ if (!this.config.enableKeyboard || !e.ctrlKey || e.key !== "ArrowUp" && e.key !== "ArrowDown")
439
+ return;
440
+ const r = this.grid, t = r._focusRow, i = r._rows ?? this.sourceRows;
441
+ if (t < 0 || t >= i.length) return;
442
+ const n = e.key === "ArrowUp" ? "up" : "down", o = n === "up" ? t - 1 : t + 1;
443
+ if (o < 0 || o >= i.length) return;
444
+ const s = i[t];
445
+ if (!(this.config.canMove && !this.config.canMove(s, t, o, n)))
446
+ return this.handleKeyboardMove(s, t, o, n, r._focusCol), e.preventDefault(), e.stopPropagation(), !0;
447
+ }
448
+ /**
449
+ * Flush pending keyboard moves when user clicks a cell.
450
+ * This commits the move immediately so focus works correctly.
451
+ * @internal
452
+ */
453
+ onCellClick() {
454
+ this.flushPendingMove();
455
+ }
456
+ // #endregion
457
+ // #region Public API
458
+ /**
459
+ * Move a row to a new position programmatically.
460
+ * @param fromIndex - Current index of the row
461
+ * @param toIndex - Target index
462
+ */
463
+ moveRow(e, r) {
464
+ const t = [...this.sourceRows];
465
+ if (e < 0 || e >= t.length || r < 0 || r >= t.length || e === r) return;
466
+ const i = r < e ? "up" : "down", n = t[e];
467
+ this.config.canMove && !this.config.canMove(n, e, r, i) || this.executeMove(n, e, r, "keyboard");
468
+ }
469
+ /**
470
+ * Check if a row can be moved to a position.
471
+ * @param fromIndex - Current index of the row
472
+ * @param toIndex - Target index
473
+ */
474
+ canMoveRow(e, r) {
475
+ const t = this.sourceRows;
476
+ if (e < 0 || e >= t.length || r < 0 || r >= t.length || e === r) return !1;
477
+ if (!this.config.canMove) return !0;
478
+ const i = r < e ? "up" : "down";
479
+ return this.config.canMove(t[e], e, r, i);
480
+ }
481
+ // #endregion
482
+ // #region Private Methods
483
+ /**
484
+ * Set up drag start/end listeners on the drag handle element.
485
+ */
486
+ setupHandleDragListeners(e, r) {
487
+ e.addEventListener("dragstart", (t) => {
488
+ const i = this.getRowIndex(r);
489
+ i < 0 || (this.isDragging = !0, this.draggedRowIndex = i, t.dataTransfer && (t.dataTransfer.effectAllowed = "move", t.dataTransfer.setData("text/plain", String(i))), r.classList.add("dragging"));
490
+ }), e.addEventListener("dragend", () => {
491
+ this.isDragging = !1, this.draggedRowIndex = null, this.dropRowIndex = null, this.clearDragClasses();
492
+ });
493
+ }
494
+ /**
495
+ * Set up drop target listeners on a row element.
496
+ * All rows are valid drop targets during drag operations.
497
+ */
498
+ setupRowDropListeners(e) {
499
+ e.addEventListener("dragover", (r) => {
500
+ if (r.preventDefault(), !this.isDragging || this.draggedRowIndex === null) return;
501
+ const t = this.getRowIndex(e);
502
+ if (t < 0 || t === this.draggedRowIndex) return;
503
+ const i = e.getBoundingClientRect(), n = i.top + i.height / 2, o = r.clientY < n;
504
+ this.dropRowIndex = o ? t : t + 1, e.classList.add("drop-target"), e.classList.toggle("drop-before", o), e.classList.toggle("drop-after", !o);
505
+ }), e.addEventListener("dragleave", () => {
506
+ e.classList.remove("drop-target", "drop-before", "drop-after");
507
+ }), e.addEventListener("drop", (r) => {
508
+ r.preventDefault();
509
+ const t = this.draggedRowIndex;
510
+ let i = this.dropRowIndex;
511
+ if (!(!this.isDragging || t === null || i === null) && (i > t && i--, t !== i)) {
512
+ const o = this.sourceRows[t], s = i < t ? "up" : "down";
513
+ (!this.config.canMove || this.config.canMove(o, t, i, s)) && this.executeMove(o, t, i, "drag");
514
+ }
515
+ });
516
+ }
517
+ /**
518
+ * Handle debounced keyboard moves.
519
+ * Rows move immediately for visual feedback, but the event emission is debounced.
520
+ */
521
+ handleKeyboardMove(e, r, t, i, n) {
522
+ this.pendingMove ? this.pendingMove.currentIndex = t : this.pendingMove = {
523
+ originalIndex: r,
524
+ currentIndex: t,
525
+ row: e
526
+ }, this.lastFocusCol = n;
527
+ const o = this.grid, s = [...o._rows ?? this.sourceRows], [a] = s.splice(r, 1);
528
+ s.splice(t, 0, a), o._rows = s, o._focusRow = t, o._focusCol = n, o.refreshVirtualWindow(!0), h(o), this.clearDebounceTimer(), this.debounceTimer = setTimeout(() => {
529
+ this.flushPendingMove();
530
+ }, this.config.debounceMs ?? 300);
531
+ }
532
+ /**
533
+ * Flush the pending move by emitting the event.
534
+ * Called when debounce timer fires or user clicks elsewhere.
535
+ */
536
+ flushPendingMove() {
537
+ if (this.clearDebounceTimer(), !this.pendingMove) return;
538
+ const { originalIndex: e, currentIndex: r, row: t } = this.pendingMove;
539
+ if (this.pendingMove = null, e === r) return;
540
+ const i = {
541
+ row: t,
542
+ fromIndex: e,
543
+ toIndex: r,
544
+ rows: [...this.sourceRows],
545
+ source: "keyboard"
546
+ };
547
+ if (this.emitCancelable("row-move", i)) {
548
+ const o = [...this.sourceRows], [s] = o.splice(r, 1);
549
+ o.splice(e, 0, s);
550
+ const a = this.grid;
551
+ a._rows = o, a._focusRow = e, a._focusCol = this.lastFocusCol, a.refreshVirtualWindow(!0), h(a);
552
+ }
553
+ }
554
+ /**
555
+ * Execute a row move and emit the event.
556
+ */
557
+ executeMove(e, r, t, i) {
558
+ const n = [...this.sourceRows], [o] = n.splice(r, 1);
559
+ n.splice(t, 0, o);
560
+ const s = {
561
+ row: e,
562
+ fromIndex: r,
563
+ toIndex: t,
564
+ rows: n,
565
+ source: i
566
+ };
567
+ this.emitCancelable("row-move", s) || (this.grid.rows = n);
568
+ }
569
+ /**
570
+ * Get the row index from a row element by checking data-row attribute on cells.
571
+ * This is consistent with how other plugins retrieve row indices.
572
+ */
573
+ getRowIndex(e) {
574
+ const r = e.querySelector(".cell[data-row]");
575
+ return r ? parseInt(r.getAttribute("data-row") ?? "-1", 10) : -1;
576
+ }
577
+ /**
578
+ * Clear all drag-related classes from rows.
579
+ */
580
+ clearDragClasses() {
581
+ this.gridElement?.querySelectorAll(".data-grid-row").forEach((e) => {
582
+ e.classList.remove("dragging", "drop-target", "drop-before", "drop-after");
583
+ });
584
+ }
585
+ /**
586
+ * Clear the debounce timer.
587
+ */
588
+ clearDebounceTimer() {
589
+ this.debounceTimer && (clearTimeout(this.debounceTimer), this.debounceTimer = null);
590
+ }
591
+ // #endregion
592
+ }
593
+ export {
594
+ C as ROW_DRAG_HANDLE_FIELD,
595
+ D as RowReorderPlugin
596
+ };
597
+ //# sourceMappingURL=index.js.map