flexdesk 0.17.3 → 0.17.4
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/CHANGELOG.md +86 -65
- package/README.md +27 -4544
- package/css/base.css +18 -1
- package/css/flexdesk.css +18 -1
- package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
- package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
- package/dist/flexdesk.css +18 -1
- package/dist/tiles.js +1 -1
- package/dist/widgets.js +14 -2
- package/dist/widgets.js.map +2 -2
- package/docs/README.md +97 -0
- package/docs/best-practices.md +45 -0
- package/docs/components/charts.md +211 -0
- package/docs/components/command-palette.md +102 -0
- package/docs/components/conversation.md +72 -0
- package/docs/components/data-table-grid.md +328 -0
- package/docs/components/data-table.md +596 -0
- package/docs/components/editor.md +22 -0
- package/docs/components/flow-canvas-editor.md +482 -0
- package/docs/components/flow-editors.md +176 -0
- package/docs/components/flow-lane-editor.md +165 -0
- package/docs/components/flow-outline-editor.md +176 -0
- package/docs/components/forms-and-dialogs.md +294 -0
- package/docs/components/input-fields.md +72 -0
- package/docs/components/keymap.md +143 -0
- package/docs/components/layout-widgets.md +163 -0
- package/docs/components/markdown-and-outline.md +106 -0
- package/docs/components/menus.md +86 -0
- package/docs/components/notifications.md +194 -0
- package/docs/components/panels-and-desktops.md +127 -0
- package/docs/components/record-list-and-fact-table.md +128 -0
- package/docs/components/settings-view.md +112 -0
- package/docs/components/shell.md +189 -0
- package/docs/components/slide-out-panel.md +68 -0
- package/docs/components/tiles.md +210 -0
- package/docs/components/wizard.md +54 -0
- package/docs/concepts/building-and-packaging.md +33 -0
- package/docs/concepts/domain-neutrality.md +110 -0
- package/docs/concepts/host-port.md +137 -0
- package/docs/concepts/packages-and-entry-points.md +32 -0
- package/docs/concepts/peer-dependencies.md +42 -0
- package/docs/concepts/state-guard.md +58 -0
- package/docs/concepts/styling.md +47 -0
- package/docs/quickstart.md +179 -0
- package/package.json +2 -1
- package/src/ui/components/slide_out_panel.js +14 -3
- package/src/ui/utils/auto_scrollbars.js +16 -2
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# Flow canvas editor
|
|
2
|
+
|
|
3
|
+
> **Packages:** `@flexdesk/flow`, `@flexdesk/canvas` (the kernel) · **Shared kit:** [Flow editors and the flow kit](flow-editors.md) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
## The canvas editor
|
|
6
|
+
|
|
7
|
+
A flow drawn as nodes and lines: a palette of step types, a pannable canvas and
|
|
8
|
+
the settings of whatever is picked, under a toolbar. It stands on the
|
|
9
|
+
`@flexdesk/canvas` kernel, which is also usable on its own for any diagram.
|
|
10
|
+
|
|
11
|
+
**The kernel** (`@flexdesk/canvas`).
|
|
12
|
+
|
|
13
|
+
| Part | What it does |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `new CanvasAdapter({container, gridSize = 16, onViewportChange, onBackgroundMenu, nodeSelector})` | A surface you put nodes and lines on: right-, middle- or Shift-drag pans, Ctrl + the wheel zooms about the pointer, a left press on the background draws a marquee (`onMarquee(box)`, `onSelectionChange(selection)`). Fields `root`, `surface`, `edgeLayer` (an SVG), `nodeLayer`, `marquee`, `selection`, `scale`, `tx`, `ty`, `gridSize`. Methods `clientToCanvas`, `canvasToClient`, `snap`, `setViewport`, `viewport`, `zoomAt`, `fit`, `visibleBox`, `contains`, `bringIntoView` (pans and never zooms, and does nothing when the box is already in view), `consumeRightDrag`, `clear`, `destroy`. **A node is whatever you put in `nodeLayer`**: a press inside one starts no pan and no marquee, and a right-click on one keeps your menu (or the browser's). Pass `nodeSelector` when the node layer holds other things too. `onViewportChange` fires once, synchronously, from the constructor, so declare what it reads before you build the adapter. `CanvasAdapter.MIN_SCALE` (0.15) and `CanvasAdapter.MAX_SCALE` (4) are the limits every zoom clamps to (0.17.0) |
|
|
16
|
+
| `routeEdge(from, to, {obstacles, simple})`, `selfLoop`, `returnEdge(from, to, {floor})` | SVG path data between two anchors `{x, y, side}`: a line with horizontal lead-outs (bent round obstacles up to `OBSTACLE_ROUTING_NODE_LIMIT` nodes), a node's line to itself, and a loop's line back, which runs under both nodes |
|
|
17
|
+
| `crowsFoot(point, side, kind)`, `footAnchor(point, side, kind)`, `cardinality({kind, optional})` | Crow's-foot ends (`many`, `one`, `zero-or-many`, `one-or-many`, `zero-or-one`, `exactly-one`) as an SVG group `twm-edge__marker twm-edge__marker--<kind>`, where the line should start so it meets the mark, and the two ends a reference implies |
|
|
18
|
+
| `arrange(nodes, edges, {columnGap, rowGap})` | A layered layout. **Two quirks, kept on purpose:** an edge points from child to parent (a node sits one column right of what it points at), and each column is sorted by `label`, which every node must have |
|
|
19
|
+
| `layeredLayout(nodes, edges, {rank, sizeOf, columnGap = 64, rowGap = 32, componentGap = 80, passes = 8})` (0.17.0) | A layered layout for any directed graph, as a diagram is read: `{from, to}` puts `from` left of `to`. Longest-path columns (cycles broken, never hung on); `rank(node)` answering `'source'` or `'sink'` puts a node in the first or the last column; barycentre sweeps reduce crossings, keeping the order with the fewest (the order given included); `sizeOf(node)` → `{width, height}` (default the node's own `width`/`height`, else 160 × 80) sizes the columns; components no line joins are stacked top to bottom, the largest first, `componentGap` apart. Returns a Map of id → the top-left `{x, y}` from (0, 0). Pure; 300 nodes and 400 edges take a few milliseconds |
|
|
20
|
+
| `new NodePlatform({collapsedPorts})` | The nodes and their ports: `hydrate(nodes)`, `get`, `all`, `port`, `setPosition`, `setCollapsed`, `visiblePorts(id)`, `on(event, fn)` (`hydrated`, `moved`, `collapsed`). A node is `{id, position, size, collapsed, ports, …}`; any other key is kept |
|
|
21
|
+
| `new ConnectorRouter({platform, selfReference, policy, endsOf, disabledEnds, strings})` | Edges and whether a drop may become one. `canConnect(source, target, {pendingEdges})` refuses, in order, a node that is gone, a port that is gone, a disabled port (with its `disabledReason`), a self-reference (unless `selfReference`), and a port at its `maxConnections`. Only then does it ask your `policy(ctx)`, which may refuse, allow, or answer `{ok: true, offer}`. Also `hydrate`, `edges` (a Map), `edgesFor`, `stage`, `unstage`, `remapNodeConnections(nodeId, portIds)` (keeps the edges whose ports survive and reports the rest) |
|
|
22
|
+
| `new HistoryService({actions, apply, onState, limit, requires, refusal})` | Undo over recorded edits (deltas), behind a closed action list: `record` throws on any action you did not declare. Nothing is recorded before `hydrated()`, and a batch (`startBatch`/`endBatch`) is one entry. Also `undo`, `redo`, `reset`, `adopt`, `undoStack`, `redoStack`, `canUndo`, `canRedo`. A missing `actions`, `apply` or anything named in `requires` throws at construction, not at the first silent no-op |
|
|
23
|
+
|
|
24
|
+
A diagram with its own rules subclasses the three and passes them in. For
|
|
25
|
+
example, a schema diagram's router answers the generic refusals in its own
|
|
26
|
+
words and keeps its foreign-key rules in the policy:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
class ConnectorRouter extends Base.ConnectorRouter {
|
|
30
|
+
constructor({ platform }) {
|
|
31
|
+
super({ platform, selfReference: true, disabledEnds: ['source'],
|
|
32
|
+
endsOf: (e) => ({ from: `col:${e.from_col}`, to: `col:${e.to_col}` }),
|
|
33
|
+
strings: { nodeGone: 'That table is no longer on the diagram.' },
|
|
34
|
+
policy: (ctx) => myForeignKeyRules(ctx) });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`tests/canvas_graph.test.mjs` §7 builds such a diagram on the generic layer.
|
|
40
|
+
|
|
41
|
+
**The editor.**
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { createCanvasEditor, createStepCatalogue } from '@flexdesk/flow';
|
|
45
|
+
|
|
46
|
+
const editor = createCanvasEditor(host, {
|
|
47
|
+
catalogue: createStepCatalogue(types, { categories }),
|
|
48
|
+
flowSettings: { title: 'Sync', schema, value }, // the flow's own settings, shown when nothing is picked
|
|
49
|
+
actions: [{ id: 'publish', label: 'Publish', icon: 'publish', primary: true, run: (ed) => publish(ed) }],
|
|
50
|
+
onChange: ({ graph, flowSettings, text, action, key, via }) => autosave(text),
|
|
51
|
+
values: async ({ stepId, field, before, loops }) => groups, // Insert a value
|
|
52
|
+
dataPortRead: (node, port) => `\${steps.${node.id}.${port.name}}`,
|
|
53
|
+
summariseReads: (node) => `Later steps read it as \${steps.${node.id}.…}`,
|
|
54
|
+
});
|
|
55
|
+
editor.load({ graph }); // a baseline: there is nothing to undo after it
|
|
56
|
+
editor.setFindings(validatorResult); // on the nodes, under the fields, in the strip
|
|
57
|
+
editor.setRunOverlay({ steps, ports, banner });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Option | Meaning |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `catalogue` | `createStepCatalogue(...)`, or the raw list of types. The palette shows them by category, except start-role types (`paletteTypes(type)` decides). A type with `unavailable` is shown greyed, with its sentence under it |
|
|
63
|
+
| `widgets`, `references`, `strings`, `services` | The kit's widget registry, the reference syntaxes by name, your words (`CANVAS_STRINGS` and the kit's), and your bag, handed to every widget unchanged |
|
|
64
|
+
| `values` | *Insert a value*: called with `{graph, stepId, field, key, before, loops}`, where `before` is the steps that always run before this one (`alwaysBefore`, nearest first) and `loops` the loops around it. Returns groups, or `{groups, note}` |
|
|
65
|
+
| `summarise(node, type)` | A line under a node's ports. `summariseReads(node, type)` is the panel's id line |
|
|
66
|
+
| `dataPortRead(node, port)` | How a data port is read, in your syntax. It is shown on the port, which cannot be connected (unless `connections.dataPorts` is `'connect'`) |
|
|
67
|
+
| `connections` | `{dataPorts: 'connect', policy(ctx)}`, absent by default, and then nothing below is drawn or bound. With it every line is asked, in order: what the canvas cannot draw (`canConnect` with `dataPorts`: a data port connects only to a data port, and a data output carries any number of lines); the kernel's `ConnectorRouter` over the catalogue ports, which take its descriptor keys (`disabled`, `disabledReason`, `maxConnections`, `allowMultiple`); the port types (an input's `accepts: ['number']` refuses an output whose `provides` names none of them, in a sentence); and your `policy(ctx)` — the router's context plus `connection` and a copy of the `graph` — which may refuse with a `reason`, allow, or answer `{ok: true, offer: {message, accept, apply(draft)}}`. An offer is shown with Accept and Not now: Accept makes the line and runs `apply` on a copy of the graph (change it in place or return a new one), as one undo entry. An output can also be dragged: while it is, the inputs it may end on carry `twm-flow-graph__port--can-drop`; released on one it connects; released on the empty canvas it opens the step picker with the types that have an input it may connect to, and the step picked is added with the line as one `flow:node:add` |
|
|
68
|
+
| `flowSettings` | `{title, schema, value}` — the flow's own settings, edited as `flow:settings` |
|
|
69
|
+
| `readOnly` | `false`, or `{reason}`, which is shown above the editor. Every verb is then refused with that reason, and no gesture writes |
|
|
70
|
+
| `actions` | Your toolbar verbs, `[{id, label, icon, primary, danger, title, run(editor)}]`, after Undo, Redo, Arrange and Fit (and the zoom buttons, with `zoomControls`) |
|
|
71
|
+
| `slots` | `toolbarEnd(el)`, `flowPanel(el, editor)` and `stepPanel(el, node, editor)`, drawn by you; `setRunOverlay` redraws the panel slots and nothing else |
|
|
72
|
+
| `onChange`, `onSelect` | After every committed edit. An undo or a redo is a change too (`action: null`, `via: 'undo' \| 'redo'`), because you autosave what is undone. `onSelect({kind, id})`: `step`, `flow`, `connection` (with `connection`) or `null`; with `multiSelect`, two or more picked steps are `{kind: 'many', ids}`; with `notes`, a picked note is `{kind: 'note', id}` and a pick with notes in it `{kind: 'many', ids, notes}` |
|
|
73
|
+
| `multiSelect` (0.17.0) | `true` makes the pick a set (below, *Many picked*). Without it none of it is bound or drawn |
|
|
74
|
+
| `groups`, `notes` (0.17.0) | `true` draws the graph's groups as frames behind their steps and its notes as text on the canvas, and lets the reader make and edit them (below, *Groups and notes*). Without them none of it is bound or drawn, and a graph's groups and notes are kept as they are |
|
|
75
|
+
| `clipboard`, `prepareCopy(node, {from, via})` (0.17.0) | `true` keeps what Ctrl/⌘+C copies inside the editor while it is open; `{read(), write(text)}` hands it to you: `write` hears the copied JSON and `read` (text, or a promise of it) supplies what Ctrl/⌘+V pastes. `prepareCopy` may change a copy's `config` and `label` before it is added (`via`: `'paste'` or `'duplicate'`). Unset, Ctrl/⌘+C, X, V and D are not the editor's (below, *Copy and paste*) |
|
|
76
|
+
| `loop`, `waitsForAll` | The loop's port names (`{entry: 'in', next, body, done}`): a line into a loop-role step on any input but `entry` is its body going back round, drawn dashed under both steps. `waitsForAll(join)` goes to `alwaysBefore` |
|
|
77
|
+
| `removable(node, type)` | Whether a step may be removed. By default every step except a start step |
|
|
78
|
+
| `node` | `{width: 208, header: 34, row: 22}` (`CANVAS_NODE`). Lines are anchored by arithmetic over these numbers, and the stylesheet is held to them |
|
|
79
|
+
| `edgeStyle(connection, {source, target, back})` | How a line is drawn, asked for each line on every paint with copies of its two steps (`back`: it is a loop's line back). Returns `null` or `{tone, dashed, width, marker, label}`: `tone` is one of `TONES`, `width` a stroke width, `marker` `'arrow'` or a crow's-foot kind (`'many'`, `'one'`, `'zero-or-many'`, `'one-or-many'`, `'zero-or-one'`, `'exactly-one'`) drawn at the input end, and `label` text drawn at the line's midpoint, above the steps (a press on it picks the line). A picked or highlighted line still shows as one. Unset, the lines are drawn exactly as before |
|
|
80
|
+
| `routing` | `'simple'` (the default: direct curves) or `'obstacles'`: each line is bent round the steps between its ends by the kernel's `routeEdge`, up to `OBSTACLE_ROUTING_NODE_LIMIT` steps. Above that the lines are direct curves and the findings strip says so (code `canvas-routing-simple`) |
|
|
81
|
+
| `hoverHighlight` | `true`: the pointer over a step gives its lines `twm-flow-graph__edge--highlighted`, and over a line that line; leaving takes it away |
|
|
82
|
+
| `history`, `gridSize`, `label` | `{limit, mergeMs}`, the snap grid, and the canvas's accessible name |
|
|
83
|
+
| `viewport`, `onViewportChange(viewport)` | Where the reader looks, kept by you: the editor stores none of it. `viewport` (`{scale, tx, ty}`, as `getViewport()` gives it) is where the first `load` opens, instead of fitting. `onViewportChange` hears ONE report per finished gesture of the reader's (a pan's release, a run of wheel turns once the wheel has been still for 300 ms, Fit, Arrange, a zoom button, a minimap click, *Go to it*), never one per pointer move, never a gesture that ended where it began, and never what you set yourself (`load`, `setViewport`, `fit()`, `arrange()`). A wheel report still waiting is handed on by the next press and by `destroy` (0.17.0) |
|
|
84
|
+
| `zoomControls` | `true` adds Zoom out, Zoom in and 100% after Fit. They step through 15%, 25%, 33%, 50%, 67%, 80%, 100%, 125%, 150%, 200%, 300% and 400%, about the centre of the view. At the kernel's limits the button is refused in place with its sentence. Off by default (0.17.0) |
|
|
85
|
+
| `renameInPlace` | `true`: a double-click on a step's title edits its name on the node ([Edited on the node](#edited-on-the-node-0170)). Off by default, and then a double-click does nothing (0.17.0) |
|
|
86
|
+
| `collapsible` | `true` puts a fold button on every step's header: it folds the step to its header and back, one `flow:node:collapse` entry each way (below, *Live values and folded steps*). Off by default, and then there is no button in the DOM and a `collapsed` flag in a graph is carried through untouched, the step drawn unfolded (0.17.0) |
|
|
87
|
+
| `minimap` | `true` draws a small overview over the canvas's lower right corner, with the steps and the view's frame, redrawn when the canvas changes size as well as when the view moves. A click on it centres the view on that point, at the same scale. Off by default, and then there is no minimap in the DOM (0.17.0) |
|
|
88
|
+
| `layout` (0.17.0) | What Arrange does. `'flow'` (the default): in the order a run takes the steps, from the start step(s). `'layered'`: the kernel's `layeredLayout` over the lines, with each type's `rank` key (`'source'` or `'sink'`, read by the catalogue's `rank(typeId)`) as its hint. Or your own `fn(graph, ctx)` returning a Map of id → `{x, y}` from the origin (a step it leaves out stays where it is; a place that is not finite numbers `{x, y}`, like anything but a Map, throws), handed a copy of what is arranged and `ctx = {catalogue, node, ids, sizeOf(node), layeredLayout}` (`ids` is `null` for the whole graph). With `multiSelect` and several steps picked, the toolbar's Arrange lays out only those steps and the lines between them, and only they move. The places are anchored at the top-left corner of what is arranged and snapped to the grid |
|
|
89
|
+
|
|
90
|
+
| Method | Does |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `load({graph, flowSettings}, {baseline = true, fit = true, viewport})` | Replaces the content. A baseline clears both stacks. With `baseline: false` the stacks are kept, and the loaded content becomes the present they undo from. With `viewport` it opens there and does not fit |
|
|
93
|
+
| `getViewport()`, `setViewport({scale, tx, ty})` | The kernel's viewport, as a copy; and putting the view there, clamped to the kernel's limits. `setViewport` drops a fit still waiting for its frame, is not reported to `onViewportChange`, and answers `false` (changing nothing) for anything that is not three finite numbers with a scale above 0 (0.17.0) |
|
|
94
|
+
| `getGraph()`, `serialise()`, `snapshot()`, `getFlowSettings()` | A copy of the graph; its byte-stable text; the text the history holds (graph and settings); the settings |
|
|
95
|
+
| `setFindings(list)`, `setRunOverlay(overlay \| null)`, `setReadOnly(false \| {reason})` | — |
|
|
96
|
+
| `setNodeValues({[id]: {badge, tone, ports: {[name]: text}}} \| null)` (0.17.0) | What you show on the steps right now, replacing what you showed before. Painted in place: no undo entry, no `onChange`, nothing rebuilt and the panel untouched (below) |
|
|
97
|
+
| `setCollapsed(id, collapsed)` (0.17.0) | With `collapsible`: fold a step to its header (`true`) or unfold it, one `flow:node:collapse` entry. `false` when refused (no `collapsible`, read only, no such step, or no change) |
|
|
98
|
+
| `setActionState(id, {disabled, reason, busy})`, `setStatus(text)`, `setMessage(text, tone)` | A verb refused in place, with its reason printed beside the toolbar; the status line; the gesture line |
|
|
99
|
+
| `align(kind)`, `distribute(axis)` (0.17.0) | With `multiSelect`: line the picked steps up (`'left'`, `'center'`, `'right'`, `'top'`, `'middle'`, `'bottom'`; two or more picked) or give them equal gaps (`'horizontal'`, `'vertical'`; three or more), one `flow:nodes:align` entry. `false` when refused (too few picked, read only, or no `multiSelect`); an unknown kind or axis throws |
|
|
100
|
+
| `copy()`, `cut()`, `paste(text?)`, `duplicate()` (0.17.0) | With `clipboard`: the keys' verbs from code. Each answers `{ok: true, ids}` (the steps copied, cut or made) or `{ok: false, reason}`, the reason also on the message line; `paste()` answers a promise when your `read` is async, and `paste(text)` pastes that text. Without `clipboard`: `{ok: false, reason: null}` |
|
|
101
|
+
| `select(id \| [id, …] \| 'flow' \| null)`, `repaintPanel()`, `focus()`, `undo()`, `redo()`, `fit({whole})` | `select([id, …])` (0.17.0, `multiSelect`) picks several steps at once (unknown ids are left out; one left is one step picked, none lets go) |
|
|
102
|
+
| `group(ids?, {label, tone})`, `ungroup(groupIds?)`, `setGroup(id, {label, tone})`, `setGroupMembers(id, ids)` (0.17.0) | With `groups`: group these steps (by default the picked ones), each leaving the group it was in, and answer the new group's id (`group`, `group-2`, …) or `null`; remove these groups (by default the ones the picked steps are in), their steps staying put; a group's name and tone; exactly these members, none removing the group. One `flow:group:add`, `…:remove`, `…:label` or `…:members` entry each, `false`/`null` when refused (read only, nothing to do) |
|
|
103
|
+
| `addNote({text, position, size})`, `editNote(id, text)`, `moveNote(id, {x, y})`, `removeNote(id)` (0.17.0) | With `notes`: a note at `position` (by default the centre of the view), answered as a copy (`note`, `note-2`, …) or `null`; its text; where it is; taking it away. One `flow:note:add`, `…:edit`, `…:move` or `…:remove` entry each |
|
|
104
|
+
| `setCatalogue(catalogue \| types)` | Swaps the step types while the editor is open, for a type registered after `load`: the palette is redrawn and a node whose type's definition changed is rebuilt, so a node of a type that was unknown now draws. A definition is compared by what the node is drawn from (known or not, `icon`, `label`, tone, ports, `render`, `update` and `inline`), not by object identity: a new catalogue of the same types plus one more rebuilds no node and calls no `render` again. Under the `connections` option the connection rules and the empty-drop picker judge by the new catalogue, and a pending offer is taken away. The graph and the history are untouched (0.17.0) |
|
|
105
|
+
| `arrange({ids})` | Arrange the whole graph, as the toolbar's button does when fewer than two steps are picked. With `ids` (0.17.0), only those steps and the lines between them are laid out and only they move, still one `flow:arrange` entry; fewer than two known steps, or a read-only editor, answers `false` |
|
|
106
|
+
| `history`, `canvas` | The editor's `FlowHistory` (`keep()`, `adopt(kept, editor.snapshot())`, `baseline(editor.snapshot())` for your publish) and its `CanvasAdapter` |
|
|
107
|
+
| `destroy()` | Removes every listener, the ghost of a drag, the minimap and any picker it opened. It saves nothing, because the editor holds nothing; a viewport report still waiting for the wheel to settle is handed to `onViewportChange` first |
|
|
108
|
+
|
|
109
|
+
Its edits are the closed list `CANVAS_ACTIONS`: `flow:node:add`,
|
|
110
|
+
`flow:node:remove`, `flow:node:move` (one per drag), `flow:edge:connect`,
|
|
111
|
+
`flow:edge:disconnect`, `flow:node:label`, `flow:node:config`,
|
|
112
|
+
`flow:settings`, `flow:arrange` (one entry for the whole layout), and with
|
|
113
|
+
`multiSelect` `flow:nodes:move`, `flow:nodes:remove` and `flow:nodes:align`
|
|
114
|
+
(0.17.0; one entry per gesture on several steps), with `clipboard`
|
|
115
|
+
`flow:paste` and `flow:node:duplicate` (0.17.0; one entry per paste or
|
|
116
|
+
duplicate, however many steps it makes), with `collapsible`
|
|
117
|
+
`flow:node:collapse` (0.17.0), and with `groups` and `notes`
|
|
118
|
+
`flow:group:add`, `flow:group:remove`, `flow:group:label`,
|
|
119
|
+
`flow:group:members`, `flow:note:add`, `flow:note:remove`, `flow:note:edit` and
|
|
120
|
+
`flow:note:move` (0.17.0). Typing a name
|
|
121
|
+
or a setting is one entry. The geometry under it is exported as well, all of it
|
|
122
|
+
pure: `CANVAS_NODE`, `arrangeCanvasGraph(graph, catalogue, {loopEntry})` (where
|
|
123
|
+
Arrange puts each step: a Map of id → `{x, y}` from the origin),
|
|
124
|
+
`canvasPortAnchor`, `canvasNodeBottom` (both fold a node with `collapsed: true`
|
|
125
|
+
to its header, or as the box's `folded(node)` says; 0.17.0), `canvasNodeWidth` (a type's own
|
|
126
|
+
`size.w`, else the box's; 0.17.0), `isCanvasReturnEdge`,
|
|
127
|
+
`canvasPathPoints(d, {steps})` (the points along a drawn line's path `d`:
|
|
128
|
+
absolute `M`, `L`, `C`, `S` and `Z`, each curve sampled at `steps` points) and
|
|
129
|
+
`canvasPathMidpoint(d)` (the point halfway along it by length, where a style's
|
|
130
|
+
`label` sits).
|
|
131
|
+
|
|
132
|
+
### Node bodies (0.17.0)
|
|
133
|
+
|
|
134
|
+
A catalogue type can draw its own controls and values inside its node:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const gauge = {
|
|
138
|
+
type_id: 'gauge', label: 'Gauge', ports, size: { w: 280, h: 170 }, accent: 'teal',
|
|
139
|
+
render(body, node, ctx) { // once, when the node is built
|
|
140
|
+
const range = document.createElement('input');
|
|
141
|
+
range.type = 'range';
|
|
142
|
+
range.addEventListener('change', () => ctx.setConfig({ level: Number(range.value) }));
|
|
143
|
+
body.append(range);
|
|
144
|
+
},
|
|
145
|
+
update(body, node, ctx) { // after render, and on every repaint
|
|
146
|
+
body.querySelector('input').value = node.config.level ?? 50;
|
|
147
|
+
body.querySelector('input').disabled = ctx.readOnly;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
editor.setCatalogue(createStepCatalogue([...types, gauge], { categories })); // after load too
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **The body sits below the ports** and is built once with the node. `update`
|
|
154
|
+
runs on the same element on every repaint, so a control inside it keeps its
|
|
155
|
+
focus and what was typed into it. `node` is a copy: the body writes only
|
|
156
|
+
through `ctx`. A later `setCatalogue` keeps that body too, unless the type's
|
|
157
|
+
own definition changed (its ports, `label`, `icon`, category tone, `render`
|
|
158
|
+
or `update`): then the node is built again and `render` runs again. A new
|
|
159
|
+
`size` or `accent` is applied in place.
|
|
160
|
+
- **`ctx`** is `{setConfig(patch), readOnly, values, services}`. `values` is
|
|
161
|
+
what `setNodeValues` shows on this step now (`{badge, tone, ports}`, a copy),
|
|
162
|
+
or `null`. `setConfig` merges
|
|
163
|
+
`patch` into the node's config (an `undefined` value removes the key) as one
|
|
164
|
+
`flow:node:config` edit: one undo entry, and `onChange` is called. It is
|
|
165
|
+
refused (answers `false`) while the editor is read only, from inside
|
|
166
|
+
`render` or `update`, and when a value in `patch` cannot be cloned (a
|
|
167
|
+
function, a DOM node): then nothing is written. `services` is the editor's `services` option.
|
|
168
|
+
- **`size: {w, h}`** is the node's box in pixels; either may be left out. Lines
|
|
169
|
+
leave a node's outputs at its own width, and a node with `h` is exactly that
|
|
170
|
+
tall, its body taking what is left. A body node without `h` is as tall as
|
|
171
|
+
it is drawn. Arrange spaces its columns by the widest node.
|
|
172
|
+
- **`accent`** is a tone (`violet`, `teal`, `amber`, `grey`, `blue`, `indigo`),
|
|
173
|
+
drawn as a stripe over the node's header.
|
|
174
|
+
- **A `render` that throws** leaves that node drawn as a type without a body
|
|
175
|
+
is, and what it threw is one of the editor's own findings: on the node and
|
|
176
|
+
in the strip, beside yours from `setFindings`. An `update` that throws keeps
|
|
177
|
+
the body and says so the same way until it succeeds. No other node is
|
|
178
|
+
affected.
|
|
179
|
+
- A type without these keys is drawn exactly as before, with the same markup,
|
|
180
|
+
and `serialise()` is unchanged.
|
|
181
|
+
|
|
182
|
+
### Edited on the node (0.17.0)
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
const counter = { type_id: 'counter', label: 'Counter', ports, inline: ['count'],
|
|
186
|
+
config_schema: { type: 'object', properties: { count: { type: 'number' } } } };
|
|
187
|
+
const editor = createCanvasEditor(host, { catalogue: [counter], renameInPlace: true });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- **`renameInPlace: true`.** A double-click on a step's title puts a text box
|
|
191
|
+
in its place, the name selected. Enter, or leaving the box, is one
|
|
192
|
+
`flow:node:label` edit, and none when the name is unchanged. Escape puts the
|
|
193
|
+
name back and records nothing. An empty name is refused with a sentence on
|
|
194
|
+
the gesture line and the name stays: on Enter the box stays open
|
|
195
|
+
(`aria-invalid`), on leaving it the box closes. Read only, a double-click
|
|
196
|
+
opens nothing, and going read only closes an open box without writing. The
|
|
197
|
+
settings panel, if it is showing that step, shows the new name.
|
|
198
|
+
- **A type's `inline` settings** are drawn under the node's ports, above its
|
|
199
|
+
own body, by the same widget registry as the panel (`widgets`), with the
|
|
200
|
+
same labels and controls. A change on the node is a `flow:node:config` edit,
|
|
201
|
+
and the panel shows it at once. A change in the panel, or an undo, redraws
|
|
202
|
+
the node's control. The control being typed in is never redrawn under the
|
|
203
|
+
reader; a focused control whose value changes elsewhere (an undo pressed
|
|
204
|
+
while it holds the focus) is redrawn with its focus and caret kept. Typing
|
|
205
|
+
on the node and then in the panel are two undo entries. Read only disables
|
|
206
|
+
the node's controls. A node with inline settings and no
|
|
207
|
+
`size.h` is as tall as it is drawn.
|
|
208
|
+
- Without the option and the key nothing is bound or drawn: the same markup,
|
|
209
|
+
and `serialise()` is unchanged.
|
|
210
|
+
|
|
211
|
+
### Live values and folded steps (0.17.0)
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
const editor = createCanvasEditor(host, { catalogue, collapsible: true });
|
|
215
|
+
editor.setNodeValues({ mix: { badge: '0.42', tone: 'teal', ports: { out: '0.42' } } }); // as often as you like
|
|
216
|
+
editor.setNodeValues(null); // none shown
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- **`setNodeValues`** paints what you are showing right now: `badge` is drawn
|
|
220
|
+
on the step's header, before its id tag, in a tone of `TONES` when `tone`
|
|
221
|
+
names one, and each `ports[name]` beside that port. It replaces what the
|
|
222
|
+
last call showed, and a step it leaves out shows nothing. It extends the run
|
|
223
|
+
overlay: nothing is rebuilt or recorded, `onChange` is not called, and the
|
|
224
|
+
settings panel is not touched, so a field being typed into keeps its focus
|
|
225
|
+
and its selection however often you call it. A type with its own body reads
|
|
226
|
+
them as `ctx.values`, and its `update` runs for each step whose values
|
|
227
|
+
changed (a `setConfig` from there is refused, as from any `update`). The
|
|
228
|
+
badge and port-value elements exist only while there is a value to show;
|
|
229
|
+
the editor keeps nothing of them past the next call.
|
|
230
|
+
- **`collapsible: true`** puts a fold button on each header. A folded step
|
|
231
|
+
shows its header only: its port rows, its body and what is drawn under them
|
|
232
|
+
are hidden, and its lines stay, meeting the middle of the header on its left
|
|
233
|
+
(lines in) and its right (lines out). Each fold or unfold is one
|
|
234
|
+
`flow:node:collapse` entry, so Ctrl+Z unfolds a step just folded. The graph
|
|
235
|
+
carries `collapsed: true` on a folded step, written last in its `serialise()`
|
|
236
|
+
text; an unfolded step has no `collapsed` key, so a graph that never folded
|
|
237
|
+
a step is the text it always was. Read only refuses it.
|
|
238
|
+
|
|
239
|
+
**Gestures.** A node's header moves it. A connection is made by clicking an
|
|
240
|
+
output and then an input, and Escape cancels one in progress. Under the
|
|
241
|
+
`connections` option an output can also be dragged to an input, or onto the
|
|
242
|
+
empty canvas to pick a step that arrives connected. A line is picked
|
|
243
|
+
by clicking it, and Delete removes it or the picked step. A palette step is
|
|
244
|
+
added by clicking it (at the centre of the view) or by dragging it onto the
|
|
245
|
+
canvas (where it is dropped). The drag uses pointer events and a ghost on
|
|
246
|
+
`document.body`. It never uses `draggable`, which cancels the pointer after
|
|
247
|
+
about 5px, and never `setPointerCapture`, which would send the release, and so
|
|
248
|
+
a second click, to the palette button. No press rebuilds a node, so a click lands
|
|
249
|
+
on the node it went down on. Arrange lays the run out left to right, a branch's
|
|
250
|
+
arms in their declared order, from the graph's own top-left corner. Fit frames
|
|
251
|
+
the graph at natural size or smaller; pressing Fit frames all of it.
|
|
252
|
+
|
|
253
|
+
**Many picked** (`multiSelect: true`, 0.17.0). A left drag on the empty canvas
|
|
254
|
+
draws a marquee and picks every step it touches; Shift-, Ctrl- or ⌘-click on
|
|
255
|
+
a step adds it to the pick or takes it away; Ctrl/⌘+A picks every step and
|
|
256
|
+
Escape lets go. `select([id, …])` picks several from code. Dragging any picked
|
|
257
|
+
step moves them all (`flow:nodes:move`), Delete removes every one that may be
|
|
258
|
+
removed, with their lines (`flow:nodes:remove`; a start step stays), and the
|
|
259
|
+
panel says how many are picked, with their Remove. On the canvas the arrow
|
|
260
|
+
keys nudge the pick one grid step (`gridSize`), ten with Shift; a held arrow is
|
|
261
|
+
one entry. Eight icon verbs after Fit align the pick's left edges, centres,
|
|
262
|
+
right edges, tops, middles and bottoms, and distribute it horizontally and
|
|
263
|
+
vertically, each refused in place with fewer than two (three) picked. Arrange
|
|
264
|
+
lays out only the pick and the lines between them, and nothing else moves. Tab and
|
|
265
|
+
Shift+Tab move the focus from step to step in graph order, and past either end
|
|
266
|
+
Tab is the browser's again, so the keyboard can leave the canvas. Every one of
|
|
267
|
+
these is a single undo entry. In read-only mode a marquee still picks, and no
|
|
268
|
+
gesture writes.
|
|
269
|
+
|
|
270
|
+
**Groups and notes** (`groups: true`, `notes: true`, 0.17.0). A group is a
|
|
271
|
+
named frame, in one of `TONES` if it has a `tone`, drawn behind its steps and
|
|
272
|
+
round all of them wherever they are. Its title bar is its handle: dragging it
|
|
273
|
+
moves every member (one `flow:nodes:move`; with `multiSelect` the press picks
|
|
274
|
+
them), and a double-click renames it in place (Enter keeps, Escape puts it
|
|
275
|
+
back). Groups are flat and a step is in one at most: Ctrl/⌘+G (or Group on the
|
|
276
|
+
toolbar) groups the picked steps, taking each out of the group it was in, and
|
|
277
|
+
a group left empty goes; Ctrl/⌘+Shift+G (or Ungroup) removes the groups the
|
|
278
|
+
picked steps are in. Removing a step shrinks its group, and removing its last
|
|
279
|
+
member removes the group. Between the steps the frame takes no pointer, so a
|
|
280
|
+
marquee or a pan can start inside it. A note is free text on the canvas: Note
|
|
281
|
+
on the toolbar adds one at the centre of the view and opens it for typing; a
|
|
282
|
+
double-click, or Enter on a focused note, opens it again (Ctrl/⌘+Enter or
|
|
283
|
+
leaving the box keeps the text, Escape puts it back); dragging it moves it,
|
|
284
|
+
and Delete removes the picked one. With `multiSelect` a marquee picks notes
|
|
285
|
+
with the steps, Shift/Ctrl+click adds or takes one away, Ctrl/⌘+A picks them
|
|
286
|
+
too, and dragging or deleting the pick takes its notes along, one entry. A
|
|
287
|
+
graph keeps them as `groups` and `notes` (see `normalise` above) and
|
|
288
|
+
`serialise()` writes them after the connections; a graph without any is the
|
|
289
|
+
same text as before.
|
|
290
|
+
|
|
291
|
+
**Copy and paste** (`clipboard`, 0.17.0). Ctrl/⌘+C copies the picked steps
|
|
292
|
+
and the lines among them; a line to a step left out of the pick is not copied,
|
|
293
|
+
nor is a step whose type the palette does not offer (a start step) or that is
|
|
294
|
+
`unavailable`. What is copied is the graph's own text, `{nodes, connections}`
|
|
295
|
+
as `serialise()` writes it, so a graph from elsewhere pastes too. Ctrl/⌘+V
|
|
296
|
+
gives every pasted step a fresh id (as a new step gets one), keeps the lines
|
|
297
|
+
between them (data lines too, under `connections` with `dataPorts: 'connect'`, from 0.17.1), sets the copy off two grid steps right and down (each further
|
|
298
|
+
paste of the same text one more), picks it, and is one `flow:paste` entry. A
|
|
299
|
+
paste right after Ctrl/⌘+X puts the steps back where they were; a cut is one
|
|
300
|
+
remove entry. A step whose title the flow already has gets a derived label,
|
|
301
|
+
`Title (copy)`, then `Title (copy 2)`, …, as the lanes editor's duplicate does.
|
|
302
|
+
Ctrl/⌘+D does the same beside the pick without touching the clipboard, as one
|
|
303
|
+
`flow:node:duplicate`. With nothing picked Ctrl/⌘+C, X and D are not the
|
|
304
|
+
editor's. What is not steps, or names a type the flow does not have, is refused
|
|
305
|
+
with its sentence on the message line. In read-only mode copying still works,
|
|
306
|
+
and Paste, Cut and Duplicate are refused with the read-only reason and change
|
|
307
|
+
nothing. With `notes` the picked notes are copied, cut and duplicated with the
|
|
308
|
+
steps (a note alone too), and pasted under fresh ids, set off as the steps
|
|
309
|
+
are; with `groups` a group whose every member is copied is in the copy, and a
|
|
310
|
+
paste makes it again round the copies of its members (a fresh id, its name and
|
|
311
|
+
tone kept), while a group with a member left out is not copied. `copy()` and
|
|
312
|
+
`cut()` answer `notes: [id, …]` and `paste()` and `duplicate()` answer `notes`
|
|
313
|
+
and `groups`, each only when there are some.
|
|
314
|
+
|
|
315
|
+
**Keys** (bound on the editor's root): Ctrl/⌘+Z and Ctrl/⌘+Y or Ctrl/⌘+Shift+Z,
|
|
316
|
+
stopped at the root even inside a text field; Delete; F2 renames the picked
|
|
317
|
+
step; Enter on a focused node opens its settings; Escape; with `groups`,
|
|
318
|
+
Ctrl/⌘+G and Ctrl/⌘+Shift+G. Backspace is never the editor's.
|
|
319
|
+
|
|
320
|
+
**What it does not do.** It does not load, save, validate, publish or run
|
|
321
|
+
anything, and it does not keep the stacks across page loads. Those belong to
|
|
322
|
+
the consumer. Nor does it add anything a flow must have: a consumer whose new
|
|
323
|
+
flow opens on a start step adds that step to the graph before `load`, so it is
|
|
324
|
+
part of the baseline and the first Ctrl+Z does not take it away.
|
|
325
|
+
|
|
326
|
+
**Where each claim was checked.** `tests/canvas_adapter.test.mjs`,
|
|
327
|
+
`canvas_edge_router`, `canvas_graph`, `flow_canvas_layout`, `flow_canvas_editor`,
|
|
328
|
+
`flow_canvas_multiselect`, `flow_canvas_clipboard`, `flow_canvas_node_body`, `flow_canvas_live_values`, `flow_canvas_groups_notes` and `flow_canvas_css` check the DOM, the events, the
|
|
329
|
+
geometry and the stylesheet as text. In headless Edge, `demo/flow_canvas_probe.mjs` drives
|
|
330
|
+
`demo/flow_canvas.html` with real input:
|
|
331
|
+
- `elementFromPoint` at the centre of Undo, Arrange, Fit, a consumer verb, a
|
|
332
|
+
palette item, a node's header, a port, and the panel's name box, first field
|
|
333
|
+
and Remove step returned each one, and the panel was not covered.
|
|
334
|
+
- Every line began and ended within 1px of its ports' dots, measured.
|
|
335
|
+
- The loop's line back ran below both of its steps.
|
|
336
|
+
- A real pointer drag from the palette added exactly one step, under the
|
|
337
|
+
pointer, with a `dragstart` counter of 0. A drag ending off the canvas added
|
|
338
|
+
nothing.
|
|
339
|
+
- A real click-then-click connected two steps.
|
|
340
|
+
- A header drag moved its node, and a real Ctrl+Z put it back. Backspace removed
|
|
341
|
+
nothing, Delete removed the step, and the focus stayed on the canvas.
|
|
342
|
+
- On a 300-step graph with `multiSelect`, a real marquee picked three steps, a
|
|
343
|
+
real drag of one moved all three and one real Ctrl+Z put them back; a move
|
|
344
|
+
of 50 picked steps and its undo each finished in under 100 ms.
|
|
345
|
+
- With `groups` and `notes`, a real Ctrl+G on two picked steps drew one frame
|
|
346
|
+
round both and not round the third. Its title bar, and a step inside it, were
|
|
347
|
+
each the element at their own centre, and between the steps the canvas was
|
|
348
|
+
(the frame takes no pointer). A real drag of the title bar moved both by the
|
|
349
|
+
pointer's travel as one entry, and one real Ctrl+Z put them back. A real
|
|
350
|
+
double-click opened a note for typing, real keys and Ctrl+Enter kept the
|
|
351
|
+
text, a real drag moved it, and a real marquee picked the steps and the note
|
|
352
|
+
together.
|
|
353
|
+
- With `collapsible`, ten `setNodeValues` calls 100 ms apart each put their
|
|
354
|
+
value on the step's badge, inside its header, while real keys typed into a
|
|
355
|
+
settings field: the field kept the focus and every key, and the values made
|
|
356
|
+
no undo entry and no `onChange`. A real click on a header's fold button hid
|
|
357
|
+
every port row and left the step as tall as its header, with its lines
|
|
358
|
+
ending on the header's middle at its edges, within 1px; a real Ctrl+Z
|
|
359
|
+
unfolded it and its lines met their port dots again.
|
|
360
|
+
- A type registered with `setCatalogue` after the page opened was in the
|
|
361
|
+
palette. Its node was drawn at its 280×170 size, with its body below its
|
|
362
|
+
ports and its accent. Every line into and out of it began and ended on its
|
|
363
|
+
port dots, within 1px.
|
|
364
|
+
- At 700px the canvas took the editor's whole width.
|
|
365
|
+
|
|
366
|
+
`tests/flow_canvas_edge_style.test.mjs` checks `edgeStyle`, `routing` and
|
|
367
|
+
`hoverHighlight` in the DOM, and `demo/flow_canvas_edges_probe.mjs` in headless
|
|
368
|
+
Edge:
|
|
369
|
+
- A dashed line had a computed dash pattern, and an unstyled line kept its old
|
|
370
|
+
stroke and width.
|
|
371
|
+
- An arrowhead's tip sat on the input port's centre line, and the line ended at
|
|
372
|
+
its base.
|
|
373
|
+
- A label sat on its line's midpoint over a step, and `elementFromPoint` there
|
|
374
|
+
returned the label.
|
|
375
|
+
- With `routing: 'obstacles'`, no point of a line lay inside the drawn box of
|
|
376
|
+
the step between its ends. Above the node limit the strip said so.
|
|
377
|
+
- A real pointer over a step highlighted its lines, and moving off cleared them.
|
|
378
|
+
|
|
379
|
+
### Building a modeler (0.17.1)
|
|
380
|
+
|
|
381
|
+
A typed node modeler (a dataflow, a signal chain, a diagram of blocks) is the
|
|
382
|
+
canvas editor with its modeler options turned on. Every one of them is a
|
|
383
|
+
`createCanvasEditor` option or a key on a catalogue type, off by default: an
|
|
384
|
+
editor that sets none of them draws, binds and serialises exactly as before.
|
|
385
|
+
None of them stores anything: the graph, the viewport and the values shown are
|
|
386
|
+
the host's, handed in and handed back. Each edit they add is on the closed list
|
|
387
|
+
`CANVAS_ACTIONS`, so one gesture is one undo entry. `demo/flow_modeler.html`
|
|
388
|
+
(linked from `demo/index.html`) uses all of them together on a signal-flow
|
|
389
|
+
diagram of sources, gains, a sum and displays.
|
|
390
|
+
|
|
391
|
+
| Option | What it gives | Details |
|
|
392
|
+
|---|---|---|
|
|
393
|
+
| `render`, `update`, `size`, `accent` (type keys) | A node that draws its own controls and values, at its own size, marked in a tone; `setCatalogue` adds a type while the editor is open | [Node bodies](#node-bodies-0170) |
|
|
394
|
+
| `connections` (`dataPorts`, `policy`) with `accepts` / `provides` (port keys) | Data ports that connect, typed, under the host's rules, by drag or click; an empty drop picks a step that arrives connected | `connections` above |
|
|
395
|
+
| `multiSelect` | Marquee, bulk move and delete, nudge, align and distribute | *Many picked* above |
|
|
396
|
+
| `clipboard`, `prepareCopy` | Copy, cut, paste and duplicate of the picked steps and the lines among them | *Copy and paste* above |
|
|
397
|
+
| `groups`, `notes` | Flat named frames round steps, and free-text notes | *Groups and notes* above |
|
|
398
|
+
| `layout`, `rank` (type key) | Arrange by the kernel's layered layout, sources first and sinks last, or your own | `layout` above |
|
|
399
|
+
| `edgeStyle`, `routing`, `hoverHighlight` | Lines in a tone, dashed, wider, with an arrow or a crow's foot and a label; bent round steps; lit while pointed at | `edgeStyle` above |
|
|
400
|
+
| `viewport`, `onViewportChange`, `zoomControls`, `minimap` | Where the reader looks, kept by you; zoom buttons; an overview | `viewport` above |
|
|
401
|
+
| `renameInPlace`, `inline` (type key) | A step's name and chosen settings edited on the node | [Edited on the node](#edited-on-the-node-0170) |
|
|
402
|
+
| `setNodeValues`, `collapsible` | Values painted on the steps as often as you like; steps folded to their header | [Live values and folded steps](#live-values-and-folded-steps-0170) |
|
|
403
|
+
|
|
404
|
+
A type draws its own body (`render` once, `update` on every repaint), with its
|
|
405
|
+
own `size` and `accent`; `inline` draws chosen settings on the node, and
|
|
406
|
+
`rank` places it when Arrange runs the layered layout:
|
|
407
|
+
|
|
408
|
+
```js
|
|
409
|
+
const IN = { name: 'in', direction: 'input', port_type: 'DATA', accepts: ['number'], maxConnections: 1 };
|
|
410
|
+
const OUT = { name: 'out', direction: 'output', port_type: 'DATA', provides: ['number'] };
|
|
411
|
+
const types = [
|
|
412
|
+
{ type_id: 'source', label: 'Source', rank: 'source', inline: ['amplitude'], ports: [OUT],
|
|
413
|
+
config_schema: { type: 'object', properties: { amplitude: { type: 'number', title: 'Amplitude' } } } },
|
|
414
|
+
{ type_id: 'gain', label: 'Gain', size: { w: 224, h: 132 }, accent: 'amber', ports: [IN, OUT],
|
|
415
|
+
render(body, node, ctx) {
|
|
416
|
+
const range = Object.assign(document.createElement('input'), { type: 'range', min: -2, max: 2, step: 0.1 });
|
|
417
|
+
range.addEventListener('change', () => ctx.setConfig({ k: Number(range.value) })); // one undo entry
|
|
418
|
+
body.append(range);
|
|
419
|
+
},
|
|
420
|
+
update(body, node, ctx) { body.querySelector('input').value = node.config.k ?? 1; } },
|
|
421
|
+
{ type_id: 'display', label: 'Display', rank: 'sink', ports: [IN],
|
|
422
|
+
render: (body) => body.append(document.createElement('output')),
|
|
423
|
+
update: (body, node, ctx) => { body.querySelector('output').textContent = ctx.values?.badge ?? '—'; } },
|
|
424
|
+
];
|
|
425
|
+
editor.setCatalogue(createStepCatalogue([...types, laterType], { categories })); // a type added after load
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
The editor's options, each one independent of the others:
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
const editor = createCanvasEditor(host, {
|
|
432
|
+
catalogue: createStepCatalogue(types, { categories }),
|
|
433
|
+
connections: { // data ports connect, typed by accepts/provides
|
|
434
|
+
dataPorts: 'connect',
|
|
435
|
+
policy: ({ connection, graph }) => (closesLoop(graph, connection)
|
|
436
|
+
? { ok: false, reason: 'That line would feed a block its own output.' } : null),
|
|
437
|
+
},
|
|
438
|
+
multiSelect: true, // marquee, bulk move, align, distribute
|
|
439
|
+
clipboard: true, // or {read(), write(text)} to hold it yourself
|
|
440
|
+
prepareCopy: (copy, { from, via }) => { copy.config.phase = 0; },
|
|
441
|
+
groups: true, notes: true, // Ctrl+G frames, free-text notes
|
|
442
|
+
layout: 'layered', // Arrange: columns by the lines, `rank` hints
|
|
443
|
+
edgeStyle: (c, { source, target }) => ({ tone: 'teal', marker: 'arrow', width: 2,
|
|
444
|
+
dashed: c.targetPort === 'b', label: source.label }),
|
|
445
|
+
routing: 'obstacles', hoverHighlight: true,
|
|
446
|
+
viewport: savedView, // opens there instead of fitting
|
|
447
|
+
onViewportChange: (view) => { savedView = view; }, // once per finished pan or zoom
|
|
448
|
+
zoomControls: true, minimap: true,
|
|
449
|
+
renameInPlace: true, // double-click a title to rename it
|
|
450
|
+
collapsible: true, // a fold button on every header
|
|
451
|
+
});
|
|
452
|
+
editor.load({ graph }, { viewport: savedView });
|
|
453
|
+
setInterval(() => editor.setNodeValues({ mix: { badge: '0.42', tone: 'teal', ports: { out: '0.42' } } }), 250);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
`setNodeValues` records nothing and leaves the focus where it is, so a host
|
|
457
|
+
can paint as often as its values change.
|
|
458
|
+
|
|
459
|
+
**Where it was checked.** `tests/flow_modeler_demo.test.mjs` mounts the demo
|
|
460
|
+
under jsdom: what it shows, its live badges, and this section's list against
|
|
461
|
+
the options. `tests/flow_kit_vocabulary.test.mjs` §4 holds every flow and
|
|
462
|
+
canvas source the demo imports, and the demo itself, to the vocabulary gate.
|
|
463
|
+
In headless Edge, `demo/flow_modeler_probe.mjs` opens the page by a real click
|
|
464
|
+
on its link in `demo/index.html` and drives it with real input:
|
|
465
|
+
- The bodies were drawn at their own size below their ports; every line left
|
|
466
|
+
its output dot and its arrowhead ended on its input dot, within 1px; the
|
|
467
|
+
group frame was round both sources and nothing else; a dashed line had a
|
|
468
|
+
dash pattern and a label was the element at its own centre.
|
|
469
|
+
- The badges changed in each of three seconds while a settings field, focused
|
|
470
|
+
by a real click, kept the focus, and nothing was recorded.
|
|
471
|
+
- A real drag from an output to an input added one line, dot to dot, as one
|
|
472
|
+
undo entry. The gesture line appears above the canvas when a drag starts and
|
|
473
|
+
moves the canvas down by its height, so the probe aims at the input where it
|
|
474
|
+
is drawn once the line has started, as a reader would.
|
|
475
|
+
- A real click and Shift+click and a real Ctrl+G drew a second frame round
|
|
476
|
+
exactly the two steps; real Ctrl+C and Ctrl+V put two copies two grid steps
|
|
477
|
+
right of and below the originals, with the line between them; a real click
|
|
478
|
+
on Arrange ran every line left to right, the sources first and the displays
|
|
479
|
+
last, with no two steps overlapping. Each was one undo entry.
|
|
480
|
+
- A real Ctrl+Z after each, last first, put the steps back where they were on
|
|
481
|
+
the canvas, took the copies, the second frame and the dragged line away, and
|
|
482
|
+
left the graph the text it was before each step.
|