@tsdraw/react 0.5.1 → 0.6.1
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/index.cjs +301 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -5
- package/dist/index.d.ts +24 -5
- package/dist/index.js +302 -87
- package/dist/index.js.map +1 -1
- package/dist/tsdraw.css +24 -3
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -203,23 +203,46 @@ function getDefaultToolbarIcon(toolId, isActive) {
|
|
|
203
203
|
if (toolId === "hand") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconHandStop, { size: 16, stroke: isActive ? 1 : 1.8, fill: isActive ? "currentColor" : "none", style: isActive ? { stroke: "#000000" } : void 0 });
|
|
204
204
|
return null;
|
|
205
205
|
}
|
|
206
|
-
function
|
|
207
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
206
|
+
function getActionIcon(actionId) {
|
|
207
|
+
if (actionId === "undo") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconArrowBackUp, { size: 16, stroke: 1.8 });
|
|
208
|
+
return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconArrowForwardUp, { size: 16, stroke: 1.8 });
|
|
209
|
+
}
|
|
210
|
+
function Toolbar({ parts, currentTool, onToolChange, disabled, style }) {
|
|
211
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-toolbar", style, children: parts.map((part, partIndex) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tsdraw-toolbar-part", children: [
|
|
212
|
+
part.items.map((item) => {
|
|
213
|
+
if (item.type === "action") {
|
|
214
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
215
|
+
"button",
|
|
216
|
+
{
|
|
217
|
+
type: "button",
|
|
218
|
+
className: "tsdraw-toolbar-btn",
|
|
219
|
+
onClick: item.onSelect,
|
|
220
|
+
title: item.label,
|
|
221
|
+
"aria-label": item.label,
|
|
222
|
+
disabled: disabled || item.disabled,
|
|
223
|
+
children: getActionIcon(item.id)
|
|
224
|
+
},
|
|
225
|
+
item.id
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const isActive = currentTool === item.id;
|
|
229
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
230
|
+
"button",
|
|
231
|
+
{
|
|
232
|
+
type: "button",
|
|
233
|
+
className: "tsdraw-toolbar-btn",
|
|
234
|
+
"data-active": isActive ? "true" : void 0,
|
|
235
|
+
onClick: () => onToolChange(item.id),
|
|
236
|
+
title: item.label,
|
|
237
|
+
"aria-label": item.label,
|
|
238
|
+
disabled,
|
|
239
|
+
children: typeof item.icon === "function" ? item.icon(isActive) : item.icon
|
|
240
|
+
},
|
|
241
|
+
item.id
|
|
242
|
+
);
|
|
243
|
+
}),
|
|
244
|
+
partIndex < parts.length - 1 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-toolbar-separator" }) : null
|
|
245
|
+
] }, part.id)) });
|
|
223
246
|
}
|
|
224
247
|
function getCanvasCursor(currentTool, state) {
|
|
225
248
|
if (currentTool === "hand") return "grab";
|
|
@@ -234,10 +257,11 @@ function getCanvasCursor(currentTool, state) {
|
|
|
234
257
|
|
|
235
258
|
// src/persistence/localIndexedDb.ts
|
|
236
259
|
var DATABASE_PREFIX = "tsdraw_v1_";
|
|
237
|
-
var DATABASE_VERSION =
|
|
260
|
+
var DATABASE_VERSION = 2;
|
|
238
261
|
var STORE = {
|
|
239
262
|
records: "records",
|
|
240
|
-
state: "state"
|
|
263
|
+
state: "state",
|
|
264
|
+
history: "history"
|
|
241
265
|
};
|
|
242
266
|
function requestToPromise(request) {
|
|
243
267
|
return new Promise((resolve, reject) => {
|
|
@@ -263,6 +287,9 @@ function openLocalDatabase(persistenceKey) {
|
|
|
263
287
|
if (!database.objectStoreNames.contains(STORE.state)) {
|
|
264
288
|
database.createObjectStore(STORE.state);
|
|
265
289
|
}
|
|
290
|
+
if (!database.objectStoreNames.contains(STORE.history)) {
|
|
291
|
+
database.createObjectStore(STORE.history);
|
|
292
|
+
}
|
|
266
293
|
};
|
|
267
294
|
request.onsuccess = () => resolve(request.result);
|
|
268
295
|
request.onerror = () => reject(request.error ?? new Error("Failed to open IndexedDB"));
|
|
@@ -279,11 +306,13 @@ var TsdrawLocalIndexedDb = class {
|
|
|
279
306
|
}
|
|
280
307
|
async load(sessionId) {
|
|
281
308
|
const database = await this.databasePromise;
|
|
282
|
-
const transaction = database.transaction([STORE.records, STORE.state], "readonly");
|
|
309
|
+
const transaction = database.transaction([STORE.records, STORE.state, STORE.history], "readonly");
|
|
283
310
|
const recordStore = transaction.objectStore(STORE.records);
|
|
284
311
|
const stateStore = transaction.objectStore(STORE.state);
|
|
312
|
+
const historyStore = transaction.objectStore(STORE.history);
|
|
285
313
|
const records = await requestToPromise(recordStore.getAll());
|
|
286
314
|
let state = (await requestToPromise(stateStore.get(sessionId)))?.snapshot ?? null;
|
|
315
|
+
let history = (await requestToPromise(historyStore.get(sessionId)))?.snapshot ?? null;
|
|
287
316
|
if (!state) {
|
|
288
317
|
const allStates = await requestToPromise(stateStore.getAll());
|
|
289
318
|
if (allStates.length > 0) {
|
|
@@ -291,14 +320,22 @@ var TsdrawLocalIndexedDb = class {
|
|
|
291
320
|
state = allStates[allStates.length - 1]?.snapshot ?? null;
|
|
292
321
|
}
|
|
293
322
|
}
|
|
323
|
+
if (!history) {
|
|
324
|
+
const allHistoryRows = await requestToPromise(historyStore.getAll());
|
|
325
|
+
if (allHistoryRows.length > 0) {
|
|
326
|
+
allHistoryRows.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
327
|
+
history = allHistoryRows[allHistoryRows.length - 1]?.snapshot ?? null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
294
330
|
await transactionDone(transaction);
|
|
295
|
-
return { records, state };
|
|
331
|
+
return { records, state, history };
|
|
296
332
|
}
|
|
297
333
|
async storeSnapshot(args) {
|
|
298
334
|
const database = await this.databasePromise;
|
|
299
|
-
const transaction = database.transaction([STORE.records, STORE.state], "readwrite");
|
|
335
|
+
const transaction = database.transaction([STORE.records, STORE.state, STORE.history], "readwrite");
|
|
300
336
|
const recordStore = transaction.objectStore(STORE.records);
|
|
301
337
|
const stateStore = transaction.objectStore(STORE.state);
|
|
338
|
+
const historyStore = transaction.objectStore(STORE.history);
|
|
302
339
|
recordStore.clear();
|
|
303
340
|
for (const record of args.records) {
|
|
304
341
|
recordStore.put(record, record.id);
|
|
@@ -309,6 +346,12 @@ var TsdrawLocalIndexedDb = class {
|
|
|
309
346
|
updatedAt: Date.now()
|
|
310
347
|
};
|
|
311
348
|
stateStore.put(stateRow, args.sessionId);
|
|
349
|
+
const historyRow = {
|
|
350
|
+
id: args.sessionId,
|
|
351
|
+
snapshot: args.history,
|
|
352
|
+
updatedAt: Date.now()
|
|
353
|
+
};
|
|
354
|
+
historyStore.put(historyRow, args.sessionId);
|
|
312
355
|
await transactionDone(transaction);
|
|
313
356
|
}
|
|
314
357
|
};
|
|
@@ -386,6 +429,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
386
429
|
const [isMovingSelection, setIsMovingSelection] = react.useState(false);
|
|
387
430
|
const [isResizingSelection, setIsResizingSelection] = react.useState(false);
|
|
388
431
|
const [isRotatingSelection, setIsRotatingSelection] = react.useState(false);
|
|
432
|
+
const [canUndo, setCanUndo] = react.useState(false);
|
|
433
|
+
const [canRedo, setCanRedo] = react.useState(false);
|
|
434
|
+
const [isPersistenceReady, setIsPersistenceReady] = react.useState(!options.persistenceKey);
|
|
389
435
|
const [pointerScreenPoint, setPointerScreenPoint] = react.useState({ x: 0, y: 0 });
|
|
390
436
|
const [isPointerInsideCanvas, setIsPointerInsideCanvas] = react.useState(false);
|
|
391
437
|
react.useEffect(() => {
|
|
@@ -463,6 +509,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
463
509
|
startBounds: bounds,
|
|
464
510
|
startShapes: core.buildTransformSnapshots(editor, selectedShapeIdsRef.current)
|
|
465
511
|
};
|
|
512
|
+
editor.beginHistoryEntry();
|
|
466
513
|
selectDragRef.current.mode = "resize";
|
|
467
514
|
const p = getPagePointFromClient(editor, e.clientX, e.clientY);
|
|
468
515
|
editor.input.pointerDown(p.x, p.y, 0.5, false);
|
|
@@ -492,6 +539,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
492
539
|
startSelectionRotationDeg: selectionRotationRef.current,
|
|
493
540
|
startShapes: core.buildTransformSnapshots(editor, selectedShapeIdsRef.current)
|
|
494
541
|
};
|
|
542
|
+
editor.beginHistoryEntry();
|
|
495
543
|
selectDragRef.current.mode = "rotate";
|
|
496
544
|
editor.input.pointerDown(p.x, p.y, 0.5, false);
|
|
497
545
|
selectDragRef.current.startPage = p;
|
|
@@ -524,10 +572,15 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
524
572
|
let persistenceActive = false;
|
|
525
573
|
const persistenceKey = options.persistenceKey;
|
|
526
574
|
const sessionId = getOrCreateSessionId();
|
|
575
|
+
const syncHistoryState = () => {
|
|
576
|
+
setCanUndo(editor.canUndo());
|
|
577
|
+
setCanRedo(editor.canRedo());
|
|
578
|
+
};
|
|
527
579
|
const activeTool = editor.getCurrentToolId();
|
|
528
580
|
editorRef.current = editor;
|
|
529
581
|
setCurrentToolState(activeTool);
|
|
530
582
|
currentToolRef.current = activeTool;
|
|
583
|
+
syncHistoryState();
|
|
531
584
|
const initialStyle = editor.getCurrentDrawStyle();
|
|
532
585
|
setDrawColor(initialStyle.color);
|
|
533
586
|
setDrawDash(initialStyle.dash);
|
|
@@ -554,8 +607,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
554
607
|
await persistenceDb.storeSnapshot({
|
|
555
608
|
records: snapshot.document.records,
|
|
556
609
|
state: snapshot.state,
|
|
610
|
+
history: editor.getHistorySnapshot(),
|
|
557
611
|
sessionId
|
|
558
612
|
});
|
|
613
|
+
if (disposed) return;
|
|
559
614
|
persistenceChannel?.postMessage({
|
|
560
615
|
type: "tsdraw:persisted",
|
|
561
616
|
senderSessionId: sessionId
|
|
@@ -594,6 +649,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
594
649
|
const applyRemoteDocumentSnapshot = (document) => {
|
|
595
650
|
ignorePersistenceChanges = true;
|
|
596
651
|
editor.loadDocumentSnapshot(document);
|
|
652
|
+
editor.clearRedoHistory();
|
|
597
653
|
reconcileSelectionAfterDocumentLoad();
|
|
598
654
|
render();
|
|
599
655
|
ignorePersistenceChanges = false;
|
|
@@ -626,6 +682,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
626
682
|
const handlePointerDown = (e) => {
|
|
627
683
|
if (!canvas.contains(e.target)) return;
|
|
628
684
|
isPointerActiveRef.current = true;
|
|
685
|
+
editor.beginHistoryEntry();
|
|
629
686
|
canvas.setPointerCapture(e.pointerId);
|
|
630
687
|
lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
|
|
631
688
|
updatePointerPreview(e.clientX, e.clientY);
|
|
@@ -750,6 +807,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
750
807
|
};
|
|
751
808
|
render();
|
|
752
809
|
refreshSelectionBounds(editor);
|
|
810
|
+
editor.endHistoryEntry();
|
|
753
811
|
return;
|
|
754
812
|
}
|
|
755
813
|
if (drag.mode === "resize") {
|
|
@@ -758,6 +816,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
758
816
|
resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
|
|
759
817
|
render();
|
|
760
818
|
refreshSelectionBounds(editor);
|
|
819
|
+
editor.endHistoryEntry();
|
|
761
820
|
return;
|
|
762
821
|
}
|
|
763
822
|
if (drag.mode === "move") {
|
|
@@ -765,6 +824,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
765
824
|
selectDragRef.current.mode = "none";
|
|
766
825
|
render();
|
|
767
826
|
refreshSelectionBounds(editor);
|
|
827
|
+
editor.endHistoryEntry();
|
|
768
828
|
return;
|
|
769
829
|
}
|
|
770
830
|
if (drag.mode === "marquee") {
|
|
@@ -796,6 +856,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
796
856
|
pendingRemoteDocumentRef.current = null;
|
|
797
857
|
applyRemoteDocumentSnapshot(pendingRemoteDocument);
|
|
798
858
|
}
|
|
859
|
+
editor.endHistoryEntry();
|
|
799
860
|
return;
|
|
800
861
|
}
|
|
801
862
|
}
|
|
@@ -807,8 +868,25 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
807
868
|
pendingRemoteDocumentRef.current = null;
|
|
808
869
|
applyRemoteDocumentSnapshot(pendingRemoteDocument);
|
|
809
870
|
}
|
|
871
|
+
editor.endHistoryEntry();
|
|
810
872
|
};
|
|
811
873
|
const handleKeyDown = (e) => {
|
|
874
|
+
const isMetaPressed = e.metaKey || e.ctrlKey;
|
|
875
|
+
const loweredKey = e.key.toLowerCase();
|
|
876
|
+
const isUndoOrRedoKey = loweredKey === "z" || loweredKey === "y";
|
|
877
|
+
if (isMetaPressed && isUndoOrRedoKey) {
|
|
878
|
+
const shouldRedo = loweredKey === "y" || loweredKey === "z" && e.shiftKey;
|
|
879
|
+
const changed = shouldRedo ? editor.redo() : editor.undo();
|
|
880
|
+
if (changed) {
|
|
881
|
+
e.preventDefault();
|
|
882
|
+
e.stopPropagation();
|
|
883
|
+
reconcileSelectionAfterDocumentLoad();
|
|
884
|
+
setSelectionRotationDeg(0);
|
|
885
|
+
render();
|
|
886
|
+
syncHistoryState();
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
812
890
|
editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
|
|
813
891
|
editor.tools.keyDown({ key: e.key });
|
|
814
892
|
render();
|
|
@@ -819,40 +897,56 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
819
897
|
render();
|
|
820
898
|
};
|
|
821
899
|
const initializePersistence = async () => {
|
|
822
|
-
if (!persistenceKey)
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const snapshot = {};
|
|
826
|
-
if (loaded.records.length > 0) {
|
|
827
|
-
snapshot.document = { records: loaded.records };
|
|
828
|
-
}
|
|
829
|
-
if (loaded.state) {
|
|
830
|
-
snapshot.state = loaded.state;
|
|
831
|
-
}
|
|
832
|
-
if (snapshot.document || snapshot.state) {
|
|
833
|
-
applyLoadedSnapshot(snapshot);
|
|
900
|
+
if (!persistenceKey) {
|
|
901
|
+
setIsPersistenceReady(true);
|
|
902
|
+
return;
|
|
834
903
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
if (
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
904
|
+
try {
|
|
905
|
+
persistenceDb = new TsdrawLocalIndexedDb(persistenceKey);
|
|
906
|
+
const loaded = await persistenceDb.load(sessionId);
|
|
907
|
+
const snapshot = {};
|
|
908
|
+
if (loaded.records.length > 0) {
|
|
909
|
+
snapshot.document = { records: loaded.records };
|
|
910
|
+
}
|
|
911
|
+
if (loaded.state) {
|
|
912
|
+
snapshot.state = loaded.state;
|
|
913
|
+
}
|
|
914
|
+
if (snapshot.document || snapshot.state) {
|
|
915
|
+
applyLoadedSnapshot(snapshot);
|
|
916
|
+
}
|
|
917
|
+
editor.loadHistorySnapshot(loaded.history);
|
|
918
|
+
syncHistoryState();
|
|
919
|
+
persistenceActive = true;
|
|
920
|
+
persistenceChannel = new BroadcastChannel(`tsdraw:persistence:${persistenceKey}`);
|
|
921
|
+
persistenceChannel.onmessage = async (event) => {
|
|
922
|
+
const data = event.data;
|
|
923
|
+
if (data?.type !== "tsdraw:persisted" || data.senderSessionId === sessionId) return;
|
|
924
|
+
if (!persistenceDb || disposed) return;
|
|
925
|
+
const nextLoaded = await persistenceDb.load(sessionId);
|
|
926
|
+
if (nextLoaded.records.length > 0) {
|
|
927
|
+
const nextDocument = { records: nextLoaded.records };
|
|
928
|
+
if (isPointerActiveRef.current) {
|
|
929
|
+
pendingRemoteDocumentRef.current = nextDocument;
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
applyRemoteDocumentSnapshot(nextDocument);
|
|
847
933
|
}
|
|
848
|
-
|
|
934
|
+
};
|
|
935
|
+
} finally {
|
|
936
|
+
if (!disposed) {
|
|
937
|
+
setIsPersistenceReady(true);
|
|
849
938
|
}
|
|
850
|
-
}
|
|
939
|
+
}
|
|
851
940
|
};
|
|
852
941
|
const cleanupEditorListener = editor.listen(() => {
|
|
853
942
|
if (ignorePersistenceChanges) return;
|
|
854
943
|
schedulePersist();
|
|
855
944
|
});
|
|
945
|
+
const cleanupHistoryListener = editor.listenHistory(() => {
|
|
946
|
+
syncHistoryState();
|
|
947
|
+
if (ignorePersistenceChanges) return;
|
|
948
|
+
schedulePersist();
|
|
949
|
+
});
|
|
856
950
|
resize();
|
|
857
951
|
const ro = new ResizeObserver(resize);
|
|
858
952
|
ro.observe(container);
|
|
@@ -875,6 +969,26 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
875
969
|
currentToolRef.current = tool;
|
|
876
970
|
},
|
|
877
971
|
getCurrentTool: () => editor.getCurrentToolId(),
|
|
972
|
+
undo: () => {
|
|
973
|
+
const changed = editor.undo();
|
|
974
|
+
if (!changed) return false;
|
|
975
|
+
reconcileSelectionAfterDocumentLoad();
|
|
976
|
+
setSelectionRotationDeg(0);
|
|
977
|
+
render();
|
|
978
|
+
syncHistoryState();
|
|
979
|
+
return true;
|
|
980
|
+
},
|
|
981
|
+
redo: () => {
|
|
982
|
+
const changed = editor.redo();
|
|
983
|
+
if (!changed) return false;
|
|
984
|
+
reconcileSelectionAfterDocumentLoad();
|
|
985
|
+
setSelectionRotationDeg(0);
|
|
986
|
+
render();
|
|
987
|
+
syncHistoryState();
|
|
988
|
+
return true;
|
|
989
|
+
},
|
|
990
|
+
canUndo: () => editor.canUndo(),
|
|
991
|
+
canRedo: () => editor.canRedo(),
|
|
878
992
|
applyDrawStyle: (partial) => {
|
|
879
993
|
editor.setCurrentDrawStyle(partial);
|
|
880
994
|
if (partial.color) setDrawColor(partial.color);
|
|
@@ -887,6 +1001,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
887
1001
|
disposed = true;
|
|
888
1002
|
schedulePersistRef.current = null;
|
|
889
1003
|
cleanupEditorListener();
|
|
1004
|
+
cleanupHistoryListener();
|
|
890
1005
|
disposeMount?.();
|
|
891
1006
|
ro.disconnect();
|
|
892
1007
|
canvas.removeEventListener("pointerdown", handlePointerDown);
|
|
@@ -940,6 +1055,38 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
940
1055
|
},
|
|
941
1056
|
[render]
|
|
942
1057
|
);
|
|
1058
|
+
const undo = react.useCallback(() => {
|
|
1059
|
+
const editor = editorRef.current;
|
|
1060
|
+
if (!editor) return false;
|
|
1061
|
+
const changed = editor.undo();
|
|
1062
|
+
if (!changed) return false;
|
|
1063
|
+
const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
|
|
1064
|
+
if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
|
|
1065
|
+
selectedShapeIdsRef.current = nextSelectedShapeIds;
|
|
1066
|
+
setSelectedShapeIds(nextSelectedShapeIds);
|
|
1067
|
+
}
|
|
1068
|
+
setSelectionRotationDeg(0);
|
|
1069
|
+
render();
|
|
1070
|
+
setCanUndo(editor.canUndo());
|
|
1071
|
+
setCanRedo(editor.canRedo());
|
|
1072
|
+
return true;
|
|
1073
|
+
}, [render]);
|
|
1074
|
+
const redo = react.useCallback(() => {
|
|
1075
|
+
const editor = editorRef.current;
|
|
1076
|
+
if (!editor) return false;
|
|
1077
|
+
const changed = editor.redo();
|
|
1078
|
+
if (!changed) return false;
|
|
1079
|
+
const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
|
|
1080
|
+
if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
|
|
1081
|
+
selectedShapeIdsRef.current = nextSelectedShapeIds;
|
|
1082
|
+
setSelectedShapeIds(nextSelectedShapeIds);
|
|
1083
|
+
}
|
|
1084
|
+
setSelectionRotationDeg(0);
|
|
1085
|
+
render();
|
|
1086
|
+
setCanUndo(editor.canUndo());
|
|
1087
|
+
setCanRedo(editor.canRedo());
|
|
1088
|
+
return true;
|
|
1089
|
+
}, [render]);
|
|
943
1090
|
const showToolOverlay = isPointerInsideCanvas && (currentTool === "pen" || currentTool === "eraser");
|
|
944
1091
|
const canvasCursor = getCanvasCursor(currentTool, {
|
|
945
1092
|
isMovingSelection,
|
|
@@ -978,14 +1125,19 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
978
1125
|
canvasCursor,
|
|
979
1126
|
cursorContext,
|
|
980
1127
|
toolOverlay,
|
|
1128
|
+
isPersistenceReady,
|
|
981
1129
|
showStylePanel: stylePanelToolIdsRef.current.includes(currentTool),
|
|
1130
|
+
canUndo,
|
|
1131
|
+
canRedo,
|
|
1132
|
+
undo,
|
|
1133
|
+
redo,
|
|
982
1134
|
setTool,
|
|
983
1135
|
applyDrawStyle,
|
|
984
1136
|
handleResizePointerDown,
|
|
985
1137
|
handleRotatePointerDown
|
|
986
1138
|
};
|
|
987
1139
|
}
|
|
988
|
-
var
|
|
1140
|
+
var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser"]];
|
|
989
1141
|
var DEFAULT_TOOL_LABELS = {
|
|
990
1142
|
select: "Select",
|
|
991
1143
|
pen: "Pen",
|
|
@@ -1002,11 +1154,8 @@ function parseAnchor(anchor) {
|
|
|
1002
1154
|
}
|
|
1003
1155
|
return { vertical, horizontal };
|
|
1004
1156
|
}
|
|
1005
|
-
function
|
|
1006
|
-
return
|
|
1007
|
-
}
|
|
1008
|
-
function getToolId(toolItem) {
|
|
1009
|
-
return typeof toolItem === "string" ? toolItem : toolItem.id;
|
|
1157
|
+
function isToolbarAction(item) {
|
|
1158
|
+
return item === "undo" || item === "redo";
|
|
1010
1159
|
}
|
|
1011
1160
|
function resolvePlacementStyle(placement, fallbackAnchor, fallbackOffsetX, fallbackOffsetY) {
|
|
1012
1161
|
const anchor = placement?.anchor ?? fallbackAnchor;
|
|
@@ -1041,40 +1190,55 @@ function Tsdraw(props) {
|
|
|
1041
1190
|
if (typeof window === "undefined") return "light";
|
|
1042
1191
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
1043
1192
|
});
|
|
1044
|
-
const
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
[
|
|
1048
|
-
);
|
|
1049
|
-
const toolDefinitions = react.useMemo(
|
|
1050
|
-
() => customTools.map((tool) => tool.definition),
|
|
1193
|
+
const customTools = props.customTools ?? [];
|
|
1194
|
+
const toolbarPartIds = props.uiOptions?.toolbar?.parts ?? DEFAULT_TOOLBAR_PARTS;
|
|
1195
|
+
const customToolMap = react.useMemo(
|
|
1196
|
+
() => new Map(customTools.map((customTool) => [customTool.id, customTool])),
|
|
1051
1197
|
[customTools]
|
|
1052
1198
|
);
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1199
|
+
const toolbarToolIds = react.useMemo(() => {
|
|
1200
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1201
|
+
for (const toolbarPart of toolbarPartIds) {
|
|
1202
|
+
for (const item of toolbarPart) {
|
|
1203
|
+
if (isToolbarAction(item)) continue;
|
|
1204
|
+
if (item in DEFAULT_TOOL_LABELS || customToolMap.has(item)) {
|
|
1205
|
+
ids.add(item);
|
|
1206
|
+
}
|
|
1061
1207
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
[toolItems]
|
|
1208
|
+
}
|
|
1209
|
+
return ids;
|
|
1210
|
+
}, [customToolMap, toolbarPartIds]);
|
|
1211
|
+
const toolDefinitions = react.useMemo(
|
|
1212
|
+
() => customTools.filter((customTool) => toolbarToolIds.has(customTool.id)).map((customTool) => customTool.definition),
|
|
1213
|
+
[customTools, toolbarToolIds]
|
|
1069
1214
|
);
|
|
1070
1215
|
const stylePanelToolIds = react.useMemo(
|
|
1071
|
-
() =>
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1216
|
+
() => {
|
|
1217
|
+
const nextToolIds = /* @__PURE__ */ new Set();
|
|
1218
|
+
if (toolbarToolIds.has("pen")) {
|
|
1219
|
+
nextToolIds.add("pen");
|
|
1220
|
+
}
|
|
1221
|
+
for (const customTool of customTools) {
|
|
1222
|
+
if ((customTool.showStylePanel ?? false) && toolbarToolIds.has(customTool.id)) {
|
|
1223
|
+
nextToolIds.add(customTool.id);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return [...nextToolIds];
|
|
1227
|
+
},
|
|
1228
|
+
[customTools, toolbarToolIds]
|
|
1076
1229
|
);
|
|
1077
|
-
const
|
|
1230
|
+
const firstToolbarTool = react.useMemo(() => {
|
|
1231
|
+
for (const toolbarPart of toolbarPartIds) {
|
|
1232
|
+
for (const item of toolbarPart) {
|
|
1233
|
+
if (isToolbarAction(item)) continue;
|
|
1234
|
+
if (item in DEFAULT_TOOL_LABELS || customToolMap.has(item)) {
|
|
1235
|
+
return item;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return void 0;
|
|
1240
|
+
}, [customToolMap, toolbarPartIds]);
|
|
1241
|
+
const initialTool = props.initialToolId ?? firstToolbarTool ?? "pen";
|
|
1078
1242
|
const requestedTheme = props.theme ?? "light";
|
|
1079
1243
|
react.useEffect(() => {
|
|
1080
1244
|
if (requestedTheme !== "system" || typeof window === "undefined") return;
|
|
@@ -1099,7 +1263,12 @@ function Tsdraw(props) {
|
|
|
1099
1263
|
canvasCursor: defaultCanvasCursor,
|
|
1100
1264
|
cursorContext,
|
|
1101
1265
|
toolOverlay,
|
|
1266
|
+
isPersistenceReady,
|
|
1102
1267
|
showStylePanel,
|
|
1268
|
+
canUndo,
|
|
1269
|
+
canRedo,
|
|
1270
|
+
undo,
|
|
1271
|
+
redo,
|
|
1103
1272
|
setTool,
|
|
1104
1273
|
applyDrawStyle,
|
|
1105
1274
|
handleResizePointerDown,
|
|
@@ -1129,6 +1298,51 @@ function Tsdraw(props) {
|
|
|
1129
1298
|
);
|
|
1130
1299
|
const overlayNode = props.uiOptions?.overlays?.renderToolOverlay?.({ defaultOverlay: defaultToolOverlay, overlayState: toolOverlay, currentTool }) ?? defaultToolOverlay;
|
|
1131
1300
|
const customElements = props.uiOptions?.customElements ?? [];
|
|
1301
|
+
const toolbarParts = react.useMemo(
|
|
1302
|
+
() => toolbarPartIds.map((toolbarPart, partIndex) => {
|
|
1303
|
+
const items = toolbarPart.map((item) => {
|
|
1304
|
+
if (item === "undo") {
|
|
1305
|
+
return {
|
|
1306
|
+
type: "action",
|
|
1307
|
+
id: "undo",
|
|
1308
|
+
label: "Undo",
|
|
1309
|
+
disabled: !canUndo,
|
|
1310
|
+
onSelect: undo
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
if (item === "redo") {
|
|
1314
|
+
return {
|
|
1315
|
+
type: "action",
|
|
1316
|
+
id: "redo",
|
|
1317
|
+
label: "Redo",
|
|
1318
|
+
disabled: !canRedo,
|
|
1319
|
+
onSelect: redo
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
if (item in DEFAULT_TOOL_LABELS) {
|
|
1323
|
+
return {
|
|
1324
|
+
type: "tool",
|
|
1325
|
+
id: item,
|
|
1326
|
+
label: DEFAULT_TOOL_LABELS[item],
|
|
1327
|
+
icon: (isActive) => getDefaultToolbarIcon(item, isActive)
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
const customTool = customToolMap.get(item);
|
|
1331
|
+
if (!customTool) return null;
|
|
1332
|
+
return {
|
|
1333
|
+
type: "tool",
|
|
1334
|
+
id: customTool.id,
|
|
1335
|
+
label: customTool.label,
|
|
1336
|
+
icon: (isActive) => isActive && customTool.iconSelected ? customTool.iconSelected : customTool.icon
|
|
1337
|
+
};
|
|
1338
|
+
}).filter((nextItem) => nextItem != null);
|
|
1339
|
+
return {
|
|
1340
|
+
id: `toolbar-part-${partIndex.toString(36)}`,
|
|
1341
|
+
items
|
|
1342
|
+
};
|
|
1343
|
+
}).filter((part) => part.items.length > 0),
|
|
1344
|
+
[canRedo, canUndo, customToolMap, redo, toolbarPartIds, undo]
|
|
1345
|
+
);
|
|
1132
1346
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1133
1347
|
"div",
|
|
1134
1348
|
{
|
|
@@ -1172,7 +1386,7 @@ function Tsdraw(props) {
|
|
|
1172
1386
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1173
1387
|
StylePanel,
|
|
1174
1388
|
{
|
|
1175
|
-
visible: showStylePanel,
|
|
1389
|
+
visible: isPersistenceReady && showStylePanel,
|
|
1176
1390
|
style: stylePanelPlacementStyle,
|
|
1177
1391
|
theme: resolvedTheme,
|
|
1178
1392
|
drawColor,
|
|
@@ -1199,10 +1413,11 @@ function Tsdraw(props) {
|
|
|
1199
1413
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1200
1414
|
Toolbar,
|
|
1201
1415
|
{
|
|
1202
|
-
|
|
1416
|
+
parts: toolbarParts,
|
|
1203
1417
|
style: toolbarPlacementStyle,
|
|
1204
|
-
currentTool,
|
|
1205
|
-
onToolChange: setTool
|
|
1418
|
+
currentTool: isPersistenceReady ? currentTool : null,
|
|
1419
|
+
onToolChange: setTool,
|
|
1420
|
+
disabled: !isPersistenceReady
|
|
1206
1421
|
}
|
|
1207
1422
|
)
|
|
1208
1423
|
]
|