@fusion.dev/helix-headless 0.103.3 → 0.104.1

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 (107) hide show
  1. package/custom-elements.json +9260 -880
  2. package/dist/announcement/root/announcement-root.d.ts +9 -2
  3. package/dist/announcement/root/announcement-root.js +9 -6
  4. package/dist/badge/root/badge-root.js +2 -3
  5. package/dist/canvas/edge/canvas-edge.d.ts +112 -0
  6. package/dist/canvas/edge/canvas-edge.js +66 -0
  7. package/dist/canvas/index.d.ts +4 -0
  8. package/dist/canvas/index.js +3 -0
  9. package/dist/canvas/node/canvas-node.d.ts +211 -0
  10. package/dist/canvas/node/canvas-node.js +148 -0
  11. package/dist/canvas/root/canvas-document.d.ts +215 -0
  12. package/dist/canvas/root/canvas-document.js +284 -0
  13. package/dist/canvas/root/canvas-root.d.ts +924 -0
  14. package/dist/canvas/root/canvas-root.js +1831 -0
  15. package/dist/carousel/root/carousel-root.d.ts +53 -16
  16. package/dist/carousel/root/carousel-root.js +46 -11
  17. package/dist/chat/root/chat-root.d.ts +4 -0
  18. package/dist/chat/root/chat-root.js +11 -7
  19. package/dist/checkbox/root/checkbox-root.js +1 -0
  20. package/dist/checkbox-group/root/checkbox-group-root.d.ts +10 -0
  21. package/dist/checkbox-group/root/checkbox-group-root.js +5 -0
  22. package/dist/color-input/root/color-input-root.js +21 -5
  23. package/dist/color-picker/root/color-picker-root.d.ts +55 -0
  24. package/dist/color-picker/root/color-picker-root.js +61 -12
  25. package/dist/combobox/root/combobox-root.d.ts +21 -2
  26. package/dist/combobox/root/combobox-root.js +23 -9
  27. package/dist/command/root/command-root.d.ts +1 -0
  28. package/dist/command/root/command-root.js +1 -1
  29. package/dist/data-table/root/data-table-root.d.ts +43 -42
  30. package/dist/data-table/root/data-table-root.js +35 -34
  31. package/dist/date-input/root/date-input-root.d.ts +27 -0
  32. package/dist/date-input/root/date-input-root.js +38 -9
  33. package/dist/date-picker/root/date-picker-root.d.ts +28 -16
  34. package/dist/date-picker/root/date-picker-root.js +17 -15
  35. package/dist/drilldown/root/drilldown-root.d.ts +1 -0
  36. package/dist/drilldown/root/drilldown-root.js +5 -2
  37. package/dist/element-events.generated.js +15 -0
  38. package/dist/empty/root/empty-root.d.ts +12 -7
  39. package/dist/empty/root/empty-root.js +13 -10
  40. package/dist/file-input/root/file-input-root.d.ts +5 -1
  41. package/dist/file-input/root/file-input-root.js +6 -4
  42. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  43. package/dist/filter-input/menu/filter-menu.js +1 -1
  44. package/dist/filter-input/root/filter-input-root.js +5 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/input/root/input-root.js +1 -1
  48. package/dist/lightbox/root/lightbox-root.d.ts +9 -1
  49. package/dist/lightbox/root/lightbox-root.js +17 -7
  50. package/dist/listbox/root/listbox-root.d.ts +16 -14
  51. package/dist/listbox/root/listbox-root.js +24 -15
  52. package/dist/map/root/map-root.d.ts +6 -3
  53. package/dist/map/root/map-root.js +8 -2
  54. package/dist/menu/root/menu-root.d.ts +1 -0
  55. package/dist/menu/root/menu-root.js +1 -1
  56. package/dist/notifications/group/notification-group.js +5 -4
  57. package/dist/notifications/notification/notification.d.ts +10 -10
  58. package/dist/notifications/notification/notification.js +3 -3
  59. package/dist/notifications/root/notifications-root.d.ts +1 -0
  60. package/dist/notifications/root/notifications-root.js +1 -0
  61. package/dist/number-input/root/number-input-root.d.ts +17 -0
  62. package/dist/number-input/root/number-input-root.js +22 -6
  63. package/dist/object-inspector/root/object-inspector-root.d.ts +10 -0
  64. package/dist/object-inspector/root/object-inspector-root.js +10 -2
  65. package/dist/outline/root/outline-root.d.ts +15 -0
  66. package/dist/outline/root/outline-root.js +12 -7
  67. package/dist/pagination/root/pagination-root.d.ts +18 -18
  68. package/dist/pagination/root/pagination-root.js +5 -5
  69. package/dist/pagination/size/pagination-size.d.ts +10 -9
  70. package/dist/pagination/size/pagination-size.js +4 -4
  71. package/dist/pagination/summary/pagination-summary.js +6 -5
  72. package/dist/pin-input/root/pin-input-root.d.ts +5 -0
  73. package/dist/pin-input/root/pin-input-root.js +10 -6
  74. package/dist/rating/root/rating-root.js +3 -2
  75. package/dist/select/root/select-root.d.ts +1 -0
  76. package/dist/select/root/select-root.js +8 -3
  77. package/dist/slider/root/slider-root.d.ts +12 -0
  78. package/dist/slider/root/slider-root.js +13 -1
  79. package/dist/splitter/root/splitter-root.d.ts +44 -35
  80. package/dist/splitter/root/splitter-root.js +27 -22
  81. package/dist/switch/root/switch-root.js +1 -0
  82. package/dist/table/column/table-column.d.ts +17 -0
  83. package/dist/table/column/table-column.js +16 -4
  84. package/dist/table/root/table-root.d.ts +1 -0
  85. package/dist/table/root/table-root.js +1 -1
  86. package/dist/textarea/root/textarea-root.js +1 -1
  87. package/dist/time-input/root/time-input-root.d.ts +29 -0
  88. package/dist/time-input/root/time-input-root.js +31 -10
  89. package/dist/time-picker/root/time-picker-root.d.ts +14 -0
  90. package/dist/time-picker/root/time-picker-root.js +16 -14
  91. package/dist/toast/item/toast-item.d.ts +12 -11
  92. package/dist/toast/item/toast-item.js +11 -5
  93. package/dist/toolbar/root/toolbar-root.js +2 -16
  94. package/dist/tooltip/root/tooltip-root.d.ts +1 -0
  95. package/dist/tooltip/root/tooltip-root.js +6 -0
  96. package/dist/tour/root/tour-root.d.ts +10 -10
  97. package/dist/tour/root/tour-root.js +5 -5
  98. package/dist/tree/root/tree-root.d.ts +1 -0
  99. package/dist/tree/root/tree-root.js +1 -1
  100. package/dist/video/root/video-root.d.ts +84 -0
  101. package/dist/video/root/video-root.js +116 -41
  102. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  103. package/dist/video-playlist/menu/video-playlist-menu.js +4 -3
  104. package/package.json +5 -5
  105. package/types/jsx.d.ts +5419 -716
  106. package/types/svelte.d.ts +3 -0
  107. package/types/vue.d.ts +1607 -213
@@ -0,0 +1,1831 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { capturePointer, customElement, fillTemplate, HelixBeforeMoveEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixConnectEvent, HelixElement, HelixMoveEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixSelectEvent, hostAria, inlineStep, isRtl, MixinViewport, } from '@fusion.dev/helix-core';
8
+ import { html, nothing, svg } from 'lit';
9
+ import { property, state } from 'lit/decorators.js';
10
+ import { HelixHeadlessCanvasEdge } from '../edge/canvas-edge.js';
11
+ import { HelixHeadlessCanvasNode } from '../node/canvas-node.js';
12
+ import { colorOf, connector, ends, extent, groupOf, inside, port, reading, sideNear, } from './canvas-document.js';
13
+ export { fromJsonCanvas } from './canvas-document.js';
14
+ const MARGIN = 32;
15
+ const ARROW = 'canvas-arrow';
16
+ const NUDGE = 16;
17
+ const MIN_WIDTH = 48;
18
+ const MIN_HEIGHT = 32;
19
+ const ZOOM_MIN = 0.1;
20
+ const ZOOM_MAX = 8;
21
+ const ruling = (zoom) => zoom >= 0.5 ? zoom : zoom * 2 ** Math.ceil(Math.log2(0.5 / zoom));
22
+ const SLOP = 3;
23
+ const heads = (edge) => ({
24
+ end: (edge.toEnd ?? 'arrow') === 'arrow',
25
+ start: edge.fromEnd === 'arrow',
26
+ });
27
+ const keyBase = (edge) => `edge:${edge.id || `${edge.fromNode}->${edge.toNode}`}`;
28
+ const onPlane = (node, origin) => ({
29
+ ...node,
30
+ x: node.x - origin.x,
31
+ y: node.y - origin.y,
32
+ });
33
+ const INTERACTIVE = 'input, textarea, select, button, a[href], [contenteditable]:not([contenteditable="false"])';
34
+ const custom = (element, name) => element?.style.getPropertyValue(name).trim() ?? '';
35
+ const finite = (value) => (Number.isFinite(value) ? value : 0);
36
+ const size = (element, name, value) => {
37
+ if (value > 0) {
38
+ element.style.setProperty(name, `${value}px`);
39
+ }
40
+ else {
41
+ element.style.removeProperty(name);
42
+ }
43
+ };
44
+ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixElement) {
45
+ constructor() {
46
+ super(...arguments);
47
+ this.comesFromLabel = 'from {cards}';
48
+ this.connectedLabel = 'Asked to connect {from} to {to}.';
49
+ this.connectingLabel = 'Connecting from {from}. Move to another card and press Enter, or Escape to stop.';
50
+ this.connectionLabel = 'From {from} to {to}';
51
+ this.label = '';
52
+ this.labelEditorLabel = 'Label for {name}';
53
+ this.leadsToLabel = 'leads to {cards}';
54
+ this.readOnly = false;
55
+ this.reconnectingLabel = 'Moving the end of the connection from {from} to {to}. Move to a card and press Enter, or Escape to stop.';
56
+ this.selected = '';
57
+ this.stoppedLabel = 'Stopped connecting.';
58
+ this.said = '';
59
+ this.at = { x: 0, y: 0, zoom: 1 };
60
+ this.direct = false;
61
+ this.touched = false;
62
+ this.painted = new WeakMap();
63
+ this.placed = new WeakMap();
64
+ this.cache = {};
65
+ this.sides = new Map();
66
+ this.pressing = false;
67
+ this.pointing = false;
68
+ this.edgeKeys = new WeakMap();
69
+ this.handlePointerDown = (event) => {
70
+ const path = event.composedPath();
71
+ const on = (name) => path.some((one) => one instanceof Element && (one.getAttribute('part') ?? '').split(' ').includes(name));
72
+ const card = this.cardOf(event);
73
+ if (event.button !== 0 || on('controls') || !(card || on('viewport'))) {
74
+ return;
75
+ }
76
+ if (this.held) {
77
+ event.preventDefault();
78
+ this.putDown(event);
79
+ return;
80
+ }
81
+ if (path.some((one) => one instanceof Element && one.matches(INTERACTIVE))) {
82
+ return;
83
+ }
84
+ this.pressing = true;
85
+ const adding = event.shiftKey || event.metaKey || event.ctrlKey;
86
+ const grip = path.find((one) => one instanceof Element && one.hasAttribute('data-corner'));
87
+ if (card && grip && !this.fixed(card)) {
88
+ this.sizing = {
89
+ card,
90
+ corner: grip.getAttribute('data-corner') ?? 'bottom-right',
91
+ from: { x: event.clientX, y: event.clientY },
92
+ pointer: event.pointerId,
93
+ start: { height: card.height, width: card.width, x: card.x, y: card.y },
94
+ };
95
+ capturePointer(this, event.pointerId);
96
+ return;
97
+ }
98
+ const pressed = this.edgeOf(event) ?? card?.name;
99
+ this.pointing = pressed !== undefined;
100
+ if (pressed !== undefined && !adding && !this.picked.includes(pressed)) {
101
+ this.choose(pressed);
102
+ }
103
+ if (!card && pressed === undefined && event.shiftKey) {
104
+ const at = this.toDocument(event);
105
+ this.marquee = { base: this.picked, from: at, pointer: event.pointerId, to: at };
106
+ capturePointer(this, event.pointerId);
107
+ return;
108
+ }
109
+ const end = path.find((one) => one instanceof Element && one.hasAttribute('data-end'));
110
+ const ended = end?.getAttribute('data-edge') ?? undefined;
111
+ if (end && ended !== undefined && !this.readOnly && !this.edgeElement(ended)?.readOnly) {
112
+ this.startReconnect(ended, end.getAttribute('data-end') === 'from' ? 'from' : 'to', event);
113
+ if (this.linking) {
114
+ this.linking = { ...this.linking, origin: { x: event.clientX, y: event.clientY } };
115
+ }
116
+ capturePointer(this, event.pointerId);
117
+ return;
118
+ }
119
+ const pressedEdge = this.edgeOf(event);
120
+ if (pressedEdge !== undefined && !this.readOnly && !this.edgeElement(pressedEdge)?.readOnly) {
121
+ const edge = this.edges.find((one) => this.keyOf(one) === pressedEdge);
122
+ const byId = new Map(this.nodes.map((one) => [one.id, one]));
123
+ const from = edge ? byId.get(edge.fromNode) : undefined;
124
+ const to = edge ? byId.get(edge.toNode) : undefined;
125
+ if (edge && from && to) {
126
+ const at = this.toDocument(event);
127
+ const { end, start } = ends(edge, from, to);
128
+ const moving = Math.hypot(at.x - start.x, at.y - start.y) < Math.hypot(at.x - end.x, at.y - end.y)
129
+ ? 'from'
130
+ : 'to';
131
+ this.startReconnect(pressedEdge, moving, event);
132
+ if (this.linking) {
133
+ this.linking = { ...this.linking, pressed: { x: event.clientX, y: event.clientY } };
134
+ }
135
+ return;
136
+ }
137
+ }
138
+ const handle = path.find((one) => one instanceof Element && one.hasAttribute('data-side'));
139
+ if (card && handle && !this.fixed(card)) {
140
+ this.linking = {
141
+ at: this.toDocument(event),
142
+ from: card.name,
143
+ origin: { x: event.clientX, y: event.clientY },
144
+ pointer: event.pointerId,
145
+ side: handle.getAttribute('data-side'),
146
+ };
147
+ capturePointer(this, event.pointerId);
148
+ return;
149
+ }
150
+ const mover = path.some((one) => one instanceof Element && one.hasAttribute('data-move'));
151
+ if (card && mover && !this.fixed(card)) {
152
+ this.carrying = {
153
+ adding: false,
154
+ cards: this.carriedAll(card).map((one) => ({ card: one, x: one.x, y: one.y })),
155
+ from: { x: event.clientX, y: event.clientY },
156
+ handle: true,
157
+ pointer: event.pointerId,
158
+ pressed: card.name,
159
+ started: true,
160
+ };
161
+ capturePointer(this, event.pointerId);
162
+ return;
163
+ }
164
+ if (card && !this.fixed(card)) {
165
+ this.carrying = {
166
+ adding,
167
+ cards: this.carriedAll(card).map((one) => ({ card: one, x: one.x, y: one.y })),
168
+ from: { x: event.clientX, y: event.clientY },
169
+ pointer: event.pointerId,
170
+ pressed: card.name,
171
+ started: false,
172
+ };
173
+ return;
174
+ }
175
+ this.touched = true;
176
+ this.direct = true;
177
+ this.dragging = {
178
+ at: { x: this.at.x, y: this.at.y },
179
+ from: { x: event.clientX, y: event.clientY },
180
+ pointer: event.pointerId,
181
+ pressed,
182
+ };
183
+ capturePointer(this, event.pointerId);
184
+ };
185
+ this.handlePointerMove = (event) => {
186
+ this.idle.stir();
187
+ const pressed = this.carrying ?? this.dragging ?? this.sizing ?? this.marquee ?? this.linking?.pointer;
188
+ if (event.pointerType === 'mouse' &&
189
+ event.buttons === 0 &&
190
+ pressed !== undefined &&
191
+ !this.held) {
192
+ this.handlePointerUp(new PointerEvent('pointercancel', { pointerId: event.pointerId }));
193
+ return;
194
+ }
195
+ const sizing = this.sizing;
196
+ if (sizing && (sizing.held || sizing.pointer === event.pointerId)) {
197
+ const box = this.resized(sizing.start, sizing.corner, (event.clientX - sizing.from.x) / this.at.zoom, (event.clientY - sizing.from.y) / this.at.zoom);
198
+ const detail = { nodes: [{ name: sizing.card.name, ...box }] };
199
+ this.touched = true;
200
+ sizing.asked = true;
201
+ if (this.requestMove(detail)) {
202
+ sizing.moved = detail;
203
+ }
204
+ return;
205
+ }
206
+ if (this.marquee?.pointer === event.pointerId) {
207
+ this.marquee = { ...this.marquee, to: this.toDocument(event) };
208
+ return;
209
+ }
210
+ if (this.linking && (this.linking.held || this.linking.pointer === event.pointerId)) {
211
+ const pressed = this.linking.pressed;
212
+ if (pressed && Math.hypot(event.clientX - pressed.x, event.clientY - pressed.y) < SLOP) {
213
+ return;
214
+ }
215
+ if (pressed) {
216
+ capturePointer(this, event.pointerId);
217
+ }
218
+ const at = this.toDocument(event);
219
+ const over = this.cardAt(at);
220
+ const to = this.arrival(over?.id);
221
+ this.linking = {
222
+ ...this.linking,
223
+ at,
224
+ over: over?.id,
225
+ pressed: undefined,
226
+ to,
227
+ toSide: to !== undefined && over ? sideNear(over, at) : undefined,
228
+ };
229
+ return;
230
+ }
231
+ const carry = this.carrying;
232
+ if (carry && (carry.held || carry.pointer === event.pointerId)) {
233
+ const across = event.clientX - carry.from.x;
234
+ const down = event.clientY - carry.from.y;
235
+ if (!carry.started && Math.hypot(across, down) < SLOP) {
236
+ return;
237
+ }
238
+ if (!carry.started) {
239
+ capturePointer(this, event.pointerId);
240
+ }
241
+ carry.started = true;
242
+ this.touched = true;
243
+ const detail = {
244
+ nodes: carry.cards.map(({ card, x, y }) => ({
245
+ name: card.name,
246
+ x: Math.round(x + across / this.at.zoom),
247
+ y: Math.round(y + down / this.at.zoom),
248
+ })),
249
+ };
250
+ carry.asked = true;
251
+ if (this.requestMove(detail)) {
252
+ carry.moved = detail;
253
+ }
254
+ return;
255
+ }
256
+ const drag = this.dragging;
257
+ if (!drag || drag.pointer !== event.pointerId) {
258
+ return;
259
+ }
260
+ this.at = {
261
+ ...this.at,
262
+ x: drag.at.x + (event.clientX - drag.from.x),
263
+ y: drag.at.y + (event.clientY - drag.from.y),
264
+ };
265
+ };
266
+ this.handlePointerUp = (event) => {
267
+ this.pressing = false;
268
+ const pointing = this.pointing;
269
+ this.pointing = false;
270
+ const sizing = this.sizing;
271
+ if (sizing?.pointer === event.pointerId) {
272
+ if (this.picksUp(sizing, event, sizing.from)) {
273
+ this.sizing = { ...sizing, held: true };
274
+ return;
275
+ }
276
+ this.sizing = undefined;
277
+ if (sizing.moved) {
278
+ this.dispatchEvent(new HelixMoveEvent(sizing.moved));
279
+ }
280
+ return;
281
+ }
282
+ const marquee = this.marquee;
283
+ if (marquee?.pointer === event.pointerId) {
284
+ this.marquee = undefined;
285
+ if (event.type !== 'pointercancel') {
286
+ this.choose([...marquee.base, ...this.within(marquee)]);
287
+ }
288
+ return;
289
+ }
290
+ if (this.linking?.pointer === event.pointerId) {
291
+ if (this.linking.pressed) {
292
+ const clicked = this.linking.edge;
293
+ this.linking = undefined;
294
+ if (clicked !== undefined && pointing && event.type !== 'pointercancel') {
295
+ this.clicked(clicked, event);
296
+ }
297
+ return;
298
+ }
299
+ const origin = this.linking.origin;
300
+ if (origin && this.picksUp(this.linking, event, origin)) {
301
+ this.linking = { ...this.linking, held: true };
302
+ return;
303
+ }
304
+ if (event.type === 'pointercancel') {
305
+ this.linking = undefined;
306
+ this.said = '';
307
+ return;
308
+ }
309
+ this.finishLinking(this.linking.to ?? this.linking.over, this.linking.toSide);
310
+ return;
311
+ }
312
+ const carry = this.carrying;
313
+ if (carry?.pointer === event.pointerId) {
314
+ if (carry.handle && this.picksUp(carry, event, carry.from)) {
315
+ this.carrying = { ...carry, held: true };
316
+ return;
317
+ }
318
+ this.carrying = undefined;
319
+ if (!carry.started && pointing && event.type !== 'pointercancel') {
320
+ this.clicked(carry.pressed, event);
321
+ }
322
+ if (carry.moved) {
323
+ this.dispatchEvent(new HelixMoveEvent(carry.moved));
324
+ }
325
+ return;
326
+ }
327
+ if (this.dragging?.pointer === event.pointerId) {
328
+ const drag = this.dragging;
329
+ if (event.type !== 'pointercancel' &&
330
+ Math.hypot(event.clientX - drag.from.x, event.clientY - drag.from.y) < SLOP) {
331
+ if (drag.pressed !== undefined && pointing) {
332
+ this.clicked(drag.pressed, event);
333
+ }
334
+ else {
335
+ this.choose(undefined);
336
+ }
337
+ }
338
+ this.dragging = undefined;
339
+ this.direct = false;
340
+ }
341
+ };
342
+ this.holding = false;
343
+ this.putting = false;
344
+ this.putAt = Number.NEGATIVE_INFINITY;
345
+ this.handleHeldKey = (event) => {
346
+ if (event.key === 'Escape' && this.held) {
347
+ event.preventDefault();
348
+ event.stopPropagation();
349
+ this.putBack();
350
+ }
351
+ };
352
+ this.handleHeldOutside = (event) => {
353
+ if (this.held && !event.composedPath().includes(this)) {
354
+ this.putBack();
355
+ }
356
+ };
357
+ this.handleWheel = (event) => {
358
+ if (!event.ctrlKey && !this.filled) {
359
+ return;
360
+ }
361
+ event.preventDefault();
362
+ this.touched = true;
363
+ this.direct = true;
364
+ if (event.ctrlKey) {
365
+ const box = event.currentTarget.getBoundingClientRect();
366
+ const about = { x: event.clientX - box.left, y: event.clientY - box.top };
367
+ this.zoomAbout(event.deltaY < 0 ? 1.1 : 1 / 1.1, about);
368
+ return;
369
+ }
370
+ this.at = { ...this.at, x: this.at.x - event.deltaX, y: this.at.y - event.deltaY };
371
+ };
372
+ this.handleStopKeydown = (event) => {
373
+ const origin = event.composedPath()[0];
374
+ const edge = this.edgeOf(event);
375
+ const card = edge === undefined ? this.cardOf(event) : undefined;
376
+ if (edge === undefined && (!card || origin?.getAttribute?.('part') !== 'node')) {
377
+ return;
378
+ }
379
+ const key = edge ?? card.name;
380
+ const along = inlineStep(this, event.key);
381
+ const down = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
382
+ if (card && event.shiftKey && (along !== 0 || down !== 0)) {
383
+ event.preventDefault();
384
+ if (this.fixed(card)) {
385
+ return;
386
+ }
387
+ const detail = {
388
+ nodes: this.carriedAll(card).map((one) => ({
389
+ name: one.name,
390
+ x: one.x + along * NUDGE,
391
+ y: one.y + down * NUDGE,
392
+ })),
393
+ };
394
+ if (this.requestMove(detail)) {
395
+ this.dispatchEvent(new HelixMoveEvent(detail));
396
+ }
397
+ void this.updateComplete.then(() => this.reveal(card.name));
398
+ return;
399
+ }
400
+ if (card && event.altKey && (along !== 0 || down !== 0)) {
401
+ event.preventDefault();
402
+ if (this.fixed(card)) {
403
+ return;
404
+ }
405
+ const box = this.resized({ height: card.height, width: card.width, x: card.x, y: card.y }, 'bottom-right', along * NUDGE, down * NUDGE);
406
+ const detail = { nodes: [{ name: card.name, ...box }] };
407
+ if (this.requestMove(detail)) {
408
+ this.dispatchEvent(new HelixMoveEvent(detail));
409
+ }
410
+ return;
411
+ }
412
+ if (event.key === ' ' && !this.linking) {
413
+ event.preventDefault();
414
+ this.toggle(key);
415
+ return;
416
+ }
417
+ if ((event.metaKey || event.ctrlKey) && (event.key === 'a' || event.key === 'A')) {
418
+ event.preventDefault();
419
+ this.choose(this.walk.map((one) => one.key));
420
+ return;
421
+ }
422
+ if (event.key === 'Escape') {
423
+ event.preventDefault();
424
+ if (this.linking) {
425
+ const back = this.linking.edge ?? this.linking.from;
426
+ this.linking = undefined;
427
+ this.said = this.stoppedLabel;
428
+ this.focusStop(back);
429
+ return;
430
+ }
431
+ this.choose(undefined);
432
+ this.partNamed('region')?.focus();
433
+ return;
434
+ }
435
+ if (!this.linking && (event.key === 'F2' || (card && event.key === 'Enter'))) {
436
+ event.preventDefault();
437
+ this.editStop(key);
438
+ return;
439
+ }
440
+ if (event.key === 'Enter') {
441
+ if (card && this.linking) {
442
+ event.preventDefault();
443
+ this.finishLinking(card.name);
444
+ return;
445
+ }
446
+ if (edge !== undefined) {
447
+ event.preventDefault();
448
+ const to = this.edges.find((one) => this.keyOf(one) === edge)?.toNode;
449
+ this.focusStop(to);
450
+ }
451
+ return;
452
+ }
453
+ if (card && !this.fixed(card) && !this.linking && (event.key === 'c' || event.key === 'C')) {
454
+ event.preventDefault();
455
+ this.startLinking(card.name);
456
+ return;
457
+ }
458
+ if (edge !== undefined &&
459
+ !this.linking &&
460
+ !this.readOnly &&
461
+ !this.edgeElement(edge)?.readOnly &&
462
+ (event.key === 'c' || event.key === 'C')) {
463
+ event.preventDefault();
464
+ this.startReconnect(edge, 'to');
465
+ return;
466
+ }
467
+ if (event.key === 'Delete' || event.key === 'Backspace') {
468
+ const picked = this.picked;
469
+ const keys = picked.includes(key) ? picked : [key];
470
+ const loose = keys.filter((one) => one.startsWith('edge:')
471
+ ? !this.readOnly && !this.edgeElement(one)?.readOnly
472
+ : !this.fixed(this.cardNamed(one)));
473
+ if (loose.length > 0) {
474
+ event.preventDefault();
475
+ this.requestRemove(loose);
476
+ }
477
+ return;
478
+ }
479
+ const walk = this.linking ? this.walk.filter((one) => one.kind === 'node') : this.walk;
480
+ const at = walk.findIndex((one) => one.key === key);
481
+ const to = event.key === 'Home'
482
+ ? 0
483
+ : event.key === 'End'
484
+ ? walk.length - 1
485
+ : along + down !== 0
486
+ ? Math.min(Math.max(at + Math.sign(along + down), 0), walk.length - 1)
487
+ : undefined;
488
+ if (to === undefined) {
489
+ return;
490
+ }
491
+ event.preventDefault();
492
+ this.focusStop(walk[to]?.key);
493
+ };
494
+ this.handleFocusIn = (event) => {
495
+ const edge = this.edgeOf(event);
496
+ const card = edge === undefined ? this.cardOf(event) : undefined;
497
+ if (edge === undefined && !card) {
498
+ return;
499
+ }
500
+ this.refocus = undefined;
501
+ this.stop = edge ?? card?.name;
502
+ if (!this.pointing && this.stop !== undefined && !this.picked.includes(this.stop)) {
503
+ this.choose(this.stop);
504
+ }
505
+ if (card) {
506
+ if (this.linking) {
507
+ this.linking = { ...this.linking, to: this.arrival(card.name) };
508
+ }
509
+ this.reveal(card.name);
510
+ }
511
+ };
512
+ this.handleFocusOut = (event) => {
513
+ const next = event.relatedTarget;
514
+ const linking = this.linking;
515
+ if (!linking ||
516
+ linking.pointer !== undefined ||
517
+ linking.held ||
518
+ !(next instanceof Node) ||
519
+ this.contains(next) ||
520
+ this.renderRoot.contains(next)) {
521
+ return;
522
+ }
523
+ this.linking = undefined;
524
+ this.said = this.stoppedLabel;
525
+ };
526
+ this.handleDoubleClick = (event) => {
527
+ const path = event.composedPath();
528
+ if (path.some((one) => one instanceof Element && one.matches(INTERACTIVE))) {
529
+ return;
530
+ }
531
+ if (this.held || event.timeStamp - this.putAt < 500) {
532
+ return;
533
+ }
534
+ const edge = this.edgeOf(event);
535
+ const key = edge ?? this.cardOf(event)?.name;
536
+ if (key !== undefined) {
537
+ event.preventDefault();
538
+ this.editStop(key);
539
+ }
540
+ };
541
+ this.handleEditorKeydown = (event) => {
542
+ event.stopPropagation();
543
+ if (event.key === 'Enter' || event.key === 'Escape') {
544
+ event.preventDefault();
545
+ this.finishEditing(event.currentTarget, event.key === 'Enter');
546
+ }
547
+ };
548
+ this.handleEditorBlur = (event) => {
549
+ this.finishEditing(event.currentTarget, true);
550
+ };
551
+ this.handleSlotChange = () => {
552
+ this.requestUpdate();
553
+ };
554
+ }
555
+ get box() {
556
+ const viewport = this.renderRoot?.querySelector('[part="viewport"]');
557
+ return {
558
+ height: viewport?.clientHeight ?? 0,
559
+ width: viewport?.clientWidth ?? 0,
560
+ };
561
+ }
562
+ get cards() {
563
+ return this.remember('cards', () => this.knownChildren.filter((child) => child instanceof HelixHeadlessCanvasNode));
564
+ }
565
+ get byName() {
566
+ return this.remember('byName', () => new Map(this.cards.map((card) => [card.name, card])));
567
+ }
568
+ get byId() {
569
+ return this.remember('byId', () => new Map(this.nodes.map((node) => [node.id, node])));
570
+ }
571
+ get groups() {
572
+ return this.remember('groups', () => this.nodes.filter((node) => node.type === 'group'));
573
+ }
574
+ remember(key, compute) {
575
+ const cache = this.cache;
576
+ if (Object.keys(cache).length === 0) {
577
+ queueMicrotask(() => {
578
+ if (this.cache === cache) {
579
+ this.cache = {};
580
+ }
581
+ });
582
+ }
583
+ cache[key] ??= compute();
584
+ return cache[key];
585
+ }
586
+ requestUpdate(name, oldValue, options) {
587
+ this.cache = {};
588
+ super.requestUpdate(name, oldValue, options);
589
+ }
590
+ get nodes() {
591
+ return this.remember('nodes', () => this.readNodes());
592
+ }
593
+ readNodes() {
594
+ return this.cards.map((card) => ({
595
+ color: colorOf(card.tone, this.paint(card, '--canvas-node-color')),
596
+ height: finite(card.height),
597
+ id: card.name,
598
+ label: card.label || undefined,
599
+ text: card.textContent?.trim() || undefined,
600
+ type: card.group ? 'group' : 'text',
601
+ width: finite(card.width),
602
+ x: finite(card.x),
603
+ y: finite(card.y),
604
+ }));
605
+ }
606
+ paint(element, name) {
607
+ const value = custom(element, name);
608
+ this.painted.set(element, value);
609
+ return value;
610
+ }
611
+ get edges() {
612
+ return this.remember('edges', () => this.readEdges());
613
+ }
614
+ readEdges() {
615
+ const byName = new Map(this.nodes.map((node) => [node.id, node]));
616
+ return this.edgeElements
617
+ .map((element) => ({ element, key: this.keyOfElement(element) }))
618
+ .map(({ element, key }) => {
619
+ const read = (edge) => {
620
+ this.edgeKeys.set(edge, key);
621
+ return edge;
622
+ };
623
+ const edge = {
624
+ color: colorOf(element.tone, this.paint(element, '--canvas-edge-color')),
625
+ fromEnd: element.fromEnd,
626
+ fromNode: element.from,
627
+ fromSide: element.fromSide,
628
+ id: element.name,
629
+ label: element.label,
630
+ path: element.path,
631
+ toEnd: element.toEnd,
632
+ toNode: element.to,
633
+ toSide: element.toSide,
634
+ };
635
+ if (edge.fromSide !== undefined && edge.toSide !== undefined) {
636
+ return read(edge);
637
+ }
638
+ const from = byName.get(edge.fromNode);
639
+ const to = byName.get(edge.toNode);
640
+ if (!from || !to) {
641
+ return read(edge);
642
+ }
643
+ const cards = `${edge.fromNode}->${edge.toNode}`;
644
+ let kept = this.sides.get(key);
645
+ if (kept?.cards !== cards) {
646
+ const drawn = ends(edge, from, to);
647
+ kept = { cards, from: drawn.fromSide, to: drawn.toSide };
648
+ this.sides.set(key, kept);
649
+ }
650
+ return read({
651
+ ...edge,
652
+ fromSide: edge.fromSide ?? kept.from,
653
+ toSide: edge.toSide ?? kept.to,
654
+ });
655
+ });
656
+ }
657
+ panEvent(detail) {
658
+ return new HelixCanvasPanEvent(detail);
659
+ }
660
+ zoomEvent(detail) {
661
+ return new HelixCanvasZoomEvent(detail);
662
+ }
663
+ get restingControlsLabel() {
664
+ return 'Board controls';
665
+ }
666
+ get restingControls() {
667
+ return 'zoom fullscreen';
668
+ }
669
+ pan(to) {
670
+ this.direct = false;
671
+ const across = this.box.width || extent(this.nodes).width;
672
+ const step = across * this.panStep;
673
+ const inline = isRtl(this) ? -step : step;
674
+ const by = {
675
+ down: { x: 0, y: -step },
676
+ end: { x: -inline, y: 0 },
677
+ start: { x: inline, y: 0 },
678
+ up: { x: 0, y: step },
679
+ }[to];
680
+ this.touched = true;
681
+ this.at = { ...this.at, x: this.at.x + by.x, y: this.at.y + by.y };
682
+ super.pan(to);
683
+ }
684
+ zoom(by) {
685
+ this.direct = false;
686
+ this.touched = true;
687
+ this.zoomAbout(by > 0 ? 1.25 : 1 / 1.25, { x: this.box.width / 2, y: this.box.height / 2 });
688
+ super.zoom(by);
689
+ }
690
+ show(name) {
691
+ this.direct = false;
692
+ const node = this.byId.get(name);
693
+ if (!node) {
694
+ return;
695
+ }
696
+ this.touched = true;
697
+ this.stop = name;
698
+ const board = extent(this.nodes);
699
+ const origin = { x: board.x - MARGIN, y: board.y - MARGIN };
700
+ const middle = {
701
+ x: (node.x - origin.x + node.width / 2) * this.at.zoom,
702
+ y: (node.y - origin.y + node.height / 2) * this.at.zoom,
703
+ };
704
+ this.at = {
705
+ ...this.at,
706
+ x: this.box.width / 2 - middle.x,
707
+ y: this.box.height / 2 - middle.y,
708
+ };
709
+ }
710
+ get walk() {
711
+ return this.remember('walk', () => this.readWalk());
712
+ }
713
+ readWalk() {
714
+ const edges = this.edges;
715
+ const ids = new Set(this.nodes.map((node) => node.id));
716
+ const leaving = new Map();
717
+ for (const edge of edges) {
718
+ if (ids.has(edge.toNode)) {
719
+ const stop = { edge, key: this.keyOf(edge), kind: 'edge' };
720
+ leaving.set(edge.fromNode, [...(leaving.get(edge.fromNode) ?? []), stop]);
721
+ }
722
+ }
723
+ return reading(this.nodes, edges, this.order ?? 'spatial').flatMap((node) => [
724
+ { key: node.id, kind: 'node' },
725
+ ...(leaving.get(node.id) ?? []),
726
+ ]);
727
+ }
728
+ focusStop(key) {
729
+ if (key === undefined) {
730
+ return;
731
+ }
732
+ if (key.startsWith('edge:')) {
733
+ const target = [...(this.renderRoot?.querySelectorAll('[data-edge]') ?? [])].find((one) => one.getAttribute('data-edge') === key);
734
+ target?.focus();
735
+ return;
736
+ }
737
+ this.byName.get(key)?.focus();
738
+ }
739
+ edgeOf(event) {
740
+ const origin = event.composedPath()[0];
741
+ return origin instanceof Element ? (origin.getAttribute('data-edge') ?? undefined) : undefined;
742
+ }
743
+ toDocument(event) {
744
+ const box = this.partNamed('viewport')?.getBoundingClientRect();
745
+ const origin = this.origin ?? { x: 0, y: 0 };
746
+ return {
747
+ x: origin.x + (event.clientX - (box?.left ?? 0) - this.at.x) / this.at.zoom,
748
+ y: origin.y + (event.clientY - (box?.top ?? 0) - this.at.y) / this.at.zoom,
749
+ };
750
+ }
751
+ cardAt(point) {
752
+ return this.nodes
753
+ .filter((one) => point.x >= one.x &&
754
+ point.x <= one.x + one.width &&
755
+ point.y >= one.y &&
756
+ point.y <= one.y + one.height)
757
+ .sort((a, b) => a.width * a.height - b.width * b.height)[0];
758
+ }
759
+ nameOf(id) {
760
+ const node = this.byId.get(id);
761
+ return node ? this.textOf(node) : id;
762
+ }
763
+ startLinking(from) {
764
+ this.linking = { from };
765
+ this.said = fillTemplate(this.connectingLabel, { from: this.nameOf(from) });
766
+ }
767
+ arrival(name) {
768
+ const card = this.cardNamed(name);
769
+ return card && name !== this.linking?.from && !this.fixed(card) ? name : undefined;
770
+ }
771
+ get drawing() {
772
+ return this.linking !== undefined && this.linking.pressed === undefined;
773
+ }
774
+ finishLinking(to, toSide) {
775
+ const linking = this.linking;
776
+ if (linking?.edge !== undefined) {
777
+ this.finishReconnect(to, toSide);
778
+ return;
779
+ }
780
+ const arriving = this.arrival(to);
781
+ this.linking = undefined;
782
+ if (!linking || arriving === undefined) {
783
+ this.said = linking ? this.stoppedLabel : '';
784
+ return;
785
+ }
786
+ this.said = fillTemplate(this.connectedLabel, {
787
+ from: this.nameOf(linking.from),
788
+ to: this.nameOf(arriving),
789
+ });
790
+ this.dispatchEvent(new HelixConnectEvent({
791
+ from: linking.from,
792
+ to: arriving,
793
+ ...(linking.side === undefined ? {} : { fromSide: linking.side }),
794
+ ...(toSide === undefined ? {} : { toSide }),
795
+ }));
796
+ }
797
+ startReconnect(key, moving, pointer) {
798
+ const edge = this.edges.find((one) => this.keyOf(one) === key);
799
+ if (!edge) {
800
+ return;
801
+ }
802
+ const stays = moving === 'to' ? edge.fromNode : edge.toNode;
803
+ const byId = new Map(this.nodes.map((one) => [one.id, one]));
804
+ const fromNode = byId.get(edge.fromNode);
805
+ const toNode = byId.get(edge.toNode);
806
+ const drawn = fromNode && toNode ? ends(edge, fromNode, toNode) : undefined;
807
+ this.linking = {
808
+ edge: key,
809
+ from: stays,
810
+ moving,
811
+ side: moving === 'to' ? drawn?.fromSide : drawn?.toSide,
812
+ ...(pointer ? { at: this.toDocument(pointer), pointer: pointer.pointerId } : {}),
813
+ };
814
+ this.said = pointer
815
+ ? ''
816
+ : fillTemplate(this.reconnectingLabel, {
817
+ from: this.nameOf(edge.fromNode),
818
+ to: this.nameOf(edge.toNode),
819
+ });
820
+ }
821
+ finishReconnect(to, toSide) {
822
+ const linking = this.linking;
823
+ this.linking = undefined;
824
+ const edge = linking ? this.edges.find((one) => this.keyOf(one) === linking.edge) : undefined;
825
+ if (!linking || !edge || linking.edge === undefined) {
826
+ return;
827
+ }
828
+ const arriving = to === undefined || to === linking.from ? undefined : to;
829
+ if (arriving === undefined || this.fixed(this.cardNamed(arriving))) {
830
+ this.said = linking.pointer === undefined ? this.stoppedLabel : '';
831
+ if (linking.pointer !== undefined && to === undefined) {
832
+ this.requestRemove(linking.edge);
833
+ }
834
+ return;
835
+ }
836
+ const detail = linking.moving === 'from'
837
+ ? {
838
+ from: arriving,
839
+ name: edge.id,
840
+ to: edge.toNode,
841
+ was: { from: edge.fromNode, to: edge.toNode },
842
+ ...(toSide === undefined ? {} : { fromSide: toSide }),
843
+ ...(linking.side === undefined ? {} : { toSide: linking.side }),
844
+ }
845
+ : {
846
+ from: edge.fromNode,
847
+ name: edge.id,
848
+ to: arriving,
849
+ was: { from: edge.fromNode, to: edge.toNode },
850
+ ...(linking.side === undefined ? {} : { fromSide: linking.side }),
851
+ ...(toSide === undefined ? {} : { toSide }),
852
+ };
853
+ this.said = '';
854
+ if (!this.dispatchEvent(new HelixBeforeReconnectEvent(detail))) {
855
+ return;
856
+ }
857
+ const element = this.edgeElement(linking.edge);
858
+ if (element) {
859
+ element.from = detail.from;
860
+ element.to = detail.to;
861
+ element.fromSide = detail.fromSide;
862
+ element.toSide = detail.toSide;
863
+ this.rekey(linking.edge, this.keyOfElement(element));
864
+ }
865
+ this.dispatchEvent(new HelixReconnectEvent(detail));
866
+ }
867
+ rekey(was, now) {
868
+ if (was === now) {
869
+ return;
870
+ }
871
+ if (this.stop === was) {
872
+ this.stop = now;
873
+ }
874
+ const old = this.publicName(was);
875
+ const names = this.selected
876
+ .split(',')
877
+ .map((one) => one.trim())
878
+ .filter(Boolean);
879
+ if (names.includes(old)) {
880
+ this.selected = names.map((one) => (one === old ? this.publicName(now) : one)).join(',');
881
+ }
882
+ }
883
+ requestRemove(keys) {
884
+ const asked = typeof keys === 'string' ? [keys] : keys;
885
+ const walk = this.walk;
886
+ const edges = this.edges;
887
+ const nodes = asked.filter((one) => !one.startsWith('edge:'));
888
+ const going = new Set(nodes);
889
+ const leaving = edges.filter((edge) => asked.includes(this.keyOf(edge)) || going.has(edge.fromNode) || going.has(edge.toNode));
890
+ const detail = {
891
+ edges: leaving.map((edge) => ({ from: edge.fromNode, name: edge.id, to: edge.toNode })),
892
+ nodes,
893
+ };
894
+ const gone = new Set([...asked, ...leaving.map((edge) => this.keyOf(edge))]);
895
+ const at = Math.min(...asked.map((one) => walk.findIndex((stop) => stop.key === one)));
896
+ const next = walk.slice(at + 1).find((one) => !gone.has(one.key)) ??
897
+ walk
898
+ .slice(0, Math.max(at, 0))
899
+ .reverse()
900
+ .find((one) => !gone.has(one.key));
901
+ this.refocus = { gone: [...gone], next: next?.key };
902
+ this.dispatchEvent(new HelixRemoveEvent(detail));
903
+ }
904
+ clicked(key, event) {
905
+ if (event.shiftKey || event.metaKey || event.ctrlKey) {
906
+ this.toggle(key);
907
+ return;
908
+ }
909
+ this.choose(key);
910
+ }
911
+ within(box) {
912
+ const left = Math.min(box.from.x, box.to.x);
913
+ const right = Math.max(box.from.x, box.to.x);
914
+ const top = Math.min(box.from.y, box.to.y);
915
+ const bottom = Math.max(box.from.y, box.to.y);
916
+ const drawn = { height: bottom - top, width: right - left, x: left, y: top };
917
+ return this.nodes
918
+ .filter((one) => one.type === 'group'
919
+ ? inside(one, drawn)
920
+ : one.x < right && one.x + one.width > left && one.y < bottom && one.y + one.height > top)
921
+ .map((one) => one.id);
922
+ }
923
+ geometryOf(edge, origin) {
924
+ const from = this.byId.get(edge.fromNode);
925
+ const to = this.byId.get(edge.toNode);
926
+ if (!from || !to) {
927
+ return undefined;
928
+ }
929
+ const drawn = connector(edge, onPlane(from, origin), onPlane(to, origin), edge.path ?? this.path ?? 'curve', heads(edge));
930
+ return { ...drawn, from, to };
931
+ }
932
+ cardOf(event) {
933
+ return event
934
+ .composedPath()
935
+ .find((one) => one instanceof HelixHeadlessCanvasNode);
936
+ }
937
+ carried(card) {
938
+ if (!card.group) {
939
+ return [card];
940
+ }
941
+ const box = { height: card.height, width: card.width, x: card.x, y: card.y };
942
+ return [
943
+ card,
944
+ ...this.cards.filter((one) => one !== card &&
945
+ !one.readOnly &&
946
+ inside({ height: one.height, width: one.width, x: one.x, y: one.y }, box)),
947
+ ];
948
+ }
949
+ carriedAll(card) {
950
+ const picked = this.pickedCards;
951
+ const roots = picked.includes(card) ? picked.filter((one) => !this.fixed(one)) : [card];
952
+ return [...new Set(roots.flatMap((one) => this.carried(one)))];
953
+ }
954
+ resized(start, corner, across, down) {
955
+ const left = corner.endsWith('left');
956
+ const top = corner.startsWith('top');
957
+ const width = Math.max(MIN_WIDTH, Math.round(start.width + (left ? -across : across)));
958
+ const height = Math.max(MIN_HEIGHT, Math.round(start.height + (top ? -down : down)));
959
+ return {
960
+ height,
961
+ width,
962
+ x: left ? start.x + start.width - width : start.x,
963
+ y: top ? start.y + start.height - height : start.y,
964
+ };
965
+ }
966
+ fixed(card) {
967
+ return this.readOnly || card === undefined || card.readOnly;
968
+ }
969
+ edgeElement(key) {
970
+ return this.edgeElements.find((child) => this.keyOfElement(child) === key);
971
+ }
972
+ get edgeElements() {
973
+ return this.knownChildren.filter((child) => child instanceof HelixHeadlessCanvasEdge);
974
+ }
975
+ keyOfElement(element) {
976
+ const base = keyBase({ fromNode: element.from, id: element.name, toNode: element.to });
977
+ if (element.name) {
978
+ return base;
979
+ }
980
+ const twins = this.edgeElements.filter((one) => !one.name && one.from === element.from && one.to === element.to);
981
+ const at = twins.indexOf(element);
982
+ return at > 0 ? `${base}#${at + 1}` : base;
983
+ }
984
+ keyOf(edge) {
985
+ return this.edgeKeys.get(edge) ?? keyBase(edge);
986
+ }
987
+ publicName(key) {
988
+ return key === undefined ? '' : key.startsWith('edge:') ? key.slice(5) : key;
989
+ }
990
+ keyNamed(name) {
991
+ if (name === '') {
992
+ return undefined;
993
+ }
994
+ const stops = this.remember('stops', () => new Set(this.walk.map((one) => one.key)));
995
+ return stops.has(name) ? name : stops.has(`edge:${name}`) ? `edge:${name}` : undefined;
996
+ }
997
+ get picked() {
998
+ return this.remember('picked', () => this.selected
999
+ .split(',')
1000
+ .map((one) => this.keyNamed(one.trim()))
1001
+ .filter((one) => one !== undefined));
1002
+ }
1003
+ choose(keys) {
1004
+ const list = keys === undefined ? [] : typeof keys === 'string' ? [keys] : [...new Set(keys)];
1005
+ const value = list.map((one) => this.publicName(one)).join(',');
1006
+ if (value === this.selected) {
1007
+ return;
1008
+ }
1009
+ if (this.dispatchEvent(new HelixSelectEvent({ value }))) {
1010
+ this.selected = value;
1011
+ }
1012
+ }
1013
+ toggle(key) {
1014
+ const picked = this.picked;
1015
+ this.choose(picked.includes(key) ? picked.filter((one) => one !== key) : [...picked, key]);
1016
+ }
1017
+ get pickedCards() {
1018
+ const picked = new Set(this.picked);
1019
+ return this.cards.filter((one) => picked.has(one.name));
1020
+ }
1021
+ cardNamed(name) {
1022
+ return name === undefined ? undefined : this.byName.get(name);
1023
+ }
1024
+ requestMove(detail) {
1025
+ if (!this.dispatchEvent(new HelixBeforeMoveEvent(detail))) {
1026
+ return false;
1027
+ }
1028
+ for (const { height, name, width, x, y } of detail.nodes) {
1029
+ const card = this.byName.get(name);
1030
+ if (card) {
1031
+ card.x = x;
1032
+ card.y = y;
1033
+ if (width !== undefined && height !== undefined) {
1034
+ card.width = width;
1035
+ card.height = height;
1036
+ }
1037
+ }
1038
+ }
1039
+ this.requestUpdate();
1040
+ return true;
1041
+ }
1042
+ picksUp(gesture, event, from) {
1043
+ if (gesture.held || this.putting || event.type === 'pointercancel') {
1044
+ return false;
1045
+ }
1046
+ if (Math.hypot(event.clientX - from.x, event.clientY - from.y) >= SLOP) {
1047
+ return false;
1048
+ }
1049
+ this.toggleState('holding', true);
1050
+ this.holding = true;
1051
+ document.addEventListener('keydown', this.handleHeldKey, true);
1052
+ document.addEventListener('pointerdown', this.handleHeldOutside, true);
1053
+ return true;
1054
+ }
1055
+ get held() {
1056
+ return Boolean(this.sizing?.held || this.linking?.held || this.carrying?.held);
1057
+ }
1058
+ putDown(event) {
1059
+ const pointer = event.pointerId;
1060
+ if (this.sizing) {
1061
+ this.sizing = { ...this.sizing, pointer };
1062
+ }
1063
+ if (this.linking) {
1064
+ this.linking = { ...this.linking, pointer };
1065
+ }
1066
+ if (this.carrying) {
1067
+ this.carrying = { ...this.carrying, pointer };
1068
+ }
1069
+ this.handlePointerMove(event);
1070
+ this.putting = true;
1071
+ try {
1072
+ this.handlePointerUp(event);
1073
+ }
1074
+ finally {
1075
+ this.putting = false;
1076
+ }
1077
+ this.putAt = event.timeStamp;
1078
+ this.letGo();
1079
+ }
1080
+ putBack() {
1081
+ const sizing = this.sizing;
1082
+ if (sizing?.held) {
1083
+ this.sizing = undefined;
1084
+ if (sizing.asked) {
1085
+ this.requestMove({ nodes: [{ name: sizing.card.name, ...sizing.start }] });
1086
+ }
1087
+ }
1088
+ const carry = this.carrying;
1089
+ if (carry?.held) {
1090
+ this.carrying = undefined;
1091
+ if (carry.asked) {
1092
+ this.requestMove({
1093
+ nodes: carry.cards.map(({ card, x, y }) => ({ name: card.name, x, y })),
1094
+ });
1095
+ }
1096
+ }
1097
+ if (this.linking?.held) {
1098
+ this.linking = undefined;
1099
+ this.said = this.stoppedLabel;
1100
+ }
1101
+ this.letGo();
1102
+ }
1103
+ letGo() {
1104
+ this.holding = false;
1105
+ this.toggleState('holding', false);
1106
+ document.removeEventListener('keydown', this.handleHeldKey, true);
1107
+ document.removeEventListener('pointerdown', this.handleHeldOutside, true);
1108
+ }
1109
+ reveal(id) {
1110
+ const node = this.byId.get(id);
1111
+ const origin = this.origin;
1112
+ if (!node || !origin) {
1113
+ return;
1114
+ }
1115
+ const free = this.free;
1116
+ const left = this.at.x + (node.x - origin.x) * this.at.zoom;
1117
+ const top = this.at.y + (node.y - origin.y) * this.at.zoom;
1118
+ const seen = left >= free.x &&
1119
+ top >= free.y &&
1120
+ left + node.width * this.at.zoom <= free.x + free.width &&
1121
+ top + node.height * this.at.zoom <= free.y + free.height;
1122
+ if (!seen) {
1123
+ this.show(id);
1124
+ }
1125
+ }
1126
+ editStop(key) {
1127
+ if (this.readOnly) {
1128
+ return;
1129
+ }
1130
+ if (key.startsWith('edge:')) {
1131
+ const edge = this.edgeElement(key);
1132
+ if (edge && !edge.readOnly) {
1133
+ this.editing = { key, target: 'edge', was: edge.label ?? '' };
1134
+ }
1135
+ return;
1136
+ }
1137
+ const card = this.cardNamed(key);
1138
+ if (!card || this.fixed(card)) {
1139
+ return;
1140
+ }
1141
+ if (card.group) {
1142
+ this.editing = { key, target: 'group', was: card.label };
1143
+ return;
1144
+ }
1145
+ this.dispatchEvent(new HelixCanvasEditEvent({ name: card.name }));
1146
+ }
1147
+ finishEditing(field, keep) {
1148
+ const editing = this.editing;
1149
+ if (!editing) {
1150
+ return;
1151
+ }
1152
+ this.editing = undefined;
1153
+ const label = (field?.value ?? '').trim();
1154
+ const back = () => {
1155
+ void this.updateComplete.then(() => this.focusStop(editing.key));
1156
+ };
1157
+ if (!keep || label === editing.was) {
1158
+ back();
1159
+ return;
1160
+ }
1161
+ const edge = editing.target === 'edge' ? this.edgeElement(editing.key) : undefined;
1162
+ const card = editing.target === 'group' ? this.cardNamed(editing.key) : undefined;
1163
+ const detail = {
1164
+ label,
1165
+ name: edge ? edge.name : (card?.name ?? ''),
1166
+ target: editing.target,
1167
+ was: editing.was,
1168
+ ...(edge ? { from: edge.from, to: edge.to } : {}),
1169
+ };
1170
+ if (this.dispatchEvent(new HelixBeforeRelabelEvent(detail))) {
1171
+ if (edge) {
1172
+ edge.label = label || undefined;
1173
+ }
1174
+ else if (card) {
1175
+ card.label = label;
1176
+ }
1177
+ this.dispatchEvent(new HelixRelabelEvent(detail));
1178
+ }
1179
+ back();
1180
+ }
1181
+ renderEditor(origin) {
1182
+ const editing = this.editing;
1183
+ if (!editing) {
1184
+ return nothing;
1185
+ }
1186
+ let at;
1187
+ let named = '';
1188
+ if (editing.target === 'edge') {
1189
+ const edge = this.edges.find((one) => this.keyOf(one) === editing.key);
1190
+ const drawn = edge ? this.geometryOf(edge, origin) : undefined;
1191
+ if (drawn) {
1192
+ at = drawn.mid;
1193
+ named = fillTemplate(this.connectionLabel, {
1194
+ from: this.textOf(drawn.from),
1195
+ to: this.textOf(drawn.to),
1196
+ });
1197
+ }
1198
+ }
1199
+ else {
1200
+ const group = this.byId.get(editing.key);
1201
+ if (group) {
1202
+ at = { x: group.x - origin.x, y: group.y - origin.y };
1203
+ named = this.textOf(group);
1204
+ }
1205
+ }
1206
+ if (!at) {
1207
+ return nothing;
1208
+ }
1209
+ return html `<input
1210
+ part="label-editor"
1211
+ aria-label=${fillTemplate(this.labelEditorLabel, { name: named })}
1212
+ data-target=${editing.target}
1213
+ style=${`--_canvas-editor-x: ${at.x}px; --_canvas-editor-y: ${at.y}px`}
1214
+ type="text"
1215
+ .value=${editing.was}
1216
+ @blur=${this.handleEditorBlur}
1217
+ @keydown=${this.handleEditorKeydown}
1218
+ />`;
1219
+ }
1220
+ renderMarquee(origin) {
1221
+ const box = this.marquee;
1222
+ if (!box) {
1223
+ return nothing;
1224
+ }
1225
+ const left = Math.min(box.from.x, box.to.x) - origin.x;
1226
+ const top = Math.min(box.from.y, box.to.y) - origin.y;
1227
+ return html `<div
1228
+ part="marquee"
1229
+ aria-hidden="true"
1230
+ style=${`--_canvas-marquee-x: ${left}px; --_canvas-marquee-y: ${top}px; --_canvas-marquee-inline-size: ${Math.abs(box.to.x - box.from.x)}px; --_canvas-marquee-block-size: ${Math.abs(box.to.y - box.from.y)}px`}
1231
+ ></div>`;
1232
+ }
1233
+ zoomAbout(by, about) {
1234
+ const from = this.at.zoom;
1235
+ const to = Math.min(Math.max(from * by, ZOOM_MIN), ZOOM_MAX);
1236
+ this.at = {
1237
+ x: about.x - ((about.x - this.at.x) * to) / from,
1238
+ y: about.y - ((about.y - this.at.y) * to) / from,
1239
+ zoom: to,
1240
+ };
1241
+ }
1242
+ linksOf(node) {
1243
+ return this.remember('links', () => this.readLinks()).get(node.id) ?? '';
1244
+ }
1245
+ readLinks() {
1246
+ const named = (id) => {
1247
+ const other = this.byId.get(id);
1248
+ return other ? this.textOf(other) : id;
1249
+ };
1250
+ const leads = new Map();
1251
+ const from = new Map();
1252
+ for (const edge of this.edges) {
1253
+ leads.set(edge.fromNode, [...(leads.get(edge.fromNode) ?? []), named(edge.toNode)]);
1254
+ from.set(edge.toNode, [...(from.get(edge.toNode) ?? []), named(edge.fromNode)]);
1255
+ }
1256
+ const lang = this.closest('[lang]')?.getAttribute('lang') || undefined;
1257
+ const list = new Intl.ListFormat(lang, { type: 'conjunction' });
1258
+ return new Map(this.nodes.map((one) => {
1259
+ const out = leads.get(one.id) ?? [];
1260
+ const into = from.get(one.id) ?? [];
1261
+ return [
1262
+ one.id,
1263
+ [
1264
+ out.length > 0 ? fillTemplate(this.leadsToLabel, { cards: list.format(out) }) : '',
1265
+ into.length > 0 ? fillTemplate(this.comesFromLabel, { cards: list.format(into) }) : '',
1266
+ ]
1267
+ .filter(Boolean)
1268
+ .join('; '),
1269
+ ];
1270
+ }));
1271
+ }
1272
+ textOf(node) {
1273
+ return node.label ?? node.text ?? node.file ?? node.url ?? node.id;
1274
+ }
1275
+ get free() {
1276
+ const { height, width } = this.box;
1277
+ const whole = { height, width, x: 0, y: 0 };
1278
+ const viewport = this.renderRoot?.querySelector('[part="viewport"]');
1279
+ const controls = this.controlsElement;
1280
+ if (!viewport || !controls) {
1281
+ return whole;
1282
+ }
1283
+ const box = viewport.getBoundingClientRect();
1284
+ const cluster = controls.getBoundingClientRect();
1285
+ if (cluster.width === 0 || cluster.height === 0) {
1286
+ return whole;
1287
+ }
1288
+ const atTop = cluster.top - box.top <= box.bottom - cluster.bottom;
1289
+ const atLeft = cluster.left - box.left <= box.right - cluster.right;
1290
+ const band = atTop
1291
+ ? cluster.bottom - box.top + (cluster.top - box.top)
1292
+ : box.bottom - cluster.top + (box.bottom - cluster.bottom);
1293
+ const column = atLeft
1294
+ ? cluster.right - box.left + (cluster.left - box.left)
1295
+ : box.right - cluster.left + (box.right - cluster.right);
1296
+ if (band / height <= column / width && band <= height / 2) {
1297
+ return { height: height - band, width, x: 0, y: atTop ? band : 0 };
1298
+ }
1299
+ if (column <= width / 2) {
1300
+ return { height, width: width - column, x: atLeft ? column : 0, y: 0 };
1301
+ }
1302
+ return whole;
1303
+ }
1304
+ get view() {
1305
+ const origin = this.origin ?? { x: 0, y: 0 };
1306
+ const { height, width } = this.box;
1307
+ return {
1308
+ x: origin.x + (width / 2 - this.at.x) / this.at.zoom,
1309
+ y: origin.y + (height / 2 - this.at.y) / this.at.zoom,
1310
+ zoom: this.at.zoom,
1311
+ };
1312
+ }
1313
+ set view(next) {
1314
+ const origin = this.origin ?? { x: 0, y: 0 };
1315
+ const { height, width } = this.box;
1316
+ const zoom = Math.min(Math.max(next.zoom, ZOOM_MIN), ZOOM_MAX);
1317
+ this.touched = true;
1318
+ this.direct = false;
1319
+ this.at = {
1320
+ x: width / 2 - (next.x - origin.x) * zoom,
1321
+ y: height / 2 - (next.y - origin.y) * zoom,
1322
+ zoom,
1323
+ };
1324
+ }
1325
+ toJsonCanvas() {
1326
+ const taken = new Set(this.edges.map((edge) => edge.id).filter(Boolean));
1327
+ const edges = this.edges.map((edge) => {
1328
+ if (edge.id) {
1329
+ return { ...edge };
1330
+ }
1331
+ const base = this.publicName(this.keyOf(edge));
1332
+ let id = base;
1333
+ for (let count = 2; taken.has(id); count++) {
1334
+ id = `${base}-${count}`;
1335
+ }
1336
+ taken.add(id);
1337
+ return { ...edge, id };
1338
+ });
1339
+ return { edges, nodes: this.nodes.map((node) => ({ ...node })) };
1340
+ }
1341
+ fit() {
1342
+ this.fitInto(false);
1343
+ }
1344
+ fitInto(direct) {
1345
+ const board = extent(this.nodes);
1346
+ const free = this.free;
1347
+ if (board.width === 0 || free.width === 0 || free.height === 0) {
1348
+ return;
1349
+ }
1350
+ const whole = { height: board.height + MARGIN * 2, width: board.width + MARGIN * 2 };
1351
+ const zoom = Math.min(1, free.width / whole.width, free.height / whole.height);
1352
+ if (!Number.isFinite(zoom) || zoom <= 0) {
1353
+ return;
1354
+ }
1355
+ const next = {
1356
+ x: free.x + (free.width - whole.width * zoom) / 2,
1357
+ y: free.y + (free.height - whole.height * zoom) / 2,
1358
+ zoom,
1359
+ };
1360
+ if (next.x === this.at.x && next.y === this.at.y && next.zoom === this.at.zoom) {
1361
+ return;
1362
+ }
1363
+ this.direct = direct;
1364
+ this.at = next;
1365
+ }
1366
+ firstUpdated(changed) {
1367
+ super.firstUpdated(changed);
1368
+ this.finishHydration();
1369
+ }
1370
+ updated(changed) {
1371
+ super.updated(changed);
1372
+ if (this.holding && !this.held) {
1373
+ this.letGo();
1374
+ }
1375
+ if (changed.has('editing') && this.editing) {
1376
+ const field = this.partNamed('label-editor');
1377
+ field?.focus();
1378
+ field?.select();
1379
+ }
1380
+ this.place({ x: extent(this.nodes).x - MARGIN, y: extent(this.nodes).y - MARGIN });
1381
+ const view = this.view;
1382
+ const was = this.reported;
1383
+ if (!was || was.x !== view.x || was.y !== view.y || was.zoom !== view.zoom) {
1384
+ this.reported = view;
1385
+ if (was) {
1386
+ this.dispatchEvent(new HelixCanvasViewEvent(view));
1387
+ }
1388
+ }
1389
+ const refocus = this.refocus;
1390
+ if (refocus) {
1391
+ const keys = new Set(this.walk.map((one) => one.key));
1392
+ if (refocus.gone.every((one) => !keys.has(one))) {
1393
+ this.refocus = undefined;
1394
+ this.focusStop(refocus.next !== undefined && keys.has(refocus.next) ? refocus.next : this.walk[0]?.key);
1395
+ }
1396
+ }
1397
+ this.watchBox();
1398
+ if (!this.touched) {
1399
+ this.fitInto(true);
1400
+ }
1401
+ }
1402
+ watchWords() {
1403
+ if (typeof MutationObserver === 'undefined') {
1404
+ return;
1405
+ }
1406
+ this.words ??= new MutationObserver((records) => {
1407
+ const seen = new Set();
1408
+ const heard = records.filter((record) => {
1409
+ if (record.type !== 'attributes') {
1410
+ return true;
1411
+ }
1412
+ if (seen.has(record.target)) {
1413
+ return false;
1414
+ }
1415
+ seen.add(record.target);
1416
+ return true;
1417
+ });
1418
+ if (heard.some((record) => this.redraws(record))) {
1419
+ this.requestUpdate();
1420
+ }
1421
+ });
1422
+ this.words.observe(this, {
1423
+ attributeFilter: ['style'],
1424
+ characterData: true,
1425
+ childList: true,
1426
+ subtree: true,
1427
+ });
1428
+ }
1429
+ redraws(record) {
1430
+ if (record.type !== 'attributes') {
1431
+ return true;
1432
+ }
1433
+ const target = record.target;
1434
+ if (target instanceof HelixHeadlessCanvasNode &&
1435
+ this.placed.has(target) &&
1436
+ custom(target, '--_canvas-card-x') === '') {
1437
+ this.placed.delete(target);
1438
+ return true;
1439
+ }
1440
+ const name = target instanceof HelixHeadlessCanvasNode
1441
+ ? '--canvas-node-color'
1442
+ : target instanceof HelixHeadlessCanvasEdge
1443
+ ? '--canvas-edge-color'
1444
+ : undefined;
1445
+ return (name !== undefined &&
1446
+ custom(target, name) !== (this.painted.get(target) ?? ''));
1447
+ }
1448
+ watchBox() {
1449
+ const viewport = this.renderRoot?.querySelector('[part="viewport"]');
1450
+ if (typeof ResizeObserver === 'undefined' || !viewport || viewport === this.watched) {
1451
+ return;
1452
+ }
1453
+ this.watched = viewport;
1454
+ this.sizes ??= new ResizeObserver(() => {
1455
+ const box = this.box;
1456
+ const was = this.measured;
1457
+ this.measured = box;
1458
+ if (!this.touched) {
1459
+ this.fitInto(true);
1460
+ return;
1461
+ }
1462
+ if (was && (was.width !== box.width || was.height !== box.height)) {
1463
+ this.direct = true;
1464
+ this.at = {
1465
+ ...this.at,
1466
+ x: this.at.x + (box.width - was.width) / 2,
1467
+ y: this.at.y + (box.height - was.height) / 2,
1468
+ };
1469
+ }
1470
+ });
1471
+ this.sizes.disconnect();
1472
+ this.sizes.observe(viewport);
1473
+ }
1474
+ connectedCallback() {
1475
+ super.connectedCallback();
1476
+ this.watchBox();
1477
+ this.watchWords();
1478
+ this.addEventListener('dblclick', this.handleDoubleClick);
1479
+ this.addEventListener('focusin', this.handleFocusIn);
1480
+ this.addEventListener('focusout', this.handleFocusOut);
1481
+ this.addEventListener('keydown', this.handleStopKeydown);
1482
+ this.addEventListener('pointercancel', this.handlePointerUp);
1483
+ this.addEventListener('pointerdown', this.handlePointerDown);
1484
+ this.addEventListener('pointermove', this.handlePointerMove);
1485
+ this.addEventListener('pointerup', this.handlePointerUp);
1486
+ }
1487
+ disconnectedCallback() {
1488
+ super.disconnectedCallback();
1489
+ if (this.held) {
1490
+ this.putBack();
1491
+ }
1492
+ this.sizes?.disconnect();
1493
+ this.watched = undefined;
1494
+ this.words?.disconnect();
1495
+ this.removeEventListener('dblclick', this.handleDoubleClick);
1496
+ this.removeEventListener('focusin', this.handleFocusIn);
1497
+ this.removeEventListener('focusout', this.handleFocusOut);
1498
+ this.removeEventListener('keydown', this.handleStopKeydown);
1499
+ this.removeEventListener('pointercancel', this.handlePointerUp);
1500
+ this.removeEventListener('pointerdown', this.handlePointerDown);
1501
+ this.removeEventListener('pointermove', this.handlePointerMove);
1502
+ this.removeEventListener('pointerup', this.handlePointerUp);
1503
+ }
1504
+ willUpdate(changed) {
1505
+ super.willUpdate(changed);
1506
+ const drawn = extent(this.nodes);
1507
+ const origin = { x: drawn.x - MARGIN, y: drawn.y - MARGIN };
1508
+ if (this.origin && (origin.x !== this.origin.x || origin.y !== this.origin.y)) {
1509
+ this.at = {
1510
+ ...this.at,
1511
+ x: this.at.x + (origin.x - this.origin.x) * this.at.zoom,
1512
+ y: this.at.y + (origin.y - this.origin.y) * this.at.zoom,
1513
+ };
1514
+ }
1515
+ this.origin = origin;
1516
+ hostAria(this.internals, { label: this.label });
1517
+ this.toggleState('connecting', this.drawing);
1518
+ this.toggleState('direct', this.direct);
1519
+ this.toggleState('pressing', this.pressing);
1520
+ this.toggleState('empty', this.nodes.length === 0);
1521
+ this.toggleState('fullscreen', this.filled);
1522
+ this.toggleState('idle', this.resting);
1523
+ }
1524
+ place(origin) {
1525
+ const all = this.nodes;
1526
+ const picked = new Set([...this.picked, ...(this.marquee ? this.within(this.marquee) : [])]);
1527
+ const alone = this.picked.length === 1 ? this.picked[0] : undefined;
1528
+ const stop = this.stop !== undefined && this.keyNamed(this.stop) === this.stop
1529
+ ? this.stop
1530
+ : reading(all, this.edges, this.order ?? 'spatial')[0]?.id;
1531
+ const groups = this.groups;
1532
+ for (const card of this.cards) {
1533
+ const group = card.group
1534
+ ? undefined
1535
+ : groupOf({
1536
+ height: card.height,
1537
+ id: card.name,
1538
+ type: 'text',
1539
+ width: card.width,
1540
+ x: card.x,
1541
+ y: card.y,
1542
+ }, groups);
1543
+ const x = finite(card.x) - origin.x;
1544
+ const y = finite(card.y) - origin.y;
1545
+ const placed = `${x} ${y} ${finite(card.width)} ${finite(card.height)}`;
1546
+ if (this.placed.get(card) !== placed) {
1547
+ this.placed.set(card, placed);
1548
+ card.style.setProperty('--_canvas-card-x', `${x}px`);
1549
+ card.style.setProperty('--_canvas-card-y', `${y}px`);
1550
+ size(card, '--_canvas-card-inline-size', finite(card.width));
1551
+ size(card, '--_canvas-card-block-size', finite(card.height));
1552
+ }
1553
+ if (group) {
1554
+ card.setAttribute('data-group', group.id);
1555
+ }
1556
+ else {
1557
+ card.removeAttribute('data-group');
1558
+ }
1559
+ const node = this.byId.get(card.name);
1560
+ card.links = node ? this.linksOf(node) : '';
1561
+ card.current = card.name === stop;
1562
+ card.selected = picked.has(card.name);
1563
+ card.resizable = alone === card.name && !this.fixed(card);
1564
+ card.movable = picked.has(card.name) && !this.fixed(card);
1565
+ card.connectable = !this.fixed(card);
1566
+ card.candidate = this.drawing && this.arrival(card.name) !== undefined;
1567
+ card.target = this.linking?.to === card.name;
1568
+ }
1569
+ }
1570
+ renderEnds(origin) {
1571
+ if (this.readOnly) {
1572
+ return nothing;
1573
+ }
1574
+ const picked = new Set(this.picked);
1575
+ const byId = new Map(this.nodes.map((one) => [one.id, one]));
1576
+ return this.edges.map((edge) => {
1577
+ const key = this.keyOf(edge);
1578
+ const from = byId.get(edge.fromNode);
1579
+ const to = byId.get(edge.toNode);
1580
+ const lifted = this.linking?.edge === key && !this.linking.pressed;
1581
+ if (!picked.has(key) || lifted || this.edgeElement(key)?.readOnly || !from || !to) {
1582
+ return nothing;
1583
+ }
1584
+ const { end, start } = ends(edge, onPlane(from, origin), onPlane(to, origin));
1585
+ const at = (point) => `--_canvas-end-x: ${point.x}px; --_canvas-end-y: ${point.y}px`;
1586
+ return html `<span part="edge-end" aria-hidden="true" data-edge=${key} data-end="from" style=${at(start)}></span
1587
+ ><span part="edge-end" aria-hidden="true" data-edge=${key} data-end="to" style=${at(end)}></span>`;
1588
+ });
1589
+ }
1590
+ renderEdges(origin, box) {
1591
+ const elements = new Map(this.edgeElements.map((child) => [this.keyOfElement(child), child]));
1592
+ const stop = this.stop !== undefined && this.keyNamed(this.stop) === this.stop ? this.stop : undefined;
1593
+ const pickedKeys = new Set(this.picked);
1594
+ return html `<svg
1595
+ part="edges"
1596
+ role="none"
1597
+ @focusin=${this.handleFocusIn}
1598
+ viewBox=${`0 0 ${box.width} ${box.height}`}
1599
+ width=${box.width}
1600
+ height=${box.height}
1601
+ >
1602
+ <defs>
1603
+ <marker
1604
+ id=${ARROW}
1605
+ markerHeight="8"
1606
+ markerUnits="userSpaceOnUse"
1607
+ markerWidth="8"
1608
+ orient="auto-start-reverse"
1609
+ refX="0"
1610
+ refY="4"
1611
+ >
1612
+ <path part="arrow" d="M0 0 L8 4 L0 8 z" />
1613
+ </marker>
1614
+ </defs>
1615
+ ${this.edges.map((edge) => {
1616
+ const key = this.keyOf(edge);
1617
+ const element = elements.get(key);
1618
+ const drawn = this.geometryOf(edge, origin);
1619
+ if (!drawn || (this.linking?.edge === key && !this.linking.pressed)) {
1620
+ return nothing;
1621
+ }
1622
+ const { from, to } = drawn;
1623
+ const ends = heads(edge);
1624
+ return svg `<path
1625
+ part="edge-target"
1626
+ aria-description=${edge.label || nothing}
1627
+ aria-label=${fillTemplate(this.connectionLabel, { from: this.textOf(from), to: this.textOf(to) })}
1628
+ d=${drawn.d}
1629
+ data-edge=${key}
1630
+ fill="none"
1631
+ aria-keyshortcuts="Enter F2 C Delete"
1632
+ role="button"
1633
+ tabindex=${key === stop ? '0' : '-1'}
1634
+ />
1635
+ <path
1636
+ part="edge"
1637
+ aria-hidden="true"
1638
+ d=${drawn.d}
1639
+ data-selected=${pickedKeys.has(key) ? '' : nothing}
1640
+ data-tone=${element?.tone ?? nothing}
1641
+ data-variant=${element?.variant ?? nothing}
1642
+ fill="none"
1643
+ marker-end=${ends.end ? `url(#${ARROW})` : nothing}
1644
+ marker-start=${ends.start ? `url(#${ARROW})` : nothing}
1645
+ style=${custom(element, '--canvas-edge-color') ? `--_canvas-edge-color: ${custom(element, '--canvas-edge-color')}` : nothing}
1646
+ />
1647
+ `;
1648
+ })}
1649
+ ${this.renderDraft(origin)}
1650
+ </svg>`;
1651
+ }
1652
+ renderLabels(origin) {
1653
+ return this.edges.map((edge) => {
1654
+ const lifted = this.linking?.edge === this.keyOf(edge) && !this.linking.pressed;
1655
+ const drawn = edge.label && !lifted ? this.geometryOf(edge, origin) : undefined;
1656
+ if (!drawn) {
1657
+ return nothing;
1658
+ }
1659
+ const { mid } = drawn;
1660
+ return html `<span
1661
+ part="label"
1662
+ aria-hidden="true"
1663
+ style=${`--_canvas-label-x: ${mid.x}px; --_canvas-label-y: ${mid.y}px`}
1664
+ >${edge.label}</span
1665
+ >`;
1666
+ });
1667
+ }
1668
+ renderDraft(origin) {
1669
+ const linking = this.linking;
1670
+ const byId = new Map(this.nodes.map((one) => [one.id, one]));
1671
+ const anchor = linking ? byId.get(linking.from) : undefined;
1672
+ if (!linking || !anchor || linking.pressed) {
1673
+ return nothing;
1674
+ }
1675
+ const shifted = (node) => onPlane(node, origin);
1676
+ const moved = linking.edge
1677
+ ? this.edges.find((one) => this.keyOf(one) === linking.edge)
1678
+ : undefined;
1679
+ const path = moved?.path ?? this.path ?? 'curve';
1680
+ const drawnHeads = moved ? heads(moved) : { end: true, start: false };
1681
+ const target = linking.to === undefined ? undefined : byId.get(linking.to);
1682
+ if (target) {
1683
+ const reversed = linking.moving === 'from';
1684
+ const [from, to] = reversed ? [target, anchor] : [anchor, target];
1685
+ const drawn = connector({
1686
+ fromNode: from.id,
1687
+ fromSide: reversed ? linking.toSide : linking.side,
1688
+ id: '',
1689
+ toNode: to.id,
1690
+ toSide: reversed ? linking.side : linking.toSide,
1691
+ }, shifted(from), shifted(to), path, drawnHeads);
1692
+ return svg `<path
1693
+ part="draft"
1694
+ aria-hidden="true"
1695
+ d=${drawn.d}
1696
+ fill="none"
1697
+ marker-end=${drawnHeads.end ? `url(#${ARROW})` : nothing}
1698
+ marker-start=${drawnHeads.start ? `url(#${ARROW})` : nothing}
1699
+ />`;
1700
+ }
1701
+ if (linking.at) {
1702
+ const end = { x: linking.at.x - origin.x, y: linking.at.y - origin.y };
1703
+ const start = port(shifted(anchor), linking.side ?? sideNear(anchor, linking.at));
1704
+ return svg `<path part="draft" aria-hidden="true" d=${`M ${start.x} ${start.y} L ${end.x} ${end.y}`} fill="none" />`;
1705
+ }
1706
+ return nothing;
1707
+ }
1708
+ render() {
1709
+ const nodes = this.nodes;
1710
+ const drawn = extent(nodes);
1711
+ const box = { height: drawn.height + MARGIN * 2, width: drawn.width + MARGIN * 2 };
1712
+ const origin = { x: drawn.x - MARGIN, y: drawn.y - MARGIN };
1713
+ return html `<div
1714
+ part="region"
1715
+ role="group"
1716
+ aria-label=${this.label || 'Canvas'}
1717
+ aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight Plus Minus"
1718
+ tabindex="0"
1719
+ @focusin=${this.idle.stir}
1720
+ @keydown=${this.handleKeydown}
1721
+ @pointerleave=${this.idle.leave}
1722
+ @pointermove=${this.idle.stir}
1723
+ >
1724
+ <div part="header">
1725
+ <slot name="header"></slot>
1726
+ </div>
1727
+ <div
1728
+ part="viewport"
1729
+ style=${`--_canvas-x: ${this.at.x}px; --_canvas-y: ${this.at.y}px; --_canvas-zoom: ${this.at.zoom}; --_canvas-rule-x: ${this.at.x - origin.x * this.at.zoom}px; --_canvas-rule-y: ${this.at.y - origin.y * this.at.zoom}px; --_canvas-rule-zoom: ${ruling(this.at.zoom)}`}
1730
+ @wheel=${this.handleWheel}
1731
+ >
1732
+ <!-- **One shape, whatever the board holds**, which is what hydration
1733
+ needs: the server cannot see the cards, so a template that drew
1734
+ something else while there were none rendered one thing on the
1735
+ server and another on the client, and Lit reported a value mismatch
1736
+ at the part where the two disagreed. Emptiness is a state on the
1737
+ host instead, which is a fact about the board rather than a
1738
+ different board. -->
1739
+ <!-- Where the board sits is written on the viewport rather than the
1740
+ plane, so the box can read it too: a ruling drawn behind the plane
1741
+ has to move with it, and the plane only covers the cards. The
1742
+ ruling's own offset is where the document's zero lands, so a line
1743
+ falls on every multiple of its spacing in the document's units and
1744
+ a card placed on one sits on a line. -->
1745
+ <div
1746
+ part="board"
1747
+ style=${`--_canvas-plane-inline-size: ${box.width}px; --_canvas-plane-block-size: ${box.height}px`}
1748
+ >
1749
+ ${this.renderEdges(origin, box)} ${this.renderLabels(origin)}
1750
+ <slot @slotchange=${this.handleSlotChange}></slot>
1751
+ ${this.renderEnds(origin)}
1752
+ ${this.renderEditor(origin)}
1753
+ ${this.renderMarquee(origin)}
1754
+ </div>
1755
+ <slot name="empty"></slot>
1756
+ </div>
1757
+ ${this.renderControls()}
1758
+ <span part="live" role="status">${this.said}</span>
1759
+ <div part="footer">
1760
+ <slot name="footer"></slot>
1761
+ </div>
1762
+ </div>`;
1763
+ }
1764
+ };
1765
+ __decorate([
1766
+ property({ attribute: 'comes-from-label' })
1767
+ ], HelixHeadlessCanvas.prototype, "comesFromLabel", void 0);
1768
+ __decorate([
1769
+ property({ attribute: 'connected-label' })
1770
+ ], HelixHeadlessCanvas.prototype, "connectedLabel", void 0);
1771
+ __decorate([
1772
+ property({ attribute: 'connecting-label' })
1773
+ ], HelixHeadlessCanvas.prototype, "connectingLabel", void 0);
1774
+ __decorate([
1775
+ property({ attribute: 'connection-label' })
1776
+ ], HelixHeadlessCanvas.prototype, "connectionLabel", void 0);
1777
+ __decorate([
1778
+ property()
1779
+ ], HelixHeadlessCanvas.prototype, "label", void 0);
1780
+ __decorate([
1781
+ property({ attribute: 'label-editor-label' })
1782
+ ], HelixHeadlessCanvas.prototype, "labelEditorLabel", void 0);
1783
+ __decorate([
1784
+ property({ attribute: 'leads-to-label' })
1785
+ ], HelixHeadlessCanvas.prototype, "leadsToLabel", void 0);
1786
+ __decorate([
1787
+ property({ reflect: true })
1788
+ ], HelixHeadlessCanvas.prototype, "order", void 0);
1789
+ __decorate([
1790
+ property({ reflect: true })
1791
+ ], HelixHeadlessCanvas.prototype, "path", void 0);
1792
+ __decorate([
1793
+ property({ attribute: 'readonly', reflect: true, type: Boolean })
1794
+ ], HelixHeadlessCanvas.prototype, "readOnly", void 0);
1795
+ __decorate([
1796
+ property({ attribute: 'reconnecting-label' })
1797
+ ], HelixHeadlessCanvas.prototype, "reconnectingLabel", void 0);
1798
+ __decorate([
1799
+ property()
1800
+ ], HelixHeadlessCanvas.prototype, "selected", void 0);
1801
+ __decorate([
1802
+ state()
1803
+ ], HelixHeadlessCanvas.prototype, "editing", void 0);
1804
+ __decorate([
1805
+ property({ attribute: 'stopped-label' })
1806
+ ], HelixHeadlessCanvas.prototype, "stoppedLabel", void 0);
1807
+ __decorate([
1808
+ state()
1809
+ ], HelixHeadlessCanvas.prototype, "stop", void 0);
1810
+ __decorate([
1811
+ state()
1812
+ ], HelixHeadlessCanvas.prototype, "linking", void 0);
1813
+ __decorate([
1814
+ state()
1815
+ ], HelixHeadlessCanvas.prototype, "said", void 0);
1816
+ __decorate([
1817
+ state()
1818
+ ], HelixHeadlessCanvas.prototype, "at", void 0);
1819
+ __decorate([
1820
+ state()
1821
+ ], HelixHeadlessCanvas.prototype, "direct", void 0);
1822
+ __decorate([
1823
+ state()
1824
+ ], HelixHeadlessCanvas.prototype, "pressing", void 0);
1825
+ __decorate([
1826
+ state()
1827
+ ], HelixHeadlessCanvas.prototype, "marquee", void 0);
1828
+ HelixHeadlessCanvas = __decorate([
1829
+ customElement('helix-headless-canvas')
1830
+ ], HelixHeadlessCanvas);
1831
+ export { HelixHeadlessCanvas };