@tsdraw/react 0.9.1 → 0.9.3
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 +217 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -9
- package/dist/index.d.ts +12 -9
- package/dist/index.js +218 -46
- package/dist/index.js.map +1 -1
- package/dist/tsdraw.css +29 -8
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -98,6 +98,167 @@ function SelectionOverlay({
|
|
|
98
98
|
)
|
|
99
99
|
] });
|
|
100
100
|
}
|
|
101
|
+
function parseAnchor(anchor) {
|
|
102
|
+
const parts = anchor.split("-");
|
|
103
|
+
let vertical = "center";
|
|
104
|
+
let horizontal = "center";
|
|
105
|
+
for (const part of parts) {
|
|
106
|
+
if (part === "top" || part === "bottom") vertical = part;
|
|
107
|
+
else if (part === "left" || part === "right") horizontal = part;
|
|
108
|
+
}
|
|
109
|
+
return { vertical, horizontal };
|
|
110
|
+
}
|
|
111
|
+
function resolvePlacementStyle(placement, fallbackAnchor, fallbackEdgeOffset) {
|
|
112
|
+
const anchor = placement?.anchor ?? fallbackAnchor;
|
|
113
|
+
const edgeOffset = placement?.edgeOffset ?? fallbackEdgeOffset;
|
|
114
|
+
const { vertical, horizontal } = parseAnchor(anchor);
|
|
115
|
+
const result = {};
|
|
116
|
+
const transforms = [];
|
|
117
|
+
if (horizontal === "left") {
|
|
118
|
+
result.left = edgeOffset;
|
|
119
|
+
} else if (horizontal === "right") {
|
|
120
|
+
result.right = edgeOffset;
|
|
121
|
+
} else {
|
|
122
|
+
result.left = "50%";
|
|
123
|
+
transforms.push("translateX(-50%)");
|
|
124
|
+
}
|
|
125
|
+
if (vertical === "top") {
|
|
126
|
+
result.top = edgeOffset;
|
|
127
|
+
} else if (vertical === "bottom") {
|
|
128
|
+
result.bottom = edgeOffset;
|
|
129
|
+
} else {
|
|
130
|
+
result.top = "50%";
|
|
131
|
+
transforms.push("translateY(-50%)");
|
|
132
|
+
}
|
|
133
|
+
if (transforms.length > 0) result.transform = transforms.join(" ");
|
|
134
|
+
return placement?.style ? { ...result, ...placement.style } : result;
|
|
135
|
+
}
|
|
136
|
+
function resolveOrientation(anchor) {
|
|
137
|
+
const { vertical, horizontal } = parseAnchor(anchor);
|
|
138
|
+
if ((horizontal === "left" || horizontal === "right") && vertical === "center") return "vertical";
|
|
139
|
+
return "horizontal";
|
|
140
|
+
}
|
|
141
|
+
var SNAP_TARGETS = [
|
|
142
|
+
{ anchor: "top-center", nx: 0.5, ny: 0 },
|
|
143
|
+
{ anchor: "bottom-center", nx: 0.5, ny: 1 },
|
|
144
|
+
{ anchor: "left-center", nx: 0, ny: 0.5 },
|
|
145
|
+
{ anchor: "right-center", nx: 1, ny: 0.5 },
|
|
146
|
+
{ anchor: "top-left", nx: 0, ny: 0 },
|
|
147
|
+
{ anchor: "top-right", nx: 1, ny: 0 },
|
|
148
|
+
{ anchor: "bottom-left", nx: 0, ny: 1 },
|
|
149
|
+
{ anchor: "bottom-right", nx: 1, ny: 1 }
|
|
150
|
+
];
|
|
151
|
+
function calculateSnap(payload, containerRect, disabledPositions) {
|
|
152
|
+
const nx = Math.max(0, Math.min(1, (payload.centerX - containerRect.left) / containerRect.width));
|
|
153
|
+
const ny = Math.max(0, Math.min(1, (payload.centerY - containerRect.top) / containerRect.height));
|
|
154
|
+
let closestAnchor = "bottom-center";
|
|
155
|
+
let closestDistSq = Infinity;
|
|
156
|
+
for (const target of SNAP_TARGETS) {
|
|
157
|
+
if (disabledPositions && disabledPositions.has(target.anchor)) continue;
|
|
158
|
+
const dx = nx - target.nx;
|
|
159
|
+
const dy = ny - target.ny;
|
|
160
|
+
const distSq = dx * dx + dy * dy;
|
|
161
|
+
if (distSq < closestDistSq) {
|
|
162
|
+
closestDistSq = distSq;
|
|
163
|
+
closestAnchor = target.anchor;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { anchor: closestAnchor };
|
|
167
|
+
}
|
|
168
|
+
function BaseComponent({ children, className, style, draggable = false, onDragEnd, "aria-label": ariaLabel }) {
|
|
169
|
+
const componentRef = react.useRef(null);
|
|
170
|
+
const dragStateRef = react.useRef(null);
|
|
171
|
+
const suppressClickRef = react.useRef(false);
|
|
172
|
+
const pendingSnapRef = react.useRef(false);
|
|
173
|
+
const [isDragging, setIsDragging] = react.useState(false);
|
|
174
|
+
react.useLayoutEffect(() => {
|
|
175
|
+
if (!pendingSnapRef.current) return;
|
|
176
|
+
const componentNode = componentRef.current;
|
|
177
|
+
if (componentNode) componentNode.style.translate = "";
|
|
178
|
+
pendingSnapRef.current = false;
|
|
179
|
+
setIsDragging(false);
|
|
180
|
+
});
|
|
181
|
+
const finishDrag = (event, shouldSnap) => {
|
|
182
|
+
const componentNode = componentRef.current;
|
|
183
|
+
const dragState = dragStateRef.current;
|
|
184
|
+
if (!componentNode || !dragState || dragState.pointerId !== event.pointerId) return;
|
|
185
|
+
if (componentNode.hasPointerCapture(event.pointerId)) {
|
|
186
|
+
componentNode.releasePointerCapture(event.pointerId);
|
|
187
|
+
}
|
|
188
|
+
if (dragState.isDragging && shouldSnap && onDragEnd) {
|
|
189
|
+
const rect = componentNode.getBoundingClientRect();
|
|
190
|
+
onDragEnd({
|
|
191
|
+
left: rect.left,
|
|
192
|
+
top: rect.top,
|
|
193
|
+
width: rect.width,
|
|
194
|
+
height: rect.height,
|
|
195
|
+
centerX: rect.left + rect.width / 2,
|
|
196
|
+
centerY: rect.top + rect.height / 2
|
|
197
|
+
});
|
|
198
|
+
pendingSnapRef.current = true;
|
|
199
|
+
} else {
|
|
200
|
+
componentNode.style.translate = "";
|
|
201
|
+
setIsDragging(false);
|
|
202
|
+
}
|
|
203
|
+
suppressClickRef.current = dragState.didDrag;
|
|
204
|
+
dragStateRef.current = null;
|
|
205
|
+
};
|
|
206
|
+
const handlePointerDown = (event) => {
|
|
207
|
+
if (!draggable || event.button !== 0) return;
|
|
208
|
+
dragStateRef.current = {
|
|
209
|
+
pointerId: event.pointerId,
|
|
210
|
+
startX: event.clientX,
|
|
211
|
+
startY: event.clientY,
|
|
212
|
+
didDrag: false,
|
|
213
|
+
isDragging: false
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const handlePointerMove = (event) => {
|
|
217
|
+
const componentNode = componentRef.current;
|
|
218
|
+
const dragState = dragStateRef.current;
|
|
219
|
+
if (!draggable || !componentNode || !dragState || dragState.pointerId !== event.pointerId) return;
|
|
220
|
+
const deltaX = event.clientX - dragState.startX;
|
|
221
|
+
const deltaY = event.clientY - dragState.startY;
|
|
222
|
+
if (!dragState.isDragging && deltaX * deltaX + deltaY * deltaY >= 25) {
|
|
223
|
+
dragState.isDragging = true;
|
|
224
|
+
dragState.didDrag = true;
|
|
225
|
+
componentNode.setPointerCapture(event.pointerId);
|
|
226
|
+
setIsDragging(true);
|
|
227
|
+
}
|
|
228
|
+
if (dragState.isDragging) {
|
|
229
|
+
componentNode.style.translate = `${deltaX}px ${deltaY}px`;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const handlePointerUp = (event) => {
|
|
233
|
+
finishDrag(event, true);
|
|
234
|
+
};
|
|
235
|
+
const handlePointerCancel = (event) => {
|
|
236
|
+
finishDrag(event, false);
|
|
237
|
+
};
|
|
238
|
+
const handleClickCapture = (event) => {
|
|
239
|
+
if (!draggable || !suppressClickRef.current) return;
|
|
240
|
+
suppressClickRef.current = false;
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
event.stopPropagation();
|
|
243
|
+
};
|
|
244
|
+
const draggableClass = draggable ? " tsdraw-component--draggable" : "";
|
|
245
|
+
const draggingClass = isDragging ? " tsdraw-component--dragging" : "";
|
|
246
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
247
|
+
"div",
|
|
248
|
+
{
|
|
249
|
+
ref: componentRef,
|
|
250
|
+
className: `tsdraw-component${draggableClass}${draggingClass}${className ? ` ${className}` : ""}`,
|
|
251
|
+
style,
|
|
252
|
+
"aria-label": ariaLabel,
|
|
253
|
+
onPointerDown: handlePointerDown,
|
|
254
|
+
onPointerMove: handlePointerMove,
|
|
255
|
+
onPointerUp: handlePointerUp,
|
|
256
|
+
onPointerCancel: handlePointerCancel,
|
|
257
|
+
onClickCapture: handleClickCapture,
|
|
258
|
+
children
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
}
|
|
101
262
|
var STYLE_COLORS = Object.entries(core.DEFAULT_COLORS).filter(([key]) => key !== "white").map(([value]) => ({ value }));
|
|
102
263
|
var STYLE_DASHES = ["draw", "solid", "dashed", "dotted"];
|
|
103
264
|
var STYLE_FILLS = ["none", "blank", "semi", "solid"];
|
|
@@ -129,7 +290,7 @@ function StylePanel({
|
|
|
129
290
|
onSizeSelect
|
|
130
291
|
};
|
|
131
292
|
const customPartMap = new Map((customParts ?? []).map((customPart) => [customPart.id, customPart]));
|
|
132
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
293
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BaseComponent, { className: "tsdraw-style-panel", style, "aria-label": "Draw style panel", children: parts.map((part) => {
|
|
133
294
|
if (part === "colors") {
|
|
134
295
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-colors", children: STYLE_COLORS.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
135
296
|
"button",
|
|
@@ -249,8 +410,9 @@ function getActionIcon(actionId) {
|
|
|
249
410
|
if (actionId === "undo") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconArrowBackUp, { size: 16, stroke: 1.8 });
|
|
250
411
|
return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconArrowForwardUp, { size: 16, stroke: 1.8 });
|
|
251
412
|
}
|
|
252
|
-
function Toolbar({ parts, currentTool, onToolChange, disabled, style }) {
|
|
253
|
-
|
|
413
|
+
function Toolbar({ parts, currentTool, onToolChange, disabled, style, orientation = "horizontal", draggable = false, onDragEnd }) {
|
|
414
|
+
const orientationClass = orientation === "vertical" ? " tsdraw-toolbar--vertical" : "";
|
|
415
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BaseComponent, { className: `tsdraw-toolbar${orientationClass}`, style, draggable, onDragEnd, children: parts.map((part, partIndex) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tsdraw-toolbar-part", children: [
|
|
254
416
|
part.items.map((item) => {
|
|
255
417
|
if (item.type === "action") {
|
|
256
418
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1613,6 +1775,10 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1613
1775
|
if (ignorePersistenceChanges) return;
|
|
1614
1776
|
schedulePersist();
|
|
1615
1777
|
});
|
|
1778
|
+
const cleanupRenderRequest = editor.onRequestRender(() => {
|
|
1779
|
+
render();
|
|
1780
|
+
refreshSelectionBounds(editor);
|
|
1781
|
+
});
|
|
1616
1782
|
resize();
|
|
1617
1783
|
const ro = new ResizeObserver(resize);
|
|
1618
1784
|
ro.observe(container);
|
|
@@ -1681,6 +1847,7 @@ function useTsdrawCanvasController(options = {}) {
|
|
|
1681
1847
|
schedulePersistRef.current = null;
|
|
1682
1848
|
cleanupEditorListener();
|
|
1683
1849
|
cleanupHistoryListener();
|
|
1850
|
+
cleanupRenderRequest();
|
|
1684
1851
|
disposeMount?.();
|
|
1685
1852
|
ro.disconnect();
|
|
1686
1853
|
canvas.removeEventListener("pointerdown", handlePointerDown);
|
|
@@ -1850,47 +2017,9 @@ var DEFAULT_TOOL_LABELS = {
|
|
|
1850
2017
|
eraser: "Eraser",
|
|
1851
2018
|
hand: "Hand"
|
|
1852
2019
|
};
|
|
1853
|
-
function parseAnchor(anchor) {
|
|
1854
|
-
const parts = anchor.split("-");
|
|
1855
|
-
let vertical = "center";
|
|
1856
|
-
let horizontal = "center";
|
|
1857
|
-
for (const part of parts) {
|
|
1858
|
-
if (part === "top" || part === "bottom") vertical = part;
|
|
1859
|
-
else if (part === "left" || part === "right") horizontal = part;
|
|
1860
|
-
}
|
|
1861
|
-
return { vertical, horizontal };
|
|
1862
|
-
}
|
|
1863
2020
|
function isToolbarAction(item) {
|
|
1864
2021
|
return item === "undo" || item === "redo";
|
|
1865
2022
|
}
|
|
1866
|
-
function resolvePlacementStyle(placement, fallbackAnchor, fallbackOffsetX, fallbackOffsetY) {
|
|
1867
|
-
const anchor = placement?.anchor ?? fallbackAnchor;
|
|
1868
|
-
const offsetX = placement?.offsetX ?? fallbackOffsetX;
|
|
1869
|
-
const offsetY = placement?.offsetY ?? fallbackOffsetY;
|
|
1870
|
-
const { vertical, horizontal } = parseAnchor(anchor);
|
|
1871
|
-
const result = {};
|
|
1872
|
-
const transforms = [];
|
|
1873
|
-
if (horizontal === "left") {
|
|
1874
|
-
result.left = offsetX;
|
|
1875
|
-
} else if (horizontal === "right") {
|
|
1876
|
-
result.right = offsetX;
|
|
1877
|
-
} else {
|
|
1878
|
-
result.left = "50%";
|
|
1879
|
-
transforms.push("translateX(-50%)");
|
|
1880
|
-
if (offsetX) transforms.push(`translateX(${offsetX}px)`);
|
|
1881
|
-
}
|
|
1882
|
-
if (vertical === "top") {
|
|
1883
|
-
result.top = offsetY;
|
|
1884
|
-
} else if (vertical === "bottom") {
|
|
1885
|
-
result.bottom = offsetY;
|
|
1886
|
-
} else {
|
|
1887
|
-
result.top = "50%";
|
|
1888
|
-
transforms.push("translateY(-50%)");
|
|
1889
|
-
if (offsetY) transforms.push(`translateY(${offsetY}px)`);
|
|
1890
|
-
}
|
|
1891
|
-
if (transforms.length > 0) result.transform = transforms.join(" ");
|
|
1892
|
-
return placement?.style ? { ...result, ...placement.style } : result;
|
|
1893
|
-
}
|
|
1894
2023
|
function Tsdraw(props) {
|
|
1895
2024
|
const [systemTheme, setSystemTheme] = react.useState(() => {
|
|
1896
2025
|
if (typeof window === "undefined") return "light";
|
|
@@ -1898,6 +2027,20 @@ function Tsdraw(props) {
|
|
|
1898
2027
|
});
|
|
1899
2028
|
const customTools = props.customTools ?? EMPTY_CUSTOM_TOOLS;
|
|
1900
2029
|
const toolbarPartIds = props.uiOptions?.toolbar?.parts ?? DEFAULT_TOOLBAR_PARTS;
|
|
2030
|
+
const toolbarPlacement = props.uiOptions?.toolbar?.placement;
|
|
2031
|
+
const toolbarEdgeOffset = toolbarPlacement?.edgeOffset ?? 14;
|
|
2032
|
+
const isToolbarDraggable = props.uiOptions?.toolbar?.draggable === true;
|
|
2033
|
+
const shouldSaveDraggedToolbarPosition = props.uiOptions?.toolbar?.saveDraggedPosition === true;
|
|
2034
|
+
const disabledDragPositionsArray = props.uiOptions?.toolbar?.disabledDragPositions;
|
|
2035
|
+
const disabledDragPositionsSet = react.useMemo(
|
|
2036
|
+
() => disabledDragPositionsArray && disabledDragPositionsArray.length > 0 ? new Set(disabledDragPositionsArray) : void 0,
|
|
2037
|
+
[disabledDragPositionsArray]
|
|
2038
|
+
);
|
|
2039
|
+
const toolbarDraggedSessionKey = react.useMemo(
|
|
2040
|
+
() => `tsdraw-toolbar-pos-${props.persistenceKey ?? "default"}`,
|
|
2041
|
+
[props.persistenceKey]
|
|
2042
|
+
);
|
|
2043
|
+
const [draggedToolbarPosition, setDraggedToolbarPosition] = react.useState(null);
|
|
1901
2044
|
const customToolMap = react.useMemo(
|
|
1902
2045
|
() => new Map(customTools.map((customTool) => [customTool.id, customTool])),
|
|
1903
2046
|
[customTools]
|
|
@@ -1982,9 +2125,35 @@ function Tsdraw(props) {
|
|
|
1982
2125
|
onCameraChange: props.onCameraChange,
|
|
1983
2126
|
onToolChange: props.onToolChange
|
|
1984
2127
|
});
|
|
1985
|
-
const
|
|
1986
|
-
const
|
|
2128
|
+
const toolbarPlacementAnchor = draggedToolbarPosition?.anchor ?? toolbarPlacement?.anchor ?? "bottom-center";
|
|
2129
|
+
const effectiveToolbarPlacement = draggedToolbarPosition ? { anchor: draggedToolbarPosition.anchor, edgeOffset: toolbarEdgeOffset, style: toolbarPlacement?.style } : toolbarPlacement;
|
|
2130
|
+
const toolbarPlacementStyle = resolvePlacementStyle(effectiveToolbarPlacement, "bottom-center", 14);
|
|
2131
|
+
const toolbarOrientation = resolveOrientation(toolbarPlacementAnchor);
|
|
2132
|
+
const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8);
|
|
1987
2133
|
const isToolbarHidden = props.uiOptions?.toolbar?.hide === true;
|
|
2134
|
+
react.useEffect(() => {
|
|
2135
|
+
if (!isToolbarDraggable || !shouldSaveDraggedToolbarPosition || typeof window === "undefined") return;
|
|
2136
|
+
try {
|
|
2137
|
+
const rawPosition = window.sessionStorage.getItem(toolbarDraggedSessionKey);
|
|
2138
|
+
if (!rawPosition) return;
|
|
2139
|
+
const parsedPosition = JSON.parse(rawPosition);
|
|
2140
|
+
if (typeof parsedPosition.anchor !== "string") return;
|
|
2141
|
+
setDraggedToolbarPosition({ anchor: parsedPosition.anchor });
|
|
2142
|
+
} catch {
|
|
2143
|
+
}
|
|
2144
|
+
}, [isToolbarDraggable, shouldSaveDraggedToolbarPosition, toolbarDraggedSessionKey]);
|
|
2145
|
+
react.useEffect(() => {
|
|
2146
|
+
if (isToolbarDraggable) return;
|
|
2147
|
+
setDraggedToolbarPosition(null);
|
|
2148
|
+
}, [isToolbarDraggable]);
|
|
2149
|
+
const handleToolbarDragEnd = react.useCallback((payload) => {
|
|
2150
|
+
const containerNode = containerRef.current;
|
|
2151
|
+
if (!containerNode) return;
|
|
2152
|
+
const nextPosition = calculateSnap(payload, containerNode.getBoundingClientRect(), disabledDragPositionsSet);
|
|
2153
|
+
setDraggedToolbarPosition(nextPosition);
|
|
2154
|
+
if (!shouldSaveDraggedToolbarPosition || typeof window === "undefined") return;
|
|
2155
|
+
window.sessionStorage.setItem(toolbarDraggedSessionKey, JSON.stringify(nextPosition));
|
|
2156
|
+
}, [containerRef, disabledDragPositionsSet, shouldSaveDraggedToolbarPosition, toolbarDraggedSessionKey]);
|
|
1988
2157
|
const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true || props.readOnly === true;
|
|
1989
2158
|
const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
|
|
1990
2159
|
const defaultToolOverlay = /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2137,7 +2306,7 @@ function Tsdraw(props) {
|
|
|
2137
2306
|
position: "absolute",
|
|
2138
2307
|
zIndex: 130,
|
|
2139
2308
|
pointerEvents: "all",
|
|
2140
|
-
...resolvePlacementStyle(customElement.placement, "top-left", 8
|
|
2309
|
+
...resolvePlacementStyle(customElement.placement, "top-left", 8)
|
|
2141
2310
|
},
|
|
2142
2311
|
children: customElement.render({ currentTool, setTool, applyDrawStyle })
|
|
2143
2312
|
},
|
|
@@ -2148,9 +2317,12 @@ function Tsdraw(props) {
|
|
|
2148
2317
|
{
|
|
2149
2318
|
parts: toolbarParts,
|
|
2150
2319
|
style: toolbarPlacementStyle,
|
|
2320
|
+
orientation: toolbarOrientation,
|
|
2151
2321
|
currentTool: isPersistenceReady ? currentTool : null,
|
|
2152
2322
|
onToolChange: setTool,
|
|
2153
|
-
disabled: !isPersistenceReady
|
|
2323
|
+
disabled: !isPersistenceReady,
|
|
2324
|
+
draggable: isToolbarDraggable,
|
|
2325
|
+
onDragEnd: handleToolbarDragEnd
|
|
2154
2326
|
}
|
|
2155
2327
|
) : null
|
|
2156
2328
|
]
|