@dou-ui/flow 0.0.0-stage → 0.1.0-alpha.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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/collaboration/index.js +360 -0
  4. package/dist/collaboration/index.js.map +1 -0
  5. package/dist/collaboration/peers.js +76 -0
  6. package/dist/collaboration/peers.js.map +1 -0
  7. package/dist/components/default-node.js +6 -0
  8. package/dist/components/default-node.js.map +1 -0
  9. package/dist/components/default-node.vue_vue_type_script_setup_true_lang.js +87 -0
  10. package/dist/components/default-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
  11. package/dist/components/default-node2.js +116 -0
  12. package/dist/components/default-node2.js.map +1 -0
  13. package/dist/components/flow-controls.js +6 -0
  14. package/dist/components/flow-controls.js.map +1 -0
  15. package/dist/components/flow-controls.vue_vue_type_script_setup_true_lang.js +148 -0
  16. package/dist/components/flow-controls.vue_vue_type_script_setup_true_lang.js.map +1 -0
  17. package/dist/components/flow-menu.js +6 -0
  18. package/dist/components/flow-menu.js.map +1 -0
  19. package/dist/components/flow-menu.vue_vue_type_script_setup_true_lang.js +52 -0
  20. package/dist/components/flow-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
  21. package/dist/components/flow-minimap.js +6 -0
  22. package/dist/components/flow-minimap.js.map +1 -0
  23. package/dist/components/flow-minimap.vue_vue_type_script_setup_true_lang.js +238 -0
  24. package/dist/components/flow-minimap.vue_vue_type_script_setup_true_lang.js.map +1 -0
  25. package/dist/components/flow-port.js +6 -0
  26. package/dist/components/flow-port.js.map +1 -0
  27. package/dist/components/flow-port.vue_vue_type_script_setup_true_lang.js +57 -0
  28. package/dist/components/flow-port.vue_vue_type_script_setup_true_lang.js.map +1 -0
  29. package/dist/components/flow-quick-add.js +6 -0
  30. package/dist/components/flow-quick-add.js.map +1 -0
  31. package/dist/components/flow-quick-add.vue_vue_type_script_setup_true_lang.js +126 -0
  32. package/dist/components/flow-quick-add.vue_vue_type_script_setup_true_lang.js.map +1 -0
  33. package/dist/components/flow-toolbar.js +6 -0
  34. package/dist/components/flow-toolbar.js.map +1 -0
  35. package/dist/components/flow-toolbar.vue_vue_type_script_setup_true_lang.js +161 -0
  36. package/dist/components/flow-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  37. package/dist/components/group-node.js +6 -0
  38. package/dist/components/group-node.js.map +1 -0
  39. package/dist/components/group-node.vue_vue_type_script_setup_true_lang.js +84 -0
  40. package/dist/components/group-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
  41. package/dist/components/group-node2.js +42 -0
  42. package/dist/components/group-node2.js.map +1 -0
  43. package/dist/components/node-view.js +6 -0
  44. package/dist/components/node-view.js.map +1 -0
  45. package/dist/components/node-view.vue_vue_type_script_setup_true_lang.js +179 -0
  46. package/dist/components/node-view.vue_vue_type_script_setup_true_lang.js.map +1 -0
  47. package/dist/context.js +13 -0
  48. package/dist/context.js.map +1 -0
  49. package/dist/core/clipboard.js +37 -0
  50. package/dist/core/clipboard.js.map +1 -0
  51. package/dist/core/connect.js +74 -0
  52. package/dist/core/connect.js.map +1 -0
  53. package/dist/core/edge-types.js +240 -0
  54. package/dist/core/edge-types.js.map +1 -0
  55. package/dist/core/extension.js +63 -0
  56. package/dist/core/extension.js.map +1 -0
  57. package/dist/core/geometry.js +232 -0
  58. package/dist/core/geometry.js.map +1 -0
  59. package/dist/core/groups.js +76 -0
  60. package/dist/core/groups.js.map +1 -0
  61. package/dist/core/history.js +79 -0
  62. package/dist/core/history.js.map +1 -0
  63. package/dist/core/model.js +21 -0
  64. package/dist/core/model.js.map +1 -0
  65. package/dist/core/node-types.js +32 -0
  66. package/dist/core/node-types.js.map +1 -0
  67. package/dist/core/order.js +42 -0
  68. package/dist/core/order.js.map +1 -0
  69. package/dist/core/transaction.js +288 -0
  70. package/dist/core/transaction.js.map +1 -0
  71. package/dist/document.js +220 -0
  72. package/dist/document.js.map +1 -0
  73. package/dist/export/color.js +38 -0
  74. package/dist/export/color.js.map +1 -0
  75. package/dist/export/context.js +223 -0
  76. package/dist/export/context.js.map +1 -0
  77. package/dist/export/draw.js +249 -0
  78. package/dist/export/draw.js.map +1 -0
  79. package/dist/flow-editor.js +6 -0
  80. package/dist/flow-editor.js.map +1 -0
  81. package/dist/flow-editor.vue_vue_type_script_setup_true_lang.js +3721 -0
  82. package/dist/flow-editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
  83. package/dist/index.js +31 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/kit.js +234 -0
  86. package/dist/kit.js.map +1 -0
  87. package/dist/layers.js +150 -0
  88. package/dist/layers.js.map +1 -0
  89. package/dist/locale.js +426 -0
  90. package/dist/locale.js.map +1 -0
  91. package/dist/style.css +1 -0
  92. package/dist/style.d.ts +1 -0
  93. package/dist/style.js +8 -0
  94. package/dist/types/collaboration/index.d.ts +42 -0
  95. package/dist/types/collaboration/peers.d.ts +27 -0
  96. package/dist/types/components/default-node.d.ts +18 -0
  97. package/dist/types/components/default-node.vue.d.ts +6 -0
  98. package/dist/types/components/flow-controls.vue.d.ts +8 -0
  99. package/dist/types/components/flow-menu.vue.d.ts +29 -0
  100. package/dist/types/components/flow-minimap.vue.d.ts +8 -0
  101. package/dist/types/components/flow-port.vue.d.ts +9 -0
  102. package/dist/types/components/flow-quick-add.vue.d.ts +31 -0
  103. package/dist/types/components/flow-toolbar.vue.d.ts +25 -0
  104. package/dist/types/components/group-node.d.ts +13 -0
  105. package/dist/types/components/group-node.vue.d.ts +6 -0
  106. package/dist/types/components/node-view.vue.d.ts +28 -0
  107. package/dist/types/context.d.ts +362 -0
  108. package/dist/types/core/clipboard.d.ts +9 -0
  109. package/dist/types/core/connect.d.ts +63 -0
  110. package/dist/types/core/edge-types.d.ts +50 -0
  111. package/dist/types/core/extension.d.ts +141 -0
  112. package/dist/types/core/geometry.d.ts +63 -0
  113. package/dist/types/core/groups.d.ts +23 -0
  114. package/dist/types/core/history.d.ts +24 -0
  115. package/dist/types/core/menu.d.ts +40 -0
  116. package/dist/types/core/model.d.ts +87 -0
  117. package/dist/types/core/node-types.d.ts +103 -0
  118. package/dist/types/core/order.d.ts +7 -0
  119. package/dist/types/core/transaction.d.ts +97 -0
  120. package/dist/types/document.d.ts +57 -0
  121. package/dist/types/export/color.d.ts +7 -0
  122. package/dist/types/export/context.d.ts +90 -0
  123. package/dist/types/export/draw.d.ts +72 -0
  124. package/dist/types/flow-editor.vue.d.ts +63 -0
  125. package/dist/types/index.d.ts +202 -0
  126. package/dist/types/kit.d.ts +22 -0
  127. package/dist/types/layers.d.ts +24 -0
  128. package/dist/types/locale.d.ts +172 -0
  129. package/dist/types/whiteboard/board-draft.vue.d.ts +3 -0
  130. package/dist/types/whiteboard/board-node.vue.d.ts +19 -0
  131. package/dist/types/whiteboard/connector.d.ts +12 -0
  132. package/dist/types/whiteboard/draw.d.ts +5 -0
  133. package/dist/types/whiteboard/image.d.ts +27 -0
  134. package/dist/types/whiteboard/index.d.ts +6 -0
  135. package/dist/types/whiteboard/ink.d.ts +30 -0
  136. package/dist/types/whiteboard/kit.d.ts +36 -0
  137. package/dist/types/whiteboard/model.d.ts +79 -0
  138. package/dist/types/whiteboard/pen.d.ts +7 -0
  139. package/dist/types/whiteboard/state.d.ts +37 -0
  140. package/dist/types/whiteboard/transform.d.ts +67 -0
  141. package/dist/types/whiteboard/whiteboard-toolbar.vue.d.ts +3 -0
  142. package/dist/whiteboard/board-draft.js +6 -0
  143. package/dist/whiteboard/board-draft.js.map +1 -0
  144. package/dist/whiteboard/board-draft.vue_vue_type_script_setup_true_lang.js +58 -0
  145. package/dist/whiteboard/board-draft.vue_vue_type_script_setup_true_lang.js.map +1 -0
  146. package/dist/whiteboard/board-node.js +6 -0
  147. package/dist/whiteboard/board-node.js.map +1 -0
  148. package/dist/whiteboard/board-node.vue_vue_type_script_setup_true_lang.js +332 -0
  149. package/dist/whiteboard/board-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
  150. package/dist/whiteboard/connector.js +180 -0
  151. package/dist/whiteboard/connector.js.map +1 -0
  152. package/dist/whiteboard/draw.js +68 -0
  153. package/dist/whiteboard/draw.js.map +1 -0
  154. package/dist/whiteboard/image.js +104 -0
  155. package/dist/whiteboard/image.js.map +1 -0
  156. package/dist/whiteboard/index.js +5 -0
  157. package/dist/whiteboard/ink.js +168 -0
  158. package/dist/whiteboard/ink.js.map +1 -0
  159. package/dist/whiteboard/kit.js +406 -0
  160. package/dist/whiteboard/kit.js.map +1 -0
  161. package/dist/whiteboard/model.js +52 -0
  162. package/dist/whiteboard/model.js.map +1 -0
  163. package/dist/whiteboard/pen.js +88 -0
  164. package/dist/whiteboard/pen.js.map +1 -0
  165. package/dist/whiteboard/state.js +26 -0
  166. package/dist/whiteboard/state.js.map +1 -0
  167. package/dist/whiteboard/transform.js +256 -0
  168. package/dist/whiteboard/transform.js.map +1 -0
  169. package/dist/whiteboard/whiteboard-toolbar.js +6 -0
  170. package/dist/whiteboard/whiteboard-toolbar.js.map +1 -0
  171. package/dist/whiteboard/whiteboard-toolbar.vue_vue_type_script_setup_true_lang.js +171 -0
  172. package/dist/whiteboard/whiteboard-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  173. package/package.json +74 -4
@@ -0,0 +1,3721 @@
1
+ import { flowEditorKey } from "./context.js";
2
+ import { boundsOf, clampPan, intersects, intersectsTurned, rectFrom, snapBox } from "./core/geometry.js";
3
+ import { labelOf, nodeBounds, portsOf, resizeOf } from "./core/node-types.js";
4
+ import node_view_default from "./components/node-view.js";
5
+ import flow_menu_default from "./components/flow-menu.js";
6
+ import flow_quick_add_default from "./components/flow-quick-add.js";
7
+ import flow_toolbar_default from "./components/flow-toolbar.js";
8
+ import { drawTools } from "./export/context.js";
9
+ import { drawFlow, pathPoints, readImage } from "./export/draw.js";
10
+ import { defaultNode } from "./components/default-node2.js";
11
+ import { bezierEdge } from "./core/edge-types.js";
12
+ import { canJoin, displacedEdges, joinRefusal, primaryType, typesMatch } from "./core/connect.js";
13
+ import { FlowTransaction } from "./core/transaction.js";
14
+ import { FlowHistory } from "./core/history.js";
15
+ import { flowOrder } from "./core/order.js";
16
+ import { resolveFlowExtensions } from "./core/extension.js";
17
+ import { format, useFlowMessages } from "./locale.js";
18
+ import { flowKit } from "./kit.js";
19
+ import { readClipboard, writeClipboard } from "./core/clipboard.js";
20
+ import { createId, emptyDocument, emptySelection } from "./core/model.js";
21
+ import { readDocument } from "./document.js";
22
+ import { FlowTree, containerAt } from "./core/groups.js";
23
+ import { DouFindReplace, DouToggle, DouTooltip, fitPixelRatio, holdPointer, layoutGraph, matchesHotkey, svgToPNG, useLocale, useReducedMotion } from "dou-ui";
24
+ import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, customRef, defineComponent, guardReactiveProps, isRef, mergeModels, nextTick, normalizeClass, normalizeProps, normalizeStyle, onBeforeUnmount, onMounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, shallowReactive, shallowRef, toDisplayString, toRaw, unref, useModel, useTemplateRef, vModelText, watch, withCtx, withDirectives } from "vue";
25
+ import { LetterCaseIcon } from "@dou-ui/icons";
26
+ // src/flow-editor.vue?vue&type=script&setup=true&lang.ts
27
+ var _hoisted_1 = ["aria-label", "aria-description"];
28
+ var _hoisted_2 = [
29
+ "data-node-id",
30
+ "aria-roledescription",
31
+ "aria-label"
32
+ ];
33
+ var _hoisted_3 = {
34
+ key: 0,
35
+ class: "dou-flow-editor__simple-label"
36
+ };
37
+ var _hoisted_4 = {
38
+ class: "dou-flow-editor__edges",
39
+ "aria-hidden": "true"
40
+ };
41
+ var _hoisted_5 = ["data-edge-id", "data-edge-type"];
42
+ var _hoisted_6 = ["d"];
43
+ var _hoisted_7 = ["d"];
44
+ var _hoisted_8 = ["d"];
45
+ var _hoisted_9 = ["d"];
46
+ var _hoisted_10 = ["data-edge-id", "data-value"];
47
+ var _hoisted_11 = ["aria-label"];
48
+ var _hoisted_12 = [
49
+ "data-node-id",
50
+ "aria-roledescription",
51
+ "aria-label"
52
+ ];
53
+ var _hoisted_13 = {
54
+ key: 0,
55
+ class: "dou-flow-editor__simple-label"
56
+ };
57
+ var _hoisted_14 = {
58
+ key: 1,
59
+ class: "dou-flow-editor__guides",
60
+ "aria-hidden": "true"
61
+ };
62
+ var _hoisted_15 = [
63
+ "x1",
64
+ "y1",
65
+ "x2",
66
+ "y2"
67
+ ];
68
+ var _hoisted_16 = { class: "dou-flow-editor__ghost-card" };
69
+ var _hoisted_17 = {
70
+ key: 0,
71
+ class: "dou-flow-editor__ghost-icon"
72
+ };
73
+ var _hoisted_18 = {
74
+ class: "dou-flow-editor__live",
75
+ "aria-live": "polite"
76
+ };
77
+ var _hoisted_19 = {
78
+ key: 1,
79
+ class: "dou-flow-editor__notice",
80
+ "aria-hidden": "true"
81
+ };
82
+ var LONG_PRESS = 500;
83
+ /** How long a finger rests on a palette item before it picks the node up, in ms. */
84
+ var PICK_UP = 300;
85
+ /** How close to the edge a drag starts scrolling the canvas, in px. */
86
+ var EDGE = 24;
87
+ /** Auto-scroll speed in px per frame, at `d` px from the edge. */
88
+ var EDGE_REACH = 80;
89
+ /** Every edge's route and the box around it; independent of the view, so panning skips it. */
90
+ var FIT_PADDING = 40;
91
+ /**
92
+ * Shows the given nodes, or all. With `floor`, zooms out no further than
93
+ * that; what still doesn't fit starts at the top left, to be panned to.
94
+ */
95
+ var PANEL_GAP = 16;
96
+ var FIELDS = "input, textarea, [contenteditable]:not([contenteditable=\"false\"]), [data-dou-nodrag]";
97
+ var MAGNET = 24;
98
+ var FIND_INSET = 12;
99
+ var DRAW_MARGIN = 200;
100
+ /** The part of the canvas that is drawn: the view and a margin around it. */
101
+ var TINY = .2;
102
+ var flow_editor_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
103
+ name: "DouFlowEditor",
104
+ __name: "flow-editor",
105
+ props: /*@__PURE__*/ mergeModels({
106
+ extensions: {},
107
+ nodeTypes: {},
108
+ edgeTypes: {},
109
+ edgeType: { default: void 0 },
110
+ simpleZoom: { default: .4 },
111
+ grid: { default: 8 },
112
+ guides: {
113
+ type: Boolean,
114
+ default: true
115
+ },
116
+ minZoom: { default: .1 },
117
+ maxZoom: { default: 4 },
118
+ direction: { default: "horizontal" },
119
+ fitView: {
120
+ type: Boolean,
121
+ default: false
122
+ },
123
+ fitMinZoom: { default: .5 },
124
+ readonly: {
125
+ type: Boolean,
126
+ default: false
127
+ },
128
+ searchable: {
129
+ type: Boolean,
130
+ default: true
131
+ },
132
+ panOnScroll: {
133
+ type: Boolean,
134
+ default: true
135
+ },
136
+ portTypes: {},
137
+ acyclic: { type: Boolean },
138
+ canConnect: {},
139
+ ariaLabel: {},
140
+ messages: {}
141
+ }, {
142
+ "modelValue": { default: emptyDocument },
143
+ "modelModifiers": {},
144
+ "selection": { default: emptySelection },
145
+ "selectionModifiers": {},
146
+ "viewport": { default: () => ({
147
+ x: 0,
148
+ y: 0,
149
+ zoom: 1
150
+ }) },
151
+ "viewportModifiers": {}
152
+ }),
153
+ emits: /*@__PURE__*/ mergeModels([
154
+ "transaction",
155
+ "transactionCancel",
156
+ "nodeClick",
157
+ "nodeDblclick",
158
+ "nodeContextmenu",
159
+ "edgeClick",
160
+ "edgeContextmenu",
161
+ "canvasClick",
162
+ "canvasContextmenu",
163
+ "connect",
164
+ "connectToCanvas"
165
+ ], [
166
+ "update:modelValue",
167
+ "update:selection",
168
+ "update:viewport"
169
+ ]),
170
+ setup(__props, { expose: __expose, emit: __emit }) {
171
+ const props = __props;
172
+ const model = useModel(__props, "modelValue");
173
+ /** The `v-model`, with a wrongly shaped one read as well as it can be. */
174
+ const doc = computed({
175
+ get: () => readDocument(model.value),
176
+ set: (value) => model.value = value
177
+ });
178
+ const selection = useModel(__props, "selection");
179
+ const viewport = useModel(__props, "viewport");
180
+ const emit = __emit;
181
+ const messages = useFlowMessages(() => props.messages);
182
+ const { t: common } = useLocale();
183
+ const reducedMotion = useReducedMotion();
184
+ const root = useTemplateRef("root");
185
+ /** Movement in screen px before a press becomes a drag. */
186
+ const THRESHOLD = {
187
+ mouse: 3,
188
+ touch: 6
189
+ };
190
+ const edgeSpeed = (d) => d < EDGE ? Math.min(1, (EDGE - d) / EDGE) * 12 : 0;
191
+ /** The axis that levels two ports on this side. */
192
+ const level = (side) => side === "left" || side === "right" ? "y" : "x";
193
+ const same = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
194
+ const resolved = computed(() => resolveFlowExtensions(props.extensions ?? flowKit()));
195
+ const types = computed(() => {
196
+ const map = new Map(resolved.value.nodeTypes);
197
+ for (const spec of props.nodeTypes ?? []) map.set(spec.type, spec);
198
+ return map;
199
+ });
200
+ const portTypes = computed(() => ({
201
+ ...resolved.value.portTypes,
202
+ ...props.portTypes
203
+ }));
204
+ const edgeTypes = computed(() => {
205
+ const map = new Map(resolved.value.edgeTypes);
206
+ for (const spec of props.edgeTypes ?? []) map.set(spec.type, spec);
207
+ return map;
208
+ });
209
+ /** The type of edges without one: the prop, else the extensions', else bezier. */
210
+ const defaultEdgeType = computed(() => props.edgeType ?? resolved.value.edgeType ?? "bezier");
211
+ const edgeTypeOf = (type) => edgeTypes.value.get(type ?? defaultEdgeType.value) ?? edgeTypes.value.get(defaultEdgeType.value) ?? bezierEdge;
212
+ const specOf = (node) => types.value.get(node.type ?? "default") ?? types.value.get("default") ?? defaultNode;
213
+ /** Each node's ports by id, worked out once per change rather than per edge. */
214
+ const portsByNode = computed(() => {
215
+ const map = /* @__PURE__ */ new Map();
216
+ for (const node of nodeById.value.values()) map.set(node.id, new Map(portsOf(specOf(node), node).map((port) => [port.id, port])));
217
+ return map;
218
+ });
219
+ function portSpec(end) {
220
+ if (end.port == null) return void 0;
221
+ return portsByNode.value.get(end.node)?.get(end.port);
222
+ }
223
+ const portTypeOf = (port) => {
224
+ const type = primaryType(port);
225
+ return type ? portTypes.value[type] : void 0;
226
+ };
227
+ const tones = /* @__PURE__ */ new Set([
228
+ "neutral",
229
+ "primary",
230
+ "success",
231
+ "warning",
232
+ "danger"
233
+ ]);
234
+ /** A port type's color as CSS: theme tones by name, anything else as is. */
235
+ const colorOf = (spec) => spec?.color ? tones.has(spec.color) ? `var(--dou-color-${spec.color})` : spec.color : void 0;
236
+ const history = new FlowHistory();
237
+ /** An extension's history, used instead of the editor's own. */
238
+ let customHistory = null;
239
+ const canUndo = ref(false);
240
+ const canRedo = ref(false);
241
+ const syncHistory = () => {
242
+ canUndo.value = customHistory ? customHistory.canUndo() : history.canUndo;
243
+ canRedo.value = customHistory ? customHistory.canRedo() : history.canRedo;
244
+ };
245
+ function setSelection(next) {
246
+ const current = selection.value;
247
+ if (same(current.nodes, next.nodes) && same(current.edges, next.edges)) return;
248
+ selection.value = next;
249
+ }
250
+ /** Drops selected ids that are no longer in the document. */
251
+ function pruneSelection(next, sel = selection.value) {
252
+ const nodes = new Set(next.nodes.map((n) => n.id));
253
+ const edges = new Set(next.edges.map((e) => e.id));
254
+ return {
255
+ nodes: sel.nodes.filter((id) => nodes.has(id)),
256
+ edges: sel.edges.filter((id) => edges.has(id))
257
+ };
258
+ }
259
+ let latest = null;
260
+ const docOf = () => latest ?? toRaw(doc.value);
261
+ /** The document as extensions see it: the latest commit, even before `v-model` catches up. */
262
+ const currentDoc = customRef(() => ({
263
+ get: () => {
264
+ const value = doc.value;
265
+ return latest ?? value;
266
+ },
267
+ set: (value) => doc.value = value
268
+ }));
269
+ /** Runs the extensions' `transaction` hooks; `false` when one cancelled the change. */
270
+ function check(tr) {
271
+ for (const hook of resolved.value.transactions) {
272
+ hook(tr, context);
273
+ if (tr.cancelled) {
274
+ emit("transactionCancel", tr);
275
+ refuse(tr.cancelReason ?? messages.value.changeCancelled);
276
+ return false;
277
+ }
278
+ }
279
+ return true;
280
+ }
281
+ function commit(tr) {
282
+ if (!check(tr)) return false;
283
+ if (!latest) nextTick(() => latest = null);
284
+ latest = tr.doc;
285
+ doc.value = tr.doc;
286
+ if (tr.steps.some((s) => s.type === "removeNode" || s.type === "removeEdge")) setSelection(pruneSelection(tr.doc));
287
+ emit("transaction", tr);
288
+ return true;
289
+ }
290
+ function transact(fn, meta) {
291
+ const tr = new FlowTransaction(docOf());
292
+ if (meta) tr.setMeta(meta);
293
+ fn(tr);
294
+ if (!tr.changed) return tr;
295
+ const before = selection.value;
296
+ if (!commit(tr)) return tr;
297
+ if (!customHistory) history.record(tr, before, selection.value);
298
+ syncHistory();
299
+ return tr;
300
+ }
301
+ function undo() {
302
+ if (customHistory) return travel(customHistory.undo, messages.value.undone);
303
+ const result = history.undo(docOf());
304
+ if (!result) return false;
305
+ result.tr.setMeta({
306
+ origin: "undo",
307
+ history: false
308
+ });
309
+ commit(result.tr);
310
+ setSelection(pruneSelection(result.tr.doc, result.selection));
311
+ syncHistory();
312
+ announce(messages.value.undone);
313
+ return true;
314
+ }
315
+ /** Undo or redo through an extension's history. */
316
+ function travel(fn, done) {
317
+ if (!fn()) return false;
318
+ syncHistory();
319
+ announce(done);
320
+ return true;
321
+ }
322
+ function redo() {
323
+ if (customHistory) return travel(customHistory.redo, messages.value.redone);
324
+ const result = history.redo(docOf());
325
+ if (!result) return false;
326
+ result.tr.setMeta({
327
+ origin: "redo",
328
+ history: false
329
+ });
330
+ commit(result.tr);
331
+ setSelection(pruneSelection(result.tr.doc, result.selection));
332
+ syncHistory();
333
+ announce(messages.value.redone);
334
+ return true;
335
+ }
336
+ const metrics = shallowReactive(/* @__PURE__ */ new Map());
337
+ /** Where nodes are drawn while tidying animates them; their real place is already in `doc`. */
338
+ const tween = shallowRef(null);
339
+ /** Nodes shown changed while a handle is dragged, see `previewNode`; the document changes when it is let go. */
340
+ const previews = shallowRef(/* @__PURE__ */ new Map());
341
+ const shownNodes = computed(() => {
342
+ const moving = tween.value;
343
+ const changed = previews.value;
344
+ if (!moving && !changed.size) return doc.value.nodes;
345
+ return doc.value.nodes.map((node) => {
346
+ const at = moving?.get(node.id);
347
+ const patch = changed.get(node.id);
348
+ if (!at && !patch) return node;
349
+ return {
350
+ ...node,
351
+ ...at && {
352
+ x: at.x,
353
+ y: at.y
354
+ },
355
+ ...patch
356
+ };
357
+ });
358
+ });
359
+ function previewNode(id, patch) {
360
+ const next = new Map(previews.value);
361
+ if (patch) next.set(id, patch);
362
+ else if (!next.delete(id)) return;
363
+ previews.value = next;
364
+ }
365
+ const nodeById = computed(() => new Map(shownNodes.value.map((node) => [node.id, node])));
366
+ const tree = computed(() => new FlowTree(doc.value.nodes));
367
+ const isContainer = (node) => !!specOf(node).container;
368
+ /** How far a node is drawn turned, as its type says, in degrees. */
369
+ function nodeRotation(id) {
370
+ const node = nodeById.value.get(id);
371
+ return node && specOf(node).rotation?.(node) || 0;
372
+ }
373
+ /** For each node inside a collapsed container: the outermost collapsed one, where its edges end. */
374
+ const hiddenIn = computed(() => {
375
+ const map = /* @__PURE__ */ new Map();
376
+ for (const node of doc.value.nodes) {
377
+ if (!node.data?.collapsed || !isContainer(node)) continue;
378
+ for (const id of tree.value.descendants(node.id)) {
379
+ let outer;
380
+ for (const a of tree.value.ancestors(id)) if (nodeById.value.get(a)?.data?.collapsed) outer = a;
381
+ if (outer) map.set(id, outer);
382
+ }
383
+ }
384
+ return map;
385
+ });
386
+ watch(hiddenIn, (hidden) => {
387
+ const { nodes, edges } = selection.value;
388
+ if (nodes.some((id) => hidden.has(id))) setSelection({
389
+ nodes: nodes.filter((id) => !hidden.has(id)),
390
+ edges
391
+ });
392
+ });
393
+ /** Containers with a size, innermost last; `skip` leaves some out, e.g. those being dragged. */
394
+ function containerRects(skip) {
395
+ return doc.value.nodes.flatMap((node) => isContainer(node) && !hiddenIn.value.has(node.id) && !skip?.has(node.id) && metrics.has(node.id) ? [{
396
+ id: node.id,
397
+ rect: rectOf(node)
398
+ }] : []);
399
+ }
400
+ /** Two measurements of a node that place it and its ports the same. */
401
+ function sameMetrics(a, b) {
402
+ if (!a || a.width !== b.width || a.height !== b.height || a.ports.size !== b.ports.size) return false;
403
+ for (const [id, p] of b.ports) {
404
+ const q = a.ports.get(id);
405
+ if (!q || q.x !== p.x || q.y !== p.y || q.side !== p.side) return false;
406
+ }
407
+ return true;
408
+ }
409
+ function onMeasure(id, value) {
410
+ if (!sameMetrics(metrics.get(id), value)) metrics.set(id, value);
411
+ const wait = pending.get(id);
412
+ if (!wait) return;
413
+ pending.delete(id);
414
+ const port = wait.port ? value.ports.get(wait.port) : void 0;
415
+ const vertical = props.direction === "vertical";
416
+ const along = (v, size, flow) => wait.start && flow ? v : v - size / 2;
417
+ let x = snapTo(port ? wait.at.x - port.x : along(wait.at.x, value.width, !vertical));
418
+ let y = snapTo(port ? wait.at.y - port.y : along(wait.at.y, value.height, vertical));
419
+ if (wait.avoid) {
420
+ const others = doc.value.nodes.flatMap((n) => n.id === id ? [] : getNodeRect(n.id) ?? []);
421
+ const step = snapTo((vertical ? value.width : value.height) + 16);
422
+ const free = (px, py) => !others.some((r) => intersects({
423
+ x: px - 8,
424
+ y: py - 8,
425
+ width: value.width + 16,
426
+ height: value.height + 16
427
+ }, r));
428
+ for (let i = 0; i < 40 && !free(x, y); i++) if (vertical) x += step;
429
+ else y += step;
430
+ }
431
+ const node = doc.value.nodes.find((n) => n.id === id);
432
+ if (node && (node.x !== x || node.y !== y)) transact((tr) => tr.moveNode(id, x, y), { group: wait.group });
433
+ nextTick(() => {
434
+ reveal(id);
435
+ if (wait.focus) root.value?.querySelector(`[data-node-id="${CSS.escape(id)}"]`)?.focus({ preventScroll: true });
436
+ });
437
+ }
438
+ /** Nodes just added, waiting for their size to be placed: the point and the port to put on it. */
439
+ const pending = /* @__PURE__ */ new Map();
440
+ const snapTo = (v) => props.grid ? Math.round(v / props.grid) * props.grid : Math.round(v);
441
+ watch(nodeById, (nodes) => {
442
+ for (const id of metrics.keys()) if (!nodes.has(id)) metrics.delete(id);
443
+ const kept = [...previews.value].filter(([id]) => nodes.has(id));
444
+ if (kept.length < previews.value.size) previews.value = new Map(kept);
445
+ });
446
+ function rectOf(node) {
447
+ const m = metrics.get(node.id);
448
+ return {
449
+ x: node.x,
450
+ y: node.y,
451
+ width: m?.width ?? node.width ?? 0,
452
+ height: m?.height ?? node.height ?? 0
453
+ };
454
+ }
455
+ function getNodeRect(id) {
456
+ const node = nodeById.value.get(id);
457
+ return node ? rectOf(node) : null;
458
+ }
459
+ function anchor(node, port, role) {
460
+ const m = metrics.get(node.id);
461
+ if (!m) return null;
462
+ const at = port ? m.ports.get(port) : void 0;
463
+ if (at) return {
464
+ x: node.x + at.x,
465
+ y: node.y + at.y,
466
+ side: at.side
467
+ };
468
+ return role === "source" ? {
469
+ x: node.x + m.width,
470
+ y: node.y + m.height / 2,
471
+ side: "right"
472
+ } : {
473
+ x: node.x,
474
+ y: node.y + m.height / 2,
475
+ side: "left"
476
+ };
477
+ }
478
+ function anchorOf(end, role) {
479
+ const node = nodeById.value.get(end.node);
480
+ return node ? anchor(node, end.port, role) : null;
481
+ }
482
+ /** Where an edge ends: on the collapsed container hiding its node, if any. */
483
+ function edgeEnd(end) {
484
+ const outer = hiddenIn.value.get(end.node);
485
+ return outer ? { node: outer } : end;
486
+ }
487
+ /**
488
+ * Edges whose ports no longer fit: a port is gone, or its type changed so
489
+ * the two ends don't match any more, e.g. after a form in a node changed it.
490
+ */
491
+ const staleEdges = computed(() => {
492
+ const out = /* @__PURE__ */ new Set();
493
+ for (const edge of doc.value.edges) {
494
+ const { source, target } = edge;
495
+ if (source.port == null && target.port == null) continue;
496
+ const a = source.port == null ? void 0 : portSpec(source);
497
+ const b = target.port == null ? void 0 : portSpec(target);
498
+ if (source.port != null && !a || target.port != null && !b) out.add(edge.id);
499
+ else if (a && b && (a.kind !== "output" || b.kind !== "input" || !typesMatch(a, b, portTypes.value))) out.add(edge.id);
500
+ }
501
+ return out;
502
+ });
503
+ watch(staleEdges, (now, before) => {
504
+ let added = 0;
505
+ for (const id of now) if (!before.has(id)) added++;
506
+ if (added) announce(format(messages.value.stale, { count: added }));
507
+ });
508
+ function removeStaleEdges() {
509
+ const ids = [...staleEdges.value];
510
+ if (ids.length) transact((tr) => ids.forEach((id) => tr.removeEdge(id)));
511
+ return ids.length;
512
+ }
513
+ /** How far an edge may bulge past its ends, in canvas px. */
514
+ const edgeRoutes = computed(() => {
515
+ const out = [];
516
+ const edges = doc.value.edges;
517
+ edges.length;
518
+ for (const edge of toRaw(edges)) {
519
+ const from = edgeEnd(edge.source);
520
+ const to = edgeEnd(edge.target);
521
+ if (from.node === to.node && (from !== edge.source || to !== edge.target)) continue;
522
+ const source = nodeById.value.get(from.node);
523
+ const target = nodeById.value.get(to.node);
524
+ if (!source || !target) continue;
525
+ const a = anchor(source, from.port, "source");
526
+ const b = anchor(target, to.port, "target");
527
+ if (!a || !b) continue;
528
+ out.push({
529
+ edge,
530
+ route: edgeTypeOf(edge.type).route(a, b, edge),
531
+ box: {
532
+ x: Math.min(a.x, b.x) - EDGE_REACH,
533
+ y: Math.min(a.y, b.y) - EDGE_REACH,
534
+ width: Math.abs(a.x - b.x) + 160,
535
+ height: Math.abs(a.y - b.y) + 160
536
+ },
537
+ style: edgeStyle(edge)
538
+ });
539
+ }
540
+ return out;
541
+ });
542
+ /** The edges in view. */
543
+ const edgePaths = computed(() => {
544
+ const selected = new Set(selection.value.edges);
545
+ const moving = connecting.value?.edge;
546
+ const area = drawnArea.value;
547
+ const stale = staleEdges.value;
548
+ const out = [];
549
+ for (const item of edgeRoutes.value) {
550
+ const { edge } = item;
551
+ if (edge.id === moving || area && !intersects(item.box, area)) continue;
552
+ out.push({
553
+ edge,
554
+ route: item.route,
555
+ selected: selected.has(edge.id),
556
+ stale: stale.has(edge.id),
557
+ style: item.style
558
+ });
559
+ }
560
+ return out;
561
+ });
562
+ function edgeStyle(edge) {
563
+ const port = portSpec(edge.source);
564
+ const type = port && portTypeOf(port);
565
+ const own = edgeTypeOf(edge.type).style?.(edge);
566
+ if (!type) return own;
567
+ return {
568
+ "--dou-flow-editor-edge": colorOf(type),
569
+ "--dou-flow-editor-edge-width": type.edgeWidth ? `${type.edgeWidth}px` : void 0,
570
+ strokeDasharray: type.dashed ? "4 4" : void 0,
571
+ ...own
572
+ };
573
+ }
574
+ /** The handle that reshapes the one selected edge, e.g. a step edge's middle. */
575
+ const bend = computed(() => {
576
+ const { nodes, edges } = selection.value;
577
+ if (props.readonly || nodes.length || edges.length !== 1) return null;
578
+ const item = edgeRoutes.value.find((e) => e.edge.id === edges[0]);
579
+ const handle = item?.route.handle;
580
+ return handle && item ? {
581
+ edge: item.edge,
582
+ handle
583
+ } : null;
584
+ });
585
+ /** The node whose edges `E` goes through, and the one last selected. */
586
+ let edgeCycle = null;
587
+ /**
588
+ * Selects the next edge of the selected node, or of the node whose edges
589
+ * are being gone through, and says which nodes it joins.
590
+ */
591
+ function cycleEdges(step) {
592
+ const { nodes, edges } = selection.value;
593
+ let node;
594
+ let current;
595
+ if (nodes.length === 1 && !edges.length) node = nodes[0];
596
+ else {
597
+ const last = edgeCycle;
598
+ if (last && !nodes.length && edges.length === 1 && last.edge === edges[0]) {
599
+ node = last.node;
600
+ current = last.edge;
601
+ }
602
+ }
603
+ if (!node) return false;
604
+ const all = doc.value.edges;
605
+ const list = [...all.filter((e) => e.source.node === node), ...all.filter((e) => e.target.node === node && e.source.node !== node)];
606
+ if (!list.length) {
607
+ announce(messages.value.noEdges);
608
+ return true;
609
+ }
610
+ const edge = list[((current ? list.findIndex((e) => e.id === current) : -1) + step + list.length) % list.length];
611
+ edgeCycle = {
612
+ node,
613
+ edge: edge.id
614
+ };
615
+ setSelection({
616
+ nodes: [],
617
+ edges: [edge.id]
618
+ });
619
+ announce(describeEdge(edge));
620
+ return true;
621
+ }
622
+ function nodeName(id) {
623
+ const node = nodeById.value.get(id);
624
+ return node ? nameOf(node) : id;
625
+ }
626
+ function describeEdge(edge) {
627
+ const t = messages.value;
628
+ const text = format(t.edge, {
629
+ from: nodeName(edge.source.node),
630
+ to: nodeName(edge.target.node)
631
+ });
632
+ return edge.label ? text + format(t.edgeLabel, { label: edge.label }) : text;
633
+ }
634
+ const editingLabel = ref(null);
635
+ const labelDraft = ref("");
636
+ const edgeLabels = computed(() => edgePaths.value.filter((item) => item.edge.id === editingLabel.value || item.edge.label && !simple.value));
637
+ /** Opens the text field on an edge's label. */
638
+ function editLabel(id) {
639
+ if (props.readonly || !doc.value.edges.some((e) => e.id === id)) return;
640
+ setSelection({
641
+ nodes: [],
642
+ edges: [id]
643
+ });
644
+ labelDraft.value = doc.value.edges.find((e) => e.id === id).label ?? "";
645
+ editingLabel.value = id;
646
+ nextTick(() => {
647
+ const input = root.value?.querySelector(".dou-flow-editor__label-input");
648
+ input?.focus({ preventScroll: true });
649
+ input?.select();
650
+ });
651
+ }
652
+ function finishLabel(save) {
653
+ const id = editingLabel.value;
654
+ if (!id) return;
655
+ editingLabel.value = null;
656
+ const text = labelDraft.value.trim();
657
+ if (save && transact((tr) => tr.updateEdge(id, { label: text || void 0 })).changed) announce(messages.value.labelSaved);
658
+ root.value?.focus({ preventScroll: true });
659
+ }
660
+ function onLabelKeydown(event) {
661
+ if (event.isComposing) return;
662
+ if (event.key === "Enter") finishLabel(true);
663
+ else if (event.key === "Escape") finishLabel(false);
664
+ else return;
665
+ event.preventDefault();
666
+ event.stopPropagation();
667
+ }
668
+ /** Connected port ids, by node. */
669
+ const connected = computed(() => {
670
+ const map = /* @__PURE__ */ new Map();
671
+ const add = (node, port) => {
672
+ if (!port) return;
673
+ let set = map.get(node);
674
+ if (!set) map.set(node, set = /* @__PURE__ */ new Set());
675
+ set.add(port);
676
+ };
677
+ for (const edge of doc.value.edges) {
678
+ add(edge.source.node, edge.source.port);
679
+ add(edge.target.node, edge.target.port);
680
+ }
681
+ return map;
682
+ });
683
+ const none = /* @__PURE__ */ new Set();
684
+ const noStates = /* @__PURE__ */ new Map();
685
+ /** How many edges touch each node. */
686
+ const degree = computed(() => {
687
+ const map = /* @__PURE__ */ new Map();
688
+ const add = (id) => map.set(id, (map.get(id) ?? 0) + 1);
689
+ for (const edge of toRaw(doc.value).edges) {
690
+ add(edge.source.node);
691
+ if (edge.target.node !== edge.source.node) add(edge.target.node);
692
+ }
693
+ return map;
694
+ });
695
+ /** A node's name; a container without one goes by the word for a group. */
696
+ function nameOf(node) {
697
+ return labelOf(specOf(node), node, messages.value) || (isContainer(node) ? messages.value.group : "");
698
+ }
699
+ const roleOf = (node) => isContainer(node) ? messages.value.group : messages.value.node;
700
+ /** Read out after a node's name: the group it is in, what it holds, its ports. */
701
+ function describe(node) {
702
+ const t = messages.value;
703
+ const parts = [];
704
+ const parent = tree.value.parentOf(node.id);
705
+ const group = parent && nodeById.value.get(parent);
706
+ if (group) parts.push(format(t.inGroup, { group: nameOf(group) }));
707
+ if (isContainer(node)) parts.push(format(t.groupSize, { count: tree.value.children.get(node.id)?.length ?? 0 }));
708
+ const ports = portsOf(specOf(node), node);
709
+ if (ports.length) parts.push(format(t.ports, {
710
+ inputs: ports.filter((p) => p.kind === "input").length,
711
+ outputs: ports.filter((p) => p.kind === "output").length,
712
+ connected: degree.value.get(node.id) ?? 0
713
+ }));
714
+ return parts.join(t.separator);
715
+ }
716
+ const clampZoom = (zoom) => Math.min(props.maxZoom, Math.max(props.minZoom, zoom));
717
+ let animation = 0;
718
+ function stopAnimation() {
719
+ cancelAnimationFrame(animation);
720
+ animation = 0;
721
+ }
722
+ /** `next`, kept within reach of the nodes; see `clampPan`. */
723
+ function reachable(next) {
724
+ return clampPan(next, viewport.value, boundsOf(shownNodes.value.map(rectOf)), size.value);
725
+ }
726
+ /** Moves the view, as far as the nodes allow; `v-model:viewport` alone can go further. */
727
+ function setViewport(target, animate = false) {
728
+ stopAnimation();
729
+ const next = reachable(target);
730
+ if (!animate || reducedMotion.value || typeof requestAnimationFrame === "undefined") {
731
+ viewport.value = next;
732
+ return;
733
+ }
734
+ const from = { ...viewport.value };
735
+ const start = performance.now();
736
+ const duration = 240;
737
+ const frame = (now) => {
738
+ const t = Math.min(1, (now - start) / duration);
739
+ const e = 1 - (1 - t) ** 3;
740
+ viewport.value = {
741
+ x: from.x + (next.x - from.x) * e,
742
+ y: from.y + (next.y - from.y) * e,
743
+ zoom: from.zoom + (next.zoom - from.zoom) * e
744
+ };
745
+ animation = t < 1 ? requestAnimationFrame(frame) : 0;
746
+ };
747
+ animation = requestAnimationFrame(frame);
748
+ }
749
+ /** Point on the screen relative to the editor's top left corner. */
750
+ function local(clientX, clientY) {
751
+ const rect = root.value.getBoundingClientRect();
752
+ return {
753
+ x: clientX - rect.left,
754
+ y: clientY - rect.top
755
+ };
756
+ }
757
+ function toCanvas(point) {
758
+ const p = local(point.x, point.y);
759
+ const { x, y, zoom } = viewport.value;
760
+ return {
761
+ x: (p.x - x) / zoom,
762
+ y: (p.y - y) / zoom
763
+ };
764
+ }
765
+ /** Zooms keeping the canvas point under `at` (local px) in place. */
766
+ function zoomAround(zoom, at, animate = false) {
767
+ const { x, y, zoom: k } = viewport.value;
768
+ const next = clampZoom(zoom);
769
+ const cx = (at.x - x) / k;
770
+ const cy = (at.y - y) / k;
771
+ setViewport({
772
+ x: at.x - cx * next,
773
+ y: at.y - cy * next,
774
+ zoom: next
775
+ }, animate);
776
+ }
777
+ const center = () => ({
778
+ x: root.value.clientWidth / 2,
779
+ y: root.value.clientHeight / 2
780
+ });
781
+ const zoomTo = (zoom) => zoomAround(zoom, center(), true);
782
+ const zoomIn = () => zoomTo(viewport.value.zoom * 1.25);
783
+ const zoomOut = () => zoomTo(viewport.value.zoom / 1.25);
784
+ /** Room left around the nodes when fitting them into view. */
785
+ function fit(ids, animate = true, floor = 0) {
786
+ const nodes = ids ? ids.map((id) => nodeById.value.get(id)).filter(Boolean) : doc.value.nodes;
787
+ const bounds = boundsOf(nodes.map(rectOf));
788
+ if (bounds) fitRect(bounds, animate, floor);
789
+ }
790
+ /**
791
+ * Room to leave around the nodes: the padding, or more where a panel
792
+ * (controls, minimap…) sits in a corner, so fitted nodes don't end
793
+ * up under it. Each panel takes room on the side where it costs less: a
794
+ * wide one above or below, a tall one on the left or right.
795
+ */
796
+ function fitInsets() {
797
+ const box = root.value.getBoundingClientRect();
798
+ const insets = {
799
+ top: FIT_PADDING,
800
+ bottom: FIT_PADDING,
801
+ left: FIT_PADDING,
802
+ right: FIT_PADDING
803
+ };
804
+ for (const panel of root.value.querySelectorAll(".dou-flow-editor__panels > :not(.dou-flow-toolbar)")) {
805
+ const rect = panel.getBoundingClientRect();
806
+ if (!rect.height) continue;
807
+ const upper = rect.top + rect.height / 2 < box.top + box.height / 2;
808
+ const leftward = rect.left + rect.width / 2 < box.left + box.width / 2;
809
+ const vertical = upper ? rect.bottom - box.top : box.bottom - rect.top;
810
+ const horizontal = leftward ? rect.right - box.left : box.right - rect.left;
811
+ if (vertical / box.height <= horizontal / box.width) {
812
+ const side = upper ? "top" : "bottom";
813
+ insets[side] = Math.max(insets[side], vertical + PANEL_GAP);
814
+ } else {
815
+ const side = leftward ? "left" : "right";
816
+ insets[side] = Math.max(insets[side], horizontal + PANEL_GAP);
817
+ }
818
+ }
819
+ return insets;
820
+ }
821
+ /** Room between a panel and the nodes fitted next to it. */
822
+ function fitRect(bounds, animate, floor) {
823
+ if (!root.value) return;
824
+ const width = root.value.clientWidth;
825
+ const height = root.value.clientHeight;
826
+ const insets = fitInsets();
827
+ const zoom = clampZoom(Math.max(Math.min((width - insets.left - insets.right) / Math.max(1, bounds.width), (height - insets.top - insets.bottom) / Math.max(1, bounds.height), 1), Math.min(floor, 1)));
828
+ const offset = (room, size, start, before, after) => Math.round(size * zoom > room - before - after ? before - start * zoom : before + (room - before - after - size * zoom) / 2 - start * zoom);
829
+ setViewport({
830
+ x: offset(width, bounds.width, bounds.x, insets.left, insets.right),
831
+ y: offset(height, bounds.height, bounds.y, insets.top, insets.bottom),
832
+ zoom
833
+ }, animate);
834
+ }
835
+ /** Room between ranks (along the flow) and between nodes in a rank when tidying. */
836
+ const TIDY_GAP = {
837
+ horizontal: {
838
+ rank: 80,
839
+ row: 40
840
+ },
841
+ vertical: {
842
+ rank: 56,
843
+ row: 40
844
+ }
845
+ };
846
+ /**
847
+ * Lays the given nodes, or the selected ones when two or more are selected,
848
+ * or all, out in ranks along their edges in the flow's `direction`, as one
849
+ * undo step. The group keeps its top left corner; locked nodes stay put.
850
+ */
851
+ function tidy(ids) {
852
+ const chosen = new Set(ids ?? (selection.value.nodes.length > 1 ? selection.value.nodes : doc.value.nodes.map((n) => n.id)));
853
+ const parents = new Set(tree.value.topmost(chosen).map((id) => tree.value.parentOf(id)));
854
+ const scope = parents.size === 1 ? [...parents][0] : void 0;
855
+ const blockOf = (id) => {
856
+ let at = id;
857
+ while (at && tree.value.parentOf(at) !== scope) at = tree.value.parentOf(at);
858
+ return at;
859
+ };
860
+ const blocks = new Set([...chosen].flatMap((id) => blockOf(id) ?? []));
861
+ const nodes = doc.value.nodes.filter((node) => blocks.has(node.id));
862
+ const bounds = boundsOf(nodes.map(rectOf));
863
+ if (!bounds || nodes.length < 2) return;
864
+ const vertical = props.direction === "vertical";
865
+ const gap = TIDY_GAP[vertical ? "vertical" : "horizontal"];
866
+ const across = vertical ? ["top", "bottom"] : ["left", "right"];
867
+ const layout = layoutGraph(nodes.map((node) => {
868
+ const m = metrics.get(node.id);
869
+ const { width, height } = rectOf(node);
870
+ let offset = (vertical ? width : height) / 2;
871
+ for (const port of m?.ports.values() ?? []) if (across.includes(port.side)) {
872
+ offset = vertical ? port.x : port.y;
873
+ break;
874
+ }
875
+ return {
876
+ id: node.id,
877
+ width,
878
+ height,
879
+ anchor: offset
880
+ };
881
+ }), doc.value.edges.flatMap((e) => {
882
+ const from = blockOf(e.source.node);
883
+ const to = blockOf(e.target.node);
884
+ return from && to && from !== to && blocks.has(from) && blocks.has(to) ? [{
885
+ from,
886
+ to
887
+ }] : [];
888
+ }), {
889
+ direction: props.direction,
890
+ rankGap: gap.rank,
891
+ laneGap: gap.row,
892
+ equalWidth: false,
893
+ grid: props.grid || 8
894
+ });
895
+ const snap = (v) => props.grid ? Math.round(v / props.grid) * props.grid : Math.round(v);
896
+ const origin = {
897
+ x: snap(bounds.x),
898
+ y: snap(bounds.y)
899
+ };
900
+ const from = new Map(nodes.map((node) => [node.id, {
901
+ x: node.x,
902
+ y: node.y
903
+ }]));
904
+ const moved = /* @__PURE__ */ new Map();
905
+ for (const node of nodes) {
906
+ const placed = layout.nodes.get(node.id);
907
+ if (!placed || node.locked) continue;
908
+ const to = {
909
+ x: origin.x + snap(placed.x),
910
+ y: origin.y + snap(placed.y)
911
+ };
912
+ if (to.x !== node.x || to.y !== node.y) moved.set(node.id, to);
913
+ }
914
+ if (!moved.size) return;
915
+ const count = moved.size;
916
+ for (const { id } of nodes) {
917
+ const to = moved.get(id);
918
+ if (!to) continue;
919
+ const block = from.get(id);
920
+ for (const inner of tree.value.descendants(id)) {
921
+ const node = nodeById.value.get(inner);
922
+ if (!node) continue;
923
+ from.set(inner, {
924
+ x: node.x,
925
+ y: node.y
926
+ });
927
+ moved.set(inner, {
928
+ x: node.x + to.x - block.x,
929
+ y: node.y + to.y - block.y
930
+ });
931
+ }
932
+ }
933
+ transact((tr) => {
934
+ for (const [id, to] of moved) tr.moveNode(id, to.x, to.y);
935
+ });
936
+ announce(format(messages.value.tidied, { count }));
937
+ animateTidy(from, moved);
938
+ const after = boundsOf(nodes.map((node) => ({
939
+ ...rectOf(node),
940
+ ...moved.get(node.id) ?? from.get(node.id)
941
+ })));
942
+ const { x, y, zoom } = viewport.value;
943
+ const { width, height } = size.value;
944
+ if (!(after.x * zoom + x >= 0 && after.y * zoom + y >= 0 && (after.x + after.width) * zoom + x <= width && (after.y + after.height) * zoom + y <= height)) fitRect(after, true, props.fitMinZoom);
945
+ }
946
+ let tweenFrame = 0;
947
+ const easeOut = (t) => 1 - (1 - t) ** 3;
948
+ /** Glides moved nodes from where they were to where tidying put them. */
949
+ function animateTidy(from, to) {
950
+ cancelAnimationFrame(tweenFrame);
951
+ if (reducedMotion.value || typeof requestAnimationFrame === "undefined") return;
952
+ const start = performance.now();
953
+ const duration = 220;
954
+ const step = (now) => {
955
+ const t = Math.min(1, (now - start) / duration);
956
+ const k = easeOut(t);
957
+ const frame = /* @__PURE__ */ new Map();
958
+ for (const [id, end] of to) {
959
+ const a = from.get(id);
960
+ frame.set(id, {
961
+ x: a.x + (end.x - a.x) * k,
962
+ y: a.y + (end.y - a.y) * k
963
+ });
964
+ }
965
+ tween.value = t < 1 ? frame : null;
966
+ if (t < 1) tweenFrame = requestAnimationFrame(step);
967
+ };
968
+ tween.value = new Map(from);
969
+ tweenFrame = requestAnimationFrame(step);
970
+ }
971
+ function centerNode(id) {
972
+ const rect = getNodeRect(id);
973
+ if (!rect || !root.value) return;
974
+ const { zoom } = viewport.value;
975
+ const c = center();
976
+ setViewport({
977
+ x: c.x - (rect.x + rect.width / 2) * zoom,
978
+ y: c.y - (rect.y + rect.height / 2) * zoom,
979
+ zoom
980
+ }, true);
981
+ }
982
+ /** Keeps a node in view, e.g. when it gets keyboard focus. */
983
+ function reveal(id) {
984
+ const rect = getNodeRect(id);
985
+ if (!rect || !root.value) return;
986
+ const { x, y, zoom } = viewport.value;
987
+ const pad = 16;
988
+ const left = rect.x * zoom + x;
989
+ const top = rect.y * zoom + y;
990
+ const right = left + rect.width * zoom;
991
+ const bottom = top + rect.height * zoom;
992
+ const w = root.value.clientWidth;
993
+ const h = root.value.clientHeight;
994
+ let dx = 0;
995
+ let dy = 0;
996
+ if (left < pad) dx = pad - left;
997
+ else if (right > w - pad) dx = Math.max(pad - left, w - pad - right);
998
+ if (top < pad) dy = pad - top;
999
+ else if (bottom > h - pad) dy = Math.max(pad - top, h - pad - bottom);
1000
+ if (dx || dy) setViewport({
1001
+ x: x + dx,
1002
+ y: y + dy,
1003
+ zoom
1004
+ }, true);
1005
+ }
1006
+ let fitted = !props.fitView;
1007
+ watch(() => metrics.size, (size) => {
1008
+ if (fitted || !size || size < doc.value.nodes.length) return;
1009
+ fitted = true;
1010
+ fit(void 0, false, props.fitMinZoom);
1011
+ });
1012
+ /** Dots every 16 canvas px, spread out further when zoomed out. */
1013
+ const background = computed(() => {
1014
+ const { x, y, zoom } = viewport.value;
1015
+ let gap = 16;
1016
+ while (gap * zoom < 12) gap *= 2;
1017
+ const size = gap * zoom;
1018
+ return {
1019
+ backgroundSize: `${size}px ${size}px`,
1020
+ backgroundPosition: `${x - size / 2}px ${y - size / 2}px`
1021
+ };
1022
+ });
1023
+ const layerStyle = computed(() => {
1024
+ const { x, y, zoom } = viewport.value;
1025
+ return {
1026
+ transform: `translate(${x}px, ${y}px) scale(${zoom})`,
1027
+ "--dou-flow-editor-zoom": zoom
1028
+ };
1029
+ });
1030
+ const gesture = shallowRef(null);
1031
+ const guides = shallowRef([]);
1032
+ const panning = ref(false);
1033
+ const spaceHeld = ref(false);
1034
+ let pressTimer = 0;
1035
+ let dragCount = 0;
1036
+ /** When a finger last went down, to tell a long press's own context menu event. */
1037
+ let lastTouch = 0;
1038
+ /** Active touch points, for pinching. */
1039
+ const touches = /* @__PURE__ */ new Map();
1040
+ let pinch = null;
1041
+ /** Where people type or read: presses there don't drag, and text can be selected. */
1042
+ const isInteractive = (el) => !!el.closest(`${FIELDS}, select, button, a[href]`);
1043
+ const inPanels = (el) => !!el.closest(".dou-flow-editor__panels > *");
1044
+ /**
1045
+ * No text selection starts on the canvas, only in fields and in parts of
1046
+ * nodes marked `data-dou-nodrag`.
1047
+ * `user-select: none` says the same, but a user style or a browser
1048
+ * extension can turn selecting back on, and then every drag paints its way
1049
+ * across the nodes and panels.
1050
+ */
1051
+ function onSelectstart(event) {
1052
+ const node = event.target;
1053
+ if (!(node instanceof Element ? node : node.parentElement)?.closest(FIELDS)) event.preventDefault();
1054
+ }
1055
+ /** Keeps receiving a pointer's events once it leaves the editor. */
1056
+ function capture(id) {
1057
+ try {
1058
+ root.value.setPointerCapture(id);
1059
+ } catch {}
1060
+ }
1061
+ function clearPress() {
1062
+ clearTimeout(pressTimer);
1063
+ pressTimer = 0;
1064
+ }
1065
+ function onPointerDown(event) {
1066
+ const target = event.target;
1067
+ if (inPanels(target)) return;
1068
+ stopAnimation();
1069
+ const touch = event.pointerType === "touch";
1070
+ if (touch) {
1071
+ lastTouch = Date.now();
1072
+ if (event.isPrimary) {
1073
+ touches.clear();
1074
+ pinch = null;
1075
+ }
1076
+ touches.set(event.pointerId, {
1077
+ x: event.clientX,
1078
+ y: event.clientY
1079
+ });
1080
+ if (touches.size === 2) {
1081
+ startPinch();
1082
+ return;
1083
+ }
1084
+ if (touches.size > 2) return;
1085
+ }
1086
+ if (gesture.value) return;
1087
+ const nodeEl = target.closest("[data-node-id]");
1088
+ const edgeEl = target.closest("[data-edge-id]");
1089
+ const additive = event.shiftKey || event.metaKey || event.ctrlKey;
1090
+ const client = {
1091
+ x: event.clientX,
1092
+ y: event.clientY
1093
+ };
1094
+ if (event.button === 1 || event.button === 0 && spaceHeld.value) {
1095
+ event.preventDefault();
1096
+ startPan(event, false);
1097
+ return;
1098
+ }
1099
+ if (event.button !== 0) return;
1100
+ if (target.closest(".dou-flow-editor__label-input")) return;
1101
+ if (resolved.value.presses.length && !props.readonly && !isInteractive(target)) {
1102
+ const pressedId = target.closest("[data-node-id]")?.dataset.nodeId;
1103
+ const press = {
1104
+ point: toCanvas(client),
1105
+ node: pressedId && nodeById.value.get(pressedId) || null,
1106
+ event
1107
+ };
1108
+ for (const hook of resolved.value.presses) {
1109
+ const handler = hook(context, press);
1110
+ if (!handler) continue;
1111
+ gesture.value = {
1112
+ kind: "press",
1113
+ id: event.pointerId,
1114
+ touch,
1115
+ handler,
1116
+ done: false
1117
+ };
1118
+ capture(event.pointerId);
1119
+ return;
1120
+ }
1121
+ }
1122
+ if (target.closest(".dou-flow-editor__resize") && nodeEl && !props.readonly) {
1123
+ const node = nodeById.value.get(nodeEl.dataset.nodeId);
1124
+ const min = node && resizeOf(specOf(node), node);
1125
+ if (node && min) {
1126
+ const rect = rectOf(node);
1127
+ gesture.value = {
1128
+ kind: "resize",
1129
+ id: event.pointerId,
1130
+ touch,
1131
+ node: node.id,
1132
+ start: toCanvas(client),
1133
+ origin: {
1134
+ width: rect.width,
1135
+ height: rect.height
1136
+ },
1137
+ min: {
1138
+ width: min.minWidth ?? 40,
1139
+ height: min.minHeight ?? 40
1140
+ },
1141
+ group: `resize:${++dragCount}`
1142
+ };
1143
+ capture(event.pointerId);
1144
+ return;
1145
+ }
1146
+ }
1147
+ if (target.closest(".dou-flow-editor__bend, .dou-flow-editor__label.is-bend") && bend.value) {
1148
+ const { edge, handle } = bend.value;
1149
+ const at = toCanvas(client);
1150
+ gesture.value = {
1151
+ kind: "bend",
1152
+ id: event.pointerId,
1153
+ touch,
1154
+ edge: edge.id,
1155
+ origin: edge.data,
1156
+ move: handle.move,
1157
+ group: `bend:${++dragCount}`,
1158
+ start: client,
1159
+ offset: {
1160
+ x: handle.at.x - at.x,
1161
+ y: handle.at.y - at.y
1162
+ },
1163
+ started: false
1164
+ };
1165
+ return;
1166
+ }
1167
+ const portEl = target.closest(".dou-flow-port");
1168
+ if (nodeEl && portEl && !props.readonly) {
1169
+ const link = linkFrom({
1170
+ node: nodeEl.dataset.nodeId,
1171
+ port: portEl.dataset.port
1172
+ });
1173
+ if (link) {
1174
+ gesture.value = {
1175
+ kind: "connect",
1176
+ id: event.pointerId,
1177
+ touch,
1178
+ node: nodeEl.dataset.nodeId,
1179
+ start: client,
1180
+ client,
1181
+ started: false,
1182
+ link
1183
+ };
1184
+ capture(event.pointerId);
1185
+ return;
1186
+ }
1187
+ }
1188
+ const pressed = nodeEl && nodeById.value.get(nodeEl.dataset.nodeId);
1189
+ if (pressed && isContainer(pressed) && event.shiftKey && !touch && !isInteractive(target)) {
1190
+ startBox(event, client);
1191
+ return;
1192
+ }
1193
+ if (nodeEl) {
1194
+ if (isInteractive(target)) return;
1195
+ const id = nodeEl.dataset.nodeId;
1196
+ const wasSelected = selection.value.nodes.includes(id);
1197
+ if (!wasSelected) setSelection(additive ? {
1198
+ nodes: [...selection.value.nodes, id],
1199
+ edges: selection.value.edges
1200
+ } : {
1201
+ nodes: [id],
1202
+ edges: []
1203
+ });
1204
+ gesture.value = {
1205
+ kind: "drag",
1206
+ id: event.pointerId,
1207
+ touch,
1208
+ node: id,
1209
+ start: toCanvas(client),
1210
+ client,
1211
+ origins: /* @__PURE__ */ new Map(),
1212
+ others: [],
1213
+ links: [],
1214
+ started: false,
1215
+ additive,
1216
+ wasSelected,
1217
+ held: false,
1218
+ group: "",
1219
+ snap: !event.altKey
1220
+ };
1221
+ if (touch) pressTimer = window.setTimeout(() => {
1222
+ const g = gesture.value;
1223
+ if (g?.kind !== "drag" || g.started) return;
1224
+ g.held = true;
1225
+ if (!selection.value.nodes.includes(id)) setSelection({
1226
+ nodes: [...selection.value.nodes, id],
1227
+ edges: selection.value.edges
1228
+ });
1229
+ }, LONG_PRESS);
1230
+ return;
1231
+ }
1232
+ if (edgeEl) {
1233
+ const id = edgeEl.getAttribute("data-edge-id");
1234
+ const has = selection.value.edges.includes(id);
1235
+ if (additive) setSelection({
1236
+ nodes: selection.value.nodes,
1237
+ edges: has ? selection.value.edges.filter((e) => e !== id) : [...selection.value.edges, id]
1238
+ });
1239
+ else setSelection({
1240
+ nodes: [],
1241
+ edges: [id]
1242
+ });
1243
+ const edge = doc.value.edges.find((e) => e.id === id);
1244
+ if (edge) emit("edgeClick", edge, event);
1245
+ root.value.focus({ preventScroll: true });
1246
+ return;
1247
+ }
1248
+ if (event.shiftKey && !touch) startBox(event, client);
1249
+ else startPan(event, touch, additive);
1250
+ }
1251
+ function startPan(event, touch, additive = false) {
1252
+ gesture.value = {
1253
+ kind: "pan",
1254
+ id: event.pointerId,
1255
+ touch,
1256
+ start: {
1257
+ x: event.clientX,
1258
+ y: event.clientY
1259
+ },
1260
+ origin: { ...viewport.value },
1261
+ moved: false,
1262
+ additive
1263
+ };
1264
+ if (touch) pressTimer = window.setTimeout(() => {
1265
+ const g = gesture.value;
1266
+ if (g?.kind !== "pan" || g.moved) return;
1267
+ startBox(event, g.start);
1268
+ }, LONG_PRESS);
1269
+ capture(event.pointerId);
1270
+ }
1271
+ function startBox(event, client) {
1272
+ const start = toCanvas(client);
1273
+ gesture.value = {
1274
+ kind: "box",
1275
+ id: event.pointerId,
1276
+ touch: event.pointerType === "touch",
1277
+ start,
1278
+ current: start,
1279
+ client,
1280
+ base: event.shiftKey ? selection.value : emptySelection()
1281
+ };
1282
+ capture(event.pointerId);
1283
+ }
1284
+ function startPinch() {
1285
+ const [a, b] = [...touches.values()];
1286
+ clearPress();
1287
+ endGesture();
1288
+ for (const id of touches.keys()) capture(id);
1289
+ pinch = {
1290
+ distance: Math.hypot(a.x - b.x, a.y - b.y) || 1,
1291
+ mid: local((a.x + b.x) / 2, (a.y + b.y) / 2),
1292
+ origin: { ...viewport.value }
1293
+ };
1294
+ }
1295
+ function movePinch() {
1296
+ if (!pinch) return;
1297
+ const [a, b] = [...touches.values()];
1298
+ const { origin } = pinch;
1299
+ const zoom = clampZoom(origin.zoom * Math.hypot(a.x - b.x, a.y - b.y) / pinch.distance);
1300
+ const mid = local((a.x + b.x) / 2, (a.y + b.y) / 2);
1301
+ const cx = (pinch.mid.x - origin.x) / origin.zoom;
1302
+ const cy = (pinch.mid.y - origin.y) / origin.zoom;
1303
+ setViewport({
1304
+ x: mid.x - cx * zoom,
1305
+ y: mid.y - cy * zoom,
1306
+ zoom
1307
+ });
1308
+ }
1309
+ /** Where the pointer is over the editor, for pasting under it. */
1310
+ let hover = null;
1311
+ function onPointerMove(event) {
1312
+ if (event.pointerType === "mouse" && root.value?.contains(event.target)) hover = {
1313
+ x: event.clientX,
1314
+ y: event.clientY
1315
+ };
1316
+ if (event.pointerType === "touch" && touches.has(event.pointerId)) {
1317
+ touches.set(event.pointerId, {
1318
+ x: event.clientX,
1319
+ y: event.clientY
1320
+ });
1321
+ if (pinch) {
1322
+ movePinch();
1323
+ return;
1324
+ }
1325
+ }
1326
+ const g = gesture.value;
1327
+ if (!g || g.id !== event.pointerId) return;
1328
+ const client = {
1329
+ x: event.clientX,
1330
+ y: event.clientY
1331
+ };
1332
+ if (g.kind === "pan") {
1333
+ const dx = client.x - g.start.x;
1334
+ const dy = client.y - g.start.y;
1335
+ if (!g.moved) {
1336
+ if (Math.hypot(dx, dy) < THRESHOLD[g.touch ? "touch" : "mouse"]) return;
1337
+ g.moved = true;
1338
+ clearPress();
1339
+ panning.value = true;
1340
+ }
1341
+ setViewport({
1342
+ x: g.origin.x + dx,
1343
+ y: g.origin.y + dy,
1344
+ zoom: g.origin.zoom
1345
+ });
1346
+ } else if (g.kind === "press") g.handler.move?.(toCanvas(client), event);
1347
+ else if (g.kind === "box") {
1348
+ g.client = client;
1349
+ updateBox(g);
1350
+ autoPan(client);
1351
+ } else if (g.kind === "resize") {
1352
+ const p = toCanvas(client);
1353
+ const width = snapTo(Math.max(g.min.width, g.origin.width + p.x - g.start.x));
1354
+ const height = snapTo(Math.max(g.min.height, g.origin.height + p.y - g.start.y));
1355
+ transact((tr) => tr.updateNode(g.node, {
1356
+ width,
1357
+ height
1358
+ }), { group: g.group });
1359
+ } else if (g.kind === "bend") {
1360
+ if (!g.started) {
1361
+ if (Math.hypot(client.x - g.start.x, client.y - g.start.y) < THRESHOLD[g.touch ? "touch" : "mouse"]) return;
1362
+ g.started = true;
1363
+ capture(g.id);
1364
+ }
1365
+ const p = toCanvas(client);
1366
+ const patch = g.move({
1367
+ x: p.x + g.offset.x,
1368
+ y: p.y + g.offset.y
1369
+ });
1370
+ transact((tr) => tr.updateEdge(g.edge, { data: {
1371
+ ...g.origin,
1372
+ ...patch
1373
+ } }), { group: g.group });
1374
+ } else if (g.kind === "connect") {
1375
+ g.client = client;
1376
+ if (!g.started) {
1377
+ if (Math.hypot(client.x - g.start.x, client.y - g.start.y) < THRESHOLD[g.touch ? "touch" : "mouse"]) return;
1378
+ g.started = true;
1379
+ }
1380
+ updateLink(g);
1381
+ autoPan(client);
1382
+ } else {
1383
+ g.client = client;
1384
+ if (!g.started) {
1385
+ const { zoom } = viewport.value;
1386
+ const p = toCanvas(client);
1387
+ if (Math.hypot(p.x - g.start.x, p.y - g.start.y) * zoom < THRESHOLD[g.touch ? "touch" : "mouse"] || g.held) return;
1388
+ clearPress();
1389
+ if (props.readonly) return;
1390
+ startDrag(g);
1391
+ }
1392
+ g.snap = !event.altKey;
1393
+ moveDrag(g);
1394
+ autoPan(client);
1395
+ }
1396
+ }
1397
+ function updateBox(g) {
1398
+ g.current = toCanvas(g.client);
1399
+ const box = rectFrom(g.start, g.current);
1400
+ const inside = (r) => r.x >= box.x && r.y >= box.y && r.x + r.width <= box.x + box.width && r.y + r.height <= box.y + box.height;
1401
+ const hit = doc.value.nodes.filter((node) => {
1402
+ if (hiddenIn.value.has(node.id)) return false;
1403
+ const rect = rectOf(node);
1404
+ if (isContainer(node)) return inside(rect);
1405
+ return intersectsTurned(box, rect, specOf(node).rotation?.(node) ?? 0);
1406
+ }).map((n) => n.id);
1407
+ setSelection({
1408
+ nodes: [.../* @__PURE__ */ new Set([...g.base.nodes, ...hit])],
1409
+ edges: g.base.edges
1410
+ });
1411
+ gesture.value = { ...g };
1412
+ }
1413
+ /**
1414
+ * What moving `ids` moves: the unlocked ones and everything inside them,
1415
+ * locked or not, since a container carries its contents.
1416
+ */
1417
+ function carried(ids) {
1418
+ const out = /* @__PURE__ */ new Set();
1419
+ for (const id of ids) {
1420
+ if (out.has(id) || nodeById.value.get(id)?.locked) continue;
1421
+ out.add(id);
1422
+ for (const inner of tree.value.descendants(id)) out.add(inner);
1423
+ }
1424
+ return out;
1425
+ }
1426
+ function startDrag(g) {
1427
+ g.started = true;
1428
+ capture(g.id);
1429
+ g.group = `drag:${++dragCount}`;
1430
+ const moving = carried(selection.value.nodes);
1431
+ for (const node of doc.value.nodes) if (moving.has(node.id)) g.origins.set(node.id, {
1432
+ x: node.x,
1433
+ y: node.y
1434
+ });
1435
+ g.others = doc.value.nodes.filter((node) => !moving.has(node.id)).map(rectOf);
1436
+ for (const edge of doc.value.edges) {
1437
+ const fromSource = g.origins.has(edge.source.node);
1438
+ if (fromSource === g.origins.has(edge.target.node)) continue;
1439
+ const [mine, theirs] = fromSource ? [edge.source, edge.target] : [edge.target, edge.source];
1440
+ const a = anchorOf(mine, fromSource ? "source" : "target");
1441
+ const b = anchorOf(theirs, fromSource ? "target" : "source");
1442
+ if (a && b && level(a.side) === level(b.side)) g.links.push({
1443
+ from: a,
1444
+ to: b,
1445
+ axis: level(a.side)
1446
+ });
1447
+ }
1448
+ gesture.value = { ...g };
1449
+ }
1450
+ function moveDrag(g) {
1451
+ if (!g.origins.size) return;
1452
+ const p = toCanvas(g.client);
1453
+ const dx = p.x - g.start.x;
1454
+ const dy = p.y - g.start.y;
1455
+ const moved = boundsOf([...g.origins].map(([id, o]) => {
1456
+ return {
1457
+ ...getNodeRect(id),
1458
+ x: o.x + dx,
1459
+ y: o.y + dy
1460
+ };
1461
+ }));
1462
+ const snap = g.snap ? snapBox(moved, props.guides ? g.others : [], {
1463
+ threshold: 6 / viewport.value.zoom,
1464
+ grid: props.grid,
1465
+ links: props.guides ? g.links.map((link) => ({
1466
+ ...link,
1467
+ from: {
1468
+ x: link.from.x + dx,
1469
+ y: link.from.y + dy
1470
+ }
1471
+ })) : []
1472
+ }) : {
1473
+ dx: 0,
1474
+ dy: 0,
1475
+ guides: []
1476
+ };
1477
+ const sx = dx + snap.dx;
1478
+ const sy = dy + snap.dy;
1479
+ transact((tr) => {
1480
+ for (const [id, o] of g.origins) tr.moveNode(id, Math.round(o.x + sx), Math.round(o.y + sy));
1481
+ }, { group: g.group });
1482
+ guides.value = snap.guides;
1483
+ dropTarget.value = containerAt(p, containerRects(new Set(g.origins.keys()))) ?? null;
1484
+ }
1485
+ /** The container the dragged nodes will go into, or `null` for the canvas. */
1486
+ const dropTarget = ref(null);
1487
+ /** After a drag: the moved nodes go into the container under the pointer, or out of theirs. */
1488
+ function settleDrop(g) {
1489
+ const target = dropTarget.value ?? void 0;
1490
+ dropTarget.value = null;
1491
+ const moved = tree.value.topmost(g.origins.keys());
1492
+ if (!moved.some((id) => tree.value.parentOf(id) !== target)) return;
1493
+ reparent(moved, target, g.group);
1494
+ }
1495
+ /** Puts nodes into a container, or out on the canvas, and says so. */
1496
+ function reparent(ids, target, group) {
1497
+ const changing = ids.filter((id) => tree.value.parentOf(id) !== target);
1498
+ if (!changing.length) return;
1499
+ const left = target ? void 0 : tree.value.parentOf(changing[0]);
1500
+ transact((tr) => {
1501
+ for (const id of changing) tr.updateNode(id, { parent: target });
1502
+ }, { group });
1503
+ const t = messages.value;
1504
+ if (target) announce(format(t.joinedGroup, { group: nodeName(target) }));
1505
+ else if (left) announce(format(t.leftGroup, { group: nodeName(left) }));
1506
+ }
1507
+ let autoPanFrame = 0;
1508
+ /** Scrolls the canvas while a drag or a selection box nears its edge. */
1509
+ function autoPan(client) {
1510
+ cancelAnimationFrame(autoPanFrame);
1511
+ const el = root.value;
1512
+ if (!el) return;
1513
+ const p = local(client.x, client.y);
1514
+ const vx = edgeSpeed(p.x) - edgeSpeed(el.clientWidth - p.x);
1515
+ const vy = edgeSpeed(p.y) - edgeSpeed(el.clientHeight - p.y);
1516
+ if (!vx && !vy) return;
1517
+ const step = () => {
1518
+ const g = gesture.value;
1519
+ if (!g || g.kind === "pan" || g.kind === "bend" || g.kind === "press" || g.kind === "resize" || (g.kind === "drag" || g.kind === "connect") && !g.started) return;
1520
+ const { x, y, zoom } = viewport.value;
1521
+ viewport.value = reachable({
1522
+ x: x + vx,
1523
+ y: y + vy,
1524
+ zoom
1525
+ });
1526
+ if (g.kind === "box") updateBox(g);
1527
+ else if (g.kind === "connect") updateLink(g);
1528
+ else moveDrag(g);
1529
+ autoPanFrame = requestAnimationFrame(step);
1530
+ };
1531
+ autoPanFrame = requestAnimationFrame(step);
1532
+ }
1533
+ /** The prop's rule, then each extension's, in priority order; the first refusal wins. */
1534
+ const canConnect = computed(() => {
1535
+ const rules = [...props.canConnect ? [props.canConnect] : [], ...resolved.value.rules];
1536
+ if (!rules.length) return void 0;
1537
+ return (connection, current) => {
1538
+ for (const rule of rules) {
1539
+ const verdict = rule(connection, current);
1540
+ if (verdict !== true) return verdict;
1541
+ }
1542
+ return true;
1543
+ };
1544
+ });
1545
+ const connectOptions = () => ({
1546
+ portTypes: portTypes.value,
1547
+ acyclic: props.acyclic,
1548
+ canConnect: canConnect.value
1549
+ });
1550
+ /**
1551
+ * Starts an edge at a port. From an output, or an empty input, it draws a
1552
+ * new edge; from an input that has an edge, it picks that edge up.
1553
+ */
1554
+ function linkFrom(at) {
1555
+ const port = portSpec(at);
1556
+ if (!port) return null;
1557
+ let link;
1558
+ let taken;
1559
+ if (port.kind === "input") {
1560
+ for (const e of doc.value.edges) if (e.target.node === at.node && e.target.port === at.port) taken = e;
1561
+ }
1562
+ if (taken && taken.source.port != null) link = {
1563
+ fixed: taken.source,
1564
+ moving: "target",
1565
+ edge: taken.id,
1566
+ point: anchorOf(at, "target") ?? {
1567
+ x: 0,
1568
+ y: 0
1569
+ },
1570
+ candidate: at
1571
+ };
1572
+ else link = {
1573
+ fixed: at,
1574
+ moving: port.kind === "output" ? "target" : "source",
1575
+ point: anchorOf(at, port.kind === "output" ? "source" : "target") ?? {
1576
+ x: 0,
1577
+ y: 0
1578
+ },
1579
+ candidate: null
1580
+ };
1581
+ return {
1582
+ ...link,
1583
+ valid: validEnds(link)
1584
+ };
1585
+ }
1586
+ const connectionOf = (link, end) => link.moving === "target" ? {
1587
+ source: link.fixed,
1588
+ target: end
1589
+ } : {
1590
+ source: end,
1591
+ target: link.fixed
1592
+ };
1593
+ function validEnds(link) {
1594
+ const valid = /* @__PURE__ */ new Map();
1595
+ const fixedPort = portSpec(link.fixed);
1596
+ if (!fixedPort) return valid;
1597
+ const options = connectOptions();
1598
+ for (const node of doc.value.nodes) {
1599
+ const ids = /* @__PURE__ */ new Set();
1600
+ for (const port of portsOf(specOf(node), node)) {
1601
+ const connection = connectionOf(link, {
1602
+ node: node.id,
1603
+ port: port.id
1604
+ });
1605
+ const ports = link.moving === "target" ? {
1606
+ source: fixedPort,
1607
+ target: port
1608
+ } : {
1609
+ source: port,
1610
+ target: fixedPort
1611
+ };
1612
+ if (canJoin(toRaw(doc.value), connection, ports, options, link.edge)) ids.add(port.id);
1613
+ }
1614
+ if (ids.size) valid.set(node.id, ids);
1615
+ }
1616
+ return valid;
1617
+ }
1618
+ /** How close, in screen px, the pointer must come to a port to snap to it. */
1619
+ function updateLink(g) {
1620
+ const link = g.link;
1621
+ const point = toCanvas(g.client);
1622
+ const radius = (g.touch ? MAGNET * 1.5 : MAGNET) / viewport.value.zoom;
1623
+ let best = null;
1624
+ const consider = (node, port, limit) => {
1625
+ const at = anchorOf({
1626
+ node,
1627
+ port
1628
+ }, link.moving);
1629
+ if (!at) return;
1630
+ const d = Math.hypot(at.x - point.x, at.y - point.y);
1631
+ if (d <= limit && (!best || d < best.d)) best = {
1632
+ end: {
1633
+ node,
1634
+ port
1635
+ },
1636
+ d
1637
+ };
1638
+ };
1639
+ for (const [node, ports] of link.valid) for (const port of ports) consider(node, port, radius);
1640
+ if (!best) {
1641
+ const over = document.elementFromPoint(g.client.x, g.client.y)?.closest("[data-node-id]")?.dataset.nodeId;
1642
+ for (const port of over && link.valid.get(over) || []) consider(over, port, Infinity);
1643
+ }
1644
+ const candidate = best?.end ?? null;
1645
+ g.link = {
1646
+ ...link,
1647
+ point,
1648
+ candidate,
1649
+ refused: candidate ? null : refusedAt(link, point, radius)
1650
+ };
1651
+ gesture.value = { ...g };
1652
+ }
1653
+ /** The nearest port within `radius` of `point` that the link can't land on, and why. */
1654
+ function refusedAt(link, point, radius) {
1655
+ const fixedPort = portSpec(link.fixed);
1656
+ if (!fixedPort) return null;
1657
+ let best = null;
1658
+ for (const node of doc.value.nodes) for (const port of portsOf(specOf(node), node)) {
1659
+ if (link.valid.get(node.id)?.has(port.id)) continue;
1660
+ const end = {
1661
+ node: node.id,
1662
+ port: port.id
1663
+ };
1664
+ if (end.node === link.fixed.node && end.port === link.fixed.port) continue;
1665
+ const at = anchorOf(end, link.moving);
1666
+ const d = at ? Math.hypot(at.x - point.x, at.y - point.y) : Infinity;
1667
+ if (d <= radius && (!best || d < best.d)) best = {
1668
+ end,
1669
+ d
1670
+ };
1671
+ }
1672
+ if (!best) return null;
1673
+ const { end } = best;
1674
+ const port = portSpec(end);
1675
+ const ports = link.moving === "target" ? {
1676
+ source: fixedPort,
1677
+ target: port
1678
+ } : {
1679
+ source: port,
1680
+ target: fixedPort
1681
+ };
1682
+ const refusal = joinRefusal(toRaw(doc.value), connectionOf(link, end), ports, connectOptions(), link.edge);
1683
+ return refusal && {
1684
+ end,
1685
+ refusal
1686
+ };
1687
+ }
1688
+ /** What screen readers hear when a link is refused. */
1689
+ function refusalText(refusal) {
1690
+ if (refusal.reason === "custom") return refusal.message ?? messages.value.refused.custom;
1691
+ return messages.value.refused[refusal.reason];
1692
+ }
1693
+ /** Whether a link was let go over a node other than its own. */
1694
+ function overNode(link) {
1695
+ const { x, y } = link.point;
1696
+ return doc.value.nodes.some((node) => {
1697
+ if (node.id === link.fixed.node || isContainer(node)) return false;
1698
+ const r = rectOf(node);
1699
+ return x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height;
1700
+ });
1701
+ }
1702
+ /** Adds the edge a link describes, replacing what it must. */
1703
+ function finishLink(link, event) {
1704
+ const { candidate } = link;
1705
+ if (!candidate) {
1706
+ if (link.refused) announce(refusalText(link.refused.refusal));
1707
+ else if (!link.edge && overNode(link)) announce(messages.value.noTarget);
1708
+ else if (link.edge) {
1709
+ const id = link.edge;
1710
+ transact((tr) => tr.removeEdge(id));
1711
+ } else if (event) {
1712
+ emit("connectToCanvas", link.fixed, link.point, event);
1713
+ if (!event.defaultPrevented && paletteTypes.value.length) quickAdd.value = {
1714
+ link,
1715
+ at: link.point,
1716
+ client: {
1717
+ x: event.clientX,
1718
+ y: event.clientY
1719
+ },
1720
+ types: typesTaking(link.fixed),
1721
+ touch: event.pointerType === "touch"
1722
+ };
1723
+ }
1724
+ return;
1725
+ }
1726
+ const connection = connectionOf(link, candidate);
1727
+ const source = portSpec(connection.source);
1728
+ const target = portSpec(connection.target);
1729
+ const edge = {
1730
+ id: createId("e"),
1731
+ source: connection.source,
1732
+ target: connection.target
1733
+ };
1734
+ const displaced = source && target ? displacedEdges(toRaw(doc.value), connection, {
1735
+ source,
1736
+ target
1737
+ }, link.edge) : [];
1738
+ transact((tr) => {
1739
+ if (link.edge) tr.removeEdge(link.edge);
1740
+ for (const id of displaced) tr.removeEdge(id);
1741
+ tr.addEdge(edge);
1742
+ });
1743
+ emit("connect", edge);
1744
+ announce(messages.value.connected);
1745
+ }
1746
+ /** What the canvas shows while an edge is being drawn, by mouse, touch or keyboard. */
1747
+ const connecting = computed(() => {
1748
+ const g = gesture.value;
1749
+ if (g?.kind === "connect" && g.started) return g.link;
1750
+ return keyLink.value;
1751
+ });
1752
+ const previewPath = computed(() => {
1753
+ const link = connecting.value ?? quickAdd.value?.link;
1754
+ if (!link) return null;
1755
+ const fixedRole = link.moving === "target" ? "source" : "target";
1756
+ const a = anchorOf(link.fixed, fixedRole);
1757
+ if (!a) return null;
1758
+ const c = link.candidate && anchorOf(link.candidate, link.moving);
1759
+ const opposite = {
1760
+ left: "right",
1761
+ right: "left",
1762
+ top: "bottom",
1763
+ bottom: "top"
1764
+ };
1765
+ const b = c ?? {
1766
+ ...link.point,
1767
+ side: opposite[a.side]
1768
+ };
1769
+ const moved = link.edge ? doc.value.edges.find((e) => e.id === link.edge) : void 0;
1770
+ const route = edgeTypeOf(moved?.type).route;
1771
+ return (link.moving === "target" ? route(a, b, moved) : route(b, a, moved)).path;
1772
+ });
1773
+ /** How each port looks while connecting: by node, then port. */
1774
+ const portStates = computed(() => {
1775
+ const link = connecting.value;
1776
+ if (!link) return null;
1777
+ const states = /* @__PURE__ */ new Map();
1778
+ const set = (end, state) => {
1779
+ let ports = states.get(end.node);
1780
+ if (!ports) states.set(end.node, ports = /* @__PURE__ */ new Map());
1781
+ ports.set(end.port, state);
1782
+ };
1783
+ for (const [node, ports] of link.valid) for (const port of ports) set({
1784
+ node,
1785
+ port
1786
+ }, "valid");
1787
+ set(link.fixed, "active");
1788
+ if (link.candidate) set(link.candidate, "candidate");
1789
+ if (link.refused) set(link.refused.end, "refused");
1790
+ return states;
1791
+ });
1792
+ const keyLink = shallowRef(null);
1793
+ /** Valid ends of a link, nearest first. */
1794
+ /** A port's anchor; nodes not measured yet fall back to their corner. */
1795
+ function place(end, role) {
1796
+ return anchorOf(end, role) ?? nodeById.value.get(end.node) ?? {
1797
+ x: 0,
1798
+ y: 0
1799
+ };
1800
+ }
1801
+ function rankEnds(link) {
1802
+ const at = place(link.fixed, link.moving === "target" ? "source" : "target");
1803
+ const ends = [];
1804
+ for (const [node, ports] of link.valid) for (const port of ports) {
1805
+ const p = place({
1806
+ node,
1807
+ port
1808
+ }, link.moving);
1809
+ ends.push({
1810
+ end: {
1811
+ node,
1812
+ port
1813
+ },
1814
+ d: Math.hypot(p.x - at.x, p.y - at.y)
1815
+ });
1816
+ }
1817
+ return ends.sort((a, b) => a.d - b.d).map((e) => e.end);
1818
+ }
1819
+ function describeEnd(end) {
1820
+ const node = nodeById.value.get(end.node);
1821
+ const port = portSpec(end);
1822
+ const name = node ? labelOf(specOf(node), node, messages.value) : end.node;
1823
+ return format(messages.value.connectTo, {
1824
+ node: name,
1825
+ port: port?.label ?? port?.id ?? ""
1826
+ });
1827
+ }
1828
+ /** Starts keyboard connecting from the next port of a node that has somewhere to go. */
1829
+ function startKeyLink(nodeId, after) {
1830
+ const node = nodeById.value.get(nodeId);
1831
+ if (!node) return false;
1832
+ const all = portsOf(specOf(node), node);
1833
+ const ports = [...all.filter((p) => p.kind === "output"), ...all.filter((p) => p.kind !== "output")];
1834
+ const start = after ? ports.findIndex((p) => p.id === after.port) + 1 : 0;
1835
+ for (let i = 0; i < ports.length; i++) {
1836
+ const port = ports[(start + i) % ports.length];
1837
+ const link = linkFrom({
1838
+ node: nodeId,
1839
+ port: port.id
1840
+ });
1841
+ if (!link) continue;
1842
+ const options = rankEnds(link);
1843
+ if (!options.length) continue;
1844
+ const candidate = link.edge ? link.candidate : options[0];
1845
+ keyLink.value = {
1846
+ ...link,
1847
+ candidate,
1848
+ from: {
1849
+ node: nodeId,
1850
+ port: port.id
1851
+ },
1852
+ options
1853
+ };
1854
+ if (candidate) {
1855
+ reveal(candidate.node);
1856
+ announce(describeEnd(candidate));
1857
+ }
1858
+ return true;
1859
+ }
1860
+ announce(messages.value.noTarget);
1861
+ return false;
1862
+ }
1863
+ function onKeyLink(event) {
1864
+ const link = keyLink.value;
1865
+ const { options } = link;
1866
+ const index = link.candidate ? options.findIndex((o) => o.node === link.candidate.node && o.port === link.candidate.port) : -1;
1867
+ switch (event.key) {
1868
+ case "ArrowRight":
1869
+ case "ArrowDown":
1870
+ case "ArrowLeft":
1871
+ case "ArrowUp": {
1872
+ const candidate = options[(index + (event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length];
1873
+ keyLink.value = {
1874
+ ...link,
1875
+ candidate
1876
+ };
1877
+ reveal(candidate.node);
1878
+ announce(describeEnd(candidate));
1879
+ break;
1880
+ }
1881
+ case "Tab":
1882
+ startKeyLink(link.from.node, link.from);
1883
+ break;
1884
+ case "Enter":
1885
+ keyLink.value = null;
1886
+ finishLink(link);
1887
+ break;
1888
+ case "Escape":
1889
+ keyLink.value = null;
1890
+ break;
1891
+ default: return;
1892
+ }
1893
+ event.preventDefault();
1894
+ }
1895
+ /** The selected nodes and the edges between them. */
1896
+ /** `ids` and every node inside the containers among them. */
1897
+ function withInside(ids) {
1898
+ const out = new Set(ids);
1899
+ for (const id of ids) for (const inner of tree.value.descendants(id)) out.add(inner);
1900
+ return [...out];
1901
+ }
1902
+ function copySelection() {
1903
+ const ids = new Set(withInside(selection.value.nodes));
1904
+ if (!ids.size) return null;
1905
+ const raw = toRaw(doc.value);
1906
+ return {
1907
+ version: 1,
1908
+ nodes: raw.nodes.filter((node) => ids.has(node.id)),
1909
+ edges: raw.edges.filter((edge) => ids.has(edge.source.node) && ids.has(edge.target.node))
1910
+ };
1911
+ }
1912
+ let pastes = 0;
1913
+ /** Adds a copy of `data` with new ids, at the pointer or next to the original. */
1914
+ function paste(data, at) {
1915
+ for (const transform of resolved.value.pasteTransforms) data = transform(data, context);
1916
+ const bounds = boundsOf(data.nodes.map((n) => ({
1917
+ x: n.x,
1918
+ y: n.y,
1919
+ width: n.width ?? 0,
1920
+ height: n.height ?? 0
1921
+ })));
1922
+ if (!bounds) return;
1923
+ const step = (props.grid || 8) * 3;
1924
+ const shift = at ? {
1925
+ x: Math.round(at.x - bounds.x),
1926
+ y: Math.round(at.y - bounds.y)
1927
+ } : {
1928
+ x: step * ++pastes,
1929
+ y: step * pastes
1930
+ };
1931
+ const ids = new Map(data.nodes.map((n) => [n.id, createId()]));
1932
+ const containers = containerRects();
1933
+ const nodes = data.nodes.map((n) => {
1934
+ const x = n.x + shift.x;
1935
+ const y = n.y + shift.y;
1936
+ const copied = n.parent ? ids.get(n.parent) : void 0;
1937
+ const m = metrics.get(n.id);
1938
+ const centre = {
1939
+ x: x + (m?.width ?? n.width ?? 0) / 2,
1940
+ y: y + (m?.height ?? n.height ?? 0) / 2
1941
+ };
1942
+ return {
1943
+ ...n,
1944
+ data: n.data && { ...n.data },
1945
+ id: ids.get(n.id),
1946
+ x,
1947
+ y,
1948
+ locked: void 0,
1949
+ parent: copied ?? containerAt(centre, containers)
1950
+ };
1951
+ });
1952
+ transact((tr) => {
1953
+ for (const node of nodes) tr.addNode(node);
1954
+ for (const edge of data.edges) {
1955
+ const source = ids.get(edge.source.node);
1956
+ const target = ids.get(edge.target.node);
1957
+ if (!source || !target) continue;
1958
+ tr.addEdge({
1959
+ ...edge,
1960
+ id: createId("e"),
1961
+ source: {
1962
+ ...edge.source,
1963
+ node: source
1964
+ },
1965
+ target: {
1966
+ ...edge.target,
1967
+ node: target
1968
+ }
1969
+ });
1970
+ }
1971
+ });
1972
+ const copies = new Set(ids.values());
1973
+ setSelection({
1974
+ nodes: nodes.filter((n) => !n.parent || !copies.has(n.parent)).map((n) => n.id),
1975
+ edges: []
1976
+ });
1977
+ }
1978
+ function duplicate() {
1979
+ const data = copySelection();
1980
+ if (!data || props.readonly) return;
1981
+ pastes = 0;
1982
+ paste(data);
1983
+ }
1984
+ function selectAll() {
1985
+ setSelection({
1986
+ nodes: doc.value.nodes.flatMap((n) => hiddenIn.value.has(n.id) ? [] : [n.id]),
1987
+ edges: []
1988
+ });
1989
+ }
1990
+ /** Copies the selection to the system clipboard, and keeps it for pasting from the menu. */
1991
+ function copy() {
1992
+ const data = copySelection();
1993
+ if (!data) return false;
1994
+ const text = writeClipboard(data);
1995
+ pastes = 0;
1996
+ navigator.clipboard?.writeText(text).catch(() => {});
1997
+ return true;
1998
+ }
1999
+ function cut() {
2000
+ if (props.readonly || !copy()) return false;
2001
+ deleteSelection();
2002
+ return true;
2003
+ }
2004
+ /** Pastes from the system clipboard when the page may read it, or what was copied here. */
2005
+ async function pasteFromClipboard(at) {
2006
+ if (props.readonly) return false;
2007
+ let text = null;
2008
+ try {
2009
+ text = await navigator.clipboard?.readText() ?? null;
2010
+ } catch {}
2011
+ const data = readClipboard(text) ?? readClipboard();
2012
+ if (!data) return false;
2013
+ paste(data, at ?? null);
2014
+ return true;
2015
+ }
2016
+ const paletteTypes = computed(() => [...types.value.values()].filter((spec) => spec.palette));
2017
+ /** A node of a type, not yet in the document. */
2018
+ function draft(spec) {
2019
+ return {
2020
+ id: createId(),
2021
+ type: spec.type,
2022
+ x: 0,
2023
+ y: 0,
2024
+ ...spec.size,
2025
+ data: spec.defaults?.() ?? {}
2026
+ };
2027
+ }
2028
+ /** The first port of a new node that can take an edge from `from`, if any. */
2029
+ function takingPort(node, from) {
2030
+ const fromPort = portSpec(from);
2031
+ if (!fromPort) return void 0;
2032
+ const spec = specOf(node);
2033
+ const next = {
2034
+ ...doc.value,
2035
+ nodes: [...doc.value.nodes, node]
2036
+ };
2037
+ for (const port of portsOf(spec, node)) {
2038
+ if (port.kind === fromPort.kind) continue;
2039
+ const end = {
2040
+ node: node.id,
2041
+ port: port.id
2042
+ };
2043
+ const connection = fromPort.kind === "output" ? {
2044
+ source: from,
2045
+ target: end
2046
+ } : {
2047
+ source: end,
2048
+ target: from
2049
+ };
2050
+ const ports = fromPort.kind === "output" ? {
2051
+ source: fromPort,
2052
+ target: port
2053
+ } : {
2054
+ source: port,
2055
+ target: fromPort
2056
+ };
2057
+ if (!joinRefusal(next, connection, ports, connectOptions())) return port;
2058
+ }
2059
+ }
2060
+ /** Palette types a new node of which could take an edge from `from`. */
2061
+ function typesTaking(from) {
2062
+ return paletteTypes.value.filter((spec) => takingPort(draft(spec), from));
2063
+ }
2064
+ function insertNode(type, options = {}) {
2065
+ const spec = types.value.get(type);
2066
+ if (!spec || props.readonly) return null;
2067
+ const node = draft(spec);
2068
+ if (options.data) node.data = {
2069
+ ...node.data,
2070
+ ...options.data
2071
+ };
2072
+ if (options.size) Object.assign(node, options.size);
2073
+ let at = options.at;
2074
+ let from = options.from;
2075
+ let start = false;
2076
+ const after = !at && !from ? soleSelected() : void 0;
2077
+ if (after) {
2078
+ const rect = getNodeRect(after.id);
2079
+ const vertical = props.direction === "vertical";
2080
+ const gap = TIDY_GAP[props.direction].rank;
2081
+ for (const out of portsOf(specOf(after), after)) {
2082
+ const end = {
2083
+ node: after.id,
2084
+ port: out.id
2085
+ };
2086
+ if (out.kind === "output" && takingPort(node, end)) {
2087
+ from = end;
2088
+ break;
2089
+ }
2090
+ }
2091
+ const fromAt = from && anchorOf(from, "source");
2092
+ at = vertical ? {
2093
+ x: fromAt?.x ?? rect.x + rect.width / 2,
2094
+ y: rect.y + rect.height + gap
2095
+ } : {
2096
+ x: rect.x + rect.width + gap,
2097
+ y: fromAt?.y ?? rect.y + rect.height / 2
2098
+ };
2099
+ start = true;
2100
+ }
2101
+ at ??= toCanvas(clientCenter());
2102
+ node.x = snapTo(at.x);
2103
+ node.y = snapTo(at.y);
2104
+ const inside = containerAt(at, containerRects());
2105
+ if (inside) node.parent = inside;
2106
+ const port = from ? takingPort(node, from) : void 0;
2107
+ const group = `insert:${node.id}`;
2108
+ transact((tr) => {
2109
+ tr.addNode(node);
2110
+ if (from && port) {
2111
+ const end = {
2112
+ node: node.id,
2113
+ port: port.id
2114
+ };
2115
+ const fromPort = portSpec(from);
2116
+ tr.addEdge({
2117
+ id: createId("e"),
2118
+ source: fromPort.kind === "output" ? from : end,
2119
+ target: fromPort.kind === "output" ? end : from
2120
+ });
2121
+ }
2122
+ }, { group });
2123
+ pending.set(node.id, {
2124
+ at,
2125
+ port: port?.id,
2126
+ group,
2127
+ start,
2128
+ avoid: !options.at,
2129
+ focus: options.focus ?? true
2130
+ });
2131
+ setSelection({
2132
+ nodes: [node.id],
2133
+ edges: []
2134
+ });
2135
+ announce(format(messages.value.added, { node: labelOf(spec, node, messages.value) }));
2136
+ return node.id;
2137
+ }
2138
+ /** The node selected on its own, if any. */
2139
+ function soleSelected() {
2140
+ const { nodes, edges } = selection.value;
2141
+ return nodes.length === 1 && !edges.length ? nodeById.value.get(nodes[0]) : void 0;
2142
+ }
2143
+ /** The middle of the editor on screen. */
2144
+ function clientCenter() {
2145
+ const box = root.value.getBoundingClientRect();
2146
+ return {
2147
+ x: box.left + box.width / 2,
2148
+ y: box.top + box.height / 2
2149
+ };
2150
+ }
2151
+ const menu = useTemplateRef("menu");
2152
+ const menuItems = shallowRef([]);
2153
+ /** The extensions' items for a target, a separator between each extension's. */
2154
+ function itemsFor(target) {
2155
+ const items = [];
2156
+ for (const fn of resolved.value.menus) fn(context, target).forEach((item, i) => items.push({
2157
+ ...item,
2158
+ divided: !!items.length && (i === 0 || !!item.divided)
2159
+ }));
2160
+ return items;
2161
+ }
2162
+ const menuOpen = ref(false);
2163
+ /** The last press while the menu was open, which closed it. */
2164
+ let menuPress = null;
2165
+ const onMenuPress = (event) => {
2166
+ menuPress = menuOpen.value ? {
2167
+ target: event.target,
2168
+ at: Date.now()
2169
+ } : null;
2170
+ };
2171
+ const onMenuClick = () => menuPress = null;
2172
+ /** The button that opened the menu, while it is open. */
2173
+ const menuFrom = shallowRef(null);
2174
+ /** The button that opened the menu last, open or not. */
2175
+ let lastMenuFrom = null;
2176
+ watch(menuOpen, (open) => {
2177
+ if (!open) menuFrom.value = null;
2178
+ });
2179
+ function openMenu(target, client, from) {
2180
+ return showMenu(itemsFor(target), client, from);
2181
+ }
2182
+ function showMenu(items, client, from, options = {}) {
2183
+ const press = menuPress;
2184
+ menuPress = null;
2185
+ if (from && from === lastMenuFrom && press && Date.now() - press.at < 500 && press.target instanceof Node && from.contains(press.target)) {
2186
+ lastMenuFrom = null;
2187
+ return false;
2188
+ }
2189
+ if (!items.length) return false;
2190
+ menuItems.value = items;
2191
+ lastMenuFrom = from ?? null;
2192
+ const rect = from?.getBoundingClientRect();
2193
+ if (rect) menu.value?.open(rect.left, rect.top - 2, {
2194
+ box: {
2195
+ width: rect.width,
2196
+ height: rect.height + 4
2197
+ },
2198
+ focusChecked: options.focusChecked
2199
+ });
2200
+ else menu.value?.open(client.x, client.y, { focusChecked: options.focusChecked });
2201
+ menuFrom.value = from ?? null;
2202
+ return true;
2203
+ }
2204
+ onMounted(() => {
2205
+ window.addEventListener("pointerdown", onMenuPress, true);
2206
+ window.addEventListener("click", onMenuClick);
2207
+ });
2208
+ onBeforeUnmount(() => {
2209
+ window.removeEventListener("pointerdown", onMenuPress, true);
2210
+ window.removeEventListener("click", onMenuClick);
2211
+ });
2212
+ const toolbarItems = computed(() => {
2213
+ if (props.readonly || gesture.value || keyLink.value || quickAdd.value) return [];
2214
+ const nodes = selection.value.nodes.flatMap((id) => nodeById.value.get(id) ?? []);
2215
+ if (nodes.length) return resolved.value.toolbars.map((fn) => fn(context, nodes)).filter((list) => list.length);
2216
+ const ids = new Set(selection.value.edges);
2217
+ const edges = doc.value.edges.filter((edge) => ids.has(edge.id));
2218
+ if (!edges.length) return [];
2219
+ return resolved.value.edgeToolbars.map((fn) => fn(context, edges)).filter((list) => list.length);
2220
+ });
2221
+ /** Where the toolbar floats: above the selected nodes, in screen px within the editor. */
2222
+ const toolbarAnchor = computed(() => {
2223
+ const { nodes, edges } = selection.value;
2224
+ const bounds = boundsOf(nodes.length ? nodes.flatMap((id) => {
2225
+ const node = nodeById.value.get(id);
2226
+ if (!node) return [];
2227
+ return nodeBounds(specOf(node), node, rectOf(node));
2228
+ }) : edgeRoutes.value.filter((item) => edges.includes(item.edge.id)).flatMap((item) => pathPoints(item.route.path)).map((p) => ({
2229
+ x: p.x,
2230
+ y: p.y,
2231
+ width: 0,
2232
+ height: 0
2233
+ })));
2234
+ if (!bounds) return null;
2235
+ const { x, y, zoom } = viewport.value;
2236
+ const screen = (rect) => ({
2237
+ left: rect.x * zoom + x,
2238
+ top: rect.y * zoom + y,
2239
+ right: (rect.x + rect.width) * zoom + x,
2240
+ bottom: (rect.y + rect.height) * zoom + y
2241
+ });
2242
+ const reach = 64 / zoom;
2243
+ const band = {
2244
+ x: bounds.x - reach * 4,
2245
+ y: bounds.y - reach,
2246
+ width: bounds.width + reach * 8,
2247
+ height: bounds.height + reach * 2
2248
+ };
2249
+ const avoid = nodes.length ? void 0 : shownNodes.value.filter((node) => !hiddenIn.value.has(node.id) && !isContainer(node)).map((node) => rectOf(node)).filter((rect) => intersects(rect, band)).map(screen);
2250
+ return {
2251
+ x: (bounds.x + bounds.width / 2) * zoom + x,
2252
+ top: bounds.y * zoom + y,
2253
+ bottom: (bounds.y + bounds.height) * zoom + y,
2254
+ avoid
2255
+ };
2256
+ });
2257
+ const quickAdd = shallowRef(null);
2258
+ function chooseQuickAdd(type) {
2259
+ const pendingAdd = quickAdd.value;
2260
+ quickAdd.value = null;
2261
+ if (!pendingAdd) return;
2262
+ if (type) insertNode(type, {
2263
+ at: pendingAdd.at,
2264
+ from: pendingAdd.link.fixed
2265
+ });
2266
+ else root.value?.focus({ preventScroll: true });
2267
+ }
2268
+ const insertDrag = shallowRef(null);
2269
+ let pickUpTimer;
2270
+ /** Keeps a finger that picked a node up from scrolling the page. */
2271
+ let insertHold;
2272
+ /** Whether a point on screen is on the canvas itself, not on a panel over it. */
2273
+ function onCanvasAt(client) {
2274
+ const el = document.elementFromPoint(client.x, client.y);
2275
+ return !!root.value && !!el && root.value.contains(el) && !inPanels(el);
2276
+ }
2277
+ function startInsertDrag(type, event) {
2278
+ const spec = types.value.get(type);
2279
+ if (!spec || props.readonly || event.button !== 0 || insertDrag.value) return false;
2280
+ const client = {
2281
+ x: event.clientX,
2282
+ y: event.clientY
2283
+ };
2284
+ insertDrag.value = {
2285
+ spec,
2286
+ id: event.pointerId,
2287
+ touch: event.pointerType === "touch",
2288
+ start: client,
2289
+ client,
2290
+ started: false,
2291
+ over: false
2292
+ };
2293
+ window.addEventListener("pointermove", onInsertMove, true);
2294
+ window.addEventListener("pointerup", onInsertUp, true);
2295
+ window.addEventListener("pointercancel", onInsertUp, true);
2296
+ window.addEventListener("keydown", onInsertKey, true);
2297
+ window.addEventListener("contextmenu", onInsertMenu, true);
2298
+ insertHold = holdPointer(event);
2299
+ if (event.pointerType === "touch") pickUpTimer = setTimeout(() => {
2300
+ const d = insertDrag.value;
2301
+ if (!d || d.started) return;
2302
+ insertHold?.grab();
2303
+ insertDrag.value = {
2304
+ ...d,
2305
+ started: true,
2306
+ over: onCanvasAt(d.client)
2307
+ };
2308
+ }, PICK_UP);
2309
+ return true;
2310
+ }
2311
+ function onInsertMenu(event) {
2312
+ event.preventDefault();
2313
+ }
2314
+ function onInsertMove(event) {
2315
+ const d = insertDrag.value;
2316
+ if (!d || d.id !== event.pointerId) return;
2317
+ const client = {
2318
+ x: event.clientX,
2319
+ y: event.clientY
2320
+ };
2321
+ if (!d.started) {
2322
+ if (Math.hypot(client.x - d.start.x, client.y - d.start.y) < THRESHOLD[d.touch ? "touch" : "mouse"]) return;
2323
+ if (d.touch) return stopInsertDrag();
2324
+ }
2325
+ insertDrag.value = {
2326
+ ...d,
2327
+ client,
2328
+ started: true,
2329
+ over: onCanvasAt(client)
2330
+ };
2331
+ }
2332
+ function onInsertUp(event) {
2333
+ const d = insertDrag.value;
2334
+ if (!d || d.id !== event.pointerId) return;
2335
+ stopInsertDrag();
2336
+ if (!d.started) return;
2337
+ swallowClick();
2338
+ const client = {
2339
+ x: event.clientX,
2340
+ y: event.clientY
2341
+ };
2342
+ if (event.type === "pointerup" && onCanvasAt(client)) insertNode(d.spec.type, { at: toCanvas(client) });
2343
+ }
2344
+ function onInsertKey(event) {
2345
+ if (event.key !== "Escape" || !insertDrag.value) return;
2346
+ event.preventDefault();
2347
+ event.stopPropagation();
2348
+ if (insertDrag.value.started) swallowClick();
2349
+ stopInsertDrag();
2350
+ }
2351
+ function stopInsertDrag() {
2352
+ insertDrag.value = null;
2353
+ clearTimeout(pickUpTimer);
2354
+ insertHold?.release();
2355
+ insertHold = void 0;
2356
+ window.removeEventListener("contextmenu", onInsertMenu, true);
2357
+ window.removeEventListener("pointermove", onInsertMove, true);
2358
+ window.removeEventListener("pointerup", onInsertUp, true);
2359
+ window.removeEventListener("pointercancel", onInsertUp, true);
2360
+ window.removeEventListener("keydown", onInsertKey, true);
2361
+ }
2362
+ const stopEvent = (event) => {
2363
+ event.stopPropagation();
2364
+ event.preventDefault();
2365
+ };
2366
+ /** The click that ends a drag isn't a click on the source: it doesn't add another node. */
2367
+ function swallowClick() {
2368
+ window.addEventListener("click", stopEvent, {
2369
+ capture: true,
2370
+ once: true
2371
+ });
2372
+ setTimeout(() => window.removeEventListener("click", stopEvent, true));
2373
+ }
2374
+ onBeforeUnmount(stopInsertDrag);
2375
+ const ghostStyle = computed(() => {
2376
+ const d = insertDrag.value;
2377
+ return d ? { transform: `translate(${d.client.x}px, ${d.client.y}px)` } : void 0;
2378
+ });
2379
+ function onPointerUp(event) {
2380
+ if (event.pointerType === "touch") {
2381
+ touches.delete(event.pointerId);
2382
+ if (pinch) {
2383
+ if (touches.size < 2) pinch = null;
2384
+ return;
2385
+ }
2386
+ }
2387
+ const g = gesture.value;
2388
+ if (!g || g.id !== event.pointerId) return;
2389
+ const cancelled = event.type === "pointercancel";
2390
+ if (g.kind === "press" && !cancelled) {
2391
+ g.done = true;
2392
+ g.handler.end?.(toCanvas({
2393
+ x: event.clientX,
2394
+ y: event.clientY
2395
+ }), event);
2396
+ }
2397
+ if (!cancelled) {
2398
+ if (g.kind === "pan" && !g.moved) {
2399
+ if (!g.additive) setSelection(emptySelection());
2400
+ emit("canvasClick", toCanvas({
2401
+ x: event.clientX,
2402
+ y: event.clientY
2403
+ }), event);
2404
+ } else if (g.kind === "box" && g.touch && g.current === g.start) {
2405
+ endGesture();
2406
+ menuFromPointer({
2407
+ kind: "canvas",
2408
+ point: g.start
2409
+ }, {
2410
+ x: event.clientX,
2411
+ y: event.clientY
2412
+ }, event);
2413
+ return;
2414
+ } else if (g.kind === "connect") {
2415
+ if (g.started) finishLink(g.link, event);
2416
+ else setSelection({
2417
+ nodes: [g.node],
2418
+ edges: []
2419
+ });
2420
+ } else if (g.kind === "drag" && g.started) settleDrop(g);
2421
+ else if (g.kind === "drag" && !g.started && !g.held) {
2422
+ const node = nodeById.value.get(g.node);
2423
+ if (g.additive && g.wasSelected) setSelection({
2424
+ nodes: selection.value.nodes.filter((id) => id !== g.node),
2425
+ edges: selection.value.edges
2426
+ });
2427
+ else if (!g.additive && g.wasSelected) setSelection({
2428
+ nodes: [g.node],
2429
+ edges: []
2430
+ });
2431
+ if (node) emit("nodeClick", node, event);
2432
+ }
2433
+ }
2434
+ endGesture();
2435
+ }
2436
+ function endGesture() {
2437
+ const g = gesture.value;
2438
+ if (g?.kind === "press" && !g.done) {
2439
+ g.done = true;
2440
+ g.handler.cancel?.();
2441
+ }
2442
+ clearPress();
2443
+ cancelAnimationFrame(autoPanFrame);
2444
+ if (g && root.value?.hasPointerCapture(g.id)) root.value.releasePointerCapture(g.id);
2445
+ if ((g?.kind === "drag" || g?.kind === "bend") && g.started) history.close();
2446
+ if (g?.kind === "resize") history.close();
2447
+ gesture.value = null;
2448
+ guides.value = [];
2449
+ dropTarget.value = null;
2450
+ panning.value = false;
2451
+ }
2452
+ /** Puts dragged nodes back where they started. */
2453
+ function cancelDrag() {
2454
+ const g = gesture.value;
2455
+ if (g?.kind !== "drag" && g?.kind !== "bend" || !g.started) return false;
2456
+ const tr = new FlowTransaction(docOf()).setMeta({ history: false });
2457
+ if (g.kind === "bend") tr.updateEdge(g.edge, { data: g.origin });
2458
+ else for (const [id, o] of g.origins) tr.moveNode(id, o.x, o.y);
2459
+ if (tr.changed) commit(tr);
2460
+ history.drop(g.group);
2461
+ syncHistory();
2462
+ endGesture();
2463
+ return true;
2464
+ }
2465
+ function onWheel(event) {
2466
+ const target = event.target;
2467
+ if (inPanels(target)) return;
2468
+ if (quickAdd.value) chooseQuickAdd(null);
2469
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? root.value.clientHeight : 1;
2470
+ let dx = event.deltaX * unit;
2471
+ let dy = event.deltaY * unit;
2472
+ if (event.ctrlKey || event.metaKey) {
2473
+ event.preventDefault();
2474
+ stopAnimation();
2475
+ const factor = 2 ** (-Math.max(-100, Math.min(100, dy)) / 240);
2476
+ zoomAround(viewport.value.zoom * factor, local(event.clientX, event.clientY));
2477
+ return;
2478
+ }
2479
+ if (!props.panOnScroll || scrollsInside(target, dx, dy)) return;
2480
+ event.preventDefault();
2481
+ stopAnimation();
2482
+ if (event.shiftKey && !dx) {
2483
+ dx = dy;
2484
+ dy = 0;
2485
+ }
2486
+ const { x, y, zoom } = viewport.value;
2487
+ setViewport({
2488
+ x: x - dx,
2489
+ y: y - dy,
2490
+ zoom
2491
+ });
2492
+ }
2493
+ /** Whether something scrollable inside a node can take the scroll itself. */
2494
+ function scrollsInside(target, dx, dy) {
2495
+ let el = target;
2496
+ while (el && el !== root.value && !el.dataset?.nodeId) {
2497
+ const style = getComputedStyle(el);
2498
+ if (dy && /auto|scroll/.test(style.overflowY) && el.scrollHeight > el.clientHeight) {
2499
+ if (dy < 0 ? el.scrollTop > 0 : el.scrollTop + el.clientHeight < el.scrollHeight) return true;
2500
+ }
2501
+ if (dx && /auto|scroll/.test(style.overflowX) && el.scrollWidth > el.clientWidth) {
2502
+ if (dx < 0 ? el.scrollLeft > 0 : el.scrollLeft + el.clientWidth < el.scrollWidth) return true;
2503
+ }
2504
+ el = el.parentElement;
2505
+ }
2506
+ return false;
2507
+ }
2508
+ function onDblclick(event) {
2509
+ const nodeEl = event.target.closest("[data-node-id]");
2510
+ const node = nodeEl && nodeById.value.get(nodeEl.dataset.nodeId);
2511
+ if (node) emit("nodeDblclick", node, event);
2512
+ else {
2513
+ const edgeEl = event.target.closest("[data-edge-id]");
2514
+ if (edgeEl) editLabel(edgeEl.getAttribute("data-edge-id"));
2515
+ }
2516
+ }
2517
+ function onContextmenu(event) {
2518
+ const target = event.target;
2519
+ if (inPanels(target)) return;
2520
+ if (event.pointerType === "touch" || Date.now() - lastTouch < 1e3) {
2521
+ event.preventDefault();
2522
+ return;
2523
+ }
2524
+ if (isEditable(target)) return;
2525
+ const nodeEl = target.closest("[data-node-id]");
2526
+ const edgeEl = target.closest("[data-edge-id]");
2527
+ const keyboard = event.button === -1 || event.clientX === 0 && event.clientY === 0;
2528
+ let client = {
2529
+ x: event.clientX,
2530
+ y: event.clientY
2531
+ };
2532
+ if (keyboard) {
2533
+ const box = (nodeEl ?? root.value).getBoundingClientRect();
2534
+ client = nodeEl ? {
2535
+ x: box.left,
2536
+ y: box.bottom
2537
+ } : clientCenter();
2538
+ }
2539
+ const point = toCanvas(client);
2540
+ if (nodeEl) {
2541
+ const node = nodeById.value.get(nodeEl.dataset.nodeId);
2542
+ if (!node) return;
2543
+ emit("nodeContextmenu", node, event);
2544
+ if (event.defaultPrevented) return;
2545
+ if (!selection.value.nodes.includes(node.id)) setSelection({
2546
+ nodes: [node.id],
2547
+ edges: []
2548
+ });
2549
+ menuFromPointer({
2550
+ kind: "node",
2551
+ node,
2552
+ point
2553
+ }, client, event);
2554
+ } else if (edgeEl) {
2555
+ const edge = doc.value.edges.find((e) => e.id === edgeEl.getAttribute("data-edge-id"));
2556
+ if (!edge) return;
2557
+ emit("edgeContextmenu", edge, event);
2558
+ if (event.defaultPrevented) return;
2559
+ if (!selection.value.edges.includes(edge.id)) setSelection({
2560
+ nodes: [],
2561
+ edges: [edge.id]
2562
+ });
2563
+ menuFromPointer({
2564
+ kind: "edge",
2565
+ edge,
2566
+ point
2567
+ }, client, event);
2568
+ } else {
2569
+ emit("canvasContextmenu", point, event);
2570
+ if (event.defaultPrevented) return;
2571
+ menuFromPointer({
2572
+ kind: "canvas",
2573
+ point
2574
+ }, client, event);
2575
+ }
2576
+ }
2577
+ /** Opens the menu for a pointer event, keeping the browser's own when there's nothing to show. */
2578
+ function menuFromPointer(target, client, event) {
2579
+ if (target.kind === "canvas" && event.type !== "contextmenu") {
2580
+ const menuEvent = new MouseEvent("contextmenu", {
2581
+ cancelable: true,
2582
+ clientX: client.x,
2583
+ clientY: client.y
2584
+ });
2585
+ emit("canvasContextmenu", target.point, menuEvent);
2586
+ if (menuEvent.defaultPrevented) return;
2587
+ }
2588
+ if (openMenu(target, client)) event.preventDefault();
2589
+ }
2590
+ const isEditable = (el) => !!el.closest("input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]");
2591
+ /** Nodes in the order Tab visits them: along the edges, left to right. */
2592
+ const readingOrder = () => flowOrder(doc.value);
2593
+ function focusNode(id) {
2594
+ focusedNode.value = id;
2595
+ setSelection({
2596
+ nodes: [id],
2597
+ edges: []
2598
+ });
2599
+ reveal(id);
2600
+ nextTick(() => root.value?.querySelector(`[data-node-id="${CSS.escape(id)}"]`)?.focus({ preventScroll: true }));
2601
+ }
2602
+ /** Room around grouped nodes; more on top for the title. */
2603
+ const GROUP_PAD = {
2604
+ side: 24,
2605
+ top: 56
2606
+ };
2607
+ /**
2608
+ * Puts nodes, the selected ones by default, into a new container of `type`
2609
+ * drawn around them, as one undo step; returns its id.
2610
+ */
2611
+ function groupNodes(ids = selection.value.nodes, type = "group") {
2612
+ const spec = types.value.get(type);
2613
+ if (props.readonly || !spec?.container) return null;
2614
+ const top = tree.value.topmost(ids.filter((id) => !hiddenIn.value.has(id)));
2615
+ const bounds = boundsOf(top.flatMap((id) => getNodeRect(id) ?? []));
2616
+ if (!bounds) return null;
2617
+ const parents = new Set(top.map((id) => tree.value.parentOf(id)));
2618
+ if (parents.size > 1) {
2619
+ announce(messages.value.groupMixed);
2620
+ return null;
2621
+ }
2622
+ const group = {
2623
+ ...draft(spec),
2624
+ x: snapTo(bounds.x - GROUP_PAD.side),
2625
+ y: snapTo(bounds.y - GROUP_PAD.top),
2626
+ width: snapTo(bounds.width + GROUP_PAD.side * 2),
2627
+ height: snapTo(bounds.height + GROUP_PAD.top + GROUP_PAD.side),
2628
+ parent: [...parents][0]
2629
+ };
2630
+ transact((tr) => {
2631
+ const order = tr.doc.nodes.map((n) => n.id);
2632
+ const index = Math.min(...top.map((id) => order.indexOf(id)));
2633
+ tr.addNode(group, index);
2634
+ for (const id of top) tr.updateNode(id, { parent: group.id });
2635
+ });
2636
+ setSelection({
2637
+ nodes: [group.id],
2638
+ edges: []
2639
+ });
2640
+ announce(format(messages.value.added, { node: nameOf(group) }));
2641
+ return group.id;
2642
+ }
2643
+ /** Removes a container and keeps what was inside it, as one undo step. */
2644
+ function ungroupNode(id) {
2645
+ const node = nodeById.value.get(id);
2646
+ if (props.readonly || !node || !isContainer(node)) return false;
2647
+ const children = tree.value.children.get(id) ?? [];
2648
+ const name = nameOf(node);
2649
+ transact((tr) => {
2650
+ for (const child of children) tr.updateNode(child, { parent: node.parent });
2651
+ tr.removeNode(id);
2652
+ });
2653
+ setSelection({
2654
+ nodes: [...children],
2655
+ edges: []
2656
+ });
2657
+ announce(format(messages.value.ungrouped, { group: name }));
2658
+ return true;
2659
+ }
2660
+ /**
2661
+ * Takes nodes, the selected ones by default, out of the containers they
2662
+ * are in and puts them just below, as one undo step; returns how many moved.
2663
+ */
2664
+ function leaveGroup(ids = selection.value.nodes) {
2665
+ if (props.readonly) return 0;
2666
+ const byParent = /* @__PURE__ */ new Map();
2667
+ for (const id of tree.value.topmost(ids)) {
2668
+ const parent = tree.value.parentOf(id);
2669
+ if (parent && !nodeById.value.get(id)?.locked) byParent.set(parent, [...byParent.get(parent) ?? [], id]);
2670
+ }
2671
+ if (!byParent.size) return 0;
2672
+ let count = 0;
2673
+ transact((tr) => {
2674
+ for (const [parent, list] of byParent) {
2675
+ const frame = getNodeRect(parent);
2676
+ const top = Math.min(...list.map((id) => nodeById.value.get(id).y));
2677
+ const dy = snapTo(frame.y + frame.height + GROUP_PAD.side - top);
2678
+ for (const id of list) {
2679
+ tr.updateNode(id, { parent: nodeById.value.get(parent).parent });
2680
+ for (const inner of [id, ...tree.value.descendants(id)]) {
2681
+ const node = tr.getNode(inner);
2682
+ tr.moveNode(inner, node.x, node.y + dy);
2683
+ }
2684
+ count++;
2685
+ }
2686
+ }
2687
+ });
2688
+ if (byParent.size === 1) announce(format(messages.value.leftGroup, { group: nodeName([...byParent.keys()][0]) }));
2689
+ return count;
2690
+ }
2691
+ function deleteSelection() {
2692
+ const { edges } = selection.value;
2693
+ const nodes = withInside(selection.value.nodes);
2694
+ let count = 0;
2695
+ transact((tr) => {
2696
+ for (const id of edges) if (tr.getEdge(id)) {
2697
+ tr.removeEdge(id);
2698
+ count++;
2699
+ }
2700
+ for (const id of nodes) {
2701
+ const node = tr.getNode(id);
2702
+ if (node && !node.locked) {
2703
+ tr.removeNode(id);
2704
+ count++;
2705
+ }
2706
+ }
2707
+ });
2708
+ if (count) {
2709
+ root.value?.focus({ preventScroll: true });
2710
+ announce(format(messages.value.deleted, { count }));
2711
+ }
2712
+ }
2713
+ function nudge(dx, dy) {
2714
+ const ids = selection.value.nodes;
2715
+ const moving = carried(ids);
2716
+ transact((tr) => {
2717
+ for (const id of moving) {
2718
+ const node = tr.getNode(id);
2719
+ if (node) tr.moveNode(id, node.x + dx, node.y + dy);
2720
+ }
2721
+ }, { group: "nudge" });
2722
+ const others = containerRects(moving);
2723
+ const settle = /* @__PURE__ */ new Map();
2724
+ for (const id of tree.value.topmost(moving)) {
2725
+ const rect = getNodeRect(id);
2726
+ if (!rect) continue;
2727
+ const target = containerAt({
2728
+ x: rect.x + rect.width / 2,
2729
+ y: rect.y + rect.height / 2
2730
+ }, others);
2731
+ settle.set(target, [...settle.get(target) ?? [], id]);
2732
+ }
2733
+ for (const [target, list] of settle) reparent(list, target, "nudge");
2734
+ if (ids.length === 1) reveal(ids[0]);
2735
+ }
2736
+ /** Resizes the selected resizable nodes from the keyboard. */
2737
+ function resizeBy(dw, dh) {
2738
+ const nodes = selection.value.nodes.flatMap((id) => {
2739
+ const node = nodeById.value.get(id);
2740
+ return node && resizeOf(specOf(node), node) && !node.locked ? [node] : [];
2741
+ });
2742
+ if (!nodes.length) return false;
2743
+ const sizes = nodes.map((node) => {
2744
+ const min = resizeOf(specOf(node), node);
2745
+ const rect = rectOf(node);
2746
+ return {
2747
+ node,
2748
+ width: Math.max(min.minWidth ?? 40, rect.width + dw),
2749
+ height: Math.max(min.minHeight ?? 40, rect.height + dh)
2750
+ };
2751
+ });
2752
+ transact((tr) => {
2753
+ for (const { node, width, height } of sizes) tr.updateNode(node.id, {
2754
+ width,
2755
+ height
2756
+ });
2757
+ }, { group: "resize-keys" });
2758
+ if (sizes.length === 1) {
2759
+ const { node, width, height } = sizes[0];
2760
+ announce(format(messages.value.resized, {
2761
+ node: nameOf(node),
2762
+ width,
2763
+ height
2764
+ }));
2765
+ }
2766
+ return true;
2767
+ }
2768
+ function onKeydown(event) {
2769
+ const target = event.target;
2770
+ if (isEditable(target) || inPanels(target)) return;
2771
+ const nodeEl = target.closest("[data-node-id]");
2772
+ if (nodeEl && nodeEl !== target) return;
2773
+ if (keyLink.value) return onKeyLink(event);
2774
+ if (event.key === "Escape" && gesture.value?.kind === "press") {
2775
+ event.preventDefault();
2776
+ endGesture();
2777
+ return;
2778
+ }
2779
+ for (const keymap of keymaps.value) for (const [hotkey, handler] of Object.entries(keymap)) if (matchesHotkey(event, hotkey) && handler(event) !== false) {
2780
+ event.preventDefault();
2781
+ return;
2782
+ }
2783
+ const mod = event.metaKey || event.ctrlKey;
2784
+ const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
2785
+ const onCanvas = target === root.value || !!target.dataset?.nodeId;
2786
+ if (mod) {
2787
+ let handled = true;
2788
+ if (key === "z" && !props.readonly) {
2789
+ if (event.shiftKey) redo();
2790
+ else undo();
2791
+ } else if (key === "y" && !props.readonly) redo();
2792
+ else if (key === "a") selectAll();
2793
+ else if (key === "f" && !event.altKey && props.searchable) openSearch();
2794
+ else if (key === "c" || key === "x" || key === "v") return;
2795
+ else if (key === "d" && !props.readonly) {
2796
+ if (!selection.value.nodes.length) return;
2797
+ duplicate();
2798
+ } else if (key === "=" || key === "+") zoomIn();
2799
+ else if (key === "-") zoomOut();
2800
+ else if (key === "0") zoomTo(1);
2801
+ else handled = false;
2802
+ if (handled) event.preventDefault();
2803
+ return;
2804
+ }
2805
+ if (event.altKey && !key.startsWith("Arrow")) return;
2806
+ switch (key) {
2807
+ case "F3":
2808
+ if (!stepFound(event.shiftKey ? -1 : 1)) return;
2809
+ break;
2810
+ case "Tab": {
2811
+ if (!onCanvas) return;
2812
+ const order = readingOrder();
2813
+ if (!order.length) return;
2814
+ const current = target.dataset?.nodeId;
2815
+ const index = current ? order.findIndex((n) => n.id === current) : -1;
2816
+ if (event.shiftKey) {
2817
+ if (index > 0) focusNode(order[index - 1].id);
2818
+ else if (index === 0) root.value.focus({ preventScroll: true });
2819
+ else return;
2820
+ } else if (index < order.length - 1) focusNode(order[index + 1].id);
2821
+ else {
2822
+ end.value?.focus({ preventScroll: true });
2823
+ return;
2824
+ }
2825
+ event.preventDefault();
2826
+ return;
2827
+ }
2828
+ case "Escape":
2829
+ if (cancelDrag() || closeSearch()) break;
2830
+ if (!selection.value.nodes.length && !selection.value.edges.length) return;
2831
+ setSelection(emptySelection());
2832
+ break;
2833
+ case "Enter":
2834
+ case "F2": {
2835
+ const { nodes, edges } = selection.value;
2836
+ if (props.readonly || nodes.length || edges.length !== 1) return;
2837
+ editLabel(edges[0]);
2838
+ break;
2839
+ }
2840
+ case "Delete":
2841
+ case "Backspace":
2842
+ if (props.readonly) return;
2843
+ deleteSelection();
2844
+ break;
2845
+ case "ArrowLeft":
2846
+ case "ArrowRight":
2847
+ case "ArrowUp":
2848
+ case "ArrowDown": {
2849
+ const sign = key === "ArrowLeft" || key === "ArrowUp" ? -1 : 1;
2850
+ const horizontal = key === "ArrowLeft" || key === "ArrowRight";
2851
+ if (selection.value.nodes.length && !props.readonly) {
2852
+ const step = (props.grid || 1) * (event.shiftKey ? 4 : 1);
2853
+ if (event.altKey) {
2854
+ if (!resizeBy(horizontal ? sign * step : 0, horizontal ? 0 : sign * step)) return;
2855
+ } else nudge(horizontal ? sign * step : 0, horizontal ? 0 : sign * step);
2856
+ } else {
2857
+ const step = event.shiftKey ? 160 : 40;
2858
+ const { x, y, zoom } = viewport.value;
2859
+ setViewport({
2860
+ x: horizontal ? x - sign * step : x,
2861
+ y: horizontal ? y : y - sign * step,
2862
+ zoom
2863
+ }, true);
2864
+ }
2865
+ break;
2866
+ }
2867
+ case " ":
2868
+ if (!onCanvas) return;
2869
+ spaceHeld.value = true;
2870
+ break;
2871
+ case "c":
2872
+ if (props.readonly || selection.value.nodes.length !== 1) return;
2873
+ startKeyLink(selection.value.nodes[0]);
2874
+ break;
2875
+ case "e":
2876
+ if (!cycleEdges(event.shiftKey ? -1 : 1)) return;
2877
+ break;
2878
+ default: return;
2879
+ }
2880
+ event.preventDefault();
2881
+ }
2882
+ function onKeyup(event) {
2883
+ if (event.key === " ") spaceHeld.value = false;
2884
+ }
2885
+ const end = useTemplateRef("end");
2886
+ const live = ref("");
2887
+ /** Set while a cancelled change is being told of, so what follows doesn't say it was made. */
2888
+ let refusing = false;
2889
+ function announce(text) {
2890
+ if (refusing) return;
2891
+ live.value = "";
2892
+ nextTick(() => live.value = text);
2893
+ }
2894
+ /** Why the last change didn't go ahead, shown for a moment over the editor. */
2895
+ const notice = ref("");
2896
+ let noticeTimer;
2897
+ onBeforeUnmount(() => clearTimeout(noticeTimer));
2898
+ /** Says why a change didn't go ahead, once while the same one repeats, as in a drag. */
2899
+ let lastRefusal = {
2900
+ text: "",
2901
+ at: 0
2902
+ };
2903
+ function refuse(text) {
2904
+ const at = Date.now();
2905
+ if (refusing || text === lastRefusal.text && at - lastRefusal.at < 1e3) return;
2906
+ lastRefusal = {
2907
+ text,
2908
+ at
2909
+ };
2910
+ announce(text);
2911
+ notice.value = text;
2912
+ clearTimeout(noticeTimer);
2913
+ noticeTimer = setTimeout(() => notice.value = "", 3200);
2914
+ refusing = true;
2915
+ nextTick(() => refusing = false);
2916
+ }
2917
+ const findOpen = ref(false);
2918
+ const findQuery = ref({
2919
+ search: "",
2920
+ caseSensitive: false
2921
+ });
2922
+ const findPanel = ref();
2923
+ /** The nodes whose names contain the text, in reading order. */
2924
+ function findNodes({ search, caseSensitive }) {
2925
+ if (!search) return [];
2926
+ const fold = (text) => caseSensitive ? text : text.toLowerCase();
2927
+ const wanted = fold(search);
2928
+ return readingOrder().filter((node) => fold(nameOf(node)).includes(wanted)).map((node) => node.id);
2929
+ }
2930
+ const found = computed(() => findOpen.value ? findNodes(findQuery.value) : []);
2931
+ /** The match that is the one node selected; `-1` when none is. */
2932
+ const currentFound = computed(() => {
2933
+ const { nodes } = selection.value;
2934
+ return nodes.length === 1 ? found.value.indexOf(nodes[0]) : -1;
2935
+ });
2936
+ /** The matches' boxes, in canvas coordinates, to mark them. */
2937
+ const foundRects = computed(() => found.value.flatMap((id) => {
2938
+ const rect = getNodeRect(id);
2939
+ return rect ? [{
2940
+ id,
2941
+ rect,
2942
+ rotation: nodeRotation(id)
2943
+ }] : [];
2944
+ }));
2945
+ /** Selects a match and brings it to the middle of the view. */
2946
+ function goToFound(index) {
2947
+ const id = found.value[index];
2948
+ if (!id) return;
2949
+ setSelection({
2950
+ nodes: [id],
2951
+ edges: []
2952
+ });
2953
+ centerNode(id);
2954
+ const node = nodeById.value.get(id);
2955
+ if (node) announce(nameOf(node));
2956
+ }
2957
+ function stepFound(step) {
2958
+ const list = found.value;
2959
+ if (!list.length) return false;
2960
+ const i = currentFound.value;
2961
+ goToFound(i >= 0 ? (i + step + list.length) % list.length : step > 0 ? 0 : list.length - 1);
2962
+ return true;
2963
+ }
2964
+ /** Opens the panel, with a query put in. */
2965
+ function openSearch(query) {
2966
+ if (!props.searchable) return false;
2967
+ if (query) findQuery.value = {
2968
+ ...findQuery.value,
2969
+ ...query
2970
+ };
2971
+ findOpen.value = true;
2972
+ nextTick(() => findPanel.value?.focus());
2973
+ return true;
2974
+ }
2975
+ function closeSearch() {
2976
+ if (!findOpen.value) return false;
2977
+ findOpen.value = false;
2978
+ root.value?.focus({ preventScroll: true });
2979
+ return true;
2980
+ }
2981
+ const panelsLayer = useTemplateRef("panelsLayer");
2982
+ const findBox = useTemplateRef("findBox");
2983
+ const findTop = ref(FIND_INSET);
2984
+ function placeFind() {
2985
+ const layer = panelsLayer.value;
2986
+ const box = findBox.value;
2987
+ const panel = box?.firstElementChild;
2988
+ if (!layer || !box || !panel) return;
2989
+ const area = layer.getBoundingClientRect();
2990
+ const width = panel.offsetWidth;
2991
+ const height = panel.offsetHeight;
2992
+ const right = area.right - FIND_INSET;
2993
+ const left = right - width;
2994
+ const others = [...layer.children].filter((el) => el !== box && !el.classList.contains("dou-flow-toolbar")).map((el) => el.getBoundingClientRect()).filter((r) => r.width && r.height).sort((a, b) => a.top - b.top);
2995
+ let top = area.top + FIND_INSET;
2996
+ for (const r of others) if (r.left < right && r.right > left && r.top < top + height && r.bottom > top) top = r.bottom + 8;
2997
+ findTop.value = Math.round(top - area.top);
2998
+ }
2999
+ watch(() => [findQuery.value.search, findQuery.value.caseSensitive], () => {
3000
+ if (findOpen.value && found.value.length && currentFound.value < 0) goToFound(0);
3001
+ });
3002
+ watch(viewport, () => root.value?.dispatchEvent(new Event("scroll")));
3003
+ /**
3004
+ * Copy, cut and paste while the canvas or a node has focus. Going through the
3005
+ * clipboard events puts the nodes on the system clipboard without asking for
3006
+ * permission.
3007
+ */
3008
+ function onClipboard(event) {
3009
+ const active = document.activeElement;
3010
+ if (!active || !root.value?.contains(active)) return;
3011
+ if (active !== root.value && !active.dataset?.nodeId) return;
3012
+ if (event.type === "paste") {
3013
+ if (props.readonly) return;
3014
+ const data = readClipboard(event.clipboardData?.getData("text/plain"));
3015
+ if (!data) return;
3016
+ event.preventDefault();
3017
+ paste(data, hover ? toCanvas(hover) : null);
3018
+ return;
3019
+ }
3020
+ const data = copySelection();
3021
+ if (!data) return;
3022
+ event.preventDefault();
3023
+ event.clipboardData?.setData("text/plain", writeClipboard(data));
3024
+ pastes = 0;
3025
+ if (event.type === "cut" && !props.readonly) deleteSelection();
3026
+ }
3027
+ const clipboardEvents = [
3028
+ "copy",
3029
+ "cut",
3030
+ "paste"
3031
+ ];
3032
+ const pointerListeners = [
3033
+ ["pointermove", onPointerMove],
3034
+ ["pointerup", onPointerUp],
3035
+ ["pointercancel", onPointerUp]
3036
+ ];
3037
+ /** Keeps the page from scrolling while a finger drags or pinches in the editor. */
3038
+ function holdTouch(event) {
3039
+ if ((gesture.value?.touch || pinch) && event.cancelable) event.preventDefault();
3040
+ }
3041
+ /** The editor's size on screen, for the minimap. */
3042
+ const size = ref({
3043
+ width: 0,
3044
+ height: 0
3045
+ });
3046
+ watch([findOpen, size], () => findOpen.value && void nextTick(placeFind), { flush: "post" });
3047
+ let sizeObserver;
3048
+ onMounted(() => {
3049
+ const el = root.value;
3050
+ size.value = {
3051
+ width: el.clientWidth,
3052
+ height: el.clientHeight
3053
+ };
3054
+ if (typeof ResizeObserver !== "undefined") {
3055
+ sizeObserver = new ResizeObserver(() => {
3056
+ size.value = {
3057
+ width: el.clientWidth,
3058
+ height: el.clientHeight
3059
+ };
3060
+ });
3061
+ sizeObserver.observe(el);
3062
+ }
3063
+ root.value.addEventListener("wheel", onWheel, { passive: false });
3064
+ for (const type of clipboardEvents) document.addEventListener(type, onClipboard);
3065
+ for (const [type, fn] of pointerListeners) window.addEventListener(type, fn, true);
3066
+ window.addEventListener("touchmove", holdTouch, { passive: false });
3067
+ });
3068
+ onBeforeUnmount(() => {
3069
+ sizeObserver?.disconnect();
3070
+ cancelAnimationFrame(tweenFrame);
3071
+ for (const type of clipboardEvents) document.removeEventListener(type, onClipboard);
3072
+ root.value?.removeEventListener("wheel", onWheel);
3073
+ for (const [type, fn] of pointerListeners) window.removeEventListener(type, fn, true);
3074
+ window.removeEventListener("touchmove", holdTouch);
3075
+ stopInsertDrag();
3076
+ stopAnimation();
3077
+ endGesture();
3078
+ });
3079
+ const marquee = computed(() => {
3080
+ const g = gesture.value;
3081
+ if (g?.kind !== "box") return null;
3082
+ const { x, y, zoom } = viewport.value;
3083
+ const box = rectFrom(g.start, g.current);
3084
+ return {
3085
+ transform: `translate(${box.x * zoom + x}px, ${box.y * zoom + y}px)`,
3086
+ width: `${box.width * zoom}px`,
3087
+ height: `${box.height * zoom}px`
3088
+ };
3089
+ });
3090
+ /** A gesture that changes the document as it goes: moving nodes, resizing, bending an edge. */
3091
+ const gesturing = computed(() => {
3092
+ const g = gesture.value;
3093
+ if (g?.kind === "resize") return true;
3094
+ return (g?.kind === "drag" || g?.kind === "bend") && g.started;
3095
+ });
3096
+ const dragging = computed(() => {
3097
+ const g = gesture.value;
3098
+ return g?.kind === "drag" && g.started ? new Set(g.origins.keys()) : null;
3099
+ });
3100
+ /** How far past the editor's edges nodes and edges are still drawn, in screen px. */
3101
+ const drawnArea = computed(() => {
3102
+ const { width, height } = size.value;
3103
+ if (!width || !height) return null;
3104
+ const { x, y, zoom } = viewport.value;
3105
+ return {
3106
+ x: (-x - DRAW_MARGIN) / zoom,
3107
+ y: (-y - DRAW_MARGIN) / zoom,
3108
+ width: (width + 400) / zoom,
3109
+ height: (height + 400) / zoom
3110
+ };
3111
+ });
3112
+ /** The node holding focus, kept drawn when it scrolls away. */
3113
+ const focusedNode = ref(null);
3114
+ function onFocusin(event) {
3115
+ const nodeEl = event.target.closest("[data-node-id]");
3116
+ focusedNode.value = nodeEl?.dataset.nodeId ?? null;
3117
+ }
3118
+ /**
3119
+ * Nodes in view. A node not measured yet is drawn wherever it is, so it gets
3120
+ * a size; selected and focused ones too, so dragging and keys keep working.
3121
+ */
3122
+ const drawnNodes = computed(() => {
3123
+ const area = drawnArea.value;
3124
+ if (!area) return shownNodes.value.filter((node) => !hiddenIn.value.has(node.id));
3125
+ const keep = selectedNodes.value;
3126
+ const hidden = hiddenIn.value;
3127
+ return shownNodes.value.filter((node) => {
3128
+ if (hidden.has(node.id)) return false;
3129
+ const m = metrics.get(node.id);
3130
+ if (!m || keep.has(node.id) || node.id === focusedNode.value) return true;
3131
+ const rect = {
3132
+ x: node.x,
3133
+ y: node.y,
3134
+ width: m.width,
3135
+ height: m.height
3136
+ };
3137
+ return intersects(nodeBounds(specOf(node), node, rect), area);
3138
+ });
3139
+ });
3140
+ /** Containers, drawn under the edges: outer ones first. */
3141
+ const drawnContainers = computed(() => drawnNodes.value.filter(isContainer).sort((a, b) => tree.value.depth(a.id) - tree.value.depth(b.id)));
3142
+ /** The other nodes, lowest `z` first; equal ones in document order. */
3143
+ const drawnOthers = computed(() => {
3144
+ const others = drawnNodes.value.filter((node) => !isContainer(node));
3145
+ if (!others.some((node) => node.z)) return others;
3146
+ const index = new Map(others.map((node, i) => [node.id, i]));
3147
+ return others.sort((a, b) => (a.z ?? 0) - (b.z ?? 0) || index.get(a.id) - index.get(b.id));
3148
+ });
3149
+ /** Zoomed out this far, measured nodes are drawn as plain boxes. */
3150
+ const simple = computed(() => viewport.value.zoom < props.simpleZoom);
3151
+ /** Names below this zoom would be a few px tall: only the boxes are drawn. */
3152
+ const classes = computed(() => ({
3153
+ "is-simple": simple.value,
3154
+ "is-tiny": simple.value && viewport.value.zoom < TINY,
3155
+ "is-readonly": props.readonly,
3156
+ "is-panning": panning.value,
3157
+ "is-space": spaceHeld.value,
3158
+ "is-dragging": !!dragging.value,
3159
+ "is-connecting": !!connecting.value,
3160
+ "is-refused": !!connecting.value?.refused
3161
+ }));
3162
+ const context = {
3163
+ exportSVG,
3164
+ exportPNG,
3165
+ doc: currentDoc,
3166
+ selection,
3167
+ viewport,
3168
+ setViewport,
3169
+ readonly: computed(() => props.readonly),
3170
+ messages,
3171
+ minZoom: computed(() => props.minZoom),
3172
+ maxZoom: computed(() => props.maxZoom),
3173
+ transact,
3174
+ undo,
3175
+ redo,
3176
+ canUndo,
3177
+ canRedo,
3178
+ fit,
3179
+ zoomTo,
3180
+ zoomIn,
3181
+ zoomOut,
3182
+ portTypeOf,
3183
+ colorOf,
3184
+ getNodeRect,
3185
+ tidy,
3186
+ centerNode,
3187
+ announce,
3188
+ size,
3189
+ selectedNodes: computed(() => new Set(selection.value.nodes)),
3190
+ root,
3191
+ insertNode,
3192
+ paletteTypes,
3193
+ startInsertDrag,
3194
+ editLabel,
3195
+ groupNodes,
3196
+ ungroupNode,
3197
+ leaveGroup,
3198
+ tree,
3199
+ isContainer,
3200
+ nodeRotation,
3201
+ gesturing,
3202
+ previewNode,
3203
+ connectingPath: previewPath,
3204
+ openMenu,
3205
+ menuItems: itemsFor,
3206
+ showMenu,
3207
+ menuFrom,
3208
+ toCanvas,
3209
+ copy,
3210
+ cut,
3211
+ paste: pasteFromClipboard,
3212
+ duplicate,
3213
+ deleteSelection,
3214
+ selectAll
3215
+ };
3216
+ provide(flowEditorKey, context);
3217
+ let cleanups = [];
3218
+ let mounted = false;
3219
+ function teardownExtensions() {
3220
+ for (const cleanup of cleanups.splice(0)) cleanup();
3221
+ customHistory = null;
3222
+ }
3223
+ let setUp = false;
3224
+ let setupTimes = [];
3225
+ let setupWarned = false;
3226
+ /** An extension made in the template is new on every render, and set up again each time. */
3227
+ function warnRepeatedSetup() {
3228
+ if (process.env.NODE_ENV === "production" || setupWarned) return;
3229
+ const now = Date.now();
3230
+ setupTimes = [...setupTimes.filter((time) => now - time < 1e3), now];
3231
+ if (setupTimes.length < 4) return;
3232
+ setupWarned = true;
3233
+ console.warn("[@dou-ui/flow] the extensions keep being set up again, which also clears undo each time. Create them once in <script setup> instead of in the template.");
3234
+ }
3235
+ function setupExtensions() {
3236
+ if (setUp) warnRepeatedSetup();
3237
+ setUp = true;
3238
+ teardownExtensions();
3239
+ customHistory = resolved.value.history?.(context) ?? null;
3240
+ for (const setup of resolved.value.setups) {
3241
+ const cleanup = setup(context);
3242
+ if (typeof cleanup === "function") cleanups.push(cleanup);
3243
+ }
3244
+ syncHistory();
3245
+ }
3246
+ onMounted(() => {
3247
+ mounted = true;
3248
+ setupExtensions();
3249
+ });
3250
+ watch(() => [resolved.value.history, ...resolved.value.setups], (hooks, before) => {
3251
+ if (mounted && (hooks.length !== before.length || hooks.some((hook, i) => hook !== before[i]))) setupExtensions();
3252
+ });
3253
+ onBeforeUnmount(teardownExtensions);
3254
+ const keymaps = computed(() => resolved.value.keymaps.map((keymap) => keymap(context)));
3255
+ const selectedNodes = computed(() => new Set(selection.value.nodes));
3256
+ function updateData(id, patch) {
3257
+ transact((tr) => tr.updateData(id, patch), { group: `data:${id}` });
3258
+ }
3259
+ /** The flow drawn as SVG, with image addresses turned by `imageHref`. */
3260
+ function drawing(options, imageHref) {
3261
+ const el = root.value;
3262
+ if (!el) throw new Error("[@dou-ui/flow] the editor is not mounted");
3263
+ let keep = null;
3264
+ let frame;
3265
+ if (options.range === "selection") {
3266
+ if (!selection.value.nodes.length) throw new Error("[@dou-ui/flow] nothing is selected to export");
3267
+ keep = /* @__PURE__ */ new Set();
3268
+ for (const id of selection.value.nodes) {
3269
+ keep.add(id);
3270
+ for (const inner of tree.value.descendants(id)) keep.add(inner);
3271
+ }
3272
+ } else if (options.range === "view") {
3273
+ const { x, y, zoom } = viewport.value;
3274
+ frame = {
3275
+ x: -x / zoom,
3276
+ y: -y / zoom,
3277
+ width: size.value.width / zoom,
3278
+ height: size.value.height / zoom
3279
+ };
3280
+ }
3281
+ const hidden = hiddenIn.value;
3282
+ const nodes = shownNodes.value.filter((node) => {
3283
+ if (hidden.has(node.id) || keep && !keep.has(node.id)) return false;
3284
+ if (!frame) return true;
3285
+ return intersects(nodeBounds(specOf(node), node, rectOf(node)), frame);
3286
+ });
3287
+ const index = new Map(nodes.map((node, i) => [node.id, i]));
3288
+ const ordered = nodes.sort((a, b) => {
3289
+ const ca = isContainer(a);
3290
+ if (ca !== isContainer(b)) return ca ? -1 : 1;
3291
+ if (ca) return tree.value.depth(a.id) - tree.value.depth(b.id) || index.get(a.id) - index.get(b.id);
3292
+ return (a.z ?? 0) - (b.z ?? 0) || index.get(a.id) - index.get(b.id);
3293
+ });
3294
+ const tools = drawTools(el, messages.value, imageHref);
3295
+ const empty = /* @__PURE__ */ new Map();
3296
+ try {
3297
+ const drawNodes = ordered.map((node) => ({
3298
+ node,
3299
+ spec: specOf(node),
3300
+ rect: rectOf(node),
3301
+ ports: metrics.get(node.id)?.ports ?? empty,
3302
+ container: isContainer(node),
3303
+ label: nameOf(node),
3304
+ port: (id) => drawPort(node, id, tools)
3305
+ }));
3306
+ const stale = staleEdges.value;
3307
+ const edges = [];
3308
+ for (const item of edgeRoutes.value) {
3309
+ if (keep && !(keep.has(edgeEnd(item.edge.source).node) && keep.has(edgeEnd(item.edge.target).node))) continue;
3310
+ edges.push({
3311
+ edge: item.edge,
3312
+ type: edgeTypeOf(item.edge.type).type,
3313
+ route: item.route,
3314
+ style: item.style,
3315
+ stale: stale.has(item.edge.id)
3316
+ });
3317
+ }
3318
+ return {
3319
+ ...drawFlow({
3320
+ nodes: drawNodes,
3321
+ edges,
3322
+ padding: options.padding ?? 24,
3323
+ background: options.background ?? true,
3324
+ frame,
3325
+ layers: resolved.value.drawLayers.map((fn) => (draw) => fn(context, draw, ordered))
3326
+ }, tools, el),
3327
+ images: tools.images
3328
+ };
3329
+ } finally {
3330
+ tools.dispose();
3331
+ }
3332
+ }
3333
+ /** A port as `flow-port` shows it, at rest. */
3334
+ function drawPort(node, id, tools) {
3335
+ const at = metrics.get(node.id)?.ports.get(id);
3336
+ const spec = portsOf(specOf(node), node).find((port) => port.id === id);
3337
+ if (!at || !spec) return "";
3338
+ const type = portTypeOf(spec);
3339
+ const ring = tools.color(colorOf(type) ?? "var(--dou-flow-editor-port)");
3340
+ const paint = `fill="${connected.value.get(node.id)?.has(id) ? ring : tools.color("var(--dou-flow-editor-node-bg)")}" stroke="${ring}" stroke-width="1"`;
3341
+ const x = Math.round(at.x * 100) / 100;
3342
+ const y = Math.round(at.y * 100) / 100;
3343
+ const shape = type?.shape ?? "circle";
3344
+ if (shape === "square") return `<rect x="${x - 3.5}" y="${y - 3.5}" width="7" height="7" rx="1.5" ${paint}/>`;
3345
+ if (shape === "triangle") return `<path d="M${x - 2.4} ${y - 2.8}L${x + 2.4} ${y}L${x - 2.4} ${y + 2.8}Z" ${paint} stroke-linejoin="round"/>`;
3346
+ return `<circle cx="${x}" cy="${y}" r="3.5" ${paint}/>`;
3347
+ }
3348
+ function exportSVG(options = {}) {
3349
+ return drawing(options).svg;
3350
+ }
3351
+ /** Image addresses this page couldn't read, not asked for again. */
3352
+ const unreadable = /* @__PURE__ */ new Set();
3353
+ const PNG_SOURCE = {
3354
+ scope: "@dou-ui/flow",
3355
+ hint: "export range 'view' or 'selection' a part at a time, or use exportSVG()"
3356
+ };
3357
+ async function exportPNG(options = {}) {
3358
+ const { images, width: w, height: h } = drawing(options);
3359
+ fitPixelRatio(w, h, options.pixelRatio, PNG_SOURCE);
3360
+ const read = /* @__PURE__ */ new Map();
3361
+ await Promise.all([...images].map(async (src) => {
3362
+ const data = unreadable.has(src) ? null : await readImage(src);
3363
+ if (data === null) unreadable.add(src);
3364
+ read.set(src, data);
3365
+ }));
3366
+ const { svg, width, height } = drawing(options, (src) => read.get(src) ?? null);
3367
+ return svgToPNG(svg, {
3368
+ width,
3369
+ height,
3370
+ pixelRatio: options.pixelRatio,
3371
+ ...PNG_SOURCE
3372
+ });
3373
+ }
3374
+ __expose({
3375
+ setViewport,
3376
+ openSearch,
3377
+ closeSearch,
3378
+ findNodes,
3379
+ exportSVG,
3380
+ exportPNG,
3381
+ transact,
3382
+ undo,
3383
+ redo,
3384
+ get canUndo() {
3385
+ return canUndo.value;
3386
+ },
3387
+ get canRedo() {
3388
+ return canRedo.value;
3389
+ },
3390
+ fit,
3391
+ zoomTo,
3392
+ zoomIn,
3393
+ zoomOut,
3394
+ centerNode,
3395
+ toCanvas,
3396
+ getNodeRect,
3397
+ tidy,
3398
+ get staleEdges() {
3399
+ return [...staleEdges.value];
3400
+ },
3401
+ removeStaleEdges,
3402
+ insertNode,
3403
+ startInsertDrag,
3404
+ editLabel,
3405
+ groupNodes,
3406
+ ungroupNode,
3407
+ leaveGroup,
3408
+ get paletteTypes() {
3409
+ return paletteTypes.value;
3410
+ }
3411
+ });
3412
+ return (_ctx, _cache) => {
3413
+ return openBlock(), createElementBlock("div", {
3414
+ ref_key: "root",
3415
+ ref: root,
3416
+ class: normalizeClass(["dou-flow-editor", classes.value]),
3417
+ style: normalizeStyle(background.value),
3418
+ role: "application",
3419
+ "aria-label": __props.ariaLabel,
3420
+ "aria-description": unref(messages).canvas,
3421
+ tabindex: "0",
3422
+ onPointerdown: onPointerDown,
3423
+ onPointerleave: _cache[7] || (_cache[7] = ($event) => isRef(hover) ? hover.value = null : hover = null),
3424
+ onDblclick,
3425
+ onSelectstart,
3426
+ onContextmenu,
3427
+ onKeydown,
3428
+ onKeyup,
3429
+ onBlurCapture: _cache[8] || (_cache[8] = ($event) => spaceHeld.value = false),
3430
+ onFocusin
3431
+ }, [
3432
+ createElementVNode("div", {
3433
+ class: "dou-flow-editor__layer",
3434
+ style: normalizeStyle(layerStyle.value)
3435
+ }, [
3436
+ (openBlock(true), createElementBlock(Fragment, null, renderList(resolved.value.below, (layer, i) => {
3437
+ return openBlock(), createBlock(resolveDynamicComponent(layer), { key: `below-${i}` });
3438
+ }), 128)),
3439
+ (openBlock(true), createElementBlock(Fragment, null, renderList(foundRects.value, (mark) => {
3440
+ return openBlock(), createElementBlock("div", {
3441
+ key: `found-${mark.id}`,
3442
+ class: normalizeClass(["dou-flow-editor__found", { "is-current": found.value[currentFound.value] === mark.id }]),
3443
+ style: normalizeStyle({
3444
+ translate: `${mark.rect.x}px ${mark.rect.y}px`,
3445
+ rotate: mark.rotation ? `${mark.rotation}deg` : void 0,
3446
+ width: `${mark.rect.width}px`,
3447
+ height: `${mark.rect.height}px`
3448
+ }),
3449
+ "aria-hidden": "true"
3450
+ }, null, 6);
3451
+ }), 128)),
3452
+ (openBlock(true), createElementBlock(Fragment, null, renderList(drawnContainers.value, (node) => {
3453
+ return openBlock(), createElementBlock(Fragment, { key: node.id }, [simple.value && unref(metrics).has(node.id) ? (openBlock(), createElementBlock("div", {
3454
+ key: 0,
3455
+ class: normalizeClass(["dou-flow-editor__simple", {
3456
+ "is-selected": selectedNodes.value.has(node.id),
3457
+ "is-drop-target": dropTarget.value === node.id
3458
+ }]),
3459
+ style: normalizeStyle({
3460
+ transform: `translate(${node.x}px, ${node.y}px)`,
3461
+ width: `${unref(metrics).get(node.id).width}px`,
3462
+ height: `${unref(metrics).get(node.id).height}px`
3463
+ }),
3464
+ "data-node-id": node.id,
3465
+ role: "group",
3466
+ "aria-roledescription": roleOf(node),
3467
+ "aria-label": nameOf(node),
3468
+ tabindex: "-1"
3469
+ }, [renderSlot(_ctx.$slots, "simple", { node }, () => [viewport.value.zoom >= TINY ? (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(unref(labelOf)(specOf(node), node, unref(messages))), 1)) : createCommentVNode("", true)])], 14, _hoisted_2)) : (openBlock(), createBlock(node_view_default, {
3470
+ key: 1,
3471
+ node,
3472
+ spec: specOf(node),
3473
+ selected: selectedNodes.value.has(node.id),
3474
+ readonly: __props.readonly,
3475
+ dragging: !!dragging.value?.has(node.id),
3476
+ connected: connected.value.get(node.id) ?? unref(none),
3477
+ "port-states": portStates.value ? portStates.value.get(node.id) ?? unref(noStates) : null,
3478
+ "role-description": roleOf(node),
3479
+ label: nameOf(node),
3480
+ description: describe(node),
3481
+ "drop-target": dropTarget.value === node.id,
3482
+ onMeasure,
3483
+ onUpdate: updateData
3484
+ }, null, 8, [
3485
+ "node",
3486
+ "spec",
3487
+ "selected",
3488
+ "readonly",
3489
+ "dragging",
3490
+ "connected",
3491
+ "port-states",
3492
+ "role-description",
3493
+ "label",
3494
+ "description",
3495
+ "drop-target"
3496
+ ]))], 64);
3497
+ }), 128)),
3498
+ (openBlock(), createElementBlock("svg", _hoisted_4, [(openBlock(true), createElementBlock(Fragment, null, renderList(edgePaths.value, (item) => {
3499
+ return openBlock(), createElementBlock("g", {
3500
+ key: item.edge.id,
3501
+ "data-edge-id": item.edge.id,
3502
+ "data-edge-type": edgeTypeOf(item.edge.type).type
3503
+ }, [
3504
+ createElementVNode("path", {
3505
+ class: "dou-flow-editor__edge-hit",
3506
+ d: item.route.path
3507
+ }, null, 8, _hoisted_6),
3508
+ createElementVNode("path", {
3509
+ class: normalizeClass(["dou-flow-editor__edge", {
3510
+ "is-selected": item.selected,
3511
+ "is-stale": item.stale,
3512
+ "is-animated": item.edge.animated
3513
+ }]),
3514
+ style: normalizeStyle(item.style),
3515
+ d: item.route.path
3516
+ }, null, 14, _hoisted_7),
3517
+ item.route.marks ? (openBlock(), createElementBlock("path", {
3518
+ key: 0,
3519
+ class: normalizeClass(["dou-flow-editor__edge is-mark", {
3520
+ "is-selected": item.selected,
3521
+ "is-stale": item.stale
3522
+ }]),
3523
+ style: normalizeStyle(item.style),
3524
+ d: item.route.marks
3525
+ }, null, 14, _hoisted_8)) : createCommentVNode("", true)
3526
+ ], 8, _hoisted_5);
3527
+ }), 128)), previewPath.value ? (openBlock(), createElementBlock("path", {
3528
+ key: 0,
3529
+ class: "dou-flow-editor__edge is-preview",
3530
+ d: previewPath.value
3531
+ }, null, 8, _hoisted_9)) : createCommentVNode("", true)])),
3532
+ (openBlock(true), createElementBlock(Fragment, null, renderList(edgeLabels.value, (item) => {
3533
+ return openBlock(), createElementBlock("div", {
3534
+ key: item.edge.id,
3535
+ class: normalizeClass(["dou-flow-editor__label", [bend.value?.edge.id === item.edge.id && `is-bend is-${bend.value.handle.axis}`, {
3536
+ "is-selected": item.selected,
3537
+ "is-editing": item.edge.id === editingLabel.value
3538
+ }]]),
3539
+ style: normalizeStyle({ transform: `translate(${item.route.label.x}px, ${item.route.label.y}px)` }),
3540
+ "data-edge-id": item.edge.id,
3541
+ "data-value": item.edge.id === editingLabel.value ? labelDraft.value || " " : void 0
3542
+ }, [item.edge.id === editingLabel.value ? withDirectives((openBlock(), createElementBlock("input", {
3543
+ key: 0,
3544
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => labelDraft.value = $event),
3545
+ class: "dou-flow-editor__label-input",
3546
+ "aria-label": unref(messages).label,
3547
+ onKeydown: onLabelKeydown,
3548
+ onBlur: _cache[1] || (_cache[1] = ($event) => finishLabel(true))
3549
+ }, null, 40, _hoisted_11)), [[vModelText, labelDraft.value]]) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(item.edge.label), 1)], 64))], 14, _hoisted_10);
3550
+ }), 128)),
3551
+ bend.value && !bend.value.edge.label && editingLabel.value !== bend.value.edge.id ? (openBlock(), createElementBlock("div", {
3552
+ key: 0,
3553
+ class: normalizeClass(["dou-flow-editor__bend", `is-${bend.value.handle.axis}`]),
3554
+ style: normalizeStyle({ translate: `calc(${bend.value.handle.at.x}px - 50%) calc(${bend.value.handle.at.y}px - 50%)` }),
3555
+ "aria-hidden": "true"
3556
+ }, null, 6)) : createCommentVNode("", true),
3557
+ (openBlock(true), createElementBlock(Fragment, null, renderList(drawnOthers.value, (node) => {
3558
+ return openBlock(), createElementBlock(Fragment, { key: node.id }, [simple.value && unref(metrics).has(node.id) ? (openBlock(), createElementBlock("div", {
3559
+ key: 0,
3560
+ class: normalizeClass(["dou-flow-editor__simple", {
3561
+ "is-selected": selectedNodes.value.has(node.id),
3562
+ "is-drop-target": dropTarget.value === node.id
3563
+ }]),
3564
+ style: normalizeStyle({
3565
+ transform: `translate(${node.x}px, ${node.y}px)`,
3566
+ width: `${unref(metrics).get(node.id).width}px`,
3567
+ height: `${unref(metrics).get(node.id).height}px`
3568
+ }),
3569
+ "data-node-id": node.id,
3570
+ role: "group",
3571
+ "aria-roledescription": roleOf(node),
3572
+ "aria-label": nameOf(node),
3573
+ tabindex: "-1"
3574
+ }, [renderSlot(_ctx.$slots, "simple", { node }, () => [viewport.value.zoom >= TINY ? (openBlock(), createElementBlock("span", _hoisted_13, toDisplayString(unref(labelOf)(specOf(node), node, unref(messages))), 1)) : createCommentVNode("", true)])], 14, _hoisted_12)) : (openBlock(), createBlock(node_view_default, {
3575
+ key: 1,
3576
+ node,
3577
+ spec: specOf(node),
3578
+ selected: selectedNodes.value.has(node.id),
3579
+ readonly: __props.readonly,
3580
+ dragging: !!dragging.value?.has(node.id),
3581
+ connected: connected.value.get(node.id) ?? unref(none),
3582
+ "port-states": portStates.value ? portStates.value.get(node.id) ?? unref(noStates) : null,
3583
+ "role-description": roleOf(node),
3584
+ label: nameOf(node),
3585
+ description: describe(node),
3586
+ "drop-target": dropTarget.value === node.id,
3587
+ onMeasure,
3588
+ onUpdate: updateData
3589
+ }, null, 8, [
3590
+ "node",
3591
+ "spec",
3592
+ "selected",
3593
+ "readonly",
3594
+ "dragging",
3595
+ "connected",
3596
+ "port-states",
3597
+ "role-description",
3598
+ "label",
3599
+ "description",
3600
+ "drop-target"
3601
+ ]))], 64);
3602
+ }), 128)),
3603
+ (openBlock(true), createElementBlock(Fragment, null, renderList(resolved.value.above, (layer, i) => {
3604
+ return openBlock(), createBlock(resolveDynamicComponent(layer), { key: `above-${i}` });
3605
+ }), 128)),
3606
+ guides.value.length ? (openBlock(), createElementBlock("svg", _hoisted_14, [(openBlock(true), createElementBlock(Fragment, null, renderList(guides.value, (guide, i) => {
3607
+ return openBlock(), createElementBlock("line", {
3608
+ key: i,
3609
+ x1: guide.axis === "x" ? guide.at : guide.from,
3610
+ y1: guide.axis === "x" ? guide.from : guide.at,
3611
+ x2: guide.axis === "x" ? guide.at : guide.to,
3612
+ y2: guide.axis === "x" ? guide.to : guide.at
3613
+ }, null, 8, _hoisted_15);
3614
+ }), 128))])) : createCommentVNode("", true)
3615
+ ], 4),
3616
+ marquee.value ? (openBlock(), createElementBlock("div", {
3617
+ key: 0,
3618
+ class: "dou-flow-editor__marquee",
3619
+ style: normalizeStyle(marquee.value)
3620
+ }, null, 4)) : createCommentVNode("", true),
3621
+ createElementVNode("div", {
3622
+ ref_key: "panelsLayer",
3623
+ ref: panelsLayer,
3624
+ class: "dou-flow-editor__panels"
3625
+ }, [
3626
+ renderSlot(_ctx.$slots, "default"),
3627
+ (openBlock(true), createElementBlock(Fragment, null, renderList(resolved.value.overlays, (overlay, i) => {
3628
+ return openBlock(), createBlock(resolveDynamicComponent(overlay), { key: i });
3629
+ }), 128)),
3630
+ toolbarItems.value.length && toolbarAnchor.value ? (openBlock(), createBlock(flow_toolbar_default, {
3631
+ key: 0,
3632
+ groups: toolbarItems.value,
3633
+ anchor: toolbarAnchor.value,
3634
+ size: size.value
3635
+ }, null, 8, [
3636
+ "groups",
3637
+ "anchor",
3638
+ "size"
3639
+ ])) : createCommentVNode("", true),
3640
+ findOpen.value ? (openBlock(), createElementBlock("div", {
3641
+ key: 1,
3642
+ ref_key: "findBox",
3643
+ ref: findBox,
3644
+ class: "dou-flow-editor__find",
3645
+ style: normalizeStyle({ top: `${findTop.value}px` })
3646
+ }, [createVNode(unref(DouFindReplace), {
3647
+ ref_key: "findPanel",
3648
+ ref: findPanel,
3649
+ search: findQuery.value.search,
3650
+ count: found.value.length,
3651
+ current: currentFound.value,
3652
+ readonly: "",
3653
+ "onUpdate:search": _cache[3] || (_cache[3] = ($event) => findQuery.value = {
3654
+ ...findQuery.value,
3655
+ search: $event
3656
+ }),
3657
+ onNext: _cache[4] || (_cache[4] = ($event) => stepFound(1)),
3658
+ onPrevious: _cache[5] || (_cache[5] = ($event) => stepFound(-1)),
3659
+ onClose: closeSearch
3660
+ }, {
3661
+ options: withCtx(() => [createVNode(unref(DouTooltip), { content: unref(common)("findReplace.caseSensitive") }, {
3662
+ default: withCtx(() => [createVNode(unref(DouToggle), {
3663
+ pressed: findQuery.value.caseSensitive,
3664
+ size: "sm",
3665
+ "aria-label": unref(common)("findReplace.caseSensitive"),
3666
+ "onUpdate:pressed": _cache[2] || (_cache[2] = ($event) => findQuery.value = {
3667
+ ...findQuery.value,
3668
+ caseSensitive: $event
3669
+ })
3670
+ }, {
3671
+ default: withCtx(() => [createVNode(unref(LetterCaseIcon))]),
3672
+ _: 1
3673
+ }, 8, ["pressed", "aria-label"])]),
3674
+ _: 1
3675
+ }, 8, ["content"])]),
3676
+ _: 1
3677
+ }, 8, [
3678
+ "search",
3679
+ "count",
3680
+ "current"
3681
+ ])], 4)) : createCommentVNode("", true)
3682
+ ], 512),
3683
+ createVNode(flow_menu_default, {
3684
+ ref_key: "menu",
3685
+ ref: menu,
3686
+ open: menuOpen.value,
3687
+ "onUpdate:open": _cache[6] || (_cache[6] = ($event) => menuOpen.value = $event),
3688
+ items: menuItems.value
3689
+ }, null, 8, ["open", "items"]),
3690
+ createVNode(flow_quick_add_default, {
3691
+ state: quickAdd.value,
3692
+ onChoose: chooseQuickAdd
3693
+ }, createSlots({ _: 2 }, [_ctx.$slots.quickAdd ? {
3694
+ name: "default",
3695
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "quickAdd", normalizeProps(guardReactiveProps(scope)))]),
3696
+ key: "0"
3697
+ } : void 0]), 1032, ["state"]),
3698
+ (openBlock(), createBlock(Teleport, { to: "body" }, [insertDrag.value?.started ? (openBlock(), createElementBlock("div", {
3699
+ key: 0,
3700
+ class: normalizeClass(["dou-flow-editor__ghost", { "is-outside": !insertDrag.value.over }]),
3701
+ style: normalizeStyle(ghostStyle.value),
3702
+ "aria-hidden": "true"
3703
+ }, [renderSlot(_ctx.$slots, "ghost", {
3704
+ spec: insertDrag.value.spec,
3705
+ over: insertDrag.value.over
3706
+ }, () => [createElementVNode("span", _hoisted_16, [insertDrag.value.spec.palette?.icon ? (openBlock(), createElementBlock("span", _hoisted_17, [(openBlock(), createBlock(resolveDynamicComponent(insertDrag.value.spec.palette.icon)))])) : createCommentVNode("", true), createTextVNode(" " + toDisplayString(insertDrag.value.spec.palette?.label ?? insertDrag.value.spec.type), 1)])])], 6)) : createCommentVNode("", true)])),
3707
+ createElementVNode("span", {
3708
+ ref_key: "end",
3709
+ ref: end,
3710
+ class: "dou-flow-editor__end",
3711
+ tabindex: "-1"
3712
+ }, null, 512),
3713
+ createElementVNode("div", _hoisted_18, toDisplayString(live.value), 1),
3714
+ notice.value ? (openBlock(), createElementBlock("div", _hoisted_19, toDisplayString(notice.value), 1)) : createCommentVNode("", true)
3715
+ ], 46, _hoisted_1);
3716
+ };
3717
+ }
3718
+ });
3719
+ export { flow_editor_vue_vue_type_script_setup_true_lang_default as default };
3720
+
3721
+ //# sourceMappingURL=flow-editor.vue_vue_type_script_setup_true_lang.js.map