@unofficialbox/box-open-elements 0.24.0 → 0.25.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/components/actions/button.d.ts +1 -0
- package/dist/components/actions/button.js +13 -0
- package/dist/components/forms/checkbox.d.ts +1 -0
- package/dist/components/forms/checkbox.js +2 -0
- package/dist/components/forms/code-editor.d.ts +28 -3
- package/dist/components/forms/code-editor.js +71 -6
- package/dist/components/forms/combobox.js +7 -8
- package/dist/components/forms/number-input.d.ts +1 -0
- package/dist/components/forms/number-input.js +4 -1
- package/dist/components/forms/select.d.ts +1 -0
- package/dist/components/forms/select.js +3 -1
- package/dist/components/forms/text-area.js +5 -7
- package/dist/components/forms/text-field.d.ts +1 -0
- package/dist/components/forms/text-field.js +2 -0
- package/dist/components/navigation/accordion.d.ts +7 -0
- package/dist/components/navigation/accordion.js +31 -8
- package/dist/components/output/code-block.js +3 -1
- package/dist/components/overlays/drawer.js +2 -2
- package/dist/foundations/geometry/tokens.d.ts +2 -2
- package/dist/foundations/geometry/tokens.js +3 -3
- package/dist/foundations/icons/box-iconography.generated.d.ts +472 -1
- package/dist/foundations/icons/box-iconography.generated.js +473 -2
- package/dist/patterns/flow-builder/flow-builder.d.ts +5 -1
- package/dist/patterns/flow-builder/flow-builder.js +18 -7
- package/dist/patterns/flow-builder/primitives.js +1 -1
- package/dist/patterns/process-modeler/bridge.d.ts +10 -0
- package/dist/patterns/process-modeler/bridge.js +173 -0
- package/dist/patterns/process-modeler/geometry.d.ts +18 -0
- package/dist/patterns/process-modeler/geometry.js +307 -0
- package/dist/patterns/process-modeler/index.d.ts +2 -0
- package/dist/patterns/process-modeler/index.js +2 -0
- package/dist/patterns/process-modeler/model.d.ts +41 -1
- package/dist/patterns/process-modeler/process-modeler.d.ts +48 -0
- package/dist/patterns/process-modeler/process-modeler.js +709 -63
- package/package.json +4 -3
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
2
|
import { announce } from "../../foundations/a11y/index.js";
|
|
3
3
|
import { boeFocusVisibleStyles } from "../../foundations/tokens/interaction.js";
|
|
4
|
+
import { dismissModal, promoteModal } from "../../foundations/overlay/index.js";
|
|
5
|
+
import { KindPicker } from "../flow-builder/primitives.js";
|
|
6
|
+
import { arrangeProcess, routeProcessLine, lineMidpoint, nearProcessLine } from "./geometry.js";
|
|
7
|
+
import { restoreProcessPositions, snapshotProcessPositions, graphChecks, normalizeProcessSelectionPath } from "./bridge.js";
|
|
4
8
|
import { completeLayout, ProcessHistory, validateProjection, } from "./model.js";
|
|
5
9
|
const svgElement = (tag) => document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
6
10
|
const emit = (element, name, detail) => element.dispatchEvent(new CustomEvent(name, {
|
|
@@ -13,7 +17,7 @@ const emit = (element, name, detail) => element.dispatchEvent(new CustomEvent(na
|
|
|
13
17
|
export class ProcessModeler extends BaseElement {
|
|
14
18
|
static tagName = "box-process-modeler";
|
|
15
19
|
static get observedAttributes() {
|
|
16
|
-
return ["locked", "snap-to-grid"];
|
|
20
|
+
return ["locked", "snap-to-grid", "heading-level", "disable-connections"];
|
|
17
21
|
}
|
|
18
22
|
documentValue;
|
|
19
23
|
modelValue = {
|
|
@@ -35,6 +39,78 @@ export class ProcessModeler extends BaseElement {
|
|
|
35
39
|
drag;
|
|
36
40
|
pinchDistance = 0;
|
|
37
41
|
gestureZoom = 1;
|
|
42
|
+
insertion;
|
|
43
|
+
selectedIds = new Set();
|
|
44
|
+
versionValue = 0;
|
|
45
|
+
lastProjection = "";
|
|
46
|
+
lastDocument;
|
|
47
|
+
connectionsValue = [];
|
|
48
|
+
variablesValue = [];
|
|
49
|
+
activePane = "Outline";
|
|
50
|
+
portDrag;
|
|
51
|
+
marquee;
|
|
52
|
+
segmentDrag;
|
|
53
|
+
dropLine;
|
|
54
|
+
narrowValue = false;
|
|
55
|
+
suppressClick = false;
|
|
56
|
+
resizeObserver;
|
|
57
|
+
routedLines = new Map();
|
|
58
|
+
computedChecks = [];
|
|
59
|
+
get selectedBoxes() { return this.projection.boxes.filter(box => this.selectedIds.has(box.id)); }
|
|
60
|
+
get selectedPath() { return this.selected?.path ? [...this.selected.path] : null; }
|
|
61
|
+
set selectedPath(path) {
|
|
62
|
+
if (!this.isRendered && this.documentValue !== undefined) {
|
|
63
|
+
this.projection = this.model.project(this.documentValue);
|
|
64
|
+
validateProjection(this.projection);
|
|
65
|
+
}
|
|
66
|
+
const normalized = normalizeProcessSelectionPath(this.projection, path);
|
|
67
|
+
this.select(normalized === null ? null : this.projection.boxes.find(box => JSON.stringify(box.path) === JSON.stringify(normalized))?.id ?? null);
|
|
68
|
+
}
|
|
69
|
+
get connections() { return this.connectionsValue; }
|
|
70
|
+
set connections(value) { this.connectionsValue = value; this.refresh(); }
|
|
71
|
+
get variables() { return this.variablesValue; }
|
|
72
|
+
set variables(value) { this.variablesValue = value; this.refresh(); }
|
|
73
|
+
get version() { return this.versionValue; }
|
|
74
|
+
get narrow() { return this.narrowValue; }
|
|
75
|
+
get positions() { return snapshotProcessPositions(this.projection, this.layoutValue); }
|
|
76
|
+
load(document, options = {}) {
|
|
77
|
+
const projection = this.model.project(document);
|
|
78
|
+
validateProjection(projection);
|
|
79
|
+
this.documentValue = document;
|
|
80
|
+
this.projection = projection;
|
|
81
|
+
this.versionValue = options.version ?? 0;
|
|
82
|
+
this.lastProjection = "";
|
|
83
|
+
this.layoutValue = restoreProcessPositions(projection, options.positions ?? []);
|
|
84
|
+
this.history.clear();
|
|
85
|
+
this.selectedId = null;
|
|
86
|
+
this.selectedIds.clear();
|
|
87
|
+
this.refresh();
|
|
88
|
+
if (options.selectedPath)
|
|
89
|
+
this.selectedPath = options.selectedPath;
|
|
90
|
+
}
|
|
91
|
+
selectMany(ids) {
|
|
92
|
+
if (!this.isRendered && this.documentValue !== undefined) {
|
|
93
|
+
this.projection = this.model.project(this.documentValue);
|
|
94
|
+
validateProjection(this.projection);
|
|
95
|
+
}
|
|
96
|
+
const next = new Set(ids.filter(id => this.projection.boxes.some(box => box.id === id)));
|
|
97
|
+
if (next.size === this.selectedIds.size && [...next].every(id => this.selectedIds.has(id)))
|
|
98
|
+
return;
|
|
99
|
+
this.selectedIds = next;
|
|
100
|
+
this.selectedId = this.selectedIds.values().next().value ?? null;
|
|
101
|
+
this.renderSelection();
|
|
102
|
+
if (this.isRendered)
|
|
103
|
+
this.updateToolbar();
|
|
104
|
+
emit(this, "selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath });
|
|
105
|
+
}
|
|
106
|
+
get headingLevel() { const level = Number(this.getAttribute("heading-level") ?? 2); return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 2; }
|
|
107
|
+
set headingLevel(value) { this.setAttribute("heading-level", String(value)); }
|
|
108
|
+
get disableConnections() { return this.hasAttribute("disable-connections"); }
|
|
109
|
+
set disableConnections(value) { this.toggleAttribute("disable-connections", value); }
|
|
110
|
+
arrangedLayout(layout) {
|
|
111
|
+
const arranged = this.model.arrange?.(this.projection) ?? arrangeProcess(this.projection);
|
|
112
|
+
return completeLayout(this.projection, { ...layout, boxes: { ...arranged?.boxes, ...layout.boxes } });
|
|
113
|
+
}
|
|
38
114
|
get document() {
|
|
39
115
|
return this.documentValue;
|
|
40
116
|
}
|
|
@@ -100,12 +176,21 @@ export class ProcessModeler extends BaseElement {
|
|
|
100
176
|
this.refresh();
|
|
101
177
|
}
|
|
102
178
|
disconnectedCallback() {
|
|
179
|
+
this.resizeObserver?.disconnect();
|
|
180
|
+
this.shadowRoot?.querySelectorAll("[part=pane-drawer]").forEach(dialog => dismissModal(dialog));
|
|
181
|
+
const chooser = this.shadowRoot?.querySelector("[part=insert-chooser]");
|
|
182
|
+
if (chooser)
|
|
183
|
+
dismissModal(chooser);
|
|
103
184
|
this.cleanupInspector?.();
|
|
104
185
|
this.cleanupInspector = undefined;
|
|
105
186
|
this.inspected = undefined;
|
|
106
187
|
this.pointers.clear();
|
|
107
188
|
this.drag = undefined;
|
|
189
|
+
this.portDrag = undefined;
|
|
190
|
+
this.marquee = undefined;
|
|
191
|
+
this.segmentDrag = undefined;
|
|
108
192
|
}
|
|
193
|
+
connectedCallback() { super.connectedCallback(); this.resizeObserver?.observe(this); }
|
|
109
194
|
refresh() {
|
|
110
195
|
if (this.isRendered)
|
|
111
196
|
this.update();
|
|
@@ -117,11 +202,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
117
202
|
}
|
|
118
203
|
if (id !== null && !this.projection.boxes.some((box) => box.id === id))
|
|
119
204
|
return;
|
|
205
|
+
if (id === this.selectedId && this.selectedIds.size === (id ? 1 : 0))
|
|
206
|
+
return;
|
|
120
207
|
this.selectedId = id;
|
|
208
|
+
this.selectedIds = new Set(id ? [id] : []);
|
|
121
209
|
this.renderSelection();
|
|
122
210
|
if (this.isRendered)
|
|
123
211
|
this.updateToolbar();
|
|
124
|
-
emit(this, "selection-changed", { box: this.selected });
|
|
212
|
+
emit(this, "selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath });
|
|
125
213
|
}
|
|
126
214
|
setValidation(checks) {
|
|
127
215
|
this.checks = checks;
|
|
@@ -131,7 +219,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
131
219
|
this.setValidation(message ? [{ message, path }] : []);
|
|
132
220
|
}
|
|
133
221
|
requestEdit(edit) {
|
|
134
|
-
if (this.locked)
|
|
222
|
+
if (this.locked || (this.disableConnections && (edit.type === "connect" || edit.type === "disconnect")))
|
|
135
223
|
return;
|
|
136
224
|
const before = this.layout;
|
|
137
225
|
const previousIds = new Set(this.projection.boxes.map((box) => box.id));
|
|
@@ -142,11 +230,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
142
230
|
if (accepted)
|
|
143
231
|
return;
|
|
144
232
|
accepted = true;
|
|
233
|
+
if (command.layout)
|
|
234
|
+
this.layoutValue = structuredClone(command.layout);
|
|
145
235
|
this.refresh();
|
|
146
|
-
if (
|
|
236
|
+
if (!command.layout &&
|
|
237
|
+
edit.position &&
|
|
147
238
|
Number.isFinite(edit.position.x) &&
|
|
148
239
|
Number.isFinite(edit.position.y)) {
|
|
149
|
-
const added = this.projection.boxes.find((box) => !previousIds.has(box.id));
|
|
240
|
+
const added = this.projection.boxes.find(box => box.id === edit.boxId) ?? this.projection.boxes.find((box) => !previousIds.has(box.id));
|
|
150
241
|
if (added)
|
|
151
242
|
this.layoutValue = this.translatedLayout(added.id, edit.position.x, edit.position.y);
|
|
152
243
|
}
|
|
@@ -154,7 +245,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
154
245
|
const restore = (layout) => {
|
|
155
246
|
this.layoutValue = structuredClone(layout);
|
|
156
247
|
this.refresh();
|
|
157
|
-
|
|
248
|
+
this.emitPositions();
|
|
158
249
|
};
|
|
159
250
|
this.history.record({
|
|
160
251
|
undo: () => {
|
|
@@ -168,7 +259,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
168
259
|
});
|
|
169
260
|
this.refresh();
|
|
170
261
|
if (JSON.stringify(before) !== JSON.stringify(after))
|
|
171
|
-
|
|
262
|
+
this.emitPositions();
|
|
172
263
|
announce("Process updated", "polite", this.ownerDocument);
|
|
173
264
|
},
|
|
174
265
|
};
|
|
@@ -194,7 +285,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
194
285
|
const apply = (layout) => {
|
|
195
286
|
this.layoutValue = structuredClone(layout);
|
|
196
287
|
this.refresh();
|
|
197
|
-
|
|
288
|
+
this.emitPositions();
|
|
198
289
|
};
|
|
199
290
|
apply(after);
|
|
200
291
|
this.history.record({
|
|
@@ -213,6 +304,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
213
304
|
if (emit(this, "move-request", { boxId: id, position: next.boxes[id] }))
|
|
214
305
|
this.commitLayout(next);
|
|
215
306
|
}
|
|
307
|
+
emitPositions() {
|
|
308
|
+
emit(this, "layout-changed", { layout: this.layout });
|
|
309
|
+
emit(this, "positions-changed", { positions: this.positions, version: this.versionValue });
|
|
310
|
+
}
|
|
216
311
|
translatedLayout(id, x, y) {
|
|
217
312
|
const snap = (n) => (this.snapToGrid ? Math.round(n / 20) * 20 : n);
|
|
218
313
|
const next = this.layout;
|
|
@@ -241,7 +336,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
241
336
|
tidy() {
|
|
242
337
|
if (this.locked)
|
|
243
338
|
return;
|
|
244
|
-
this.commitLayout(
|
|
339
|
+
this.commitLayout(this.arrangedLayout({ ...this.layout, boxes: {} }));
|
|
245
340
|
this.fit();
|
|
246
341
|
}
|
|
247
342
|
resize(id, width, height) {
|
|
@@ -333,7 +428,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
333
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}
|
|
334
429
|
${boeFocusVisibleStyles(":is(button,input,[part=canvas],[part=minimap])")}
|
|
335
430
|
[part=toolbar]{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
|
|
336
|
-
[part=layout]{display:grid;grid-template-columns:minmax(0,1fr) minmax(
|
|
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}
|
|
337
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}
|
|
338
433
|
[part=world]{position:absolute;inset:0;transform-origin:0 0}
|
|
339
434
|
[part=lines]{position:absolute;inset:0;width:1px;height:1px;overflow:visible;pointer-events:none}
|
|
@@ -344,7 +439,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
344
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}
|
|
345
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}
|
|
346
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}
|
|
347
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}
|
|
348
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}
|
|
349
449
|
[part=choices]{display:grid;gap:6px;max-height:300px;overflow:auto}[part=group]{font-weight:600;margin:8px 0 0}
|
|
350
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)}
|
|
@@ -356,6 +456,207 @@ export class ProcessModeler extends BaseElement {
|
|
|
356
456
|
@media(max-width:800px){[part=layout]{grid-template-columns:minmax(0,1fr)}[part=canvas]{height:420px}}
|
|
357
457
|
@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important;scroll-behavior:auto!important}}
|
|
358
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>`;
|
|
459
|
+
const chooser = document.createElement("dialog");
|
|
460
|
+
chooser.setAttribute("part", "insert-chooser");
|
|
461
|
+
chooser.setAttribute("aria-label", "Insert a building block");
|
|
462
|
+
const picker = new KindPicker();
|
|
463
|
+
picker.variant = "menu";
|
|
464
|
+
chooser.append(picker);
|
|
465
|
+
this.shadowRoot.append(chooser);
|
|
466
|
+
picker.addEventListener("kind-pick", event => {
|
|
467
|
+
const insertion = this.insertion;
|
|
468
|
+
dismissModal(chooser);
|
|
469
|
+
this.insertion = undefined;
|
|
470
|
+
if (insertion)
|
|
471
|
+
this.requestEdit({ ...insertion, kind: event.detail.kind });
|
|
472
|
+
});
|
|
473
|
+
picker.addEventListener("picker-cancel", () => { dismissModal(chooser); this.insertion = undefined; });
|
|
474
|
+
this.setupPanes();
|
|
475
|
+
}
|
|
476
|
+
setupPanes() {
|
|
477
|
+
const root = this.shadowRoot;
|
|
478
|
+
const style = document.createElement("style");
|
|
479
|
+
style.textContent = `
|
|
480
|
+
[part=layout]{grid-template-columns:180px minmax(0,1fr) minmax(var(--boe-process-inspector-min-width,220px),var(--boe-process-inspector-width,280px))}
|
|
481
|
+
[part=pane-drawer]{display:contents;color:inherit}[part=pane-drawer]::backdrop{background:rgb(0 0 0 / .45)}
|
|
482
|
+
[part=pane-close],[data-command=palette],[data-command=details]{display:none}
|
|
483
|
+
[part=palette]{align-content:start;border:1px solid var(--boe-control-edge,#6f6f6f);border-radius:12px;padding:12px;background:var(--boe-token-surface-surface,#fff)}
|
|
484
|
+
[part=pane-tabs]{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px}[part=pane-tabs] button{font-size:12px;min-height:28px}
|
|
485
|
+
[part=pane-content]{display:grid;gap:8px}[part=pane-content] button{min-height:28px;text-align:start}
|
|
486
|
+
[part=port]{position:absolute;width:24px;height:24px;min-height:24px;border-radius:50%;padding:0;background:var(--boe-token-surface-surface-brand,#0061d5);border:2px solid var(--boe-token-surface-surface,#fff);opacity:0;z-index:3}
|
|
487
|
+
[part=box]:hover [part=port],[part=box]:focus-within [part=port],[part=box][aria-current=true] [part=port],[part=frame]:hover [part=port]{opacity:1}
|
|
488
|
+
[data-side=north]{left:calc(50% - 12px);top:-12px}[data-side=south]{left:calc(50% - 12px);bottom:-12px}[data-side=east]{right:-12px;top:calc(50% - 12px)}[data-side=west]{left:-12px;top:calc(50% - 12px)}
|
|
489
|
+
[part=box][aria-current=true],[part=frame][aria-current=true]{border:2px solid var(--boe-token-surface-surface-brand,#0061d5)}
|
|
490
|
+
[part=line-hit]{fill:none;stroke:transparent;stroke-width:24;pointer-events:stroke}[data-drop=true]{stroke:var(--boe-token-surface-surface-brand,#0061d5);stroke-width:4}
|
|
491
|
+
[part=connection]{transform:translate(-50%,-50%);font-size:12px}
|
|
492
|
+
[part=connection-actions]{position:absolute;top:100%;left:50%;transform:translateX(-50%);display:flex;gap:4px;width:max-content;background:var(--boe-token-surface-surface,#fff);border-radius:8px}
|
|
493
|
+
[part=marquee]{position:absolute;border:2px solid var(--boe-token-surface-surface-brand,#0061d5);background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 12%,transparent);pointer-events:none}
|
|
494
|
+
[part=guide]{position:absolute;background:var(--boe-token-surface-surface-brand,#0061d5);pointer-events:none}
|
|
495
|
+
[part=measure]{position:absolute;background:var(--boe-token-surface-surface,#fff);font-size:12px;padding:4px;pointer-events:none}
|
|
496
|
+
[part=selection-toolbar]{position:absolute;left:12px;bottom:12px;display:flex;gap:4px;flex-wrap:wrap;background:var(--boe-token-surface-surface,#fff);padding:6px;border:1px solid var(--boe-control-edge,#6f6f6f);border-radius:8px;z-index:5}
|
|
497
|
+
:host([data-narrow]) [part=layout]{grid-template-columns:minmax(0,1fr)}
|
|
498
|
+
:host([data-narrow]) [part=pane-drawer]{display:none;max-width:calc(100vw - 24px);width:320px;max-height:85vh;overflow:auto;border:1px solid var(--boe-control-edge,#6f6f6f);border-radius:12px;padding:12px;background:var(--boe-token-surface-surface,#fff)}
|
|
499
|
+
:host([data-narrow]) [part=pane-drawer][open]{display:block}
|
|
500
|
+
:host([data-narrow]) [part=pane-close],:host([data-narrow]) [data-command=palette],:host([data-narrow]) [data-command=details]{display:inline-block}
|
|
501
|
+
`;
|
|
502
|
+
root.append(style);
|
|
503
|
+
const layout = root.querySelector('[part=layout]');
|
|
504
|
+
for (const [part, label] of [["palette", "Building blocks"], ["inspector", "Process details"]]) {
|
|
505
|
+
const pane = root.querySelector(`[part=${part}]`);
|
|
506
|
+
const dialog = document.createElement("dialog");
|
|
507
|
+
dialog.setAttribute("part", "pane-drawer");
|
|
508
|
+
dialog.setAttribute("aria-label", label);
|
|
509
|
+
const close = document.createElement("button");
|
|
510
|
+
close.setAttribute("part", "pane-close");
|
|
511
|
+
close.textContent = `Close ${label.toLowerCase()}`;
|
|
512
|
+
close.onclick = () => dismissModal(dialog);
|
|
513
|
+
dialog.append(close, pane);
|
|
514
|
+
if (part === "palette")
|
|
515
|
+
layout.prepend(dialog);
|
|
516
|
+
else
|
|
517
|
+
layout.append(dialog);
|
|
518
|
+
const trigger = document.createElement("button");
|
|
519
|
+
trigger.dataset.command = part === "palette" ? "palette" : "details";
|
|
520
|
+
trigger.textContent = label;
|
|
521
|
+
trigger.onclick = () => promoteModal(dialog);
|
|
522
|
+
root.querySelector('[part=toolbar]').append(trigger);
|
|
523
|
+
}
|
|
524
|
+
const inspector = root.querySelector('[part=inspector]');
|
|
525
|
+
const tabs = document.createElement("div");
|
|
526
|
+
tabs.setAttribute("part", "pane-tabs");
|
|
527
|
+
tabs.setAttribute("role", "tablist");
|
|
528
|
+
tabs.setAttribute("aria-label", "Process details");
|
|
529
|
+
for (const name of ["Outline", "Checks", "Variables", "Connections", "Shortcuts"]) {
|
|
530
|
+
const button = document.createElement("button");
|
|
531
|
+
button.textContent = name;
|
|
532
|
+
button.setAttribute("role", "tab");
|
|
533
|
+
button.id = `process-tab-${name.toLowerCase()}`;
|
|
534
|
+
button.setAttribute("aria-controls", "process-pane-content");
|
|
535
|
+
button.onclick = () => { this.activePane = name; this.renderPane(); };
|
|
536
|
+
button.onkeydown = event => {
|
|
537
|
+
const buttons = Array.from(tabs.querySelectorAll("button"));
|
|
538
|
+
const index = buttons.indexOf(button);
|
|
539
|
+
const next = event.key === "ArrowRight" ? (index + 1) % buttons.length : event.key === "ArrowLeft" ? (index + buttons.length - 1) % buttons.length : event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : -1;
|
|
540
|
+
if (next >= 0) {
|
|
541
|
+
event.preventDefault();
|
|
542
|
+
buttons[next].click();
|
|
543
|
+
buttons[next].focus();
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
tabs.append(button);
|
|
547
|
+
}
|
|
548
|
+
const content = document.createElement("div");
|
|
549
|
+
content.setAttribute("part", "pane-content");
|
|
550
|
+
content.setAttribute("role", "tabpanel");
|
|
551
|
+
content.id = "process-pane-content";
|
|
552
|
+
inspector.prepend(tabs);
|
|
553
|
+
inspector.append(content);
|
|
554
|
+
root.querySelector('[part=checks]').removeAttribute("role");
|
|
555
|
+
const canvas = root.querySelector('[part=canvas]');
|
|
556
|
+
const floating = document.createElement("div");
|
|
557
|
+
floating.setAttribute("part", "selection-toolbar");
|
|
558
|
+
floating.setAttribute("role", "toolbar");
|
|
559
|
+
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
|
+
canvas.append(floating);
|
|
567
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
568
|
+
this.resizeObserver = new ResizeObserver(entries => {
|
|
569
|
+
const narrow = entries[0].contentRect.width < 1000;
|
|
570
|
+
this.narrowValue = narrow;
|
|
571
|
+
this.toggleAttribute("data-narrow", narrow);
|
|
572
|
+
if (!narrow)
|
|
573
|
+
root.querySelectorAll('[part=pane-drawer]').forEach(dialog => dismissModal(dialog));
|
|
574
|
+
});
|
|
575
|
+
this.resizeObserver.observe(this);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
renderPane() {
|
|
579
|
+
if (!this.isRendered)
|
|
580
|
+
return;
|
|
581
|
+
const root = this.shadowRoot;
|
|
582
|
+
root.querySelectorAll('[part=pane-tabs] button').forEach(button => {
|
|
583
|
+
const selected = button.textContent === this.activePane;
|
|
584
|
+
button.setAttribute("aria-selected", String(selected));
|
|
585
|
+
button.tabIndex = selected ? 0 : -1;
|
|
586
|
+
});
|
|
587
|
+
const content = root.querySelector('[part=pane-content]');
|
|
588
|
+
content.setAttribute("aria-labelledby", `process-tab-${this.activePane.toLowerCase()}`);
|
|
589
|
+
const checks = root.querySelector('[part=checks]');
|
|
590
|
+
checks.hidden = this.activePane !== "Checks";
|
|
591
|
+
// Move the checks node out before replacing its parent; keep its handlers.
|
|
592
|
+
root.querySelector('[part=inspector]').append(checks);
|
|
593
|
+
content.replaceChildren();
|
|
594
|
+
root.querySelector('[part=editor]').hidden = this.activePane !== "Outline";
|
|
595
|
+
if (this.activePane === "Outline") {
|
|
596
|
+
this.projection.boxes.forEach(box => {
|
|
597
|
+
const button = document.createElement("button");
|
|
598
|
+
button.textContent = box.title;
|
|
599
|
+
button.setAttribute("aria-current", String(this.selectedIds.has(box.id)));
|
|
600
|
+
button.onclick = () => { this.select(box.id); this.focusBox(box.id); };
|
|
601
|
+
content.append(button);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
else if (this.activePane === "Checks") {
|
|
605
|
+
content.append(checks);
|
|
606
|
+
if (!this.allChecks.length) {
|
|
607
|
+
const message = document.createElement("p");
|
|
608
|
+
message.textContent = "No checks to resolve.";
|
|
609
|
+
content.prepend(message);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else if (this.activePane === "Connections" || this.activePane === "Variables") {
|
|
613
|
+
const items = this.activePane === "Connections" ? this.connections : this.variables;
|
|
614
|
+
for (const item of items) {
|
|
615
|
+
const row = document.createElement("p");
|
|
616
|
+
row.textContent = "kind" in item ? `${item.name} (${item.kind})` : `${item.name}${item.description ? `: ${item.description}` : ""}`;
|
|
617
|
+
content.append(row);
|
|
618
|
+
}
|
|
619
|
+
if (!items.length)
|
|
620
|
+
content.textContent = "None supplied by the host.";
|
|
621
|
+
}
|
|
622
|
+
else if (this.activePane === "Shortcuts") {
|
|
623
|
+
content.textContent = "Arrow keys: move selected step. Shift+arrows: move farther. C: connect selected step. Shift+drag background: select steps. Delete: remove selected. Escape: cancel. Ctrl/Command+Z: undo. Shift+Ctrl/Command+Z: redo.";
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
selectionCommand(command) {
|
|
627
|
+
if (this.locked)
|
|
628
|
+
return;
|
|
629
|
+
const boxes = this.selectedBoxes;
|
|
630
|
+
if (command === "delete-many") {
|
|
631
|
+
for (const box of boxes)
|
|
632
|
+
this.requestEdit({ type: "delete", boxId: box.id });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (boxes.length < 2)
|
|
636
|
+
return;
|
|
637
|
+
const next = this.layout;
|
|
638
|
+
const sorted = [...boxes].filter(box => {
|
|
639
|
+
let parent = box.parentId;
|
|
640
|
+
while (parent) {
|
|
641
|
+
if (this.selectedIds.has(parent))
|
|
642
|
+
return false;
|
|
643
|
+
parent = this.projection.boxes.find(box => box.id === parent)?.parentId;
|
|
644
|
+
}
|
|
645
|
+
return true;
|
|
646
|
+
}).sort((a, b) => next.boxes[a.id].x - next.boxes[b.id].x);
|
|
647
|
+
if (sorted.length < 2)
|
|
648
|
+
return;
|
|
649
|
+
const first = next.boxes[sorted[0].id];
|
|
650
|
+
const last = next.boxes[sorted.at(-1).id];
|
|
651
|
+
sorted.forEach((box, i) => {
|
|
652
|
+
const current = next.boxes[box.id];
|
|
653
|
+
const moved = this.translatedLayout(box.id, command === "align" ? current.x : first.x + i * (last.x - first.x) / (sorted.length - 1), command === "align" ? first.y : current.y);
|
|
654
|
+
for (const candidate of this.projection.boxes)
|
|
655
|
+
if (moved.boxes[candidate.id].x !== this.layoutValue.boxes[candidate.id].x || moved.boxes[candidate.id].y !== this.layoutValue.boxes[candidate.id].y)
|
|
656
|
+
next.boxes[candidate.id] = moved.boxes[candidate.id];
|
|
657
|
+
});
|
|
658
|
+
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
|
+
this.commitLayout(next);
|
|
359
660
|
}
|
|
360
661
|
setupListeners() {
|
|
361
662
|
const toolbar = this.shadowRoot.querySelector("[part=toolbar]");
|
|
@@ -389,12 +690,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
389
690
|
this.snapToGrid = !this.snapToGrid;
|
|
390
691
|
else if (command === "lock")
|
|
391
692
|
this.locked = !this.locked;
|
|
392
|
-
else if (command === "connect" && this.selected && !this.locked) {
|
|
693
|
+
else if (command === "connect" && this.selected && !this.locked && !this.disableConnections) {
|
|
393
694
|
this.connecting = this.selected.id;
|
|
394
695
|
this.setStatus(`Select the next box to connect from ${this.selected.title}`);
|
|
395
696
|
}
|
|
396
697
|
else if (command === "delete" && this.selected)
|
|
397
|
-
this.
|
|
698
|
+
this.selectionCommand("delete-many");
|
|
398
699
|
});
|
|
399
700
|
this.shadowRoot.querySelector("[part=search]").addEventListener("input", () => this.renderPalette());
|
|
400
701
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
@@ -413,6 +714,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
413
714
|
canvas.addEventListener("pointermove", (event) => this.pointerMove(event));
|
|
414
715
|
canvas.addEventListener("pointerup", (event) => this.pointerEnd(event, true));
|
|
415
716
|
canvas.addEventListener("pointercancel", (event) => this.pointerEnd(event, false));
|
|
717
|
+
canvas.addEventListener("lostpointercapture", event => {
|
|
718
|
+
// Touch transfers implicit box capture to the canvas during a drag.
|
|
719
|
+
if (event.target === canvas)
|
|
720
|
+
this.pointerEnd(event, false);
|
|
721
|
+
});
|
|
416
722
|
canvas.addEventListener("gesturestart", (event) => {
|
|
417
723
|
event.preventDefault();
|
|
418
724
|
this.gestureZoom = this.viewport.zoom;
|
|
@@ -470,6 +776,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
470
776
|
if (event.target.closest("input,textarea,select,[contenteditable]"))
|
|
471
777
|
return;
|
|
472
778
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "z") {
|
|
779
|
+
if (this.locked || !(event.shiftKey ? this.history.canRedo : this.history.canUndo))
|
|
780
|
+
return;
|
|
473
781
|
event.preventDefault();
|
|
474
782
|
if (event.shiftKey)
|
|
475
783
|
this.redo();
|
|
@@ -479,20 +787,28 @@ export class ProcessModeler extends BaseElement {
|
|
|
479
787
|
}
|
|
480
788
|
if (event.key === "Escape") {
|
|
481
789
|
this.connecting = null;
|
|
790
|
+
this.portDrag = undefined;
|
|
791
|
+
this.marquee = undefined;
|
|
792
|
+
this.segmentDrag = undefined;
|
|
793
|
+
this.drag = undefined;
|
|
794
|
+
this.pointers.clear();
|
|
795
|
+
this.refresh();
|
|
482
796
|
this.setStatus("Connecting cancelled");
|
|
483
797
|
return;
|
|
484
798
|
}
|
|
485
799
|
const box = this.selected;
|
|
486
800
|
if (!box || this.locked)
|
|
487
801
|
return;
|
|
488
|
-
if (event.key.
|
|
802
|
+
if (event.key === "Enter" || event.key === " ")
|
|
803
|
+
return;
|
|
804
|
+
if (event.key.toLowerCase() === "c" && !this.disableConnections) {
|
|
489
805
|
event.preventDefault();
|
|
490
806
|
this.connecting = box.id;
|
|
491
807
|
this.setStatus(`Select the next box to connect from ${box.title}`);
|
|
492
808
|
}
|
|
493
809
|
if (event.key === "Delete" || event.key === "Backspace") {
|
|
494
810
|
event.preventDefault();
|
|
495
|
-
this.
|
|
811
|
+
this.selectionCommand("delete-many");
|
|
496
812
|
}
|
|
497
813
|
const direction = {
|
|
498
814
|
ArrowLeft: [-1, 0],
|
|
@@ -509,8 +825,29 @@ export class ProcessModeler extends BaseElement {
|
|
|
509
825
|
}
|
|
510
826
|
}
|
|
511
827
|
pointerDown(event) {
|
|
828
|
+
if (event.button !== 0)
|
|
829
|
+
return;
|
|
830
|
+
this.suppressClick = false;
|
|
831
|
+
if (this.portDrag || this.segmentDrag || this.marquee)
|
|
832
|
+
return;
|
|
833
|
+
const control = event.target.closest("[part=port]");
|
|
834
|
+
if (control && !this.locked && !this.disableConnections) {
|
|
835
|
+
event.preventDefault();
|
|
836
|
+
const point = this.canvasPoint(event);
|
|
837
|
+
this.portDrag = { pointerId: event.pointerId, id: control.dataset.owner, side: control.dataset.side, start: point, point };
|
|
838
|
+
this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const segment = event.target.closest("[data-segment]");
|
|
842
|
+
if (segment && !this.locked) {
|
|
843
|
+
const line = this.projection.lines.find(line => line.id === segment.dataset.lineId);
|
|
844
|
+
this.segmentDrag = { pointerId: event.pointerId, id: line.id, index: Number(segment.dataset.segment), points: routeProcessLine(line, this.layoutValue, this.projection) };
|
|
845
|
+
this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
|
|
846
|
+
event.preventDefault();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
512
849
|
if (event.button !== 0 ||
|
|
513
|
-
event.target.closest("[part=connection]"))
|
|
850
|
+
event.target.closest("[part=connection],button,input,select,textarea,[contenteditable]"))
|
|
514
851
|
return;
|
|
515
852
|
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
516
853
|
if (this.pointers.size === 2) {
|
|
@@ -522,7 +859,15 @@ export class ProcessModeler extends BaseElement {
|
|
|
522
859
|
const target = event.target.closest("[data-box-id]");
|
|
523
860
|
const id = target?.dataset.boxId;
|
|
524
861
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
525
|
-
(
|
|
862
|
+
if (id && !this.selectedIds.has(id) && !event.shiftKey)
|
|
863
|
+
this.select(id);
|
|
864
|
+
if (!id && event.shiftKey && !this.locked) {
|
|
865
|
+
const point = this.canvasPoint(event);
|
|
866
|
+
this.marquee = { pointerId: event.pointerId, start: point, point };
|
|
867
|
+
canvas.setPointerCapture?.(event.pointerId);
|
|
868
|
+
event.preventDefault();
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
526
871
|
this.drag = {
|
|
527
872
|
id: this.locked ? undefined : id,
|
|
528
873
|
x: event.clientX,
|
|
@@ -537,6 +882,44 @@ export class ProcessModeler extends BaseElement {
|
|
|
537
882
|
return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
|
538
883
|
}
|
|
539
884
|
pointerMove(event) {
|
|
885
|
+
const gesture = this.portDrag ?? this.segmentDrag ?? this.marquee;
|
|
886
|
+
if (gesture && gesture.pointerId !== event.pointerId)
|
|
887
|
+
return;
|
|
888
|
+
const point = this.canvasPoint(event);
|
|
889
|
+
if (this.portDrag) {
|
|
890
|
+
this.portDrag.point = point;
|
|
891
|
+
let ghost = this.shadowRoot.querySelector('[part=connection-preview]');
|
|
892
|
+
if (!ghost) {
|
|
893
|
+
ghost = svgElement("polyline");
|
|
894
|
+
ghost.setAttribute("part", "connection-preview");
|
|
895
|
+
ghost.style.cssText = "fill:none;stroke:var(--boe-token-surface-surface-brand,#0061d5);stroke-width:3";
|
|
896
|
+
this.shadowRoot.querySelector('[part=lines]').append(ghost);
|
|
897
|
+
}
|
|
898
|
+
const start = this.portDrag.start;
|
|
899
|
+
ghost.setAttribute("points", `${start.x},${start.y} ${point.x},${start.y} ${point.x},${point.y}`);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (this.segmentDrag) {
|
|
903
|
+
const { index, points } = this.segmentDrag;
|
|
904
|
+
if (points[index].x === points[index + 1].x)
|
|
905
|
+
points[index].x = points[index + 1].x = point.x;
|
|
906
|
+
else
|
|
907
|
+
points[index].y = points[index + 1].y = point.y;
|
|
908
|
+
const path = Array.from(this.shadowRoot.querySelectorAll('[part=line]')).find(path => path.dataset.lineId === this.segmentDrag.id);
|
|
909
|
+
path?.setAttribute("points", points.map(p => `${p.x},${p.y}`).join(" "));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (this.marquee) {
|
|
913
|
+
this.marquee.point = point;
|
|
914
|
+
let region = this.shadowRoot.querySelector('[part=marquee]');
|
|
915
|
+
if (!region) {
|
|
916
|
+
region = document.createElement("div");
|
|
917
|
+
region.setAttribute("part", "marquee");
|
|
918
|
+
this.shadowRoot.querySelector('[part=world]').append(region);
|
|
919
|
+
}
|
|
920
|
+
this.place(region, { x: Math.min(point.x, this.marquee.start.x), y: Math.min(point.y, this.marquee.start.y), width: Math.abs(point.x - this.marquee.start.x), height: Math.abs(point.y - this.marquee.start.y) });
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
540
923
|
if (!this.pointers.has(event.pointerId))
|
|
541
924
|
return;
|
|
542
925
|
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
@@ -551,11 +934,16 @@ export class ProcessModeler extends BaseElement {
|
|
|
551
934
|
return;
|
|
552
935
|
const dx = event.clientX - this.drag.x;
|
|
553
936
|
const dy = event.clientY - this.drag.y;
|
|
937
|
+
if (Math.hypot(dx, dy) <= 4)
|
|
938
|
+
return;
|
|
939
|
+
this.shadowRoot.querySelector('[part=canvas]').setPointerCapture?.(event.pointerId);
|
|
554
940
|
if (this.drag.id && this.drag.position) {
|
|
555
941
|
const element = Array.from(this.shadowRoot.querySelectorAll("[data-box-id]")).find((box) => box.dataset.boxId === this.drag.id);
|
|
556
942
|
if (element) {
|
|
557
|
-
|
|
558
|
-
element.style.
|
|
943
|
+
const position = this.alignedPoint(this.drag.id, this.drag.position.x + dx / this.viewport.zoom, this.drag.position.y + dy / this.viewport.zoom);
|
|
944
|
+
element.style.left = `${position.x}px`;
|
|
945
|
+
element.style.top = `${position.y}px`;
|
|
946
|
+
this.markDropLine(point, this.drag.id);
|
|
559
947
|
}
|
|
560
948
|
}
|
|
561
949
|
else {
|
|
@@ -565,6 +953,48 @@ export class ProcessModeler extends BaseElement {
|
|
|
565
953
|
}
|
|
566
954
|
}
|
|
567
955
|
pointerEnd(event, commit) {
|
|
956
|
+
const gesture = this.portDrag ?? this.segmentDrag ?? this.marquee;
|
|
957
|
+
if (gesture && gesture.pointerId !== event.pointerId)
|
|
958
|
+
return;
|
|
959
|
+
const point = this.canvasPoint(event);
|
|
960
|
+
if (this.portDrag) {
|
|
961
|
+
const port = this.portDrag;
|
|
962
|
+
this.portDrag = undefined;
|
|
963
|
+
this.shadowRoot.querySelector('[part=connection-preview]')?.remove();
|
|
964
|
+
if (commit) {
|
|
965
|
+
if (Math.hypot(point.x - port.start.x, point.y - port.start.y) < 8) {
|
|
966
|
+
const pos = this.layoutValue.boxes[port.id];
|
|
967
|
+
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[port.side];
|
|
968
|
+
this.openChooser({ type: "add", from: port.id, fromSide: port.side, position: { x: pos.x + offsets[0], y: pos.y + offsets[1] } });
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
const target = this.boxAt(point, port.id);
|
|
972
|
+
if (target)
|
|
973
|
+
this.requestEdit({ type: "connect", from: port.id, to: target.id, fromSide: port.side });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (this.segmentDrag) {
|
|
979
|
+
const segment = this.segmentDrag;
|
|
980
|
+
this.segmentDrag = undefined;
|
|
981
|
+
if (commit)
|
|
982
|
+
this.routeLine(segment.id, segment.points.slice(1, -1));
|
|
983
|
+
else
|
|
984
|
+
this.refresh();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (this.marquee) {
|
|
988
|
+
const { start } = this.marquee;
|
|
989
|
+
this.marquee = undefined;
|
|
990
|
+
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 ?? 220) > Math.min(start.x, point.x) && p.y < Math.max(start.y, point.y) && p.y + (p.height ?? 96) > Math.min(start.y, point.y); }).map(box => box.id));
|
|
992
|
+
this.shadowRoot.querySelector('[part=marquee]')?.remove();
|
|
993
|
+
this.pointers.delete(event.pointerId);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (!this.pointers.has(event.pointerId))
|
|
997
|
+
return;
|
|
568
998
|
this.pointers.delete(event.pointerId);
|
|
569
999
|
const drag = this.drag;
|
|
570
1000
|
this.drag = undefined;
|
|
@@ -572,22 +1002,142 @@ export class ProcessModeler extends BaseElement {
|
|
|
572
1002
|
if (drag?.id && drag.position) {
|
|
573
1003
|
const dx = event.clientX - drag.x;
|
|
574
1004
|
const dy = event.clientY - drag.y;
|
|
575
|
-
if (commit && Math.hypot(dx, dy) > 4)
|
|
576
|
-
this.
|
|
1005
|
+
if (commit && Math.hypot(dx, dy) > 4) {
|
|
1006
|
+
this.suppressClick = true;
|
|
1007
|
+
const position = this.alignedPoint(drag.id, drag.position.x + dx / this.viewport.zoom, drag.position.y + dy / this.viewport.zoom);
|
|
1008
|
+
const line = this.lineAt(point, drag.id);
|
|
1009
|
+
const frame = this.boxAt(point, drag.id, true);
|
|
1010
|
+
if (line)
|
|
1011
|
+
this.requestEdit({ type: "insert", boxId: drag.id, lineId: line.id, from: line.from, to: line.to, position });
|
|
1012
|
+
else if (this.projection.boxes.find(box => box.id === drag.id)?.parentId !== frame?.id)
|
|
1013
|
+
this.requestEdit({ type: "reparent", boxId: drag.id, parentId: frame?.id, position });
|
|
1014
|
+
else if (this.selectedIds.size > 1 && this.selectedIds.has(drag.id)) {
|
|
1015
|
+
const next = this.layout;
|
|
1016
|
+
const dx = position.x - drag.position.x;
|
|
1017
|
+
const dy = position.y - drag.position.y;
|
|
1018
|
+
const moved = new Set(this.selectedIds);
|
|
1019
|
+
for (let changed = true; changed;) {
|
|
1020
|
+
changed = false;
|
|
1021
|
+
for (const box of this.projection.boxes)
|
|
1022
|
+
if (box.parentId && moved.has(box.parentId) && !moved.has(box.id)) {
|
|
1023
|
+
moved.add(box.id);
|
|
1024
|
+
changed = true;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const primary = next.boxes[drag.id];
|
|
1028
|
+
const shiftX = this.snapToGrid ? Math.round((primary.x + dx) / 20) * 20 - primary.x : dx;
|
|
1029
|
+
const shiftY = this.snapToGrid ? Math.round((primary.y + dy) / 20) * 20 - primary.y : dy;
|
|
1030
|
+
for (const id of moved)
|
|
1031
|
+
next.boxes[id] = { ...next.boxes[id], x: next.boxes[id].x + shiftX, y: next.boxes[id].y + shiftY };
|
|
1032
|
+
if (emit(this, "move-request", { boxId: drag.id, position: next.boxes[drag.id], boxIds: [...moved], positions: next.boxes }))
|
|
1033
|
+
this.commitLayout(next);
|
|
1034
|
+
}
|
|
1035
|
+
else
|
|
1036
|
+
this.move(drag.id, position.x, position.y);
|
|
1037
|
+
}
|
|
577
1038
|
else if (!commit)
|
|
578
1039
|
this.refresh();
|
|
579
1040
|
}
|
|
1041
|
+
this.shadowRoot.querySelectorAll('[part=guide],[part=measure]').forEach(element => element.remove());
|
|
1042
|
+
this.markDropLine(undefined);
|
|
1043
|
+
if (drag?.id && (!commit || Math.hypot(event.clientX - drag.x, event.clientY - drag.y) > 4))
|
|
1044
|
+
this.refresh();
|
|
1045
|
+
}
|
|
1046
|
+
canvasPoint(event) {
|
|
1047
|
+
const rect = this.shadowRoot.querySelector('[part=canvas]').getBoundingClientRect();
|
|
1048
|
+
return { x: (event.clientX - rect.left - this.viewport.x) / this.viewport.zoom, y: (event.clientY - rect.top - this.viewport.y) / this.viewport.zoom };
|
|
1049
|
+
}
|
|
1050
|
+
boxAt(point, except, frameOnly = false) {
|
|
1051
|
+
return [...this.projection.boxes].reverse().find(box => {
|
|
1052
|
+
if (box.id === except || (frameOnly && !box.frame))
|
|
1053
|
+
return false;
|
|
1054
|
+
// A frame cannot be dropped inside its own descendants.
|
|
1055
|
+
let parent = box.parentId;
|
|
1056
|
+
while (parent) {
|
|
1057
|
+
if (parent === except)
|
|
1058
|
+
return false;
|
|
1059
|
+
parent = this.projection.boxes.find(b => b.id === parent)?.parentId;
|
|
1060
|
+
}
|
|
1061
|
+
const p = this.layoutValue.boxes[box.id];
|
|
1062
|
+
return point.x >= p.x - 12 && point.x <= p.x + (p.width ?? 220) + 12 && point.y >= p.y - 12 && point.y <= p.y + (p.height ?? 96) + 12;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
lineAt(point, except) {
|
|
1066
|
+
return this.projection.lines.find(line => line.from !== except && line.to !== except && nearProcessLine(point, this.routedLines.get(line.id) ?? [], 16 / this.viewport.zoom));
|
|
1067
|
+
}
|
|
1068
|
+
markDropLine(point, except) {
|
|
1069
|
+
this.dropLine = point ? this.lineAt(point, except)?.id : undefined;
|
|
1070
|
+
this.shadowRoot.querySelectorAll('[part=line]').forEach(path => path.dataset.drop = String(path.dataset.lineId === this.dropLine));
|
|
1071
|
+
}
|
|
1072
|
+
alignedPoint(id, x, y) {
|
|
1073
|
+
const world = this.shadowRoot.querySelector('[part=world]');
|
|
1074
|
+
world.querySelectorAll('[part=guide],[part=measure]').forEach(element => element.remove());
|
|
1075
|
+
for (const box of this.projection.boxes) {
|
|
1076
|
+
if (box.id === id)
|
|
1077
|
+
continue;
|
|
1078
|
+
const p = this.layoutValue.boxes[box.id];
|
|
1079
|
+
for (const axis of ["x", "y"]) {
|
|
1080
|
+
const value = axis === "x" ? x : y;
|
|
1081
|
+
if (Math.abs(value - p[axis]) < 8 / this.viewport.zoom) {
|
|
1082
|
+
if (axis === "x")
|
|
1083
|
+
x = p.x;
|
|
1084
|
+
else
|
|
1085
|
+
y = p.y;
|
|
1086
|
+
const guide = document.createElement("div");
|
|
1087
|
+
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) + 96}px` : `left:${Math.min(x, p.x)}px;top:${y}px;height:1px;width:${Math.abs(x - p.x) + 220}px`;
|
|
1089
|
+
world.append(guide);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (Math.abs(y - p.y) < 8) {
|
|
1093
|
+
const distance = x - p.x - (p.width ?? 220);
|
|
1094
|
+
if (distance > 0) {
|
|
1095
|
+
const measure = document.createElement("span");
|
|
1096
|
+
measure.setAttribute("part", "measure");
|
|
1097
|
+
measure.textContent = `${Math.round(distance)}px`;
|
|
1098
|
+
measure.style.cssText = `left:${p.x + (p.width ?? 220)}px;top:${y + 40}px`;
|
|
1099
|
+
world.append(measure);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return { x, y };
|
|
1104
|
+
}
|
|
1105
|
+
openChooser(edit) {
|
|
1106
|
+
this.insertion = edit;
|
|
1107
|
+
const chooser = this.shadowRoot.querySelector('[part=insert-chooser]');
|
|
1108
|
+
const picker = chooser.querySelector('box-kind-picker');
|
|
1109
|
+
picker.catalog = this.catalog;
|
|
1110
|
+
picker.refresh();
|
|
1111
|
+
promoteModal(chooser);
|
|
1112
|
+
picker.focus();
|
|
580
1113
|
}
|
|
581
1114
|
update() {
|
|
1115
|
+
const selectionBefore = JSON.stringify({ ids: [...this.selectedIds], path: this.selectedPath });
|
|
582
1116
|
if (this.documentValue !== undefined) {
|
|
583
1117
|
this.projection = this.model.project(this.documentValue);
|
|
584
1118
|
validateProjection(this.projection);
|
|
585
1119
|
}
|
|
586
1120
|
else
|
|
587
1121
|
this.projection = { boxes: [], lines: [] };
|
|
588
|
-
this.layoutValue =
|
|
1122
|
+
this.layoutValue = this.arrangedLayout(this.layoutValue);
|
|
1123
|
+
this.routedLines = new Map(this.projection.lines.map(line => [line.id, routeProcessLine(line, this.layoutValue, this.projection)]));
|
|
1124
|
+
this.computedChecks = this.collectChecks();
|
|
589
1125
|
if (this.selectedId && !this.selected)
|
|
590
1126
|
this.selectedId = null;
|
|
1127
|
+
this.selectedIds = new Set([...this.selectedIds].filter(id => this.projection.boxes.some(box => box.id === id)));
|
|
1128
|
+
if (!this.selectedId)
|
|
1129
|
+
this.selectedId = this.selectedIds.values().next().value ?? null;
|
|
1130
|
+
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 })),
|
|
1132
|
+
lines: this.projection.lines,
|
|
1133
|
+
});
|
|
1134
|
+
if (serialized !== this.lastProjection || this.documentValue !== this.lastDocument) {
|
|
1135
|
+
if (this.lastProjection)
|
|
1136
|
+
this.versionValue = typeof this.versionValue === "number" ? this.versionValue + 1 : `${this.versionValue}+1`;
|
|
1137
|
+
this.lastProjection = serialized;
|
|
1138
|
+
this.lastDocument = this.documentValue;
|
|
1139
|
+
emit(this, "projection-changed", { projection: this.projection, version: this.versionValue, checks: this.allChecks });
|
|
1140
|
+
}
|
|
591
1141
|
const focused = this.shadowRoot.activeElement
|
|
592
1142
|
?.dataset.boxId;
|
|
593
1143
|
this.renderWorld();
|
|
@@ -596,9 +1146,17 @@ export class ProcessModeler extends BaseElement {
|
|
|
596
1146
|
this.renderSelection();
|
|
597
1147
|
this.paintViewport();
|
|
598
1148
|
this.updateToolbar();
|
|
1149
|
+
if (selectionBefore !== JSON.stringify({ ids: [...this.selectedIds], path: this.selectedPath }))
|
|
1150
|
+
emit(this, "selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath });
|
|
599
1151
|
if (focused)
|
|
600
1152
|
this.focusBox(focused);
|
|
601
1153
|
}
|
|
1154
|
+
get allChecks() { return this.computedChecks; }
|
|
1155
|
+
collectChecks() {
|
|
1156
|
+
const routes = this.projection.lines.filter(line => !this.routedLines.get(line.id)?.length)
|
|
1157
|
+
.map(line => ({ message: "A connection is blocked by overlapping steps. Move a step or adjust its route.", boxId: line.from }));
|
|
1158
|
+
return [...this.checks, ...graphChecks(this.projection), ...routes, ...(this.documentValue === undefined ? [] : this.model.validate?.(this.documentValue, this.projection) ?? [])];
|
|
1159
|
+
}
|
|
602
1160
|
renderWorld() {
|
|
603
1161
|
const world = this.shadowRoot.querySelector("[part=world]");
|
|
604
1162
|
world.replaceChildren();
|
|
@@ -610,14 +1168,15 @@ export class ProcessModeler extends BaseElement {
|
|
|
610
1168
|
world.append(element);
|
|
611
1169
|
}
|
|
612
1170
|
for (const box of [...this.projection.boxes].sort((a, b) => Number(Boolean(b.frame)) - Number(Boolean(a.frame)))) {
|
|
613
|
-
const element = document.createElement("
|
|
614
|
-
element.
|
|
1171
|
+
const element = document.createElement("div");
|
|
1172
|
+
element.tabIndex = 0;
|
|
1173
|
+
element.setAttribute("role", "group");
|
|
615
1174
|
element.setAttribute("part", box.frame ? "frame" : "box");
|
|
616
1175
|
element.dataset.boxId = box.id;
|
|
617
1176
|
element.setAttribute("aria-label", [box.title, box.description, box.frame ? "Frame" : ""]
|
|
618
1177
|
.filter(Boolean)
|
|
619
1178
|
.join(". "));
|
|
620
|
-
element.setAttribute("aria-
|
|
1179
|
+
element.setAttribute("aria-current", String(this.selectedIds.has(box.id)));
|
|
621
1180
|
this.place(element, this.layoutValue.boxes[box.id]);
|
|
622
1181
|
const kind = this.catalog.find((kind) => kind.kind === box.kind);
|
|
623
1182
|
if (kind?.icon) {
|
|
@@ -635,7 +1194,17 @@ export class ProcessModeler extends BaseElement {
|
|
|
635
1194
|
description.textContent = box.description;
|
|
636
1195
|
element.append(description);
|
|
637
1196
|
}
|
|
638
|
-
element.addEventListener("
|
|
1197
|
+
element.addEventListener("keydown", event => {
|
|
1198
|
+
if (event.target === element && (event.key === "Enter" || event.key === " ")) {
|
|
1199
|
+
event.preventDefault();
|
|
1200
|
+
element.click();
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
element.addEventListener("click", event => {
|
|
1204
|
+
if (this.suppressClick && event.detail !== 0) {
|
|
1205
|
+
this.suppressClick = false;
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
639
1208
|
if (this.connecting && this.connecting !== box.id) {
|
|
640
1209
|
this.requestEdit({
|
|
641
1210
|
type: "connect",
|
|
@@ -644,8 +1213,30 @@ export class ProcessModeler extends BaseElement {
|
|
|
644
1213
|
});
|
|
645
1214
|
this.connecting = null;
|
|
646
1215
|
}
|
|
647
|
-
|
|
1216
|
+
if (event.shiftKey)
|
|
1217
|
+
this.selectMany(this.selectedIds.has(box.id) ? [...this.selectedIds].filter(id => id !== box.id) : [...this.selectedIds, box.id]);
|
|
1218
|
+
else
|
|
1219
|
+
this.select(box.id);
|
|
648
1220
|
});
|
|
1221
|
+
if (!this.disableConnections && !this.locked) {
|
|
1222
|
+
for (const side of ["north", "east", "south", "west"]) {
|
|
1223
|
+
const port = document.createElement("button");
|
|
1224
|
+
port.type = "button";
|
|
1225
|
+
port.setAttribute("part", "port");
|
|
1226
|
+
port.dataset.side = side;
|
|
1227
|
+
port.dataset.owner = box.id;
|
|
1228
|
+
port.setAttribute("aria-label", `Add or connect a step ${side} of ${box.title}`);
|
|
1229
|
+
port.addEventListener("click", event => {
|
|
1230
|
+
event.stopPropagation();
|
|
1231
|
+
if (!event.detail) {
|
|
1232
|
+
const pos = this.layoutValue.boxes[box.id];
|
|
1233
|
+
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[side];
|
|
1234
|
+
this.openChooser({ type: "add", from: box.id, fromSide: side, position: { x: pos.x + offsets[0], y: pos.y + offsets[1] } });
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
element.append(port);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
649
1240
|
world.append(element);
|
|
650
1241
|
}
|
|
651
1242
|
const lines = svgElement("svg");
|
|
@@ -653,48 +1244,93 @@ export class ProcessModeler extends BaseElement {
|
|
|
653
1244
|
lines.setAttribute("aria-hidden", "true");
|
|
654
1245
|
world.append(lines);
|
|
655
1246
|
for (const line of this.projection.lines) {
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
const x2 = to.x + (to.width ?? 220) / 2;
|
|
661
|
-
const y2 = to.y;
|
|
1247
|
+
const points = this.routedLines.get(line.id);
|
|
1248
|
+
if (!points.length)
|
|
1249
|
+
continue;
|
|
1250
|
+
const midpoint = lineMidpoint(points);
|
|
662
1251
|
const path = svgElement("polyline");
|
|
663
1252
|
path.setAttribute("part", "line");
|
|
664
|
-
path.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
{ x: x2, y: y2 },
|
|
668
|
-
]
|
|
1253
|
+
path.dataset.lineId = line.id;
|
|
1254
|
+
path.setAttribute("vector-effect", "non-scaling-stroke");
|
|
1255
|
+
path.setAttribute("points", points
|
|
669
1256
|
.map((point) => `${point.x},${point.y}`)
|
|
670
1257
|
.join(" "));
|
|
671
1258
|
if (line.dashed)
|
|
672
1259
|
path.setAttribute("stroke-dasharray", "6 4");
|
|
673
1260
|
lines.append(path);
|
|
1261
|
+
for (let i = 1; i < points.length - 2; i++) {
|
|
1262
|
+
const hit = svgElement("polyline");
|
|
1263
|
+
hit.setAttribute("part", "line-hit");
|
|
1264
|
+
hit.dataset.lineId = line.id;
|
|
1265
|
+
hit.dataset.segment = String(i);
|
|
1266
|
+
hit.setAttribute("vector-effect", "non-scaling-stroke");
|
|
1267
|
+
hit.setAttribute("points", `${points[i].x},${points[i].y} ${points[i + 1].x},${points[i + 1].y}`);
|
|
1268
|
+
lines.append(hit);
|
|
1269
|
+
}
|
|
674
1270
|
const label = document.createElement("div");
|
|
675
1271
|
label.setAttribute("part", "connection");
|
|
676
|
-
label.style.left = `${
|
|
677
|
-
label.style.top = `${
|
|
1272
|
+
label.style.left = `${midpoint.x}px`;
|
|
1273
|
+
label.style.top = `${midpoint.y}px`;
|
|
678
1274
|
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}` : ""}`;
|
|
679
1275
|
const text = document.createElement("span");
|
|
680
|
-
|
|
1276
|
+
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
1277
|
+
const branchIndex = this.projection.lines.filter(candidate => candidate.from === line.from).indexOf(line);
|
|
1278
|
+
text.textContent = line.label ?? (line.weight !== undefined ? String(line.weight) : /decision/i.test(source.kind) ? ["Yes", "No"][branchIndex] ?? `Route ${branchIndex + 1}` : /try/i.test(source.kind) && line.dashed ? "If it fails" : "");
|
|
681
1279
|
label.append(text);
|
|
1280
|
+
const actions = document.createElement("div");
|
|
1281
|
+
actions.setAttribute("part", "connection-actions");
|
|
1282
|
+
label.append(actions);
|
|
1283
|
+
if (points.length >= 4 && !this.locked) {
|
|
1284
|
+
const adjust = document.createElement("button");
|
|
1285
|
+
adjust.type = "button";
|
|
1286
|
+
adjust.textContent = "Route";
|
|
1287
|
+
adjust.dataset.segment = "1";
|
|
1288
|
+
adjust.dataset.lineId = line.id;
|
|
1289
|
+
adjust.setAttribute("aria-label", `Adjust route: ${names}. Use arrow keys`);
|
|
1290
|
+
adjust.onkeydown = event => {
|
|
1291
|
+
const direction = { ArrowLeft: [-10, 0], ArrowRight: [10, 0], ArrowUp: [0, -10], ArrowDown: [0, 10] }[event.key];
|
|
1292
|
+
if (!direction)
|
|
1293
|
+
return;
|
|
1294
|
+
event.preventDefault();
|
|
1295
|
+
event.stopPropagation();
|
|
1296
|
+
const current = routeProcessLine(line, this.layoutValue, this.projection);
|
|
1297
|
+
const vertical = current[1].x === current[2].x;
|
|
1298
|
+
for (const point of [current[1], current[2]]) {
|
|
1299
|
+
if (vertical)
|
|
1300
|
+
point.x += direction[0];
|
|
1301
|
+
else
|
|
1302
|
+
point.y += direction[1];
|
|
1303
|
+
}
|
|
1304
|
+
this.routeLine(line.id, current.slice(1, -1));
|
|
1305
|
+
Array.from(this.shadowRoot.querySelectorAll('[data-segment]')).find(button => button.dataset.lineId === line.id)?.focus();
|
|
1306
|
+
};
|
|
1307
|
+
actions.append(adjust);
|
|
1308
|
+
}
|
|
682
1309
|
for (const [type, title] of [
|
|
683
1310
|
["insert", "Insert a step here"],
|
|
684
1311
|
["disconnect", "Remove connection"],
|
|
685
1312
|
]) {
|
|
1313
|
+
if (type === "disconnect" && this.disableConnections)
|
|
1314
|
+
continue;
|
|
686
1315
|
const button = document.createElement("button");
|
|
687
1316
|
button.type = "button";
|
|
688
1317
|
button.textContent = type === "insert" ? "+" : "Remove";
|
|
689
1318
|
button.setAttribute("aria-label", `${title}: ${names}`);
|
|
690
1319
|
button.disabled = this.locked;
|
|
691
|
-
button.addEventListener("click", () =>
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1320
|
+
button.addEventListener("click", () => {
|
|
1321
|
+
const edit = {
|
|
1322
|
+
type,
|
|
1323
|
+
lineId: line.id,
|
|
1324
|
+
from: line.from,
|
|
1325
|
+
to: line.to,
|
|
1326
|
+
};
|
|
1327
|
+
if (type === "insert" && this.catalog.length) {
|
|
1328
|
+
this.openChooser(edit);
|
|
1329
|
+
}
|
|
1330
|
+
else
|
|
1331
|
+
this.requestEdit(edit);
|
|
1332
|
+
});
|
|
1333
|
+
actions.append(button);
|
|
698
1334
|
}
|
|
699
1335
|
world.append(label);
|
|
700
1336
|
}
|
|
@@ -734,36 +1370,40 @@ export class ProcessModeler extends BaseElement {
|
|
|
734
1370
|
button.textContent = kind.label;
|
|
735
1371
|
button.disabled = this.locked;
|
|
736
1372
|
button.draggable = !this.locked;
|
|
737
|
-
button.addEventListener("click", () => this.requestEdit({ type: "add", kind
|
|
1373
|
+
button.addEventListener("click", () => this.requestEdit({ type: "add", kind }));
|
|
738
1374
|
button.addEventListener("dragstart", (event) => event.dataTransfer?.setData("application/boe-process-kind", kind.kind));
|
|
739
1375
|
choices.append(button);
|
|
740
1376
|
}
|
|
741
1377
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
742
1378
|
canvas.ondragover = (event) => {
|
|
743
|
-
if (!this.locked)
|
|
1379
|
+
if (!this.locked) {
|
|
744
1380
|
event.preventDefault();
|
|
1381
|
+
this.markDropLine(this.canvasPoint(event));
|
|
1382
|
+
}
|
|
745
1383
|
};
|
|
1384
|
+
canvas.ondragleave = () => this.markDropLine(undefined);
|
|
746
1385
|
canvas.ondrop = (event) => {
|
|
747
1386
|
event.preventDefault();
|
|
748
1387
|
const kind = this.catalog.find((kind) => kind.kind ===
|
|
749
1388
|
event.dataTransfer?.getData("application/boe-process-kind"));
|
|
750
|
-
if (!kind)
|
|
1389
|
+
if (!kind || this.locked)
|
|
751
1390
|
return;
|
|
752
|
-
const
|
|
1391
|
+
const position = this.canvasPoint(event);
|
|
1392
|
+
const line = this.lineAt(position);
|
|
1393
|
+
const frame = this.boxAt(position, undefined, true);
|
|
753
1394
|
this.requestEdit({
|
|
754
|
-
type: "add",
|
|
1395
|
+
type: line ? "insert" : "add",
|
|
755
1396
|
kind,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
y: (event.clientY - rect.top - this.viewport.y) / this.viewport.zoom,
|
|
759
|
-
},
|
|
1397
|
+
lineId: line?.id, from: line?.from, to: line?.to, parentId: frame?.id,
|
|
1398
|
+
position,
|
|
760
1399
|
});
|
|
1400
|
+
this.markDropLine(undefined);
|
|
761
1401
|
};
|
|
762
1402
|
}
|
|
763
1403
|
renderChecks() {
|
|
764
1404
|
const checks = this.shadowRoot.querySelector("[part=checks]");
|
|
765
1405
|
checks.replaceChildren();
|
|
766
|
-
this.
|
|
1406
|
+
this.allChecks.forEach((check) => {
|
|
767
1407
|
const button = document.createElement("button");
|
|
768
1408
|
button.type = "button";
|
|
769
1409
|
button.textContent = check.message;
|
|
@@ -784,18 +1424,22 @@ export class ProcessModeler extends BaseElement {
|
|
|
784
1424
|
if (!this.isRendered)
|
|
785
1425
|
return;
|
|
786
1426
|
this.shadowRoot.querySelectorAll("[data-box-id]").forEach((element) => {
|
|
787
|
-
element.setAttribute("aria-
|
|
1427
|
+
element.setAttribute("aria-current", String(this.selectedIds.has(element.dataset.boxId)));
|
|
788
1428
|
const box = this.projection.boxes.find((box) => box.id === element.dataset.boxId);
|
|
789
|
-
element.dataset.invalid = String(this.
|
|
1429
|
+
element.dataset.invalid = String(this.allChecks.some((check) => check.boxId === box.id ||
|
|
790
1430
|
(check.path &&
|
|
791
1431
|
JSON.stringify(check.path) === JSON.stringify(box.path))));
|
|
792
1432
|
});
|
|
793
1433
|
const editor = this.shadowRoot.querySelector("[part=editor]");
|
|
794
1434
|
const selected = this.selected;
|
|
1435
|
+
this.shadowRoot.querySelector("[part=palette]").hidden = false;
|
|
1436
|
+
this.shadowRoot.querySelector('[part=selection-toolbar]').hidden = this.selectedIds.size === 0;
|
|
1437
|
+
this.shadowRoot.querySelectorAll('[part=selection-toolbar] button').forEach((button, index) => button.disabled = this.locked || (index < 2 && this.selectedIds.size < 2));
|
|
1438
|
+
this.renderPane();
|
|
795
1439
|
if (this.inspected === selected?.node &&
|
|
796
1440
|
this.inspectedId === selected?.id &&
|
|
797
|
-
editor.
|
|
798
|
-
editor.querySelector("
|
|
1441
|
+
editor.querySelector(`[part=inspector-heading]`)?.tagName === `H${this.headingLevel}`) {
|
|
1442
|
+
editor.querySelector("[part=inspector-heading]").textContent = selected?.title ?? "";
|
|
799
1443
|
return;
|
|
800
1444
|
}
|
|
801
1445
|
this.cleanupInspector?.();
|
|
@@ -804,7 +1448,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
804
1448
|
this.inspected = selected?.node;
|
|
805
1449
|
this.inspectedId = selected?.id;
|
|
806
1450
|
if (selected) {
|
|
807
|
-
const heading = document.createElement(
|
|
1451
|
+
const heading = document.createElement(`h${this.headingLevel}`);
|
|
1452
|
+
heading.setAttribute("part", "inspector-heading");
|
|
808
1453
|
heading.textContent = selected.title;
|
|
809
1454
|
editor.append(heading);
|
|
810
1455
|
this.cleanupInspector =
|
|
@@ -816,7 +1461,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
816
1461
|
button("undo").disabled = !this.history.canUndo || this.locked;
|
|
817
1462
|
button("redo").disabled = !this.history.canRedo || this.locked;
|
|
818
1463
|
button("tidy").disabled = this.locked;
|
|
819
|
-
button("connect").disabled = !this.selected || this.locked;
|
|
1464
|
+
button("connect").disabled = !this.selected || this.locked || this.disableConnections;
|
|
820
1465
|
button("delete").disabled = !this.selected || this.locked;
|
|
821
1466
|
button("lock").setAttribute("aria-pressed", String(this.locked));
|
|
822
1467
|
button("snap").setAttribute("aria-pressed", String(this.snapToGrid));
|
|
@@ -824,6 +1469,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
824
1469
|
paintViewport() {
|
|
825
1470
|
this.shadowRoot.querySelector("[part=world]").style.transform =
|
|
826
1471
|
`translate(${this.viewport.x}px,${this.viewport.y}px) scale(${this.viewport.zoom})`;
|
|
1472
|
+
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => port.style.transform = `scale(${1 / this.viewport.zoom})`);
|
|
827
1473
|
this.shadowRoot.querySelector("[data-command=reset]").textContent =
|
|
828
1474
|
`${Math.round(this.viewport.zoom * 100)}%`;
|
|
829
1475
|
const minimap = this.shadowRoot.querySelector("[part=minimap]");
|