@alaarab/ogrid-vue 2.1.15 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { injectGlobalStyles, Z_INDEX, getStatusBarParts, measureRange, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, validateColumns, validateRowIds, computeRowSelectionState, buildCellIndex, UndoRedoStack, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, computeAggregations, getDataGridStatusBarConfig, validateVirtualScrollConfig, computeVisibleRange, computeTotalHeight, buildHeaderRows, ROW_NUMBER_COLUMN_WIDTH, getHeaderFilterConfig, getCellRenderDescriptor, buildInlineEditorProps, buildPopoverEditorProps, resolveCellDisplayContent, resolveCellStyle, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, measureColumnContentWidth, getPinStateForColumn, parseValue, applyFillValues, applyCellDeletion, computeTabNavigation, computeArrowNavigation, computeNextSortState, mergeFilter, applyRangeRowSelection, getScrollTopForRow, getCellValue, calculateDropTarget, reorderColumnArray, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
1
+ import { injectGlobalStyles, Z_INDEX, getStatusBarParts, measureRange, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, processClientSideDataAsync, validateColumns, validateRowIds, computeRowSelectionState, buildCellIndex, UndoRedoStack, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, computeAggregations, getDataGridStatusBarConfig, validateVirtualScrollConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, partitionColumnsForVirtualization, formatCellReference, buildHeaderRows, indexToColumnLetter, ROW_NUMBER_COLUMN_WIDTH, getHeaderFilterConfig, getCellRenderDescriptor, buildInlineEditorProps, buildPopoverEditorProps, resolveCellDisplayContent, resolveCellStyle, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, measureColumnContentWidth, getPinStateForColumn, parseValue, applyFillValues, applyCellDeletion, computeTabNavigation, computeArrowNavigation, computeNextSortState, mergeFilter, applyRangeRowSelection, getScrollTopForRow, FormulaEngine, getCellValue, calculateDropTarget, reorderColumnArray, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
2
2
  export * from '@alaarab/ogrid-core';
3
3
  export { buildInlineEditorProps, buildPopoverEditorProps, getCellRenderDescriptor, getHeaderFilterConfig, isInSelectionRange, normalizeSelectionRange, resolveCellDisplayContent, resolveCellStyle, toUserLike } from '@alaarab/ogrid-core';
4
4
  import { defineComponent, ref, computed, onMounted, watch, toValue, onUnmounted, h, shallowRef, triggerRef, nextTick, Teleport, isRef, isReadonly, unref, customRef } from 'vue';
@@ -210,6 +210,126 @@ function useFilterOptions(dataSource, fields) {
210
210
  }, { immediate: true });
211
211
  return { filterOptions, loadingOptions };
212
212
  }
213
+ function useLatestRef(source) {
214
+ let value = unref(source);
215
+ return customRef((track, trigger) => ({
216
+ get() {
217
+ if (isRef(source)) {
218
+ value = source.value;
219
+ }
220
+ return value;
221
+ },
222
+ set(newValue) {
223
+ value = newValue;
224
+ trigger();
225
+ }
226
+ }));
227
+ }
228
+
229
+ // src/composables/useFormulaEngine.ts
230
+ function useFormulaEngine(params) {
231
+ const {
232
+ formulas,
233
+ items,
234
+ flatColumns,
235
+ initialFormulas,
236
+ onFormulaRecalc,
237
+ formulaFunctions,
238
+ namedRanges,
239
+ sheets
240
+ } = params;
241
+ const itemsRef = useLatestRef(items);
242
+ const flatColumnsRef = useLatestRef(flatColumns);
243
+ const onFormulaRecalcRef = useLatestRef(onFormulaRecalc);
244
+ const engineRef = shallowRef(null);
245
+ let initialLoaded = false;
246
+ const enabled = computed(() => formulas?.value ?? false);
247
+ function createAccessor() {
248
+ const currentItems = itemsRef.value;
249
+ const currentCols = flatColumnsRef.value;
250
+ return {
251
+ getCellValue: (col, row) => {
252
+ if (row < 0 || row >= currentItems.length) return null;
253
+ if (col < 0 || col >= currentCols.length) return null;
254
+ return getCellValue(currentItems[row], currentCols[col]);
255
+ },
256
+ getRowCount: () => currentItems.length,
257
+ getColumnCount: () => currentCols.length
258
+ };
259
+ }
260
+ watch(
261
+ enabled,
262
+ (isEnabled) => {
263
+ if (isEnabled && !engineRef.value) {
264
+ engineRef.value = new FormulaEngine({
265
+ customFunctions: formulaFunctions,
266
+ namedRanges
267
+ });
268
+ if (sheets) {
269
+ for (const [name, accessor] of Object.entries(sheets)) {
270
+ engineRef.value.registerSheet(name, accessor);
271
+ }
272
+ }
273
+ if (initialFormulas && !initialLoaded) {
274
+ initialLoaded = true;
275
+ const accessor = createAccessor();
276
+ const result = engineRef.value.loadFormulas(initialFormulas, accessor);
277
+ if (result.updatedCells.length > 0) {
278
+ onFormulaRecalcRef.value?.(result);
279
+ }
280
+ }
281
+ } else if (!isEnabled && engineRef.value) {
282
+ engineRef.value = null;
283
+ }
284
+ },
285
+ { immediate: true }
286
+ );
287
+ function getFormulaValue(col, row) {
288
+ return engineRef.value?.getValue(col, row);
289
+ }
290
+ function hasFormula(col, row) {
291
+ return engineRef.value?.hasFormula(col, row) ?? false;
292
+ }
293
+ function getFormula(col, row) {
294
+ return engineRef.value?.getFormula(col, row);
295
+ }
296
+ function setFormula(col, row, formula) {
297
+ if (!engineRef.value) return;
298
+ const accessor = createAccessor();
299
+ const result = engineRef.value.setFormula(col, row, formula, accessor);
300
+ if (result.updatedCells.length > 0) {
301
+ onFormulaRecalcRef.value?.(result);
302
+ }
303
+ }
304
+ function onCellChanged(col, row) {
305
+ if (!engineRef.value) return;
306
+ const accessor = createAccessor();
307
+ const result = engineRef.value.onCellChanged(col, row, accessor);
308
+ if (result.updatedCells.length > 0) {
309
+ onFormulaRecalcRef.value?.(result);
310
+ }
311
+ }
312
+ function getPrecedents(col, row) {
313
+ return engineRef.value?.getPrecedents(col, row) ?? [];
314
+ }
315
+ function getDependents(col, row) {
316
+ return engineRef.value?.getDependents(col, row) ?? [];
317
+ }
318
+ function getAuditTrail(col, row) {
319
+ return engineRef.value?.getAuditTrail(col, row) ?? null;
320
+ }
321
+ return {
322
+ enabled,
323
+ getFormulaValue,
324
+ hasFormula,
325
+ getFormula,
326
+ setFormula,
327
+ onCellChanged,
328
+ getPrecedents,
329
+ getDependents,
330
+ getAuditTrail
331
+ };
332
+ }
213
333
  var DEFAULT_PANELS = ["columns", "filters"];
214
334
  function useSideBarState(params) {
215
335
  const { config } = params;
@@ -331,7 +451,11 @@ function useOGrid(props) {
331
451
  return new Set(visible.length > 0 ? visible : columns.value.map((c) => c.columnId));
332
452
  })());
333
453
  const columnWidthOverrides = ref({});
334
- const pinnedOverrides = ref({});
454
+ const initialPinned = {};
455
+ for (const col of flattenColumns(props.value.columns)) {
456
+ if (col.pinned) initialPinned[col.columnId] = col.pinned;
457
+ }
458
+ const pinnedOverrides = ref(initialPinned);
335
459
  const page = computed(() => controlledState.value.page ?? internalPage.value);
336
460
  const pageSize = computed(() => controlledState.value.pageSize ?? internalPageSize.value);
337
461
  const sort = computed(() => controlledState.value.sort ?? internalSort.value);
@@ -389,8 +513,9 @@ function useOGrid(props) {
389
513
  if (hasServerFilterOptions.value) return serverFilterOptions.value;
390
514
  return deriveFilterOptionsFromData(displayData.value, columns.value);
391
515
  });
516
+ const workerSortEnabled = computed(() => !!props.value.workerSort);
392
517
  const clientItemsAndTotal = computed(() => {
393
- if (!isClientSide.value) return null;
518
+ if (!isClientSide.value || workerSortEnabled.value) return null;
394
519
  const rows = processClientSideData(
395
520
  displayData.value,
396
521
  columns.value,
@@ -403,6 +528,42 @@ function useOGrid(props) {
403
528
  const paged = rows.slice(start, start + pageSize.value);
404
529
  return { items: paged, totalCount: total };
405
530
  });
531
+ const asyncClientItems = ref(null);
532
+ let workerSortAbortId = 0;
533
+ watch(
534
+ [isClientSide, workerSortEnabled, displayData, columns, filters, () => sort.value.field, () => sort.value.direction, page, pageSize],
535
+ () => {
536
+ if (!isClientSide.value || !workerSortEnabled.value) return;
537
+ const data = displayData.value;
538
+ const cols = columns.value;
539
+ const f = filters.value;
540
+ const sf = sort.value.field;
541
+ const sd = sort.value.direction;
542
+ const p = page.value;
543
+ const ps = pageSize.value;
544
+ const abortId = ++workerSortAbortId;
545
+ processClientSideDataAsync(data, cols, f, sf, sd).then((rows) => {
546
+ if (abortId !== workerSortAbortId || isDestroyed) return;
547
+ const total = rows.length;
548
+ const start = (p - 1) * ps;
549
+ const paged = rows.slice(start, start + ps);
550
+ asyncClientItems.value = { items: paged, totalCount: total };
551
+ }).catch(() => {
552
+ if (abortId !== workerSortAbortId || isDestroyed) return;
553
+ const rows = processClientSideData(data, cols, f, sf, sd);
554
+ const total = rows.length;
555
+ const start = (p - 1) * ps;
556
+ const paged = rows.slice(start, start + ps);
557
+ asyncClientItems.value = { items: paged, totalCount: total };
558
+ });
559
+ },
560
+ { immediate: true }
561
+ );
562
+ const resolvedClientItems = computed(() => {
563
+ const syncResult = clientItemsAndTotal.value;
564
+ if (syncResult) return syncResult;
565
+ return asyncClientItems.value;
566
+ });
406
567
  const serverItems = ref([]);
407
568
  const serverTotalCount = ref(0);
408
569
  const loading = ref(false);
@@ -448,11 +609,22 @@ function useOGrid(props) {
448
609
  isDestroyed = true;
449
610
  });
450
611
  const displayItems = computed(
451
- () => isClientSide.value && clientItemsAndTotal.value ? clientItemsAndTotal.value.items : serverItems.value
612
+ () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.items : serverItems.value
452
613
  );
453
614
  const displayTotalCount = computed(
454
- () => isClientSide.value && clientItemsAndTotal.value ? clientItemsAndTotal.value.totalCount : serverTotalCount.value
615
+ () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.totalCount : serverTotalCount.value
455
616
  );
617
+ const formulasRef = computed(() => !!props.value.formulas);
618
+ const formulaEngine = useFormulaEngine({
619
+ formulas: formulasRef,
620
+ items: displayItems,
621
+ flatColumns: columns,
622
+ initialFormulas: props.value.initialFormulas,
623
+ onFormulaRecalc: props.value.onFormulaRecalc,
624
+ formulaFunctions: props.value.formulaFunctions,
625
+ namedRanges: props.value.namedRanges,
626
+ sheets: props.value.sheets
627
+ });
456
628
  let firstDataRendered = false;
457
629
  let rowIdsValidated = false;
458
630
  watch(displayItems, (items) => {
@@ -540,6 +712,10 @@ function useOGrid(props) {
540
712
  });
541
713
  const clearAllFilters = () => setFilters({});
542
714
  const isLoadingResolved = computed(() => isServerSide.value && loading.value || displayLoading.value);
715
+ const activeCellRef = ref(null);
716
+ const onActiveCellChange = (cellRef) => {
717
+ activeCellRef.value = cellRef;
718
+ };
543
719
  const dataGridProps = computed(() => {
544
720
  const p = props.value;
545
721
  const ds = dataProps.value.dataSource;
@@ -567,7 +743,10 @@ function useOGrid(props) {
567
743
  rowSelection: p.rowSelection ?? "none",
568
744
  selectedRows: effectiveSelectedRows.value,
569
745
  onSelectionChange: handleSelectionChange,
570
- showRowNumbers: p.showRowNumbers,
746
+ showRowNumbers: p.showRowNumbers || p.cellReferences,
747
+ showColumnLetters: !!p.cellReferences,
748
+ showNameBox: !!p.cellReferences,
749
+ onActiveCellChange: p.cellReferences ? onActiveCellChange : void 0,
571
750
  currentPage: page.value,
572
751
  pageSize: pageSize.value,
573
752
  statusBar: statusBarConfig.value,
@@ -592,7 +771,18 @@ function useOGrid(props) {
592
771
  onClearAll: clearAllFilters,
593
772
  message: p.emptyState?.message,
594
773
  render: p.emptyState?.render
595
- }
774
+ },
775
+ formulas: p.formulas,
776
+ ...formulaEngine.enabled.value ? {
777
+ getFormulaValue: formulaEngine.getFormulaValue,
778
+ hasFormula: formulaEngine.hasFormula,
779
+ getFormula: formulaEngine.getFormula,
780
+ setFormula: formulaEngine.setFormula,
781
+ onFormulaCellChanged: formulaEngine.onCellChanged,
782
+ getPrecedents: formulaEngine.getPrecedents,
783
+ getDependents: formulaEngine.getDependents,
784
+ getAuditTrail: formulaEngine.getAuditTrail
785
+ } : {}
596
786
  };
597
787
  });
598
788
  const pagination = computed(() => ({
@@ -610,14 +800,38 @@ function useOGrid(props) {
610
800
  onVisibilityChange: handleVisibilityChange,
611
801
  placement: columnChooserPlacement.value
612
802
  }));
613
- const layout = computed(() => ({
614
- toolbar: props.value.toolbar,
615
- toolbarBelow: props.value.toolbarBelow,
616
- className: props.value.className,
617
- emptyState: props.value.emptyState,
618
- sideBarProps: sideBarProps.value,
619
- fullScreen: props.value.fullScreen
620
- }));
803
+ const layout = computed(() => {
804
+ const showNameBox = !!props.value.cellReferences;
805
+ let resolvedToolbar = props.value.toolbar;
806
+ if (showNameBox) {
807
+ const nameBoxEl = h("div", {
808
+ style: {
809
+ display: "inline-flex",
810
+ alignItems: "center",
811
+ padding: "0 8px",
812
+ fontFamily: "'Consolas', 'Courier New', monospace",
813
+ fontSize: "12px",
814
+ border: "1px solid rgba(0,0,0,0.12)",
815
+ borderRadius: "3px",
816
+ height: "24px",
817
+ marginRight: "8px",
818
+ background: "#fff",
819
+ minWidth: "40px",
820
+ color: "rgba(0,0,0,0.6)"
821
+ },
822
+ "aria-label": "Active cell reference"
823
+ }, activeCellRef.value ?? "\u2014");
824
+ resolvedToolbar = [nameBoxEl, resolvedToolbar];
825
+ }
826
+ return {
827
+ toolbar: resolvedToolbar,
828
+ toolbarBelow: props.value.toolbarBelow,
829
+ className: props.value.className,
830
+ emptyState: props.value.emptyState,
831
+ sideBarProps: sideBarProps.value,
832
+ fullScreen: props.value.fullScreen
833
+ };
834
+ });
621
835
  const filtersResult = computed(() => ({
622
836
  hasActiveFilters: hasActiveFilters.value,
623
837
  setFilters
@@ -839,23 +1053,6 @@ function useActiveCell(wrapperRef, editingCell) {
839
1053
  });
840
1054
  return { activeCell, setActiveCell };
841
1055
  }
842
- function useLatestRef(source) {
843
- let value = unref(source);
844
- return customRef((track, trigger) => ({
845
- get() {
846
- if (isRef(source)) {
847
- value = source.value;
848
- }
849
- return value;
850
- },
851
- set(newValue) {
852
- value = newValue;
853
- trigger();
854
- }
855
- }));
856
- }
857
-
858
- // src/composables/useCellSelection.ts
859
1056
  var DRAG_ATTR = "data-drag-range";
860
1057
  var DRAG_ANCHOR_ATTR = "data-drag-anchor";
861
1058
  var AUTO_SCROLL_EDGE = 40;
@@ -1272,7 +1469,7 @@ function useKeyboardNavigation(params) {
1272
1469
  const maxColIndex = visibleColumnCount - 1 + colOffset;
1273
1470
  if (items.length === 0) return;
1274
1471
  if (activeCell === null) {
1275
- if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Tab", "Enter", "Home", "End"].includes(e.key)) {
1472
+ if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Tab", "Enter", "Home", "End", "PageDown", "PageUp"].includes(e.key)) {
1276
1473
  setActiveCell({ rowIndex: 0, columnIndex: colOffset });
1277
1474
  e.preventDefault();
1278
1475
  }
@@ -1372,6 +1569,36 @@ function useKeyboardNavigation(params) {
1372
1569
  setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
1373
1570
  break;
1374
1571
  }
1572
+ case "PageDown":
1573
+ case "PageUp": {
1574
+ e.preventDefault();
1575
+ const wrapperEl = wrapperRef.value;
1576
+ let pageSize = 10;
1577
+ if (wrapperEl) {
1578
+ const row = wrapperEl.querySelector("tbody tr");
1579
+ if (row && row.offsetHeight > 0) pageSize = Math.max(1, Math.floor(wrapperEl.clientHeight / row.offsetHeight));
1580
+ }
1581
+ const pgDirection = e.key === "PageDown" ? 1 : -1;
1582
+ const newRowPage = Math.max(0, Math.min(rowIndex + pgDirection * pageSize, maxRowIndex));
1583
+ if (shift) {
1584
+ setSelectionRange({
1585
+ startRow: selectionRange?.startRow ?? rowIndex,
1586
+ startCol: selectionRange?.startCol ?? dataColIndex,
1587
+ endRow: newRowPage,
1588
+ endCol: selectionRange?.endCol ?? dataColIndex
1589
+ });
1590
+ } else {
1591
+ setSelectionRange({
1592
+ startRow: newRowPage,
1593
+ startCol: dataColIndex,
1594
+ endRow: newRowPage,
1595
+ endCol: dataColIndex
1596
+ });
1597
+ }
1598
+ setActiveCell({ rowIndex: newRowPage, columnIndex });
1599
+ scrollToRow?.(newRowPage, "center");
1600
+ break;
1601
+ }
1375
1602
  case "Enter":
1376
1603
  case "F2": {
1377
1604
  e.preventDefault();
@@ -2929,14 +3156,14 @@ function useInlineCellEditorState(params) {
2929
3156
  e.stopPropagation();
2930
3157
  cancel();
2931
3158
  }
2932
- if (e.key === "Enter" && editorType === "text") {
3159
+ if (e.key === "Enter" && (editorType === "text" || editorType === "date")) {
2933
3160
  e.preventDefault();
2934
3161
  e.stopPropagation();
2935
3162
  commit(localValue.value);
2936
3163
  }
2937
3164
  };
2938
3165
  const handleBlur = () => {
2939
- if (editorType === "text") {
3166
+ if (editorType === "text" || editorType === "date") {
2940
3167
  commit(localValue.value);
2941
3168
  }
2942
3169
  };
@@ -3104,13 +3331,24 @@ function useColumnReorder(params) {
3104
3331
  }
3105
3332
  var DEFAULT_PASSTHROUGH_THRESHOLD = 100;
3106
3333
  function useVirtualScroll(params) {
3107
- const { totalRows, rowHeight, enabled, overscan = 5, threshold = DEFAULT_PASSTHROUGH_THRESHOLD } = params;
3334
+ const {
3335
+ totalRows,
3336
+ rowHeight,
3337
+ enabled,
3338
+ overscan = 5,
3339
+ threshold = DEFAULT_PASSTHROUGH_THRESHOLD,
3340
+ columnsEnabled,
3341
+ columnWidths,
3342
+ columnOverscan = 2
3343
+ } = params;
3108
3344
  onMounted(() => {
3109
3345
  validateVirtualScrollConfig({ enabled: enabled.value, rowHeight });
3110
3346
  });
3111
3347
  const containerRef = ref(null);
3112
3348
  const scrollTop = ref(0);
3349
+ const scrollLeft = ref(0);
3113
3350
  const containerHeight = ref(0);
3351
+ const containerWidth = ref(0);
3114
3352
  let rafId = 0;
3115
3353
  let resizeObserver;
3116
3354
  let prevObservedEl = null;
@@ -3131,6 +3369,17 @@ function useVirtualScroll(params) {
3131
3369
  if (!enabled.value) return 0;
3132
3370
  return computeTotalHeight(totalRows.value, rowHeight);
3133
3371
  });
3372
+ const columnRange = computed(() => {
3373
+ if (!columnsEnabled?.value) return null;
3374
+ const widths = columnWidths?.value;
3375
+ if (!widths || widths.length === 0) return null;
3376
+ return computeVisibleColumnRange(
3377
+ scrollLeft.value,
3378
+ widths,
3379
+ containerWidth.value,
3380
+ columnOverscan
3381
+ );
3382
+ });
3134
3383
  const onScroll = () => {
3135
3384
  if (!rafId) {
3136
3385
  rafId = requestAnimationFrame(() => {
@@ -3138,6 +3387,7 @@ function useVirtualScroll(params) {
3138
3387
  const el = containerRef.value;
3139
3388
  if (el) {
3140
3389
  scrollTop.value = el.scrollTop;
3390
+ scrollLeft.value = el.scrollLeft;
3141
3391
  }
3142
3392
  });
3143
3393
  }
@@ -3146,6 +3396,7 @@ function useVirtualScroll(params) {
3146
3396
  const el = containerRef.value;
3147
3397
  if (!el) return;
3148
3398
  containerHeight.value = el.clientHeight;
3399
+ containerWidth.value = el.clientWidth;
3149
3400
  };
3150
3401
  watch(containerRef, (el) => {
3151
3402
  if (el === prevObservedEl) return;
@@ -3165,6 +3416,7 @@ function useVirtualScroll(params) {
3165
3416
  }
3166
3417
  measure();
3167
3418
  scrollTop.value = el.scrollTop;
3419
+ scrollLeft.value = el.scrollLeft;
3168
3420
  }
3169
3421
  });
3170
3422
  onUnmounted(() => {
@@ -3183,7 +3435,7 @@ function useVirtualScroll(params) {
3183
3435
  if (!el) return;
3184
3436
  el.scrollTop = getScrollTopForRow(index, rowHeight, containerHeight.value, align);
3185
3437
  };
3186
- return { containerRef, visibleRange, totalHeight, scrollToRow };
3438
+ return { containerRef, visibleRange, totalHeight, scrollToRow, columnRange, scrollLeft };
3187
3439
  }
3188
3440
  function useDataGridTableSetup(params) {
3189
3441
  const { props: propsRef } = params;
@@ -3207,11 +3459,45 @@ function useDataGridTableSetup(params) {
3207
3459
  const totalRowsRef = computed(() => propsRef.value.items.length);
3208
3460
  const rowHeight = propsRef.value.virtualScroll?.rowHeight ?? 36;
3209
3461
  const overscan = propsRef.value.virtualScroll?.overscan ?? 5;
3462
+ const columnsVirtEnabled = computed(() => propsRef.value.virtualScroll?.columns === true);
3463
+ const columnOverscan = propsRef.value.virtualScroll?.columnOverscan ?? 2;
3464
+ const unpinnedColumnWidths = computed(() => {
3465
+ const layout = state.layout.value;
3466
+ const { visibleCols, columnSizingOverrides } = layout;
3467
+ const pinnedCols = propsRef.value.pinnedColumns ?? {};
3468
+ const widths = [];
3469
+ for (const col of visibleCols) {
3470
+ if (pinnedCols[col.columnId] || col.pinned) continue;
3471
+ const override = columnSizingOverrides[col.columnId];
3472
+ widths.push(override ? override.widthPx : col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
3473
+ }
3474
+ return widths;
3475
+ });
3210
3476
  const virtualScroll = useVirtualScroll({
3211
3477
  totalRows: totalRowsRef,
3212
3478
  rowHeight,
3213
3479
  enabled: virtualScrollEnabled,
3214
- overscan
3480
+ overscan,
3481
+ columnsEnabled: columnsVirtEnabled,
3482
+ columnWidths: unpinnedColumnWidths,
3483
+ columnOverscan
3484
+ });
3485
+ const columnPartition = computed(() => {
3486
+ if (!columnsVirtEnabled.value) return null;
3487
+ const layout = state.layout.value;
3488
+ const cols = layout.visibleCols;
3489
+ const range = virtualScroll.columnRange.value;
3490
+ const pinnedCols = propsRef.value.pinnedColumns;
3491
+ return partitionColumnsForVirtualization(cols, range, pinnedCols);
3492
+ });
3493
+ const globalColIndexMap = computed(() => {
3494
+ const layout = state.layout.value;
3495
+ const cols = layout.visibleCols;
3496
+ const map = /* @__PURE__ */ new Map();
3497
+ for (let i = 0; i < cols.length; i++) {
3498
+ map.set(cols[i].columnId, i);
3499
+ }
3500
+ return map;
3215
3501
  });
3216
3502
  const columnSizingOverridesRef = computed(() => state.layout.value.columnSizingOverrides);
3217
3503
  const columnResize = useColumnResize({
@@ -3227,13 +3513,16 @@ function useDataGridTableSetup(params) {
3227
3513
  columnReorder,
3228
3514
  virtualScroll,
3229
3515
  virtualScrollEnabled,
3230
- columnResize
3516
+ columnResize,
3517
+ columnPartition,
3518
+ globalColIndexMap
3231
3519
  };
3232
3520
  }
3233
3521
  function getCellInteractionProps(descriptor, columnId, handlers) {
3234
3522
  const base = {
3235
3523
  "data-row-index": descriptor.rowIndex,
3236
3524
  "data-col-index": descriptor.globalColIndex,
3525
+ ...descriptor.isActive ? { "data-active-cell": "true" } : {},
3237
3526
  ...descriptor.isInRange ? { "data-in-range": "true" } : {},
3238
3527
  tabindex: descriptor.isActive ? 0 : -1,
3239
3528
  onMousedown: (e) => handlers.handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex),
@@ -3265,8 +3554,28 @@ function createDataGridTable(ui) {
3265
3554
  columnReorder: { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown: handleReorderMouseDown },
3266
3555
  virtualScroll: { containerRef: vsContainerRef, visibleRange, totalHeight: _totalHeight, scrollToRow: _scrollToRow },
3267
3556
  virtualScrollEnabled,
3268
- columnResize: { handleResizeStart, handleResizeDoubleClick, getColumnWidth }
3557
+ columnResize: { handleResizeStart, handleResizeDoubleClick, getColumnWidth },
3558
+ columnPartition,
3559
+ globalColIndexMap
3269
3560
  } = useDataGridTableSetup({ props: propsRef });
3561
+ const rowNumberOffset = computed(() => {
3562
+ const p = propsRef.value;
3563
+ const hasRowNumbers = p.showRowNumbers || p.showColumnLetters;
3564
+ return hasRowNumbers ? ((p.currentPage ?? 1) - 1) * (p.pageSize ?? 25) : 0;
3565
+ });
3566
+ watch(
3567
+ [() => state.interaction.value.activeCell, rowNumberOffset],
3568
+ ([ac, offset]) => {
3569
+ const cb = propsRef.value.onActiveCellChange;
3570
+ if (!cb) return;
3571
+ if (ac) {
3572
+ cb(formatCellReference(ac.columnIndex, offset + ac.rowIndex + 1));
3573
+ } else {
3574
+ cb(null);
3575
+ }
3576
+ },
3577
+ { immediate: true }
3578
+ );
3270
3579
  const onWrapperMousedown = (e) => {
3271
3580
  lastMouseShift.value = e.shiftKey;
3272
3581
  };
@@ -3342,7 +3651,7 @@ function createDataGridTable(ui) {
3342
3651
  } = layout;
3343
3652
  const currentPage = p.currentPage ?? 1;
3344
3653
  const pageSize = p.pageSize ?? 25;
3345
- const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
3654
+ const rowNumberOffset2 = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
3346
3655
  const { selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
3347
3656
  const { editingCell: _editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
3348
3657
  const {
@@ -3432,13 +3741,14 @@ function createDataGridTable(ui) {
3432
3741
  ]);
3433
3742
  }
3434
3743
  const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
3435
- const cellStyle = resolveCellStyle(col, item);
3744
+ const cellStyle = resolveCellStyle(col, item, descriptor.displayValue);
3436
3745
  const interactionProps2 = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
3437
3746
  const cellClasses = ["ogrid-cell-content"];
3438
3747
  if (col.type === "numeric") cellClasses.push("ogrid-cell-content--numeric");
3439
3748
  else if (col.type === "boolean") cellClasses.push("ogrid-cell-content--boolean");
3440
3749
  if (descriptor.canEditAny) cellClasses.push("ogrid-cell-content--editable");
3441
3750
  if (descriptor.isActive) cellClasses.push("ogrid-cell-content--active");
3751
+ if (descriptor.isActive && descriptor.isInRange) cellClasses.push("ogrid-cell-content--active-in-range");
3442
3752
  if (descriptor.isInRange && !descriptor.isActive) cellClasses.push("ogrid-cell-in-range");
3443
3753
  if (descriptor.isInCutRange) cellClasses.push("ogrid-cell-cut");
3444
3754
  const styledContent = cellStyle ? h("span", { style: cellStyle }, content) : content;
@@ -3457,11 +3767,25 @@ function createDataGridTable(ui) {
3457
3767
  ]);
3458
3768
  };
3459
3769
  const { cellStyles: colCellStyles, cellClasses: colCellClasses, hdrStyles: colHdrStyles, hdrClasses: colHdrClasses } = columnMetaCache.value;
3460
- const columnLayouts = visibleCols.map((col) => ({
3770
+ const allColumnLayouts = visibleCols.map((col) => ({
3461
3771
  col,
3462
3772
  tdClasses: colCellClasses[col.columnId] || "ogrid-data-cell",
3463
3773
  tdDynamicStyle: colCellStyles[col.columnId] || {}
3464
3774
  }));
3775
+ const partition = columnPartition.value;
3776
+ let columnLayouts = allColumnLayouts;
3777
+ let leftSpacerWidth = 0;
3778
+ let rightSpacerWidth = 0;
3779
+ if (partition) {
3780
+ const visibleIds = /* @__PURE__ */ new Set();
3781
+ for (const col of partition.pinnedLeft) visibleIds.add(col.columnId);
3782
+ for (const col of partition.virtualizedUnpinned) visibleIds.add(col.columnId);
3783
+ for (const col of partition.pinnedRight) visibleIds.add(col.columnId);
3784
+ columnLayouts = allColumnLayouts.filter((cl) => visibleIds.has(cl.col.columnId));
3785
+ leftSpacerWidth = partition.leftSpacerWidth;
3786
+ rightSpacerWidth = partition.rightSpacerWidth;
3787
+ }
3788
+ const colIndexMap = globalColIndexMap.value;
3465
3789
  const getHeaderClassAndStyle = (col) => {
3466
3790
  const base = colHdrStyles[col.columnId] || {};
3467
3791
  return {
@@ -3522,13 +3846,27 @@ function createDataGridTable(ui) {
3522
3846
  },
3523
3847
  class: "ogrid-table",
3524
3848
  role: "grid",
3525
- style: { minWidth: `${minTableWidth}px` }
3849
+ style: { minWidth: `${minTableWidth}px` },
3850
+ ...virtualScrollEnabled.value ? { "data-virtual-scroll": "" } : {}
3526
3851
  }, [
3527
3852
  // Header
3528
- h(
3529
- "thead",
3530
- { class: stickyHeader ? "ogrid-thead ogrid-sticky-header" : "ogrid-thead" },
3531
- headerRows.map(
3853
+ h("thead", { class: stickyHeader ? "ogrid-thead ogrid-sticky-header" : "ogrid-thead" }, [
3854
+ // Column letter row (A, B, C...) for cell references
3855
+ ...p.showColumnLetters ? [
3856
+ h("tr", { class: "ogrid-column-letter-row" }, [
3857
+ ...hasCheckboxCol ? [h("th", { class: "ogrid-column-letter-cell" })] : [],
3858
+ ...hasRowNumbersCol ? [h("th", { class: "ogrid-column-letter-cell" })] : [],
3859
+ ...visibleCols.map((col, colIdx) => {
3860
+ const { classes: hdrCls, style: hdrSty } = getHeaderClassAndStyle(col);
3861
+ return h("th", {
3862
+ key: col.columnId,
3863
+ class: `ogrid-column-letter-cell ${hdrCls}`,
3864
+ style: hdrSty
3865
+ }, indexToColumnLetter(colIdx));
3866
+ })
3867
+ ])
3868
+ ] : [],
3869
+ ...headerRows.map(
3532
3870
  (row, rowIdx) => h("tr", { key: rowIdx, class: "ogrid-header-row" }, [
3533
3871
  // Checkbox header cell
3534
3872
  ...rowIdx === headerRows.length - 1 && hasCheckboxCol ? [
@@ -3638,7 +3976,7 @@ function createDataGridTable(ui) {
3638
3976
  })
3639
3977
  ])
3640
3978
  )
3641
- ),
3979
+ ]),
3642
3980
  // Body
3643
3981
  ...!showEmptyInGrid ? [
3644
3982
  h("tbody", {}, (() => {
@@ -3703,17 +4041,25 @@ function createDataGridTable(ui) {
3703
4041
  left: hasCheckboxCol ? `${CHECKBOX_COLUMN_WIDTH}px` : "0",
3704
4042
  zIndex: 2
3705
4043
  }
3706
- }, String(rowNumberOffset + rowIndex + 1))
4044
+ }, String(rowNumberOffset2 + rowIndex + 1))
4045
+ ] : [],
4046
+ // Left spacer for column virtualization
4047
+ ...leftSpacerWidth > 0 ? [
4048
+ h("td", { key: "__col-spacer-left", style: { width: `${leftSpacerWidth}px`, minWidth: `${leftSpacerWidth}px`, maxWidth: `${leftSpacerWidth}px`, padding: "0" } })
3707
4049
  ] : [],
3708
4050
  // Data cells
3709
4051
  ...columnLayouts.map(
3710
- (cl, colIdx) => h("td", {
4052
+ (cl) => h("td", {
3711
4053
  key: cl.col.columnId,
3712
4054
  "data-column-id": cl.col.columnId,
3713
4055
  class: cl.tdClasses,
3714
4056
  style: cl.tdDynamicStyle
3715
- }, [renderCellContent(item, cl.col, rowIndex, colIdx)])
3716
- )
4057
+ }, [renderCellContent(item, cl.col, rowIndex, colIndexMap.get(cl.col.columnId) ?? 0)])
4058
+ ),
4059
+ // Right spacer for column virtualization
4060
+ ...rightSpacerWidth > 0 ? [
4061
+ h("td", { key: "__col-spacer-right", style: { width: `${rightSpacerWidth}px`, minWidth: `${rightSpacerWidth}px`, maxWidth: `${rightSpacerWidth}px`, padding: "0" } })
4062
+ ] : []
3717
4063
  ]));
3718
4064
  }
3719
4065
  if (vsEnabled && vr.offsetBottom > 0) {
@@ -3851,6 +4197,7 @@ function createInlineCellEditor(options) {
3851
4197
  dropdown.style.maxHeight = `${maxH}px`;
3852
4198
  dropdown.style.zIndex = "9999";
3853
4199
  dropdown.style.right = "auto";
4200
+ dropdown.style.textAlign = "left";
3854
4201
  if (flipUp) {
3855
4202
  dropdown.style.top = "auto";
3856
4203
  dropdown.style.bottom = `${window.innerHeight - rect.top}px`;
@@ -3956,7 +4303,7 @@ function createInlineCellEditor(options) {
3956
4303
  selectDropdownRef.value = el;
3957
4304
  },
3958
4305
  role: "listbox",
3959
- style: { position: "absolute", top: "100%", left: "0", right: "0", maxHeight: "200px", overflowY: "auto", background: "var(--ogrid-bg, #fff)", border: "1px solid var(--ogrid-border, rgba(0,0,0,0.12))", zIndex: "10", boxShadow: "0 4px 16px rgba(0,0,0,0.2)" }
4306
+ style: { position: "absolute", top: "100%", left: "0", right: "0", maxHeight: "200px", overflowY: "auto", background: "var(--ogrid-bg, #fff)", border: "1px solid var(--ogrid-border, rgba(0,0,0,0.12))", zIndex: "10", boxShadow: "0 4px 16px rgba(0,0,0,0.2)", textAlign: "left" }
3960
4307
  }, values.map(
3961
4308
  (v, i) => h("div", {
3962
4309
  key: String(v),
@@ -140,6 +140,24 @@
140
140
  color: var(--ogrid-fg);
141
141
  }
142
142
 
143
+ /* === Column letter row (cell references) === */
144
+
145
+ .ogrid-column-letter-row {
146
+ background: var(--ogrid-column-letter-bg, var(--ogrid-header-bg));
147
+ }
148
+
149
+ .ogrid-column-letter-cell {
150
+ text-align: center;
151
+ font-size: 11px;
152
+ font-weight: 500;
153
+ color: var(--ogrid-fg-muted, rgba(0, 0, 0, 0.5));
154
+ padding: 2px 4px;
155
+ background: var(--ogrid-column-letter-bg, var(--ogrid-header-bg));
156
+ border-bottom: 1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12));
157
+ user-select: none;
158
+ font-variant-numeric: tabular-nums;
159
+ }
160
+
143
161
  /* === Checkbox column === */
144
162
 
145
163
  .ogrid-checkbox-header,
@@ -189,8 +207,16 @@
189
207
  position: relative;
190
208
  padding: 0;
191
209
  height: 1px;
210
+ contain: content;
192
211
  }
193
212
 
213
+ /* Pinned columns need contain: none because contain breaks position: sticky */
214
+ .ogrid-data-cell--pinned-left,
215
+ .ogrid-data-cell--pinned-right { contain: none; }
216
+
217
+ /* content-visibility: auto on rows for non-virtualized grids */
218
+ .ogrid-table:not([data-virtual-scroll]) tbody tr { content-visibility: auto; }
219
+
194
220
  .ogrid-data-cell--pinned-left {
195
221
  position: sticky;
196
222
  z-index: var(--ogrid-z-pinned, 6);
@@ -246,6 +272,14 @@
246
272
  overflow: visible;
247
273
  }
248
274
 
275
+ /* Active cell inside a selection range: suppress outline (Excel behavior).
276
+ The range overlay border is sufficient; the active cell is distinguished
277
+ by its white background vs. the dimmed range cells. */
278
+ .ogrid-cell-content--active-in-range {
279
+ outline: none;
280
+ background: var(--ogrid-bg, #fff);
281
+ }
282
+
249
283
  /* === Editing cell wrapper === */
250
284
 
251
285
  .ogrid-editing-cell {
@@ -30,6 +30,8 @@ export { useLatestRef } from './useLatestRef';
30
30
  export type { MaybeShallowRef } from './useLatestRef';
31
31
  export { useTableLayout } from './useTableLayout';
32
32
  export type { UseTableLayoutParams, UseTableLayoutResult } from './useTableLayout';
33
+ export { useFormulaEngine } from './useFormulaEngine';
34
+ export type { UseFormulaEngineParams, UseFormulaEngineResult } from './useFormulaEngine';
33
35
  export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
34
36
  export type { UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, } from './useColumnHeaderFilterState';
35
37
  export { useTextFilterState } from './useTextFilterState';
@@ -1,5 +1,5 @@
1
1
  import { type Ref } from 'vue';
2
- import type { IOGridDataGridProps } from '../types';
2
+ import type { IOGridDataGridProps, IColumnDef } from '../types';
3
3
  import { type UseDataGridStateResult } from './useDataGridState';
4
4
  import { type UseColumnResizeResult } from './useColumnResize';
5
5
  import { type UseColumnReorderResult } from './useColumnReorder';
@@ -27,6 +27,16 @@ export interface UseDataGridTableSetupResult<T> {
27
27
  virtualScrollEnabled: Ref<boolean>;
28
28
  /** Column resize handlers (handleResizeStart, getColumnWidth). */
29
29
  columnResize: UseColumnResizeResult<T>;
30
+ /** Column virtualization partition (or null when column virtualization is off). */
31
+ columnPartition: Ref<{
32
+ pinnedLeft: IColumnDef<T>[];
33
+ virtualizedUnpinned: IColumnDef<T>[];
34
+ pinnedRight: IColumnDef<T>[];
35
+ leftSpacerWidth: number;
36
+ rightSpacerWidth: number;
37
+ } | null>;
38
+ /** Map from columnId to its global index in visibleCols. */
39
+ globalColIndexMap: Ref<Map<string, number>>;
30
40
  }
31
41
  /**
32
42
  * Shared setup composable for Vue DataGridTable components.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * useFormulaEngine — Vue composable for integrating the formula engine with the grid.
3
+ *
4
+ * Lazily creates a FormulaEngine instance when `formulas` ref is true.
5
+ * Provides accessor bridge between grid data and formula coordinates.
6
+ * Tree-shakeable: if `formulas` is false, no formula code is loaded.
7
+ */
8
+ import { type Ref } from 'vue';
9
+ import { type IGridDataAccessor, type IFormulaFunction, type IRecalcResult, type IColumnDef, type IAuditEntry, type IAuditTrail } from '@alaarab/ogrid-core';
10
+ export interface UseFormulaEngineParams<T> {
11
+ /** Enable formula support. */
12
+ formulas?: Ref<boolean>;
13
+ /** Grid data items. */
14
+ items: Ref<T[]>;
15
+ /** Flat leaf columns (for mapping column index <-> columnId). */
16
+ flatColumns: Ref<IColumnDef<T>[]>;
17
+ /** Initial formulas to load. */
18
+ initialFormulas?: Array<{
19
+ col: number;
20
+ row: number;
21
+ formula: string;
22
+ }>;
23
+ /** Called when recalculation produces cascading updates. */
24
+ onFormulaRecalc?: (result: IRecalcResult) => void;
25
+ /** Custom formula functions. */
26
+ formulaFunctions?: Record<string, IFormulaFunction>;
27
+ /** Named ranges: name → cell/range reference string. */
28
+ namedRanges?: Record<string, string>;
29
+ /** Sheet accessors for cross-sheet references. */
30
+ sheets?: Record<string, IGridDataAccessor>;
31
+ }
32
+ export interface UseFormulaEngineResult {
33
+ /** Whether formula support is active. */
34
+ enabled: Ref<boolean>;
35
+ /** Get the formula engine's computed value for a cell coordinate. */
36
+ getFormulaValue: (col: number, row: number) => unknown;
37
+ /** Check if a cell has a formula. */
38
+ hasFormula: (col: number, row: number) => boolean;
39
+ /** Get the formula string for a cell. */
40
+ getFormula: (col: number, row: number) => string | undefined;
41
+ /** Set or clear a formula for a cell. Triggers recalculation. */
42
+ setFormula: (col: number, row: number, formula: string | null) => void;
43
+ /** Notify the engine that a non-formula cell value changed. Triggers dependent recalc. */
44
+ onCellChanged: (col: number, row: number) => void;
45
+ /** Get all cells that a cell depends on (deep, transitive). */
46
+ getPrecedents: (col: number, row: number) => IAuditEntry[];
47
+ /** Get all cells that depend on a cell (deep, transitive). */
48
+ getDependents: (col: number, row: number) => IAuditEntry[];
49
+ /** Get full audit trail for a cell. */
50
+ getAuditTrail: (col: number, row: number) => IAuditTrail | null;
51
+ }
52
+ export declare function useFormulaEngine<T>(params: UseFormulaEngineParams<T>): UseFormulaEngineResult;
@@ -1,5 +1,5 @@
1
1
  import { type Ref } from 'vue';
2
- import type { IVisibleRange } from '@alaarab/ogrid-core';
2
+ import type { IVisibleRange, IVisibleColumnRange } from '@alaarab/ogrid-core';
3
3
  export interface UseVirtualScrollParams {
4
4
  totalRows: Ref<number>;
5
5
  rowHeight: number;
@@ -10,12 +10,22 @@ export interface UseVirtualScrollParams {
10
10
  * When totalRows < threshold, all rows render without virtualization.
11
11
  */
12
12
  threshold?: number;
13
+ /** Enable column virtualization. */
14
+ columnsEnabled?: Ref<boolean>;
15
+ /** Column widths array for unpinned columns. */
16
+ columnWidths?: Ref<number[]>;
17
+ /** Number of extra columns to render outside the visible area. Default: 2. */
18
+ columnOverscan?: number;
13
19
  }
14
20
  export interface UseVirtualScrollResult {
15
21
  containerRef: Ref<HTMLElement | null>;
16
22
  visibleRange: Ref<IVisibleRange>;
17
23
  totalHeight: Ref<number>;
18
24
  scrollToRow: (index: number, align?: 'start' | 'center' | 'end') => void;
25
+ /** Visible column range for horizontal virtualization, or null when column virtualization is off. */
26
+ columnRange: Ref<IVisibleColumnRange | null>;
27
+ /** Reactive scrollLeft value. */
28
+ scrollLeft: Ref<number>;
19
29
  }
20
30
  /**
21
31
  * Manages virtual scrolling with RAF-throttled scroll handling and ResizeObserver
@@ -1,7 +1,7 @@
1
1
  import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from './columnTypes';
2
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IOGridApi, IVirtualScrollConfig, } from '@alaarab/ogrid-core';
2
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IOGridApi, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, } from '@alaarab/ogrid-core';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from '@alaarab/ogrid-core';
4
- import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig } from '@alaarab/ogrid-core';
4
+ import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail } from '@alaarab/ogrid-core';
5
5
  /** Base props shared by both client-side and server-side OGrid modes. */
6
6
  interface IOGridBaseProps<T> {
7
7
  columns: (IColumnDef<T> | IColumnGroupDef<T>)[];
@@ -42,6 +42,8 @@ interface IOGridBaseProps<T> {
42
42
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
43
43
  /** Show Excel-style row numbers column at the start of the grid (1, 2, 3...). Default: false. */
44
44
  showRowNumbers?: boolean;
45
+ /** Enable Excel-style cell references: column letter headers, row numbers, and name box. Implies showRowNumbers. */
46
+ cellReferences?: boolean;
45
47
  statusBar?: boolean | IStatusBarProps;
46
48
  defaultPageSize?: number;
47
49
  defaultSortBy?: string;
@@ -81,10 +83,28 @@ interface IOGridBaseProps<T> {
81
83
  columnReorder?: boolean;
82
84
  /** Virtual scrolling configuration. Set `enabled: true` with a fixed `rowHeight` to virtualize large datasets. */
83
85
  virtualScroll?: IVirtualScrollConfig;
86
+ /** Offload sort/filter to a Web Worker for large datasets. Falls back to sync when sort column has a custom compare. */
87
+ workerSort?: boolean;
84
88
  /** Fixed row height in pixels. Overrides default row height (36px). */
85
89
  rowHeight?: number;
86
90
  /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
87
91
  density?: 'compact' | 'normal' | 'comfortable';
92
+ /** Enable Excel-like formula support. When true, cells starting with '=' are treated as formulas. Default: false. */
93
+ formulas?: boolean;
94
+ /** Initial formulas to load when the formula engine initializes. */
95
+ initialFormulas?: Array<{
96
+ col: number;
97
+ row: number;
98
+ formula: string;
99
+ }>;
100
+ /** Called when formula recalculation produces updated cell values (e.g. cascade from an edited cell). */
101
+ onFormulaRecalc?: (result: IRecalcResult) => void;
102
+ /** Custom formula functions to register with the formula engine (e.g. { MYFUNC: { minArgs: 1, maxArgs: 1, evaluate: ... } }). */
103
+ formulaFunctions?: Record<string, IFormulaFunction>;
104
+ /** Named ranges for the formula engine: name → cell/range ref string (e.g. { Revenue: 'A1:A10' }). */
105
+ namedRanges?: Record<string, string>;
106
+ /** Sheet accessors for cross-sheet formula references (e.g. { Sheet2: accessor }). */
107
+ sheets?: Record<string, IGridDataAccessor>;
88
108
  'aria-label'?: string;
89
109
  'aria-labelledby'?: string;
90
110
  }
@@ -142,6 +162,10 @@ export interface IOGridDataGridProps<T> {
142
162
  selectedRows?: Set<RowId>;
143
163
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
144
164
  showRowNumbers?: boolean;
165
+ showColumnLetters?: boolean;
166
+ showNameBox?: boolean;
167
+ /** Callback when the active cell changes. Used by the name box to display the current cell reference. */
168
+ onActiveCellChange?: (ref: string | null) => void;
145
169
  currentPage?: number;
146
170
  pageSize?: number;
147
171
  statusBar?: IStatusBarProps;
@@ -173,4 +197,22 @@ export interface IOGridDataGridProps<T> {
173
197
  'aria-labelledby'?: string;
174
198
  /** Custom keydown handler. Called before grid's built-in handling. Call event.preventDefault() to suppress grid default. */
175
199
  onKeyDown?: (event: KeyboardEvent) => void;
200
+ /** Enable formula support. When true, cell values starting with '=' are treated as formulas. */
201
+ formulas?: boolean;
202
+ /** Get the formula engine's computed value for a cell, or undefined if no formula. */
203
+ getFormulaValue?: (col: number, row: number) => unknown;
204
+ /** Check if a cell has a formula. */
205
+ hasFormula?: (col: number, row: number) => boolean;
206
+ /** Get the formula string for a cell. */
207
+ getFormula?: (col: number, row: number) => string | undefined;
208
+ /** Set a formula for a cell (called from edit commit when value starts with '='). */
209
+ setFormula?: (col: number, row: number, formula: string | null) => void;
210
+ /** Notify the formula engine that a non-formula cell changed. */
211
+ onFormulaCellChanged?: (col: number, row: number) => void;
212
+ /** Get all cells that a cell depends on (deep, transitive). */
213
+ getPrecedents?: (col: number, row: number) => IAuditEntry[];
214
+ /** Get all cells that depend on a cell (deep, transitive). */
215
+ getDependents?: (col: number, row: number) => IAuditEntry[];
216
+ /** Get full audit trail for a cell. */
217
+ getAuditTrail?: (col: number, row: number) => IAuditTrail | null;
176
218
  }
@@ -26,6 +26,7 @@ export interface CellInteractionHandlers {
26
26
  export interface CellInteractionProps {
27
27
  'data-row-index': number;
28
28
  'data-col-index': number;
29
+ 'data-active-cell'?: 'true';
29
30
  'data-in-range'?: 'true';
30
31
  tabindex: number;
31
32
  role?: 'button';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue",
3
- "version": "2.1.15",
3
+ "version": "2.3.0",
4
4
  "description": "OGrid Vue – Vue 3 composables, headless components, and utilities for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -36,7 +36,7 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
- "@alaarab/ogrid-core": "2.1.15"
39
+ "@alaarab/ogrid-core": "2.3.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vue": "^3.3.0"