@alaarab/ogrid-js 2.1.3 → 2.1.4
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/index.js +6343 -32
- package/package.json +4 -4
- package/dist/esm/OGrid.js +0 -578
- package/dist/esm/OGridEventWiring.js +0 -178
- package/dist/esm/OGridRendering.js +0 -269
- package/dist/esm/components/ColumnChooser.js +0 -91
- package/dist/esm/components/ContextMenu.js +0 -125
- package/dist/esm/components/HeaderFilter.js +0 -281
- package/dist/esm/components/InlineCellEditor.js +0 -434
- package/dist/esm/components/MarchingAntsOverlay.js +0 -156
- package/dist/esm/components/PaginationControls.js +0 -85
- package/dist/esm/components/SideBar.js +0 -353
- package/dist/esm/components/StatusBar.js +0 -34
- package/dist/esm/renderer/TableRenderer.js +0 -846
- package/dist/esm/state/ClipboardState.js +0 -111
- package/dist/esm/state/ColumnPinningState.js +0 -82
- package/dist/esm/state/ColumnReorderState.js +0 -135
- package/dist/esm/state/ColumnResizeState.js +0 -55
- package/dist/esm/state/EventEmitter.js +0 -28
- package/dist/esm/state/FillHandleState.js +0 -206
- package/dist/esm/state/GridState.js +0 -324
- package/dist/esm/state/HeaderFilterState.js +0 -213
- package/dist/esm/state/KeyboardNavState.js +0 -216
- package/dist/esm/state/RowSelectionState.js +0 -72
- package/dist/esm/state/SelectionState.js +0 -109
- package/dist/esm/state/SideBarState.js +0 -41
- package/dist/esm/state/TableLayoutState.js +0 -97
- package/dist/esm/state/UndoRedoState.js +0 -71
- package/dist/esm/state/VirtualScrollState.js +0 -128
- package/dist/esm/types/columnTypes.js +0 -1
- package/dist/esm/types/gridTypes.js +0 -1
- package/dist/esm/types/index.js +0 -2
- package/dist/esm/utils/debounce.js +0 -2
- package/dist/esm/utils/getCellCoordinates.js +0 -15
- package/dist/esm/utils/index.js +0 -2
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear } from '@alaarab/ogrid-core';
|
|
2
|
-
import { EventEmitter } from './EventEmitter';
|
|
3
|
-
export class ClipboardState {
|
|
4
|
-
constructor(params, getActiveCell, getSelectionRange) {
|
|
5
|
-
this.emitter = new EventEmitter();
|
|
6
|
-
this._cutRange = null;
|
|
7
|
-
this._copyRange = null;
|
|
8
|
-
this.internalClipboard = null;
|
|
9
|
-
this.params = params;
|
|
10
|
-
this.getActiveCell = getActiveCell;
|
|
11
|
-
this.getSelectionRange = getSelectionRange;
|
|
12
|
-
}
|
|
13
|
-
updateParams(params) {
|
|
14
|
-
this.params = params;
|
|
15
|
-
}
|
|
16
|
-
get cutRange() {
|
|
17
|
-
return this._cutRange;
|
|
18
|
-
}
|
|
19
|
-
get copyRange() {
|
|
20
|
-
return this._copyRange;
|
|
21
|
-
}
|
|
22
|
-
getEffectiveRange() {
|
|
23
|
-
const sel = this.getSelectionRange();
|
|
24
|
-
const ac = this.getActiveCell();
|
|
25
|
-
return sel ?? (ac != null
|
|
26
|
-
? { startRow: ac.rowIndex, startCol: ac.columnIndex - this.params.colOffset, endRow: ac.rowIndex, endCol: ac.columnIndex - this.params.colOffset }
|
|
27
|
-
: null);
|
|
28
|
-
}
|
|
29
|
-
handleCopy() {
|
|
30
|
-
const range = this.getEffectiveRange();
|
|
31
|
-
if (range == null)
|
|
32
|
-
return;
|
|
33
|
-
const norm = normalizeSelectionRange(range);
|
|
34
|
-
const { items, visibleCols } = this.params;
|
|
35
|
-
const tsv = formatSelectionAsTsv(items, visibleCols, norm);
|
|
36
|
-
this.internalClipboard = tsv;
|
|
37
|
-
this._copyRange = norm;
|
|
38
|
-
this._cutRange = null;
|
|
39
|
-
this.emitter.emit('rangesChange', { copyRange: this._copyRange, cutRange: null });
|
|
40
|
-
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
41
|
-
void navigator.clipboard.writeText(tsv).catch(() => { });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
handleCut() {
|
|
45
|
-
if (this.params.editable === false)
|
|
46
|
-
return;
|
|
47
|
-
const range = this.getEffectiveRange();
|
|
48
|
-
if (range == null)
|
|
49
|
-
return;
|
|
50
|
-
const norm = normalizeSelectionRange(range);
|
|
51
|
-
this._cutRange = norm;
|
|
52
|
-
this._copyRange = null;
|
|
53
|
-
this.handleCopy();
|
|
54
|
-
// handleCopy sets copyRange — override it back since this is a cut
|
|
55
|
-
this._copyRange = null;
|
|
56
|
-
this._cutRange = norm;
|
|
57
|
-
this.emitter.emit('rangesChange', { copyRange: null, cutRange: this._cutRange });
|
|
58
|
-
}
|
|
59
|
-
async handlePaste() {
|
|
60
|
-
if (this.params.editable === false)
|
|
61
|
-
return;
|
|
62
|
-
const { onCellValueChanged } = this.params;
|
|
63
|
-
let text;
|
|
64
|
-
try {
|
|
65
|
-
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
66
|
-
text = await navigator.clipboard.readText();
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
text = '';
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
text = '';
|
|
74
|
-
}
|
|
75
|
-
if (!text.trim() && this.internalClipboard != null) {
|
|
76
|
-
text = this.internalClipboard;
|
|
77
|
-
}
|
|
78
|
-
if (!text.trim())
|
|
79
|
-
return;
|
|
80
|
-
if (onCellValueChanged == null)
|
|
81
|
-
return;
|
|
82
|
-
const norm = this.getEffectiveRange();
|
|
83
|
-
const anchorRow = norm ? norm.startRow : 0;
|
|
84
|
-
const anchorCol = norm ? norm.startCol : 0;
|
|
85
|
-
const { items, visibleCols } = this.params;
|
|
86
|
-
const parsedRows = parseTsvClipboard(text);
|
|
87
|
-
const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols);
|
|
88
|
-
for (const evt of pasteEvents)
|
|
89
|
-
onCellValueChanged(evt);
|
|
90
|
-
if (this._cutRange) {
|
|
91
|
-
const cutEvents = applyCutClear(this._cutRange, items, visibleCols);
|
|
92
|
-
for (const evt of cutEvents)
|
|
93
|
-
onCellValueChanged(evt);
|
|
94
|
-
this._cutRange = null;
|
|
95
|
-
}
|
|
96
|
-
this._copyRange = null;
|
|
97
|
-
this.emitter.emit('rangesChange', { copyRange: null, cutRange: null });
|
|
98
|
-
}
|
|
99
|
-
clearClipboardRanges() {
|
|
100
|
-
this._copyRange = null;
|
|
101
|
-
this._cutRange = null;
|
|
102
|
-
this.emitter.emit('rangesChange', { copyRange: null, cutRange: null });
|
|
103
|
-
}
|
|
104
|
-
onRangesChange(handler) {
|
|
105
|
-
this.emitter.on('rangesChange', handler);
|
|
106
|
-
return () => this.emitter.off('rangesChange', handler);
|
|
107
|
-
}
|
|
108
|
-
destroy() {
|
|
109
|
-
this.emitter.removeAllListeners();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { ROW_NUMBER_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
2
|
-
import { EventEmitter } from './EventEmitter';
|
|
3
|
-
/**
|
|
4
|
-
* Manages column pinning state — tracks which columns are pinned left/right.
|
|
5
|
-
* Computes sticky offsets for the renderer.
|
|
6
|
-
*/
|
|
7
|
-
export class ColumnPinningState {
|
|
8
|
-
constructor(pinnedColumns, columns) {
|
|
9
|
-
this.emitter = new EventEmitter();
|
|
10
|
-
this._pinnedColumns = {};
|
|
11
|
-
// Initialize from explicit pinnedColumns prop
|
|
12
|
-
if (pinnedColumns) {
|
|
13
|
-
this._pinnedColumns = { ...pinnedColumns };
|
|
14
|
-
}
|
|
15
|
-
// Also pick up pinned from column definitions
|
|
16
|
-
if (columns) {
|
|
17
|
-
for (const col of columns) {
|
|
18
|
-
if (col.pinned && !(col.columnId in this._pinnedColumns)) {
|
|
19
|
-
this._pinnedColumns[col.columnId] = col.pinned;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
get pinnedColumns() {
|
|
25
|
-
return this._pinnedColumns;
|
|
26
|
-
}
|
|
27
|
-
pinColumn(columnId, side) {
|
|
28
|
-
this._pinnedColumns = { ...this._pinnedColumns, [columnId]: side };
|
|
29
|
-
this.emitter.emit('pinningChange', { pinnedColumns: this._pinnedColumns });
|
|
30
|
-
}
|
|
31
|
-
unpinColumn(columnId) {
|
|
32
|
-
const { [columnId]: _, ...next } = this._pinnedColumns;
|
|
33
|
-
this._pinnedColumns = next;
|
|
34
|
-
this.emitter.emit('pinningChange', { pinnedColumns: this._pinnedColumns });
|
|
35
|
-
}
|
|
36
|
-
isPinned(columnId) {
|
|
37
|
-
return this._pinnedColumns[columnId];
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Compute sticky left offsets for left-pinned columns.
|
|
41
|
-
* Returns a map of columnId -> left offset in pixels.
|
|
42
|
-
*/
|
|
43
|
-
computeLeftOffsets(visibleCols, columnWidths, defaultWidth, hasCheckboxColumn, checkboxColumnWidth, hasRowNumbersColumn) {
|
|
44
|
-
const offsets = {};
|
|
45
|
-
let left = 0;
|
|
46
|
-
if (hasCheckboxColumn)
|
|
47
|
-
left += checkboxColumnWidth;
|
|
48
|
-
if (hasRowNumbersColumn)
|
|
49
|
-
left += ROW_NUMBER_COLUMN_WIDTH;
|
|
50
|
-
for (const col of visibleCols) {
|
|
51
|
-
if (this._pinnedColumns[col.columnId] === 'left') {
|
|
52
|
-
offsets[col.columnId] = left;
|
|
53
|
-
left += columnWidths[col.columnId] ?? defaultWidth;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return offsets;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Compute sticky right offsets for right-pinned columns.
|
|
60
|
-
* Returns a map of columnId -> right offset in pixels.
|
|
61
|
-
*/
|
|
62
|
-
computeRightOffsets(visibleCols, columnWidths, defaultWidth) {
|
|
63
|
-
const offsets = {};
|
|
64
|
-
let right = 0;
|
|
65
|
-
// Walk right-pinned columns from the end
|
|
66
|
-
for (let i = visibleCols.length - 1; i >= 0; i--) {
|
|
67
|
-
const col = visibleCols[i];
|
|
68
|
-
if (this._pinnedColumns[col.columnId] === 'right') {
|
|
69
|
-
offsets[col.columnId] = right;
|
|
70
|
-
right += columnWidths[col.columnId] ?? defaultWidth;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return offsets;
|
|
74
|
-
}
|
|
75
|
-
onPinningChange(handler) {
|
|
76
|
-
this.emitter.on('pinningChange', handler);
|
|
77
|
-
return () => this.emitter.off('pinningChange', handler);
|
|
78
|
-
}
|
|
79
|
-
destroy() {
|
|
80
|
-
this.emitter.removeAllListeners();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { calculateDropTarget, reorderColumnArray, getPinStateForColumn } from '@alaarab/ogrid-core';
|
|
2
|
-
import { EventEmitter } from './EventEmitter';
|
|
3
|
-
/**
|
|
4
|
-
* Manages column drag-to-reorder for the vanilla JS grid.
|
|
5
|
-
* Follows the EventEmitter + RAF pattern from FillHandleState/SelectionState.
|
|
6
|
-
*/
|
|
7
|
-
export class ColumnReorderState {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.emitter = new EventEmitter();
|
|
10
|
-
this._isDragging = false;
|
|
11
|
-
this._draggedColumnId = null;
|
|
12
|
-
this._dropIndicatorX = null;
|
|
13
|
-
this._dropTargetIndex = null;
|
|
14
|
-
this.rafId = 0;
|
|
15
|
-
this.columnOrder = [];
|
|
16
|
-
this.draggedPinState = 'unpinned';
|
|
17
|
-
this.tableElement = null;
|
|
18
|
-
this.onMoveBound = this.handleMouseMove.bind(this);
|
|
19
|
-
this.onUpBound = this.handleMouseUp.bind(this);
|
|
20
|
-
}
|
|
21
|
-
get isDragging() {
|
|
22
|
-
return this._isDragging;
|
|
23
|
-
}
|
|
24
|
-
get dropIndicatorX() {
|
|
25
|
-
return this._dropIndicatorX;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Begin a column drag operation.
|
|
29
|
-
* Called from mousedown on a header cell.
|
|
30
|
-
*/
|
|
31
|
-
startDrag(columnId, event, columns, columnOrder, pinnedColumns, tableElement) {
|
|
32
|
-
event.preventDefault();
|
|
33
|
-
this._isDragging = true;
|
|
34
|
-
this._draggedColumnId = columnId;
|
|
35
|
-
this._dropIndicatorX = null;
|
|
36
|
-
this._dropTargetIndex = null;
|
|
37
|
-
this.tableElement = tableElement;
|
|
38
|
-
// Use provided column order, or derive from columns array
|
|
39
|
-
this.columnOrder = columnOrder.length > 0
|
|
40
|
-
? [...columnOrder]
|
|
41
|
-
: columns.map(c => c.columnId);
|
|
42
|
-
// Convert Record<string, 'left' | 'right'> to { left?: string[]; right?: string[] }
|
|
43
|
-
if (pinnedColumns) {
|
|
44
|
-
const left = [];
|
|
45
|
-
const right = [];
|
|
46
|
-
for (const [id, side] of Object.entries(pinnedColumns)) {
|
|
47
|
-
if (side === 'left')
|
|
48
|
-
left.push(id);
|
|
49
|
-
else if (side === 'right')
|
|
50
|
-
right.push(id);
|
|
51
|
-
}
|
|
52
|
-
this.pinnedColumns = { left, right };
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
this.pinnedColumns = undefined;
|
|
56
|
-
}
|
|
57
|
-
this.draggedPinState = getPinStateForColumn(columnId, this.pinnedColumns);
|
|
58
|
-
window.addEventListener('mousemove', this.onMoveBound, { capture: true, passive: true });
|
|
59
|
-
window.addEventListener('mouseup', this.onUpBound, { capture: true, passive: true });
|
|
60
|
-
this.emitter.emit('stateChange', { isDragging: true, dropIndicatorX: null });
|
|
61
|
-
}
|
|
62
|
-
handleMouseMove(event) {
|
|
63
|
-
if (!this._isDragging || !this._draggedColumnId || !this.tableElement)
|
|
64
|
-
return;
|
|
65
|
-
if (this.rafId)
|
|
66
|
-
cancelAnimationFrame(this.rafId);
|
|
67
|
-
const mouseX = event.clientX;
|
|
68
|
-
this.rafId = requestAnimationFrame(() => {
|
|
69
|
-
this.rafId = 0;
|
|
70
|
-
if (!this._draggedColumnId || !this.tableElement)
|
|
71
|
-
return;
|
|
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
|
-
});
|
|
80
|
-
if (!result)
|
|
81
|
-
return;
|
|
82
|
-
const prevX = this._dropIndicatorX;
|
|
83
|
-
const prevIdx = this._dropTargetIndex;
|
|
84
|
-
this._dropTargetIndex = result.targetIndex;
|
|
85
|
-
this._dropIndicatorX = result.indicatorX;
|
|
86
|
-
// Only emit if something changed
|
|
87
|
-
if (prevX !== result.indicatorX || prevIdx !== result.targetIndex) {
|
|
88
|
-
this.emitter.emit('stateChange', {
|
|
89
|
-
isDragging: true,
|
|
90
|
-
dropIndicatorX: result.indicatorX,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
handleMouseUp() {
|
|
96
|
-
window.removeEventListener('mousemove', this.onMoveBound, true);
|
|
97
|
-
window.removeEventListener('mouseup', this.onUpBound, true);
|
|
98
|
-
if (this.rafId) {
|
|
99
|
-
cancelAnimationFrame(this.rafId);
|
|
100
|
-
this.rafId = 0;
|
|
101
|
-
}
|
|
102
|
-
// Commit reorder if we have a valid drop target that isn't a no-op
|
|
103
|
-
if (this._isDragging &&
|
|
104
|
-
this._draggedColumnId &&
|
|
105
|
-
this._dropTargetIndex !== null &&
|
|
106
|
-
this._dropIndicatorX !== null // null indicatorX means no-op (same position)
|
|
107
|
-
) {
|
|
108
|
-
const newOrder = reorderColumnArray(this.columnOrder, this._draggedColumnId, this._dropTargetIndex);
|
|
109
|
-
this.emitter.emit('reorder', { columnOrder: newOrder });
|
|
110
|
-
}
|
|
111
|
-
this._isDragging = false;
|
|
112
|
-
this._draggedColumnId = null;
|
|
113
|
-
this._dropIndicatorX = null;
|
|
114
|
-
this._dropTargetIndex = null;
|
|
115
|
-
this.tableElement = null;
|
|
116
|
-
this.emitter.emit('stateChange', { isDragging: false, dropIndicatorX: null });
|
|
117
|
-
}
|
|
118
|
-
onStateChange(handler) {
|
|
119
|
-
this.emitter.on('stateChange', handler);
|
|
120
|
-
return () => this.emitter.off('stateChange', handler);
|
|
121
|
-
}
|
|
122
|
-
onReorder(handler) {
|
|
123
|
-
this.emitter.on('reorder', handler);
|
|
124
|
-
return () => this.emitter.off('reorder', handler);
|
|
125
|
-
}
|
|
126
|
-
destroy() {
|
|
127
|
-
if (this._isDragging) {
|
|
128
|
-
window.removeEventListener('mousemove', this.onMoveBound, true);
|
|
129
|
-
window.removeEventListener('mouseup', this.onUpBound, true);
|
|
130
|
-
}
|
|
131
|
-
if (this.rafId)
|
|
132
|
-
cancelAnimationFrame(this.rafId);
|
|
133
|
-
this.emitter.removeAllListeners();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
2
|
-
import { EventEmitter } from './EventEmitter';
|
|
3
|
-
export class ColumnResizeState {
|
|
4
|
-
constructor() {
|
|
5
|
-
this.emitter = new EventEmitter();
|
|
6
|
-
this.columnWidths = new Map();
|
|
7
|
-
this.isResizing = false;
|
|
8
|
-
this.resizeColumnId = null;
|
|
9
|
-
this.resizeStartX = 0;
|
|
10
|
-
this.resizeStartWidth = 0;
|
|
11
|
-
}
|
|
12
|
-
get resizingColumnId() {
|
|
13
|
-
return this.resizeColumnId;
|
|
14
|
-
}
|
|
15
|
-
getColumnWidth(columnId) {
|
|
16
|
-
return this.columnWidths.get(columnId);
|
|
17
|
-
}
|
|
18
|
-
getAllColumnWidths() {
|
|
19
|
-
const result = {};
|
|
20
|
-
this.columnWidths.forEach((width, id) => {
|
|
21
|
-
result[id] = width;
|
|
22
|
-
});
|
|
23
|
-
return result;
|
|
24
|
-
}
|
|
25
|
-
startResize(columnId, clientX, currentWidth) {
|
|
26
|
-
this.isResizing = true;
|
|
27
|
-
this.resizeColumnId = columnId;
|
|
28
|
-
this.resizeStartX = clientX;
|
|
29
|
-
this.resizeStartWidth = currentWidth;
|
|
30
|
-
}
|
|
31
|
-
updateResize(clientX) {
|
|
32
|
-
if (!this.isResizing || !this.resizeColumnId)
|
|
33
|
-
return null;
|
|
34
|
-
const delta = clientX - this.resizeStartX;
|
|
35
|
-
const newWidth = Math.max(DEFAULT_MIN_COLUMN_WIDTH, this.resizeStartWidth + delta);
|
|
36
|
-
return newWidth;
|
|
37
|
-
}
|
|
38
|
-
endResize(clientX) {
|
|
39
|
-
if (!this.isResizing || !this.resizeColumnId)
|
|
40
|
-
return;
|
|
41
|
-
const delta = clientX - this.resizeStartX;
|
|
42
|
-
const newWidth = Math.max(DEFAULT_MIN_COLUMN_WIDTH, this.resizeStartWidth + delta);
|
|
43
|
-
this.columnWidths.set(this.resizeColumnId, newWidth);
|
|
44
|
-
this.emitter.emit('columnWidthChange', { columnId: this.resizeColumnId, widthPx: newWidth });
|
|
45
|
-
this.isResizing = false;
|
|
46
|
-
this.resizeColumnId = null;
|
|
47
|
-
}
|
|
48
|
-
onColumnWidthChange(handler) {
|
|
49
|
-
this.emitter.on('columnWidthChange', handler);
|
|
50
|
-
return () => this.emitter.off('columnWidthChange', handler);
|
|
51
|
-
}
|
|
52
|
-
destroy() {
|
|
53
|
-
this.emitter.removeAllListeners();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export class EventEmitter {
|
|
2
|
-
constructor() {
|
|
3
|
-
this.handlers = new Map();
|
|
4
|
-
}
|
|
5
|
-
on(event, handler) {
|
|
6
|
-
if (!this.handlers.has(event)) {
|
|
7
|
-
this.handlers.set(event, new Set());
|
|
8
|
-
}
|
|
9
|
-
this.handlers.get(event)?.add(handler);
|
|
10
|
-
}
|
|
11
|
-
off(event, handler) {
|
|
12
|
-
this.handlers.get(event)?.delete(handler);
|
|
13
|
-
}
|
|
14
|
-
emit(event, ...args) {
|
|
15
|
-
const data = args[0];
|
|
16
|
-
this.handlers.get(event)?.forEach(handler => {
|
|
17
|
-
handler(data);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
removeAllListeners(event) {
|
|
21
|
-
if (event) {
|
|
22
|
-
this.handlers.delete(event);
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
this.handlers.clear();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { normalizeSelectionRange, applyFillValues } from '@alaarab/ogrid-core';
|
|
2
|
-
import { EventEmitter } from './EventEmitter';
|
|
3
|
-
import { getCellCoordinates } from '../utils/getCellCoordinates';
|
|
4
|
-
/**
|
|
5
|
-
* Manages Excel-style fill handle drag-to-fill for cell ranges (vanilla JS).
|
|
6
|
-
* Mirrors the React `useFillHandle` hook as a class-based state.
|
|
7
|
-
*/
|
|
8
|
-
export class FillHandleState {
|
|
9
|
-
constructor(params, getSelectionRange, setSelectionRange, setActiveCell) {
|
|
10
|
-
this.emitter = new EventEmitter();
|
|
11
|
-
this.wrapperRef = null;
|
|
12
|
-
this._isFillDragging = false;
|
|
13
|
-
this.fillDragStart = null;
|
|
14
|
-
this.fillDragEnd = { endRow: 0, endCol: 0 };
|
|
15
|
-
this.rafHandle = 0;
|
|
16
|
-
this.liveFillRange = null;
|
|
17
|
-
this.lastMousePos = null;
|
|
18
|
-
this.cachedCells = null;
|
|
19
|
-
this.params = params;
|
|
20
|
-
this.getSelectionRange = getSelectionRange;
|
|
21
|
-
this.setSelectionRange = setSelectionRange;
|
|
22
|
-
this.setActiveCell = setActiveCell;
|
|
23
|
-
this.onMoveBound = this.onMouseMove.bind(this);
|
|
24
|
-
this.onUpBound = this.onMouseUp.bind(this);
|
|
25
|
-
}
|
|
26
|
-
get isFillDragging() {
|
|
27
|
-
return this._isFillDragging;
|
|
28
|
-
}
|
|
29
|
-
get fillRange() {
|
|
30
|
-
return this.liveFillRange;
|
|
31
|
-
}
|
|
32
|
-
setWrapperRef(ref) {
|
|
33
|
-
this.wrapperRef = ref;
|
|
34
|
-
}
|
|
35
|
-
updateParams(params) {
|
|
36
|
-
this.params = params;
|
|
37
|
-
}
|
|
38
|
-
/** Called when the fill handle square is mousedown'd. */
|
|
39
|
-
startFillDrag(e) {
|
|
40
|
-
e.preventDefault();
|
|
41
|
-
e.stopPropagation();
|
|
42
|
-
const range = this.getSelectionRange();
|
|
43
|
-
if (!range)
|
|
44
|
-
return;
|
|
45
|
-
if (this.params.editable === false || !this.params.onCellValueChanged)
|
|
46
|
-
return;
|
|
47
|
-
this._isFillDragging = true;
|
|
48
|
-
this.fillDragStart = { startRow: range.startRow, startCol: range.startCol };
|
|
49
|
-
this.fillDragEnd = { endRow: range.startRow, endCol: range.startCol };
|
|
50
|
-
this.liveFillRange = null;
|
|
51
|
-
// Cache querySelectorAll result once on drag start
|
|
52
|
-
this.cachedCells = this.wrapperRef ? this.wrapperRef.querySelectorAll('[data-row-index][data-col-index]') : null;
|
|
53
|
-
window.addEventListener('mousemove', this.onMoveBound, { capture: true, passive: true });
|
|
54
|
-
window.addEventListener('mouseup', this.onUpBound, { capture: true, passive: true });
|
|
55
|
-
}
|
|
56
|
-
onMouseMove(e) {
|
|
57
|
-
if (!this._isFillDragging || !this.fillDragStart)
|
|
58
|
-
return;
|
|
59
|
-
this.lastMousePos = { cx: e.clientX, cy: e.clientY };
|
|
60
|
-
if (this.rafHandle)
|
|
61
|
-
cancelAnimationFrame(this.rafHandle);
|
|
62
|
-
this.rafHandle = requestAnimationFrame(() => {
|
|
63
|
-
this.rafHandle = 0;
|
|
64
|
-
if (!this.lastMousePos)
|
|
65
|
-
return;
|
|
66
|
-
const newRange = this.resolveRange(this.lastMousePos.cx, this.lastMousePos.cy);
|
|
67
|
-
if (!newRange)
|
|
68
|
-
return;
|
|
69
|
-
// Skip if unchanged
|
|
70
|
-
const prev = this.liveFillRange;
|
|
71
|
-
if (prev &&
|
|
72
|
-
prev.startRow === newRange.startRow &&
|
|
73
|
-
prev.startCol === newRange.startCol &&
|
|
74
|
-
prev.endRow === newRange.endRow &&
|
|
75
|
-
prev.endCol === newRange.endCol) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
this.liveFillRange = newRange;
|
|
79
|
-
this.fillDragEnd = { endRow: newRange.endRow, endCol: newRange.endCol };
|
|
80
|
-
this.applyDragAttrs(newRange);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
onMouseUp() {
|
|
84
|
-
if (!this._isFillDragging || !this.fillDragStart)
|
|
85
|
-
return;
|
|
86
|
-
window.removeEventListener('mousemove', this.onMoveBound, true);
|
|
87
|
-
window.removeEventListener('mouseup', this.onUpBound, true);
|
|
88
|
-
if (this.rafHandle) {
|
|
89
|
-
cancelAnimationFrame(this.rafHandle);
|
|
90
|
-
this.rafHandle = 0;
|
|
91
|
-
}
|
|
92
|
-
// Flush: resolve final position
|
|
93
|
-
if (this.lastMousePos) {
|
|
94
|
-
const flushed = this.resolveRange(this.lastMousePos.cx, this.lastMousePos.cy);
|
|
95
|
-
if (flushed) {
|
|
96
|
-
this.liveFillRange = flushed;
|
|
97
|
-
this.fillDragEnd = { endRow: flushed.endRow, endCol: flushed.endCol };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
this.clearDragAttrs();
|
|
101
|
-
const start = this.fillDragStart;
|
|
102
|
-
const end = this.fillDragEnd;
|
|
103
|
-
const norm = normalizeSelectionRange({
|
|
104
|
-
startRow: start.startRow,
|
|
105
|
-
startCol: start.startCol,
|
|
106
|
-
endRow: end.endRow,
|
|
107
|
-
endCol: end.endCol,
|
|
108
|
-
});
|
|
109
|
-
// Commit range
|
|
110
|
-
this.setSelectionRange(norm);
|
|
111
|
-
this.setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + this.params.colOffset });
|
|
112
|
-
// Apply fill values
|
|
113
|
-
this.applyFillValuesFromCore(norm, start);
|
|
114
|
-
this._isFillDragging = false;
|
|
115
|
-
this.fillDragStart = null;
|
|
116
|
-
this.liveFillRange = null;
|
|
117
|
-
this.lastMousePos = null;
|
|
118
|
-
this.emitter.emit('fillRangeChange', { fillRange: null });
|
|
119
|
-
}
|
|
120
|
-
applyFillValuesFromCore(norm, start) {
|
|
121
|
-
const { items, visibleCols, onCellValueChanged, beginBatch, endBatch } = this.params;
|
|
122
|
-
if (!onCellValueChanged)
|
|
123
|
-
return;
|
|
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?.();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
resolveRange(cx, cy) {
|
|
133
|
-
if (!this.fillDragStart || !this.wrapperRef)
|
|
134
|
-
return null;
|
|
135
|
-
const target = document.elementFromPoint(cx, cy);
|
|
136
|
-
const cell = target?.closest?.('[data-row-index][data-col-index]');
|
|
137
|
-
if (!cell || !this.wrapperRef.contains(cell))
|
|
138
|
-
return null;
|
|
139
|
-
const coords = getCellCoordinates(cell);
|
|
140
|
-
if (!coords || coords.colIndex < this.params.colOffset)
|
|
141
|
-
return null;
|
|
142
|
-
const r = coords.rowIndex;
|
|
143
|
-
const dataCol = coords.colIndex - this.params.colOffset;
|
|
144
|
-
return normalizeSelectionRange({
|
|
145
|
-
startRow: this.fillDragStart.startRow,
|
|
146
|
-
startCol: this.fillDragStart.startCol,
|
|
147
|
-
endRow: r,
|
|
148
|
-
endCol: dataCol,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
applyDragAttrs(range) {
|
|
152
|
-
const cells = this.cachedCells;
|
|
153
|
-
if (!cells)
|
|
154
|
-
return;
|
|
155
|
-
const colOff = this.params.colOffset;
|
|
156
|
-
const minR = Math.min(range.startRow, range.endRow);
|
|
157
|
-
const maxR = Math.max(range.startRow, range.endRow);
|
|
158
|
-
const minC = Math.min(range.startCol, range.endCol);
|
|
159
|
-
const maxC = Math.max(range.startCol, range.endCol);
|
|
160
|
-
for (let i = 0; i < cells.length; i++) {
|
|
161
|
-
const el = cells[i];
|
|
162
|
-
const coords = getCellCoordinates(el);
|
|
163
|
-
if (!coords)
|
|
164
|
-
continue;
|
|
165
|
-
const r = coords.rowIndex;
|
|
166
|
-
const c = coords.colIndex - colOff;
|
|
167
|
-
const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
|
|
168
|
-
if (inRange) {
|
|
169
|
-
if (!el.hasAttribute('data-drag-range'))
|
|
170
|
-
el.setAttribute('data-drag-range', '');
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
if (el.hasAttribute('data-drag-range'))
|
|
174
|
-
el.removeAttribute('data-drag-range');
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
clearDragAttrs() {
|
|
179
|
-
const cells = this.cachedCells;
|
|
180
|
-
if (cells) {
|
|
181
|
-
for (let i = 0; i < cells.length; i++)
|
|
182
|
-
cells[i].removeAttribute('data-drag-range');
|
|
183
|
-
}
|
|
184
|
-
this.cachedCells = null;
|
|
185
|
-
}
|
|
186
|
-
onFillRangeChange(handler) {
|
|
187
|
-
this.emitter.on('fillRangeChange', handler);
|
|
188
|
-
return () => this.emitter.off('fillRangeChange', handler);
|
|
189
|
-
}
|
|
190
|
-
destroy() {
|
|
191
|
-
if (this._isFillDragging) {
|
|
192
|
-
window.removeEventListener('mousemove', this.onMoveBound, true);
|
|
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;
|
|
199
|
-
}
|
|
200
|
-
if (this.rafHandle) {
|
|
201
|
-
cancelAnimationFrame(this.rafHandle);
|
|
202
|
-
this.rafHandle = 0;
|
|
203
|
-
}
|
|
204
|
-
this.emitter.removeAllListeners();
|
|
205
|
-
}
|
|
206
|
-
}
|