@unofficialbox/box-open-elements 0.34.0 → 0.36.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/forms/code-editor.d.ts +1 -0
- package/dist/components/forms/code-editor.js +18 -1
- package/dist/native-types.d.ts +1 -1
- package/dist/patterns/process-modeler/automatic-routing.d.ts +22 -0
- package/dist/patterns/process-modeler/automatic-routing.js +329 -0
- package/dist/patterns/process-modeler/bridge.js +2 -0
- package/dist/patterns/process-modeler/design.d.ts +1 -1
- package/dist/patterns/process-modeler/design.js +79 -23
- package/dist/patterns/process-modeler/geometry.js +48 -10
- package/dist/patterns/process-modeler/model.d.ts +54 -2
- package/dist/patterns/process-modeler/model.js +23 -3
- package/dist/patterns/process-modeler/process-modeler.d.ts +14 -0
- package/dist/patterns/process-modeler/process-modeler.js +782 -197
- package/package.json +2 -2
|
@@ -6,7 +6,8 @@ import { KindPicker } from "../flow-builder/primitives.js";
|
|
|
6
6
|
import { arrangeProcess, routeProcessLine, lineMidpoint, nearProcessLine, roundedProcessPath } from "./geometry.js";
|
|
7
7
|
import { restoreProcessPositions, snapshotProcessPositions, snapshotProcessProjection, graphChecks, normalizeProcessSelectionPath } from "./bridge.js";
|
|
8
8
|
import { processModelerDesign } from "./design.js";
|
|
9
|
-
import { completeLayout, ProcessHistory, validateProjection, } from "./model.js";
|
|
9
|
+
import { completeLayout, isFlowBox, isFlowLine, ProcessHistory, validateProjection, } from "./model.js";
|
|
10
|
+
let actionControlSequence = 0;
|
|
10
11
|
const svgElement = (tag) => document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
11
12
|
const variableLabel = (text) => {
|
|
12
13
|
const label = document.createElement('label'), name = document.createElement('span');
|
|
@@ -75,6 +76,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
75
76
|
checks = [];
|
|
76
77
|
renderer;
|
|
77
78
|
cleanupInspector;
|
|
79
|
+
inspectorFieldCleanups = [];
|
|
80
|
+
pendingActionChoice;
|
|
81
|
+
quietActionFocus = false;
|
|
82
|
+
cleanupInspectorFields() { for (const cleanup of this.inspectorFieldCleanups.splice(0))
|
|
83
|
+
cleanup(); }
|
|
78
84
|
inspected;
|
|
79
85
|
inspectedId;
|
|
80
86
|
inspectedControls = "";
|
|
@@ -132,6 +138,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
132
138
|
readableValue = null;
|
|
133
139
|
lastDocument;
|
|
134
140
|
connectionsValue = [];
|
|
141
|
+
connectionsHelpValue = "Steps sign in with the host’s connections. Connections are set up by the host.";
|
|
135
142
|
variablesValue = [];
|
|
136
143
|
variablesEditableValue = false;
|
|
137
144
|
outlineValue = [];
|
|
@@ -159,6 +166,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
159
166
|
narrowValue = false;
|
|
160
167
|
suppressClick = false;
|
|
161
168
|
resizeObserver;
|
|
169
|
+
autoNoteHeights = new Set();
|
|
170
|
+
unplacedNotes = new Set();
|
|
162
171
|
routedLines = new Map();
|
|
163
172
|
computedChecks = [];
|
|
164
173
|
get selectedBoxes() { return this.projection.boxes.filter(box => this.selectedIds.has(box.id)); }
|
|
@@ -174,6 +183,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
174
183
|
}
|
|
175
184
|
get connections() { return this.connectionsValue; }
|
|
176
185
|
set connections(value) { this.connectionsValue = value; this.refresh(); }
|
|
186
|
+
/** Plain-text help describing where this host owns connection setup. */
|
|
187
|
+
get connectionsHelp() { return this.connectionsHelpValue; }
|
|
188
|
+
set connectionsHelp(value) { this.connectionsHelpValue = value; this.refresh(); }
|
|
177
189
|
get variables() { return this.variablesValue; }
|
|
178
190
|
set variables(value) {
|
|
179
191
|
this.variablesValue = value;
|
|
@@ -232,6 +244,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
232
244
|
this.lastProjection = "";
|
|
233
245
|
this.lastReadableProjection = "";
|
|
234
246
|
this.readableValue = null;
|
|
247
|
+
this.marquee = undefined;
|
|
248
|
+
this.pointers.clear();
|
|
249
|
+
this.autoNoteHeights.clear();
|
|
235
250
|
this.layoutValue = restoreProcessPositions(projection, options.positions ?? []);
|
|
236
251
|
this.clearMixedSelection();
|
|
237
252
|
this.history.clear();
|
|
@@ -307,7 +322,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
307
322
|
set embedMode(value) { this.toggleAttribute('embed-mode', value); }
|
|
308
323
|
arrangedLayout(layout) {
|
|
309
324
|
const arranged = this.model.arrange?.(this.projection) ?? arrangeProcess(this.projection);
|
|
310
|
-
|
|
325
|
+
const notes = Object.fromEntries(this.projection.boxes.filter(box => !isFlowBox(box) && this.layoutValue.boxes[box.id]).map(box => [box.id, this.layoutValue.boxes[box.id]]));
|
|
326
|
+
const boxes = { ...arranged?.boxes, ...notes, ...layout.boxes };
|
|
327
|
+
this.unplacedNotes = new Set(this.projection.boxes.filter(box => !isFlowBox(box) && !boxes[box.id]).map(box => box.id));
|
|
328
|
+
return completeLayout(this.projection, { ...layout, boxes });
|
|
311
329
|
}
|
|
312
330
|
get document() {
|
|
313
331
|
return this.documentValue;
|
|
@@ -342,6 +360,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
342
360
|
if (JSON.stringify(this.layoutValue) === JSON.stringify(value))
|
|
343
361
|
return;
|
|
344
362
|
this.layoutValue = structuredClone(value);
|
|
363
|
+
this.autoNoteHeights.clear();
|
|
345
364
|
if (!this.history.replaying)
|
|
346
365
|
this.history.clear();
|
|
347
366
|
this.refresh();
|
|
@@ -387,6 +406,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
387
406
|
return this.renderer;
|
|
388
407
|
}
|
|
389
408
|
set renderInspector(value) {
|
|
409
|
+
this.cleanupInspectorFields();
|
|
390
410
|
this.cleanupInspector?.();
|
|
391
411
|
this.cleanupInspector = undefined;
|
|
392
412
|
this.inspected = undefined;
|
|
@@ -400,6 +420,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
400
420
|
this.viewMenuDocument?.removeEventListener('pointerdown', this.dismissViewMenuOutside, true);
|
|
401
421
|
this.viewMenuDocument = undefined;
|
|
402
422
|
this.layoutEditSession++;
|
|
423
|
+
this.pendingActionChoice = undefined;
|
|
403
424
|
this.resizeObserver?.disconnect();
|
|
404
425
|
this.clearClipboard();
|
|
405
426
|
this.cancelPalettePointer();
|
|
@@ -408,6 +429,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
408
429
|
if (chooser)
|
|
409
430
|
dismissModal(chooser);
|
|
410
431
|
this.closeKeyboardChooser();
|
|
432
|
+
this.cleanupInspectorFields();
|
|
411
433
|
this.cleanupInspector?.();
|
|
412
434
|
this.cleanupInspector = undefined;
|
|
413
435
|
this.inspected = undefined;
|
|
@@ -481,7 +503,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
481
503
|
return this.model.connectionProblem(this.documentValue, { from, to, ...(line ? { ignoreLineId: line.id } : {}) }, this.projection) || null;
|
|
482
504
|
}
|
|
483
505
|
requestEdit(edit) {
|
|
484
|
-
if (this.
|
|
506
|
+
if (!this.isRendered && this.documentValue !== undefined) {
|
|
507
|
+
this.projection = this.model.project(this.documentValue);
|
|
508
|
+
validateProjection(this.projection);
|
|
509
|
+
}
|
|
510
|
+
const line = edit.lineId ? this.projection.lines.find(line => line.id === edit.lineId) : undefined;
|
|
511
|
+
const source = edit.from ? this.projection.boxes.find(box => box.id === edit.from) : undefined;
|
|
512
|
+
const inserted = edit.boxId ? this.projection.boxes.find(box => box.id === edit.boxId) : undefined;
|
|
513
|
+
if (line && !isFlowLine(line) && ["insert", "reattach", "reset-line", "edit-line"].includes(edit.type))
|
|
514
|
+
return;
|
|
515
|
+
if ((edit.type === "add" && source && !isFlowBox(source)) || (edit.type === "insert" && inserted && !isFlowBox(inserted)))
|
|
516
|
+
return;
|
|
517
|
+
if (this.locked || (this.disableConnections && (edit.type === "connect" || edit.type === "disconnect" || edit.type === 'reattach' || edit.type === 'edit-line')))
|
|
518
|
+
return;
|
|
519
|
+
if (edit.type === 'edit-line' && (!this.projection.lines.some(line => line.id === edit.lineId) || (edit.label === undefined && edit.weight === undefined) || (edit.label !== undefined && typeof edit.label !== 'string') || (edit.weight !== undefined && (!Number.isFinite(edit.weight) || edit.weight < 0))))
|
|
485
520
|
return;
|
|
486
521
|
if (edit.type === 'delete-selection' && (!edit.selection?.length || edit.selection.some(item => !['box', 'line', 'note'].includes(item.type) || typeof item.id !== 'string' || !item.id))) {
|
|
487
522
|
this.setStatus('The selected graph items are invalid', true);
|
|
@@ -540,6 +575,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
540
575
|
}
|
|
541
576
|
editAcceptance(edit) {
|
|
542
577
|
const before = this.layout;
|
|
578
|
+
const beforeAutoHeights = new Set(this.autoNoteHeights);
|
|
543
579
|
const beforeSelection = [...this.selectedIds];
|
|
544
580
|
const beforeItems = this.selection;
|
|
545
581
|
const previousIds = new Set(this.projection.boxes.map((box) => box.id));
|
|
@@ -553,8 +589,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
553
589
|
// replace the document. Publish only after the whole history entry exists.
|
|
554
590
|
this.acceptanceNotifications++;
|
|
555
591
|
try {
|
|
556
|
-
if (command.layout)
|
|
592
|
+
if (command.layout) {
|
|
593
|
+
this.autoNoteHeights.clear();
|
|
557
594
|
this.layoutValue = structuredClone(command.layout);
|
|
595
|
+
}
|
|
558
596
|
if (edit.type === 'reset-line' && edit.lineId)
|
|
559
597
|
delete this.layoutValue.lines?.[edit.lineId];
|
|
560
598
|
this.refresh();
|
|
@@ -575,9 +613,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
575
613
|
if (session !== this.layoutEditSession)
|
|
576
614
|
return;
|
|
577
615
|
const after = this.layout;
|
|
616
|
+
const afterAutoHeights = new Set(this.autoNoteHeights);
|
|
578
617
|
const acceptedItems = command.selection ? command.selection.map(item => ({ type: item.type, id: item.id })) : undefined;
|
|
579
618
|
const acceptedSelection = command.selectionIds ? [...command.selectionIds] : undefined;
|
|
580
|
-
const restore = (layout) => {
|
|
619
|
+
const restore = (layout, autoHeights) => {
|
|
620
|
+
this.autoNoteHeights = new Set(autoHeights);
|
|
581
621
|
this.layoutValue = structuredClone(layout);
|
|
582
622
|
this.refresh();
|
|
583
623
|
this.emitPositions();
|
|
@@ -585,7 +625,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
585
625
|
this.history.record({
|
|
586
626
|
undo: () => {
|
|
587
627
|
command.undo();
|
|
588
|
-
restore(before);
|
|
628
|
+
restore(before, beforeAutoHeights);
|
|
589
629
|
if (acceptedItems)
|
|
590
630
|
this.selectItems(beforeItems);
|
|
591
631
|
else if (acceptedSelection)
|
|
@@ -593,7 +633,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
593
633
|
},
|
|
594
634
|
redo: () => {
|
|
595
635
|
command.redo();
|
|
596
|
-
restore(after);
|
|
636
|
+
restore(after, afterAutoHeights);
|
|
597
637
|
if (acceptedItems)
|
|
598
638
|
this.selectItems(acceptedItems);
|
|
599
639
|
else if (acceptedSelection)
|
|
@@ -677,17 +717,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
677
717
|
commitLayout(next) {
|
|
678
718
|
const before = this.layout;
|
|
679
719
|
const after = structuredClone(next);
|
|
720
|
+
const beforeAutoHeights = new Set(this.autoNoteHeights);
|
|
721
|
+
const afterAutoHeights = new Set([...beforeAutoHeights].filter(id => before.boxes[id]?.height === after.boxes[id]?.height));
|
|
680
722
|
if (JSON.stringify(before) === JSON.stringify(after) || this.locked)
|
|
681
723
|
return;
|
|
682
|
-
const apply = (layout) => {
|
|
724
|
+
const apply = (layout, autoHeights) => {
|
|
725
|
+
this.autoNoteHeights = new Set(autoHeights);
|
|
683
726
|
this.layoutValue = structuredClone(layout);
|
|
684
727
|
this.refresh();
|
|
685
728
|
this.emitPositions();
|
|
686
729
|
};
|
|
687
|
-
apply(after);
|
|
730
|
+
apply(after, afterAutoHeights);
|
|
688
731
|
this.history.record({
|
|
689
|
-
undo: () => apply(before),
|
|
690
|
-
redo: () => apply(after),
|
|
732
|
+
undo: () => apply(before, beforeAutoHeights),
|
|
733
|
+
redo: () => apply(after, afterAutoHeights),
|
|
691
734
|
});
|
|
692
735
|
this.updateToolbar();
|
|
693
736
|
}
|
|
@@ -802,7 +845,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
802
845
|
this.commitLayout(next);
|
|
803
846
|
}
|
|
804
847
|
routeLine(id, points) {
|
|
805
|
-
if (!this.projection.lines.some((line) => line.id === id) ||
|
|
848
|
+
if (!this.projection.lines.some((line) => line.id === id && isFlowLine(line)) ||
|
|
806
849
|
points.some((point) => !Number.isFinite(point.x) || !Number.isFinite(point.y)))
|
|
807
850
|
return;
|
|
808
851
|
this.commitLayout({
|
|
@@ -815,7 +858,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
815
858
|
}
|
|
816
859
|
resetLine(id) {
|
|
817
860
|
const line = this.projection.lines.find(line => line.id === id);
|
|
818
|
-
if (this.locked || !line)
|
|
861
|
+
if (this.locked || !line || !isFlowLine(line))
|
|
819
862
|
return;
|
|
820
863
|
if (line.fromSide || line.toSide || line.points?.length) {
|
|
821
864
|
this.requestEdit({ type: 'reset-line', lineId: id });
|
|
@@ -1341,7 +1384,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
1341
1384
|
const root = this.shadowRoot;
|
|
1342
1385
|
const selected = this.selected;
|
|
1343
1386
|
root.querySelector('[part=process-title]').textContent = this.selectedIds.size > 1 ? `${this.selectedIds.size} steps selected` : selected?.title ?? this.processTitleValue;
|
|
1344
|
-
|
|
1387
|
+
const kind = selected && this.catalog.find(kind => kind.kind === selected.kind);
|
|
1388
|
+
const kindSummary = kind ? `${kind.label}${kind.description !== undefined ? `. ${kind.description}` : ''}` : selected?.kind;
|
|
1389
|
+
root.querySelector('[part=process-heading]').toggleAttribute('data-selected', Boolean(selected));
|
|
1390
|
+
root.querySelector('[part=process-summary]').textContent = this.selectedIds.size > 1 ? 'Arrange or group these steps' : selected ? kindSummary : this.processSummaryValue;
|
|
1345
1391
|
root.querySelector('[part=pane-tabs]').hidden = Boolean(selected);
|
|
1346
1392
|
root.querySelectorAll('[part=pane-tabs] button').forEach(button => {
|
|
1347
1393
|
button.textContent = button.dataset.pane === "Checks" ? `Checks ${this.allChecks.length}` : button.dataset.pane ?? "";
|
|
@@ -1591,26 +1637,71 @@ export class ProcessModeler extends BaseElement {
|
|
|
1591
1637
|
summary('saved-result-summary', 'Saved by steps', savedRows, 'No step saves its result yet. Set “Save the result as” on a step.');
|
|
1592
1638
|
}
|
|
1593
1639
|
else if (this.activePane === "Connections") {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
const
|
|
1600
|
-
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1640
|
+
const help = document.createElement('p');
|
|
1641
|
+
help.setAttribute('part', 'connections-help');
|
|
1642
|
+
help.textContent = this.connectionsHelp;
|
|
1643
|
+
content.append(help);
|
|
1644
|
+
if (this.connections.length) {
|
|
1645
|
+
const list = document.createElement('ul');
|
|
1646
|
+
list.setAttribute('part', 'connections-list');
|
|
1647
|
+
for (const connection of this.connections) {
|
|
1648
|
+
const row = document.createElement('li');
|
|
1649
|
+
row.setAttribute('part', 'connection-row');
|
|
1650
|
+
const text = document.createElement('span');
|
|
1651
|
+
const name = document.createElement('b');
|
|
1652
|
+
name.textContent = connection.name;
|
|
1653
|
+
const kind = document.createElement('em');
|
|
1654
|
+
kind.textContent = `${connection.kind}${connection.signIn ? ` · ${connection.signIn}` : ''}`;
|
|
1655
|
+
text.append(name, document.createTextNode(' '), kind);
|
|
1656
|
+
row.append(text);
|
|
1657
|
+
list.append(row);
|
|
1658
|
+
}
|
|
1659
|
+
content.append(list);
|
|
1660
|
+
}
|
|
1661
|
+
else {
|
|
1662
|
+
const empty = document.createElement('p');
|
|
1663
|
+
empty.setAttribute('part', 'connections-help');
|
|
1664
|
+
empty.textContent = 'None yet.';
|
|
1665
|
+
content.append(empty);
|
|
1605
1666
|
}
|
|
1667
|
+
const actions = document.createElement('div');
|
|
1668
|
+
actions.setAttribute('part', 'connections-actions');
|
|
1606
1669
|
const setup = document.createElement('button');
|
|
1607
1670
|
setup.type = 'button';
|
|
1608
|
-
setup.textContent = '
|
|
1671
|
+
setup.textContent = 'Set up connections';
|
|
1609
1672
|
setup.onclick = () => emit(this, 'connection-setup-request', {});
|
|
1610
|
-
|
|
1673
|
+
actions.append(setup);
|
|
1674
|
+
content.append(actions);
|
|
1611
1675
|
}
|
|
1612
1676
|
else if (this.activePane === "Shortcuts") {
|
|
1613
|
-
|
|
1677
|
+
const shortcuts = document.createElement('dl');
|
|
1678
|
+
shortcuts.setAttribute('part', 'shortcut-list');
|
|
1679
|
+
for (const [term, description] of [
|
|
1680
|
+
['Drag the canvas', 'Move around'], ['Scroll', 'Move around'], ['`Ctrl` + scroll, or pinch', 'Zoom'], ['`Shift` + drag', 'Select an area'],
|
|
1681
|
+
['Drag a dot beside a step', 'Draw a line. Drop on a box to attach it (the side follows the layout), or on one of its points to pin it there'],
|
|
1682
|
+
['Click a dot', 'Add a connected step that way'], ['`Alt` `Ctrl` + arrow', 'Add a connected step that way'],
|
|
1683
|
+
["Drag a line's handle", 'Move that part of the line by hand (Reset line undoes it)'], ["Drag a line's end", 'Attach it somewhere else'],
|
|
1684
|
+
['`N`', 'Add the next step'], ['Arrow keys', 'Nudge by one grid square (`Shift` for four)'], ['`Alt` + arrow', 'Go to the nearest step that way'],
|
|
1685
|
+
['`Enter`', 'Edit the selected step'], ['`Delete`', 'Delete the selection'], ['`Ctrl` `Z` / `Shift` `Z`', 'Undo / redo'],
|
|
1686
|
+
['`Ctrl` `C` `V` `D`', 'Copy, paste, duplicate'], ['`Shift` `1`', 'Fit the whole process'], ['`+` `-` `0`', 'Zoom in, out, 100%'],
|
|
1687
|
+
['Steps view', ''], ['`↑` `↓`', 'Go to the step above or below'], ['`Alt` + `↑` `↓`', 'Move the step up or down its path'], ['`Delete`', 'Delete the step'],
|
|
1688
|
+
['Pinch, or `Ctrl` + scroll', 'Zoom'], ['Code view', ''], ['`Ctrl` `Space`', 'Suggest what can go here'], ['`F8`', 'Go to the problem'], ['`Esc`', 'Leave the editor'],
|
|
1689
|
+
]) {
|
|
1690
|
+
const dt = document.createElement('dt'), dd = document.createElement('dd');
|
|
1691
|
+
if (!description)
|
|
1692
|
+
dt.setAttribute('part', 'shortcut-heading');
|
|
1693
|
+
for (const [element, text] of [[dt, term], [dd, description]]) {
|
|
1694
|
+
text.split('`').forEach((run, index) => { if (index % 2) {
|
|
1695
|
+
const key = document.createElement('kbd');
|
|
1696
|
+
key.textContent = run;
|
|
1697
|
+
element.append(key);
|
|
1698
|
+
}
|
|
1699
|
+
else
|
|
1700
|
+
element.append(document.createTextNode(run)); });
|
|
1701
|
+
}
|
|
1702
|
+
shortcuts.append(dt, dd);
|
|
1703
|
+
}
|
|
1704
|
+
content.append(shortcuts);
|
|
1614
1705
|
}
|
|
1615
1706
|
}
|
|
1616
1707
|
requestVariableEdit(edit) {
|
|
@@ -1621,6 +1712,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
1621
1712
|
}
|
|
1622
1713
|
}
|
|
1623
1714
|
selectionDimensions(box) {
|
|
1715
|
+
if (!isFlowBox(box)) {
|
|
1716
|
+
const p = this.layoutValue.boxes[box.id];
|
|
1717
|
+
return { width: p.width ?? 208, height: p.height ?? 64 };
|
|
1718
|
+
}
|
|
1624
1719
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape;
|
|
1625
1720
|
const compact = shape === 'gateway' || shape === 'event';
|
|
1626
1721
|
const position = this.layoutValue.boxes[box.id];
|
|
@@ -1653,12 +1748,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
1653
1748
|
this.arrangeSelection(width >= height ? 'middle' : 'center');
|
|
1654
1749
|
}
|
|
1655
1750
|
tidySelection() {
|
|
1656
|
-
|
|
1751
|
+
const flow = this.selectedBoxes.filter(isFlowBox);
|
|
1752
|
+
if (this.locked || flow.length < 2)
|
|
1657
1753
|
return;
|
|
1658
|
-
if (this.requestLayoutEdit('tidy',
|
|
1754
|
+
if (this.requestLayoutEdit('tidy', flow.map(box => box.id)))
|
|
1659
1755
|
return;
|
|
1660
1756
|
const next = this.layout;
|
|
1661
|
-
const roots = this.selectedBoxes.filter(box => {
|
|
1757
|
+
const roots = this.selectedBoxes.filter(isFlowBox).filter(box => {
|
|
1662
1758
|
let parent = box.parentId;
|
|
1663
1759
|
while (parent) {
|
|
1664
1760
|
if (this.selectedIds.has(parent))
|
|
@@ -1710,7 +1806,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
1710
1806
|
arrangeSelection(action) {
|
|
1711
1807
|
if (this.locked || this.selectedBoxes.length < 2)
|
|
1712
1808
|
return;
|
|
1713
|
-
const boxes = this.selectedBoxes;
|
|
1809
|
+
const boxes = this.selectedBoxes.filter(isFlowBox);
|
|
1810
|
+
if (boxes.length < 2)
|
|
1811
|
+
return;
|
|
1714
1812
|
const next = this.layout;
|
|
1715
1813
|
const sorted = [...boxes].filter(box => {
|
|
1716
1814
|
let parent = box.parentId;
|
|
@@ -1795,13 +1893,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
1795
1893
|
this.commitLayout(next);
|
|
1796
1894
|
}
|
|
1797
1895
|
makeSection(title = 'New section') {
|
|
1798
|
-
|
|
1896
|
+
const flow = this.selectedBoxes.filter(isFlowBox);
|
|
1897
|
+
if (this.locked || !flow.length)
|
|
1799
1898
|
return;
|
|
1800
|
-
if (this.requestLayoutEdit('make-section',
|
|
1899
|
+
if (this.requestLayoutEdit('make-section', flow.map(box => box.id), title))
|
|
1801
1900
|
return;
|
|
1802
|
-
if (
|
|
1901
|
+
if (flow.length < 2)
|
|
1803
1902
|
return;
|
|
1804
|
-
const positions =
|
|
1903
|
+
const positions = flow.map(box => this.layoutValue.boxes[box.id]);
|
|
1805
1904
|
const x = Math.min(...positions.map(position => position.x)) - 24;
|
|
1806
1905
|
const y = Math.min(...positions.map(position => position.y)) - 48;
|
|
1807
1906
|
const right = Math.max(...positions.map(position => position.x + (position.width ?? 224))) + 24;
|
|
@@ -2042,6 +2141,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2042
2141
|
const target = event.target;
|
|
2043
2142
|
if (target.closest("input,textarea,select,[contenteditable],button,summary"))
|
|
2044
2143
|
return;
|
|
2144
|
+
if (target.closest('[part=canvas]') && !event.ctrlKey && !event.metaKey && !event.altKey && ['+', '=', '-', '0'].includes(event.key)) {
|
|
2145
|
+
event.preventDefault();
|
|
2146
|
+
this.zoomBy(event.key === '0' ? 1 / this.viewport.zoom : event.key === '-' ? 1 / 1.2 : 1.2);
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2045
2149
|
if (event.key === ' ') {
|
|
2046
2150
|
event.preventDefault();
|
|
2047
2151
|
this.spacePressed = true;
|
|
@@ -2116,7 +2220,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2116
2220
|
this.requestEdit({ type: 'disconnect', lineId: this.selectedLine.id });
|
|
2117
2221
|
return;
|
|
2118
2222
|
}
|
|
2119
|
-
if ((event.key === "n" || event.key === "N") && box && !this.locked && this.catalog.length && !['finish', 'end'].includes(box.kind)) {
|
|
2223
|
+
if ((event.key === "n" || event.key === "N") && box && isFlowBox(box) && !this.locked && this.catalog.length && !['finish', 'end'].includes(box.kind)) {
|
|
2120
2224
|
event.preventDefault();
|
|
2121
2225
|
this.openKeyboardChooser(box);
|
|
2122
2226
|
return;
|
|
@@ -2153,7 +2257,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2153
2257
|
this.commitLayout(next);
|
|
2154
2258
|
return;
|
|
2155
2259
|
}
|
|
2156
|
-
|
|
2260
|
+
if (direction && event.ctrlKey && event.altKey && box && !isFlowBox(box)) {
|
|
2261
|
+
event.preventDefault();
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const canInsertDirection = event.ctrlKey && event.altKey && box && isFlowBox(box) && !this.locked && this.catalog.length && !this.disableConnections;
|
|
2157
2265
|
if (direction && !canInsertDirection && (event.altKey || !box || this.locked)) {
|
|
2158
2266
|
event.preventDefault();
|
|
2159
2267
|
this.navigateSelection(direction);
|
|
@@ -2189,6 +2297,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
2189
2297
|
}
|
|
2190
2298
|
if (direction) {
|
|
2191
2299
|
event.preventDefault();
|
|
2300
|
+
if (event.ctrlKey && event.altKey && !isFlowBox(box))
|
|
2301
|
+
return;
|
|
2192
2302
|
if (event.ctrlKey && event.altKey && this.catalog.length && !this.disableConnections) {
|
|
2193
2303
|
const side = direction[0] < 0 ? "west" : direction[0] > 0 ? "east" : direction[1] < 0 ? "north" : "south";
|
|
2194
2304
|
const pos = this.layoutValue.boxes[box.id];
|
|
@@ -2287,7 +2397,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2287
2397
|
return;
|
|
2288
2398
|
}
|
|
2289
2399
|
if (event.button !== 0 ||
|
|
2290
|
-
event.target.closest("[part=connection],button,input,select,textarea,[contenteditable]"))
|
|
2400
|
+
event.target.closest("[part=connection],[part=line-hit],button,input,select,textarea,[contenteditable]"))
|
|
2291
2401
|
return;
|
|
2292
2402
|
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
2293
2403
|
if (this.pointers.size === 2) {
|
|
@@ -2305,7 +2415,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2305
2415
|
this.select(id);
|
|
2306
2416
|
if (!id && event.shiftKey && !this.locked) {
|
|
2307
2417
|
const point = this.canvasPoint(event);
|
|
2308
|
-
this.marquee = { pointerId: event.pointerId, start: point, point };
|
|
2418
|
+
this.marquee = { pointerId: event.pointerId, start: point, point, add: this.selection };
|
|
2309
2419
|
canvas.setPointerCapture?.(event.pointerId);
|
|
2310
2420
|
event.preventDefault();
|
|
2311
2421
|
return;
|
|
@@ -2319,6 +2429,23 @@ export class ProcessModeler extends BaseElement {
|
|
|
2319
2429
|
panY: this.viewport.y,
|
|
2320
2430
|
};
|
|
2321
2431
|
}
|
|
2432
|
+
selectMarquee(start, point, add) {
|
|
2433
|
+
const items = [...add];
|
|
2434
|
+
const ids = new Set(add.filter(item => item.type === 'box').map(item => item.id));
|
|
2435
|
+
for (const box of this.projection.boxes) {
|
|
2436
|
+
if (box.kind === 'section')
|
|
2437
|
+
continue;
|
|
2438
|
+
const p = this.layoutValue.boxes[box.id], size = this.selectionDimensions(box);
|
|
2439
|
+
if (p.x < Math.max(start.x, point.x) && p.x + size.width > Math.min(start.x, point.x) && p.y < Math.max(start.y, point.y) && p.y + size.height > Math.min(start.y, point.y)) {
|
|
2440
|
+
ids.add(box.id);
|
|
2441
|
+
items.push({ type: 'box', id: box.id });
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
for (const line of this.projection.lines)
|
|
2445
|
+
if (ids.has(line.from) && ids.has(line.to))
|
|
2446
|
+
items.push({ type: 'line', id: line.id });
|
|
2447
|
+
this.selectItems(items);
|
|
2448
|
+
}
|
|
2322
2449
|
distance() {
|
|
2323
2450
|
const [a, b] = [...this.pointers.values()];
|
|
2324
2451
|
return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
|
@@ -2389,7 +2516,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2389
2516
|
return;
|
|
2390
2517
|
}
|
|
2391
2518
|
if (this.marquee) {
|
|
2392
|
-
|
|
2519
|
+
const marquee = this.marquee;
|
|
2520
|
+
marquee.point = point;
|
|
2521
|
+
this.selectMarquee(marquee.start, point, marquee.add);
|
|
2522
|
+
if (this.marquee !== marquee)
|
|
2523
|
+
return;
|
|
2393
2524
|
let region = this.shadowRoot.querySelector('[part=marquee]');
|
|
2394
2525
|
if (!region) {
|
|
2395
2526
|
region = document.createElement("div");
|
|
@@ -2481,6 +2612,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2481
2612
|
const pos = this.layoutValue.boxes[port.id];
|
|
2482
2613
|
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[port.side];
|
|
2483
2614
|
const source = this.projection.boxes.find(box => box.id === port.id);
|
|
2615
|
+
if (!isFlowBox(source)) {
|
|
2616
|
+
this.connecting = source.id;
|
|
2617
|
+
this.setStatus(`Select a step to attach ${source.title}`);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2484
2620
|
const anchor = Array.from(this.shadowRoot.querySelectorAll('[part=port]'))
|
|
2485
2621
|
.find(element => element.dataset.owner === port.id && element.dataset.side === port.side);
|
|
2486
2622
|
this.openAnchoredChooser(this.nextEdit(source, port.side, { x: pos.x + offsets[0], y: pos.y + offsets[1] }), this.nextChooserTitle(source, port.side), anchor);
|
|
@@ -2491,6 +2627,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
2491
2627
|
this.requestEdit({ type: "connect", from: port.id, to: target.id, fromSide: port.side, toSide: this.sideAt(target, point) });
|
|
2492
2628
|
else {
|
|
2493
2629
|
const source = this.projection.boxes.find(box => box.id === port.id);
|
|
2630
|
+
if (!isFlowBox(source)) {
|
|
2631
|
+
this.setStatus("Drop on a step to attach the note");
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2494
2634
|
const frame = this.boxAt(point, undefined, true);
|
|
2495
2635
|
if ((frame?.id ?? null) !== (source.parentId ?? null))
|
|
2496
2636
|
this.setStatus('Drop inside the same frame to add a connected step');
|
|
@@ -2533,10 +2673,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
2533
2673
|
return;
|
|
2534
2674
|
}
|
|
2535
2675
|
if (this.marquee) {
|
|
2536
|
-
const { start } = this.marquee;
|
|
2676
|
+
const { start, add } = this.marquee;
|
|
2537
2677
|
this.marquee = undefined;
|
|
2538
2678
|
if (commit)
|
|
2539
|
-
this.
|
|
2679
|
+
this.selectMarquee(start, point, add);
|
|
2540
2680
|
this.shadowRoot.querySelector('[part=marquee]')?.remove();
|
|
2541
2681
|
this.pointers.delete(event.pointerId);
|
|
2542
2682
|
return;
|
|
@@ -2554,7 +2694,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
2554
2694
|
this.suppressClick = true;
|
|
2555
2695
|
const position = this.alignedPoint(drag.id, drag.position.x + dx / this.viewport.zoom, drag.position.y + dy / this.viewport.zoom);
|
|
2556
2696
|
const line = this.lineAt(point, drag.id);
|
|
2557
|
-
const
|
|
2697
|
+
const draggedBox = this.projection.boxes.find(box => box.id === drag.id);
|
|
2698
|
+
const frame = isFlowBox(draggedBox) ? this.boxAt(point, drag.id, true) : this.projection.boxes.find(box => box.id === draggedBox.parentId);
|
|
2558
2699
|
if (line) {
|
|
2559
2700
|
const box = this.projection.boxes.find(box => box.id === drag.id);
|
|
2560
2701
|
const { width, height } = this.selectionDimensions(box);
|
|
@@ -2621,14 +2762,15 @@ export class ProcessModeler extends BaseElement {
|
|
|
2621
2762
|
return sides.find(([, x, y]) => Math.hypot(point.x - x, point.y - y) <= 12 / this.viewport.zoom)?.[0];
|
|
2622
2763
|
}
|
|
2623
2764
|
lineAt(point, except) {
|
|
2624
|
-
return this.projection.lines.find(line => line.from !== except && line.to !== except && nearProcessLine(point, this.routedLines.get(line.id) ?? [], 16 / this.viewport.zoom));
|
|
2765
|
+
return this.projection.lines.find(line => isFlowLine(line) && (!except || isFlowBox(this.projection.boxes.find(box => box.id === except))) && line.from !== except && line.to !== except && nearProcessLine(point, this.routedLines.get(line.id) ?? [], 16 / this.viewport.zoom));
|
|
2625
2766
|
}
|
|
2626
2767
|
markDropLine(point, except) {
|
|
2627
2768
|
this.dropLine = point ? this.lineAt(point, except)?.id : undefined;
|
|
2628
2769
|
this.paintDropLine();
|
|
2629
2770
|
}
|
|
2630
2771
|
paintDropLine() {
|
|
2631
|
-
this.shadowRoot.querySelectorAll('[part=line]').forEach(path => { const active = path.dataset.lineId === this.dropLine; path.dataset.drop = String(active);
|
|
2772
|
+
this.shadowRoot.querySelectorAll('[part=line]').forEach(path => { const active = path.dataset.lineId === this.dropLine; path.dataset.drop = String(active); if (path.dataset.role !== 'association')
|
|
2773
|
+
path.setAttribute('marker-end', `url(#${active || path.dataset.selected === 'true' ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`); });
|
|
2632
2774
|
}
|
|
2633
2775
|
alignedPoint(id, x, y) {
|
|
2634
2776
|
const world = this.shadowRoot.querySelector('[part=world]');
|
|
@@ -2676,14 +2818,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
2676
2818
|
}
|
|
2677
2819
|
chooser.addEventListener('close', () => this.shadowRoot?.querySelector('[part=ghost-box]')?.remove(), { once: true });
|
|
2678
2820
|
const picker = chooser.querySelector('box-kind-picker');
|
|
2679
|
-
picker.catalog = this.catalog.filter(kind => !kind.placement);
|
|
2821
|
+
picker.catalog = this.catalog.filter(kind => kind.addable !== false && !kind.placement);
|
|
2680
2822
|
picker.refresh();
|
|
2681
2823
|
promoteModal(chooser);
|
|
2682
2824
|
picker.focus();
|
|
2683
2825
|
}
|
|
2684
2826
|
nextEdit(box, side, position) {
|
|
2685
2827
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape;
|
|
2686
|
-
const outgoing = this.projection.lines.filter(line => line.from === box.id);
|
|
2828
|
+
const outgoing = this.projection.lines.filter(line => isFlowLine(line) && line.from === box.id);
|
|
2687
2829
|
const placement = side ? { source: 'direction', side } : { source: 'next' };
|
|
2688
2830
|
if ((!side || side === 'east') && shape !== 'gateway' && outgoing.length === 1)
|
|
2689
2831
|
return { type: 'insert', lineId: outgoing[0].id, from: box.id, to: outgoing[0].to, placement };
|
|
@@ -2710,6 +2852,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
2710
2852
|
this.openAnchoredChooser(edit, title, anchor, returnFocus ?? source ?? this.shadowRoot.querySelector('[part=canvas]'));
|
|
2711
2853
|
}
|
|
2712
2854
|
openLineChooser(line, returnFocus) {
|
|
2855
|
+
if (!isFlowLine(line))
|
|
2856
|
+
return;
|
|
2713
2857
|
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
2714
2858
|
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
2715
2859
|
const points = this.routedLines.get(line.id) ?? routeProcessLine(line, this.layoutValue, this.projection);
|
|
@@ -2736,7 +2880,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2736
2880
|
};
|
|
2737
2881
|
openKeyboardChooser(box) {
|
|
2738
2882
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape ?? (box.frame ? 'frame' : 'task');
|
|
2739
|
-
const outgoing = this.projection.lines.filter(line => line.from === box.id);
|
|
2883
|
+
const outgoing = this.projection.lines.filter(line => isFlowLine(line) && line.from === box.id);
|
|
2740
2884
|
const target = outgoing.length === 1 ? this.projection.boxes.find(candidate => candidate.id === outgoing[0].to) : undefined;
|
|
2741
2885
|
const insert = shape !== 'gateway' && outgoing.length === 1 && target;
|
|
2742
2886
|
const anchor = Array.from(this.shadowRoot.querySelectorAll('[data-box-id]'))
|
|
@@ -2753,7 +2897,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2753
2897
|
this.keyboardInsertion = edit;
|
|
2754
2898
|
chooser.querySelector('[part=chooser-title]').textContent = title;
|
|
2755
2899
|
const picker = chooser.querySelector('box-kind-picker');
|
|
2756
|
-
picker.catalog = this.catalog.filter(kind => !kind.placement);
|
|
2900
|
+
picker.catalog = this.catalog.filter(kind => kind.addable !== false && !kind.placement);
|
|
2757
2901
|
picker.refresh();
|
|
2758
2902
|
this.keyboardReturn = returnFocus ?? anchor;
|
|
2759
2903
|
if (!anchor || !promotePopover(chooser)) {
|
|
@@ -2800,6 +2944,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2800
2944
|
else
|
|
2801
2945
|
this.projection = { boxes: [], lines: [] };
|
|
2802
2946
|
this.layoutValue = this.arrangedLayout(this.layoutValue);
|
|
2947
|
+
this.measureGraphNotes();
|
|
2803
2948
|
this.routedLines = new Map(this.projection.lines.map(line => [line.id, routeProcessLine(line, this.layoutValue, this.projection)]));
|
|
2804
2949
|
this.computedChecks = this.collectChecks();
|
|
2805
2950
|
if (this.selectedId && !this.selected)
|
|
@@ -2812,7 +2957,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2812
2957
|
if (!this.selectedId)
|
|
2813
2958
|
this.selectedId = this.selectedIds.values().next().value ?? null;
|
|
2814
2959
|
const serialized = JSON.stringify({
|
|
2815
|
-
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, localVariables: box.localVariables, localVariablesEditable: box.localVariablesEditable, savedResult: box.savedResult })),
|
|
2960
|
+
boxes: this.projection.boxes.map(box => ({ id: box.id, role: box.role, kind: box.kind, title: box.title, description: box.description, path: box.path, fingerprint: box.fingerprint, frame: box.frame, loopMark: box.loopMark, parentId: box.parentId, localVariables: box.localVariables, localVariablesEditable: box.localVariablesEditable, savedResult: box.savedResult })),
|
|
2816
2961
|
lines: this.projection.lines,
|
|
2817
2962
|
});
|
|
2818
2963
|
if (this.documentValue !== undefined && (serialized !== this.lastProjection || this.documentValue !== this.lastDocument)) {
|
|
@@ -2862,9 +3007,44 @@ export class ProcessModeler extends BaseElement {
|
|
|
2862
3007
|
(note ?? this.shadowRoot.querySelector('[part=canvas]'))?.focus({ preventScroll: true });
|
|
2863
3008
|
}
|
|
2864
3009
|
}
|
|
3010
|
+
measureGraphNotes() {
|
|
3011
|
+
const world = this.shadowRoot.querySelector('[part=world]');
|
|
3012
|
+
for (const box of this.projection.boxes.filter(box => !isFlowBox(box))) {
|
|
3013
|
+
const position = this.layoutValue.boxes[box.id];
|
|
3014
|
+
if (position.height !== undefined && !this.autoNoteHeights.has(box.id))
|
|
3015
|
+
continue;
|
|
3016
|
+
this.autoNoteHeights.add(box.id);
|
|
3017
|
+
const element = document.createElement('div');
|
|
3018
|
+
element.setAttribute('part', 'box');
|
|
3019
|
+
element.dataset.shape = 'note';
|
|
3020
|
+
element.style.cssText = `visibility:hidden;pointer-events:none;height:auto;width:${position.width ?? 208}px`;
|
|
3021
|
+
const body = document.createElement('div');
|
|
3022
|
+
body.setAttribute('part', 'note-body');
|
|
3023
|
+
body.textContent = box.description ?? box.title;
|
|
3024
|
+
element.append(body);
|
|
3025
|
+
world.append(element);
|
|
3026
|
+
const bounds = body.getBoundingClientRect();
|
|
3027
|
+
if (bounds.width > 0 && bounds.height > 0)
|
|
3028
|
+
position.height = bounds.height * (position.width ?? 208) / bounds.width;
|
|
3029
|
+
element.remove();
|
|
3030
|
+
}
|
|
3031
|
+
// Only library fallback positions need collision placement; a host's
|
|
3032
|
+
// arrangement is authoritative, and a containing frame is not an obstacle.
|
|
3033
|
+
for (const box of this.projection.boxes.filter(box => this.unplacedNotes.has(box.id))) {
|
|
3034
|
+
const position = this.layoutValue.boxes[box.id], ancestors = new Set();
|
|
3035
|
+
for (let parent = box.parentId; parent; parent = this.projection.boxes.find(b => b.id === parent)?.parentId)
|
|
3036
|
+
ancestors.add(parent);
|
|
3037
|
+
let overlaps;
|
|
3038
|
+
do {
|
|
3039
|
+
overlaps = Object.entries(this.layoutValue.boxes).filter(([id, p]) => id !== box.id && !ancestors.has(id) && position.x < p.x + (p.width ?? 224) && position.x + (position.width ?? 208) > p.x && position.y < p.y + (p.height ?? 64) && position.y + (position.height ?? 64) > p.y).map(([, p]) => p);
|
|
3040
|
+
if (overlaps.length)
|
|
3041
|
+
position.y = Math.max(...overlaps.map(p => p.y + (p.height ?? 64))) + 60;
|
|
3042
|
+
} while (overlaps.length);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
2865
3045
|
get allChecks() { return this.computedChecks; }
|
|
2866
3046
|
collectChecks() {
|
|
2867
|
-
const routes = this.projection.lines.filter(line => !this.routedLines.get(line.id)?.length)
|
|
3047
|
+
const routes = this.projection.lines.filter(line => isFlowLine(line) && !this.routedLines.get(line.id)?.length)
|
|
2868
3048
|
.map(line => ({ message: "A connection is blocked by overlapping steps. Move a step or adjust its route.", boxId: line.from }));
|
|
2869
3049
|
const hostChecks = this.documentValue === undefined ? undefined : this.model.validate?.(this.documentValue, this.projection);
|
|
2870
3050
|
return [...this.checks, ...(hostChecks ?? graphChecks(this.projection)), ...routes];
|
|
@@ -2900,28 +3080,38 @@ export class ProcessModeler extends BaseElement {
|
|
|
2900
3080
|
element.setAttribute("part", box.frame ? "frame" : "box");
|
|
2901
3081
|
element.dataset.boxId = box.id;
|
|
2902
3082
|
const kind = this.catalog.find((entry) => entry.kind === box.kind);
|
|
2903
|
-
const shape = box.shape ?? kind?.shape ?? (box.frame ? "frame" : "task");
|
|
3083
|
+
const shape = !isFlowBox(box) ? "note" : box.shape ?? kind?.shape ?? (box.frame ? "frame" : "task");
|
|
2904
3084
|
element.dataset.shape = shape;
|
|
3085
|
+
if (!isFlowBox(box))
|
|
3086
|
+
element.setAttribute("aria-roledescription", "note");
|
|
2905
3087
|
element.dataset.kind = box.kind;
|
|
2906
3088
|
element.dataset.tone = kind?.tone === "accent" ? "accent" : "neutral";
|
|
2907
3089
|
const problem = this.allChecks.find(check => check.boxId === box.id || (check.path && JSON.stringify(check.path) === JSON.stringify(box.path)));
|
|
2908
|
-
const incoming = this.projection.lines.filter(line => line.to === box.id).length;
|
|
2909
|
-
const outgoing = this.projection.lines.filter(line => line.from === box.id).length;
|
|
2910
|
-
element.setAttribute("aria-label", [`${kind?.label ?? box.kind}: ${box.title}
|
|
3090
|
+
const incoming = this.projection.lines.filter(line => isFlowLine(line) && line.to === box.id).length;
|
|
3091
|
+
const outgoing = this.projection.lines.filter(line => isFlowLine(line) && line.from === box.id).length;
|
|
3092
|
+
element.setAttribute("aria-label", [`${kind?.label ?? box.kind}: ${box.title}${isFlowBox(box) ? `. ${incoming} in, ${outgoing} out` : ""}`, problem ? `Needs attention: ${problem.title || problem.message}` : ""]
|
|
2911
3093
|
.filter(Boolean)
|
|
2912
3094
|
.join(". "));
|
|
2913
3095
|
element.setAttribute("aria-current", String(this.selectedIds.has(box.id)));
|
|
2914
|
-
this.place(element, this.layoutValue.boxes[box.id]);
|
|
3096
|
+
this.place(element, { ...this.layoutValue.boxes[box.id], ...(!isFlowBox(box) ? { width: this.layoutValue.boxes[box.id].width ?? 208 } : {}) });
|
|
3097
|
+
if (!isFlowBox(box)) {
|
|
3098
|
+
const text = document.createElement("div");
|
|
3099
|
+
text.setAttribute("part", "note-body");
|
|
3100
|
+
text.textContent = box.description ?? box.title;
|
|
3101
|
+
element.append(text);
|
|
3102
|
+
}
|
|
2915
3103
|
const icon = document.createElement("span");
|
|
2916
3104
|
icon.setAttribute("part", "icon");
|
|
2917
3105
|
icon.setAttribute("aria-hidden", "true");
|
|
2918
3106
|
if (kind?.icon) {
|
|
2919
3107
|
icon.append(kind.icon());
|
|
2920
3108
|
}
|
|
2921
|
-
|
|
3109
|
+
if (isFlowBox(box))
|
|
3110
|
+
element.append(icon);
|
|
2922
3111
|
const title = document.createElement("strong");
|
|
2923
3112
|
title.textContent = box.title;
|
|
2924
|
-
|
|
3113
|
+
if (isFlowBox(box))
|
|
3114
|
+
element.append(title);
|
|
2925
3115
|
let descriptionParent = element;
|
|
2926
3116
|
if (shape === 'event' && this.detailValue === 'technical' && box.technicalDetails?.length) {
|
|
2927
3117
|
const details = document.createElement('div');
|
|
@@ -2951,7 +3141,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2951
3141
|
}
|
|
2952
3142
|
if (shape === 'task' && box.technicalDetails)
|
|
2953
3143
|
icon.style.gridRow = 'span 4';
|
|
2954
|
-
if (this.detailValue === "technical" && box.technicalDetails) {
|
|
3144
|
+
if (isFlowBox(box) && this.detailValue === "technical" && box.technicalDetails) {
|
|
2955
3145
|
for (const line of box.technicalDetails) {
|
|
2956
3146
|
if (!line.text && !line.segments?.length)
|
|
2957
3147
|
continue;
|
|
@@ -2974,7 +3164,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2974
3164
|
descriptionParent.append(description);
|
|
2975
3165
|
}
|
|
2976
3166
|
}
|
|
2977
|
-
else {
|
|
3167
|
+
else if (isFlowBox(box)) {
|
|
2978
3168
|
const descriptionText = this.detailValue === "technical" ? box.technicalDescription ?? box.description : box.description;
|
|
2979
3169
|
if (descriptionText) {
|
|
2980
3170
|
const description = document.createElement("small");
|
|
@@ -3059,7 +3249,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
3059
3249
|
});
|
|
3060
3250
|
this.connecting = null;
|
|
3061
3251
|
}
|
|
3062
|
-
if (event.shiftKey)
|
|
3252
|
+
if (event.shiftKey && (this.graphSelection || this.selectedLineIds.size || this.selectedNoteIds.size))
|
|
3253
|
+
this.selectItems(this.selectedIds.has(box.id) ? this.selection.filter(item => item.type !== "box" || item.id !== box.id) : [...this.selection, { type: "box", id: box.id }]);
|
|
3254
|
+
else if (event.shiftKey)
|
|
3063
3255
|
this.selectMany(this.selectedIds.has(box.id) ? [...this.selectedIds].filter(id => id !== box.id) : [...this.selectedIds, box.id]);
|
|
3064
3256
|
else
|
|
3065
3257
|
this.select(box.id);
|
|
@@ -3076,6 +3268,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
3076
3268
|
port.addEventListener("click", event => {
|
|
3077
3269
|
event.stopPropagation();
|
|
3078
3270
|
if (!event.detail) {
|
|
3271
|
+
if (!isFlowBox(box)) {
|
|
3272
|
+
this.connecting = box.id;
|
|
3273
|
+
this.setStatus(`Select a step to attach ${box.title}`);
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3079
3276
|
const pos = this.layoutValue.boxes[box.id];
|
|
3080
3277
|
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[side];
|
|
3081
3278
|
this.openAnchoredChooser(this.nextEdit(box, side, { x: pos.x + offsets[0], y: pos.y + offsets[1] }), this.nextChooserTitle(box, side), port);
|
|
@@ -3103,6 +3300,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
3103
3300
|
element.append(handle);
|
|
3104
3301
|
}
|
|
3105
3302
|
world.append(element);
|
|
3303
|
+
if (!isFlowBox(box)) {
|
|
3304
|
+
const height = this.layoutValue.boxes[box.id].height;
|
|
3305
|
+
if (height !== undefined)
|
|
3306
|
+
element.querySelector('[part=note-body]').style.minHeight = `${height}px`;
|
|
3307
|
+
}
|
|
3106
3308
|
}
|
|
3107
3309
|
const lines = svgElement("svg");
|
|
3108
3310
|
lines.setAttribute("part", "lines");
|
|
@@ -3134,17 +3336,33 @@ export class ProcessModeler extends BaseElement {
|
|
|
3134
3336
|
path.setAttribute("part", "line");
|
|
3135
3337
|
path.dataset.lineId = line.id;
|
|
3136
3338
|
path.dataset.selected = String(this.selectedLineIds.has(line.id));
|
|
3137
|
-
|
|
3339
|
+
if (isFlowLine(line))
|
|
3340
|
+
path.setAttribute('marker-end', `url(#${this.selectedLineIds.has(line.id) ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`);
|
|
3138
3341
|
path.setAttribute("vector-effect", "non-scaling-stroke");
|
|
3139
3342
|
path.setAttribute("d", roundedProcessPath(points));
|
|
3140
|
-
|
|
3343
|
+
path.dataset.role = line.role ?? "flow";
|
|
3344
|
+
if (!isFlowLine(line))
|
|
3345
|
+
path.setAttribute("stroke-dasharray", "2 4");
|
|
3346
|
+
else if (line.dashed)
|
|
3141
3347
|
path.setAttribute("stroke-dasharray", "6 4");
|
|
3142
|
-
if (
|
|
3348
|
+
if (!isFlowLine(line)) {
|
|
3349
|
+
const hit = svgElement("polyline");
|
|
3350
|
+
hit.setAttribute("part", "line-hit");
|
|
3351
|
+
hit.dataset.lineId = line.id;
|
|
3352
|
+
hit.setAttribute("vector-effect", "non-scaling-stroke");
|
|
3353
|
+
hit.setAttribute("points", points.map(p => `${p.x},${p.y}`).join(" "));
|
|
3354
|
+
hit.addEventListener("click", event => { if (event.shiftKey)
|
|
3355
|
+
this.selectItems(this.selectedLineIds.has(line.id) ? this.selection.filter(item => item.type !== "line" || item.id !== line.id) : [...this.selection, { type: "line", id: line.id }]);
|
|
3356
|
+
else
|
|
3357
|
+
this.selectLine(line.id); });
|
|
3358
|
+
lines.append(hit);
|
|
3359
|
+
}
|
|
3360
|
+
if (isFlowLine(line) && this.showLastRunValue && line.share !== undefined) {
|
|
3143
3361
|
path.style.strokeWidth = `${Math.round((1.5 + Math.max(0, Math.min(1, line.share)) * 2.5) * 100) / 100}px`;
|
|
3144
3362
|
path.setAttribute('aria-label', `${Math.round(line.share * 100)}% of last-run traffic`);
|
|
3145
3363
|
}
|
|
3146
3364
|
lines.append(path);
|
|
3147
|
-
for (const point of [line.fromSide ? points[0] : null, line.toSide ? points.at(-1) : null]) {
|
|
3365
|
+
for (const point of [isFlowLine(line) && line.fromSide ? points[0] : null, isFlowLine(line) && line.toSide ? points.at(-1) : null]) {
|
|
3148
3366
|
if (!point)
|
|
3149
3367
|
continue;
|
|
3150
3368
|
const pin = svgElement('circle');
|
|
@@ -3154,7 +3372,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3154
3372
|
pin.setAttribute('r', '3');
|
|
3155
3373
|
lines.append(pin);
|
|
3156
3374
|
}
|
|
3157
|
-
for (let i = 1; i < points.length - 2; i++) {
|
|
3375
|
+
for (let i = 1; isFlowLine(line) && i < points.length - 2; i++) {
|
|
3158
3376
|
const hit = svgElement("polyline");
|
|
3159
3377
|
hit.setAttribute("part", "line-hit");
|
|
3160
3378
|
hit.dataset.lineId = line.id;
|
|
@@ -3165,6 +3383,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
3165
3383
|
}
|
|
3166
3384
|
const label = document.createElement("div");
|
|
3167
3385
|
label.setAttribute("part", "connection");
|
|
3386
|
+
label.dataset.lineId = line.id;
|
|
3387
|
+
label.dataset.role = line.role ?? "flow";
|
|
3168
3388
|
label.setAttribute('role', 'group');
|
|
3169
3389
|
label.tabIndex = 0;
|
|
3170
3390
|
label.style.left = `${midpoint.x}px`;
|
|
@@ -3172,17 +3392,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
3172
3392
|
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}` : ""}`;
|
|
3173
3393
|
label.setAttribute('aria-label', `Connection: ${names}${this.showLastRunValue && line.share !== undefined ? `. ${Math.round(line.share * 100)}% of last-run traffic` : ''}`);
|
|
3174
3394
|
label.addEventListener('click', event => { if (event.target.closest('button'))
|
|
3175
|
-
return;
|
|
3395
|
+
return; if (event.shiftKey)
|
|
3396
|
+
this.selectItems(this.selectedLineIds.has(line.id) ? this.selection.filter(item => item.type !== 'line' || item.id !== line.id) : [...this.selection, { type: 'line', id: line.id }]);
|
|
3397
|
+
else
|
|
3398
|
+
this.selectLine(line.id); });
|
|
3176
3399
|
label.addEventListener('keydown', event => { if (event.target === label && (event.key === 'Enter' || event.key === ' ')) {
|
|
3177
3400
|
event.preventDefault();
|
|
3178
3401
|
this.selectLine(line.id);
|
|
3179
3402
|
} });
|
|
3180
3403
|
const text = document.createElement("span");
|
|
3181
3404
|
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
3182
|
-
const branchIndex = this.projection.lines.filter(candidate => candidate.from === line.from).indexOf(line);
|
|
3183
|
-
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" : "");
|
|
3405
|
+
const branchIndex = this.projection.lines.filter(candidate => isFlowLine(candidate) && candidate.from === line.from).indexOf(line);
|
|
3406
|
+
text.textContent = !isFlowLine(line) ? "" : 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" : "");
|
|
3184
3407
|
label.append(text);
|
|
3185
|
-
if (this.showLastRunValue && line.share !== undefined) {
|
|
3408
|
+
if (isFlowLine(line) && this.showLastRunValue && line.share !== undefined) {
|
|
3186
3409
|
const share = document.createElement('small');
|
|
3187
3410
|
share.textContent = `${Math.round(line.share * 100)}%`;
|
|
3188
3411
|
label.append(share);
|
|
@@ -3190,7 +3413,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3190
3413
|
const actions = document.createElement("div");
|
|
3191
3414
|
actions.setAttribute("part", "connection-actions");
|
|
3192
3415
|
label.append(actions);
|
|
3193
|
-
if (points.length >= 4 && !this.locked) {
|
|
3416
|
+
if (isFlowLine(line) && points.length >= 4 && !this.locked) {
|
|
3194
3417
|
const adjust = document.createElement("button");
|
|
3195
3418
|
adjust.type = "button";
|
|
3196
3419
|
adjust.textContent = "Route";
|
|
@@ -3220,7 +3443,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3220
3443
|
["insert", "Insert a step here"],
|
|
3221
3444
|
["disconnect", "Remove connection"],
|
|
3222
3445
|
]) {
|
|
3223
|
-
if (type === "disconnect" && this.disableConnections)
|
|
3446
|
+
if ((type === "disconnect" && this.disableConnections) || (type === "insert" && !isFlowLine(line)))
|
|
3224
3447
|
continue;
|
|
3225
3448
|
const button = document.createElement("button");
|
|
3226
3449
|
button.type = "button";
|
|
@@ -3244,7 +3467,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3244
3467
|
actions.append(button);
|
|
3245
3468
|
}
|
|
3246
3469
|
world.append(label);
|
|
3247
|
-
if (this.selectedLineId === line.id && !this.locked && !this.disableConnections) {
|
|
3470
|
+
if (isFlowLine(line) && this.selectedLineId === line.id && !this.locked && !this.disableConnections) {
|
|
3248
3471
|
for (const [end, point] of [['from', points[0]], ['to', points.at(-1)]]) {
|
|
3249
3472
|
const grip = document.createElement('button');
|
|
3250
3473
|
grip.type = 'button';
|
|
@@ -3299,7 +3522,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3299
3522
|
return;
|
|
3300
3523
|
}
|
|
3301
3524
|
const selected = this.selectedIds.size === 1 ? this.selected : null;
|
|
3302
|
-
const from = selected && !selected.frame && selected.kind !== 'section' && !['end', 'finish'].includes(selected.kind) ? selected.id : undefined;
|
|
3525
|
+
const from = selected && isFlowBox(selected) && !selected.frame && selected.kind !== 'section' && !['end', 'finish'].includes(selected.kind) ? selected.id : undefined;
|
|
3303
3526
|
if (from && selected)
|
|
3304
3527
|
this.requestEdit({ ...this.nextEdit(selected), kind });
|
|
3305
3528
|
else {
|
|
@@ -3311,7 +3534,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3311
3534
|
paletteLineAt(point) {
|
|
3312
3535
|
let nearest;
|
|
3313
3536
|
let best = 20 / this.viewport.zoom;
|
|
3314
|
-
for (const line of this.projection.lines) {
|
|
3537
|
+
for (const line of this.projection.lines.filter(isFlowLine)) {
|
|
3315
3538
|
const points = this.routedLines.get(line.id) ?? [];
|
|
3316
3539
|
for (let i = 1; i < points.length; i++) {
|
|
3317
3540
|
const a = points[i - 1], b = points[i], dx = b.x - a.x, dy = b.y - a.y, length = dx * dx + dy * dy;
|
|
@@ -3435,7 +3658,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3435
3658
|
const query = this.shadowRoot.querySelector("[part=search]").value.toLowerCase();
|
|
3436
3659
|
choices.replaceChildren();
|
|
3437
3660
|
let group = "";
|
|
3438
|
-
for (const kind of this.catalog.filter((kind) => `${kind.label} ${kind.description} ${kind.group ?? ""} ${(kind.aliases ?? []).join(" ")}`
|
|
3661
|
+
for (const kind of this.catalog.filter((kind) => kind.addable !== false && `${kind.label} ${kind.description} ${kind.group ?? ""} ${(kind.aliases ?? []).join(" ")}`
|
|
3439
3662
|
.toLowerCase()
|
|
3440
3663
|
.includes(query))) {
|
|
3441
3664
|
if (kind.group && kind.group !== group) {
|
|
@@ -3571,16 +3794,99 @@ export class ProcessModeler extends BaseElement {
|
|
|
3571
3794
|
checks.append(button);
|
|
3572
3795
|
});
|
|
3573
3796
|
}
|
|
3797
|
+
renderFields(editor, box, help, disclosures) {
|
|
3798
|
+
let sectionKey, disclosureKey, rowKey;
|
|
3799
|
+
let section = editor, disclosure = editor, row = editor;
|
|
3800
|
+
for (const field of this.fieldsValue[box.id] ?? []) {
|
|
3801
|
+
if (field.section?.key !== sectionKey) {
|
|
3802
|
+
sectionKey = field.section?.key;
|
|
3803
|
+
disclosureKey = rowKey = undefined;
|
|
3804
|
+
section = editor;
|
|
3805
|
+
if (field.section) {
|
|
3806
|
+
section = document.createElement('section');
|
|
3807
|
+
section.setAttribute('part', 'field-section');
|
|
3808
|
+
section.dataset.section = sectionKey;
|
|
3809
|
+
const title = document.createElement('h3');
|
|
3810
|
+
title.textContent = field.section.title;
|
|
3811
|
+
section.append(title);
|
|
3812
|
+
if (field.section.description || field.section.descriptionSegments) {
|
|
3813
|
+
const description = document.createElement('p');
|
|
3814
|
+
description.setAttribute('part', 'field-section-description');
|
|
3815
|
+
if (field.section.descriptionSegments)
|
|
3816
|
+
for (const segment of field.section.descriptionSegments) {
|
|
3817
|
+
if (segment.format === 'code') {
|
|
3818
|
+
const code = document.createElement('code');
|
|
3819
|
+
code.textContent = segment.text;
|
|
3820
|
+
description.append(code);
|
|
3821
|
+
}
|
|
3822
|
+
else
|
|
3823
|
+
description.append(document.createTextNode(segment.text));
|
|
3824
|
+
}
|
|
3825
|
+
else
|
|
3826
|
+
description.textContent = field.section.description;
|
|
3827
|
+
section.append(description);
|
|
3828
|
+
}
|
|
3829
|
+
editor.append(section);
|
|
3830
|
+
}
|
|
3831
|
+
disclosure = row = section;
|
|
3832
|
+
}
|
|
3833
|
+
if (field.disclosure?.key !== disclosureKey) {
|
|
3834
|
+
disclosureKey = field.disclosure?.key;
|
|
3835
|
+
rowKey = undefined;
|
|
3836
|
+
disclosure = section;
|
|
3837
|
+
if (field.disclosure) {
|
|
3838
|
+
const details = document.createElement('details');
|
|
3839
|
+
details.setAttribute('part', 'field-disclosure');
|
|
3840
|
+
details.dataset.disclosure = JSON.stringify([sectionKey, disclosureKey]);
|
|
3841
|
+
details.dataset.session = String(this.layoutEditSession);
|
|
3842
|
+
const configuredOpen = Boolean(field.disclosure.open);
|
|
3843
|
+
const previous = disclosures.get(details.dataset.disclosure);
|
|
3844
|
+
details.dataset.configuredOpen = String(configuredOpen);
|
|
3845
|
+
details.open = previous?.configuredOpen === configuredOpen ? previous.open : configuredOpen;
|
|
3846
|
+
const summary = document.createElement('summary');
|
|
3847
|
+
summary.textContent = field.disclosure.summary;
|
|
3848
|
+
details.append(summary);
|
|
3849
|
+
section.append(details);
|
|
3850
|
+
disclosure = details;
|
|
3851
|
+
}
|
|
3852
|
+
row = disclosure;
|
|
3853
|
+
}
|
|
3854
|
+
if (field.row?.key !== rowKey) {
|
|
3855
|
+
rowKey = field.row?.key;
|
|
3856
|
+
row = disclosure;
|
|
3857
|
+
if (field.row) {
|
|
3858
|
+
row = document.createElement('div');
|
|
3859
|
+
row.setAttribute('part', 'field-row');
|
|
3860
|
+
row.dataset.row = rowKey;
|
|
3861
|
+
if (field.row.leadingWidth !== undefined && Number.isFinite(field.row.leadingWidth) && field.row.leadingWidth > 0)
|
|
3862
|
+
row.style.gridTemplateColumns = `${field.row.leadingWidth}px minmax(0, 1fr)`;
|
|
3863
|
+
if (field.row.gap !== undefined && Number.isFinite(field.row.gap) && field.row.gap >= 0)
|
|
3864
|
+
row.style.gap = `${field.row.gap}px`;
|
|
3865
|
+
disclosure.append(row);
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
row.append(this.renderField(box, field, help.get(field.key)));
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3574
3871
|
renderField(box, field, helpOpen = false) {
|
|
3575
3872
|
const row = document.createElement('div');
|
|
3576
3873
|
row.setAttribute('part', 'field');
|
|
3577
3874
|
const label = document.createElement('label');
|
|
3578
3875
|
label.textContent = field.label;
|
|
3876
|
+
if (field.optional) {
|
|
3877
|
+
const optional = document.createElement('em');
|
|
3878
|
+
optional.setAttribute('part', 'field-optional');
|
|
3879
|
+
optional.textContent = ' optional';
|
|
3880
|
+
const name = document.createElement('span');
|
|
3881
|
+
name.append(...Array.from(label.childNodes), optional);
|
|
3882
|
+
label.append(name);
|
|
3883
|
+
}
|
|
3579
3884
|
let control;
|
|
3580
3885
|
if (field.kind === 'multiline' || (field.kind === 'expression' && (field.expression?.rows ?? 1) > 1)) {
|
|
3581
3886
|
control = document.createElement('textarea');
|
|
3582
|
-
|
|
3583
|
-
|
|
3887
|
+
const rows = field.kind === 'expression' ? field.expression?.rows : field.rows;
|
|
3888
|
+
if (rows !== undefined && Number.isFinite(rows))
|
|
3889
|
+
control.rows = Math.max(field.kind === 'expression' ? 2 : 1, Math.floor(rows));
|
|
3584
3890
|
}
|
|
3585
3891
|
else if (field.kind === 'choice') {
|
|
3586
3892
|
const select = document.createElement('select');
|
|
@@ -3606,7 +3912,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3606
3912
|
}
|
|
3607
3913
|
else {
|
|
3608
3914
|
const input = document.createElement('input');
|
|
3609
|
-
input.type = field.kind === 'boolean' ? 'checkbox' : field.kind === 'number' ? 'number' : field.kind === 'search' ? 'search' : 'text';
|
|
3915
|
+
input.type = field.kind === 'boolean' ? 'checkbox' : field.kind === 'number' ? 'number' : field.kind === 'search' ? 'search' : field.kind === 'time' || field.kind === 'datetime-local' ? field.kind : 'text';
|
|
3610
3916
|
control = input;
|
|
3611
3917
|
}
|
|
3612
3918
|
control.dataset.field = field.key;
|
|
@@ -3619,6 +3925,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
3619
3925
|
control.required = Boolean(field.required);
|
|
3620
3926
|
if (field.placeholder && (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement))
|
|
3621
3927
|
control.placeholder = field.placeholder;
|
|
3928
|
+
if (control instanceof HTMLInputElement && field.kind === 'number') {
|
|
3929
|
+
if (field.min !== undefined && Number.isFinite(field.min))
|
|
3930
|
+
control.min = String(field.min);
|
|
3931
|
+
if (field.max !== undefined && Number.isFinite(field.max))
|
|
3932
|
+
control.max = String(field.max);
|
|
3933
|
+
if (field.step === 'any' || (typeof field.step === 'number' && Number.isFinite(field.step) && field.step > 0))
|
|
3934
|
+
control.step = String(field.step);
|
|
3935
|
+
}
|
|
3936
|
+
if (field.format === 'code') {
|
|
3937
|
+
control.dataset.format = 'code';
|
|
3938
|
+
control.spellcheck = false;
|
|
3939
|
+
}
|
|
3622
3940
|
if (field.kind === 'expression') {
|
|
3623
3941
|
control.spellcheck = false;
|
|
3624
3942
|
control.setAttribute('autocomplete', 'off');
|
|
@@ -3626,17 +3944,27 @@ export class ProcessModeler extends BaseElement {
|
|
|
3626
3944
|
}
|
|
3627
3945
|
if (field.problem)
|
|
3628
3946
|
control.setAttribute('aria-invalid', 'true');
|
|
3629
|
-
const
|
|
3947
|
+
const fieldSession = this.layoutEditSession;
|
|
3948
|
+
const submit = () => {
|
|
3949
|
+
if (!control.isConnected || control.disabled || this.locked || fieldSession !== this.layoutEditSession || this.selected?.id !== box.id)
|
|
3950
|
+
return;
|
|
3951
|
+
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 });
|
|
3952
|
+
};
|
|
3630
3953
|
if (field.kind !== 'action')
|
|
3631
3954
|
control.addEventListener(field.kind === 'expression' ? 'input' : 'change', submit);
|
|
3632
|
-
|
|
3955
|
+
if (field.kind === 'boolean') {
|
|
3956
|
+
row.dataset.boolean = '';
|
|
3957
|
+
label.prepend(control);
|
|
3958
|
+
}
|
|
3959
|
+
else
|
|
3960
|
+
label.append(control);
|
|
3633
3961
|
row.append(label);
|
|
3634
3962
|
if (field.kind === 'action' && control instanceof HTMLInputElement) {
|
|
3635
3963
|
const input = control;
|
|
3636
3964
|
const options = field.options ?? [];
|
|
3637
3965
|
const list = document.createElement('div');
|
|
3638
3966
|
list.setAttribute('part', 'action-options');
|
|
3639
|
-
list.id = `process-action-${
|
|
3967
|
+
list.id = `process-action-${++actionControlSequence}`;
|
|
3640
3968
|
list.hidden = true;
|
|
3641
3969
|
const optionList = document.createElement('div');
|
|
3642
3970
|
optionList.setAttribute('role', 'listbox');
|
|
@@ -3647,106 +3975,234 @@ export class ProcessModeler extends BaseElement {
|
|
|
3647
3975
|
input.setAttribute('aria-controls', optionList.id);
|
|
3648
3976
|
input.setAttribute('aria-expanded', 'false');
|
|
3649
3977
|
input.autocomplete = 'off';
|
|
3978
|
+
const availableGroups = [...new Set(options.map(option => option.group).filter((name) => Boolean(name)))];
|
|
3979
|
+
const groups = [...new Set([...(field.actionGroups ?? []).map(item => item.name).filter(name => availableGroups.includes(name)), ...availableGroups])];
|
|
3980
|
+
const shown = input.value;
|
|
3650
3981
|
let group = null;
|
|
3651
3982
|
let active = 0;
|
|
3652
|
-
let browse =
|
|
3653
|
-
const close = () => { list.hidden = true;
|
|
3654
|
-
|
|
3983
|
+
let browse = true;
|
|
3984
|
+
const close = (restore = true) => { list.hidden = true; if (restore)
|
|
3985
|
+
input.value = shown; input.setAttribute('aria-expanded', 'false'); input.removeAttribute('aria-activedescendant'); };
|
|
3986
|
+
const place = () => {
|
|
3987
|
+
if (list.hidden)
|
|
3988
|
+
return;
|
|
3989
|
+
const viewport = this.ownerDocument.defaultView;
|
|
3990
|
+
const rect = input.getBoundingClientRect();
|
|
3991
|
+
if (!input.isConnected) {
|
|
3992
|
+
close();
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
if (!rect.width && !rect.height)
|
|
3996
|
+
return;
|
|
3997
|
+
if (rect.bottom <= 0 || rect.top >= viewport.innerHeight) {
|
|
3998
|
+
close();
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
const below = Math.max(0, viewport.innerHeight - rect.bottom - 12), above = Math.max(0, rect.top - 12);
|
|
4002
|
+
const up = below < 260 && above > below;
|
|
4003
|
+
Object.assign(list.style, { left: `${Math.max(8, rect.left)}px`, right: 'auto', width: `${Math.min(rect.width, viewport.innerWidth - 16)}px`, maxHeight: `${Math.min(420, up ? above : below)}px`, top: up ? 'auto' : `${rect.bottom + 4}px`, bottom: up ? `${viewport.innerHeight - rect.top + 4}px` : 'auto' });
|
|
4004
|
+
};
|
|
4005
|
+
const dismissOutside = (event) => { const path = event.composedPath(); if (!list.hidden && !path.includes(input) && !path.includes(list) && !path.some(target => target instanceof HTMLElement && target.getAttribute('part') === 'action-caret'))
|
|
4006
|
+
close(); };
|
|
4007
|
+
const viewport = this.ownerDocument.defaultView;
|
|
4008
|
+
viewport.addEventListener('resize', place);
|
|
4009
|
+
this.ownerDocument.addEventListener('scroll', place, true);
|
|
4010
|
+
this.shadowRoot.addEventListener('scroll', place, true);
|
|
4011
|
+
this.ownerDocument.addEventListener('pointerdown', dismissOutside, true);
|
|
4012
|
+
this.inspectorFieldCleanups.push(() => { close(); viewport.removeEventListener('resize', place); this.ownerDocument.removeEventListener('scroll', place, true); this.shadowRoot?.removeEventListener('scroll', place, true); this.ownerDocument.removeEventListener('pointerdown', dismissOutside, true); });
|
|
4013
|
+
const choose = (option) => {
|
|
4014
|
+
const session = this.layoutEditSession;
|
|
4015
|
+
if (!input.isConnected || input.disabled || this.locked || this.selected?.id !== box.id)
|
|
4016
|
+
return;
|
|
4017
|
+
input.focus();
|
|
4018
|
+
if (session !== this.layoutEditSession || !input.isConnected || input.disabled || this.locked || this.selected?.id !== box.id || this.shadowRoot?.activeElement !== input || input.closest('[inert]'))
|
|
4019
|
+
return;
|
|
4020
|
+
input.value = option.value;
|
|
4021
|
+
close(false);
|
|
4022
|
+
this.pendingActionChoice = { boxId: box.id, key: field.key, value: option.value, session };
|
|
4023
|
+
submit();
|
|
4024
|
+
};
|
|
3655
4025
|
const render = () => {
|
|
3656
4026
|
list.replaceChildren();
|
|
3657
4027
|
optionList.replaceChildren();
|
|
3658
4028
|
const query = browse ? '' : input.value.trim().toLocaleLowerCase();
|
|
3659
|
-
const
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
4029
|
+
const terms = query.split(/\s+/).filter(Boolean);
|
|
4030
|
+
let matches = options.filter(option => (!group || option.group === group) && terms.every(term => `${option.label} ${option.value} ${option.group ?? ''} ${option.description ?? ''}`.toLocaleLowerCase().includes(term)));
|
|
4031
|
+
if (!group && !query && groups.length)
|
|
4032
|
+
matches = matches.filter(option => !option.group);
|
|
4033
|
+
if (query) {
|
|
4034
|
+
const starts = (text, term) => text.toLocaleLowerCase().split(/[\s›_.]+/).some(word => word.startsWith(term));
|
|
4035
|
+
const score = (option) => terms.filter(term => starts(option.group ?? '', term) || starts(option.label, term)).length;
|
|
4036
|
+
matches.sort((a, b) => score(b) - score(a) || (a.group?.length ?? 0) - (b.group?.length ?? 0) || a.label.length - b.label.length);
|
|
4037
|
+
matches = matches.slice(0, 60);
|
|
4038
|
+
}
|
|
4039
|
+
const addHeading = (text) => { const heading = document.createElement('h3'); heading.setAttribute('part', 'action-heading'); heading.setAttribute('aria-hidden', 'true'); heading.textContent = text; optionList.append(heading); };
|
|
4040
|
+
const addNavigation = (name, part, activate) => {
|
|
4041
|
+
const button = document.createElement('button');
|
|
4042
|
+
button.type = 'button';
|
|
4043
|
+
button.tabIndex = -1;
|
|
4044
|
+
button.setAttribute('part', part);
|
|
4045
|
+
button.setAttribute('role', 'option');
|
|
4046
|
+
const title = document.createElement('strong');
|
|
4047
|
+
title.textContent = name;
|
|
4048
|
+
const chevron = document.createElement('span');
|
|
4049
|
+
chevron.setAttribute('part', 'action-chevron');
|
|
4050
|
+
chevron.setAttribute('aria-hidden', 'true');
|
|
4051
|
+
chevron.textContent = part === 'action-back' ? '‹' : '›';
|
|
4052
|
+
if (part === 'action-back') {
|
|
4053
|
+
button.append(chevron, title);
|
|
4054
|
+
button.setAttribute('aria-label', 'Back to all kinds of items');
|
|
4055
|
+
}
|
|
4056
|
+
else {
|
|
4057
|
+
const count = document.createElement('small');
|
|
4058
|
+
const total = options.filter(option => option.group === name).length;
|
|
4059
|
+
count.textContent = String(total);
|
|
4060
|
+
button.append(title, count, chevron);
|
|
4061
|
+
button.setAttribute('aria-label', `${name}, ${total} actions`);
|
|
4062
|
+
}
|
|
4063
|
+
button.onclick = activate;
|
|
4064
|
+
optionList.append(button);
|
|
4065
|
+
};
|
|
4066
|
+
if (!group && (!query || groups.some(name => name.toLocaleLowerCase().includes(query)))) {
|
|
4067
|
+
const visibleGroups = query ? groups.filter(name => name.toLocaleLowerCase().includes(query)).slice(0, 4) : groups;
|
|
4068
|
+
let heading;
|
|
4069
|
+
for (const name of visibleGroups) {
|
|
4070
|
+
const nextHeading = !query ? field.actionGroups?.find(item => item.name === name)?.heading : undefined;
|
|
4071
|
+
if (nextHeading && nextHeading !== heading)
|
|
4072
|
+
addHeading(nextHeading);
|
|
4073
|
+
heading = nextHeading;
|
|
4074
|
+
addNavigation(name, 'action-group', () => { group = name; browse = true; active = 1; render(); });
|
|
3669
4075
|
}
|
|
3670
4076
|
}
|
|
3671
4077
|
if (group) {
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
back.setAttribute('part', 'action-back');
|
|
3675
|
-
back.textContent = `All actions / ${group}`;
|
|
3676
|
-
back.onclick = () => { input.focus(); group = null; browse = true; render(); };
|
|
3677
|
-
list.prepend(back);
|
|
4078
|
+
addNavigation('All kinds of items', 'action-back', () => { const previous = group; group = null; browse = true; active = Math.max(0, groups.indexOf(previous)); render(); });
|
|
4079
|
+
addHeading(group);
|
|
3678
4080
|
}
|
|
3679
|
-
|
|
4081
|
+
const addAction = (option, custom = false) => {
|
|
3680
4082
|
const item = document.createElement('button');
|
|
3681
4083
|
item.type = 'button';
|
|
4084
|
+
item.tabIndex = -1;
|
|
3682
4085
|
item.setAttribute('role', 'option');
|
|
3683
4086
|
item.setAttribute('part', 'action-option');
|
|
3684
|
-
item.id = `${list.id}-${index}`;
|
|
3685
|
-
item.setAttribute('aria-selected', String(index === active));
|
|
3686
4087
|
const title = document.createElement('strong');
|
|
3687
|
-
title.textContent = option.label;
|
|
4088
|
+
title.textContent = group && option.label.startsWith(`${group} › `) ? option.label.slice(group.length + 3) : query && option.group && !option.label.startsWith(`${option.group} › `) ? `${option.group} › ${option.label}` : option.label;
|
|
3688
4089
|
item.append(title);
|
|
3689
|
-
|
|
4090
|
+
const actionLabel = option.group && option.label.startsWith(`${option.group} › `) ? option.label.slice(option.group.length + 3) : option.label;
|
|
4091
|
+
item.setAttribute('aria-label', custom ? `Use ${option.value}, a step named in your code` : query && option.group ? `${option.group}, ${actionLabel}` : title.textContent);
|
|
4092
|
+
if (custom)
|
|
4093
|
+
item.dataset.custom = '';
|
|
4094
|
+
if (option.value === field.value)
|
|
4095
|
+
item.dataset.current = '';
|
|
4096
|
+
const detail = option.description ?? (option.value !== option.label ? option.value : undefined);
|
|
4097
|
+
if (detail) {
|
|
3690
4098
|
const code = document.createElement('small');
|
|
3691
|
-
code.textContent =
|
|
4099
|
+
code.textContent = detail;
|
|
4100
|
+
if (!custom)
|
|
4101
|
+
code.dataset.format = 'code';
|
|
3692
4102
|
item.append(code);
|
|
3693
4103
|
}
|
|
3694
4104
|
item.onclick = () => choose(option);
|
|
3695
4105
|
optionList.append(item);
|
|
3696
|
-
}
|
|
3697
|
-
|
|
4106
|
+
};
|
|
4107
|
+
for (const option of matches)
|
|
4108
|
+
addAction(option);
|
|
4109
|
+
if (field.allowCustomValue && /^[a-z0-9_]+(\.[a-z0-9_]+)+$/.test(query) && !options.some(option => option.value === query))
|
|
4110
|
+
addAction({ value: query, label: `Use “${query}”`, description: 'A step named in your code' }, true);
|
|
4111
|
+
if (!optionList.querySelector('[role=option]')) {
|
|
3698
4112
|
const empty = document.createElement('p');
|
|
3699
|
-
empty.
|
|
4113
|
+
empty.setAttribute('part', 'action-empty');
|
|
4114
|
+
empty.textContent = 'No Box action matches. Try a kind of item, like “files”, or an action, like “delete”.';
|
|
3700
4115
|
list.append(empty);
|
|
3701
4116
|
}
|
|
3702
4117
|
list.append(optionList);
|
|
3703
|
-
|
|
4118
|
+
const items = Array.from(optionList.querySelectorAll('[role=option]'));
|
|
4119
|
+
active = Math.min(active, Math.max(0, items.length - 1));
|
|
4120
|
+
items.forEach((item, index) => { item.id = `${list.id}-${index}`; item.setAttribute('aria-selected', String(index === active)); });
|
|
3704
4121
|
list.hidden = false;
|
|
3705
4122
|
input.setAttribute('aria-expanded', 'true');
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
input.setAttribute('aria-activedescendant', selected.id);
|
|
4123
|
+
if (items[active])
|
|
4124
|
+
input.setAttribute('aria-activedescendant', items[active].id);
|
|
3709
4125
|
else
|
|
3710
4126
|
input.removeAttribute('aria-activedescendant');
|
|
4127
|
+
place();
|
|
4128
|
+
items[active]?.scrollIntoView?.({ block: 'nearest' });
|
|
3711
4129
|
};
|
|
3712
|
-
|
|
3713
|
-
|
|
4130
|
+
const openSelected = () => {
|
|
4131
|
+
if (this.quietActionFocus)
|
|
4132
|
+
return;
|
|
4133
|
+
this.pendingActionChoice = undefined;
|
|
4134
|
+
const current = options.find(option => option.value === field.value);
|
|
4135
|
+
group = current?.group ?? null;
|
|
4136
|
+
browse = true;
|
|
4137
|
+
active = group ? 1 + options.filter(option => option.group === group).findIndex(option => option === current) : current ? groups.length + options.filter(option => !option.group).indexOf(current) : 0;
|
|
4138
|
+
render();
|
|
4139
|
+
};
|
|
4140
|
+
list.addEventListener('pointerdown', event => event.preventDefault());
|
|
4141
|
+
input.addEventListener('focus', () => { if (!this.quietActionFocus) {
|
|
4142
|
+
input.select();
|
|
4143
|
+
openSelected();
|
|
4144
|
+
} });
|
|
4145
|
+
input.addEventListener('input', () => { this.pendingActionChoice = undefined; group = null; browse = false; active = 0; render(); });
|
|
4146
|
+
input.addEventListener('pointerdown', () => { if (list.hidden && this.shadowRoot?.activeElement === input)
|
|
4147
|
+
openSelected(); });
|
|
3714
4148
|
input.addEventListener('keydown', event => {
|
|
4149
|
+
if (list.hidden) {
|
|
4150
|
+
if (event.key === 'ArrowDown' || event.key === 'Enter') {
|
|
4151
|
+
event.preventDefault();
|
|
4152
|
+
openSelected();
|
|
4153
|
+
}
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
3715
4156
|
if (event.key === 'Escape') {
|
|
3716
4157
|
event.preventDefault();
|
|
4158
|
+
event.stopPropagation();
|
|
4159
|
+
close();
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4162
|
+
if (event.key === 'Tab') {
|
|
3717
4163
|
close();
|
|
3718
4164
|
return;
|
|
3719
4165
|
}
|
|
3720
|
-
|
|
4166
|
+
const items = Array.from(list.querySelectorAll('[role=option]'));
|
|
4167
|
+
if (!items.length)
|
|
4168
|
+
return;
|
|
4169
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || group && (event.key === 'Home' || event.key === 'End')) {
|
|
3721
4170
|
event.preventDefault();
|
|
3722
|
-
|
|
3723
|
-
render();
|
|
3724
|
-
const items = Array.from(list.querySelectorAll('[role=option]'));
|
|
3725
|
-
if (!items.length)
|
|
3726
|
-
return;
|
|
3727
|
-
active = (active + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
4171
|
+
active = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (active + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
3728
4172
|
items.forEach((item, index) => item.setAttribute('aria-selected', String(index === active)));
|
|
3729
4173
|
input.setAttribute('aria-activedescendant', items[active].id);
|
|
3730
4174
|
items[active].scrollIntoView?.({ block: 'nearest' });
|
|
3731
4175
|
}
|
|
3732
|
-
else if (event.key === 'Enter' &&
|
|
3733
|
-
const items = Array.from(list.querySelectorAll('[role=option]'));
|
|
4176
|
+
else if (event.key === 'Enter' || event.key === 'ArrowRight' && items[active]?.getAttribute('part') === 'action-group') {
|
|
3734
4177
|
if (items[active]) {
|
|
3735
4178
|
event.preventDefault();
|
|
3736
4179
|
items[active].click();
|
|
3737
4180
|
}
|
|
3738
4181
|
}
|
|
4182
|
+
else if (event.key === 'ArrowLeft' && group && input.value === shown) {
|
|
4183
|
+
event.preventDefault();
|
|
4184
|
+
list.querySelector('[part=action-back]')?.click();
|
|
4185
|
+
}
|
|
3739
4186
|
});
|
|
3740
4187
|
const caret = document.createElement('button');
|
|
3741
4188
|
caret.type = 'button';
|
|
3742
4189
|
caret.setAttribute('part', 'action-caret');
|
|
3743
4190
|
caret.setAttribute('aria-label', `Browse ${field.label.toLowerCase()} choices`);
|
|
3744
|
-
|
|
4191
|
+
const caretIcon = document.createElement('span');
|
|
4192
|
+
caretIcon.setAttribute('part', 'action-caret-icon');
|
|
4193
|
+
caretIcon.setAttribute('aria-hidden', 'true');
|
|
4194
|
+
caret.append(caretIcon);
|
|
3745
4195
|
caret.disabled = input.disabled;
|
|
3746
|
-
caret.onclick = () => { input.focus(); group = null; browse = true; active = 0; render(); };
|
|
4196
|
+
caret.onclick = () => { this.pendingActionChoice = undefined; input.focus(); group = null; browse = true; active = 0; render(); };
|
|
3747
4197
|
row.addEventListener('focusout', event => { if (!row.contains(event.relatedTarget))
|
|
3748
4198
|
close(); });
|
|
3749
|
-
|
|
4199
|
+
const combo = document.createElement('span');
|
|
4200
|
+
combo.setAttribute('part', 'action-input');
|
|
4201
|
+
input.remove();
|
|
4202
|
+
input.id = `${list.id}-input`;
|
|
4203
|
+
label.htmlFor = input.id;
|
|
4204
|
+
combo.append(input, caret);
|
|
4205
|
+
row.append(combo, list);
|
|
3750
4206
|
}
|
|
3751
4207
|
if (field.kind === 'search' && field.options?.length && control instanceof HTMLInputElement) {
|
|
3752
4208
|
const options = document.createElement('datalist');
|
|
@@ -3801,10 +4257,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
3801
4257
|
describedBy.push(help.id);
|
|
3802
4258
|
}
|
|
3803
4259
|
const feedback = field.problem || (field.kind === 'expression' ? field.expression?.feedback?.message : undefined);
|
|
3804
|
-
if (feedback) {
|
|
4260
|
+
if (feedback || (field.kind === 'expression' && field.expression?.feedback !== undefined)) {
|
|
3805
4261
|
const message = document.createElement('small');
|
|
3806
4262
|
message.id = `${messageId}-feedback`;
|
|
3807
|
-
message.textContent = feedback;
|
|
4263
|
+
message.textContent = feedback ?? '';
|
|
3808
4264
|
message.setAttribute('part', field.problem ? 'field-problem' : 'field-feedback');
|
|
3809
4265
|
if (field.kind === 'expression')
|
|
3810
4266
|
message.setAttribute('aria-live', 'polite');
|
|
@@ -3814,6 +4270,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
3814
4270
|
row.insertBefore(message, chips);
|
|
3815
4271
|
describedBy.push(message.id);
|
|
3816
4272
|
}
|
|
4273
|
+
if (field.annotation) {
|
|
4274
|
+
const annotation = document.createElement('small');
|
|
4275
|
+
annotation.setAttribute('part', 'field-annotation');
|
|
4276
|
+
annotation.id = `${messageId}-annotation`;
|
|
4277
|
+
annotation.textContent = field.annotation;
|
|
4278
|
+
row.append(annotation);
|
|
4279
|
+
describedBy.push(annotation.id);
|
|
4280
|
+
}
|
|
3817
4281
|
if (describedBy.length)
|
|
3818
4282
|
control.setAttribute('aria-describedby', describedBy.join(' '));
|
|
3819
4283
|
if (field.kind === 'expression' && field.expression?.help) {
|
|
@@ -3845,11 +4309,118 @@ export class ProcessModeler extends BaseElement {
|
|
|
3845
4309
|
}
|
|
3846
4310
|
return row;
|
|
3847
4311
|
}
|
|
4312
|
+
renderLeadsTo(editor, selected) {
|
|
4313
|
+
const previous = editor.querySelector('[part=leads-to]');
|
|
4314
|
+
if (!isFlowBox(selected) || ['end', 'finish'].includes(selected.kind)) {
|
|
4315
|
+
previous?.remove();
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
const outgoing = this.projection.lines.filter(line => isFlowLine(line) && line.from === selected.id);
|
|
4319
|
+
const state = JSON.stringify([selected.id, selected.kind, this.locked, this.disableConnections, outgoing, this.projection.boxes.filter(box => box.parentId === selected.parentId).map(({ id, title, kind, role }) => ({ id, title, kind, role }))]);
|
|
4320
|
+
if (previous?.dataset.state === state)
|
|
4321
|
+
return;
|
|
4322
|
+
const leads = document.createElement('section');
|
|
4323
|
+
leads.setAttribute('part', 'leads-to');
|
|
4324
|
+
leads.dataset.state = state;
|
|
4325
|
+
const heading = document.createElement('h3');
|
|
4326
|
+
heading.textContent = 'Leads to';
|
|
4327
|
+
leads.append(heading);
|
|
4328
|
+
const session = this.layoutEditSession;
|
|
4329
|
+
const current = (control) => control.isConnected && this.selected?.id === selected.id && this.layoutEditSession === session && !this.locked && !this.disableConnections;
|
|
4330
|
+
const list = document.createElement('ul');
|
|
4331
|
+
list.setAttribute('part', 'lead-list');
|
|
4332
|
+
for (const line of outgoing) {
|
|
4333
|
+
const row = document.createElement('li');
|
|
4334
|
+
row.setAttribute('part', 'lead-row');
|
|
4335
|
+
if (selected.kind === 'decision' || selected.kind === 'choice') {
|
|
4336
|
+
const input = document.createElement('input');
|
|
4337
|
+
const weighted = selected.kind === 'choice';
|
|
4338
|
+
input.setAttribute('part', 'lead-control');
|
|
4339
|
+
input.dataset.lead = line.id;
|
|
4340
|
+
input.dataset.leadOwner = selected.id;
|
|
4341
|
+
input.type = weighted ? 'number' : 'text';
|
|
4342
|
+
input.setAttribute('aria-label', weighted ? 'Weight' : 'Path name');
|
|
4343
|
+
if (weighted)
|
|
4344
|
+
input.min = '0';
|
|
4345
|
+
input.value = weighted ? String(line.weight ?? 1) : line.label ?? '';
|
|
4346
|
+
input.disabled = this.locked || this.disableConnections;
|
|
4347
|
+
input.onchange = () => {
|
|
4348
|
+
if (!current(input) || !this.projection.lines.some(candidate => candidate.id === line.id && candidate.from === selected.id))
|
|
4349
|
+
return;
|
|
4350
|
+
if (weighted) {
|
|
4351
|
+
const weight = input.valueAsNumber;
|
|
4352
|
+
if (Number.isFinite(weight) && weight >= 0 && weight !== (line.weight ?? 1))
|
|
4353
|
+
this.requestEdit({ type: 'edit-line', lineId: line.id, weight });
|
|
4354
|
+
}
|
|
4355
|
+
else if (input.value !== (line.label ?? ''))
|
|
4356
|
+
this.requestEdit({ type: 'edit-line', lineId: line.id, label: input.value });
|
|
4357
|
+
};
|
|
4358
|
+
row.append(input);
|
|
4359
|
+
}
|
|
4360
|
+
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
4361
|
+
const name = document.createElement('span');
|
|
4362
|
+
name.textContent = target?.title ?? line.to;
|
|
4363
|
+
row.append(name);
|
|
4364
|
+
if (!this.disableConnections) {
|
|
4365
|
+
const remove = document.createElement('button');
|
|
4366
|
+
remove.type = 'button';
|
|
4367
|
+
remove.setAttribute('part', 'lead-remove');
|
|
4368
|
+
remove.setAttribute('aria-label', `Remove the connection to ${target?.title ?? line.to}`);
|
|
4369
|
+
remove.append(variableGlyph());
|
|
4370
|
+
remove.disabled = this.locked;
|
|
4371
|
+
remove.onclick = () => { if (current(remove))
|
|
4372
|
+
this.requestEdit({ type: 'disconnect', lineId: line.id }); };
|
|
4373
|
+
row.append(remove);
|
|
4374
|
+
}
|
|
4375
|
+
list.append(row);
|
|
4376
|
+
}
|
|
4377
|
+
if (outgoing.length)
|
|
4378
|
+
leads.append(list);
|
|
4379
|
+
else {
|
|
4380
|
+
const empty = document.createElement('p');
|
|
4381
|
+
empty.setAttribute('part', 'lead-help');
|
|
4382
|
+
empty.textContent = 'Nothing yet. Drag from a dot on the step, or pick one below.';
|
|
4383
|
+
leads.append(empty);
|
|
4384
|
+
}
|
|
4385
|
+
const eligible = this.projection.boxes.filter(box => isFlowBox(box) && box.id !== selected.id && !['start', 'timer'].includes(box.kind) && box.parentId === selected.parentId && !outgoing.some(line => line.to === box.id));
|
|
4386
|
+
if (eligible.length) {
|
|
4387
|
+
const label = variableLabel('Connect to');
|
|
4388
|
+
label.setAttribute('part', 'lead-target');
|
|
4389
|
+
const select = document.createElement('select');
|
|
4390
|
+
select.setAttribute('aria-label', 'Connect to');
|
|
4391
|
+
select.disabled = this.locked || this.disableConnections;
|
|
4392
|
+
const placeholder = document.createElement('option');
|
|
4393
|
+
placeholder.value = '';
|
|
4394
|
+
placeholder.textContent = 'Connect to…';
|
|
4395
|
+
select.append(placeholder);
|
|
4396
|
+
for (const box of eligible) {
|
|
4397
|
+
const option = document.createElement('option');
|
|
4398
|
+
option.value = box.id;
|
|
4399
|
+
option.textContent = box.title;
|
|
4400
|
+
select.append(option);
|
|
4401
|
+
}
|
|
4402
|
+
select.onchange = () => { const to = select.value; if (to && current(select) && eligible.some(box => box.id === to))
|
|
4403
|
+
this.requestEdit({ type: 'connect', from: selected.id, to }); select.value = ''; };
|
|
4404
|
+
label.append(select);
|
|
4405
|
+
leads.append(label);
|
|
4406
|
+
}
|
|
4407
|
+
if (previous)
|
|
4408
|
+
previous.replaceWith(leads);
|
|
4409
|
+
else
|
|
4410
|
+
editor.append(leads);
|
|
4411
|
+
}
|
|
3848
4412
|
renderSelection() {
|
|
3849
4413
|
this.shadowRoot?.querySelectorAll('[part=note]').forEach(note => { note.dataset.selected = String(this.selectedNoteIds.has(note.dataset.noteId)); note.setAttribute('aria-pressed', note.dataset.selected); });
|
|
3850
4414
|
if (!this.isRendered)
|
|
3851
4415
|
return;
|
|
3852
4416
|
const focusedControl = this.shadowRoot.activeElement;
|
|
4417
|
+
const focusedLead = focusedControl?.dataset.lead, focusedLeadOwner = focusedControl?.dataset.leadOwner;
|
|
4418
|
+
const focusedLeadSession = this.layoutEditSession;
|
|
4419
|
+
const summaryOwner = focusedControl?.tagName === 'SUMMARY' ? focusedControl.parentElement : null;
|
|
4420
|
+
const focusedDisclosure = summaryOwner?.matches('[part=field-disclosure]') ? summaryOwner : null;
|
|
4421
|
+
const disclosureOwner = this.inspectedId;
|
|
4422
|
+
const focusedDisclosureKey = focusedDisclosure?.dataset.disclosure;
|
|
4423
|
+
const disclosureSession = focusedDisclosure?.dataset.session;
|
|
3853
4424
|
const focusedKey = focusedControl?.dataset.field ?? focusedControl?.dataset.variable ?? focusedControl?.dataset.localVariable;
|
|
3854
4425
|
const variableKey = focusedControl?.dataset.variableKey;
|
|
3855
4426
|
const fieldBox = focusedControl?.dataset.fieldBox;
|
|
@@ -3860,6 +4431,22 @@ export class ProcessModeler extends BaseElement {
|
|
|
3860
4431
|
const direction = focusedControl && 'selectionDirection' in focusedControl ? focusedControl.selectionDirection : null;
|
|
3861
4432
|
const scrollTop = focusedControl?.scrollTop, scrollLeft = focusedControl?.scrollLeft;
|
|
3862
4433
|
const restoreControlFocus = () => {
|
|
4434
|
+
if (focusedLead !== undefined) {
|
|
4435
|
+
if (this.selected?.id !== focusedLeadOwner || focusedLeadSession !== this.layoutEditSession || this.locked || this.disableConnections)
|
|
4436
|
+
return;
|
|
4437
|
+
const replacement = Array.from(this.shadowRoot.querySelectorAll('[part=lead-control]')).find(input => input.dataset.lead === focusedLead && input.dataset.leadOwner === focusedLeadOwner);
|
|
4438
|
+
replacement?.focus({ preventScroll: true });
|
|
4439
|
+
if (replacement && caret !== null && replacement.selectionStart !== null)
|
|
4440
|
+
replacement.setSelectionRange(caret, selectionEnd ?? caret);
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
if (focusedDisclosureKey !== undefined) {
|
|
4444
|
+
if (this.selected?.id !== disclosureOwner || disclosureSession !== String(this.layoutEditSession))
|
|
4445
|
+
return;
|
|
4446
|
+
const replacement = Array.from(this.shadowRoot.querySelectorAll('[part=field-disclosure]')).find(item => item.dataset.disclosure === focusedDisclosureKey && item.dataset.session === disclosureSession);
|
|
4447
|
+
replacement?.querySelector('summary')?.focus({ preventScroll: true });
|
|
4448
|
+
return;
|
|
4449
|
+
}
|
|
3863
4450
|
if (focusedKey === undefined || !focusedType)
|
|
3864
4451
|
return;
|
|
3865
4452
|
if (focusedType === 'field' && (this.selected?.id !== fieldBox || this.locked))
|
|
@@ -3896,8 +4483,23 @@ export class ProcessModeler extends BaseElement {
|
|
|
3896
4483
|
}
|
|
3897
4484
|
if (!control || control.disabled || control.closest('[inert]'))
|
|
3898
4485
|
return;
|
|
3899
|
-
control.
|
|
3900
|
-
if (
|
|
4486
|
+
const closedDisclosure = control.closest('[part=field-disclosure]:not([open])');
|
|
4487
|
+
if (closedDisclosure) {
|
|
4488
|
+
closedDisclosure.querySelector('summary')?.focus({ preventScroll: true });
|
|
4489
|
+
return;
|
|
4490
|
+
}
|
|
4491
|
+
const choice = this.pendingActionChoice;
|
|
4492
|
+
// Hosts may echo the committed value repeatedly. Keep focus restoration
|
|
4493
|
+
// quiet until the user explicitly reopens or searches this picker.
|
|
4494
|
+
const quiet = Boolean(focusedType === 'field' && choice && choice.boxId === fieldBox && choice.key === focusedKey && choice.session === this.layoutEditSession && this.fieldsValue[fieldBox]?.some(field => field.key === focusedKey && field.value === choice.value));
|
|
4495
|
+
this.quietActionFocus = quiet;
|
|
4496
|
+
try {
|
|
4497
|
+
control.focus({ preventScroll: true });
|
|
4498
|
+
}
|
|
4499
|
+
finally {
|
|
4500
|
+
this.quietActionFocus = false;
|
|
4501
|
+
}
|
|
4502
|
+
if (caret !== null && (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.selectionStart !== null)
|
|
3901
4503
|
control.setSelectionRange(caret, selectionEnd ?? caret, direction ?? undefined);
|
|
3902
4504
|
if (scrollTop !== undefined)
|
|
3903
4505
|
control.scrollTop = scrollTop;
|
|
@@ -3905,7 +4507,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
3905
4507
|
control.scrollLeft = scrollLeft;
|
|
3906
4508
|
};
|
|
3907
4509
|
this.shadowRoot.querySelectorAll('[part=minimap] [data-map-box]').forEach(rect => rect.dataset.selected = String(this.selectedIds.has(rect.dataset.mapBox)));
|
|
3908
|
-
this.shadowRoot.querySelectorAll('[part=line]').forEach(line => { const selected = this.selectedLineIds.has(line.dataset.lineId); line.dataset.selected = String(selected); line.
|
|
4510
|
+
this.shadowRoot.querySelectorAll('[part=line]').forEach(line => { const selected = this.selectedLineIds.has(line.dataset.lineId); line.dataset.selected = String(selected); if (line.dataset.role !== 'association')
|
|
4511
|
+
line.setAttribute('marker-end', `url(#${selected ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`); });
|
|
3909
4512
|
this.shadowRoot.querySelectorAll("[data-box-id]").forEach((element) => {
|
|
3910
4513
|
element.setAttribute("aria-current", String(this.selectedIds.has(element.dataset.boxId)));
|
|
3911
4514
|
const box = this.projection.boxes.find((box) => box.id === element.dataset.boxId);
|
|
@@ -3916,7 +4519,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3916
4519
|
const editor = this.shadowRoot.querySelector("[part=editor]");
|
|
3917
4520
|
const selected = this.selected;
|
|
3918
4521
|
const fallbackScope = selected && (this.fieldsValue[selected.id] ?? []).some(field => field.kind === 'expression' && field.expression?.variables === undefined) ? this.variables.map(({ name, description }) => ({ name, description })) : undefined;
|
|
3919
|
-
const controlsKey = `${JSON.stringify(fallbackScope)}|${[...this.selectedIds].join(',')}|${this.locked}|${this.disableConnections}|${this.showLastRunValue}|${selected?.runMetrics}|${JSON.stringify([selected?.localVariables, selected?.localVariablesEditable, selected?.frame])}|${JSON.stringify(selected ? this.fieldsValue[selected.id] ?? [] : [])}`;
|
|
4522
|
+
const controlsKey = `${JSON.stringify(fallbackScope)}|${[...this.selectedIds].join(',')}|${this.locked}|${this.disableConnections}|${this.showLastRunValue}|${selected?.runMetrics}|${selected?.role}|${JSON.stringify([selected?.localVariables, selected?.localVariablesEditable, selected?.frame])}|${JSON.stringify(selected ? this.fieldsValue[selected.id] ?? [] : [])}`;
|
|
3920
4523
|
this.shadowRoot.querySelector("[part=palette]").hidden = false;
|
|
3921
4524
|
this.shadowRoot.querySelector('[part=selection-toolbar]').hidden = this.selection.length === 0;
|
|
3922
4525
|
this.renderSelectionToolbar();
|
|
@@ -3926,23 +4529,31 @@ export class ProcessModeler extends BaseElement {
|
|
|
3926
4529
|
this.inspectedId === selected?.id &&
|
|
3927
4530
|
this.inspectedControls === controlsKey &&
|
|
3928
4531
|
editor.querySelector(`[part=inspector-heading]`)?.tagName === `H${this.headingLevel}`) {
|
|
3929
|
-
editor.querySelector("[part=inspector-heading]").textContent = selected?.title ?? "";
|
|
4532
|
+
editor.querySelector("[part=inspector-heading]").textContent = this.selectedIds.size > 1 ? `${this.selectedIds.size} steps selected` : selected?.title ?? "";
|
|
3930
4533
|
const locals = editor.querySelector('[part=local-variables]');
|
|
3931
|
-
if (selected && locals && locals.dataset.localEchoRevision !== String(this.localEchoRevision)) {
|
|
4534
|
+
if (selected && this.selectedIds.size === 1 && locals && locals.dataset.localEchoRevision !== String(this.localEchoRevision)) {
|
|
3932
4535
|
// Reconcile controlled local drafts without disposing the host's custom
|
|
3933
4536
|
// inspector or unrelated focused controls on unchanged document echoes.
|
|
3934
4537
|
locals.replaceWith(this.renderLocalVariables(selected));
|
|
3935
4538
|
}
|
|
4539
|
+
if (selected && this.selectedIds.size === 1)
|
|
4540
|
+
this.renderLeadsTo(editor, selected);
|
|
3936
4541
|
restoreControlFocus();
|
|
3937
4542
|
return;
|
|
3938
4543
|
}
|
|
3939
4544
|
const expressionHelp = new Map();
|
|
4545
|
+
const disclosures = new Map();
|
|
3940
4546
|
if (this.inspectedId === selected?.id && this.selectedIds.size === 1) {
|
|
4547
|
+
for (const disclosure of Array.from(editor.querySelectorAll('[part=field-disclosure]'))) {
|
|
4548
|
+
if (disclosure.dataset.session === String(this.layoutEditSession))
|
|
4549
|
+
disclosures.set(disclosure.dataset.disclosure, { open: disclosure.open, configuredOpen: disclosure.dataset.configuredOpen === 'true' });
|
|
4550
|
+
}
|
|
3941
4551
|
for (const help of Array.from(editor.querySelectorAll('[part=expression-help]'))) {
|
|
3942
4552
|
if (help.dataset.session === String(this.layoutEditSession))
|
|
3943
4553
|
expressionHelp.set(help.dataset.helpField, help.open);
|
|
3944
4554
|
}
|
|
3945
4555
|
}
|
|
4556
|
+
this.cleanupInspectorFields();
|
|
3946
4557
|
this.cleanupInspector?.();
|
|
3947
4558
|
this.cleanupInspector = undefined;
|
|
3948
4559
|
editor.replaceChildren();
|
|
@@ -3991,44 +4602,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
3991
4602
|
purpose.textContent = selected.description;
|
|
3992
4603
|
editor.append(purpose);
|
|
3993
4604
|
}
|
|
3994
|
-
|
|
3995
|
-
editor.append(this.renderField(selected, field, expressionHelp.get(field.key)));
|
|
4605
|
+
this.renderFields(editor, selected, expressionHelp, disclosures);
|
|
3996
4606
|
if (selected.localVariables !== undefined)
|
|
3997
4607
|
editor.append(this.renderLocalVariables(selected));
|
|
3998
4608
|
this.cleanupInspector =
|
|
3999
4609
|
this.renderer?.(selected.node, editor) || undefined;
|
|
4000
|
-
|
|
4001
|
-
const
|
|
4002
|
-
leads.setAttribute('part', 'leads-to');
|
|
4003
|
-
const leadsHeading = document.createElement('h3');
|
|
4004
|
-
leadsHeading.textContent = 'Leads to';
|
|
4005
|
-
leads.append(leadsHeading);
|
|
4006
|
-
for (const line of outgoing) {
|
|
4007
|
-
const row = document.createElement('div');
|
|
4008
|
-
row.setAttribute('part', 'lead-row');
|
|
4009
|
-
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
4010
|
-
const name = document.createElement('span');
|
|
4011
|
-
name.textContent = `${line.label ? `${line.label}: ` : ''}${target?.title ?? line.to}`;
|
|
4012
|
-
row.append(name);
|
|
4013
|
-
if (!this.disableConnections) {
|
|
4014
|
-
const remove = document.createElement('button');
|
|
4015
|
-
remove.type = 'button';
|
|
4016
|
-
remove.textContent = 'Remove';
|
|
4017
|
-
remove.setAttribute('aria-label', `Remove connection to ${target?.title ?? line.to}`);
|
|
4018
|
-
remove.disabled = this.locked;
|
|
4019
|
-
remove.onclick = () => this.requestEdit({ type: 'disconnect', lineId: line.id });
|
|
4020
|
-
row.append(remove);
|
|
4021
|
-
}
|
|
4022
|
-
leads.append(row);
|
|
4023
|
-
}
|
|
4024
|
-
const connect = document.createElement('button');
|
|
4025
|
-
connect.type = 'button';
|
|
4026
|
-
connect.textContent = 'Connect to…';
|
|
4027
|
-
connect.disabled = this.locked || this.disableConnections;
|
|
4028
|
-
connect.onclick = () => { this.connecting = selected.id; this.setStatus(`Select the next box to connect from ${selected.title}`); };
|
|
4029
|
-
leads.append(connect);
|
|
4030
|
-
editor.append(leads);
|
|
4031
|
-
const metrics = this.showLastRunValue && selected.runMetrics !== false ? this.lastRunValue?.steps[selected.id] : undefined;
|
|
4610
|
+
this.renderLeadsTo(editor, selected);
|
|
4611
|
+
const metrics = isFlowBox(selected) && this.showLastRunValue && selected.runMetrics !== false ? this.lastRunValue?.steps[selected.id] : undefined;
|
|
4032
4612
|
if (metrics) {
|
|
4033
4613
|
const report = document.createElement('section');
|
|
4034
4614
|
report.setAttribute('part', 'inspector-metrics');
|
|
@@ -4062,22 +4642,25 @@ export class ProcessModeler extends BaseElement {
|
|
|
4062
4642
|
}
|
|
4063
4643
|
report.append(list);
|
|
4064
4644
|
}
|
|
4065
|
-
editor.insertBefore(report, leads);
|
|
4645
|
+
editor.insertBefore(report, editor.querySelector('[part=leads-to]'));
|
|
4066
4646
|
}
|
|
4067
4647
|
const actions = document.createElement('div');
|
|
4068
4648
|
actions.setAttribute('part', 'inspector-actions');
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4649
|
+
if (!['start', 'timer'].includes(selected.kind)) {
|
|
4650
|
+
const duplicate = document.createElement('button');
|
|
4651
|
+
duplicate.type = 'button';
|
|
4652
|
+
duplicate.textContent = 'Duplicate';
|
|
4653
|
+
duplicate.disabled = this.locked;
|
|
4654
|
+
duplicate.onclick = () => this.duplicateBoxes([selected.id], 48);
|
|
4655
|
+
const remove = document.createElement('button');
|
|
4656
|
+
remove.type = 'button';
|
|
4657
|
+
remove.textContent = 'Delete';
|
|
4658
|
+
remove.disabled = this.locked;
|
|
4659
|
+
remove.onclick = () => this.requestEdit({ type: 'delete', boxId: selected.id });
|
|
4660
|
+
actions.append(duplicate, remove);
|
|
4661
|
+
}
|
|
4662
|
+
if (actions.childElementCount)
|
|
4663
|
+
editor.append(actions);
|
|
4081
4664
|
}
|
|
4082
4665
|
restoreControlFocus();
|
|
4083
4666
|
}
|
|
@@ -4187,7 +4770,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
4187
4770
|
}
|
|
4188
4771
|
renderSelectionToolbar() {
|
|
4189
4772
|
const toolbar = this.shadowRoot.querySelector('[part=selection-toolbar]');
|
|
4190
|
-
const key = `${JSON.stringify(this.selection)}|${this.graphSelection}|${this.selected?.kind}|${this.selected?.shape}|${this.selectedLineId}|${Boolean(this.selectedLine && (this.layoutValue.lines?.[this.selectedLine.id] || this.selectedLine.fromSide || this.selectedLine.toSide || this.selectedLine.points?.length))}|${this.locked}|${this.narrowValue}|${this.projection.lines.filter(line => line.from === this.selectedId).map(line => `${line.id}:${line.label}:${line.dashed}`).join(',')}`;
|
|
4773
|
+
const key = `${JSON.stringify(this.selection)}|${this.graphSelection}|${this.selected?.kind}|${this.selected?.shape}|${this.selected?.role}|${this.selectedLine?.role}|${this.selectedLineId}|${Boolean(this.selectedLine && (this.layoutValue.lines?.[this.selectedLine.id] || this.selectedLine.fromSide || this.selectedLine.toSide || this.selectedLine.points?.length))}|${this.locked}|${this.narrowValue}|${this.projection.lines.filter(line => line.from === this.selectedId).map(line => `${line.id}:${line.label}:${line.dashed}`).join(',')}`;
|
|
4191
4774
|
if (key === this.selectionToolbarKey)
|
|
4192
4775
|
return;
|
|
4193
4776
|
this.selectionToolbarKey = key;
|
|
@@ -4202,10 +4785,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
4202
4785
|
button.disabled = this.locked;
|
|
4203
4786
|
button.onclick = action;
|
|
4204
4787
|
if (withPlus) {
|
|
4205
|
-
const icon =
|
|
4788
|
+
const icon = variableGlyph(true);
|
|
4206
4789
|
icon.setAttribute('part', 'selection-plus');
|
|
4207
4790
|
icon.setAttribute('aria-hidden', 'true');
|
|
4208
|
-
icon.
|
|
4791
|
+
icon.setAttribute('focusable', 'false');
|
|
4209
4792
|
button.append(icon);
|
|
4210
4793
|
}
|
|
4211
4794
|
const text = document.createElement('span');
|
|
@@ -4214,24 +4797,27 @@ export class ProcessModeler extends BaseElement {
|
|
|
4214
4797
|
toolbar.append(button);
|
|
4215
4798
|
};
|
|
4216
4799
|
if (this.graphSelection && (this.selectedNoteIds.size || this.selectedLineIds.size > 1 || this.selectedLineIds.size && this.selectedIds.size)) {
|
|
4217
|
-
if (this.selectedIds.size
|
|
4800
|
+
if (this.selectedIds.size) {
|
|
4218
4801
|
add('Line up', 'align', () => this.selectionCommand('align'));
|
|
4802
|
+
add('Tidy these', 'space', () => this.selectionCommand('space'));
|
|
4803
|
+
}
|
|
4219
4804
|
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
4220
4805
|
}
|
|
4221
4806
|
else if (this.selectedLine) {
|
|
4222
4807
|
const line = this.selectedLine;
|
|
4223
|
-
|
|
4224
|
-
|
|
4808
|
+
if (isFlowLine(line))
|
|
4809
|
+
add('Insert a step', 'insert', () => this.openLineChooser(line, toolbar.querySelector('[data-selection-command=insert]') ?? undefined), true);
|
|
4810
|
+
if (isFlowLine(line) && (this.layoutValue.lines?.[line.id] || line.fromSide || line.toSide || line.points?.length))
|
|
4225
4811
|
add('Reset line', 'reset-line', () => this.resetLine(line.id));
|
|
4226
4812
|
if (!this.disableConnections)
|
|
4227
4813
|
add('Delete', 'delete-line', () => this.requestEdit({ type: 'disconnect', lineId: line.id }));
|
|
4228
4814
|
}
|
|
4229
4815
|
else if (this.selectedIds.size === 1) {
|
|
4230
4816
|
const box = this.selected;
|
|
4231
|
-
const failurePath = box.kind === 'try' && !this.projection.lines.some(line => line.from === box.id && (line.dashed || line.label === 'If it fails'));
|
|
4817
|
+
const failurePath = isFlowBox(box) && box.kind === 'try' && !this.projection.lines.some(line => isFlowLine(line) && line.from === box.id && (line.dashed || line.label === 'If it fails'));
|
|
4232
4818
|
if (this.narrowValue)
|
|
4233
4819
|
add('Details', 'details', () => this.openDrawer('inspector'));
|
|
4234
|
-
if (!['end', 'finish', 'section', 'note'].includes(box.kind))
|
|
4820
|
+
if (isFlowBox(box) && !['end', 'finish', 'section', 'note'].includes(box.kind))
|
|
4235
4821
|
add(box.shape === 'gateway' ? 'Add a path' : 'Add next', 'add-next', () => this.openAnchoredChooser(this.nextEdit(box), this.nextChooserTitle(box), toolbar.querySelector('[data-selection-command=add-next]') ?? undefined), true);
|
|
4236
4822
|
if (failurePath)
|
|
4237
4823
|
add('If it fails', 'add-failure', () => this.openAnchoredChooser({ type: 'add', from: box.id, routeLabel: 'If it fails', dashed: true, placement: { source: 'next' } }, `If a step in ${box.title} fails`, toolbar.querySelector('[data-selection-command=add-failure]') ?? undefined, Array.from(this.shadowRoot.querySelectorAll('[data-box-id]')).find(element => element.dataset.boxId === box.id)), true);
|
|
@@ -4356,9 +4942,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
4356
4942
|
positions.push(...(this.layoutValue.notes ?? []).filter(note => this.selectedNoteIds.has(note.id)).map(note => ({ ...note, width: 208, height: 80 })));
|
|
4357
4943
|
if (!positions.length && this.selectedLine) {
|
|
4358
4944
|
const point = lineMidpoint(this.routedLines.get(this.selectedLine.id) ?? []);
|
|
4359
|
-
|
|
4360
|
-
toolbar.style.top = `${Math.max(8, point.y * this.viewport.zoom + this.viewport.y - 52)}px`;
|
|
4361
|
-
return;
|
|
4945
|
+
positions.push({ x: point.x, y: point.y - 14 / this.viewport.zoom, width: 0, height: 0 });
|
|
4362
4946
|
}
|
|
4363
4947
|
if (!positions.length) {
|
|
4364
4948
|
for (const id of this.selectedLineIds) {
|
|
@@ -4372,10 +4956,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
4372
4956
|
if (!positions.length)
|
|
4373
4957
|
return;
|
|
4374
4958
|
const left = Math.min(...positions.map(position => position.x * this.viewport.zoom + this.viewport.x));
|
|
4959
|
+
const right = Math.max(...positions.map(position => (position.x + (position.width ?? 224)) * this.viewport.zoom + this.viewport.x));
|
|
4375
4960
|
const top = Math.min(...positions.map(position => position.y * this.viewport.zoom + this.viewport.y));
|
|
4376
4961
|
const bottom = Math.max(...positions.map(position => (position.y + (position.height ?? 64)) * this.viewport.zoom + this.viewport.y));
|
|
4377
|
-
toolbar.style.left = `${Math.
|
|
4378
|
-
toolbar.style.top = `${top
|
|
4962
|
+
toolbar.style.left = `${Math.min(Math.max((left + right) / 2, 120), canvas.clientWidth - 120)}px`;
|
|
4963
|
+
toolbar.style.top = `${top - 12 < 56 ? bottom + 52 : top - 12}px`;
|
|
4379
4964
|
}
|
|
4380
4965
|
focusBox(id) {
|
|
4381
4966
|
Array.from(this.shadowRoot.querySelectorAll("[data-box-id]"))
|