@alaarab/ogrid-vue 2.2.0 → 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, processClientSideDataAsync, validateColumns, validateRowIds, computeRowSelectionState, buildCellIndex, UndoRedoStack, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, computeAggregations, getDataGridStatusBarConfig, validateVirtualScrollConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, partitionColumnsForVirtualization, 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;
@@ -494,6 +614,17 @@ function useOGrid(props) {
494
614
  const displayTotalCount = computed(
495
615
  () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.totalCount : serverTotalCount.value
496
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
+ });
497
628
  let firstDataRendered = false;
498
629
  let rowIdsValidated = false;
499
630
  watch(displayItems, (items) => {
@@ -581,6 +712,10 @@ function useOGrid(props) {
581
712
  });
582
713
  const clearAllFilters = () => setFilters({});
583
714
  const isLoadingResolved = computed(() => isServerSide.value && loading.value || displayLoading.value);
715
+ const activeCellRef = ref(null);
716
+ const onActiveCellChange = (cellRef) => {
717
+ activeCellRef.value = cellRef;
718
+ };
584
719
  const dataGridProps = computed(() => {
585
720
  const p = props.value;
586
721
  const ds = dataProps.value.dataSource;
@@ -608,7 +743,10 @@ function useOGrid(props) {
608
743
  rowSelection: p.rowSelection ?? "none",
609
744
  selectedRows: effectiveSelectedRows.value,
610
745
  onSelectionChange: handleSelectionChange,
611
- showRowNumbers: p.showRowNumbers,
746
+ showRowNumbers: p.showRowNumbers || p.cellReferences,
747
+ showColumnLetters: !!p.cellReferences,
748
+ showNameBox: !!p.cellReferences,
749
+ onActiveCellChange: p.cellReferences ? onActiveCellChange : void 0,
612
750
  currentPage: page.value,
613
751
  pageSize: pageSize.value,
614
752
  statusBar: statusBarConfig.value,
@@ -633,7 +771,18 @@ function useOGrid(props) {
633
771
  onClearAll: clearAllFilters,
634
772
  message: p.emptyState?.message,
635
773
  render: p.emptyState?.render
636
- }
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
+ } : {}
637
786
  };
638
787
  });
639
788
  const pagination = computed(() => ({
@@ -651,14 +800,38 @@ function useOGrid(props) {
651
800
  onVisibilityChange: handleVisibilityChange,
652
801
  placement: columnChooserPlacement.value
653
802
  }));
654
- const layout = computed(() => ({
655
- toolbar: props.value.toolbar,
656
- toolbarBelow: props.value.toolbarBelow,
657
- className: props.value.className,
658
- emptyState: props.value.emptyState,
659
- sideBarProps: sideBarProps.value,
660
- fullScreen: props.value.fullScreen
661
- }));
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
+ });
662
835
  const filtersResult = computed(() => ({
663
836
  hasActiveFilters: hasActiveFilters.value,
664
837
  setFilters
@@ -880,23 +1053,6 @@ function useActiveCell(wrapperRef, editingCell) {
880
1053
  });
881
1054
  return { activeCell, setActiveCell };
882
1055
  }
883
- function useLatestRef(source) {
884
- let value = unref(source);
885
- return customRef((track, trigger) => ({
886
- get() {
887
- if (isRef(source)) {
888
- value = source.value;
889
- }
890
- return value;
891
- },
892
- set(newValue) {
893
- value = newValue;
894
- trigger();
895
- }
896
- }));
897
- }
898
-
899
- // src/composables/useCellSelection.ts
900
1056
  var DRAG_ATTR = "data-drag-range";
901
1057
  var DRAG_ANCHOR_ATTR = "data-drag-anchor";
902
1058
  var AUTO_SCROLL_EDGE = 40;
@@ -1313,7 +1469,7 @@ function useKeyboardNavigation(params) {
1313
1469
  const maxColIndex = visibleColumnCount - 1 + colOffset;
1314
1470
  if (items.length === 0) return;
1315
1471
  if (activeCell === null) {
1316
- 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)) {
1317
1473
  setActiveCell({ rowIndex: 0, columnIndex: colOffset });
1318
1474
  e.preventDefault();
1319
1475
  }
@@ -1413,6 +1569,36 @@ function useKeyboardNavigation(params) {
1413
1569
  setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
1414
1570
  break;
1415
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
+ }
1416
1602
  case "Enter":
1417
1603
  case "F2": {
1418
1604
  e.preventDefault();
@@ -2970,14 +3156,14 @@ function useInlineCellEditorState(params) {
2970
3156
  e.stopPropagation();
2971
3157
  cancel();
2972
3158
  }
2973
- if (e.key === "Enter" && editorType === "text") {
3159
+ if (e.key === "Enter" && (editorType === "text" || editorType === "date")) {
2974
3160
  e.preventDefault();
2975
3161
  e.stopPropagation();
2976
3162
  commit(localValue.value);
2977
3163
  }
2978
3164
  };
2979
3165
  const handleBlur = () => {
2980
- if (editorType === "text") {
3166
+ if (editorType === "text" || editorType === "date") {
2981
3167
  commit(localValue.value);
2982
3168
  }
2983
3169
  };
@@ -3336,6 +3522,7 @@ function getCellInteractionProps(descriptor, columnId, handlers) {
3336
3522
  const base = {
3337
3523
  "data-row-index": descriptor.rowIndex,
3338
3524
  "data-col-index": descriptor.globalColIndex,
3525
+ ...descriptor.isActive ? { "data-active-cell": "true" } : {},
3339
3526
  ...descriptor.isInRange ? { "data-in-range": "true" } : {},
3340
3527
  tabindex: descriptor.isActive ? 0 : -1,
3341
3528
  onMousedown: (e) => handlers.handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex),
@@ -3371,6 +3558,24 @@ function createDataGridTable(ui) {
3371
3558
  columnPartition,
3372
3559
  globalColIndexMap
3373
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
+ );
3374
3579
  const onWrapperMousedown = (e) => {
3375
3580
  lastMouseShift.value = e.shiftKey;
3376
3581
  };
@@ -3446,7 +3651,7 @@ function createDataGridTable(ui) {
3446
3651
  } = layout;
3447
3652
  const currentPage = p.currentPage ?? 1;
3448
3653
  const pageSize = p.pageSize ?? 25;
3449
- const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
3654
+ const rowNumberOffset2 = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
3450
3655
  const { selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
3451
3656
  const { editingCell: _editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
3452
3657
  const {
@@ -3536,13 +3741,14 @@ function createDataGridTable(ui) {
3536
3741
  ]);
3537
3742
  }
3538
3743
  const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
3539
- const cellStyle = resolveCellStyle(col, item);
3744
+ const cellStyle = resolveCellStyle(col, item, descriptor.displayValue);
3540
3745
  const interactionProps2 = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
3541
3746
  const cellClasses = ["ogrid-cell-content"];
3542
3747
  if (col.type === "numeric") cellClasses.push("ogrid-cell-content--numeric");
3543
3748
  else if (col.type === "boolean") cellClasses.push("ogrid-cell-content--boolean");
3544
3749
  if (descriptor.canEditAny) cellClasses.push("ogrid-cell-content--editable");
3545
3750
  if (descriptor.isActive) cellClasses.push("ogrid-cell-content--active");
3751
+ if (descriptor.isActive && descriptor.isInRange) cellClasses.push("ogrid-cell-content--active-in-range");
3546
3752
  if (descriptor.isInRange && !descriptor.isActive) cellClasses.push("ogrid-cell-in-range");
3547
3753
  if (descriptor.isInCutRange) cellClasses.push("ogrid-cell-cut");
3548
3754
  const styledContent = cellStyle ? h("span", { style: cellStyle }, content) : content;
@@ -3644,10 +3850,23 @@ function createDataGridTable(ui) {
3644
3850
  ...virtualScrollEnabled.value ? { "data-virtual-scroll": "" } : {}
3645
3851
  }, [
3646
3852
  // Header
3647
- h(
3648
- "thead",
3649
- { class: stickyHeader ? "ogrid-thead ogrid-sticky-header" : "ogrid-thead" },
3650
- 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(
3651
3870
  (row, rowIdx) => h("tr", { key: rowIdx, class: "ogrid-header-row" }, [
3652
3871
  // Checkbox header cell
3653
3872
  ...rowIdx === headerRows.length - 1 && hasCheckboxCol ? [
@@ -3757,7 +3976,7 @@ function createDataGridTable(ui) {
3757
3976
  })
3758
3977
  ])
3759
3978
  )
3760
- ),
3979
+ ]),
3761
3980
  // Body
3762
3981
  ...!showEmptyInGrid ? [
3763
3982
  h("tbody", {}, (() => {
@@ -3822,7 +4041,7 @@ function createDataGridTable(ui) {
3822
4041
  left: hasCheckboxCol ? `${CHECKBOX_COLUMN_WIDTH}px` : "0",
3823
4042
  zIndex: 2
3824
4043
  }
3825
- }, String(rowNumberOffset + rowIndex + 1))
4044
+ }, String(rowNumberOffset2 + rowIndex + 1))
3826
4045
  ] : [],
3827
4046
  // Left spacer for column virtualization
3828
4047
  ...leftSpacerWidth > 0 ? [
@@ -3978,6 +4197,7 @@ function createInlineCellEditor(options) {
3978
4197
  dropdown.style.maxHeight = `${maxH}px`;
3979
4198
  dropdown.style.zIndex = "9999";
3980
4199
  dropdown.style.right = "auto";
4200
+ dropdown.style.textAlign = "left";
3981
4201
  if (flipUp) {
3982
4202
  dropdown.style.top = "auto";
3983
4203
  dropdown.style.bottom = `${window.innerHeight - rect.top}px`;
@@ -4083,7 +4303,7 @@ function createInlineCellEditor(options) {
4083
4303
  selectDropdownRef.value = el;
4084
4304
  },
4085
4305
  role: "listbox",
4086
- 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" }
4087
4307
  }, values.map(
4088
4308
  (v, i) => h("div", {
4089
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,
@@ -254,6 +272,14 @@
254
272
  overflow: visible;
255
273
  }
256
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
+
257
283
  /* === Editing cell wrapper === */
258
284
 
259
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';
@@ -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,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;
@@ -87,6 +89,22 @@ interface IOGridBaseProps<T> {
87
89
  rowHeight?: number;
88
90
  /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
89
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>;
90
108
  'aria-label'?: string;
91
109
  'aria-labelledby'?: string;
92
110
  }
@@ -144,6 +162,10 @@ export interface IOGridDataGridProps<T> {
144
162
  selectedRows?: Set<RowId>;
145
163
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
146
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;
147
169
  currentPage?: number;
148
170
  pageSize?: number;
149
171
  statusBar?: IStatusBarProps;
@@ -175,4 +197,22 @@ export interface IOGridDataGridProps<T> {
175
197
  'aria-labelledby'?: string;
176
198
  /** Custom keydown handler. Called before grid's built-in handling. Call event.preventDefault() to suppress grid default. */
177
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;
178
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.2.0",
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.2.0"
39
+ "@alaarab/ogrid-core": "2.3.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vue": "^3.3.0"