@unofficialbox/box-open-elements 0.35.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/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 +69 -22
- 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 +10 -0
- package/dist/patterns/process-modeler/process-modeler.js +747 -184
- package/package.json +1 -1
|
@@ -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 = "";
|
|
@@ -160,6 +166,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
160
166
|
narrowValue = false;
|
|
161
167
|
suppressClick = false;
|
|
162
168
|
resizeObserver;
|
|
169
|
+
autoNoteHeights = new Set();
|
|
170
|
+
unplacedNotes = new Set();
|
|
163
171
|
routedLines = new Map();
|
|
164
172
|
computedChecks = [];
|
|
165
173
|
get selectedBoxes() { return this.projection.boxes.filter(box => this.selectedIds.has(box.id)); }
|
|
@@ -236,6 +244,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
236
244
|
this.lastProjection = "";
|
|
237
245
|
this.lastReadableProjection = "";
|
|
238
246
|
this.readableValue = null;
|
|
247
|
+
this.marquee = undefined;
|
|
248
|
+
this.pointers.clear();
|
|
249
|
+
this.autoNoteHeights.clear();
|
|
239
250
|
this.layoutValue = restoreProcessPositions(projection, options.positions ?? []);
|
|
240
251
|
this.clearMixedSelection();
|
|
241
252
|
this.history.clear();
|
|
@@ -311,7 +322,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
311
322
|
set embedMode(value) { this.toggleAttribute('embed-mode', value); }
|
|
312
323
|
arrangedLayout(layout) {
|
|
313
324
|
const arranged = this.model.arrange?.(this.projection) ?? arrangeProcess(this.projection);
|
|
314
|
-
|
|
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 });
|
|
315
329
|
}
|
|
316
330
|
get document() {
|
|
317
331
|
return this.documentValue;
|
|
@@ -346,6 +360,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
346
360
|
if (JSON.stringify(this.layoutValue) === JSON.stringify(value))
|
|
347
361
|
return;
|
|
348
362
|
this.layoutValue = structuredClone(value);
|
|
363
|
+
this.autoNoteHeights.clear();
|
|
349
364
|
if (!this.history.replaying)
|
|
350
365
|
this.history.clear();
|
|
351
366
|
this.refresh();
|
|
@@ -391,6 +406,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
391
406
|
return this.renderer;
|
|
392
407
|
}
|
|
393
408
|
set renderInspector(value) {
|
|
409
|
+
this.cleanupInspectorFields();
|
|
394
410
|
this.cleanupInspector?.();
|
|
395
411
|
this.cleanupInspector = undefined;
|
|
396
412
|
this.inspected = undefined;
|
|
@@ -404,6 +420,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
404
420
|
this.viewMenuDocument?.removeEventListener('pointerdown', this.dismissViewMenuOutside, true);
|
|
405
421
|
this.viewMenuDocument = undefined;
|
|
406
422
|
this.layoutEditSession++;
|
|
423
|
+
this.pendingActionChoice = undefined;
|
|
407
424
|
this.resizeObserver?.disconnect();
|
|
408
425
|
this.clearClipboard();
|
|
409
426
|
this.cancelPalettePointer();
|
|
@@ -412,6 +429,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
412
429
|
if (chooser)
|
|
413
430
|
dismissModal(chooser);
|
|
414
431
|
this.closeKeyboardChooser();
|
|
432
|
+
this.cleanupInspectorFields();
|
|
415
433
|
this.cleanupInspector?.();
|
|
416
434
|
this.cleanupInspector = undefined;
|
|
417
435
|
this.inspected = undefined;
|
|
@@ -485,7 +503,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
485
503
|
return this.model.connectionProblem(this.documentValue, { from, to, ...(line ? { ignoreLineId: line.id } : {}) }, this.projection) || null;
|
|
486
504
|
}
|
|
487
505
|
requestEdit(edit) {
|
|
488
|
-
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))))
|
|
489
520
|
return;
|
|
490
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))) {
|
|
491
522
|
this.setStatus('The selected graph items are invalid', true);
|
|
@@ -544,6 +575,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
544
575
|
}
|
|
545
576
|
editAcceptance(edit) {
|
|
546
577
|
const before = this.layout;
|
|
578
|
+
const beforeAutoHeights = new Set(this.autoNoteHeights);
|
|
547
579
|
const beforeSelection = [...this.selectedIds];
|
|
548
580
|
const beforeItems = this.selection;
|
|
549
581
|
const previousIds = new Set(this.projection.boxes.map((box) => box.id));
|
|
@@ -557,8 +589,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
557
589
|
// replace the document. Publish only after the whole history entry exists.
|
|
558
590
|
this.acceptanceNotifications++;
|
|
559
591
|
try {
|
|
560
|
-
if (command.layout)
|
|
592
|
+
if (command.layout) {
|
|
593
|
+
this.autoNoteHeights.clear();
|
|
561
594
|
this.layoutValue = structuredClone(command.layout);
|
|
595
|
+
}
|
|
562
596
|
if (edit.type === 'reset-line' && edit.lineId)
|
|
563
597
|
delete this.layoutValue.lines?.[edit.lineId];
|
|
564
598
|
this.refresh();
|
|
@@ -579,9 +613,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
579
613
|
if (session !== this.layoutEditSession)
|
|
580
614
|
return;
|
|
581
615
|
const after = this.layout;
|
|
616
|
+
const afterAutoHeights = new Set(this.autoNoteHeights);
|
|
582
617
|
const acceptedItems = command.selection ? command.selection.map(item => ({ type: item.type, id: item.id })) : undefined;
|
|
583
618
|
const acceptedSelection = command.selectionIds ? [...command.selectionIds] : undefined;
|
|
584
|
-
const restore = (layout) => {
|
|
619
|
+
const restore = (layout, autoHeights) => {
|
|
620
|
+
this.autoNoteHeights = new Set(autoHeights);
|
|
585
621
|
this.layoutValue = structuredClone(layout);
|
|
586
622
|
this.refresh();
|
|
587
623
|
this.emitPositions();
|
|
@@ -589,7 +625,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
589
625
|
this.history.record({
|
|
590
626
|
undo: () => {
|
|
591
627
|
command.undo();
|
|
592
|
-
restore(before);
|
|
628
|
+
restore(before, beforeAutoHeights);
|
|
593
629
|
if (acceptedItems)
|
|
594
630
|
this.selectItems(beforeItems);
|
|
595
631
|
else if (acceptedSelection)
|
|
@@ -597,7 +633,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
597
633
|
},
|
|
598
634
|
redo: () => {
|
|
599
635
|
command.redo();
|
|
600
|
-
restore(after);
|
|
636
|
+
restore(after, afterAutoHeights);
|
|
601
637
|
if (acceptedItems)
|
|
602
638
|
this.selectItems(acceptedItems);
|
|
603
639
|
else if (acceptedSelection)
|
|
@@ -681,17 +717,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
681
717
|
commitLayout(next) {
|
|
682
718
|
const before = this.layout;
|
|
683
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));
|
|
684
722
|
if (JSON.stringify(before) === JSON.stringify(after) || this.locked)
|
|
685
723
|
return;
|
|
686
|
-
const apply = (layout) => {
|
|
724
|
+
const apply = (layout, autoHeights) => {
|
|
725
|
+
this.autoNoteHeights = new Set(autoHeights);
|
|
687
726
|
this.layoutValue = structuredClone(layout);
|
|
688
727
|
this.refresh();
|
|
689
728
|
this.emitPositions();
|
|
690
729
|
};
|
|
691
|
-
apply(after);
|
|
730
|
+
apply(after, afterAutoHeights);
|
|
692
731
|
this.history.record({
|
|
693
|
-
undo: () => apply(before),
|
|
694
|
-
redo: () => apply(after),
|
|
732
|
+
undo: () => apply(before, beforeAutoHeights),
|
|
733
|
+
redo: () => apply(after, afterAutoHeights),
|
|
695
734
|
});
|
|
696
735
|
this.updateToolbar();
|
|
697
736
|
}
|
|
@@ -806,7 +845,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
806
845
|
this.commitLayout(next);
|
|
807
846
|
}
|
|
808
847
|
routeLine(id, points) {
|
|
809
|
-
if (!this.projection.lines.some((line) => line.id === id) ||
|
|
848
|
+
if (!this.projection.lines.some((line) => line.id === id && isFlowLine(line)) ||
|
|
810
849
|
points.some((point) => !Number.isFinite(point.x) || !Number.isFinite(point.y)))
|
|
811
850
|
return;
|
|
812
851
|
this.commitLayout({
|
|
@@ -819,7 +858,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
819
858
|
}
|
|
820
859
|
resetLine(id) {
|
|
821
860
|
const line = this.projection.lines.find(line => line.id === id);
|
|
822
|
-
if (this.locked || !line)
|
|
861
|
+
if (this.locked || !line || !isFlowLine(line))
|
|
823
862
|
return;
|
|
824
863
|
if (line.fromSide || line.toSide || line.points?.length) {
|
|
825
864
|
this.requestEdit({ type: 'reset-line', lineId: id });
|
|
@@ -1345,7 +1384,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
1345
1384
|
const root = this.shadowRoot;
|
|
1346
1385
|
const selected = this.selected;
|
|
1347
1386
|
root.querySelector('[part=process-title]').textContent = this.selectedIds.size > 1 ? `${this.selectedIds.size} steps selected` : selected?.title ?? this.processTitleValue;
|
|
1348
|
-
|
|
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;
|
|
1349
1391
|
root.querySelector('[part=pane-tabs]').hidden = Boolean(selected);
|
|
1350
1392
|
root.querySelectorAll('[part=pane-tabs] button').forEach(button => {
|
|
1351
1393
|
button.textContent = button.dataset.pane === "Checks" ? `Checks ${this.allChecks.length}` : button.dataset.pane ?? "";
|
|
@@ -1632,7 +1674,34 @@ export class ProcessModeler extends BaseElement {
|
|
|
1632
1674
|
content.append(actions);
|
|
1633
1675
|
}
|
|
1634
1676
|
else if (this.activePane === "Shortcuts") {
|
|
1635
|
-
|
|
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);
|
|
1636
1705
|
}
|
|
1637
1706
|
}
|
|
1638
1707
|
requestVariableEdit(edit) {
|
|
@@ -1643,6 +1712,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
1643
1712
|
}
|
|
1644
1713
|
}
|
|
1645
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
|
+
}
|
|
1646
1719
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape;
|
|
1647
1720
|
const compact = shape === 'gateway' || shape === 'event';
|
|
1648
1721
|
const position = this.layoutValue.boxes[box.id];
|
|
@@ -1675,12 +1748,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
1675
1748
|
this.arrangeSelection(width >= height ? 'middle' : 'center');
|
|
1676
1749
|
}
|
|
1677
1750
|
tidySelection() {
|
|
1678
|
-
|
|
1751
|
+
const flow = this.selectedBoxes.filter(isFlowBox);
|
|
1752
|
+
if (this.locked || flow.length < 2)
|
|
1679
1753
|
return;
|
|
1680
|
-
if (this.requestLayoutEdit('tidy',
|
|
1754
|
+
if (this.requestLayoutEdit('tidy', flow.map(box => box.id)))
|
|
1681
1755
|
return;
|
|
1682
1756
|
const next = this.layout;
|
|
1683
|
-
const roots = this.selectedBoxes.filter(box => {
|
|
1757
|
+
const roots = this.selectedBoxes.filter(isFlowBox).filter(box => {
|
|
1684
1758
|
let parent = box.parentId;
|
|
1685
1759
|
while (parent) {
|
|
1686
1760
|
if (this.selectedIds.has(parent))
|
|
@@ -1732,7 +1806,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
1732
1806
|
arrangeSelection(action) {
|
|
1733
1807
|
if (this.locked || this.selectedBoxes.length < 2)
|
|
1734
1808
|
return;
|
|
1735
|
-
const boxes = this.selectedBoxes;
|
|
1809
|
+
const boxes = this.selectedBoxes.filter(isFlowBox);
|
|
1810
|
+
if (boxes.length < 2)
|
|
1811
|
+
return;
|
|
1736
1812
|
const next = this.layout;
|
|
1737
1813
|
const sorted = [...boxes].filter(box => {
|
|
1738
1814
|
let parent = box.parentId;
|
|
@@ -1817,13 +1893,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
1817
1893
|
this.commitLayout(next);
|
|
1818
1894
|
}
|
|
1819
1895
|
makeSection(title = 'New section') {
|
|
1820
|
-
|
|
1896
|
+
const flow = this.selectedBoxes.filter(isFlowBox);
|
|
1897
|
+
if (this.locked || !flow.length)
|
|
1821
1898
|
return;
|
|
1822
|
-
if (this.requestLayoutEdit('make-section',
|
|
1899
|
+
if (this.requestLayoutEdit('make-section', flow.map(box => box.id), title))
|
|
1823
1900
|
return;
|
|
1824
|
-
if (
|
|
1901
|
+
if (flow.length < 2)
|
|
1825
1902
|
return;
|
|
1826
|
-
const positions =
|
|
1903
|
+
const positions = flow.map(box => this.layoutValue.boxes[box.id]);
|
|
1827
1904
|
const x = Math.min(...positions.map(position => position.x)) - 24;
|
|
1828
1905
|
const y = Math.min(...positions.map(position => position.y)) - 48;
|
|
1829
1906
|
const right = Math.max(...positions.map(position => position.x + (position.width ?? 224))) + 24;
|
|
@@ -2064,6 +2141,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2064
2141
|
const target = event.target;
|
|
2065
2142
|
if (target.closest("input,textarea,select,[contenteditable],button,summary"))
|
|
2066
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
|
+
}
|
|
2067
2149
|
if (event.key === ' ') {
|
|
2068
2150
|
event.preventDefault();
|
|
2069
2151
|
this.spacePressed = true;
|
|
@@ -2138,7 +2220,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2138
2220
|
this.requestEdit({ type: 'disconnect', lineId: this.selectedLine.id });
|
|
2139
2221
|
return;
|
|
2140
2222
|
}
|
|
2141
|
-
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)) {
|
|
2142
2224
|
event.preventDefault();
|
|
2143
2225
|
this.openKeyboardChooser(box);
|
|
2144
2226
|
return;
|
|
@@ -2175,7 +2257,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2175
2257
|
this.commitLayout(next);
|
|
2176
2258
|
return;
|
|
2177
2259
|
}
|
|
2178
|
-
|
|
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;
|
|
2179
2265
|
if (direction && !canInsertDirection && (event.altKey || !box || this.locked)) {
|
|
2180
2266
|
event.preventDefault();
|
|
2181
2267
|
this.navigateSelection(direction);
|
|
@@ -2211,6 +2297,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
2211
2297
|
}
|
|
2212
2298
|
if (direction) {
|
|
2213
2299
|
event.preventDefault();
|
|
2300
|
+
if (event.ctrlKey && event.altKey && !isFlowBox(box))
|
|
2301
|
+
return;
|
|
2214
2302
|
if (event.ctrlKey && event.altKey && this.catalog.length && !this.disableConnections) {
|
|
2215
2303
|
const side = direction[0] < 0 ? "west" : direction[0] > 0 ? "east" : direction[1] < 0 ? "north" : "south";
|
|
2216
2304
|
const pos = this.layoutValue.boxes[box.id];
|
|
@@ -2309,7 +2397,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2309
2397
|
return;
|
|
2310
2398
|
}
|
|
2311
2399
|
if (event.button !== 0 ||
|
|
2312
|
-
event.target.closest("[part=connection],button,input,select,textarea,[contenteditable]"))
|
|
2400
|
+
event.target.closest("[part=connection],[part=line-hit],button,input,select,textarea,[contenteditable]"))
|
|
2313
2401
|
return;
|
|
2314
2402
|
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
2315
2403
|
if (this.pointers.size === 2) {
|
|
@@ -2327,7 +2415,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2327
2415
|
this.select(id);
|
|
2328
2416
|
if (!id && event.shiftKey && !this.locked) {
|
|
2329
2417
|
const point = this.canvasPoint(event);
|
|
2330
|
-
this.marquee = { pointerId: event.pointerId, start: point, point };
|
|
2418
|
+
this.marquee = { pointerId: event.pointerId, start: point, point, add: this.selection };
|
|
2331
2419
|
canvas.setPointerCapture?.(event.pointerId);
|
|
2332
2420
|
event.preventDefault();
|
|
2333
2421
|
return;
|
|
@@ -2341,6 +2429,23 @@ export class ProcessModeler extends BaseElement {
|
|
|
2341
2429
|
panY: this.viewport.y,
|
|
2342
2430
|
};
|
|
2343
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
|
+
}
|
|
2344
2449
|
distance() {
|
|
2345
2450
|
const [a, b] = [...this.pointers.values()];
|
|
2346
2451
|
return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
|
@@ -2411,7 +2516,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2411
2516
|
return;
|
|
2412
2517
|
}
|
|
2413
2518
|
if (this.marquee) {
|
|
2414
|
-
|
|
2519
|
+
const marquee = this.marquee;
|
|
2520
|
+
marquee.point = point;
|
|
2521
|
+
this.selectMarquee(marquee.start, point, marquee.add);
|
|
2522
|
+
if (this.marquee !== marquee)
|
|
2523
|
+
return;
|
|
2415
2524
|
let region = this.shadowRoot.querySelector('[part=marquee]');
|
|
2416
2525
|
if (!region) {
|
|
2417
2526
|
region = document.createElement("div");
|
|
@@ -2503,6 +2612,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2503
2612
|
const pos = this.layoutValue.boxes[port.id];
|
|
2504
2613
|
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[port.side];
|
|
2505
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
|
+
}
|
|
2506
2620
|
const anchor = Array.from(this.shadowRoot.querySelectorAll('[part=port]'))
|
|
2507
2621
|
.find(element => element.dataset.owner === port.id && element.dataset.side === port.side);
|
|
2508
2622
|
this.openAnchoredChooser(this.nextEdit(source, port.side, { x: pos.x + offsets[0], y: pos.y + offsets[1] }), this.nextChooserTitle(source, port.side), anchor);
|
|
@@ -2513,6 +2627,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
2513
2627
|
this.requestEdit({ type: "connect", from: port.id, to: target.id, fromSide: port.side, toSide: this.sideAt(target, point) });
|
|
2514
2628
|
else {
|
|
2515
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
|
+
}
|
|
2516
2634
|
const frame = this.boxAt(point, undefined, true);
|
|
2517
2635
|
if ((frame?.id ?? null) !== (source.parentId ?? null))
|
|
2518
2636
|
this.setStatus('Drop inside the same frame to add a connected step');
|
|
@@ -2555,10 +2673,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
2555
2673
|
return;
|
|
2556
2674
|
}
|
|
2557
2675
|
if (this.marquee) {
|
|
2558
|
-
const { start } = this.marquee;
|
|
2676
|
+
const { start, add } = this.marquee;
|
|
2559
2677
|
this.marquee = undefined;
|
|
2560
2678
|
if (commit)
|
|
2561
|
-
this.
|
|
2679
|
+
this.selectMarquee(start, point, add);
|
|
2562
2680
|
this.shadowRoot.querySelector('[part=marquee]')?.remove();
|
|
2563
2681
|
this.pointers.delete(event.pointerId);
|
|
2564
2682
|
return;
|
|
@@ -2576,7 +2694,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
2576
2694
|
this.suppressClick = true;
|
|
2577
2695
|
const position = this.alignedPoint(drag.id, drag.position.x + dx / this.viewport.zoom, drag.position.y + dy / this.viewport.zoom);
|
|
2578
2696
|
const line = this.lineAt(point, drag.id);
|
|
2579
|
-
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);
|
|
2580
2699
|
if (line) {
|
|
2581
2700
|
const box = this.projection.boxes.find(box => box.id === drag.id);
|
|
2582
2701
|
const { width, height } = this.selectionDimensions(box);
|
|
@@ -2643,14 +2762,15 @@ export class ProcessModeler extends BaseElement {
|
|
|
2643
2762
|
return sides.find(([, x, y]) => Math.hypot(point.x - x, point.y - y) <= 12 / this.viewport.zoom)?.[0];
|
|
2644
2763
|
}
|
|
2645
2764
|
lineAt(point, except) {
|
|
2646
|
-
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));
|
|
2647
2766
|
}
|
|
2648
2767
|
markDropLine(point, except) {
|
|
2649
2768
|
this.dropLine = point ? this.lineAt(point, except)?.id : undefined;
|
|
2650
2769
|
this.paintDropLine();
|
|
2651
2770
|
}
|
|
2652
2771
|
paintDropLine() {
|
|
2653
|
-
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'})`); });
|
|
2654
2774
|
}
|
|
2655
2775
|
alignedPoint(id, x, y) {
|
|
2656
2776
|
const world = this.shadowRoot.querySelector('[part=world]');
|
|
@@ -2698,14 +2818,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
2698
2818
|
}
|
|
2699
2819
|
chooser.addEventListener('close', () => this.shadowRoot?.querySelector('[part=ghost-box]')?.remove(), { once: true });
|
|
2700
2820
|
const picker = chooser.querySelector('box-kind-picker');
|
|
2701
|
-
picker.catalog = this.catalog.filter(kind => !kind.placement);
|
|
2821
|
+
picker.catalog = this.catalog.filter(kind => kind.addable !== false && !kind.placement);
|
|
2702
2822
|
picker.refresh();
|
|
2703
2823
|
promoteModal(chooser);
|
|
2704
2824
|
picker.focus();
|
|
2705
2825
|
}
|
|
2706
2826
|
nextEdit(box, side, position) {
|
|
2707
2827
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape;
|
|
2708
|
-
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);
|
|
2709
2829
|
const placement = side ? { source: 'direction', side } : { source: 'next' };
|
|
2710
2830
|
if ((!side || side === 'east') && shape !== 'gateway' && outgoing.length === 1)
|
|
2711
2831
|
return { type: 'insert', lineId: outgoing[0].id, from: box.id, to: outgoing[0].to, placement };
|
|
@@ -2732,6 +2852,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
2732
2852
|
this.openAnchoredChooser(edit, title, anchor, returnFocus ?? source ?? this.shadowRoot.querySelector('[part=canvas]'));
|
|
2733
2853
|
}
|
|
2734
2854
|
openLineChooser(line, returnFocus) {
|
|
2855
|
+
if (!isFlowLine(line))
|
|
2856
|
+
return;
|
|
2735
2857
|
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
2736
2858
|
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
2737
2859
|
const points = this.routedLines.get(line.id) ?? routeProcessLine(line, this.layoutValue, this.projection);
|
|
@@ -2758,7 +2880,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2758
2880
|
};
|
|
2759
2881
|
openKeyboardChooser(box) {
|
|
2760
2882
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape ?? (box.frame ? 'frame' : 'task');
|
|
2761
|
-
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);
|
|
2762
2884
|
const target = outgoing.length === 1 ? this.projection.boxes.find(candidate => candidate.id === outgoing[0].to) : undefined;
|
|
2763
2885
|
const insert = shape !== 'gateway' && outgoing.length === 1 && target;
|
|
2764
2886
|
const anchor = Array.from(this.shadowRoot.querySelectorAll('[data-box-id]'))
|
|
@@ -2775,7 +2897,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2775
2897
|
this.keyboardInsertion = edit;
|
|
2776
2898
|
chooser.querySelector('[part=chooser-title]').textContent = title;
|
|
2777
2899
|
const picker = chooser.querySelector('box-kind-picker');
|
|
2778
|
-
picker.catalog = this.catalog.filter(kind => !kind.placement);
|
|
2900
|
+
picker.catalog = this.catalog.filter(kind => kind.addable !== false && !kind.placement);
|
|
2779
2901
|
picker.refresh();
|
|
2780
2902
|
this.keyboardReturn = returnFocus ?? anchor;
|
|
2781
2903
|
if (!anchor || !promotePopover(chooser)) {
|
|
@@ -2822,6 +2944,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2822
2944
|
else
|
|
2823
2945
|
this.projection = { boxes: [], lines: [] };
|
|
2824
2946
|
this.layoutValue = this.arrangedLayout(this.layoutValue);
|
|
2947
|
+
this.measureGraphNotes();
|
|
2825
2948
|
this.routedLines = new Map(this.projection.lines.map(line => [line.id, routeProcessLine(line, this.layoutValue, this.projection)]));
|
|
2826
2949
|
this.computedChecks = this.collectChecks();
|
|
2827
2950
|
if (this.selectedId && !this.selected)
|
|
@@ -2834,7 +2957,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2834
2957
|
if (!this.selectedId)
|
|
2835
2958
|
this.selectedId = this.selectedIds.values().next().value ?? null;
|
|
2836
2959
|
const serialized = JSON.stringify({
|
|
2837
|
-
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 })),
|
|
2838
2961
|
lines: this.projection.lines,
|
|
2839
2962
|
});
|
|
2840
2963
|
if (this.documentValue !== undefined && (serialized !== this.lastProjection || this.documentValue !== this.lastDocument)) {
|
|
@@ -2884,9 +3007,44 @@ export class ProcessModeler extends BaseElement {
|
|
|
2884
3007
|
(note ?? this.shadowRoot.querySelector('[part=canvas]'))?.focus({ preventScroll: true });
|
|
2885
3008
|
}
|
|
2886
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
|
+
}
|
|
2887
3045
|
get allChecks() { return this.computedChecks; }
|
|
2888
3046
|
collectChecks() {
|
|
2889
|
-
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)
|
|
2890
3048
|
.map(line => ({ message: "A connection is blocked by overlapping steps. Move a step or adjust its route.", boxId: line.from }));
|
|
2891
3049
|
const hostChecks = this.documentValue === undefined ? undefined : this.model.validate?.(this.documentValue, this.projection);
|
|
2892
3050
|
return [...this.checks, ...(hostChecks ?? graphChecks(this.projection)), ...routes];
|
|
@@ -2922,28 +3080,38 @@ export class ProcessModeler extends BaseElement {
|
|
|
2922
3080
|
element.setAttribute("part", box.frame ? "frame" : "box");
|
|
2923
3081
|
element.dataset.boxId = box.id;
|
|
2924
3082
|
const kind = this.catalog.find((entry) => entry.kind === box.kind);
|
|
2925
|
-
const shape = box.shape ?? kind?.shape ?? (box.frame ? "frame" : "task");
|
|
3083
|
+
const shape = !isFlowBox(box) ? "note" : box.shape ?? kind?.shape ?? (box.frame ? "frame" : "task");
|
|
2926
3084
|
element.dataset.shape = shape;
|
|
3085
|
+
if (!isFlowBox(box))
|
|
3086
|
+
element.setAttribute("aria-roledescription", "note");
|
|
2927
3087
|
element.dataset.kind = box.kind;
|
|
2928
3088
|
element.dataset.tone = kind?.tone === "accent" ? "accent" : "neutral";
|
|
2929
3089
|
const problem = this.allChecks.find(check => check.boxId === box.id || (check.path && JSON.stringify(check.path) === JSON.stringify(box.path)));
|
|
2930
|
-
const incoming = this.projection.lines.filter(line => line.to === box.id).length;
|
|
2931
|
-
const outgoing = this.projection.lines.filter(line => line.from === box.id).length;
|
|
2932
|
-
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}` : ""]
|
|
2933
3093
|
.filter(Boolean)
|
|
2934
3094
|
.join(". "));
|
|
2935
3095
|
element.setAttribute("aria-current", String(this.selectedIds.has(box.id)));
|
|
2936
|
-
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
|
+
}
|
|
2937
3103
|
const icon = document.createElement("span");
|
|
2938
3104
|
icon.setAttribute("part", "icon");
|
|
2939
3105
|
icon.setAttribute("aria-hidden", "true");
|
|
2940
3106
|
if (kind?.icon) {
|
|
2941
3107
|
icon.append(kind.icon());
|
|
2942
3108
|
}
|
|
2943
|
-
|
|
3109
|
+
if (isFlowBox(box))
|
|
3110
|
+
element.append(icon);
|
|
2944
3111
|
const title = document.createElement("strong");
|
|
2945
3112
|
title.textContent = box.title;
|
|
2946
|
-
|
|
3113
|
+
if (isFlowBox(box))
|
|
3114
|
+
element.append(title);
|
|
2947
3115
|
let descriptionParent = element;
|
|
2948
3116
|
if (shape === 'event' && this.detailValue === 'technical' && box.technicalDetails?.length) {
|
|
2949
3117
|
const details = document.createElement('div');
|
|
@@ -2973,7 +3141,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2973
3141
|
}
|
|
2974
3142
|
if (shape === 'task' && box.technicalDetails)
|
|
2975
3143
|
icon.style.gridRow = 'span 4';
|
|
2976
|
-
if (this.detailValue === "technical" && box.technicalDetails) {
|
|
3144
|
+
if (isFlowBox(box) && this.detailValue === "technical" && box.technicalDetails) {
|
|
2977
3145
|
for (const line of box.technicalDetails) {
|
|
2978
3146
|
if (!line.text && !line.segments?.length)
|
|
2979
3147
|
continue;
|
|
@@ -2996,7 +3164,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2996
3164
|
descriptionParent.append(description);
|
|
2997
3165
|
}
|
|
2998
3166
|
}
|
|
2999
|
-
else {
|
|
3167
|
+
else if (isFlowBox(box)) {
|
|
3000
3168
|
const descriptionText = this.detailValue === "technical" ? box.technicalDescription ?? box.description : box.description;
|
|
3001
3169
|
if (descriptionText) {
|
|
3002
3170
|
const description = document.createElement("small");
|
|
@@ -3081,7 +3249,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
3081
3249
|
});
|
|
3082
3250
|
this.connecting = null;
|
|
3083
3251
|
}
|
|
3084
|
-
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)
|
|
3085
3255
|
this.selectMany(this.selectedIds.has(box.id) ? [...this.selectedIds].filter(id => id !== box.id) : [...this.selectedIds, box.id]);
|
|
3086
3256
|
else
|
|
3087
3257
|
this.select(box.id);
|
|
@@ -3098,6 +3268,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
3098
3268
|
port.addEventListener("click", event => {
|
|
3099
3269
|
event.stopPropagation();
|
|
3100
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
|
+
}
|
|
3101
3276
|
const pos = this.layoutValue.boxes[box.id];
|
|
3102
3277
|
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[side];
|
|
3103
3278
|
this.openAnchoredChooser(this.nextEdit(box, side, { x: pos.x + offsets[0], y: pos.y + offsets[1] }), this.nextChooserTitle(box, side), port);
|
|
@@ -3125,6 +3300,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
3125
3300
|
element.append(handle);
|
|
3126
3301
|
}
|
|
3127
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
|
+
}
|
|
3128
3308
|
}
|
|
3129
3309
|
const lines = svgElement("svg");
|
|
3130
3310
|
lines.setAttribute("part", "lines");
|
|
@@ -3156,17 +3336,33 @@ export class ProcessModeler extends BaseElement {
|
|
|
3156
3336
|
path.setAttribute("part", "line");
|
|
3157
3337
|
path.dataset.lineId = line.id;
|
|
3158
3338
|
path.dataset.selected = String(this.selectedLineIds.has(line.id));
|
|
3159
|
-
|
|
3339
|
+
if (isFlowLine(line))
|
|
3340
|
+
path.setAttribute('marker-end', `url(#${this.selectedLineIds.has(line.id) ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`);
|
|
3160
3341
|
path.setAttribute("vector-effect", "non-scaling-stroke");
|
|
3161
3342
|
path.setAttribute("d", roundedProcessPath(points));
|
|
3162
|
-
|
|
3343
|
+
path.dataset.role = line.role ?? "flow";
|
|
3344
|
+
if (!isFlowLine(line))
|
|
3345
|
+
path.setAttribute("stroke-dasharray", "2 4");
|
|
3346
|
+
else if (line.dashed)
|
|
3163
3347
|
path.setAttribute("stroke-dasharray", "6 4");
|
|
3164
|
-
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) {
|
|
3165
3361
|
path.style.strokeWidth = `${Math.round((1.5 + Math.max(0, Math.min(1, line.share)) * 2.5) * 100) / 100}px`;
|
|
3166
3362
|
path.setAttribute('aria-label', `${Math.round(line.share * 100)}% of last-run traffic`);
|
|
3167
3363
|
}
|
|
3168
3364
|
lines.append(path);
|
|
3169
|
-
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]) {
|
|
3170
3366
|
if (!point)
|
|
3171
3367
|
continue;
|
|
3172
3368
|
const pin = svgElement('circle');
|
|
@@ -3176,7 +3372,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3176
3372
|
pin.setAttribute('r', '3');
|
|
3177
3373
|
lines.append(pin);
|
|
3178
3374
|
}
|
|
3179
|
-
for (let i = 1; i < points.length - 2; i++) {
|
|
3375
|
+
for (let i = 1; isFlowLine(line) && i < points.length - 2; i++) {
|
|
3180
3376
|
const hit = svgElement("polyline");
|
|
3181
3377
|
hit.setAttribute("part", "line-hit");
|
|
3182
3378
|
hit.dataset.lineId = line.id;
|
|
@@ -3187,6 +3383,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
3187
3383
|
}
|
|
3188
3384
|
const label = document.createElement("div");
|
|
3189
3385
|
label.setAttribute("part", "connection");
|
|
3386
|
+
label.dataset.lineId = line.id;
|
|
3387
|
+
label.dataset.role = line.role ?? "flow";
|
|
3190
3388
|
label.setAttribute('role', 'group');
|
|
3191
3389
|
label.tabIndex = 0;
|
|
3192
3390
|
label.style.left = `${midpoint.x}px`;
|
|
@@ -3194,17 +3392,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
3194
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}` : ""}`;
|
|
3195
3393
|
label.setAttribute('aria-label', `Connection: ${names}${this.showLastRunValue && line.share !== undefined ? `. ${Math.round(line.share * 100)}% of last-run traffic` : ''}`);
|
|
3196
3394
|
label.addEventListener('click', event => { if (event.target.closest('button'))
|
|
3197
|
-
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); });
|
|
3198
3399
|
label.addEventListener('keydown', event => { if (event.target === label && (event.key === 'Enter' || event.key === ' ')) {
|
|
3199
3400
|
event.preventDefault();
|
|
3200
3401
|
this.selectLine(line.id);
|
|
3201
3402
|
} });
|
|
3202
3403
|
const text = document.createElement("span");
|
|
3203
3404
|
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
3204
|
-
const branchIndex = this.projection.lines.filter(candidate => candidate.from === line.from).indexOf(line);
|
|
3205
|
-
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" : "");
|
|
3206
3407
|
label.append(text);
|
|
3207
|
-
if (this.showLastRunValue && line.share !== undefined) {
|
|
3408
|
+
if (isFlowLine(line) && this.showLastRunValue && line.share !== undefined) {
|
|
3208
3409
|
const share = document.createElement('small');
|
|
3209
3410
|
share.textContent = `${Math.round(line.share * 100)}%`;
|
|
3210
3411
|
label.append(share);
|
|
@@ -3212,7 +3413,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3212
3413
|
const actions = document.createElement("div");
|
|
3213
3414
|
actions.setAttribute("part", "connection-actions");
|
|
3214
3415
|
label.append(actions);
|
|
3215
|
-
if (points.length >= 4 && !this.locked) {
|
|
3416
|
+
if (isFlowLine(line) && points.length >= 4 && !this.locked) {
|
|
3216
3417
|
const adjust = document.createElement("button");
|
|
3217
3418
|
adjust.type = "button";
|
|
3218
3419
|
adjust.textContent = "Route";
|
|
@@ -3242,7 +3443,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3242
3443
|
["insert", "Insert a step here"],
|
|
3243
3444
|
["disconnect", "Remove connection"],
|
|
3244
3445
|
]) {
|
|
3245
|
-
if (type === "disconnect" && this.disableConnections)
|
|
3446
|
+
if ((type === "disconnect" && this.disableConnections) || (type === "insert" && !isFlowLine(line)))
|
|
3246
3447
|
continue;
|
|
3247
3448
|
const button = document.createElement("button");
|
|
3248
3449
|
button.type = "button";
|
|
@@ -3266,7 +3467,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3266
3467
|
actions.append(button);
|
|
3267
3468
|
}
|
|
3268
3469
|
world.append(label);
|
|
3269
|
-
if (this.selectedLineId === line.id && !this.locked && !this.disableConnections) {
|
|
3470
|
+
if (isFlowLine(line) && this.selectedLineId === line.id && !this.locked && !this.disableConnections) {
|
|
3270
3471
|
for (const [end, point] of [['from', points[0]], ['to', points.at(-1)]]) {
|
|
3271
3472
|
const grip = document.createElement('button');
|
|
3272
3473
|
grip.type = 'button';
|
|
@@ -3321,7 +3522,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3321
3522
|
return;
|
|
3322
3523
|
}
|
|
3323
3524
|
const selected = this.selectedIds.size === 1 ? this.selected : null;
|
|
3324
|
-
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;
|
|
3325
3526
|
if (from && selected)
|
|
3326
3527
|
this.requestEdit({ ...this.nextEdit(selected), kind });
|
|
3327
3528
|
else {
|
|
@@ -3333,7 +3534,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3333
3534
|
paletteLineAt(point) {
|
|
3334
3535
|
let nearest;
|
|
3335
3536
|
let best = 20 / this.viewport.zoom;
|
|
3336
|
-
for (const line of this.projection.lines) {
|
|
3537
|
+
for (const line of this.projection.lines.filter(isFlowLine)) {
|
|
3337
3538
|
const points = this.routedLines.get(line.id) ?? [];
|
|
3338
3539
|
for (let i = 1; i < points.length; i++) {
|
|
3339
3540
|
const a = points[i - 1], b = points[i], dx = b.x - a.x, dy = b.y - a.y, length = dx * dx + dy * dy;
|
|
@@ -3457,7 +3658,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3457
3658
|
const query = this.shadowRoot.querySelector("[part=search]").value.toLowerCase();
|
|
3458
3659
|
choices.replaceChildren();
|
|
3459
3660
|
let group = "";
|
|
3460
|
-
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(" ")}`
|
|
3461
3662
|
.toLowerCase()
|
|
3462
3663
|
.includes(query))) {
|
|
3463
3664
|
if (kind.group && kind.group !== group) {
|
|
@@ -3593,16 +3794,99 @@ export class ProcessModeler extends BaseElement {
|
|
|
3593
3794
|
checks.append(button);
|
|
3594
3795
|
});
|
|
3595
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
|
+
}
|
|
3596
3871
|
renderField(box, field, helpOpen = false) {
|
|
3597
3872
|
const row = document.createElement('div');
|
|
3598
3873
|
row.setAttribute('part', 'field');
|
|
3599
3874
|
const label = document.createElement('label');
|
|
3600
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
|
+
}
|
|
3601
3884
|
let control;
|
|
3602
3885
|
if (field.kind === 'multiline' || (field.kind === 'expression' && (field.expression?.rows ?? 1) > 1)) {
|
|
3603
3886
|
control = document.createElement('textarea');
|
|
3604
|
-
|
|
3605
|
-
|
|
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));
|
|
3606
3890
|
}
|
|
3607
3891
|
else if (field.kind === 'choice') {
|
|
3608
3892
|
const select = document.createElement('select');
|
|
@@ -3628,7 +3912,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3628
3912
|
}
|
|
3629
3913
|
else {
|
|
3630
3914
|
const input = document.createElement('input');
|
|
3631
|
-
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';
|
|
3632
3916
|
control = input;
|
|
3633
3917
|
}
|
|
3634
3918
|
control.dataset.field = field.key;
|
|
@@ -3641,6 +3925,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
3641
3925
|
control.required = Boolean(field.required);
|
|
3642
3926
|
if (field.placeholder && (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement))
|
|
3643
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
|
+
}
|
|
3644
3940
|
if (field.kind === 'expression') {
|
|
3645
3941
|
control.spellcheck = false;
|
|
3646
3942
|
control.setAttribute('autocomplete', 'off');
|
|
@@ -3648,17 +3944,27 @@ export class ProcessModeler extends BaseElement {
|
|
|
3648
3944
|
}
|
|
3649
3945
|
if (field.problem)
|
|
3650
3946
|
control.setAttribute('aria-invalid', 'true');
|
|
3651
|
-
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
|
+
};
|
|
3652
3953
|
if (field.kind !== 'action')
|
|
3653
3954
|
control.addEventListener(field.kind === 'expression' ? 'input' : 'change', submit);
|
|
3654
|
-
|
|
3955
|
+
if (field.kind === 'boolean') {
|
|
3956
|
+
row.dataset.boolean = '';
|
|
3957
|
+
label.prepend(control);
|
|
3958
|
+
}
|
|
3959
|
+
else
|
|
3960
|
+
label.append(control);
|
|
3655
3961
|
row.append(label);
|
|
3656
3962
|
if (field.kind === 'action' && control instanceof HTMLInputElement) {
|
|
3657
3963
|
const input = control;
|
|
3658
3964
|
const options = field.options ?? [];
|
|
3659
3965
|
const list = document.createElement('div');
|
|
3660
3966
|
list.setAttribute('part', 'action-options');
|
|
3661
|
-
list.id = `process-action-${
|
|
3967
|
+
list.id = `process-action-${++actionControlSequence}`;
|
|
3662
3968
|
list.hidden = true;
|
|
3663
3969
|
const optionList = document.createElement('div');
|
|
3664
3970
|
optionList.setAttribute('role', 'listbox');
|
|
@@ -3669,106 +3975,234 @@ export class ProcessModeler extends BaseElement {
|
|
|
3669
3975
|
input.setAttribute('aria-controls', optionList.id);
|
|
3670
3976
|
input.setAttribute('aria-expanded', 'false');
|
|
3671
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;
|
|
3672
3981
|
let group = null;
|
|
3673
3982
|
let active = 0;
|
|
3674
|
-
let browse =
|
|
3675
|
-
const close = () => { list.hidden = true;
|
|
3676
|
-
|
|
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
|
+
};
|
|
3677
4025
|
const render = () => {
|
|
3678
4026
|
list.replaceChildren();
|
|
3679
4027
|
optionList.replaceChildren();
|
|
3680
4028
|
const query = browse ? '' : input.value.trim().toLocaleLowerCase();
|
|
3681
|
-
const
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
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(); });
|
|
3691
4075
|
}
|
|
3692
4076
|
}
|
|
3693
4077
|
if (group) {
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
back.setAttribute('part', 'action-back');
|
|
3697
|
-
back.textContent = `All actions / ${group}`;
|
|
3698
|
-
back.onclick = () => { input.focus(); group = null; browse = true; render(); };
|
|
3699
|
-
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);
|
|
3700
4080
|
}
|
|
3701
|
-
|
|
4081
|
+
const addAction = (option, custom = false) => {
|
|
3702
4082
|
const item = document.createElement('button');
|
|
3703
4083
|
item.type = 'button';
|
|
4084
|
+
item.tabIndex = -1;
|
|
3704
4085
|
item.setAttribute('role', 'option');
|
|
3705
4086
|
item.setAttribute('part', 'action-option');
|
|
3706
|
-
item.id = `${list.id}-${index}`;
|
|
3707
|
-
item.setAttribute('aria-selected', String(index === active));
|
|
3708
4087
|
const title = document.createElement('strong');
|
|
3709
|
-
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;
|
|
3710
4089
|
item.append(title);
|
|
3711
|
-
|
|
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) {
|
|
3712
4098
|
const code = document.createElement('small');
|
|
3713
|
-
code.textContent =
|
|
4099
|
+
code.textContent = detail;
|
|
4100
|
+
if (!custom)
|
|
4101
|
+
code.dataset.format = 'code';
|
|
3714
4102
|
item.append(code);
|
|
3715
4103
|
}
|
|
3716
4104
|
item.onclick = () => choose(option);
|
|
3717
4105
|
optionList.append(item);
|
|
3718
|
-
}
|
|
3719
|
-
|
|
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]')) {
|
|
3720
4112
|
const empty = document.createElement('p');
|
|
3721
|
-
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”.';
|
|
3722
4115
|
list.append(empty);
|
|
3723
4116
|
}
|
|
3724
4117
|
list.append(optionList);
|
|
3725
|
-
|
|
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)); });
|
|
3726
4121
|
list.hidden = false;
|
|
3727
4122
|
input.setAttribute('aria-expanded', 'true');
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
input.setAttribute('aria-activedescendant', selected.id);
|
|
4123
|
+
if (items[active])
|
|
4124
|
+
input.setAttribute('aria-activedescendant', items[active].id);
|
|
3731
4125
|
else
|
|
3732
4126
|
input.removeAttribute('aria-activedescendant');
|
|
4127
|
+
place();
|
|
4128
|
+
items[active]?.scrollIntoView?.({ block: 'nearest' });
|
|
3733
4129
|
};
|
|
3734
|
-
|
|
3735
|
-
|
|
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(); });
|
|
3736
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
|
+
}
|
|
3737
4156
|
if (event.key === 'Escape') {
|
|
3738
4157
|
event.preventDefault();
|
|
4158
|
+
event.stopPropagation();
|
|
3739
4159
|
close();
|
|
3740
4160
|
return;
|
|
3741
4161
|
}
|
|
3742
|
-
if (event.key === '
|
|
4162
|
+
if (event.key === 'Tab') {
|
|
4163
|
+
close();
|
|
4164
|
+
return;
|
|
4165
|
+
}
|
|
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')) {
|
|
3743
4170
|
event.preventDefault();
|
|
3744
|
-
|
|
3745
|
-
render();
|
|
3746
|
-
const items = Array.from(list.querySelectorAll('[role=option]'));
|
|
3747
|
-
if (!items.length)
|
|
3748
|
-
return;
|
|
3749
|
-
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;
|
|
3750
4172
|
items.forEach((item, index) => item.setAttribute('aria-selected', String(index === active)));
|
|
3751
4173
|
input.setAttribute('aria-activedescendant', items[active].id);
|
|
3752
4174
|
items[active].scrollIntoView?.({ block: 'nearest' });
|
|
3753
4175
|
}
|
|
3754
|
-
else if (event.key === 'Enter' &&
|
|
3755
|
-
const items = Array.from(list.querySelectorAll('[role=option]'));
|
|
4176
|
+
else if (event.key === 'Enter' || event.key === 'ArrowRight' && items[active]?.getAttribute('part') === 'action-group') {
|
|
3756
4177
|
if (items[active]) {
|
|
3757
4178
|
event.preventDefault();
|
|
3758
4179
|
items[active].click();
|
|
3759
4180
|
}
|
|
3760
4181
|
}
|
|
4182
|
+
else if (event.key === 'ArrowLeft' && group && input.value === shown) {
|
|
4183
|
+
event.preventDefault();
|
|
4184
|
+
list.querySelector('[part=action-back]')?.click();
|
|
4185
|
+
}
|
|
3761
4186
|
});
|
|
3762
4187
|
const caret = document.createElement('button');
|
|
3763
4188
|
caret.type = 'button';
|
|
3764
4189
|
caret.setAttribute('part', 'action-caret');
|
|
3765
4190
|
caret.setAttribute('aria-label', `Browse ${field.label.toLowerCase()} choices`);
|
|
3766
|
-
|
|
4191
|
+
const caretIcon = document.createElement('span');
|
|
4192
|
+
caretIcon.setAttribute('part', 'action-caret-icon');
|
|
4193
|
+
caretIcon.setAttribute('aria-hidden', 'true');
|
|
4194
|
+
caret.append(caretIcon);
|
|
3767
4195
|
caret.disabled = input.disabled;
|
|
3768
|
-
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(); };
|
|
3769
4197
|
row.addEventListener('focusout', event => { if (!row.contains(event.relatedTarget))
|
|
3770
4198
|
close(); });
|
|
3771
|
-
|
|
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);
|
|
3772
4206
|
}
|
|
3773
4207
|
if (field.kind === 'search' && field.options?.length && control instanceof HTMLInputElement) {
|
|
3774
4208
|
const options = document.createElement('datalist');
|
|
@@ -3823,10 +4257,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
3823
4257
|
describedBy.push(help.id);
|
|
3824
4258
|
}
|
|
3825
4259
|
const feedback = field.problem || (field.kind === 'expression' ? field.expression?.feedback?.message : undefined);
|
|
3826
|
-
if (feedback) {
|
|
4260
|
+
if (feedback || (field.kind === 'expression' && field.expression?.feedback !== undefined)) {
|
|
3827
4261
|
const message = document.createElement('small');
|
|
3828
4262
|
message.id = `${messageId}-feedback`;
|
|
3829
|
-
message.textContent = feedback;
|
|
4263
|
+
message.textContent = feedback ?? '';
|
|
3830
4264
|
message.setAttribute('part', field.problem ? 'field-problem' : 'field-feedback');
|
|
3831
4265
|
if (field.kind === 'expression')
|
|
3832
4266
|
message.setAttribute('aria-live', 'polite');
|
|
@@ -3836,6 +4270,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
3836
4270
|
row.insertBefore(message, chips);
|
|
3837
4271
|
describedBy.push(message.id);
|
|
3838
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
|
+
}
|
|
3839
4281
|
if (describedBy.length)
|
|
3840
4282
|
control.setAttribute('aria-describedby', describedBy.join(' '));
|
|
3841
4283
|
if (field.kind === 'expression' && field.expression?.help) {
|
|
@@ -3867,11 +4309,118 @@ export class ProcessModeler extends BaseElement {
|
|
|
3867
4309
|
}
|
|
3868
4310
|
return row;
|
|
3869
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
|
+
}
|
|
3870
4412
|
renderSelection() {
|
|
3871
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); });
|
|
3872
4414
|
if (!this.isRendered)
|
|
3873
4415
|
return;
|
|
3874
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;
|
|
3875
4424
|
const focusedKey = focusedControl?.dataset.field ?? focusedControl?.dataset.variable ?? focusedControl?.dataset.localVariable;
|
|
3876
4425
|
const variableKey = focusedControl?.dataset.variableKey;
|
|
3877
4426
|
const fieldBox = focusedControl?.dataset.fieldBox;
|
|
@@ -3882,6 +4431,22 @@ export class ProcessModeler extends BaseElement {
|
|
|
3882
4431
|
const direction = focusedControl && 'selectionDirection' in focusedControl ? focusedControl.selectionDirection : null;
|
|
3883
4432
|
const scrollTop = focusedControl?.scrollTop, scrollLeft = focusedControl?.scrollLeft;
|
|
3884
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
|
+
}
|
|
3885
4450
|
if (focusedKey === undefined || !focusedType)
|
|
3886
4451
|
return;
|
|
3887
4452
|
if (focusedType === 'field' && (this.selected?.id !== fieldBox || this.locked))
|
|
@@ -3918,8 +4483,23 @@ export class ProcessModeler extends BaseElement {
|
|
|
3918
4483
|
}
|
|
3919
4484
|
if (!control || control.disabled || control.closest('[inert]'))
|
|
3920
4485
|
return;
|
|
3921
|
-
control.
|
|
3922
|
-
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)
|
|
3923
4503
|
control.setSelectionRange(caret, selectionEnd ?? caret, direction ?? undefined);
|
|
3924
4504
|
if (scrollTop !== undefined)
|
|
3925
4505
|
control.scrollTop = scrollTop;
|
|
@@ -3927,7 +4507,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
3927
4507
|
control.scrollLeft = scrollLeft;
|
|
3928
4508
|
};
|
|
3929
4509
|
this.shadowRoot.querySelectorAll('[part=minimap] [data-map-box]').forEach(rect => rect.dataset.selected = String(this.selectedIds.has(rect.dataset.mapBox)));
|
|
3930
|
-
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'})`); });
|
|
3931
4512
|
this.shadowRoot.querySelectorAll("[data-box-id]").forEach((element) => {
|
|
3932
4513
|
element.setAttribute("aria-current", String(this.selectedIds.has(element.dataset.boxId)));
|
|
3933
4514
|
const box = this.projection.boxes.find((box) => box.id === element.dataset.boxId);
|
|
@@ -3938,7 +4519,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3938
4519
|
const editor = this.shadowRoot.querySelector("[part=editor]");
|
|
3939
4520
|
const selected = this.selected;
|
|
3940
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;
|
|
3941
|
-
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] ?? [] : [])}`;
|
|
3942
4523
|
this.shadowRoot.querySelector("[part=palette]").hidden = false;
|
|
3943
4524
|
this.shadowRoot.querySelector('[part=selection-toolbar]').hidden = this.selection.length === 0;
|
|
3944
4525
|
this.renderSelectionToolbar();
|
|
@@ -3948,23 +4529,31 @@ export class ProcessModeler extends BaseElement {
|
|
|
3948
4529
|
this.inspectedId === selected?.id &&
|
|
3949
4530
|
this.inspectedControls === controlsKey &&
|
|
3950
4531
|
editor.querySelector(`[part=inspector-heading]`)?.tagName === `H${this.headingLevel}`) {
|
|
3951
|
-
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 ?? "";
|
|
3952
4533
|
const locals = editor.querySelector('[part=local-variables]');
|
|
3953
|
-
if (selected && locals && locals.dataset.localEchoRevision !== String(this.localEchoRevision)) {
|
|
4534
|
+
if (selected && this.selectedIds.size === 1 && locals && locals.dataset.localEchoRevision !== String(this.localEchoRevision)) {
|
|
3954
4535
|
// Reconcile controlled local drafts without disposing the host's custom
|
|
3955
4536
|
// inspector or unrelated focused controls on unchanged document echoes.
|
|
3956
4537
|
locals.replaceWith(this.renderLocalVariables(selected));
|
|
3957
4538
|
}
|
|
4539
|
+
if (selected && this.selectedIds.size === 1)
|
|
4540
|
+
this.renderLeadsTo(editor, selected);
|
|
3958
4541
|
restoreControlFocus();
|
|
3959
4542
|
return;
|
|
3960
4543
|
}
|
|
3961
4544
|
const expressionHelp = new Map();
|
|
4545
|
+
const disclosures = new Map();
|
|
3962
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
|
+
}
|
|
3963
4551
|
for (const help of Array.from(editor.querySelectorAll('[part=expression-help]'))) {
|
|
3964
4552
|
if (help.dataset.session === String(this.layoutEditSession))
|
|
3965
4553
|
expressionHelp.set(help.dataset.helpField, help.open);
|
|
3966
4554
|
}
|
|
3967
4555
|
}
|
|
4556
|
+
this.cleanupInspectorFields();
|
|
3968
4557
|
this.cleanupInspector?.();
|
|
3969
4558
|
this.cleanupInspector = undefined;
|
|
3970
4559
|
editor.replaceChildren();
|
|
@@ -4013,44 +4602,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
4013
4602
|
purpose.textContent = selected.description;
|
|
4014
4603
|
editor.append(purpose);
|
|
4015
4604
|
}
|
|
4016
|
-
|
|
4017
|
-
editor.append(this.renderField(selected, field, expressionHelp.get(field.key)));
|
|
4605
|
+
this.renderFields(editor, selected, expressionHelp, disclosures);
|
|
4018
4606
|
if (selected.localVariables !== undefined)
|
|
4019
4607
|
editor.append(this.renderLocalVariables(selected));
|
|
4020
4608
|
this.cleanupInspector =
|
|
4021
4609
|
this.renderer?.(selected.node, editor) || undefined;
|
|
4022
|
-
|
|
4023
|
-
const
|
|
4024
|
-
leads.setAttribute('part', 'leads-to');
|
|
4025
|
-
const leadsHeading = document.createElement('h3');
|
|
4026
|
-
leadsHeading.textContent = 'Leads to';
|
|
4027
|
-
leads.append(leadsHeading);
|
|
4028
|
-
for (const line of outgoing) {
|
|
4029
|
-
const row = document.createElement('div');
|
|
4030
|
-
row.setAttribute('part', 'lead-row');
|
|
4031
|
-
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
4032
|
-
const name = document.createElement('span');
|
|
4033
|
-
name.textContent = `${line.label ? `${line.label}: ` : ''}${target?.title ?? line.to}`;
|
|
4034
|
-
row.append(name);
|
|
4035
|
-
if (!this.disableConnections) {
|
|
4036
|
-
const remove = document.createElement('button');
|
|
4037
|
-
remove.type = 'button';
|
|
4038
|
-
remove.textContent = 'Remove';
|
|
4039
|
-
remove.setAttribute('aria-label', `Remove connection to ${target?.title ?? line.to}`);
|
|
4040
|
-
remove.disabled = this.locked;
|
|
4041
|
-
remove.onclick = () => this.requestEdit({ type: 'disconnect', lineId: line.id });
|
|
4042
|
-
row.append(remove);
|
|
4043
|
-
}
|
|
4044
|
-
leads.append(row);
|
|
4045
|
-
}
|
|
4046
|
-
const connect = document.createElement('button');
|
|
4047
|
-
connect.type = 'button';
|
|
4048
|
-
connect.textContent = 'Connect to…';
|
|
4049
|
-
connect.disabled = this.locked || this.disableConnections;
|
|
4050
|
-
connect.onclick = () => { this.connecting = selected.id; this.setStatus(`Select the next box to connect from ${selected.title}`); };
|
|
4051
|
-
leads.append(connect);
|
|
4052
|
-
editor.append(leads);
|
|
4053
|
-
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;
|
|
4054
4612
|
if (metrics) {
|
|
4055
4613
|
const report = document.createElement('section');
|
|
4056
4614
|
report.setAttribute('part', 'inspector-metrics');
|
|
@@ -4084,22 +4642,25 @@ export class ProcessModeler extends BaseElement {
|
|
|
4084
4642
|
}
|
|
4085
4643
|
report.append(list);
|
|
4086
4644
|
}
|
|
4087
|
-
editor.insertBefore(report, leads);
|
|
4645
|
+
editor.insertBefore(report, editor.querySelector('[part=leads-to]'));
|
|
4088
4646
|
}
|
|
4089
4647
|
const actions = document.createElement('div');
|
|
4090
4648
|
actions.setAttribute('part', 'inspector-actions');
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
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);
|
|
4103
4664
|
}
|
|
4104
4665
|
restoreControlFocus();
|
|
4105
4666
|
}
|
|
@@ -4209,7 +4770,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
4209
4770
|
}
|
|
4210
4771
|
renderSelectionToolbar() {
|
|
4211
4772
|
const toolbar = this.shadowRoot.querySelector('[part=selection-toolbar]');
|
|
4212
|
-
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(',')}`;
|
|
4213
4774
|
if (key === this.selectionToolbarKey)
|
|
4214
4775
|
return;
|
|
4215
4776
|
this.selectionToolbarKey = key;
|
|
@@ -4224,10 +4785,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
4224
4785
|
button.disabled = this.locked;
|
|
4225
4786
|
button.onclick = action;
|
|
4226
4787
|
if (withPlus) {
|
|
4227
|
-
const icon =
|
|
4788
|
+
const icon = variableGlyph(true);
|
|
4228
4789
|
icon.setAttribute('part', 'selection-plus');
|
|
4229
4790
|
icon.setAttribute('aria-hidden', 'true');
|
|
4230
|
-
icon.
|
|
4791
|
+
icon.setAttribute('focusable', 'false');
|
|
4231
4792
|
button.append(icon);
|
|
4232
4793
|
}
|
|
4233
4794
|
const text = document.createElement('span');
|
|
@@ -4236,24 +4797,27 @@ export class ProcessModeler extends BaseElement {
|
|
|
4236
4797
|
toolbar.append(button);
|
|
4237
4798
|
};
|
|
4238
4799
|
if (this.graphSelection && (this.selectedNoteIds.size || this.selectedLineIds.size > 1 || this.selectedLineIds.size && this.selectedIds.size)) {
|
|
4239
|
-
if (this.selectedIds.size
|
|
4800
|
+
if (this.selectedIds.size) {
|
|
4240
4801
|
add('Line up', 'align', () => this.selectionCommand('align'));
|
|
4802
|
+
add('Tidy these', 'space', () => this.selectionCommand('space'));
|
|
4803
|
+
}
|
|
4241
4804
|
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
4242
4805
|
}
|
|
4243
4806
|
else if (this.selectedLine) {
|
|
4244
4807
|
const line = this.selectedLine;
|
|
4245
|
-
|
|
4246
|
-
|
|
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))
|
|
4247
4811
|
add('Reset line', 'reset-line', () => this.resetLine(line.id));
|
|
4248
4812
|
if (!this.disableConnections)
|
|
4249
4813
|
add('Delete', 'delete-line', () => this.requestEdit({ type: 'disconnect', lineId: line.id }));
|
|
4250
4814
|
}
|
|
4251
4815
|
else if (this.selectedIds.size === 1) {
|
|
4252
4816
|
const box = this.selected;
|
|
4253
|
-
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'));
|
|
4254
4818
|
if (this.narrowValue)
|
|
4255
4819
|
add('Details', 'details', () => this.openDrawer('inspector'));
|
|
4256
|
-
if (!['end', 'finish', 'section', 'note'].includes(box.kind))
|
|
4820
|
+
if (isFlowBox(box) && !['end', 'finish', 'section', 'note'].includes(box.kind))
|
|
4257
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);
|
|
4258
4822
|
if (failurePath)
|
|
4259
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);
|
|
@@ -4378,9 +4942,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
4378
4942
|
positions.push(...(this.layoutValue.notes ?? []).filter(note => this.selectedNoteIds.has(note.id)).map(note => ({ ...note, width: 208, height: 80 })));
|
|
4379
4943
|
if (!positions.length && this.selectedLine) {
|
|
4380
4944
|
const point = lineMidpoint(this.routedLines.get(this.selectedLine.id) ?? []);
|
|
4381
|
-
|
|
4382
|
-
toolbar.style.top = `${Math.max(8, point.y * this.viewport.zoom + this.viewport.y - 52)}px`;
|
|
4383
|
-
return;
|
|
4945
|
+
positions.push({ x: point.x, y: point.y - 14 / this.viewport.zoom, width: 0, height: 0 });
|
|
4384
4946
|
}
|
|
4385
4947
|
if (!positions.length) {
|
|
4386
4948
|
for (const id of this.selectedLineIds) {
|
|
@@ -4394,10 +4956,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
4394
4956
|
if (!positions.length)
|
|
4395
4957
|
return;
|
|
4396
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));
|
|
4397
4960
|
const top = Math.min(...positions.map(position => position.y * this.viewport.zoom + this.viewport.y));
|
|
4398
4961
|
const bottom = Math.max(...positions.map(position => (position.y + (position.height ?? 64)) * this.viewport.zoom + this.viewport.y));
|
|
4399
|
-
toolbar.style.left = `${Math.
|
|
4400
|
-
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`;
|
|
4401
4964
|
}
|
|
4402
4965
|
focusBox(id) {
|
|
4403
4966
|
Array.from(this.shadowRoot.querySelectorAll("[data-box-id]"))
|