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