@tsdraw/react 0.4.0 → 0.5.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 +250 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +250 -9
- package/dist/index.js.map +1 -1
- package/dist/tsdraw.css +20 -20
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -197,10 +197,10 @@ function ToolOverlay({
|
|
|
197
197
|
) });
|
|
198
198
|
}
|
|
199
199
|
function getDefaultToolbarIcon(toolId, isActive) {
|
|
200
|
-
if (toolId === "select") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPointer, { size:
|
|
201
|
-
if (toolId === "pen") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPencil, { size:
|
|
202
|
-
if (toolId === "eraser") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconEraser, { size:
|
|
203
|
-
if (toolId === "hand") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconHandStop, { size:
|
|
200
|
+
if (toolId === "select") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPointer, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
|
|
201
|
+
if (toolId === "pen") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPencil, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
|
|
202
|
+
if (toolId === "eraser") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconEraser, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
|
|
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
206
|
function Toolbar({ items, currentTool, onToolChange, style }) {
|
|
@@ -232,6 +232,103 @@ function getCanvasCursor(currentTool, state) {
|
|
|
232
232
|
return state.showToolOverlay ? "none" : "crosshair";
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// src/persistence/localIndexedDb.ts
|
|
236
|
+
var DATABASE_PREFIX = "tsdraw_v1_";
|
|
237
|
+
var DATABASE_VERSION = 1;
|
|
238
|
+
var STORE = {
|
|
239
|
+
records: "records",
|
|
240
|
+
state: "state"
|
|
241
|
+
};
|
|
242
|
+
function requestToPromise(request) {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
request.onsuccess = () => resolve(request.result);
|
|
245
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function transactionDone(transaction) {
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
transaction.oncomplete = () => resolve();
|
|
251
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
252
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function openLocalDatabase(persistenceKey) {
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const request = indexedDB.open(`${DATABASE_PREFIX}${persistenceKey}`, DATABASE_VERSION);
|
|
258
|
+
request.onupgradeneeded = () => {
|
|
259
|
+
const database = request.result;
|
|
260
|
+
if (!database.objectStoreNames.contains(STORE.records)) {
|
|
261
|
+
database.createObjectStore(STORE.records);
|
|
262
|
+
}
|
|
263
|
+
if (!database.objectStoreNames.contains(STORE.state)) {
|
|
264
|
+
database.createObjectStore(STORE.state);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
request.onsuccess = () => resolve(request.result);
|
|
268
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to open IndexedDB"));
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
var TsdrawLocalIndexedDb = class {
|
|
272
|
+
databasePromise;
|
|
273
|
+
constructor(persistenceKey) {
|
|
274
|
+
this.databasePromise = openLocalDatabase(persistenceKey);
|
|
275
|
+
}
|
|
276
|
+
async close() {
|
|
277
|
+
const database = await this.databasePromise;
|
|
278
|
+
database.close();
|
|
279
|
+
}
|
|
280
|
+
async load(sessionId) {
|
|
281
|
+
const database = await this.databasePromise;
|
|
282
|
+
const transaction = database.transaction([STORE.records, STORE.state], "readonly");
|
|
283
|
+
const recordStore = transaction.objectStore(STORE.records);
|
|
284
|
+
const stateStore = transaction.objectStore(STORE.state);
|
|
285
|
+
const records = await requestToPromise(recordStore.getAll());
|
|
286
|
+
let state = (await requestToPromise(stateStore.get(sessionId)))?.snapshot ?? null;
|
|
287
|
+
if (!state) {
|
|
288
|
+
const allStates = await requestToPromise(stateStore.getAll());
|
|
289
|
+
if (allStates.length > 0) {
|
|
290
|
+
allStates.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
291
|
+
state = allStates[allStates.length - 1]?.snapshot ?? null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
await transactionDone(transaction);
|
|
295
|
+
return { records, state };
|
|
296
|
+
}
|
|
297
|
+
async storeSnapshot(args) {
|
|
298
|
+
const database = await this.databasePromise;
|
|
299
|
+
const transaction = database.transaction([STORE.records, STORE.state], "readwrite");
|
|
300
|
+
const recordStore = transaction.objectStore(STORE.records);
|
|
301
|
+
const stateStore = transaction.objectStore(STORE.state);
|
|
302
|
+
recordStore.clear();
|
|
303
|
+
for (const record of args.records) {
|
|
304
|
+
recordStore.put(record, record.id);
|
|
305
|
+
}
|
|
306
|
+
const stateRow = {
|
|
307
|
+
id: args.sessionId,
|
|
308
|
+
snapshot: args.state,
|
|
309
|
+
updatedAt: Date.now()
|
|
310
|
+
};
|
|
311
|
+
stateStore.put(stateRow, args.sessionId);
|
|
312
|
+
await transactionDone(transaction);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// src/persistence/sessionId.ts
|
|
317
|
+
var SESSION_STORAGE_KEY = "TSDRAW_TAB_SESSION_ID_v1";
|
|
318
|
+
function createSessionId() {
|
|
319
|
+
const timestamp = Date.now().toString(36);
|
|
320
|
+
const randomPart = Math.random().toString(36).slice(2, 10);
|
|
321
|
+
return `tsdraw-session-${timestamp}-${randomPart}`;
|
|
322
|
+
}
|
|
323
|
+
function getOrCreateSessionId() {
|
|
324
|
+
if (typeof window === "undefined") return createSessionId();
|
|
325
|
+
const existing = window.sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
326
|
+
if (existing) return existing;
|
|
327
|
+
const newId = createSessionId();
|
|
328
|
+
window.sessionStorage.setItem(SESSION_STORAGE_KEY, newId);
|
|
329
|
+
return newId;
|
|
330
|
+
}
|
|
331
|
+
|
|
235
332
|
// src/canvas/useTsdrawCanvasController.ts
|
|
236
333
|
function toScreenRect(editor, bounds) {
|
|
237
334
|
const { x, y, zoom } = editor.viewport;
|
|
@@ -255,6 +352,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
255
352
|
const lastPointerClientRef = react.useRef(null);
|
|
256
353
|
const currentToolRef = react.useRef(options.initialTool ?? "pen");
|
|
257
354
|
const selectedShapeIdsRef = react.useRef([]);
|
|
355
|
+
const schedulePersistRef = react.useRef(null);
|
|
356
|
+
const isPointerActiveRef = react.useRef(false);
|
|
357
|
+
const pendingRemoteDocumentRef = react.useRef(null);
|
|
258
358
|
const selectionRotationRef = react.useRef(0);
|
|
259
359
|
const resizeRef = react.useRef({
|
|
260
360
|
handle: null,
|
|
@@ -300,6 +400,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
300
400
|
react.useEffect(() => {
|
|
301
401
|
selectionRotationRef.current = selectionRotationDeg;
|
|
302
402
|
}, [selectionRotationDeg]);
|
|
403
|
+
react.useEffect(() => {
|
|
404
|
+
schedulePersistRef.current?.();
|
|
405
|
+
}, [selectedShapeIds, currentTool, drawColor, drawDash, drawSize]);
|
|
303
406
|
const render = react.useCallback(() => {
|
|
304
407
|
const canvas = canvasRef.current;
|
|
305
408
|
const editor = editorRef.current;
|
|
@@ -411,6 +514,16 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
411
514
|
if (!editor.tools.hasTool(initialTool)) {
|
|
412
515
|
editor.setCurrentTool("pen");
|
|
413
516
|
}
|
|
517
|
+
let disposed = false;
|
|
518
|
+
let ignorePersistenceChanges = false;
|
|
519
|
+
let disposeMount;
|
|
520
|
+
let persistenceDb = null;
|
|
521
|
+
let persistenceChannel = null;
|
|
522
|
+
let isPersisting = false;
|
|
523
|
+
let needsAnotherPersist = false;
|
|
524
|
+
let persistenceActive = false;
|
|
525
|
+
const persistenceKey = options.persistenceKey;
|
|
526
|
+
const sessionId = getOrCreateSessionId();
|
|
414
527
|
const activeTool = editor.getCurrentToolId();
|
|
415
528
|
editorRef.current = editor;
|
|
416
529
|
setCurrentToolState(activeTool);
|
|
@@ -427,12 +540,81 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
427
540
|
canvas.height = Math.round(rect.height * dpr);
|
|
428
541
|
canvas.style.width = `${rect.width}px`;
|
|
429
542
|
canvas.style.height = `${rect.height}px`;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
543
|
+
if (!persistenceActive) {
|
|
544
|
+
editor.setViewport({ x: 0, y: 0, zoom: 1 });
|
|
545
|
+
}
|
|
433
546
|
render();
|
|
434
547
|
refreshSelectionBounds(editor);
|
|
435
548
|
};
|
|
549
|
+
const persistSnapshot = async () => {
|
|
550
|
+
if (!persistenceDb || !persistenceKey || ignorePersistenceChanges || disposed) return;
|
|
551
|
+
const snapshot = editor.getPersistenceSnapshot({
|
|
552
|
+
selectedShapeIds: selectedShapeIdsRef.current
|
|
553
|
+
});
|
|
554
|
+
await persistenceDb.storeSnapshot({
|
|
555
|
+
records: snapshot.document.records,
|
|
556
|
+
state: snapshot.state,
|
|
557
|
+
sessionId
|
|
558
|
+
});
|
|
559
|
+
persistenceChannel?.postMessage({
|
|
560
|
+
type: "tsdraw:persisted",
|
|
561
|
+
senderSessionId: sessionId
|
|
562
|
+
});
|
|
563
|
+
};
|
|
564
|
+
const schedulePersist = () => {
|
|
565
|
+
if (!persistenceDb || !persistenceKey || disposed) return;
|
|
566
|
+
const runPersist = async () => {
|
|
567
|
+
if (isPersisting) {
|
|
568
|
+
needsAnotherPersist = true;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
isPersisting = true;
|
|
572
|
+
try {
|
|
573
|
+
do {
|
|
574
|
+
needsAnotherPersist = false;
|
|
575
|
+
await persistSnapshot();
|
|
576
|
+
} while (needsAnotherPersist && !disposed);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
console.error("tsdraw persistence failed", error);
|
|
579
|
+
} finally {
|
|
580
|
+
isPersisting = false;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
void runPersist();
|
|
584
|
+
};
|
|
585
|
+
schedulePersistRef.current = schedulePersist;
|
|
586
|
+
const reconcileSelectionAfterDocumentLoad = () => {
|
|
587
|
+
const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
|
|
588
|
+
if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
|
|
589
|
+
selectedShapeIdsRef.current = nextSelectedShapeIds;
|
|
590
|
+
setSelectedShapeIds(nextSelectedShapeIds);
|
|
591
|
+
}
|
|
592
|
+
refreshSelectionBounds(editor, nextSelectedShapeIds);
|
|
593
|
+
};
|
|
594
|
+
const applyRemoteDocumentSnapshot = (document) => {
|
|
595
|
+
ignorePersistenceChanges = true;
|
|
596
|
+
editor.loadDocumentSnapshot(document);
|
|
597
|
+
reconcileSelectionAfterDocumentLoad();
|
|
598
|
+
render();
|
|
599
|
+
ignorePersistenceChanges = false;
|
|
600
|
+
};
|
|
601
|
+
const applyLoadedSnapshot = (snapshot) => {
|
|
602
|
+
ignorePersistenceChanges = true;
|
|
603
|
+
const nextSelectionIds = editor.loadPersistenceSnapshot(snapshot);
|
|
604
|
+
setSelectedShapeIds(nextSelectionIds);
|
|
605
|
+
selectedShapeIdsRef.current = nextSelectionIds;
|
|
606
|
+
const nextTool = editor.getCurrentToolId();
|
|
607
|
+
currentToolRef.current = nextTool;
|
|
608
|
+
setCurrentToolState(nextTool);
|
|
609
|
+
const nextDrawStyle = editor.getCurrentDrawStyle();
|
|
610
|
+
setDrawColor(nextDrawStyle.color);
|
|
611
|
+
setDrawDash(nextDrawStyle.dash);
|
|
612
|
+
setDrawSize(nextDrawStyle.size);
|
|
613
|
+
setSelectionRotationDeg(0);
|
|
614
|
+
render();
|
|
615
|
+
refreshSelectionBounds(editor, nextSelectionIds);
|
|
616
|
+
ignorePersistenceChanges = false;
|
|
617
|
+
};
|
|
436
618
|
const getPagePoint = (e) => {
|
|
437
619
|
const rect = canvas.getBoundingClientRect();
|
|
438
620
|
return editor.screenToPage(e.clientX - rect.left, e.clientY - rect.top);
|
|
@@ -443,6 +625,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
443
625
|
};
|
|
444
626
|
const handlePointerDown = (e) => {
|
|
445
627
|
if (!canvas.contains(e.target)) return;
|
|
628
|
+
isPointerActiveRef.current = true;
|
|
446
629
|
canvas.setPointerCapture(e.pointerId);
|
|
447
630
|
lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
|
|
448
631
|
updatePointerPreview(e.clientX, e.clientY);
|
|
@@ -547,6 +730,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
547
730
|
refreshSelectionBounds(editor);
|
|
548
731
|
};
|
|
549
732
|
const handlePointerUp = (e) => {
|
|
733
|
+
isPointerActiveRef.current = false;
|
|
550
734
|
lastPointerClientRef.current = null;
|
|
551
735
|
updatePointerPreview(e.clientX, e.clientY);
|
|
552
736
|
const { x, y } = getPagePoint(e);
|
|
@@ -607,12 +791,22 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
607
791
|
selectDragRef.current.mode = "none";
|
|
608
792
|
render();
|
|
609
793
|
refreshSelectionBounds(editor, ids);
|
|
794
|
+
if (pendingRemoteDocumentRef.current) {
|
|
795
|
+
const pendingRemoteDocument = pendingRemoteDocumentRef.current;
|
|
796
|
+
pendingRemoteDocumentRef.current = null;
|
|
797
|
+
applyRemoteDocumentSnapshot(pendingRemoteDocument);
|
|
798
|
+
}
|
|
610
799
|
return;
|
|
611
800
|
}
|
|
612
801
|
}
|
|
613
802
|
editor.tools.pointerUp();
|
|
614
803
|
render();
|
|
615
804
|
refreshSelectionBounds(editor);
|
|
805
|
+
if (pendingRemoteDocumentRef.current) {
|
|
806
|
+
const pendingRemoteDocument = pendingRemoteDocumentRef.current;
|
|
807
|
+
pendingRemoteDocumentRef.current = null;
|
|
808
|
+
applyRemoteDocumentSnapshot(pendingRemoteDocument);
|
|
809
|
+
}
|
|
616
810
|
};
|
|
617
811
|
const handleKeyDown = (e) => {
|
|
618
812
|
editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
|
|
@@ -624,6 +818,41 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
624
818
|
editor.tools.keyUp({ key: e.key });
|
|
625
819
|
render();
|
|
626
820
|
};
|
|
821
|
+
const initializePersistence = async () => {
|
|
822
|
+
if (!persistenceKey) return;
|
|
823
|
+
persistenceDb = new TsdrawLocalIndexedDb(persistenceKey);
|
|
824
|
+
const loaded = await persistenceDb.load(sessionId);
|
|
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);
|
|
834
|
+
}
|
|
835
|
+
persistenceActive = true;
|
|
836
|
+
persistenceChannel = new BroadcastChannel(`tsdraw:persistence:${persistenceKey}`);
|
|
837
|
+
persistenceChannel.onmessage = async (event) => {
|
|
838
|
+
const data = event.data;
|
|
839
|
+
if (data?.type !== "tsdraw:persisted" || data.senderSessionId === sessionId) return;
|
|
840
|
+
if (!persistenceDb || disposed) return;
|
|
841
|
+
const nextLoaded = await persistenceDb.load(sessionId);
|
|
842
|
+
if (nextLoaded.records.length > 0) {
|
|
843
|
+
const nextDocument = { records: nextLoaded.records };
|
|
844
|
+
if (isPointerActiveRef.current) {
|
|
845
|
+
pendingRemoteDocumentRef.current = nextDocument;
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
applyRemoteDocumentSnapshot(nextDocument);
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
const cleanupEditorListener = editor.listen(() => {
|
|
853
|
+
if (ignorePersistenceChanges) return;
|
|
854
|
+
schedulePersist();
|
|
855
|
+
});
|
|
627
856
|
resize();
|
|
628
857
|
const ro = new ResizeObserver(resize);
|
|
629
858
|
ro.observe(container);
|
|
@@ -632,7 +861,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
632
861
|
window.addEventListener("pointerup", handlePointerUp);
|
|
633
862
|
window.addEventListener("keydown", handleKeyDown);
|
|
634
863
|
window.addEventListener("keyup", handleKeyUp);
|
|
635
|
-
|
|
864
|
+
void initializePersistence().catch((error) => {
|
|
865
|
+
console.error("failed to initialize tsdraw persistence", error);
|
|
866
|
+
});
|
|
867
|
+
disposeMount = options.onMount?.({
|
|
636
868
|
editor,
|
|
637
869
|
container,
|
|
638
870
|
canvas,
|
|
@@ -652,6 +884,9 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
652
884
|
}
|
|
653
885
|
});
|
|
654
886
|
return () => {
|
|
887
|
+
disposed = true;
|
|
888
|
+
schedulePersistRef.current = null;
|
|
889
|
+
cleanupEditorListener();
|
|
655
890
|
disposeMount?.();
|
|
656
891
|
ro.disconnect();
|
|
657
892
|
canvas.removeEventListener("pointerdown", handlePointerDown);
|
|
@@ -659,12 +894,17 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
659
894
|
window.removeEventListener("pointerup", handlePointerUp);
|
|
660
895
|
window.removeEventListener("keydown", handleKeyDown);
|
|
661
896
|
window.removeEventListener("keyup", handleKeyUp);
|
|
897
|
+
isPointerActiveRef.current = false;
|
|
898
|
+
pendingRemoteDocumentRef.current = null;
|
|
899
|
+
persistenceChannel?.close();
|
|
900
|
+
void persistenceDb?.close();
|
|
662
901
|
editorRef.current = null;
|
|
663
902
|
};
|
|
664
903
|
}, [
|
|
665
904
|
getPagePointFromClient,
|
|
666
905
|
options.initialTool,
|
|
667
906
|
options.onMount,
|
|
907
|
+
options.persistenceKey,
|
|
668
908
|
options.toolDefinitions,
|
|
669
909
|
refreshSelectionBounds,
|
|
670
910
|
render,
|
|
@@ -868,10 +1108,11 @@ function Tsdraw(props) {
|
|
|
868
1108
|
toolDefinitions,
|
|
869
1109
|
initialTool,
|
|
870
1110
|
theme: resolvedTheme,
|
|
1111
|
+
persistenceKey: props.persistenceKey,
|
|
871
1112
|
stylePanelToolIds,
|
|
872
1113
|
onMount: props.onMount
|
|
873
1114
|
});
|
|
874
|
-
const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0,
|
|
1115
|
+
const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 14);
|
|
875
1116
|
const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8, 8);
|
|
876
1117
|
const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
|
|
877
1118
|
const defaultToolOverlay = /* @__PURE__ */ jsxRuntime.jsx(
|