@comitor/ui 1.10.0 → 1.11.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/README.md +15 -4
- package/dist/data-grid/compose-refs.d.ts +19 -0
- package/dist/data-grid/compose-refs.js +41 -0
- package/dist/data-grid/compose-refs.js.map +1 -0
- package/dist/data-grid/data-grid-cell-variants.d.ts +15 -0
- package/dist/data-grid/data-grid-cell-variants.js +1734 -0
- package/dist/data-grid/data-grid-cell-variants.js.map +1 -0
- package/dist/data-grid/data-grid-cell-wrapper.d.ts +9 -0
- package/dist/data-grid/data-grid-cell-wrapper.js +179 -0
- package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -0
- package/dist/data-grid/data-grid-cell.d.ts +8 -0
- package/dist/data-grid/data-grid-cell.js +101 -0
- package/dist/data-grid/data-grid-cell.js.map +1 -0
- package/dist/data-grid/data-grid-column-header.d.ts +12 -0
- package/dist/data-grid/data-grid-column-header.js +205 -0
- package/dist/data-grid/data-grid-column-header.js.map +1 -0
- package/dist/data-grid/data-grid-context-menu.d.ts +12 -0
- package/dist/data-grid/data-grid-context-menu.js +160 -0
- package/dist/data-grid/data-grid-context-menu.js.map +1 -0
- package/dist/data-grid/data-grid-filter-menu.d.ts +12 -0
- package/dist/data-grid/data-grid-filter-menu.js +714 -0
- package/dist/data-grid/data-grid-filter-menu.js.map +1 -0
- package/dist/data-grid/data-grid-keyboard-shortcuts.d.ts +20 -0
- package/dist/data-grid/data-grid-keyboard-shortcuts.js +227 -0
- package/dist/data-grid/data-grid-keyboard-shortcuts.js.map +1 -0
- package/dist/data-grid/data-grid-paste-dialog.d.ts +11 -0
- package/dist/data-grid/data-grid-paste-dialog.js +103 -0
- package/dist/data-grid/data-grid-paste-dialog.js.map +1 -0
- package/dist/data-grid/data-grid-presence.d.ts +14 -0
- package/dist/data-grid/data-grid-presence.js +16 -0
- package/dist/data-grid/data-grid-presence.js.map +1 -0
- package/dist/data-grid/data-grid-row-height-menu.d.ts +15 -0
- package/dist/data-grid/data-grid-row-height-menu.js +56 -0
- package/dist/data-grid/data-grid-row-height-menu.js.map +1 -0
- package/dist/data-grid/data-grid-row.d.ts +28 -0
- package/dist/data-grid/data-grid-row.js +206 -0
- package/dist/data-grid/data-grid-row.js.map +1 -0
- package/dist/data-grid/data-grid-search.d.ts +10 -0
- package/dist/data-grid/data-grid-search.js +183 -0
- package/dist/data-grid/data-grid-search.js.map +1 -0
- package/dist/data-grid/data-grid-select-column.d.ts +12 -0
- package/dist/data-grid/data-grid-select-column.js +177 -0
- package/dist/data-grid/data-grid-select-column.js.map +1 -0
- package/dist/data-grid/data-grid-skeleton.d.ts +13 -0
- package/dist/data-grid/data-grid-skeleton.js +52 -0
- package/dist/data-grid/data-grid-skeleton.js.map +1 -0
- package/dist/data-grid/data-grid-sort-menu.d.ts +12 -0
- package/dist/data-grid/data-grid-sort-menu.js +271 -0
- package/dist/data-grid/data-grid-sort-menu.js.map +1 -0
- package/dist/data-grid/data-grid-view-menu.d.ts +13 -0
- package/dist/data-grid/data-grid-view-menu.js +66 -0
- package/dist/data-grid/data-grid-view-menu.js.map +1 -0
- package/dist/data-grid/data-grid.d.ts +32 -0
- package/dist/data-grid/data-grid.js +235 -0
- package/dist/data-grid/data-grid.js.map +1 -0
- package/dist/data-grid/filters.d.ts +31 -0
- package/dist/data-grid/filters.js +212 -0
- package/dist/data-grid/filters.js.map +1 -0
- package/dist/data-grid/index.d.ts +22 -0
- package/dist/data-grid/index.js +77 -0
- package/dist/data-grid/index.js.map +1 -0
- package/dist/data-grid/messages.d.ts +280 -0
- package/dist/data-grid/messages.js +279 -0
- package/dist/data-grid/messages.js.map +1 -0
- package/dist/data-grid/sortable.d.ts +50 -0
- package/dist/data-grid/sortable.js +361 -0
- package/dist/data-grid/sortable.js.map +1 -0
- package/dist/data-grid/toast.d.ts +10 -0
- package/dist/data-grid/toast.js +21 -0
- package/dist/data-grid/toast.js.map +1 -0
- package/dist/data-grid/types.d.ts +189 -0
- package/dist/data-grid/types.js +1 -0
- package/dist/data-grid/types.js.map +1 -0
- package/dist/data-grid/use-as-ref.d.ts +5 -0
- package/dist/data-grid/use-as-ref.js +13 -0
- package/dist/data-grid/use-as-ref.js.map +1 -0
- package/dist/data-grid/use-badge-overflow.d.ts +24 -0
- package/dist/data-grid/use-badge-overflow.js +123 -0
- package/dist/data-grid/use-badge-overflow.js.map +1 -0
- package/dist/data-grid/use-callback-ref.d.ts +10 -0
- package/dist/data-grid/use-callback-ref.js +12 -0
- package/dist/data-grid/use-callback-ref.js.map +1 -0
- package/dist/data-grid/use-data-grid-undo-redo.d.ts +33 -0
- package/dist/data-grid/use-data-grid-undo-redo.js +345 -0
- package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -0
- package/dist/data-grid/use-data-grid.d.ts +92 -0
- package/dist/data-grid/use-data-grid.js +2692 -0
- package/dist/data-grid/use-data-grid.js.map +1 -0
- package/dist/data-grid/use-debounced-callback.d.ts +6 -0
- package/dist/data-grid/use-debounced-callback.js +19 -0
- package/dist/data-grid/use-debounced-callback.js.map +1 -0
- package/dist/data-grid/use-isomorphic-layout-effect.d.ts +5 -0
- package/dist/data-grid/use-isomorphic-layout-effect.js +6 -0
- package/dist/data-grid/use-isomorphic-layout-effect.js.map +1 -0
- package/dist/data-grid/use-lazy-ref.d.ts +5 -0
- package/dist/data-grid/use-lazy-ref.js +12 -0
- package/dist/data-grid/use-lazy-ref.js.map +1 -0
- package/dist/data-grid/utils.d.ts +73 -0
- package/dist/data-grid/utils.js +408 -0
- package/dist/data-grid/utils.js.map +1 -0
- package/package.json +33 -15
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { msg } from "./messages.js";
|
|
4
|
+
import { toastInfo, toastSuccess } from "./toast.js";
|
|
5
|
+
import { useAsRef } from "./use-as-ref.js";
|
|
6
|
+
import { useLazyRef } from "./use-lazy-ref.js";
|
|
7
|
+
import { getIsInPopover } from "./utils.js";
|
|
8
|
+
const DEFAULT_MAX_HISTORY = 100;
|
|
9
|
+
const BATCH_TIMEOUT = 300;
|
|
10
|
+
function useStore(store, selector) {
|
|
11
|
+
const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);
|
|
12
|
+
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
13
|
+
}
|
|
14
|
+
function buildIndexById(data, getRowId) {
|
|
15
|
+
const map = /* @__PURE__ */ new Map();
|
|
16
|
+
for (let i = 0; i < data.length; i++) {
|
|
17
|
+
const row = data[i];
|
|
18
|
+
if (row) {
|
|
19
|
+
map.set(getRowId(row), i);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
function getPendingKey(rowId, columnId) {
|
|
25
|
+
return `${rowId}\0${columnId}`;
|
|
26
|
+
}
|
|
27
|
+
function useDataGridUndoRedo({
|
|
28
|
+
data,
|
|
29
|
+
onDataChange,
|
|
30
|
+
getRowId,
|
|
31
|
+
maxHistory = DEFAULT_MAX_HISTORY,
|
|
32
|
+
enabled = true
|
|
33
|
+
}) {
|
|
34
|
+
const propsRef = useAsRef({
|
|
35
|
+
data,
|
|
36
|
+
onDataChange,
|
|
37
|
+
getRowId,
|
|
38
|
+
maxHistory,
|
|
39
|
+
enabled
|
|
40
|
+
});
|
|
41
|
+
const listenersRef = useLazyRef(() => /* @__PURE__ */ new Set());
|
|
42
|
+
const stateRef = useLazyRef(() => ({
|
|
43
|
+
undoStack: [],
|
|
44
|
+
redoStack: [],
|
|
45
|
+
hasPendingChanges: false
|
|
46
|
+
}));
|
|
47
|
+
const pendingBatchRef = React.useRef({
|
|
48
|
+
byKey: /* @__PURE__ */ new Map(),
|
|
49
|
+
timeoutId: null
|
|
50
|
+
});
|
|
51
|
+
const pendingNotifyRef = React.useRef(false);
|
|
52
|
+
const store = React.useMemo(() => {
|
|
53
|
+
return {
|
|
54
|
+
subscribe: (callback) => {
|
|
55
|
+
listenersRef.current.add(callback);
|
|
56
|
+
return () => listenersRef.current.delete(callback);
|
|
57
|
+
},
|
|
58
|
+
getState: () => stateRef.current,
|
|
59
|
+
push: (entry) => {
|
|
60
|
+
const state = stateRef.current;
|
|
61
|
+
const newUndoStack = [...state.undoStack, entry];
|
|
62
|
+
if (newUndoStack.length > propsRef.current.maxHistory) {
|
|
63
|
+
newUndoStack.shift();
|
|
64
|
+
}
|
|
65
|
+
stateRef.current = {
|
|
66
|
+
undoStack: newUndoStack,
|
|
67
|
+
redoStack: [],
|
|
68
|
+
hasPendingChanges: false
|
|
69
|
+
};
|
|
70
|
+
store.notify();
|
|
71
|
+
},
|
|
72
|
+
undo: () => {
|
|
73
|
+
const state = stateRef.current;
|
|
74
|
+
if (state.undoStack.length === 0) return null;
|
|
75
|
+
const entry = state.undoStack[state.undoStack.length - 1];
|
|
76
|
+
if (!entry) return null;
|
|
77
|
+
stateRef.current = {
|
|
78
|
+
undoStack: state.undoStack.slice(0, -1),
|
|
79
|
+
redoStack: [...state.redoStack, entry],
|
|
80
|
+
hasPendingChanges: false
|
|
81
|
+
};
|
|
82
|
+
store.notify();
|
|
83
|
+
return entry;
|
|
84
|
+
},
|
|
85
|
+
redo: () => {
|
|
86
|
+
const state = stateRef.current;
|
|
87
|
+
if (state.redoStack.length === 0) return null;
|
|
88
|
+
const entry = state.redoStack[state.redoStack.length - 1];
|
|
89
|
+
if (!entry) return null;
|
|
90
|
+
stateRef.current = {
|
|
91
|
+
undoStack: [...state.undoStack, entry],
|
|
92
|
+
redoStack: state.redoStack.slice(0, -1),
|
|
93
|
+
hasPendingChanges: false
|
|
94
|
+
};
|
|
95
|
+
store.notify();
|
|
96
|
+
return entry;
|
|
97
|
+
},
|
|
98
|
+
clear: () => {
|
|
99
|
+
stateRef.current = {
|
|
100
|
+
undoStack: [],
|
|
101
|
+
redoStack: [],
|
|
102
|
+
hasPendingChanges: false
|
|
103
|
+
};
|
|
104
|
+
store.notify();
|
|
105
|
+
},
|
|
106
|
+
setPendingChanges: (value) => {
|
|
107
|
+
if (stateRef.current.hasPendingChanges === value) return;
|
|
108
|
+
stateRef.current = {
|
|
109
|
+
...stateRef.current,
|
|
110
|
+
hasPendingChanges: value
|
|
111
|
+
};
|
|
112
|
+
if (!pendingNotifyRef.current) {
|
|
113
|
+
pendingNotifyRef.current = true;
|
|
114
|
+
queueMicrotask(() => {
|
|
115
|
+
pendingNotifyRef.current = false;
|
|
116
|
+
store.notify();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
notify: () => {
|
|
121
|
+
for (const listener of listenersRef.current) {
|
|
122
|
+
listener();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}, [listenersRef, stateRef, propsRef]);
|
|
127
|
+
const canUndo = useStore(store, (state) => state.undoStack.length > 0 || state.hasPendingChanges);
|
|
128
|
+
const canRedo = useStore(store, (state) => state.redoStack.length > 0);
|
|
129
|
+
const onCommit = React.useCallback(() => {
|
|
130
|
+
const pending = pendingBatchRef.current;
|
|
131
|
+
if (pending.byKey.size === 0) return;
|
|
132
|
+
if (pending.timeoutId) {
|
|
133
|
+
clearTimeout(pending.timeoutId);
|
|
134
|
+
pending.timeoutId = null;
|
|
135
|
+
}
|
|
136
|
+
const updates = Array.from(pending.byKey.values());
|
|
137
|
+
pending.byKey.clear();
|
|
138
|
+
const { getRowId: getRowId2 } = propsRef.current;
|
|
139
|
+
const entry = {
|
|
140
|
+
variant: "cells_update",
|
|
141
|
+
count: updates.length,
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
undo: (currentData) => {
|
|
144
|
+
const newData = [...currentData];
|
|
145
|
+
const indexById = buildIndexById(newData, getRowId2);
|
|
146
|
+
for (const update of updates) {
|
|
147
|
+
const index = indexById.get(update.rowId);
|
|
148
|
+
if (index !== void 0) {
|
|
149
|
+
const row = newData[index];
|
|
150
|
+
if (row) {
|
|
151
|
+
newData[index] = {
|
|
152
|
+
...row,
|
|
153
|
+
[update.columnId]: update.previousValue
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return newData;
|
|
159
|
+
},
|
|
160
|
+
redo: (currentData) => {
|
|
161
|
+
const newData = [...currentData];
|
|
162
|
+
const indexById = buildIndexById(newData, getRowId2);
|
|
163
|
+
for (const update of updates) {
|
|
164
|
+
const index = indexById.get(update.rowId);
|
|
165
|
+
if (index !== void 0) {
|
|
166
|
+
const row = newData[index];
|
|
167
|
+
if (row) {
|
|
168
|
+
newData[index] = { ...row, [update.columnId]: update.newValue };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return newData;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
store.push(entry);
|
|
176
|
+
}, [store, propsRef]);
|
|
177
|
+
const onUndo = React.useCallback(() => {
|
|
178
|
+
if (!propsRef.current.enabled) return;
|
|
179
|
+
onCommit();
|
|
180
|
+
const entry = store.undo();
|
|
181
|
+
if (!entry) {
|
|
182
|
+
toastInfo(msg.toast.nothingToUndo);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const newData = entry.undo(propsRef.current.data);
|
|
186
|
+
propsRef.current.onDataChange(newData);
|
|
187
|
+
toastSuccess(msg.toast.undone(entry.count));
|
|
188
|
+
}, [store, propsRef, onCommit]);
|
|
189
|
+
const onRedo = React.useCallback(() => {
|
|
190
|
+
if (!propsRef.current.enabled) return;
|
|
191
|
+
onCommit();
|
|
192
|
+
const entry = store.redo();
|
|
193
|
+
if (!entry) {
|
|
194
|
+
toastInfo(msg.toast.nothingToRedo);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const newData = entry.redo(propsRef.current.data);
|
|
198
|
+
propsRef.current.onDataChange(newData);
|
|
199
|
+
toastSuccess(msg.toast.redone(entry.count));
|
|
200
|
+
}, [store, propsRef, onCommit]);
|
|
201
|
+
const onClear = React.useCallback(() => {
|
|
202
|
+
const pending = pendingBatchRef.current;
|
|
203
|
+
if (pending.timeoutId) {
|
|
204
|
+
clearTimeout(pending.timeoutId);
|
|
205
|
+
pending.timeoutId = null;
|
|
206
|
+
}
|
|
207
|
+
pending.byKey.clear();
|
|
208
|
+
store.clear();
|
|
209
|
+
}, [store]);
|
|
210
|
+
const trackCellsUpdate = React.useCallback(
|
|
211
|
+
(updates) => {
|
|
212
|
+
if (!propsRef.current.enabled || updates.length === 0) return;
|
|
213
|
+
const filteredUpdates = updates.filter((u) => !Object.is(u.previousValue, u.newValue));
|
|
214
|
+
if (filteredUpdates.length === 0) return;
|
|
215
|
+
const pending = pendingBatchRef.current;
|
|
216
|
+
for (const update of filteredUpdates) {
|
|
217
|
+
const key = getPendingKey(update.rowId, update.columnId);
|
|
218
|
+
const existing = pending.byKey.get(key);
|
|
219
|
+
if (existing) {
|
|
220
|
+
pending.byKey.set(key, { ...existing, newValue: update.newValue });
|
|
221
|
+
} else {
|
|
222
|
+
pending.byKey.set(key, update);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
store.setPendingChanges(true);
|
|
226
|
+
if (pending.timeoutId) {
|
|
227
|
+
clearTimeout(pending.timeoutId);
|
|
228
|
+
}
|
|
229
|
+
pending.timeoutId = setTimeout(onCommit, BATCH_TIMEOUT);
|
|
230
|
+
},
|
|
231
|
+
[store, propsRef, onCommit]
|
|
232
|
+
);
|
|
233
|
+
const trackRowsAdd = React.useCallback(
|
|
234
|
+
(rows) => {
|
|
235
|
+
if (!propsRef.current.enabled || rows.length === 0) return;
|
|
236
|
+
onCommit();
|
|
237
|
+
const { getRowId: getRowId2 } = propsRef.current;
|
|
238
|
+
const rowIds = new Set(rows.map((row) => getRowId2(row)));
|
|
239
|
+
const rowsCopy = rows.map((row) => ({ ...row }));
|
|
240
|
+
const entry = {
|
|
241
|
+
variant: "rows_add",
|
|
242
|
+
count: rows.length,
|
|
243
|
+
timestamp: Date.now(),
|
|
244
|
+
undo: (currentData) => {
|
|
245
|
+
return currentData.filter((row) => !rowIds.has(getRowId2(row)));
|
|
246
|
+
},
|
|
247
|
+
redo: (currentData) => {
|
|
248
|
+
return [...currentData, ...rowsCopy.map((row) => ({ ...row }))];
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
store.push(entry);
|
|
252
|
+
},
|
|
253
|
+
[store, propsRef, onCommit]
|
|
254
|
+
);
|
|
255
|
+
const trackRowsDelete = React.useCallback(
|
|
256
|
+
(rows) => {
|
|
257
|
+
if (!propsRef.current.enabled || rows.length === 0) return;
|
|
258
|
+
onCommit();
|
|
259
|
+
const { getRowId: getRowId2, data: currentData } = propsRef.current;
|
|
260
|
+
const indexById = buildIndexById(currentData, getRowId2);
|
|
261
|
+
const rowsWithPositions = [];
|
|
262
|
+
for (const row of rows) {
|
|
263
|
+
const rowId = getRowId2(row);
|
|
264
|
+
const currentIndex = indexById.get(rowId);
|
|
265
|
+
if (currentIndex !== void 0) {
|
|
266
|
+
rowsWithPositions.push({
|
|
267
|
+
index: currentIndex,
|
|
268
|
+
row: { ...row }
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
rowsWithPositions.sort((a, b) => a.index - b.index);
|
|
273
|
+
const rowIds = new Set(rows.map((row) => getRowId2(row)));
|
|
274
|
+
const entry = {
|
|
275
|
+
variant: "rows_delete",
|
|
276
|
+
count: rows.length,
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
undo: (currentData2) => {
|
|
279
|
+
const newData = [...currentData2];
|
|
280
|
+
for (const { index, row } of rowsWithPositions) {
|
|
281
|
+
const insertIndex = Math.min(index, newData.length);
|
|
282
|
+
newData.splice(insertIndex, 0, { ...row });
|
|
283
|
+
}
|
|
284
|
+
return newData;
|
|
285
|
+
},
|
|
286
|
+
redo: (currentData2) => {
|
|
287
|
+
return currentData2.filter((row) => !rowIds.has(getRowId2(row)));
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
store.push(entry);
|
|
291
|
+
},
|
|
292
|
+
[store, propsRef, onCommit]
|
|
293
|
+
);
|
|
294
|
+
React.useEffect(() => {
|
|
295
|
+
const pending = pendingBatchRef.current;
|
|
296
|
+
return () => {
|
|
297
|
+
if (pending.timeoutId) {
|
|
298
|
+
clearTimeout(pending.timeoutId);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}, []);
|
|
302
|
+
React.useEffect(() => {
|
|
303
|
+
if (!enabled) return;
|
|
304
|
+
function onKeyDown(event) {
|
|
305
|
+
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
|
|
306
|
+
const key = event.key.toLowerCase();
|
|
307
|
+
if (!isCtrlOrCmd || key !== "z" && key !== "y") return;
|
|
308
|
+
const activeElement = document.activeElement;
|
|
309
|
+
if (activeElement) {
|
|
310
|
+
const isInput = activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA";
|
|
311
|
+
const isContentEditable = activeElement.getAttribute("contenteditable") === "true";
|
|
312
|
+
const isInPopover = getIsInPopover(activeElement);
|
|
313
|
+
if (isInput || isContentEditable || isInPopover) return;
|
|
314
|
+
}
|
|
315
|
+
if (key === "z" && !event.shiftKey) {
|
|
316
|
+
event.preventDefault();
|
|
317
|
+
onUndo();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (key === "z" && event.shiftKey || key === "y") {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
onRedo();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
document.addEventListener("keydown", onKeyDown);
|
|
326
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
327
|
+
}, [enabled, onUndo, onRedo]);
|
|
328
|
+
return React.useMemo(
|
|
329
|
+
() => ({
|
|
330
|
+
canUndo,
|
|
331
|
+
canRedo,
|
|
332
|
+
onUndo,
|
|
333
|
+
onRedo,
|
|
334
|
+
onClear,
|
|
335
|
+
trackCellsUpdate,
|
|
336
|
+
trackRowsAdd,
|
|
337
|
+
trackRowsDelete
|
|
338
|
+
}),
|
|
339
|
+
[canUndo, canRedo, onUndo, onRedo, onClear, trackCellsUpdate, trackRowsAdd, trackRowsDelete]
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
export {
|
|
343
|
+
useDataGridUndoRedo
|
|
344
|
+
};
|
|
345
|
+
//# sourceMappingURL=use-data-grid-undo-redo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/data-grid/use-data-grid-undo-redo.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nimport { msg } from \"./messages.js\";\nimport { toastInfo, toastSuccess } from \"./toast.js\";\nimport { useAsRef } from \"./use-as-ref.js\";\nimport { useLazyRef } from \"./use-lazy-ref.js\";\nimport { getIsInPopover } from \"./utils.js\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst BATCH_TIMEOUT = 300;\n\ninterface HistoryEntry<TData> {\n variant: \"cells_update\" | \"rows_add\" | \"rows_delete\";\n count: number;\n timestamp: number;\n undo: (currentData: TData[]) => TData[];\n redo: (currentData: TData[]) => TData[];\n}\n\ninterface UndoRedoCellUpdate {\n rowId: string;\n columnId: string;\n previousValue: unknown;\n newValue: unknown;\n}\n\ninterface StoreState<TData> {\n undoStack: HistoryEntry<TData>[];\n redoStack: HistoryEntry<TData>[];\n hasPendingChanges: boolean;\n}\n\ninterface Store<TData> {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState<TData>;\n push: (entry: HistoryEntry<TData>) => void;\n undo: () => HistoryEntry<TData> | null;\n redo: () => HistoryEntry<TData> | null;\n clear: () => void;\n setPendingChanges: (value: boolean) => void;\n notify: () => void;\n}\n\nfunction useStore<T>(store: Store<T>, selector: (state: StoreState<T>) => boolean): boolean {\n const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\nfunction buildIndexById<TData>(data: TData[], getRowId: (row: TData) => string): Map<string, number> {\n const map = new Map<string, number>();\n for (let i = 0; i < data.length; i++) {\n const row = data[i];\n if (row) {\n map.set(getRowId(row), i);\n }\n }\n return map;\n}\n\nfunction getPendingKey(rowId: string, columnId: string): string {\n return `${rowId}\\0${columnId}`;\n}\n\n/**\n * Tuỳ chọn của {@link useDataGridUndoRedo}. Lịch sử nằm hoàn toàn ở phía trình duyệt:\n * hook tính ra mảng dữ liệu trước/sau rồi trả về qua `onDataChange` — ứng dụng tự\n * đồng bộ lên máy chủ (hoặc bỏ qua nếu chỉ làm việc với state cục bộ).\n */\ninterface UseDataGridUndoRedoProps<TData> {\n data: TData[];\n onDataChange: (data: TData[]) => void;\n /** Khoá định danh bản ghi — phải khớp với `getRowId` của lưới */\n getRowId: (row: TData) => string;\n /** Số thao tác giữ trong lịch sử (mặc định 100) */\n maxHistory?: number;\n enabled?: boolean;\n}\n\ninterface UseDataGridUndoRedoReturn<TData> {\n canUndo: boolean;\n canRedo: boolean;\n onUndo: () => void;\n onRedo: () => void;\n onClear: () => void;\n trackCellsUpdate: (updates: UndoRedoCellUpdate[]) => void;\n trackRowsAdd: (rows: TData[]) => void;\n trackRowsDelete: (rows: TData[]) => void;\n}\n\nfunction useDataGridUndoRedo<TData>({\n data,\n onDataChange,\n getRowId,\n maxHistory = DEFAULT_MAX_HISTORY,\n enabled = true\n}: UseDataGridUndoRedoProps<TData>): UseDataGridUndoRedoReturn<TData> {\n const propsRef = useAsRef({\n data,\n onDataChange,\n getRowId,\n maxHistory,\n enabled\n });\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n\n const stateRef = useLazyRef<StoreState<TData>>(() => ({\n undoStack: [],\n redoStack: [],\n hasPendingChanges: false\n }));\n\n const pendingBatchRef = React.useRef<{\n byKey: Map<string, UndoRedoCellUpdate>;\n timeoutId: ReturnType<typeof setTimeout> | null;\n }>({\n byKey: new Map(),\n timeoutId: null\n });\n\n const pendingNotifyRef = React.useRef(false);\n\n const store = React.useMemo<Store<TData>>(() => {\n return {\n subscribe: (callback) => {\n listenersRef.current.add(callback);\n return () => listenersRef.current.delete(callback);\n },\n getState: () => stateRef.current,\n push: (entry) => {\n const state = stateRef.current;\n const newUndoStack = [...state.undoStack, entry];\n\n if (newUndoStack.length > propsRef.current.maxHistory) {\n newUndoStack.shift();\n }\n\n stateRef.current = {\n undoStack: newUndoStack,\n redoStack: [],\n hasPendingChanges: false\n };\n store.notify();\n },\n undo: () => {\n const state = stateRef.current;\n if (state.undoStack.length === 0) return null;\n\n const entry = state.undoStack[state.undoStack.length - 1];\n if (!entry) return null;\n\n stateRef.current = {\n undoStack: state.undoStack.slice(0, -1),\n redoStack: [...state.redoStack, entry],\n hasPendingChanges: false\n };\n store.notify();\n return entry;\n },\n redo: () => {\n const state = stateRef.current;\n if (state.redoStack.length === 0) return null;\n\n const entry = state.redoStack[state.redoStack.length - 1];\n if (!entry) return null;\n\n stateRef.current = {\n undoStack: [...state.undoStack, entry],\n redoStack: state.redoStack.slice(0, -1),\n hasPendingChanges: false\n };\n store.notify();\n return entry;\n },\n clear: () => {\n stateRef.current = {\n undoStack: [],\n redoStack: [],\n hasPendingChanges: false\n };\n store.notify();\n },\n setPendingChanges: (value) => {\n if (stateRef.current.hasPendingChanges === value) return;\n stateRef.current = {\n ...stateRef.current,\n hasPendingChanges: value\n };\n\n if (!pendingNotifyRef.current) {\n pendingNotifyRef.current = true;\n queueMicrotask(() => {\n pendingNotifyRef.current = false;\n store.notify();\n });\n }\n },\n notify: () => {\n for (const listener of listenersRef.current) {\n listener();\n }\n }\n };\n }, [listenersRef, stateRef, propsRef]);\n\n const canUndo = useStore(store, (state) => state.undoStack.length > 0 || state.hasPendingChanges);\n const canRedo = useStore(store, (state) => state.redoStack.length > 0);\n\n const onCommit = React.useCallback(() => {\n const pending = pendingBatchRef.current;\n if (pending.byKey.size === 0) return;\n\n if (pending.timeoutId) {\n clearTimeout(pending.timeoutId);\n pending.timeoutId = null;\n }\n\n const updates = Array.from(pending.byKey.values());\n pending.byKey.clear();\n\n const { getRowId } = propsRef.current;\n\n const entry: HistoryEntry<TData> = {\n variant: \"cells_update\",\n count: updates.length,\n timestamp: Date.now(),\n undo: (currentData) => {\n const newData = [...currentData];\n const indexById = buildIndexById(newData, getRowId);\n\n for (const update of updates) {\n const index = indexById.get(update.rowId);\n if (index !== undefined) {\n const row = newData[index];\n if (row) {\n newData[index] = {\n ...row,\n [update.columnId]: update.previousValue\n };\n }\n }\n }\n return newData;\n },\n redo: (currentData) => {\n const newData = [...currentData];\n const indexById = buildIndexById(newData, getRowId);\n\n for (const update of updates) {\n const index = indexById.get(update.rowId);\n if (index !== undefined) {\n const row = newData[index];\n if (row) {\n newData[index] = { ...row, [update.columnId]: update.newValue };\n }\n }\n }\n return newData;\n }\n };\n\n store.push(entry);\n }, [store, propsRef]);\n\n const onUndo = React.useCallback(() => {\n if (!propsRef.current.enabled) return;\n\n onCommit();\n\n const entry = store.undo();\n if (!entry) {\n toastInfo(msg.toast.nothingToUndo);\n return;\n }\n\n const newData = entry.undo(propsRef.current.data);\n propsRef.current.onDataChange(newData);\n\n toastSuccess(msg.toast.undone(entry.count));\n }, [store, propsRef, onCommit]);\n\n const onRedo = React.useCallback(() => {\n if (!propsRef.current.enabled) return;\n\n onCommit();\n\n const entry = store.redo();\n if (!entry) {\n toastInfo(msg.toast.nothingToRedo);\n return;\n }\n\n const newData = entry.redo(propsRef.current.data);\n propsRef.current.onDataChange(newData);\n\n toastSuccess(msg.toast.redone(entry.count));\n }, [store, propsRef, onCommit]);\n\n const onClear = React.useCallback(() => {\n const pending = pendingBatchRef.current;\n if (pending.timeoutId) {\n clearTimeout(pending.timeoutId);\n pending.timeoutId = null;\n }\n pending.byKey.clear();\n\n store.clear();\n }, [store]);\n\n const trackCellsUpdate = React.useCallback(\n (updates: UndoRedoCellUpdate[]) => {\n if (!propsRef.current.enabled || updates.length === 0) return;\n\n const filteredUpdates = updates.filter((u) => !Object.is(u.previousValue, u.newValue));\n if (filteredUpdates.length === 0) return;\n\n const pending = pendingBatchRef.current;\n\n for (const update of filteredUpdates) {\n const key = getPendingKey(update.rowId, update.columnId);\n const existing = pending.byKey.get(key);\n\n if (existing) {\n pending.byKey.set(key, { ...existing, newValue: update.newValue });\n } else {\n pending.byKey.set(key, update);\n }\n }\n\n store.setPendingChanges(true);\n\n if (pending.timeoutId) {\n clearTimeout(pending.timeoutId);\n }\n pending.timeoutId = setTimeout(onCommit, BATCH_TIMEOUT);\n },\n [store, propsRef, onCommit]\n );\n\n const trackRowsAdd = React.useCallback(\n (rows: TData[]) => {\n if (!propsRef.current.enabled || rows.length === 0) return;\n\n onCommit();\n\n const { getRowId } = propsRef.current;\n\n const rowIds = new Set(rows.map((row) => getRowId(row)));\n const rowsCopy = rows.map((row) => ({ ...row }));\n\n const entry: HistoryEntry<TData> = {\n variant: \"rows_add\",\n count: rows.length,\n timestamp: Date.now(),\n undo: (currentData) => {\n return currentData.filter((row) => !rowIds.has(getRowId(row)));\n },\n redo: (currentData) => {\n return [...currentData, ...rowsCopy.map((row) => ({ ...row }))];\n }\n };\n\n store.push(entry);\n },\n [store, propsRef, onCommit]\n );\n\n const trackRowsDelete = React.useCallback(\n (rows: TData[]) => {\n if (!propsRef.current.enabled || rows.length === 0) return;\n\n onCommit();\n\n const { getRowId, data: currentData } = propsRef.current;\n\n const indexById = buildIndexById(currentData, getRowId);\n\n const rowsWithPositions: Array<{ index: number; row: TData }> = [];\n for (const row of rows) {\n const rowId = getRowId(row);\n const currentIndex = indexById.get(rowId);\n if (currentIndex !== undefined) {\n rowsWithPositions.push({\n index: currentIndex,\n row: { ...row }\n });\n }\n }\n\n rowsWithPositions.sort((a, b) => a.index - b.index);\n\n const rowIds = new Set(rows.map((row) => getRowId(row)));\n\n const entry: HistoryEntry<TData> = {\n variant: \"rows_delete\",\n count: rows.length,\n timestamp: Date.now(),\n undo: (currentData) => {\n const newData = [...currentData];\n for (const { index, row } of rowsWithPositions) {\n const insertIndex = Math.min(index, newData.length);\n newData.splice(insertIndex, 0, { ...row });\n }\n return newData;\n },\n redo: (currentData) => {\n return currentData.filter((row) => !rowIds.has(getRowId(row)));\n }\n };\n\n store.push(entry);\n },\n [store, propsRef, onCommit]\n );\n\n React.useEffect(() => {\n const pending = pendingBatchRef.current;\n return () => {\n if (pending.timeoutId) {\n clearTimeout(pending.timeoutId);\n }\n };\n }, []);\n\n React.useEffect(() => {\n if (!enabled) return;\n\n function onKeyDown(event: KeyboardEvent) {\n const isCtrlOrCmd = event.ctrlKey || event.metaKey;\n const key = event.key.toLowerCase();\n\n if (!isCtrlOrCmd || (key !== \"z\" && key !== \"y\")) return;\n\n const activeElement = document.activeElement;\n if (activeElement) {\n const isInput = activeElement.tagName === \"INPUT\" || activeElement.tagName === \"TEXTAREA\";\n const isContentEditable = activeElement.getAttribute(\"contenteditable\") === \"true\";\n const isInPopover = getIsInPopover(activeElement);\n\n if (isInput || isContentEditable || isInPopover) return;\n }\n\n if (key === \"z\" && !event.shiftKey) {\n event.preventDefault();\n onUndo();\n return;\n }\n\n if ((key === \"z\" && event.shiftKey) || key === \"y\") {\n event.preventDefault();\n onRedo();\n }\n }\n\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [enabled, onUndo, onRedo]);\n\n return React.useMemo(\n () => ({\n canUndo,\n canRedo,\n onUndo,\n onRedo,\n onClear,\n trackCellsUpdate,\n trackRowsAdd,\n trackRowsDelete\n }),\n [canUndo, canRedo, onUndo, onRedo, onClear, trackCellsUpdate, trackRowsAdd, trackRowsDelete]\n );\n}\n\nexport {\n //\n type UndoRedoCellUpdate,\n useDataGridUndoRedo\n};\n"],"mappings":"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","names":["getRowId","currentData"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as _tanstack_react_virtual from '@tanstack/react-virtual';
|
|
2
|
+
import * as _tanstack_react_table from '@tanstack/react-table';
|
|
3
|
+
import { TableOptions, TableMeta, ColumnDef } from '@tanstack/react-table';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { CellPosition, CellUpdate, FileCellData, RowHeightValue, Direction, SearchState, ContextMenuState, PasteDialogState } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tuỳ chọn của {@link useDataGrid} — mở rộng `TableOptions` của TanStack Table v8.
|
|
9
|
+
*
|
|
10
|
+
* Hook KHÔNG tự gọi API nào: mọi thay đổi dữ liệu đều đi ra ngoài qua callback
|
|
11
|
+
* (`onDataChange`, `onRowAdd`, `onRowsDelete`, `onFilesUpload`…) để ứng dụng tự
|
|
12
|
+
* quyết định lưu ở đâu (REST, GraphQL, state cục bộ…).
|
|
13
|
+
*/
|
|
14
|
+
interface UseDataGridProps<TData> extends Omit<TableOptions<TData>, "pageCount" | "getCoreRowModel"> {
|
|
15
|
+
/**
|
|
16
|
+
* Nhận MẢNG DỮ LIỆU MỚI sau mỗi lần sửa ô/dán/xoá nội dung.
|
|
17
|
+
* Ứng dụng tự setState + gọi API lưu (thường lưu lạc quan rồi rollback khi lỗi).
|
|
18
|
+
*/
|
|
19
|
+
onDataChange?: (data: TData[]) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Người dùng bấm "Thêm dòng" (hoặc Shift+Enter). Trả về ô cần focus sau khi thêm
|
|
22
|
+
* (`{ rowIndex, columnId }`), hoặc `null` để lưới không cuộn/focus.
|
|
23
|
+
*/
|
|
24
|
+
onRowAdd?: (event?: React.MouseEvent<HTMLDivElement>) => Partial<CellPosition> | Promise<Partial<CellPosition> | null> | null;
|
|
25
|
+
/** Dán nhiều dòng hơn số dòng hiện có → thêm `count` dòng trong MỘT lần gọi API */
|
|
26
|
+
onRowsAdd?: (count: number) => void | Promise<void>;
|
|
27
|
+
/** Xoá dòng (menu chuột phải hoặc Ctrl+Backspace) — nhận cả bản ghi lẫn chỉ số dòng */
|
|
28
|
+
onRowsDelete?: (rows: TData[], rowIndices: number[]) => void | Promise<void>;
|
|
29
|
+
/** Chạy TRƯỚC khi áp dụng dữ liệu dán — dùng để gọi API bulk update nếu cần */
|
|
30
|
+
onPaste?: (updates: Array<CellUpdate>) => void | Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Tải tệp lên cho ô `variant: "file"`. Không truyền thì tệp chỉ nằm trong bộ nhớ
|
|
33
|
+
* trình duyệt (blob URL) và mất khi tải lại trang.
|
|
34
|
+
*/
|
|
35
|
+
onFilesUpload?: (params: {
|
|
36
|
+
files: File[];
|
|
37
|
+
rowIndex: number;
|
|
38
|
+
columnId: string;
|
|
39
|
+
}) => Promise<FileCellData[]>;
|
|
40
|
+
onFilesDelete?: (params: {
|
|
41
|
+
fileIds: string[];
|
|
42
|
+
rowIndex: number;
|
|
43
|
+
columnId: string;
|
|
44
|
+
}) => void | Promise<void>;
|
|
45
|
+
/** Chiều cao dòng ban đầu (mặc định "short" = 36px) */
|
|
46
|
+
rowHeight?: RowHeightValue;
|
|
47
|
+
onRowHeightChange?: (rowHeight: RowHeightValue) => void;
|
|
48
|
+
/** Số dòng render thêm ngoài vùng nhìn thấy (mặc định 6) */
|
|
49
|
+
overscan?: number;
|
|
50
|
+
dir?: Direction;
|
|
51
|
+
/** Tự focus ô đầu tiên (hoặc ô chỉ định) khi có dữ liệu */
|
|
52
|
+
autoFocus?: boolean | Partial<CellPosition>;
|
|
53
|
+
/** Bấm chuột vào ô là chọn luôn ô đó (mặc định chỉ focus) */
|
|
54
|
+
enableSingleCellSelection?: boolean;
|
|
55
|
+
/** Bấm tiêu đề cột để chọn cả cột */
|
|
56
|
+
enableColumnSelection?: boolean;
|
|
57
|
+
/** Bật tìm trong bảng (Ctrl+F) */
|
|
58
|
+
enableSearch?: boolean;
|
|
59
|
+
/** Bật dán từ clipboard (Ctrl+V) */
|
|
60
|
+
enablePaste?: boolean;
|
|
61
|
+
/** Chỉ xem: khoá mọi thao tác sửa/xoá/dán */
|
|
62
|
+
readOnly?: boolean;
|
|
63
|
+
}
|
|
64
|
+
declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, overscan, dir: dirProp, initialState, ...props }: UseDataGridProps<TData>): {
|
|
65
|
+
dataGridRef: React.RefObject<HTMLDivElement | null>;
|
|
66
|
+
headerRef: React.RefObject<HTMLDivElement | null>;
|
|
67
|
+
rowMapRef: React.RefObject<Map<number, HTMLDivElement>>;
|
|
68
|
+
footerRef: React.RefObject<HTMLDivElement | null>;
|
|
69
|
+
dir: "ltr" | "rtl";
|
|
70
|
+
table: _tanstack_react_table.Table<TData>;
|
|
71
|
+
tableMeta: TableMeta<TData>;
|
|
72
|
+
virtualTotalSize: number;
|
|
73
|
+
virtualItems: _tanstack_react_virtual.VirtualItem[];
|
|
74
|
+
measureElement: (node: Element | null) => void;
|
|
75
|
+
columns: ColumnDef<TData, any>[];
|
|
76
|
+
columnSizeVars: {
|
|
77
|
+
[key: string]: number;
|
|
78
|
+
};
|
|
79
|
+
searchState: SearchState | undefined;
|
|
80
|
+
searchMatchesByRow: Map<number, Set<string>> | null;
|
|
81
|
+
activeSearchMatch: CellPosition | null;
|
|
82
|
+
cellSelectionMap: Map<number, Set<string>> | null;
|
|
83
|
+
focusedCell: CellPosition | null;
|
|
84
|
+
editingCell: CellPosition | null;
|
|
85
|
+
rowHeight: RowHeightValue;
|
|
86
|
+
contextMenu: ContextMenuState;
|
|
87
|
+
pasteDialog: PasteDialogState;
|
|
88
|
+
onRowAdd: ((event?: React.MouseEvent<HTMLDivElement>) => Promise<void>) | undefined;
|
|
89
|
+
adjustLayout: boolean;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export { type UseDataGridProps, useDataGrid };
|