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