@tsdraw/react 0.5.1 → 0.6.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/index.cjs +300 -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 +301 -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,6 +607,7 @@ 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
|
});
|
|
559
613
|
persistenceChannel?.postMessage({
|
|
@@ -594,6 +648,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
594
648
|
const applyRemoteDocumentSnapshot = (document) => {
|
|
595
649
|
ignorePersistenceChanges = true;
|
|
596
650
|
editor.loadDocumentSnapshot(document);
|
|
651
|
+
editor.clearRedoHistory();
|
|
597
652
|
reconcileSelectionAfterDocumentLoad();
|
|
598
653
|
render();
|
|
599
654
|
ignorePersistenceChanges = false;
|
|
@@ -626,6 +681,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
626
681
|
const handlePointerDown = (e) => {
|
|
627
682
|
if (!canvas.contains(e.target)) return;
|
|
628
683
|
isPointerActiveRef.current = true;
|
|
684
|
+
editor.beginHistoryEntry();
|
|
629
685
|
canvas.setPointerCapture(e.pointerId);
|
|
630
686
|
lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
|
|
631
687
|
updatePointerPreview(e.clientX, e.clientY);
|
|
@@ -750,6 +806,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
750
806
|
};
|
|
751
807
|
render();
|
|
752
808
|
refreshSelectionBounds(editor);
|
|
809
|
+
editor.endHistoryEntry();
|
|
753
810
|
return;
|
|
754
811
|
}
|
|
755
812
|
if (drag.mode === "resize") {
|
|
@@ -758,6 +815,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
758
815
|
resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
|
|
759
816
|
render();
|
|
760
817
|
refreshSelectionBounds(editor);
|
|
818
|
+
editor.endHistoryEntry();
|
|
761
819
|
return;
|
|
762
820
|
}
|
|
763
821
|
if (drag.mode === "move") {
|
|
@@ -765,6 +823,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
765
823
|
selectDragRef.current.mode = "none";
|
|
766
824
|
render();
|
|
767
825
|
refreshSelectionBounds(editor);
|
|
826
|
+
editor.endHistoryEntry();
|
|
768
827
|
return;
|
|
769
828
|
}
|
|
770
829
|
if (drag.mode === "marquee") {
|
|
@@ -796,6 +855,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
796
855
|
pendingRemoteDocumentRef.current = null;
|
|
797
856
|
applyRemoteDocumentSnapshot(pendingRemoteDocument);
|
|
798
857
|
}
|
|
858
|
+
editor.endHistoryEntry();
|
|
799
859
|
return;
|
|
800
860
|
}
|
|
801
861
|
}
|
|
@@ -807,8 +867,25 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
807
867
|
pendingRemoteDocumentRef.current = null;
|
|
808
868
|
applyRemoteDocumentSnapshot(pendingRemoteDocument);
|
|
809
869
|
}
|
|
870
|
+
editor.endHistoryEntry();
|
|
810
871
|
};
|
|
811
872
|
const handleKeyDown = (e) => {
|
|
873
|
+
const isMetaPressed = e.metaKey || e.ctrlKey;
|
|
874
|
+
const loweredKey = e.key.toLowerCase();
|
|
875
|
+
const isUndoOrRedoKey = loweredKey === "z" || loweredKey === "y";
|
|
876
|
+
if (isMetaPressed && isUndoOrRedoKey) {
|
|
877
|
+
const shouldRedo = loweredKey === "y" || loweredKey === "z" && e.shiftKey;
|
|
878
|
+
const changed = shouldRedo ? editor.redo() : editor.undo();
|
|
879
|
+
if (changed) {
|
|
880
|
+
e.preventDefault();
|
|
881
|
+
e.stopPropagation();
|
|
882
|
+
reconcileSelectionAfterDocumentLoad();
|
|
883
|
+
setSelectionRotationDeg(0);
|
|
884
|
+
render();
|
|
885
|
+
syncHistoryState();
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
812
889
|
editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
|
|
813
890
|
editor.tools.keyDown({ key: e.key });
|
|
814
891
|
render();
|
|
@@ -819,40 +896,56 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
819
896
|
render();
|
|
820
897
|
};
|
|
821
898
|
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);
|
|
899
|
+
if (!persistenceKey) {
|
|
900
|
+
setIsPersistenceReady(true);
|
|
901
|
+
return;
|
|
834
902
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
if (
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
903
|
+
try {
|
|
904
|
+
persistenceDb = new TsdrawLocalIndexedDb(persistenceKey);
|
|
905
|
+
const loaded = await persistenceDb.load(sessionId);
|
|
906
|
+
const snapshot = {};
|
|
907
|
+
if (loaded.records.length > 0) {
|
|
908
|
+
snapshot.document = { records: loaded.records };
|
|
909
|
+
}
|
|
910
|
+
if (loaded.state) {
|
|
911
|
+
snapshot.state = loaded.state;
|
|
912
|
+
}
|
|
913
|
+
if (snapshot.document || snapshot.state) {
|
|
914
|
+
applyLoadedSnapshot(snapshot);
|
|
915
|
+
}
|
|
916
|
+
editor.loadHistorySnapshot(loaded.history);
|
|
917
|
+
syncHistoryState();
|
|
918
|
+
persistenceActive = true;
|
|
919
|
+
persistenceChannel = new BroadcastChannel(`tsdraw:persistence:${persistenceKey}`);
|
|
920
|
+
persistenceChannel.onmessage = async (event) => {
|
|
921
|
+
const data = event.data;
|
|
922
|
+
if (data?.type !== "tsdraw:persisted" || data.senderSessionId === sessionId) return;
|
|
923
|
+
if (!persistenceDb || disposed) return;
|
|
924
|
+
const nextLoaded = await persistenceDb.load(sessionId);
|
|
925
|
+
if (nextLoaded.records.length > 0) {
|
|
926
|
+
const nextDocument = { records: nextLoaded.records };
|
|
927
|
+
if (isPointerActiveRef.current) {
|
|
928
|
+
pendingRemoteDocumentRef.current = nextDocument;
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
applyRemoteDocumentSnapshot(nextDocument);
|
|
847
932
|
}
|
|
848
|
-
|
|
933
|
+
};
|
|
934
|
+
} finally {
|
|
935
|
+
if (!disposed) {
|
|
936
|
+
setIsPersistenceReady(true);
|
|
849
937
|
}
|
|
850
|
-
}
|
|
938
|
+
}
|
|
851
939
|
};
|
|
852
940
|
const cleanupEditorListener = editor.listen(() => {
|
|
853
941
|
if (ignorePersistenceChanges) return;
|
|
854
942
|
schedulePersist();
|
|
855
943
|
});
|
|
944
|
+
const cleanupHistoryListener = editor.listenHistory(() => {
|
|
945
|
+
syncHistoryState();
|
|
946
|
+
if (ignorePersistenceChanges) return;
|
|
947
|
+
schedulePersist();
|
|
948
|
+
});
|
|
856
949
|
resize();
|
|
857
950
|
const ro = new ResizeObserver(resize);
|
|
858
951
|
ro.observe(container);
|
|
@@ -875,6 +968,26 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
875
968
|
currentToolRef.current = tool;
|
|
876
969
|
},
|
|
877
970
|
getCurrentTool: () => editor.getCurrentToolId(),
|
|
971
|
+
undo: () => {
|
|
972
|
+
const changed = editor.undo();
|
|
973
|
+
if (!changed) return false;
|
|
974
|
+
reconcileSelectionAfterDocumentLoad();
|
|
975
|
+
setSelectionRotationDeg(0);
|
|
976
|
+
render();
|
|
977
|
+
syncHistoryState();
|
|
978
|
+
return true;
|
|
979
|
+
},
|
|
980
|
+
redo: () => {
|
|
981
|
+
const changed = editor.redo();
|
|
982
|
+
if (!changed) return false;
|
|
983
|
+
reconcileSelectionAfterDocumentLoad();
|
|
984
|
+
setSelectionRotationDeg(0);
|
|
985
|
+
render();
|
|
986
|
+
syncHistoryState();
|
|
987
|
+
return true;
|
|
988
|
+
},
|
|
989
|
+
canUndo: () => editor.canUndo(),
|
|
990
|
+
canRedo: () => editor.canRedo(),
|
|
878
991
|
applyDrawStyle: (partial) => {
|
|
879
992
|
editor.setCurrentDrawStyle(partial);
|
|
880
993
|
if (partial.color) setDrawColor(partial.color);
|
|
@@ -887,6 +1000,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
887
1000
|
disposed = true;
|
|
888
1001
|
schedulePersistRef.current = null;
|
|
889
1002
|
cleanupEditorListener();
|
|
1003
|
+
cleanupHistoryListener();
|
|
890
1004
|
disposeMount?.();
|
|
891
1005
|
ro.disconnect();
|
|
892
1006
|
canvas.removeEventListener("pointerdown", handlePointerDown);
|
|
@@ -940,6 +1054,38 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
940
1054
|
},
|
|
941
1055
|
[render]
|
|
942
1056
|
);
|
|
1057
|
+
const undo = react.useCallback(() => {
|
|
1058
|
+
const editor = editorRef.current;
|
|
1059
|
+
if (!editor) return false;
|
|
1060
|
+
const changed = editor.undo();
|
|
1061
|
+
if (!changed) return false;
|
|
1062
|
+
const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
|
|
1063
|
+
if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
|
|
1064
|
+
selectedShapeIdsRef.current = nextSelectedShapeIds;
|
|
1065
|
+
setSelectedShapeIds(nextSelectedShapeIds);
|
|
1066
|
+
}
|
|
1067
|
+
setSelectionRotationDeg(0);
|
|
1068
|
+
render();
|
|
1069
|
+
setCanUndo(editor.canUndo());
|
|
1070
|
+
setCanRedo(editor.canRedo());
|
|
1071
|
+
return true;
|
|
1072
|
+
}, [render]);
|
|
1073
|
+
const redo = react.useCallback(() => {
|
|
1074
|
+
const editor = editorRef.current;
|
|
1075
|
+
if (!editor) return false;
|
|
1076
|
+
const changed = editor.redo();
|
|
1077
|
+
if (!changed) return false;
|
|
1078
|
+
const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
|
|
1079
|
+
if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
|
|
1080
|
+
selectedShapeIdsRef.current = nextSelectedShapeIds;
|
|
1081
|
+
setSelectedShapeIds(nextSelectedShapeIds);
|
|
1082
|
+
}
|
|
1083
|
+
setSelectionRotationDeg(0);
|
|
1084
|
+
render();
|
|
1085
|
+
setCanUndo(editor.canUndo());
|
|
1086
|
+
setCanRedo(editor.canRedo());
|
|
1087
|
+
return true;
|
|
1088
|
+
}, [render]);
|
|
943
1089
|
const showToolOverlay = isPointerInsideCanvas && (currentTool === "pen" || currentTool === "eraser");
|
|
944
1090
|
const canvasCursor = getCanvasCursor(currentTool, {
|
|
945
1091
|
isMovingSelection,
|
|
@@ -978,14 +1124,19 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
978
1124
|
canvasCursor,
|
|
979
1125
|
cursorContext,
|
|
980
1126
|
toolOverlay,
|
|
1127
|
+
isPersistenceReady,
|
|
981
1128
|
showStylePanel: stylePanelToolIdsRef.current.includes(currentTool),
|
|
1129
|
+
canUndo,
|
|
1130
|
+
canRedo,
|
|
1131
|
+
undo,
|
|
1132
|
+
redo,
|
|
982
1133
|
setTool,
|
|
983
1134
|
applyDrawStyle,
|
|
984
1135
|
handleResizePointerDown,
|
|
985
1136
|
handleRotatePointerDown
|
|
986
1137
|
};
|
|
987
1138
|
}
|
|
988
|
-
var
|
|
1139
|
+
var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser"]];
|
|
989
1140
|
var DEFAULT_TOOL_LABELS = {
|
|
990
1141
|
select: "Select",
|
|
991
1142
|
pen: "Pen",
|
|
@@ -1002,11 +1153,8 @@ function parseAnchor(anchor) {
|
|
|
1002
1153
|
}
|
|
1003
1154
|
return { vertical, horizontal };
|
|
1004
1155
|
}
|
|
1005
|
-
function
|
|
1006
|
-
return
|
|
1007
|
-
}
|
|
1008
|
-
function getToolId(toolItem) {
|
|
1009
|
-
return typeof toolItem === "string" ? toolItem : toolItem.id;
|
|
1156
|
+
function isToolbarAction(item) {
|
|
1157
|
+
return item === "undo" || item === "redo";
|
|
1010
1158
|
}
|
|
1011
1159
|
function resolvePlacementStyle(placement, fallbackAnchor, fallbackOffsetX, fallbackOffsetY) {
|
|
1012
1160
|
const anchor = placement?.anchor ?? fallbackAnchor;
|
|
@@ -1041,40 +1189,55 @@ function Tsdraw(props) {
|
|
|
1041
1189
|
if (typeof window === "undefined") return "light";
|
|
1042
1190
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
1043
1191
|
});
|
|
1044
|
-
const
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
[
|
|
1048
|
-
);
|
|
1049
|
-
const toolDefinitions = react.useMemo(
|
|
1050
|
-
() => customTools.map((tool) => tool.definition),
|
|
1192
|
+
const customTools = props.customTools ?? [];
|
|
1193
|
+
const toolbarPartIds = props.uiOptions?.toolbar?.parts ?? DEFAULT_TOOLBAR_PARTS;
|
|
1194
|
+
const customToolMap = react.useMemo(
|
|
1195
|
+
() => new Map(customTools.map((customTool) => [customTool.id, customTool])),
|
|
1051
1196
|
[customTools]
|
|
1052
1197
|
);
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1198
|
+
const toolbarToolIds = react.useMemo(() => {
|
|
1199
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1200
|
+
for (const toolbarPart of toolbarPartIds) {
|
|
1201
|
+
for (const item of toolbarPart) {
|
|
1202
|
+
if (isToolbarAction(item)) continue;
|
|
1203
|
+
if (item in DEFAULT_TOOL_LABELS || customToolMap.has(item)) {
|
|
1204
|
+
ids.add(item);
|
|
1205
|
+
}
|
|
1061
1206
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
[toolItems]
|
|
1207
|
+
}
|
|
1208
|
+
return ids;
|
|
1209
|
+
}, [customToolMap, toolbarPartIds]);
|
|
1210
|
+
const toolDefinitions = react.useMemo(
|
|
1211
|
+
() => customTools.filter((customTool) => toolbarToolIds.has(customTool.id)).map((customTool) => customTool.definition),
|
|
1212
|
+
[customTools, toolbarToolIds]
|
|
1069
1213
|
);
|
|
1070
1214
|
const stylePanelToolIds = react.useMemo(
|
|
1071
|
-
() =>
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1215
|
+
() => {
|
|
1216
|
+
const nextToolIds = /* @__PURE__ */ new Set();
|
|
1217
|
+
if (toolbarToolIds.has("pen")) {
|
|
1218
|
+
nextToolIds.add("pen");
|
|
1219
|
+
}
|
|
1220
|
+
for (const customTool of customTools) {
|
|
1221
|
+
if ((customTool.showStylePanel ?? false) && toolbarToolIds.has(customTool.id)) {
|
|
1222
|
+
nextToolIds.add(customTool.id);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return [...nextToolIds];
|
|
1226
|
+
},
|
|
1227
|
+
[customTools, toolbarToolIds]
|
|
1076
1228
|
);
|
|
1077
|
-
const
|
|
1229
|
+
const firstToolbarTool = react.useMemo(() => {
|
|
1230
|
+
for (const toolbarPart of toolbarPartIds) {
|
|
1231
|
+
for (const item of toolbarPart) {
|
|
1232
|
+
if (isToolbarAction(item)) continue;
|
|
1233
|
+
if (item in DEFAULT_TOOL_LABELS || customToolMap.has(item)) {
|
|
1234
|
+
return item;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return void 0;
|
|
1239
|
+
}, [customToolMap, toolbarPartIds]);
|
|
1240
|
+
const initialTool = props.initialToolId ?? firstToolbarTool ?? "pen";
|
|
1078
1241
|
const requestedTheme = props.theme ?? "light";
|
|
1079
1242
|
react.useEffect(() => {
|
|
1080
1243
|
if (requestedTheme !== "system" || typeof window === "undefined") return;
|
|
@@ -1099,7 +1262,12 @@ function Tsdraw(props) {
|
|
|
1099
1262
|
canvasCursor: defaultCanvasCursor,
|
|
1100
1263
|
cursorContext,
|
|
1101
1264
|
toolOverlay,
|
|
1265
|
+
isPersistenceReady,
|
|
1102
1266
|
showStylePanel,
|
|
1267
|
+
canUndo,
|
|
1268
|
+
canRedo,
|
|
1269
|
+
undo,
|
|
1270
|
+
redo,
|
|
1103
1271
|
setTool,
|
|
1104
1272
|
applyDrawStyle,
|
|
1105
1273
|
handleResizePointerDown,
|
|
@@ -1129,6 +1297,51 @@ function Tsdraw(props) {
|
|
|
1129
1297
|
);
|
|
1130
1298
|
const overlayNode = props.uiOptions?.overlays?.renderToolOverlay?.({ defaultOverlay: defaultToolOverlay, overlayState: toolOverlay, currentTool }) ?? defaultToolOverlay;
|
|
1131
1299
|
const customElements = props.uiOptions?.customElements ?? [];
|
|
1300
|
+
const toolbarParts = react.useMemo(
|
|
1301
|
+
() => toolbarPartIds.map((toolbarPart, partIndex) => {
|
|
1302
|
+
const items = toolbarPart.map((item) => {
|
|
1303
|
+
if (item === "undo") {
|
|
1304
|
+
return {
|
|
1305
|
+
type: "action",
|
|
1306
|
+
id: "undo",
|
|
1307
|
+
label: "Undo",
|
|
1308
|
+
disabled: !canUndo,
|
|
1309
|
+
onSelect: undo
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
if (item === "redo") {
|
|
1313
|
+
return {
|
|
1314
|
+
type: "action",
|
|
1315
|
+
id: "redo",
|
|
1316
|
+
label: "Redo",
|
|
1317
|
+
disabled: !canRedo,
|
|
1318
|
+
onSelect: redo
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
if (item in DEFAULT_TOOL_LABELS) {
|
|
1322
|
+
return {
|
|
1323
|
+
type: "tool",
|
|
1324
|
+
id: item,
|
|
1325
|
+
label: DEFAULT_TOOL_LABELS[item],
|
|
1326
|
+
icon: (isActive) => getDefaultToolbarIcon(item, isActive)
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
const customTool = customToolMap.get(item);
|
|
1330
|
+
if (!customTool) return null;
|
|
1331
|
+
return {
|
|
1332
|
+
type: "tool",
|
|
1333
|
+
id: customTool.id,
|
|
1334
|
+
label: customTool.label,
|
|
1335
|
+
icon: (isActive) => isActive && customTool.iconSelected ? customTool.iconSelected : customTool.icon
|
|
1336
|
+
};
|
|
1337
|
+
}).filter((nextItem) => nextItem != null);
|
|
1338
|
+
return {
|
|
1339
|
+
id: `toolbar-part-${partIndex.toString(36)}`,
|
|
1340
|
+
items
|
|
1341
|
+
};
|
|
1342
|
+
}).filter((part) => part.items.length > 0),
|
|
1343
|
+
[canRedo, canUndo, customToolMap, redo, toolbarPartIds, undo]
|
|
1344
|
+
);
|
|
1132
1345
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1133
1346
|
"div",
|
|
1134
1347
|
{
|
|
@@ -1172,7 +1385,7 @@ function Tsdraw(props) {
|
|
|
1172
1385
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1173
1386
|
StylePanel,
|
|
1174
1387
|
{
|
|
1175
|
-
visible: showStylePanel,
|
|
1388
|
+
visible: isPersistenceReady && showStylePanel,
|
|
1176
1389
|
style: stylePanelPlacementStyle,
|
|
1177
1390
|
theme: resolvedTheme,
|
|
1178
1391
|
drawColor,
|
|
@@ -1199,10 +1412,11 @@ function Tsdraw(props) {
|
|
|
1199
1412
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1200
1413
|
Toolbar,
|
|
1201
1414
|
{
|
|
1202
|
-
|
|
1415
|
+
parts: toolbarParts,
|
|
1203
1416
|
style: toolbarPlacementStyle,
|
|
1204
|
-
currentTool,
|
|
1205
|
-
onToolChange: setTool
|
|
1417
|
+
currentTool: isPersistenceReady ? currentTool : null,
|
|
1418
|
+
onToolChange: setTool,
|
|
1419
|
+
disabled: !isPersistenceReady
|
|
1206
1420
|
}
|
|
1207
1421
|
)
|
|
1208
1422
|
]
|