zubin-grid 0.41.13 → 0.42.13

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/core/grid.js CHANGED
@@ -1,4 +1,3 @@
1
- import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
2
1
  import { cell } from "./cell.js";
3
2
  import { assertGridId, createGridIdKey, formatGridId } from "./grid-id.js";
4
3
  import { createGridPersistController, defaultGridPersistAdapter } from "./persist.js";
@@ -28,10 +27,36 @@ export function createDimensionGrid(parentGrid, initialCells, dimension, options
28
27
  createGridIdKey(boundId),
29
28
  cell(initialCells[index]),
30
29
  ]));
30
+ const dirtyBoundIdKeys = new Set();
31
+ const persistedCellValuesById = new Map(boundIds.map((boundId, index) => [createGridIdKey(boundId), initialCells[index]]));
31
32
  let dimensionGridApi;
33
+ const resetPersistedDimensionCells = () => {
34
+ persistedCellValuesById.clear();
35
+ boundIds.forEach((boundId) => {
36
+ const idKey = createGridIdKey(boundId);
37
+ persistedCellValuesById.set(idKey, cellsById.get(idKey)?.get());
38
+ });
39
+ dirtyBoundIdKeys.clear();
40
+ };
41
+ const syncDimensionCellDirty = (boundId) => {
42
+ const idKey = createGridIdKey(boundId);
43
+ const currentValue = cellsById.get(idKey)?.get();
44
+ if (!persistedCellValuesById.has(idKey)) {
45
+ if (currentValue === undefined)
46
+ dirtyBoundIdKeys.delete(idKey);
47
+ else
48
+ dirtyBoundIdKeys.add(idKey);
49
+ return;
50
+ }
51
+ if (haveSameRuntimeValue(currentValue, persistedCellValuesById.get(idKey))) {
52
+ dirtyBoundIdKeys.delete(idKey);
53
+ return;
54
+ }
55
+ dirtyBoundIdKeys.add(idKey);
56
+ };
32
57
  const getCellAtIndex = (index) => {
33
58
  if (!Number.isInteger(index) || index < 0 || index >= boundIds.length) {
34
- throw new Error(`Dimension grid ${dimension} index ${index} is out of bounds.`);
59
+ throw new Error(`Dimension grid ${dimension} index ${index} is out of range. ${describeIndexRange(boundIds.length, index)}`);
35
60
  }
36
61
  const boundId = boundIds[index];
37
62
  const currentCell = cellsById.get(createGridIdKey(boundId));
@@ -40,6 +65,17 @@ export function createDimensionGrid(parentGrid, initialCells, dimension, options
40
65
  }
41
66
  return currentCell;
42
67
  };
68
+ const readCellAtIndex = (index) => {
69
+ const currentCell = getCellAtIndex(index);
70
+ const boundId = boundIds[index];
71
+ return {
72
+ value: currentCell.get(),
73
+ meta: {
74
+ existsInDb: currentCell.get() !== undefined,
75
+ isDirty: dirtyBoundIdKeys.has(createGridIdKey(boundId)),
76
+ },
77
+ };
78
+ };
43
79
  const getState = () => ({
44
80
  dimension,
45
81
  cells: boundIds.map((boundId) => {
@@ -78,6 +114,12 @@ export function createDimensionGrid(parentGrid, initialCells, dimension, options
78
114
  finally {
79
115
  isApplyingState = false;
80
116
  }
117
+ changedIndices.forEach((index) => {
118
+ const boundId = boundIds[index];
119
+ if (boundId !== undefined) {
120
+ syncDimensionCellDirty(boundId);
121
+ }
122
+ });
81
123
  return changedIndices;
82
124
  };
83
125
  const { hydrate, markStateChanged } = createGridPersistController(resolvedPersistOption, {
@@ -86,6 +128,8 @@ export function createDimensionGrid(parentGrid, initialCells, dimension, options
86
128
  applyDimensionCells(normalizeDimensionGridStateInput(nextState, dimension).cells, "replace");
87
129
  },
88
130
  isApplyingState: () => isApplyingState,
131
+ onHydratedStateApplied: resetPersistedDimensionCells,
132
+ onPersistedStateApplied: resetPersistedDimensionCells,
89
133
  });
90
134
  const emitDimensionGridUpdate = (diff, persistStateChanged = false) => {
91
135
  if (persistStateChanged) {
@@ -110,6 +154,20 @@ export function createDimensionGrid(parentGrid, initialCells, dimension, options
110
154
  nextCellsById.forEach((currentCell, boundIdKey) => {
111
155
  cellsById.set(boundIdKey, currentCell);
112
156
  });
157
+ const nextIdKeys = new Set(nextBoundIds.map((boundId) => createGridIdKey(boundId)));
158
+ [...persistedCellValuesById.keys()].forEach((idKey) => {
159
+ if (nextIdKeys.has(idKey))
160
+ return;
161
+ persistedCellValuesById.delete(idKey);
162
+ dirtyBoundIdKeys.delete(idKey);
163
+ });
164
+ nextBoundIds.forEach((boundId) => {
165
+ const idKey = createGridIdKey(boundId);
166
+ if (!persistedCellValuesById.has(idKey)) {
167
+ persistedCellValuesById.set(idKey, undefined);
168
+ }
169
+ syncDimensionCellDirty(boundId);
170
+ });
113
171
  emitDimensionGridUpdate({
114
172
  type: "dimension",
115
173
  action: previousSize === nextBoundIds.length ? "update" : "resize",
@@ -160,37 +218,43 @@ export function createDimensionGrid(parentGrid, initialCells, dimension, options
160
218
  },
161
219
  getState,
162
220
  setGrid,
163
- getCell: getCellAtIndex,
164
- getValue: (index) => getCellAtIndex(index).get(),
165
- hasCell: (index) => Number.isInteger(index) && index >= 0 && index < boundIds.length,
166
- clearGrid,
167
- subscribeGrid: (callback) => {
168
- dimensionSubscribers.add(callback);
169
- return () => {
170
- dimensionSubscribers.delete(callback);
171
- };
172
- },
173
- __setCellValue: (index, newValue) => {
221
+ readCell: readCellAtIndex,
222
+ setCell: (index, nextValue) => {
223
+ const boundId = boundIds[index];
174
224
  const currentCell = getCellAtIndex(index);
175
- if (Object.is(currentCell.get(), newValue)) {
225
+ const resolvedNextValue = resolveUpdater(nextValue, currentCell.get());
226
+ if (Object.is(currentCell.get(), resolvedNextValue)) {
176
227
  return;
177
228
  }
178
229
  isApplyingState = true;
179
230
  try {
180
- currentCell.set(newValue);
231
+ currentCell.set(resolvedNextValue);
181
232
  }
182
233
  finally {
183
234
  isApplyingState = false;
184
235
  }
236
+ if (boundId !== undefined) {
237
+ syncDimensionCellDirty(boundId);
238
+ }
185
239
  emitDimensionGridUpdate({
186
240
  type: "cells",
187
- action: "update",
188
- source: "cell.set",
241
+ action: resolvedNextValue === undefined ? "clear" : "update",
242
+ source: "setCell",
189
243
  dimension,
190
244
  size: boundIds.length,
191
245
  indices: [index],
192
246
  }, true);
193
247
  },
248
+ getCell: getCellAtIndex,
249
+ getValue: (index) => getCellAtIndex(index).get(),
250
+ hasCell: (index) => Number.isInteger(index) && index >= 0 && index < boundIds.length,
251
+ clearGrid,
252
+ subscribeGrid: (callback) => {
253
+ dimensionSubscribers.add(callback);
254
+ return () => {
255
+ dimensionSubscribers.delete(callback);
256
+ };
257
+ },
194
258
  };
195
259
  Object.defineProperty(dimensionGridApi, "__persistOption", {
196
260
  value: resolvedPersistOption,
@@ -347,7 +411,12 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
347
411
  const columnTailUpdaters = new Map();
348
412
  const cellsMap = new Map();
349
413
  const cellSubscriptions = new Map();
414
+ const dirtyCellKeys = new Set();
350
415
  const gridSubscribers = new Set();
416
+ let persistedCellValues = new Map(initialCells.map(({ rowId, columnId, cell: currentCell }) => [
417
+ createGridKey(rowId, columnId),
418
+ currentCell.get(),
419
+ ]));
351
420
  let nextRowFallbackOrder = initialRows.length;
352
421
  let nextColumnFallbackOrder = initialColumns.length;
353
422
  let isApplyingState = false;
@@ -374,6 +443,44 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
374
443
  const normalizeCellValue = (rowId, columnId, value) => {
375
444
  return stateAdapter.deserializeCell(stateAdapter.serializeCell(rowId, columnId, value)).value;
376
445
  };
446
+ const assertCellAxesExist = (rowId, columnId) => {
447
+ if (!rowHeadCells.has(createGridIdKey(rowId))) {
448
+ throw new Error(`Missing row header "${formatGridId(rowId)}".`);
449
+ }
450
+ if (!columnHeadCells.has(createGridIdKey(columnId))) {
451
+ throw new Error(`Missing column header "${formatGridId(columnId)}".`);
452
+ }
453
+ };
454
+ const syncDirtyCellKey = (gridKey) => {
455
+ const currentCell = cellsMap.get(gridKey);
456
+ if (!persistedCellValues.has(gridKey)) {
457
+ if (!currentCell)
458
+ dirtyCellKeys.delete(gridKey);
459
+ else
460
+ dirtyCellKeys.add(gridKey);
461
+ return;
462
+ }
463
+ if (currentCell &&
464
+ haveSameRuntimeValue(currentCell.get(), persistedCellValues.get(gridKey))) {
465
+ dirtyCellKeys.delete(gridKey);
466
+ return;
467
+ }
468
+ dirtyCellKeys.add(gridKey);
469
+ };
470
+ const refreshDirtyCellKeys = () => {
471
+ const nextGridKeys = new Set([...persistedCellValues.keys(), ...cellsMap.keys()]);
472
+ dirtyCellKeys.clear();
473
+ nextGridKeys.forEach((gridKey) => {
474
+ syncDirtyCellKey(gridKey);
475
+ });
476
+ };
477
+ const resetPersistedCellState = () => {
478
+ persistedCellValues = new Map([...cellsMap.entries()].map(([gridKey, currentCell]) => [
479
+ gridKey,
480
+ currentCell.get(),
481
+ ]));
482
+ dirtyCellKeys.clear();
483
+ };
377
484
  const recomputeRowTailInternal = (rowId) => {
378
485
  const onRowUpdate = rowTailUpdaters.get(createGridIdKey(rowId));
379
486
  if (!onRowUpdate)
@@ -410,12 +517,7 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
410
517
  cellsMap.delete(gridKey);
411
518
  };
412
519
  const attachCell = (rowId, columnId, currentCell) => {
413
- if (!rowHeadCells.has(createGridIdKey(rowId))) {
414
- throw new Error(`Missing row header "${formatGridId(rowId)}".`);
415
- }
416
- if (!columnHeadCells.has(createGridIdKey(columnId))) {
417
- throw new Error(`Missing column header "${formatGridId(columnId)}".`);
418
- }
520
+ assertCellAxesExist(rowId, columnId);
419
521
  const gridKey = createGridKey(rowId, columnId);
420
522
  if (cellsMap.has(gridKey)) {
421
523
  throw new Error(`Duplicate cell for row "${formatGridId(rowId)}" and column "${formatGridId(columnId)}".`);
@@ -428,6 +530,7 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
428
530
  previousValue = nextValue;
429
531
  return;
430
532
  }
533
+ syncDirtyCellKey(gridKey);
431
534
  const rowTailIds = recomputeRowTailInternal(rowId) ? [rowId] : [];
432
535
  const columnTailIds = recomputeColumnTailInternal(columnId) ? [columnId] : [];
433
536
  emitGridUpdate({
@@ -447,12 +550,25 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
447
550
  cellSubscriptions.set(gridKey, unsubscribe);
448
551
  };
449
552
  const getCell = (rowId, columnId) => {
553
+ assertCellAxesExist(rowId, columnId);
450
554
  const currentCell = cellsMap.get(createGridKey(rowId, columnId));
451
555
  if (!currentCell) {
452
556
  throw new Error(`Missing cell for row "${formatGridId(rowId)}" and column "${formatGridId(columnId)}".`);
453
557
  }
454
558
  return currentCell;
455
559
  };
560
+ const readCell = (rowId, columnId) => {
561
+ assertCellAxesExist(rowId, columnId);
562
+ const gridKey = createGridKey(rowId, columnId);
563
+ const currentCell = cellsMap.get(gridKey);
564
+ return {
565
+ value: currentCell?.get(),
566
+ meta: {
567
+ existsInDb: currentCell !== undefined,
568
+ isDirty: dirtyCellKeys.has(gridKey),
569
+ },
570
+ };
571
+ };
456
572
  const createAxisCellSnapshot = (rowId, columnId) => {
457
573
  const currentCell = getCell(rowId, columnId);
458
574
  return {
@@ -548,6 +664,7 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
548
664
  const rowTailIds = recomputeAllRowTails();
549
665
  const columnTailIds = recomputeAllColumnTails();
550
666
  refreshAxisIdsSnapshot();
667
+ refreshDirtyCellKeys();
551
668
  return {
552
669
  rowTailIds,
553
670
  columnTailIds,
@@ -557,6 +674,8 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
557
674
  getState,
558
675
  replaceState,
559
676
  isApplyingState: () => isApplyingState,
677
+ onHydratedStateApplied: resetPersistedCellState,
678
+ onPersistedStateApplied: resetPersistedCellState,
560
679
  });
561
680
  const emitGridUpdate = (diff, persistStateChanged = false) => {
562
681
  refreshAxisIdsSnapshot();
@@ -605,6 +724,9 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
605
724
  finally {
606
725
  isApplyingState = false;
607
726
  }
727
+ cellKeys.forEach((gridKey) => {
728
+ syncDirtyCellKey(gridKey);
729
+ });
608
730
  const rowIds = [...touchedRowIds.values()];
609
731
  const columnIds = [...touchedColumnIds.values()];
610
732
  return {
@@ -803,6 +925,66 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
803
925
  const upsertCell = (nextCell) => {
804
926
  emitCellUpsert("upsertCell", [nextCell]);
805
927
  };
928
+ const setCell = (rowId, columnId, nextValue) => {
929
+ assertCellAxesExist(rowId, columnId);
930
+ const gridKey = createGridKey(rowId, columnId);
931
+ const currentCell = cellsMap.get(gridKey);
932
+ const currentValue = currentCell?.get();
933
+ const resolvedNextValue = resolveUpdater(nextValue, currentValue);
934
+ if (resolvedNextValue === undefined) {
935
+ if (!currentCell) {
936
+ return;
937
+ }
938
+ detachCell(gridKey);
939
+ syncDirtyCellKey(gridKey);
940
+ const rowTailIds = recomputeRowTailInternal(rowId) ? [rowId] : [];
941
+ const columnTailIds = recomputeColumnTailInternal(columnId) ? [columnId] : [];
942
+ emitGridUpdate({
943
+ type: "cells",
944
+ action: "clear",
945
+ source: "setCell",
946
+ rowIds: [rowId],
947
+ columnIds: [columnId],
948
+ rowTailIds: toOptionalArray(rowTailIds),
949
+ columnTailIds: toOptionalArray(columnTailIds),
950
+ cellKeys: [gridKey],
951
+ previousCells: [
952
+ stateAdapter.serializeCell(rowId, columnId, currentValue),
953
+ ],
954
+ }, true);
955
+ return;
956
+ }
957
+ const normalizedValue = normalizeCellValue(rowId, columnId, resolvedNextValue);
958
+ isApplyingState = true;
959
+ try {
960
+ if (currentCell) {
961
+ currentCell.set(normalizedValue);
962
+ }
963
+ else {
964
+ attachCell(rowId, columnId, cell(normalizedValue));
965
+ }
966
+ }
967
+ finally {
968
+ isApplyingState = false;
969
+ }
970
+ syncDirtyCellKey(gridKey);
971
+ const rowTailIds = recomputeRowTailInternal(rowId) ? [rowId] : [];
972
+ const columnTailIds = recomputeColumnTailInternal(columnId) ? [columnId] : [];
973
+ emitGridUpdate({
974
+ type: "cells",
975
+ action: currentCell ? "update" : "upsert",
976
+ source: "setCell",
977
+ rowIds: [rowId],
978
+ columnIds: [columnId],
979
+ rowTailIds: toOptionalArray(rowTailIds),
980
+ columnTailIds: toOptionalArray(columnTailIds),
981
+ cellKeys: [gridKey],
982
+ cells: [stateAdapter.serializeCell(rowId, columnId, normalizedValue)],
983
+ previousCells: currentCell
984
+ ? [stateAdapter.serializeCell(rowId, columnId, currentValue)]
985
+ : undefined,
986
+ }, true);
987
+ };
806
988
  function getState() {
807
989
  const rows = getOrderedRowHeads();
808
990
  const columns = getOrderedColumnHeads();
@@ -1012,31 +1194,10 @@ function createGridStore(initialRows, initialColumns, initialCells, stateAdapter
1012
1194
  },
1013
1195
  getState,
1014
1196
  setGrid,
1197
+ readCell,
1198
+ setCell,
1015
1199
  getCell,
1016
1200
  getValue: (rowId, columnId) => getCell(rowId, columnId).get(),
1017
- __setCellValue: (rowId, columnId, newValue) => {
1018
- const currentCell = getCell(rowId, columnId);
1019
- isApplyingState = true;
1020
- try {
1021
- currentCell.set(normalizeCellValue(rowId, columnId, newValue));
1022
- }
1023
- finally {
1024
- isApplyingState = false;
1025
- }
1026
- const rowTailIds = recomputeRowTailInternal(rowId) ? [rowId] : [];
1027
- const columnTailIds = recomputeColumnTailInternal(columnId) ? [columnId] : [];
1028
- emitGridUpdate({
1029
- type: "cells",
1030
- action: "update",
1031
- source: "cell.set",
1032
- rowIds: [rowId],
1033
- columnIds: [columnId],
1034
- rowTailIds: toOptionalArray(rowTailIds),
1035
- columnTailIds: toOptionalArray(columnTailIds),
1036
- cellKeys: [createGridKey(rowId, columnId)],
1037
- cells: [stateAdapter.serializeCell(rowId, columnId, currentCell.get())],
1038
- }, true);
1039
- },
1040
1201
  hasCell: (rowId, columnId) => cellsMap.has(createGridKey(rowId, columnId)),
1041
1202
  getRowHead: (rowId) => getHeadCell(rowHeadCells, rowId, "row").get(),
1042
1203
  getColumnHead: (columnId) => getHeadCell(columnHeadCells, columnId, "column").get(),
@@ -1094,40 +1255,44 @@ function resolveUpdater(nextValue, currentValue) {
1094
1255
  export function createGridKey(rowId, columnId) {
1095
1256
  return `row=${createGridIdKey(rowId)}|col=${createGridIdKey(columnId)}`;
1096
1257
  }
1097
- export function useGrid(currentGrid, options) {
1098
- const snapshotRef = useRef({
1099
- rows: currentGrid.rowHeaders,
1100
- cols: currentGrid.colHeaders,
1101
- });
1102
- const onGridUpdateRef = useRef(options?.onGridUpdate);
1103
- useEffect(() => {
1104
- onGridUpdateRef.current = options?.onGridUpdate;
1105
- }, [options?.onGridUpdate]);
1106
- useEffect(() => {
1107
- return currentGrid.subscribeGrid((updatedGrid, diff) => {
1108
- onGridUpdateRef.current?.(updatedGrid, diff);
1109
- });
1110
- }, [currentGrid]);
1111
- const subscribe = useCallback((callback) => currentGrid.subscribeGrid(() => callback()), [currentGrid]);
1112
- const getSnapshot = useCallback(() => {
1113
- const nextRows = currentGrid.rowHeaders;
1114
- const nextCols = currentGrid.colHeaders;
1115
- const currentSnapshot = snapshotRef.current;
1116
- if (currentSnapshot.rows === nextRows && currentSnapshot.cols === nextCols) {
1117
- return currentSnapshot;
1118
- }
1119
- const nextSnapshot = {
1120
- rows: nextRows,
1121
- cols: nextCols,
1122
- };
1123
- snapshotRef.current = nextSnapshot;
1124
- return nextSnapshot;
1125
- }, [currentGrid]);
1126
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1127
- }
1128
1258
  function toOptionalArray(values) {
1129
1259
  return values.length === 0 ? undefined : values;
1130
1260
  }
1261
+ function describeIndexRange(size, index) {
1262
+ if (size === 0) {
1263
+ return `The grid currently has no items, so index ${index} cannot be selected.`;
1264
+ }
1265
+ return `Expected an index between 0 and ${size - 1} (${size} total items).`;
1266
+ }
1267
+ function haveSameRuntimeValue(leftValue, rightValue) {
1268
+ if (Object.is(leftValue, rightValue)) {
1269
+ return true;
1270
+ }
1271
+ if (leftValue instanceof Date && rightValue instanceof Date) {
1272
+ return leftValue.getTime() === rightValue.getTime();
1273
+ }
1274
+ if (Array.isArray(leftValue) && Array.isArray(rightValue)) {
1275
+ return (leftValue.length === rightValue.length &&
1276
+ leftValue.every((currentValue, index) => haveSameRuntimeValue(currentValue, rightValue[index])));
1277
+ }
1278
+ if (!isPlainObject(leftValue) || !isPlainObject(rightValue)) {
1279
+ return false;
1280
+ }
1281
+ const leftKeys = Object.keys(leftValue);
1282
+ const rightKeys = Object.keys(rightValue);
1283
+ if (leftKeys.length !== rightKeys.length) {
1284
+ return false;
1285
+ }
1286
+ return leftKeys.every((key) => {
1287
+ if (!(key in rightValue)) {
1288
+ return false;
1289
+ }
1290
+ return haveSameRuntimeValue(leftValue[key], rightValue[key]);
1291
+ });
1292
+ }
1293
+ function isPlainObject(value) {
1294
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1295
+ }
1131
1296
  function haveSameItems(left, right) {
1132
1297
  return (left.length === right.length &&
1133
1298
  left.every((currentValue, index) => createGridIdKey(currentValue) === createGridIdKey(right[index])));