@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.
- package/custom-elements.json +2658 -481
- package/dist/canvas/arrange.d.ts +32 -0
- package/dist/canvas/arrange.js +144 -0
- package/dist/canvas/paste.d.ts +25 -0
- package/dist/canvas/paste.js +58 -0
- package/dist/canvas/root/canvas-root.d.ts +60 -3
- package/dist/canvas/root/canvas-root.js +166 -2
- package/dist/chat/composer/chat-composer.js +5 -1
- package/dist/combobox/root/combobox-root.d.ts +6 -1
- package/dist/combobox/root/combobox-root.js +2 -2
- package/dist/date-input/root/date-input-root.d.ts +1 -1
- package/dist/element-events.generated.js +2 -0
- package/dist/file-input/root/file-input-root.d.ts +1 -1
- package/dist/file-input/root/file-input-root.js +1 -1
- package/dist/format-date/root/format-date-root.d.ts +10 -3
- package/dist/format-date/root/format-date-root.js +6 -1
- package/dist/listbox/root/listbox-root.d.ts +12 -1
- package/dist/listbox/root/listbox-root.js +13 -1
- package/dist/select/root/select-root.js +1 -1
- package/dist/table/grid-controller.d.ts +10 -0
- package/dist/table/grid-controller.js +7 -0
- package/dist/table/root/table-root.d.ts +71 -1
- package/dist/table/root/table-root.js +122 -7
- package/dist/table/row/table-row.d.ts +17 -0
- package/dist/table/row/table-row.js +23 -0
- package/dist/time-input/root/time-input-root.d.ts +2 -1
- package/dist/time-input/root/time-input-root.js +1 -1
- package/dist/time-picker/root/time-picker-root.d.ts +48 -1
- package/dist/time-picker/root/time-picker-root.js +79 -21
- package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
- package/dist/virtual-list/root/virtual-list-root.js +216 -32
- package/dist/virtual-list/sections.d.ts +27 -0
- package/dist/virtual-list/sections.js +43 -0
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +454 -76
- package/types/vue.d.ts +89 -7
- 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.
|
|
64
|
-
*
|
|
65
|
-
* cards
|
|
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',
|
|
123
|
+
this.addEventListener('invalid', () => {
|
|
124
|
+
if (!this.checkingValidity) {
|
|
125
|
+
this.touch();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
124
128
|
}
|
|
125
129
|
willUpdate(changed) {
|
|
126
130
|
super.willUpdate(changed);
|