@alaarab/ogrid-js 2.0.15 → 2.0.16

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.
@@ -39,6 +39,26 @@ export class InlineCellEditor {
39
39
  this.editor = editor;
40
40
  this.container.appendChild(editor);
41
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
+ }
42
62
  }
43
63
  /** Returns the cell currently being edited, or null if no editor is open. */
44
64
  getEditingCell() {
@@ -205,31 +225,121 @@ export class InlineCellEditor {
205
225
  return input;
206
226
  }
207
227
  createSelectEditor(value, column) {
208
- const select = document.createElement('select');
209
228
  const values = column.cellEditorParams?.values ?? [];
210
- for (const val of values) {
211
- const option = document.createElement('option');
212
- option.value = String(val);
213
- option.textContent = String(val);
214
- select.appendChild(option);
215
- }
216
- select.value = value != null ? String(value) : '';
217
- Object.assign(select.style, EDITOR_STYLE);
218
- select.addEventListener('change', () => {
219
- if (this.editingCell) {
220
- this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, select.value);
229
+ const formatValue = column.cellEditorParams?.formatValue;
230
+ const getDisplayText = (v) => formatValue ? formatValue(v) : (v != null ? String(v) : '');
231
+ const wrapper = document.createElement('div');
232
+ Object.assign(wrapper.style, EDITOR_STYLE);
233
+ wrapper.style.padding = '6px 10px';
234
+ wrapper.style.display = 'flex';
235
+ wrapper.style.alignItems = 'center';
236
+ wrapper.tabIndex = 0;
237
+ // Display current value + chevron
238
+ const display = document.createElement('div');
239
+ display.style.display = 'flex';
240
+ display.style.alignItems = 'center';
241
+ display.style.justifyContent = 'space-between';
242
+ display.style.width = '100%';
243
+ display.style.cursor = 'pointer';
244
+ display.style.fontSize = '13px';
245
+ const valueSpan = document.createElement('span');
246
+ valueSpan.textContent = getDisplayText(value);
247
+ display.appendChild(valueSpan);
248
+ const chevron = document.createElement('span');
249
+ chevron.textContent = '\u25BE';
250
+ chevron.style.marginLeft = '4px';
251
+ chevron.style.fontSize = '10px';
252
+ chevron.style.opacity = '0.5';
253
+ display.appendChild(chevron);
254
+ wrapper.appendChild(display);
255
+ // Dropdown list
256
+ const dropdown = document.createElement('div');
257
+ dropdown.setAttribute('role', 'listbox');
258
+ dropdown.style.position = 'absolute';
259
+ dropdown.style.top = '100%';
260
+ dropdown.style.left = '0';
261
+ dropdown.style.right = '0';
262
+ dropdown.style.maxHeight = '200px';
263
+ dropdown.style.overflowY = 'auto';
264
+ dropdown.style.backgroundColor = 'var(--ogrid-bg, #fff)';
265
+ dropdown.style.border = '1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12))';
266
+ dropdown.style.zIndex = '1001';
267
+ dropdown.style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
268
+ wrapper.appendChild(dropdown);
269
+ let highlightedIndex = Math.max(values.findIndex((v) => String(v) === String(value)), 0);
270
+ const renderOptions = () => {
271
+ dropdown.innerHTML = '';
272
+ for (let i = 0; i < values.length; i++) {
273
+ const val = values[i];
274
+ const option = document.createElement('div');
275
+ option.setAttribute('role', 'option');
276
+ option.setAttribute('aria-selected', String(i === highlightedIndex));
277
+ option.textContent = getDisplayText(val);
278
+ option.style.padding = '6px 8px';
279
+ option.style.cursor = 'pointer';
280
+ option.style.color = 'var(--ogrid-fg, #242424)';
281
+ if (i === highlightedIndex) {
282
+ option.style.background = 'var(--ogrid-bg-hover, #e8f0fe)';
283
+ }
284
+ option.addEventListener('mousedown', (e) => {
285
+ e.preventDefault();
286
+ if (this.editingCell) {
287
+ this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, val);
288
+ }
289
+ this.closeEditor();
290
+ });
291
+ dropdown.appendChild(option);
221
292
  }
222
- this.closeEditor();
223
- });
224
- select.addEventListener('keydown', (e) => {
225
- if (e.key === 'Escape') {
226
- e.preventDefault();
227
- e.stopPropagation();
228
- this.onCancel?.();
229
- this.closeEditor();
293
+ };
294
+ const scrollHighlightedIntoView = () => {
295
+ const highlighted = dropdown.children[highlightedIndex];
296
+ highlighted?.scrollIntoView({ block: 'nearest' });
297
+ };
298
+ renderOptions();
299
+ wrapper.addEventListener('keydown', (e) => {
300
+ switch (e.key) {
301
+ case 'ArrowDown':
302
+ e.preventDefault();
303
+ highlightedIndex = Math.min(highlightedIndex + 1, values.length - 1);
304
+ renderOptions();
305
+ scrollHighlightedIntoView();
306
+ break;
307
+ case 'ArrowUp':
308
+ e.preventDefault();
309
+ highlightedIndex = Math.max(highlightedIndex - 1, 0);
310
+ renderOptions();
311
+ scrollHighlightedIntoView();
312
+ break;
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;
230
340
  }
231
341
  });
232
- return select;
342
+ return wrapper;
233
343
  }
234
344
  createRichSelectEditor(value, column) {
235
345
  const wrapper = document.createElement('div');
@@ -1,4 +1,4 @@
1
- import { injectGlobalStyles, measureRange as measureRangeCore } from '@alaarab/ogrid-core';
1
+ import { injectGlobalStyles, measureRange as measureRangeCore, rangesEqual } from '@alaarab/ogrid-core';
2
2
  /**
3
3
  * Measure the bounding rect of a range within a container, with scroll offsets.
4
4
  * This variant adds scroll offsets for the JS implementation's scrollable container.
@@ -15,14 +15,6 @@ function measureRange(container, range, colOffset) {
15
15
  height: rect.height,
16
16
  };
17
17
  }
18
- function rangesEqual(a, b) {
19
- if (a === b)
20
- return true;
21
- if (!a || !b)
22
- return false;
23
- return a.startRow === b.startRow && a.endRow === b.endRow &&
24
- a.startCol === b.startCol && a.endCol === b.endCol;
25
- }
26
18
  /**
27
19
  * MarchingAntsOverlay — renders SVG overlays on top of the grid:
28
20
  * 1. Selection range: solid green border
@@ -1,4 +1,4 @@
1
- import { normalizeSelectionRange, getCellValue } from '@alaarab/ogrid-core';
1
+ import { normalizeSelectionRange, getCellValue, formatSelectionAsTsv, parseTsvClipboard } from '@alaarab/ogrid-core';
2
2
  import { parseValue } from '@alaarab/ogrid-core';
3
3
  import { EventEmitter } from './EventEmitter';
4
4
  export class ClipboardState {
@@ -33,21 +33,7 @@ export class ClipboardState {
33
33
  return;
34
34
  const norm = normalizeSelectionRange(range);
35
35
  const { items, visibleCols } = this.params;
36
- const rows = [];
37
- for (let r = norm.startRow; r <= norm.endRow; r++) {
38
- const cells = [];
39
- for (let c = norm.startCol; c <= norm.endCol; c++) {
40
- if (r >= items.length || c >= visibleCols.length)
41
- break;
42
- const item = items[r];
43
- const col = visibleCols[c];
44
- const raw = getCellValue(item, col);
45
- const val = col.valueFormatter ? col.valueFormatter(raw, item) : raw;
46
- cells.push(val != null && val !== '' ? String(val).replace(/\t/g, ' ').replace(/\n/g, ' ') : '');
47
- }
48
- rows.push(cells.join('\t'));
49
- }
50
- const tsv = rows.join('\r\n');
36
+ const tsv = formatSelectionAsTsv(items, visibleCols, norm);
51
37
  this.internalClipboard = tsv;
52
38
  this._copyRange = norm;
53
39
  this._cutRange = null;
@@ -98,9 +84,9 @@ export class ClipboardState {
98
84
  const anchorRow = norm ? norm.startRow : 0;
99
85
  const anchorCol = norm ? norm.startCol : 0;
100
86
  const { items, visibleCols } = this.params;
101
- const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
87
+ const lines = parseTsvClipboard(text);
102
88
  for (let r = 0; r < lines.length; r++) {
103
- const cells = lines[r].split('\t');
89
+ const cells = lines[r];
104
90
  for (let c = 0; c < cells.length; c++) {
105
91
  const targetRow = anchorRow + r;
106
92
  const targetCol = anchorCol + c;
@@ -1,31 +1,5 @@
1
- import { normalizeSelectionRange, getCellValue } from '@alaarab/ogrid-core';
1
+ import { normalizeSelectionRange, getCellValue, findCtrlArrowTarget as findCtrlTarget, computeTabNavigation } from '@alaarab/ogrid-core';
2
2
  import { parseValue } from '@alaarab/ogrid-core';
3
- /**
4
- * Excel-style Ctrl+Arrow: find the target position along a 1D axis.
5
- * - Non-empty current + non-empty next → scan through non-empties, stop at last before empty/edge.
6
- * - Otherwise → skip empties, land on next non-empty or edge.
7
- */
8
- function findCtrlTarget(pos, edge, step, isEmpty) {
9
- if (pos === edge)
10
- return pos;
11
- const next = pos + step;
12
- if (!isEmpty(pos) && !isEmpty(next)) {
13
- let p = next;
14
- while (p !== edge) {
15
- if (isEmpty(p + step))
16
- return p;
17
- p += step;
18
- }
19
- return edge;
20
- }
21
- let p = next;
22
- while (p !== edge) {
23
- if (!isEmpty(p))
24
- return p;
25
- p += step;
26
- }
27
- return edge;
28
- }
29
3
  export class KeyboardNavState {
30
4
  constructor(params, getActiveCell, getSelectionRange, setActiveCell, setSelectionRange) {
31
5
  this.wrapperRef = null;
@@ -184,32 +158,13 @@ export class KeyboardNavState {
184
158
  }
185
159
  case 'Tab': {
186
160
  e.preventDefault();
187
- let newRowTab = rowIndex;
188
- let newColTab = columnIndex;
189
- if (e.shiftKey) {
190
- if (columnIndex > colOffset) {
191
- newColTab = columnIndex - 1;
192
- }
193
- else if (rowIndex > 0) {
194
- newRowTab = rowIndex - 1;
195
- newColTab = maxColIndex;
196
- }
197
- }
198
- else {
199
- if (columnIndex < maxColIndex) {
200
- newColTab = columnIndex + 1;
201
- }
202
- else if (rowIndex < maxRowIndex) {
203
- newRowTab = rowIndex + 1;
204
- newColTab = colOffset;
205
- }
206
- }
207
- const newDataColTab = newColTab - colOffset;
208
- this.setActiveCell({ rowIndex: newRowTab, columnIndex: newColTab });
161
+ const tabResult = computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColIndex, colOffset, e.shiftKey);
162
+ const newDataColTab = tabResult.columnIndex - colOffset;
163
+ this.setActiveCell({ rowIndex: tabResult.rowIndex, columnIndex: tabResult.columnIndex });
209
164
  this.setSelectionRange({
210
- startRow: newRowTab,
165
+ startRow: tabResult.rowIndex,
211
166
  startCol: newDataColTab,
212
- endRow: newRowTab,
167
+ endRow: tabResult.rowIndex,
213
168
  endCol: newDataColTab,
214
169
  });
215
170
  break;
@@ -1,13 +1,5 @@
1
+ import { rangesEqual } from '@alaarab/ogrid-core';
1
2
  import { EventEmitter } from './EventEmitter';
2
- /** Compares two selection ranges by value to avoid redundant RAF work. */
3
- function rangesEqual(a, b) {
4
- if (a === b)
5
- return true;
6
- if (!a || !b)
7
- return false;
8
- return a.startRow === b.startRow && a.endRow === b.endRow &&
9
- a.startCol === b.startCol && a.endCol === b.endCol;
10
- }
11
3
  export class SelectionState {
12
4
  constructor() {
13
5
  this.emitter = new EventEmitter();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-js",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "description": "OGrid vanilla JS – framework-free data grid with sorting, filtering, pagination, and spreadsheet-style editing.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",