@zuilib/text-editor 0.0.2 → 0.2.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/README.md +56 -5
- package/dist/index.d.ts +97 -4
- package/dist/index.js +2283 -57
- package/dist/styles.css +420 -0
- package/package.json +33 -23
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/MarkdownEditor.tsx
|
|
2
|
-
import { useCallback, useEffect as
|
|
2
|
+
import { useCallback as useCallback5, useEffect as useEffect9, useMemo, useRef as useRef4, useState as useState5 } from "react";
|
|
3
3
|
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
|
4
4
|
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
5
5
|
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
|
@@ -10,12 +10,14 @@ import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin";
|
|
|
10
10
|
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
|
|
11
11
|
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
|
12
12
|
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { TablePlugin } from "@lexical/react/LexicalTablePlugin";
|
|
14
|
+
import { CHECK_LIST, CODE, TRANSFORMERS as TRANSFORMERS2 } from "@lexical/markdown";
|
|
15
|
+
import { useLexicalComposerContext as useLexicalComposerContext9 } from "@lexical/react/LexicalComposerContext";
|
|
15
16
|
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
|
|
16
17
|
import { ListItemNode, ListNode as ListNode2 } from "@lexical/list";
|
|
17
18
|
import { CodeHighlightNode, CodeNode } from "@lexical/code";
|
|
18
19
|
import { AutoLinkNode, LinkNode } from "@lexical/link";
|
|
20
|
+
import { TableCellNode as TableCellNode2, TableNode as TableNode2, TableRowNode as TableRowNode2 } from "@lexical/table";
|
|
19
21
|
|
|
20
22
|
// src/plugins/ChecklistShortcutPlugin.tsx
|
|
21
23
|
import { useEffect } from "react";
|
|
@@ -79,6 +81,7 @@ import { useLexicalComposerContext as useLexicalComposerContext2 } from "@lexica
|
|
|
79
81
|
import {
|
|
80
82
|
$createParagraphNode,
|
|
81
83
|
$getSelection,
|
|
84
|
+
$isLineBreakNode,
|
|
82
85
|
$isParagraphNode,
|
|
83
86
|
$isRangeSelection,
|
|
84
87
|
$isRootOrShadowRoot,
|
|
@@ -122,10 +125,20 @@ function CodeBlockShortcutPlugin() {
|
|
|
122
125
|
if (!$isCodeNode(topNode)) return false;
|
|
123
126
|
const root = topNode.getParent();
|
|
124
127
|
if (!root || root.getLastChild()?.getKey() !== topNode.getKey()) return false;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
const hasNewline = (node) => $isLineBreakNode(node) || node.getTextContent().includes("\n");
|
|
129
|
+
let hasContentBelow;
|
|
130
|
+
if (anchorNode.getKey() === topNode.getKey()) {
|
|
131
|
+
hasContentBelow = topNode.getChildren().slice(anchor.offset).some(hasNewline);
|
|
132
|
+
} else if (anchorNode.getTextContent().slice(anchor.offset).includes("\n")) {
|
|
133
|
+
hasContentBelow = true;
|
|
134
|
+
} else {
|
|
135
|
+
let child = anchorNode;
|
|
136
|
+
while (child.getParent() && child.getParent().getKey() !== topNode.getKey()) {
|
|
137
|
+
child = child.getParent();
|
|
138
|
+
}
|
|
139
|
+
hasContentBelow = child.getNextSiblings().some(hasNewline);
|
|
140
|
+
}
|
|
141
|
+
if (hasContentBelow) return false;
|
|
129
142
|
const paragraph = $createParagraphNode();
|
|
130
143
|
topNode.insertAfter(paragraph);
|
|
131
144
|
paragraph.selectStart();
|
|
@@ -153,9 +166,146 @@ function CodeHighlightPlugin() {
|
|
|
153
166
|
return null;
|
|
154
167
|
}
|
|
155
168
|
|
|
156
|
-
// src/plugins/
|
|
157
|
-
import {
|
|
169
|
+
// src/plugins/FoldingPlugin.tsx
|
|
170
|
+
import {
|
|
171
|
+
useCallback,
|
|
172
|
+
useEffect as useEffect4,
|
|
173
|
+
useRef,
|
|
174
|
+
useState
|
|
175
|
+
} from "react";
|
|
176
|
+
import { $getRoot, $getSelection as $getSelection2, $isRangeSelection as $isRangeSelection2 } from "lexical";
|
|
158
177
|
import { useLexicalComposerContext as useLexicalComposerContext4 } from "@lexical/react/LexicalComposerContext";
|
|
178
|
+
import { $isHeadingNode } from "@lexical/rich-text";
|
|
179
|
+
import { jsx } from "react/jsx-runtime";
|
|
180
|
+
var HEADING_LEVELS = {
|
|
181
|
+
h1: 1,
|
|
182
|
+
h2: 2,
|
|
183
|
+
h3: 3,
|
|
184
|
+
h4: 4,
|
|
185
|
+
h5: 5,
|
|
186
|
+
h6: 6
|
|
187
|
+
};
|
|
188
|
+
function FoldingPlugin() {
|
|
189
|
+
const [editor] = useLexicalComposerContext4();
|
|
190
|
+
const [foldedKeys, setFoldedKeys] = useState(/* @__PURE__ */ new Set());
|
|
191
|
+
const [buttons, setButtons] = useState([]);
|
|
192
|
+
const foldedRef = useRef(foldedKeys);
|
|
193
|
+
foldedRef.current = foldedKeys;
|
|
194
|
+
const sync = useCallback(() => {
|
|
195
|
+
editor.getEditorState().read(() => {
|
|
196
|
+
const children = $getRoot().getChildren();
|
|
197
|
+
const folded = foldedRef.current;
|
|
198
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
199
|
+
const headings = [];
|
|
200
|
+
for (let i = 0; i < children.length; i++) {
|
|
201
|
+
const node = children[i];
|
|
202
|
+
if (!$isHeadingNode(node)) continue;
|
|
203
|
+
const level = HEADING_LEVELS[node.getTag()] ?? 6;
|
|
204
|
+
const isFolded = folded.has(node.getKey());
|
|
205
|
+
headings.push({ key: node.getKey(), folded: isFolded });
|
|
206
|
+
if (!isFolded) continue;
|
|
207
|
+
for (let j = i + 1; j < children.length; j++) {
|
|
208
|
+
const sibling = children[j];
|
|
209
|
+
if ($isHeadingNode(sibling) && (HEADING_LEVELS[sibling.getTag()] ?? 6) <= level) {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
hidden.add(sibling.getKey());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const selection = $getSelection2();
|
|
216
|
+
if ($isRangeSelection2(selection)) {
|
|
217
|
+
const topLevel = selection.anchor.getNode().getTopLevelElement();
|
|
218
|
+
if (topLevel && hidden.has(topLevel.getKey())) {
|
|
219
|
+
let node = topLevel.getPreviousSibling();
|
|
220
|
+
while (node) {
|
|
221
|
+
if ($isHeadingNode(node) && folded.has(node.getKey())) {
|
|
222
|
+
const unfoldKey = node.getKey();
|
|
223
|
+
setFoldedKeys((prev) => {
|
|
224
|
+
const next = new Set(prev);
|
|
225
|
+
next.delete(unfoldKey);
|
|
226
|
+
return next;
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
node = node.getPreviousSibling();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const child of children) {
|
|
235
|
+
const element = editor.getElementByKey(child.getKey());
|
|
236
|
+
if (!element) continue;
|
|
237
|
+
const shouldHide = hidden.has(child.getKey());
|
|
238
|
+
if (shouldHide) {
|
|
239
|
+
element.style.display = "none";
|
|
240
|
+
} else if (element.style.display === "none") {
|
|
241
|
+
element.style.display = "";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
setButtons(
|
|
245
|
+
headings.map(({ key, folded: isFolded }) => {
|
|
246
|
+
const element = editor.getElementByKey(key);
|
|
247
|
+
element?.classList.toggle("zui-heading-folded", isFolded);
|
|
248
|
+
return {
|
|
249
|
+
key,
|
|
250
|
+
folded: isFolded,
|
|
251
|
+
top: element?.offsetTop ?? 0,
|
|
252
|
+
height: element?.offsetHeight ?? 0
|
|
253
|
+
};
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}, [editor]);
|
|
258
|
+
useEffect4(() => {
|
|
259
|
+
sync();
|
|
260
|
+
const unregister = editor.registerUpdateListener(sync);
|
|
261
|
+
const rootElement = editor.getRootElement();
|
|
262
|
+
const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(sync) : null;
|
|
263
|
+
if (rootElement && observer) observer.observe(rootElement);
|
|
264
|
+
return () => {
|
|
265
|
+
unregister();
|
|
266
|
+
observer?.disconnect();
|
|
267
|
+
};
|
|
268
|
+
}, [editor, sync, foldedKeys]);
|
|
269
|
+
const toggle = useCallback((key) => {
|
|
270
|
+
setFoldedKeys((prev) => {
|
|
271
|
+
const next = new Set(prev);
|
|
272
|
+
if (next.has(key)) next.delete(key);
|
|
273
|
+
else next.add(key);
|
|
274
|
+
return next;
|
|
275
|
+
});
|
|
276
|
+
}, []);
|
|
277
|
+
return /* @__PURE__ */ jsx("div", { className: "zui-text-editor-fold-gutter", children: buttons.map(({ key, folded, top, height }) => /* @__PURE__ */ jsx(
|
|
278
|
+
"button",
|
|
279
|
+
{
|
|
280
|
+
type: "button",
|
|
281
|
+
className: `zui-text-editor-fold ${folded ? "is-folded" : ""}`,
|
|
282
|
+
style: { top: top + Math.max(0, (Math.min(height, 44) - 18) / 2) },
|
|
283
|
+
title: folded ? "Expand section" : "Collapse section",
|
|
284
|
+
"aria-label": folded ? "Expand section" : "Collapse section",
|
|
285
|
+
"aria-expanded": !folded,
|
|
286
|
+
onClick: () => toggle(key),
|
|
287
|
+
children: /* @__PURE__ */ jsx(
|
|
288
|
+
"svg",
|
|
289
|
+
{
|
|
290
|
+
viewBox: "0 0 20 20",
|
|
291
|
+
width: "12",
|
|
292
|
+
height: "12",
|
|
293
|
+
fill: "none",
|
|
294
|
+
stroke: "currentColor",
|
|
295
|
+
strokeWidth: "2",
|
|
296
|
+
strokeLinecap: "round",
|
|
297
|
+
strokeLinejoin: "round",
|
|
298
|
+
children: /* @__PURE__ */ jsx("path", { d: "M6 8l4 4 4-4" })
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
},
|
|
302
|
+
key
|
|
303
|
+
)) });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/plugins/MarkdownSyncPlugin.tsx
|
|
307
|
+
import { useEffect as useEffect5, useRef as useRef2 } from "react";
|
|
308
|
+
import { useLexicalComposerContext as useLexicalComposerContext5 } from "@lexical/react/LexicalComposerContext";
|
|
159
309
|
import {
|
|
160
310
|
$convertFromMarkdownString,
|
|
161
311
|
$convertToMarkdownString
|
|
@@ -165,11 +315,11 @@ function MarkdownSyncPlugin({
|
|
|
165
315
|
onChange,
|
|
166
316
|
transformers
|
|
167
317
|
}) {
|
|
168
|
-
const [editor] =
|
|
169
|
-
const lastMarkdownRef =
|
|
170
|
-
const onChangeRef =
|
|
318
|
+
const [editor] = useLexicalComposerContext5();
|
|
319
|
+
const lastMarkdownRef = useRef2("");
|
|
320
|
+
const onChangeRef = useRef2(onChange);
|
|
171
321
|
onChangeRef.current = onChange;
|
|
172
|
-
|
|
322
|
+
useEffect5(() => {
|
|
173
323
|
if (initialMarkdown && initialMarkdown !== lastMarkdownRef.current) {
|
|
174
324
|
lastMarkdownRef.current = initialMarkdown;
|
|
175
325
|
editor.update(() => {
|
|
@@ -177,7 +327,7 @@ function MarkdownSyncPlugin({
|
|
|
177
327
|
}, { tag: "initial-load" });
|
|
178
328
|
}
|
|
179
329
|
}, [editor, initialMarkdown]);
|
|
180
|
-
|
|
330
|
+
useEffect5(() => {
|
|
181
331
|
return editor.registerUpdateListener(
|
|
182
332
|
({ editorState, dirtyElements, dirtyLeaves, tags }) => {
|
|
183
333
|
if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;
|
|
@@ -192,9 +342,1943 @@ function MarkdownSyncPlugin({
|
|
|
192
342
|
return null;
|
|
193
343
|
}
|
|
194
344
|
|
|
195
|
-
// src/
|
|
345
|
+
// src/plugins/OutlinePlugin.tsx
|
|
346
|
+
import { useCallback as useCallback2, useEffect as useEffect6, useState as useState2 } from "react";
|
|
347
|
+
import { useLexicalComposerContext as useLexicalComposerContext6 } from "@lexical/react/LexicalComposerContext";
|
|
348
|
+
import {
|
|
349
|
+
TableOfContentsPlugin
|
|
350
|
+
} from "@lexical/react/LexicalTableOfContentsPlugin";
|
|
351
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
352
|
+
var INDENT_PER_LEVEL = {
|
|
353
|
+
h1: 0,
|
|
354
|
+
h2: 1,
|
|
355
|
+
h3: 2,
|
|
356
|
+
h4: 3,
|
|
357
|
+
h5: 4,
|
|
358
|
+
h6: 5
|
|
359
|
+
};
|
|
360
|
+
function OutlinePlugin() {
|
|
361
|
+
const [editor] = useLexicalComposerContext6();
|
|
362
|
+
const [collapsed, setCollapsed] = useState2(false);
|
|
363
|
+
const [activeKey, setActiveKey] = useState2(null);
|
|
364
|
+
useEffect6(() => {
|
|
365
|
+
const rootElement = editor.getRootElement();
|
|
366
|
+
const scroller = rootElement?.closest(".zui-text-editor");
|
|
367
|
+
if (!rootElement || !(scroller instanceof HTMLElement)) return;
|
|
368
|
+
let frame = 0;
|
|
369
|
+
const updateActive = () => {
|
|
370
|
+
cancelAnimationFrame(frame);
|
|
371
|
+
frame = requestAnimationFrame(() => {
|
|
372
|
+
const headings = rootElement.querySelectorAll(
|
|
373
|
+
"h1, h2, h3, h4, h5, h6"
|
|
374
|
+
);
|
|
375
|
+
const threshold = scroller.scrollTop + 80;
|
|
376
|
+
let current = null;
|
|
377
|
+
for (const el of headings) {
|
|
378
|
+
if (el.offsetTop <= threshold) {
|
|
379
|
+
current = el.getAttribute("data-outline-key");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
setActiveKey(current);
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
updateActive();
|
|
386
|
+
scroller.addEventListener("scroll", updateActive, { passive: true });
|
|
387
|
+
const unregister = editor.registerUpdateListener(updateActive);
|
|
388
|
+
return () => {
|
|
389
|
+
cancelAnimationFrame(frame);
|
|
390
|
+
scroller.removeEventListener("scroll", updateActive);
|
|
391
|
+
unregister();
|
|
392
|
+
};
|
|
393
|
+
}, [editor]);
|
|
394
|
+
const scrollTo = useCallback2(
|
|
395
|
+
(key) => {
|
|
396
|
+
editor.getEditorState().read(() => {
|
|
397
|
+
const element = editor.getElementByKey(key);
|
|
398
|
+
element?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
[editor]
|
|
402
|
+
);
|
|
403
|
+
return /* @__PURE__ */ jsx2(TableOfContentsPlugin, { children: (entries) => {
|
|
404
|
+
for (const [key] of entries) {
|
|
405
|
+
editor.getElementByKey(key)?.setAttribute("data-outline-key", key);
|
|
406
|
+
}
|
|
407
|
+
return /* @__PURE__ */ jsxs(
|
|
408
|
+
"div",
|
|
409
|
+
{
|
|
410
|
+
className: `zui-text-editor-outline ${collapsed ? "is-collapsed" : ""}`,
|
|
411
|
+
children: [
|
|
412
|
+
/* @__PURE__ */ jsx2(
|
|
413
|
+
"button",
|
|
414
|
+
{
|
|
415
|
+
type: "button",
|
|
416
|
+
className: "zui-text-editor-outline-toggle",
|
|
417
|
+
title: collapsed ? "Show outline" : "Hide outline",
|
|
418
|
+
"aria-label": collapsed ? "Show outline" : "Hide outline",
|
|
419
|
+
"aria-expanded": !collapsed,
|
|
420
|
+
onClick: () => setCollapsed((c) => !c),
|
|
421
|
+
children: /* @__PURE__ */ jsx2(
|
|
422
|
+
"svg",
|
|
423
|
+
{
|
|
424
|
+
viewBox: "0 0 20 20",
|
|
425
|
+
width: "14",
|
|
426
|
+
height: "14",
|
|
427
|
+
fill: "none",
|
|
428
|
+
stroke: "currentColor",
|
|
429
|
+
strokeWidth: "1.8",
|
|
430
|
+
strokeLinecap: "round",
|
|
431
|
+
children: /* @__PURE__ */ jsx2("path", { d: "M4 5h12M4 10h8M4 15h10" })
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
),
|
|
436
|
+
!collapsed && /* @__PURE__ */ jsxs("nav", { className: "zui-text-editor-outline-list", "aria-label": "Table of contents", children: [
|
|
437
|
+
entries.length === 0 && /* @__PURE__ */ jsx2("div", { className: "zui-text-editor-outline-empty", children: "No headings" }),
|
|
438
|
+
entries.map(([key, text, tag]) => /* @__PURE__ */ jsx2(
|
|
439
|
+
"button",
|
|
440
|
+
{
|
|
441
|
+
type: "button",
|
|
442
|
+
className: `zui-text-editor-outline-item ${key === activeKey ? "is-active" : ""}`,
|
|
443
|
+
style: {
|
|
444
|
+
paddingLeft: `${0.5 + INDENT_PER_LEVEL[tag] * 0.75}rem`
|
|
445
|
+
},
|
|
446
|
+
onClick: () => scrollTo(key),
|
|
447
|
+
children: text || "Untitled"
|
|
448
|
+
},
|
|
449
|
+
key
|
|
450
|
+
))
|
|
451
|
+
] })
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
} });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/plugins/ToolbarPlugin.tsx
|
|
459
|
+
import { useCallback as useCallback4, useEffect as useEffect8, useState as useState4 } from "react";
|
|
460
|
+
import {
|
|
461
|
+
$getSelection as $getSelection3,
|
|
462
|
+
$isRangeSelection as $isRangeSelection3,
|
|
463
|
+
FORMAT_TEXT_COMMAND
|
|
464
|
+
} from "lexical";
|
|
465
|
+
import { useLexicalComposerContext as useLexicalComposerContext8 } from "@lexical/react/LexicalComposerContext";
|
|
466
|
+
import { INSERT_TABLE_COMMAND } from "@lexical/table";
|
|
467
|
+
import { $insertNodeToNearestRoot } from "@lexical/utils";
|
|
468
|
+
|
|
469
|
+
// src/nodes/DrawingNode.tsx
|
|
196
470
|
import {
|
|
197
471
|
$applyNodeReplacement,
|
|
472
|
+
DecoratorNode
|
|
473
|
+
} from "lexical";
|
|
474
|
+
|
|
475
|
+
// src/components/DrawingCanvas.tsx
|
|
476
|
+
import {
|
|
477
|
+
useCallback as useCallback3,
|
|
478
|
+
useEffect as useEffect7,
|
|
479
|
+
useRef as useRef3,
|
|
480
|
+
useState as useState3
|
|
481
|
+
} from "react";
|
|
482
|
+
import { $getNodeByKey as $getNodeByKey2 } from "lexical";
|
|
483
|
+
import { useLexicalComposerContext as useLexicalComposerContext7 } from "@lexical/react/LexicalComposerContext";
|
|
484
|
+
import { useLexicalEditable } from "@lexical/react/useLexicalEditable";
|
|
485
|
+
|
|
486
|
+
// src/components/drawingTypes.ts
|
|
487
|
+
var BOX_TYPES = ["rect", "ellipse", "triangle", "pentagon"];
|
|
488
|
+
function isBoxType(type) {
|
|
489
|
+
return BOX_TYPES.includes(type);
|
|
490
|
+
}
|
|
491
|
+
function isConnectorType(type) {
|
|
492
|
+
return type === "arrow" || type === "line";
|
|
493
|
+
}
|
|
494
|
+
var DEFAULT_DRAWING_HEIGHT = 320;
|
|
495
|
+
var FONT_SIZE = 15;
|
|
496
|
+
var SMALL_FONT_SIZE = 11;
|
|
497
|
+
var LINE_HEIGHT = 1.35;
|
|
498
|
+
var EMPTY_DRAWING = {
|
|
499
|
+
version: 1,
|
|
500
|
+
height: DEFAULT_DRAWING_HEIGHT,
|
|
501
|
+
shapes: []
|
|
502
|
+
};
|
|
503
|
+
var STROKE_COLORS = [
|
|
504
|
+
"#1e1e1e",
|
|
505
|
+
"#e03131",
|
|
506
|
+
"#2f9e44",
|
|
507
|
+
"#1971c2",
|
|
508
|
+
"#f08c00"
|
|
509
|
+
];
|
|
510
|
+
var FILL_COLORS = [
|
|
511
|
+
"transparent",
|
|
512
|
+
"#ffc9c9",
|
|
513
|
+
"#b2f2bb",
|
|
514
|
+
"#a5d8ff",
|
|
515
|
+
"#ffec99"
|
|
516
|
+
];
|
|
517
|
+
var shapeIdCounter = 0;
|
|
518
|
+
function createShapeId() {
|
|
519
|
+
shapeIdCounter += 1;
|
|
520
|
+
return `s${Date.now().toString(36)}${shapeIdCounter.toString(36)}`;
|
|
521
|
+
}
|
|
522
|
+
function serializeDrawingData(data) {
|
|
523
|
+
return JSON.stringify(data);
|
|
524
|
+
}
|
|
525
|
+
function parseDrawingData(json) {
|
|
526
|
+
try {
|
|
527
|
+
const parsed = JSON.parse(json);
|
|
528
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.shapes)) {
|
|
529
|
+
return EMPTY_DRAWING;
|
|
530
|
+
}
|
|
531
|
+
const raw = parsed;
|
|
532
|
+
const shapes = raw.shapes.filter(isValidShape);
|
|
533
|
+
return {
|
|
534
|
+
version: 1,
|
|
535
|
+
height: typeof raw.height === "number" && raw.height >= 80 ? raw.height : DEFAULT_DRAWING_HEIGHT,
|
|
536
|
+
shapes
|
|
537
|
+
};
|
|
538
|
+
} catch {
|
|
539
|
+
return EMPTY_DRAWING;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function isValidShape(value) {
|
|
543
|
+
if (typeof value !== "object" || value === null) return false;
|
|
544
|
+
const shape = value;
|
|
545
|
+
return typeof shape.id === "string" && ["rect", "ellipse", "triangle", "pentagon", "arrow", "line", "text"].includes(
|
|
546
|
+
shape.type
|
|
547
|
+
) && typeof shape.x === "number" && typeof shape.y === "number" && typeof shape.w === "number" && typeof shape.h === "number" && typeof shape.stroke === "string" && typeof shape.fill === "string" && typeof shape.strokeWidth === "number" && (shape.text === void 0 || typeof shape.text === "string") && (shape.label === void 0 || typeof shape.label === "string") && (shape.footer === void 0 || typeof shape.footer === "string") && (shape.startBinding === void 0 || typeof shape.startBinding === "string") && (shape.endBinding === void 0 || typeof shape.endBinding === "string") && (shape.bidirectional === void 0 || typeof shape.bidirectional === "boolean") && (shape.routing === void 0 || shape.routing === "elbow") && (shape.elbow === void 0 || typeof shape.elbow === "number") && (shape.waypoints === void 0 || Array.isArray(shape.waypoints) && shape.waypoints.every(
|
|
548
|
+
(p) => typeof p === "object" && p !== null && typeof p.x === "number" && typeof p.y === "number"
|
|
549
|
+
));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/components/drawingGeometry.ts
|
|
553
|
+
var BINDING_GAP = 6;
|
|
554
|
+
var BINDING_TOLERANCE = 8;
|
|
555
|
+
function bbox(shape) {
|
|
556
|
+
return {
|
|
557
|
+
x: Math.min(shape.x, shape.x + shape.w),
|
|
558
|
+
y: Math.min(shape.y, shape.y + shape.h),
|
|
559
|
+
w: Math.abs(shape.w),
|
|
560
|
+
h: Math.abs(shape.h)
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function normalize(shape) {
|
|
564
|
+
if (isConnectorType(shape.type)) return shape;
|
|
565
|
+
const b = bbox(shape);
|
|
566
|
+
return { ...shape, x: b.x, y: b.y, w: b.w, h: b.h };
|
|
567
|
+
}
|
|
568
|
+
function polygonPoints(shape) {
|
|
569
|
+
const b = bbox(shape);
|
|
570
|
+
if (shape.type === "triangle") {
|
|
571
|
+
return [
|
|
572
|
+
{ x: b.x + b.w / 2, y: b.y },
|
|
573
|
+
{ x: b.x + b.w, y: b.y + b.h },
|
|
574
|
+
{ x: b.x, y: b.y + b.h }
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
const cx = b.x + b.w / 2;
|
|
578
|
+
const cy = b.y + b.h / 2;
|
|
579
|
+
return Array.from({ length: 5 }, (_, i) => {
|
|
580
|
+
const angle = -Math.PI / 2 + i * 2 * Math.PI / 5;
|
|
581
|
+
return {
|
|
582
|
+
x: cx + b.w / 2 * Math.cos(angle),
|
|
583
|
+
y: cy + b.h / 2 * Math.sin(angle)
|
|
584
|
+
};
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
function center(shape) {
|
|
588
|
+
const b = bbox(shape);
|
|
589
|
+
return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
590
|
+
}
|
|
591
|
+
function wrapText(text, maxWidth, fontSize) {
|
|
592
|
+
const charW = fontSize * 0.6;
|
|
593
|
+
const maxChars = Math.max(1, Math.floor(maxWidth / charW));
|
|
594
|
+
const lines = [];
|
|
595
|
+
for (const para of text.split("\n")) {
|
|
596
|
+
if (para.length <= maxChars) {
|
|
597
|
+
lines.push(para);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
let current = "";
|
|
601
|
+
for (let word of para.split(" ")) {
|
|
602
|
+
while (word.length > maxChars) {
|
|
603
|
+
if (current) {
|
|
604
|
+
lines.push(current);
|
|
605
|
+
current = "";
|
|
606
|
+
}
|
|
607
|
+
lines.push(word.slice(0, maxChars));
|
|
608
|
+
word = word.slice(maxChars);
|
|
609
|
+
}
|
|
610
|
+
if (!current) {
|
|
611
|
+
current = word;
|
|
612
|
+
} else if (current.length + 1 + word.length <= maxChars) {
|
|
613
|
+
current += ` ${word}`;
|
|
614
|
+
} else {
|
|
615
|
+
lines.push(current);
|
|
616
|
+
current = word;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
lines.push(current);
|
|
620
|
+
}
|
|
621
|
+
return lines;
|
|
622
|
+
}
|
|
623
|
+
function textBoxSize(text, fontSize = FONT_SIZE) {
|
|
624
|
+
const lines = text.split("\n");
|
|
625
|
+
const longest = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
626
|
+
return {
|
|
627
|
+
w: Math.max(20, longest * fontSize * 0.6),
|
|
628
|
+
h: lines.length * fontSize * LINE_HEIGHT
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function arrowHead(x, y, angle, length) {
|
|
632
|
+
const spread = Math.PI / 7;
|
|
633
|
+
const hx1 = x - length * Math.cos(angle - spread);
|
|
634
|
+
const hy1 = y - length * Math.sin(angle - spread);
|
|
635
|
+
const hx2 = x - length * Math.cos(angle + spread);
|
|
636
|
+
const hy2 = y - length * Math.sin(angle + spread);
|
|
637
|
+
return `M ${hx1} ${hy1} L ${x} ${y} L ${hx2} ${hy2}`;
|
|
638
|
+
}
|
|
639
|
+
function connectorPoints(shape) {
|
|
640
|
+
const p1 = { x: shape.x, y: shape.y };
|
|
641
|
+
const p2 = { x: shape.x + shape.w, y: shape.y + shape.h };
|
|
642
|
+
if (shape.waypoints?.length) return [p1, ...shape.waypoints, p2];
|
|
643
|
+
if (shape.routing !== "elbow") return [p1, p2];
|
|
644
|
+
const t = Math.min(1, Math.max(0, shape.elbow ?? 0.5));
|
|
645
|
+
if (Math.abs(shape.w) >= Math.abs(shape.h)) {
|
|
646
|
+
const midX = shape.x + shape.w * t;
|
|
647
|
+
return [p1, { x: midX, y: p1.y }, { x: midX, y: p2.y }, p2];
|
|
648
|
+
}
|
|
649
|
+
const midY = shape.y + shape.h * t;
|
|
650
|
+
return [p1, { x: p1.x, y: midY }, { x: p2.x, y: midY }, p2];
|
|
651
|
+
}
|
|
652
|
+
function connectorPath(shape) {
|
|
653
|
+
const points = connectorPoints(shape);
|
|
654
|
+
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
|
|
655
|
+
}
|
|
656
|
+
function connectorMidpoint(shape) {
|
|
657
|
+
const points = connectorPoints(shape);
|
|
658
|
+
const a = points[Math.floor((points.length - 1) / 2)];
|
|
659
|
+
const b = points[Math.ceil((points.length + 1) / 2) - 1];
|
|
660
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
661
|
+
}
|
|
662
|
+
function segmentAngleAt(points, index, backward) {
|
|
663
|
+
const step = backward ? 1 : -1;
|
|
664
|
+
for (let i = index + step; i >= 0 && i < points.length; i += step) {
|
|
665
|
+
const dx = points[index].x - points[i].x;
|
|
666
|
+
const dy = points[index].y - points[i].y;
|
|
667
|
+
if (Math.abs(dx) > 0.01 || Math.abs(dy) > 0.01) {
|
|
668
|
+
return Math.atan2(dy, dx);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return 0;
|
|
672
|
+
}
|
|
673
|
+
function arrowPath(shape) {
|
|
674
|
+
const points = connectorPoints(shape);
|
|
675
|
+
const line = connectorPath(shape);
|
|
676
|
+
const length = Math.hypot(shape.w, shape.h);
|
|
677
|
+
if (length < 1) return line;
|
|
678
|
+
const headLength = Math.min(14, 4 + length / 4);
|
|
679
|
+
const last = points.length - 1;
|
|
680
|
+
const parts = [
|
|
681
|
+
line,
|
|
682
|
+
arrowHead(
|
|
683
|
+
points[last].x,
|
|
684
|
+
points[last].y,
|
|
685
|
+
segmentAngleAt(points, last, false),
|
|
686
|
+
headLength
|
|
687
|
+
)
|
|
688
|
+
];
|
|
689
|
+
if (shape.bidirectional) {
|
|
690
|
+
parts.push(
|
|
691
|
+
arrowHead(
|
|
692
|
+
points[0].x,
|
|
693
|
+
points[0].y,
|
|
694
|
+
segmentAngleAt(points, 0, true),
|
|
695
|
+
headLength
|
|
696
|
+
)
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return parts.join(" ");
|
|
700
|
+
}
|
|
701
|
+
function boxContainsPoint(box, point) {
|
|
702
|
+
const b = bbox(box);
|
|
703
|
+
const pad = BINDING_TOLERANCE;
|
|
704
|
+
if (box.type === "ellipse") {
|
|
705
|
+
const rx = b.w / 2 + pad;
|
|
706
|
+
const ry = b.h / 2 + pad;
|
|
707
|
+
if (rx <= 0 || ry <= 0) return false;
|
|
708
|
+
const dx = (point.x - (b.x + b.w / 2)) / rx;
|
|
709
|
+
const dy = (point.y - (b.y + b.h / 2)) / ry;
|
|
710
|
+
return dx * dx + dy * dy <= 1;
|
|
711
|
+
}
|
|
712
|
+
return point.x >= b.x - pad && point.x <= b.x + b.w + pad && point.y >= b.y - pad && point.y <= b.y + b.h + pad;
|
|
713
|
+
}
|
|
714
|
+
function findBoxAt(shapes, point, excludeId) {
|
|
715
|
+
for (let i = shapes.length - 1; i >= 0; i--) {
|
|
716
|
+
const shape = shapes[i];
|
|
717
|
+
if (shape.id === excludeId || !isBoxType(shape.type)) continue;
|
|
718
|
+
if (boxContainsPoint(shape, point)) return shape;
|
|
719
|
+
}
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
function borderPoint(box, toward) {
|
|
723
|
+
const c = center(box);
|
|
724
|
+
const dx = toward.x - c.x;
|
|
725
|
+
const dy = toward.y - c.y;
|
|
726
|
+
const dist = Math.hypot(dx, dy);
|
|
727
|
+
if (dist < 1) return c;
|
|
728
|
+
let t;
|
|
729
|
+
const b = bbox(box);
|
|
730
|
+
if (box.type === "ellipse") {
|
|
731
|
+
const rx = b.w / 2;
|
|
732
|
+
const ry = b.h / 2;
|
|
733
|
+
if (rx <= 0 || ry <= 0) return c;
|
|
734
|
+
t = 1 / Math.hypot(dx / rx, dy / ry);
|
|
735
|
+
} else if (box.type === "triangle" || box.type === "pentagon") {
|
|
736
|
+
t = rayPolygonT(c, { x: dx, y: dy }, polygonPoints(box)) ?? 0.5;
|
|
737
|
+
} else {
|
|
738
|
+
const sx = dx !== 0 ? b.w / 2 / Math.abs(dx) : Infinity;
|
|
739
|
+
const sy = dy !== 0 ? b.h / 2 / Math.abs(dy) : Infinity;
|
|
740
|
+
t = Math.min(sx, sy);
|
|
741
|
+
}
|
|
742
|
+
const gapT = BINDING_GAP / dist;
|
|
743
|
+
return { x: c.x + dx * (t + gapT), y: c.y + dy * (t + gapT) };
|
|
744
|
+
}
|
|
745
|
+
function rayPolygonT(origin, dir, points) {
|
|
746
|
+
let best = null;
|
|
747
|
+
for (let i = 0; i < points.length; i++) {
|
|
748
|
+
const p1 = points[i];
|
|
749
|
+
const p2 = points[(i + 1) % points.length];
|
|
750
|
+
const ex = p2.x - p1.x;
|
|
751
|
+
const ey = p2.y - p1.y;
|
|
752
|
+
const denom = dir.x * ey - dir.y * ex;
|
|
753
|
+
if (Math.abs(denom) < 1e-9) continue;
|
|
754
|
+
const qx = p1.x - origin.x;
|
|
755
|
+
const qy = p1.y - origin.y;
|
|
756
|
+
const t = (qx * ey - qy * ex) / denom;
|
|
757
|
+
const s = (qx * dir.y - qy * dir.x) / denom;
|
|
758
|
+
if (t > 0 && s >= 0 && s <= 1 && (best === null || t < best)) {
|
|
759
|
+
best = t;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return best;
|
|
763
|
+
}
|
|
764
|
+
function resolveBindings(shapes) {
|
|
765
|
+
const byId = new Map(shapes.map((s) => [s.id, s]));
|
|
766
|
+
return shapes.map((shape) => {
|
|
767
|
+
if (shape.type !== "arrow" && shape.type !== "line") return shape;
|
|
768
|
+
if (!shape.startBinding && !shape.endBinding) return shape;
|
|
769
|
+
const startBox = shape.startBinding ? byId.get(shape.startBinding) : void 0;
|
|
770
|
+
const endBox = shape.endBinding ? byId.get(shape.endBinding) : void 0;
|
|
771
|
+
if (!startBox && !endBox) {
|
|
772
|
+
return { ...shape, startBinding: void 0, endBinding: void 0 };
|
|
773
|
+
}
|
|
774
|
+
const freeStart = { x: shape.x, y: shape.y };
|
|
775
|
+
const freeEnd = { x: shape.x + shape.w, y: shape.y + shape.h };
|
|
776
|
+
const waypoints = shape.waypoints;
|
|
777
|
+
const startTarget = waypoints?.length ? waypoints[0] : endBox ? center(endBox) : freeEnd;
|
|
778
|
+
const endTarget = waypoints?.length ? waypoints[waypoints.length - 1] : startBox ? center(startBox) : freeStart;
|
|
779
|
+
const p1 = startBox ? borderPoint(startBox, startTarget) : freeStart;
|
|
780
|
+
const p2 = endBox ? borderPoint(endBox, endTarget) : freeEnd;
|
|
781
|
+
return {
|
|
782
|
+
...shape,
|
|
783
|
+
x: p1.x,
|
|
784
|
+
y: p1.y,
|
|
785
|
+
w: p2.x - p1.x,
|
|
786
|
+
h: p2.y - p1.y,
|
|
787
|
+
startBinding: startBox ? shape.startBinding : void 0,
|
|
788
|
+
endBinding: endBox ? shape.endBinding : void 0
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/components/DrawingCanvas.tsx
|
|
794
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
795
|
+
import { createElement } from "react";
|
|
796
|
+
var MIN_HEIGHT = 120;
|
|
797
|
+
var MAX_HEIGHT = 900;
|
|
798
|
+
var SELECTION_COLOR = "#6965db";
|
|
799
|
+
var CONNECTOR_FONT_SIZE = 12;
|
|
800
|
+
var CONNECTOR_LABEL_MAX_WIDTH = 160;
|
|
801
|
+
var BOX_TEXT_PADDING = 8;
|
|
802
|
+
var WAYPOINT_SNAP = 8;
|
|
803
|
+
function BoxTexts({
|
|
804
|
+
shape,
|
|
805
|
+
hideField,
|
|
806
|
+
showHints
|
|
807
|
+
}) {
|
|
808
|
+
const b = bbox(shape);
|
|
809
|
+
const cx = b.x + b.w / 2;
|
|
810
|
+
const wrapW = Math.max(b.w - BOX_TEXT_PADDING * 2, 20);
|
|
811
|
+
const mainLines = wrapText(shape.text ?? "", wrapW, FONT_SIZE);
|
|
812
|
+
const labelLines = wrapText(shape.label ?? "", wrapW, SMALL_FONT_SIZE);
|
|
813
|
+
const footerLines = wrapText(shape.footer ?? "", wrapW, SMALL_FONT_SIZE);
|
|
814
|
+
const mainLineH = FONT_SIZE * LINE_HEIGHT;
|
|
815
|
+
const smallLineH = SMALL_FONT_SIZE * LINE_HEIGHT;
|
|
816
|
+
const mainStart = b.y + b.h / 2 - (mainLines.length - 1) * mainLineH / 2 + FONT_SIZE * 0.35;
|
|
817
|
+
const common = {
|
|
818
|
+
fill: shape.stroke,
|
|
819
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
820
|
+
textAnchor: "middle",
|
|
821
|
+
style: { userSelect: "none" }
|
|
822
|
+
};
|
|
823
|
+
const hint = {
|
|
824
|
+
...common,
|
|
825
|
+
opacity: 0.35,
|
|
826
|
+
style: { userSelect: "none", pointerEvents: "none" }
|
|
827
|
+
};
|
|
828
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
829
|
+
hideField !== "label" && (shape.label ? labelLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
830
|
+
"text",
|
|
831
|
+
{
|
|
832
|
+
...common,
|
|
833
|
+
key: i,
|
|
834
|
+
x: cx,
|
|
835
|
+
y: b.y + SMALL_FONT_SIZE + 7 + i * smallLineH,
|
|
836
|
+
fontSize: SMALL_FONT_SIZE,
|
|
837
|
+
fontWeight: 600,
|
|
838
|
+
opacity: 0.85
|
|
839
|
+
},
|
|
840
|
+
line
|
|
841
|
+
)) : showHints && /* @__PURE__ */ jsx3("text", { ...hint, x: cx, y: b.y + SMALL_FONT_SIZE + 7, fontSize: SMALL_FONT_SIZE, children: "Label" })),
|
|
842
|
+
hideField !== "text" && (shape.text ? mainLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
843
|
+
"text",
|
|
844
|
+
{
|
|
845
|
+
...common,
|
|
846
|
+
key: i,
|
|
847
|
+
x: cx,
|
|
848
|
+
y: mainStart + i * mainLineH,
|
|
849
|
+
fontSize: FONT_SIZE
|
|
850
|
+
},
|
|
851
|
+
line
|
|
852
|
+
)) : showHints && /* @__PURE__ */ jsx3("text", { ...hint, x: cx, y: mainStart, fontSize: FONT_SIZE, children: "Text" })),
|
|
853
|
+
hideField !== "footer" && (shape.footer ? footerLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
854
|
+
"text",
|
|
855
|
+
{
|
|
856
|
+
...common,
|
|
857
|
+
key: i,
|
|
858
|
+
x: cx,
|
|
859
|
+
y: b.y + b.h - 9 - (footerLines.length - 1 - i) * smallLineH,
|
|
860
|
+
fontSize: SMALL_FONT_SIZE,
|
|
861
|
+
opacity: 0.65
|
|
862
|
+
},
|
|
863
|
+
line
|
|
864
|
+
)) : showHints && /* @__PURE__ */ jsx3("text", { ...hint, x: cx, y: b.y + b.h - 9, fontSize: SMALL_FONT_SIZE, children: "Footer" }))
|
|
865
|
+
] });
|
|
866
|
+
}
|
|
867
|
+
function ShapeView({
|
|
868
|
+
shape,
|
|
869
|
+
hideField,
|
|
870
|
+
showHints
|
|
871
|
+
}) {
|
|
872
|
+
const common = {
|
|
873
|
+
stroke: shape.stroke,
|
|
874
|
+
strokeWidth: shape.strokeWidth,
|
|
875
|
+
strokeLinecap: "round",
|
|
876
|
+
strokeLinejoin: "round"
|
|
877
|
+
};
|
|
878
|
+
switch (shape.type) {
|
|
879
|
+
case "rect": {
|
|
880
|
+
const b = bbox(shape);
|
|
881
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
882
|
+
/* @__PURE__ */ jsx3(
|
|
883
|
+
"rect",
|
|
884
|
+
{
|
|
885
|
+
...common,
|
|
886
|
+
x: b.x,
|
|
887
|
+
y: b.y,
|
|
888
|
+
width: b.w,
|
|
889
|
+
height: b.h,
|
|
890
|
+
rx: Math.min(8, b.w / 4, b.h / 4),
|
|
891
|
+
fill: shape.fill
|
|
892
|
+
}
|
|
893
|
+
),
|
|
894
|
+
/* @__PURE__ */ jsx3(BoxTexts, { shape, hideField, showHints })
|
|
895
|
+
] });
|
|
896
|
+
}
|
|
897
|
+
case "ellipse": {
|
|
898
|
+
const b = bbox(shape);
|
|
899
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
900
|
+
/* @__PURE__ */ jsx3(
|
|
901
|
+
"ellipse",
|
|
902
|
+
{
|
|
903
|
+
...common,
|
|
904
|
+
cx: b.x + b.w / 2,
|
|
905
|
+
cy: b.y + b.h / 2,
|
|
906
|
+
rx: b.w / 2,
|
|
907
|
+
ry: b.h / 2,
|
|
908
|
+
fill: shape.fill
|
|
909
|
+
}
|
|
910
|
+
),
|
|
911
|
+
/* @__PURE__ */ jsx3(BoxTexts, { shape, hideField, showHints })
|
|
912
|
+
] });
|
|
913
|
+
}
|
|
914
|
+
case "triangle":
|
|
915
|
+
case "pentagon":
|
|
916
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
917
|
+
/* @__PURE__ */ jsx3(
|
|
918
|
+
"polygon",
|
|
919
|
+
{
|
|
920
|
+
...common,
|
|
921
|
+
points: polygonPoints(shape).map((p) => `${p.x},${p.y}`).join(" "),
|
|
922
|
+
fill: shape.fill
|
|
923
|
+
}
|
|
924
|
+
),
|
|
925
|
+
/* @__PURE__ */ jsx3(BoxTexts, { shape, hideField, showHints })
|
|
926
|
+
] });
|
|
927
|
+
case "line":
|
|
928
|
+
case "arrow":
|
|
929
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
930
|
+
/* @__PURE__ */ jsx3(
|
|
931
|
+
"path",
|
|
932
|
+
{
|
|
933
|
+
...common,
|
|
934
|
+
d: shape.type === "line" ? connectorPath(shape) : arrowPath(shape),
|
|
935
|
+
fill: "none"
|
|
936
|
+
}
|
|
937
|
+
),
|
|
938
|
+
shape.text && hideField !== "text" && /* @__PURE__ */ jsx3(ConnectorLabel, { shape })
|
|
939
|
+
] });
|
|
940
|
+
case "text":
|
|
941
|
+
return /* @__PURE__ */ jsx3(
|
|
942
|
+
"text",
|
|
943
|
+
{
|
|
944
|
+
x: shape.x,
|
|
945
|
+
y: shape.y + FONT_SIZE,
|
|
946
|
+
fill: shape.stroke,
|
|
947
|
+
fontSize: FONT_SIZE,
|
|
948
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
949
|
+
style: { userSelect: "none", whiteSpace: "pre" },
|
|
950
|
+
children: (shape.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx3("tspan", { x: shape.x, dy: i === 0 ? 0 : FONT_SIZE * 1.35, children: line }, i))
|
|
951
|
+
}
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function ConnectorLabel({ shape }) {
|
|
956
|
+
const text = shape.text ?? "";
|
|
957
|
+
const lines = wrapText(text, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
|
|
958
|
+
const size = textBoxSize(lines.join("\n"), CONNECTOR_FONT_SIZE);
|
|
959
|
+
const { x: midX, y: midY } = connectorMidpoint(shape);
|
|
960
|
+
const lineH = CONNECTOR_FONT_SIZE * LINE_HEIGHT;
|
|
961
|
+
const startY = midY - (lines.length - 1) * lineH / 2 + CONNECTOR_FONT_SIZE * 0.35;
|
|
962
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
963
|
+
/* @__PURE__ */ jsx3(
|
|
964
|
+
"rect",
|
|
965
|
+
{
|
|
966
|
+
x: midX - size.w / 2 - 5,
|
|
967
|
+
y: midY - size.h / 2 - 3,
|
|
968
|
+
width: size.w + 10,
|
|
969
|
+
height: size.h + 6,
|
|
970
|
+
rx: 4,
|
|
971
|
+
fill: "#ffffff",
|
|
972
|
+
opacity: 0.92
|
|
973
|
+
}
|
|
974
|
+
),
|
|
975
|
+
lines.map((line, i) => /* @__PURE__ */ jsx3(
|
|
976
|
+
"text",
|
|
977
|
+
{
|
|
978
|
+
x: midX,
|
|
979
|
+
y: startY + i * lineH,
|
|
980
|
+
fill: shape.stroke,
|
|
981
|
+
fontSize: CONNECTOR_FONT_SIZE,
|
|
982
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
983
|
+
textAnchor: "middle",
|
|
984
|
+
style: { userSelect: "none" },
|
|
985
|
+
children: line
|
|
986
|
+
},
|
|
987
|
+
i
|
|
988
|
+
))
|
|
989
|
+
] });
|
|
990
|
+
}
|
|
991
|
+
var TOOL_ICONS = {
|
|
992
|
+
select: /* @__PURE__ */ jsx3("path", { d: "M4 2l12 6.5-5.2 1.6L8 16z", fill: "currentColor", stroke: "none" }),
|
|
993
|
+
rect: /* @__PURE__ */ jsx3("rect", { x: "3", y: "4.5", width: "14", height: "11", rx: "2" }),
|
|
994
|
+
ellipse: /* @__PURE__ */ jsx3("ellipse", { cx: "10", cy: "10", rx: "7", ry: "5.5" }),
|
|
995
|
+
triangle: /* @__PURE__ */ jsx3("path", { d: "M10 3.5L17 16.5H3z", strokeLinejoin: "round" }),
|
|
996
|
+
pentagon: /* @__PURE__ */ jsx3("path", { d: "M10 3L16.8 8.1L14.2 16.2H5.8L3.2 8.1z", strokeLinejoin: "round" }),
|
|
997
|
+
arrow: /* @__PURE__ */ jsxs2("g", { children: [
|
|
998
|
+
/* @__PURE__ */ jsx3("path", { d: "M4 16L16 4" }),
|
|
999
|
+
/* @__PURE__ */ jsx3("path", { d: "M9 4h7v7" })
|
|
1000
|
+
] }),
|
|
1001
|
+
line: /* @__PURE__ */ jsx3("path", { d: "M4 16L16 4" }),
|
|
1002
|
+
text: /* @__PURE__ */ jsxs2("g", { children: [
|
|
1003
|
+
/* @__PURE__ */ jsx3("path", { d: "M4 5V3.5h12V5" }),
|
|
1004
|
+
/* @__PURE__ */ jsx3("path", { d: "M10 3.5V16.5" }),
|
|
1005
|
+
/* @__PURE__ */ jsx3("path", { d: "M7 16.5h6" })
|
|
1006
|
+
] })
|
|
1007
|
+
};
|
|
1008
|
+
function bindEndpoints(shape, shapes) {
|
|
1009
|
+
const startBox = findBoxAt(shapes, { x: shape.x, y: shape.y }, shape.id);
|
|
1010
|
+
const endBox = findBoxAt(
|
|
1011
|
+
shapes,
|
|
1012
|
+
{ x: shape.x + shape.w, y: shape.y + shape.h },
|
|
1013
|
+
shape.id
|
|
1014
|
+
);
|
|
1015
|
+
if (startBox && endBox && startBox.id === endBox.id) return shape;
|
|
1016
|
+
return { ...shape, startBinding: startBox?.id, endBinding: endBox?.id };
|
|
1017
|
+
}
|
|
1018
|
+
var TOOLS = [
|
|
1019
|
+
{ tool: "select", label: "Select" },
|
|
1020
|
+
{ tool: "rect", label: "Rectangle" },
|
|
1021
|
+
{ tool: "ellipse", label: "Ellipse" },
|
|
1022
|
+
{ tool: "triangle", label: "Triangle" },
|
|
1023
|
+
{ tool: "pentagon", label: "Pentagon" },
|
|
1024
|
+
{ tool: "arrow", label: "Arrow" },
|
|
1025
|
+
{ tool: "line", label: "Line" },
|
|
1026
|
+
{ tool: "text", label: "Text" }
|
|
1027
|
+
];
|
|
1028
|
+
function DrawingCanvas({ nodeKey, data }) {
|
|
1029
|
+
const [editor] = useLexicalComposerContext7();
|
|
1030
|
+
const isEditable = useLexicalEditable();
|
|
1031
|
+
const [shapes, setShapes] = useState3(data.shapes);
|
|
1032
|
+
const [height, setHeight] = useState3(data.height);
|
|
1033
|
+
const [tool, setTool] = useState3("select");
|
|
1034
|
+
const [selectedId, setSelectedId] = useState3(null);
|
|
1035
|
+
const [editingText, setEditingText] = useState3(null);
|
|
1036
|
+
const [stroke, setStroke] = useState3(STROKE_COLORS[0]);
|
|
1037
|
+
const [fill, setFill] = useState3(FILL_COLORS[0]);
|
|
1038
|
+
const svgRef = useRef3(null);
|
|
1039
|
+
const dragRef = useRef3(null);
|
|
1040
|
+
const lastCommittedRef = useRef3(serializeDrawingData(data));
|
|
1041
|
+
const shapesRef = useRef3(shapes);
|
|
1042
|
+
shapesRef.current = shapes;
|
|
1043
|
+
const heightRef = useRef3(height);
|
|
1044
|
+
heightRef.current = height;
|
|
1045
|
+
useEffect7(() => {
|
|
1046
|
+
const incoming = serializeDrawingData(data);
|
|
1047
|
+
if (incoming !== lastCommittedRef.current) {
|
|
1048
|
+
lastCommittedRef.current = incoming;
|
|
1049
|
+
setShapes(data.shapes);
|
|
1050
|
+
setHeight(data.height);
|
|
1051
|
+
setSelectedId(null);
|
|
1052
|
+
setEditingText(null);
|
|
1053
|
+
}
|
|
1054
|
+
}, [data]);
|
|
1055
|
+
const commit = useCallback3(
|
|
1056
|
+
(nextShapes, nextHeight) => {
|
|
1057
|
+
const payload = {
|
|
1058
|
+
version: 1,
|
|
1059
|
+
height: nextHeight ?? heightRef.current,
|
|
1060
|
+
shapes: nextShapes
|
|
1061
|
+
};
|
|
1062
|
+
const json = serializeDrawingData(payload);
|
|
1063
|
+
if (json === lastCommittedRef.current) return;
|
|
1064
|
+
lastCommittedRef.current = json;
|
|
1065
|
+
editor.update(() => {
|
|
1066
|
+
const node = $getNodeByKey2(nodeKey);
|
|
1067
|
+
if ($isDrawingNode(node)) {
|
|
1068
|
+
node.setData(payload);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
},
|
|
1072
|
+
[editor, nodeKey]
|
|
1073
|
+
);
|
|
1074
|
+
const updateShapes = useCallback3(
|
|
1075
|
+
(updater, options) => {
|
|
1076
|
+
setShapes((prev) => {
|
|
1077
|
+
const next = resolveBindings(updater(prev));
|
|
1078
|
+
if (options?.commit) commit(next);
|
|
1079
|
+
return next;
|
|
1080
|
+
});
|
|
1081
|
+
},
|
|
1082
|
+
[commit]
|
|
1083
|
+
);
|
|
1084
|
+
const getPoint = useCallback3((e) => {
|
|
1085
|
+
const rect = svgRef.current?.getBoundingClientRect();
|
|
1086
|
+
if (!rect) return { x: 0, y: 0 };
|
|
1087
|
+
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
1088
|
+
}, []);
|
|
1089
|
+
const selectedShape = shapes.find((s) => s.id === selectedId) ?? null;
|
|
1090
|
+
const startTextEditing = useCallback3((id, field) => {
|
|
1091
|
+
setEditingText({ id, field });
|
|
1092
|
+
setSelectedId(id);
|
|
1093
|
+
}, []);
|
|
1094
|
+
const handleBackgroundPointerDown = useCallback3(
|
|
1095
|
+
(e) => {
|
|
1096
|
+
if (!isEditable || editingText) return;
|
|
1097
|
+
if (e.button !== 0) return;
|
|
1098
|
+
const point = getPoint(e);
|
|
1099
|
+
svgRef.current?.setPointerCapture(e.pointerId);
|
|
1100
|
+
if (tool === "select") {
|
|
1101
|
+
setSelectedId(null);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (tool === "text") {
|
|
1105
|
+
const id2 = createShapeId();
|
|
1106
|
+
const size = textBoxSize("");
|
|
1107
|
+
updateShapes((prev) => [
|
|
1108
|
+
...prev,
|
|
1109
|
+
{
|
|
1110
|
+
id: id2,
|
|
1111
|
+
type: "text",
|
|
1112
|
+
x: point.x,
|
|
1113
|
+
y: point.y - FONT_SIZE / 2,
|
|
1114
|
+
w: size.w,
|
|
1115
|
+
h: size.h,
|
|
1116
|
+
stroke,
|
|
1117
|
+
fill: "transparent",
|
|
1118
|
+
strokeWidth: 2,
|
|
1119
|
+
text: ""
|
|
1120
|
+
}
|
|
1121
|
+
]);
|
|
1122
|
+
setTool("select");
|
|
1123
|
+
startTextEditing(id2, "text");
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const id = createShapeId();
|
|
1127
|
+
dragRef.current = { mode: "draw", id };
|
|
1128
|
+
updateShapes((prev) => [
|
|
1129
|
+
...prev,
|
|
1130
|
+
{
|
|
1131
|
+
id,
|
|
1132
|
+
type: tool,
|
|
1133
|
+
x: point.x,
|
|
1134
|
+
y: point.y,
|
|
1135
|
+
w: 0,
|
|
1136
|
+
h: 0,
|
|
1137
|
+
stroke,
|
|
1138
|
+
fill: isBoxType(tool) ? fill : "transparent",
|
|
1139
|
+
strokeWidth: 2
|
|
1140
|
+
}
|
|
1141
|
+
]);
|
|
1142
|
+
},
|
|
1143
|
+
[
|
|
1144
|
+
isEditable,
|
|
1145
|
+
editingText,
|
|
1146
|
+
getPoint,
|
|
1147
|
+
tool,
|
|
1148
|
+
stroke,
|
|
1149
|
+
fill,
|
|
1150
|
+
updateShapes,
|
|
1151
|
+
startTextEditing
|
|
1152
|
+
]
|
|
1153
|
+
);
|
|
1154
|
+
const handleShapePointerDown = useCallback3(
|
|
1155
|
+
(e, shape) => {
|
|
1156
|
+
if (!isEditable || tool !== "select" || editingText) return;
|
|
1157
|
+
if (e.button !== 0) return;
|
|
1158
|
+
e.stopPropagation();
|
|
1159
|
+
const point = getPoint(e);
|
|
1160
|
+
svgRef.current?.setPointerCapture(e.pointerId);
|
|
1161
|
+
dragRef.current = {
|
|
1162
|
+
mode: "move",
|
|
1163
|
+
id: shape.id,
|
|
1164
|
+
startX: point.x,
|
|
1165
|
+
startY: point.y,
|
|
1166
|
+
orig: shape,
|
|
1167
|
+
wasSelected: selectedId === shape.id
|
|
1168
|
+
};
|
|
1169
|
+
setSelectedId(shape.id);
|
|
1170
|
+
},
|
|
1171
|
+
[isEditable, tool, editingText, getPoint, selectedId]
|
|
1172
|
+
);
|
|
1173
|
+
const handleHandlePointerDown = useCallback3(
|
|
1174
|
+
(e, drag) => {
|
|
1175
|
+
if (!isEditable) return;
|
|
1176
|
+
if (e.button !== 0) return;
|
|
1177
|
+
e.stopPropagation();
|
|
1178
|
+
svgRef.current?.setPointerCapture(e.pointerId);
|
|
1179
|
+
dragRef.current = drag;
|
|
1180
|
+
if (drag.mode === "endpoint") {
|
|
1181
|
+
const key = drag.end === "start" ? "startBinding" : "endBinding";
|
|
1182
|
+
updateShapes(
|
|
1183
|
+
(prev) => prev.map((s) => s.id === drag.id ? { ...s, [key]: void 0 } : s)
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
[isEditable, updateShapes]
|
|
1188
|
+
);
|
|
1189
|
+
const handlePointerMove = useCallback3(
|
|
1190
|
+
(e) => {
|
|
1191
|
+
const drag = dragRef.current;
|
|
1192
|
+
if (!drag) return;
|
|
1193
|
+
const point = getPoint(e);
|
|
1194
|
+
if (drag.mode === "height") {
|
|
1195
|
+
const next = Math.round(
|
|
1196
|
+
Math.min(
|
|
1197
|
+
MAX_HEIGHT,
|
|
1198
|
+
Math.max(MIN_HEIGHT, drag.origHeight + (e.clientY - drag.startY))
|
|
1199
|
+
)
|
|
1200
|
+
);
|
|
1201
|
+
setHeight(next);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
updateShapes(
|
|
1205
|
+
(prev) => prev.map((shape) => {
|
|
1206
|
+
if (shape.id !== ("id" in drag ? drag.id : "")) return shape;
|
|
1207
|
+
switch (drag.mode) {
|
|
1208
|
+
case "draw":
|
|
1209
|
+
return { ...shape, w: point.x - shape.x, h: point.y - shape.y };
|
|
1210
|
+
case "move": {
|
|
1211
|
+
const dx = point.x - drag.startX;
|
|
1212
|
+
const dy = point.y - drag.startY;
|
|
1213
|
+
return {
|
|
1214
|
+
...shape,
|
|
1215
|
+
x: drag.orig.x + dx,
|
|
1216
|
+
y: drag.orig.y + dy,
|
|
1217
|
+
waypoints: drag.orig.waypoints?.map((p) => ({
|
|
1218
|
+
x: p.x + dx,
|
|
1219
|
+
y: p.y + dy
|
|
1220
|
+
}))
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
case "resize": {
|
|
1224
|
+
const o = drag.orig;
|
|
1225
|
+
const right = o.x + o.w;
|
|
1226
|
+
const bottom = o.y + o.h;
|
|
1227
|
+
const nx = drag.corner === "nw" || drag.corner === "sw" ? point.x : o.x;
|
|
1228
|
+
const ny = drag.corner === "nw" || drag.corner === "ne" ? point.y : o.y;
|
|
1229
|
+
const nr = drag.corner === "ne" || drag.corner === "se" ? point.x : right;
|
|
1230
|
+
const nb = drag.corner === "sw" || drag.corner === "se" ? point.y : bottom;
|
|
1231
|
+
return { ...shape, x: nx, y: ny, w: nr - nx, h: nb - ny };
|
|
1232
|
+
}
|
|
1233
|
+
case "endpoint": {
|
|
1234
|
+
const o = drag.orig;
|
|
1235
|
+
if (drag.end === "start") {
|
|
1236
|
+
return {
|
|
1237
|
+
...shape,
|
|
1238
|
+
x: point.x,
|
|
1239
|
+
y: point.y,
|
|
1240
|
+
w: o.x + o.w - point.x,
|
|
1241
|
+
h: o.y + o.h - point.y
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
return { ...shape, w: point.x - o.x, h: point.y - o.y };
|
|
1245
|
+
}
|
|
1246
|
+
case "elbow": {
|
|
1247
|
+
const o = drag.orig;
|
|
1248
|
+
const horizontal = Math.abs(o.w) >= Math.abs(o.h);
|
|
1249
|
+
const span = horizontal ? o.w : o.h;
|
|
1250
|
+
if (Math.abs(span) < 1) return shape;
|
|
1251
|
+
const raw = horizontal ? (point.x - o.x) / span : (point.y - o.y) / span;
|
|
1252
|
+
return {
|
|
1253
|
+
...shape,
|
|
1254
|
+
elbow: Math.min(0.95, Math.max(0.05, raw))
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
case "waypoint": {
|
|
1258
|
+
const waypoints = shape.waypoints;
|
|
1259
|
+
if (!waypoints || drag.index < 0 || drag.index >= waypoints.length) {
|
|
1260
|
+
return shape;
|
|
1261
|
+
}
|
|
1262
|
+
const prevPt = drag.index === 0 ? { x: shape.x, y: shape.y } : waypoints[drag.index - 1];
|
|
1263
|
+
const nextPt = drag.index === waypoints.length - 1 ? { x: shape.x + shape.w, y: shape.y + shape.h } : waypoints[drag.index + 1];
|
|
1264
|
+
let px = point.x;
|
|
1265
|
+
let py = point.y;
|
|
1266
|
+
if (Math.abs(px - prevPt.x) < WAYPOINT_SNAP) px = prevPt.x;
|
|
1267
|
+
else if (Math.abs(px - nextPt.x) < WAYPOINT_SNAP) px = nextPt.x;
|
|
1268
|
+
if (Math.abs(py - prevPt.y) < WAYPOINT_SNAP) py = prevPt.y;
|
|
1269
|
+
else if (Math.abs(py - nextPt.y) < WAYPOINT_SNAP) py = nextPt.y;
|
|
1270
|
+
const next = [...waypoints];
|
|
1271
|
+
next[drag.index] = { x: px, y: py };
|
|
1272
|
+
return { ...shape, waypoints: next };
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
})
|
|
1276
|
+
);
|
|
1277
|
+
},
|
|
1278
|
+
[getPoint, updateShapes]
|
|
1279
|
+
);
|
|
1280
|
+
const handlePointerUp = useCallback3(() => {
|
|
1281
|
+
const drag = dragRef.current;
|
|
1282
|
+
dragRef.current = null;
|
|
1283
|
+
if (!drag) return;
|
|
1284
|
+
if (drag.mode === "height") {
|
|
1285
|
+
commit(shapesRef.current, heightRef.current);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (drag.mode === "move" && drag.wasSelected) {
|
|
1289
|
+
const current = shapesRef.current.find((s) => s.id === drag.id);
|
|
1290
|
+
if (current && current.x === drag.orig.x && current.y === drag.orig.y) {
|
|
1291
|
+
if (current.type === "text" || isConnectorType(current.type)) {
|
|
1292
|
+
startTextEditing(current.id, "text");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (isBoxType(current.type)) {
|
|
1296
|
+
const b = bbox(current);
|
|
1297
|
+
const rel = (drag.startY - b.y) / Math.max(b.h, 1);
|
|
1298
|
+
startTextEditing(
|
|
1299
|
+
current.id,
|
|
1300
|
+
rel < 0.3 ? "label" : rel > 0.7 ? "footer" : "text"
|
|
1301
|
+
);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (drag.mode === "draw") {
|
|
1307
|
+
const drawn = shapesRef.current.find((s) => s.id === drag.id);
|
|
1308
|
+
if (drawn && Math.abs(drawn.w) < 4 && Math.abs(drawn.h) < 4) {
|
|
1309
|
+
updateShapes((prev) => prev.filter((s) => s.id !== drag.id));
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (drawn && (drawn.type === "arrow" || drawn.type === "line")) {
|
|
1313
|
+
updateShapes((prev) => prev.map((s) => s.id === drag.id ? bindEndpoints(s, prev) : s));
|
|
1314
|
+
}
|
|
1315
|
+
setTool("select");
|
|
1316
|
+
setSelectedId(drag.id);
|
|
1317
|
+
}
|
|
1318
|
+
if (drag.mode === "endpoint") {
|
|
1319
|
+
updateShapes(
|
|
1320
|
+
(prev) => prev.map((s) => {
|
|
1321
|
+
if (s.id !== drag.id) return s;
|
|
1322
|
+
const point = drag.end === "start" ? { x: s.x, y: s.y } : { x: s.x + s.w, y: s.y + s.h };
|
|
1323
|
+
const box = findBoxAt(prev, point, s.id);
|
|
1324
|
+
const otherBinding = drag.end === "start" ? s.endBinding : s.startBinding;
|
|
1325
|
+
if (box && box.id === otherBinding) return s;
|
|
1326
|
+
const key = drag.end === "start" ? "startBinding" : "endBinding";
|
|
1327
|
+
return { ...s, [key]: box?.id };
|
|
1328
|
+
})
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
updateShapes((prev) => prev.map(normalize), { commit: true });
|
|
1332
|
+
}, [commit, updateShapes, startTextEditing]);
|
|
1333
|
+
const deleteSelected = useCallback3(() => {
|
|
1334
|
+
if (!selectedId) return;
|
|
1335
|
+
setEditingText(null);
|
|
1336
|
+
setSelectedId(null);
|
|
1337
|
+
updateShapes((prev) => prev.filter((s) => s.id !== selectedId), {
|
|
1338
|
+
commit: true
|
|
1339
|
+
});
|
|
1340
|
+
}, [selectedId, updateShapes]);
|
|
1341
|
+
const handleKeyDown = useCallback3(
|
|
1342
|
+
(e) => {
|
|
1343
|
+
if (!isEditable || editingText) return;
|
|
1344
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selectedId) {
|
|
1345
|
+
e.preventDefault();
|
|
1346
|
+
e.stopPropagation();
|
|
1347
|
+
deleteSelected();
|
|
1348
|
+
} else if (e.key === "Enter" && selectedId) {
|
|
1349
|
+
const shape = shapes.find((s) => s.id === selectedId);
|
|
1350
|
+
if (shape) {
|
|
1351
|
+
e.preventDefault();
|
|
1352
|
+
e.stopPropagation();
|
|
1353
|
+
startTextEditing(shape.id, "text");
|
|
1354
|
+
}
|
|
1355
|
+
} else if (e.key === "Escape") {
|
|
1356
|
+
setSelectedId(null);
|
|
1357
|
+
setTool("select");
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
[
|
|
1361
|
+
isEditable,
|
|
1362
|
+
editingText,
|
|
1363
|
+
selectedId,
|
|
1364
|
+
deleteSelected,
|
|
1365
|
+
shapes,
|
|
1366
|
+
startTextEditing
|
|
1367
|
+
]
|
|
1368
|
+
);
|
|
1369
|
+
const applyStroke = useCallback3(
|
|
1370
|
+
(color) => {
|
|
1371
|
+
setStroke(color);
|
|
1372
|
+
if (selectedId) {
|
|
1373
|
+
updateShapes(
|
|
1374
|
+
(prev) => prev.map((s) => s.id === selectedId ? { ...s, stroke: color } : s),
|
|
1375
|
+
{ commit: true }
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
},
|
|
1379
|
+
[selectedId, updateShapes]
|
|
1380
|
+
);
|
|
1381
|
+
const applyFill = useCallback3(
|
|
1382
|
+
(color) => {
|
|
1383
|
+
setFill(color);
|
|
1384
|
+
if (selectedId) {
|
|
1385
|
+
updateShapes(
|
|
1386
|
+
(prev) => prev.map((s) => s.id === selectedId ? { ...s, fill: color } : s),
|
|
1387
|
+
{ commit: true }
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
},
|
|
1391
|
+
[selectedId, updateShapes]
|
|
1392
|
+
);
|
|
1393
|
+
const applyRouting = useCallback3(
|
|
1394
|
+
(elbow) => {
|
|
1395
|
+
if (!selectedId) return;
|
|
1396
|
+
updateShapes(
|
|
1397
|
+
(prev) => prev.map(
|
|
1398
|
+
(s) => s.id === selectedId && isConnectorType(s.type) ? {
|
|
1399
|
+
...s,
|
|
1400
|
+
routing: elbow ? "elbow" : void 0,
|
|
1401
|
+
elbow: elbow ? s.elbow : void 0,
|
|
1402
|
+
waypoints: void 0
|
|
1403
|
+
} : s
|
|
1404
|
+
),
|
|
1405
|
+
{ commit: true }
|
|
1406
|
+
);
|
|
1407
|
+
},
|
|
1408
|
+
[selectedId, updateShapes]
|
|
1409
|
+
);
|
|
1410
|
+
const addWaypoint = useCallback3(
|
|
1411
|
+
(e, shapeId, segmentIndex, point) => {
|
|
1412
|
+
if (!isEditable) return;
|
|
1413
|
+
if (e.button !== 0) return;
|
|
1414
|
+
e.stopPropagation();
|
|
1415
|
+
svgRef.current?.setPointerCapture(e.pointerId);
|
|
1416
|
+
updateShapes(
|
|
1417
|
+
(prev) => prev.map((s) => {
|
|
1418
|
+
if (s.id !== shapeId || !isConnectorType(s.type)) return s;
|
|
1419
|
+
const interior = connectorPoints(s).slice(1, -1);
|
|
1420
|
+
const waypoints = [
|
|
1421
|
+
...interior.slice(0, segmentIndex),
|
|
1422
|
+
point,
|
|
1423
|
+
...interior.slice(segmentIndex)
|
|
1424
|
+
];
|
|
1425
|
+
return { ...s, waypoints, routing: void 0, elbow: void 0 };
|
|
1426
|
+
})
|
|
1427
|
+
);
|
|
1428
|
+
dragRef.current = { mode: "waypoint", id: shapeId, index: segmentIndex };
|
|
1429
|
+
},
|
|
1430
|
+
[isEditable, updateShapes]
|
|
1431
|
+
);
|
|
1432
|
+
const removeWaypoint = useCallback3(
|
|
1433
|
+
(shapeId, index) => {
|
|
1434
|
+
updateShapes(
|
|
1435
|
+
(prev) => prev.map((s) => {
|
|
1436
|
+
if (s.id !== shapeId || !s.waypoints) return s;
|
|
1437
|
+
const waypoints = s.waypoints.filter((_, i) => i !== index);
|
|
1438
|
+
return { ...s, waypoints: waypoints.length ? waypoints : void 0 };
|
|
1439
|
+
}),
|
|
1440
|
+
{ commit: true }
|
|
1441
|
+
);
|
|
1442
|
+
},
|
|
1443
|
+
[updateShapes]
|
|
1444
|
+
);
|
|
1445
|
+
const applyDirection = useCallback3(
|
|
1446
|
+
(bidirectional) => {
|
|
1447
|
+
if (!selectedId) return;
|
|
1448
|
+
updateShapes(
|
|
1449
|
+
(prev) => prev.map(
|
|
1450
|
+
(s) => s.id === selectedId && s.type === "arrow" ? { ...s, bidirectional: bidirectional || void 0 } : s
|
|
1451
|
+
),
|
|
1452
|
+
{ commit: true }
|
|
1453
|
+
);
|
|
1454
|
+
},
|
|
1455
|
+
[selectedId, updateShapes]
|
|
1456
|
+
);
|
|
1457
|
+
const commitText = useCallback3(
|
|
1458
|
+
(id, field, value) => {
|
|
1459
|
+
setEditingText(null);
|
|
1460
|
+
updateShapes(
|
|
1461
|
+
(prev) => prev.flatMap((s) => {
|
|
1462
|
+
if (s.id !== id) return [s];
|
|
1463
|
+
if (s.type === "text") {
|
|
1464
|
+
return value.trim() === "" ? [] : [{ ...s, text: value, ...textBoxSize(value) }];
|
|
1465
|
+
}
|
|
1466
|
+
return [
|
|
1467
|
+
{ ...s, [field]: value.trim() === "" ? void 0 : value }
|
|
1468
|
+
];
|
|
1469
|
+
}),
|
|
1470
|
+
{ commit: true }
|
|
1471
|
+
);
|
|
1472
|
+
},
|
|
1473
|
+
[updateShapes]
|
|
1474
|
+
);
|
|
1475
|
+
const editingShape = shapes.find((s) => s.id === editingText?.id) ?? null;
|
|
1476
|
+
const showFill = selectedShape != null ? isBoxType(selectedShape.type) : tool !== "select" && isBoxType(tool);
|
|
1477
|
+
const canvasCursor = tool === "select" ? "default" : tool === "text" ? "text" : "crosshair";
|
|
1478
|
+
return /* @__PURE__ */ jsxs2(
|
|
1479
|
+
"div",
|
|
1480
|
+
{
|
|
1481
|
+
className: "zui-drawing-canvas",
|
|
1482
|
+
tabIndex: isEditable ? 0 : void 0,
|
|
1483
|
+
onKeyDown: handleKeyDown,
|
|
1484
|
+
children: [
|
|
1485
|
+
isEditable && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-toolbar", onPointerDown: (e) => e.stopPropagation(), children: [
|
|
1486
|
+
/* @__PURE__ */ jsx3("div", { className: "zui-drawing-toolbar-group", children: TOOLS.map(({ tool: t, label }) => /* @__PURE__ */ jsx3(
|
|
1487
|
+
"button",
|
|
1488
|
+
{
|
|
1489
|
+
type: "button",
|
|
1490
|
+
title: label,
|
|
1491
|
+
"aria-label": label,
|
|
1492
|
+
className: `zui-drawing-tool ${tool === t ? "is-active" : ""}`,
|
|
1493
|
+
onClick: () => {
|
|
1494
|
+
setTool(t);
|
|
1495
|
+
if (t !== "select") setSelectedId(null);
|
|
1496
|
+
},
|
|
1497
|
+
children: /* @__PURE__ */ jsx3(
|
|
1498
|
+
"svg",
|
|
1499
|
+
{
|
|
1500
|
+
viewBox: "0 0 20 20",
|
|
1501
|
+
width: "16",
|
|
1502
|
+
height: "16",
|
|
1503
|
+
fill: "none",
|
|
1504
|
+
stroke: "currentColor",
|
|
1505
|
+
strokeWidth: "1.6",
|
|
1506
|
+
strokeLinecap: "round",
|
|
1507
|
+
strokeLinejoin: "round",
|
|
1508
|
+
children: TOOL_ICONS[t]
|
|
1509
|
+
}
|
|
1510
|
+
)
|
|
1511
|
+
},
|
|
1512
|
+
t
|
|
1513
|
+
)) }),
|
|
1514
|
+
/* @__PURE__ */ jsxs2("div", { className: "zui-drawing-toolbar-group", children: [
|
|
1515
|
+
/* @__PURE__ */ jsx3("span", { className: "zui-drawing-swatch-label", children: "Stroke" }),
|
|
1516
|
+
STROKE_COLORS.map((color) => /* @__PURE__ */ jsx3(
|
|
1517
|
+
"button",
|
|
1518
|
+
{
|
|
1519
|
+
type: "button",
|
|
1520
|
+
title: `Stroke ${color}`,
|
|
1521
|
+
"aria-label": `Stroke color ${color}`,
|
|
1522
|
+
className: `zui-drawing-swatch ${(selectedShape?.stroke ?? stroke) === color ? "is-active" : ""}`,
|
|
1523
|
+
style: { backgroundColor: color },
|
|
1524
|
+
onClick: () => applyStroke(color)
|
|
1525
|
+
},
|
|
1526
|
+
color
|
|
1527
|
+
))
|
|
1528
|
+
] }),
|
|
1529
|
+
showFill && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-toolbar-group", children: [
|
|
1530
|
+
/* @__PURE__ */ jsx3("span", { className: "zui-drawing-swatch-label", children: "Fill" }),
|
|
1531
|
+
FILL_COLORS.map((color) => /* @__PURE__ */ jsx3(
|
|
1532
|
+
"button",
|
|
1533
|
+
{
|
|
1534
|
+
type: "button",
|
|
1535
|
+
title: `Fill ${color === "transparent" ? "none" : color}`,
|
|
1536
|
+
"aria-label": `Fill color ${color === "transparent" ? "none" : color}`,
|
|
1537
|
+
className: `zui-drawing-swatch ${color === "transparent" ? "is-transparent" : ""} ${(selectedShape?.fill ?? fill) === color ? "is-active" : ""}`,
|
|
1538
|
+
style: { backgroundColor: color },
|
|
1539
|
+
onClick: () => applyFill(color)
|
|
1540
|
+
},
|
|
1541
|
+
color
|
|
1542
|
+
))
|
|
1543
|
+
] }),
|
|
1544
|
+
selectedShape && isConnectorType(selectedShape.type) && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-toolbar-group", children: [
|
|
1545
|
+
/* @__PURE__ */ jsx3(
|
|
1546
|
+
"button",
|
|
1547
|
+
{
|
|
1548
|
+
type: "button",
|
|
1549
|
+
title: "Straight connector",
|
|
1550
|
+
"aria-label": "Straight connector",
|
|
1551
|
+
className: `zui-drawing-tool ${selectedShape.routing !== "elbow" ? "is-active" : ""}`,
|
|
1552
|
+
onClick: () => applyRouting(false),
|
|
1553
|
+
children: /* @__PURE__ */ jsx3(
|
|
1554
|
+
"svg",
|
|
1555
|
+
{
|
|
1556
|
+
viewBox: "0 0 20 20",
|
|
1557
|
+
width: "16",
|
|
1558
|
+
height: "16",
|
|
1559
|
+
fill: "none",
|
|
1560
|
+
stroke: "currentColor",
|
|
1561
|
+
strokeWidth: "1.6",
|
|
1562
|
+
strokeLinecap: "round",
|
|
1563
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M4 16L16 4" })
|
|
1564
|
+
}
|
|
1565
|
+
)
|
|
1566
|
+
}
|
|
1567
|
+
),
|
|
1568
|
+
/* @__PURE__ */ jsx3(
|
|
1569
|
+
"button",
|
|
1570
|
+
{
|
|
1571
|
+
type: "button",
|
|
1572
|
+
title: "Elbow connector (right angles)",
|
|
1573
|
+
"aria-label": "Elbow connector",
|
|
1574
|
+
className: `zui-drawing-tool ${selectedShape.routing === "elbow" ? "is-active" : ""}`,
|
|
1575
|
+
onClick: () => applyRouting(true),
|
|
1576
|
+
children: /* @__PURE__ */ jsx3(
|
|
1577
|
+
"svg",
|
|
1578
|
+
{
|
|
1579
|
+
viewBox: "0 0 20 20",
|
|
1580
|
+
width: "16",
|
|
1581
|
+
height: "16",
|
|
1582
|
+
fill: "none",
|
|
1583
|
+
stroke: "currentColor",
|
|
1584
|
+
strokeWidth: "1.6",
|
|
1585
|
+
strokeLinecap: "round",
|
|
1586
|
+
strokeLinejoin: "round",
|
|
1587
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M4 16v-6h12V4" })
|
|
1588
|
+
}
|
|
1589
|
+
)
|
|
1590
|
+
}
|
|
1591
|
+
)
|
|
1592
|
+
] }),
|
|
1593
|
+
selectedShape?.type === "arrow" && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-toolbar-group", children: [
|
|
1594
|
+
/* @__PURE__ */ jsx3(
|
|
1595
|
+
"button",
|
|
1596
|
+
{
|
|
1597
|
+
type: "button",
|
|
1598
|
+
title: "One-way arrow",
|
|
1599
|
+
"aria-label": "One-way arrow",
|
|
1600
|
+
className: `zui-drawing-tool ${!selectedShape.bidirectional ? "is-active" : ""}`,
|
|
1601
|
+
onClick: () => applyDirection(false),
|
|
1602
|
+
children: /* @__PURE__ */ jsx3(
|
|
1603
|
+
"svg",
|
|
1604
|
+
{
|
|
1605
|
+
viewBox: "0 0 20 20",
|
|
1606
|
+
width: "16",
|
|
1607
|
+
height: "16",
|
|
1608
|
+
fill: "none",
|
|
1609
|
+
stroke: "currentColor",
|
|
1610
|
+
strokeWidth: "1.6",
|
|
1611
|
+
strokeLinecap: "round",
|
|
1612
|
+
strokeLinejoin: "round",
|
|
1613
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M3 10h13M12 5.5L16.5 10L12 14.5" })
|
|
1614
|
+
}
|
|
1615
|
+
)
|
|
1616
|
+
}
|
|
1617
|
+
),
|
|
1618
|
+
/* @__PURE__ */ jsx3(
|
|
1619
|
+
"button",
|
|
1620
|
+
{
|
|
1621
|
+
type: "button",
|
|
1622
|
+
title: "Two-way arrow",
|
|
1623
|
+
"aria-label": "Two-way arrow",
|
|
1624
|
+
className: `zui-drawing-tool ${selectedShape.bidirectional ? "is-active" : ""}`,
|
|
1625
|
+
onClick: () => applyDirection(true),
|
|
1626
|
+
children: /* @__PURE__ */ jsx3(
|
|
1627
|
+
"svg",
|
|
1628
|
+
{
|
|
1629
|
+
viewBox: "0 0 20 20",
|
|
1630
|
+
width: "16",
|
|
1631
|
+
height: "16",
|
|
1632
|
+
fill: "none",
|
|
1633
|
+
stroke: "currentColor",
|
|
1634
|
+
strokeWidth: "1.6",
|
|
1635
|
+
strokeLinecap: "round",
|
|
1636
|
+
strokeLinejoin: "round",
|
|
1637
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M3.5 10h13M8 5.5L3.5 10L8 14.5M12 5.5L16.5 10L12 14.5" })
|
|
1638
|
+
}
|
|
1639
|
+
)
|
|
1640
|
+
}
|
|
1641
|
+
)
|
|
1642
|
+
] }),
|
|
1643
|
+
selectedShape && /* @__PURE__ */ jsx3("div", { className: "zui-drawing-toolbar-group", children: /* @__PURE__ */ jsx3(
|
|
1644
|
+
"button",
|
|
1645
|
+
{
|
|
1646
|
+
type: "button",
|
|
1647
|
+
title: "Delete shape",
|
|
1648
|
+
"aria-label": "Delete shape",
|
|
1649
|
+
className: "zui-drawing-tool zui-drawing-tool-danger",
|
|
1650
|
+
onClick: deleteSelected,
|
|
1651
|
+
children: /* @__PURE__ */ jsx3(
|
|
1652
|
+
"svg",
|
|
1653
|
+
{
|
|
1654
|
+
viewBox: "0 0 20 20",
|
|
1655
|
+
width: "16",
|
|
1656
|
+
height: "16",
|
|
1657
|
+
fill: "none",
|
|
1658
|
+
stroke: "currentColor",
|
|
1659
|
+
strokeWidth: "1.6",
|
|
1660
|
+
strokeLinecap: "round",
|
|
1661
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M4 6h12M8 6V4h4v2M6 6l1 10h6l1-10M8.5 9v4M11.5 9v4" })
|
|
1662
|
+
}
|
|
1663
|
+
)
|
|
1664
|
+
}
|
|
1665
|
+
) })
|
|
1666
|
+
] }),
|
|
1667
|
+
/* @__PURE__ */ jsxs2(
|
|
1668
|
+
"svg",
|
|
1669
|
+
{
|
|
1670
|
+
ref: svgRef,
|
|
1671
|
+
className: "zui-drawing-surface",
|
|
1672
|
+
style: { height, cursor: canvasCursor },
|
|
1673
|
+
onPointerDown: handleBackgroundPointerDown,
|
|
1674
|
+
onPointerMove: handlePointerMove,
|
|
1675
|
+
onPointerUp: handlePointerUp,
|
|
1676
|
+
onDoubleClick: (e) => {
|
|
1677
|
+
if (!isEditable) return;
|
|
1678
|
+
const target = e.target.closest("[data-shape-id]");
|
|
1679
|
+
const id = target?.getAttribute("data-shape-id");
|
|
1680
|
+
const shape = id ? shapes.find((s) => s.id === id) : null;
|
|
1681
|
+
if (!shape) return;
|
|
1682
|
+
if (shape.type === "text" || isConnectorType(shape.type)) {
|
|
1683
|
+
startTextEditing(shape.id, "text");
|
|
1684
|
+
} else if (isBoxType(shape.type)) {
|
|
1685
|
+
const b = bbox(shape);
|
|
1686
|
+
const rel = (getPoint(e).y - b.y) / Math.max(b.h, 1);
|
|
1687
|
+
startTextEditing(
|
|
1688
|
+
shape.id,
|
|
1689
|
+
rel < 0.3 ? "label" : rel > 0.7 ? "footer" : "text"
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
},
|
|
1693
|
+
children: [
|
|
1694
|
+
shapes.map((shape) => /* @__PURE__ */ jsxs2(
|
|
1695
|
+
"g",
|
|
1696
|
+
{
|
|
1697
|
+
"data-shape-id": shape.id,
|
|
1698
|
+
style: {
|
|
1699
|
+
cursor: isEditable && tool === "select" ? "move" : void 0
|
|
1700
|
+
},
|
|
1701
|
+
onPointerDown: (e) => handleShapePointerDown(e, shape),
|
|
1702
|
+
children: [
|
|
1703
|
+
isConnectorType(shape.type) && /* @__PURE__ */ jsx3(
|
|
1704
|
+
"path",
|
|
1705
|
+
{
|
|
1706
|
+
d: connectorPath(shape),
|
|
1707
|
+
fill: "none",
|
|
1708
|
+
stroke: "transparent",
|
|
1709
|
+
strokeWidth: 14
|
|
1710
|
+
}
|
|
1711
|
+
),
|
|
1712
|
+
(isBoxType(shape.type) || shape.type === "text") && /* @__PURE__ */ jsx3(HitArea, { shape }),
|
|
1713
|
+
shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx3(
|
|
1714
|
+
ShapeView,
|
|
1715
|
+
{
|
|
1716
|
+
shape,
|
|
1717
|
+
hideField: shape.id === editingText?.id ? editingText.field : null,
|
|
1718
|
+
showHints: isEditable && tool === "select" && shape.id === selectedId && shape.id !== editingText?.id
|
|
1719
|
+
}
|
|
1720
|
+
)
|
|
1721
|
+
]
|
|
1722
|
+
},
|
|
1723
|
+
shape.id
|
|
1724
|
+
)),
|
|
1725
|
+
selectedShape && isEditable && !editingText && /* @__PURE__ */ jsx3(
|
|
1726
|
+
SelectionOverlay,
|
|
1727
|
+
{
|
|
1728
|
+
shape: selectedShape,
|
|
1729
|
+
onHandlePointerDown: handleHandlePointerDown,
|
|
1730
|
+
onWaypointAdd: addWaypoint,
|
|
1731
|
+
onWaypointRemove: removeWaypoint
|
|
1732
|
+
}
|
|
1733
|
+
)
|
|
1734
|
+
]
|
|
1735
|
+
}
|
|
1736
|
+
),
|
|
1737
|
+
editingShape && editingText && /* @__PURE__ */ jsx3(
|
|
1738
|
+
TextEditOverlay,
|
|
1739
|
+
{
|
|
1740
|
+
shape: editingShape,
|
|
1741
|
+
field: editingText.field,
|
|
1742
|
+
onCommit: commitText
|
|
1743
|
+
},
|
|
1744
|
+
`${editingShape.id}:${editingText.field}`
|
|
1745
|
+
),
|
|
1746
|
+
isEditable && /* @__PURE__ */ jsx3(
|
|
1747
|
+
"div",
|
|
1748
|
+
{
|
|
1749
|
+
className: "zui-drawing-resize",
|
|
1750
|
+
title: "Drag to resize canvas",
|
|
1751
|
+
onPointerDown: (e) => {
|
|
1752
|
+
e.preventDefault();
|
|
1753
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1754
|
+
dragRef.current = {
|
|
1755
|
+
mode: "height",
|
|
1756
|
+
startY: e.clientY,
|
|
1757
|
+
origHeight: heightRef.current
|
|
1758
|
+
};
|
|
1759
|
+
},
|
|
1760
|
+
onPointerMove: (e) => {
|
|
1761
|
+
const drag = dragRef.current;
|
|
1762
|
+
if (drag?.mode !== "height") return;
|
|
1763
|
+
setHeight(
|
|
1764
|
+
Math.round(
|
|
1765
|
+
Math.min(
|
|
1766
|
+
MAX_HEIGHT,
|
|
1767
|
+
Math.max(MIN_HEIGHT, drag.origHeight + (e.clientY - drag.startY))
|
|
1768
|
+
)
|
|
1769
|
+
)
|
|
1770
|
+
);
|
|
1771
|
+
},
|
|
1772
|
+
onPointerUp: () => {
|
|
1773
|
+
if (dragRef.current?.mode !== "height") return;
|
|
1774
|
+
dragRef.current = null;
|
|
1775
|
+
commit(shapesRef.current, heightRef.current);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
)
|
|
1779
|
+
]
|
|
1780
|
+
}
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
function HitArea({ shape }) {
|
|
1784
|
+
const b = bbox(shape);
|
|
1785
|
+
return /* @__PURE__ */ jsx3(
|
|
1786
|
+
"rect",
|
|
1787
|
+
{
|
|
1788
|
+
x: b.x,
|
|
1789
|
+
y: b.y,
|
|
1790
|
+
width: Math.max(b.w, 8),
|
|
1791
|
+
height: Math.max(b.h, 8),
|
|
1792
|
+
fill: "transparent",
|
|
1793
|
+
stroke: "none"
|
|
1794
|
+
}
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
function SelectionOverlay({
|
|
1798
|
+
shape,
|
|
1799
|
+
onHandlePointerDown,
|
|
1800
|
+
onWaypointAdd,
|
|
1801
|
+
onWaypointRemove
|
|
1802
|
+
}) {
|
|
1803
|
+
if (shape.type === "line" || shape.type === "arrow") {
|
|
1804
|
+
const ends = [
|
|
1805
|
+
{ end: "start", x: shape.x, y: shape.y },
|
|
1806
|
+
{ end: "end", x: shape.x + shape.w, y: shape.y + shape.h }
|
|
1807
|
+
];
|
|
1808
|
+
const points = connectorPoints(shape);
|
|
1809
|
+
const waypoints = shape.waypoints ?? [];
|
|
1810
|
+
const hasElbowHandle = shape.routing === "elbow" && !waypoints.length;
|
|
1811
|
+
const elbowMid = hasElbowHandle ? connectorMidpoint(shape) : null;
|
|
1812
|
+
const elbowCursor = Math.abs(shape.w) >= Math.abs(shape.h) ? "ew-resize" : "ns-resize";
|
|
1813
|
+
const ghosts = points.slice(0, -1).flatMap((p, i) => {
|
|
1814
|
+
const q = points[i + 1];
|
|
1815
|
+
if (Math.hypot(q.x - p.x, q.y - p.y) < 28) return [];
|
|
1816
|
+
if (hasElbowHandle && i === 1) return [];
|
|
1817
|
+
return [{ index: i, x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 }];
|
|
1818
|
+
});
|
|
1819
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
1820
|
+
ghosts.map(({ index, x, y }) => /* @__PURE__ */ jsx3(
|
|
1821
|
+
"circle",
|
|
1822
|
+
{
|
|
1823
|
+
cx: x,
|
|
1824
|
+
cy: y,
|
|
1825
|
+
r: 4.5,
|
|
1826
|
+
fill: "#fff",
|
|
1827
|
+
stroke: SELECTION_COLOR,
|
|
1828
|
+
strokeWidth: 1.5,
|
|
1829
|
+
strokeDasharray: "2 2",
|
|
1830
|
+
opacity: 0.6,
|
|
1831
|
+
style: { cursor: "copy" },
|
|
1832
|
+
onPointerDown: (e) => onWaypointAdd(e, shape.id, index, { x, y }),
|
|
1833
|
+
children: /* @__PURE__ */ jsx3("title", { children: "Drag to add a point" })
|
|
1834
|
+
},
|
|
1835
|
+
`ghost-${index}`
|
|
1836
|
+
)),
|
|
1837
|
+
waypoints.map((p, i) => /* @__PURE__ */ jsx3(
|
|
1838
|
+
"rect",
|
|
1839
|
+
{
|
|
1840
|
+
x: p.x - 4,
|
|
1841
|
+
y: p.y - 4,
|
|
1842
|
+
width: 8,
|
|
1843
|
+
height: 8,
|
|
1844
|
+
rx: 2,
|
|
1845
|
+
fill: "#fff",
|
|
1846
|
+
stroke: SELECTION_COLOR,
|
|
1847
|
+
strokeWidth: 1.5,
|
|
1848
|
+
style: { cursor: "move" },
|
|
1849
|
+
onPointerDown: (e) => onHandlePointerDown(e, {
|
|
1850
|
+
mode: "waypoint",
|
|
1851
|
+
id: shape.id,
|
|
1852
|
+
index: i
|
|
1853
|
+
}),
|
|
1854
|
+
onDoubleClick: (e) => {
|
|
1855
|
+
e.stopPropagation();
|
|
1856
|
+
onWaypointRemove(shape.id, i);
|
|
1857
|
+
},
|
|
1858
|
+
children: /* @__PURE__ */ jsx3("title", { children: "Drag to move, double-click to remove" })
|
|
1859
|
+
},
|
|
1860
|
+
`wp-${i}`
|
|
1861
|
+
)),
|
|
1862
|
+
ends.map(({ end, x, y }) => /* @__PURE__ */ jsx3(
|
|
1863
|
+
"circle",
|
|
1864
|
+
{
|
|
1865
|
+
cx: x,
|
|
1866
|
+
cy: y,
|
|
1867
|
+
r: 5,
|
|
1868
|
+
fill: "#fff",
|
|
1869
|
+
stroke: SELECTION_COLOR,
|
|
1870
|
+
strokeWidth: 1.5,
|
|
1871
|
+
style: { cursor: "crosshair" },
|
|
1872
|
+
onPointerDown: (e) => onHandlePointerDown(e, {
|
|
1873
|
+
mode: "endpoint",
|
|
1874
|
+
id: shape.id,
|
|
1875
|
+
end,
|
|
1876
|
+
orig: shape
|
|
1877
|
+
})
|
|
1878
|
+
},
|
|
1879
|
+
end
|
|
1880
|
+
)),
|
|
1881
|
+
elbowMid && /* @__PURE__ */ jsx3(
|
|
1882
|
+
"rect",
|
|
1883
|
+
{
|
|
1884
|
+
x: elbowMid.x - 4,
|
|
1885
|
+
y: elbowMid.y - 4,
|
|
1886
|
+
width: 8,
|
|
1887
|
+
height: 8,
|
|
1888
|
+
rx: 2,
|
|
1889
|
+
fill: "#fff",
|
|
1890
|
+
stroke: SELECTION_COLOR,
|
|
1891
|
+
strokeWidth: 1.5,
|
|
1892
|
+
style: { cursor: elbowCursor },
|
|
1893
|
+
onPointerDown: (e) => onHandlePointerDown(e, {
|
|
1894
|
+
mode: "elbow",
|
|
1895
|
+
id: shape.id,
|
|
1896
|
+
orig: shape
|
|
1897
|
+
})
|
|
1898
|
+
}
|
|
1899
|
+
)
|
|
1900
|
+
] });
|
|
1901
|
+
}
|
|
1902
|
+
const b = bbox(shape);
|
|
1903
|
+
const pad = 5;
|
|
1904
|
+
const corners = [
|
|
1905
|
+
{ corner: "nw", x: b.x - pad, y: b.y - pad },
|
|
1906
|
+
{ corner: "ne", x: b.x + b.w + pad, y: b.y - pad },
|
|
1907
|
+
{ corner: "sw", x: b.x - pad, y: b.y + b.h + pad },
|
|
1908
|
+
{ corner: "se", x: b.x + b.w + pad, y: b.y + b.h + pad }
|
|
1909
|
+
];
|
|
1910
|
+
const resizable = shape.type !== "text";
|
|
1911
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
1912
|
+
/* @__PURE__ */ jsx3(
|
|
1913
|
+
"rect",
|
|
1914
|
+
{
|
|
1915
|
+
x: b.x - pad,
|
|
1916
|
+
y: b.y - pad,
|
|
1917
|
+
width: b.w + pad * 2,
|
|
1918
|
+
height: b.h + pad * 2,
|
|
1919
|
+
fill: "none",
|
|
1920
|
+
stroke: SELECTION_COLOR,
|
|
1921
|
+
strokeWidth: 1,
|
|
1922
|
+
strokeDasharray: "4 3",
|
|
1923
|
+
pointerEvents: "none"
|
|
1924
|
+
}
|
|
1925
|
+
),
|
|
1926
|
+
resizable && corners.map(({ corner, x, y }) => /* @__PURE__ */ jsx3(
|
|
1927
|
+
"rect",
|
|
1928
|
+
{
|
|
1929
|
+
x: x - 4,
|
|
1930
|
+
y: y - 4,
|
|
1931
|
+
width: 8,
|
|
1932
|
+
height: 8,
|
|
1933
|
+
rx: 2,
|
|
1934
|
+
fill: "#fff",
|
|
1935
|
+
stroke: SELECTION_COLOR,
|
|
1936
|
+
strokeWidth: 1.5,
|
|
1937
|
+
style: {
|
|
1938
|
+
cursor: corner === "nw" || corner === "se" ? "nwse-resize" : "nesw-resize"
|
|
1939
|
+
},
|
|
1940
|
+
onPointerDown: (e) => onHandlePointerDown(e, { mode: "resize", id: shape.id, corner, orig: shape })
|
|
1941
|
+
},
|
|
1942
|
+
corner
|
|
1943
|
+
))
|
|
1944
|
+
] });
|
|
1945
|
+
}
|
|
1946
|
+
function TextEditOverlay({
|
|
1947
|
+
shape,
|
|
1948
|
+
field,
|
|
1949
|
+
onCommit
|
|
1950
|
+
}) {
|
|
1951
|
+
const original = shape[field] ?? "";
|
|
1952
|
+
const [value, setValue] = useState3(original);
|
|
1953
|
+
const ref = useRef3(null);
|
|
1954
|
+
useEffect7(() => {
|
|
1955
|
+
ref.current?.focus();
|
|
1956
|
+
ref.current?.select();
|
|
1957
|
+
}, []);
|
|
1958
|
+
const style = { color: shape.stroke };
|
|
1959
|
+
if (shape.type === "text") {
|
|
1960
|
+
Object.assign(style, {
|
|
1961
|
+
left: shape.x,
|
|
1962
|
+
top: shape.y,
|
|
1963
|
+
fontSize: FONT_SIZE,
|
|
1964
|
+
minWidth: Math.max(80, textBoxSize(value).w + 20),
|
|
1965
|
+
height: textBoxSize(value).h + 8
|
|
1966
|
+
});
|
|
1967
|
+
} else if (isConnectorType(shape.type)) {
|
|
1968
|
+
const lines = wrapText(value, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
|
|
1969
|
+
const size = textBoxSize(lines.join("\n"), CONNECTOR_FONT_SIZE);
|
|
1970
|
+
const width = Math.max(70, size.w + 16);
|
|
1971
|
+
const height = size.h + 8;
|
|
1972
|
+
const mid = connectorMidpoint(shape);
|
|
1973
|
+
Object.assign(style, {
|
|
1974
|
+
left: mid.x - width / 2,
|
|
1975
|
+
top: mid.y - height / 2,
|
|
1976
|
+
width,
|
|
1977
|
+
height,
|
|
1978
|
+
fontSize: CONNECTOR_FONT_SIZE,
|
|
1979
|
+
textAlign: "center",
|
|
1980
|
+
whiteSpace: "pre-wrap"
|
|
1981
|
+
});
|
|
1982
|
+
} else {
|
|
1983
|
+
const b = bbox(shape);
|
|
1984
|
+
const fontSize = field === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
|
|
1985
|
+
const width = Math.max(b.w - 8, 40);
|
|
1986
|
+
const lines = wrapText(value, Math.max(width - 8, 20), fontSize).length;
|
|
1987
|
+
const boxH = lines * fontSize * LINE_HEIGHT + 8;
|
|
1988
|
+
const top = field === "label" ? b.y + 3 : field === "footer" ? b.y + b.h - boxH - 3 : b.y + b.h / 2 - boxH / 2;
|
|
1989
|
+
Object.assign(style, {
|
|
1990
|
+
left: b.x + 4,
|
|
1991
|
+
top,
|
|
1992
|
+
width,
|
|
1993
|
+
height: boxH,
|
|
1994
|
+
fontSize,
|
|
1995
|
+
fontWeight: field === "label" ? 600 : void 0,
|
|
1996
|
+
textAlign: "center",
|
|
1997
|
+
background: "transparent",
|
|
1998
|
+
whiteSpace: "pre-wrap"
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
const placeholder = field === "label" ? "Label" : field === "footer" ? "Footer" : "Text";
|
|
2002
|
+
return /* @__PURE__ */ jsx3(
|
|
2003
|
+
"textarea",
|
|
2004
|
+
{
|
|
2005
|
+
ref,
|
|
2006
|
+
className: "zui-drawing-text-input",
|
|
2007
|
+
style,
|
|
2008
|
+
value,
|
|
2009
|
+
placeholder,
|
|
2010
|
+
onChange: (e) => setValue(e.target.value),
|
|
2011
|
+
onBlur: () => onCommit(shape.id, field, value),
|
|
2012
|
+
onKeyDown: (e) => {
|
|
2013
|
+
e.stopPropagation();
|
|
2014
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
2015
|
+
e.preventDefault();
|
|
2016
|
+
onCommit(shape.id, field, value);
|
|
2017
|
+
} else if (e.key === "Escape") {
|
|
2018
|
+
e.preventDefault();
|
|
2019
|
+
onCommit(shape.id, field, original);
|
|
2020
|
+
}
|
|
2021
|
+
},
|
|
2022
|
+
onPointerDown: (e) => e.stopPropagation()
|
|
2023
|
+
}
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/nodes/DrawingNode.tsx
|
|
2028
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
2029
|
+
var DrawingNode = class _DrawingNode extends DecoratorNode {
|
|
2030
|
+
static getType() {
|
|
2031
|
+
return "drawing";
|
|
2032
|
+
}
|
|
2033
|
+
static clone(node) {
|
|
2034
|
+
return new _DrawingNode(node.__data, node.__key);
|
|
2035
|
+
}
|
|
2036
|
+
constructor(data, key) {
|
|
2037
|
+
super(key);
|
|
2038
|
+
this.__data = data;
|
|
2039
|
+
}
|
|
2040
|
+
static importJSON(serializedNode) {
|
|
2041
|
+
return $createDrawingNode(serializedNode.data);
|
|
2042
|
+
}
|
|
2043
|
+
exportJSON() {
|
|
2044
|
+
return {
|
|
2045
|
+
data: this.__data,
|
|
2046
|
+
type: "drawing",
|
|
2047
|
+
version: 1
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
createDOM(config) {
|
|
2051
|
+
const div = document.createElement("div");
|
|
2052
|
+
const className = config.theme.drawing;
|
|
2053
|
+
div.className = typeof className === "string" ? className : "zui-drawing";
|
|
2054
|
+
return div;
|
|
2055
|
+
}
|
|
2056
|
+
updateDOM() {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
exportDOM() {
|
|
2060
|
+
const element = document.createElement("pre");
|
|
2061
|
+
element.setAttribute("data-drawing", this.__data);
|
|
2062
|
+
return { element };
|
|
2063
|
+
}
|
|
2064
|
+
static importDOM() {
|
|
2065
|
+
return {
|
|
2066
|
+
pre: (domNode) => {
|
|
2067
|
+
if (!domNode.hasAttribute("data-drawing")) return null;
|
|
2068
|
+
return {
|
|
2069
|
+
conversion: (element) => ({
|
|
2070
|
+
node: $createDrawingNode(
|
|
2071
|
+
serializeDrawingData(
|
|
2072
|
+
parseDrawingData(element.getAttribute("data-drawing") ?? "")
|
|
2073
|
+
)
|
|
2074
|
+
)
|
|
2075
|
+
}),
|
|
2076
|
+
priority: 2
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
getData() {
|
|
2082
|
+
return parseDrawingData(this.getLatest().__data);
|
|
2083
|
+
}
|
|
2084
|
+
setData(data) {
|
|
2085
|
+
const writable = this.getWritable();
|
|
2086
|
+
writable.__data = serializeDrawingData(data);
|
|
2087
|
+
}
|
|
2088
|
+
isInline() {
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
decorate(_editor, _config) {
|
|
2092
|
+
return /* @__PURE__ */ jsx4(DrawingCanvas, { nodeKey: this.getKey(), data: this.getData() });
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
function $createDrawingNode(data = serializeDrawingData(EMPTY_DRAWING)) {
|
|
2096
|
+
return $applyNodeReplacement(new DrawingNode(data));
|
|
2097
|
+
}
|
|
2098
|
+
function $isDrawingNode(node) {
|
|
2099
|
+
return node instanceof DrawingNode;
|
|
2100
|
+
}
|
|
2101
|
+
var DRAWING_START = /^```drawing\s*$/;
|
|
2102
|
+
var DRAWING_END = /^```\s*$/;
|
|
2103
|
+
var DRAWING = {
|
|
2104
|
+
dependencies: [DrawingNode],
|
|
2105
|
+
export: (node) => {
|
|
2106
|
+
if (!$isDrawingNode(node)) return null;
|
|
2107
|
+
return `\`\`\`drawing
|
|
2108
|
+
${node.getLatest().__data}
|
|
2109
|
+
\`\`\``;
|
|
2110
|
+
},
|
|
2111
|
+
regExpStart: DRAWING_START,
|
|
2112
|
+
regExpEnd: DRAWING_END,
|
|
2113
|
+
replace: (rootNode, children, _startMatch, _endMatch, linesInBetween, isImport) => {
|
|
2114
|
+
if (!isImport || children != null || linesInBetween == null) return false;
|
|
2115
|
+
const json = linesInBetween.join("\n").trim();
|
|
2116
|
+
const data = parseDrawingData(json);
|
|
2117
|
+
rootNode.append($createDrawingNode(serializeDrawingData(data)));
|
|
2118
|
+
},
|
|
2119
|
+
type: "multiline-element"
|
|
2120
|
+
};
|
|
2121
|
+
|
|
2122
|
+
// src/plugins/ToolbarPlugin.tsx
|
|
2123
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2124
|
+
var FORMAT_BUTTONS = [
|
|
2125
|
+
{
|
|
2126
|
+
format: "bold",
|
|
2127
|
+
label: "Bold",
|
|
2128
|
+
icon: /* @__PURE__ */ jsx5(
|
|
2129
|
+
"path",
|
|
2130
|
+
{
|
|
2131
|
+
d: "M6 3.5h5a3 3 0 0 1 0 6H6zm0 6h6a3 3 0 0 1 0 6H6z",
|
|
2132
|
+
strokeWidth: "1.8"
|
|
2133
|
+
}
|
|
2134
|
+
)
|
|
2135
|
+
},
|
|
2136
|
+
{
|
|
2137
|
+
format: "italic",
|
|
2138
|
+
label: "Italic",
|
|
2139
|
+
icon: /* @__PURE__ */ jsx5("path", { d: "M8.5 3.5h6M5.5 16.5h6M11.5 3.5l-3 13", strokeWidth: "1.6" })
|
|
2140
|
+
},
|
|
2141
|
+
{
|
|
2142
|
+
format: "strikethrough",
|
|
2143
|
+
label: "Strikethrough",
|
|
2144
|
+
icon: /* @__PURE__ */ jsx5(
|
|
2145
|
+
"path",
|
|
2146
|
+
{
|
|
2147
|
+
d: "M4 10h12M13.5 5.5c-.6-1.2-2-2-3.5-2-2 0-3.5 1.2-3.5 2.8 0 .5.1.9.4 1.3m-.4 5c.5 1.6 2 2.9 3.9 2.9 2 0 3.6-1.2 3.6-2.9 0-.4-.1-.8-.2-1.1",
|
|
2148
|
+
strokeWidth: "1.6"
|
|
2149
|
+
}
|
|
2150
|
+
)
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
format: "code",
|
|
2154
|
+
label: "Inline code",
|
|
2155
|
+
icon: /* @__PURE__ */ jsx5("path", { d: "M7.5 6L4 10l3.5 4M12.5 6L16 10l-3.5 4", strokeWidth: "1.6" })
|
|
2156
|
+
}
|
|
2157
|
+
];
|
|
2158
|
+
function ToolbarPlugin() {
|
|
2159
|
+
const [editor] = useLexicalComposerContext8();
|
|
2160
|
+
const [activeFormats, setActiveFormats] = useState4(
|
|
2161
|
+
/* @__PURE__ */ new Set()
|
|
2162
|
+
);
|
|
2163
|
+
useEffect8(() => {
|
|
2164
|
+
return editor.registerUpdateListener(({ editorState }) => {
|
|
2165
|
+
editorState.read(() => {
|
|
2166
|
+
const selection = $getSelection3();
|
|
2167
|
+
if (!$isRangeSelection3(selection)) {
|
|
2168
|
+
setActiveFormats(/* @__PURE__ */ new Set());
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const next = /* @__PURE__ */ new Set();
|
|
2172
|
+
for (const { format } of FORMAT_BUTTONS) {
|
|
2173
|
+
if (selection.hasFormat(format)) next.add(format);
|
|
2174
|
+
}
|
|
2175
|
+
setActiveFormats(next);
|
|
2176
|
+
});
|
|
2177
|
+
});
|
|
2178
|
+
}, [editor]);
|
|
2179
|
+
const insertTable = useCallback4(() => {
|
|
2180
|
+
editor.dispatchCommand(INSERT_TABLE_COMMAND, {
|
|
2181
|
+
columns: "3",
|
|
2182
|
+
rows: "3",
|
|
2183
|
+
includeHeaders: { rows: true, columns: false }
|
|
2184
|
+
});
|
|
2185
|
+
}, [editor]);
|
|
2186
|
+
const insertDrawing = useCallback4(() => {
|
|
2187
|
+
editor.update(() => {
|
|
2188
|
+
const drawing = $createDrawingNode();
|
|
2189
|
+
$insertNodeToNearestRoot(drawing);
|
|
2190
|
+
});
|
|
2191
|
+
}, [editor]);
|
|
2192
|
+
return /* @__PURE__ */ jsxs3("div", { className: "zui-text-editor-toolbar", children: [
|
|
2193
|
+
FORMAT_BUTTONS.map(({ format, label, icon }) => /* @__PURE__ */ jsx5(
|
|
2194
|
+
"button",
|
|
2195
|
+
{
|
|
2196
|
+
type: "button",
|
|
2197
|
+
title: label,
|
|
2198
|
+
"aria-label": label,
|
|
2199
|
+
"aria-pressed": activeFormats.has(format),
|
|
2200
|
+
className: `zui-text-editor-toolbar-button ${activeFormats.has(format) ? "is-active" : ""}`,
|
|
2201
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2202
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, format),
|
|
2203
|
+
children: /* @__PURE__ */ jsx5(
|
|
2204
|
+
"svg",
|
|
2205
|
+
{
|
|
2206
|
+
viewBox: "0 0 20 20",
|
|
2207
|
+
width: "16",
|
|
2208
|
+
height: "16",
|
|
2209
|
+
fill: "none",
|
|
2210
|
+
stroke: "currentColor",
|
|
2211
|
+
strokeLinecap: "round",
|
|
2212
|
+
strokeLinejoin: "round",
|
|
2213
|
+
children: icon
|
|
2214
|
+
}
|
|
2215
|
+
)
|
|
2216
|
+
},
|
|
2217
|
+
format
|
|
2218
|
+
)),
|
|
2219
|
+
/* @__PURE__ */ jsx5("div", { className: "zui-text-editor-toolbar-divider" }),
|
|
2220
|
+
/* @__PURE__ */ jsx5(
|
|
2221
|
+
"button",
|
|
2222
|
+
{
|
|
2223
|
+
type: "button",
|
|
2224
|
+
title: "Insert table",
|
|
2225
|
+
"aria-label": "Insert table",
|
|
2226
|
+
className: "zui-text-editor-toolbar-button",
|
|
2227
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2228
|
+
onClick: insertTable,
|
|
2229
|
+
children: /* @__PURE__ */ jsxs3(
|
|
2230
|
+
"svg",
|
|
2231
|
+
{
|
|
2232
|
+
viewBox: "0 0 20 20",
|
|
2233
|
+
width: "16",
|
|
2234
|
+
height: "16",
|
|
2235
|
+
fill: "none",
|
|
2236
|
+
stroke: "currentColor",
|
|
2237
|
+
strokeWidth: "1.5",
|
|
2238
|
+
strokeLinecap: "round",
|
|
2239
|
+
children: [
|
|
2240
|
+
/* @__PURE__ */ jsx5("rect", { x: "3", y: "3.5", width: "14", height: "13", rx: "1.5" }),
|
|
2241
|
+
/* @__PURE__ */ jsx5("path", { d: "M3 8h14M8 8v8.5M13 8v8.5" })
|
|
2242
|
+
]
|
|
2243
|
+
}
|
|
2244
|
+
)
|
|
2245
|
+
}
|
|
2246
|
+
),
|
|
2247
|
+
/* @__PURE__ */ jsx5(
|
|
2248
|
+
"button",
|
|
2249
|
+
{
|
|
2250
|
+
type: "button",
|
|
2251
|
+
title: "Insert drawing",
|
|
2252
|
+
"aria-label": "Insert drawing",
|
|
2253
|
+
className: "zui-text-editor-toolbar-button",
|
|
2254
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2255
|
+
onClick: insertDrawing,
|
|
2256
|
+
children: /* @__PURE__ */ jsxs3(
|
|
2257
|
+
"svg",
|
|
2258
|
+
{
|
|
2259
|
+
viewBox: "0 0 20 20",
|
|
2260
|
+
width: "16",
|
|
2261
|
+
height: "16",
|
|
2262
|
+
fill: "none",
|
|
2263
|
+
stroke: "currentColor",
|
|
2264
|
+
strokeWidth: "1.5",
|
|
2265
|
+
strokeLinecap: "round",
|
|
2266
|
+
strokeLinejoin: "round",
|
|
2267
|
+
children: [
|
|
2268
|
+
/* @__PURE__ */ jsx5("rect", { x: "3", y: "5", width: "8", height: "6", rx: "1.5" }),
|
|
2269
|
+
/* @__PURE__ */ jsx5("path", { d: "M13.5 8h3M14.5 6.5L17.5 8l-3 1.5" }),
|
|
2270
|
+
/* @__PURE__ */ jsx5("ellipse", { cx: "13", cy: "14.5", rx: "4", ry: "2.8" })
|
|
2271
|
+
]
|
|
2272
|
+
}
|
|
2273
|
+
)
|
|
2274
|
+
}
|
|
2275
|
+
)
|
|
2276
|
+
] });
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/nodes/FrontmatterNode.ts
|
|
2280
|
+
import {
|
|
2281
|
+
$applyNodeReplacement as $applyNodeReplacement2,
|
|
198
2282
|
$createLineBreakNode,
|
|
199
2283
|
$createTextNode,
|
|
200
2284
|
ElementNode
|
|
@@ -239,7 +2323,7 @@ var FrontmatterNode = class _FrontmatterNode extends ElementNode {
|
|
|
239
2323
|
}
|
|
240
2324
|
};
|
|
241
2325
|
function $createFrontmatterNode() {
|
|
242
|
-
return $
|
|
2326
|
+
return $applyNodeReplacement2(new FrontmatterNode());
|
|
243
2327
|
}
|
|
244
2328
|
function $isFrontmatterNode(node) {
|
|
245
2329
|
return node instanceof FrontmatterNode;
|
|
@@ -272,6 +2356,122 @@ ${body}
|
|
|
272
2356
|
type: "multiline-element"
|
|
273
2357
|
};
|
|
274
2358
|
|
|
2359
|
+
// src/transformers/tableTransformer.ts
|
|
2360
|
+
import {
|
|
2361
|
+
$isParagraphNode as $isParagraphNode2,
|
|
2362
|
+
$isTextNode
|
|
2363
|
+
} from "lexical";
|
|
2364
|
+
import {
|
|
2365
|
+
$convertFromMarkdownString as $convertFromMarkdownString2,
|
|
2366
|
+
$convertToMarkdownString as $convertToMarkdownString2,
|
|
2367
|
+
TRANSFORMERS
|
|
2368
|
+
} from "@lexical/markdown";
|
|
2369
|
+
import {
|
|
2370
|
+
$createTableCellNode,
|
|
2371
|
+
$createTableNode,
|
|
2372
|
+
$createTableRowNode,
|
|
2373
|
+
$isTableCellNode,
|
|
2374
|
+
$isTableNode,
|
|
2375
|
+
$isTableRowNode,
|
|
2376
|
+
TableCellHeaderStates,
|
|
2377
|
+
TableCellNode,
|
|
2378
|
+
TableNode,
|
|
2379
|
+
TableRowNode
|
|
2380
|
+
} from "@lexical/table";
|
|
2381
|
+
var TABLE_ROW_REG_EXP = /^\|(.+)\|\s?$/;
|
|
2382
|
+
var TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/;
|
|
2383
|
+
function getTableColumnsSize(table) {
|
|
2384
|
+
const row = table.getFirstChild();
|
|
2385
|
+
return $isTableRowNode(row) ? row.getChildrenSize() : 0;
|
|
2386
|
+
}
|
|
2387
|
+
function createTableCell(textContent) {
|
|
2388
|
+
const unescaped = textContent.replace(/\\n/g, "\n");
|
|
2389
|
+
const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
|
|
2390
|
+
$convertFromMarkdownString2(unescaped, TRANSFORMERS, cell);
|
|
2391
|
+
return cell;
|
|
2392
|
+
}
|
|
2393
|
+
function mapToTableCells(textContent) {
|
|
2394
|
+
const match = textContent.match(TABLE_ROW_REG_EXP);
|
|
2395
|
+
if (!match || !match[1]) return null;
|
|
2396
|
+
return match[1].split("|").map((text) => createTableCell(text.trim()));
|
|
2397
|
+
}
|
|
2398
|
+
var TABLE = {
|
|
2399
|
+
dependencies: [TableNode, TableRowNode, TableCellNode],
|
|
2400
|
+
export: (node) => {
|
|
2401
|
+
if (!$isTableNode(node)) return null;
|
|
2402
|
+
const output = [];
|
|
2403
|
+
for (const row of node.getChildren()) {
|
|
2404
|
+
if (!$isTableRowNode(row)) continue;
|
|
2405
|
+
const rowOutput = [];
|
|
2406
|
+
let isHeaderRow = false;
|
|
2407
|
+
for (const cell of row.getChildren()) {
|
|
2408
|
+
if (!$isTableCellNode(cell)) continue;
|
|
2409
|
+
rowOutput.push(
|
|
2410
|
+
$convertToMarkdownString2(TRANSFORMERS, cell).replace(/\n/g, "\\n")
|
|
2411
|
+
);
|
|
2412
|
+
if (cell.__headerState === TableCellHeaderStates.ROW) {
|
|
2413
|
+
isHeaderRow = true;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
output.push(`| ${rowOutput.join(" | ")} |`);
|
|
2417
|
+
if (isHeaderRow) {
|
|
2418
|
+
output.push(`| ${rowOutput.map(() => "---").join(" | ")} |`);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return output.join("\n");
|
|
2422
|
+
},
|
|
2423
|
+
regExp: TABLE_ROW_REG_EXP,
|
|
2424
|
+
replace: (parentNode, _children, match) => {
|
|
2425
|
+
if (TABLE_ROW_DIVIDER_REG_EXP.test(match[0])) {
|
|
2426
|
+
const table2 = parentNode.getPreviousSibling();
|
|
2427
|
+
if (!table2 || !$isTableNode(table2)) return;
|
|
2428
|
+
const lastRow = table2.getLastChild();
|
|
2429
|
+
if (!lastRow || !$isTableRowNode(lastRow)) return;
|
|
2430
|
+
lastRow.getChildren().forEach((cell) => {
|
|
2431
|
+
if ($isTableCellNode(cell)) {
|
|
2432
|
+
cell.setHeaderStyles(TableCellHeaderStates.ROW, TableCellHeaderStates.ROW);
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
parentNode.remove();
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
const matchCells = mapToTableCells(match[0]);
|
|
2439
|
+
if (matchCells == null) return;
|
|
2440
|
+
const rows = [matchCells];
|
|
2441
|
+
let sibling = parentNode.getPreviousSibling();
|
|
2442
|
+
let maxCells = matchCells.length;
|
|
2443
|
+
while (sibling) {
|
|
2444
|
+
if (!$isParagraphNode2(sibling) || sibling.getChildrenSize() !== 1) break;
|
|
2445
|
+
const firstChild = sibling.getFirstChild();
|
|
2446
|
+
if (!$isTextNode(firstChild)) break;
|
|
2447
|
+
const cells = mapToTableCells(firstChild.getTextContent());
|
|
2448
|
+
if (cells == null) break;
|
|
2449
|
+
maxCells = Math.max(maxCells, cells.length);
|
|
2450
|
+
rows.unshift(cells);
|
|
2451
|
+
const previousSibling2 = sibling.getPreviousSibling();
|
|
2452
|
+
sibling.remove();
|
|
2453
|
+
sibling = previousSibling2;
|
|
2454
|
+
}
|
|
2455
|
+
const table = $createTableNode();
|
|
2456
|
+
for (const cells of rows) {
|
|
2457
|
+
const tableRow = $createTableRowNode();
|
|
2458
|
+
table.append(tableRow);
|
|
2459
|
+
for (let i = 0; i < maxCells; i++) {
|
|
2460
|
+
tableRow.append(i < cells.length ? cells[i] : createTableCell(""));
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const previousSibling = parentNode.getPreviousSibling();
|
|
2464
|
+
if ($isTableNode(previousSibling) && getTableColumnsSize(previousSibling) === maxCells) {
|
|
2465
|
+
previousSibling.append(...table.getChildren());
|
|
2466
|
+
parentNode.remove();
|
|
2467
|
+
} else {
|
|
2468
|
+
parentNode.replace(table);
|
|
2469
|
+
}
|
|
2470
|
+
table.selectEnd();
|
|
2471
|
+
},
|
|
2472
|
+
type: "element"
|
|
2473
|
+
};
|
|
2474
|
+
|
|
275
2475
|
// src/theme.ts
|
|
276
2476
|
var editorTheme = {
|
|
277
2477
|
ltr: "text-left",
|
|
@@ -342,13 +2542,19 @@ var editorTheme = {
|
|
|
342
2542
|
variable: "text-blue-500"
|
|
343
2543
|
},
|
|
344
2544
|
quote: "border-l-4 border-border pl-4 italic",
|
|
345
|
-
frontmatter: "zui-frontmatter"
|
|
2545
|
+
frontmatter: "zui-frontmatter",
|
|
2546
|
+
table: "zui-table",
|
|
2547
|
+
tableCell: "zui-table-cell",
|
|
2548
|
+
tableCellHeader: "zui-table-cell-header",
|
|
2549
|
+
tableSelected: "zui-table-selected",
|
|
2550
|
+
tableSelection: "zui-table-selection",
|
|
2551
|
+
drawing: "zui-drawing"
|
|
346
2552
|
};
|
|
347
2553
|
|
|
348
2554
|
// src/MarkdownEditor.tsx
|
|
349
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
350
|
-
var SYNC_TRANSFORMERS = [FRONTMATTER, CHECK_LIST, ...
|
|
351
|
-
var SHORTCUT_TRANSFORMERS =
|
|
2555
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2556
|
+
var SYNC_TRANSFORMERS = [FRONTMATTER, DRAWING, CHECK_LIST, TABLE, ...TRANSFORMERS2];
|
|
2557
|
+
var SHORTCUT_TRANSFORMERS = [TABLE, ...TRANSFORMERS2.filter((t) => t !== CODE)];
|
|
352
2558
|
function onError(error) {
|
|
353
2559
|
console.error(error);
|
|
354
2560
|
}
|
|
@@ -361,11 +2567,15 @@ var editorNodes = [
|
|
|
361
2567
|
CodeHighlightNode,
|
|
362
2568
|
AutoLinkNode,
|
|
363
2569
|
LinkNode,
|
|
364
|
-
FrontmatterNode
|
|
2570
|
+
FrontmatterNode,
|
|
2571
|
+
TableNode2,
|
|
2572
|
+
TableRowNode2,
|
|
2573
|
+
TableCellNode2,
|
|
2574
|
+
DrawingNode
|
|
365
2575
|
];
|
|
366
2576
|
function ReadOnlyPlugin({ readOnly }) {
|
|
367
|
-
const [editor] =
|
|
368
|
-
|
|
2577
|
+
const [editor] = useLexicalComposerContext9();
|
|
2578
|
+
useEffect9(() => {
|
|
369
2579
|
editor.setEditable(!readOnly);
|
|
370
2580
|
}, [editor, readOnly]);
|
|
371
2581
|
return null;
|
|
@@ -377,17 +2587,20 @@ function MarkdownEditor({
|
|
|
377
2587
|
readOnly = false,
|
|
378
2588
|
className,
|
|
379
2589
|
mode = "edit-md",
|
|
380
|
-
autoFocus = false
|
|
2590
|
+
autoFocus = false,
|
|
2591
|
+
toolbar = true,
|
|
2592
|
+
outline = false,
|
|
2593
|
+
foldable = true
|
|
381
2594
|
}) {
|
|
382
|
-
const latestValueRef =
|
|
383
|
-
const [mountKey, setMountKey] =
|
|
384
|
-
const [capturedMarkdown, setCapturedMarkdown] =
|
|
385
|
-
|
|
2595
|
+
const latestValueRef = useRef4(value ?? "");
|
|
2596
|
+
const [mountKey, setMountKey] = useState5(0);
|
|
2597
|
+
const [capturedMarkdown, setCapturedMarkdown] = useState5(value ?? "");
|
|
2598
|
+
useEffect9(() => {
|
|
386
2599
|
if (value !== void 0) {
|
|
387
2600
|
latestValueRef.current = value;
|
|
388
2601
|
}
|
|
389
2602
|
}, [value]);
|
|
390
|
-
const handleTextChange =
|
|
2603
|
+
const handleTextChange = useCallback5(
|
|
391
2604
|
(e) => {
|
|
392
2605
|
const newValue = e.target.value;
|
|
393
2606
|
latestValueRef.current = newValue;
|
|
@@ -395,15 +2608,15 @@ function MarkdownEditor({
|
|
|
395
2608
|
},
|
|
396
2609
|
[onChange]
|
|
397
2610
|
);
|
|
398
|
-
const handleLexicalChange =
|
|
2611
|
+
const handleLexicalChange = useCallback5(
|
|
399
2612
|
(newValue) => {
|
|
400
2613
|
latestValueRef.current = newValue;
|
|
401
2614
|
onChange?.(newValue);
|
|
402
2615
|
},
|
|
403
2616
|
[onChange]
|
|
404
2617
|
);
|
|
405
|
-
const prevModeRef =
|
|
406
|
-
|
|
2618
|
+
const prevModeRef = useRef4(mode);
|
|
2619
|
+
useEffect9(() => {
|
|
407
2620
|
if (prevModeRef.current === "edit-raw" && mode !== "edit-raw") {
|
|
408
2621
|
setCapturedMarkdown(latestValueRef.current);
|
|
409
2622
|
setMountKey((k) => k + 1);
|
|
@@ -420,7 +2633,7 @@ function MarkdownEditor({
|
|
|
420
2633
|
[]
|
|
421
2634
|
);
|
|
422
2635
|
if (mode === "edit-raw") {
|
|
423
|
-
return /* @__PURE__ */
|
|
2636
|
+
return /* @__PURE__ */ jsx6("div", { className: `zui-text-editor ${className ?? ""}`, children: /* @__PURE__ */ jsx6(
|
|
424
2637
|
"textarea",
|
|
425
2638
|
{
|
|
426
2639
|
className: "zui-text-editor-textarea",
|
|
@@ -433,22 +2646,29 @@ function MarkdownEditor({
|
|
|
433
2646
|
}
|
|
434
2647
|
) });
|
|
435
2648
|
}
|
|
436
|
-
return /* @__PURE__ */
|
|
437
|
-
/* @__PURE__ */
|
|
438
|
-
|
|
439
|
-
{
|
|
440
|
-
|
|
441
|
-
|
|
2649
|
+
return /* @__PURE__ */ jsx6(LexicalComposer, { initialConfig, children: /* @__PURE__ */ jsxs4("div", { className: `zui-text-editor ${className ?? ""}`, children: [
|
|
2650
|
+
toolbar && mode === "edit-md" && !readOnly && /* @__PURE__ */ jsx6(ToolbarPlugin, {}),
|
|
2651
|
+
/* @__PURE__ */ jsxs4("div", { className: "zui-text-editor-body", children: [
|
|
2652
|
+
/* @__PURE__ */ jsxs4("div", { className: "zui-text-editor-main", children: [
|
|
2653
|
+
/* @__PURE__ */ jsx6(
|
|
2654
|
+
RichTextPlugin,
|
|
442
2655
|
{
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
2656
|
+
contentEditable: /* @__PURE__ */ jsx6(
|
|
2657
|
+
ContentEditable,
|
|
2658
|
+
{
|
|
2659
|
+
className: "zui-text-editor-content",
|
|
2660
|
+
"aria-placeholder": placeholder,
|
|
2661
|
+
placeholder: /* @__PURE__ */ jsx6("div", { className: "zui-text-editor-placeholder", children: placeholder })
|
|
2662
|
+
}
|
|
2663
|
+
),
|
|
2664
|
+
ErrorBoundary: LexicalErrorBoundary
|
|
446
2665
|
}
|
|
447
2666
|
),
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
2667
|
+
foldable && /* @__PURE__ */ jsx6(FoldingPlugin, {})
|
|
2668
|
+
] }),
|
|
2669
|
+
outline && /* @__PURE__ */ jsx6(OutlinePlugin, {})
|
|
2670
|
+
] }),
|
|
2671
|
+
/* @__PURE__ */ jsx6(
|
|
452
2672
|
MarkdownSyncPlugin,
|
|
453
2673
|
{
|
|
454
2674
|
initialMarkdown: mode === "view" ? value ?? "" : capturedMarkdown,
|
|
@@ -456,22 +2676,28 @@ function MarkdownEditor({
|
|
|
456
2676
|
transformers: SYNC_TRANSFORMERS
|
|
457
2677
|
}
|
|
458
2678
|
),
|
|
459
|
-
/* @__PURE__ */
|
|
460
|
-
/* @__PURE__ */
|
|
461
|
-
/* @__PURE__ */
|
|
462
|
-
/* @__PURE__ */
|
|
463
|
-
/* @__PURE__ */
|
|
464
|
-
/* @__PURE__ */
|
|
465
|
-
/* @__PURE__ */
|
|
466
|
-
/* @__PURE__ */
|
|
467
|
-
/* @__PURE__ */
|
|
468
|
-
|
|
2679
|
+
/* @__PURE__ */ jsx6(HistoryPlugin, {}),
|
|
2680
|
+
/* @__PURE__ */ jsx6(ListPlugin, {}),
|
|
2681
|
+
/* @__PURE__ */ jsx6(CheckListPlugin, {}),
|
|
2682
|
+
/* @__PURE__ */ jsx6(ChecklistShortcutPlugin, {}),
|
|
2683
|
+
/* @__PURE__ */ jsx6(CodeBlockShortcutPlugin, {}),
|
|
2684
|
+
/* @__PURE__ */ jsx6(CodeHighlightPlugin, {}),
|
|
2685
|
+
/* @__PURE__ */ jsx6(TablePlugin, {}),
|
|
2686
|
+
/* @__PURE__ */ jsx6(LinkPlugin, {}),
|
|
2687
|
+
/* @__PURE__ */ jsx6(MarkdownShortcutPlugin, { transformers: SHORTCUT_TRANSFORMERS }),
|
|
2688
|
+
/* @__PURE__ */ jsx6(ReadOnlyPlugin, { readOnly: readOnly || mode === "view" }),
|
|
2689
|
+
autoFocus && /* @__PURE__ */ jsx6(AutoFocusPlugin, {})
|
|
469
2690
|
] }) }, mountKey);
|
|
470
2691
|
}
|
|
471
2692
|
export {
|
|
2693
|
+
$createDrawingNode,
|
|
472
2694
|
$createFrontmatterNode,
|
|
2695
|
+
$isDrawingNode,
|
|
473
2696
|
$isFrontmatterNode,
|
|
2697
|
+
DRAWING,
|
|
2698
|
+
DrawingNode,
|
|
474
2699
|
FRONTMATTER,
|
|
475
2700
|
FrontmatterNode,
|
|
476
|
-
MarkdownEditor
|
|
2701
|
+
MarkdownEditor,
|
|
2702
|
+
TABLE
|
|
477
2703
|
};
|