flexdesk 0.16.0 → 0.17.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/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2261 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3373 -238
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +709 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/chunk-UWO43BZU.js +111 -0
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3373 -238
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +1131 -253
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3254 -2112
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -345
- package/dist/wm.js.map +4 -4
- package/package.json +3 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +422 -0
- package/src/tiles/scope_picker.js +96 -0
- package/src/tiles/tile_base.js +12 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +38 -1
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +211 -0
- package/src/ui/components/cell_grid.js +648 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +601 -62
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +26 -4
- package/src/ui/components/staged_edits.js +343 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-CWPMLX67.js +0 -475
- package/dist/chunk-CWPMLX67.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
package/dist/flow.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
2
|
modalHost
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-D3COLZZ2.js";
|
|
4
4
|
import {
|
|
5
5
|
DataTable
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-6JSOVNID.js";
|
|
8
|
-
import "./chunk-FL5KFNQH.js";
|
|
6
|
+
} from "./chunk-Q36EPT2R.js";
|
|
9
7
|
import {
|
|
8
|
+
choiceList,
|
|
9
|
+
selectBox,
|
|
10
|
+
shownValue,
|
|
11
|
+
toggleChips
|
|
12
|
+
} from "./chunk-CVMCZZX2.js";
|
|
13
|
+
import "./chunk-HNU6MM2B.js";
|
|
14
|
+
import "./chunk-WVFGV5FT.js";
|
|
15
|
+
import {
|
|
16
|
+
CONNECTOR_STRINGS,
|
|
10
17
|
CanvasAdapter,
|
|
18
|
+
ConnectorRouter,
|
|
19
|
+
NodePlatform,
|
|
20
|
+
OBSTACLE_ROUTING_NODE_LIMIT,
|
|
11
21
|
arrange,
|
|
22
|
+
crowsFoot,
|
|
23
|
+
footAnchor,
|
|
24
|
+
layeredLayout,
|
|
12
25
|
returnEdge,
|
|
13
26
|
routeEdge
|
|
14
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-GOQLR5WR.js";
|
|
15
28
|
import {
|
|
16
29
|
__export
|
|
17
30
|
} from "./chunk-JYWURG5T.js";
|
|
@@ -56,7 +69,9 @@ var FLOW_STRINGS = Object.freeze({
|
|
|
56
69
|
value: "Value",
|
|
57
70
|
formula: "Formula",
|
|
58
71
|
textOrValue: "Text, or insert a value",
|
|
59
|
-
noColumns: "
|
|
72
|
+
noColumns: "Columns not known yet: type the name.",
|
|
73
|
+
columnName: "Column name",
|
|
74
|
+
columnNames: "Column name, then Enter",
|
|
60
75
|
noColumnsChosen: "None chosen",
|
|
61
76
|
fieldFinding: "Fix this before publishing",
|
|
62
77
|
// ── reference chips and Insert a value ─────────────────────────────
|
|
@@ -274,8 +289,11 @@ var UI_HINTS = Object.freeze([
|
|
|
274
289
|
"x-ui-when",
|
|
275
290
|
"x-ui-placeholder",
|
|
276
291
|
"x-ui-enum-labels",
|
|
292
|
+
"x-ui-enum-descriptions",
|
|
277
293
|
"x-ui-multiline",
|
|
278
|
-
"x-ui-fold"
|
|
294
|
+
"x-ui-fold",
|
|
295
|
+
"x-ui-add-label",
|
|
296
|
+
"x-ui-formula-references"
|
|
279
297
|
]);
|
|
280
298
|
var isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
281
299
|
function pyRepr(value) {
|
|
@@ -381,6 +399,35 @@ function defaultIdBase(type) {
|
|
|
381
399
|
function cleanStem(stem) {
|
|
382
400
|
return String(stem ?? "").replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 40) || "step";
|
|
383
401
|
}
|
|
402
|
+
var positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
403
|
+
function readSize(raw) {
|
|
404
|
+
if (raw === void 0 || raw === null) return { size: null, problem: null };
|
|
405
|
+
const refused = { size: null, problem: "size must be {w, h} in positive pixels; it is not used." };
|
|
406
|
+
if (!isPlainObject2(raw)) return refused;
|
|
407
|
+
const w = raw.w === void 0 ? null : positive(raw.w);
|
|
408
|
+
const h = raw.h === void 0 ? null : positive(raw.h);
|
|
409
|
+
if (raw.w !== void 0 && w === null || raw.h !== void 0 && h === null || w === null && h === null) {
|
|
410
|
+
return refused;
|
|
411
|
+
}
|
|
412
|
+
return { size: Object.freeze({ w, h }), problem: null };
|
|
413
|
+
}
|
|
414
|
+
function readInline(raw, schema, id, problems) {
|
|
415
|
+
if (raw === void 0 || raw === null) return Object.freeze([]);
|
|
416
|
+
if (!Array.isArray(raw)) {
|
|
417
|
+
problems.push({ type_id: id, message: `"${id}"'s inline must be a list of setting names; it is not used.` });
|
|
418
|
+
return Object.freeze([]);
|
|
419
|
+
}
|
|
420
|
+
const known = new Set(fieldsFromSchema(schema).map((f) => f.key));
|
|
421
|
+
const keys = [];
|
|
422
|
+
for (const key of raw) {
|
|
423
|
+
if (typeof key === "string" && known.has(key)) {
|
|
424
|
+
if (!keys.includes(key)) keys.push(key);
|
|
425
|
+
} else {
|
|
426
|
+
problems.push({ type_id: id, message: `"${id}"'s inline names "${String(key)}", which its settings do not have; it is not drawn.` });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return Object.freeze(keys);
|
|
430
|
+
}
|
|
384
431
|
function normalisePort(p) {
|
|
385
432
|
return {
|
|
386
433
|
...p,
|
|
@@ -396,6 +443,10 @@ function createStepCatalogue(types, { categories = [], idBase = defaultIdBase }
|
|
|
396
443
|
const problems = [];
|
|
397
444
|
const byId = /* @__PURE__ */ new Map();
|
|
398
445
|
const order = [];
|
|
446
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
447
|
+
const accents = /* @__PURE__ */ new Map();
|
|
448
|
+
const inlines = /* @__PURE__ */ new Map();
|
|
449
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
399
450
|
for (const raw of Array.isArray(types) ? types : []) {
|
|
400
451
|
if (!isPlainObject2(raw)) {
|
|
401
452
|
problems.push({ type_id: null, message: "A step type must be an object." });
|
|
@@ -434,6 +485,24 @@ function createStepCatalogue(types, { categories = [], idBase = defaultIdBase }
|
|
|
434
485
|
});
|
|
435
486
|
byId.set(id, type);
|
|
436
487
|
order.push(type);
|
|
488
|
+
const { size, problem } = readSize(raw.size);
|
|
489
|
+
if (problem) problems.push({ type_id: id, message: `"${id}"'s ${problem}` });
|
|
490
|
+
if (size) sizes.set(id, size);
|
|
491
|
+
if (raw.accent !== void 0 && raw.accent !== null) {
|
|
492
|
+
if (TONES.includes(raw.accent)) accents.set(id, raw.accent);
|
|
493
|
+
else problems.push({ type_id: id, message: `"${id}"'s accent "${raw.accent}" is not a tone; it is not used.` });
|
|
494
|
+
}
|
|
495
|
+
const inline = readInline(raw.inline, schema, id, problems);
|
|
496
|
+
if (inline.length) inlines.set(id, inline);
|
|
497
|
+
if (raw.rank !== void 0 && raw.rank !== null) {
|
|
498
|
+
if (raw.rank === "source" || raw.rank === "sink") ranks.set(id, raw.rank);
|
|
499
|
+
else problems.push({ type_id: id, message: `"${id}"'s rank "${String(raw.rank)}" is not 'source' or 'sink'; it is not used.` });
|
|
500
|
+
}
|
|
501
|
+
for (const hook of ["render", "update"]) {
|
|
502
|
+
if (raw[hook] !== void 0 && typeof raw[hook] !== "function") {
|
|
503
|
+
problems.push({ type_id: id, message: `"${id}"'s ${hook} is not a function; it is not used.` });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
437
506
|
}
|
|
438
507
|
const cats = (Array.isArray(categories) ? categories : []).filter(isPlainObject2).map((c) => Object.freeze({
|
|
439
508
|
id: String(c.id),
|
|
@@ -452,11 +521,11 @@ function createStepCatalogue(types, { categories = [], idBase = defaultIdBase }
|
|
|
452
521
|
extra.push(Object.freeze({ id: "", label: "", tone: "grey", layout: "list" }));
|
|
453
522
|
}
|
|
454
523
|
const allCats = Object.freeze([...cats, ...extra]);
|
|
455
|
-
const
|
|
524
|
+
const categoryIndex = (t) => {
|
|
456
525
|
const id = t.category === null ? "" : t.category;
|
|
457
526
|
return allCats.findIndex((c) => c.id === id);
|
|
458
527
|
};
|
|
459
|
-
const sorted2 = [...order].sort((a, b) =>
|
|
528
|
+
const sorted2 = [...order].sort((a, b) => categoryIndex(a) - categoryIndex(b) || String(a.label).localeCompare(String(b.label)));
|
|
460
529
|
const portsOf2 = (typeId, direction, flow) => {
|
|
461
530
|
const t = byId.get(typeId);
|
|
462
531
|
if (!t) return [];
|
|
@@ -478,6 +547,20 @@ function createStepCatalogue(types, { categories = [], idBase = defaultIdBase }
|
|
|
478
547
|
const t = byId.get(typeId);
|
|
479
548
|
return t && allCats.find((c) => c.id === (t.category ?? ""))?.tone || "grey";
|
|
480
549
|
},
|
|
550
|
+
/** The type's own box, `{w, h}` (either null), or null when it has none. */
|
|
551
|
+
size: (typeId) => sizes.get(typeId) ?? null,
|
|
552
|
+
/** The type's accent tone, or null when it has none. */
|
|
553
|
+
accent: (typeId) => accents.get(typeId) ?? null,
|
|
554
|
+
/** The setting names the type draws on its node (`inline`), in order; `[]` when none. */
|
|
555
|
+
inline: (typeId) => inlines.get(typeId) ?? Object.freeze([]),
|
|
556
|
+
/** The type's layered-layout hint, 'source' or 'sink', or null when it has none. */
|
|
557
|
+
rank: (typeId) => ranks.get(typeId) ?? null,
|
|
558
|
+
/** The type's `{render, update}` (`update` may be null), or null when it has no `render`. */
|
|
559
|
+
body: (typeId) => {
|
|
560
|
+
const t = byId.get(typeId);
|
|
561
|
+
const render = typeof t?.render === "function" ? t.render : null;
|
|
562
|
+
return render ? { render, update: typeof t.update === "function" ? t.update : null } : null;
|
|
563
|
+
},
|
|
481
564
|
inputs: (typeId, { flow = false } = {}) => portsOf2(typeId, "input", flow),
|
|
482
565
|
outputs: (typeId, { flow = false } = {}) => portsOf2(typeId, "output", flow),
|
|
483
566
|
port: (typeId, name, direction) => byId.get(typeId)?.ports.find((p) => p.name === name && (!direction || p.direction === direction)) ?? null,
|
|
@@ -1044,7 +1127,6 @@ function createChipInput({
|
|
|
1044
1127
|
}
|
|
1045
1128
|
|
|
1046
1129
|
// src/flow/kit/widgets.js
|
|
1047
|
-
var shownValue = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
1048
1130
|
function typedValue(text, stored) {
|
|
1049
1131
|
if (text === shownValue(stored) && stored !== void 0) return stored;
|
|
1050
1132
|
if (stored === void 0 || typeof stored === "string") return text;
|
|
@@ -1063,30 +1145,6 @@ function textBox(value, { type = "text", placeholder = "", label = "" } = {}) {
|
|
|
1063
1145
|
if (label) input.setAttribute("aria-label", label);
|
|
1064
1146
|
return input;
|
|
1065
1147
|
}
|
|
1066
|
-
function selectBox(options, value, { empty = null, label = "", notFound = (v) => `${v} (not found)` } = {}) {
|
|
1067
|
-
const box = el("select", "twm-flow-input twm-flow-input--select");
|
|
1068
|
-
if (label) box.setAttribute("aria-label", label);
|
|
1069
|
-
const values = [];
|
|
1070
|
-
if (empty !== null) {
|
|
1071
|
-
box.appendChild(new Option(empty, ""));
|
|
1072
|
-
values.push(void 0);
|
|
1073
|
-
}
|
|
1074
|
-
options.forEach((opt) => {
|
|
1075
|
-
const o = new Option(opt.label, String(values.length));
|
|
1076
|
-
if (opt.disabled) o.disabled = true;
|
|
1077
|
-
box.appendChild(o);
|
|
1078
|
-
values.push(opt.value);
|
|
1079
|
-
});
|
|
1080
|
-
const at = values.findIndex((v, i) => i >= (empty !== null ? 1 : 0) && v === value);
|
|
1081
|
-
if (value !== void 0 && value !== null && value !== "" && at < 0) {
|
|
1082
|
-
box.appendChild(new Option(notFound(shownValue(value)), String(values.length)));
|
|
1083
|
-
values.push(value);
|
|
1084
|
-
box.value = String(values.length - 1);
|
|
1085
|
-
} else {
|
|
1086
|
-
box.value = at >= 0 ? String(at) : empty !== null ? "" : "0";
|
|
1087
|
-
}
|
|
1088
|
-
return { box, valueOf: () => box.value === "" ? void 0 : values[Number(box.value)] };
|
|
1089
|
-
}
|
|
1090
1148
|
function fitToText(area, maxLines = 12) {
|
|
1091
1149
|
let width = -1;
|
|
1092
1150
|
const fit = () => {
|
|
@@ -1376,50 +1434,6 @@ function stringListWidget(spec, value, ctx) {
|
|
|
1376
1434
|
});
|
|
1377
1435
|
return { el: rows.el, destroy: rows.destroy };
|
|
1378
1436
|
}
|
|
1379
|
-
function toggleChips({ options, value, label, readOnly, onChange, notFound }) {
|
|
1380
|
-
const root = el("div", "twm-flow-togglechips");
|
|
1381
|
-
root.setAttribute("role", "group");
|
|
1382
|
-
if (label) root.setAttribute("aria-label", label);
|
|
1383
|
-
const known = options.map((o) => o.value);
|
|
1384
|
-
const chosen = new Set((Array.isArray(value) ? value : []).filter((v) => known.includes(v)));
|
|
1385
|
-
const strays = (Array.isArray(value) ? value : []).filter((v) => !known.includes(v));
|
|
1386
|
-
const emit = () => {
|
|
1387
|
-
const out = [...known.filter((v) => chosen.has(v)), ...strays];
|
|
1388
|
-
onChange(out.length ? out : void 0);
|
|
1389
|
-
};
|
|
1390
|
-
const chipFor = (text, pressed, toggle, stray = false) => {
|
|
1391
|
-
const b = el("button", `twm-flow-togglechip${stray ? " twm-flow-togglechip--missing" : ""}`, text);
|
|
1392
|
-
b.type = "button";
|
|
1393
|
-
b.setAttribute("aria-pressed", pressed ? "true" : "false");
|
|
1394
|
-
b.disabled = Boolean(readOnly);
|
|
1395
|
-
b.addEventListener("click", () => {
|
|
1396
|
-
const on = toggle();
|
|
1397
|
-
b.setAttribute("aria-pressed", on ? "true" : "false");
|
|
1398
|
-
emit();
|
|
1399
|
-
});
|
|
1400
|
-
return b;
|
|
1401
|
-
};
|
|
1402
|
-
for (const o of options) {
|
|
1403
|
-
root.appendChild(chipFor(o.label, chosen.has(o.value), () => {
|
|
1404
|
-
if (chosen.has(o.value)) chosen.delete(o.value);
|
|
1405
|
-
else chosen.add(o.value);
|
|
1406
|
-
return chosen.has(o.value);
|
|
1407
|
-
}));
|
|
1408
|
-
}
|
|
1409
|
-
for (const v of [...strays]) {
|
|
1410
|
-
const b = chipFor(notFound(shownValue(v)), true, () => {
|
|
1411
|
-
const i = strays.indexOf(v);
|
|
1412
|
-
if (i >= 0) {
|
|
1413
|
-
strays.splice(i, 1);
|
|
1414
|
-
return false;
|
|
1415
|
-
}
|
|
1416
|
-
strays.push(v);
|
|
1417
|
-
return true;
|
|
1418
|
-
}, true);
|
|
1419
|
-
root.appendChild(b);
|
|
1420
|
-
}
|
|
1421
|
-
return root;
|
|
1422
|
-
}
|
|
1423
1437
|
function enumChipsWidget(spec, value, ctx) {
|
|
1424
1438
|
const S = ctx.strings;
|
|
1425
1439
|
const itemSpec = spec.items && typeof spec.items === "object" ? spec.items : {};
|
|
@@ -1433,48 +1447,41 @@ function enumChipsWidget(spec, value, ctx) {
|
|
|
1433
1447
|
readOnly: ctx.readOnly,
|
|
1434
1448
|
onChange: (v) => ctx.set(v),
|
|
1435
1449
|
notFound: (v) => say(S, "notFound", v)
|
|
1436
|
-
})
|
|
1450
|
+
}).el
|
|
1437
1451
|
};
|
|
1438
1452
|
}
|
|
1439
1453
|
function choiceCardsWidget(spec, value, ctx) {
|
|
1440
1454
|
const values = Array.isArray(spec.enum) ? spec.enum : [];
|
|
1441
1455
|
const descriptions = spec["x-ui-enum-descriptions"] && typeof spec["x-ui-enum-descriptions"] === "object" ? spec["x-ui-enum-descriptions"] : {};
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
radio.name = name;
|
|
1453
|
-
radio.checked = v === current;
|
|
1454
|
-
radio.disabled = Boolean(ctx.readOnly);
|
|
1455
|
-
radio.addEventListener("change", () => {
|
|
1456
|
-
if (radio.checked) ctx.set(v);
|
|
1457
|
-
});
|
|
1458
|
-
radios.push(radio);
|
|
1459
|
-
const words3 = el("span", "twm-flow-choice__words");
|
|
1460
|
-
words3.appendChild(el("span", "twm-flow-choice__label", enumLabel(spec, v)));
|
|
1461
|
-
if (descriptions[v] !== void 0) words3.appendChild(el("span", "twm-flow-choice__sub", descriptions[v]));
|
|
1462
|
-
row.append(radio, words3);
|
|
1463
|
-
root.appendChild(row);
|
|
1464
|
-
}
|
|
1465
|
-
if (current !== void 0 && !values.includes(current)) {
|
|
1466
|
-
root.appendChild(el("p", "twm-flow-field__note", say(ctx.strings, "notFound", shownValue(current))));
|
|
1467
|
-
}
|
|
1468
|
-
return { el: root, focus: () => (radios.find((r) => r.checked) || radios[0])?.focus({ preventScroll: true }) };
|
|
1456
|
+
const list = choiceList({
|
|
1457
|
+
options: values.map((v) => ({ value: v, label: enumLabel(spec, v), description: descriptions[v] })),
|
|
1458
|
+
value: value !== void 0 ? value : spec.default,
|
|
1459
|
+
name: uid("choice"),
|
|
1460
|
+
label: ctx.label,
|
|
1461
|
+
readOnly: ctx.readOnly,
|
|
1462
|
+
onChange: (v) => ctx.set(v),
|
|
1463
|
+
notFound: (v) => say(ctx.strings, "notFound", v)
|
|
1464
|
+
});
|
|
1465
|
+
return { el: list.el, focus: list.focus };
|
|
1469
1466
|
}
|
|
1470
1467
|
var columnWords = (c) => c.type ? `${c.name} \xB7 ${c.type}` : String(c.name);
|
|
1468
|
+
function columnsNotKnownNote(S) {
|
|
1469
|
+
return el("p", "twm-flow-field__note", say(S, "noColumns"));
|
|
1470
|
+
}
|
|
1471
1471
|
function upstreamColumnWidget(spec, value, ctx) {
|
|
1472
1472
|
const S = ctx.strings;
|
|
1473
1473
|
const columns = ctx.columns?.();
|
|
1474
1474
|
if (!Array.isArray(columns)) {
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1475
|
+
const root = el("div", "twm-flow-field__typed");
|
|
1476
|
+
const box2 = textBox(shownValue(value), {
|
|
1477
|
+
label: ctx.label,
|
|
1478
|
+
placeholder: spec["x-ui-placeholder"] ?? say(S, "columnName")
|
|
1479
|
+
});
|
|
1480
|
+
box2.spellcheck = false;
|
|
1481
|
+
box2.readOnly = Boolean(ctx.readOnly);
|
|
1482
|
+
box2.addEventListener("input", () => ctx.set(clean(typedValue(box2.value, value))));
|
|
1483
|
+
root.append(box2, columnsNotKnownNote(S));
|
|
1484
|
+
return { el: root, focus: () => box2.focus({ preventScroll: true }) };
|
|
1478
1485
|
}
|
|
1479
1486
|
const { box, valueOf } = selectBox(
|
|
1480
1487
|
columns.map((c) => ({ value: String(c.name), label: columnWords(c) })),
|
|
@@ -1489,15 +1496,68 @@ function upstreamColumnWidget(spec, value, ctx) {
|
|
|
1489
1496
|
box.addEventListener("change", () => ctx.set(clean(valueOf())));
|
|
1490
1497
|
return { el: box };
|
|
1491
1498
|
}
|
|
1499
|
+
function typedChips({ value, label, placeholder, readOnly, onChange, strings }) {
|
|
1500
|
+
const root = el("div", "twm-flow-togglechips twm-flow-typedchips");
|
|
1501
|
+
root.setAttribute("role", "group");
|
|
1502
|
+
if (label) root.setAttribute("aria-label", label);
|
|
1503
|
+
const list = Array.isArray(value) ? [...value] : [];
|
|
1504
|
+
const input = textBox("", { label, placeholder });
|
|
1505
|
+
input.classList.add("twm-flow-typedchips__input");
|
|
1506
|
+
input.spellcheck = false;
|
|
1507
|
+
input.readOnly = Boolean(readOnly);
|
|
1508
|
+
const emit = () => onChange(list.length ? [...list] : void 0);
|
|
1509
|
+
const chipFor = (v) => {
|
|
1510
|
+
const text = shownValue(v);
|
|
1511
|
+
const b = el("button", "twm-flow-togglechip", text);
|
|
1512
|
+
b.type = "button";
|
|
1513
|
+
b.setAttribute("aria-pressed", "true");
|
|
1514
|
+
b.setAttribute("aria-label", say(strings, "removeNamed", text));
|
|
1515
|
+
b.title = say(strings, "removeNamed", text);
|
|
1516
|
+
b.disabled = Boolean(readOnly);
|
|
1517
|
+
b.addEventListener("click", () => {
|
|
1518
|
+
const i = list.indexOf(v);
|
|
1519
|
+
if (i >= 0) list.splice(i, 1);
|
|
1520
|
+
b.remove();
|
|
1521
|
+
emit();
|
|
1522
|
+
input.focus({ preventScroll: true });
|
|
1523
|
+
});
|
|
1524
|
+
return b;
|
|
1525
|
+
};
|
|
1526
|
+
const take = () => {
|
|
1527
|
+
const name = input.value.replace(/,+$/, "").trim();
|
|
1528
|
+
input.value = "";
|
|
1529
|
+
if (!name || list.includes(name)) return;
|
|
1530
|
+
list.push(name);
|
|
1531
|
+
root.insertBefore(chipFor(name), input);
|
|
1532
|
+
emit();
|
|
1533
|
+
};
|
|
1534
|
+
for (const v of list) root.appendChild(chipFor(v));
|
|
1535
|
+
root.appendChild(input);
|
|
1536
|
+
input.addEventListener("keydown", (ev) => {
|
|
1537
|
+
if (ev.isComposing || ev.key !== "Enter" && ev.key !== ",") return;
|
|
1538
|
+
ev.preventDefault();
|
|
1539
|
+
take();
|
|
1540
|
+
});
|
|
1541
|
+
input.addEventListener("blur", () => {
|
|
1542
|
+
if (input.value.trim()) take();
|
|
1543
|
+
});
|
|
1544
|
+
return { el: root, input };
|
|
1545
|
+
}
|
|
1492
1546
|
function upstreamColumnsWidget(spec, value, ctx) {
|
|
1493
1547
|
const S = ctx.strings;
|
|
1494
1548
|
const columns = ctx.columns?.();
|
|
1495
1549
|
if (!Array.isArray(columns)) {
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1550
|
+
const root = el("div", "twm-flow-field__typed");
|
|
1551
|
+
const chips = typedChips({
|
|
1552
|
+
value,
|
|
1553
|
+
label: ctx.label,
|
|
1554
|
+
readOnly: ctx.readOnly,
|
|
1555
|
+
strings: S,
|
|
1556
|
+
placeholder: spec["x-ui-placeholder"] ?? say(S, "columnNames"),
|
|
1557
|
+
onChange: (v) => ctx.set(v)
|
|
1558
|
+
});
|
|
1559
|
+
root.append(chips.el, columnsNotKnownNote(S));
|
|
1560
|
+
return { el: root, focus: () => chips.input.focus({ preventScroll: true }) };
|
|
1501
1561
|
}
|
|
1502
1562
|
return {
|
|
1503
1563
|
el: toggleChips({
|
|
@@ -1507,7 +1567,7 @@ function upstreamColumnsWidget(spec, value, ctx) {
|
|
|
1507
1567
|
readOnly: ctx.readOnly,
|
|
1508
1568
|
onChange: (v) => ctx.set(v),
|
|
1509
1569
|
notFound: (v) => say(S, "notFound", v)
|
|
1510
|
-
})
|
|
1570
|
+
}).el
|
|
1511
1571
|
};
|
|
1512
1572
|
}
|
|
1513
1573
|
var GENERIC_WIDGETS = Object.freeze({
|
|
@@ -2137,6 +2197,18 @@ function bindFlowKeys(root, handlers = {}) {
|
|
|
2137
2197
|
run("paste", ev);
|
|
2138
2198
|
return;
|
|
2139
2199
|
}
|
|
2200
|
+
if (lower === "a") {
|
|
2201
|
+
run("selectAll", ev);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
if (lower === "g") {
|
|
2205
|
+
run("group", ev);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
if (mod && lower === "g") {
|
|
2211
|
+
run("ungroup", ev);
|
|
2140
2212
|
return;
|
|
2141
2213
|
}
|
|
2142
2214
|
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
|
@@ -3041,21 +3113,70 @@ var LOGIC_GRAPH_STRINGS = Object.freeze({
|
|
|
3041
3113
|
connectNoOutput: (port) => `This step has no output called ${port}.`,
|
|
3042
3114
|
connectNoInput: (port) => `That step has no input called ${port}.`,
|
|
3043
3115
|
connectNotFlow: (how) => how ? `Only flow ports connect; a data port is read as ${how}.` : "Only flow ports connect; a data port is read, not connected.",
|
|
3116
|
+
connectKinds: "A flow port connects to a flow port, and a data port to a data port.",
|
|
3044
3117
|
connectTwice: "These two are already connected.",
|
|
3045
3118
|
connectOneLine: (port) => `The ${port} port takes one connection; use a step that runs several branches.`
|
|
3046
3119
|
});
|
|
3047
3120
|
function emptyGraph() {
|
|
3048
3121
|
return { nodes: [], connections: [] };
|
|
3049
3122
|
}
|
|
3123
|
+
var round = (v) => Math.round(Number(v) || 0);
|
|
3124
|
+
function groupsOf(list, nodeIds) {
|
|
3125
|
+
const out = [];
|
|
3126
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3127
|
+
const placed = /* @__PURE__ */ new Set();
|
|
3128
|
+
for (const gr of Array.isArray(list) ? list : []) {
|
|
3129
|
+
if (!gr || typeof gr !== "object" || gr.id === void 0 || gr.id === null || gr.id === "") continue;
|
|
3130
|
+
const id = String(gr.id);
|
|
3131
|
+
if (ids.has(id)) continue;
|
|
3132
|
+
const nodes = [];
|
|
3133
|
+
for (const raw of Array.isArray(gr.nodes) ? gr.nodes : []) {
|
|
3134
|
+
const n = String(raw);
|
|
3135
|
+
if (!nodeIds.has(n) || placed.has(n)) continue;
|
|
3136
|
+
placed.add(n);
|
|
3137
|
+
nodes.push(n);
|
|
3138
|
+
}
|
|
3139
|
+
if (!nodes.length) continue;
|
|
3140
|
+
ids.add(id);
|
|
3141
|
+
out.push({
|
|
3142
|
+
id,
|
|
3143
|
+
...gr.label ? { label: String(gr.label) } : {},
|
|
3144
|
+
...gr.tone ? { tone: String(gr.tone) } : {},
|
|
3145
|
+
nodes
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
return out;
|
|
3149
|
+
}
|
|
3150
|
+
function notesOf(list) {
|
|
3151
|
+
const out = [];
|
|
3152
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3153
|
+
for (const note of Array.isArray(list) ? list : []) {
|
|
3154
|
+
if (!note || typeof note !== "object" || note.id === void 0 || note.id === null || note.id === "") continue;
|
|
3155
|
+
const id = String(note.id);
|
|
3156
|
+
if (ids.has(id)) continue;
|
|
3157
|
+
ids.add(id);
|
|
3158
|
+
const w = round(note.size?.w);
|
|
3159
|
+
const h = round(note.size?.h);
|
|
3160
|
+
const size = { ...w > 0 ? { w } : {}, ...h > 0 ? { h } : {} };
|
|
3161
|
+
out.push({
|
|
3162
|
+
id,
|
|
3163
|
+
text: note.text === void 0 || note.text === null ? "" : String(note.text),
|
|
3164
|
+
position: { x: round(note.position?.x), y: round(note.position?.y) },
|
|
3165
|
+
...Object.keys(size).length ? { size } : {}
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
return out;
|
|
3169
|
+
}
|
|
3050
3170
|
function normalise(graph) {
|
|
3051
3171
|
const g = graph && typeof graph === "object" ? graph : emptyGraph();
|
|
3052
|
-
|
|
3172
|
+
const out = {
|
|
3053
3173
|
nodes: (Array.isArray(g.nodes) ? g.nodes : []).map((n) => ({
|
|
3054
3174
|
id: String(n.id),
|
|
3055
3175
|
type: String(n.type),
|
|
3056
3176
|
...n.label ? { label: String(n.label) } : {},
|
|
3057
3177
|
config: n.config && typeof n.config === "object" ? structuredClone(n.config) : {},
|
|
3058
|
-
position: { x: Math.round(Number(n.position?.x) || 0), y: Math.round(Number(n.position?.y) || 0) }
|
|
3178
|
+
position: { x: Math.round(Number(n.position?.x) || 0), y: Math.round(Number(n.position?.y) || 0) },
|
|
3179
|
+
...n.collapsed === true ? { collapsed: true } : {}
|
|
3059
3180
|
})),
|
|
3060
3181
|
connections: (Array.isArray(g.connections) ? g.connections : []).map((c) => ({
|
|
3061
3182
|
source: String(c.source),
|
|
@@ -3064,6 +3185,11 @@ function normalise(graph) {
|
|
|
3064
3185
|
targetPort: String(c.targetPort || "in")
|
|
3065
3186
|
}))
|
|
3066
3187
|
};
|
|
3188
|
+
const groups = groupsOf(g.groups, new Set(out.nodes.map((n) => n.id)));
|
|
3189
|
+
if (groups.length) out.groups = groups;
|
|
3190
|
+
const notes = notesOf(g.notes);
|
|
3191
|
+
if (notes.length) out.notes = notes;
|
|
3192
|
+
return out;
|
|
3067
3193
|
}
|
|
3068
3194
|
function sortedKeys(value) {
|
|
3069
3195
|
if (Array.isArray(value)) return value.map(sortedKeys);
|
|
@@ -3080,14 +3206,17 @@ function serialise(graph) {
|
|
|
3080
3206
|
type: n.type,
|
|
3081
3207
|
...n.label ? { label: n.label } : {},
|
|
3082
3208
|
config: sortedKeys(n.config),
|
|
3083
|
-
position: n.position
|
|
3209
|
+
position: n.position,
|
|
3210
|
+
...n.collapsed ? { collapsed: true } : {}
|
|
3084
3211
|
})),
|
|
3085
3212
|
connections: g.connections.map((c) => ({
|
|
3086
3213
|
source: c.source,
|
|
3087
3214
|
target: c.target,
|
|
3088
3215
|
sourcePort: c.sourcePort,
|
|
3089
3216
|
targetPort: c.targetPort
|
|
3090
|
-
}))
|
|
3217
|
+
})),
|
|
3218
|
+
...g.groups ? { groups: g.groups } : {},
|
|
3219
|
+
...g.notes ? { notes: g.notes } : {}
|
|
3091
3220
|
});
|
|
3092
3221
|
}
|
|
3093
3222
|
function nextId(graph, typeId, { catalogue = null } = {}) {
|
|
@@ -3118,6 +3247,11 @@ function addNode(graph, typeId, position = null, {
|
|
|
3118
3247
|
function removeNode(graph, nodeId) {
|
|
3119
3248
|
graph.nodes = graph.nodes.filter((n) => n.id !== nodeId);
|
|
3120
3249
|
graph.connections = graph.connections.filter((c) => c.source !== nodeId && c.target !== nodeId);
|
|
3250
|
+
if (Array.isArray(graph.groups)) {
|
|
3251
|
+
for (const gr of graph.groups) gr.nodes = gr.nodes.filter((id) => id !== nodeId);
|
|
3252
|
+
graph.groups = graph.groups.filter((gr) => gr.nodes.length);
|
|
3253
|
+
if (!graph.groups.length) delete graph.groups;
|
|
3254
|
+
}
|
|
3121
3255
|
}
|
|
3122
3256
|
function moveNode(graph, nodeId, position) {
|
|
3123
3257
|
const node = graph.nodes.find((n) => n.id === nodeId);
|
|
@@ -3130,7 +3264,7 @@ function portOf(catalogue, node, name, direction) {
|
|
|
3130
3264
|
const def = catalogue?.get?.(node.type);
|
|
3131
3265
|
return def?.ports?.find((p) => p.name === name && p.direction === direction) || null;
|
|
3132
3266
|
}
|
|
3133
|
-
function canConnect(graph, catalogue, { source, sourcePort, target, targetPort }, { strings = null, dataRead = null } = {}) {
|
|
3267
|
+
function canConnect(graph, catalogue, { source, sourcePort, target, targetPort }, { strings = null, dataRead = null, dataPorts = false } = {}) {
|
|
3134
3268
|
const S = createStrings(LOGIC_GRAPH_STRINGS, strings);
|
|
3135
3269
|
if (source === target) return { ok: false, reason: S.connectSelf };
|
|
3136
3270
|
const from = graph.nodes.find((n) => n.id === source);
|
|
@@ -3140,29 +3274,26 @@ function canConnect(graph, catalogue, { source, sourcePort, target, targetPort }
|
|
|
3140
3274
|
const inp = portOf(catalogue, to, targetPort, "input");
|
|
3141
3275
|
if (!out) return { ok: false, reason: S.connectNoOutput(sourcePort) };
|
|
3142
3276
|
if (!inp) return { ok: false, reason: S.connectNoInput(targetPort) };
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
}
|
|
3277
|
+
const flowOut = (out.port_type || "FLOW") === "FLOW";
|
|
3278
|
+
const flowIn = (inp.port_type || "FLOW") === "FLOW";
|
|
3279
|
+
if (!dataPorts && (!flowOut || !flowIn)) return { ok: false, reason: S.connectNotFlow(dataRead) };
|
|
3280
|
+
if (flowOut !== flowIn) return { ok: false, reason: S.connectKinds };
|
|
3146
3281
|
if (graph.connections.some((c) => c.source === source && c.sourcePort === sourcePort && c.target === target && c.targetPort === targetPort)) {
|
|
3147
3282
|
return { ok: false, reason: S.connectTwice };
|
|
3148
3283
|
}
|
|
3149
|
-
if (!out.multiple && graph.connections.some((c) => c.source === source && c.sourcePort === sourcePort)) {
|
|
3284
|
+
if (flowOut && !out.multiple && graph.connections.some((c) => c.source === source && c.sourcePort === sourcePort)) {
|
|
3150
3285
|
return { ok: false, reason: S.connectOneLine(out.label || sourcePort) };
|
|
3151
3286
|
}
|
|
3152
3287
|
return { ok: true };
|
|
3153
3288
|
}
|
|
3154
3289
|
function connect(graph, catalogue, edge, options = {}) {
|
|
3155
3290
|
const verdict = canConnect(graph, catalogue, edge, options);
|
|
3156
|
-
if (verdict.ok)
|
|
3157
|
-
graph.connections.push({
|
|
3158
|
-
source: edge.source,
|
|
3159
|
-
target: edge.target,
|
|
3160
|
-
sourcePort: edge.sourcePort,
|
|
3161
|
-
targetPort: edge.targetPort
|
|
3162
|
-
});
|
|
3163
|
-
}
|
|
3291
|
+
if (verdict.ok) addConnection(graph, edge);
|
|
3164
3292
|
return verdict;
|
|
3165
3293
|
}
|
|
3294
|
+
function addConnection(graph, { source, target, sourcePort, targetPort }) {
|
|
3295
|
+
graph.connections.push({ source, target, sourcePort, targetPort });
|
|
3296
|
+
}
|
|
3166
3297
|
function disconnect(graph, edge) {
|
|
3167
3298
|
graph.connections = graph.connections.filter((c) => !sameEdge(c, edge));
|
|
3168
3299
|
}
|
|
@@ -3188,14 +3319,25 @@ function inputNamesOf(graph, syntax, scope) {
|
|
|
3188
3319
|
}
|
|
3189
3320
|
|
|
3190
3321
|
// src/flow/canvas_editor/strings.js
|
|
3322
|
+
var counted = (steps, notes) => [[steps, "step"], [notes, "note"]].filter(([n]) => n > 0).map(([n, w]) => `${n} ${w}${n === 1 ? "" : "s"}`).join(" and ");
|
|
3191
3323
|
var CANVAS_STRINGS = Object.freeze({
|
|
3192
3324
|
palette: "Steps",
|
|
3193
3325
|
canvasLabel: "Flow canvas",
|
|
3194
3326
|
toolbarLabel: "Flow",
|
|
3195
3327
|
arrange: "Arrange",
|
|
3196
3328
|
arrangeTitle: "Lay the steps out left to right, in the order a run takes them",
|
|
3329
|
+
arrangeLayeredTitle: "Lay the steps out left to right, following their connections",
|
|
3197
3330
|
fit: "Fit",
|
|
3198
3331
|
fitTitle: "Show the whole flow",
|
|
3332
|
+
zoomOut: "Zoom out",
|
|
3333
|
+
zoomOutTitle: "Make the flow smaller, one step",
|
|
3334
|
+
zoomIn: "Zoom in",
|
|
3335
|
+
zoomInTitle: "Make the flow larger, one step",
|
|
3336
|
+
zoomReset: "100%",
|
|
3337
|
+
zoomResetTitle: "Show the flow at its natural size",
|
|
3338
|
+
zoomAtMin: "This is as small as the flow is drawn.",
|
|
3339
|
+
zoomAtMax: "This is as large as the flow is drawn.",
|
|
3340
|
+
minimapTitle: "Click to move the view there",
|
|
3199
3341
|
nothingToUndo: "There is nothing to undo.",
|
|
3200
3342
|
nothingToRedo: "There is nothing to redo.",
|
|
3201
3343
|
arrangeNeedsTwo: "Arrange needs two steps or more.",
|
|
@@ -3209,30 +3351,158 @@ var CANVAS_STRINGS = Object.freeze({
|
|
|
3209
3351
|
returnLine: "Back to the loop for its next pass",
|
|
3210
3352
|
connectedReturn: "Goes round the loop again.",
|
|
3211
3353
|
unknownType: (type) => `There is no step type "${type}".`,
|
|
3354
|
+
renderFailed: (name, why) => `${name} could not draw its body${why ? `: ${why}` : "."}`,
|
|
3355
|
+
updateFailed: (name, why) => `${name} could not bring its body up to date${why ? `: ${why}` : "."}`,
|
|
3212
3356
|
stepId: (id) => `Step id: ${id}`,
|
|
3357
|
+
// `renameInPlace`
|
|
3358
|
+
nameEmpty: "A step needs a name: the one it had is kept.",
|
|
3359
|
+
// `collapsible`
|
|
3360
|
+
collapseNode: "Fold this step to its header",
|
|
3361
|
+
expandNode: "Show this step's ports again",
|
|
3213
3362
|
connection: "Connection",
|
|
3214
3363
|
connectionLine: (e) => `${e.source} \xB7 ${e.sourcePort} \u2192 ${e.target} \xB7 ${e.targetPort}`,
|
|
3215
3364
|
removeConnection: "Remove connection",
|
|
3216
3365
|
portCount: (n) => `\xD7${n}`,
|
|
3217
|
-
portNotTaken: "not taken"
|
|
3366
|
+
portNotTaken: "not taken",
|
|
3367
|
+
// ── under the `connections` option ───────────────────────────────
|
|
3368
|
+
connectTypes: ({ from, fromStep, to, toStep, accepts, provides }) => `${to} on ${toStep} takes ${accepts.join(" or ")}, and ${from} on ${fromStep} gives ${provides.join(" or ")}.`,
|
|
3369
|
+
connectRefused: "These two cannot be connected.",
|
|
3370
|
+
connectDrag: "Drop it on an input to connect, or on the canvas to add a step there. Esc cancels.",
|
|
3371
|
+
dropOnInput: "Drop the line on an input to connect it.",
|
|
3372
|
+
dropPickerTitle: "Add a step and connect it",
|
|
3373
|
+
dropNothingFits: "No step has an input this output can connect to.",
|
|
3374
|
+
offerAccept: "Accept",
|
|
3375
|
+
offerDismiss: "Not now",
|
|
3376
|
+
// `multiSelect`
|
|
3377
|
+
alignLeft: "Align left edges",
|
|
3378
|
+
alignCenter: "Align centres",
|
|
3379
|
+
alignRight: "Align right edges",
|
|
3380
|
+
alignTop: "Align top edges",
|
|
3381
|
+
alignMiddle: "Align middles",
|
|
3382
|
+
alignBottom: "Align bottom edges",
|
|
3383
|
+
distributeHorizontal: "Distribute horizontally",
|
|
3384
|
+
distributeVertical: "Distribute vertically",
|
|
3385
|
+
alignNeedsTwo: "Pick two steps or more to align them.",
|
|
3386
|
+
distributeNeedsThree: "Pick three steps or more to distribute them.",
|
|
3387
|
+
stepsPicked: (n) => `${n} steps picked`,
|
|
3388
|
+
stepsPickedLine: "Drag one to move them all. The arrow keys nudge them.",
|
|
3389
|
+
removeSteps: (n) => n === 1 ? "Remove 1 step" : `Remove ${n} steps`,
|
|
3390
|
+
// `groups`
|
|
3391
|
+
group: "Group",
|
|
3392
|
+
groupTitle: "Group the picked steps (Ctrl+G)",
|
|
3393
|
+
ungroup: "Ungroup",
|
|
3394
|
+
ungroupTitle: "Ungroup the groups the picked steps are in (Ctrl+Shift+G)",
|
|
3395
|
+
groupNeedsOne: "Pick the steps to group first.",
|
|
3396
|
+
ungroupNeedsGroup: "None of the picked steps is in a group.",
|
|
3397
|
+
groupUntitled: "Group",
|
|
3398
|
+
groupName: "Group name",
|
|
3399
|
+
// `notes`
|
|
3400
|
+
note: "Note",
|
|
3401
|
+
noteTitle: "Add a note to the canvas",
|
|
3402
|
+
noteKind: "Note",
|
|
3403
|
+
noteText: "Note text",
|
|
3404
|
+
noteEmpty: "Empty note \u2014 double-click to write in it",
|
|
3405
|
+
removeNote: "Remove note",
|
|
3406
|
+
itemsPicked: (steps, notes) => `${counted(steps, notes)} picked`,
|
|
3407
|
+
removeItems: (n) => n === 1 ? "Remove 1 item" : `Remove ${n} items`,
|
|
3408
|
+
// `clipboard`
|
|
3409
|
+
copyLabel: (title, n) => n > 1 ? `${title} (copy ${n})` : `${title} (copy)`,
|
|
3410
|
+
copied: (n) => n === 1 ? "Copied 1 step." : `Copied ${n} steps.`,
|
|
3411
|
+
cutDone: (n) => n === 1 ? "Cut 1 step." : `Cut ${n} steps.`,
|
|
3412
|
+
// a copy or a cut that took notes too (`notes`)
|
|
3413
|
+
copiedItems: (steps, notes) => `Copied ${counted(steps, notes)}.`,
|
|
3414
|
+
cutDoneItems: (steps, notes) => `Cut ${counted(steps, notes)}.`,
|
|
3415
|
+
nothingPicked: "Pick a step first.",
|
|
3416
|
+
cannotCopy: "None of the picked steps can be copied.",
|
|
3417
|
+
cannotCut: "None of the picked steps can be removed, so none can be cut.",
|
|
3418
|
+
nothingToPaste: "There is nothing to paste.",
|
|
3419
|
+
pasteNotSteps: "What is on the clipboard is not steps of a flow.",
|
|
3420
|
+
notAddable: (name) => `\u2018${name}\u2019 cannot be added to this flow.`,
|
|
3421
|
+
clipboardFailed: "The clipboard could not be reached.",
|
|
3422
|
+
routingFallback: (n) => `More than ${n} steps: the lines are drawn as plain curves, not routed round the steps.`
|
|
3218
3423
|
});
|
|
3219
3424
|
|
|
3425
|
+
// src/flow/canvas_editor/connections.js
|
|
3426
|
+
var portId = (direction, name) => `${direction}:${name}`;
|
|
3427
|
+
var typeList = (v) => Array.isArray(v) && v.length ? v.map(String) : null;
|
|
3428
|
+
function connectionRules({ catalogue, policy = null, dataPorts = false, strings }) {
|
|
3429
|
+
const S = strings;
|
|
3430
|
+
const nameOf2 = (n) => n?.label || catalogue.get(n?.type)?.label || n?.type || "";
|
|
3431
|
+
let asked = null;
|
|
3432
|
+
const typed = (ctx) => {
|
|
3433
|
+
const accepts = typeList(ctx.toPort.accepts);
|
|
3434
|
+
const provides = typeList(ctx.fromPort.provides);
|
|
3435
|
+
if (!accepts || !provides || provides.some((x) => accepts.includes(x))) return null;
|
|
3436
|
+
return {
|
|
3437
|
+
ok: false,
|
|
3438
|
+
reason: say(S, "connectTypes", {
|
|
3439
|
+
from: ctx.fromPort.label || ctx.fromPort.name,
|
|
3440
|
+
fromStep: nameOf2(ctx.fromNode),
|
|
3441
|
+
to: ctx.toPort.label || ctx.toPort.name,
|
|
3442
|
+
toStep: nameOf2(ctx.toNode),
|
|
3443
|
+
accepts,
|
|
3444
|
+
provides
|
|
3445
|
+
})
|
|
3446
|
+
};
|
|
3447
|
+
};
|
|
3448
|
+
const platform = new NodePlatform();
|
|
3449
|
+
const router = new ConnectorRouter({
|
|
3450
|
+
platform,
|
|
3451
|
+
// The generic refusals in the host's words, when its `strings` has them.
|
|
3452
|
+
strings: Object.fromEntries(Object.keys(CONNECTOR_STRINGS).filter((k) => S[k] !== void 0).map((k) => [k, S[k]])),
|
|
3453
|
+
policy: (ctx) => typed(ctx) || (typeof policy === "function" ? policy({ ...ctx, connection: { ...asked.connection }, graph: normalise(asked.graph) }) : null)
|
|
3454
|
+
});
|
|
3455
|
+
return function verdictOf(graph, edge) {
|
|
3456
|
+
const first = canConnect(graph, catalogue, edge, { strings: S, dataPorts });
|
|
3457
|
+
if (!first.ok) return first;
|
|
3458
|
+
platform.hydrate(graph.nodes.map((n) => ({
|
|
3459
|
+
...n,
|
|
3460
|
+
ports: (catalogue.get(n.type)?.ports || []).map((p) => ({ ...p, id: portId(p.direction, p.name) }))
|
|
3461
|
+
})));
|
|
3462
|
+
router.hydrate(graph.connections.map((c, i) => ({
|
|
3463
|
+
id: i,
|
|
3464
|
+
from: c.source,
|
|
3465
|
+
to: c.target,
|
|
3466
|
+
fromPort: portId("output", c.sourcePort),
|
|
3467
|
+
toPort: portId("input", c.targetPort)
|
|
3468
|
+
})));
|
|
3469
|
+
asked = { connection: edge, graph };
|
|
3470
|
+
try {
|
|
3471
|
+
const v = router.canConnect(
|
|
3472
|
+
{ nodeId: edge.source, portId: portId("output", edge.sourcePort) },
|
|
3473
|
+
{ nodeId: edge.target, portId: portId("input", edge.targetPort) }
|
|
3474
|
+
);
|
|
3475
|
+
if (!v || v.ok === false) return { ok: false, reason: String(v?.reason || say(S, "connectRefused")) };
|
|
3476
|
+
return v.offer ? { ok: true, offer: v.offer } : { ok: true };
|
|
3477
|
+
} finally {
|
|
3478
|
+
asked = null;
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3220
3483
|
// src/flow/canvas_editor/layout.js
|
|
3221
3484
|
var CANVAS_NODE = Object.freeze({ width: 208, header: 34, row: 22 });
|
|
3222
3485
|
var FOOT = 12;
|
|
3223
3486
|
var inputsOf = (catalogue, node) => catalogue?.inputs ? catalogue.inputs(node?.type) : [];
|
|
3224
3487
|
var outputsOf = (catalogue, node) => catalogue?.outputs ? catalogue.outputs(node?.type) : [];
|
|
3225
3488
|
var roleOf = (catalogue, node) => catalogue?.role ? catalogue.role(node?.type) : "step";
|
|
3489
|
+
var sizeOf = (catalogue, node) => catalogue?.size ? catalogue.size(node?.type) : null;
|
|
3490
|
+
var foldedOf = (node, box) => typeof box?.folded === "function" ? Boolean(box.folded(node)) : node?.collapsed === true;
|
|
3491
|
+
function nodeWidth(catalogue, node, box = CANVAS_NODE) {
|
|
3492
|
+
return sizeOf(catalogue, node)?.w || box.width;
|
|
3493
|
+
}
|
|
3226
3494
|
function portAnchor(catalogue, node, portName, direction, box = CANVAS_NODE) {
|
|
3495
|
+
const x = node.position.x + (direction === "input" ? 0 : nodeWidth(catalogue, node, box));
|
|
3496
|
+
const side = direction === "input" ? "left" : "right";
|
|
3497
|
+
if (foldedOf(node, box)) return { x, y: node.position.y + box.header / 2, side };
|
|
3227
3498
|
const list = direction === "input" ? inputsOf(catalogue, node) : outputsOf(catalogue, node);
|
|
3228
3499
|
const i = Math.max(0, list.findIndex((p) => p.name === portName));
|
|
3229
|
-
return {
|
|
3230
|
-
x: node.position.x + (direction === "input" ? 0 : box.width),
|
|
3231
|
-
y: node.position.y + box.header + box.row * i + box.row / 2,
|
|
3232
|
-
side: direction === "input" ? "left" : "right"
|
|
3233
|
-
};
|
|
3500
|
+
return { x, y: node.position.y + box.header + box.row * i + box.row / 2, side };
|
|
3234
3501
|
}
|
|
3235
3502
|
function nodeBottom(catalogue, node, box = CANVAS_NODE) {
|
|
3503
|
+
if (foldedOf(node, box)) return node.position.y + box.header;
|
|
3504
|
+
const h = sizeOf(catalogue, node)?.h;
|
|
3505
|
+
if (h) return node.position.y + h;
|
|
3236
3506
|
const rows = Math.max(inputsOf(catalogue, node).length, outputsOf(catalogue, node).length, 1);
|
|
3237
3507
|
return node.position.y + box.header + box.row * rows + FOOT;
|
|
3238
3508
|
}
|
|
@@ -3245,7 +3515,9 @@ function layoutGraph(graph, catalogue, {
|
|
|
3245
3515
|
row = CANVAS_NODE.row,
|
|
3246
3516
|
gap = 96,
|
|
3247
3517
|
rowGap = 40,
|
|
3248
|
-
loopEntry = "in"
|
|
3518
|
+
loopEntry = "in",
|
|
3519
|
+
heightOf = null,
|
|
3520
|
+
folded = void 0
|
|
3249
3521
|
} = {}) {
|
|
3250
3522
|
const nodes = graph?.nodes || [];
|
|
3251
3523
|
const connections = graph?.connections || [];
|
|
@@ -3271,16 +3543,86 @@ function layoutGraph(graph, catalogue, {
|
|
|
3271
3543
|
}
|
|
3272
3544
|
for (const n of nodes) if (!order.has(n.id)) order.set(n.id, order.size);
|
|
3273
3545
|
const rows = (n) => Math.max(inputsOf(catalogue, n).length, outputsOf(catalogue, n).length);
|
|
3546
|
+
const box = { width, header, row, ...folded !== void 0 ? { folded } : {} };
|
|
3547
|
+
const height = (n) => {
|
|
3548
|
+
if (foldedOf(n, box)) return header + 24;
|
|
3549
|
+
const own = sizeOf(catalogue, n)?.h || (typeof heightOf === "function" ? heightOf(n) : null);
|
|
3550
|
+
return own ? own + 2 * FOOT : header + row * rows(n) + 24;
|
|
3551
|
+
};
|
|
3552
|
+
const widest = Math.max(width, ...nodes.map((n) => nodeWidth(catalogue, n, box)));
|
|
3274
3553
|
return arrange(
|
|
3275
|
-
nodes.map((n) => ({
|
|
3276
|
-
id: n.id,
|
|
3277
|
-
label: String(order.get(n.id)).padStart(6, "0"),
|
|
3278
|
-
height: header + row * rows(n) + 24
|
|
3279
|
-
})),
|
|
3554
|
+
nodes.map((n) => ({ id: n.id, label: String(order.get(n.id)).padStart(6, "0"), height: height(n) })),
|
|
3280
3555
|
connections.filter((c) => !isReturn(c)).map((c) => ({ from: c.target, to: c.source })),
|
|
3281
|
-
{ columnGap:
|
|
3556
|
+
{ columnGap: widest + gap, rowGap }
|
|
3282
3557
|
);
|
|
3283
3558
|
}
|
|
3559
|
+
var CURVE_STEPS = 24;
|
|
3560
|
+
function pathPoints(d, { steps = CURVE_STEPS } = {}) {
|
|
3561
|
+
const tokens = String(d || "").match(/-?\d*\.?\d+(?:[eE][-+]?\d+)?|[A-Za-z]/g) || [];
|
|
3562
|
+
const points = [];
|
|
3563
|
+
let at = null;
|
|
3564
|
+
let lastControl = null;
|
|
3565
|
+
let opened = null;
|
|
3566
|
+
let i = 0;
|
|
3567
|
+
const num2 = () => Number(tokens[i++]);
|
|
3568
|
+
const cubic = (c1, c2, end) => {
|
|
3569
|
+
for (let k = 1; k <= steps; k += 1) {
|
|
3570
|
+
const t = k / steps;
|
|
3571
|
+
const u = 1 - t;
|
|
3572
|
+
points.push({
|
|
3573
|
+
x: u * u * u * at.x + 3 * u * u * t * c1.x + 3 * u * t * t * c2.x + t * t * t * end.x,
|
|
3574
|
+
y: u * u * u * at.y + 3 * u * u * t * c1.y + 3 * u * t * t * c2.y + t * t * t * end.y
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
lastControl = c2;
|
|
3578
|
+
at = end;
|
|
3579
|
+
};
|
|
3580
|
+
while (i < tokens.length) {
|
|
3581
|
+
const command = tokens[i++];
|
|
3582
|
+
if (command === "M") {
|
|
3583
|
+
at = { x: num2(), y: num2() };
|
|
3584
|
+
opened = at;
|
|
3585
|
+
points.push(at);
|
|
3586
|
+
lastControl = null;
|
|
3587
|
+
} else if (command === "Z") {
|
|
3588
|
+
at = opened;
|
|
3589
|
+
points.push(at);
|
|
3590
|
+
lastControl = null;
|
|
3591
|
+
} else if (command === "L") {
|
|
3592
|
+
at = { x: num2(), y: num2() };
|
|
3593
|
+
points.push(at);
|
|
3594
|
+
lastControl = null;
|
|
3595
|
+
} else if (command === "C") {
|
|
3596
|
+
const c1 = { x: num2(), y: num2() };
|
|
3597
|
+
const c2 = { x: num2(), y: num2() };
|
|
3598
|
+
cubic(c1, c2, { x: num2(), y: num2() });
|
|
3599
|
+
} else if (command === "S") {
|
|
3600
|
+
const c1 = lastControl ? { x: 2 * at.x - lastControl.x, y: 2 * at.y - lastControl.y } : at;
|
|
3601
|
+
const c2 = { x: num2(), y: num2() };
|
|
3602
|
+
cubic(c1, c2, { x: num2(), y: num2() });
|
|
3603
|
+
} else {
|
|
3604
|
+
throw new Error(`pathPoints reads M, L, C, S and Z only, not "${command}".`);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
return points;
|
|
3608
|
+
}
|
|
3609
|
+
function pathMidpoint(d) {
|
|
3610
|
+
const points = pathPoints(d);
|
|
3611
|
+
if (points.length < 2) return points[0] ? { ...points[0] } : { x: 0, y: 0 };
|
|
3612
|
+
const lengths = [0];
|
|
3613
|
+
for (let k2 = 1; k2 < points.length; k2 += 1) {
|
|
3614
|
+
lengths.push(lengths[k2 - 1] + Math.hypot(points[k2].x - points[k2 - 1].x, points[k2].y - points[k2 - 1].y));
|
|
3615
|
+
}
|
|
3616
|
+
const half = lengths[lengths.length - 1] / 2;
|
|
3617
|
+
let k = 1;
|
|
3618
|
+
while (k < lengths.length - 1 && lengths[k] < half) k += 1;
|
|
3619
|
+
const span = lengths[k] - lengths[k - 1];
|
|
3620
|
+
const t = span > 0 ? (half - lengths[k - 1]) / span : 0;
|
|
3621
|
+
return {
|
|
3622
|
+
x: points[k - 1].x + (points[k].x - points[k - 1].x) * t,
|
|
3623
|
+
y: points[k - 1].y + (points[k].y - points[k - 1].y) * t
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3284
3626
|
|
|
3285
3627
|
// src/flow/canvas_editor/editor.js
|
|
3286
3628
|
var CANVAS_ACTIONS = Object.freeze([
|
|
@@ -3292,12 +3634,61 @@ var CANVAS_ACTIONS = Object.freeze([
|
|
|
3292
3634
|
"flow:node:label",
|
|
3293
3635
|
"flow:node:config",
|
|
3294
3636
|
"flow:settings",
|
|
3295
|
-
"flow:arrange"
|
|
3637
|
+
"flow:arrange",
|
|
3638
|
+
"flow:nodes:move",
|
|
3639
|
+
"flow:nodes:remove",
|
|
3640
|
+
"flow:nodes:align",
|
|
3641
|
+
"flow:paste",
|
|
3642
|
+
"flow:node:duplicate",
|
|
3643
|
+
"flow:node:collapse",
|
|
3644
|
+
"flow:group:add",
|
|
3645
|
+
"flow:group:remove",
|
|
3646
|
+
"flow:group:label",
|
|
3647
|
+
"flow:group:members",
|
|
3648
|
+
"flow:note:add",
|
|
3649
|
+
"flow:note:remove",
|
|
3650
|
+
"flow:note:edit",
|
|
3651
|
+
"flow:note:move"
|
|
3296
3652
|
]);
|
|
3653
|
+
var ALIGN = Object.freeze(["left", "center", "right", "top", "middle", "bottom"]);
|
|
3654
|
+
var ALIGN_ICON = Object.freeze({
|
|
3655
|
+
left: "align_horizontal_left",
|
|
3656
|
+
center: "align_horizontal_center",
|
|
3657
|
+
right: "align_horizontal_right",
|
|
3658
|
+
top: "align_vertical_top",
|
|
3659
|
+
middle: "align_vertical_center",
|
|
3660
|
+
bottom: "align_vertical_bottom"
|
|
3661
|
+
});
|
|
3662
|
+
var DISTRIBUTE = Object.freeze(["horizontal", "vertical"]);
|
|
3663
|
+
var DISTRIBUTE_ICON = Object.freeze({ horizontal: "horizontal_distribute", vertical: "vertical_distribute" });
|
|
3664
|
+
var COPY_OFFSET = 2;
|
|
3665
|
+
var NUDGE_FAR = 10;
|
|
3666
|
+
var FRAME = Object.freeze({ pad: 16, head: 26 });
|
|
3667
|
+
var NOTE_WIDTH = 192;
|
|
3297
3668
|
var NATURAL = 1;
|
|
3298
3669
|
var READABLE = 0.8;
|
|
3670
|
+
var ZOOM_STEPS = Object.freeze([
|
|
3671
|
+
CanvasAdapter.MIN_SCALE,
|
|
3672
|
+
0.25,
|
|
3673
|
+
0.33,
|
|
3674
|
+
0.5,
|
|
3675
|
+
0.67,
|
|
3676
|
+
0.8,
|
|
3677
|
+
1,
|
|
3678
|
+
1.25,
|
|
3679
|
+
1.5,
|
|
3680
|
+
2,
|
|
3681
|
+
3,
|
|
3682
|
+
CanvasAdapter.MAX_SCALE
|
|
3683
|
+
]);
|
|
3684
|
+
var ZOOM_EPSILON = 1e-6;
|
|
3685
|
+
var WHEEL_SETTLE_MS = 300;
|
|
3686
|
+
var MINIMAP = Object.freeze({ width: 180, height: 120, margin: 40 });
|
|
3299
3687
|
var DRAG_PX = 4;
|
|
3300
3688
|
var RETURN_DROP = 40;
|
|
3689
|
+
var CLEARANCE = 16;
|
|
3690
|
+
var ARROW = Object.freeze({ length: 9, half: 4.5 });
|
|
3691
|
+
var ROUTINGS = Object.freeze(["simple", "obstacles"]);
|
|
3301
3692
|
var SVG = "http://www.w3.org/2000/svg";
|
|
3302
3693
|
var DEFAULT_REFERENCES = Object.freeze({
|
|
3303
3694
|
template: TEMPLATE_REFERENCES,
|
|
@@ -3311,7 +3702,15 @@ function sortedKeys2(value) {
|
|
|
3311
3702
|
}
|
|
3312
3703
|
return value;
|
|
3313
3704
|
}
|
|
3705
|
+
var cap = (word2) => word2.charAt(0).toUpperCase() + word2.slice(1);
|
|
3314
3706
|
var plainObject = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
3707
|
+
function toCatalogue(value, who) {
|
|
3708
|
+
const catalogue = Array.isArray(value) ? createStepCatalogue(value) : value;
|
|
3709
|
+
if (!catalogue?.get || !catalogue?.byCategory) {
|
|
3710
|
+
throw new Error(`${who} needs a step catalogue (createStepCatalogue(types, \u2026)).`);
|
|
3711
|
+
}
|
|
3712
|
+
return catalogue;
|
|
3713
|
+
}
|
|
3315
3714
|
function createCanvasEditor(host, options = {}) {
|
|
3316
3715
|
const {
|
|
3317
3716
|
widgets = createWidgetRegistry(),
|
|
@@ -3336,22 +3735,56 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3336
3735
|
paletteTypes = null,
|
|
3337
3736
|
label = null,
|
|
3338
3737
|
gridSize = 16,
|
|
3339
|
-
waitsForAll = void 0
|
|
3738
|
+
waitsForAll = void 0,
|
|
3739
|
+
connections: linkOptions = null,
|
|
3740
|
+
multiSelect = false,
|
|
3741
|
+
edgeStyle = null,
|
|
3742
|
+
routing = "simple",
|
|
3743
|
+
hoverHighlight = false,
|
|
3744
|
+
onViewportChange = null,
|
|
3745
|
+
zoomControls = false,
|
|
3746
|
+
minimap = false,
|
|
3747
|
+
renameInPlace = false,
|
|
3748
|
+
clipboard = null,
|
|
3749
|
+
prepareCopy = null,
|
|
3750
|
+
collapsible = false,
|
|
3751
|
+
groups: groupsOption = false,
|
|
3752
|
+
notes: notesOption = false,
|
|
3753
|
+
layout = "flow"
|
|
3340
3754
|
} = options;
|
|
3341
|
-
const
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3755
|
+
const folding = Boolean(collapsible);
|
|
3756
|
+
const multi = Boolean(multiSelect);
|
|
3757
|
+
const grouping = Boolean(groupsOption);
|
|
3758
|
+
const noting = Boolean(notesOption);
|
|
3759
|
+
if (layout !== "flow" && layout !== "layered" && typeof layout !== "function") {
|
|
3760
|
+
throw new Error(`createCanvasEditor's layout is 'flow', 'layered' or a function (graph, ctx) \u2192 Map; got ${String(layout)}.`);
|
|
3761
|
+
}
|
|
3762
|
+
if (!ROUTINGS.includes(routing)) {
|
|
3763
|
+
throw new Error(`createCanvasEditor: routing is ${ROUTINGS.map((r) => `'${r}'`).join(" or ")}, not '${routing}'.`);
|
|
3764
|
+
}
|
|
3765
|
+
if (clipboard && clipboard !== true && (typeof clipboard.read !== "function" || typeof clipboard.write !== "function")) {
|
|
3766
|
+
throw new Error("createCanvasEditor: clipboard is true or {read(), write(text)}.");
|
|
3767
|
+
}
|
|
3768
|
+
const clip = clipboard === true ? "memory" : clipboard || null;
|
|
3769
|
+
const styled = typeof edgeStyle === "function";
|
|
3770
|
+
let catalogue = toCatalogue(options.catalogue, "createCanvasEditor");
|
|
3345
3771
|
const S = createStrings(CANVAS_STRINGS, strings);
|
|
3346
3772
|
const BOX = Object.freeze({
|
|
3347
3773
|
width: Number(nodeOptions.width) || CANVAS_NODE.width,
|
|
3348
3774
|
header: Number(nodeOptions.header) || CANVAS_NODE.header,
|
|
3349
|
-
row: Number(nodeOptions.row) || CANVAS_NODE.row
|
|
3775
|
+
row: Number(nodeOptions.row) || CANVAS_NODE.row,
|
|
3776
|
+
/** Is this node drawn folded to its header? Only with `collapsible`. */
|
|
3777
|
+
folded: (n) => folding && n?.collapsed === true
|
|
3350
3778
|
});
|
|
3351
3779
|
const loopPorts = Object.freeze({ ...DEFAULT_LOOP_PORTS, ...plainObject(loop) });
|
|
3352
3780
|
const canRemove = typeof removable === "function" ? removable : (_node, type) => (type?.role || "step") !== "start";
|
|
3353
3781
|
const offered = typeof paletteTypes === "function" ? paletteTypes : (type) => type.role !== "start";
|
|
3782
|
+
const linking = linkOptions && typeof linkOptions === "object" ? linkOptions : null;
|
|
3783
|
+
const dataConnect = linking?.dataPorts === "connect";
|
|
3784
|
+
const rulesFor = (types) => linking ? connectionRules({ catalogue: types, policy: linking.policy, dataPorts: dataConnect, strings: S }) : null;
|
|
3785
|
+
let verdictOf = rulesFor(catalogue);
|
|
3354
3786
|
const root = el("div", "twm-flow-graph");
|
|
3787
|
+
if (linking) root.classList.add("twm-flow-graph--linking");
|
|
3355
3788
|
if (BOX.width !== CANVAS_NODE.width) root.style.setProperty("--twm-canvas-node-width", `${BOX.width}px`);
|
|
3356
3789
|
if (BOX.header !== CANVAS_NODE.header) root.style.setProperty("--twm-canvas-node-header", `${BOX.header}px`);
|
|
3357
3790
|
if (BOX.row !== CANVAS_NODE.row) root.style.setProperty("--twm-canvas-node-row", `${BOX.row}px`);
|
|
@@ -3367,6 +3800,11 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3367
3800
|
message.setAttribute("aria-live", "polite");
|
|
3368
3801
|
const banner = el("p", "twm-flow-graph__banner");
|
|
3369
3802
|
banner.hidden = true;
|
|
3803
|
+
const offerBox = linking ? el("div", "twm-flow-graph__offer") : null;
|
|
3804
|
+
if (offerBox) {
|
|
3805
|
+
offerBox.hidden = true;
|
|
3806
|
+
offerBox.setAttribute("role", "status");
|
|
3807
|
+
}
|
|
3370
3808
|
const body = el("div", "twm-flow-graph__body");
|
|
3371
3809
|
const palette = el("nav", "twm-flow-graph__palette");
|
|
3372
3810
|
palette.setAttribute("aria-label", say(S, "palette"));
|
|
@@ -3383,19 +3821,46 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3383
3821
|
let findings = [];
|
|
3384
3822
|
let findingsBy = /* @__PURE__ */ new Map();
|
|
3385
3823
|
let overlay = null;
|
|
3824
|
+
let liveValues = /* @__PURE__ */ new Map();
|
|
3386
3825
|
let selected = null;
|
|
3826
|
+
let pressed = null;
|
|
3827
|
+
let released = null;
|
|
3387
3828
|
let connecting = null;
|
|
3829
|
+
let pendingOffer = null;
|
|
3388
3830
|
let suppressClick = false;
|
|
3389
3831
|
let dragCleanup = null;
|
|
3390
3832
|
let destroyed = false;
|
|
3391
3833
|
let fitFrame = null;
|
|
3392
3834
|
let travelling = null;
|
|
3393
3835
|
let lastSelect = "";
|
|
3836
|
+
let hovered = null;
|
|
3837
|
+
let edgeEls = [];
|
|
3838
|
+
let routingFallback = false;
|
|
3394
3839
|
const nodeEls = /* @__PURE__ */ new Map();
|
|
3840
|
+
const frameEls = /* @__PURE__ */ new Map();
|
|
3841
|
+
const noteEls = /* @__PURE__ */ new Map();
|
|
3842
|
+
let editing = null;
|
|
3843
|
+
const builtFrom = /* @__PURE__ */ new Map();
|
|
3844
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
3845
|
+
const inlines = /* @__PURE__ */ new Map();
|
|
3846
|
+
let renaming = null;
|
|
3847
|
+
const bodyFindings = /* @__PURE__ */ new Map();
|
|
3848
|
+
let painting = false;
|
|
3395
3849
|
const consumerState = /* @__PURE__ */ new Map();
|
|
3850
|
+
let startView = viewportOf(options.viewport);
|
|
3851
|
+
let reportedView = "";
|
|
3852
|
+
let wheelTimer = null;
|
|
3853
|
+
let pressEnd = null;
|
|
3854
|
+
let zoomLimit = "";
|
|
3855
|
+
let miniMap = null;
|
|
3856
|
+
let copiedText = null;
|
|
3857
|
+
let pasteRun = { text: null, times: 0 };
|
|
3396
3858
|
const canvas = new CanvasAdapter({ container: canvasHost, gridSize });
|
|
3859
|
+
if (startView) canvas.setViewport(startView);
|
|
3397
3860
|
canvas.root.setAttribute("role", "application");
|
|
3398
3861
|
canvas.root.setAttribute("aria-label", label || say(S, "canvasLabel"));
|
|
3862
|
+
const labelLayer = styled ? el("div", "twm-flow-graph__edge-labels") : null;
|
|
3863
|
+
if (labelLayer) canvas.nodeLayer.appendChild(labelLayer);
|
|
3399
3864
|
canvas.onSelectionChange = () => {
|
|
3400
3865
|
if (connecting) cancelConnect();
|
|
3401
3866
|
if (selected && selected.kind !== "flow") select(null);
|
|
@@ -3404,6 +3869,17 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3404
3869
|
const readOnlyReason = () => ro && typeof ro === "object" && ro.reason ? String(ro.reason) : "";
|
|
3405
3870
|
const nodeOf = (id) => graph.nodes.find((x) => x.id === id) || null;
|
|
3406
3871
|
const typeOf = (n) => catalogue.get(n?.type);
|
|
3872
|
+
const pickedIds = () => selected?.kind === "node" ? [selected.id] : selected?.kind === "many" ? selected.ids : [];
|
|
3873
|
+
const pickedSet = () => new Set(pickedIds());
|
|
3874
|
+
const isPicked = (id) => selected?.kind === "node" ? selected.id === id : selected?.kind === "many" ? selected.ids.includes(id) : false;
|
|
3875
|
+
const toggles = (ev) => multi && Boolean(ev.shiftKey || ev.ctrlKey || ev.metaKey);
|
|
3876
|
+
const groupList = () => Array.isArray(graph.groups) ? graph.groups : [];
|
|
3877
|
+
const noteList = () => Array.isArray(graph.notes) ? graph.notes : [];
|
|
3878
|
+
const groupOf = (id) => groupList().find((gr) => gr.id === id) || null;
|
|
3879
|
+
const noteOf = (id) => noteList().find((x) => x.id === id) || null;
|
|
3880
|
+
const pickedNotes = () => selected?.kind === "note" ? [selected.id] : selected?.kind === "many" ? selected.notes || [] : [];
|
|
3881
|
+
const isNotePicked = (id) => pickedNotes().includes(id);
|
|
3882
|
+
const allFindings = () => bodyFindings.size ? [...findings, ...bodyFindings.values()] : findings;
|
|
3407
3883
|
const snapshot = () => JSON.stringify({ graph: JSON.parse(serialise(graph)), flowSettings: sortedKeys2(settingsValue) });
|
|
3408
3884
|
const history = new FlowHistory({
|
|
3409
3885
|
actions: CANVAS_ACTIONS,
|
|
@@ -3419,23 +3895,40 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3419
3895
|
onChange({ graph: normalise(graph), flowSettings: structuredClone(settingsValue), text, action, key, via });
|
|
3420
3896
|
}
|
|
3421
3897
|
function commit(action, key = null) {
|
|
3898
|
+
if (pendingOffer) clearOffer();
|
|
3422
3899
|
if (history.commit(snapshot(), action, key)) emitChange(action, key);
|
|
3423
3900
|
paintToolbar();
|
|
3424
3901
|
}
|
|
3425
3902
|
function restore(state) {
|
|
3903
|
+
editing?.done(false);
|
|
3426
3904
|
const p = JSON.parse(state);
|
|
3427
3905
|
graph = normalise(p.graph);
|
|
3428
3906
|
settingsValue = plainObject(p.flowSettings);
|
|
3429
3907
|
if (connecting) cancelConnect();
|
|
3430
|
-
if (
|
|
3431
|
-
|
|
3432
|
-
findingsBy = groupFindings(
|
|
3908
|
+
if (pendingOffer) clearOffer();
|
|
3909
|
+
prunePick();
|
|
3910
|
+
findingsBy = groupFindings(allFindings(), { graph });
|
|
3433
3911
|
paintNodes();
|
|
3434
3912
|
paintPanel();
|
|
3435
3913
|
keepFocus();
|
|
3436
3914
|
emitChange(null, null, travelling);
|
|
3437
3915
|
announceSelection();
|
|
3438
3916
|
}
|
|
3917
|
+
function prunePick() {
|
|
3918
|
+
if (selected?.kind === "node" && !nodeOf(selected.id)) selected = null;
|
|
3919
|
+
if (selected?.kind === "edge" && !graph.connections.some((c) => sameEdge(c, selected.edge))) selected = null;
|
|
3920
|
+
if (selected?.kind === "note" && !noteOf(selected.id)) selected = null;
|
|
3921
|
+
if (selected?.kind === "many") selected = pickOf(selected.ids, selected.notes || []);
|
|
3922
|
+
}
|
|
3923
|
+
function pickOf(ids, noteIds = []) {
|
|
3924
|
+
const live = new Set(graph.nodes.map((x) => x.id));
|
|
3925
|
+
const list = [...new Set(ids)].filter((id) => live.has(id));
|
|
3926
|
+
const liveNotes = new Set(noteList().map((x) => x.id));
|
|
3927
|
+
const notes = [...new Set(noteIds)].filter((id) => liveNotes.has(id));
|
|
3928
|
+
if (list.length + notes.length > 1) return { kind: "many", ids: list, ...notes.length ? { notes } : {} };
|
|
3929
|
+
if (list.length) return { kind: "node", id: list[0] };
|
|
3930
|
+
return notes.length ? { kind: "note", id: notes[0] } : null;
|
|
3931
|
+
}
|
|
3439
3932
|
function keepFocus() {
|
|
3440
3933
|
if (destroyed || !root.isConnected) return;
|
|
3441
3934
|
const active = document.activeElement;
|
|
@@ -3458,16 +3951,97 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3458
3951
|
arrange: tool("arrange", {
|
|
3459
3952
|
label: say(S, "arrange"),
|
|
3460
3953
|
icon: "account_tree",
|
|
3461
|
-
title: say(S, "arrangeTitle"),
|
|
3462
|
-
onClick: () =>
|
|
3954
|
+
title: say(S, layout === "flow" ? "arrangeTitle" : "arrangeLayeredTitle"),
|
|
3955
|
+
onClick: () => {
|
|
3956
|
+
if (arrangeGraph(arrangeTarget())) viewportSettled();
|
|
3957
|
+
}
|
|
3463
3958
|
}),
|
|
3464
3959
|
fit: tool("fit", {
|
|
3465
3960
|
label: say(S, "fit"),
|
|
3466
3961
|
icon: "fit_screen",
|
|
3467
3962
|
title: say(S, "fitTitle"),
|
|
3468
|
-
onClick: () =>
|
|
3963
|
+
onClick: () => {
|
|
3964
|
+
fit({ whole: true });
|
|
3965
|
+
viewportSettled();
|
|
3966
|
+
}
|
|
3469
3967
|
})
|
|
3470
3968
|
};
|
|
3969
|
+
const pickTools = [];
|
|
3970
|
+
if (multi) {
|
|
3971
|
+
for (const kind of ALIGN) {
|
|
3972
|
+
const id = `align-${kind}`;
|
|
3973
|
+
const words3 = say(S, `align${cap(kind)}`);
|
|
3974
|
+
tools[id] = tool(id, {
|
|
3975
|
+
label: words3,
|
|
3976
|
+
title: words3,
|
|
3977
|
+
icon: ALIGN_ICON[kind],
|
|
3978
|
+
iconOnly: true,
|
|
3979
|
+
onClick: () => alignNodes(kind)
|
|
3980
|
+
});
|
|
3981
|
+
pickTools.push(tools[id]);
|
|
3982
|
+
}
|
|
3983
|
+
for (const axis of DISTRIBUTE) {
|
|
3984
|
+
const id = `distribute-${axis}`;
|
|
3985
|
+
const words3 = say(S, `distribute${cap(axis)}`);
|
|
3986
|
+
tools[id] = tool(id, {
|
|
3987
|
+
label: words3,
|
|
3988
|
+
title: words3,
|
|
3989
|
+
icon: DISTRIBUTE_ICON[axis],
|
|
3990
|
+
iconOnly: true,
|
|
3991
|
+
onClick: () => distributeNodes(axis)
|
|
3992
|
+
});
|
|
3993
|
+
pickTools.push(tools[id]);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
const itemTools = [];
|
|
3997
|
+
if (grouping) {
|
|
3998
|
+
tools.group = tool("group", {
|
|
3999
|
+
label: say(S, "group"),
|
|
4000
|
+
title: say(S, "groupTitle"),
|
|
4001
|
+
icon: "select_all",
|
|
4002
|
+
iconOnly: true,
|
|
4003
|
+
onClick: () => groupPicked()
|
|
4004
|
+
});
|
|
4005
|
+
tools.ungroup = tool("ungroup", {
|
|
4006
|
+
label: say(S, "ungroup"),
|
|
4007
|
+
title: say(S, "ungroupTitle"),
|
|
4008
|
+
icon: "deselect",
|
|
4009
|
+
iconOnly: true,
|
|
4010
|
+
onClick: () => ungroupPicked()
|
|
4011
|
+
});
|
|
4012
|
+
itemTools.push(tools.group, tools.ungroup);
|
|
4013
|
+
}
|
|
4014
|
+
if (noting) {
|
|
4015
|
+
tools.note = tool("note", {
|
|
4016
|
+
label: say(S, "note"),
|
|
4017
|
+
title: say(S, "noteTitle"),
|
|
4018
|
+
icon: "sticky_note_2",
|
|
4019
|
+
onClick: () => {
|
|
4020
|
+
const n = addNote();
|
|
4021
|
+
if (n) editNote(n.id);
|
|
4022
|
+
}
|
|
4023
|
+
});
|
|
4024
|
+
itemTools.push(tools.note);
|
|
4025
|
+
}
|
|
4026
|
+
if (zoomControls) {
|
|
4027
|
+
tools["zoom-out"] = tool("zoom-out", {
|
|
4028
|
+
label: say(S, "zoomOut"),
|
|
4029
|
+
icon: "zoom_out",
|
|
4030
|
+
title: say(S, "zoomOutTitle"),
|
|
4031
|
+
onClick: () => zoomStep(-1)
|
|
4032
|
+
});
|
|
4033
|
+
tools["zoom-in"] = tool("zoom-in", {
|
|
4034
|
+
label: say(S, "zoomIn"),
|
|
4035
|
+
icon: "zoom_in",
|
|
4036
|
+
title: say(S, "zoomInTitle"),
|
|
4037
|
+
onClick: () => zoomStep(1)
|
|
4038
|
+
});
|
|
4039
|
+
tools["zoom-reset"] = tool("zoom-reset", {
|
|
4040
|
+
label: say(S, "zoomReset"),
|
|
4041
|
+
title: say(S, "zoomResetTitle"),
|
|
4042
|
+
onClick: () => zoomTo(NATURAL)
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
3471
4045
|
const own = new Set(Object.keys(tools));
|
|
3472
4046
|
const consumerTools = [];
|
|
3473
4047
|
for (const a of Array.isArray(actions) ? actions : []) {
|
|
@@ -3489,6 +4063,9 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3489
4063
|
tools.redo,
|
|
3490
4064
|
tools.arrange,
|
|
3491
4065
|
tools.fit,
|
|
4066
|
+
...["zoom-out", "zoom-in", "zoom-reset"].filter((id) => tools[id]).map((id) => tools[id]),
|
|
4067
|
+
...pickTools,
|
|
4068
|
+
...itemTools,
|
|
3492
4069
|
...consumerTools.map((t) => t.el),
|
|
3493
4070
|
status,
|
|
3494
4071
|
toolbarEnd
|
|
@@ -3496,12 +4073,26 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3496
4073
|
if (typeof slots.toolbarEnd === "function") slots.toolbarEnd(toolbarEnd);
|
|
3497
4074
|
function refusals() {
|
|
3498
4075
|
const stop = isReadOnly() ? readOnlyReason() || say(S, "readOnlyRefusal") : null;
|
|
3499
|
-
|
|
4076
|
+
const r = {
|
|
3500
4077
|
undo: stop || (history.canUndo ? null : say(S, "nothingToUndo")),
|
|
3501
4078
|
redo: stop || (history.canRedo ? null : say(S, "nothingToRedo")),
|
|
3502
4079
|
arrange: stop || (graph.nodes.length < 2 ? say(S, "arrangeNeedsTwo") : null),
|
|
3503
|
-
fit: null
|
|
4080
|
+
fit: null,
|
|
4081
|
+
"zoom-out": canvas.scale <= ZOOM_STEPS[0] + ZOOM_EPSILON ? say(S, "zoomAtMin") : null,
|
|
4082
|
+
"zoom-in": canvas.scale >= ZOOM_STEPS[ZOOM_STEPS.length - 1] - ZOOM_EPSILON ? say(S, "zoomAtMax") : null,
|
|
4083
|
+
"zoom-reset": null
|
|
3504
4084
|
};
|
|
4085
|
+
if (multi) {
|
|
4086
|
+
const n = pickedIds().length;
|
|
4087
|
+
for (const kind of ALIGN) r[`align-${kind}`] = stop || (n < 2 ? say(S, "alignNeedsTwo") : null);
|
|
4088
|
+
for (const axis of DISTRIBUTE) r[`distribute-${axis}`] = stop || (n < 3 ? say(S, "distributeNeedsThree") : null);
|
|
4089
|
+
}
|
|
4090
|
+
if (grouping) {
|
|
4091
|
+
r.group = stop || (pickedIds().length ? null : say(S, "groupNeedsOne"));
|
|
4092
|
+
r.ungroup = stop || (groupsOfPick().length ? null : say(S, "ungroupNeedsGroup"));
|
|
4093
|
+
}
|
|
4094
|
+
if (noting) r.note = stop;
|
|
4095
|
+
return r;
|
|
3505
4096
|
}
|
|
3506
4097
|
function refuse(control, sentence) {
|
|
3507
4098
|
control.disabled = Boolean(sentence);
|
|
@@ -3557,6 +4148,129 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3557
4148
|
travelling = null;
|
|
3558
4149
|
}
|
|
3559
4150
|
}
|
|
4151
|
+
const viewKey = (v) => `${v.scale}|${v.tx}|${v.ty}`;
|
|
4152
|
+
function viewportBaseline() {
|
|
4153
|
+
reportedView = viewKey(canvas.viewport());
|
|
4154
|
+
}
|
|
4155
|
+
function viewportSettled() {
|
|
4156
|
+
if (wheelTimer !== null) {
|
|
4157
|
+
clearTimeout(wheelTimer);
|
|
4158
|
+
wheelTimer = null;
|
|
4159
|
+
}
|
|
4160
|
+
if (destroyed) return;
|
|
4161
|
+
const now = canvas.viewport();
|
|
4162
|
+
const key = viewKey(now);
|
|
4163
|
+
if (key === reportedView) return;
|
|
4164
|
+
reportedView = key;
|
|
4165
|
+
onViewportChange?.({ ...now });
|
|
4166
|
+
}
|
|
4167
|
+
function zoomTo(scale) {
|
|
4168
|
+
const r = canvas.root.getBoundingClientRect();
|
|
4169
|
+
const centre = canvas.clientToCanvas(r.left + r.width / 2, r.top + r.height / 2);
|
|
4170
|
+
const s = Math.max(ZOOM_STEPS[0], Math.min(scale, ZOOM_STEPS[ZOOM_STEPS.length - 1]));
|
|
4171
|
+
canvas.setViewport({ scale: s, tx: r.width / 2 - centre.x * s, ty: r.height / 2 - centre.y * s });
|
|
4172
|
+
viewportSettled();
|
|
4173
|
+
}
|
|
4174
|
+
function zoomStep(direction) {
|
|
4175
|
+
const s = canvas.scale;
|
|
4176
|
+
const next = direction > 0 ? ZOOM_STEPS.find((z) => z > s + ZOOM_EPSILON) : [...ZOOM_STEPS].reverse().find((z) => z < s - ZOOM_EPSILON);
|
|
4177
|
+
if (next === void 0) return false;
|
|
4178
|
+
zoomTo(next);
|
|
4179
|
+
return true;
|
|
4180
|
+
}
|
|
4181
|
+
function viewportMoved() {
|
|
4182
|
+
if (destroyed) return;
|
|
4183
|
+
if (zoomControls) {
|
|
4184
|
+
const r = refusals();
|
|
4185
|
+
const limit = `${r["zoom-out"] || ""}|${r["zoom-in"] || ""}`;
|
|
4186
|
+
if (limit !== zoomLimit) {
|
|
4187
|
+
zoomLimit = limit;
|
|
4188
|
+
paintToolbar();
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
paintMinimap();
|
|
4192
|
+
}
|
|
4193
|
+
if (zoomControls || minimap) canvas.onViewportChange = () => viewportMoved();
|
|
4194
|
+
if (typeof onViewportChange === "function") {
|
|
4195
|
+
canvas.root.addEventListener("pointerdown", () => {
|
|
4196
|
+
viewportSettled();
|
|
4197
|
+
pressEnd?.();
|
|
4198
|
+
const end = () => {
|
|
4199
|
+
pressEnd?.();
|
|
4200
|
+
viewportSettled();
|
|
4201
|
+
};
|
|
4202
|
+
window.addEventListener("pointerup", end, true);
|
|
4203
|
+
window.addEventListener("pointercancel", end, true);
|
|
4204
|
+
pressEnd = () => {
|
|
4205
|
+
window.removeEventListener("pointerup", end, true);
|
|
4206
|
+
window.removeEventListener("pointercancel", end, true);
|
|
4207
|
+
pressEnd = null;
|
|
4208
|
+
};
|
|
4209
|
+
}, true);
|
|
4210
|
+
canvas.root.addEventListener("wheel", () => {
|
|
4211
|
+
if (wheelTimer !== null) clearTimeout(wheelTimer);
|
|
4212
|
+
wheelTimer = setTimeout(() => {
|
|
4213
|
+
wheelTimer = null;
|
|
4214
|
+
viewportSettled();
|
|
4215
|
+
}, WHEEL_SETTLE_MS);
|
|
4216
|
+
}, { passive: true });
|
|
4217
|
+
}
|
|
4218
|
+
const mini = minimap ? el("div", "twm-flow-graph__minimap") : null;
|
|
4219
|
+
const miniSvg = minimap ? document.createElementNS(SVG, "svg") : null;
|
|
4220
|
+
if (mini) {
|
|
4221
|
+
miniSvg.setAttribute("class", "twm-flow-graph__minimap-map");
|
|
4222
|
+
miniSvg.setAttribute("width", String(MINIMAP.width));
|
|
4223
|
+
miniSvg.setAttribute("height", String(MINIMAP.height));
|
|
4224
|
+
mini.title = say(S, "minimapTitle");
|
|
4225
|
+
mini.setAttribute("aria-hidden", "true");
|
|
4226
|
+
mini.appendChild(miniSvg);
|
|
4227
|
+
mini.addEventListener("click", (ev) => {
|
|
4228
|
+
if (!miniMap) return;
|
|
4229
|
+
const r = miniSvg.getBoundingClientRect();
|
|
4230
|
+
const x = (ev.clientX - r.left - miniMap.ox) / miniMap.k;
|
|
4231
|
+
const y = (ev.clientY - r.top - miniMap.oy) / miniMap.k;
|
|
4232
|
+
const view = canvas.root.getBoundingClientRect();
|
|
4233
|
+
canvas.setViewport({ tx: view.width / 2 - x * canvas.scale, ty: view.height / 2 - y * canvas.scale });
|
|
4234
|
+
viewportSettled();
|
|
4235
|
+
});
|
|
4236
|
+
canvasHost.appendChild(mini);
|
|
4237
|
+
}
|
|
4238
|
+
const miniSized = mini && typeof ResizeObserver === "function" ? new ResizeObserver(() => paintMinimap()) : null;
|
|
4239
|
+
miniSized?.observe(canvas.root);
|
|
4240
|
+
function paintMinimap() {
|
|
4241
|
+
if (!mini || destroyed) return;
|
|
4242
|
+
const view = canvas.visibleBox();
|
|
4243
|
+
const boxes = graph.nodes.map((n) => ({
|
|
4244
|
+
x: n.position.x,
|
|
4245
|
+
y: n.position.y,
|
|
4246
|
+
width: BOX.width,
|
|
4247
|
+
height: bottomOf(n) - n.position.y
|
|
4248
|
+
}));
|
|
4249
|
+
const all = view.width > 0 && view.height > 0 ? [...boxes, view] : boxes;
|
|
4250
|
+
if (!all.length) {
|
|
4251
|
+
miniMap = null;
|
|
4252
|
+
miniSvg.replaceChildren();
|
|
4253
|
+
return;
|
|
4254
|
+
}
|
|
4255
|
+
const x0 = Math.min(...all.map((b) => b.x)) - MINIMAP.margin;
|
|
4256
|
+
const y0 = Math.min(...all.map((b) => b.y)) - MINIMAP.margin;
|
|
4257
|
+
const x1 = Math.max(...all.map((b) => b.x + b.width)) + MINIMAP.margin;
|
|
4258
|
+
const y1 = Math.max(...all.map((b) => b.y + b.height)) + MINIMAP.margin;
|
|
4259
|
+
const k = Math.min(MINIMAP.width / (x1 - x0), MINIMAP.height / (y1 - y0));
|
|
4260
|
+
miniMap = { k, ox: (MINIMAP.width - (x1 - x0) * k) / 2 - x0 * k, oy: (MINIMAP.height - (y1 - y0) * k) / 2 - y0 * k };
|
|
4261
|
+
const rect = (b, cls) => {
|
|
4262
|
+
const r = document.createElementNS(SVG, "rect");
|
|
4263
|
+
r.setAttribute("class", cls);
|
|
4264
|
+
r.setAttribute("x", String(b.x * k + miniMap.ox));
|
|
4265
|
+
r.setAttribute("y", String(b.y * k + miniMap.oy));
|
|
4266
|
+
r.setAttribute("width", String(b.width * k));
|
|
4267
|
+
r.setAttribute("height", String(b.height * k));
|
|
4268
|
+
return r;
|
|
4269
|
+
};
|
|
4270
|
+
const parts = boxes.map((b) => rect(b, "twm-flow-graph__minimap-node"));
|
|
4271
|
+
if (view.width > 0 && view.height > 0) parts.push(rect(view, "twm-flow-graph__minimap-view"));
|
|
4272
|
+
miniSvg.replaceChildren(...parts);
|
|
4273
|
+
}
|
|
3560
4274
|
function paintPalette() {
|
|
3561
4275
|
palette.replaceChildren(el("h3", "twm-flow-graph__palette-title", say(S, "palette")));
|
|
3562
4276
|
for (const { category, types } of catalogue.byCategory()) {
|
|
@@ -3593,8 +4307,9 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3593
4307
|
let where = at;
|
|
3594
4308
|
if (!where) {
|
|
3595
4309
|
const box = canvas.visibleBox();
|
|
4310
|
+
const width = nodeWidth(catalogue, { type: typeId }, BOX);
|
|
3596
4311
|
where = {
|
|
3597
|
-
x: canvas.snap((box.width ? box.x + box.width / 2 -
|
|
4312
|
+
x: canvas.snap((box.width ? box.x + box.width / 2 - width / 2 : 80) + n % 4 * 24),
|
|
3598
4313
|
y: canvas.snap((box.height ? box.y + box.height / 3 : 80) + n % 4 * 24)
|
|
3599
4314
|
};
|
|
3600
4315
|
}
|
|
@@ -3653,7 +4368,8 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3653
4368
|
return;
|
|
3654
4369
|
}
|
|
3655
4370
|
const p = canvas.clientToCanvas(u.clientX, u.clientY);
|
|
3656
|
-
|
|
4371
|
+
const width = nodeWidth(catalogue, { type: t.type_id }, BOX);
|
|
4372
|
+
addStep(t.type_id, { x: canvas.snap(p.x - width / 2), y: canvas.snap(p.y - BOX.header / 2) });
|
|
3657
4373
|
}
|
|
3658
4374
|
function onCancel() {
|
|
3659
4375
|
finish();
|
|
@@ -3673,10 +4389,39 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3673
4389
|
dragCleanup = finish;
|
|
3674
4390
|
}
|
|
3675
4391
|
const anchor = (n, port2, direction) => portAnchor(catalogue, n, port2, direction, BOX);
|
|
3676
|
-
const
|
|
4392
|
+
const widthOf = (n) => nodeWidth(catalogue, n, BOX);
|
|
4393
|
+
const measured = (n) => (bodies.has(n.id) || inlines.has(n.id)) && !catalogue.size?.(n.type)?.h && !BOX.folded(n) ? nodeEls.get(n.id)?.offsetHeight || 0 : 0;
|
|
4394
|
+
const bottomOf = (n) => Math.max(nodeBottom(catalogue, n, BOX), n.position.y + measured(n));
|
|
3677
4395
|
const isReturn = (c, target) => isReturnEdge(c, target, catalogue, loopPorts.entry);
|
|
3678
|
-
|
|
4396
|
+
const portKey = (p) => {
|
|
4397
|
+
try {
|
|
4398
|
+
return JSON.stringify(p);
|
|
4399
|
+
} catch {
|
|
4400
|
+
return p;
|
|
4401
|
+
}
|
|
4402
|
+
};
|
|
4403
|
+
function buildKey(nodeRow) {
|
|
3679
4404
|
const def = typeOf(nodeRow);
|
|
4405
|
+
const own2 = def ? catalogue.body?.(nodeRow.type) : null;
|
|
4406
|
+
return {
|
|
4407
|
+
known: Boolean(def),
|
|
4408
|
+
icon: def?.icon || "",
|
|
4409
|
+
label: def?.label ?? null,
|
|
4410
|
+
tone: catalogue.tone(nodeRow.type),
|
|
4411
|
+
ports: [...catalogue.inputs(nodeRow.type), ...catalogue.outputs(nodeRow.type)].map(portKey),
|
|
4412
|
+
render: own2?.render ?? null,
|
|
4413
|
+
update: own2?.update ?? null,
|
|
4414
|
+
inline: def ? portKey(inlineFields(nodeRow)) : ""
|
|
4415
|
+
};
|
|
4416
|
+
}
|
|
4417
|
+
function inlineFields(nodeRow) {
|
|
4418
|
+
const keys = catalogue.inline?.(nodeRow.type) || [];
|
|
4419
|
+
if (!keys.length) return [];
|
|
4420
|
+
const all = fieldsFromSchema(typeOf(nodeRow)?.config_schema);
|
|
4421
|
+
return keys.map((k) => all.find((f) => f.key === k)).filter(Boolean);
|
|
4422
|
+
}
|
|
4423
|
+
const sameBuild = (a, b) => Boolean(a && b) && a.known === b.known && a.icon === b.icon && a.label === b.label && a.tone === b.tone && a.render === b.render && a.update === b.update && a.inline === b.inline && a.ports.length === b.ports.length && a.ports.every((p, i) => p === b.ports[i]);
|
|
4424
|
+
function buildNode(nodeRow, def) {
|
|
3680
4425
|
const tone = catalogue.tone(nodeRow.type);
|
|
3681
4426
|
const n = el("div", "twm-flow-graph__node");
|
|
3682
4427
|
n.dataset.nodeId = nodeRow.id;
|
|
@@ -3690,9 +4435,11 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3690
4435
|
n.setAttribute("aria-label", name);
|
|
3691
4436
|
const title = el("span", "twm-flow-graph__node-title", name);
|
|
3692
4437
|
title.title = name;
|
|
4438
|
+
if (renameInPlace) title.addEventListener("dblclick", () => startRename(nodeRow.id));
|
|
3693
4439
|
const tag = el("span", "twm-flow-graph__node-tag", nodeRow.id);
|
|
3694
4440
|
tag.title = say(S, "stepId", nodeRow.id);
|
|
3695
4441
|
header.append(chip, title, tag);
|
|
4442
|
+
if (folding) header.appendChild(foldButton(nodeRow.id));
|
|
3696
4443
|
header.addEventListener("pointerdown", (ev) => startMove(ev, nodeRow.id));
|
|
3697
4444
|
n.appendChild(header);
|
|
3698
4445
|
const inputs = catalogue.inputs(nodeRow.type);
|
|
@@ -3706,20 +4453,332 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3706
4453
|
);
|
|
3707
4454
|
n.appendChild(row);
|
|
3708
4455
|
}
|
|
4456
|
+
if (def) mountInline(n, nodeRow);
|
|
4457
|
+
const own2 = def ? catalogue.body?.(nodeRow.type) : null;
|
|
4458
|
+
if (own2) mountBody(n, nodeRow, own2);
|
|
3709
4459
|
if (!def) n.appendChild(el("p", "twm-flow-graph__node-finding", say(S, "unknownType", nodeRow.type)));
|
|
3710
4460
|
if (typeof summarise === "function") n.appendChild(el("p", "twm-flow-graph__node-summary"));
|
|
3711
4461
|
const notes = el("div", "twm-flow-graph__node-findings");
|
|
3712
4462
|
const runLine = el("p", "twm-flow-graph__node-run");
|
|
3713
4463
|
n.append(notes, runLine);
|
|
4464
|
+
if (multi) {
|
|
4465
|
+
n.addEventListener("pointerdown", (ev) => {
|
|
4466
|
+
released = null;
|
|
4467
|
+
if (ev.button !== 0 || ev.target.closest(".twm-flow-graph__port")) return;
|
|
4468
|
+
pressed = { id: nodeRow.id, moved: false };
|
|
4469
|
+
}, true);
|
|
4470
|
+
}
|
|
3714
4471
|
n.addEventListener("click", (ev) => {
|
|
3715
4472
|
if (ev.target.closest(".twm-flow-graph__port")) return;
|
|
4473
|
+
if (multi) {
|
|
4474
|
+
const press = pressed || released;
|
|
4475
|
+
pressed = released = null;
|
|
4476
|
+
if (toggles(ev)) {
|
|
4477
|
+
togglePick(nodeRow.id);
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
if (press?.id === nodeRow.id && press.moved) return;
|
|
4481
|
+
}
|
|
3716
4482
|
if (!(selected?.kind === "node" && selected.id === nodeRow.id)) select({ kind: "node", id: nodeRow.id });
|
|
3717
4483
|
});
|
|
3718
4484
|
n.addEventListener("focus", () => {
|
|
4485
|
+
if (multi && (pressed?.id === nodeRow.id || isPicked(nodeRow.id))) return;
|
|
3719
4486
|
if (!(selected?.kind === "node" && selected.id === nodeRow.id)) select({ kind: "node", id: nodeRow.id });
|
|
3720
4487
|
});
|
|
4488
|
+
if (hoverHighlight) {
|
|
4489
|
+
n.addEventListener("pointerenter", () => hover({ kind: "node", id: nodeRow.id }));
|
|
4490
|
+
n.addEventListener("pointerleave", () => {
|
|
4491
|
+
if (hovered?.kind === "node" && hovered.id === nodeRow.id) hover(null);
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
3721
4494
|
return n;
|
|
3722
4495
|
}
|
|
4496
|
+
function mountInline(n, nodeRow) {
|
|
4497
|
+
const list = inlineFields(nodeRow);
|
|
4498
|
+
if (!list.length) return;
|
|
4499
|
+
const box = el("div", "twm-flow-graph__node-inline");
|
|
4500
|
+
const drawn = { readOnly: isReadOnly(), fields: [] };
|
|
4501
|
+
for (const f of list) {
|
|
4502
|
+
const slot = { key: f.key, spec: f, shown: shownKey(nodeRow.config[f.key]), field: null };
|
|
4503
|
+
slot.field = inlineField(nodeRow.id, slot, nodeRow.config[f.key]);
|
|
4504
|
+
drawn.fields.push(slot);
|
|
4505
|
+
box.appendChild(slot.field.el);
|
|
4506
|
+
}
|
|
4507
|
+
n.appendChild(box);
|
|
4508
|
+
inlines.set(nodeRow.id, drawn);
|
|
4509
|
+
}
|
|
4510
|
+
const shownKey = (v) => v === void 0 ? "" : JSON.stringify(v);
|
|
4511
|
+
function inlineField(id, slot, value) {
|
|
4512
|
+
return renderField(slot.spec, value, {
|
|
4513
|
+
widgets,
|
|
4514
|
+
referenceMap: references,
|
|
4515
|
+
strings: S,
|
|
4516
|
+
services,
|
|
4517
|
+
readOnly: isReadOnly(),
|
|
4518
|
+
step: nodeOf(id),
|
|
4519
|
+
type: typeOf(nodeOf(id)),
|
|
4520
|
+
path: "",
|
|
4521
|
+
set: (key, v) => {
|
|
4522
|
+
const before = slot.shown;
|
|
4523
|
+
slot.shown = shownKey(v);
|
|
4524
|
+
if (!setNodeConfig(id, { [key]: v }, "inline")) slot.shown = before;
|
|
4525
|
+
}
|
|
4526
|
+
});
|
|
4527
|
+
}
|
|
4528
|
+
function paintInline(nodeRow) {
|
|
4529
|
+
const drawn = inlines.get(nodeRow.id);
|
|
4530
|
+
if (!drawn) return;
|
|
4531
|
+
const roNow = isReadOnly();
|
|
4532
|
+
const roChanged = drawn.readOnly !== roNow;
|
|
4533
|
+
drawn.readOnly = roNow;
|
|
4534
|
+
for (const slot of drawn.fields) {
|
|
4535
|
+
const value = nodeRow.config[slot.key];
|
|
4536
|
+
const shown = shownKey(value);
|
|
4537
|
+
if (!roChanged && shown === slot.shown) continue;
|
|
4538
|
+
const held = heldCaret(slot.field);
|
|
4539
|
+
slot.field.destroy();
|
|
4540
|
+
const fresh = inlineField(nodeRow.id, slot, value);
|
|
4541
|
+
slot.field.el.replaceWith(fresh.el);
|
|
4542
|
+
slot.field = fresh;
|
|
4543
|
+
slot.shown = shown;
|
|
4544
|
+
if (held) restoreCaret(fresh, held.caret);
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
function heldCaret(field) {
|
|
4548
|
+
const active = document.activeElement;
|
|
4549
|
+
if (!active || !field.el.contains(active)) return null;
|
|
4550
|
+
let caret = field.caret?.() ?? null;
|
|
4551
|
+
if (!caret) {
|
|
4552
|
+
try {
|
|
4553
|
+
if (Number.isInteger(active.selectionStart)) {
|
|
4554
|
+
caret = { start: active.selectionStart, end: active.selectionEnd, direction: active.selectionDirection };
|
|
4555
|
+
}
|
|
4556
|
+
} catch {
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
return { caret };
|
|
4560
|
+
}
|
|
4561
|
+
function restoreCaret(field, caret) {
|
|
4562
|
+
if (!field.focus() || !caret) return;
|
|
4563
|
+
if (field.caret?.()) {
|
|
4564
|
+
field.restoreCaret(caret);
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
const box = document.activeElement;
|
|
4568
|
+
try {
|
|
4569
|
+
const end = box.value.length;
|
|
4570
|
+
box.setSelectionRange(Math.min(caret.start, end), Math.min(caret.end, end), caret.direction || "none");
|
|
4571
|
+
} catch {
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
function dropInline(id) {
|
|
4575
|
+
inlines.get(id)?.fields.forEach((slot) => slot.field.destroy());
|
|
4576
|
+
inlines.delete(id);
|
|
4577
|
+
}
|
|
4578
|
+
const shownName = (nodeRow) => nodeRow.label || typeOf(nodeRow)?.label || nodeRow.type;
|
|
4579
|
+
function renameNode(id, text) {
|
|
4580
|
+
const nodeRow = nodeOf(id);
|
|
4581
|
+
if (!nodeRow) return;
|
|
4582
|
+
if (text) nodeRow.label = text;
|
|
4583
|
+
else delete nodeRow.label;
|
|
4584
|
+
const n = nodeEls.get(nodeRow.id);
|
|
4585
|
+
const name = shownName(nodeRow);
|
|
4586
|
+
const title = n?.querySelector(".twm-flow-graph__node-title");
|
|
4587
|
+
if (title) title.textContent = title.title = name;
|
|
4588
|
+
if (n) {
|
|
4589
|
+
n.dataset.shape = `${nodeRow.type}|${nodeRow.label || ""}`;
|
|
4590
|
+
n.setAttribute("aria-label", name);
|
|
4591
|
+
}
|
|
4592
|
+
commit("flow:node:label", `label:${id}`);
|
|
4593
|
+
}
|
|
4594
|
+
function startRename(id) {
|
|
4595
|
+
if (!renameInPlace || isReadOnly() || destroyed) return false;
|
|
4596
|
+
renaming?.finish(false);
|
|
4597
|
+
const nodeRow = nodeOf(id);
|
|
4598
|
+
const n = nodeEls.get(id);
|
|
4599
|
+
const title = n?.querySelector(".twm-flow-graph__node-title");
|
|
4600
|
+
if (!nodeRow || !title) return false;
|
|
4601
|
+
const before = shownName(nodeRow);
|
|
4602
|
+
const input = el("input", "twm-flow-graph__node-title-input");
|
|
4603
|
+
input.type = "text";
|
|
4604
|
+
input.value = before;
|
|
4605
|
+
input.setAttribute("aria-label", say(S, "rename"));
|
|
4606
|
+
const edit = { id };
|
|
4607
|
+
let done = false;
|
|
4608
|
+
edit.finish = (keep, { enter = false } = {}) => {
|
|
4609
|
+
if (done) return;
|
|
4610
|
+
const text = input.value.trim();
|
|
4611
|
+
if (keep && !text) {
|
|
4612
|
+
setMessage(say(S, "nameEmpty"), "warning");
|
|
4613
|
+
input.setAttribute("aria-invalid", "true");
|
|
4614
|
+
if (enter && input.isConnected) return;
|
|
4615
|
+
} else if (input.getAttribute("aria-invalid")) setMessage("");
|
|
4616
|
+
done = true;
|
|
4617
|
+
if (renaming === edit) renaming = null;
|
|
4618
|
+
const hadFocus = input === document.activeElement;
|
|
4619
|
+
input.remove();
|
|
4620
|
+
title.hidden = false;
|
|
4621
|
+
if (keep && text && text !== before && nodeOf(id)) {
|
|
4622
|
+
renameNode(id, text);
|
|
4623
|
+
if (selected?.kind === "node" && selected.id === id) paintPanel();
|
|
4624
|
+
}
|
|
4625
|
+
if (hadFocus && n.isConnected) n.focus({ preventScroll: true });
|
|
4626
|
+
};
|
|
4627
|
+
input.addEventListener("keydown", (ev) => {
|
|
4628
|
+
if (ev.isComposing) return;
|
|
4629
|
+
if (ev.key === "Enter") {
|
|
4630
|
+
ev.preventDefault();
|
|
4631
|
+
edit.finish(true, { enter: true });
|
|
4632
|
+
} else if (ev.key === "Escape") {
|
|
4633
|
+
ev.preventDefault();
|
|
4634
|
+
ev.stopPropagation();
|
|
4635
|
+
edit.finish(false);
|
|
4636
|
+
}
|
|
4637
|
+
});
|
|
4638
|
+
input.addEventListener("blur", () => edit.finish(true));
|
|
4639
|
+
input.addEventListener("pointerdown", (ev) => ev.stopPropagation());
|
|
4640
|
+
title.hidden = true;
|
|
4641
|
+
title.after(input);
|
|
4642
|
+
renaming = edit;
|
|
4643
|
+
input.focus({ preventScroll: true });
|
|
4644
|
+
input.select();
|
|
4645
|
+
return true;
|
|
4646
|
+
}
|
|
4647
|
+
const nodeCopy = (nodeRow) => normalise({ nodes: [nodeRow], connections: [] }).nodes[0];
|
|
4648
|
+
function mountBody(n, nodeRow, own2) {
|
|
4649
|
+
const id = nodeRow.id;
|
|
4650
|
+
const box = el("div", "twm-flow-graph__node-body");
|
|
4651
|
+
const ctx = Object.freeze({
|
|
4652
|
+
get readOnly() {
|
|
4653
|
+
return isReadOnly();
|
|
4654
|
+
},
|
|
4655
|
+
/** What the host shows on this node now (`setNodeValues`), a copy; null when nothing. */
|
|
4656
|
+
get values() {
|
|
4657
|
+
return valuesCopy(id);
|
|
4658
|
+
},
|
|
4659
|
+
services,
|
|
4660
|
+
setConfig: (patch) => setNodeConfig(id, patch)
|
|
4661
|
+
});
|
|
4662
|
+
try {
|
|
4663
|
+
own2.render(box, nodeCopy(nodeRow), ctx);
|
|
4664
|
+
} catch (err) {
|
|
4665
|
+
bodyFailed(nodeRow, "render", err);
|
|
4666
|
+
return;
|
|
4667
|
+
}
|
|
4668
|
+
n.appendChild(box);
|
|
4669
|
+
bodies.set(id, { el: box, ctx, update: own2.update });
|
|
4670
|
+
}
|
|
4671
|
+
function bodyFailed(nodeRow, stage, err) {
|
|
4672
|
+
const name = nodeRow.label || typeOf(nodeRow)?.label || nodeRow.type;
|
|
4673
|
+
const why2 = err && typeof err === "object" && "message" in err ? err.message : err;
|
|
4674
|
+
bodyFindings.set(nodeRow.id, Object.freeze({
|
|
4675
|
+
code: `node-${stage}`,
|
|
4676
|
+
severity: "error",
|
|
4677
|
+
node_id: nodeRow.id,
|
|
4678
|
+
message: say(S, stage === "render" ? "renderFailed" : "updateFailed", name, String(why2 ?? ""))
|
|
4679
|
+
}));
|
|
4680
|
+
}
|
|
4681
|
+
function setNodeConfig(id, patch, from = "config") {
|
|
4682
|
+
if (destroyed || painting || isReadOnly()) return false;
|
|
4683
|
+
const nodeRow = nodeOf(id);
|
|
4684
|
+
if (!nodeRow || !patch || typeof patch !== "object" || Array.isArray(patch)) return false;
|
|
4685
|
+
const keys = Object.keys(patch).sort();
|
|
4686
|
+
if (!keys.length) return false;
|
|
4687
|
+
let next;
|
|
4688
|
+
try {
|
|
4689
|
+
next = structuredClone(patch);
|
|
4690
|
+
} catch {
|
|
4691
|
+
return false;
|
|
4692
|
+
}
|
|
4693
|
+
for (const k of keys) {
|
|
4694
|
+
if (next[k] === void 0) delete nodeRow.config[k];
|
|
4695
|
+
else nodeRow.config[k] = next[k];
|
|
4696
|
+
}
|
|
4697
|
+
paintNodes();
|
|
4698
|
+
if (selected?.kind === "node" && selected.id === id) paintPanel();
|
|
4699
|
+
commit("flow:node:config", `${from}:${id}:${keys.join(",")}`);
|
|
4700
|
+
return true;
|
|
4701
|
+
}
|
|
4702
|
+
function foldButton(id) {
|
|
4703
|
+
const b = el("button", "twm-flow-graph__node-fold");
|
|
4704
|
+
b.type = "button";
|
|
4705
|
+
b.appendChild(icon("expand_less"));
|
|
4706
|
+
b.addEventListener("pointerdown", (ev) => ev.stopPropagation());
|
|
4707
|
+
b.addEventListener("click", (ev) => {
|
|
4708
|
+
ev.stopPropagation();
|
|
4709
|
+
const nodeRow = nodeOf(id);
|
|
4710
|
+
if (nodeRow) setCollapsed(id, nodeRow.collapsed !== true);
|
|
4711
|
+
});
|
|
4712
|
+
return b;
|
|
4713
|
+
}
|
|
4714
|
+
function setCollapsed(id, collapsed) {
|
|
4715
|
+
if (!folding || destroyed || isReadOnly()) return false;
|
|
4716
|
+
const nodeRow = nodeOf(id);
|
|
4717
|
+
if (!nodeRow || nodeRow.collapsed === true === Boolean(collapsed)) return false;
|
|
4718
|
+
if (collapsed) nodeRow.collapsed = true;
|
|
4719
|
+
else delete nodeRow.collapsed;
|
|
4720
|
+
if (connecting?.source === id) cancelConnect();
|
|
4721
|
+
paintNodes();
|
|
4722
|
+
commit("flow:node:collapse");
|
|
4723
|
+
return true;
|
|
4724
|
+
}
|
|
4725
|
+
function valueEntry(v) {
|
|
4726
|
+
if (!v || typeof v !== "object") return null;
|
|
4727
|
+
const text = (x) => x === void 0 || x === null || x === "" ? null : String(x);
|
|
4728
|
+
const ports = {};
|
|
4729
|
+
for (const [name, x] of Object.entries(plainObject(v.ports))) {
|
|
4730
|
+
const shown = text(x);
|
|
4731
|
+
if (shown !== null) ports[name] = shown;
|
|
4732
|
+
}
|
|
4733
|
+
const badge = text(v.badge);
|
|
4734
|
+
if (badge === null && !Object.keys(ports).length) return null;
|
|
4735
|
+
return Object.freeze({ badge, tone: TONES.includes(v.tone) ? v.tone : null, ports: Object.freeze(ports) });
|
|
4736
|
+
}
|
|
4737
|
+
const valuesCopy = (id) => {
|
|
4738
|
+
const v = liveValues.get(id);
|
|
4739
|
+
return v ? { badge: v.badge, tone: v.tone, ports: { ...v.ports } } : null;
|
|
4740
|
+
};
|
|
4741
|
+
function paintValues(n, nodeRow) {
|
|
4742
|
+
const v = liveValues.get(nodeRow.id) || null;
|
|
4743
|
+
const header = n.querySelector(".twm-flow-graph__node-head");
|
|
4744
|
+
let badge = header.querySelector(".twm-flow-graph__node-badge");
|
|
4745
|
+
if (v?.badge !== null && v?.badge !== void 0) {
|
|
4746
|
+
if (!badge) {
|
|
4747
|
+
badge = el("span", "twm-flow-graph__node-badge");
|
|
4748
|
+
header.insertBefore(badge, header.querySelector(".twm-flow-graph__node-tag"));
|
|
4749
|
+
}
|
|
4750
|
+
if (badge.textContent !== v.badge) badge.textContent = badge.title = v.badge;
|
|
4751
|
+
if (v.tone) badge.dataset.tone = v.tone;
|
|
4752
|
+
else delete badge.dataset.tone;
|
|
4753
|
+
} else badge?.remove();
|
|
4754
|
+
for (const b of n.querySelectorAll(".twm-flow-graph__port")) {
|
|
4755
|
+
const shown = v && Object.hasOwn(v.ports, b.dataset.port) ? v.ports[b.dataset.port] : null;
|
|
4756
|
+
let tag = b.querySelector(".twm-flow-graph__port-value");
|
|
4757
|
+
if (shown === null) {
|
|
4758
|
+
tag?.remove();
|
|
4759
|
+
continue;
|
|
4760
|
+
}
|
|
4761
|
+
if (!tag) {
|
|
4762
|
+
tag = el("span", "twm-flow-graph__port-value");
|
|
4763
|
+
b.insertBefore(tag, b.querySelector(".twm-flow-graph__port-count"));
|
|
4764
|
+
}
|
|
4765
|
+
if (tag.textContent !== shown) tag.textContent = shown;
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
function runUpdate(nodeRow) {
|
|
4769
|
+
const own2 = bodies.get(nodeRow.id);
|
|
4770
|
+
if (typeof own2?.update !== "function") return false;
|
|
4771
|
+
try {
|
|
4772
|
+
own2.update(own2.el, nodeCopy(nodeRow), own2.ctx);
|
|
4773
|
+
if (bodyFindings.get(nodeRow.id)?.code !== "node-update") return false;
|
|
4774
|
+
bodyFindings.delete(nodeRow.id);
|
|
4775
|
+
return true;
|
|
4776
|
+
} catch (err) {
|
|
4777
|
+
const before = bodyFindings.get(nodeRow.id)?.message;
|
|
4778
|
+
bodyFailed(nodeRow, "update", err);
|
|
4779
|
+
return before !== bodyFindings.get(nodeRow.id).message;
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
3723
4782
|
function port(nodeRow, p) {
|
|
3724
4783
|
const b = el("button", `twm-flow-graph__port twm-flow-graph__port--${p.direction === "input" ? "in" : "out"}`);
|
|
3725
4784
|
b.type = "button";
|
|
@@ -3730,23 +4789,29 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3730
4789
|
el("span", "twm-flow-graph__port-label", p.label || p.name),
|
|
3731
4790
|
el("span", "twm-flow-graph__port-count")
|
|
3732
4791
|
);
|
|
3733
|
-
|
|
4792
|
+
const isData = (p.port_type || "FLOW") !== "FLOW";
|
|
4793
|
+
if (isData) {
|
|
3734
4794
|
b.classList.add("twm-flow-graph__port--data");
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
4795
|
+
if (!dataConnect) {
|
|
4796
|
+
b.disabled = true;
|
|
4797
|
+
b.dataset.data = "";
|
|
4798
|
+
b.title = say(S, "dataPort", dataPortRead ? dataPortRead(nodeRow, p) : null);
|
|
4799
|
+
return b;
|
|
4800
|
+
}
|
|
3739
4801
|
}
|
|
3740
4802
|
b.title = p.direction === "output" ? say(S, "connectFrom") : say(S, "connectTo");
|
|
3741
4803
|
b.addEventListener("click", (ev) => {
|
|
3742
4804
|
ev.stopPropagation();
|
|
4805
|
+
if (linking && suppressClick) return;
|
|
3743
4806
|
portClicked(nodeRow.id, p);
|
|
3744
4807
|
});
|
|
4808
|
+
if (linking && p.direction === "output") b.addEventListener("pointerdown", (ev) => startConnectDrag(ev, nodeRow.id, p));
|
|
3745
4809
|
return b;
|
|
3746
4810
|
}
|
|
3747
4811
|
function portClicked(nodeId, p) {
|
|
3748
4812
|
if (isReadOnly()) return;
|
|
3749
4813
|
if (p.direction === "output") {
|
|
4814
|
+
if (pendingOffer) clearOffer();
|
|
3750
4815
|
connecting = { source: nodeId, sourcePort: p.name };
|
|
3751
4816
|
root.classList.add("twm-flow-graph--connecting");
|
|
3752
4817
|
paintNodes();
|
|
@@ -3758,6 +4823,11 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3758
4823
|
return;
|
|
3759
4824
|
}
|
|
3760
4825
|
const edge = { ...connecting, target: nodeId, targetPort: p.name };
|
|
4826
|
+
if (verdictOf) {
|
|
4827
|
+
cancelConnect();
|
|
4828
|
+
linkTo(edge);
|
|
4829
|
+
return;
|
|
4830
|
+
}
|
|
3761
4831
|
const verdict = connect(graph, catalogue, edge, { strings: S });
|
|
3762
4832
|
cancelConnect();
|
|
3763
4833
|
if (!verdict.ok) {
|
|
@@ -3773,15 +4843,244 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3773
4843
|
root.classList.remove("twm-flow-graph--connecting");
|
|
3774
4844
|
paintNodes();
|
|
3775
4845
|
}
|
|
4846
|
+
function linkTo(edge) {
|
|
4847
|
+
const verdict = verdictOf(graph, edge);
|
|
4848
|
+
if (!verdict.ok) {
|
|
4849
|
+
setMessage(verdict.reason, "warning");
|
|
4850
|
+
return;
|
|
4851
|
+
}
|
|
4852
|
+
const build = () => addConnection(graph, edge);
|
|
4853
|
+
if (verdict.offer) {
|
|
4854
|
+
showOffer(verdict.offer, build, "flow:edge:connect");
|
|
4855
|
+
return;
|
|
4856
|
+
}
|
|
4857
|
+
build();
|
|
4858
|
+
setMessage(isReturn(edge, nodeOf(edge.target)) ? say(S, "connectedReturn") : "");
|
|
4859
|
+
paintEdges();
|
|
4860
|
+
commit("flow:edge:connect");
|
|
4861
|
+
}
|
|
4862
|
+
function showOffer(offer, build, action, after = null) {
|
|
4863
|
+
const words3 = String(offer?.message ?? "");
|
|
4864
|
+
pendingOffer = { offer, build, action, after };
|
|
4865
|
+
const accept = button({
|
|
4866
|
+
label: offer?.accept ? String(offer.accept) : say(S, "offerAccept"),
|
|
4867
|
+
primary: true,
|
|
4868
|
+
className: "twm-flow-graph__offer-accept",
|
|
4869
|
+
onClick: () => acceptOffer()
|
|
4870
|
+
});
|
|
4871
|
+
accept.dataset.action = "accept-offer";
|
|
4872
|
+
const dismiss = button({
|
|
4873
|
+
label: say(S, "offerDismiss"),
|
|
4874
|
+
className: "twm-flow-graph__offer-dismiss",
|
|
4875
|
+
onClick: () => {
|
|
4876
|
+
clearOffer();
|
|
4877
|
+
keepFocus();
|
|
4878
|
+
}
|
|
4879
|
+
});
|
|
4880
|
+
dismiss.dataset.action = "dismiss-offer";
|
|
4881
|
+
offerBox.replaceChildren(el("p", "twm-flow-graph__offer-text", words3), accept, dismiss);
|
|
4882
|
+
offerBox.hidden = false;
|
|
4883
|
+
setMessage("");
|
|
4884
|
+
}
|
|
4885
|
+
function clearOffer() {
|
|
4886
|
+
pendingOffer = null;
|
|
4887
|
+
if (!offerBox) return;
|
|
4888
|
+
offerBox.hidden = true;
|
|
4889
|
+
offerBox.replaceChildren();
|
|
4890
|
+
}
|
|
4891
|
+
function acceptOffer() {
|
|
4892
|
+
const held = pendingOffer;
|
|
4893
|
+
if (!held || destroyed) return;
|
|
4894
|
+
clearOffer();
|
|
4895
|
+
if (isReadOnly()) return;
|
|
4896
|
+
const made = held.build();
|
|
4897
|
+
if (typeof held.offer?.apply === "function") {
|
|
4898
|
+
const draft = normalise(graph);
|
|
4899
|
+
const back = held.offer.apply(draft);
|
|
4900
|
+
graph = normalise(back && typeof back === "object" ? back : draft);
|
|
4901
|
+
}
|
|
4902
|
+
held.after?.(made);
|
|
4903
|
+
prunePick();
|
|
4904
|
+
findingsBy = groupFindings(allFindings(), { graph });
|
|
4905
|
+
paintNodes();
|
|
4906
|
+
paintPanel();
|
|
4907
|
+
announceSelection();
|
|
4908
|
+
commit(held.action);
|
|
4909
|
+
keepFocus();
|
|
4910
|
+
}
|
|
4911
|
+
function startConnectDrag(ev, nodeId, p) {
|
|
4912
|
+
if (ev.button !== 0 || ev.isPrimary === false || isReadOnly()) return;
|
|
4913
|
+
dragCleanup?.();
|
|
4914
|
+
const start = { x: ev.clientX, y: ev.clientY };
|
|
4915
|
+
let draft = null;
|
|
4916
|
+
const onMove = (m) => {
|
|
4917
|
+
if (!draft) {
|
|
4918
|
+
if (Math.abs(m.clientX - start.x) + Math.abs(m.clientY - start.y) < DRAG_PX) return;
|
|
4919
|
+
if (pendingOffer) clearOffer();
|
|
4920
|
+
connecting = { source: nodeId, sourcePort: p.name };
|
|
4921
|
+
root.classList.add("twm-flow-graph--connecting");
|
|
4922
|
+
paintNodes();
|
|
4923
|
+
setMessage(say(S, "connectDrag"));
|
|
4924
|
+
draft = document.createElementNS(SVG, "path");
|
|
4925
|
+
draft.setAttribute("class", "twm-flow-graph__edge twm-flow-graph__edge--draft");
|
|
4926
|
+
}
|
|
4927
|
+
const from = nodeOf(nodeId);
|
|
4928
|
+
if (!from) return;
|
|
4929
|
+
if (!draft.isConnected) canvas.edgeLayer.appendChild(draft);
|
|
4930
|
+
draft.setAttribute("d", routeEdge(
|
|
4931
|
+
anchor(from, p.name, "output"),
|
|
4932
|
+
canvas.clientToCanvas(m.clientX, m.clientY),
|
|
4933
|
+
{ simple: true }
|
|
4934
|
+
));
|
|
4935
|
+
};
|
|
4936
|
+
const finish = () => {
|
|
4937
|
+
window.removeEventListener("pointermove", onMove);
|
|
4938
|
+
window.removeEventListener("pointerup", onUp);
|
|
4939
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
4940
|
+
window.removeEventListener("keydown", onKey, true);
|
|
4941
|
+
draft?.remove();
|
|
4942
|
+
dragCleanup = null;
|
|
4943
|
+
};
|
|
4944
|
+
function onUp(u) {
|
|
4945
|
+
const dragged = Boolean(draft);
|
|
4946
|
+
finish();
|
|
4947
|
+
if (!dragged) return;
|
|
4948
|
+
swallowNextClick({ afterRelease: true });
|
|
4949
|
+
dropConnection(u);
|
|
4950
|
+
}
|
|
4951
|
+
function onCancel() {
|
|
4952
|
+
const dragged = Boolean(draft);
|
|
4953
|
+
finish();
|
|
4954
|
+
if (dragged) {
|
|
4955
|
+
cancelConnect();
|
|
4956
|
+
setMessage("");
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
function onKey(k) {
|
|
4960
|
+
if (k.key !== "Escape") return;
|
|
4961
|
+
k.preventDefault();
|
|
4962
|
+
k.stopPropagation();
|
|
4963
|
+
const dragged = Boolean(draft);
|
|
4964
|
+
finish();
|
|
4965
|
+
if (!dragged) return;
|
|
4966
|
+
cancelConnect();
|
|
4967
|
+
setMessage("");
|
|
4968
|
+
swallowNextClick();
|
|
4969
|
+
}
|
|
4970
|
+
window.addEventListener("pointermove", onMove);
|
|
4971
|
+
window.addEventListener("pointerup", onUp);
|
|
4972
|
+
window.addEventListener("pointercancel", onCancel);
|
|
4973
|
+
window.addEventListener("keydown", onKey, true);
|
|
4974
|
+
dragCleanup = finish;
|
|
4975
|
+
}
|
|
4976
|
+
function dropConnection(u) {
|
|
4977
|
+
const from = connecting;
|
|
4978
|
+
const under = document.elementFromPoint(u.clientX, u.clientY);
|
|
4979
|
+
const nodeHit = under?.closest("[data-node-id]");
|
|
4980
|
+
const onNode = Boolean(nodeHit && canvas.nodeLayer.contains(nodeHit));
|
|
4981
|
+
const portHit = onNode ? under.closest(".twm-flow-graph__port") : null;
|
|
4982
|
+
if (portHit && portHit.dataset.direction === "input") {
|
|
4983
|
+
portClicked(nodeHit.dataset.nodeId, { direction: "input", name: portHit.dataset.port });
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
cancelConnect();
|
|
4987
|
+
if (onNode) {
|
|
4988
|
+
setMessage(say(S, "dropOnInput"));
|
|
4989
|
+
return;
|
|
4990
|
+
}
|
|
4991
|
+
if (under && canvas.root.contains(under) || overCanvas(u.clientX, u.clientY)) {
|
|
4992
|
+
openDropPicker(from, u);
|
|
4993
|
+
return;
|
|
4994
|
+
}
|
|
4995
|
+
setMessage("");
|
|
4996
|
+
}
|
|
4997
|
+
function openDropPicker(from, u) {
|
|
4998
|
+
const p = canvas.clientToCanvas(u.clientX, u.clientY);
|
|
4999
|
+
const where = { x: canvas.snap(p.x), y: canvas.snap(p.y - BOX.header / 2) };
|
|
5000
|
+
const fits = /* @__PURE__ */ new Map();
|
|
5001
|
+
for (const t of catalogue.list()) {
|
|
5002
|
+
if (t.unavailable || !offered(t)) continue;
|
|
5003
|
+
const trial = normalise(graph);
|
|
5004
|
+
const added = addNode(trial, t.type_id, where, { catalogue });
|
|
5005
|
+
let pick = null;
|
|
5006
|
+
for (const inp of catalogue.inputs(t.type_id)) {
|
|
5007
|
+
const v = verdictOf(trial, { ...from, target: added.id, targetPort: inp.name });
|
|
5008
|
+
if (v.ok && (!pick || pick.offer && !v.offer)) pick = { port: inp.name, offer: v.offer || null };
|
|
5009
|
+
if (pick && !pick.offer) break;
|
|
5010
|
+
}
|
|
5011
|
+
if (pick) fits.set(t.type_id, pick);
|
|
5012
|
+
}
|
|
5013
|
+
if (!fits.size) {
|
|
5014
|
+
setMessage(say(S, "dropNothingFits"), "warning");
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
const mark = el("span", "twm-flow-graph__drop-anchor");
|
|
5018
|
+
const box = canvasHost.getBoundingClientRect();
|
|
5019
|
+
mark.style.left = `${Math.round(u.clientX - box.left)}px`;
|
|
5020
|
+
mark.style.top = `${Math.round(u.clientY - box.top)}px`;
|
|
5021
|
+
canvasHost.appendChild(mark);
|
|
5022
|
+
const asked = snapshot();
|
|
5023
|
+
openStepPicker({
|
|
5024
|
+
anchor: mark,
|
|
5025
|
+
strings: S,
|
|
5026
|
+
title: say(S, "dropPickerTitle"),
|
|
5027
|
+
categories: catalogue.categories.map((c) => ({ id: c.id, label: c.label, layout: c.layout })),
|
|
5028
|
+
entries: catalogue.list().filter((t) => fits.has(t.type_id)).map((t) => ({
|
|
5029
|
+
id: t.type_id,
|
|
5030
|
+
label: t.label,
|
|
5031
|
+
description: t.description,
|
|
5032
|
+
icon: t.icon || "widgets",
|
|
5033
|
+
tone: catalogue.tone(t.type_id),
|
|
5034
|
+
category: t.category ?? ""
|
|
5035
|
+
}))
|
|
5036
|
+
}).then((choice) => {
|
|
5037
|
+
mark.remove();
|
|
5038
|
+
if (destroyed) return;
|
|
5039
|
+
if (!choice?.entry || isReadOnly() || snapshot() !== asked) {
|
|
5040
|
+
keepFocus();
|
|
5041
|
+
return;
|
|
5042
|
+
}
|
|
5043
|
+
const fitting = fits.get(choice.entry.id);
|
|
5044
|
+
const build = () => {
|
|
5045
|
+
const added = addNode(graph, choice.entry.id, where, { catalogue });
|
|
5046
|
+
addConnection(graph, { ...from, target: added.id, targetPort: fitting.port });
|
|
5047
|
+
return added.id;
|
|
5048
|
+
};
|
|
5049
|
+
const after = (id) => {
|
|
5050
|
+
if (nodeOf(id)) selected = { kind: "node", id };
|
|
5051
|
+
};
|
|
5052
|
+
if (fitting.offer) {
|
|
5053
|
+
showOffer(fitting.offer, build, "flow:node:add", after);
|
|
5054
|
+
keepFocus();
|
|
5055
|
+
return;
|
|
5056
|
+
}
|
|
5057
|
+
after(build());
|
|
5058
|
+
paintNodes();
|
|
5059
|
+
paintPanel();
|
|
5060
|
+
announceSelection();
|
|
5061
|
+
commit("flow:node:add");
|
|
5062
|
+
keepFocus();
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
3776
5065
|
function startMove(ev, nodeId) {
|
|
3777
5066
|
if (ev.button !== 0) return;
|
|
3778
5067
|
ev.stopPropagation();
|
|
3779
|
-
|
|
5068
|
+
if (toggles(ev)) return;
|
|
5069
|
+
if (!(multi && isPicked(nodeId))) select({ kind: "node", id: nodeId });
|
|
3780
5070
|
if (isReadOnly()) return;
|
|
3781
|
-
const
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
5071
|
+
const ids = selected?.kind === "many" ? selected.ids : [nodeId];
|
|
5072
|
+
if (!ids.includes(nodeId)) return;
|
|
5073
|
+
dragItems(ev, ids, pickedNotes(), () => {
|
|
5074
|
+
if (pressed?.id === nodeId) pressed.moved = true;
|
|
5075
|
+
});
|
|
5076
|
+
}
|
|
5077
|
+
function dragItems(ev, nodeIds, noteIds, onFirstMove = null) {
|
|
5078
|
+
const rows = [
|
|
5079
|
+
...nodeIds.map((id) => ({ item: nodeOf(id), n: nodeEls.get(id) })),
|
|
5080
|
+
...noteIds.map((id) => ({ item: noteOf(id), n: noteEls.get(id), note: true }))
|
|
5081
|
+
].filter((r) => r.item && r.n);
|
|
5082
|
+
if (!rows.length) return;
|
|
5083
|
+
const from = rows.map((r) => ({ ...r.item.position }));
|
|
3785
5084
|
const start = { x: ev.clientX, y: ev.clientY };
|
|
3786
5085
|
let moved = false;
|
|
3787
5086
|
const onMove = (m) => {
|
|
@@ -3789,16 +5088,21 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3789
5088
|
const dy = (m.clientY - start.y) / canvas.scale;
|
|
3790
5089
|
if (!moved && Math.abs(dx) + Math.abs(dy) < 3) return;
|
|
3791
5090
|
moved = true;
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
5091
|
+
onFirstMove?.();
|
|
5092
|
+
rows.forEach(({ item, n }, i) => {
|
|
5093
|
+
item.position = { x: canvas.snap(from[i].x + dx), y: canvas.snap(from[i].y + dy) };
|
|
5094
|
+
n.style.left = `${item.position.x}px`;
|
|
5095
|
+
n.style.top = `${item.position.y}px`;
|
|
5096
|
+
});
|
|
3795
5097
|
paintEdges();
|
|
3796
5098
|
};
|
|
3797
5099
|
const onUp = () => {
|
|
3798
5100
|
window.removeEventListener("pointermove", onMove);
|
|
3799
5101
|
window.removeEventListener("pointerup", onUp);
|
|
3800
5102
|
window.removeEventListener("pointercancel", onUp);
|
|
3801
|
-
if (moved)
|
|
5103
|
+
if (!moved) return;
|
|
5104
|
+
const nodes = rows.filter((r) => !r.note).length;
|
|
5105
|
+
commit(!nodes ? "flow:note:move" : rows.length > 1 ? "flow:nodes:move" : "flow:node:move");
|
|
3802
5106
|
};
|
|
3803
5107
|
window.addEventListener("pointermove", onMove);
|
|
3804
5108
|
window.addEventListener("pointerup", onUp);
|
|
@@ -3806,69 +5110,187 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3806
5110
|
}
|
|
3807
5111
|
function paintNodes() {
|
|
3808
5112
|
if (destroyed) return;
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
if (n) n.replaceWith(fresh);
|
|
3824
|
-
else canvas.nodeLayer.appendChild(fresh);
|
|
3825
|
-
nodeEls.set(nodeRow.id, fresh);
|
|
3826
|
-
n = fresh;
|
|
3827
|
-
if (focused) fresh.focus({ preventScroll: true });
|
|
3828
|
-
}
|
|
3829
|
-
n.style.left = `${nodeRow.position.x}px`;
|
|
3830
|
-
n.style.top = `${nodeRow.position.y}px`;
|
|
3831
|
-
n.style.width = `${BOX.width}px`;
|
|
3832
|
-
const picked = selected?.kind === "node" && selected.id === nodeRow.id;
|
|
3833
|
-
n.classList.toggle("twm-flow-graph__node--picked", picked);
|
|
3834
|
-
if (picked) n.setAttribute("aria-current", "true");
|
|
3835
|
-
else n.removeAttribute("aria-current");
|
|
3836
|
-
n.classList.toggle("twm-flow-graph__node--connect-source", connecting?.source === nodeRow.id);
|
|
3837
|
-
const mine = findingsBy.get(nodeRow.id) || [];
|
|
3838
|
-
const errors = mine.filter((f) => f.severity !== "warning");
|
|
3839
|
-
n.classList.toggle("twm-flow-graph__node--error", errors.length > 0);
|
|
3840
|
-
n.classList.toggle("twm-flow-graph__node--warning", errors.length === 0 && mine.length > 0);
|
|
3841
|
-
n.querySelector(".twm-flow-graph__node-findings").replaceChildren(...mine.slice(0, 2).map((f) => el(
|
|
3842
|
-
"p",
|
|
3843
|
-
`twm-flow-graph__node-finding${f.severity === "warning" ? " twm-flow-graph__node-finding--warning" : ""}`,
|
|
3844
|
-
String(f.message ?? "")
|
|
3845
|
-
)));
|
|
3846
|
-
const summary = n.querySelector(".twm-flow-graph__node-summary");
|
|
3847
|
-
if (summary) summary.textContent = summarise(nodeRow, typeOf(nodeRow)) || "";
|
|
3848
|
-
const step = overlay?.steps?.[nodeRow.id] || null;
|
|
3849
|
-
const line = n.querySelector(".twm-flow-graph__node-run");
|
|
3850
|
-
line.textContent = step ? String(step.line ?? step.state ?? "") : "";
|
|
3851
|
-
if (step?.state) line.dataset.state = String(step.state);
|
|
3852
|
-
else delete line.dataset.state;
|
|
3853
|
-
if (step?.tone) line.dataset.tone = String(step.tone);
|
|
3854
|
-
else delete line.dataset.tone;
|
|
3855
|
-
n.classList.toggle("twm-flow-graph__node--skipped", step?.state === "skipped" || step?.state === "not-reached");
|
|
3856
|
-
const ports = overlay?.ports?.[nodeRow.id] || null;
|
|
3857
|
-
for (const b of n.querySelectorAll(".twm-flow-graph__port")) {
|
|
3858
|
-
if (!("data" in b.dataset)) b.disabled = isReadOnly();
|
|
3859
|
-
const mark = b.dataset.direction === "output" ? ports?.[b.dataset.port] : null;
|
|
3860
|
-
b.classList.toggle("twm-flow-graph__port--taken", Boolean(mark?.taken));
|
|
3861
|
-
b.classList.toggle("twm-flow-graph__port--untaken", Boolean(mark) && !mark.taken);
|
|
3862
|
-
const count2 = b.querySelector(".twm-flow-graph__port-count");
|
|
3863
|
-
count2.textContent = mark?.taken && Number.isFinite(Number(mark.count)) && mark.count !== null ? say(S, "portCount", mark.count) : mark && !mark.taken ? say(S, "portNotTaken") : "";
|
|
5113
|
+
painting = true;
|
|
5114
|
+
let bodyChanged = false;
|
|
5115
|
+
try {
|
|
5116
|
+
const live = new Set(graph.nodes.map((x) => x.id));
|
|
5117
|
+
for (const [id, n] of nodeEls) {
|
|
5118
|
+
if (!live.has(id)) {
|
|
5119
|
+
n.remove();
|
|
5120
|
+
nodeEls.delete(id);
|
|
5121
|
+
builtFrom.delete(id);
|
|
5122
|
+
bodies.delete(id);
|
|
5123
|
+
dropInline(id);
|
|
5124
|
+
if (renaming?.id === id) renaming.finish(false);
|
|
5125
|
+
bodyChanged = bodyFindings.delete(id) || bodyChanged;
|
|
5126
|
+
}
|
|
3864
5127
|
}
|
|
5128
|
+
if (bodyChanged) findingsBy = groupFindings(allFindings(), { graph });
|
|
5129
|
+
const picks = pickedSet();
|
|
5130
|
+
for (const nodeRow of graph.nodes) bodyChanged = paintNode(nodeRow, picks) || bodyChanged;
|
|
5131
|
+
paintNotes();
|
|
5132
|
+
} finally {
|
|
5133
|
+
painting = false;
|
|
5134
|
+
}
|
|
5135
|
+
if (bodyChanged) {
|
|
5136
|
+
paintStrip();
|
|
5137
|
+
panel.setFindings(allFindings());
|
|
3865
5138
|
}
|
|
3866
5139
|
paintEdges();
|
|
3867
5140
|
}
|
|
5141
|
+
function edgeLook(c, source, target, back) {
|
|
5142
|
+
const look = edgeStyle({ ...c }, { source, target, back });
|
|
5143
|
+
if (!look || typeof look !== "object") return null;
|
|
5144
|
+
const width = Number(look.width);
|
|
5145
|
+
return {
|
|
5146
|
+
tone: TONES.includes(look.tone) ? look.tone : null,
|
|
5147
|
+
dashed: Boolean(look.dashed),
|
|
5148
|
+
width: Number.isFinite(width) && width > 0 ? width : null,
|
|
5149
|
+
marker: typeof look.marker === "string" && look.marker ? look.marker : null,
|
|
5150
|
+
label: look.label === void 0 || look.label === null || look.label === "" ? null : String(look.label)
|
|
5151
|
+
};
|
|
5152
|
+
}
|
|
5153
|
+
function applyLook(element, look) {
|
|
5154
|
+
if (look.tone) element.style.setProperty("--twm-canvas-edge-stroke", `var(--twm-flow-${look.tone}-fg)`);
|
|
5155
|
+
if (look.width) element.style.setProperty("--twm-canvas-edge-width", String(look.width));
|
|
5156
|
+
}
|
|
5157
|
+
function arrowHead(tip, side) {
|
|
5158
|
+
const out = side === "right" ? 1 : -1;
|
|
5159
|
+
const baseX = tip.x + out * ARROW.length;
|
|
5160
|
+
const head = document.createElementNS(SVG, "path");
|
|
5161
|
+
head.setAttribute("class", "twm-flow-graph__edge-arrow");
|
|
5162
|
+
head.setAttribute("d", `M ${tip.x} ${tip.y} L ${baseX} ${tip.y - ARROW.half} L ${baseX} ${tip.y + ARROW.half} Z`);
|
|
5163
|
+
return head;
|
|
5164
|
+
}
|
|
5165
|
+
function lineEnd(to, marker) {
|
|
5166
|
+
if (marker === "arrow") return { ...to, x: to.x + (to.side === "right" ? 1 : -1) * ARROW.length };
|
|
5167
|
+
return marker ? footAnchor(to, to.side, marker) : to;
|
|
5168
|
+
}
|
|
5169
|
+
const touches = (c) => hovered?.kind === "node" ? c.source === hovered.id || c.target === hovered.id : hovered?.kind === "edge" ? sameEdge(c, hovered.edge) : false;
|
|
5170
|
+
function hover(next) {
|
|
5171
|
+
hovered = next;
|
|
5172
|
+
for (const { c, line } of edgeEls) line.classList.toggle("twm-flow-graph__edge--highlighted", touches(c));
|
|
5173
|
+
}
|
|
5174
|
+
function paintStrip() {
|
|
5175
|
+
strip.set(routingFallback ? [...allFindings(), {
|
|
5176
|
+
code: "canvas-routing-simple",
|
|
5177
|
+
severity: "warning",
|
|
5178
|
+
message: say(S, "routingFallback", OBSTACLE_ROUTING_NODE_LIMIT)
|
|
5179
|
+
}] : allFindings());
|
|
5180
|
+
}
|
|
5181
|
+
function paintNode(nodeRow, picks) {
|
|
5182
|
+
let bodyChanged = false;
|
|
5183
|
+
let n = nodeEls.get(nodeRow.id);
|
|
5184
|
+
const def = typeOf(nodeRow);
|
|
5185
|
+
const shape = `${nodeRow.type}|${nodeRow.label || ""}`;
|
|
5186
|
+
const key = buildKey(nodeRow);
|
|
5187
|
+
if (!n || n.dataset.shape !== shape || !sameBuild(builtFrom.get(nodeRow.id), key)) {
|
|
5188
|
+
if (renaming?.id === nodeRow.id) renaming.finish(false);
|
|
5189
|
+
bodies.delete(nodeRow.id);
|
|
5190
|
+
dropInline(nodeRow.id);
|
|
5191
|
+
bodyChanged = bodyFindings.delete(nodeRow.id);
|
|
5192
|
+
const fresh = buildNode(nodeRow, def);
|
|
5193
|
+
bodyChanged = bodyChanged || bodyFindings.has(nodeRow.id);
|
|
5194
|
+
fresh.dataset.shape = shape;
|
|
5195
|
+
const focused = n && n.contains(document.activeElement);
|
|
5196
|
+
if (n) n.replaceWith(fresh);
|
|
5197
|
+
else canvas.nodeLayer.appendChild(fresh);
|
|
5198
|
+
nodeEls.set(nodeRow.id, fresh);
|
|
5199
|
+
builtFrom.set(nodeRow.id, key);
|
|
5200
|
+
n = fresh;
|
|
5201
|
+
if (focused) fresh.focus({ preventScroll: true });
|
|
5202
|
+
} else paintInline(nodeRow);
|
|
5203
|
+
bodyChanged = runUpdate(nodeRow) || bodyChanged;
|
|
5204
|
+
if (bodyChanged) findingsBy = groupFindings(allFindings(), { graph });
|
|
5205
|
+
n.style.left = `${nodeRow.position.x}px`;
|
|
5206
|
+
n.style.top = `${nodeRow.position.y}px`;
|
|
5207
|
+
n.style.width = `${widthOf(nodeRow)}px`;
|
|
5208
|
+
const folded = BOX.folded(nodeRow);
|
|
5209
|
+
const h = folded ? null : catalogue.size?.(nodeRow.type)?.h;
|
|
5210
|
+
if (h) n.style.height = `${h}px`;
|
|
5211
|
+
else n.style.removeProperty("height");
|
|
5212
|
+
n.classList.toggle("twm-flow-graph__node--sized", Boolean(h));
|
|
5213
|
+
if (folding) paintFold(n, folded);
|
|
5214
|
+
if (liveValues.size || n.querySelector(".twm-flow-graph__node-badge, .twm-flow-graph__port-value")) {
|
|
5215
|
+
paintValues(n, nodeRow);
|
|
5216
|
+
}
|
|
5217
|
+
const accent = catalogue.accent?.(nodeRow.type) || null;
|
|
5218
|
+
if (accent) n.dataset.accent = accent;
|
|
5219
|
+
else delete n.dataset.accent;
|
|
5220
|
+
const picked = picks.has(nodeRow.id);
|
|
5221
|
+
n.classList.toggle("twm-flow-graph__node--picked", picked);
|
|
5222
|
+
if (picked) n.setAttribute("aria-current", "true");
|
|
5223
|
+
else n.removeAttribute("aria-current");
|
|
5224
|
+
n.classList.toggle("twm-flow-graph__node--connect-source", connecting?.source === nodeRow.id);
|
|
5225
|
+
const mine = findingsBy.get(nodeRow.id) || [];
|
|
5226
|
+
const errors = mine.filter((f) => f.severity !== "warning");
|
|
5227
|
+
n.classList.toggle("twm-flow-graph__node--error", errors.length > 0);
|
|
5228
|
+
n.classList.toggle("twm-flow-graph__node--warning", errors.length === 0 && mine.length > 0);
|
|
5229
|
+
n.querySelector(".twm-flow-graph__node-findings").replaceChildren(...mine.slice(0, 2).map((f) => el(
|
|
5230
|
+
"p",
|
|
5231
|
+
`twm-flow-graph__node-finding${f.severity === "warning" ? " twm-flow-graph__node-finding--warning" : ""}`,
|
|
5232
|
+
String(f.message ?? "")
|
|
5233
|
+
)));
|
|
5234
|
+
const summary = n.querySelector(".twm-flow-graph__node-summary");
|
|
5235
|
+
if (summary) summary.textContent = summarise(nodeRow, typeOf(nodeRow)) || "";
|
|
5236
|
+
const step = overlay?.steps?.[nodeRow.id] || null;
|
|
5237
|
+
const line = n.querySelector(".twm-flow-graph__node-run");
|
|
5238
|
+
line.textContent = step ? String(step.line ?? step.state ?? "") : "";
|
|
5239
|
+
if (step?.state) line.dataset.state = String(step.state);
|
|
5240
|
+
else delete line.dataset.state;
|
|
5241
|
+
if (step?.tone) line.dataset.tone = String(step.tone);
|
|
5242
|
+
else delete line.dataset.tone;
|
|
5243
|
+
n.classList.toggle("twm-flow-graph__node--skipped", step?.state === "skipped" || step?.state === "not-reached");
|
|
5244
|
+
const ports = overlay?.ports?.[nodeRow.id] || null;
|
|
5245
|
+
for (const b of n.querySelectorAll(".twm-flow-graph__port")) {
|
|
5246
|
+
if (!("data" in b.dataset)) b.disabled = isReadOnly();
|
|
5247
|
+
if (verdictOf && b.dataset.direction === "input") {
|
|
5248
|
+
b.classList.toggle("twm-flow-graph__port--can-drop", Boolean(connecting) && verdictOf(graph, {
|
|
5249
|
+
...connecting,
|
|
5250
|
+
target: nodeRow.id,
|
|
5251
|
+
targetPort: b.dataset.port
|
|
5252
|
+
}).ok);
|
|
5253
|
+
}
|
|
5254
|
+
const mark = b.dataset.direction === "output" ? ports?.[b.dataset.port] : null;
|
|
5255
|
+
b.classList.toggle("twm-flow-graph__port--taken", Boolean(mark?.taken));
|
|
5256
|
+
b.classList.toggle("twm-flow-graph__port--untaken", Boolean(mark) && !mark.taken);
|
|
5257
|
+
const count2 = b.querySelector(".twm-flow-graph__port-count");
|
|
5258
|
+
count2.textContent = mark?.taken && Number.isFinite(Number(mark.count)) && mark.count !== null ? say(S, "portCount", mark.count) : mark && !mark.taken ? say(S, "portNotTaken") : "";
|
|
5259
|
+
}
|
|
5260
|
+
return bodyChanged;
|
|
5261
|
+
}
|
|
5262
|
+
function paintFold(n, folded) {
|
|
5263
|
+
n.classList.toggle("twm-flow-graph__node--collapsed", folded);
|
|
5264
|
+
const b = n.querySelector(".twm-flow-graph__node-fold");
|
|
5265
|
+
if (!b) return;
|
|
5266
|
+
b.disabled = isReadOnly();
|
|
5267
|
+
b.setAttribute("aria-expanded", String(!folded));
|
|
5268
|
+
b.title = say(S, folded ? "expandNode" : "collapseNode");
|
|
5269
|
+
b.setAttribute("aria-label", b.title);
|
|
5270
|
+
const glyph = b.querySelector(".material-symbols-outlined");
|
|
5271
|
+
const name = folded ? "expand_more" : "expand_less";
|
|
5272
|
+
if (glyph && glyph.textContent !== name) glyph.textContent = name;
|
|
5273
|
+
}
|
|
3868
5274
|
function paintEdges() {
|
|
3869
5275
|
if (destroyed) return;
|
|
5276
|
+
const fallback = routing === "obstacles" && graph.nodes.length > OBSTACLE_ROUTING_NODE_LIMIT;
|
|
5277
|
+
if (fallback !== routingFallback) {
|
|
5278
|
+
routingFallback = fallback;
|
|
5279
|
+
paintStrip();
|
|
5280
|
+
}
|
|
5281
|
+
const routed = routing === "obstacles" && !fallback;
|
|
3870
5282
|
const byId = new Map(graph.nodes.map((x) => [x.id, x]));
|
|
5283
|
+
const copies = styled ? new Map(normalise(graph).nodes.map((x) => [x.id, x])) : null;
|
|
5284
|
+
const boxes = routed ? graph.nodes.map((x) => ({
|
|
5285
|
+
id: x.id,
|
|
5286
|
+
x: x.position.x - CLEARANCE,
|
|
5287
|
+
y: x.position.y - CLEARANCE,
|
|
5288
|
+
width: widthOf(x) + 2 * CLEARANCE,
|
|
5289
|
+
height: bottomOf(x) - x.position.y + 2 * CLEARANCE
|
|
5290
|
+
})) : null;
|
|
3871
5291
|
const paths = [];
|
|
5292
|
+
const labels = [];
|
|
5293
|
+
edgeEls = [];
|
|
3872
5294
|
graph.connections.forEach((c) => {
|
|
3873
5295
|
const a = byId.get(c.source);
|
|
3874
5296
|
const b = byId.get(c.target);
|
|
@@ -3876,18 +5298,23 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3876
5298
|
const from = anchor(a, c.sourcePort, "output");
|
|
3877
5299
|
const to = anchor(b, c.targetPort, "input");
|
|
3878
5300
|
const back = isReturn(c, b);
|
|
3879
|
-
const
|
|
5301
|
+
const look = styled ? edgeLook(c, copies.get(c.source), copies.get(c.target), back) : null;
|
|
5302
|
+
const end = look?.marker ? lineEnd(to, look.marker) : to;
|
|
5303
|
+
const d = back ? returnEdge(from, end, { floor: Math.max(bottomOf(a), bottomOf(b)) + RETURN_DROP }) : routed ? routeEdge(from, end, { obstacles: boxes.filter((x) => x.id !== a.id && x.id !== b.id) }) : routeEdge(from, end, { simple: true });
|
|
3880
5304
|
const picked = selected?.kind === "edge" && sameEdge(selected.edge, c);
|
|
3881
5305
|
const mark = overlay?.ports?.[c.source]?.[c.sourcePort];
|
|
3882
5306
|
const line = document.createElementNS(SVG, "path");
|
|
3883
5307
|
line.setAttribute("class", [
|
|
3884
5308
|
"twm-flow-graph__edge",
|
|
3885
5309
|
back ? "twm-flow-graph__edge--back" : "",
|
|
5310
|
+
look?.dashed ? "twm-flow-graph__edge--dashed" : "",
|
|
3886
5311
|
picked ? "twm-flow-graph__edge--picked" : "",
|
|
3887
5312
|
mark?.taken ? "twm-flow-graph__edge--taken" : "",
|
|
3888
|
-
mark && !mark.taken ? "twm-flow-graph__edge--untaken" : ""
|
|
5313
|
+
mark && !mark.taken ? "twm-flow-graph__edge--untaken" : "",
|
|
5314
|
+
hovered && touches(c) ? "twm-flow-graph__edge--highlighted" : ""
|
|
3889
5315
|
].filter(Boolean).join(" "));
|
|
3890
5316
|
line.setAttribute("d", d);
|
|
5317
|
+
if (look) applyLook(line, look);
|
|
3891
5318
|
const hit = document.createElementNS(SVG, "path");
|
|
3892
5319
|
hit.setAttribute("class", "twm-flow-graph__edge-hit");
|
|
3893
5320
|
hit.setAttribute("d", d);
|
|
@@ -3896,42 +5323,434 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3896
5323
|
title.textContent = say(S, "returnLine");
|
|
3897
5324
|
hit.appendChild(title);
|
|
3898
5325
|
}
|
|
3899
|
-
|
|
5326
|
+
const pick = (ev) => {
|
|
3900
5327
|
ev.stopPropagation();
|
|
3901
5328
|
select({ kind: "edge", edge: { ...c } });
|
|
3902
|
-
}
|
|
3903
|
-
|
|
5329
|
+
};
|
|
5330
|
+
hit.addEventListener("pointerdown", pick);
|
|
5331
|
+
if (hoverHighlight) {
|
|
5332
|
+
hit.addEventListener("pointerenter", () => hover({ kind: "edge", edge: { ...c } }));
|
|
5333
|
+
hit.addEventListener("pointerleave", () => {
|
|
5334
|
+
if (hovered?.kind === "edge" && sameEdge(hovered.edge, c)) hover(null);
|
|
5335
|
+
});
|
|
5336
|
+
}
|
|
5337
|
+
paths.push(line);
|
|
5338
|
+
if (look?.marker) {
|
|
5339
|
+
const head = look.marker === "arrow" ? arrowHead(to, to.side) : crowsFoot(to, to.side, look.marker);
|
|
5340
|
+
if (look.marker !== "arrow") head.classList.add("twm-flow-graph__edge-foot");
|
|
5341
|
+
applyLook(head, look);
|
|
5342
|
+
paths.push(head);
|
|
5343
|
+
}
|
|
5344
|
+
paths.push(hit);
|
|
5345
|
+
if (look?.label !== null && look?.label !== void 0) {
|
|
5346
|
+
const at = pathMidpoint(d);
|
|
5347
|
+
const tag = el("div", "twm-flow-graph__edge-label", look.label);
|
|
5348
|
+
tag.style.left = `${at.x}px`;
|
|
5349
|
+
tag.style.top = `${at.y}px`;
|
|
5350
|
+
applyLook(tag, look);
|
|
5351
|
+
tag.addEventListener("pointerdown", pick);
|
|
5352
|
+
labels.push(tag);
|
|
5353
|
+
}
|
|
5354
|
+
edgeEls.push({ c, line });
|
|
3904
5355
|
});
|
|
3905
5356
|
canvas.edgeLayer.replaceChildren(...paths);
|
|
5357
|
+
labelLayer?.replaceChildren(...labels);
|
|
5358
|
+
paintFrames();
|
|
5359
|
+
paintMinimap();
|
|
3906
5360
|
}
|
|
3907
5361
|
function fit({ whole = false } = {}) {
|
|
3908
5362
|
if (!graph.nodes.length || destroyed) return;
|
|
3909
|
-
const
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
5363
|
+
const notes = noting ? noteList().map((n) => ({
|
|
5364
|
+
x: n.position.x,
|
|
5365
|
+
y: n.position.y,
|
|
5366
|
+
right: n.position.x + (n.size?.w || NOTE_WIDTH),
|
|
5367
|
+
bottom: n.position.y + (n.size?.h || noteEls.get(n.id)?.offsetHeight || 0)
|
|
5368
|
+
})) : [];
|
|
5369
|
+
const x = Math.min(...graph.nodes.map((n) => n.position.x), ...notes.map((n) => n.x));
|
|
5370
|
+
const y = Math.min(...graph.nodes.map((n) => n.position.y), ...notes.map((n) => n.y));
|
|
5371
|
+
const height = Math.max(...graph.nodes.map(bottomOf), ...notes.map((n) => n.bottom)) - y;
|
|
5372
|
+
const right = Math.max(...graph.nodes.map((n) => n.position.x + widthOf(n)), ...notes.map((n) => n.right));
|
|
5373
|
+
canvas.fit({ x, y, width: right - x, height });
|
|
3915
5374
|
const scale = canvas.scale > NATURAL ? NATURAL : !whole && canvas.scale < READABLE ? READABLE : null;
|
|
3916
5375
|
if (scale !== null) {
|
|
3917
5376
|
const box = canvas.root.getBoundingClientRect();
|
|
3918
5377
|
canvas.setViewport({ scale, tx: 40 - x * scale, ty: box.height / 2 - (y + height / 2) * scale });
|
|
3919
5378
|
}
|
|
3920
5379
|
}
|
|
3921
|
-
function
|
|
5380
|
+
function placeSteps(part, ids) {
|
|
5381
|
+
if (layout === "flow") {
|
|
5382
|
+
return layoutGraph(part, catalogue, { ...BOX, loopEntry: loopPorts.entry, heightOf: (n) => measured(n) || null });
|
|
5383
|
+
}
|
|
5384
|
+
const sizeOf2 = (n) => ({ width: widthOf(n), height: heightOf(n) });
|
|
5385
|
+
if (layout === "layered") {
|
|
5386
|
+
return layeredLayout(part.nodes, part.connections.map((c) => ({ from: c.source, to: c.target })), {
|
|
5387
|
+
rank: (n) => catalogue.rank(n.type),
|
|
5388
|
+
sizeOf: sizeOf2,
|
|
5389
|
+
columnGap: 96,
|
|
5390
|
+
rowGap: 40,
|
|
5391
|
+
componentGap: 80
|
|
5392
|
+
});
|
|
5393
|
+
}
|
|
5394
|
+
const at = layout(normalise(part), { catalogue, node: BOX, ids, sizeOf: sizeOf2, layeredLayout });
|
|
5395
|
+
if (!(at instanceof Map)) throw new Error("The canvas editor's layout function must return a Map of id \u2192 {x, y}.");
|
|
5396
|
+
for (const [id, p] of at) {
|
|
5397
|
+
if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) {
|
|
5398
|
+
throw new Error(`The canvas editor's layout function placed "${id}" at ${JSON.stringify(p)}; a place must be finite numbers {x, y}.`);
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
return at;
|
|
5402
|
+
}
|
|
5403
|
+
const arrangeTarget = () => selected?.kind === "many" && selected.ids.length >= 2 ? { ids: [...selected.ids] } : {};
|
|
5404
|
+
function arrangeGraph({ ids = null } = {}) {
|
|
3922
5405
|
if (refusals().arrange) return false;
|
|
3923
|
-
const
|
|
3924
|
-
const
|
|
3925
|
-
|
|
3926
|
-
|
|
5406
|
+
const only = Array.isArray(ids) ? new Set(ids.map(String)) : null;
|
|
5407
|
+
const nodes = only ? graph.nodes.filter((n) => only.has(n.id)) : graph.nodes;
|
|
5408
|
+
if (nodes.length < 2) return false;
|
|
5409
|
+
const part = only ? { nodes, connections: graph.connections.filter((c) => only.has(c.source) && only.has(c.target)) } : graph;
|
|
5410
|
+
const at = placeSteps(part, only ? nodes.map((n) => n.id) : null);
|
|
5411
|
+
const ox = Math.min(...nodes.map((x) => x.position.x));
|
|
5412
|
+
const oy = Math.min(...nodes.map((x) => x.position.y));
|
|
5413
|
+
for (const nodeRow of nodes) {
|
|
3927
5414
|
const p = at.get(nodeRow.id);
|
|
3928
|
-
if (p)
|
|
5415
|
+
if (p) {
|
|
5416
|
+
nodeRow.position = { x: canvas.snap(ox + p.x), y: canvas.snap(oy + p.y) };
|
|
5417
|
+
}
|
|
3929
5418
|
}
|
|
3930
5419
|
paintNodes();
|
|
3931
5420
|
commit("flow:arrange");
|
|
3932
|
-
fit();
|
|
5421
|
+
if (!only) fit();
|
|
5422
|
+
return true;
|
|
5423
|
+
}
|
|
5424
|
+
function freshId(list, stem) {
|
|
5425
|
+
const taken = new Set(list.map((x) => x.id));
|
|
5426
|
+
if (!taken.has(stem)) return stem;
|
|
5427
|
+
for (let i = 2; ; i += 1) if (!taken.has(`${stem}-${i}`)) return `${stem}-${i}`;
|
|
5428
|
+
}
|
|
5429
|
+
const groupsOfPick = () => {
|
|
5430
|
+
const ids = pickedSet();
|
|
5431
|
+
return groupList().filter((gr) => gr.nodes.some((id) => ids.has(id)));
|
|
5432
|
+
};
|
|
5433
|
+
function ungroupNodes(ids) {
|
|
5434
|
+
const out = new Set(ids);
|
|
5435
|
+
for (const gr of groupList()) gr.nodes = gr.nodes.filter((id) => !out.has(id));
|
|
5436
|
+
setGroups(groupList().filter((gr) => gr.nodes.length));
|
|
5437
|
+
}
|
|
5438
|
+
function setGroups(list) {
|
|
5439
|
+
if (list.length) graph.groups = list;
|
|
5440
|
+
else delete graph.groups;
|
|
5441
|
+
}
|
|
5442
|
+
function groupNodes(ids, { label: label2 = "", tone = null } = {}) {
|
|
5443
|
+
if (!grouping || isReadOnly()) return null;
|
|
5444
|
+
const want = new Set(ids);
|
|
5445
|
+
const members = graph.nodes.filter((x) => want.has(x.id)).map((x) => x.id);
|
|
5446
|
+
if (!members.length) return null;
|
|
5447
|
+
ungroupNodes(members);
|
|
5448
|
+
const id = freshId(groupList(), "group");
|
|
5449
|
+
setGroups([...groupList(), {
|
|
5450
|
+
id,
|
|
5451
|
+
...label2 ? { label: String(label2) } : {},
|
|
5452
|
+
...tone ? { tone: String(tone) } : {},
|
|
5453
|
+
nodes: members
|
|
5454
|
+
}]);
|
|
5455
|
+
paintFrames();
|
|
5456
|
+
commit("flow:group:add");
|
|
5457
|
+
paintToolbar();
|
|
5458
|
+
return id;
|
|
5459
|
+
}
|
|
5460
|
+
function groupPicked() {
|
|
5461
|
+
if (refusals().group) return false;
|
|
5462
|
+
return groupNodes(pickedIds()) !== null;
|
|
5463
|
+
}
|
|
5464
|
+
function ungroupGroups(groupIds) {
|
|
5465
|
+
if (!grouping || isReadOnly()) return false;
|
|
5466
|
+
const gone = new Set(groupIds.filter((id) => groupOf(id)));
|
|
5467
|
+
if (!gone.size) return false;
|
|
5468
|
+
setGroups(groupList().filter((gr) => !gone.has(gr.id)));
|
|
5469
|
+
paintFrames();
|
|
5470
|
+
commit("flow:group:remove");
|
|
5471
|
+
paintToolbar();
|
|
3933
5472
|
return true;
|
|
3934
5473
|
}
|
|
5474
|
+
function ungroupPicked() {
|
|
5475
|
+
if (refusals().ungroup) return false;
|
|
5476
|
+
return ungroupGroups(groupsOfPick().map((gr) => gr.id));
|
|
5477
|
+
}
|
|
5478
|
+
function setGroupLook(id, { label: label2, tone } = {}) {
|
|
5479
|
+
const gr = grouping && !isReadOnly() ? groupOf(id) : null;
|
|
5480
|
+
if (!gr) return false;
|
|
5481
|
+
const name = label2 === void 0 ? gr.label || "" : String(label2 ?? "");
|
|
5482
|
+
const look = tone === void 0 ? gr.tone || "" : String(tone ?? "");
|
|
5483
|
+
if (name === (gr.label || "") && look === (gr.tone || "")) return false;
|
|
5484
|
+
setGroups(groupList().map((x) => x.id !== id ? x : {
|
|
5485
|
+
id,
|
|
5486
|
+
...name ? { label: name } : {},
|
|
5487
|
+
...look ? { tone: look } : {},
|
|
5488
|
+
nodes: x.nodes
|
|
5489
|
+
}));
|
|
5490
|
+
paintFrames();
|
|
5491
|
+
commit("flow:group:label", `group:${id}`);
|
|
5492
|
+
return true;
|
|
5493
|
+
}
|
|
5494
|
+
function setGroupMembers(id, ids) {
|
|
5495
|
+
const gr = grouping && !isReadOnly() ? groupOf(id) : null;
|
|
5496
|
+
if (!gr) return false;
|
|
5497
|
+
const want = new Set(ids);
|
|
5498
|
+
const members = graph.nodes.filter((x) => want.has(x.id)).map((x) => x.id);
|
|
5499
|
+
if (members.length === gr.nodes.length && members.every((m, i) => m === gr.nodes[i])) return false;
|
|
5500
|
+
const taken = new Set(members);
|
|
5501
|
+
setGroups(groupList().map((x) => x.id === id ? { ...x, nodes: members } : { ...x, nodes: x.nodes.filter((n) => !taken.has(n)) }).filter((x) => x.nodes.length));
|
|
5502
|
+
paintFrames();
|
|
5503
|
+
commit("flow:group:members");
|
|
5504
|
+
paintToolbar();
|
|
5505
|
+
return true;
|
|
5506
|
+
}
|
|
5507
|
+
function frameBox(gr) {
|
|
5508
|
+
const rows = gr.nodes.map(nodeOf).filter(Boolean);
|
|
5509
|
+
if (!rows.length) return null;
|
|
5510
|
+
const x = Math.min(...rows.map((n) => n.position.x)) - FRAME.pad;
|
|
5511
|
+
const y = Math.min(...rows.map((n) => n.position.y)) - FRAME.pad - FRAME.head;
|
|
5512
|
+
const right = Math.max(...rows.map((n) => n.position.x + widthOf(n))) + FRAME.pad;
|
|
5513
|
+
const bottom = Math.max(...rows.map(bottomOf)) + FRAME.pad;
|
|
5514
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
5515
|
+
}
|
|
5516
|
+
function buildFrame(id) {
|
|
5517
|
+
const f = el("div", "twm-flow-graph__group");
|
|
5518
|
+
f.dataset.groupId = id;
|
|
5519
|
+
const head = el("div", "twm-flow-graph__group-head");
|
|
5520
|
+
head.appendChild(el("span", "twm-flow-graph__group-label"));
|
|
5521
|
+
head.addEventListener("pointerdown", (ev) => {
|
|
5522
|
+
if (ev.button !== 0 || editing?.kind === "group" && editing.id === id) return;
|
|
5523
|
+
ev.stopPropagation();
|
|
5524
|
+
const gr = groupOf(id);
|
|
5525
|
+
if (!gr) return;
|
|
5526
|
+
if (multi) pickNodes(gr.nodes);
|
|
5527
|
+
if (!isReadOnly()) dragItems(ev, gr.nodes, []);
|
|
5528
|
+
});
|
|
5529
|
+
head.addEventListener("dblclick", () => editGroupLabel(id));
|
|
5530
|
+
f.appendChild(head);
|
|
5531
|
+
return f;
|
|
5532
|
+
}
|
|
5533
|
+
function paintFrames() {
|
|
5534
|
+
if (!grouping || destroyed) return;
|
|
5535
|
+
const live = new Set(groupList().map((gr) => gr.id));
|
|
5536
|
+
if (editing?.kind === "group" && !live.has(editing.id)) editing.done(false);
|
|
5537
|
+
for (const [id, f] of frameEls) if (!live.has(id)) {
|
|
5538
|
+
f.remove();
|
|
5539
|
+
frameEls.delete(id);
|
|
5540
|
+
}
|
|
5541
|
+
for (const gr of groupList()) {
|
|
5542
|
+
let f = frameEls.get(gr.id);
|
|
5543
|
+
if (!f) {
|
|
5544
|
+
f = buildFrame(gr.id);
|
|
5545
|
+
frameEls.set(gr.id, f);
|
|
5546
|
+
}
|
|
5547
|
+
const box = frameBox(gr);
|
|
5548
|
+
Object.assign(f.style, {
|
|
5549
|
+
left: `${box.x}px`,
|
|
5550
|
+
top: `${box.y}px`,
|
|
5551
|
+
width: `${box.width}px`,
|
|
5552
|
+
height: `${box.height}px`
|
|
5553
|
+
});
|
|
5554
|
+
if (TONES.includes(gr.tone)) f.dataset.tone = gr.tone;
|
|
5555
|
+
else delete f.dataset.tone;
|
|
5556
|
+
const name = gr.label || say(S, "groupUntitled");
|
|
5557
|
+
f.setAttribute("aria-label", name);
|
|
5558
|
+
if (editing?.kind !== "group" || editing.id !== gr.id) {
|
|
5559
|
+
const tag = f.querySelector(".twm-flow-graph__group-label");
|
|
5560
|
+
tag.textContent = name;
|
|
5561
|
+
tag.title = name;
|
|
5562
|
+
tag.classList.toggle("twm-flow-graph__group-label--untitled", !gr.label);
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
const order = groupList().map((gr) => frameEls.get(gr.id));
|
|
5566
|
+
const lead = [...canvas.nodeLayer.children].slice(0, order.length);
|
|
5567
|
+
if (order.some((f, i) => lead[i] !== f)) canvas.nodeLayer.prepend(...order);
|
|
5568
|
+
}
|
|
5569
|
+
function typeInPlace(kind, id, shown, input, keep) {
|
|
5570
|
+
editing?.done(true);
|
|
5571
|
+
input.value = kind === "note" ? noteOf(id)?.text ?? "" : groupOf(id)?.label ?? "";
|
|
5572
|
+
shown.hidden = true;
|
|
5573
|
+
shown.after(input);
|
|
5574
|
+
let over = false;
|
|
5575
|
+
const done = (save) => {
|
|
5576
|
+
if (over) return;
|
|
5577
|
+
over = true;
|
|
5578
|
+
editing = null;
|
|
5579
|
+
const text = input.value;
|
|
5580
|
+
input.remove();
|
|
5581
|
+
shown.hidden = false;
|
|
5582
|
+
if (save) keep(text);
|
|
5583
|
+
if (kind === "note") paintNotes();
|
|
5584
|
+
else paintFrames();
|
|
5585
|
+
keepFocus();
|
|
5586
|
+
};
|
|
5587
|
+
input.addEventListener("keydown", (k) => {
|
|
5588
|
+
if (k.key === "Escape") {
|
|
5589
|
+
k.preventDefault();
|
|
5590
|
+
k.stopPropagation();
|
|
5591
|
+
done(false);
|
|
5592
|
+
return;
|
|
5593
|
+
}
|
|
5594
|
+
if (k.key === "Enter" && (kind !== "note" || k.ctrlKey || k.metaKey)) {
|
|
5595
|
+
k.preventDefault();
|
|
5596
|
+
k.stopPropagation();
|
|
5597
|
+
done(true);
|
|
5598
|
+
}
|
|
5599
|
+
});
|
|
5600
|
+
input.addEventListener("blur", () => done(true));
|
|
5601
|
+
input.addEventListener("pointerdown", (p) => p.stopPropagation());
|
|
5602
|
+
editing = { kind, id, done };
|
|
5603
|
+
input.focus({ preventScroll: true });
|
|
5604
|
+
input.select?.();
|
|
5605
|
+
}
|
|
5606
|
+
function editGroupLabel(id) {
|
|
5607
|
+
const f = frameEls.get(id);
|
|
5608
|
+
if (!f || !groupOf(id) || isReadOnly()) return false;
|
|
5609
|
+
if (editing?.kind === "group" && editing.id === id) return true;
|
|
5610
|
+
const input = el("input", "twm-flow-graph__group-input");
|
|
5611
|
+
input.type = "text";
|
|
5612
|
+
input.setAttribute("aria-label", say(S, "groupName"));
|
|
5613
|
+
typeInPlace(
|
|
5614
|
+
"group",
|
|
5615
|
+
id,
|
|
5616
|
+
f.querySelector(".twm-flow-graph__group-label"),
|
|
5617
|
+
input,
|
|
5618
|
+
(text) => setGroupLook(id, { label: text.trim() })
|
|
5619
|
+
);
|
|
5620
|
+
return true;
|
|
5621
|
+
}
|
|
5622
|
+
function addNote({ text = "", position = null, size = null } = {}) {
|
|
5623
|
+
if (!noting || isReadOnly()) return null;
|
|
5624
|
+
let where = position;
|
|
5625
|
+
if (!where) {
|
|
5626
|
+
const box = canvas.visibleBox();
|
|
5627
|
+
where = {
|
|
5628
|
+
x: canvas.snap(box.width ? box.x + box.width / 2 - NOTE_WIDTH / 2 : 80),
|
|
5629
|
+
y: canvas.snap(box.height ? box.y + box.height / 3 : 80)
|
|
5630
|
+
};
|
|
5631
|
+
}
|
|
5632
|
+
const id = freshId(noteList(), "note");
|
|
5633
|
+
const next = normalise({ nodes: [], connections: [], notes: [{ id, text, position: where, size }] }).notes[0];
|
|
5634
|
+
graph.notes = [...noteList(), next];
|
|
5635
|
+
selected = { kind: "note", id };
|
|
5636
|
+
paintNodes();
|
|
5637
|
+
paintPanel();
|
|
5638
|
+
announceSelection();
|
|
5639
|
+
commit("flow:note:add");
|
|
5640
|
+
return structuredClone(next);
|
|
5641
|
+
}
|
|
5642
|
+
function setNoteText(id, text) {
|
|
5643
|
+
const note = noting && !isReadOnly() ? noteOf(id) : null;
|
|
5644
|
+
if (!note || note.text === String(text ?? "")) return false;
|
|
5645
|
+
note.text = String(text ?? "");
|
|
5646
|
+
paintNotes();
|
|
5647
|
+
if (selected?.kind === "note" && selected.id === id) paintPanel();
|
|
5648
|
+
commit("flow:note:edit", `note:${id}`);
|
|
5649
|
+
return true;
|
|
5650
|
+
}
|
|
5651
|
+
function moveNote(id, position) {
|
|
5652
|
+
const note = noting && !isReadOnly() ? noteOf(id) : null;
|
|
5653
|
+
const x = Math.round(Number(position?.x));
|
|
5654
|
+
const y = Math.round(Number(position?.y));
|
|
5655
|
+
if (!note || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
|
5656
|
+
if (note.position.x === x && note.position.y === y) return false;
|
|
5657
|
+
note.position = { x, y };
|
|
5658
|
+
paintNotes();
|
|
5659
|
+
commit("flow:note:move");
|
|
5660
|
+
return true;
|
|
5661
|
+
}
|
|
5662
|
+
function dropNotes(ids, commitAs = "flow:note:remove") {
|
|
5663
|
+
const gone = new Set(ids);
|
|
5664
|
+
const kept = noteList().filter((x) => !gone.has(x.id));
|
|
5665
|
+
if (kept.length === noteList().length) return false;
|
|
5666
|
+
if (kept.length) graph.notes = kept;
|
|
5667
|
+
else delete graph.notes;
|
|
5668
|
+
if (commitAs) {
|
|
5669
|
+
prunePick();
|
|
5670
|
+
paintNodes();
|
|
5671
|
+
paintPanel();
|
|
5672
|
+
announceSelection();
|
|
5673
|
+
commit(commitAs);
|
|
5674
|
+
}
|
|
5675
|
+
return true;
|
|
5676
|
+
}
|
|
5677
|
+
function editNote(id) {
|
|
5678
|
+
const box = noteEls.get(id);
|
|
5679
|
+
if (!box || !noteOf(id) || isReadOnly()) return false;
|
|
5680
|
+
if (editing?.kind === "note" && editing.id === id) return true;
|
|
5681
|
+
const input = el("textarea", "twm-flow-graph__note-input");
|
|
5682
|
+
input.setAttribute("aria-label", say(S, "noteText"));
|
|
5683
|
+
typeInPlace(
|
|
5684
|
+
"note",
|
|
5685
|
+
id,
|
|
5686
|
+
box.querySelector(".twm-flow-graph__note-text"),
|
|
5687
|
+
input,
|
|
5688
|
+
(text) => setNoteText(id, text)
|
|
5689
|
+
);
|
|
5690
|
+
return true;
|
|
5691
|
+
}
|
|
5692
|
+
function buildNote(id) {
|
|
5693
|
+
const box = el("div", "twm-flow-graph__note");
|
|
5694
|
+
box.dataset.noteId = id;
|
|
5695
|
+
box.setAttribute("data-twm-flow-item", "");
|
|
5696
|
+
box.setAttribute("role", "note");
|
|
5697
|
+
box.tabIndex = 0;
|
|
5698
|
+
box.appendChild(el("p", "twm-flow-graph__note-text"));
|
|
5699
|
+
box.addEventListener("pointerdown", (ev) => {
|
|
5700
|
+
if (ev.button !== 0 || editing?.kind === "note" && editing.id === id) return;
|
|
5701
|
+
ev.stopPropagation();
|
|
5702
|
+
if (toggles(ev)) return;
|
|
5703
|
+
if (!isNotePicked(id)) select({ kind: "note", id });
|
|
5704
|
+
if (isReadOnly()) return;
|
|
5705
|
+
dragItems(ev, selected?.kind === "many" ? selected.ids : [], pickedNotes());
|
|
5706
|
+
});
|
|
5707
|
+
box.addEventListener("click", (ev) => {
|
|
5708
|
+
if (!toggles(ev)) return;
|
|
5709
|
+
const notes = pickedNotes();
|
|
5710
|
+
pickItems(pickedIds(), notes.includes(id) ? notes.filter((x) => x !== id) : [...notes, id]);
|
|
5711
|
+
});
|
|
5712
|
+
box.addEventListener("dblclick", () => editNote(id));
|
|
5713
|
+
return box;
|
|
5714
|
+
}
|
|
5715
|
+
function paintNotes() {
|
|
5716
|
+
if (!noting || destroyed) return;
|
|
5717
|
+
const live = new Set(noteList().map((x) => x.id));
|
|
5718
|
+
if (editing?.kind === "note" && !live.has(editing.id)) editing.done(false);
|
|
5719
|
+
for (const [id, box] of noteEls) if (!live.has(id)) {
|
|
5720
|
+
box.remove();
|
|
5721
|
+
noteEls.delete(id);
|
|
5722
|
+
}
|
|
5723
|
+
const picks = new Set(pickedNotes());
|
|
5724
|
+
for (const note of noteList()) {
|
|
5725
|
+
let box = noteEls.get(note.id);
|
|
5726
|
+
if (!box) {
|
|
5727
|
+
box = buildNote(note.id);
|
|
5728
|
+
noteEls.set(note.id, box);
|
|
5729
|
+
canvas.nodeLayer.appendChild(box);
|
|
5730
|
+
}
|
|
5731
|
+
box.style.left = `${note.position.x}px`;
|
|
5732
|
+
box.style.top = `${note.position.y}px`;
|
|
5733
|
+
box.style.width = `${note.size?.w || NOTE_WIDTH}px`;
|
|
5734
|
+
if (note.size?.h) box.style.height = `${note.size.h}px`;
|
|
5735
|
+
else box.style.removeProperty("height");
|
|
5736
|
+
const picked = picks.has(note.id);
|
|
5737
|
+
box.classList.toggle("twm-flow-graph__note--picked", picked);
|
|
5738
|
+
if (picked) box.setAttribute("aria-current", "true");
|
|
5739
|
+
else box.removeAttribute("aria-current");
|
|
5740
|
+
const shown = box.querySelector(".twm-flow-graph__note-text");
|
|
5741
|
+
shown.textContent = note.text || say(S, "noteEmpty");
|
|
5742
|
+
shown.classList.toggle("twm-flow-graph__note-text--empty", !note.text);
|
|
5743
|
+
box.setAttribute("aria-label", note.text || say(S, "noteEmpty"));
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
function notesIn(box) {
|
|
5747
|
+
return noteList().filter((x) => {
|
|
5748
|
+
const el2 = noteEls.get(x.id);
|
|
5749
|
+
const w = x.size?.w || NOTE_WIDTH;
|
|
5750
|
+
const h = x.size?.h || el2?.offsetHeight || FRAME.head;
|
|
5751
|
+
return x.position.x < box.x + box.width && x.position.x + w > box.x && x.position.y < box.y + box.height && x.position.y + h > box.y;
|
|
5752
|
+
}).map((x) => x.id);
|
|
5753
|
+
}
|
|
3935
5754
|
const panel = createSettingsPanel({
|
|
3936
5755
|
widgets,
|
|
3937
5756
|
references,
|
|
@@ -3955,19 +5774,7 @@ function createCanvasEditor(host, options = {}) {
|
|
|
3955
5774
|
},
|
|
3956
5775
|
onRename: (stepId, text) => {
|
|
3957
5776
|
if (isReadOnly()) return;
|
|
3958
|
-
|
|
3959
|
-
if (!nodeRow) return;
|
|
3960
|
-
if (text) nodeRow.label = text;
|
|
3961
|
-
else delete nodeRow.label;
|
|
3962
|
-
const n = nodeEls.get(nodeRow.id);
|
|
3963
|
-
const name = text || typeOf(nodeRow)?.label || nodeRow.type;
|
|
3964
|
-
const title = n?.querySelector(".twm-flow-graph__node-title");
|
|
3965
|
-
if (title) title.textContent = title.title = name;
|
|
3966
|
-
if (n) {
|
|
3967
|
-
n.dataset.shape = `${nodeRow.type}|${nodeRow.label || ""}`;
|
|
3968
|
-
n.setAttribute("aria-label", name);
|
|
3969
|
-
}
|
|
3970
|
-
commit("flow:node:label", `label:${stepId}`);
|
|
5777
|
+
renameNode(stepId, text);
|
|
3971
5778
|
},
|
|
3972
5779
|
onFocusLost: () => keepFocus(),
|
|
3973
5780
|
values: typeof values === "function" ? (q) => values({
|
|
@@ -4002,9 +5809,10 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4002
5809
|
canvas.bringIntoView({
|
|
4003
5810
|
x: n.position.x,
|
|
4004
5811
|
y: n.position.y,
|
|
4005
|
-
width:
|
|
5812
|
+
width: widthOf(n),
|
|
4006
5813
|
height: bottomOf(n) - n.position.y
|
|
4007
5814
|
});
|
|
5815
|
+
viewportSettled();
|
|
4008
5816
|
}
|
|
4009
5817
|
if (!(f.field && panel.focusField(f.field))) focusFirstField();
|
|
4010
5818
|
}
|
|
@@ -4019,33 +5827,63 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4019
5827
|
}
|
|
4020
5828
|
function select(next) {
|
|
4021
5829
|
selected = next;
|
|
5830
|
+
const picks = pickedSet();
|
|
4022
5831
|
for (const [id, n] of nodeEls) {
|
|
4023
|
-
const picked =
|
|
5832
|
+
const picked = picks.has(id);
|
|
4024
5833
|
n.classList.toggle("twm-flow-graph__node--picked", picked);
|
|
4025
5834
|
if (picked) n.setAttribute("aria-current", "true");
|
|
4026
5835
|
else n.removeAttribute("aria-current");
|
|
4027
5836
|
}
|
|
5837
|
+
paintNotes();
|
|
4028
5838
|
paintEdges();
|
|
4029
5839
|
paintPanel();
|
|
4030
5840
|
announceSelection();
|
|
5841
|
+
if (multi || grouping) paintToolbar();
|
|
5842
|
+
}
|
|
5843
|
+
function pickItems(ids, noteIds = []) {
|
|
5844
|
+
const next = pickOf(ids, noteIds);
|
|
5845
|
+
if (JSON.stringify(next) !== JSON.stringify(selected)) select(next);
|
|
5846
|
+
}
|
|
5847
|
+
const pickNodes = (ids) => pickItems(ids);
|
|
5848
|
+
function togglePick(id) {
|
|
5849
|
+
const ids = pickedIds();
|
|
5850
|
+
pickItems(ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id], pickedNotes());
|
|
5851
|
+
}
|
|
5852
|
+
function nodesIn(box) {
|
|
5853
|
+
return graph.nodes.filter((x) => x.position.x < box.x + box.width && x.position.x + widthOf(x) > box.x && x.position.y < box.y + box.height && bottomOf(x) > box.y).map((x) => x.id);
|
|
4031
5854
|
}
|
|
4032
5855
|
function announceSelection() {
|
|
4033
|
-
const now = selected?.kind === "node" ? `step:${selected.id}` : selected?.kind === "edge" ? `connection:${JSON.stringify(selected.edge)}` : selected?.kind === "flow" ? "flow" : "";
|
|
5856
|
+
const now = selected?.kind === "node" ? `step:${selected.id}` : selected?.kind === "many" ? `many:${JSON.stringify([selected.ids, selected.notes || []])}` : selected?.kind === "note" ? `note:${selected.id}` : selected?.kind === "edge" ? `connection:${JSON.stringify(selected.edge)}` : selected?.kind === "flow" ? "flow" : "";
|
|
4034
5857
|
if (now === lastSelect) return;
|
|
4035
5858
|
lastSelect = now;
|
|
4036
5859
|
if (!onSelect || destroyed) return;
|
|
4037
5860
|
if (selected?.kind === "node") onSelect({ kind: "step", id: selected.id });
|
|
5861
|
+
else if (selected?.kind === "many") {
|
|
5862
|
+
onSelect({ kind: "many", ids: [...selected.ids], ...selected.notes ? { notes: [...selected.notes] } : {} });
|
|
5863
|
+
} else if (selected?.kind === "note") onSelect({ kind: "note", id: selected.id });
|
|
4038
5864
|
else if (selected?.kind === "edge") onSelect({ kind: "connection", id: null, connection: { ...selected.edge } });
|
|
4039
5865
|
else if (selected?.kind === "flow") onSelect({ kind: "flow", id: null });
|
|
4040
5866
|
else onSelect({ kind: null, id: null });
|
|
4041
5867
|
}
|
|
4042
5868
|
function removeSelected() {
|
|
4043
5869
|
if (!selected || isReadOnly()) return false;
|
|
4044
|
-
if (selected.kind === "
|
|
5870
|
+
if (selected.kind === "many") {
|
|
5871
|
+
const doomed = removableOf(selected.ids);
|
|
5872
|
+
const notes = selected.notes || [];
|
|
5873
|
+
if (!doomed.length && !notes.length) return false;
|
|
5874
|
+
for (const id of doomed) removeNode(graph, id);
|
|
5875
|
+
dropNotes(notes, null);
|
|
5876
|
+
findingsBy = groupFindings(allFindings(), { graph });
|
|
5877
|
+
select(null);
|
|
5878
|
+
paintNodes();
|
|
5879
|
+
commit(doomed.length ? "flow:nodes:remove" : "flow:note:remove");
|
|
5880
|
+
} else if (selected.kind === "note") {
|
|
5881
|
+
if (!dropNotes([selected.id])) return false;
|
|
5882
|
+
} else if (selected.kind === "node") {
|
|
4045
5883
|
const nodeRow = nodeOf(selected.id);
|
|
4046
5884
|
if (!nodeRow || !canRemove(nodeRow, typeOf(nodeRow))) return false;
|
|
4047
5885
|
removeNode(graph, selected.id);
|
|
4048
|
-
findingsBy = groupFindings(
|
|
5886
|
+
findingsBy = groupFindings(allFindings(), { graph });
|
|
4049
5887
|
select(null);
|
|
4050
5888
|
paintNodes();
|
|
4051
5889
|
commit("flow:node:remove");
|
|
@@ -4058,30 +5896,76 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4058
5896
|
keepFocus();
|
|
4059
5897
|
return true;
|
|
4060
5898
|
}
|
|
4061
|
-
|
|
4062
|
-
const
|
|
5899
|
+
const removableOf = (ids) => ids.filter((id) => {
|
|
5900
|
+
const nodeRow = nodeOf(id);
|
|
5901
|
+
return nodeRow && canRemove(nodeRow, typeOf(nodeRow));
|
|
5902
|
+
});
|
|
5903
|
+
function paintPickBox({ title, line, removeLabel, removeIcon, action, refused: refused2 = false }) {
|
|
4063
5904
|
const remove = button({
|
|
4064
|
-
label:
|
|
4065
|
-
icon:
|
|
5905
|
+
label: removeLabel,
|
|
5906
|
+
icon: removeIcon,
|
|
4066
5907
|
danger: true,
|
|
4067
5908
|
className: "twm-flow-graph__connection-remove",
|
|
4068
5909
|
onClick: () => removeSelected()
|
|
4069
5910
|
});
|
|
4070
|
-
remove.dataset.action =
|
|
4071
|
-
remove.disabled = isReadOnly();
|
|
5911
|
+
remove.dataset.action = action;
|
|
5912
|
+
remove.disabled = isReadOnly() || refused2;
|
|
4072
5913
|
connectionBox.replaceChildren(
|
|
4073
|
-
el("h3", "twm-flow-graph__connection-title",
|
|
4074
|
-
el("p", "twm-flow-graph__connection-line",
|
|
5914
|
+
el("h3", "twm-flow-graph__connection-title", title),
|
|
5915
|
+
el("p", "twm-flow-graph__connection-line", line),
|
|
4075
5916
|
remove
|
|
4076
5917
|
);
|
|
4077
5918
|
connectionBox.hidden = false;
|
|
4078
5919
|
}
|
|
5920
|
+
function paintConnection() {
|
|
5921
|
+
paintPickBox({
|
|
5922
|
+
title: say(S, "connection"),
|
|
5923
|
+
line: say(S, "connectionLine", selected.edge),
|
|
5924
|
+
removeLabel: say(S, "removeConnection"),
|
|
5925
|
+
removeIcon: "link_off",
|
|
5926
|
+
action: "remove-connection"
|
|
5927
|
+
});
|
|
5928
|
+
}
|
|
5929
|
+
function paintMany() {
|
|
5930
|
+
const n = removableOf(selected.ids).length;
|
|
5931
|
+
const notes = selected.notes?.length || 0;
|
|
5932
|
+
if (!notes) {
|
|
5933
|
+
paintPickBox({
|
|
5934
|
+
title: say(S, "stepsPicked", selected.ids.length),
|
|
5935
|
+
line: say(S, "stepsPickedLine"),
|
|
5936
|
+
removeLabel: say(S, "removeSteps", n),
|
|
5937
|
+
removeIcon: "delete",
|
|
5938
|
+
action: "remove-steps",
|
|
5939
|
+
refused: n === 0
|
|
5940
|
+
});
|
|
5941
|
+
return;
|
|
5942
|
+
}
|
|
5943
|
+
paintPickBox({
|
|
5944
|
+
title: say(S, "itemsPicked", selected.ids.length, notes),
|
|
5945
|
+
line: say(S, "stepsPickedLine"),
|
|
5946
|
+
removeLabel: say(S, "removeItems", n + notes),
|
|
5947
|
+
removeIcon: "delete",
|
|
5948
|
+
action: "remove-steps"
|
|
5949
|
+
});
|
|
5950
|
+
}
|
|
5951
|
+
function paintNote() {
|
|
5952
|
+
const text = noteOf(selected.id)?.text || "";
|
|
5953
|
+
paintPickBox({
|
|
5954
|
+
title: say(S, "noteKind"),
|
|
5955
|
+
line: text || say(S, "noteEmpty"),
|
|
5956
|
+
removeLabel: say(S, "removeNote"),
|
|
5957
|
+
removeIcon: "delete",
|
|
5958
|
+
action: "remove-note"
|
|
5959
|
+
});
|
|
5960
|
+
}
|
|
4079
5961
|
function paintPanel() {
|
|
4080
5962
|
if (destroyed) return;
|
|
4081
|
-
if (selected?.kind === "edge") {
|
|
5963
|
+
if (selected?.kind === "edge" || selected?.kind === "many" || selected?.kind === "note") {
|
|
4082
5964
|
panel.clear();
|
|
4083
5965
|
panel.el.hidden = true;
|
|
4084
|
-
paintConnection();
|
|
5966
|
+
if (selected.kind === "edge") paintConnection();
|
|
5967
|
+
else if (selected.kind === "note") paintNote();
|
|
5968
|
+
else paintMany();
|
|
4085
5969
|
return;
|
|
4086
5970
|
}
|
|
4087
5971
|
connectionBox.hidden = true;
|
|
@@ -4128,8 +6012,304 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4128
6012
|
slots: { after }
|
|
4129
6013
|
});
|
|
4130
6014
|
}
|
|
4131
|
-
panel.setFindings(
|
|
6015
|
+
panel.setFindings(allFindings());
|
|
6016
|
+
}
|
|
6017
|
+
const heightOf = (n) => bottomOf(n) - n.position.y;
|
|
6018
|
+
const pickedRows = () => pickedIds().map(nodeOf).filter(Boolean);
|
|
6019
|
+
function alignNodes(kind) {
|
|
6020
|
+
if (!ALIGN.includes(kind)) throw new Error(`align("${kind}"): the kinds are ${ALIGN.join(", ")}.`);
|
|
6021
|
+
if (refusals()[`align-${kind}`]) return false;
|
|
6022
|
+
const rows = pickedRows();
|
|
6023
|
+
const left = Math.min(...rows.map((n) => n.position.x));
|
|
6024
|
+
const right = Math.max(...rows.map((n) => n.position.x + widthOf(n)));
|
|
6025
|
+
const top = Math.min(...rows.map((n) => n.position.y));
|
|
6026
|
+
const bottom = Math.max(...rows.map(bottomOf));
|
|
6027
|
+
for (const n of rows) {
|
|
6028
|
+
const { x, y } = n.position;
|
|
6029
|
+
const w = widthOf(n);
|
|
6030
|
+
const h = heightOf(n);
|
|
6031
|
+
n.position = {
|
|
6032
|
+
x: kind === "left" ? left : kind === "right" ? right - w : kind === "center" ? Math.round((left + right - w) / 2) : x,
|
|
6033
|
+
y: kind === "top" ? top : kind === "bottom" ? bottom - h : kind === "middle" ? Math.round((top + bottom - h) / 2) : y
|
|
6034
|
+
};
|
|
6035
|
+
}
|
|
6036
|
+
paintNodes();
|
|
6037
|
+
commit("flow:nodes:align");
|
|
6038
|
+
return true;
|
|
6039
|
+
}
|
|
6040
|
+
function distributeNodes(axis) {
|
|
6041
|
+
if (!DISTRIBUTE.includes(axis)) throw new Error(`distribute("${axis}"): the axes are ${DISTRIBUTE.join(", ")}.`);
|
|
6042
|
+
if (refusals()[`distribute-${axis}`]) return false;
|
|
6043
|
+
const across = axis === "horizontal";
|
|
6044
|
+
const at = (n) => across ? n.position.x : n.position.y;
|
|
6045
|
+
const size = (n) => across ? widthOf(n) : heightOf(n);
|
|
6046
|
+
const rows = pickedRows().sort((a, b) => at(a) - at(b));
|
|
6047
|
+
const first = at(rows[0]);
|
|
6048
|
+
const end = Math.max(...rows.map((n) => at(n) + size(n)));
|
|
6049
|
+
const gap = (end - first - rows.reduce((sum, n) => sum + size(n), 0)) / (rows.length - 1);
|
|
6050
|
+
let cursor = first;
|
|
6051
|
+
for (const n of rows) {
|
|
6052
|
+
const p = Math.round(cursor);
|
|
6053
|
+
n.position = across ? { x: p, y: n.position.y } : { x: n.position.x, y: p };
|
|
6054
|
+
cursor += size(n) + gap;
|
|
6055
|
+
}
|
|
6056
|
+
paintNodes();
|
|
6057
|
+
commit("flow:nodes:align");
|
|
6058
|
+
return true;
|
|
6059
|
+
}
|
|
6060
|
+
function nudge(dx, dy, ev) {
|
|
6061
|
+
if (!multi || !canvas.root.contains(ev.target)) return false;
|
|
6062
|
+
const rows = pickedRows();
|
|
6063
|
+
const notes = pickedNotes().map(noteOf).filter(Boolean);
|
|
6064
|
+
if (!(rows.length + notes.length) || isReadOnly()) return false;
|
|
6065
|
+
const step = (canvas.gridSize || 1) * (ev.shiftKey ? NUDGE_FAR : 1);
|
|
6066
|
+
for (const n of [...rows, ...notes]) {
|
|
6067
|
+
n.position = { x: n.position.x + dx * step, y: n.position.y + dy * step };
|
|
6068
|
+
const box = (rows.includes(n) ? nodeEls : noteEls).get(n.id);
|
|
6069
|
+
if (box) {
|
|
6070
|
+
box.style.left = `${n.position.x}px`;
|
|
6071
|
+
box.style.top = `${n.position.y}px`;
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
paintEdges();
|
|
6075
|
+
const action = !rows.length ? "flow:note:move" : rows.length + notes.length > 1 ? "flow:nodes:move" : "flow:node:move";
|
|
6076
|
+
commit(action, `nudge:${[...rows, ...notes].map((n) => n.id).join(",")}`);
|
|
6077
|
+
return void 0;
|
|
6078
|
+
}
|
|
6079
|
+
function onTab(ev) {
|
|
6080
|
+
if (ev.key !== "Tab" || ev.ctrlKey || ev.metaKey || ev.altKey || ev.defaultPrevented) return;
|
|
6081
|
+
const from = ev.target.closest?.("[data-node-id]");
|
|
6082
|
+
if (!from && (ev.target !== canvas.root || ev.shiftKey)) return;
|
|
6083
|
+
const ids = graph.nodes.map((x) => x.id);
|
|
6084
|
+
const i = from ? ids.indexOf(from.dataset.nodeId) : -1;
|
|
6085
|
+
const next = nodeEls.get(ids[ev.shiftKey ? i - 1 : i + 1]);
|
|
6086
|
+
if (!next) return;
|
|
6087
|
+
ev.preventDefault();
|
|
6088
|
+
next.focus({ preventScroll: true });
|
|
6089
|
+
const n = nodeOf(next.dataset.nodeId);
|
|
6090
|
+
canvas.bringIntoView({ x: n.position.x, y: n.position.y, width: widthOf(n), height: heightOf(n) });
|
|
6091
|
+
}
|
|
6092
|
+
function pressLetGo(ev) {
|
|
6093
|
+
released = ev.type === "pointerup" ? pressed : null;
|
|
6094
|
+
pressed = null;
|
|
6095
|
+
}
|
|
6096
|
+
if (multi) {
|
|
6097
|
+
canvas.onMarquee = (box) => pickItems(nodesIn(box), noting ? notesIn(box) : []);
|
|
6098
|
+
canvas.root.addEventListener("keydown", onTab);
|
|
6099
|
+
window.addEventListener("pointerup", pressLetGo, true);
|
|
6100
|
+
window.addEventListener("pointercancel", pressLetGo, true);
|
|
6101
|
+
}
|
|
6102
|
+
const CLIPBOARD_OFF = Object.freeze({ ok: false, reason: null });
|
|
6103
|
+
function refused(reason) {
|
|
6104
|
+
setMessage(reason, "warning");
|
|
6105
|
+
return { ok: false, reason };
|
|
6106
|
+
}
|
|
6107
|
+
const roRefusal = () => refused(readOnlyReason() || say(S, "readOnlyRefusal"));
|
|
6108
|
+
const titleOf2 = (n) => n.label || typeOf(n)?.label || n.type;
|
|
6109
|
+
const addable = (type) => Boolean(type) && !type.unavailable && offered(type);
|
|
6110
|
+
const copyRows = () => {
|
|
6111
|
+
const picks = pickedSet();
|
|
6112
|
+
return graph.nodes.filter((n) => picks.has(n.id) && addable(typeOf(n)));
|
|
6113
|
+
};
|
|
6114
|
+
const copyNoteRows = () => {
|
|
6115
|
+
if (!noting) return [];
|
|
6116
|
+
const picks = new Set(pickedNotes());
|
|
6117
|
+
return noteList().filter((x) => picks.has(x.id));
|
|
6118
|
+
};
|
|
6119
|
+
const pickedAny = () => pickedIds().length > 0 || copyNoteRows().length > 0;
|
|
6120
|
+
function copyText(rows, notes = []) {
|
|
6121
|
+
const ids = new Set(rows.map((n) => n.id));
|
|
6122
|
+
const groups = grouping ? groupList().filter((gr) => gr.nodes.every((id) => ids.has(id))) : [];
|
|
6123
|
+
return serialise({
|
|
6124
|
+
nodes: rows,
|
|
6125
|
+
connections: graph.connections.filter((c) => ids.has(c.source) && ids.has(c.target)),
|
|
6126
|
+
...groups.length ? { groups } : {},
|
|
6127
|
+
...notes.length ? { notes } : {}
|
|
6128
|
+
});
|
|
6129
|
+
}
|
|
6130
|
+
const tookSay = (key, rows, notes) => notes.length ? say(S, `${key}Items`, rows.length, notes.length) : say(S, key, rows.length);
|
|
6131
|
+
const tookIds = (rows, notes) => ({
|
|
6132
|
+
ok: true,
|
|
6133
|
+
ids: rows.map((n) => n.id),
|
|
6134
|
+
...notes.length ? { notes: notes.map((x) => x.id) } : {}
|
|
6135
|
+
});
|
|
6136
|
+
function writeClipboard(text, times) {
|
|
6137
|
+
pasteRun = { text, times };
|
|
6138
|
+
if (clip === "memory") {
|
|
6139
|
+
copiedText = text;
|
|
6140
|
+
return true;
|
|
6141
|
+
}
|
|
6142
|
+
const failed = () => setMessage(say(S, "clipboardFailed"), "warning");
|
|
6143
|
+
try {
|
|
6144
|
+
const done = clip.write(text);
|
|
6145
|
+
if (done && typeof done.then === "function") done.then(null, failed);
|
|
6146
|
+
} catch {
|
|
6147
|
+
failed();
|
|
6148
|
+
return false;
|
|
6149
|
+
}
|
|
6150
|
+
return true;
|
|
6151
|
+
}
|
|
6152
|
+
function copyNodes() {
|
|
6153
|
+
if (!clip) return CLIPBOARD_OFF;
|
|
6154
|
+
if (!pickedAny()) return refused(say(S, "nothingPicked"));
|
|
6155
|
+
const rows = copyRows();
|
|
6156
|
+
const notes = copyNoteRows();
|
|
6157
|
+
if (!rows.length && !notes.length) return refused(say(S, "cannotCopy"));
|
|
6158
|
+
if (!writeClipboard(copyText(rows, notes), 0)) return { ok: false, reason: say(S, "clipboardFailed") };
|
|
6159
|
+
setMessage(tookSay("copied", rows, notes), "ok");
|
|
6160
|
+
return tookIds(rows, notes);
|
|
6161
|
+
}
|
|
6162
|
+
function cutNodes() {
|
|
6163
|
+
if (!clip) return CLIPBOARD_OFF;
|
|
6164
|
+
if (isReadOnly()) return roRefusal();
|
|
6165
|
+
if (!pickedAny()) return refused(say(S, "nothingPicked"));
|
|
6166
|
+
const rows = copyRows().filter((n) => canRemove(n, typeOf(n)));
|
|
6167
|
+
const notes = copyNoteRows();
|
|
6168
|
+
if (!rows.length && !notes.length) return refused(say(S, "cannotCut"));
|
|
6169
|
+
if (!writeClipboard(copyText(rows, notes), -1)) return { ok: false, reason: say(S, "clipboardFailed") };
|
|
6170
|
+
for (const n of rows) removeNode(graph, n.id);
|
|
6171
|
+
dropNotes(notes.map((x) => x.id), null);
|
|
6172
|
+
findingsBy = groupFindings(allFindings(), { graph });
|
|
6173
|
+
select(null);
|
|
6174
|
+
paintNodes();
|
|
6175
|
+
commit(!rows.length ? "flow:note:remove" : rows.length + notes.length > 1 ? "flow:nodes:remove" : "flow:node:remove");
|
|
6176
|
+
setMessage(tookSay("cutDone", rows, notes), "ok");
|
|
6177
|
+
keepFocus();
|
|
6178
|
+
return tookIds(rows, notes);
|
|
6179
|
+
}
|
|
6180
|
+
function stepsOf(text) {
|
|
6181
|
+
if (typeof text !== "string" || !text.trim()) return { reason: say(S, "nothingToPaste") };
|
|
6182
|
+
let raw;
|
|
6183
|
+
try {
|
|
6184
|
+
raw = JSON.parse(text);
|
|
6185
|
+
} catch {
|
|
6186
|
+
return { reason: say(S, "pasteNotSteps") };
|
|
6187
|
+
}
|
|
6188
|
+
const notesToo = noting && Array.isArray(raw?.notes) && raw.notes.length > 0;
|
|
6189
|
+
if (!raw || !Array.isArray(raw.nodes) || !raw.nodes.length && !notesToo || raw.nodes.some((n) => !n || typeof n !== "object" || !n.id || !n.type)) {
|
|
6190
|
+
return { reason: say(S, "pasteNotSteps") };
|
|
6191
|
+
}
|
|
6192
|
+
const g = normalise(raw);
|
|
6193
|
+
if (!g.nodes.length && !g.notes?.length) return { reason: say(S, "pasteNotSteps") };
|
|
6194
|
+
const odd = g.nodes.find((n) => !addable(typeOf(n)));
|
|
6195
|
+
if (odd) return { reason: typeOf(odd) ? say(S, "notAddable", titleOf2(odd)) : say(S, "unknownType", odd.type) };
|
|
6196
|
+
return { g };
|
|
6197
|
+
}
|
|
6198
|
+
function addCopies(g, offset, via, action) {
|
|
6199
|
+
const step = (canvas.gridSize || CANVAS_NODE.row) * offset;
|
|
6200
|
+
const titles = new Set(graph.nodes.map(titleOf2));
|
|
6201
|
+
const made = [];
|
|
6202
|
+
const idOf = /* @__PURE__ */ new Map();
|
|
6203
|
+
for (const from of g.nodes) {
|
|
6204
|
+
const id = nextId({ nodes: [...graph.nodes, ...made] }, from.type, { catalogue });
|
|
6205
|
+
const title = titleOf2(from);
|
|
6206
|
+
let label2 = from.label || null;
|
|
6207
|
+
if (titles.has(title)) {
|
|
6208
|
+
let i = 1;
|
|
6209
|
+
label2 = say(S, "copyLabel", title, i);
|
|
6210
|
+
while (titles.has(label2)) {
|
|
6211
|
+
i += 1;
|
|
6212
|
+
label2 = say(S, "copyLabel", title, i);
|
|
6213
|
+
}
|
|
6214
|
+
titles.add(label2);
|
|
6215
|
+
}
|
|
6216
|
+
const copy = {
|
|
6217
|
+
id,
|
|
6218
|
+
type: from.type,
|
|
6219
|
+
...label2 ? { label: label2 } : {},
|
|
6220
|
+
config: structuredClone(from.config),
|
|
6221
|
+
position: { x: from.position.x + step, y: from.position.y + step }
|
|
6222
|
+
};
|
|
6223
|
+
if (typeof prepareCopy === "function") prepareCopy(copy, { from: from.id, via });
|
|
6224
|
+
const [clean2] = normalise({
|
|
6225
|
+
nodes: [{ ...copy, id, type: from.type, position: { ...copy.position } }],
|
|
6226
|
+
connections: []
|
|
6227
|
+
}).nodes;
|
|
6228
|
+
made.push(clean2);
|
|
6229
|
+
idOf.set(from.id, id);
|
|
6230
|
+
}
|
|
6231
|
+
graph.nodes.push(...made);
|
|
6232
|
+
for (const c of g.connections) {
|
|
6233
|
+
const source = idOf.get(c.source);
|
|
6234
|
+
const target = idOf.get(c.target);
|
|
6235
|
+
if (source && target) connect(graph, catalogue, { ...c, source, target }, { strings: S });
|
|
6236
|
+
}
|
|
6237
|
+
const noteIds = [];
|
|
6238
|
+
for (const from of noting ? g.notes || [] : []) {
|
|
6239
|
+
const id = freshId(noteList(), "note");
|
|
6240
|
+
graph.notes = [...noteList(), {
|
|
6241
|
+
...structuredClone(from),
|
|
6242
|
+
id,
|
|
6243
|
+
position: { x: from.position.x + step, y: from.position.y + step }
|
|
6244
|
+
}];
|
|
6245
|
+
noteIds.push(id);
|
|
6246
|
+
}
|
|
6247
|
+
const groupIds = [];
|
|
6248
|
+
for (const gr of grouping ? g.groups || [] : []) {
|
|
6249
|
+
const members = gr.nodes.map((id2) => idOf.get(id2)).filter(Boolean);
|
|
6250
|
+
if (!members.length || members.length !== gr.nodes.length) continue;
|
|
6251
|
+
const id = freshId(groupList(), "group");
|
|
6252
|
+
setGroups([...groupList(), {
|
|
6253
|
+
id,
|
|
6254
|
+
...gr.label ? { label: gr.label } : {},
|
|
6255
|
+
...gr.tone ? { tone: gr.tone } : {},
|
|
6256
|
+
nodes: members
|
|
6257
|
+
}]);
|
|
6258
|
+
groupIds.push(id);
|
|
6259
|
+
}
|
|
6260
|
+
const ids = made.map((n) => n.id);
|
|
6261
|
+
if (connecting) cancelConnect();
|
|
6262
|
+
paintNodes();
|
|
6263
|
+
if (multi) pickItems(ids, noteIds);
|
|
6264
|
+
else select(ids.length ? { kind: "node", id: ids[0] } : { kind: "note", id: noteIds[0] });
|
|
6265
|
+
commit(action);
|
|
6266
|
+
setMessage("");
|
|
6267
|
+
return {
|
|
6268
|
+
ok: true,
|
|
6269
|
+
ids,
|
|
6270
|
+
...noteIds.length ? { notes: noteIds } : {},
|
|
6271
|
+
...groupIds.length ? { groups: groupIds } : {}
|
|
6272
|
+
};
|
|
6273
|
+
}
|
|
6274
|
+
function pasteNodes(text) {
|
|
6275
|
+
if (!clip) return CLIPBOARD_OFF;
|
|
6276
|
+
if (isReadOnly()) return roRefusal();
|
|
6277
|
+
const settle = (t) => {
|
|
6278
|
+
if (destroyed) return CLIPBOARD_OFF;
|
|
6279
|
+
if (isReadOnly()) return roRefusal();
|
|
6280
|
+
const { g, reason } = stepsOf(t);
|
|
6281
|
+
if (!g) return refused(reason);
|
|
6282
|
+
const times = pasteRun.text === t ? pasteRun.times + 1 : 1;
|
|
6283
|
+
pasteRun = { text: t, times };
|
|
6284
|
+
return addCopies(g, COPY_OFFSET * times, "paste", "flow:paste");
|
|
6285
|
+
};
|
|
6286
|
+
if (text !== void 0) return settle(text);
|
|
6287
|
+
if (clip === "memory") return settle(copiedText);
|
|
6288
|
+
let got;
|
|
6289
|
+
try {
|
|
6290
|
+
got = clip.read();
|
|
6291
|
+
} catch {
|
|
6292
|
+
return refused(say(S, "clipboardFailed"));
|
|
6293
|
+
}
|
|
6294
|
+
if (got && typeof got.then === "function") {
|
|
6295
|
+
return got.then(settle, () => destroyed ? CLIPBOARD_OFF : refused(say(S, "clipboardFailed")));
|
|
6296
|
+
}
|
|
6297
|
+
return settle(got);
|
|
4132
6298
|
}
|
|
6299
|
+
function duplicateNodes() {
|
|
6300
|
+
if (!clip) return CLIPBOARD_OFF;
|
|
6301
|
+
if (isReadOnly()) return roRefusal();
|
|
6302
|
+
if (!pickedAny()) return refused(say(S, "nothingPicked"));
|
|
6303
|
+
const rows = copyRows();
|
|
6304
|
+
const notes = copyNoteRows();
|
|
6305
|
+
if (!rows.length && !notes.length) return refused(say(S, "cannotCopy"));
|
|
6306
|
+
return addCopies(JSON.parse(copyText(rows, notes)), COPY_OFFSET, "duplicate", "flow:node:duplicate");
|
|
6307
|
+
}
|
|
6308
|
+
const clipKey = (run, needsPick = true) => () => {
|
|
6309
|
+
if (!clip || needsPick && !pickedAny()) return false;
|
|
6310
|
+
run();
|
|
6311
|
+
return void 0;
|
|
6312
|
+
};
|
|
4133
6313
|
const unbindKeys = bindFlowKeys(root, {
|
|
4134
6314
|
undo: () => {
|
|
4135
6315
|
undo();
|
|
@@ -4148,6 +6328,8 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4148
6328
|
},
|
|
4149
6329
|
open: (ev) => {
|
|
4150
6330
|
if (ev.target.closest?.(".twm-flow-graph__port")) return false;
|
|
6331
|
+
const noteEl = noting ? ev.target.closest?.("[data-note-id]") : null;
|
|
6332
|
+
if (noteEl && canvas.nodeLayer.contains(noteEl)) return editNote(noteEl.dataset.noteId) ? void 0 : false;
|
|
4151
6333
|
const nodeEl = ev.target.closest?.("[data-node-id]");
|
|
4152
6334
|
if (nodeEl && canvas.nodeLayer.contains(nodeEl)) {
|
|
4153
6335
|
if (!(selected?.kind === "node" && selected.id === nodeEl.dataset.nodeId)) {
|
|
@@ -4158,14 +6340,37 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4158
6340
|
return false;
|
|
4159
6341
|
},
|
|
4160
6342
|
escape: () => {
|
|
4161
|
-
if (!connecting)
|
|
6343
|
+
if (!connecting) {
|
|
6344
|
+
if (!pickedNotes().length && !(multi && pickedIds().length)) return false;
|
|
6345
|
+
select(null);
|
|
6346
|
+
return void 0;
|
|
6347
|
+
}
|
|
4162
6348
|
cancelConnect();
|
|
4163
6349
|
setMessage("");
|
|
4164
6350
|
return void 0;
|
|
4165
|
-
}
|
|
6351
|
+
},
|
|
6352
|
+
copy: clipKey(copyNodes),
|
|
6353
|
+
cut: clipKey(cutNodes),
|
|
6354
|
+
paste: clipKey(() => pasteNodes(), false),
|
|
6355
|
+
duplicate: clipKey(duplicateNodes),
|
|
6356
|
+
selectAll: () => {
|
|
6357
|
+
if (!multi) return false;
|
|
6358
|
+
pickItems(graph.nodes.map((x) => x.id), noting ? noteList().map((x) => x.id) : []);
|
|
6359
|
+
return void 0;
|
|
6360
|
+
},
|
|
6361
|
+
group: () => grouping && groupPicked() ? void 0 : false,
|
|
6362
|
+
ungroup: () => grouping && ungroupPicked() ? void 0 : false,
|
|
6363
|
+
up: (ev) => nudge(0, -1, ev),
|
|
6364
|
+
down: (ev) => nudge(0, 1, ev),
|
|
6365
|
+
left: (ev) => nudge(-1, 0, ev),
|
|
6366
|
+
right: (ev) => nudge(1, 0, ev)
|
|
4166
6367
|
});
|
|
4167
|
-
root.append(toolbar, why, readonlyBanner, message, strip.el, banner, body);
|
|
6368
|
+
root.append(toolbar, why, readonlyBanner, message, ...offerBox ? [offerBox] : [], strip.el, banner, body);
|
|
4168
6369
|
if (host) host.appendChild(root);
|
|
6370
|
+
function cancelFit() {
|
|
6371
|
+
if (fitFrame !== null) globalThis.cancelAnimationFrame?.(fitFrame);
|
|
6372
|
+
fitFrame = null;
|
|
6373
|
+
}
|
|
4169
6374
|
function paintReadOnly() {
|
|
4170
6375
|
root.classList.toggle("twm-flow-graph--readonly", isReadOnly());
|
|
4171
6376
|
readonlyBanner.textContent = readOnlyReason() || (isReadOnly() ? say(S, "readOnlyRefusal") : "");
|
|
@@ -4182,18 +6387,19 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4182
6387
|
* was loaded is where undo starts. With `baseline: false` the stacks are
|
|
4183
6388
|
* kept and the loaded content is the present they undo from.
|
|
4184
6389
|
*/
|
|
4185
|
-
load({ graph: g = null, flowSettings: fs } = {}, { baseline = true, fit: andFit = true } = {}) {
|
|
6390
|
+
load({ graph: g = null, flowSettings: fs } = {}, { baseline = true, fit: andFit = true, viewport } = {}) {
|
|
4186
6391
|
if (destroyed) return;
|
|
6392
|
+
editing?.done(false);
|
|
4187
6393
|
if (connecting) cancelConnect();
|
|
6394
|
+
if (pendingOffer) clearOffer();
|
|
4188
6395
|
graph = normalise(g);
|
|
4189
6396
|
if (fs !== void 0) {
|
|
4190
6397
|
settingsSchema = fs?.schema ?? settingsSchema;
|
|
4191
6398
|
settingsTitle = fs?.title ?? settingsTitle;
|
|
4192
6399
|
settingsValue = structuredClone(plainObject(fs?.value));
|
|
4193
6400
|
}
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
findingsBy = groupFindings(findings, { graph });
|
|
6401
|
+
prunePick();
|
|
6402
|
+
findingsBy = groupFindings(allFindings(), { graph });
|
|
4197
6403
|
paintNodes();
|
|
4198
6404
|
paintPanel();
|
|
4199
6405
|
const state = snapshot();
|
|
@@ -4201,14 +6407,53 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4201
6407
|
else if (state !== history.settled) history.adopt({ ...history.keep(), settled: state }, state);
|
|
4202
6408
|
paintToolbar();
|
|
4203
6409
|
announceSelection();
|
|
4204
|
-
|
|
4205
|
-
|
|
6410
|
+
const at = viewportOf(viewport) || startView;
|
|
6411
|
+
startView = null;
|
|
6412
|
+
if (at) {
|
|
6413
|
+
cancelFit();
|
|
6414
|
+
canvas.setViewport(at);
|
|
6415
|
+
viewportBaseline();
|
|
6416
|
+
} else if (andFit) {
|
|
6417
|
+
cancelFit();
|
|
6418
|
+
const fitNow = () => {
|
|
6419
|
+
fit();
|
|
6420
|
+
viewportBaseline();
|
|
6421
|
+
};
|
|
4206
6422
|
fitFrame = globalThis.requestAnimationFrame ? globalThis.requestAnimationFrame(() => {
|
|
4207
6423
|
fitFrame = null;
|
|
4208
|
-
|
|
4209
|
-
}) : (
|
|
6424
|
+
fitNow();
|
|
6425
|
+
}) : (fitNow(), null);
|
|
4210
6426
|
}
|
|
4211
6427
|
},
|
|
6428
|
+
/**
|
|
6429
|
+
* Swap the step types while the editor is open — a type registered
|
|
6430
|
+
* after `load`. The palette is redrawn; a node whose type's definition
|
|
6431
|
+
* changed — what it is built from, not the object — is rebuilt (so a node
|
|
6432
|
+
* of a type that was unknown now draws); the rest keep their elements;
|
|
6433
|
+
* the graph, and so the history, are untouched.
|
|
6434
|
+
*/
|
|
6435
|
+
setCatalogue(next) {
|
|
6436
|
+
if (destroyed) return;
|
|
6437
|
+
catalogue = toCatalogue(next, "setCatalogue");
|
|
6438
|
+
verdictOf = rulesFor(catalogue);
|
|
6439
|
+
if (connecting) cancelConnect();
|
|
6440
|
+
if (pendingOffer) clearOffer();
|
|
6441
|
+
paintPalette();
|
|
6442
|
+
paintNodes();
|
|
6443
|
+
paintPanel();
|
|
6444
|
+
},
|
|
6445
|
+
/** `{scale, tx, ty}`, as the kernel holds it. */
|
|
6446
|
+
getViewport: () => canvas.viewport(),
|
|
6447
|
+
/** Put the view there (clamped to the kernel's limits) — a fit still waiting
|
|
6448
|
+
* for its frame is dropped, and nothing is reported back. */
|
|
6449
|
+
setViewport(next) {
|
|
6450
|
+
const at = viewportOf(next);
|
|
6451
|
+
if (!at || destroyed) return false;
|
|
6452
|
+
cancelFit();
|
|
6453
|
+
canvas.setViewport(at);
|
|
6454
|
+
viewportBaseline();
|
|
6455
|
+
return true;
|
|
6456
|
+
},
|
|
4212
6457
|
getGraph: () => normalise(graph),
|
|
4213
6458
|
serialise: () => serialise(graph),
|
|
4214
6459
|
/** The text `onChange` hands on and the history holds: the graph and the flow's settings. */
|
|
@@ -4216,11 +6461,51 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4216
6461
|
getFlowSettings: () => structuredClone(settingsValue),
|
|
4217
6462
|
setFindings(list) {
|
|
4218
6463
|
findings = findingsList(list);
|
|
4219
|
-
findingsBy = groupFindings(
|
|
4220
|
-
|
|
6464
|
+
findingsBy = groupFindings(allFindings(), { graph });
|
|
6465
|
+
paintStrip();
|
|
4221
6466
|
paintNodes();
|
|
4222
|
-
panel.setFindings(
|
|
6467
|
+
panel.setFindings(allFindings());
|
|
6468
|
+
},
|
|
6469
|
+
/**
|
|
6470
|
+
* What the host shows on the nodes now — `{[id]: {badge, tone, ports:
|
|
6471
|
+
* {[name]: text}}}`, or null for none — replacing what it showed
|
|
6472
|
+
* before. Painted in place: nothing is rebuilt or recorded, `onChange`
|
|
6473
|
+
* is not called and the panel is not touched. A type's own `update`
|
|
6474
|
+
* runs for each node whose values changed, and reads them as `ctx.values`.
|
|
6475
|
+
*/
|
|
6476
|
+
setNodeValues(next) {
|
|
6477
|
+
if (destroyed) return;
|
|
6478
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
6479
|
+
for (const [id, v] of Object.entries(plainObject(next))) {
|
|
6480
|
+
const entry = valueEntry(v);
|
|
6481
|
+
if (entry) fresh.set(String(id), entry);
|
|
6482
|
+
}
|
|
6483
|
+
const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
6484
|
+
const changed = [.../* @__PURE__ */ new Set([...liveValues.keys(), ...fresh.keys()])].filter((id) => !same(liveValues.get(id), fresh.get(id)));
|
|
6485
|
+
liveValues = fresh;
|
|
6486
|
+
let bodyChanged = false;
|
|
6487
|
+
painting = true;
|
|
6488
|
+
try {
|
|
6489
|
+
for (const id of changed) {
|
|
6490
|
+
const nodeRow = nodeOf(id);
|
|
6491
|
+
const n = nodeEls.get(id);
|
|
6492
|
+
if (!nodeRow || !n) continue;
|
|
6493
|
+
paintValues(n, nodeRow);
|
|
6494
|
+
bodyChanged = runUpdate(nodeRow) || bodyChanged;
|
|
6495
|
+
}
|
|
6496
|
+
} finally {
|
|
6497
|
+
painting = false;
|
|
6498
|
+
}
|
|
6499
|
+
if (bodyChanged) {
|
|
6500
|
+
findingsBy = groupFindings(allFindings(), { graph });
|
|
6501
|
+
paintNodes();
|
|
6502
|
+
paintStrip();
|
|
6503
|
+
panel.setFindings(allFindings());
|
|
6504
|
+
}
|
|
4223
6505
|
},
|
|
6506
|
+
/** `collapsible`: fold a node to its header (true) or unfold it, one
|
|
6507
|
+
* `flow:node:collapse` entry; false when refused or nothing changes. */
|
|
6508
|
+
setCollapsed: (id, collapsed) => setCollapsed(String(id), collapsed),
|
|
4224
6509
|
/** Draw a run on the canvas — never changes the graph and rebuilds no field. */
|
|
4225
6510
|
setRunOverlay(next) {
|
|
4226
6511
|
overlay = next && typeof next === "object" ? next : null;
|
|
@@ -4232,6 +6517,9 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4232
6517
|
setReadOnly(next) {
|
|
4233
6518
|
ro = next;
|
|
4234
6519
|
if (ro && connecting) cancelConnect();
|
|
6520
|
+
if (ro) editing?.done(false);
|
|
6521
|
+
if (isReadOnly()) renaming?.finish(false);
|
|
6522
|
+
if (ro && pendingOffer) clearOffer();
|
|
4235
6523
|
dragCleanup?.();
|
|
4236
6524
|
paintReadOnly();
|
|
4237
6525
|
paintPalette();
|
|
@@ -4251,6 +6539,7 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4251
6539
|
select(target) {
|
|
4252
6540
|
if (target === null || target === void 0) select(null);
|
|
4253
6541
|
else if (target === "flow") select({ kind: "flow" });
|
|
6542
|
+
else if (multi && Array.isArray(target)) pickNodes(target.map(String));
|
|
4254
6543
|
else if (nodeOf(String(target))) select({ kind: "node", id: String(target) });
|
|
4255
6544
|
},
|
|
4256
6545
|
/** Rebuild the panel's fields, keeping the focus and the caret — for a
|
|
@@ -4263,14 +6552,62 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4263
6552
|
},
|
|
4264
6553
|
undo,
|
|
4265
6554
|
redo,
|
|
4266
|
-
arrange: () =>
|
|
4267
|
-
|
|
6555
|
+
arrange: (opts) => {
|
|
6556
|
+
const done = arrangeGraph(opts);
|
|
6557
|
+
viewportBaseline();
|
|
6558
|
+
return done;
|
|
6559
|
+
},
|
|
6560
|
+
/** `multiSelect`: line the picked nodes up — left, center, right, top,
|
|
6561
|
+
* middle or bottom. False when refused (fewer than two, read only). */
|
|
6562
|
+
align: (kind) => multi ? alignNodes(kind) : false,
|
|
6563
|
+
/** `multiSelect`: equal gaps, horizontal or vertical (three or more). */
|
|
6564
|
+
distribute: (axis) => multi ? distributeNodes(axis) : false,
|
|
6565
|
+
fit: (opts) => {
|
|
6566
|
+
fit(opts);
|
|
6567
|
+
viewportBaseline();
|
|
6568
|
+
},
|
|
6569
|
+
/** `groups`: group these steps (default: the picked ones), each leaving
|
|
6570
|
+
* the group it was in — one `flow:group:add`. The new group's id, or null. */
|
|
6571
|
+
group: (ids = null, look = {}) => ids === null ? refusals().group ? null : groupNodes(pickedIds(), look) : groupNodes(Array.isArray(ids) ? ids.map(String) : [], look),
|
|
6572
|
+
/** `groups`: remove these groups (default: the ones the pick is in), their
|
|
6573
|
+
* steps staying where they are — one `flow:group:remove`. */
|
|
6574
|
+
ungroup: (ids = null) => ids === null ? ungroupPicked() : ungroupGroups((Array.isArray(ids) ? ids : [ids]).map(String)),
|
|
6575
|
+
/** `groups`: a group's `{label, tone}` — one `flow:group:label`. */
|
|
6576
|
+
setGroup: (id, look) => setGroupLook(String(id), look || {}),
|
|
6577
|
+
/** `groups`: exactly these steps in the group — one `flow:group:members`. */
|
|
6578
|
+
setGroupMembers: (id, ids) => setGroupMembers(String(id), Array.isArray(ids) ? ids.map(String) : []),
|
|
6579
|
+
/** `notes`: add a note — one `flow:note:add`. A copy of it, or null. */
|
|
6580
|
+
addNote: (note) => addNote(note || {}),
|
|
6581
|
+
/** `notes`: a note's text — one `flow:note:edit`. */
|
|
6582
|
+
editNote: (id, text) => setNoteText(String(id), text),
|
|
6583
|
+
/** `notes`: put a note at `{x, y}` — one `flow:note:move`. */
|
|
6584
|
+
moveNote: (id, position) => moveNote(String(id), position),
|
|
6585
|
+
/** `notes`: take a note away — one `flow:note:remove`. */
|
|
6586
|
+
removeNote: (id) => noting && !isReadOnly() ? dropNotes([String(id)]) : false,
|
|
6587
|
+
/** `clipboard`: copy the picked steps and the lines among them (and the picked notes, the groups wholly
|
|
6588
|
+
* inside); `{ok, ids, notes?}` or `{ok: false, reason}`. */
|
|
6589
|
+
copy: () => copyNodes(),
|
|
6590
|
+
/** `clipboard`: copy, then remove what was copied (one remove entry). Refused read only. */
|
|
6591
|
+
cut: () => cutNodes(),
|
|
6592
|
+
/** `clipboard`: paste `text`, or what the clipboard holds — a promise when the consumer's `read` is async. */
|
|
6593
|
+
paste: (text) => pasteNodes(text),
|
|
6594
|
+
/** `clipboard`: a copy of the picked steps beside them, one `flow:node:duplicate` entry. */
|
|
6595
|
+
duplicate: () => duplicateNodes(),
|
|
4268
6596
|
destroy() {
|
|
4269
6597
|
if (destroyed) return;
|
|
6598
|
+
editing?.done(false);
|
|
6599
|
+
if (wheelTimer !== null) viewportSettled();
|
|
6600
|
+
pressEnd?.();
|
|
4270
6601
|
dragCleanup?.();
|
|
4271
|
-
|
|
6602
|
+
cancelFit();
|
|
6603
|
+
renaming?.finish(false);
|
|
6604
|
+
for (const id of [...inlines.keys()]) dropInline(id);
|
|
6605
|
+
miniSized?.disconnect();
|
|
4272
6606
|
closeFlowPopovers();
|
|
4273
6607
|
unbindKeys();
|
|
6608
|
+
canvas.root.removeEventListener("keydown", onTab);
|
|
6609
|
+
window.removeEventListener("pointerup", pressLetGo, true);
|
|
6610
|
+
window.removeEventListener("pointercancel", pressLetGo, true);
|
|
4274
6611
|
panel.destroy();
|
|
4275
6612
|
strip.destroy();
|
|
4276
6613
|
destroyed = true;
|
|
@@ -4283,9 +6620,18 @@ function createCanvasEditor(host, options = {}) {
|
|
|
4283
6620
|
paintNodes();
|
|
4284
6621
|
paintPanel();
|
|
4285
6622
|
paintToolbar();
|
|
6623
|
+
viewportBaseline();
|
|
4286
6624
|
history.baseline(snapshot());
|
|
4287
6625
|
return api;
|
|
4288
6626
|
}
|
|
6627
|
+
function viewportOf(v) {
|
|
6628
|
+
if (!v || typeof v !== "object") return null;
|
|
6629
|
+
const scale = Number(v.scale);
|
|
6630
|
+
const tx = Number(v.tx);
|
|
6631
|
+
const ty = Number(v.ty);
|
|
6632
|
+
if (!(scale > 0) || !Number.isFinite(scale) || !Number.isFinite(tx) || !Number.isFinite(ty)) return null;
|
|
6633
|
+
return { scale, tx, ty };
|
|
6634
|
+
}
|
|
4289
6635
|
|
|
4290
6636
|
// src/flow/outline/strings.js
|
|
4291
6637
|
var plural2 = (n, one, many) => n === 1 ? one : many.replace("{n}", String(n));
|
|
@@ -8045,9 +10391,11 @@ var pipeline_exports = {};
|
|
|
8045
10391
|
__export(pipeline_exports, {
|
|
8046
10392
|
LANE_ACTIONS: () => LANE_ACTIONS,
|
|
8047
10393
|
PARAMETER_NAME: () => PARAMETER_NAME,
|
|
10394
|
+
PIPELINE_STRINGS: () => PIPELINE_STRINGS,
|
|
8048
10395
|
addSource: () => addSource,
|
|
8049
10396
|
addStepAfter: () => addStepAfter,
|
|
8050
10397
|
downstreamOf: () => downstreamOf,
|
|
10398
|
+
duplicateStep: () => duplicateStep,
|
|
8051
10399
|
emptyPipeline: () => emptyPipeline,
|
|
8052
10400
|
inputCandidates: () => inputCandidates,
|
|
8053
10401
|
inputOf: () => inputOf,
|
|
@@ -8069,6 +10417,8 @@ var LANE_ACTIONS = Object.freeze([
|
|
|
8069
10417
|
"flow:step:remove",
|
|
8070
10418
|
"flow:step:label",
|
|
8071
10419
|
"flow:step:config",
|
|
10420
|
+
"flow:step:duplicate",
|
|
10421
|
+
"flow:step:menu",
|
|
8072
10422
|
"flow:join:set",
|
|
8073
10423
|
"flow:source:add",
|
|
8074
10424
|
"flow:parameter:add",
|
|
@@ -8076,6 +10426,11 @@ var LANE_ACTIONS = Object.freeze([
|
|
|
8076
10426
|
"flow:parameter:remove",
|
|
8077
10427
|
"flow:settings"
|
|
8078
10428
|
]);
|
|
10429
|
+
var PIPELINE_STRINGS = Object.freeze({
|
|
10430
|
+
copyLabel: (title, n) => n > 1 ? `${title} (copy ${n})` : `${title} (copy)`,
|
|
10431
|
+
duplicateNoStep: (id) => `There is no step \u2018${id}\u2019 to copy.`,
|
|
10432
|
+
duplicateUnavailable: (title, why) => `\u2018${title}\u2019 cannot be copied: ${why}`
|
|
10433
|
+
});
|
|
8079
10434
|
var PARAMETER_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8080
10435
|
var ID_OK2 = /^[A-Za-z0-9_-]{1,64}$/;
|
|
8081
10436
|
var isObject3 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -8248,6 +10603,29 @@ function removeStep(pipeline, catalogue, id) {
|
|
|
8248
10603
|
pipeline.nodes = pipeline.nodes.filter((n) => n.id !== id);
|
|
8249
10604
|
return true;
|
|
8250
10605
|
}
|
|
10606
|
+
function duplicateStep(pipeline, catalogue, id, { strings = null } = {}) {
|
|
10607
|
+
const S = { ...PIPELINE_STRINGS, ...isObject3(strings) ? strings : {} };
|
|
10608
|
+
const original = findNode(pipeline, id);
|
|
10609
|
+
if (!original) return { ok: false, reason: S.duplicateNoStep(String(id)) };
|
|
10610
|
+
const type = catalogue?.get?.(original.type) ?? null;
|
|
10611
|
+
const title = original.label || type?.label || String(original.type);
|
|
10612
|
+
if (type?.unavailable) return { ok: false, reason: S.duplicateUnavailable(title, String(type.unavailable)) };
|
|
10613
|
+
const titles = new Set(pipeline.nodes.map((n) => n.label || catalogue?.get?.(n.type)?.label || String(n.type)));
|
|
10614
|
+
let label = S.copyLabel(title, 1);
|
|
10615
|
+
for (let i = 2; titles.has(label); i += 1) label = S.copyLabel(title, i);
|
|
10616
|
+
const copy = {
|
|
10617
|
+
id: nextStepId(pipeline, original.type, catalogue),
|
|
10618
|
+
type: original.type,
|
|
10619
|
+
label,
|
|
10620
|
+
config: isObject3(original.config) ? clone2(original.config) : {}
|
|
10621
|
+
};
|
|
10622
|
+
for (const [k, v] of Object.entries(original)) if (!NODE_KEYS.includes(k)) copy[k] = clone2(v);
|
|
10623
|
+
pipeline.nodes.splice(pipeline.nodes.indexOf(original) + 1, 0, copy);
|
|
10624
|
+
for (const c of pipeline.connections.filter((x) => x.targetId === id)) {
|
|
10625
|
+
pipeline.connections.push({ ...clone2(c), id: nextConnectionId(pipeline), targetId: copy.id });
|
|
10626
|
+
}
|
|
10627
|
+
return { ok: true, node: copy };
|
|
10628
|
+
}
|
|
8251
10629
|
function mayFeed(pipeline, stepId, sourceId) {
|
|
8252
10630
|
if (!sourceId || sourceId === stepId) return false;
|
|
8253
10631
|
return !downstreamOf(pipeline, stepId).has(sourceId);
|
|
@@ -8435,6 +10813,7 @@ var LANE_STRINGS = Object.freeze({
|
|
|
8435
10813
|
stepMenu: (title) => `${title}: what to do`,
|
|
8436
10814
|
openSettings: "Open its settings",
|
|
8437
10815
|
addStepAfter: "Add a step after it",
|
|
10816
|
+
unavailable: "Not available here.",
|
|
8438
10817
|
// ── the toolbar ────────────────────────────────────────────────────
|
|
8439
10818
|
toolbar: "Flow",
|
|
8440
10819
|
readOnlyTitle: "This flow is open to read only.",
|
|
@@ -8559,6 +10938,7 @@ function createLaneEditor(host, options = {}) {
|
|
|
8559
10938
|
let travelling = null;
|
|
8560
10939
|
let shownColumns = "";
|
|
8561
10940
|
let panelReadOnly = false;
|
|
10941
|
+
let panelPainting = false;
|
|
8562
10942
|
const node = (id) => pipeline.nodes.find((n) => n.id === id) ?? null;
|
|
8563
10943
|
const typeOf = (n) => catalogue.get(n?.type) ?? null;
|
|
8564
10944
|
const typeLabel = (n) => typeOf(n)?.label ?? String(n?.type ?? "");
|
|
@@ -8590,6 +10970,7 @@ function createLaneEditor(host, options = {}) {
|
|
|
8590
10970
|
let undoBtn = null;
|
|
8591
10971
|
let redoBtn = null;
|
|
8592
10972
|
let statusEl = null;
|
|
10973
|
+
let noticeEl = null;
|
|
8593
10974
|
let showPreviewBtn = null;
|
|
8594
10975
|
let showRunBtn = null;
|
|
8595
10976
|
const actionButtons = /* @__PURE__ */ new Map();
|
|
@@ -8635,6 +11016,10 @@ function createLaneEditor(host, options = {}) {
|
|
|
8635
11016
|
statusEl = el("span", "twm-flow-lanes__status");
|
|
8636
11017
|
statusEl.setAttribute("aria-live", "polite");
|
|
8637
11018
|
toolbar.appendChild(statusEl);
|
|
11019
|
+
noticeEl = el("span", "twm-flow-lanes__notice");
|
|
11020
|
+
noticeEl.setAttribute("role", "status");
|
|
11021
|
+
noticeEl.hidden = true;
|
|
11022
|
+
toolbar.appendChild(noticeEl);
|
|
8638
11023
|
toolbar.appendChild(el("span", "twm-flow-lanes__spacer"));
|
|
8639
11024
|
const showId = uid("lanes-show");
|
|
8640
11025
|
const showLabel = el("span", "twm-flow-lanes__show-label", say(S, "stepsShow"));
|
|
@@ -8797,6 +11182,7 @@ function createLaneEditor(host, options = {}) {
|
|
|
8797
11182
|
function commit(action, key = null, { preview = true } = {}) {
|
|
8798
11183
|
const moved = history.commit(snapshot(), action, key);
|
|
8799
11184
|
if (!moved) return false;
|
|
11185
|
+
notice("");
|
|
8800
11186
|
report(action, key);
|
|
8801
11187
|
if (preview) runner.schedule();
|
|
8802
11188
|
return true;
|
|
@@ -9207,6 +11593,15 @@ function createLaneEditor(host, options = {}) {
|
|
|
9207
11593
|
}
|
|
9208
11594
|
const n = selected && selected !== "flow" ? node(selected) : null;
|
|
9209
11595
|
settingsHint.hidden = true;
|
|
11596
|
+
panelPainting = true;
|
|
11597
|
+
try {
|
|
11598
|
+
paintPanelBody(n);
|
|
11599
|
+
} finally {
|
|
11600
|
+
panelPainting = false;
|
|
11601
|
+
}
|
|
11602
|
+
panel.setFindings(findings);
|
|
11603
|
+
}
|
|
11604
|
+
function paintPanelBody(n) {
|
|
9210
11605
|
if (n) {
|
|
9211
11606
|
panel.show(stepPanelSpec(n));
|
|
9212
11607
|
shownColumns = JSON.stringify(inputColumns(n.id));
|
|
@@ -9227,13 +11622,12 @@ function createLaneEditor(host, options = {}) {
|
|
|
9227
11622
|
settingsHint.hidden = false;
|
|
9228
11623
|
shownColumns = "";
|
|
9229
11624
|
}
|
|
9230
|
-
panel.setFindings(findings);
|
|
9231
11625
|
}
|
|
9232
|
-
function reshowForColumns() {
|
|
11626
|
+
function reshowForColumns({ leaving = false } = {}) {
|
|
9233
11627
|
if (!panel || !selected || selected === "flow" || !node(selected)) return;
|
|
9234
11628
|
const now = JSON.stringify(inputColumns(selected));
|
|
9235
11629
|
if (now === shownColumns) return;
|
|
9236
|
-
if (panel.el.contains(root.ownerDocument.activeElement)) return;
|
|
11630
|
+
if (!leaving && panel.el.contains(root.ownerDocument.activeElement)) return;
|
|
9237
11631
|
paintPanel();
|
|
9238
11632
|
}
|
|
9239
11633
|
function availableDataTabs() {
|
|
@@ -9513,6 +11907,46 @@ function createLaneEditor(host, options = {}) {
|
|
|
9513
11907
|
options.onSelect?.({ kind: selected ? "step" : null, id: selected });
|
|
9514
11908
|
return true;
|
|
9515
11909
|
}
|
|
11910
|
+
function duplicateRefusal(id) {
|
|
11911
|
+
if (isReadOnly()) return readOnlyReason()[0] ?? say(S, "readOnlyTitle");
|
|
11912
|
+
const r = duplicateStep(normalisePipeline(pipeline), catalogue, id, { strings: options.strings });
|
|
11913
|
+
return r.ok ? null : r.reason;
|
|
11914
|
+
}
|
|
11915
|
+
function duplicate(id) {
|
|
11916
|
+
if (isReadOnly() || !node(id)) return false;
|
|
11917
|
+
const r = duplicateStep(pipeline, catalogue, id, { strings: options.strings });
|
|
11918
|
+
if (!r.ok) {
|
|
11919
|
+
notice(r.reason);
|
|
11920
|
+
return false;
|
|
11921
|
+
}
|
|
11922
|
+
selected = r.node.id;
|
|
11923
|
+
commit("flow:step:duplicate");
|
|
11924
|
+
afterShapeChange(r.node.id);
|
|
11925
|
+
options.onSelect?.({ kind: "step", id: r.node.id });
|
|
11926
|
+
return true;
|
|
11927
|
+
}
|
|
11928
|
+
function notice(text) {
|
|
11929
|
+
if (!noticeEl) return;
|
|
11930
|
+
noticeEl.textContent = String(text ?? "");
|
|
11931
|
+
noticeEl.hidden = !noticeEl.textContent;
|
|
11932
|
+
}
|
|
11933
|
+
async function runHostItem(item, id) {
|
|
11934
|
+
if (isReadOnly() || !node(id)) return false;
|
|
11935
|
+
const before = serialisePipeline(pipeline);
|
|
11936
|
+
const draft = normalisePipeline(pipeline);
|
|
11937
|
+
const step = clone3(node(id));
|
|
11938
|
+
const answer = await item.run?.({ step, pipeline: draft, editor: api });
|
|
11939
|
+
if (answer === false || destroyed || isReadOnly()) return false;
|
|
11940
|
+
if (serialisePipeline(pipeline) !== before) return false;
|
|
11941
|
+
if (serialisePipeline(draft) === before) return false;
|
|
11942
|
+
pipeline = normalisePipeline(draft);
|
|
11943
|
+
relayout();
|
|
11944
|
+
if (selected && selected !== "flow" && !node(selected)) selected = firstStep();
|
|
11945
|
+
commit("flow:step:menu");
|
|
11946
|
+
paintAll();
|
|
11947
|
+
focusCard(selected);
|
|
11948
|
+
return true;
|
|
11949
|
+
}
|
|
9516
11950
|
function changeSetting(stepId, key, value) {
|
|
9517
11951
|
if (isReadOnly()) return;
|
|
9518
11952
|
if (stepId === null) {
|
|
@@ -9667,27 +12101,56 @@ function createLaneEditor(host, options = {}) {
|
|
|
9667
12101
|
const menu = el("div", "twm-flow-lanes__menu");
|
|
9668
12102
|
menu.setAttribute("role", "menu");
|
|
9669
12103
|
let handle = null;
|
|
9670
|
-
const item = (label, glyph, run, { danger = false,
|
|
12104
|
+
const item = (label, glyph, run, { danger = false, refusal = null, id: itemId = null } = {}) => {
|
|
9671
12105
|
const b = el("button", `twm-flow-lanes__menu-item${danger ? " twm-flow-lanes__menu-item--danger" : ""}`);
|
|
12106
|
+
b.classList.toggle("twm-flow-lanes__menu-item--refused", Boolean(refusal));
|
|
9672
12107
|
b.type = "button";
|
|
9673
12108
|
b.setAttribute("role", "menuitem");
|
|
9674
|
-
b.
|
|
9675
|
-
|
|
12109
|
+
if (itemId) b.dataset.item = itemId;
|
|
12110
|
+
const words3 = el("span", "twm-flow-lanes__menu-words");
|
|
12111
|
+
words3.appendChild(el("span", "twm-flow-lanes__menu-label", label));
|
|
12112
|
+
if (refusal) {
|
|
12113
|
+
b.disabled = true;
|
|
12114
|
+
b.setAttribute("aria-disabled", "true");
|
|
12115
|
+
words3.appendChild(el("span", "twm-flow-lanes__menu-reason", refusal));
|
|
12116
|
+
}
|
|
12117
|
+
b.append(icon(glyph || ""), words3);
|
|
9676
12118
|
b.addEventListener("click", () => {
|
|
12119
|
+
if (b.disabled) return;
|
|
9677
12120
|
handle?.close("chosen");
|
|
9678
12121
|
run();
|
|
9679
12122
|
});
|
|
9680
12123
|
menu.appendChild(b);
|
|
9681
12124
|
return b;
|
|
9682
12125
|
};
|
|
9683
|
-
const first = item(say(S, "openSettings"), "tune", () => openSettingsOf(id));
|
|
12126
|
+
const first = item(say(S, "openSettings"), "tune", () => openSettingsOf(id), { id: "settings" });
|
|
9684
12127
|
if (canAddAfter(id)) {
|
|
9685
12128
|
item(say(S, "addStepAfter"), "add", () => {
|
|
9686
12129
|
const add = [...surface.querySelectorAll(".twm-flow-lanes__node")].find((w) => w.dataset.step === id)?.querySelector(".twm-flow-lanes__add");
|
|
9687
12130
|
if (add) pickAfter(id, add);
|
|
9688
|
-
});
|
|
12131
|
+
}, { id: "add" });
|
|
12132
|
+
}
|
|
12133
|
+
item(say(S, "duplicate"), "content_copy", () => duplicate(id), { refusal: duplicateRefusal(id), id: "duplicate" });
|
|
12134
|
+
if (!isReadOnly()) item(say(S, "removeStep"), "delete", () => remove(id), { danger: true, id: "remove" });
|
|
12135
|
+
const extra = typeof options.menuItems === "function" ? options.menuItems(clone3(node(id))) : null;
|
|
12136
|
+
const hosts = (Array.isArray(extra) ? extra : []).filter((x) => isObject4(x) && x.label !== void 0);
|
|
12137
|
+
if (hosts.length) {
|
|
12138
|
+
const sep = el("div", "twm-flow-lanes__menu-sep");
|
|
12139
|
+
sep.setAttribute("role", "separator");
|
|
12140
|
+
menu.appendChild(sep);
|
|
12141
|
+
}
|
|
12142
|
+
const roWhy = isReadOnly() ? readOnlyReason()[0] ?? say(S, "readOnlyTitle") : null;
|
|
12143
|
+
for (const h of hosts) {
|
|
12144
|
+
const refusal = roWhy ?? (h.disabled ? String(h.reason || say(S, "unavailable")) : null);
|
|
12145
|
+
item(
|
|
12146
|
+
String(h.label),
|
|
12147
|
+
h.icon || "",
|
|
12148
|
+
() => {
|
|
12149
|
+
runHostItem(h, id);
|
|
12150
|
+
},
|
|
12151
|
+
{ danger: Boolean(h.danger), refusal, id: h.id !== void 0 ? String(h.id) : null }
|
|
12152
|
+
);
|
|
9689
12153
|
}
|
|
9690
|
-
if (!isReadOnly()) item(say(S, "removeStep"), "delete", () => remove(id), { danger: true });
|
|
9691
12154
|
menu.addEventListener("keydown", (ev) => {
|
|
9692
12155
|
if (ev.key !== "ArrowDown" && ev.key !== "ArrowUp") return;
|
|
9693
12156
|
ev.preventDefault();
|
|
@@ -9733,6 +12196,14 @@ function createLaneEditor(host, options = {}) {
|
|
|
9733
12196
|
remove(id);
|
|
9734
12197
|
return true;
|
|
9735
12198
|
},
|
|
12199
|
+
duplicate: (ev) => {
|
|
12200
|
+
if (!(inSurface(ev) || inTabs(ev))) return false;
|
|
12201
|
+
const id = stepAt(ev) ?? selected;
|
|
12202
|
+
if (!id || id === "flow" || !node(id)) return false;
|
|
12203
|
+
if (isReadOnly()) notice(readOnlyReason()[0] ?? say(S, "readOnlyTitle"));
|
|
12204
|
+
else duplicate(id);
|
|
12205
|
+
return true;
|
|
12206
|
+
},
|
|
9736
12207
|
rename: () => {
|
|
9737
12208
|
if (!selected || selected === "flow" || !panel) return false;
|
|
9738
12209
|
const t = panel.el.querySelector("[data-twm-flow-title]");
|
|
@@ -9762,6 +12233,12 @@ function createLaneEditor(host, options = {}) {
|
|
|
9762
12233
|
if (focusCard(selected)) ev.preventDefault();
|
|
9763
12234
|
};
|
|
9764
12235
|
settingsCol?.addEventListener("keydown", onPanelKey);
|
|
12236
|
+
const onPanelLeave = (ev) => {
|
|
12237
|
+
const to = ev.relatedTarget;
|
|
12238
|
+
if (panelPainting || to && (panel.el.contains(to) || to.closest?.(".twm-flow-popover"))) return;
|
|
12239
|
+
reshowForColumns({ leaving: true });
|
|
12240
|
+
};
|
|
12241
|
+
settingsCol?.addEventListener("focusout", onPanelLeave);
|
|
9765
12242
|
function arrow(ev, dir) {
|
|
9766
12243
|
if (inTabs(ev)) {
|
|
9767
12244
|
if (dir !== "left" && dir !== "right") return false;
|
|
@@ -9907,6 +12384,7 @@ function createLaneEditor(host, options = {}) {
|
|
|
9907
12384
|
lastRunAsk?.abort();
|
|
9908
12385
|
unbindKeys();
|
|
9909
12386
|
settingsCol?.removeEventListener("keydown", onPanelKey);
|
|
12387
|
+
settingsCol?.removeEventListener("focusout", onPanelLeave);
|
|
9910
12388
|
closeFlowPopovers();
|
|
9911
12389
|
table?.dispose?.();
|
|
9912
12390
|
table = null;
|
|
@@ -9943,6 +12421,7 @@ export {
|
|
|
9943
12421
|
LANE_GEOMETRY,
|
|
9944
12422
|
LAYOUT_STRINGS as LANE_LAYOUT_STRINGS,
|
|
9945
12423
|
LANE_PAD,
|
|
12424
|
+
PIPELINE_STRINGS as LANE_PIPELINE_STRINGS,
|
|
9946
12425
|
LANE_STRINGS,
|
|
9947
12426
|
LOGIC_GRAPH_STRINGS,
|
|
9948
12427
|
OUTLINE_ACTIONS,
|
|
@@ -9955,6 +12434,7 @@ export {
|
|
|
9955
12434
|
TEMPLATE_REFERENCES,
|
|
9956
12435
|
TONES,
|
|
9957
12436
|
UI_HINTS,
|
|
12437
|
+
addConnection,
|
|
9958
12438
|
addNode,
|
|
9959
12439
|
admittedTypes,
|
|
9960
12440
|
alwaysBefore,
|
|
@@ -9963,6 +12443,9 @@ export {
|
|
|
9963
12443
|
button,
|
|
9964
12444
|
canConnect,
|
|
9965
12445
|
nodeBottom as canvasNodeBottom,
|
|
12446
|
+
nodeWidth as canvasNodeWidth,
|
|
12447
|
+
pathMidpoint as canvasPathMidpoint,
|
|
12448
|
+
pathPoints as canvasPathPoints,
|
|
9966
12449
|
portAnchor as canvasPortAnchor,
|
|
9967
12450
|
checkSettingsSchema,
|
|
9968
12451
|
closeFlowPopovers,
|