@unofficialbox/box-open-elements 0.25.0 → 0.26.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/README.md +13 -13
- package/dist/patterns/flow-builder/primitives.d.ts +1 -0
- package/dist/patterns/flow-builder/primitives.js +10 -3
- package/dist/patterns/process-modeler/design.d.ts +2 -0
- package/dist/patterns/process-modeler/design.js +288 -0
- package/dist/patterns/process-modeler/geometry.d.ts +2 -0
- package/dist/patterns/process-modeler/geometry.js +30 -7
- package/dist/patterns/process-modeler/model.d.ts +67 -2
- package/dist/patterns/process-modeler/model.js +2 -2
- package/dist/patterns/process-modeler/process-modeler.d.ts +68 -4
- package/dist/patterns/process-modeler/process-modeler.js +1257 -167
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
2
|
import { announce } from "../../foundations/a11y/index.js";
|
|
3
|
-
import { boeFocusVisibleStyles } from "../../foundations/tokens/interaction.js";
|
|
4
3
|
import { dismissModal, promoteModal } from "../../foundations/overlay/index.js";
|
|
5
4
|
import { KindPicker } from "../flow-builder/primitives.js";
|
|
6
|
-
import { arrangeProcess, routeProcessLine, lineMidpoint, nearProcessLine } from "./geometry.js";
|
|
5
|
+
import { arrangeProcess, routeProcessLine, lineMidpoint, nearProcessLine, roundedProcessPath } from "./geometry.js";
|
|
7
6
|
import { restoreProcessPositions, snapshotProcessPositions, graphChecks, normalizeProcessSelectionPath } from "./bridge.js";
|
|
7
|
+
import { processModelerDesign } from "./design.js";
|
|
8
8
|
import { completeLayout, ProcessHistory, validateProjection, } from "./model.js";
|
|
9
9
|
const svgElement = (tag) => document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
10
10
|
const emit = (element, name, detail) => element.dispatchEvent(new CustomEvent(name, {
|
|
@@ -17,7 +17,7 @@ const emit = (element, name, detail) => element.dispatchEvent(new CustomEvent(na
|
|
|
17
17
|
export class ProcessModeler extends BaseElement {
|
|
18
18
|
static tagName = "box-process-modeler";
|
|
19
19
|
static get observedAttributes() {
|
|
20
|
-
return ["locked", "snap-to-grid", "heading-level", "disable-connections"];
|
|
20
|
+
return ["locked", "snap-to-grid", "heading-level", "disable-connections", "embed-mode"];
|
|
21
21
|
}
|
|
22
22
|
documentValue;
|
|
23
23
|
modelValue = {
|
|
@@ -26,13 +26,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
26
26
|
projection = { boxes: [], lines: [] };
|
|
27
27
|
layoutValue = { boxes: {} };
|
|
28
28
|
catalogValue = [];
|
|
29
|
+
detailValue = "business";
|
|
30
|
+
lastRunValue;
|
|
31
|
+
showLastRunValue = false;
|
|
29
32
|
selectedId = null;
|
|
33
|
+
selectedLineId = null;
|
|
30
34
|
connecting = null;
|
|
31
35
|
checks = [];
|
|
32
36
|
renderer;
|
|
33
37
|
cleanupInspector;
|
|
34
38
|
inspected;
|
|
35
39
|
inspectedId;
|
|
40
|
+
inspectedControls = "";
|
|
36
41
|
historyValue = new ProcessHistory();
|
|
37
42
|
viewport = { x: 0, y: 0, zoom: 1 };
|
|
38
43
|
pointers = new Map();
|
|
@@ -41,22 +46,37 @@ export class ProcessModeler extends BaseElement {
|
|
|
41
46
|
gestureZoom = 1;
|
|
42
47
|
insertion;
|
|
43
48
|
selectedIds = new Set();
|
|
49
|
+
clipboardIds = [];
|
|
50
|
+
selectionToolbarKey = "";
|
|
51
|
+
sectionSequence = 0;
|
|
44
52
|
versionValue = 0;
|
|
45
53
|
lastProjection = "";
|
|
54
|
+
lastReadableProjection = "";
|
|
55
|
+
readableValue = null;
|
|
46
56
|
lastDocument;
|
|
47
57
|
connectionsValue = [];
|
|
48
58
|
variablesValue = [];
|
|
59
|
+
outlineValue = [];
|
|
60
|
+
fieldsValue = {};
|
|
61
|
+
processTitleValue = "Process";
|
|
62
|
+
processSummaryValue = "";
|
|
49
63
|
activePane = "Outline";
|
|
50
64
|
portDrag;
|
|
65
|
+
endDrag;
|
|
66
|
+
pendingReattach;
|
|
51
67
|
marquee;
|
|
52
68
|
segmentDrag;
|
|
69
|
+
frameResize;
|
|
70
|
+
spacePressed = false;
|
|
53
71
|
dropLine;
|
|
72
|
+
paletteDragKind = null;
|
|
54
73
|
narrowValue = false;
|
|
55
74
|
suppressClick = false;
|
|
56
75
|
resizeObserver;
|
|
57
76
|
routedLines = new Map();
|
|
58
77
|
computedChecks = [];
|
|
59
78
|
get selectedBoxes() { return this.projection.boxes.filter(box => this.selectedIds.has(box.id)); }
|
|
79
|
+
get selectedLine() { return this.projection.lines.find(line => line.id === this.selectedLineId) ?? null; }
|
|
60
80
|
get selectedPath() { return this.selected?.path ? [...this.selected.path] : null; }
|
|
61
81
|
set selectedPath(path) {
|
|
62
82
|
if (!this.isRendered && this.documentValue !== undefined) {
|
|
@@ -70,9 +90,29 @@ export class ProcessModeler extends BaseElement {
|
|
|
70
90
|
set connections(value) { this.connectionsValue = value; this.refresh(); }
|
|
71
91
|
get variables() { return this.variablesValue; }
|
|
72
92
|
set variables(value) { this.variablesValue = value; this.refresh(); }
|
|
93
|
+
get outline() { return this.outlineValue; }
|
|
94
|
+
set outline(value) { this.outlineValue = value; this.refresh(); }
|
|
95
|
+
get fields() { return this.fieldsValue; }
|
|
96
|
+
set fields(value) { this.fieldsValue = value; this.inspected = undefined; this.refresh(); }
|
|
97
|
+
get processTitle() { return this.processTitleValue; }
|
|
98
|
+
set processTitle(value) { this.processTitleValue = value; this.refresh(); }
|
|
99
|
+
get processSummary() { return this.processSummaryValue; }
|
|
100
|
+
set processSummary(value) { this.processSummaryValue = value; this.refresh(); }
|
|
101
|
+
get view() { return { ...this.viewport }; }
|
|
102
|
+
setView(view) {
|
|
103
|
+
if (![view.x, view.y, view.zoom].every(Number.isFinite) || view.zoom < 0.25 || view.zoom > 2)
|
|
104
|
+
return;
|
|
105
|
+
this.viewport = { ...view };
|
|
106
|
+
if (this.isRendered)
|
|
107
|
+
this.paintViewport();
|
|
108
|
+
}
|
|
73
109
|
get version() { return this.versionValue; }
|
|
74
110
|
get narrow() { return this.narrowValue; }
|
|
75
111
|
get positions() { return snapshotProcessPositions(this.projection, this.layoutValue); }
|
|
112
|
+
/** Current drawing and last valid drawing; hosts own conversion and persistence. */
|
|
113
|
+
get readback() {
|
|
114
|
+
return { current: this.allChecks.length ? null : this.projection, lastReadable: this.readableValue, checks: [...this.allChecks], version: this.versionValue };
|
|
115
|
+
}
|
|
76
116
|
load(document, options = {}) {
|
|
77
117
|
const projection = this.model.project(document);
|
|
78
118
|
validateProjection(projection);
|
|
@@ -80,10 +120,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
80
120
|
this.projection = projection;
|
|
81
121
|
this.versionValue = options.version ?? 0;
|
|
82
122
|
this.lastProjection = "";
|
|
123
|
+
this.lastReadableProjection = "";
|
|
124
|
+
this.readableValue = null;
|
|
83
125
|
this.layoutValue = restoreProcessPositions(projection, options.positions ?? []);
|
|
84
126
|
this.history.clear();
|
|
85
127
|
this.selectedId = null;
|
|
128
|
+
this.selectedLineId = null;
|
|
86
129
|
this.selectedIds.clear();
|
|
130
|
+
this.clipboardIds = [];
|
|
87
131
|
this.refresh();
|
|
88
132
|
if (options.selectedPath)
|
|
89
133
|
this.selectedPath = options.selectedPath;
|
|
@@ -98,6 +142,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
98
142
|
return;
|
|
99
143
|
this.selectedIds = next;
|
|
100
144
|
this.selectedId = this.selectedIds.values().next().value ?? null;
|
|
145
|
+
this.selectedLineId = null;
|
|
101
146
|
this.renderSelection();
|
|
102
147
|
if (this.isRendered)
|
|
103
148
|
this.updateToolbar();
|
|
@@ -107,6 +152,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
107
152
|
set headingLevel(value) { this.setAttribute("heading-level", String(value)); }
|
|
108
153
|
get disableConnections() { return this.hasAttribute("disable-connections"); }
|
|
109
154
|
set disableConnections(value) { this.toggleAttribute("disable-connections", value); }
|
|
155
|
+
/** Hide duplicate process heading/view controls when the host supplies them. */
|
|
156
|
+
get embedMode() { return this.hasAttribute('embed-mode'); }
|
|
157
|
+
set embedMode(value) { this.toggleAttribute('embed-mode', value); }
|
|
110
158
|
arrangedLayout(layout) {
|
|
111
159
|
const arranged = this.model.arrange?.(this.projection) ?? arrangeProcess(this.projection);
|
|
112
160
|
return completeLayout(this.projection, { ...layout, boxes: { ...arranged?.boxes, ...layout.boxes } });
|
|
@@ -150,6 +198,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
150
198
|
this.catalogValue = value;
|
|
151
199
|
this.refresh();
|
|
152
200
|
}
|
|
201
|
+
get detail() { return this.detailValue; }
|
|
202
|
+
set detail(value) {
|
|
203
|
+
if (value === this.detailValue)
|
|
204
|
+
return;
|
|
205
|
+
this.detailValue = value;
|
|
206
|
+
this.refresh();
|
|
207
|
+
emit(this, "detail-changed", { detail: value });
|
|
208
|
+
}
|
|
209
|
+
get lastRun() { return this.lastRunValue; }
|
|
210
|
+
set lastRun(value) { this.lastRunValue = value; this.inspected = undefined; this.refresh(); }
|
|
211
|
+
get showLastRun() { return this.showLastRunValue; }
|
|
212
|
+
set showLastRun(value) { this.showLastRunValue = Boolean(value); this.inspected = undefined; this.refresh(); }
|
|
153
213
|
get locked() {
|
|
154
214
|
return this.hasAttribute("locked");
|
|
155
215
|
}
|
|
@@ -187,6 +247,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
187
247
|
this.pointers.clear();
|
|
188
248
|
this.drag = undefined;
|
|
189
249
|
this.portDrag = undefined;
|
|
250
|
+
this.endDrag = undefined;
|
|
251
|
+
this.pendingReattach = undefined;
|
|
190
252
|
this.marquee = undefined;
|
|
191
253
|
this.segmentDrag = undefined;
|
|
192
254
|
}
|
|
@@ -202,15 +264,27 @@ export class ProcessModeler extends BaseElement {
|
|
|
202
264
|
}
|
|
203
265
|
if (id !== null && !this.projection.boxes.some((box) => box.id === id))
|
|
204
266
|
return;
|
|
205
|
-
if (id === this.selectedId && this.selectedIds.size === (id ? 1 : 0))
|
|
267
|
+
if (id === this.selectedId && this.selectedIds.size === (id ? 1 : 0) && !this.selectedLineId)
|
|
206
268
|
return;
|
|
207
269
|
this.selectedId = id;
|
|
208
270
|
this.selectedIds = new Set(id ? [id] : []);
|
|
271
|
+
this.selectedLineId = null;
|
|
209
272
|
this.renderSelection();
|
|
210
273
|
if (this.isRendered)
|
|
211
274
|
this.updateToolbar();
|
|
212
275
|
emit(this, "selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath });
|
|
213
276
|
}
|
|
277
|
+
selectLine(id) {
|
|
278
|
+
if (id !== null && !this.projection.lines.some(line => line.id === id))
|
|
279
|
+
return;
|
|
280
|
+
if (this.selectedLineId === id && !this.selectedIds.size)
|
|
281
|
+
return;
|
|
282
|
+
this.selectedLineId = id;
|
|
283
|
+
this.selectedId = null;
|
|
284
|
+
this.selectedIds.clear();
|
|
285
|
+
this.refresh();
|
|
286
|
+
emit(this, 'selection-changed', { box: null, boxes: [], line: this.selectedLine, path: null });
|
|
287
|
+
}
|
|
214
288
|
setValidation(checks) {
|
|
215
289
|
this.checks = checks;
|
|
216
290
|
this.refresh();
|
|
@@ -219,7 +293,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
219
293
|
this.setValidation(message ? [{ message, path }] : []);
|
|
220
294
|
}
|
|
221
295
|
requestEdit(edit) {
|
|
222
|
-
if (this.locked || (this.disableConnections && (edit.type === "connect" || edit.type === "disconnect")))
|
|
296
|
+
if (this.locked || (this.disableConnections && (edit.type === "connect" || edit.type === "disconnect" || edit.type === 'reattach')))
|
|
223
297
|
return;
|
|
224
298
|
const before = this.layout;
|
|
225
299
|
const previousIds = new Set(this.projection.boxes.map((box) => box.id));
|
|
@@ -309,7 +383,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
309
383
|
emit(this, "positions-changed", { positions: this.positions, version: this.versionValue });
|
|
310
384
|
}
|
|
311
385
|
translatedLayout(id, x, y) {
|
|
312
|
-
const snap = (n) => (this.snapToGrid ? Math.round(n /
|
|
386
|
+
const snap = (n) => (this.snapToGrid ? Math.round(n / 16) * 16 : n);
|
|
313
387
|
const next = this.layout;
|
|
314
388
|
next.boxes[id] = { ...next.boxes[id], x: snap(x), y: snap(y) };
|
|
315
389
|
const dx = next.boxes[id].x - this.layoutValue.boxes[id].x;
|
|
@@ -336,7 +410,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
336
410
|
tidy() {
|
|
337
411
|
if (this.locked)
|
|
338
412
|
return;
|
|
413
|
+
const before = this.layout.boxes;
|
|
339
414
|
this.commitLayout(this.arrangedLayout({ ...this.layout, boxes: {} }));
|
|
415
|
+
if (!globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
|
|
416
|
+
for (const element of Array.from(this.shadowRoot?.querySelectorAll('[data-box-id]') ?? [])) {
|
|
417
|
+
const previous = before[element.dataset.boxId];
|
|
418
|
+
const next = this.layoutValue.boxes[element.dataset.boxId];
|
|
419
|
+
if (!previous || !next || !element.animate)
|
|
420
|
+
continue;
|
|
421
|
+
const dx = previous.x - next.x;
|
|
422
|
+
const dy = previous.y - next.y;
|
|
423
|
+
if (dx || dy)
|
|
424
|
+
element.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: 'translate(0, 0)' }], { duration: 320, easing: 'ease-out' });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
340
427
|
this.fit();
|
|
341
428
|
}
|
|
342
429
|
resize(id, width, height) {
|
|
@@ -364,6 +451,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
364
451
|
},
|
|
365
452
|
});
|
|
366
453
|
}
|
|
454
|
+
resetLine(id) {
|
|
455
|
+
if (!this.layoutValue.lines?.[id])
|
|
456
|
+
return;
|
|
457
|
+
const next = this.layout;
|
|
458
|
+
delete next.lines?.[id];
|
|
459
|
+
this.commitLayout(next);
|
|
460
|
+
}
|
|
367
461
|
setNote(note, id = note?.id) {
|
|
368
462
|
if (!id || (note && (!Number.isFinite(note.x) || !Number.isFinite(note.y))))
|
|
369
463
|
return;
|
|
@@ -395,7 +489,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
395
489
|
const centerX = viewport.clientWidth / 2;
|
|
396
490
|
const centerY = viewport.clientHeight / 2;
|
|
397
491
|
const previous = this.viewport.zoom;
|
|
398
|
-
const next = Math.max(0.25, Math.min(
|
|
492
|
+
const next = Math.max(0.25, Math.min(2, previous * factor));
|
|
399
493
|
this.viewport.x = centerX - ((centerX - this.viewport.x) / previous) * next;
|
|
400
494
|
this.viewport.y = centerY - ((centerY - this.viewport.y) / previous) * next;
|
|
401
495
|
this.viewport.zoom = next;
|
|
@@ -416,51 +510,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
416
510
|
return {
|
|
417
511
|
x,
|
|
418
512
|
y,
|
|
419
|
-
width: Math.max(100, ...positions.map((pos) => pos.x + (pos.width ??
|
|
513
|
+
width: Math.max(100, ...positions.map((pos) => pos.x + (pos.width ?? 224))) -
|
|
420
514
|
x,
|
|
421
|
-
height: Math.max(100, ...positions.map((pos) => pos.y + (pos.height ??
|
|
515
|
+
height: Math.max(100, ...positions.map((pos) => pos.y + (pos.height ?? 64))) -
|
|
422
516
|
y,
|
|
423
517
|
};
|
|
424
518
|
}
|
|
425
519
|
renderTemplate() {
|
|
426
|
-
this.shadowRoot.innerHTML = `<style>
|
|
427
|
-
:host{display:block;font:inherit;color:var(--boe-token-text-text,#222);min-width:0}:host([hidden]){display:none!important}
|
|
428
|
-
*{box-sizing:border-box}button,input{font:inherit;color:inherit}button{background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;padding:8px;min-height:36px;cursor:pointer}button:disabled{opacity:.5;cursor:default}
|
|
429
|
-
${boeFocusVisibleStyles(":is(button,input,[part=canvas],[part=minimap])")}
|
|
430
|
-
[part=toolbar]{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
|
|
431
|
-
[part=layout]{display:grid;grid-template-columns:minmax(0,1fr) minmax(var(--boe-process-inspector-min-width,240px),var(--boe-process-inspector-width,320px));gap:16px}
|
|
432
|
-
[part=canvas]{position:relative;overflow:hidden;height:var(--boe-process-height,560px);background:var(--boe-token-surface-surface-secondary,#fbfbfb);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;touch-action:none}
|
|
433
|
-
[part=world]{position:absolute;inset:0;transform-origin:0 0}
|
|
434
|
-
[part=lines]{position:absolute;inset:0;width:1px;height:1px;overflow:visible;pointer-events:none}
|
|
435
|
-
[part=line]{stroke:var(--boe-token-text-text-secondary,#666);stroke-width:2;fill:none}
|
|
436
|
-
[part=box],[part=frame]{position:absolute;padding:12px;display:flex;flex-direction:column;gap:6px;text-align:start;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;touch-action:none}
|
|
437
|
-
[part=box] strong,[part=frame] strong{overflow-wrap:anywhere}[part=box] small{color:var(--boe-token-text-text-secondary,#666)}
|
|
438
|
-
[part=frame]{background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 4%,var(--boe-token-surface-surface,#fff));justify-content:flex-start}
|
|
439
|
-
[aria-pressed=true]{border-color:var(--boe-token-surface-surface-brand,#0061d5)}[data-invalid=true]{border-color:var(--boe-token-text-status-text-error,#b92340);border-width:2px}
|
|
440
|
-
[part=connection]{position:absolute;display:flex;gap:4px;align-items:center;background:var(--boe-token-surface-surface,#fff);border-radius:8px;padding:4px;font-size:12px;white-space:nowrap}
|
|
441
|
-
[part=connection] button{font-size:12px;min-height:32px;padding:4px}
|
|
442
|
-
[part=connection] button{opacity:0;pointer-events:none}
|
|
443
|
-
[part=connection]:hover button,[part=connection]:focus-within button{opacity:1;pointer-events:auto}
|
|
444
|
-
[part=connection]{min-width:40px;min-height:32px}
|
|
445
|
-
[part=insert-chooser]{padding:12px;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;max-width:calc(100vw - 32px);max-height:65vh;overflow:auto}
|
|
446
|
-
[part=inspector]{min-width:0;padding:12px;border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;background:var(--boe-token-surface-surface,#fff)}
|
|
447
|
-
[hidden]{display:none!important}
|
|
448
|
-
[part=palette]{display:grid;gap:8px}[part=palette] input{width:100%;min-width:0;padding:8px;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px}
|
|
449
|
-
[part=choices]{display:grid;gap:6px;max-height:300px;overflow:auto}[part=group]{font-weight:600;margin:8px 0 0}
|
|
450
|
-
[part=checks]{display:grid;gap:6px;margin-top:12px}[part=checks] button{text-align:start;color:var(--boe-token-text-status-text-error,#b92340)}
|
|
451
|
-
[part=minimap]{position:absolute;right:12px;bottom:12px;width:150px;height:90px;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;touch-action:none}
|
|
452
|
-
[part=minimap] rect{fill:var(--boe-token-surface-surface-brand,#0061d5)}[part=minimap] [data-viewport]{fill:none;stroke:var(--boe-token-text-text,#222);stroke-width:3}
|
|
453
|
-
[part=icon]{width:20px;height:20px;color:var(--boe-token-surface-surface-brand,#0061d5)}[part=icon] svg{width:20px;height:20px}
|
|
454
|
-
[part=note],[part=section]{position:absolute;border:1px dashed var(--boe-token-stroke-stroke,#ddd);background:var(--boe-token-surface-surface,#fff);padding:8px;pointer-events:none}
|
|
455
|
-
[part=help]{font-size:13px;color:var(--boe-token-text-text-secondary,#666)}
|
|
456
|
-
@media(max-width:800px){[part=layout]{grid-template-columns:minmax(0,1fr)}[part=canvas]{height:420px}}
|
|
457
|
-
@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important;scroll-behavior:auto!important}}
|
|
458
|
-
</style><div part="toolbar" role="toolbar" aria-label="Diagram controls"><button data-command="zoom-out">Zoom out</button><button data-command="reset">100%</button><button data-command="zoom-in">Zoom in</button><button data-command="fit">Fit the whole process</button><button data-command="tidy">Tidy up</button><button data-command="undo">Undo</button><button data-command="redo">Redo</button><button data-command="snap" aria-pressed="false">Snap to grid</button><button data-command="lock" aria-pressed="false">Lock diagram</button></div><div part="layout"><div part="canvas" tabindex="0" role="region" aria-label="Process diagram" aria-describedby="process-help"><div part="world"></div><svg part="minimap" role="img" aria-label="Diagram overview. Click to jump"></svg></div><div part="inspector" role="region" aria-label="Process details"><div part="palette"><label>Find a building block<input type="search" part="search"></label><div part="choices"></div></div><div part="editor"></div><div part="checks" role="region" aria-label="Checks"></div></div></div><p part="help" id="process-help">Select a box. Arrow keys move it; C then another box connects it; Delete removes it. Drag the background to pan. Use zoom controls or pinch to zoom. Escape cancels connecting.</p><div part="status" role="status" aria-live="polite"></div>`;
|
|
520
|
+
this.shadowRoot.innerHTML = `<style>${processModelerDesign}</style><div part="toolbar" role="toolbar" aria-label="Diagram controls"><button data-command="zoom-out">Zoom out</button><button data-command="reset">100%</button><button data-command="zoom-in">Zoom in</button><button data-command="fit">Fit the whole process</button><button data-command="tidy">Tidy up</button><button data-command="undo">Undo</button><button data-command="redo">Redo</button><button data-command="snap" aria-pressed="false">Snap to grid</button><button data-command="lock" aria-pressed="false">Lock diagram</button></div><div part="layout"><div part="canvas" tabindex="0" role="region" aria-label="Process diagram" aria-describedby="process-help"><div part="world"></div><svg part="minimap" role="img" aria-label="Diagram overview. Click to jump"></svg></div><div part="inspector" role="region" aria-label="Process details"><div part="palette"><label>Find a building block<input type="search" part="search"></label><div part="choices"></div></div><div part="editor"></div><div part="checks" role="region" aria-label="Checks"></div></div></div><p part="help" id="process-help">Tab moves between steps. Alt plus an arrow selects the next step that way. Arrows move the selected step; Shift moves it four grid squares. N opens the building-block chooser. Enter edits. Delete removes. Shift plus 1 fits the process. Drag the background to pan; pinch or use the controls to zoom. Escape cancels connecting.</p><div part="status" role="status" aria-live="polite"></div>`;
|
|
459
521
|
const chooser = document.createElement("dialog");
|
|
460
522
|
chooser.setAttribute("part", "insert-chooser");
|
|
461
523
|
chooser.setAttribute("aria-label", "Insert a building block");
|
|
462
524
|
const picker = new KindPicker();
|
|
463
525
|
picker.variant = "menu";
|
|
526
|
+
picker.searchable = true;
|
|
464
527
|
chooser.append(picker);
|
|
465
528
|
this.shadowRoot.append(chooser);
|
|
466
529
|
picker.addEventListener("kind-pick", event => {
|
|
@@ -475,40 +538,96 @@ export class ProcessModeler extends BaseElement {
|
|
|
475
538
|
}
|
|
476
539
|
setupPanes() {
|
|
477
540
|
const root = this.shadowRoot;
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
541
|
+
const toolbar = root.querySelector('[part=toolbar]');
|
|
542
|
+
const viewSwitch = document.createElement("div");
|
|
543
|
+
viewSwitch.setAttribute("part", "view-switch");
|
|
544
|
+
viewSwitch.setAttribute("role", "group");
|
|
545
|
+
viewSwitch.setAttribute("aria-label", "Diagram detail");
|
|
546
|
+
for (const [value, label] of [["business", "Business view"], ["technical", "Technical view"]]) {
|
|
547
|
+
const button = document.createElement("button");
|
|
548
|
+
button.type = "button";
|
|
549
|
+
button.dataset.detail = value;
|
|
550
|
+
button.textContent = label;
|
|
551
|
+
button.onclick = () => { this.detail = value; };
|
|
552
|
+
viewSwitch.append(button);
|
|
553
|
+
}
|
|
554
|
+
const runToggle = document.createElement("label");
|
|
555
|
+
runToggle.setAttribute("part", "run-toggle");
|
|
556
|
+
const checkbox = document.createElement("input");
|
|
557
|
+
checkbox.type = "checkbox";
|
|
558
|
+
checkbox.setAttribute("aria-label", "Show last run");
|
|
559
|
+
checkbox.onchange = () => { this.showLastRun = checkbox.checked; };
|
|
560
|
+
runToggle.append(checkbox, document.createTextNode("Last run"));
|
|
561
|
+
const viewMenu = document.createElement("details");
|
|
562
|
+
viewMenu.setAttribute("part", "view-menu");
|
|
563
|
+
const summary = document.createElement("summary");
|
|
564
|
+
summary.textContent = "View";
|
|
565
|
+
viewMenu.append(summary);
|
|
566
|
+
const viewOptions = document.createElement('div');
|
|
567
|
+
viewOptions.setAttribute('part', 'view-menu-options');
|
|
568
|
+
viewMenu.append(viewOptions);
|
|
569
|
+
for (const [value, label] of [["business", "Business view"], ["technical", "Technical view"]]) {
|
|
570
|
+
const button = document.createElement("button");
|
|
571
|
+
button.type = "button";
|
|
572
|
+
button.dataset.detail = value;
|
|
573
|
+
button.textContent = label;
|
|
574
|
+
button.onclick = () => { this.detail = value; viewMenu.open = false; };
|
|
575
|
+
viewOptions.append(button);
|
|
576
|
+
}
|
|
577
|
+
const mobileRun = document.createElement('button');
|
|
578
|
+
mobileRun.type = 'button';
|
|
579
|
+
mobileRun.dataset.viewOption = 'last-run';
|
|
580
|
+
mobileRun.textContent = 'Last run';
|
|
581
|
+
mobileRun.onclick = () => { this.showLastRun = !this.showLastRun; viewMenu.open = false; };
|
|
582
|
+
viewOptions.append(mobileRun);
|
|
583
|
+
const mobileChecks = document.createElement('button');
|
|
584
|
+
mobileChecks.type = 'button';
|
|
585
|
+
mobileChecks.textContent = 'Checks';
|
|
586
|
+
mobileChecks.onclick = () => { this.select(null); this.activePane = 'Checks'; this.renderPane(); viewMenu.open = false; promoteModal(root.querySelector('[part=pane-drawer][data-pane=inspector]')); };
|
|
587
|
+
viewOptions.append(mobileChecks);
|
|
588
|
+
toolbar.prepend(viewSwitch, runToggle, viewMenu);
|
|
503
589
|
const layout = root.querySelector('[part=layout]');
|
|
590
|
+
const palette = root.querySelector('[part=palette]');
|
|
591
|
+
const searchLabel = palette.querySelector('label');
|
|
592
|
+
searchLabel.querySelector('input')?.setAttribute('placeholder', 'Find a building block');
|
|
593
|
+
const searchIcon = svgElement('svg');
|
|
594
|
+
searchIcon.setAttribute('part', 'search-icon');
|
|
595
|
+
searchIcon.setAttribute('viewBox', '0 0 24 24');
|
|
596
|
+
searchIcon.setAttribute('fill', 'none');
|
|
597
|
+
searchIcon.setAttribute('stroke', 'currentColor');
|
|
598
|
+
searchIcon.setAttribute('stroke-width', '1.8');
|
|
599
|
+
searchIcon.setAttribute('aria-hidden', 'true');
|
|
600
|
+
const searchCircle = svgElement('circle');
|
|
601
|
+
searchCircle.setAttribute('cx', '10.5');
|
|
602
|
+
searchCircle.setAttribute('cy', '10.5');
|
|
603
|
+
searchCircle.setAttribute('r', '6.5');
|
|
604
|
+
const searchHandle = svgElement('path');
|
|
605
|
+
searchHandle.setAttribute('d', 'm16 16 5 5');
|
|
606
|
+
searchIcon.append(searchCircle, searchHandle);
|
|
607
|
+
searchLabel.append(searchIcon);
|
|
608
|
+
const skip = document.createElement("button");
|
|
609
|
+
skip.type = "button";
|
|
610
|
+
skip.setAttribute("part", "skip-link");
|
|
611
|
+
skip.textContent = "Skip to the diagram";
|
|
612
|
+
skip.onclick = () => { const target = this.selectedId ?? this.projection.boxes.find(box => box.kind === "start")?.id; if (target)
|
|
613
|
+
this.focusBox(target);
|
|
614
|
+
else
|
|
615
|
+
root.querySelector('[part=canvas]')?.focus(); };
|
|
616
|
+
palette.prepend(skip);
|
|
617
|
+
const hint = document.createElement("p");
|
|
618
|
+
hint.setAttribute("part", "palette-hint");
|
|
619
|
+
hint.textContent = "Drag onto the canvas, a connection, or a frame. Select a step first to add after it.";
|
|
620
|
+
palette.append(hint);
|
|
504
621
|
for (const [part, label] of [["palette", "Building blocks"], ["inspector", "Process details"]]) {
|
|
505
622
|
const pane = root.querySelector(`[part=${part}]`);
|
|
506
623
|
const dialog = document.createElement("dialog");
|
|
507
624
|
dialog.setAttribute("part", "pane-drawer");
|
|
508
625
|
dialog.setAttribute("aria-label", label);
|
|
626
|
+
dialog.dataset.pane = part;
|
|
509
627
|
const close = document.createElement("button");
|
|
510
628
|
close.setAttribute("part", "pane-close");
|
|
511
|
-
close.textContent =
|
|
629
|
+
close.textContent = 'Close';
|
|
630
|
+
close.setAttribute('aria-label', `Close ${label.toLowerCase()}`);
|
|
512
631
|
close.onclick = () => dismissModal(dialog);
|
|
513
632
|
dialog.append(close, pane);
|
|
514
633
|
if (part === "palette")
|
|
@@ -517,11 +636,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
517
636
|
layout.append(dialog);
|
|
518
637
|
const trigger = document.createElement("button");
|
|
519
638
|
trigger.dataset.command = part === "palette" ? "palette" : "details";
|
|
520
|
-
trigger.textContent =
|
|
639
|
+
trigger.textContent = part === "palette" ? "Add" : "Details";
|
|
640
|
+
trigger.setAttribute("aria-label", `Open ${label.toLowerCase()}`);
|
|
521
641
|
trigger.onclick = () => promoteModal(dialog);
|
|
522
|
-
|
|
642
|
+
toolbar.append(trigger);
|
|
523
643
|
}
|
|
524
644
|
const inspector = root.querySelector('[part=inspector]');
|
|
645
|
+
const heading = document.createElement("div");
|
|
646
|
+
heading.setAttribute("part", "process-heading");
|
|
647
|
+
const title = document.createElement("strong");
|
|
648
|
+
title.setAttribute("part", "process-title");
|
|
649
|
+
const summaryText = document.createElement("span");
|
|
650
|
+
summaryText.setAttribute("part", "process-summary");
|
|
651
|
+
heading.append(title, summaryText);
|
|
652
|
+
inspector.prepend(heading);
|
|
525
653
|
const tabs = document.createElement("div");
|
|
526
654
|
tabs.setAttribute("part", "pane-tabs");
|
|
527
655
|
tabs.setAttribute("role", "tablist");
|
|
@@ -529,6 +657,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
529
657
|
for (const name of ["Outline", "Checks", "Variables", "Connections", "Shortcuts"]) {
|
|
530
658
|
const button = document.createElement("button");
|
|
531
659
|
button.textContent = name;
|
|
660
|
+
button.dataset.pane = name;
|
|
532
661
|
button.setAttribute("role", "tab");
|
|
533
662
|
button.id = `process-tab-${name.toLowerCase()}`;
|
|
534
663
|
button.setAttribute("aria-controls", "process-pane-content");
|
|
@@ -549,26 +678,62 @@ export class ProcessModeler extends BaseElement {
|
|
|
549
678
|
content.setAttribute("part", "pane-content");
|
|
550
679
|
content.setAttribute("role", "tabpanel");
|
|
551
680
|
content.id = "process-pane-content";
|
|
552
|
-
|
|
681
|
+
heading.after(tabs);
|
|
553
682
|
inspector.append(content);
|
|
554
683
|
root.querySelector('[part=checks]').removeAttribute("role");
|
|
555
684
|
const canvas = root.querySelector('[part=canvas]');
|
|
685
|
+
const hold = document.createElement('div');
|
|
686
|
+
hold.setAttribute('part', 'hold');
|
|
687
|
+
hold.setAttribute('role', 'status');
|
|
688
|
+
hold.hidden = true;
|
|
689
|
+
const holdText = document.createElement('span');
|
|
690
|
+
holdText.setAttribute('part', 'hold-text');
|
|
691
|
+
const showChecks = document.createElement('button');
|
|
692
|
+
showChecks.type = 'button';
|
|
693
|
+
showChecks.textContent = 'Show checks';
|
|
694
|
+
showChecks.onclick = () => { this.select(null); this.activePane = 'Checks'; this.renderPane(); if (this.narrowValue)
|
|
695
|
+
promoteModal(root.querySelector('[part=pane-drawer][data-pane=inspector]')); root.querySelector('#process-tab-checks')?.focus(); };
|
|
696
|
+
hold.append(holdText, showChecks);
|
|
697
|
+
canvas.append(hold);
|
|
698
|
+
const controls = document.createElement("div");
|
|
699
|
+
controls.setAttribute("part", "controls");
|
|
700
|
+
controls.setAttribute("role", "toolbar");
|
|
701
|
+
controls.setAttribute("aria-label", "Canvas view controls");
|
|
702
|
+
for (const command of ["zoom-out", "reset", "zoom-in", "fit", "snap", "lock"]) {
|
|
703
|
+
const button = root.querySelector(`[data-command=${command}]`);
|
|
704
|
+
if (button)
|
|
705
|
+
controls.append(button);
|
|
706
|
+
}
|
|
707
|
+
canvas.append(controls);
|
|
708
|
+
const commandIcons = {
|
|
709
|
+
"zoom-out": '<circle cx="12" cy="12" r="9"/><path d="M8 12h8"/>',
|
|
710
|
+
"zoom-in": '<circle cx="12" cy="12" r="9"/><path d="M8 12h8m-4-4v8"/>',
|
|
711
|
+
fit: '<path d="M4 9V4h5m6 0h5v5m0 6v5h-5m-6 0H4v-5"/>',
|
|
712
|
+
snap: '<circle cx="5" cy="5" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="19" cy="5" r="1"/><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="19" r="1"/><circle cx="12" cy="19" r="1"/><circle cx="19" cy="19" r="1"/>',
|
|
713
|
+
lock: '<rect x="5" y="10" width="14" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
|
|
714
|
+
undo: '<path d="M9 7 4 12l5 5M4 12h10a6 6 0 0 1 6 6"/>',
|
|
715
|
+
redo: '<path d="m15 7 5 5-5 5m5-5H10a6 6 0 0 0-6 6"/>',
|
|
716
|
+
};
|
|
717
|
+
for (const [command, path] of Object.entries(commandIcons)) {
|
|
718
|
+
const button = root.querySelector(`[data-command=${command}]`);
|
|
719
|
+
const label = button.textContent ?? command;
|
|
720
|
+
button.setAttribute("aria-label", label);
|
|
721
|
+
button.title = label;
|
|
722
|
+
button.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${path}</svg><span part="sr-only">${label}</span>`;
|
|
723
|
+
}
|
|
556
724
|
const floating = document.createElement("div");
|
|
557
725
|
floating.setAttribute("part", "selection-toolbar");
|
|
558
726
|
floating.setAttribute("role", "toolbar");
|
|
559
727
|
floating.setAttribute("aria-label", "Selected steps");
|
|
560
|
-
for (const [command, label] of [["align", "Align tops"], ["space", "Space evenly"], ["delete-many", "Delete selected"]]) {
|
|
561
|
-
const button = document.createElement("button");
|
|
562
|
-
button.textContent = label;
|
|
563
|
-
button.onclick = () => this.selectionCommand(command);
|
|
564
|
-
floating.append(button);
|
|
565
|
-
}
|
|
566
728
|
canvas.append(floating);
|
|
567
729
|
if (typeof ResizeObserver !== "undefined") {
|
|
568
730
|
this.resizeObserver = new ResizeObserver(entries => {
|
|
569
|
-
const narrow = entries[0].contentRect.width <
|
|
731
|
+
const narrow = entries[0].contentRect.width < 900;
|
|
570
732
|
this.narrowValue = narrow;
|
|
571
733
|
this.toggleAttribute("data-narrow", narrow);
|
|
734
|
+
this.toggleAttribute('data-phone', entries[0].contentRect.width < 560);
|
|
735
|
+
this.renderSelectionToolbar();
|
|
736
|
+
this.positionSelectionToolbar();
|
|
572
737
|
if (!narrow)
|
|
573
738
|
root.querySelectorAll('[part=pane-drawer]').forEach(dialog => dismissModal(dialog));
|
|
574
739
|
});
|
|
@@ -579,27 +744,71 @@ export class ProcessModeler extends BaseElement {
|
|
|
579
744
|
if (!this.isRendered)
|
|
580
745
|
return;
|
|
581
746
|
const root = this.shadowRoot;
|
|
747
|
+
const selected = this.selected;
|
|
748
|
+
root.querySelector('[part=process-title]').textContent = this.selectedIds.size > 1 ? `${this.selectedIds.size} steps selected` : selected?.title ?? this.processTitleValue;
|
|
749
|
+
root.querySelector('[part=process-summary]').textContent = this.selectedIds.size > 1 ? 'Arrange or group these steps' : selected ? this.catalog.find(kind => kind.kind === selected.kind)?.label ?? selected.kind : this.processSummaryValue;
|
|
750
|
+
root.querySelector('[part=pane-tabs]').hidden = Boolean(selected);
|
|
582
751
|
root.querySelectorAll('[part=pane-tabs] button').forEach(button => {
|
|
583
|
-
|
|
752
|
+
button.textContent = button.dataset.pane === "Checks" ? `Checks ${this.allChecks.length}` : button.dataset.pane ?? "";
|
|
753
|
+
const selected = button.dataset.pane === this.activePane;
|
|
584
754
|
button.setAttribute("aria-selected", String(selected));
|
|
585
755
|
button.tabIndex = selected ? 0 : -1;
|
|
586
756
|
});
|
|
587
757
|
const content = root.querySelector('[part=pane-content]');
|
|
758
|
+
content.hidden = Boolean(selected);
|
|
588
759
|
content.setAttribute("aria-labelledby", `process-tab-${this.activePane.toLowerCase()}`);
|
|
589
760
|
const checks = root.querySelector('[part=checks]');
|
|
590
761
|
checks.hidden = this.activePane !== "Checks";
|
|
591
762
|
// Move the checks node out before replacing its parent; keep its handlers.
|
|
592
763
|
root.querySelector('[part=inspector]').append(checks);
|
|
593
764
|
content.replaceChildren();
|
|
594
|
-
root.querySelector('[part=editor]').hidden =
|
|
765
|
+
root.querySelector('[part=editor]').hidden = !selected;
|
|
595
766
|
if (this.activePane === "Outline") {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
767
|
+
const intro = document.createElement("p");
|
|
768
|
+
intro.setAttribute("part", "outline-intro");
|
|
769
|
+
intro.textContent = "The process as numbered steps, written from the diagram. Share it as documentation or read it aloud.";
|
|
770
|
+
content.append(intro);
|
|
771
|
+
const fallback = this.projection.boxes.map(box => ({ title: box.title, description: box.description, boxId: box.id }));
|
|
772
|
+
const outline = this.outlineValue.length ? this.outlineValue : fallback;
|
|
773
|
+
const copy = document.createElement('button');
|
|
774
|
+
copy.type = 'button';
|
|
775
|
+
copy.setAttribute('part', 'copy-outline');
|
|
776
|
+
copy.textContent = 'Copy as text';
|
|
777
|
+
copy.onclick = () => {
|
|
778
|
+
const lines = [];
|
|
779
|
+
const walk = (items, depth) => items.forEach((item, index) => { lines.push(`${' '.repeat(depth)}${index + 1}. ${item.title}${item.description ? ` — ${item.description}` : ''}`); if (item.children)
|
|
780
|
+
walk(item.children, depth + 1); });
|
|
781
|
+
walk(outline, 0);
|
|
782
|
+
const value = lines.join('\n');
|
|
783
|
+
emit(this, 'outline-copy-request', { text: value });
|
|
784
|
+
void navigator.clipboard?.writeText(value).then(() => this.setStatus('Outline copied')).catch(() => this.setStatus('Use your host copy action to copy the outline'));
|
|
785
|
+
};
|
|
786
|
+
content.append(copy);
|
|
787
|
+
const appendOutline = (items, parent) => {
|
|
788
|
+
const list = document.createElement("ol");
|
|
789
|
+
list.setAttribute("part", "outline-list");
|
|
790
|
+
for (const item of items) {
|
|
791
|
+
const row = document.createElement("li");
|
|
792
|
+
const label = document.createElement(item.boxId ? "button" : "strong");
|
|
793
|
+
label.textContent = item.title;
|
|
794
|
+
if (item.boxId && label instanceof HTMLButtonElement) {
|
|
795
|
+
label.type = "button";
|
|
796
|
+
label.setAttribute("aria-current", String(this.selectedIds.has(item.boxId)));
|
|
797
|
+
label.onclick = () => { this.select(item.boxId); this.focusBox(item.boxId); };
|
|
798
|
+
}
|
|
799
|
+
row.append(label);
|
|
800
|
+
if (item.description) {
|
|
801
|
+
const detail = document.createElement("p");
|
|
802
|
+
detail.textContent = item.description;
|
|
803
|
+
row.append(detail);
|
|
804
|
+
}
|
|
805
|
+
if (item.children?.length)
|
|
806
|
+
appendOutline(item.children, row);
|
|
807
|
+
list.append(row);
|
|
808
|
+
}
|
|
809
|
+
parent.append(list);
|
|
810
|
+
};
|
|
811
|
+
appendOutline(outline, content);
|
|
603
812
|
}
|
|
604
813
|
else if (this.activePane === "Checks") {
|
|
605
814
|
content.append(checks);
|
|
@@ -609,18 +818,69 @@ export class ProcessModeler extends BaseElement {
|
|
|
609
818
|
content.prepend(message);
|
|
610
819
|
}
|
|
611
820
|
}
|
|
612
|
-
else if (this.activePane === "
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
row
|
|
821
|
+
else if (this.activePane === "Variables") {
|
|
822
|
+
if (!this.variables.length)
|
|
823
|
+
content.textContent = 'No variables supplied by the host.';
|
|
824
|
+
for (const variable of this.variables) {
|
|
825
|
+
const row = document.createElement('div');
|
|
826
|
+
row.setAttribute('part', 'variable-row');
|
|
827
|
+
const heading = document.createElement('strong');
|
|
828
|
+
heading.textContent = variable.name;
|
|
829
|
+
row.append(heading);
|
|
830
|
+
if (variable.scope) {
|
|
831
|
+
const scope = document.createElement('span');
|
|
832
|
+
scope.textContent = variable.scope;
|
|
833
|
+
scope.setAttribute('part', 'variable-scope');
|
|
834
|
+
row.append(scope);
|
|
835
|
+
}
|
|
836
|
+
if (variable.description) {
|
|
837
|
+
const description = document.createElement('p');
|
|
838
|
+
description.textContent = variable.description;
|
|
839
|
+
row.append(description);
|
|
840
|
+
}
|
|
841
|
+
if (variable.startingValue !== undefined) {
|
|
842
|
+
const label = document.createElement('label');
|
|
843
|
+
label.textContent = 'Starting value';
|
|
844
|
+
const input = document.createElement('input');
|
|
845
|
+
input.type = 'text';
|
|
846
|
+
input.dataset.variable = variable.name;
|
|
847
|
+
input.value = variable.startingValue;
|
|
848
|
+
input.disabled = this.locked;
|
|
849
|
+
input.setAttribute('aria-invalid', String(Boolean(variable.problem)));
|
|
850
|
+
input.addEventListener('input', () => emit(this, 'process-variable-change-request', { name: variable.name, value: input.value }));
|
|
851
|
+
label.append(input);
|
|
852
|
+
row.append(label);
|
|
853
|
+
}
|
|
854
|
+
if (variable.problem) {
|
|
855
|
+
const problem = document.createElement('p');
|
|
856
|
+
problem.setAttribute('part', 'field-problem');
|
|
857
|
+
problem.textContent = variable.problem;
|
|
858
|
+
row.append(problem);
|
|
859
|
+
}
|
|
860
|
+
content.append(row);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
else if (this.activePane === "Connections") {
|
|
864
|
+
if (!this.connections.length)
|
|
865
|
+
content.textContent = 'No connections supplied by the host.';
|
|
866
|
+
for (const connection of this.connections) {
|
|
867
|
+
const row = document.createElement('div');
|
|
868
|
+
row.setAttribute('part', 'connection-row');
|
|
869
|
+
const name = document.createElement('strong');
|
|
870
|
+
name.textContent = connection.name;
|
|
871
|
+
const kind = document.createElement('span');
|
|
872
|
+
kind.textContent = `(${connection.kind})${connection.signIn ? ` · ${connection.signIn}` : ''}`;
|
|
873
|
+
row.append(name, document.createTextNode(' '), kind);
|
|
617
874
|
content.append(row);
|
|
618
875
|
}
|
|
619
|
-
|
|
620
|
-
|
|
876
|
+
const setup = document.createElement('button');
|
|
877
|
+
setup.type = 'button';
|
|
878
|
+
setup.textContent = 'Manage connections';
|
|
879
|
+
setup.onclick = () => emit(this, 'connection-setup-request', {});
|
|
880
|
+
content.append(setup);
|
|
621
881
|
}
|
|
622
882
|
else if (this.activePane === "Shortcuts") {
|
|
623
|
-
content.textContent = "
|
|
883
|
+
content.textContent = "Tab: move between steps. Alt+arrows: select the nearest step that way. Arrows: move one grid square. Shift+arrows: move four. N: add the next step. Ctrl+Alt+arrows: add in a direction. Enter: edit. Delete: remove. Ctrl/Command+A: select all. Ctrl/Command+C/V/D: copy, paste, duplicate. Ctrl/Command+Z and Shift+Ctrl/Command+Z: undo and redo. Shift+1: fit the whole process. Shift+drag: select a region. Drag a port: connect. Escape: cancel.";
|
|
624
884
|
}
|
|
625
885
|
}
|
|
626
886
|
selectionCommand(command) {
|
|
@@ -632,8 +892,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
632
892
|
this.requestEdit({ type: "delete", boxId: box.id });
|
|
633
893
|
return;
|
|
634
894
|
}
|
|
635
|
-
|
|
895
|
+
this.arrangeSelection(command === 'align' ? 'top' : 'distribute-horizontal');
|
|
896
|
+
}
|
|
897
|
+
arrangeSelection(action) {
|
|
898
|
+
if (this.locked || this.selectedBoxes.length < 2)
|
|
636
899
|
return;
|
|
900
|
+
const boxes = this.selectedBoxes;
|
|
637
901
|
const next = this.layout;
|
|
638
902
|
const sorted = [...boxes].filter(box => {
|
|
639
903
|
let parent = box.parentId;
|
|
@@ -643,34 +907,51 @@ export class ProcessModeler extends BaseElement {
|
|
|
643
907
|
parent = this.projection.boxes.find(box => box.id === parent)?.parentId;
|
|
644
908
|
}
|
|
645
909
|
return true;
|
|
646
|
-
})
|
|
910
|
+
});
|
|
647
911
|
if (sorted.length < 2)
|
|
648
912
|
return;
|
|
649
|
-
const
|
|
650
|
-
const
|
|
651
|
-
sorted.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
913
|
+
const width = (box) => next.boxes[box.id].width ?? (box.shape === 'gateway' || box.shape === 'event' ? 56 : 224);
|
|
914
|
+
const height = (box) => next.boxes[box.id].height ?? (box.shape === 'gateway' || box.shape === 'event' ? 56 : 64);
|
|
915
|
+
const minX = Math.min(...sorted.map(box => next.boxes[box.id].x));
|
|
916
|
+
const maxX = Math.max(...sorted.map(box => next.boxes[box.id].x + width(box)));
|
|
917
|
+
const minY = Math.min(...sorted.map(box => next.boxes[box.id].y));
|
|
918
|
+
const maxY = Math.max(...sorted.map(box => next.boxes[box.id].y + height(box)));
|
|
919
|
+
const ordered = [...sorted].sort((a, b) => action === 'distribute-vertical' ? next.boxes[a.id].y - next.boxes[b.id].y : next.boxes[a.id].x - next.boxes[b.id].x);
|
|
920
|
+
const starts = ordered.map(box => action === 'distribute-vertical' ? next.boxes[box.id].y : next.boxes[box.id].x);
|
|
921
|
+
for (const box of sorted) {
|
|
922
|
+
const position = next.boxes[box.id];
|
|
923
|
+
const index = ordered.indexOf(box);
|
|
924
|
+
const x = action === 'left' ? minX : action === 'center' ? (minX + maxX - width(box)) / 2 : action === 'right' ? maxX - width(box) : action === 'distribute-horizontal' ? starts[0] + index * (starts.at(-1) - starts[0]) / (ordered.length - 1) : position.x;
|
|
925
|
+
const y = action === 'top' ? minY : action === 'middle' ? (minY + maxY - height(box)) / 2 : action === 'bottom' ? maxY - height(box) : action === 'distribute-vertical' ? starts[0] + index * (starts.at(-1) - starts[0]) / (ordered.length - 1) : position.y;
|
|
926
|
+
const dx = x - position.x, dy = y - position.y;
|
|
927
|
+
const group = new Set([box.id]);
|
|
928
|
+
for (let changed = true; changed;) {
|
|
929
|
+
changed = false;
|
|
930
|
+
for (const candidate of this.projection.boxes)
|
|
931
|
+
if (candidate.parentId && group.has(candidate.parentId) && !group.has(candidate.id)) {
|
|
932
|
+
group.add(candidate.id);
|
|
933
|
+
changed = true;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
for (const id of group)
|
|
937
|
+
next.boxes[id] = { ...next.boxes[id], x: next.boxes[id].x + dx, y: next.boxes[id].y + dy };
|
|
938
|
+
}
|
|
658
939
|
if (emit(this, "move-request", { boxId: sorted[0].id, position: next.boxes[sorted[0].id], boxIds: sorted.map(box => box.id), positions: next.boxes }))
|
|
659
940
|
this.commitLayout(next);
|
|
660
941
|
}
|
|
942
|
+
makeSection(title = 'New section') {
|
|
943
|
+
if (this.locked || this.selectedBoxes.length < 2)
|
|
944
|
+
return;
|
|
945
|
+
const positions = this.selectedBoxes.map(box => this.layoutValue.boxes[box.id]);
|
|
946
|
+
const x = Math.min(...positions.map(position => position.x)) - 24;
|
|
947
|
+
const y = Math.min(...positions.map(position => position.y)) - 48;
|
|
948
|
+
const right = Math.max(...positions.map(position => position.x + (position.width ?? 224))) + 24;
|
|
949
|
+
const bottom = Math.max(...positions.map(position => position.y + (position.height ?? 64))) + 24;
|
|
950
|
+
const id = `section-${++this.sectionSequence}`;
|
|
951
|
+
this.setSection({ id, title, x, y, width: right - x, height: bottom - y });
|
|
952
|
+
}
|
|
661
953
|
setupListeners() {
|
|
662
|
-
|
|
663
|
-
for (const [command, label] of [
|
|
664
|
-
["connect", "Connect selected"],
|
|
665
|
-
["delete", "Delete selected"],
|
|
666
|
-
]) {
|
|
667
|
-
const button = document.createElement("button");
|
|
668
|
-
button.type = "button";
|
|
669
|
-
button.dataset.command = command;
|
|
670
|
-
button.textContent = label;
|
|
671
|
-
toolbar.append(button);
|
|
672
|
-
}
|
|
673
|
-
this.shadowRoot.querySelector("[part=toolbar]").addEventListener("click", (event) => {
|
|
954
|
+
this.shadowRoot.addEventListener("click", (event) => {
|
|
674
955
|
const command = event.target.closest("button")?.dataset.command;
|
|
675
956
|
if (command === "zoom-in")
|
|
676
957
|
this.zoomBy(1.2);
|
|
@@ -700,6 +981,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
700
981
|
this.shadowRoot.querySelector("[part=search]").addEventListener("input", () => this.renderPalette());
|
|
701
982
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
702
983
|
canvas.addEventListener("keydown", (event) => this.onKey(event));
|
|
984
|
+
canvas.addEventListener('keyup', event => { if (event.key === ' ')
|
|
985
|
+
this.spacePressed = false; });
|
|
986
|
+
canvas.addEventListener('blur', () => { this.spacePressed = false; }, true);
|
|
703
987
|
canvas.addEventListener("wheel", (event) => {
|
|
704
988
|
event.preventDefault();
|
|
705
989
|
if (event.ctrlKey || event.metaKey)
|
|
@@ -773,8 +1057,16 @@ export class ProcessModeler extends BaseElement {
|
|
|
773
1057
|
});
|
|
774
1058
|
}
|
|
775
1059
|
onKey(event) {
|
|
776
|
-
if (event.
|
|
1060
|
+
if (event.defaultPrevented)
|
|
1061
|
+
return;
|
|
1062
|
+
const target = event.target;
|
|
1063
|
+
if (target.closest("input,textarea,select,[contenteditable],button,summary"))
|
|
1064
|
+
return;
|
|
1065
|
+
if (event.key === ' ') {
|
|
1066
|
+
event.preventDefault();
|
|
1067
|
+
this.spacePressed = true;
|
|
777
1068
|
return;
|
|
1069
|
+
}
|
|
778
1070
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "z") {
|
|
779
1071
|
if (this.locked || !(event.shiftKey ? this.history.canRedo : this.history.canUndo))
|
|
780
1072
|
return;
|
|
@@ -785,8 +1077,34 @@ export class ProcessModeler extends BaseElement {
|
|
|
785
1077
|
this.undo();
|
|
786
1078
|
return;
|
|
787
1079
|
}
|
|
1080
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
|
|
1081
|
+
event.preventDefault();
|
|
1082
|
+
this.selectMany(this.projection.boxes.map(box => box.id));
|
|
1083
|
+
this.setStatus(`${this.projection.boxes.length} steps selected`);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c' && this.selectedIds.size) {
|
|
1087
|
+
event.preventDefault();
|
|
1088
|
+
this.clipboardIds = [...this.selectedIds];
|
|
1089
|
+
this.setStatus(`${this.clipboardIds.length} steps copied`);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v' && this.clipboardIds.length && !this.locked) {
|
|
1093
|
+
event.preventDefault();
|
|
1094
|
+
this.clipboardIds.forEach((id, index) => { const position = this.layoutValue.boxes[id]; if (position)
|
|
1095
|
+
this.requestEdit({ type: 'duplicate', sourceId: id, position: { x: position.x + 32 * (index + 1), y: position.y + 32 * (index + 1) } }); });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'd' && this.selectedIds.size && !this.locked) {
|
|
1099
|
+
event.preventDefault();
|
|
1100
|
+
[...this.selectedIds].forEach(id => { const position = this.layoutValue.boxes[id]; this.requestEdit({ type: 'duplicate', sourceId: id, position: { x: position.x + 32, y: position.y + 32 } }); });
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
788
1103
|
if (event.key === "Escape") {
|
|
789
1104
|
this.connecting = null;
|
|
1105
|
+
this.pendingReattach = undefined;
|
|
1106
|
+
if (this.selectedLineId)
|
|
1107
|
+
this.selectLine(null);
|
|
790
1108
|
this.portDrag = undefined;
|
|
791
1109
|
this.marquee = undefined;
|
|
792
1110
|
this.segmentDrag = undefined;
|
|
@@ -796,11 +1114,42 @@ export class ProcessModeler extends BaseElement {
|
|
|
796
1114
|
this.setStatus("Connecting cancelled");
|
|
797
1115
|
return;
|
|
798
1116
|
}
|
|
1117
|
+
if (event.shiftKey && (event.key === "1" || event.key === "!" || event.code === "Digit1")) {
|
|
1118
|
+
event.preventDefault();
|
|
1119
|
+
this.fit();
|
|
1120
|
+
this.setStatus("Whole process fitted to the canvas");
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
799
1123
|
const box = this.selected;
|
|
1124
|
+
if (this.selectedLine && (event.key === 'Delete' || event.key === 'Backspace') && !this.locked && !this.disableConnections) {
|
|
1125
|
+
event.preventDefault();
|
|
1126
|
+
this.requestEdit({ type: 'disconnect', lineId: this.selectedLine.id });
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if ((event.key === "n" || event.key === "N") && !this.locked && this.catalog.length) {
|
|
1130
|
+
event.preventDefault();
|
|
1131
|
+
this.openChooser({ type: "add", ...(box ? { from: box.id } : {}) });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
800
1134
|
if (!box || this.locked)
|
|
801
1135
|
return;
|
|
802
|
-
if (event.key === "Enter"
|
|
1136
|
+
if (event.key === "Enter") {
|
|
1137
|
+
event.preventDefault();
|
|
1138
|
+
this.activePane = "Outline";
|
|
1139
|
+
this.renderPane();
|
|
1140
|
+
const editor = this.shadowRoot.querySelector('[part=editor]');
|
|
1141
|
+
if (this.narrowValue)
|
|
1142
|
+
promoteModal(this.shadowRoot.querySelector('[part=pane-drawer][data-pane=inspector]'));
|
|
1143
|
+
const field = editor.querySelector('input,select,textarea,button,[contenteditable]');
|
|
1144
|
+
if (field)
|
|
1145
|
+
field.focus();
|
|
1146
|
+
else {
|
|
1147
|
+
const heading = editor.querySelector('[part=inspector-heading]');
|
|
1148
|
+
heading?.setAttribute('tabindex', '-1');
|
|
1149
|
+
heading?.focus();
|
|
1150
|
+
}
|
|
803
1151
|
return;
|
|
1152
|
+
}
|
|
804
1153
|
if (event.key.toLowerCase() === "c" && !this.disableConnections) {
|
|
805
1154
|
event.preventDefault();
|
|
806
1155
|
this.connecting = box.id;
|
|
@@ -818,8 +1167,33 @@ export class ProcessModeler extends BaseElement {
|
|
|
818
1167
|
}[event.key];
|
|
819
1168
|
if (direction) {
|
|
820
1169
|
event.preventDefault();
|
|
1170
|
+
if (event.ctrlKey && event.altKey && this.catalog.length && !this.disableConnections) {
|
|
1171
|
+
const side = direction[0] < 0 ? "west" : direction[0] > 0 ? "east" : direction[1] < 0 ? "north" : "south";
|
|
1172
|
+
const pos = this.layoutValue.boxes[box.id];
|
|
1173
|
+
this.openChooser({ type: "add", from: box.id, fromSide: side, position: { x: pos.x + direction[0] * 320, y: pos.y + direction[1] * 170 } });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (event.altKey) {
|
|
1177
|
+
const origin = this.layoutValue.boxes[box.id];
|
|
1178
|
+
const center = { x: origin.x + (origin.width ?? 224) / 2, y: origin.y + (origin.height ?? 64) / 2 };
|
|
1179
|
+
const candidates = this.projection.boxes.filter(candidate => candidate.id !== box.id).map(candidate => {
|
|
1180
|
+
const position = this.layoutValue.boxes[candidate.id];
|
|
1181
|
+
const dx = position.x + (position.width ?? 224) / 2 - center.x;
|
|
1182
|
+
const dy = position.y + (position.height ?? 64) / 2 - center.y;
|
|
1183
|
+
const forward = dx * direction[0] + dy * direction[1];
|
|
1184
|
+
const sideways = Math.abs(dx * direction[1] - dy * direction[0]);
|
|
1185
|
+
return { candidate, forward, score: forward + sideways * 2 };
|
|
1186
|
+
}).filter(item => item.forward > 0).sort((a, b) => a.score - b.score || a.candidate.id.localeCompare(b.candidate.id));
|
|
1187
|
+
const next = candidates[0]?.candidate;
|
|
1188
|
+
if (next) {
|
|
1189
|
+
this.select(next.id);
|
|
1190
|
+
this.focusBox(next.id);
|
|
1191
|
+
this.setStatus(`${next.title} selected`);
|
|
1192
|
+
}
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
821
1195
|
const pos = this.layoutValue.boxes[box.id];
|
|
822
|
-
const distance = event.shiftKey ?
|
|
1196
|
+
const distance = event.shiftKey ? 64 : 16;
|
|
823
1197
|
this.move(box.id, pos.x + direction[0] * distance, pos.y + direction[1] * distance);
|
|
824
1198
|
this.focusBox(box.id);
|
|
825
1199
|
}
|
|
@@ -828,8 +1202,26 @@ export class ProcessModeler extends BaseElement {
|
|
|
828
1202
|
if (event.button !== 0)
|
|
829
1203
|
return;
|
|
830
1204
|
this.suppressClick = false;
|
|
831
|
-
if (this.portDrag || this.segmentDrag || this.marquee)
|
|
1205
|
+
if (this.portDrag || this.endDrag || this.segmentDrag || this.marquee || this.frameResize)
|
|
1206
|
+
return;
|
|
1207
|
+
const resize = event.target.closest('[part=frame-resize]');
|
|
1208
|
+
if (resize && !this.locked) {
|
|
1209
|
+
event.preventDefault();
|
|
1210
|
+
event.stopPropagation();
|
|
1211
|
+
const id = resize.dataset.owner;
|
|
1212
|
+
const position = this.layoutValue.boxes[id];
|
|
1213
|
+
this.frameResize = { pointerId: event.pointerId, id, start: this.canvasPoint(event), width: position.width ?? 224, height: position.height ?? 64 };
|
|
1214
|
+
this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const grip = event.target.closest('[part=end-grip]');
|
|
1218
|
+
if (grip && !this.locked && !this.disableConnections) {
|
|
1219
|
+
event.preventDefault();
|
|
1220
|
+
const point = this.canvasPoint(event);
|
|
1221
|
+
this.endDrag = { pointerId: event.pointerId, lineId: grip.dataset.lineId, end: grip.dataset.end, start: point, point };
|
|
1222
|
+
this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
|
|
832
1223
|
return;
|
|
1224
|
+
}
|
|
833
1225
|
const control = event.target.closest("[part=port]");
|
|
834
1226
|
if (control && !this.locked && !this.disableConnections) {
|
|
835
1227
|
event.preventDefault();
|
|
@@ -841,6 +1233,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
841
1233
|
const segment = event.target.closest("[data-segment]");
|
|
842
1234
|
if (segment && !this.locked) {
|
|
843
1235
|
const line = this.projection.lines.find(line => line.id === segment.dataset.lineId);
|
|
1236
|
+
this.selectLine(line.id);
|
|
844
1237
|
this.segmentDrag = { pointerId: event.pointerId, id: line.id, index: Number(segment.dataset.segment), points: routeProcessLine(line, this.layoutValue, this.projection) };
|
|
845
1238
|
this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
|
|
846
1239
|
event.preventDefault();
|
|
@@ -859,7 +1252,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
859
1252
|
const target = event.target.closest("[data-box-id]");
|
|
860
1253
|
const id = target?.dataset.boxId;
|
|
861
1254
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
862
|
-
if (id && !this.selectedIds.has(id) && !event.shiftKey)
|
|
1255
|
+
if (id && !this.spacePressed && !this.selectedIds.has(id) && !event.shiftKey)
|
|
863
1256
|
this.select(id);
|
|
864
1257
|
if (!id && event.shiftKey && !this.locked) {
|
|
865
1258
|
const point = this.canvasPoint(event);
|
|
@@ -869,7 +1262,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
869
1262
|
return;
|
|
870
1263
|
}
|
|
871
1264
|
this.drag = {
|
|
872
|
-
id: this.locked ? undefined : id,
|
|
1265
|
+
id: this.locked || this.spacePressed ? undefined : id,
|
|
873
1266
|
x: event.clientX,
|
|
874
1267
|
y: event.clientY,
|
|
875
1268
|
position: id ? { ...this.layoutValue.boxes[id] } : undefined,
|
|
@@ -882,10 +1275,19 @@ export class ProcessModeler extends BaseElement {
|
|
|
882
1275
|
return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
|
883
1276
|
}
|
|
884
1277
|
pointerMove(event) {
|
|
885
|
-
const gesture = this.portDrag ?? this.segmentDrag ?? this.marquee;
|
|
1278
|
+
const gesture = this.portDrag ?? this.endDrag ?? this.segmentDrag ?? this.marquee ?? this.frameResize;
|
|
886
1279
|
if (gesture && gesture.pointerId !== event.pointerId)
|
|
887
1280
|
return;
|
|
888
1281
|
const point = this.canvasPoint(event);
|
|
1282
|
+
if (this.frameResize) {
|
|
1283
|
+
const resize = this.frameResize;
|
|
1284
|
+
const element = Array.from(this.shadowRoot.querySelectorAll('[part=frame]')).find(frame => frame.dataset.boxId === resize.id);
|
|
1285
|
+
if (element) {
|
|
1286
|
+
element.style.width = `${Math.max(120, resize.width + point.x - resize.start.x)}px`;
|
|
1287
|
+
element.style.height = `${Math.max(64, resize.height + point.y - resize.start.y)}px`;
|
|
1288
|
+
}
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
889
1291
|
if (this.portDrag) {
|
|
890
1292
|
this.portDrag.point = point;
|
|
891
1293
|
let ghost = this.shadowRoot.querySelector('[part=connection-preview]');
|
|
@@ -897,6 +1299,31 @@ export class ProcessModeler extends BaseElement {
|
|
|
897
1299
|
}
|
|
898
1300
|
const start = this.portDrag.start;
|
|
899
1301
|
ghost.setAttribute("points", `${start.x},${start.y} ${point.x},${start.y} ${point.x},${point.y}`);
|
|
1302
|
+
const target = this.boxAt(point, this.portDrag.id);
|
|
1303
|
+
const pinned = target ? this.sideAt(target, point) : undefined;
|
|
1304
|
+
this.shadowRoot.querySelectorAll('[data-box-id]').forEach(box => { box.dataset.connectTarget = String(box.dataset.boxId === target?.id); });
|
|
1305
|
+
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { port.dataset.hot = String(port.dataset.owner === target?.id && port.dataset.side === pinned); });
|
|
1306
|
+
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Connect to ${target.title}` : 'Drop on a step to connect', !target);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (this.endDrag) {
|
|
1310
|
+
this.endDrag.point = point;
|
|
1311
|
+
let ghost = this.shadowRoot.querySelector('[part=connection-preview]');
|
|
1312
|
+
if (!ghost) {
|
|
1313
|
+
ghost = svgElement('polyline');
|
|
1314
|
+
ghost.setAttribute('part', 'connection-preview');
|
|
1315
|
+
this.shadowRoot.querySelector('[part=lines]').append(ghost);
|
|
1316
|
+
}
|
|
1317
|
+
const points = this.routedLines.get(this.endDrag.lineId);
|
|
1318
|
+
const fixed = this.endDrag.end === 'from' ? points?.at(-1) : points?.[0];
|
|
1319
|
+
if (fixed)
|
|
1320
|
+
ghost.setAttribute('points', `${fixed.x},${fixed.y} ${point.x},${fixed.y} ${point.x},${point.y}`);
|
|
1321
|
+
const target = this.boxAt(point);
|
|
1322
|
+
const pinned = target ? this.sideAt(target, point) : undefined;
|
|
1323
|
+
this.shadowRoot.querySelectorAll('[data-box-id]').forEach(box => { box.dataset.connectTarget = String(box.dataset.boxId === target?.id); });
|
|
1324
|
+
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { port.dataset.hot = String(port.dataset.owner === target?.id && port.dataset.side === pinned); });
|
|
1325
|
+
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Attach to ${target.title}` : 'Drop on a step to reattach', !target);
|
|
1326
|
+
this.setStatus(target ? `Release to attach the connection to ${target.title}` : 'Move over a step to attach this connection');
|
|
900
1327
|
return;
|
|
901
1328
|
}
|
|
902
1329
|
if (this.segmentDrag) {
|
|
@@ -906,7 +1333,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
906
1333
|
else
|
|
907
1334
|
points[index].y = points[index + 1].y = point.y;
|
|
908
1335
|
const path = Array.from(this.shadowRoot.querySelectorAll('[part=line]')).find(path => path.dataset.lineId === this.segmentDrag.id);
|
|
909
|
-
path?.setAttribute("
|
|
1336
|
+
path?.setAttribute("d", roundedProcessPath(points));
|
|
910
1337
|
return;
|
|
911
1338
|
}
|
|
912
1339
|
if (this.marquee) {
|
|
@@ -953,14 +1380,26 @@ export class ProcessModeler extends BaseElement {
|
|
|
953
1380
|
}
|
|
954
1381
|
}
|
|
955
1382
|
pointerEnd(event, commit) {
|
|
956
|
-
const gesture = this.portDrag ?? this.segmentDrag ?? this.marquee;
|
|
1383
|
+
const gesture = this.portDrag ?? this.endDrag ?? this.segmentDrag ?? this.marquee ?? this.frameResize;
|
|
957
1384
|
if (gesture && gesture.pointerId !== event.pointerId)
|
|
958
1385
|
return;
|
|
959
1386
|
const point = this.canvasPoint(event);
|
|
1387
|
+
if (this.frameResize) {
|
|
1388
|
+
const resize = this.frameResize;
|
|
1389
|
+
this.frameResize = undefined;
|
|
1390
|
+
if (commit)
|
|
1391
|
+
this.resize(resize.id, resize.width + point.x - resize.start.x, resize.height + point.y - resize.start.y);
|
|
1392
|
+
else
|
|
1393
|
+
this.refresh();
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
960
1396
|
if (this.portDrag) {
|
|
961
1397
|
const port = this.portDrag;
|
|
962
1398
|
this.portDrag = undefined;
|
|
963
1399
|
this.shadowRoot.querySelector('[part=connection-preview]')?.remove();
|
|
1400
|
+
this.shadowRoot.querySelector('[part=connect-tooltip]')?.remove();
|
|
1401
|
+
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; });
|
|
1402
|
+
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { delete port.dataset.hot; });
|
|
964
1403
|
if (commit) {
|
|
965
1404
|
if (Math.hypot(point.x - port.start.x, point.y - port.start.y) < 8) {
|
|
966
1405
|
const pos = this.layoutValue.boxes[port.id];
|
|
@@ -970,7 +1409,29 @@ export class ProcessModeler extends BaseElement {
|
|
|
970
1409
|
else {
|
|
971
1410
|
const target = this.boxAt(point, port.id);
|
|
972
1411
|
if (target)
|
|
973
|
-
this.requestEdit({ type: "connect", from: port.id, to: target.id, fromSide: port.side });
|
|
1412
|
+
this.requestEdit({ type: "connect", from: port.id, to: target.id, fromSide: port.side, toSide: this.sideAt(target, point) });
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (this.endDrag) {
|
|
1418
|
+
const drag = this.endDrag;
|
|
1419
|
+
this.endDrag = undefined;
|
|
1420
|
+
this.shadowRoot.querySelector('[part=connection-preview]')?.remove();
|
|
1421
|
+
this.shadowRoot.querySelector('[part=connect-tooltip]')?.remove();
|
|
1422
|
+
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; });
|
|
1423
|
+
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { delete port.dataset.hot; });
|
|
1424
|
+
if (commit) {
|
|
1425
|
+
if (Math.hypot(point.x - drag.start.x, point.y - drag.start.y) < 8) {
|
|
1426
|
+
this.pendingReattach = { lineId: drag.lineId, end: drag.end };
|
|
1427
|
+
this.setStatus('Select a step to reattach the connection');
|
|
1428
|
+
}
|
|
1429
|
+
else {
|
|
1430
|
+
const target = this.boxAt(point);
|
|
1431
|
+
if (target)
|
|
1432
|
+
this.requestEdit({ type: 'reattach', lineId: drag.lineId, [drag.end]: target.id, [drag.end === 'from' ? 'fromSide' : 'toSide']: this.sideAt(target, point) });
|
|
1433
|
+
else
|
|
1434
|
+
this.setStatus('Connection unchanged. Drop on a step to reattach.');
|
|
974
1435
|
}
|
|
975
1436
|
}
|
|
976
1437
|
return;
|
|
@@ -988,7 +1449,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
988
1449
|
const { start } = this.marquee;
|
|
989
1450
|
this.marquee = undefined;
|
|
990
1451
|
if (commit)
|
|
991
|
-
this.selectMany(this.projection.boxes.filter(box => { const p = this.layoutValue.boxes[box.id]; return p.x < Math.max(start.x, point.x) && p.x + (p.width ??
|
|
1452
|
+
this.selectMany(this.projection.boxes.filter(box => { const p = this.layoutValue.boxes[box.id]; return p.x < Math.max(start.x, point.x) && p.x + (p.width ?? 224) > Math.min(start.x, point.x) && p.y < Math.max(start.y, point.y) && p.y + (p.height ?? 64) > Math.min(start.y, point.y); }).map(box => box.id));
|
|
992
1453
|
this.shadowRoot.querySelector('[part=marquee]')?.remove();
|
|
993
1454
|
this.pointers.delete(event.pointerId);
|
|
994
1455
|
return;
|
|
@@ -1025,8 +1486,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
1025
1486
|
}
|
|
1026
1487
|
}
|
|
1027
1488
|
const primary = next.boxes[drag.id];
|
|
1028
|
-
const shiftX = this.snapToGrid ? Math.round((primary.x + dx) /
|
|
1029
|
-
const shiftY = this.snapToGrid ? Math.round((primary.y + dy) /
|
|
1489
|
+
const shiftX = this.snapToGrid ? Math.round((primary.x + dx) / 16) * 16 - primary.x : dx;
|
|
1490
|
+
const shiftY = this.snapToGrid ? Math.round((primary.y + dy) / 16) * 16 - primary.y : dy;
|
|
1030
1491
|
for (const id of moved)
|
|
1031
1492
|
next.boxes[id] = { ...next.boxes[id], x: next.boxes[id].x + shiftX, y: next.boxes[id].y + shiftY };
|
|
1032
1493
|
if (emit(this, "move-request", { boxId: drag.id, position: next.boxes[drag.id], boxIds: [...moved], positions: next.boxes }))
|
|
@@ -1059,15 +1520,22 @@ export class ProcessModeler extends BaseElement {
|
|
|
1059
1520
|
parent = this.projection.boxes.find(b => b.id === parent)?.parentId;
|
|
1060
1521
|
}
|
|
1061
1522
|
const p = this.layoutValue.boxes[box.id];
|
|
1062
|
-
return point.x >= p.x - 12 && point.x <= p.x + (p.width ??
|
|
1523
|
+
return point.x >= p.x - 12 && point.x <= p.x + (p.width ?? 224) + 12 && point.y >= p.y - 12 && point.y <= p.y + (p.height ?? 64) + 12;
|
|
1063
1524
|
});
|
|
1064
1525
|
}
|
|
1526
|
+
sideAt(box, point) {
|
|
1527
|
+
const p = this.layoutValue.boxes[box.id];
|
|
1528
|
+
const width = p.width ?? 224;
|
|
1529
|
+
const height = p.height ?? 64;
|
|
1530
|
+
const sides = [['north', p.x + width / 2, p.y], ['east', p.x + width, p.y + height / 2], ['south', p.x + width / 2, p.y + height], ['west', p.x, p.y + height / 2]];
|
|
1531
|
+
return sides.find(([, x, y]) => Math.hypot(point.x - x, point.y - y) <= 12 / this.viewport.zoom)?.[0];
|
|
1532
|
+
}
|
|
1065
1533
|
lineAt(point, except) {
|
|
1066
1534
|
return this.projection.lines.find(line => line.from !== except && line.to !== except && nearProcessLine(point, this.routedLines.get(line.id) ?? [], 16 / this.viewport.zoom));
|
|
1067
1535
|
}
|
|
1068
1536
|
markDropLine(point, except) {
|
|
1069
1537
|
this.dropLine = point ? this.lineAt(point, except)?.id : undefined;
|
|
1070
|
-
this.shadowRoot.querySelectorAll('[part=line]').forEach(path => path.dataset.drop = String(path.dataset.
|
|
1538
|
+
this.shadowRoot.querySelectorAll('[part=line]').forEach(path => { const active = path.dataset.lineId === this.dropLine; path.dataset.drop = String(active); path.setAttribute('marker-end', `url(#${active || path.dataset.selected === 'true' ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`); });
|
|
1071
1539
|
}
|
|
1072
1540
|
alignedPoint(id, x, y) {
|
|
1073
1541
|
const world = this.shadowRoot.querySelector('[part=world]');
|
|
@@ -1085,17 +1553,17 @@ export class ProcessModeler extends BaseElement {
|
|
|
1085
1553
|
y = p.y;
|
|
1086
1554
|
const guide = document.createElement("div");
|
|
1087
1555
|
guide.setAttribute("part", "guide");
|
|
1088
|
-
guide.style.cssText = axis === "x" ? `left:${x}px;top:${Math.min(y, p.y)}px;width:1px;height:${Math.abs(y - p.y) +
|
|
1556
|
+
guide.style.cssText = axis === "x" ? `left:${x}px;top:${Math.min(y, p.y)}px;width:1px;height:${Math.abs(y - p.y) + 64}px` : `left:${Math.min(x, p.x)}px;top:${y}px;height:1px;width:${Math.abs(x - p.x) + 224}px`;
|
|
1089
1557
|
world.append(guide);
|
|
1090
1558
|
}
|
|
1091
1559
|
}
|
|
1092
1560
|
if (Math.abs(y - p.y) < 8) {
|
|
1093
|
-
const distance = x - p.x - (p.width ??
|
|
1561
|
+
const distance = x - p.x - (p.width ?? 224);
|
|
1094
1562
|
if (distance > 0) {
|
|
1095
1563
|
const measure = document.createElement("span");
|
|
1096
1564
|
measure.setAttribute("part", "measure");
|
|
1097
1565
|
measure.textContent = `${Math.round(distance)}px`;
|
|
1098
|
-
measure.style.cssText = `left:${p.x + (p.width ??
|
|
1566
|
+
measure.style.cssText = `left:${p.x + (p.width ?? 224)}px;top:${y + 40}px`;
|
|
1099
1567
|
world.append(measure);
|
|
1100
1568
|
}
|
|
1101
1569
|
}
|
|
@@ -1105,12 +1573,43 @@ export class ProcessModeler extends BaseElement {
|
|
|
1105
1573
|
openChooser(edit) {
|
|
1106
1574
|
this.insertion = edit;
|
|
1107
1575
|
const chooser = this.shadowRoot.querySelector('[part=insert-chooser]');
|
|
1576
|
+
this.shadowRoot.querySelector('[part=ghost-box]')?.remove();
|
|
1577
|
+
if ('position' in edit && edit.position) {
|
|
1578
|
+
const ghost = document.createElement('div');
|
|
1579
|
+
ghost.setAttribute('part', 'ghost-box');
|
|
1580
|
+
ghost.textContent = edit.kind?.label ?? 'New step';
|
|
1581
|
+
this.place(ghost, { ...edit.position, width: 224, height: 64 });
|
|
1582
|
+
this.shadowRoot.querySelector('[part=world]')?.append(ghost);
|
|
1583
|
+
}
|
|
1584
|
+
chooser.addEventListener('close', () => this.shadowRoot?.querySelector('[part=ghost-box]')?.remove(), { once: true });
|
|
1108
1585
|
const picker = chooser.querySelector('box-kind-picker');
|
|
1109
|
-
picker.catalog = this.catalog;
|
|
1586
|
+
picker.catalog = this.catalog.filter(kind => !kind.placement);
|
|
1110
1587
|
picker.refresh();
|
|
1111
1588
|
promoteModal(chooser);
|
|
1112
1589
|
picker.focus();
|
|
1113
1590
|
}
|
|
1591
|
+
addLayoutKind(kind, point) {
|
|
1592
|
+
const canvas = this.shadowRoot.querySelector('[part=canvas]');
|
|
1593
|
+
const position = point ?? { x: (canvas.clientWidth / 2 - this.viewport.x) / this.viewport.zoom, y: (canvas.clientHeight / 2 - this.viewport.y) / this.viewport.zoom };
|
|
1594
|
+
const id = `${kind.placement}-${++this.sectionSequence}`;
|
|
1595
|
+
if (kind.placement === 'note')
|
|
1596
|
+
this.setNote({ id, text: kind.label, x: position.x, y: position.y });
|
|
1597
|
+
if (kind.placement === 'section')
|
|
1598
|
+
this.setSection({ id, title: kind.label, description: '', x: position.x, y: position.y, width: 320, height: 240 });
|
|
1599
|
+
}
|
|
1600
|
+
showConnectTooltip(point, message, invalid = false) {
|
|
1601
|
+
const world = this.shadowRoot.querySelector('[part=world]');
|
|
1602
|
+
let tooltip = world.querySelector('[part=connect-tooltip]');
|
|
1603
|
+
if (!tooltip) {
|
|
1604
|
+
tooltip = document.createElement('div');
|
|
1605
|
+
tooltip.setAttribute('part', 'connect-tooltip');
|
|
1606
|
+
world.append(tooltip);
|
|
1607
|
+
}
|
|
1608
|
+
tooltip.textContent = message;
|
|
1609
|
+
tooltip.dataset.invalid = String(invalid);
|
|
1610
|
+
tooltip.style.left = `${point.x + 16}px`;
|
|
1611
|
+
tooltip.style.top = `${point.y + 16}px`;
|
|
1612
|
+
}
|
|
1114
1613
|
update() {
|
|
1115
1614
|
const selectionBefore = JSON.stringify({ ids: [...this.selectedIds], path: this.selectedPath });
|
|
1116
1615
|
if (this.documentValue !== undefined) {
|
|
@@ -1124,11 +1623,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
1124
1623
|
this.computedChecks = this.collectChecks();
|
|
1125
1624
|
if (this.selectedId && !this.selected)
|
|
1126
1625
|
this.selectedId = null;
|
|
1626
|
+
if (this.selectedLineId && !this.selectedLine)
|
|
1627
|
+
this.selectedLineId = null;
|
|
1127
1628
|
this.selectedIds = new Set([...this.selectedIds].filter(id => this.projection.boxes.some(box => box.id === id)));
|
|
1128
1629
|
if (!this.selectedId)
|
|
1129
1630
|
this.selectedId = this.selectedIds.values().next().value ?? null;
|
|
1130
1631
|
const serialized = JSON.stringify({
|
|
1131
|
-
boxes: this.projection.boxes.map(box => ({ id: box.id, kind: box.kind, title: box.title, description: box.description, path: box.path, fingerprint: box.fingerprint, frame: box.frame, parentId: box.parentId })),
|
|
1632
|
+
boxes: this.projection.boxes.map(box => ({ id: box.id, kind: box.kind, title: box.title, description: box.description, path: box.path, fingerprint: box.fingerprint, frame: box.frame, loopMark: box.loopMark, parentId: box.parentId })),
|
|
1132
1633
|
lines: this.projection.lines,
|
|
1133
1634
|
});
|
|
1134
1635
|
if (serialized !== this.lastProjection || this.documentValue !== this.lastDocument) {
|
|
@@ -1138,6 +1639,15 @@ export class ProcessModeler extends BaseElement {
|
|
|
1138
1639
|
this.lastDocument = this.documentValue;
|
|
1139
1640
|
emit(this, "projection-changed", { projection: this.projection, version: this.versionValue, checks: this.allChecks });
|
|
1140
1641
|
}
|
|
1642
|
+
// Riptide or another host owns conversion. It supplies validate() and
|
|
1643
|
+
// receives this event only when that conversion can read the drawing.
|
|
1644
|
+
if (this.allChecks.length)
|
|
1645
|
+
this.lastReadableProjection = "";
|
|
1646
|
+
if (!this.allChecks.length && serialized !== this.lastReadableProjection) {
|
|
1647
|
+
this.lastReadableProjection = serialized;
|
|
1648
|
+
this.readableValue = this.projection;
|
|
1649
|
+
emit(this, "readable-projection-changed", { projection: this.projection, version: this.versionValue });
|
|
1650
|
+
}
|
|
1141
1651
|
const focused = this.shadowRoot.activeElement
|
|
1142
1652
|
?.dataset.boxId;
|
|
1143
1653
|
this.renderWorld();
|
|
@@ -1155,7 +1665,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
1155
1665
|
collectChecks() {
|
|
1156
1666
|
const routes = this.projection.lines.filter(line => !this.routedLines.get(line.id)?.length)
|
|
1157
1667
|
.map(line => ({ message: "A connection is blocked by overlapping steps. Move a step or adjust its route.", boxId: line.from }));
|
|
1158
|
-
|
|
1668
|
+
const hostChecks = this.documentValue === undefined ? undefined : this.model.validate?.(this.documentValue, this.projection);
|
|
1669
|
+
return [...this.checks, ...(hostChecks ?? graphChecks(this.projection)), ...routes];
|
|
1159
1670
|
}
|
|
1160
1671
|
renderWorld() {
|
|
1161
1672
|
const world = this.shadowRoot.querySelector("[part=world]");
|
|
@@ -1163,7 +1674,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
1163
1674
|
for (const section of this.layoutValue.sections ?? []) {
|
|
1164
1675
|
const element = document.createElement("div");
|
|
1165
1676
|
element.setAttribute("part", "section");
|
|
1166
|
-
|
|
1677
|
+
const title = document.createElement('strong');
|
|
1678
|
+
title.textContent = section.title;
|
|
1679
|
+
element.append(title);
|
|
1680
|
+
if (section.description) {
|
|
1681
|
+
const description = document.createElement('small');
|
|
1682
|
+
description.textContent = section.description;
|
|
1683
|
+
element.append(description);
|
|
1684
|
+
}
|
|
1167
1685
|
this.place(element, section);
|
|
1168
1686
|
world.append(element);
|
|
1169
1687
|
}
|
|
@@ -1173,27 +1691,66 @@ export class ProcessModeler extends BaseElement {
|
|
|
1173
1691
|
element.setAttribute("role", "group");
|
|
1174
1692
|
element.setAttribute("part", box.frame ? "frame" : "box");
|
|
1175
1693
|
element.dataset.boxId = box.id;
|
|
1176
|
-
|
|
1694
|
+
const kind = this.catalog.find((entry) => entry.kind === box.kind);
|
|
1695
|
+
const shape = box.shape ?? kind?.shape ?? (box.frame ? "frame" : "task");
|
|
1696
|
+
element.dataset.shape = shape;
|
|
1697
|
+
element.dataset.kind = box.kind;
|
|
1698
|
+
element.dataset.tone = kind?.tone === "accent" ? "accent" : "neutral";
|
|
1699
|
+
const problem = this.allChecks.find(check => check.boxId === box.id || (check.path && JSON.stringify(check.path) === JSON.stringify(box.path)));
|
|
1700
|
+
const incoming = this.projection.lines.filter(line => line.to === box.id).length;
|
|
1701
|
+
const outgoing = this.projection.lines.filter(line => line.from === box.id).length;
|
|
1702
|
+
element.setAttribute("aria-label", [`${kind?.label ?? box.kind}: ${box.title}. ${incoming} in, ${outgoing} out`, problem ? `Needs attention: ${problem.message}` : ""]
|
|
1177
1703
|
.filter(Boolean)
|
|
1178
1704
|
.join(". "));
|
|
1179
1705
|
element.setAttribute("aria-current", String(this.selectedIds.has(box.id)));
|
|
1180
1706
|
this.place(element, this.layoutValue.boxes[box.id]);
|
|
1181
|
-
const
|
|
1707
|
+
const icon = document.createElement("span");
|
|
1708
|
+
icon.setAttribute("part", "icon");
|
|
1709
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1182
1710
|
if (kind?.icon) {
|
|
1183
|
-
const icon = document.createElement("span");
|
|
1184
|
-
icon.setAttribute("part", "icon");
|
|
1185
|
-
icon.setAttribute("aria-hidden", "true");
|
|
1186
1711
|
icon.append(kind.icon());
|
|
1187
|
-
element.append(icon);
|
|
1188
1712
|
}
|
|
1713
|
+
element.append(icon);
|
|
1189
1714
|
const title = document.createElement("strong");
|
|
1190
1715
|
title.textContent = box.title;
|
|
1191
1716
|
element.append(title);
|
|
1192
|
-
|
|
1717
|
+
const descriptionText = this.detailValue === "technical" ? box.technicalDescription ?? box.description : box.description;
|
|
1718
|
+
if (descriptionText) {
|
|
1193
1719
|
const description = document.createElement("small");
|
|
1194
|
-
description.textContent =
|
|
1720
|
+
description.textContent = descriptionText;
|
|
1721
|
+
if (this.detailValue === "technical")
|
|
1722
|
+
description.setAttribute("part", "technical-description");
|
|
1195
1723
|
element.append(description);
|
|
1196
1724
|
}
|
|
1725
|
+
if (box.frame && box.loopMark) {
|
|
1726
|
+
const loop = document.createElement('span');
|
|
1727
|
+
loop.setAttribute('part', 'loop-mark');
|
|
1728
|
+
loop.setAttribute('aria-hidden', 'true');
|
|
1729
|
+
loop.textContent = '↻';
|
|
1730
|
+
element.append(loop);
|
|
1731
|
+
}
|
|
1732
|
+
const metrics = this.showLastRunValue ? this.lastRunValue?.steps[box.id] : undefined;
|
|
1733
|
+
if (metrics) {
|
|
1734
|
+
const line = document.createElement("span");
|
|
1735
|
+
line.setAttribute("part", "metrics");
|
|
1736
|
+
line.textContent = metrics.notInRun ? "Not in this run" : [
|
|
1737
|
+
metrics.callsPerSecond === undefined ? "" : `${metrics.callsPerSecond.toLocaleString()}/s`,
|
|
1738
|
+
metrics.p95Ms === undefined ? "" : `p95 ${metrics.p95Ms.toLocaleString()} ms`,
|
|
1739
|
+
].filter(Boolean).join(" · ");
|
|
1740
|
+
if (metrics.failedShare !== undefined && metrics.failedShare > 0) {
|
|
1741
|
+
const failed = document.createElement('span');
|
|
1742
|
+
failed.setAttribute('part', 'metric-warning');
|
|
1743
|
+
failed.textContent = `${(metrics.failedShare * 100).toFixed(1)}% failed`;
|
|
1744
|
+
line.append(document.createTextNode(' · '), failed);
|
|
1745
|
+
}
|
|
1746
|
+
element.append(line);
|
|
1747
|
+
}
|
|
1748
|
+
if (problem) {
|
|
1749
|
+
const message = document.createElement("span");
|
|
1750
|
+
message.setAttribute("part", "problem");
|
|
1751
|
+
message.textContent = problem.message;
|
|
1752
|
+
element.append(message);
|
|
1753
|
+
}
|
|
1197
1754
|
element.addEventListener("keydown", event => {
|
|
1198
1755
|
if (event.target === element && (event.key === "Enter" || event.key === " ")) {
|
|
1199
1756
|
event.preventDefault();
|
|
@@ -1205,6 +1762,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
1205
1762
|
this.suppressClick = false;
|
|
1206
1763
|
return;
|
|
1207
1764
|
}
|
|
1765
|
+
if (this.pendingReattach) {
|
|
1766
|
+
const pending = this.pendingReattach;
|
|
1767
|
+
this.pendingReattach = undefined;
|
|
1768
|
+
this.requestEdit({ type: 'reattach', lineId: pending.lineId, [pending.end]: box.id });
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1208
1771
|
if (this.connecting && this.connecting !== box.id) {
|
|
1209
1772
|
this.requestEdit({
|
|
1210
1773
|
type: "connect",
|
|
@@ -1237,27 +1800,76 @@ export class ProcessModeler extends BaseElement {
|
|
|
1237
1800
|
element.append(port);
|
|
1238
1801
|
}
|
|
1239
1802
|
}
|
|
1803
|
+
if (box.frame && !this.locked) {
|
|
1804
|
+
const handle = document.createElement('button');
|
|
1805
|
+
handle.type = 'button';
|
|
1806
|
+
handle.setAttribute('part', 'frame-resize');
|
|
1807
|
+
handle.dataset.owner = box.id;
|
|
1808
|
+
handle.setAttribute('aria-label', `Resize ${box.title}`);
|
|
1809
|
+
handle.addEventListener('keydown', event => {
|
|
1810
|
+
const delta = { ArrowLeft: [-16, 0], ArrowRight: [16, 0], ArrowUp: [0, -16], ArrowDown: [0, 16] }[event.key];
|
|
1811
|
+
if (!delta)
|
|
1812
|
+
return;
|
|
1813
|
+
event.preventDefault();
|
|
1814
|
+
event.stopPropagation();
|
|
1815
|
+
const current = this.layoutValue.boxes[box.id];
|
|
1816
|
+
this.resize(box.id, (current.width ?? 224) + delta[0], (current.height ?? 64) + delta[1]);
|
|
1817
|
+
Array.from(this.shadowRoot.querySelectorAll('[part=frame-resize]')).find(handle => handle.dataset.owner === box.id)?.focus();
|
|
1818
|
+
});
|
|
1819
|
+
element.append(handle);
|
|
1820
|
+
}
|
|
1240
1821
|
world.append(element);
|
|
1241
1822
|
}
|
|
1242
1823
|
const lines = svgElement("svg");
|
|
1243
1824
|
lines.setAttribute("part", "lines");
|
|
1244
1825
|
lines.setAttribute("aria-hidden", "true");
|
|
1826
|
+
const defs = svgElement('defs');
|
|
1827
|
+
for (const [id, fill] of [['boe-process-arrow', 'var(--boe-token-text-text-secondary, #6f6f6f)'], ['boe-process-arrow-brand', 'var(--boe-token-surface-surface-brand, #0061d5)']]) {
|
|
1828
|
+
const marker = svgElement('marker');
|
|
1829
|
+
marker.id = id;
|
|
1830
|
+
marker.setAttribute('viewBox', '0 0 8 8');
|
|
1831
|
+
marker.setAttribute('refX', '7');
|
|
1832
|
+
marker.setAttribute('refY', '4');
|
|
1833
|
+
marker.setAttribute('markerWidth', '8');
|
|
1834
|
+
marker.setAttribute('markerHeight', '8');
|
|
1835
|
+
marker.setAttribute('orient', 'auto');
|
|
1836
|
+
const arrow = svgElement('path');
|
|
1837
|
+
arrow.setAttribute('d', 'M 0 0 L 8 4 L 0 8 Z');
|
|
1838
|
+
arrow.setAttribute('fill', fill);
|
|
1839
|
+
marker.append(arrow);
|
|
1840
|
+
defs.append(marker);
|
|
1841
|
+
}
|
|
1842
|
+
lines.append(defs);
|
|
1245
1843
|
world.append(lines);
|
|
1246
1844
|
for (const line of this.projection.lines) {
|
|
1247
1845
|
const points = this.routedLines.get(line.id);
|
|
1248
1846
|
if (!points.length)
|
|
1249
1847
|
continue;
|
|
1250
1848
|
const midpoint = lineMidpoint(points);
|
|
1251
|
-
const path = svgElement("
|
|
1849
|
+
const path = svgElement("path");
|
|
1252
1850
|
path.setAttribute("part", "line");
|
|
1253
1851
|
path.dataset.lineId = line.id;
|
|
1852
|
+
path.dataset.selected = String(this.selectedLineId === line.id);
|
|
1853
|
+
path.setAttribute('marker-end', `url(#${this.selectedLineId === line.id ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`);
|
|
1254
1854
|
path.setAttribute("vector-effect", "non-scaling-stroke");
|
|
1255
|
-
path.setAttribute("
|
|
1256
|
-
.map((point) => `${point.x},${point.y}`)
|
|
1257
|
-
.join(" "));
|
|
1855
|
+
path.setAttribute("d", roundedProcessPath(points));
|
|
1258
1856
|
if (line.dashed)
|
|
1259
1857
|
path.setAttribute("stroke-dasharray", "6 4");
|
|
1858
|
+
if (this.showLastRunValue && line.share !== undefined) {
|
|
1859
|
+
path.style.strokeWidth = `${1.5 + Math.max(0, Math.min(1, line.share)) * 2}px`;
|
|
1860
|
+
path.setAttribute('aria-label', `${Math.round(line.share * 100)}% of last-run traffic`);
|
|
1861
|
+
}
|
|
1260
1862
|
lines.append(path);
|
|
1863
|
+
for (const point of [line.fromSide ? points[0] : null, line.toSide ? points.at(-1) : null]) {
|
|
1864
|
+
if (!point)
|
|
1865
|
+
continue;
|
|
1866
|
+
const pin = svgElement('circle');
|
|
1867
|
+
pin.setAttribute('part', 'pinned-end');
|
|
1868
|
+
pin.setAttribute('cx', String(point.x));
|
|
1869
|
+
pin.setAttribute('cy', String(point.y));
|
|
1870
|
+
pin.setAttribute('r', '3');
|
|
1871
|
+
lines.append(pin);
|
|
1872
|
+
}
|
|
1261
1873
|
for (let i = 1; i < points.length - 2; i++) {
|
|
1262
1874
|
const hit = svgElement("polyline");
|
|
1263
1875
|
hit.setAttribute("part", "line-hit");
|
|
@@ -1269,9 +1881,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
1269
1881
|
}
|
|
1270
1882
|
const label = document.createElement("div");
|
|
1271
1883
|
label.setAttribute("part", "connection");
|
|
1884
|
+
label.setAttribute('role', 'group');
|
|
1885
|
+
label.tabIndex = 0;
|
|
1272
1886
|
label.style.left = `${midpoint.x}px`;
|
|
1273
1887
|
label.style.top = `${midpoint.y}px`;
|
|
1274
1888
|
const names = `${this.projection.boxes.find((box) => box.id === line.from).title} to ${this.projection.boxes.find((box) => box.id === line.to).title}${line.label ? `, ${line.label}` : ""}`;
|
|
1889
|
+
label.setAttribute('aria-label', `Connection: ${names}${this.showLastRunValue && line.share !== undefined ? `. ${Math.round(line.share * 100)}% of last-run traffic` : ''}`);
|
|
1890
|
+
label.addEventListener('click', event => { if (event.target.closest('button'))
|
|
1891
|
+
return; this.selectLine(line.id); });
|
|
1892
|
+
label.addEventListener('keydown', event => { if (event.target === label && (event.key === 'Enter' || event.key === ' ')) {
|
|
1893
|
+
event.preventDefault();
|
|
1894
|
+
this.selectLine(line.id);
|
|
1895
|
+
} });
|
|
1275
1896
|
const text = document.createElement("span");
|
|
1276
1897
|
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
1277
1898
|
const branchIndex = this.projection.lines.filter(candidate => candidate.from === line.from).indexOf(line);
|
|
@@ -1333,12 +1954,26 @@ export class ProcessModeler extends BaseElement {
|
|
|
1333
1954
|
actions.append(button);
|
|
1334
1955
|
}
|
|
1335
1956
|
world.append(label);
|
|
1957
|
+
if (this.selectedLineId === line.id && !this.locked && !this.disableConnections) {
|
|
1958
|
+
for (const [end, point] of [['from', points[0]], ['to', points.at(-1)]]) {
|
|
1959
|
+
const grip = document.createElement('button');
|
|
1960
|
+
grip.type = 'button';
|
|
1961
|
+
grip.setAttribute('part', 'end-grip');
|
|
1962
|
+
grip.dataset.lineId = line.id;
|
|
1963
|
+
grip.dataset.end = end;
|
|
1964
|
+
grip.style.left = `${point.x - 12}px`;
|
|
1965
|
+
grip.style.top = `${point.y - 12}px`;
|
|
1966
|
+
grip.setAttribute('aria-label', `Reattach ${end === 'from' ? 'start' : 'end'} of connection: ${names}`);
|
|
1967
|
+
grip.addEventListener('click', event => { event.stopPropagation(); this.pendingReattach = { lineId: line.id, end }; this.setStatus('Select a step to reattach the connection'); });
|
|
1968
|
+
world.append(grip);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1336
1971
|
}
|
|
1337
1972
|
for (const note of this.layoutValue.notes ?? []) {
|
|
1338
1973
|
const element = document.createElement("div");
|
|
1339
1974
|
element.setAttribute("part", "note");
|
|
1340
1975
|
element.textContent = note.text;
|
|
1341
|
-
this.place(element, { ...note, width:
|
|
1976
|
+
this.place(element, { ...note, width: 208, height: 80 });
|
|
1342
1977
|
world.append(element);
|
|
1343
1978
|
}
|
|
1344
1979
|
}
|
|
@@ -1346,8 +1981,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
1346
1981
|
Object.assign(element.style, {
|
|
1347
1982
|
left: `${position.x}px`,
|
|
1348
1983
|
top: `${position.y}px`,
|
|
1349
|
-
width: `${position.width ??
|
|
1350
|
-
height: `${position.height ??
|
|
1984
|
+
width: `${position.width ?? 224}px`,
|
|
1985
|
+
height: `${position.height ?? 64}px`,
|
|
1351
1986
|
});
|
|
1352
1987
|
}
|
|
1353
1988
|
renderPalette() {
|
|
@@ -1355,7 +1990,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1355
1990
|
const query = this.shadowRoot.querySelector("[part=search]").value.toLowerCase();
|
|
1356
1991
|
choices.replaceChildren();
|
|
1357
1992
|
let group = "";
|
|
1358
|
-
for (const kind of this.catalog.filter((kind) => `${kind.label} ${kind.description} ${kind.group ?? ""}`
|
|
1993
|
+
for (const kind of this.catalog.filter((kind) => `${kind.label} ${kind.description} ${kind.group ?? ""} ${(kind.aliases ?? []).join(" ")}`
|
|
1359
1994
|
.toLowerCase()
|
|
1360
1995
|
.includes(query))) {
|
|
1361
1996
|
if (kind.group && kind.group !== group) {
|
|
@@ -1367,28 +2002,69 @@ export class ProcessModeler extends BaseElement {
|
|
|
1367
2002
|
}
|
|
1368
2003
|
const button = document.createElement("button");
|
|
1369
2004
|
button.type = "button";
|
|
1370
|
-
button.
|
|
2005
|
+
button.setAttribute("part", "choice");
|
|
2006
|
+
button.dataset.tone = kind.tone === "accent" ? "accent" : "neutral";
|
|
2007
|
+
const icon = document.createElement("span");
|
|
2008
|
+
icon.setAttribute("part", "choice-icon");
|
|
2009
|
+
icon.setAttribute("aria-hidden", "true");
|
|
2010
|
+
if (kind.icon)
|
|
2011
|
+
icon.append(kind.icon());
|
|
2012
|
+
const name = document.createElement("span");
|
|
2013
|
+
name.setAttribute("part", "choice-name");
|
|
2014
|
+
name.textContent = kind.label;
|
|
2015
|
+
const description = document.createElement("span");
|
|
2016
|
+
description.setAttribute("part", "choice-description");
|
|
2017
|
+
description.textContent = kind.description;
|
|
2018
|
+
button.append(icon, name, description);
|
|
1371
2019
|
button.disabled = this.locked;
|
|
1372
2020
|
button.draggable = !this.locked;
|
|
1373
|
-
button.addEventListener("click", () =>
|
|
1374
|
-
|
|
2021
|
+
button.addEventListener("click", () => {
|
|
2022
|
+
if (kind.placement) {
|
|
2023
|
+
this.addLayoutKind(kind);
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const from = this.selectedId;
|
|
2027
|
+
const outgoing = from ? this.projection.lines.filter(line => line.from === from) : [];
|
|
2028
|
+
const line = outgoing.length === 1 ? outgoing[0] : undefined;
|
|
2029
|
+
this.requestEdit(line ? { type: "insert", kind, lineId: line.id, from: line.from, to: line.to } : { type: "add", kind, ...(from ? { from } : {}) });
|
|
2030
|
+
});
|
|
2031
|
+
button.addEventListener("dragstart", event => { this.paletteDragKind = kind; event.dataTransfer?.setData("application/boe-process-kind", kind.kind); });
|
|
2032
|
+
button.addEventListener("dragend", () => { this.paletteDragKind = null; this.shadowRoot?.querySelector('[part=palette-ghost]')?.remove(); this.markDropLine(undefined); });
|
|
1375
2033
|
choices.append(button);
|
|
1376
2034
|
}
|
|
1377
2035
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
1378
2036
|
canvas.ondragover = (event) => {
|
|
1379
2037
|
if (!this.locked) {
|
|
1380
2038
|
event.preventDefault();
|
|
1381
|
-
this.
|
|
2039
|
+
const point = this.canvasPoint(event);
|
|
2040
|
+
this.markDropLine(point);
|
|
2041
|
+
if (this.paletteDragKind) {
|
|
2042
|
+
let ghost = this.shadowRoot.querySelector('[part=palette-ghost]');
|
|
2043
|
+
if (!ghost) {
|
|
2044
|
+
ghost = document.createElement('div');
|
|
2045
|
+
ghost.setAttribute('part', 'palette-ghost');
|
|
2046
|
+
this.shadowRoot.querySelector('[part=world]').append(ghost);
|
|
2047
|
+
}
|
|
2048
|
+
ghost.textContent = this.paletteDragKind.label;
|
|
2049
|
+
this.place(ghost, { x: point.x - 112, y: point.y - 32, width: 224, height: 64 });
|
|
2050
|
+
}
|
|
1382
2051
|
}
|
|
1383
2052
|
};
|
|
1384
|
-
canvas.ondragleave = () => this.markDropLine(undefined);
|
|
2053
|
+
canvas.ondragleave = () => { this.markDropLine(undefined); this.shadowRoot.querySelector('[part=palette-ghost]')?.remove(); };
|
|
1385
2054
|
canvas.ondrop = (event) => {
|
|
1386
2055
|
event.preventDefault();
|
|
2056
|
+
this.shadowRoot.querySelector('[part=palette-ghost]')?.remove();
|
|
2057
|
+
this.paletteDragKind = null;
|
|
1387
2058
|
const kind = this.catalog.find((kind) => kind.kind ===
|
|
1388
2059
|
event.dataTransfer?.getData("application/boe-process-kind"));
|
|
1389
2060
|
if (!kind || this.locked)
|
|
1390
2061
|
return;
|
|
1391
2062
|
const position = this.canvasPoint(event);
|
|
2063
|
+
if (kind.placement) {
|
|
2064
|
+
this.addLayoutKind(kind, position);
|
|
2065
|
+
this.markDropLine(undefined);
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
1392
2068
|
const line = this.lineAt(position);
|
|
1393
2069
|
const frame = this.boxAt(position, undefined, true);
|
|
1394
2070
|
this.requestEdit({
|
|
@@ -1403,6 +2079,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
1403
2079
|
renderChecks() {
|
|
1404
2080
|
const checks = this.shadowRoot.querySelector("[part=checks]");
|
|
1405
2081
|
checks.replaceChildren();
|
|
2082
|
+
const hold = this.shadowRoot.querySelector('[part=hold]');
|
|
2083
|
+
hold.hidden = this.allChecks.length === 0;
|
|
2084
|
+
hold.querySelector('[part=hold-text]').textContent = `${this.allChecks.length} ${this.allChecks.length === 1 ? 'problem needs' : 'problems need'} fixing before this drawing can be read back.`;
|
|
1406
2085
|
this.allChecks.forEach((check) => {
|
|
1407
2086
|
const button = document.createElement("button");
|
|
1408
2087
|
button.type = "button";
|
|
@@ -1420,9 +2099,231 @@ export class ProcessModeler extends BaseElement {
|
|
|
1420
2099
|
checks.append(button);
|
|
1421
2100
|
});
|
|
1422
2101
|
}
|
|
2102
|
+
renderField(box, field) {
|
|
2103
|
+
const row = document.createElement('div');
|
|
2104
|
+
row.setAttribute('part', 'field');
|
|
2105
|
+
const label = document.createElement('label');
|
|
2106
|
+
label.textContent = field.label;
|
|
2107
|
+
let control;
|
|
2108
|
+
if (field.kind === 'multiline')
|
|
2109
|
+
control = document.createElement('textarea');
|
|
2110
|
+
else if (field.kind === 'choice') {
|
|
2111
|
+
const select = document.createElement('select');
|
|
2112
|
+
const groups = new Map();
|
|
2113
|
+
for (const option of field.options ?? []) {
|
|
2114
|
+
const item = document.createElement('option');
|
|
2115
|
+
item.value = option.value;
|
|
2116
|
+
item.textContent = option.label;
|
|
2117
|
+
if (option.group) {
|
|
2118
|
+
let group = groups.get(option.group);
|
|
2119
|
+
if (!group) {
|
|
2120
|
+
group = document.createElement('optgroup');
|
|
2121
|
+
group.label = option.group;
|
|
2122
|
+
groups.set(option.group, group);
|
|
2123
|
+
select.append(group);
|
|
2124
|
+
}
|
|
2125
|
+
group.append(item);
|
|
2126
|
+
}
|
|
2127
|
+
else
|
|
2128
|
+
select.append(item);
|
|
2129
|
+
}
|
|
2130
|
+
control = select;
|
|
2131
|
+
}
|
|
2132
|
+
else {
|
|
2133
|
+
const input = document.createElement('input');
|
|
2134
|
+
input.type = field.kind === 'boolean' ? 'checkbox' : field.kind === 'number' ? 'number' : field.kind === 'search' ? 'search' : 'text';
|
|
2135
|
+
control = input;
|
|
2136
|
+
}
|
|
2137
|
+
control.dataset.field = field.key;
|
|
2138
|
+
if (control instanceof HTMLInputElement && control.type === 'checkbox')
|
|
2139
|
+
control.checked = Boolean(field.value);
|
|
2140
|
+
else
|
|
2141
|
+
control.value = field.kind === 'action' ? field.options?.find(option => option.value === field.value)?.label ?? String(field.value) : String(field.value);
|
|
2142
|
+
control.disabled = Boolean(field.disabled || this.locked);
|
|
2143
|
+
control.required = Boolean(field.required);
|
|
2144
|
+
if (field.placeholder && control instanceof HTMLInputElement)
|
|
2145
|
+
control.placeholder = field.placeholder;
|
|
2146
|
+
if (field.problem)
|
|
2147
|
+
control.setAttribute('aria-invalid', 'true');
|
|
2148
|
+
const submit = () => emit(this, 'process-field-change-request', { boxId: box.id, path: box.path, key: field.key, value: control instanceof HTMLInputElement && control.type === 'checkbox' ? control.checked : control instanceof HTMLInputElement && control.type === 'number' && control.value !== '' ? Number(control.value) : control.value });
|
|
2149
|
+
if (field.kind !== 'action')
|
|
2150
|
+
control.addEventListener(field.kind === 'expression' ? 'input' : 'change', submit);
|
|
2151
|
+
label.append(control);
|
|
2152
|
+
row.append(label);
|
|
2153
|
+
if (field.kind === 'action' && control instanceof HTMLInputElement) {
|
|
2154
|
+
const input = control;
|
|
2155
|
+
const options = field.options ?? [];
|
|
2156
|
+
const list = document.createElement('div');
|
|
2157
|
+
list.setAttribute('part', 'action-options');
|
|
2158
|
+
list.id = `process-action-${box.id}-${field.key}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
2159
|
+
list.hidden = true;
|
|
2160
|
+
const optionList = document.createElement('div');
|
|
2161
|
+
optionList.setAttribute('role', 'listbox');
|
|
2162
|
+
optionList.id = `${list.id}-choices`;
|
|
2163
|
+
optionList.setAttribute('aria-label', `${field.label} choices`);
|
|
2164
|
+
input.setAttribute('role', 'combobox');
|
|
2165
|
+
input.setAttribute('aria-autocomplete', 'list');
|
|
2166
|
+
input.setAttribute('aria-controls', optionList.id);
|
|
2167
|
+
input.setAttribute('aria-expanded', 'false');
|
|
2168
|
+
input.autocomplete = 'off';
|
|
2169
|
+
let group = null;
|
|
2170
|
+
let active = 0;
|
|
2171
|
+
let browse = false;
|
|
2172
|
+
const close = () => { list.hidden = true; browse = false; input.setAttribute('aria-expanded', 'false'); input.removeAttribute('aria-activedescendant'); };
|
|
2173
|
+
const choose = (option) => { input.focus(); input.value = option.value; close(); submit(); };
|
|
2174
|
+
const render = () => {
|
|
2175
|
+
list.replaceChildren();
|
|
2176
|
+
optionList.replaceChildren();
|
|
2177
|
+
const query = browse ? '' : input.value.trim().toLocaleLowerCase();
|
|
2178
|
+
const matches = options.filter(option => (!group || option.group === group) && (!query || `${option.label} ${option.value} ${option.group ?? ''}`.toLocaleLowerCase().includes(query)));
|
|
2179
|
+
if (!group && !query) {
|
|
2180
|
+
const groups = [...new Set(options.map(option => option.group).filter((name) => Boolean(name)))];
|
|
2181
|
+
for (const name of groups) {
|
|
2182
|
+
const button = document.createElement('button');
|
|
2183
|
+
button.type = 'button';
|
|
2184
|
+
button.setAttribute('part', 'action-group');
|
|
2185
|
+
button.textContent = name;
|
|
2186
|
+
button.onclick = () => { input.focus(); group = name; browse = true; render(); };
|
|
2187
|
+
list.append(button);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
if (group) {
|
|
2191
|
+
const back = document.createElement('button');
|
|
2192
|
+
back.type = 'button';
|
|
2193
|
+
back.setAttribute('part', 'action-back');
|
|
2194
|
+
back.textContent = `All actions / ${group}`;
|
|
2195
|
+
back.onclick = () => { input.focus(); group = null; browse = true; render(); };
|
|
2196
|
+
list.prepend(back);
|
|
2197
|
+
}
|
|
2198
|
+
for (const [index, option] of matches.entries()) {
|
|
2199
|
+
const item = document.createElement('button');
|
|
2200
|
+
item.type = 'button';
|
|
2201
|
+
item.setAttribute('role', 'option');
|
|
2202
|
+
item.setAttribute('part', 'action-option');
|
|
2203
|
+
item.id = `${list.id}-${index}`;
|
|
2204
|
+
item.setAttribute('aria-selected', String(index === active));
|
|
2205
|
+
const title = document.createElement('strong');
|
|
2206
|
+
title.textContent = option.label;
|
|
2207
|
+
item.append(title);
|
|
2208
|
+
if (option.value !== option.label) {
|
|
2209
|
+
const code = document.createElement('small');
|
|
2210
|
+
code.textContent = option.value;
|
|
2211
|
+
item.append(code);
|
|
2212
|
+
}
|
|
2213
|
+
item.onclick = () => choose(option);
|
|
2214
|
+
optionList.append(item);
|
|
2215
|
+
}
|
|
2216
|
+
if (!matches.length) {
|
|
2217
|
+
const empty = document.createElement('p');
|
|
2218
|
+
empty.textContent = 'No matching actions';
|
|
2219
|
+
list.append(empty);
|
|
2220
|
+
}
|
|
2221
|
+
list.append(optionList);
|
|
2222
|
+
active = Math.min(active, Math.max(0, matches.length - 1));
|
|
2223
|
+
list.hidden = false;
|
|
2224
|
+
input.setAttribute('aria-expanded', 'true');
|
|
2225
|
+
const selected = list.querySelectorAll('[role=option]')[active];
|
|
2226
|
+
if (selected)
|
|
2227
|
+
input.setAttribute('aria-activedescendant', selected.id);
|
|
2228
|
+
else
|
|
2229
|
+
input.removeAttribute('aria-activedescendant');
|
|
2230
|
+
};
|
|
2231
|
+
input.addEventListener('focus', () => { group = null; browse = false; render(); });
|
|
2232
|
+
input.addEventListener('input', () => { group = null; browse = false; active = 0; render(); });
|
|
2233
|
+
input.addEventListener('keydown', event => {
|
|
2234
|
+
if (event.key === 'Escape') {
|
|
2235
|
+
event.preventDefault();
|
|
2236
|
+
close();
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
2240
|
+
event.preventDefault();
|
|
2241
|
+
if (list.hidden)
|
|
2242
|
+
render();
|
|
2243
|
+
const items = Array.from(list.querySelectorAll('[role=option]'));
|
|
2244
|
+
if (!items.length)
|
|
2245
|
+
return;
|
|
2246
|
+
active = (active + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
2247
|
+
items.forEach((item, index) => item.setAttribute('aria-selected', String(index === active)));
|
|
2248
|
+
input.setAttribute('aria-activedescendant', items[active].id);
|
|
2249
|
+
items[active].scrollIntoView?.({ block: 'nearest' });
|
|
2250
|
+
}
|
|
2251
|
+
else if (event.key === 'Enter' && !list.hidden) {
|
|
2252
|
+
const items = Array.from(list.querySelectorAll('[role=option]'));
|
|
2253
|
+
if (items[active]) {
|
|
2254
|
+
event.preventDefault();
|
|
2255
|
+
items[active].click();
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
});
|
|
2259
|
+
const caret = document.createElement('button');
|
|
2260
|
+
caret.type = 'button';
|
|
2261
|
+
caret.setAttribute('part', 'action-caret');
|
|
2262
|
+
caret.setAttribute('aria-label', `Browse ${field.label.toLowerCase()} choices`);
|
|
2263
|
+
caret.textContent = '⌄';
|
|
2264
|
+
caret.disabled = input.disabled;
|
|
2265
|
+
caret.onclick = () => { input.focus(); group = null; browse = true; active = 0; render(); };
|
|
2266
|
+
row.addEventListener('focusout', event => { if (!row.contains(event.relatedTarget))
|
|
2267
|
+
close(); });
|
|
2268
|
+
row.append(caret, list);
|
|
2269
|
+
}
|
|
2270
|
+
if (field.kind === 'search' && field.options?.length && control instanceof HTMLInputElement) {
|
|
2271
|
+
const options = document.createElement('datalist');
|
|
2272
|
+
options.id = `process-field-${box.id}-${field.key}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
2273
|
+
for (const option of field.options) {
|
|
2274
|
+
const item = document.createElement('option');
|
|
2275
|
+
item.value = option.value;
|
|
2276
|
+
item.label = option.label;
|
|
2277
|
+
options.append(item);
|
|
2278
|
+
}
|
|
2279
|
+
control.setAttribute('list', options.id);
|
|
2280
|
+
row.append(options);
|
|
2281
|
+
}
|
|
2282
|
+
if (field.kind === 'expression' && this.variables.length) {
|
|
2283
|
+
const chips = document.createElement('div');
|
|
2284
|
+
chips.setAttribute('part', 'variable-chips');
|
|
2285
|
+
chips.setAttribute('aria-label', 'Variables in scope');
|
|
2286
|
+
for (const variable of this.variables) {
|
|
2287
|
+
const chip = document.createElement('button');
|
|
2288
|
+
chip.type = 'button';
|
|
2289
|
+
chip.textContent = variable.name;
|
|
2290
|
+
chip.disabled = control.disabled;
|
|
2291
|
+
chip.onclick = () => { control.value += variable.name; submit(); control.focus(); };
|
|
2292
|
+
chips.append(chip);
|
|
2293
|
+
}
|
|
2294
|
+
row.append(chips);
|
|
2295
|
+
}
|
|
2296
|
+
if (field.description) {
|
|
2297
|
+
const help = document.createElement('small');
|
|
2298
|
+
help.textContent = field.description;
|
|
2299
|
+
row.append(help);
|
|
2300
|
+
}
|
|
2301
|
+
if (field.problem) {
|
|
2302
|
+
const problem = document.createElement('small');
|
|
2303
|
+
problem.setAttribute('part', 'field-problem');
|
|
2304
|
+
problem.textContent = field.problem;
|
|
2305
|
+
row.append(problem);
|
|
2306
|
+
}
|
|
2307
|
+
return row;
|
|
2308
|
+
}
|
|
1423
2309
|
renderSelection() {
|
|
1424
2310
|
if (!this.isRendered)
|
|
1425
2311
|
return;
|
|
2312
|
+
const focusedControl = this.shadowRoot.activeElement;
|
|
2313
|
+
const focusedKey = focusedControl?.dataset.field ?? focusedControl?.dataset.variable;
|
|
2314
|
+
const focusedType = focusedControl?.dataset.field ? 'field' : focusedControl?.dataset.variable ? 'variable' : null;
|
|
2315
|
+
const caret = focusedControl && 'selectionStart' in focusedControl ? focusedControl.selectionStart : null;
|
|
2316
|
+
const restoreControlFocus = () => {
|
|
2317
|
+
if (!focusedKey || !focusedType)
|
|
2318
|
+
return;
|
|
2319
|
+
const control = Array.from(this.shadowRoot.querySelectorAll('input,textarea')).find(item => item.dataset[focusedType] === focusedKey);
|
|
2320
|
+
if (!control)
|
|
2321
|
+
return;
|
|
2322
|
+
control.focus();
|
|
2323
|
+
if (caret !== null && control.type !== 'number' && control.type !== 'checkbox')
|
|
2324
|
+
control.setSelectionRange(caret, caret);
|
|
2325
|
+
};
|
|
2326
|
+
this.shadowRoot.querySelectorAll('[part=line]').forEach(line => { const selected = line.dataset.lineId === this.selectedLineId; line.dataset.selected = String(selected); line.setAttribute('marker-end', `url(#${selected ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`); });
|
|
1426
2327
|
this.shadowRoot.querySelectorAll("[data-box-id]").forEach((element) => {
|
|
1427
2328
|
element.setAttribute("aria-current", String(this.selectedIds.has(element.dataset.boxId)));
|
|
1428
2329
|
const box = this.projection.boxes.find((box) => box.id === element.dataset.boxId);
|
|
@@ -1432,14 +2333,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
1432
2333
|
});
|
|
1433
2334
|
const editor = this.shadowRoot.querySelector("[part=editor]");
|
|
1434
2335
|
const selected = this.selected;
|
|
2336
|
+
const controlsKey = `${[...this.selectedIds].join(',')}|${this.locked}|${this.disableConnections}|${this.showLastRunValue}|${JSON.stringify(selected ? this.fieldsValue[selected.id] ?? [] : [])}`;
|
|
1435
2337
|
this.shadowRoot.querySelector("[part=palette]").hidden = false;
|
|
1436
|
-
this.shadowRoot.querySelector('[part=selection-toolbar]').hidden = this.selectedIds.size === 0;
|
|
1437
|
-
this.
|
|
2338
|
+
this.shadowRoot.querySelector('[part=selection-toolbar]').hidden = this.selectedIds.size === 0 && !this.selectedLineId;
|
|
2339
|
+
this.renderSelectionToolbar();
|
|
1438
2340
|
this.renderPane();
|
|
2341
|
+
this.positionSelectionToolbar();
|
|
1439
2342
|
if (this.inspected === selected?.node &&
|
|
1440
2343
|
this.inspectedId === selected?.id &&
|
|
2344
|
+
this.inspectedControls === controlsKey &&
|
|
1441
2345
|
editor.querySelector(`[part=inspector-heading]`)?.tagName === `H${this.headingLevel}`) {
|
|
1442
2346
|
editor.querySelector("[part=inspector-heading]").textContent = selected?.title ?? "";
|
|
2347
|
+
restoreControlFocus();
|
|
1443
2348
|
return;
|
|
1444
2349
|
}
|
|
1445
2350
|
this.cleanupInspector?.();
|
|
@@ -1447,13 +2352,170 @@ export class ProcessModeler extends BaseElement {
|
|
|
1447
2352
|
editor.replaceChildren();
|
|
1448
2353
|
this.inspected = selected?.node;
|
|
1449
2354
|
this.inspectedId = selected?.id;
|
|
2355
|
+
this.inspectedControls = controlsKey;
|
|
2356
|
+
if (this.selectedIds.size > 1) {
|
|
2357
|
+
const heading = document.createElement(`h${this.headingLevel}`);
|
|
2358
|
+
heading.setAttribute('part', 'inspector-heading');
|
|
2359
|
+
heading.textContent = `${this.selectedIds.size} steps selected`;
|
|
2360
|
+
editor.append(heading);
|
|
2361
|
+
const actions = document.createElement('div');
|
|
2362
|
+
actions.setAttribute('part', 'arrange-actions');
|
|
2363
|
+
for (const [label, action] of [['Align left', 'left'], ['Align centre', 'center'], ['Align right', 'right'], ['Align top', 'top'], ['Align middle', 'middle'], ['Align bottom', 'bottom'], ['Distribute horizontally', 'distribute-horizontal'], ['Distribute vertically', 'distribute-vertical']]) {
|
|
2364
|
+
const button = document.createElement('button');
|
|
2365
|
+
button.type = 'button';
|
|
2366
|
+
button.textContent = label;
|
|
2367
|
+
button.disabled = this.locked;
|
|
2368
|
+
button.onclick = () => this.arrangeSelection(action);
|
|
2369
|
+
actions.append(button);
|
|
2370
|
+
}
|
|
2371
|
+
const section = document.createElement('button');
|
|
2372
|
+
section.type = 'button';
|
|
2373
|
+
section.textContent = 'Make a section';
|
|
2374
|
+
section.disabled = this.locked;
|
|
2375
|
+
section.onclick = () => this.makeSection();
|
|
2376
|
+
actions.append(section);
|
|
2377
|
+
editor.append(actions);
|
|
2378
|
+
restoreControlFocus();
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
1450
2381
|
if (selected) {
|
|
1451
2382
|
const heading = document.createElement(`h${this.headingLevel}`);
|
|
1452
2383
|
heading.setAttribute("part", "inspector-heading");
|
|
2384
|
+
heading.dataset.single = 'true';
|
|
1453
2385
|
heading.textContent = selected.title;
|
|
1454
2386
|
editor.append(heading);
|
|
2387
|
+
const kind = this.catalog.find(entry => entry.kind === selected.kind);
|
|
2388
|
+
const kindLabel = document.createElement('p');
|
|
2389
|
+
kindLabel.setAttribute('part', 'inspector-kind');
|
|
2390
|
+
kindLabel.textContent = kind?.label ?? selected.kind;
|
|
2391
|
+
editor.append(kindLabel);
|
|
2392
|
+
if (selected.description) {
|
|
2393
|
+
const purpose = document.createElement('p');
|
|
2394
|
+
purpose.setAttribute('part', 'inspector-purpose');
|
|
2395
|
+
purpose.textContent = selected.description;
|
|
2396
|
+
editor.append(purpose);
|
|
2397
|
+
}
|
|
2398
|
+
for (const field of this.fieldsValue[selected.id] ?? [])
|
|
2399
|
+
editor.append(this.renderField(selected, field));
|
|
1455
2400
|
this.cleanupInspector =
|
|
1456
2401
|
this.renderer?.(selected.node, editor) || undefined;
|
|
2402
|
+
const outgoing = this.projection.lines.filter(line => line.from === selected.id);
|
|
2403
|
+
const leads = document.createElement('section');
|
|
2404
|
+
leads.setAttribute('part', 'leads-to');
|
|
2405
|
+
const leadsHeading = document.createElement('h3');
|
|
2406
|
+
leadsHeading.textContent = 'Leads to';
|
|
2407
|
+
leads.append(leadsHeading);
|
|
2408
|
+
for (const line of outgoing) {
|
|
2409
|
+
const row = document.createElement('div');
|
|
2410
|
+
row.setAttribute('part', 'lead-row');
|
|
2411
|
+
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
2412
|
+
const name = document.createElement('span');
|
|
2413
|
+
name.textContent = `${line.label ? `${line.label}: ` : ''}${target?.title ?? line.to}`;
|
|
2414
|
+
row.append(name);
|
|
2415
|
+
if (!this.disableConnections) {
|
|
2416
|
+
const remove = document.createElement('button');
|
|
2417
|
+
remove.type = 'button';
|
|
2418
|
+
remove.textContent = 'Remove';
|
|
2419
|
+
remove.setAttribute('aria-label', `Remove connection to ${target?.title ?? line.to}`);
|
|
2420
|
+
remove.disabled = this.locked;
|
|
2421
|
+
remove.onclick = () => this.requestEdit({ type: 'disconnect', lineId: line.id });
|
|
2422
|
+
row.append(remove);
|
|
2423
|
+
}
|
|
2424
|
+
leads.append(row);
|
|
2425
|
+
}
|
|
2426
|
+
const connect = document.createElement('button');
|
|
2427
|
+
connect.type = 'button';
|
|
2428
|
+
connect.textContent = 'Connect to…';
|
|
2429
|
+
connect.disabled = this.locked || this.disableConnections;
|
|
2430
|
+
connect.onclick = () => { this.connecting = selected.id; this.setStatus(`Select the next box to connect from ${selected.title}`); };
|
|
2431
|
+
leads.append(connect);
|
|
2432
|
+
editor.append(leads);
|
|
2433
|
+
const metrics = this.showLastRunValue ? this.lastRunValue?.steps[selected.id] : undefined;
|
|
2434
|
+
if (metrics) {
|
|
2435
|
+
const report = document.createElement('section');
|
|
2436
|
+
report.setAttribute('part', 'inspector-metrics');
|
|
2437
|
+
const title = document.createElement('h3');
|
|
2438
|
+
title.textContent = this.lastRunValue?.label ?? 'Last run';
|
|
2439
|
+
report.append(title);
|
|
2440
|
+
if (metrics.notInRun) {
|
|
2441
|
+
const note = document.createElement('p');
|
|
2442
|
+
note.textContent = 'Not in this run';
|
|
2443
|
+
report.append(note);
|
|
2444
|
+
}
|
|
2445
|
+
else {
|
|
2446
|
+
const list = document.createElement('dl');
|
|
2447
|
+
for (const [label, value] of [['Per second', metrics.callsPerSecond?.toLocaleString()], ['95% within', metrics.p95Ms === undefined ? undefined : `${metrics.p95Ms.toLocaleString()} ms`], ['Failed', metrics.failedShare === undefined ? undefined : `${(metrics.failedShare * 100).toFixed(1)}%`]]) {
|
|
2448
|
+
if (value === undefined)
|
|
2449
|
+
continue;
|
|
2450
|
+
const term = document.createElement('dt');
|
|
2451
|
+
term.textContent = label;
|
|
2452
|
+
const detail = document.createElement('dd');
|
|
2453
|
+
detail.textContent = value;
|
|
2454
|
+
if (label === 'Failed' && metrics.failedShare)
|
|
2455
|
+
detail.setAttribute('part', 'metric-warning');
|
|
2456
|
+
list.append(term, detail);
|
|
2457
|
+
}
|
|
2458
|
+
report.append(list);
|
|
2459
|
+
}
|
|
2460
|
+
editor.append(report);
|
|
2461
|
+
}
|
|
2462
|
+
const actions = document.createElement('div');
|
|
2463
|
+
actions.setAttribute('part', 'inspector-actions');
|
|
2464
|
+
const duplicate = document.createElement('button');
|
|
2465
|
+
duplicate.type = 'button';
|
|
2466
|
+
duplicate.textContent = 'Duplicate';
|
|
2467
|
+
duplicate.disabled = this.locked;
|
|
2468
|
+
duplicate.onclick = () => this.requestEdit({ type: 'duplicate', sourceId: selected.id, position: { x: this.layoutValue.boxes[selected.id].x + 32, y: this.layoutValue.boxes[selected.id].y + 32 } });
|
|
2469
|
+
const remove = document.createElement('button');
|
|
2470
|
+
remove.type = 'button';
|
|
2471
|
+
remove.textContent = 'Delete';
|
|
2472
|
+
remove.disabled = this.locked;
|
|
2473
|
+
remove.onclick = () => this.requestEdit({ type: 'delete', boxId: selected.id });
|
|
2474
|
+
actions.append(duplicate, remove);
|
|
2475
|
+
editor.append(actions);
|
|
2476
|
+
}
|
|
2477
|
+
restoreControlFocus();
|
|
2478
|
+
}
|
|
2479
|
+
renderSelectionToolbar() {
|
|
2480
|
+
const toolbar = this.shadowRoot.querySelector('[part=selection-toolbar]');
|
|
2481
|
+
const key = `${[...this.selectedIds].join(',')}|${this.selectedLineId}|${Boolean(this.selectedLineId && this.layoutValue.lines?.[this.selectedLineId])}|${this.locked}|${this.narrowValue}|${this.projection.lines.filter(line => line.from === this.selectedId).map(line => `${line.id}:${line.label}:${line.dashed}`).join(',')}`;
|
|
2482
|
+
if (key === this.selectionToolbarKey)
|
|
2483
|
+
return;
|
|
2484
|
+
this.selectionToolbarKey = key;
|
|
2485
|
+
toolbar.replaceChildren();
|
|
2486
|
+
if (!this.selectedIds.size && !this.selectedLineId)
|
|
2487
|
+
return;
|
|
2488
|
+
const add = (label, command, action) => {
|
|
2489
|
+
const button = document.createElement('button');
|
|
2490
|
+
button.type = 'button';
|
|
2491
|
+
button.dataset.selectionCommand = command;
|
|
2492
|
+
button.textContent = label;
|
|
2493
|
+
button.setAttribute('aria-label', label);
|
|
2494
|
+
button.disabled = this.locked;
|
|
2495
|
+
button.onclick = action;
|
|
2496
|
+
toolbar.append(button);
|
|
2497
|
+
};
|
|
2498
|
+
if (this.selectedLine) {
|
|
2499
|
+
const line = this.selectedLine;
|
|
2500
|
+
add('Insert a step', 'insert', () => this.openChooser({ type: 'insert', lineId: line.id, from: line.from, to: line.to }));
|
|
2501
|
+
if (this.layoutValue.lines?.[line.id])
|
|
2502
|
+
add('Reset line', 'reset-line', () => this.resetLine(line.id));
|
|
2503
|
+
if (!this.disableConnections)
|
|
2504
|
+
add('Delete', 'delete-line', () => this.requestEdit({ type: 'disconnect', lineId: line.id }));
|
|
2505
|
+
}
|
|
2506
|
+
else if (this.selectedIds.size === 1) {
|
|
2507
|
+
const box = this.selected;
|
|
2508
|
+
const failurePath = box.kind === 'try' && !this.projection.lines.some(line => line.from === box.id && (line.dashed || line.label === 'If it fails'));
|
|
2509
|
+
add(box.shape === 'gateway' ? 'Add a path' : failurePath ? 'If it fails' : 'Add next', 'add-next', () => this.openChooser({ type: 'add', from: box.id, ...(failurePath ? { routeLabel: 'If it fails', dashed: true } : {}) }));
|
|
2510
|
+
add('Duplicate', 'duplicate', () => { const position = this.layoutValue.boxes[box.id]; this.requestEdit({ type: 'duplicate', sourceId: box.id, position: { x: position.x + 32, y: position.y + 32 } }); });
|
|
2511
|
+
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
2512
|
+
if (this.narrowValue)
|
|
2513
|
+
add('Details', 'details', () => promoteModal(this.shadowRoot.querySelector('[part=pane-drawer][data-pane=inspector]')));
|
|
2514
|
+
}
|
|
2515
|
+
else {
|
|
2516
|
+
add('Line up', 'align', () => this.selectionCommand('align'));
|
|
2517
|
+
add('Tidy these', 'space', () => this.selectionCommand('space'));
|
|
2518
|
+
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
1457
2519
|
}
|
|
1458
2520
|
}
|
|
1459
2521
|
updateToolbar() {
|
|
@@ -1461,15 +2523,23 @@ export class ProcessModeler extends BaseElement {
|
|
|
1461
2523
|
button("undo").disabled = !this.history.canUndo || this.locked;
|
|
1462
2524
|
button("redo").disabled = !this.history.canRedo || this.locked;
|
|
1463
2525
|
button("tidy").disabled = this.locked;
|
|
1464
|
-
button("connect").disabled = !this.selected || this.locked || this.disableConnections;
|
|
1465
|
-
button("delete").disabled = !this.selected || this.locked;
|
|
1466
2526
|
button("lock").setAttribute("aria-pressed", String(this.locked));
|
|
1467
2527
|
button("snap").setAttribute("aria-pressed", String(this.snapToGrid));
|
|
2528
|
+
this.shadowRoot.querySelectorAll('[data-detail]').forEach(control => {
|
|
2529
|
+
control.setAttribute("aria-pressed", String(control.dataset.detail === this.detailValue));
|
|
2530
|
+
});
|
|
2531
|
+
const runToggle = this.shadowRoot.querySelector('[part=run-toggle]');
|
|
2532
|
+
runToggle.hidden = !this.lastRunValue;
|
|
2533
|
+
runToggle.querySelector('input').checked = this.showLastRunValue;
|
|
2534
|
+
const mobileRun = this.shadowRoot.querySelector('[data-view-option=last-run]');
|
|
2535
|
+
mobileRun.hidden = !this.lastRunValue;
|
|
2536
|
+
mobileRun.setAttribute('aria-pressed', String(this.showLastRunValue));
|
|
1468
2537
|
}
|
|
1469
2538
|
paintViewport() {
|
|
1470
2539
|
this.shadowRoot.querySelector("[part=world]").style.transform =
|
|
1471
2540
|
`translate(${this.viewport.x}px,${this.viewport.y}px) scale(${this.viewport.zoom})`;
|
|
1472
2541
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => port.style.transform = `scale(${1 / this.viewport.zoom})`);
|
|
2542
|
+
this.positionSelectionToolbar();
|
|
1473
2543
|
this.shadowRoot.querySelector("[data-command=reset]").textContent =
|
|
1474
2544
|
`${Math.round(this.viewport.zoom * 100)}%`;
|
|
1475
2545
|
const minimap = this.shadowRoot.querySelector("[part=minimap]");
|
|
@@ -1482,8 +2552,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
1482
2552
|
const rect = svgElement("rect");
|
|
1483
2553
|
rect.setAttribute("x", String(pos.x));
|
|
1484
2554
|
rect.setAttribute("y", String(pos.y));
|
|
1485
|
-
rect.setAttribute("width", String(pos.width ??
|
|
1486
|
-
rect.setAttribute("height", String(pos.height ??
|
|
2555
|
+
rect.setAttribute("width", String(pos.width ?? 224));
|
|
2556
|
+
rect.setAttribute("height", String(pos.height ?? 64));
|
|
1487
2557
|
minimap.append(rect);
|
|
1488
2558
|
});
|
|
1489
2559
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
@@ -1495,6 +2565,26 @@ export class ProcessModeler extends BaseElement {
|
|
|
1495
2565
|
viewport.setAttribute("height", String(canvas.clientHeight / this.viewport.zoom));
|
|
1496
2566
|
minimap.append(viewport);
|
|
1497
2567
|
}
|
|
2568
|
+
positionSelectionToolbar() {
|
|
2569
|
+
if (!this.isRendered || (!this.selectedIds.size && !this.selectedLineId))
|
|
2570
|
+
return;
|
|
2571
|
+
const canvas = this.shadowRoot.querySelector('[part=canvas]');
|
|
2572
|
+
const toolbar = this.shadowRoot.querySelector('[part=selection-toolbar]');
|
|
2573
|
+
const positions = [...this.selectedIds].map(id => this.layoutValue.boxes[id]).filter(Boolean);
|
|
2574
|
+
if (!positions.length && this.selectedLine) {
|
|
2575
|
+
const point = lineMidpoint(this.routedLines.get(this.selectedLine.id) ?? []);
|
|
2576
|
+
toolbar.style.left = `${Math.max(8, Math.min(point.x * this.viewport.zoom + this.viewport.x, Math.max(8, canvas.clientWidth - toolbar.offsetWidth - 8)))}px`;
|
|
2577
|
+
toolbar.style.top = `${Math.max(8, point.y * this.viewport.zoom + this.viewport.y - 52)}px`;
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
if (!positions.length)
|
|
2581
|
+
return;
|
|
2582
|
+
const left = Math.min(...positions.map(position => position.x * this.viewport.zoom + this.viewport.x));
|
|
2583
|
+
const top = Math.min(...positions.map(position => position.y * this.viewport.zoom + this.viewport.y));
|
|
2584
|
+
const bottom = Math.max(...positions.map(position => (position.y + (position.height ?? 64)) * this.viewport.zoom + this.viewport.y));
|
|
2585
|
+
toolbar.style.left = `${Math.max(8, Math.min(left, Math.max(8, canvas.clientWidth - toolbar.offsetWidth - 8)))}px`;
|
|
2586
|
+
toolbar.style.top = `${top > 56 ? top - 48 : Math.min(bottom + 8, Math.max(8, canvas.clientHeight - toolbar.offsetHeight - 8))}px`;
|
|
2587
|
+
}
|
|
1498
2588
|
focusBox(id) {
|
|
1499
2589
|
Array.from(this.shadowRoot.querySelectorAll("[data-box-id]"))
|
|
1500
2590
|
.find((box) => box.dataset.boxId === id)
|