@tsdraw/react 0.9.1 → 0.9.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.cjs +212 -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 +213 -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(
|
|
@@ -1850,47 +2012,9 @@ var DEFAULT_TOOL_LABELS = {
|
|
|
1850
2012
|
eraser: "Eraser",
|
|
1851
2013
|
hand: "Hand"
|
|
1852
2014
|
};
|
|
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
2015
|
function isToolbarAction(item) {
|
|
1864
2016
|
return item === "undo" || item === "redo";
|
|
1865
2017
|
}
|
|
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
2018
|
function Tsdraw(props) {
|
|
1895
2019
|
const [systemTheme, setSystemTheme] = react.useState(() => {
|
|
1896
2020
|
if (typeof window === "undefined") return "light";
|
|
@@ -1898,6 +2022,20 @@ function Tsdraw(props) {
|
|
|
1898
2022
|
});
|
|
1899
2023
|
const customTools = props.customTools ?? EMPTY_CUSTOM_TOOLS;
|
|
1900
2024
|
const toolbarPartIds = props.uiOptions?.toolbar?.parts ?? DEFAULT_TOOLBAR_PARTS;
|
|
2025
|
+
const toolbarPlacement = props.uiOptions?.toolbar?.placement;
|
|
2026
|
+
const toolbarEdgeOffset = toolbarPlacement?.edgeOffset ?? 14;
|
|
2027
|
+
const isToolbarDraggable = props.uiOptions?.toolbar?.draggable === true;
|
|
2028
|
+
const shouldSaveDraggedToolbarPosition = props.uiOptions?.toolbar?.saveDraggedPosition === true;
|
|
2029
|
+
const disabledDragPositionsArray = props.uiOptions?.toolbar?.disabledDragPositions;
|
|
2030
|
+
const disabledDragPositionsSet = react.useMemo(
|
|
2031
|
+
() => disabledDragPositionsArray && disabledDragPositionsArray.length > 0 ? new Set(disabledDragPositionsArray) : void 0,
|
|
2032
|
+
[disabledDragPositionsArray]
|
|
2033
|
+
);
|
|
2034
|
+
const toolbarDraggedSessionKey = react.useMemo(
|
|
2035
|
+
() => `tsdraw-toolbar-pos-${props.persistenceKey ?? "default"}`,
|
|
2036
|
+
[props.persistenceKey]
|
|
2037
|
+
);
|
|
2038
|
+
const [draggedToolbarPosition, setDraggedToolbarPosition] = react.useState(null);
|
|
1901
2039
|
const customToolMap = react.useMemo(
|
|
1902
2040
|
() => new Map(customTools.map((customTool) => [customTool.id, customTool])),
|
|
1903
2041
|
[customTools]
|
|
@@ -1982,9 +2120,35 @@ function Tsdraw(props) {
|
|
|
1982
2120
|
onCameraChange: props.onCameraChange,
|
|
1983
2121
|
onToolChange: props.onToolChange
|
|
1984
2122
|
});
|
|
1985
|
-
const
|
|
1986
|
-
const
|
|
2123
|
+
const toolbarPlacementAnchor = draggedToolbarPosition?.anchor ?? toolbarPlacement?.anchor ?? "bottom-center";
|
|
2124
|
+
const effectiveToolbarPlacement = draggedToolbarPosition ? { anchor: draggedToolbarPosition.anchor, edgeOffset: toolbarEdgeOffset, style: toolbarPlacement?.style } : toolbarPlacement;
|
|
2125
|
+
const toolbarPlacementStyle = resolvePlacementStyle(effectiveToolbarPlacement, "bottom-center", 14);
|
|
2126
|
+
const toolbarOrientation = resolveOrientation(toolbarPlacementAnchor);
|
|
2127
|
+
const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8);
|
|
1987
2128
|
const isToolbarHidden = props.uiOptions?.toolbar?.hide === true;
|
|
2129
|
+
react.useEffect(() => {
|
|
2130
|
+
if (!isToolbarDraggable || !shouldSaveDraggedToolbarPosition || typeof window === "undefined") return;
|
|
2131
|
+
try {
|
|
2132
|
+
const rawPosition = window.sessionStorage.getItem(toolbarDraggedSessionKey);
|
|
2133
|
+
if (!rawPosition) return;
|
|
2134
|
+
const parsedPosition = JSON.parse(rawPosition);
|
|
2135
|
+
if (typeof parsedPosition.anchor !== "string") return;
|
|
2136
|
+
setDraggedToolbarPosition({ anchor: parsedPosition.anchor });
|
|
2137
|
+
} catch {
|
|
2138
|
+
}
|
|
2139
|
+
}, [isToolbarDraggable, shouldSaveDraggedToolbarPosition, toolbarDraggedSessionKey]);
|
|
2140
|
+
react.useEffect(() => {
|
|
2141
|
+
if (isToolbarDraggable) return;
|
|
2142
|
+
setDraggedToolbarPosition(null);
|
|
2143
|
+
}, [isToolbarDraggable]);
|
|
2144
|
+
const handleToolbarDragEnd = react.useCallback((payload) => {
|
|
2145
|
+
const containerNode = containerRef.current;
|
|
2146
|
+
if (!containerNode) return;
|
|
2147
|
+
const nextPosition = calculateSnap(payload, containerNode.getBoundingClientRect(), disabledDragPositionsSet);
|
|
2148
|
+
setDraggedToolbarPosition(nextPosition);
|
|
2149
|
+
if (!shouldSaveDraggedToolbarPosition || typeof window === "undefined") return;
|
|
2150
|
+
window.sessionStorage.setItem(toolbarDraggedSessionKey, JSON.stringify(nextPosition));
|
|
2151
|
+
}, [containerRef, disabledDragPositionsSet, shouldSaveDraggedToolbarPosition, toolbarDraggedSessionKey]);
|
|
1988
2152
|
const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true || props.readOnly === true;
|
|
1989
2153
|
const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
|
|
1990
2154
|
const defaultToolOverlay = /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2137,7 +2301,7 @@ function Tsdraw(props) {
|
|
|
2137
2301
|
position: "absolute",
|
|
2138
2302
|
zIndex: 130,
|
|
2139
2303
|
pointerEvents: "all",
|
|
2140
|
-
...resolvePlacementStyle(customElement.placement, "top-left", 8
|
|
2304
|
+
...resolvePlacementStyle(customElement.placement, "top-left", 8)
|
|
2141
2305
|
},
|
|
2142
2306
|
children: customElement.render({ currentTool, setTool, applyDrawStyle })
|
|
2143
2307
|
},
|
|
@@ -2148,9 +2312,12 @@ function Tsdraw(props) {
|
|
|
2148
2312
|
{
|
|
2149
2313
|
parts: toolbarParts,
|
|
2150
2314
|
style: toolbarPlacementStyle,
|
|
2315
|
+
orientation: toolbarOrientation,
|
|
2151
2316
|
currentTool: isPersistenceReady ? currentTool : null,
|
|
2152
2317
|
onToolChange: setTool,
|
|
2153
|
-
disabled: !isPersistenceReady
|
|
2318
|
+
disabled: !isPersistenceReady,
|
|
2319
|
+
draggable: isToolbarDraggable,
|
|
2320
|
+
onDragEnd: handleToolbarDragEnd
|
|
2154
2321
|
}
|
|
2155
2322
|
) : null
|
|
2156
2323
|
]
|