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