@alaarab/ogrid-js 2.0.22 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/OGrid.js +189 -503
- package/dist/esm/OGridEventWiring.js +178 -0
- package/dist/esm/OGridRendering.js +269 -0
- package/dist/esm/components/ColumnChooser.js +26 -3
- package/dist/esm/components/InlineCellEditor.js +18 -36
- package/dist/esm/index.js +2 -0
- package/dist/esm/renderer/TableRenderer.js +102 -61
- package/dist/esm/state/ClipboardState.js +8 -54
- package/dist/esm/state/ColumnPinningState.js +1 -2
- package/dist/esm/state/ColumnReorderState.js +8 -1
- package/dist/esm/state/EventEmitter.js +3 -2
- package/dist/esm/state/FillHandleState.js +27 -41
- package/dist/esm/state/GridState.js +36 -10
- package/dist/esm/state/HeaderFilterState.js +19 -11
- package/dist/esm/state/KeyboardNavState.js +19 -132
- package/dist/esm/state/RowSelectionState.js +6 -15
- package/dist/esm/state/SideBarState.js +1 -1
- package/dist/esm/state/TableLayoutState.js +6 -4
- package/dist/esm/utils/getCellCoordinates.js +15 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/types/OGrid.d.ts +97 -9
- package/dist/types/OGridEventWiring.d.ts +60 -0
- package/dist/types/OGridRendering.d.ts +93 -0
- package/dist/types/components/ColumnChooser.d.ts +5 -0
- package/dist/types/components/InlineCellEditor.d.ts +5 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/renderer/TableRenderer.d.ts +12 -5
- package/dist/types/state/EventEmitter.d.ts +1 -1
- package/dist/types/state/FillHandleState.d.ts +1 -1
- package/dist/types/state/GridState.d.ts +7 -1
- package/dist/types/state/HeaderFilterState.d.ts +2 -0
- package/dist/types/types/gridTypes.d.ts +15 -0
- package/dist/types/utils/getCellCoordinates.d.ts +8 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +11 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getCellValue, buildHeaderRows, isInSelectionRange, ROW_NUMBER_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
2
|
-
|
|
1
|
+
import { getCellValue, buildHeaderRows, isInSelectionRange, ROW_NUMBER_COLUMN_WIDTH, CHECKBOX_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
2
|
+
import { getCellCoordinates } from '../utils/getCellCoordinates';
|
|
3
3
|
export class TableRenderer {
|
|
4
4
|
constructor(container, state) {
|
|
5
5
|
this.table = null;
|
|
@@ -17,6 +17,9 @@ export class TableRenderer {
|
|
|
17
17
|
this._tbodyMousedownHandler = null;
|
|
18
18
|
this._tbodyDblclickHandler = null;
|
|
19
19
|
this._tbodyContextmenuHandler = null;
|
|
20
|
+
// Delegated event handlers bound to thead (avoids per-<th> inline listeners)
|
|
21
|
+
this._theadClickHandler = null;
|
|
22
|
+
this._theadMousedownHandler = null;
|
|
20
23
|
// State tracking for incremental DOM patching
|
|
21
24
|
this.lastActiveCell = null;
|
|
22
25
|
this.lastSelectionRange = null;
|
|
@@ -47,11 +50,10 @@ export class TableRenderer {
|
|
|
47
50
|
const cell = target.closest('td[data-row-index]');
|
|
48
51
|
if (!cell)
|
|
49
52
|
return null;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (Number.isNaN(rowIndex) || Number.isNaN(colIndex))
|
|
53
|
+
const coords = getCellCoordinates(cell);
|
|
54
|
+
if (!coords)
|
|
53
55
|
return null;
|
|
54
|
-
return { el: cell, rowIndex, colIndex };
|
|
56
|
+
return { el: cell, rowIndex: coords.rowIndex, colIndex: coords.colIndex };
|
|
55
57
|
}
|
|
56
58
|
attachBodyDelegation() {
|
|
57
59
|
if (!this.tbody)
|
|
@@ -60,13 +62,19 @@ export class TableRenderer {
|
|
|
60
62
|
const cell = this.getCellFromEvent(e);
|
|
61
63
|
if (!cell)
|
|
62
64
|
return;
|
|
63
|
-
this.interactionState?.onCellClick?.(cell.rowIndex, cell.colIndex, e);
|
|
65
|
+
this.interactionState?.onCellClick?.({ rowIndex: cell.rowIndex, colIndex: cell.colIndex, event: e });
|
|
64
66
|
};
|
|
65
67
|
this._tbodyMousedownHandler = (e) => {
|
|
68
|
+
// Fill handle mousedown — delegated from per-cell inline listener
|
|
69
|
+
const target = e.target;
|
|
70
|
+
if (target.classList.contains('ogrid-fill-handle') || target.getAttribute('data-fill-handle') === 'true') {
|
|
71
|
+
this.interactionState?.onFillHandleMouseDown?.(e);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
66
74
|
const cell = this.getCellFromEvent(e);
|
|
67
75
|
if (!cell)
|
|
68
76
|
return;
|
|
69
|
-
this.interactionState?.onCellMouseDown?.(cell.rowIndex, cell.colIndex, e);
|
|
77
|
+
this.interactionState?.onCellMouseDown?.({ rowIndex: cell.rowIndex, colIndex: cell.colIndex, event: e });
|
|
70
78
|
};
|
|
71
79
|
this._tbodyDblclickHandler = (e) => {
|
|
72
80
|
const cell = this.getCellFromEvent(e);
|
|
@@ -79,13 +87,13 @@ export class TableRenderer {
|
|
|
79
87
|
if (!item)
|
|
80
88
|
return;
|
|
81
89
|
const rowId = this.state.getRowId(item);
|
|
82
|
-
this.interactionState?.onCellDoubleClick?.(cell.rowIndex, cell.colIndex, rowId, columnId);
|
|
90
|
+
this.interactionState?.onCellDoubleClick?.({ rowIndex: cell.rowIndex, colIndex: cell.colIndex, rowId, columnId });
|
|
83
91
|
};
|
|
84
92
|
this._tbodyContextmenuHandler = (e) => {
|
|
85
93
|
const cell = this.getCellFromEvent(e);
|
|
86
94
|
if (!cell)
|
|
87
95
|
return;
|
|
88
|
-
this.interactionState?.onCellContextMenu?.(cell.rowIndex, cell.colIndex, e);
|
|
96
|
+
this.interactionState?.onCellContextMenu?.({ rowIndex: cell.rowIndex, colIndex: cell.colIndex, event: e });
|
|
89
97
|
};
|
|
90
98
|
this.tbody.addEventListener('click', this._tbodyClickHandler, { passive: true });
|
|
91
99
|
this.tbody.addEventListener('mousedown', this._tbodyMousedownHandler);
|
|
@@ -108,6 +116,57 @@ export class TableRenderer {
|
|
|
108
116
|
this._tbodyDblclickHandler = null;
|
|
109
117
|
this._tbodyContextmenuHandler = null;
|
|
110
118
|
}
|
|
119
|
+
/** Attach delegated event listeners to <thead> for sort clicks, resize, reorder, and filter icon clicks. */
|
|
120
|
+
attachHeaderDelegation() {
|
|
121
|
+
if (!this.thead)
|
|
122
|
+
return;
|
|
123
|
+
// Sort clicks and filter icon clicks use inline listeners for stale-reference compatibility
|
|
124
|
+
// (tests hold references to <th> elements that become detached after header re-render).
|
|
125
|
+
// Delegation handles resize and column reorder mousedown events only.
|
|
126
|
+
this._theadClickHandler = null;
|
|
127
|
+
this._theadMousedownHandler = (e) => {
|
|
128
|
+
const target = e.target;
|
|
129
|
+
// Resize handle mousedown
|
|
130
|
+
if (target.classList.contains('ogrid-resize-handle')) {
|
|
131
|
+
e.stopPropagation();
|
|
132
|
+
const th = target.closest('th[data-column-id]');
|
|
133
|
+
if (!th)
|
|
134
|
+
return;
|
|
135
|
+
const columnId = th.getAttribute('data-column-id');
|
|
136
|
+
if (columnId) {
|
|
137
|
+
const rect = th.getBoundingClientRect();
|
|
138
|
+
this.interactionState?.onResizeStart?.(columnId, e.clientX, rect.width);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Don't start reorder from filter icon
|
|
143
|
+
if (target.classList.contains('ogrid-filter-icon'))
|
|
144
|
+
return;
|
|
145
|
+
// Column reorder mousedown
|
|
146
|
+
if (this.interactionState?.onColumnReorderStart) {
|
|
147
|
+
const th = target.closest('th[data-column-id]');
|
|
148
|
+
if (!th)
|
|
149
|
+
return;
|
|
150
|
+
const columnId = th.getAttribute('data-column-id');
|
|
151
|
+
if (columnId) {
|
|
152
|
+
this.interactionState.onColumnReorderStart(columnId, e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
if (this._theadClickHandler)
|
|
157
|
+
this.thead.addEventListener('click', this._theadClickHandler);
|
|
158
|
+
this.thead.addEventListener('mousedown', this._theadMousedownHandler);
|
|
159
|
+
}
|
|
160
|
+
detachHeaderDelegation() {
|
|
161
|
+
if (!this.thead)
|
|
162
|
+
return;
|
|
163
|
+
if (this._theadClickHandler)
|
|
164
|
+
this.thead.removeEventListener('click', this._theadClickHandler);
|
|
165
|
+
if (this._theadMousedownHandler)
|
|
166
|
+
this.thead.removeEventListener('mousedown', this._theadMousedownHandler);
|
|
167
|
+
this._theadClickHandler = null;
|
|
168
|
+
this._theadMousedownHandler = null;
|
|
169
|
+
}
|
|
111
170
|
getWrapperElement() {
|
|
112
171
|
return this.wrapperEl;
|
|
113
172
|
}
|
|
@@ -121,13 +180,11 @@ export class TableRenderer {
|
|
|
121
180
|
wrapper.setAttribute('role', 'grid');
|
|
122
181
|
wrapper.setAttribute('tabindex', '0'); // Make focusable for keyboard nav
|
|
123
182
|
wrapper.style.position = 'relative'; // For MarchingAnts absolute positioning
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
wrapper.style.setProperty('--ogrid-row-height', `${rowHeight}px`);
|
|
183
|
+
if (this.state.rowHeight) {
|
|
184
|
+
wrapper.style.setProperty('--ogrid-row-height', `${this.state.rowHeight}px`);
|
|
127
185
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
wrapper.setAttribute('aria-label', ariaLabel);
|
|
186
|
+
if (this.state.ariaLabel) {
|
|
187
|
+
wrapper.setAttribute('aria-label', this.state.ariaLabel);
|
|
131
188
|
}
|
|
132
189
|
this.wrapperEl = wrapper;
|
|
133
190
|
// Create table
|
|
@@ -136,6 +193,7 @@ export class TableRenderer {
|
|
|
136
193
|
// Render header
|
|
137
194
|
this.thead = document.createElement('thead');
|
|
138
195
|
this.renderHeader();
|
|
196
|
+
this.attachHeaderDelegation();
|
|
139
197
|
this.table.appendChild(this.thead);
|
|
140
198
|
// Render body
|
|
141
199
|
this.tbody = document.createElement('tbody');
|
|
@@ -254,8 +312,11 @@ export class TableRenderer {
|
|
|
254
312
|
const cells = this.tbody.querySelectorAll('td[data-row-index][data-col-index]');
|
|
255
313
|
for (let i = 0; i < cells.length; i++) {
|
|
256
314
|
const el = cells[i];
|
|
257
|
-
const
|
|
258
|
-
|
|
315
|
+
const coords = getCellCoordinates(el);
|
|
316
|
+
if (!coords)
|
|
317
|
+
continue;
|
|
318
|
+
const rowIndex = coords.rowIndex;
|
|
319
|
+
const globalColIndex = coords.colIndex;
|
|
259
320
|
const colOffset = this.getColOffset();
|
|
260
321
|
const colIndex = globalColIndex - colOffset;
|
|
261
322
|
// --- Active cell ---
|
|
@@ -311,7 +372,7 @@ export class TableRenderer {
|
|
|
311
372
|
colIndex === Math.max(selectionRange.startCol, selectionRange.endCol);
|
|
312
373
|
const hadFill = !!oldFill;
|
|
313
374
|
if (hadFill && !shouldHaveFill) {
|
|
314
|
-
oldFill
|
|
375
|
+
oldFill?.remove();
|
|
315
376
|
}
|
|
316
377
|
else if (!hadFill && shouldHaveFill) {
|
|
317
378
|
const fillHandle = document.createElement('div');
|
|
@@ -416,7 +477,7 @@ export class TableRenderer {
|
|
|
416
477
|
if (hasCheckbox) {
|
|
417
478
|
const th = document.createElement('th');
|
|
418
479
|
th.className = 'ogrid-header-cell ogrid-checkbox-header';
|
|
419
|
-
th.style.width = `${
|
|
480
|
+
th.style.width = `${CHECKBOX_COLUMN_WIDTH}px`;
|
|
420
481
|
// Select-all checkbox only on last header row
|
|
421
482
|
if (row === headerRows[headerRows.length - 1]) {
|
|
422
483
|
this.appendSelectAllCheckbox(th);
|
|
@@ -431,32 +492,21 @@ export class TableRenderer {
|
|
|
431
492
|
th.colSpan = cell.colSpan;
|
|
432
493
|
if (!cell.isGroup && cell.columnDef?.sortable) {
|
|
433
494
|
th.classList.add('ogrid-sortable');
|
|
495
|
+
// Sort click also inline for compatibility with tests that hold stale <th> references
|
|
434
496
|
th.addEventListener('click', () => {
|
|
435
|
-
if (cell.columnDef)
|
|
497
|
+
if (cell.columnDef)
|
|
436
498
|
this.state.toggleSort(cell.columnDef.columnId);
|
|
437
|
-
}
|
|
438
499
|
});
|
|
439
500
|
}
|
|
440
501
|
if (!cell.isGroup && cell.columnDef) {
|
|
441
502
|
th.setAttribute('data-column-id', cell.columnDef.columnId);
|
|
442
503
|
this.applyPinningStyles(th, cell.columnDef.columnId, true);
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
th.addEventListener('mousedown', (e) => {
|
|
446
|
-
const target = e.target;
|
|
447
|
-
if (target.classList.contains('ogrid-resize-handle') ||
|
|
448
|
-
target.classList.contains('ogrid-filter-icon')) {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (cell.columnDef) {
|
|
452
|
-
this.interactionState?.onColumnReorderStart?.(cell.columnDef.columnId, e);
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
}
|
|
504
|
+
// Resize, reorder, and filter icon clicks are handled
|
|
505
|
+
// via delegated listeners on <thead> (attachHeaderDelegation).
|
|
456
506
|
}
|
|
457
507
|
tr.appendChild(th);
|
|
458
508
|
}
|
|
459
|
-
this.thead
|
|
509
|
+
this.thead?.appendChild(tr);
|
|
460
510
|
}
|
|
461
511
|
}
|
|
462
512
|
else {
|
|
@@ -466,7 +516,7 @@ export class TableRenderer {
|
|
|
466
516
|
if (hasCheckbox) {
|
|
467
517
|
const th = document.createElement('th');
|
|
468
518
|
th.className = 'ogrid-header-cell ogrid-checkbox-header';
|
|
469
|
-
th.style.width = `${
|
|
519
|
+
th.style.width = `${CHECKBOX_COLUMN_WIDTH}px`;
|
|
470
520
|
this.appendSelectAllCheckbox(th);
|
|
471
521
|
tr.appendChild(th);
|
|
472
522
|
}
|
|
@@ -490,6 +540,7 @@ export class TableRenderer {
|
|
|
490
540
|
th.appendChild(textSpan);
|
|
491
541
|
if (col.sortable) {
|
|
492
542
|
th.classList.add('ogrid-sortable');
|
|
543
|
+
// Sort click also inline for compatibility with tests that hold stale <th> references
|
|
493
544
|
th.addEventListener('click', () => this.state.toggleSort(col.columnId));
|
|
494
545
|
}
|
|
495
546
|
if (col.type === 'numeric') {
|
|
@@ -513,11 +564,7 @@ export class TableRenderer {
|
|
|
513
564
|
resizeHandle.style.userSelect = 'none';
|
|
514
565
|
th.style.position = th.style.position || 'relative';
|
|
515
566
|
th.appendChild(resizeHandle);
|
|
516
|
-
|
|
517
|
-
e.stopPropagation();
|
|
518
|
-
const rect = th.getBoundingClientRect();
|
|
519
|
-
this.interactionState?.onResizeStart?.(col.columnId, e.clientX, rect.width);
|
|
520
|
-
});
|
|
567
|
+
// Resize mousedown handled via delegated listener on <thead>
|
|
521
568
|
// Filter icon (if column is filterable)
|
|
522
569
|
const filterConfig = this.filterConfigs.get(col.columnId);
|
|
523
570
|
if (filterConfig && this.onFilterIconClick) {
|
|
@@ -546,21 +593,10 @@ export class TableRenderer {
|
|
|
546
593
|
});
|
|
547
594
|
th.appendChild(filterBtn);
|
|
548
595
|
}
|
|
549
|
-
// Column reorder
|
|
550
|
-
if (this.interactionState?.onColumnReorderStart) {
|
|
551
|
-
th.addEventListener('mousedown', (e) => {
|
|
552
|
-
// Don't start reorder if clicking resize handle or filter button
|
|
553
|
-
const target = e.target;
|
|
554
|
-
if (target.classList.contains('ogrid-resize-handle') ||
|
|
555
|
-
target.classList.contains('ogrid-filter-icon')) {
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
this.interactionState?.onColumnReorderStart?.(col.columnId, e);
|
|
559
|
-
});
|
|
560
|
-
}
|
|
596
|
+
// Column reorder mousedown handled via delegated listener on <thead>
|
|
561
597
|
tr.appendChild(th);
|
|
562
598
|
}
|
|
563
|
-
this.thead
|
|
599
|
+
this.thead?.appendChild(tr);
|
|
564
600
|
}
|
|
565
601
|
}
|
|
566
602
|
appendSelectAllCheckbox(th) {
|
|
@@ -605,7 +641,9 @@ export class TableRenderer {
|
|
|
605
641
|
let startIndex = 0;
|
|
606
642
|
let endIndex = items.length - 1;
|
|
607
643
|
if (isVirtual) {
|
|
608
|
-
const range = vs
|
|
644
|
+
const range = vs?.visibleRange;
|
|
645
|
+
if (!range)
|
|
646
|
+
return;
|
|
609
647
|
startIndex = Math.max(0, range.startIndex);
|
|
610
648
|
endIndex = Math.min(items.length - 1, range.endIndex);
|
|
611
649
|
// Top spacer row
|
|
@@ -638,7 +676,7 @@ export class TableRenderer {
|
|
|
638
676
|
if (hasCheckbox) {
|
|
639
677
|
const td = document.createElement('td');
|
|
640
678
|
td.className = 'ogrid-cell ogrid-checkbox-cell';
|
|
641
|
-
td.style.width = `${
|
|
679
|
+
td.style.width = `${CHECKBOX_COLUMN_WIDTH}px`;
|
|
642
680
|
td.style.textAlign = 'center';
|
|
643
681
|
const checkbox = document.createElement('input');
|
|
644
682
|
checkbox.type = 'checkbox';
|
|
@@ -747,9 +785,7 @@ export class TableRenderer {
|
|
|
747
785
|
fillHandle.style.cursor = 'crosshair';
|
|
748
786
|
fillHandle.style.zIndex = '5';
|
|
749
787
|
td.style.position = td.style.position || 'relative';
|
|
750
|
-
|
|
751
|
-
this.interactionState?.onFillHandleMouseDown?.(e);
|
|
752
|
-
});
|
|
788
|
+
// Fill handle mousedown handled via delegated listener on <tbody>
|
|
753
789
|
td.appendChild(fillHandle);
|
|
754
790
|
}
|
|
755
791
|
}
|
|
@@ -758,7 +794,7 @@ export class TableRenderer {
|
|
|
758
794
|
this.tbody.appendChild(tr);
|
|
759
795
|
}
|
|
760
796
|
// Virtual scrolling: bottom spacer row
|
|
761
|
-
if (isVirtual) {
|
|
797
|
+
if (isVirtual && vs) {
|
|
762
798
|
const range = vs.visibleRange;
|
|
763
799
|
if (range.offsetBottom > 0) {
|
|
764
800
|
const bottomSpacer = document.createElement('tr');
|
|
@@ -777,6 +813,10 @@ export class TableRenderer {
|
|
|
777
813
|
getTableElement() {
|
|
778
814
|
return this.table;
|
|
779
815
|
}
|
|
816
|
+
/** Get the current onResizeStart handler from interaction state (avoids bracket notation access). */
|
|
817
|
+
getOnResizeStart() {
|
|
818
|
+
return this.interactionState?.onResizeStart;
|
|
819
|
+
}
|
|
780
820
|
/** Update the drop indicator position during column reorder. */
|
|
781
821
|
updateDropIndicator(x, isDragging) {
|
|
782
822
|
if (!this.dropIndicator || !this.wrapperEl)
|
|
@@ -792,6 +832,7 @@ export class TableRenderer {
|
|
|
792
832
|
this.dropIndicator.style.left = `${relativeX}px`;
|
|
793
833
|
}
|
|
794
834
|
destroy() {
|
|
835
|
+
this.detachHeaderDelegation();
|
|
795
836
|
this.detachBodyDelegation();
|
|
796
837
|
this.container.innerHTML = '';
|
|
797
838
|
this.table = null;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { normalizeSelectionRange,
|
|
2
|
-
import { parseValue } from '@alaarab/ogrid-core';
|
|
1
|
+
import { normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear } from '@alaarab/ogrid-core';
|
|
3
2
|
import { EventEmitter } from './EventEmitter';
|
|
4
3
|
export class ClipboardState {
|
|
5
4
|
constructor(params, getActiveCell, getSelectionRange) {
|
|
@@ -84,59 +83,14 @@ export class ClipboardState {
|
|
|
84
83
|
const anchorRow = norm ? norm.startRow : 0;
|
|
85
84
|
const anchorCol = norm ? norm.startCol : 0;
|
|
86
85
|
const { items, visibleCols } = this.params;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const targetRow = anchorRow + r;
|
|
92
|
-
const targetCol = anchorCol + c;
|
|
93
|
-
if (targetRow >= items.length || targetCol >= visibleCols.length)
|
|
94
|
-
continue;
|
|
95
|
-
const item = items[targetRow];
|
|
96
|
-
const col = visibleCols[targetCol];
|
|
97
|
-
const colEditable = col.editable === true ||
|
|
98
|
-
(typeof col.editable === 'function' && col.editable(item));
|
|
99
|
-
if (!colEditable)
|
|
100
|
-
continue;
|
|
101
|
-
const rawValue = cells[c] ?? '';
|
|
102
|
-
const oldValue = getCellValue(item, col);
|
|
103
|
-
const result = parseValue(rawValue, oldValue, item, col);
|
|
104
|
-
if (!result.valid)
|
|
105
|
-
continue;
|
|
106
|
-
onCellValueChanged({
|
|
107
|
-
item,
|
|
108
|
-
columnId: col.columnId,
|
|
109
|
-
oldValue,
|
|
110
|
-
newValue: result.value,
|
|
111
|
-
rowIndex: targetRow,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
86
|
+
const parsedRows = parseTsvClipboard(text);
|
|
87
|
+
const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols);
|
|
88
|
+
for (const evt of pasteEvents)
|
|
89
|
+
onCellValueChanged(evt);
|
|
115
90
|
if (this._cutRange) {
|
|
116
|
-
const
|
|
117
|
-
for (
|
|
118
|
-
|
|
119
|
-
if (r >= items.length || c >= visibleCols.length)
|
|
120
|
-
continue;
|
|
121
|
-
const item = items[r];
|
|
122
|
-
const col = visibleCols[c];
|
|
123
|
-
const colEditable = col.editable === true ||
|
|
124
|
-
(typeof col.editable === 'function' && col.editable(item));
|
|
125
|
-
if (!colEditable)
|
|
126
|
-
continue;
|
|
127
|
-
const oldValue = getCellValue(item, col);
|
|
128
|
-
const result = parseValue('', oldValue, item, col);
|
|
129
|
-
if (!result.valid)
|
|
130
|
-
continue;
|
|
131
|
-
onCellValueChanged({
|
|
132
|
-
item,
|
|
133
|
-
columnId: col.columnId,
|
|
134
|
-
oldValue,
|
|
135
|
-
newValue: result.value,
|
|
136
|
-
rowIndex: r,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
91
|
+
const cutEvents = applyCutClear(this._cutRange, items, visibleCols);
|
|
92
|
+
for (const evt of cutEvents)
|
|
93
|
+
onCellValueChanged(evt);
|
|
140
94
|
this._cutRange = null;
|
|
141
95
|
}
|
|
142
96
|
this._copyRange = null;
|
|
@@ -29,8 +29,7 @@ export class ColumnPinningState {
|
|
|
29
29
|
this.emitter.emit('pinningChange', { pinnedColumns: this._pinnedColumns });
|
|
30
30
|
}
|
|
31
31
|
unpinColumn(columnId) {
|
|
32
|
-
const next =
|
|
33
|
-
delete next[columnId];
|
|
32
|
+
const { [columnId]: _, ...next } = this._pinnedColumns;
|
|
34
33
|
this._pinnedColumns = next;
|
|
35
34
|
this.emitter.emit('pinningChange', { pinnedColumns: this._pinnedColumns });
|
|
36
35
|
}
|
|
@@ -69,7 +69,14 @@ export class ColumnReorderState {
|
|
|
69
69
|
this.rafId = 0;
|
|
70
70
|
if (!this._draggedColumnId || !this.tableElement)
|
|
71
71
|
return;
|
|
72
|
-
const result = calculateDropTarget(
|
|
72
|
+
const result = calculateDropTarget({
|
|
73
|
+
mouseX,
|
|
74
|
+
columnOrder: this.columnOrder,
|
|
75
|
+
draggedColumnId: this._draggedColumnId,
|
|
76
|
+
draggedPinState: this.draggedPinState,
|
|
77
|
+
tableElement: this.tableElement,
|
|
78
|
+
pinnedColumns: this.pinnedColumns,
|
|
79
|
+
});
|
|
73
80
|
if (!result)
|
|
74
81
|
return;
|
|
75
82
|
const prevX = this._dropIndicatorX;
|
|
@@ -6,12 +6,13 @@ export class EventEmitter {
|
|
|
6
6
|
if (!this.handlers.has(event)) {
|
|
7
7
|
this.handlers.set(event, new Set());
|
|
8
8
|
}
|
|
9
|
-
this.handlers.get(event)
|
|
9
|
+
this.handlers.get(event)?.add(handler);
|
|
10
10
|
}
|
|
11
11
|
off(event, handler) {
|
|
12
12
|
this.handlers.get(event)?.delete(handler);
|
|
13
13
|
}
|
|
14
|
-
emit(event,
|
|
14
|
+
emit(event, ...args) {
|
|
15
|
+
const data = args[0];
|
|
15
16
|
this.handlers.get(event)?.forEach(handler => {
|
|
16
17
|
handler(data);
|
|
17
18
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { normalizeSelectionRange,
|
|
1
|
+
import { normalizeSelectionRange, applyFillValues } from '@alaarab/ogrid-core';
|
|
2
2
|
import { EventEmitter } from './EventEmitter';
|
|
3
|
+
import { getCellCoordinates } from '../utils/getCellCoordinates';
|
|
3
4
|
/**
|
|
4
5
|
* Manages Excel-style fill handle drag-to-fill for cell ranges (vanilla JS).
|
|
5
6
|
* Mirrors the React `useFillHandle` hook as a class-based state.
|
|
@@ -109,49 +110,24 @@ export class FillHandleState {
|
|
|
109
110
|
this.setSelectionRange(norm);
|
|
110
111
|
this.setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + this.params.colOffset });
|
|
111
112
|
// Apply fill values
|
|
112
|
-
this.
|
|
113
|
+
this.applyFillValuesFromCore(norm, start);
|
|
113
114
|
this._isFillDragging = false;
|
|
114
115
|
this.fillDragStart = null;
|
|
115
116
|
this.liveFillRange = null;
|
|
116
117
|
this.lastMousePos = null;
|
|
117
118
|
this.emitter.emit('fillRangeChange', { fillRange: null });
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
+
applyFillValuesFromCore(norm, start) {
|
|
120
121
|
const { items, visibleCols, onCellValueChanged, beginBatch, endBatch } = this.params;
|
|
121
122
|
if (!onCellValueChanged)
|
|
122
123
|
return;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for (let row = norm.startRow; row <= norm.endRow; row++) {
|
|
130
|
-
for (let col = norm.startCol; col <= norm.endCol; col++) {
|
|
131
|
-
if (row === start.startRow && col === start.startCol)
|
|
132
|
-
continue;
|
|
133
|
-
if (row >= items.length || col >= visibleCols.length)
|
|
134
|
-
continue;
|
|
135
|
-
const item = items[row];
|
|
136
|
-
const colDef = visibleCols[col];
|
|
137
|
-
const colEditable = colDef.editable === true ||
|
|
138
|
-
(typeof colDef.editable === 'function' && colDef.editable(item));
|
|
139
|
-
if (!colEditable)
|
|
140
|
-
continue;
|
|
141
|
-
const oldValue = getCellValue(item, colDef);
|
|
142
|
-
const result = parseValue(startValue, oldValue, item, colDef);
|
|
143
|
-
if (!result.valid)
|
|
144
|
-
continue;
|
|
145
|
-
onCellValueChanged({
|
|
146
|
-
item,
|
|
147
|
-
columnId: colDef.columnId,
|
|
148
|
-
oldValue,
|
|
149
|
-
newValue: result.value,
|
|
150
|
-
rowIndex: row,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
124
|
+
const fillEvents = applyFillValues(norm, start.startRow, start.startCol, items, visibleCols);
|
|
125
|
+
if (fillEvents.length > 0) {
|
|
126
|
+
beginBatch?.();
|
|
127
|
+
for (const evt of fillEvents)
|
|
128
|
+
onCellValueChanged(evt);
|
|
129
|
+
endBatch?.();
|
|
153
130
|
}
|
|
154
|
-
endBatch?.();
|
|
155
131
|
}
|
|
156
132
|
resolveRange(cx, cy) {
|
|
157
133
|
if (!this.fillDragStart || !this.wrapperRef)
|
|
@@ -160,11 +136,11 @@ export class FillHandleState {
|
|
|
160
136
|
const cell = target?.closest?.('[data-row-index][data-col-index]');
|
|
161
137
|
if (!cell || !this.wrapperRef.contains(cell))
|
|
162
138
|
return null;
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
if (Number.isNaN(r) || Number.isNaN(c) || c < this.params.colOffset)
|
|
139
|
+
const coords = getCellCoordinates(cell);
|
|
140
|
+
if (!coords || coords.colIndex < this.params.colOffset)
|
|
166
141
|
return null;
|
|
167
|
-
const
|
|
142
|
+
const r = coords.rowIndex;
|
|
143
|
+
const dataCol = coords.colIndex - this.params.colOffset;
|
|
168
144
|
return normalizeSelectionRange({
|
|
169
145
|
startRow: this.fillDragStart.startRow,
|
|
170
146
|
startCol: this.fillDragStart.startCol,
|
|
@@ -183,8 +159,11 @@ export class FillHandleState {
|
|
|
183
159
|
const maxC = Math.max(range.startCol, range.endCol);
|
|
184
160
|
for (let i = 0; i < cells.length; i++) {
|
|
185
161
|
const el = cells[i];
|
|
186
|
-
const
|
|
187
|
-
|
|
162
|
+
const coords = getCellCoordinates(el);
|
|
163
|
+
if (!coords)
|
|
164
|
+
continue;
|
|
165
|
+
const r = coords.rowIndex;
|
|
166
|
+
const c = coords.colIndex - colOff;
|
|
188
167
|
const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
|
|
189
168
|
if (inRange) {
|
|
190
169
|
if (!el.hasAttribute('data-drag-range'))
|
|
@@ -212,9 +191,16 @@ export class FillHandleState {
|
|
|
212
191
|
if (this._isFillDragging) {
|
|
213
192
|
window.removeEventListener('mousemove', this.onMoveBound, true);
|
|
214
193
|
window.removeEventListener('mouseup', this.onUpBound, true);
|
|
194
|
+
this.clearDragAttrs();
|
|
195
|
+
this._isFillDragging = false;
|
|
196
|
+
this.fillDragStart = null;
|
|
197
|
+
this.liveFillRange = null;
|
|
198
|
+
this.lastMousePos = null;
|
|
215
199
|
}
|
|
216
|
-
if (this.rafHandle)
|
|
200
|
+
if (this.rafHandle) {
|
|
217
201
|
cancelAnimationFrame(this.rafHandle);
|
|
202
|
+
this.rafHandle = 0;
|
|
203
|
+
}
|
|
218
204
|
this.emitter.removeAllListeners();
|
|
219
205
|
}
|
|
220
206
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { flattenColumns, processClientSideData, exportToCsv as coreExportToCsv, getCellValue, deriveFilterOptionsFromData, mergeFilter, } from '@alaarab/ogrid-core';
|
|
1
|
+
import { flattenColumns, processClientSideData, exportToCsv as coreExportToCsv, getCellValue, deriveFilterOptionsFromData, mergeFilter, validateColumns, validateRowIds, } from '@alaarab/ogrid-core';
|
|
2
2
|
import { EventEmitter } from './EventEmitter';
|
|
3
3
|
export class GridState {
|
|
4
4
|
constructor(options) {
|
|
@@ -15,6 +15,9 @@ export class GridState {
|
|
|
15
15
|
this._filterOptions = {};
|
|
16
16
|
// Column display order (array of columnIds)
|
|
17
17
|
this._columnOrder = [];
|
|
18
|
+
// Dirty-flag memoization for visibleColumnDefs getter
|
|
19
|
+
this._visibleColsCache = null;
|
|
20
|
+
this._visibleColsDirty = true;
|
|
18
21
|
this._allColumns = options.columns;
|
|
19
22
|
this._columns = flattenColumns(options.columns);
|
|
20
23
|
this._getRowId = options.getRowId;
|
|
@@ -28,10 +31,18 @@ export class GridState {
|
|
|
28
31
|
this._columnOrder = this._columns.map(c => c.columnId);
|
|
29
32
|
this._onError = options.onError;
|
|
30
33
|
this._onFirstDataRendered = options.onFirstDataRendered;
|
|
34
|
+
this._rowHeight = options.rowHeight;
|
|
35
|
+
this._ariaLabel = options.ariaLabel;
|
|
31
36
|
// Derive initial filter options for client-side data
|
|
32
37
|
if (!this._dataSource) {
|
|
33
38
|
this._filterOptions = deriveFilterOptionsFromData(this._data, this._columns);
|
|
34
39
|
}
|
|
40
|
+
// Runtime validation — runs once at construction
|
|
41
|
+
validateColumns(this._columns);
|
|
42
|
+
if (!this._dataSource && this._data.length > 0) {
|
|
43
|
+
validateRowIds(this._data, this._getRowId);
|
|
44
|
+
this._firstDataRendered = true;
|
|
45
|
+
}
|
|
35
46
|
// If server-side, trigger initial fetch
|
|
36
47
|
if (this._dataSource) {
|
|
37
48
|
this._isLoading = true;
|
|
@@ -52,17 +63,26 @@ export class GridState {
|
|
|
52
63
|
get isServerSide() { return this._dataSource != null; }
|
|
53
64
|
get filterOptions() { return this._filterOptions; }
|
|
54
65
|
get columnOrder() { return this._columnOrder; }
|
|
55
|
-
|
|
66
|
+
get rowHeight() { return this._rowHeight; }
|
|
67
|
+
get ariaLabel() { return this._ariaLabel; }
|
|
68
|
+
/** Get the visible columns in display order (respects column reorder). Memoized via dirty flag. */
|
|
56
69
|
get visibleColumnDefs() {
|
|
70
|
+
if (!this._visibleColsDirty && this._visibleColsCache)
|
|
71
|
+
return this._visibleColsCache;
|
|
57
72
|
const visible = this._columns.filter(c => this._visibleColumns.has(c.columnId));
|
|
58
|
-
if (this._columnOrder.length === 0)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
if (this._columnOrder.length === 0) {
|
|
74
|
+
this._visibleColsCache = visible;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const orderMap = new Map(this._columnOrder.map((id, idx) => [id, idx]));
|
|
78
|
+
this._visibleColsCache = [...visible].sort((a, b) => {
|
|
79
|
+
const ai = orderMap.get(a.columnId) ?? Infinity;
|
|
80
|
+
const bi = orderMap.get(b.columnId) ?? Infinity;
|
|
81
|
+
return ai - bi;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
this._visibleColsDirty = false;
|
|
85
|
+
return this._visibleColsCache;
|
|
66
86
|
}
|
|
67
87
|
/** Get processed (sorted, filtered, paginated) items for current page. */
|
|
68
88
|
getProcessedItems() {
|
|
@@ -105,6 +125,7 @@ export class GridState {
|
|
|
105
125
|
this._isLoading = false;
|
|
106
126
|
if (!this._firstDataRendered && res.items.length > 0) {
|
|
107
127
|
this._firstDataRendered = true;
|
|
128
|
+
validateRowIds(res.items, this._getRowId);
|
|
108
129
|
this._onFirstDataRendered?.();
|
|
109
130
|
}
|
|
110
131
|
this.emitter.emit('stateChange', { type: 'data' });
|
|
@@ -196,10 +217,12 @@ export class GridState {
|
|
|
196
217
|
}
|
|
197
218
|
setVisibleColumns(columns) {
|
|
198
219
|
this._visibleColumns = columns;
|
|
220
|
+
this._visibleColsDirty = true;
|
|
199
221
|
this.emitter.emit('stateChange', { type: 'columns' });
|
|
200
222
|
}
|
|
201
223
|
setColumnOrder(order) {
|
|
202
224
|
this._columnOrder = order;
|
|
225
|
+
this._visibleColsDirty = true;
|
|
203
226
|
this.emitter.emit('stateChange', { type: 'columns' });
|
|
204
227
|
}
|
|
205
228
|
setLoading(loading) {
|
|
@@ -265,6 +288,9 @@ export class GridState {
|
|
|
265
288
|
},
|
|
266
289
|
getDisplayedRows: () => this.getProcessedItems().items,
|
|
267
290
|
refreshData: () => this.refreshData(),
|
|
291
|
+
// scrollToRow is wired by OGrid after construction when virtualScrollState is present.
|
|
292
|
+
// This stub is replaced by OGrid.ts (see "Wire scrollToRow API method") for virtual scroll.
|
|
293
|
+
// For non-virtual grids it remains a no-op (native browser scroll handles row visibility).
|
|
268
294
|
scrollToRow: () => { },
|
|
269
295
|
getColumnOrder: () => [...this._columnOrder],
|
|
270
296
|
setColumnOrder: (order) => this.setColumnOrder(order),
|