@alaarab/ogrid-vue 2.3.0 → 2.4.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, 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';
1
+ import { injectGlobalStyles, Z_INDEX, getStatusBarParts, FORMULA_BAR_STYLES, handleFormulaBarKeyDown, measureRange, FORMULA_REF_COLORS, deriveFormulaBarText, extractFormulaReferences, 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, processFormulaBarCommit, applyCellDeletion, computeTabNavigation, computeArrowNavigation, computeNextSortState, mergeFilter, columnLetterToIndex, getCellValue, applyRangeRowSelection, getScrollTopForRow, FormulaEngine, calculateDropTarget, reorderColumnArray, createGridDataAccessor, 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';
@@ -173,6 +173,211 @@ var StatusBar = defineComponent({
173
173
  };
174
174
  }
175
175
  });
176
+ var FormulaBar = defineComponent({
177
+ name: "FormulaBar",
178
+ props: {
179
+ cellRef: { type: [String, null], default: null },
180
+ formulaText: { type: String, required: true },
181
+ isEditing: { type: Boolean, required: true }
182
+ },
183
+ emits: ["inputChange", "commit", "cancel", "startEditing"],
184
+ setup(props, { emit }) {
185
+ const inputRef = ref(null);
186
+ watch(() => props.isEditing, (editing) => {
187
+ if (editing && inputRef.value) {
188
+ inputRef.value.focus();
189
+ }
190
+ });
191
+ return () => h("div", { style: FORMULA_BAR_STYLES.bar, role: "toolbar", "aria-label": "Formula bar" }, [
192
+ h(
193
+ "div",
194
+ { style: FORMULA_BAR_STYLES.nameBox, "aria-label": "Active cell reference" },
195
+ props.cellRef ?? "\u2014"
196
+ ),
197
+ h("div", { style: FORMULA_BAR_STYLES.fxLabel, "aria-hidden": "true" }, "fx"),
198
+ h("input", {
199
+ ref: inputRef,
200
+ type: "text",
201
+ style: FORMULA_BAR_STYLES.input,
202
+ value: props.formulaText,
203
+ readonly: !props.isEditing,
204
+ onInput: (e) => emit("inputChange", e.target.value),
205
+ onKeydown: (e) => handleFormulaBarKeyDown(e.key, () => e.preventDefault(), () => emit("commit"), () => emit("cancel")),
206
+ onClick: () => {
207
+ if (!props.isEditing) emit("startEditing");
208
+ },
209
+ onDblclick: () => {
210
+ if (!props.isEditing) emit("startEditing");
211
+ },
212
+ "aria-label": "Formula input",
213
+ spellcheck: false,
214
+ autocomplete: "off"
215
+ })
216
+ ]);
217
+ }
218
+ });
219
+ var barStyle = {
220
+ display: "flex",
221
+ alignItems: "center",
222
+ borderTop: "1px solid var(--ogrid-border, #e0e0e0)",
223
+ background: "var(--ogrid-header-bg, #f5f5f5)",
224
+ minHeight: "30px",
225
+ overflowX: "auto",
226
+ overflowY: "hidden",
227
+ gap: "0",
228
+ fontSize: "12px"
229
+ };
230
+ var addBtnStyle = {
231
+ background: "none",
232
+ border: "none",
233
+ cursor: "pointer",
234
+ padding: "4px 10px",
235
+ fontSize: "16px",
236
+ lineHeight: "22px",
237
+ color: "var(--ogrid-fg-secondary, #666)",
238
+ flexShrink: 0
239
+ };
240
+ var tabBaseStyle = {
241
+ background: "none",
242
+ border: "none",
243
+ borderBottom: "2px solid transparent",
244
+ cursor: "pointer",
245
+ padding: "4px 16px",
246
+ fontSize: "12px",
247
+ lineHeight: "22px",
248
+ color: "var(--ogrid-fg, #242424)",
249
+ whiteSpace: "nowrap",
250
+ position: "relative"
251
+ };
252
+ var activeTabStyle = {
253
+ ...tabBaseStyle,
254
+ fontWeight: 600,
255
+ borderBottomColor: "var(--ogrid-primary, #217346)",
256
+ background: "var(--ogrid-bg, #fff)"
257
+ };
258
+ var SheetTabs = defineComponent({
259
+ name: "SheetTabs",
260
+ props: {
261
+ sheets: { type: Array, required: true },
262
+ activeSheet: { type: String, required: true },
263
+ showAddButton: { type: Boolean, default: false }
264
+ },
265
+ emits: ["sheetChange", "sheetAdd"],
266
+ setup(props, { emit }) {
267
+ return () => h("div", { style: barStyle, role: "tablist", "aria-label": "Sheet tabs" }, [
268
+ props.showAddButton ? h("button", {
269
+ type: "button",
270
+ style: addBtnStyle,
271
+ onClick: () => emit("sheetAdd"),
272
+ title: "Add sheet",
273
+ "aria-label": "Add sheet"
274
+ }, "+") : null,
275
+ ...props.sheets.map((sheet) => {
276
+ const isActive = sheet.id === props.activeSheet;
277
+ const base = isActive ? activeTabStyle : tabBaseStyle;
278
+ const style = isActive && sheet.color ? { ...base, borderBottomColor: sheet.color } : base;
279
+ return h("button", {
280
+ key: sheet.id,
281
+ type: "button",
282
+ role: "tab",
283
+ "aria-selected": isActive,
284
+ style,
285
+ onClick: () => emit("sheetChange", sheet.id)
286
+ }, sheet.name);
287
+ })
288
+ ]);
289
+ }
290
+ });
291
+ function measureRef(container, r, colOffset) {
292
+ const startCol = r.col + colOffset;
293
+ const endCol = (r.endCol ?? r.col) + colOffset;
294
+ const endRow = r.endRow ?? r.row;
295
+ const tl = container.querySelector(
296
+ `[data-row-index="${r.row}"][data-col-index="${startCol}"]`
297
+ );
298
+ const br = container.querySelector(
299
+ `[data-row-index="${endRow}"][data-col-index="${endCol}"]`
300
+ );
301
+ if (!tl || !br) return null;
302
+ const cRect = container.getBoundingClientRect();
303
+ const tlRect = tl.getBoundingClientRect();
304
+ const brRect = br.getBoundingClientRect();
305
+ return {
306
+ top: Math.round(tlRect.top - cRect.top),
307
+ left: Math.round(tlRect.left - cRect.left),
308
+ width: Math.round(brRect.right - tlRect.left),
309
+ height: Math.round(brRect.bottom - tlRect.top),
310
+ color: FORMULA_REF_COLORS[r.colorIndex % FORMULA_REF_COLORS.length]
311
+ };
312
+ }
313
+ var FormulaRefOverlay = defineComponent({
314
+ name: "FormulaRefOverlay",
315
+ props: {
316
+ containerEl: { type: Object, default: null },
317
+ references: { type: Array, required: true },
318
+ colOffset: { type: Number, required: true }
319
+ },
320
+ setup(props) {
321
+ const rects = ref([]);
322
+ let rafId = 0;
323
+ function measureAll() {
324
+ const container = props.containerEl;
325
+ const refs = props.references;
326
+ if (!container || refs.length === 0) {
327
+ rects.value = [];
328
+ return;
329
+ }
330
+ const measured = [];
331
+ for (const r of refs) {
332
+ const rect = measureRef(container, r, props.colOffset);
333
+ if (rect) measured.push(rect);
334
+ }
335
+ rects.value = measured;
336
+ }
337
+ watch(
338
+ () => [props.references, props.containerEl, props.colOffset],
339
+ () => {
340
+ cancelAnimationFrame(rafId);
341
+ if (!props.containerEl || props.references.length === 0) {
342
+ rects.value = [];
343
+ return;
344
+ }
345
+ rafId = requestAnimationFrame(measureAll);
346
+ },
347
+ { immediate: true }
348
+ );
349
+ return () => {
350
+ if (rects.value.length === 0) return null;
351
+ return rects.value.map(
352
+ (r, i) => h("svg", {
353
+ key: i,
354
+ style: {
355
+ position: "absolute",
356
+ top: `${r.top}px`,
357
+ left: `${r.left}px`,
358
+ width: `${r.width}px`,
359
+ height: `${r.height}px`,
360
+ pointerEvents: "none",
361
+ zIndex: 3,
362
+ overflow: "visible"
363
+ },
364
+ "aria-hidden": "true"
365
+ }, [
366
+ h("rect", {
367
+ x: "1",
368
+ y: "1",
369
+ width: Math.max(0, r.width - 2),
370
+ height: Math.max(0, r.height - 2),
371
+ fill: "none",
372
+ stroke: r.color,
373
+ "stroke-width": "2",
374
+ style: "shape-rendering: crispEdges"
375
+ })
376
+ ])
377
+ );
378
+ };
379
+ }
380
+ });
176
381
  function useFilterOptions(dataSource, fields) {
177
382
  const filterOptions = ref({});
178
383
  const loadingOptions = ref({});
@@ -245,17 +450,7 @@ function useFormulaEngine(params) {
245
450
  let initialLoaded = false;
246
451
  const enabled = computed(() => formulas?.value ?? false);
247
452
  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
- };
453
+ return createGridDataAccessor(itemsRef.value, flatColumnsRef.value);
259
454
  }
260
455
  watch(
261
456
  enabled,
@@ -330,6 +525,53 @@ function useFormulaEngine(params) {
330
525
  getAuditTrail
331
526
  };
332
527
  }
528
+ function useFormulaBar(params) {
529
+ const { activeCol, activeRow, activeCellRef, getFormula, getRawValue, setFormula, onCellValueChanged } = params;
530
+ const isEditing = ref(false);
531
+ const editText = ref("");
532
+ const isFormulaBarEditing = ref(false);
533
+ const displayText = computed(
534
+ () => deriveFormulaBarText(activeCol.value, activeRow.value, getFormula, getRawValue)
535
+ );
536
+ watch([activeCol, activeRow], () => {
537
+ isEditing.value = false;
538
+ isFormulaBarEditing.value = false;
539
+ });
540
+ const startEditing = () => {
541
+ editText.value = displayText.value;
542
+ isEditing.value = true;
543
+ isFormulaBarEditing.value = true;
544
+ };
545
+ const onInputChange = (text) => {
546
+ editText.value = text;
547
+ };
548
+ const onCommit = () => {
549
+ const col = activeCol.value;
550
+ const row = activeRow.value;
551
+ if (col == null || row == null || !setFormula) return;
552
+ processFormulaBarCommit(editText.value, col, row, setFormula, onCellValueChanged);
553
+ isEditing.value = false;
554
+ isFormulaBarEditing.value = false;
555
+ };
556
+ const onCancel = () => {
557
+ isEditing.value = false;
558
+ isFormulaBarEditing.value = false;
559
+ editText.value = "";
560
+ };
561
+ const formulaText = computed(() => isEditing.value ? editText.value : displayText.value);
562
+ const referencedCells = computed(() => extractFormulaReferences(formulaText.value));
563
+ return {
564
+ cellRef: activeCellRef,
565
+ formulaText,
566
+ isEditing,
567
+ onInputChange,
568
+ onCommit,
569
+ onCancel,
570
+ startEditing,
571
+ referencedCells,
572
+ isFormulaBarEditing
573
+ };
574
+ }
333
575
  var DEFAULT_PANELS = ["columns", "filters"];
334
576
  function useSideBarState(params) {
335
577
  const { config } = params;
@@ -615,12 +857,17 @@ function useOGrid(props) {
615
857
  () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.totalCount : serverTotalCount.value
616
858
  );
617
859
  const formulasRef = computed(() => !!props.value.formulas);
860
+ const formulaVersionRef = ref(0);
861
+ const wrappedOnFormulaRecalc = (result) => {
862
+ formulaVersionRef.value += 1;
863
+ props.value.onFormulaRecalc?.(result);
864
+ };
618
865
  const formulaEngine = useFormulaEngine({
619
866
  formulas: formulasRef,
620
867
  items: displayItems,
621
868
  flatColumns: columns,
622
869
  initialFormulas: props.value.initialFormulas,
623
- onFormulaRecalc: props.value.onFormulaRecalc,
870
+ onFormulaRecalc: wrappedOnFormulaRecalc,
624
871
  formulaFunctions: props.value.formulaFunctions,
625
872
  namedRanges: props.value.namedRanges,
626
873
  sheets: props.value.sheets
@@ -713,9 +960,36 @@ function useOGrid(props) {
713
960
  const clearAllFilters = () => setFilters({});
714
961
  const isLoadingResolved = computed(() => isServerSide.value && loading.value || displayLoading.value);
715
962
  const activeCellRef = ref(null);
963
+ const activeCellCoords = ref(null);
716
964
  const onActiveCellChange = (cellRef) => {
717
965
  activeCellRef.value = cellRef;
966
+ if (cellRef) {
967
+ const m = cellRef.match(/^([A-Z]+)(\d+)$/);
968
+ if (m) {
969
+ activeCellCoords.value = { col: columnLetterToIndex(m[1]), row: parseInt(m[2], 10) - 1 };
970
+ } else {
971
+ activeCellCoords.value = null;
972
+ }
973
+ } else {
974
+ activeCellCoords.value = null;
975
+ }
718
976
  };
977
+ const formulaBarActiveCol = computed(() => activeCellCoords.value?.col ?? null);
978
+ const formulaBarActiveRow = computed(() => activeCellCoords.value?.row ?? null);
979
+ const getRawValue = (col, row) => {
980
+ const items = displayItems.value;
981
+ const cols = columns.value;
982
+ if (row < 0 || row >= items.length || col < 0 || col >= cols.length) return void 0;
983
+ return getCellValue(items[row], cols[col]);
984
+ };
985
+ const formulaBarState = useFormulaBar({
986
+ activeCol: formulaBarActiveCol,
987
+ activeRow: formulaBarActiveRow,
988
+ activeCellRef,
989
+ getFormula: formulaEngine.enabled.value ? formulaEngine.getFormula : void 0,
990
+ getRawValue,
991
+ setFormula: formulaEngine.enabled.value ? formulaEngine.setFormula : void 0
992
+ });
719
993
  const dataGridProps = computed(() => {
720
994
  const p = props.value;
721
995
  const ds = dataProps.value.dataSource;
@@ -743,10 +1017,11 @@ function useOGrid(props) {
743
1017
  rowSelection: p.rowSelection ?? "none",
744
1018
  selectedRows: effectiveSelectedRows.value,
745
1019
  onSelectionChange: handleSelectionChange,
746
- showRowNumbers: p.showRowNumbers || p.cellReferences,
747
- showColumnLetters: !!p.cellReferences,
748
- showNameBox: !!p.cellReferences,
749
- onActiveCellChange: p.cellReferences ? onActiveCellChange : void 0,
1020
+ showRowNumbers: p.showRowNumbers || p.cellReferences || p.formulas,
1021
+ showColumnLetters: !!(p.cellReferences || p.formulas),
1022
+ showNameBox: !!(p.cellReferences && !p.formulas),
1023
+ // formula bar includes name box
1024
+ onActiveCellChange: p.cellReferences || p.formulas ? onActiveCellChange : void 0,
750
1025
  currentPage: page.value,
751
1026
  pageSize: pageSize.value,
752
1027
  statusBar: statusBarConfig.value,
@@ -773,6 +1048,7 @@ function useOGrid(props) {
773
1048
  render: p.emptyState?.render
774
1049
  },
775
1050
  formulas: p.formulas,
1051
+ formulaVersion: formulaVersionRef.value,
776
1052
  ...formulaEngine.enabled.value ? {
777
1053
  getFormulaValue: formulaEngine.getFormulaValue,
778
1054
  hasFormula: formulaEngine.hasFormula,
@@ -782,7 +1058,8 @@ function useOGrid(props) {
782
1058
  getPrecedents: formulaEngine.getPrecedents,
783
1059
  getDependents: formulaEngine.getDependents,
784
1060
  getAuditTrail: formulaEngine.getAuditTrail
785
- } : {}
1061
+ } : {},
1062
+ formulaReferences: formulaBarState.referencedCells.value.length > 0 ? formulaBarState.referencedCells.value : void 0
786
1063
  };
787
1064
  });
788
1065
  const pagination = computed(() => ({
@@ -801,8 +1078,10 @@ function useOGrid(props) {
801
1078
  placement: columnChooserPlacement.value
802
1079
  }));
803
1080
  const layout = computed(() => {
804
- const showNameBox = !!props.value.cellReferences;
805
- let resolvedToolbar = props.value.toolbar;
1081
+ const p = props.value;
1082
+ const formulas = !!p.formulas;
1083
+ const showNameBox = !!p.cellReferences && !formulas;
1084
+ let resolvedToolbar = p.toolbar;
806
1085
  if (showNameBox) {
807
1086
  const nameBoxEl = h("div", {
808
1087
  style: {
@@ -823,13 +1102,32 @@ function useOGrid(props) {
823
1102
  }, activeCellRef.value ?? "\u2014");
824
1103
  resolvedToolbar = [nameBoxEl, resolvedToolbar];
825
1104
  }
1105
+ const formulaBarEl = formulas ? h(FormulaBar, {
1106
+ cellRef: formulaBarState.cellRef.value,
1107
+ formulaText: formulaBarState.formulaText.value,
1108
+ isEditing: formulaBarState.isEditing.value,
1109
+ onInputChange: formulaBarState.onInputChange,
1110
+ onCommit: formulaBarState.onCommit,
1111
+ onCancel: formulaBarState.onCancel,
1112
+ onStartEditing: formulaBarState.startEditing
1113
+ }) : void 0;
1114
+ const sheetTabsEl = p.sheetDefs && p.sheetDefs.length > 0 && p.activeSheet && p.onSheetChange ? h(SheetTabs, {
1115
+ sheets: p.sheetDefs,
1116
+ activeSheet: p.activeSheet,
1117
+ showAddButton: !!p.onSheetAdd,
1118
+ onSheetChange: p.onSheetChange,
1119
+ onSheetAdd: p.onSheetAdd ?? (() => {
1120
+ })
1121
+ }) : void 0;
826
1122
  return {
827
1123
  toolbar: resolvedToolbar,
828
- toolbarBelow: props.value.toolbarBelow,
829
- className: props.value.className,
830
- emptyState: props.value.emptyState,
1124
+ toolbarBelow: p.toolbarBelow,
1125
+ className: p.className,
1126
+ emptyState: p.emptyState,
831
1127
  sideBarProps: sideBarProps.value,
832
- fullScreen: props.value.fullScreen
1128
+ fullScreen: p.fullScreen,
1129
+ formulaBar: formulaBarEl,
1130
+ sheetTabs: sheetTabsEl
833
1131
  };
834
1132
  });
835
1133
  const filtersResult = computed(() => ({
@@ -2449,7 +2747,10 @@ function useDataGridState(params) {
2449
2747
  getRowId,
2450
2748
  editable: editableProp.value,
2451
2749
  onCellValueChanged: onCellValueChanged.value,
2452
- isDragging: cellSelection.value ? isDragging.value : false
2750
+ isDragging: cellSelection.value ? isDragging.value : false,
2751
+ getFormulaValue: props.value.getFormulaValue,
2752
+ hasFormula: props.value.hasFormula,
2753
+ formulaVersion: props.value.formulaVersion
2453
2754
  }));
2454
2755
  const popoverAnchorEl = ref(null);
2455
2756
  const setPopoverAnchorEl = (el) => {
@@ -3138,9 +3439,14 @@ function useColumnChooserState(params) {
3138
3439
  }
3139
3440
  function useInlineCellEditorState(params) {
3140
3441
  const { value, editorType, onCommit, onCancel } = params;
3141
- const localValue = ref(
3142
- value !== null && value !== void 0 ? String(value) : ""
3143
- );
3442
+ const localValue = ref((() => {
3443
+ if (value === null || value === void 0) return "";
3444
+ if (editorType === "date") {
3445
+ const str = String(value);
3446
+ return str.match(/^\d{4}-\d{2}-\d{2}/) ? str.substring(0, 10) : str;
3447
+ }
3448
+ return String(value);
3449
+ })());
3144
3450
  const setLocalValue = (v) => {
3145
3451
  localValue.value = v;
3146
3452
  };
@@ -3569,7 +3875,7 @@ function createDataGridTable(ui) {
3569
3875
  const cb = propsRef.value.onActiveCellChange;
3570
3876
  if (!cb) return;
3571
3877
  if (ac) {
3572
- cb(formatCellReference(ac.columnIndex, offset + ac.rowIndex + 1));
3878
+ cb(formatCellReference(ac.columnIndex - state.layout.value.colOffset, offset + ac.rowIndex + 1));
3573
3879
  } else {
3574
3880
  cb(null);
3575
3881
  }
@@ -4112,6 +4418,14 @@ function createDataGridTable(ui) {
4112
4418
  columnSizingOverrides: layout.columnSizingOverrides,
4113
4419
  columnOrder: p.columnOrder
4114
4420
  }),
4421
+ // Formula reference overlay
4422
+ ...p.formulaReferences && p.formulaReferences.length > 0 ? [
4423
+ h(FormulaRefOverlay, {
4424
+ containerEl: tableContainerRef.value,
4425
+ references: p.formulaReferences,
4426
+ colOffset: _colOffset
4427
+ })
4428
+ ] : [],
4115
4429
  // Column header menu
4116
4430
  h(ui.ColumnHeaderMenu, {
4117
4431
  isOpen: headerMenu.isOpen,
@@ -4713,8 +5027,12 @@ function createOGrid(ui) {
4713
5027
  style: { padding: "8px 12px", borderBottom: "1px solid var(--ogrid-border, rgba(0,0,0,0.12))" }
4714
5028
  }, [layout.value.toolbarBelow])
4715
5029
  ] : [],
5030
+ // Formula bar (between toolbar and grid)
5031
+ ...layout.value.formulaBar ? [layout.value.formulaBar] : [],
4716
5032
  // Main content area (sidebar + grid)
4717
5033
  h("div", { style: { display: "flex", flex: "1", minHeight: "0" } }, mainAreaChildren),
5034
+ // Sheet tabs (between grid and footer)
5035
+ ...layout.value.sheetTabs ? [layout.value.sheetTabs] : [],
4718
5036
  // Footer strip (pagination)
4719
5037
  h("div", {
4720
5038
  style: {
@@ -4731,4 +5049,4 @@ function createOGrid(ui) {
4731
5049
  });
4732
5050
  }
4733
5051
 
4734
- export { MarchingAntsOverlay, StatusBar, createDataGridTable, createInlineCellEditor, createOGrid, getCellInteractionProps, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnHeaderMenuState, useColumnPinning, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableSetup, useDateFilterState, useDebounce, useDebouncedCallback, useFillHandle, useFilterOptions, useInlineCellEditorState, useKeyboardNavigation, useMultiSelectFilterState, useOGrid, usePeopleFilterState, useRichSelectState, useRowSelection, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
5052
+ export { FormulaBar, FormulaRefOverlay, MarchingAntsOverlay, SheetTabs, StatusBar, createDataGridTable, createInlineCellEditor, createOGrid, getCellInteractionProps, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnHeaderMenuState, useColumnPinning, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableSetup, useDateFilterState, useDebounce, useDebouncedCallback, useFillHandle, useFilterOptions, useFormulaBar, useInlineCellEditorState, useKeyboardNavigation, useMultiSelectFilterState, useOGrid, usePeopleFilterState, useRichSelectState, useRowSelection, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * FormulaBar — Headless Excel-style formula bar component.
3
+ *
4
+ * Layout: [Name Box] [fx] [Formula Input]
5
+ *
6
+ * Uses --ogrid-* CSS variables for theming.
7
+ */
8
+ import { type PropType } from 'vue';
9
+ export interface FormulaBarProps {
10
+ /** Active cell reference (e.g. "A1"). */
11
+ cellRef: string | null;
12
+ /** Text displayed/edited in the formula input. */
13
+ formulaText: string;
14
+ /** Whether the input is in editing mode. */
15
+ isEditing: boolean;
16
+ }
17
+ export declare const FormulaBar: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
18
+ cellRef: {
19
+ type: PropType<string | null>;
20
+ default: null;
21
+ };
22
+ formulaText: {
23
+ type: StringConstructor;
24
+ required: true;
25
+ };
26
+ isEditing: {
27
+ type: BooleanConstructor;
28
+ required: true;
29
+ };
30
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
31
+ [key: string]: any;
32
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("cancel" | "inputChange" | "commit" | "startEditing")[], "cancel" | "inputChange" | "commit" | "startEditing", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
33
+ cellRef: {
34
+ type: PropType<string | null>;
35
+ default: null;
36
+ };
37
+ formulaText: {
38
+ type: StringConstructor;
39
+ required: true;
40
+ };
41
+ isEditing: {
42
+ type: BooleanConstructor;
43
+ required: true;
44
+ };
45
+ }>> & Readonly<{
46
+ onCancel?: ((...args: any[]) => any) | undefined;
47
+ onInputChange?: ((...args: any[]) => any) | undefined;
48
+ onCommit?: ((...args: any[]) => any) | undefined;
49
+ onStartEditing?: ((...args: any[]) => any) | undefined;
50
+ }>, {
51
+ cellRef: string | null;
52
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * FormulaRefOverlay — Renders colored border overlays on cells referenced by
3
+ * the active formula, like Excel's reference highlighting.
4
+ *
5
+ * Port of React's FormulaRefOverlay component.
6
+ */
7
+ import { type PropType } from 'vue';
8
+ import { type FormulaReference } from '@alaarab/ogrid-core';
9
+ export declare const FormulaRefOverlay: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
10
+ containerEl: {
11
+ type: PropType<HTMLElement | null>;
12
+ default: null;
13
+ };
14
+ references: {
15
+ type: PropType<FormulaReference[]>;
16
+ required: true;
17
+ };
18
+ colOffset: {
19
+ type: NumberConstructor;
20
+ required: true;
21
+ };
22
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
23
+ [key: string]: any;
24
+ }>[] | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
25
+ containerEl: {
26
+ type: PropType<HTMLElement | null>;
27
+ default: null;
28
+ };
29
+ references: {
30
+ type: PropType<FormulaReference[]>;
31
+ required: true;
32
+ };
33
+ colOffset: {
34
+ type: NumberConstructor;
35
+ required: true;
36
+ };
37
+ }>> & Readonly<{}>, {
38
+ containerEl: HTMLElement | null;
39
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SheetTabs — Excel-style sheet tab bar at the bottom of the grid.
3
+ *
4
+ * Layout: [+] [Sheet1] [Sheet2] [Sheet3]
5
+ */
6
+ import { type PropType } from 'vue';
7
+ import type { ISheetDef } from '@alaarab/ogrid-core';
8
+ export interface SheetTabsProps {
9
+ sheets: ISheetDef[];
10
+ activeSheet: string;
11
+ }
12
+ export declare const SheetTabs: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
13
+ sheets: {
14
+ type: PropType<ISheetDef[]>;
15
+ required: true;
16
+ };
17
+ activeSheet: {
18
+ type: StringConstructor;
19
+ required: true;
20
+ };
21
+ showAddButton: {
22
+ type: BooleanConstructor;
23
+ default: boolean;
24
+ };
25
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
26
+ [key: string]: any;
27
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("sheetChange" | "sheetAdd")[], "sheetChange" | "sheetAdd", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
28
+ sheets: {
29
+ type: PropType<ISheetDef[]>;
30
+ required: true;
31
+ };
32
+ activeSheet: {
33
+ type: StringConstructor;
34
+ required: true;
35
+ };
36
+ showAddButton: {
37
+ type: BooleanConstructor;
38
+ default: boolean;
39
+ };
40
+ }>> & Readonly<{
41
+ onSheetChange?: ((...args: any[]) => any) | undefined;
42
+ onSheetAdd?: ((...args: any[]) => any) | undefined;
43
+ }>, {
44
+ showAddButton: boolean;
45
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -32,6 +32,8 @@ export { useTableLayout } from './useTableLayout';
32
32
  export type { UseTableLayoutParams, UseTableLayoutResult } from './useTableLayout';
33
33
  export { useFormulaEngine } from './useFormulaEngine';
34
34
  export type { UseFormulaEngineParams, UseFormulaEngineResult } from './useFormulaEngine';
35
+ export { useFormulaBar } from './useFormulaBar';
36
+ export type { UseFormulaBarParams, UseFormulaBarResult } from './useFormulaBar';
35
37
  export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
36
38
  export type { UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, } from './useColumnHeaderFilterState';
37
39
  export { useTextFilterState } from './useTextFilterState';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * useFormulaBar — Vue composable for formula bar state.
3
+ *
4
+ * Manages the formula bar text, editing mode, and reference extraction.
5
+ */
6
+ import { type Ref } from 'vue';
7
+ import { type FormulaReference } from '@alaarab/ogrid-core';
8
+ export interface UseFormulaBarParams {
9
+ /** Active cell column index (0-based). */
10
+ activeCol: Ref<number | null>;
11
+ /** Active cell row index (0-based). */
12
+ activeRow: Ref<number | null>;
13
+ /** Active cell reference string (e.g. "A1"). */
14
+ activeCellRef: Ref<string | null>;
15
+ /** Get formula string for a cell. */
16
+ getFormula?: (col: number, row: number) => string | undefined;
17
+ /** Get raw display value for a cell. */
18
+ getRawValue?: (col: number, row: number) => unknown;
19
+ /** Set formula for a cell. */
20
+ setFormula?: (col: number, row: number, formula: string | null) => void;
21
+ /** Commit a non-formula value change. */
22
+ onCellValueChanged?: (col: number, row: number, value: unknown) => void;
23
+ }
24
+ export interface UseFormulaBarResult {
25
+ /** Cell reference string (e.g. "A1"). */
26
+ cellRef: Ref<string | null>;
27
+ /** Text shown in the formula bar input. */
28
+ formulaText: Ref<string>;
29
+ /** Whether the formula bar input is being edited. */
30
+ isEditing: Ref<boolean>;
31
+ /** Update the formula bar input text. */
32
+ onInputChange: (text: string) => void;
33
+ /** Commit the current edit. */
34
+ onCommit: () => void;
35
+ /** Cancel the current edit. */
36
+ onCancel: () => void;
37
+ /** Start editing the formula bar. */
38
+ startEditing: () => void;
39
+ /** References extracted from the current formula text (for highlighting). */
40
+ referencedCells: Ref<FormulaReference[]>;
41
+ /** Whether the formula bar is actively being edited (for click-to-insert-ref guards). */
42
+ isFormulaBarEditing: Ref<boolean>;
43
+ }
44
+ export declare function useFormulaBar(params: UseFormulaBarParams): UseFormulaBarResult;
@@ -31,6 +31,10 @@ export interface UseOGridLayout {
31
31
  };
32
32
  sideBarProps: SideBarProps | null;
33
33
  fullScreen?: boolean;
34
+ /** Formula bar element (rendered between toolbar and grid). */
35
+ formulaBar?: unknown;
36
+ /** Sheet tabs element (rendered between grid and footer). */
37
+ sheetTabs?: unknown;
34
38
  }
35
39
  /** Filter state. */
36
40
  export interface UseOGridFilters {
@@ -4,8 +4,11 @@ export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnGroupDef,
4
4
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
5
5
  export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
6
6
  export { StatusBar, type StatusBarProps } from './components/StatusBar';
7
- export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useColumnReorder, useVirtualScroll, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, useColumnPinning, useColumnHeaderMenuState, useDataGridTableSetup, } from './composables';
8
- export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, UseActiveCellResult, EditingCell, UseCellEditingParams, UseCellEditingResult, UseCellSelectionParams, UseCellSelectionResult, UseClipboardParams, UseClipboardResult, UseRowSelectionParams, UseRowSelectionResult, UseKeyboardNavigationParams, UseKeyboardNavigationResult, UseFillHandleParams, UseFillHandleResult, UseUndoRedoParams, UseUndoRedoResult, ContextMenuPosition, UseContextMenuResult, UseColumnResizeParams, UseColumnResizeResult, UseColumnReorderParams, UseColumnReorderResult, UseVirtualScrollParams, UseVirtualScrollResult, UseFilterOptionsResult, UseTableLayoutParams, UseTableLayoutResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, InlineCellEditorType, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, DebouncedFn, UseColumnPinningParams, UseColumnPinningResult, UseColumnHeaderMenuStateParams, UseColumnHeaderMenuStateResult, UseDataGridTableSetupParams, UseDataGridTableSetupResult, MaybeShallowRef, } from './composables';
7
+ export { FormulaBar, type FormulaBarProps } from './components/FormulaBar';
8
+ export { SheetTabs, type SheetTabsProps } from './components/SheetTabs';
9
+ export { FormulaRefOverlay } from './components/FormulaRefOverlay';
10
+ export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useColumnReorder, useVirtualScroll, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, useColumnPinning, useColumnHeaderMenuState, useDataGridTableSetup, useFormulaBar, } from './composables';
11
+ export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, UseActiveCellResult, EditingCell, UseCellEditingParams, UseCellEditingResult, UseCellSelectionParams, UseCellSelectionResult, UseClipboardParams, UseClipboardResult, UseRowSelectionParams, UseRowSelectionResult, UseKeyboardNavigationParams, UseKeyboardNavigationResult, UseFillHandleParams, UseFillHandleResult, UseUndoRedoParams, UseUndoRedoResult, ContextMenuPosition, UseContextMenuResult, UseColumnResizeParams, UseColumnResizeResult, UseColumnReorderParams, UseColumnReorderResult, UseVirtualScrollParams, UseVirtualScrollResult, UseFilterOptionsResult, UseTableLayoutParams, UseTableLayoutResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, InlineCellEditorType, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, DebouncedFn, UseColumnPinningParams, UseColumnPinningResult, UseColumnHeaderMenuStateParams, UseColumnHeaderMenuStateResult, UseDataGridTableSetupParams, UseDataGridTableSetupResult, MaybeShallowRef, UseFormulaBarParams, UseFormulaBarResult, } from './composables';
9
12
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
10
13
  export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, CellInteractionProps, } from './utils';
11
14
  export { createDataGridTable, type IDataGridTableUIBindings } from './components/createDataGridTable';
@@ -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, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, } 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, ISheetDef, FormulaReference, } 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, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail } from '@alaarab/ogrid-core';
4
+ import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, ISheetDef, FormulaReference } 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>)[];
@@ -105,6 +105,14 @@ interface IOGridBaseProps<T> {
105
105
  namedRanges?: Record<string, string>;
106
106
  /** Sheet accessors for cross-sheet formula references (e.g. { Sheet2: accessor }). */
107
107
  sheets?: Record<string, IGridDataAccessor>;
108
+ /** Sheet definitions for the tab bar at grid bottom. */
109
+ sheetDefs?: ISheetDef[];
110
+ /** Active sheet ID (controlled). */
111
+ activeSheet?: string;
112
+ /** Called when user clicks a sheet tab. */
113
+ onSheetChange?: (sheetId: string) => void;
114
+ /** Called when user clicks the "+" add sheet button. */
115
+ onSheetAdd?: () => void;
108
116
  'aria-label'?: string;
109
117
  'aria-labelledby'?: string;
110
118
  }
@@ -215,4 +223,8 @@ export interface IOGridDataGridProps<T> {
215
223
  getDependents?: (col: number, row: number) => IAuditEntry[];
216
224
  /** Get full audit trail for a cell. */
217
225
  getAuditTrail?: (col: number, row: number) => IAuditTrail | null;
226
+ /** Monotonic counter incremented on each formula recalculation — used for cache invalidation. */
227
+ formulaVersion?: number;
228
+ /** Cell references to highlight (from active formula in formula bar). */
229
+ formulaReferences?: FormulaReference[];
218
230
  }
@@ -1,3 +1,3 @@
1
1
  export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, } from './columnTypes';
2
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, } from './dataGridTypes';
2
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, ISheetDef, FormulaReference, } from './dataGridTypes';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue",
3
- "version": "2.3.0",
3
+ "version": "2.4.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.3.0"
39
+ "@alaarab/ogrid-core": "2.4.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vue": "^3.3.0"