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