@tsdraw/react 0.6.0 → 0.6.2

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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
1
+ import { useState, useMemo, useEffect, useCallback, useRef } 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
4
  import { IconPointer, IconPencil, IconEraser, IconHandStop, IconArrowBackUp, IconArrowForwardUp } from '@tabler/icons-react';
@@ -363,11 +363,15 @@ function createSessionId() {
363
363
  }
364
364
  function getOrCreateSessionId() {
365
365
  if (typeof window === "undefined") return createSessionId();
366
- const existing = window.sessionStorage.getItem(SESSION_STORAGE_KEY);
367
- if (existing) return existing;
368
- const newId = createSessionId();
369
- window.sessionStorage.setItem(SESSION_STORAGE_KEY, newId);
370
- return newId;
366
+ try {
367
+ const existing = window.sessionStorage.getItem(SESSION_STORAGE_KEY);
368
+ if (existing) return existing;
369
+ const newId = createSessionId();
370
+ window.sessionStorage.setItem(SESSION_STORAGE_KEY, newId);
371
+ return newId;
372
+ } catch {
373
+ return createSessionId();
374
+ }
371
375
  }
372
376
 
373
377
  // src/canvas/useTsdrawCanvasController.ts
@@ -386,6 +390,7 @@ function resolveDrawColor(colorStyle, theme) {
386
390
  function useTsdrawCanvasController(options = {}) {
387
391
  const stylePanelToolIds = options.stylePanelToolIds ?? ["pen"];
388
392
  const stylePanelToolIdsRef = useRef(stylePanelToolIds);
393
+ const onMountRef = useRef(options.onMount);
389
394
  const containerRef = useRef(null);
390
395
  const canvasRef = useRef(null);
391
396
  const editorRef = useRef(null);
@@ -438,6 +443,9 @@ function useTsdrawCanvasController(options = {}) {
438
443
  useEffect(() => {
439
444
  stylePanelToolIdsRef.current = stylePanelToolIds;
440
445
  }, [stylePanelToolIds]);
446
+ useEffect(() => {
447
+ onMountRef.current = options.onMount;
448
+ }, [options.onMount]);
441
449
  useEffect(() => {
442
450
  selectedShapeIdsRef.current = selectedShapeIds;
443
451
  }, [selectedShapeIds]);
@@ -608,6 +616,7 @@ function useTsdrawCanvasController(options = {}) {
608
616
  history: editor.getHistorySnapshot(),
609
617
  sessionId
610
618
  });
619
+ if (disposed) return;
611
620
  persistenceChannel?.postMessage({
612
621
  type: "tsdraw:persisted",
613
622
  senderSessionId: sessionId
@@ -710,7 +719,7 @@ function useTsdrawCanvasController(options = {}) {
710
719
  additive: first.shiftKey,
711
720
  initialSelection: [...selectedShapeIdsRef.current]
712
721
  };
713
- setSelectionBrush({ left: e.offsetX, top: e.offsetY, width: 0, height: 0 });
722
+ setSelectionBrush(toScreenRect(editor, { minX: x, minY: y, maxX: x, maxY: y }));
714
723
  if (!e.shiftKey) {
715
724
  setSelectedShapeIds([]);
716
725
  selectedShapeIdsRef.current = [];
@@ -867,6 +876,35 @@ function useTsdrawCanvasController(options = {}) {
867
876
  }
868
877
  editor.endHistoryEntry();
869
878
  };
879
+ const handlePointerCancel = () => {
880
+ if (!isPointerActiveRef.current) return;
881
+ isPointerActiveRef.current = false;
882
+ lastPointerClientRef.current = null;
883
+ editor.input.pointerUp();
884
+ if (currentToolRef.current === "select") {
885
+ const drag = selectDragRef.current;
886
+ if (drag.mode === "rotate") setIsRotatingSelection(false);
887
+ if (drag.mode === "resize") setIsResizingSelection(false);
888
+ if (drag.mode === "move") setIsMovingSelection(false);
889
+ if (drag.mode === "marquee") setSelectionBrush(null);
890
+ if (drag.mode !== "none") {
891
+ selectDragRef.current.mode = "none";
892
+ render();
893
+ refreshSelectionBounds(editor);
894
+ }
895
+ editor.endHistoryEntry();
896
+ } else {
897
+ editor.tools.pointerUp();
898
+ render();
899
+ refreshSelectionBounds(editor);
900
+ editor.endHistoryEntry();
901
+ }
902
+ if (pendingRemoteDocumentRef.current) {
903
+ const pending = pendingRemoteDocumentRef.current;
904
+ pendingRemoteDocumentRef.current = null;
905
+ applyRemoteDocumentSnapshot(pending);
906
+ }
907
+ };
870
908
  const handleKeyDown = (e) => {
871
909
  const isMetaPressed = e.metaKey || e.ctrlKey;
872
910
  const loweredKey = e.key.toLowerCase();
@@ -913,22 +951,42 @@ function useTsdrawCanvasController(options = {}) {
913
951
  }
914
952
  editor.loadHistorySnapshot(loaded.history);
915
953
  syncHistoryState();
954
+ if (disposed) return;
916
955
  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;
956
+ if (typeof BroadcastChannel !== "undefined") {
957
+ persistenceChannel = new BroadcastChannel(`tsdraw:persistence:${persistenceKey}`);
958
+ let isLoadingRemote = false;
959
+ let pendingRemoteLoad = false;
960
+ persistenceChannel.onmessage = () => {
961
+ if (disposed) return;
962
+ if (isLoadingRemote) {
963
+ pendingRemoteLoad = true;
927
964
  return;
928
965
  }
929
- applyRemoteDocumentSnapshot(nextDocument);
930
- }
931
- };
966
+ isLoadingRemote = true;
967
+ const processLoad = async () => {
968
+ try {
969
+ do {
970
+ pendingRemoteLoad = false;
971
+ if (!persistenceDb || disposed) return;
972
+ const nextLoaded = await persistenceDb.load(sessionId);
973
+ if (disposed) return;
974
+ if (nextLoaded.records.length > 0) {
975
+ const nextDocument = { records: nextLoaded.records };
976
+ if (isPointerActiveRef.current) {
977
+ pendingRemoteDocumentRef.current = nextDocument;
978
+ return;
979
+ }
980
+ applyRemoteDocumentSnapshot(nextDocument);
981
+ }
982
+ } while (pendingRemoteLoad && !disposed);
983
+ } finally {
984
+ isLoadingRemote = false;
985
+ }
986
+ };
987
+ void processLoad();
988
+ };
989
+ }
932
990
  } finally {
933
991
  if (!disposed) {
934
992
  setIsPersistenceReady(true);
@@ -950,12 +1008,13 @@ function useTsdrawCanvasController(options = {}) {
950
1008
  canvas.addEventListener("pointerdown", handlePointerDown);
951
1009
  window.addEventListener("pointermove", handlePointerMove);
952
1010
  window.addEventListener("pointerup", handlePointerUp);
1011
+ window.addEventListener("pointercancel", handlePointerCancel);
953
1012
  window.addEventListener("keydown", handleKeyDown);
954
1013
  window.addEventListener("keyup", handleKeyUp);
955
1014
  void initializePersistence().catch((error) => {
956
1015
  console.error("failed to initialize tsdraw persistence", error);
957
1016
  });
958
- disposeMount = options.onMount?.({
1017
+ disposeMount = onMountRef.current?.({
959
1018
  editor,
960
1019
  container,
961
1020
  canvas,
@@ -1004,6 +1063,7 @@ function useTsdrawCanvasController(options = {}) {
1004
1063
  canvas.removeEventListener("pointerdown", handlePointerDown);
1005
1064
  window.removeEventListener("pointermove", handlePointerMove);
1006
1065
  window.removeEventListener("pointerup", handlePointerUp);
1066
+ window.removeEventListener("pointercancel", handlePointerCancel);
1007
1067
  window.removeEventListener("keydown", handleKeyDown);
1008
1068
  window.removeEventListener("keyup", handleKeyUp);
1009
1069
  isPointerActiveRef.current = false;
@@ -1015,7 +1075,6 @@ function useTsdrawCanvasController(options = {}) {
1015
1075
  }, [
1016
1076
  getPagePointFromClient,
1017
1077
  options.initialTool,
1018
- options.onMount,
1019
1078
  options.persistenceKey,
1020
1079
  options.toolDefinitions,
1021
1080
  refreshSelectionBounds,
@@ -1135,6 +1194,8 @@ function useTsdrawCanvasController(options = {}) {
1135
1194
  };
1136
1195
  }
1137
1196
  var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser"]];
1197
+ var EMPTY_CUSTOM_TOOLS = [];
1198
+ var EMPTY_CUSTOM_ELEMENTS = [];
1138
1199
  var DEFAULT_TOOL_LABELS = {
1139
1200
  select: "Select",
1140
1201
  pen: "Pen",
@@ -1180,14 +1241,14 @@ function resolvePlacementStyle(placement, fallbackAnchor, fallbackOffsetX, fallb
1180
1241
  if (offsetY) transforms.push(`translateY(${offsetY}px)`);
1181
1242
  }
1182
1243
  if (transforms.length > 0) result.transform = transforms.join(" ");
1183
- return { ...result, ...placement?.style ?? {} };
1244
+ return placement?.style ? { ...result, ...placement.style } : result;
1184
1245
  }
1185
1246
  function Tsdraw(props) {
1186
1247
  const [systemTheme, setSystemTheme] = useState(() => {
1187
1248
  if (typeof window === "undefined") return "light";
1188
1249
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1189
1250
  });
1190
- const customTools = props.customTools ?? [];
1251
+ const customTools = props.customTools ?? EMPTY_CUSTOM_TOOLS;
1191
1252
  const toolbarPartIds = props.uiOptions?.toolbar?.parts ?? DEFAULT_TOOLBAR_PARTS;
1192
1253
  const customToolMap = useMemo(
1193
1254
  () => new Map(customTools.map((customTool) => [customTool.id, customTool])),
@@ -1294,7 +1355,16 @@ function Tsdraw(props) {
1294
1355
  }
1295
1356
  );
1296
1357
  const overlayNode = props.uiOptions?.overlays?.renderToolOverlay?.({ defaultOverlay: defaultToolOverlay, overlayState: toolOverlay, currentTool }) ?? defaultToolOverlay;
1297
- const customElements = props.uiOptions?.customElements ?? [];
1358
+ const customElements = props.uiOptions?.customElements ?? EMPTY_CUSTOM_ELEMENTS;
1359
+ const onColorSelect = useCallback((color) => {
1360
+ applyDrawStyle({ color });
1361
+ }, [applyDrawStyle]);
1362
+ const onDashSelect = useCallback((dash) => {
1363
+ applyDrawStyle({ dash });
1364
+ }, [applyDrawStyle]);
1365
+ const onSizeSelect = useCallback((size) => {
1366
+ applyDrawStyle({ size });
1367
+ }, [applyDrawStyle]);
1298
1368
  const toolbarParts = useMemo(
1299
1369
  () => toolbarPartIds.map((toolbarPart, partIndex) => {
1300
1370
  const items = toolbarPart.map((item) => {
@@ -1389,9 +1459,9 @@ function Tsdraw(props) {
1389
1459
  drawColor,
1390
1460
  drawDash,
1391
1461
  drawSize,
1392
- onColorSelect: (color) => applyDrawStyle({ color }),
1393
- onDashSelect: (dash) => applyDrawStyle({ dash }),
1394
- onSizeSelect: (size) => applyDrawStyle({ size })
1462
+ onColorSelect,
1463
+ onDashSelect,
1464
+ onSizeSelect
1395
1465
  }
1396
1466
  ),
1397
1467
  customElements.map((customElement) => /* @__PURE__ */ jsx(