@fusion.dev/helix-headless 0.166.0 → 0.168.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 (38) hide show
  1. package/custom-elements.json +2658 -481
  2. package/dist/canvas/arrange.d.ts +32 -0
  3. package/dist/canvas/arrange.js +144 -0
  4. package/dist/canvas/paste.d.ts +25 -0
  5. package/dist/canvas/paste.js +58 -0
  6. package/dist/canvas/root/canvas-root.d.ts +60 -3
  7. package/dist/canvas/root/canvas-root.js +166 -2
  8. package/dist/chat/composer/chat-composer.js +5 -1
  9. package/dist/combobox/root/combobox-root.d.ts +6 -1
  10. package/dist/combobox/root/combobox-root.js +2 -2
  11. package/dist/date-input/root/date-input-root.d.ts +1 -1
  12. package/dist/element-events.generated.js +2 -0
  13. package/dist/file-input/root/file-input-root.d.ts +1 -1
  14. package/dist/file-input/root/file-input-root.js +1 -1
  15. package/dist/format-date/root/format-date-root.d.ts +10 -3
  16. package/dist/format-date/root/format-date-root.js +6 -1
  17. package/dist/listbox/root/listbox-root.d.ts +12 -1
  18. package/dist/listbox/root/listbox-root.js +13 -1
  19. package/dist/select/root/select-root.js +1 -1
  20. package/dist/table/grid-controller.d.ts +10 -0
  21. package/dist/table/grid-controller.js +7 -0
  22. package/dist/table/root/table-root.d.ts +71 -1
  23. package/dist/table/root/table-root.js +122 -7
  24. package/dist/table/row/table-row.d.ts +17 -0
  25. package/dist/table/row/table-row.js +23 -0
  26. package/dist/time-input/root/time-input-root.d.ts +2 -1
  27. package/dist/time-input/root/time-input-root.js +1 -1
  28. package/dist/time-picker/root/time-picker-root.d.ts +48 -1
  29. package/dist/time-picker/root/time-picker-root.js +79 -21
  30. package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
  31. package/dist/virtual-list/root/virtual-list-root.js +216 -32
  32. package/dist/virtual-list/sections.d.ts +27 -0
  33. package/dist/virtual-list/sections.js +43 -0
  34. package/lint.json +1 -1
  35. package/package.json +5 -5
  36. package/types/jsx.d.ts +454 -76
  37. package/types/vue.d.ts +89 -7
  38. package/types/words.d.ts +27 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Where `arrange()` puts every card: the arithmetic, kept apart from the board
3
+ * that moves them there.
4
+ */
5
+ import { type CanvasEdge, type CanvasNode } from './root/canvas-document.js';
6
+ /** How a board is arranged. */
7
+ export interface CanvasArrangeOptions {
8
+ /**
9
+ * Which way the arrows run: `down` puts each card below the cards that point
10
+ * to it, and `right` beside them. `down` by default.
11
+ */
12
+ direction?: 'down' | 'right';
13
+ /** The space between two cards, in the document's own units. 64 by default. */
14
+ gap?: number;
15
+ }
16
+ /**
17
+ * Where each card goes, by id, so that no two cards overlap.
18
+ *
19
+ * **Layers where there are connections.** A card goes one layer past the
20
+ * furthest card that points to it, a connection that would close a loop is
21
+ * read as pointing back, and the cards in each layer are ordered by where
22
+ * the cards they connect to sit, so connections cross less. **A grid where
23
+ * there are none**, in reading order, about as many columns as rows.
24
+ *
25
+ * A group is placed as one box and carries the cards inside it, so a card
26
+ * stays in its group. What is arranged starts where the board's top left
27
+ * corner was, and every place lands on a multiple of `snap` where it is set.
28
+ */
29
+ export declare const arrangement: (nodes: readonly CanvasNode[], edges: readonly CanvasEdge[], options?: CanvasArrangeOptions, snap?: number) => Map<string, {
30
+ x: number;
31
+ y: number;
32
+ }>;
@@ -0,0 +1,144 @@
1
+ import { groupOf, reading } from './root/canvas-document.js';
2
+ import { snapTo } from './snap.js';
3
+ export const arrangement = (nodes, edges, options = {}, snap) => {
4
+ const gap = Math.max(0, options.gap ?? 64, snap ?? 0);
5
+ const across = options.direction === 'right';
6
+ const groups = nodes.filter((node) => node.type === 'group');
7
+ const top = (node) => {
8
+ const seen = new Set([node]);
9
+ let at = node;
10
+ for (let held = groupOf(at, groups); held && !seen.has(held); held = groupOf(at, groups)) {
11
+ seen.add(held);
12
+ at = held;
13
+ }
14
+ return at;
15
+ };
16
+ const holder = new Map(nodes.map((node) => [node.id, top(node)]));
17
+ const units = reading([...nodes], [], 'spatial').filter((node) => holder.get(node.id) === node);
18
+ const order = new Map(units.map((unit, index) => [unit.id, index]));
19
+ const into = new Map();
20
+ const out = new Map();
21
+ for (const edge of edges) {
22
+ const from = holder.get(edge.fromNode)?.id;
23
+ const to = holder.get(edge.toNode)?.id;
24
+ if (from !== undefined && to !== undefined && from !== to) {
25
+ out.set(from, (out.get(from) ?? new Set()).add(to));
26
+ into.set(to, (into.get(to) ?? new Set()).add(from));
27
+ }
28
+ }
29
+ const layer = new Map();
30
+ if (out.size === 0) {
31
+ const columns = Math.ceil(Math.sqrt(units.length));
32
+ for (const [index, unit] of units.entries()) {
33
+ layer.set(unit.id, Math.floor(index / Math.max(columns, 1)));
34
+ }
35
+ }
36
+ else {
37
+ const done = new Set();
38
+ const open = new Set();
39
+ const back = new Set();
40
+ const visit = (id) => {
41
+ open.add(id);
42
+ for (const next of out.get(id) ?? []) {
43
+ if (open.has(next)) {
44
+ back.add(`${id}>${next}`);
45
+ }
46
+ else if (!done.has(next)) {
47
+ visit(next);
48
+ }
49
+ }
50
+ open.delete(id);
51
+ done.add(id);
52
+ };
53
+ for (const unit of units) {
54
+ if (!done.has(unit.id) && !into.get(unit.id)?.size) {
55
+ visit(unit.id);
56
+ }
57
+ }
58
+ for (const unit of units) {
59
+ if (!done.has(unit.id)) {
60
+ visit(unit.id);
61
+ }
62
+ }
63
+ const depth = (id, seen) => {
64
+ const known = layer.get(id);
65
+ if (known !== undefined) {
66
+ return known;
67
+ }
68
+ seen.add(id);
69
+ let deepest = 0;
70
+ for (const from of into.get(id) ?? []) {
71
+ if (!back.has(`${from}>${id}`) && !seen.has(from)) {
72
+ deepest = Math.max(deepest, depth(from, seen) + 1);
73
+ }
74
+ }
75
+ layer.set(id, deepest);
76
+ return deepest;
77
+ };
78
+ for (const unit of units) {
79
+ depth(unit.id, new Set());
80
+ }
81
+ }
82
+ const layers = [];
83
+ for (const unit of units) {
84
+ const at = layer.get(unit.id) ?? 0;
85
+ layers[at] = [...(layers[at] ?? []), unit];
86
+ }
87
+ if (out.size > 0) {
88
+ const place = new Map();
89
+ const settle = (row, near) => {
90
+ const mean = (unit) => {
91
+ const known = [...(near.get(unit.id) ?? [])].filter((id) => place.has(id));
92
+ return known.length === 0
93
+ ? (order.get(unit.id) ?? 0) / Math.max(units.length, 1)
94
+ : known.reduce((sum, id) => sum + (place.get(id) ?? 0), 0) / known.length;
95
+ };
96
+ const means = new Map(row.map((unit) => [unit.id, mean(unit)]));
97
+ row.sort((a, b) => (means.get(a.id) ?? 0) - (means.get(b.id) ?? 0));
98
+ for (const [index, unit] of row.entries()) {
99
+ place.set(unit.id, (index + 0.5) / row.length);
100
+ }
101
+ };
102
+ for (const row of layers) {
103
+ settle(row ?? [], into);
104
+ }
105
+ for (const row of [...layers].reverse()) {
106
+ settle(row ?? [], out);
107
+ }
108
+ }
109
+ const main = (node) => (across ? node.width : node.height);
110
+ const cross = (node) => (across ? node.height : node.width);
111
+ const span = (row) => row.reduce((sum, unit) => sum + cross(unit), 0) + gap * Math.max(row.length - 1, 0);
112
+ const widest = Math.max(0, ...layers.map((row) => span(row ?? [])));
113
+ const start = {
114
+ x: Math.min(...units.map((unit) => unit.x)),
115
+ y: Math.min(...units.map((unit) => unit.y)),
116
+ };
117
+ const moved = new Map();
118
+ let along = 0;
119
+ for (const row of layers) {
120
+ if (!row) {
121
+ continue;
122
+ }
123
+ let at = out.size === 0 ? 0 : (widest - span(row)) / 2;
124
+ for (const unit of row) {
125
+ const x = snapTo(start.x + (across ? along : at), snap);
126
+ const y = snapTo(start.y + (across ? at : along), snap);
127
+ const dx = x - unit.x;
128
+ const dy = y - unit.y;
129
+ for (const node of nodes) {
130
+ if (holder.get(node.id) === unit) {
131
+ moved.set(node.id, { x: node.x + dx, y: node.y + dy });
132
+ }
133
+ }
134
+ at += cross(unit) + gap;
135
+ }
136
+ along += Math.max(...row.map(main)) + gap;
137
+ }
138
+ for (const node of nodes) {
139
+ if (!moved.has(node.id)) {
140
+ moved.set(node.id, { x: node.x, y: node.y });
141
+ }
142
+ }
143
+ return moved;
144
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What a paste adds to a board: the arithmetic, kept apart from the board that
3
+ * adds it.
4
+ */
5
+ import type { CanvasDocument, CanvasNode } from './root/canvas-document.js';
6
+ /**
7
+ * A JSON Canvas document read from pasted text, or nothing where the text is
8
+ * not one: any text that does not parse, or a node without an id, a type and
9
+ * a box. An edge whose ends are not both among the nodes is left out.
10
+ */
11
+ export declare const parseCanvas: (text: string) => CanvasDocument | undefined;
12
+ /**
13
+ * The document a paste adds to a board that already holds `nodes` and the
14
+ * connections named in `taken`.
15
+ *
16
+ * **Every id is made new where the board has it**, by a count after it, and
17
+ * the connections follow their cards to the new ids. **The cards move clear
18
+ * of what they were copied from**: a step down and to the right, and another
19
+ * while any pasted card would still sit where a card already is, so a second
20
+ * paste lands beside the first. Cards from another board, where nothing sits,
21
+ * stay where they were.
22
+ *
23
+ * @param step How far each move is, in the document's own units.
24
+ */
25
+ export declare const pasteInto: (document: CanvasDocument, nodes: readonly CanvasNode[], taken: ReadonlySet<string>, step: number) => CanvasDocument;
@@ -0,0 +1,58 @@
1
+ const finite = (value) => typeof value === 'number' && Number.isFinite(value);
2
+ export const parseCanvas = (text) => {
3
+ let read;
4
+ try {
5
+ read = JSON.parse(text);
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ const { edges, nodes } = (read ?? {});
11
+ if (!Array.isArray(nodes) || nodes.length === 0) {
12
+ return undefined;
13
+ }
14
+ const valid = nodes.every((node) => typeof node?.id === 'string' &&
15
+ typeof node.type === 'string' &&
16
+ [node.x, node.y, node.width, node.height].every(finite));
17
+ if (!valid) {
18
+ return undefined;
19
+ }
20
+ const ids = new Set(nodes.map((node) => node.id));
21
+ return {
22
+ edges: (Array.isArray(edges) ? edges : []).filter((edge) => ids.has(edge?.fromNode) && ids.has(edge?.toNode)),
23
+ nodes: nodes,
24
+ };
25
+ };
26
+ export const pasteInto = (document, nodes, taken, step) => {
27
+ const used = new Set([...taken, ...nodes.map((node) => node.id)]);
28
+ const unique = (id) => {
29
+ let next = id;
30
+ for (let count = 2; used.has(next); count++) {
31
+ next = `${id}-${count}`;
32
+ }
33
+ used.add(next);
34
+ return next;
35
+ };
36
+ const corners = new Set(nodes.map((node) => `${Math.round(node.x)},${Math.round(node.y)}`));
37
+ const pasted = document.nodes ?? [];
38
+ let by = 0;
39
+ while (step > 0 &&
40
+ pasted.some((node) => corners.has(`${Math.round(node.x + by)},${Math.round(node.y + by)}`))) {
41
+ by += step;
42
+ }
43
+ const renamed = new Map();
44
+ const out = pasted.map((node) => {
45
+ const id = unique(node.id);
46
+ renamed.set(node.id, id);
47
+ return { ...node, id, x: node.x + by, y: node.y + by };
48
+ });
49
+ return {
50
+ edges: (document.edges ?? []).map((edge) => ({
51
+ ...edge,
52
+ fromNode: renamed.get(edge.fromNode) ?? edge.fromNode,
53
+ id: unique(edge.id || `${edge.fromNode}-${edge.toNode}`),
54
+ toNode: renamed.get(edge.toNode) ?? edge.toNode,
55
+ })),
56
+ nodes: out,
57
+ };
58
+ };
@@ -1,6 +1,8 @@
1
1
  import { type CanvasView, HelixElement, type ViewportPanDetail, type ViewportWay, type ViewportZoomDetail } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
+ import { type CanvasArrangeOptions } from '../arrange.js';
3
4
  import { type CanvasDocument, type CanvasRoute, type CanvasTone } from './canvas-document.js';
5
+ export type { CanvasArrangeOptions } from '../arrange.js';
4
6
  export type { CanvasDocument, CanvasEdge, CanvasEdgeProps, CanvasEnd, CanvasNode, CanvasNodeProps, CanvasPath, CanvasRoute, CanvasRouteEnds, CanvasSide, CanvasTone, } from './canvas-document.js';
5
7
  export { fromJsonCanvas } from './canvas-document.js';
6
8
  declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion.dev/helix-core").ViewportElement) & typeof HelixElement;
@@ -60,15 +62,24 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
60
62
  * card or connection says so when a reader arrives on it.
61
63
  * - Control or Command with C copies the selected cards, and the connections
62
64
  * between them, as a JSON Canvas document in text, which another board or
63
- * an app that reads the format takes. A paste is your page's: listen for
64
- * `paste` on the board, read the text with `fromJsonCanvas`, and add the
65
- * cards it holds.
65
+ * an app that reads the format takes.
66
+ * - Control or Command with V pastes a JSON Canvas document: the board adds
67
+ * its cards and connections, a step down and to the right of any card they
68
+ * would sit on, and selects them. It asks with `helix-before-paste` first.
69
+ * A page that renders its cards from its own state cancels it and adds the
70
+ * detail's document to that state, through `fromJsonCanvas`. `paste()` does
71
+ * the same from a script.
66
72
  * - Control or Command with Z undoes the last move or resize the board made,
67
73
  * and with Shift, or Control with Y, makes it again. `undo()` and `redo()`
68
74
  * do the same from a script. Each asks with `helix-before-move` and tells
69
75
  * with `helix-move`, as a move does, so a board that holds its positions in
70
76
  * your state is undone through your state.
71
77
  *
78
+ * **`arrange()` moves every card so that none overlaps another**, in layers
79
+ * that follow the arrows, or in a grid on a board with no connections. The
80
+ * cards glide for as long as the board's own transition, and `undo()` puts
81
+ * them back.
82
+ *
72
83
  * Every drag also works with clicks, as WCAG 2.5.7 requires for a pointer that
73
84
  * cannot hold a button down. Click a selected card's move handle, a corner, a
74
85
  * card's connection point, or the end of a selected connection to pick it up.
@@ -129,6 +140,7 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
129
140
  * @slot zoom-out-icon - Replaces the glyph inside the zoom-out button.
130
141
  *
131
142
  * @event {HelixBeforeMoveEvent} helix-before-move - Cards are about to move. Cancelable: cancel it to keep the positions in your own state.
143
+ * @event {HelixBeforePasteEvent} helix-before-paste - Cards and connections are about to be pasted. Cancelable: cancel it to add the detail's document to your own state.
132
144
  * @event {HelixBeforeReconnectEvent} helix-before-reconnect - A connection's end is about to move to another card or side. Cancelable: cancel it to keep connections in your own state.
133
145
  * @event {HelixBeforeRelabelEvent} helix-before-relabel - A connection's label or a group's name is about to change. Cancelable: cancel it to keep labels in your own state.
134
146
  * @event {HelixCanvasEditEvent} helix-canvas-edit - A card was double-clicked, or Enter or F2 was pressed on it. Open your own editor for the card.
@@ -137,6 +149,7 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
137
149
  * @event {HelixCanvasZoomEvent} helix-canvas-zoom - The board zoomed one step, from its controls or the keys. A wheel or a pinch sends `helix-canvas-view`.
138
150
  * @event {HelixConnectEvent} helix-connect - Two cards were chosen to be joined. Add a `helix-canvas-edge` to join them.
139
151
  * @event {HelixMoveEvent} helix-move - Cards moved and the move is finished: a drag was released, a click put a card down, or a key was pressed.
152
+ * @event {HelixPastedEvent} helix-pasted - Cards and connections were pasted, and the board holds them.
140
153
  * @event {HelixReconnectEvent} helix-reconnect - A connection's end moved, and the move is finished.
141
154
  * @event {HelixRelabelEvent} helix-relabel - A connection's label or a group's name changed.
142
155
  * @event {HelixRemoveEvent} helix-remove - Cards or connections were chosen for removal. Remove the elements it names.
@@ -198,6 +211,8 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
198
211
  * `gestures="activate"`, is scrolled or swiped over.
199
212
  */
200
213
  activateHintLabel: string;
214
+ /** What screen readers hear after `arrange()` moves the cards. */
215
+ arrangedLabel: string;
201
216
  /** The text read with a card to say where its connections come from, with `{cards}`. */
202
217
  comesFromLabel: string;
203
218
  /** The text announced when a connection is requested, with `{from}` and `{to}`. */
@@ -282,6 +297,8 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
282
297
  * that reflected would write a choice onto every host that no server sent.
283
298
  */
284
299
  order?: 'spatial' | 'edges';
300
+ /** What screen readers hear after a paste, with `{count}` for the cards pasted. */
301
+ pastedLabel: string;
285
302
  /**
286
303
  * The route every connection takes when it sets no `path` of its own:
287
304
  * `straight`, or `step` for right angles.
@@ -308,6 +325,12 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
308
325
  reconnectingLabel: string;
309
326
  /** What screen readers hear after a move or a resize is made again. */
310
327
  redoneLabel: string;
328
+ /**
329
+ * What screen readers hear when Delete asks to remove what is selected, with
330
+ * `{count}` for how many cards and connections, and `{name}` for the one
331
+ * where there is one.
332
+ */
333
+ removedLabel: string;
311
334
  /**
312
335
  * What screen readers hear after a key resizes a card, with `{name}` for the
313
336
  * card and `{width}` and `{height}` for its new size, in the document's
@@ -846,6 +869,40 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
846
869
  * holds, nor over text a reader selected, which are the platform's to copy.
847
870
  */
848
871
  private readonly handleCopy;
872
+ /**
873
+ * A paste on the board of a JSON Canvas document, the text a copy writes,
874
+ * adds its cards and connections. Not into a field a card holds, nor onto a
875
+ * `readonly` board, nor text that is not a board, which are the platform's.
876
+ */
877
+ private readonly handlePaste;
878
+ /**
879
+ * Adds the cards and connections of a JSON Canvas document, as a paste does,
880
+ * and selects the cards. Ids the board already holds are given a count after
881
+ * them, and the cards move a step down and to the right while they would sit
882
+ * on cards already there.
883
+ *
884
+ * Asks with `helix-before-paste` first: cancel it to add the detail's
885
+ * document to your own state. Returns whether the board added the cards:
886
+ * false on a `readonly` board, for text that is not a board, or when the
887
+ * request was refused.
888
+ */
889
+ paste(document: CanvasDocument | string): boolean;
890
+ /** The glide `arrange()` started, while it runs. */
891
+ private gliding;
892
+ /** Ends a glide where it was going. */
893
+ private settle;
894
+ /**
895
+ * Moves the cards so that none overlaps another: in layers that follow the
896
+ * connections, or in a grid where there are none. A group moves as one box
897
+ * with its cards. Each place lands on a multiple of `snap` where it is set.
898
+ *
899
+ * Asks with `helix-before-move` first and tells with `helix-move`, as a
900
+ * move does, so `undo()` puts every card back. The cards glide to their
901
+ * places for as long as the board's plane takes to glide, and land at once
902
+ * where it does not, such as under `prefers-reduced-motion`. Returns whether
903
+ * the cards moved.
904
+ */
905
+ arrange(options?: CanvasArrangeOptions): boolean;
849
906
  /**
850
907
  * Control or Command with Z undoes, and with Shift, or Control with Y,
851
908
  * redoes, from anywhere on the board but a field a card holds, where the
@@ -4,16 +4,18 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
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
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { assignSlots, auditName, capturePointer, customElement, formatPercent, formatWords, HelixBeforeMoveEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixConnectEvent, HelixElement, HelixMoveEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixSelectEvent, hostAria, inlineStep, isRtl, MixinViewport, words, wordsConverter, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { assignSlots, auditName, capturePointer, customElement, formatPercent, formatWords, HelixBeforeMoveEvent, HelixBeforePasteEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixConnectEvent, HelixElement, HelixMoveEvent, HelixPastedEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixSelectEvent, hostAria, inlineStep, isRtl, MixinViewport, words, wordsConverter, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
+ import { arrangement } from '../arrange.js';
10
11
  import { descriptionOf } from '../describe.js';
11
12
  import { HelixHeadlessCanvasEdge } from '../edge/canvas-edge.js';
12
13
  import { EdgePan } from '../edge-pan.js';
13
14
  import { neighborOf } from '../neighbor.js';
14
15
  import { HelixHeadlessCanvasNode } from '../node/canvas-node.js';
16
+ import { parseCanvas, pasteInto } from '../paste.js';
15
17
  import { snapBox, snapTo, stepTo } from '../snap.js';
16
- import { colorOf, connector, distanceTo, ends, extent, inside, port, reading, sideNear, } from './canvas-document.js';
18
+ import { colorOf, connector, distanceTo, ends, extent, fromJsonCanvas, inside, port, reading, sideNear, } from './canvas-document.js';
17
19
  export { fromJsonCanvas } from './canvas-document.js';
18
20
  const MARGIN = 32;
19
21
  const ARROW = 'canvas-arrow';
@@ -43,6 +45,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
43
45
  constructor() {
44
46
  super(...arguments);
45
47
  this.activateHintLabel = 'Press the board to use it';
48
+ this.arrangedLabel = 'Cards arranged';
46
49
  this.comesFromLabel = 'from {cards}';
47
50
  this.connectedLabel = 'Asked to connect {from} to {to}.';
48
51
  this.connectingLabel = 'Connecting from {from}. Move to another card and press Enter, or Escape to stop.';
@@ -55,9 +58,11 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
55
58
  this.leadsToLabel = 'leads to {cards}';
56
59
  this.linkedLabel = '{leadsTo}; {comesFrom}';
57
60
  this.movedLabel = '{name} moved to {x}, {y}';
61
+ this.pastedLabel = '{count, plural, one {# card pasted} other {# cards pasted}}';
58
62
  this.readOnly = false;
59
63
  this.reconnectingLabel = 'Moving the end of the connection from {from} to {to}. Move to a card and press Enter, or Escape to stop.';
60
64
  this.redoneLabel = 'Move made again';
65
+ this.removedLabel = '{count, plural, one {Asked to remove {name}.} other {Asked to remove # items.}}';
61
66
  this.resizedLabel = '{name} resized to {width} by {height}';
62
67
  this.roleLabel = 'canvas';
63
68
  this.selected = '';
@@ -106,6 +111,15 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
106
111
  event.preventDefault();
107
112
  this.say(formatWords(this, 'copiedLabel', { count: copy.nodes?.length ?? 0 }));
108
113
  };
114
+ this.handlePaste = (event) => {
115
+ const origin = event.composedPath()[0];
116
+ if (event.defaultPrevented || origin?.closest?.(INTERACTIVE)) {
117
+ return;
118
+ }
119
+ if (this.paste(event.clipboardData?.getData('text/plain') ?? '')) {
120
+ event.preventDefault();
121
+ }
122
+ };
109
123
  this.handleHistoryKey = (event) => {
110
124
  if (event.defaultPrevented || event.altKey || !(event.ctrlKey || event.metaKey)) {
111
125
  return;
@@ -129,6 +143,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
129
143
  }
130
144
  });
131
145
  this.handlePointerDown = (event) => {
146
+ this.settle();
132
147
  if (!this.gestureAllowed(event)) {
133
148
  return;
134
149
  }
@@ -494,6 +509,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
494
509
  }
495
510
  };
496
511
  this.handleStopKeydown = (event) => {
512
+ this.settle();
497
513
  const origin = event.composedPath()[0];
498
514
  const edge = this.edgeOf(event);
499
515
  const card = edge === undefined ? this.cardOf(event) : undefined;
@@ -1122,6 +1138,19 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1122
1138
  const at = Math.min(...asked.map((one) => walk.findIndex((stop) => stop.key === one)));
1123
1139
  const next = neighborOf(walk.map((one) => one.key), at, (key) => !gone.has(key));
1124
1140
  this.refocus = { gone: [...gone], next };
1141
+ const only = asked[0];
1142
+ const edge = only?.startsWith('edge:')
1143
+ ? edges.find((one) => this.keyOf(one) === only)
1144
+ : undefined;
1145
+ this.say(formatWords(this, 'removedLabel', {
1146
+ count: asked.length,
1147
+ name: edge
1148
+ ? formatWords(this, 'connectionLabel', {
1149
+ from: this.nameOf(edge.fromNode),
1150
+ to: this.nameOf(edge.toNode),
1151
+ })
1152
+ : this.nameOf(only ?? ''),
1153
+ }));
1125
1154
  this.dispatchEvent(new HelixRemoveEvent(detail));
1126
1155
  }
1127
1156
  clicked(key, event) {
@@ -1290,6 +1319,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1290
1319
  return name === undefined ? undefined : this.byName.get(name);
1291
1320
  }
1292
1321
  requestMove(detail) {
1322
+ this.settle();
1293
1323
  if (!this.dispatchEvent(new HelixBeforeMoveEvent(detail))) {
1294
1324
  return false;
1295
1325
  }
@@ -1359,6 +1389,125 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1359
1389
  this.say(words);
1360
1390
  return true;
1361
1391
  }
1392
+ paste(document) {
1393
+ const read = typeof document === 'string' ? parseCanvas(document) : document;
1394
+ if (this.readOnly || !read?.nodes?.length) {
1395
+ return false;
1396
+ }
1397
+ const taken = new Set(this.edgeElements.map((one) => one.name).filter(Boolean));
1398
+ const placed = pasteInto(read, this.nodes, taken, (this.snap ?? 0) > 0 ? this.snap : NUDGE * 2);
1399
+ if (!this.dispatchEvent(new HelixBeforePasteEvent({ document: placed }))) {
1400
+ return false;
1401
+ }
1402
+ const tier = this.constructor.tier;
1403
+ const add = (name, attributes, color, text = '') => {
1404
+ const element = this.ownerDocument.createElement(`${tier}${name}`);
1405
+ for (const [key, value] of Object.entries(attributes)) {
1406
+ if (value !== undefined && value !== false && value !== '') {
1407
+ element.setAttribute(key, value === true ? '' : String(value));
1408
+ }
1409
+ }
1410
+ if (color) {
1411
+ element.style.setProperty(`--${name}-color`, color);
1412
+ }
1413
+ element.textContent = text;
1414
+ this.append(element);
1415
+ };
1416
+ const { edges, nodes } = fromJsonCanvas(placed);
1417
+ for (const one of nodes) {
1418
+ const { color, group, height, label, name, text, tone, width, x, y } = one;
1419
+ add('canvas-node', { group, height, label, name, tone, width, x, y }, color, text ?? one.url ?? one.file);
1420
+ }
1421
+ for (const one of edges) {
1422
+ add('canvas-edge', {
1423
+ from: one.from,
1424
+ 'from-end': one.fromEnd,
1425
+ 'from-side': one.fromSide,
1426
+ label: one.label,
1427
+ name: one.name,
1428
+ to: one.to,
1429
+ 'to-end': one.toEnd,
1430
+ 'to-side': one.toSide,
1431
+ tone: one.tone,
1432
+ }, one.color);
1433
+ }
1434
+ this.requestUpdate();
1435
+ this.choose(nodes.map((one) => one.name));
1436
+ this.say(formatWords(this, 'pastedLabel', { count: nodes.length }));
1437
+ this.dispatchEvent(new HelixPastedEvent({ document: placed }));
1438
+ return true;
1439
+ }
1440
+ settle() {
1441
+ const glide = this.gliding;
1442
+ if (!glide) {
1443
+ return;
1444
+ }
1445
+ cancelAnimationFrame(glide.frame);
1446
+ this.gliding = undefined;
1447
+ for (const { name, x, y } of glide.to) {
1448
+ const card = this.byName.get(name);
1449
+ if (card) {
1450
+ card.x = x;
1451
+ card.y = y;
1452
+ }
1453
+ }
1454
+ this.requestUpdate();
1455
+ }
1456
+ arrange(options = {}) {
1457
+ this.settle();
1458
+ const places = arrangement(this.nodes, this.edges, options, this.snap);
1459
+ const from = new Map(this.cards.map((card) => [card.name, { x: card.x, y: card.y }]));
1460
+ const detail = {
1461
+ nodes: [...places]
1462
+ .filter(([name, at]) => from.get(name)?.x !== at.x || from.get(name)?.y !== at.y)
1463
+ .map(([name, at]) => ({ name, ...at })),
1464
+ };
1465
+ if (this.readOnly || detail.nodes.length === 0 || !this.requestMove(detail)) {
1466
+ return false;
1467
+ }
1468
+ this.finishMove(detail);
1469
+ this.say(this.arrangedLabel);
1470
+ const board = this.partNamed('board');
1471
+ let time = 0;
1472
+ if (board && !globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
1473
+ this.toggleState('direct', false);
1474
+ time = Number.parseFloat(getComputedStyle(board).transitionDuration) * 1000;
1475
+ this.toggleState('direct', this.direct);
1476
+ }
1477
+ if (!(time > 0) || typeof requestAnimationFrame === 'undefined') {
1478
+ return true;
1479
+ }
1480
+ for (const { name } of detail.nodes) {
1481
+ const card = this.byName.get(name);
1482
+ const start = from.get(name);
1483
+ if (card && start) {
1484
+ card.x = start.x;
1485
+ card.y = start.y;
1486
+ }
1487
+ }
1488
+ const begun = performance.now();
1489
+ const step = (now) => {
1490
+ const t = Math.min(1, (now - begun) / time);
1491
+ const eased = t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;
1492
+ for (const { name, x, y } of detail.nodes) {
1493
+ const card = this.byName.get(name);
1494
+ const start = from.get(name);
1495
+ if (card && start) {
1496
+ card.x = start.x + (x - start.x) * eased;
1497
+ card.y = start.y + (y - start.y) * eased;
1498
+ }
1499
+ }
1500
+ this.requestUpdate();
1501
+ if (t < 1 && this.gliding) {
1502
+ this.gliding.frame = requestAnimationFrame(step);
1503
+ }
1504
+ else {
1505
+ this.gliding = undefined;
1506
+ }
1507
+ };
1508
+ this.gliding = { frame: requestAnimationFrame(step), to: detail.nodes };
1509
+ return true;
1510
+ }
1362
1511
  panCarrying(by) {
1363
1512
  const last = this.carryPointer;
1364
1513
  if (!last) {
@@ -1925,6 +2074,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1925
2074
  this.addEventListener('keydown', this.handleStopKeydown);
1926
2075
  this.addEventListener('keydown', this.handleHistoryKey);
1927
2076
  this.addEventListener('copy', this.handleCopy);
2077
+ this.addEventListener('paste', this.handlePaste);
1928
2078
  this.addEventListener('pointercancel', this.handlePointerUp);
1929
2079
  this.addEventListener('pointerdown', this.handlePointerDown);
1930
2080
  this.addEventListener('pointermove', this.handlePointerMove);
@@ -1947,6 +2097,8 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1947
2097
  this.removeEventListener('keydown', this.handleStopKeydown);
1948
2098
  this.removeEventListener('keydown', this.handleHistoryKey);
1949
2099
  this.removeEventListener('copy', this.handleCopy);
2100
+ this.removeEventListener('paste', this.handlePaste);
2101
+ this.settle();
1950
2102
  this.removeEventListener('pointercancel', this.handlePointerUp);
1951
2103
  this.removeEventListener('pointerdown', this.handlePointerDown);
1952
2104
  this.removeEventListener('pointermove', this.handlePointerMove);
@@ -2323,6 +2475,10 @@ __decorate([
2323
2475
  words,
2324
2476
  property({ attribute: 'activate-hint-label' })
2325
2477
  ], HelixHeadlessCanvas.prototype, "activateHintLabel", void 0);
2478
+ __decorate([
2479
+ words,
2480
+ property({ attribute: 'arranged-label' })
2481
+ ], HelixHeadlessCanvas.prototype, "arrangedLabel", void 0);
2326
2482
  __decorate([
2327
2483
  words,
2328
2484
  property({ attribute: 'comes-from-label' })
@@ -2376,6 +2532,10 @@ __decorate([
2376
2532
  __decorate([
2377
2533
  property({ reflect: true })
2378
2534
  ], HelixHeadlessCanvas.prototype, "order", void 0);
2535
+ __decorate([
2536
+ words,
2537
+ property({ attribute: 'pasted-label' })
2538
+ ], HelixHeadlessCanvas.prototype, "pastedLabel", void 0);
2379
2539
  __decorate([
2380
2540
  property({ reflect: true })
2381
2541
  ], HelixHeadlessCanvas.prototype, "path", void 0);
@@ -2390,6 +2550,10 @@ __decorate([
2390
2550
  words,
2391
2551
  property({ attribute: 'redone-label' })
2392
2552
  ], HelixHeadlessCanvas.prototype, "redoneLabel", void 0);
2553
+ __decorate([
2554
+ words,
2555
+ property({ attribute: 'removed-label' })
2556
+ ], HelixHeadlessCanvas.prototype, "removedLabel", void 0);
2393
2557
  __decorate([
2394
2558
  words,
2395
2559
  property({ attribute: 'resized-label' })
@@ -120,7 +120,11 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
120
120
  }
121
121
  firstUpdated(changed) {
122
122
  super.firstUpdated(changed);
123
- this.addEventListener('invalid', this.touch);
123
+ this.addEventListener('invalid', () => {
124
+ if (!this.checkingValidity) {
125
+ this.touch();
126
+ }
127
+ });
124
128
  }
125
129
  willUpdate(changed) {
126
130
  super.willUpdate(changed);