@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,434 +0,0 @@
|
|
|
1
|
-
import { getCellValue } from '@alaarab/ogrid-core';
|
|
2
|
-
const EDITOR_STYLE = {
|
|
3
|
-
position: 'absolute',
|
|
4
|
-
zIndex: '1000',
|
|
5
|
-
boxSizing: 'border-box',
|
|
6
|
-
border: '2px solid var(--ogrid-selection, #217346)',
|
|
7
|
-
background: 'var(--ogrid-bg, #fff)',
|
|
8
|
-
color: 'var(--ogrid-fg, #242424)',
|
|
9
|
-
outline: 'none',
|
|
10
|
-
fontFamily: 'inherit',
|
|
11
|
-
fontSize: 'inherit',
|
|
12
|
-
};
|
|
13
|
-
export class InlineCellEditor {
|
|
14
|
-
constructor(container) {
|
|
15
|
-
this.editor = null;
|
|
16
|
-
this.editingCell = null;
|
|
17
|
-
this.editingCellElement = null;
|
|
18
|
-
this.onCommit = null;
|
|
19
|
-
this.onCancel = null;
|
|
20
|
-
this.onAfterCommit = null;
|
|
21
|
-
this.container = container;
|
|
22
|
-
}
|
|
23
|
-
startEdit(rowId, columnId, item, column, cell, onCommit, onCancel, onAfterCommit) {
|
|
24
|
-
this.closeEditor();
|
|
25
|
-
this.editingCell = { rowId, columnId };
|
|
26
|
-
this.editingCellElement = cell;
|
|
27
|
-
this.onCommit = onCommit;
|
|
28
|
-
this.onCancel = onCancel;
|
|
29
|
-
this.onAfterCommit = onAfterCommit ?? null;
|
|
30
|
-
const value = getCellValue(item, column);
|
|
31
|
-
const rect = cell.getBoundingClientRect();
|
|
32
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
33
|
-
const editor = this.createEditor(column, item, value, cell);
|
|
34
|
-
editor.style.position = 'absolute';
|
|
35
|
-
editor.style.left = `${rect.left - containerRect.left + this.container.scrollLeft}px`;
|
|
36
|
-
editor.style.top = `${rect.top - containerRect.top + this.container.scrollTop}px`;
|
|
37
|
-
editor.style.width = `${rect.width}px`;
|
|
38
|
-
editor.style.height = `${rect.height}px`;
|
|
39
|
-
this.editor = editor;
|
|
40
|
-
this.container.appendChild(editor);
|
|
41
|
-
editor.focus();
|
|
42
|
-
// Position dropdown with fixed positioning to escape container overflow
|
|
43
|
-
const dropdownEl = editor.querySelector('[role="listbox"]');
|
|
44
|
-
if (dropdownEl) {
|
|
45
|
-
const maxH = 200;
|
|
46
|
-
const spaceBelow = window.innerHeight - rect.bottom;
|
|
47
|
-
const flipUp = spaceBelow < maxH && rect.top > spaceBelow;
|
|
48
|
-
dropdownEl.style.position = 'fixed';
|
|
49
|
-
dropdownEl.style.left = `${rect.left}px`;
|
|
50
|
-
dropdownEl.style.width = `${rect.width}px`;
|
|
51
|
-
dropdownEl.style.maxHeight = `${maxH}px`;
|
|
52
|
-
dropdownEl.style.zIndex = '9999';
|
|
53
|
-
dropdownEl.style.right = 'auto';
|
|
54
|
-
if (flipUp) {
|
|
55
|
-
dropdownEl.style.top = 'auto';
|
|
56
|
-
dropdownEl.style.bottom = `${window.innerHeight - rect.top}px`;
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
dropdownEl.style.top = `${rect.bottom}px`;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/** Returns the cell currently being edited, or null if no editor is open. */
|
|
64
|
-
getEditingCell() {
|
|
65
|
-
return this.editingCell;
|
|
66
|
-
}
|
|
67
|
-
closeEditor() {
|
|
68
|
-
// Reset visibility on the cell that was being edited (Bug 1 & 2 fix:
|
|
69
|
-
// the renderer sets visibility:hidden on the editing cell, and it may
|
|
70
|
-
// not re-render before the next click lands, so we clear it explicitly).
|
|
71
|
-
// Look up the cell by data attributes since the original element reference
|
|
72
|
-
// may have been replaced by a re-render.
|
|
73
|
-
if (this.editingCell && this.container.isConnected) {
|
|
74
|
-
const { rowId, columnId } = this.editingCell;
|
|
75
|
-
const row = this.container.querySelector(`tr[data-row-id="${rowId}"]`);
|
|
76
|
-
if (row) {
|
|
77
|
-
const td = row.querySelector(`td[data-column-id="${columnId}"]`);
|
|
78
|
-
if (td) {
|
|
79
|
-
td.style.visibility = '';
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (this.editingCellElement) {
|
|
84
|
-
// Also reset the original element if it's still connected in the DOM
|
|
85
|
-
if (this.editingCellElement.isConnected) {
|
|
86
|
-
this.editingCellElement.style.visibility = '';
|
|
87
|
-
}
|
|
88
|
-
this.editingCellElement = null;
|
|
89
|
-
}
|
|
90
|
-
if (this.editor) {
|
|
91
|
-
this.editor.remove();
|
|
92
|
-
this.editor = null;
|
|
93
|
-
}
|
|
94
|
-
this.editingCell = null;
|
|
95
|
-
this.onCommit = null;
|
|
96
|
-
this.onCancel = null;
|
|
97
|
-
this.onAfterCommit = null;
|
|
98
|
-
}
|
|
99
|
-
createEditor(column, item, value, cell) {
|
|
100
|
-
const editorType = column.cellEditor;
|
|
101
|
-
if (typeof editorType === 'function') {
|
|
102
|
-
const context = {
|
|
103
|
-
value,
|
|
104
|
-
onValueChange: (newValue) => {
|
|
105
|
-
if (this.editingCell) {
|
|
106
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, newValue);
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
onCommit: () => this.closeEditor(),
|
|
110
|
-
onCancel: () => {
|
|
111
|
-
this.onCancel?.();
|
|
112
|
-
this.closeEditor();
|
|
113
|
-
},
|
|
114
|
-
item,
|
|
115
|
-
column,
|
|
116
|
-
cell,
|
|
117
|
-
cellEditorParams: column.cellEditorParams,
|
|
118
|
-
};
|
|
119
|
-
return editorType(context);
|
|
120
|
-
}
|
|
121
|
-
// Built-in editor types
|
|
122
|
-
if (editorType === 'checkbox' || column.type === 'boolean') {
|
|
123
|
-
return this.createCheckboxEditor(value);
|
|
124
|
-
}
|
|
125
|
-
if (editorType === 'select') {
|
|
126
|
-
return this.createSelectEditor(value, column);
|
|
127
|
-
}
|
|
128
|
-
if (editorType === 'richSelect') {
|
|
129
|
-
return this.createRichSelectEditor(value, column);
|
|
130
|
-
}
|
|
131
|
-
if (editorType === 'date' || column.type === 'date') {
|
|
132
|
-
return this.createDateEditor(value);
|
|
133
|
-
}
|
|
134
|
-
// Default: text editor
|
|
135
|
-
return this.createTextEditor(value);
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Shared factory for text/date input editors — both types have identical event handling,
|
|
139
|
-
* differing only in input.type and initial value formatting.
|
|
140
|
-
*/
|
|
141
|
-
createInputEditor(type, initialValue) {
|
|
142
|
-
const input = document.createElement('input');
|
|
143
|
-
input.type = type;
|
|
144
|
-
input.value = initialValue;
|
|
145
|
-
Object.assign(input.style, EDITOR_STYLE);
|
|
146
|
-
input.addEventListener('keydown', (e) => {
|
|
147
|
-
if (e.key === 'Enter') {
|
|
148
|
-
e.preventDefault();
|
|
149
|
-
e.stopPropagation(); // Prevent grid wrapper from re-opening the editor
|
|
150
|
-
if (this.editingCell) {
|
|
151
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
152
|
-
}
|
|
153
|
-
const afterCommit = this.onAfterCommit;
|
|
154
|
-
this.closeEditor();
|
|
155
|
-
afterCommit?.(); // Move active cell down after closing
|
|
156
|
-
}
|
|
157
|
-
else if (e.key === 'Escape') {
|
|
158
|
-
e.preventDefault();
|
|
159
|
-
e.stopPropagation();
|
|
160
|
-
this.onCancel?.();
|
|
161
|
-
this.closeEditor();
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
input.addEventListener('blur', () => {
|
|
165
|
-
if (this.editingCell) {
|
|
166
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
167
|
-
}
|
|
168
|
-
this.closeEditor();
|
|
169
|
-
});
|
|
170
|
-
setTimeout(() => input.select(), 0);
|
|
171
|
-
return input;
|
|
172
|
-
}
|
|
173
|
-
createTextEditor(value) {
|
|
174
|
-
return this.createInputEditor('text', value != null ? String(value) : '');
|
|
175
|
-
}
|
|
176
|
-
createCheckboxEditor(value) {
|
|
177
|
-
const input = document.createElement('input');
|
|
178
|
-
input.type = 'checkbox';
|
|
179
|
-
input.checked = Boolean(value);
|
|
180
|
-
Object.assign(input.style, EDITOR_STYLE);
|
|
181
|
-
input.style.width = '20px';
|
|
182
|
-
input.style.height = '20px';
|
|
183
|
-
input.addEventListener('change', () => {
|
|
184
|
-
if (this.editingCell) {
|
|
185
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.checked);
|
|
186
|
-
}
|
|
187
|
-
this.closeEditor();
|
|
188
|
-
});
|
|
189
|
-
input.addEventListener('keydown', (e) => {
|
|
190
|
-
if (e.key === 'Escape') {
|
|
191
|
-
e.preventDefault();
|
|
192
|
-
e.stopPropagation();
|
|
193
|
-
this.onCancel?.();
|
|
194
|
-
this.closeEditor();
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
return input;
|
|
198
|
-
}
|
|
199
|
-
createDateEditor(value) {
|
|
200
|
-
let initialValue = '';
|
|
201
|
-
if (value != null) {
|
|
202
|
-
const dateStr = String(value);
|
|
203
|
-
if (dateStr.match(/^\d{4}-\d{2}-\d{2}/)) {
|
|
204
|
-
initialValue = dateStr.substring(0, 10);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return this.createInputEditor('date', initialValue);
|
|
208
|
-
}
|
|
209
|
-
createSelectEditor(value, column) {
|
|
210
|
-
const values = column.cellEditorParams?.values ?? [];
|
|
211
|
-
const formatValue = column.cellEditorParams?.formatValue;
|
|
212
|
-
const getDisplayText = (v) => formatValue ? formatValue(v) : (v != null ? String(v) : '');
|
|
213
|
-
const wrapper = document.createElement('div');
|
|
214
|
-
Object.assign(wrapper.style, EDITOR_STYLE);
|
|
215
|
-
wrapper.style.padding = '6px 10px';
|
|
216
|
-
wrapper.style.display = 'flex';
|
|
217
|
-
wrapper.style.alignItems = 'center';
|
|
218
|
-
wrapper.tabIndex = 0;
|
|
219
|
-
// Display current value + chevron
|
|
220
|
-
const display = document.createElement('div');
|
|
221
|
-
display.style.display = 'flex';
|
|
222
|
-
display.style.alignItems = 'center';
|
|
223
|
-
display.style.justifyContent = 'space-between';
|
|
224
|
-
display.style.width = '100%';
|
|
225
|
-
display.style.cursor = 'pointer';
|
|
226
|
-
display.style.fontSize = '13px';
|
|
227
|
-
const valueSpan = document.createElement('span');
|
|
228
|
-
valueSpan.textContent = getDisplayText(value);
|
|
229
|
-
display.appendChild(valueSpan);
|
|
230
|
-
const chevron = document.createElement('span');
|
|
231
|
-
chevron.textContent = '\u25BE';
|
|
232
|
-
chevron.style.marginLeft = '4px';
|
|
233
|
-
chevron.style.fontSize = '10px';
|
|
234
|
-
chevron.style.opacity = '0.5';
|
|
235
|
-
display.appendChild(chevron);
|
|
236
|
-
wrapper.appendChild(display);
|
|
237
|
-
// Dropdown list
|
|
238
|
-
const dropdown = document.createElement('div');
|
|
239
|
-
dropdown.setAttribute('role', 'listbox');
|
|
240
|
-
dropdown.style.position = 'absolute';
|
|
241
|
-
dropdown.style.top = '100%';
|
|
242
|
-
dropdown.style.left = '0';
|
|
243
|
-
dropdown.style.right = '0';
|
|
244
|
-
dropdown.style.maxHeight = '200px';
|
|
245
|
-
dropdown.style.overflowY = 'auto';
|
|
246
|
-
dropdown.style.backgroundColor = 'var(--ogrid-bg, #fff)';
|
|
247
|
-
dropdown.style.border = '1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))';
|
|
248
|
-
dropdown.style.zIndex = '1001';
|
|
249
|
-
dropdown.style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
|
|
250
|
-
wrapper.appendChild(dropdown);
|
|
251
|
-
let highlightedIndex = Math.max(values.findIndex((v) => String(v) === String(value)), 0);
|
|
252
|
-
// Build all option elements once
|
|
253
|
-
const buildOptions = () => {
|
|
254
|
-
dropdown.innerHTML = '';
|
|
255
|
-
for (let i = 0; i < values.length; i++) {
|
|
256
|
-
const val = values[i];
|
|
257
|
-
const option = document.createElement('div');
|
|
258
|
-
option.setAttribute('role', 'option');
|
|
259
|
-
option.setAttribute('aria-selected', String(i === highlightedIndex));
|
|
260
|
-
option.textContent = getDisplayText(val);
|
|
261
|
-
option.style.padding = '6px 8px';
|
|
262
|
-
option.style.cursor = 'pointer';
|
|
263
|
-
option.style.color = 'var(--ogrid-fg, #242424)';
|
|
264
|
-
if (i === highlightedIndex) {
|
|
265
|
-
option.style.background = 'var(--ogrid-bg-hover, #e8f0fe)';
|
|
266
|
-
}
|
|
267
|
-
option.addEventListener('mousedown', (e) => {
|
|
268
|
-
e.preventDefault();
|
|
269
|
-
if (this.editingCell) {
|
|
270
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, val);
|
|
271
|
-
}
|
|
272
|
-
this.closeEditor();
|
|
273
|
-
});
|
|
274
|
-
dropdown.appendChild(option);
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
// Only update CSS class on old/new highlighted item — avoids rebuilding the DOM
|
|
278
|
-
const updateHighlight = (prevIndex, nextIndex) => {
|
|
279
|
-
const prev = dropdown.children[prevIndex];
|
|
280
|
-
const next = dropdown.children[nextIndex];
|
|
281
|
-
if (prev) {
|
|
282
|
-
prev.style.background = '';
|
|
283
|
-
prev.setAttribute('aria-selected', 'false');
|
|
284
|
-
}
|
|
285
|
-
if (next) {
|
|
286
|
-
next.style.background = 'var(--ogrid-bg-hover, #e8f0fe)';
|
|
287
|
-
next.setAttribute('aria-selected', 'true');
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
const scrollHighlightedIntoView = () => {
|
|
291
|
-
const highlighted = dropdown.children[highlightedIndex];
|
|
292
|
-
highlighted?.scrollIntoView({ block: 'nearest' });
|
|
293
|
-
};
|
|
294
|
-
buildOptions();
|
|
295
|
-
wrapper.addEventListener('keydown', (e) => {
|
|
296
|
-
switch (e.key) {
|
|
297
|
-
case 'ArrowDown': {
|
|
298
|
-
e.preventDefault();
|
|
299
|
-
const prevDown = highlightedIndex;
|
|
300
|
-
highlightedIndex = Math.min(highlightedIndex + 1, values.length - 1);
|
|
301
|
-
updateHighlight(prevDown, highlightedIndex);
|
|
302
|
-
scrollHighlightedIntoView();
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
case 'ArrowUp': {
|
|
306
|
-
e.preventDefault();
|
|
307
|
-
const prevUp = highlightedIndex;
|
|
308
|
-
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
309
|
-
updateHighlight(prevUp, highlightedIndex);
|
|
310
|
-
scrollHighlightedIntoView();
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
case 'Enter':
|
|
314
|
-
e.preventDefault();
|
|
315
|
-
e.stopPropagation();
|
|
316
|
-
if (values.length > 0 && highlightedIndex < values.length) {
|
|
317
|
-
if (this.editingCell) {
|
|
318
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, values[highlightedIndex]);
|
|
319
|
-
}
|
|
320
|
-
const afterCommit = this.onAfterCommit;
|
|
321
|
-
this.closeEditor();
|
|
322
|
-
afterCommit?.();
|
|
323
|
-
}
|
|
324
|
-
break;
|
|
325
|
-
case 'Tab':
|
|
326
|
-
e.preventDefault();
|
|
327
|
-
if (values.length > 0 && highlightedIndex < values.length) {
|
|
328
|
-
if (this.editingCell) {
|
|
329
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, values[highlightedIndex]);
|
|
330
|
-
}
|
|
331
|
-
this.closeEditor();
|
|
332
|
-
}
|
|
333
|
-
break;
|
|
334
|
-
case 'Escape':
|
|
335
|
-
e.preventDefault();
|
|
336
|
-
e.stopPropagation();
|
|
337
|
-
this.onCancel?.();
|
|
338
|
-
this.closeEditor();
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
return wrapper;
|
|
343
|
-
}
|
|
344
|
-
createRichSelectEditor(value, column) {
|
|
345
|
-
const wrapper = document.createElement('div');
|
|
346
|
-
Object.assign(wrapper.style, EDITOR_STYLE);
|
|
347
|
-
wrapper.style.padding = '0';
|
|
348
|
-
const input = document.createElement('input');
|
|
349
|
-
input.type = 'text';
|
|
350
|
-
input.value = value != null ? String(value) : '';
|
|
351
|
-
input.style.width = '100%';
|
|
352
|
-
input.style.border = 'none';
|
|
353
|
-
input.style.outline = 'none';
|
|
354
|
-
input.style.padding = '4px';
|
|
355
|
-
input.style.boxSizing = 'border-box';
|
|
356
|
-
input.style.background = 'var(--ogrid-bg, #fff)';
|
|
357
|
-
input.style.color = 'var(--ogrid-fg, rgba(0, 0, 0, 0.87))';
|
|
358
|
-
wrapper.appendChild(input);
|
|
359
|
-
const dropdown = document.createElement('div');
|
|
360
|
-
dropdown.style.position = 'absolute';
|
|
361
|
-
dropdown.style.top = '100%';
|
|
362
|
-
dropdown.style.left = '0';
|
|
363
|
-
dropdown.style.width = '100%';
|
|
364
|
-
dropdown.style.maxHeight = '200px';
|
|
365
|
-
dropdown.style.overflowY = 'auto';
|
|
366
|
-
dropdown.style.backgroundColor = 'var(--ogrid-bg, #fff)';
|
|
367
|
-
dropdown.style.border = '1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))';
|
|
368
|
-
dropdown.style.zIndex = '1001';
|
|
369
|
-
wrapper.appendChild(dropdown);
|
|
370
|
-
const values = column.cellEditorParams?.values ?? [];
|
|
371
|
-
const formatValue = column.cellEditorParams?.formatValue ?? ((v) => String(v));
|
|
372
|
-
const renderOptions = (filter) => {
|
|
373
|
-
dropdown.innerHTML = '';
|
|
374
|
-
const filtered = values.filter((v) => String(formatValue(v)).toLowerCase().includes(filter.toLowerCase()));
|
|
375
|
-
for (const val of filtered) {
|
|
376
|
-
const option = document.createElement('div');
|
|
377
|
-
option.textContent = String(formatValue(val));
|
|
378
|
-
option.style.padding = '4px 8px';
|
|
379
|
-
option.style.cursor = 'pointer';
|
|
380
|
-
option.addEventListener('mousedown', (e) => {
|
|
381
|
-
e.preventDefault();
|
|
382
|
-
if (this.editingCell) {
|
|
383
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, val);
|
|
384
|
-
}
|
|
385
|
-
this.closeEditor();
|
|
386
|
-
});
|
|
387
|
-
option.addEventListener('mouseenter', () => {
|
|
388
|
-
option.style.backgroundColor = 'var(--ogrid-hover-bg, rgba(0, 0, 0, 0.04))';
|
|
389
|
-
}, { passive: true });
|
|
390
|
-
option.addEventListener('mouseleave', () => {
|
|
391
|
-
option.style.backgroundColor = 'var(--ogrid-bg, #fff)';
|
|
392
|
-
}, { passive: true });
|
|
393
|
-
dropdown.appendChild(option);
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
input.addEventListener('input', () => {
|
|
397
|
-
renderOptions(input.value);
|
|
398
|
-
});
|
|
399
|
-
input.addEventListener('keydown', (e) => {
|
|
400
|
-
if (e.key === 'Enter') {
|
|
401
|
-
e.preventDefault();
|
|
402
|
-
e.stopPropagation(); // Prevent grid wrapper from re-opening the editor
|
|
403
|
-
if (this.editingCell) {
|
|
404
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
405
|
-
}
|
|
406
|
-
const afterCommit = this.onAfterCommit;
|
|
407
|
-
this.closeEditor();
|
|
408
|
-
afterCommit?.(); // Move active cell down after closing
|
|
409
|
-
}
|
|
410
|
-
else if (e.key === 'Escape') {
|
|
411
|
-
e.preventDefault();
|
|
412
|
-
e.stopPropagation();
|
|
413
|
-
this.onCancel?.();
|
|
414
|
-
this.closeEditor();
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
input.addEventListener('blur', (e) => {
|
|
418
|
-
const related = e.relatedTarget;
|
|
419
|
-
if (related && this.editor?.contains(related)) {
|
|
420
|
-
return; // Focus moved within the editor (e.g., to dropdown), don't close
|
|
421
|
-
}
|
|
422
|
-
if (this.editingCell) {
|
|
423
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
424
|
-
}
|
|
425
|
-
this.closeEditor();
|
|
426
|
-
});
|
|
427
|
-
renderOptions('');
|
|
428
|
-
setTimeout(() => input.select(), 0);
|
|
429
|
-
return wrapper;
|
|
430
|
-
}
|
|
431
|
-
destroy() {
|
|
432
|
-
this.closeEditor();
|
|
433
|
-
}
|
|
434
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { injectGlobalStyles, measureRange as measureRangeCore, rangesEqual } from '@alaarab/ogrid-core';
|
|
2
|
-
/**
|
|
3
|
-
* Measure the bounding rect of a range within a container, with scroll offsets.
|
|
4
|
-
* This variant adds scroll offsets for the JS implementation's scrollable container.
|
|
5
|
-
*/
|
|
6
|
-
function measureRange(container, range, colOffset) {
|
|
7
|
-
const rect = measureRangeCore(container, range, colOffset);
|
|
8
|
-
if (!rect)
|
|
9
|
-
return null;
|
|
10
|
-
// Add scroll offsets for JS implementation's scrollable container
|
|
11
|
-
return {
|
|
12
|
-
top: rect.top + container.scrollTop,
|
|
13
|
-
left: rect.left + container.scrollLeft,
|
|
14
|
-
width: rect.width,
|
|
15
|
-
height: rect.height,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* MarchingAntsOverlay — renders SVG overlays on top of the grid:
|
|
20
|
-
* 1. Selection range: solid green border
|
|
21
|
-
* 2. Copy/Cut range: animated dashed border (marching ants)
|
|
22
|
-
*
|
|
23
|
-
* Vanilla JS equivalent of React's MarchingAntsOverlay component.
|
|
24
|
-
*/
|
|
25
|
-
export class MarchingAntsOverlay {
|
|
26
|
-
constructor(container, colOffset = 0) {
|
|
27
|
-
this.selSvg = null;
|
|
28
|
-
this.clipSvg = null;
|
|
29
|
-
this.selectionRange = null;
|
|
30
|
-
this.copyRange = null;
|
|
31
|
-
this.cutRange = null;
|
|
32
|
-
this.rafHandle = 0;
|
|
33
|
-
this.layoutVersion = 0; // Tracks layout changes to force re-measurement
|
|
34
|
-
this.container = container;
|
|
35
|
-
this.colOffset = colOffset;
|
|
36
|
-
injectGlobalStyles('ogrid-marching-ants-keyframes', '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}');
|
|
37
|
-
// The container must be positioned for absolute SVGs
|
|
38
|
-
const pos = getComputedStyle(container).position;
|
|
39
|
-
if (pos === 'static' || pos === '') {
|
|
40
|
-
container.style.position = 'relative';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
update(selectionRange, copyRange, cutRange, layoutVersion) {
|
|
44
|
-
// Track layout changes separately from range changes
|
|
45
|
-
const layoutChanged = layoutVersion !== undefined && layoutVersion !== this.layoutVersion;
|
|
46
|
-
if (layoutChanged && layoutVersion !== undefined) {
|
|
47
|
-
this.layoutVersion = layoutVersion;
|
|
48
|
-
}
|
|
49
|
-
// Skip if nothing changed (ranges or layout)
|
|
50
|
-
if (!layoutChanged &&
|
|
51
|
-
rangesEqual(this.selectionRange, selectionRange) &&
|
|
52
|
-
rangesEqual(this.copyRange, copyRange) &&
|
|
53
|
-
rangesEqual(this.cutRange, cutRange)) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
this.selectionRange = selectionRange;
|
|
57
|
-
this.copyRange = copyRange;
|
|
58
|
-
this.cutRange = cutRange;
|
|
59
|
-
// Delay one frame so cells are rendered
|
|
60
|
-
if (this.rafHandle)
|
|
61
|
-
cancelAnimationFrame(this.rafHandle);
|
|
62
|
-
this.rafHandle = requestAnimationFrame(() => {
|
|
63
|
-
this.rafHandle = 0;
|
|
64
|
-
this.render();
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
render() {
|
|
68
|
-
const clipRange = this.copyRange ?? this.cutRange;
|
|
69
|
-
// Selection range SVG
|
|
70
|
-
const selRect = this.selectionRange
|
|
71
|
-
? measureRange(this.container, this.selectionRange, this.colOffset)
|
|
72
|
-
: null;
|
|
73
|
-
// When clipboard range matches selection, hide selection border so marching ants show
|
|
74
|
-
const clipRangeMatchesSel = this.selectionRange != null &&
|
|
75
|
-
clipRange != null &&
|
|
76
|
-
rangesEqual(this.selectionRange, clipRange);
|
|
77
|
-
if (selRect && !clipRangeMatchesSel) {
|
|
78
|
-
if (!this.selSvg) {
|
|
79
|
-
this.selSvg = this.createSvg(4);
|
|
80
|
-
this.container.appendChild(this.selSvg);
|
|
81
|
-
}
|
|
82
|
-
this.positionSvg(this.selSvg, selRect);
|
|
83
|
-
const rect = this.selSvg.querySelector('rect');
|
|
84
|
-
if (rect) {
|
|
85
|
-
rect.setAttribute('width', String(Math.max(0, selRect.width - 2)));
|
|
86
|
-
rect.setAttribute('height', String(Math.max(0, selRect.height - 2)));
|
|
87
|
-
rect.setAttribute('stroke', 'var(--ogrid-selection, #217346)');
|
|
88
|
-
rect.setAttribute('stroke-width', '2');
|
|
89
|
-
rect.removeAttribute('stroke-dasharray');
|
|
90
|
-
rect.style.animation = '';
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
this.removeSvg('sel');
|
|
95
|
-
}
|
|
96
|
-
// Copy/Cut range SVG (marching ants)
|
|
97
|
-
const clipRect = clipRange
|
|
98
|
-
? measureRange(this.container, clipRange, this.colOffset)
|
|
99
|
-
: null;
|
|
100
|
-
if (clipRect) {
|
|
101
|
-
if (!this.clipSvg) {
|
|
102
|
-
this.clipSvg = this.createSvg(5);
|
|
103
|
-
this.container.appendChild(this.clipSvg);
|
|
104
|
-
}
|
|
105
|
-
this.positionSvg(this.clipSvg, clipRect);
|
|
106
|
-
const rect = this.clipSvg.querySelector('rect');
|
|
107
|
-
if (rect) {
|
|
108
|
-
rect.setAttribute('width', String(Math.max(0, clipRect.width - 2)));
|
|
109
|
-
rect.setAttribute('height', String(Math.max(0, clipRect.height - 2)));
|
|
110
|
-
rect.setAttribute('stroke', 'var(--ogrid-selection, #217346)');
|
|
111
|
-
rect.setAttribute('stroke-width', '2');
|
|
112
|
-
rect.setAttribute('stroke-dasharray', '4 4');
|
|
113
|
-
rect.style.animation = 'ogrid-marching-ants 0.5s linear infinite';
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
this.removeSvg('clip');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
createSvg(zIndex) {
|
|
121
|
-
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
122
|
-
svg.setAttribute('aria-hidden', 'true');
|
|
123
|
-
svg.style.position = 'absolute';
|
|
124
|
-
svg.style.pointerEvents = 'none';
|
|
125
|
-
svg.style.zIndex = String(zIndex);
|
|
126
|
-
svg.style.overflow = 'visible';
|
|
127
|
-
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
128
|
-
rect.setAttribute('x', '1');
|
|
129
|
-
rect.setAttribute('y', '1');
|
|
130
|
-
rect.setAttribute('fill', 'none');
|
|
131
|
-
svg.appendChild(rect);
|
|
132
|
-
return svg;
|
|
133
|
-
}
|
|
134
|
-
positionSvg(svg, rect) {
|
|
135
|
-
svg.style.top = `${rect.top}px`;
|
|
136
|
-
svg.style.left = `${rect.left}px`;
|
|
137
|
-
svg.style.width = `${rect.width}px`;
|
|
138
|
-
svg.style.height = `${rect.height}px`;
|
|
139
|
-
}
|
|
140
|
-
removeSvg(which) {
|
|
141
|
-
if (which === 'sel' && this.selSvg) {
|
|
142
|
-
this.selSvg.remove();
|
|
143
|
-
this.selSvg = null;
|
|
144
|
-
}
|
|
145
|
-
else if (which === 'clip' && this.clipSvg) {
|
|
146
|
-
this.clipSvg.remove();
|
|
147
|
-
this.clipSvg = null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
destroy() {
|
|
151
|
-
if (this.rafHandle)
|
|
152
|
-
cancelAnimationFrame(this.rafHandle);
|
|
153
|
-
this.removeSvg('sel');
|
|
154
|
-
this.removeSvg('clip');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { getPaginationViewModel } from '@alaarab/ogrid-core';
|
|
2
|
-
export class PaginationControls {
|
|
3
|
-
constructor(container, state) {
|
|
4
|
-
this.el = null;
|
|
5
|
-
this.container = container;
|
|
6
|
-
this.state = state;
|
|
7
|
-
}
|
|
8
|
-
render(totalCount, pageSizeOptions) {
|
|
9
|
-
if (this.el)
|
|
10
|
-
this.el.remove();
|
|
11
|
-
const vm = getPaginationViewModel(this.state.page, this.state.pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined);
|
|
12
|
-
if (!vm)
|
|
13
|
-
return; // No pagination if totalCount is 0
|
|
14
|
-
this.el = document.createElement('div');
|
|
15
|
-
this.el.className = 'ogrid-pagination';
|
|
16
|
-
// Page size selector
|
|
17
|
-
const pageSizeDiv = document.createElement('div');
|
|
18
|
-
pageSizeDiv.className = 'ogrid-pagination-size';
|
|
19
|
-
const label = document.createElement('span');
|
|
20
|
-
label.textContent = 'Rows per page: ';
|
|
21
|
-
pageSizeDiv.appendChild(label);
|
|
22
|
-
const select = document.createElement('select');
|
|
23
|
-
select.className = 'ogrid-page-size-select';
|
|
24
|
-
for (const size of vm.pageSizeOptions) {
|
|
25
|
-
const option = document.createElement('option');
|
|
26
|
-
option.value = String(size);
|
|
27
|
-
option.textContent = String(size);
|
|
28
|
-
option.selected = size === this.state.pageSize;
|
|
29
|
-
select.appendChild(option);
|
|
30
|
-
}
|
|
31
|
-
select.addEventListener('change', () => {
|
|
32
|
-
this.state.setPageSize(Number(select.value));
|
|
33
|
-
});
|
|
34
|
-
pageSizeDiv.appendChild(select);
|
|
35
|
-
this.el.appendChild(pageSizeDiv);
|
|
36
|
-
// Page info
|
|
37
|
-
const info = document.createElement('span');
|
|
38
|
-
info.className = 'ogrid-pagination-info';
|
|
39
|
-
info.textContent = `${vm.startItem}-${vm.endItem} of ${totalCount}`;
|
|
40
|
-
this.el.appendChild(info);
|
|
41
|
-
// Navigation buttons
|
|
42
|
-
const nav = document.createElement('div');
|
|
43
|
-
nav.className = 'ogrid-pagination-nav';
|
|
44
|
-
const prevBtn = document.createElement('button');
|
|
45
|
-
prevBtn.textContent = '\u25C0';
|
|
46
|
-
prevBtn.className = 'ogrid-pagination-btn';
|
|
47
|
-
prevBtn.disabled = this.state.page === 1;
|
|
48
|
-
prevBtn.addEventListener('click', () => this.state.setPage(this.state.page - 1));
|
|
49
|
-
nav.appendChild(prevBtn);
|
|
50
|
-
// Start ellipsis
|
|
51
|
-
if (vm.showStartEllipsis) {
|
|
52
|
-
const ellipsis = document.createElement('span');
|
|
53
|
-
ellipsis.textContent = '...';
|
|
54
|
-
ellipsis.className = 'ogrid-pagination-ellipsis';
|
|
55
|
-
nav.appendChild(ellipsis);
|
|
56
|
-
}
|
|
57
|
-
// Page number buttons
|
|
58
|
-
for (const pageNum of vm.pageNumbers) {
|
|
59
|
-
const btn = document.createElement('button');
|
|
60
|
-
btn.textContent = String(pageNum);
|
|
61
|
-
btn.className = 'ogrid-pagination-btn' + (pageNum === this.state.page ? ' ogrid-pagination-active' : '');
|
|
62
|
-
btn.addEventListener('click', () => this.state.setPage(pageNum));
|
|
63
|
-
nav.appendChild(btn);
|
|
64
|
-
}
|
|
65
|
-
// End ellipsis
|
|
66
|
-
if (vm.showEndEllipsis) {
|
|
67
|
-
const ellipsis = document.createElement('span');
|
|
68
|
-
ellipsis.textContent = '...';
|
|
69
|
-
ellipsis.className = 'ogrid-pagination-ellipsis';
|
|
70
|
-
nav.appendChild(ellipsis);
|
|
71
|
-
}
|
|
72
|
-
const nextBtn = document.createElement('button');
|
|
73
|
-
nextBtn.textContent = '\u25B6';
|
|
74
|
-
nextBtn.className = 'ogrid-pagination-btn';
|
|
75
|
-
nextBtn.disabled = this.state.page === vm.totalPages;
|
|
76
|
-
nextBtn.addEventListener('click', () => this.state.setPage(this.state.page + 1));
|
|
77
|
-
nav.appendChild(nextBtn);
|
|
78
|
-
this.el.appendChild(nav);
|
|
79
|
-
this.container.appendChild(this.el);
|
|
80
|
-
}
|
|
81
|
-
destroy() {
|
|
82
|
-
this.el?.remove();
|
|
83
|
-
this.el = null;
|
|
84
|
-
}
|
|
85
|
-
}
|