@tsdraw/react 0.6.1 → 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]);
@@ -711,7 +719,7 @@ function useTsdrawCanvasController(options = {}) {
711
719
  additive: first.shiftKey,
712
720
  initialSelection: [...selectedShapeIdsRef.current]
713
721
  };
714
- setSelectionBrush({ left: e.offsetX, top: e.offsetY, width: 0, height: 0 });
722
+ setSelectionBrush(toScreenRect(editor, { minX: x, minY: y, maxX: x, maxY: y }));
715
723
  if (!e.shiftKey) {
716
724
  setSelectedShapeIds([]);
717
725
  selectedShapeIdsRef.current = [];
@@ -868,6 +876,35 @@ function useTsdrawCanvasController(options = {}) {
868
876
  }
869
877
  editor.endHistoryEntry();
870
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
+ };
871
908
  const handleKeyDown = (e) => {
872
909
  const isMetaPressed = e.metaKey || e.ctrlKey;
873
910
  const loweredKey = e.key.toLowerCase();
@@ -914,22 +951,42 @@ function useTsdrawCanvasController(options = {}) {
914
951
  }
915
952
  editor.loadHistorySnapshot(loaded.history);
916
953
  syncHistoryState();
954
+ if (disposed) return;
917
955
  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;
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;
928
964
  return;
929
965
  }
930
- applyRemoteDocumentSnapshot(nextDocument);
931
- }
932
- };
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
+ }
933
990
  } finally {
934
991
  if (!disposed) {
935
992
  setIsPersistenceReady(true);
@@ -951,12 +1008,13 @@ function useTsdrawCanvasController(options = {}) {
951
1008
  canvas.addEventListener("pointerdown", handlePointerDown);
952
1009
  window.addEventListener("pointermove", handlePointerMove);
953
1010
  window.addEventListener("pointerup", handlePointerUp);
1011
+ window.addEventListener("pointercancel", handlePointerCancel);
954
1012
  window.addEventListener("keydown", handleKeyDown);
955
1013
  window.addEventListener("keyup", handleKeyUp);
956
1014
  void initializePersistence().catch((error) => {
957
1015
  console.error("failed to initialize tsdraw persistence", error);
958
1016
  });
959
- disposeMount = options.onMount?.({
1017
+ disposeMount = onMountRef.current?.({
960
1018
  editor,
961
1019
  container,
962
1020
  canvas,
@@ -1005,6 +1063,7 @@ function useTsdrawCanvasController(options = {}) {
1005
1063
  canvas.removeEventListener("pointerdown", handlePointerDown);
1006
1064
  window.removeEventListener("pointermove", handlePointerMove);
1007
1065
  window.removeEventListener("pointerup", handlePointerUp);
1066
+ window.removeEventListener("pointercancel", handlePointerCancel);
1008
1067
  window.removeEventListener("keydown", handleKeyDown);
1009
1068
  window.removeEventListener("keyup", handleKeyUp);
1010
1069
  isPointerActiveRef.current = false;
@@ -1016,7 +1075,6 @@ function useTsdrawCanvasController(options = {}) {
1016
1075
  }, [
1017
1076
  getPagePointFromClient,
1018
1077
  options.initialTool,
1019
- options.onMount,
1020
1078
  options.persistenceKey,
1021
1079
  options.toolDefinitions,
1022
1080
  refreshSelectionBounds,
@@ -1136,6 +1194,8 @@ function useTsdrawCanvasController(options = {}) {
1136
1194
  };
1137
1195
  }
1138
1196
  var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser"]];
1197
+ var EMPTY_CUSTOM_TOOLS = [];
1198
+ var EMPTY_CUSTOM_ELEMENTS = [];
1139
1199
  var DEFAULT_TOOL_LABELS = {
1140
1200
  select: "Select",
1141
1201
  pen: "Pen",
@@ -1181,14 +1241,14 @@ function resolvePlacementStyle(placement, fallbackAnchor, fallbackOffsetX, fallb
1181
1241
  if (offsetY) transforms.push(`translateY(${offsetY}px)`);
1182
1242
  }
1183
1243
  if (transforms.length > 0) result.transform = transforms.join(" ");
1184
- return { ...result, ...placement?.style ?? {} };
1244
+ return placement?.style ? { ...result, ...placement.style } : result;
1185
1245
  }
1186
1246
  function Tsdraw(props) {
1187
1247
  const [systemTheme, setSystemTheme] = useState(() => {
1188
1248
  if (typeof window === "undefined") return "light";
1189
1249
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1190
1250
  });
1191
- const customTools = props.customTools ?? [];
1251
+ const customTools = props.customTools ?? EMPTY_CUSTOM_TOOLS;
1192
1252
  const toolbarPartIds = props.uiOptions?.toolbar?.parts ?? DEFAULT_TOOLBAR_PARTS;
1193
1253
  const customToolMap = useMemo(
1194
1254
  () => new Map(customTools.map((customTool) => [customTool.id, customTool])),
@@ -1295,7 +1355,16 @@ function Tsdraw(props) {
1295
1355
  }
1296
1356
  );
1297
1357
  const overlayNode = props.uiOptions?.overlays?.renderToolOverlay?.({ defaultOverlay: defaultToolOverlay, overlayState: toolOverlay, currentTool }) ?? defaultToolOverlay;
1298
- 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]);
1299
1368
  const toolbarParts = useMemo(
1300
1369
  () => toolbarPartIds.map((toolbarPart, partIndex) => {
1301
1370
  const items = toolbarPart.map((item) => {
@@ -1390,9 +1459,9 @@ function Tsdraw(props) {
1390
1459
  drawColor,
1391
1460
  drawDash,
1392
1461
  drawSize,
1393
- onColorSelect: (color) => applyDrawStyle({ color }),
1394
- onDashSelect: (dash) => applyDrawStyle({ dash }),
1395
- onSizeSelect: (size) => applyDrawStyle({ size })
1462
+ onColorSelect,
1463
+ onDashSelect,
1464
+ onSizeSelect
1396
1465
  }
1397
1466
  ),
1398
1467
  customElements.map((customElement) => /* @__PURE__ */ jsx(