@unofficialbox/box-open-elements 0.31.2 → 0.33.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/native-types.d.ts +38 -0
- package/dist/patterns/process-modeler/bridge.js +1 -0
- package/dist/patterns/process-modeler/design.d.ts +1 -1
- package/dist/patterns/process-modeler/design.js +52 -8
- package/dist/patterns/process-modeler/model.d.ts +132 -1
- package/dist/patterns/process-modeler/process-modeler.d.ts +78 -2
- package/dist/patterns/process-modeler/process-modeler.js +1021 -127
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { iconPlus, iconXBatsu } from "../../foundations/icons/glyphs/index.js";
|
|
2
3
|
import { announce } from "../../foundations/a11y/index.js";
|
|
3
4
|
import { dismissModal, dismissPopover, promoteModal, promotePopover, trackAnchor } from "../../foundations/overlay/index.js";
|
|
4
5
|
import { KindPicker } from "../flow-builder/primitives.js";
|
|
@@ -7,6 +8,20 @@ import { restoreProcessPositions, snapshotProcessPositions, snapshotProcessProje
|
|
|
7
8
|
import { processModelerDesign } from "./design.js";
|
|
8
9
|
import { completeLayout, ProcessHistory, validateProjection, } from "./model.js";
|
|
9
10
|
const svgElement = (tag) => document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
11
|
+
const variableLabel = (text) => {
|
|
12
|
+
const label = document.createElement('label'), name = document.createElement('span');
|
|
13
|
+
name.setAttribute('part', 'sr-only');
|
|
14
|
+
name.textContent = text;
|
|
15
|
+
label.append(name);
|
|
16
|
+
return label;
|
|
17
|
+
};
|
|
18
|
+
const variableGlyph = (add = false) => {
|
|
19
|
+
const template = document.createElement('template');
|
|
20
|
+
template.innerHTML = add ? iconPlus : iconXBatsu;
|
|
21
|
+
const glyph = template.content.firstElementChild;
|
|
22
|
+
glyph.setAttribute('part', 'variable-icon');
|
|
23
|
+
return glyph;
|
|
24
|
+
};
|
|
10
25
|
const checkGlyph = (success = false) => {
|
|
11
26
|
const icon = svgElement('svg');
|
|
12
27
|
icon.setAttribute('part', 'check-icon');
|
|
@@ -53,6 +68,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
53
68
|
showLastRunValue = false;
|
|
54
69
|
selectedId = null;
|
|
55
70
|
selectedLineId = null;
|
|
71
|
+
selectedLineIds = new Set();
|
|
72
|
+
selectedNoteIds = new Set();
|
|
73
|
+
graphSelection = false;
|
|
56
74
|
connecting = null;
|
|
57
75
|
checks = [];
|
|
58
76
|
renderer;
|
|
@@ -60,6 +78,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
60
78
|
inspected;
|
|
61
79
|
inspectedId;
|
|
62
80
|
inspectedControls = "";
|
|
81
|
+
localEchoRevision = 0;
|
|
82
|
+
pendingLocalRename;
|
|
63
83
|
historyValue = new ProcessHistory();
|
|
64
84
|
viewport = { x: 0, y: 0, zoom: 1 };
|
|
65
85
|
pointers = new Map();
|
|
@@ -74,7 +94,36 @@ export class ProcessModeler extends BaseElement {
|
|
|
74
94
|
activeDrawer;
|
|
75
95
|
drawerReturn;
|
|
76
96
|
selectedIds = new Set();
|
|
77
|
-
|
|
97
|
+
clipboard = null;
|
|
98
|
+
clipboardSession = 0;
|
|
99
|
+
layoutEditSession = 0;
|
|
100
|
+
acceptanceNotifications = 0;
|
|
101
|
+
deferredNotifications = [];
|
|
102
|
+
notify(name, detail) {
|
|
103
|
+
if (this.acceptanceNotifications)
|
|
104
|
+
this.deferredNotifications.push({ name, detail, session: this.layoutEditSession });
|
|
105
|
+
else
|
|
106
|
+
emit(this, name, detail);
|
|
107
|
+
}
|
|
108
|
+
flushAcceptanceNotifications() {
|
|
109
|
+
if (this.acceptanceNotifications)
|
|
110
|
+
return;
|
|
111
|
+
this.acceptanceNotifications++;
|
|
112
|
+
try {
|
|
113
|
+
// Observer edits append behind the complete accepted batch, rather than
|
|
114
|
+
// publishing newer state before older queued persistence notifications.
|
|
115
|
+
while (this.deferredNotifications.length) {
|
|
116
|
+
const notification = this.deferredNotifications.shift();
|
|
117
|
+
if (notification.session !== this.layoutEditSession)
|
|
118
|
+
continue;
|
|
119
|
+
emit(this, notification.name, notification.detail);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
this.acceptanceNotifications--;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
restoringBoxFocus = false;
|
|
78
127
|
selectionToolbarKey = "";
|
|
79
128
|
sectionSequence = 0;
|
|
80
129
|
versionValue = 0;
|
|
@@ -99,6 +148,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
99
148
|
spacePressed = false;
|
|
100
149
|
dropLine;
|
|
101
150
|
paletteDragKind = null;
|
|
151
|
+
palettePointer;
|
|
152
|
+
suppressPaletteClick = false;
|
|
153
|
+
escapePalettePointer = (event) => {
|
|
154
|
+
if (event.key !== 'Escape' || !this.palettePointer)
|
|
155
|
+
return;
|
|
156
|
+
this.cancelPalettePointer();
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
};
|
|
102
159
|
narrowValue = false;
|
|
103
160
|
suppressClick = false;
|
|
104
161
|
resizeObserver;
|
|
@@ -157,6 +214,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
157
214
|
};
|
|
158
215
|
}
|
|
159
216
|
load(document, options = {}) {
|
|
217
|
+
this.pendingLocalRename = undefined;
|
|
218
|
+
this.layoutEditSession++;
|
|
219
|
+
this.cancelPalettePointer();
|
|
160
220
|
const projection = this.model.project(document);
|
|
161
221
|
validateProjection(projection);
|
|
162
222
|
this.documentValue = document;
|
|
@@ -166,11 +226,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
166
226
|
this.lastReadableProjection = "";
|
|
167
227
|
this.readableValue = null;
|
|
168
228
|
this.layoutValue = restoreProcessPositions(projection, options.positions ?? []);
|
|
229
|
+
this.clearMixedSelection();
|
|
169
230
|
this.history.clear();
|
|
170
231
|
this.selectedId = null;
|
|
171
232
|
this.selectedLineId = null;
|
|
172
233
|
this.selectedIds.clear();
|
|
173
|
-
this.
|
|
234
|
+
this.clearClipboard();
|
|
174
235
|
this.refresh();
|
|
175
236
|
if (options.selectedPath)
|
|
176
237
|
this.selectedPath = options.selectedPath;
|
|
@@ -181,15 +242,54 @@ export class ProcessModeler extends BaseElement {
|
|
|
181
242
|
validateProjection(this.projection);
|
|
182
243
|
}
|
|
183
244
|
const next = new Set(ids.filter(id => this.projection.boxes.some(box => box.id === id)));
|
|
184
|
-
if (next.size === this.selectedIds.size && [...next].every(id => this.selectedIds.has(id)))
|
|
245
|
+
if (!this.graphSelection && !this.selectedLineIds.size && !this.selectedNoteIds.size && next.size === this.selectedIds.size && [...next].every(id => this.selectedIds.has(id)))
|
|
185
246
|
return;
|
|
247
|
+
this.clearMixedSelection();
|
|
186
248
|
this.selectedIds = next;
|
|
187
249
|
this.selectedId = this.selectedIds.values().next().value ?? null;
|
|
188
250
|
this.selectedLineId = null;
|
|
189
251
|
this.renderSelection();
|
|
190
252
|
if (this.isRendered)
|
|
191
253
|
this.updateToolbar();
|
|
192
|
-
|
|
254
|
+
this.notify("selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath, ...(this.graphSelection ? { selection: this.selection } : {}) });
|
|
255
|
+
}
|
|
256
|
+
/** Detached mixed selection. Invalid and duplicate IDs are ignored by selectItems. */
|
|
257
|
+
get selection() {
|
|
258
|
+
return [...[...this.selectedIds].map(id => ({ type: 'box', id })),
|
|
259
|
+
...[...this.selectedLineIds].map(id => ({ type: 'line', id })),
|
|
260
|
+
...[...this.selectedNoteIds].map(id => ({ type: 'note', id }))];
|
|
261
|
+
}
|
|
262
|
+
selectItems(items) {
|
|
263
|
+
if (!this.isRendered && this.documentValue !== undefined) {
|
|
264
|
+
this.projection = this.model.project(this.documentValue);
|
|
265
|
+
validateProjection(this.projection);
|
|
266
|
+
}
|
|
267
|
+
const boxes = new Set(), lines = new Set(), notes = new Set();
|
|
268
|
+
for (const item of items) {
|
|
269
|
+
if (item.type === 'box' && this.projection.boxes.some(box => box.id === item.id))
|
|
270
|
+
boxes.add(item.id);
|
|
271
|
+
if (item.type === 'line' && this.projection.lines.some(line => line.id === item.id))
|
|
272
|
+
lines.add(item.id);
|
|
273
|
+
if (item.type === 'note' && this.layoutValue.notes?.some(note => note.id === item.id))
|
|
274
|
+
notes.add(item.id);
|
|
275
|
+
}
|
|
276
|
+
const before = JSON.stringify(this.selection);
|
|
277
|
+
this.selectedIds = boxes;
|
|
278
|
+
this.selectedLineIds = lines;
|
|
279
|
+
this.selectedNoteIds = notes;
|
|
280
|
+
this.selectedId = boxes.values().next().value ?? null;
|
|
281
|
+
this.selectedLineId = !boxes.size && !notes.size && lines.size === 1 ? lines.values().next().value : null;
|
|
282
|
+
this.graphSelection = true;
|
|
283
|
+
this.renderSelection();
|
|
284
|
+
if (this.isRendered)
|
|
285
|
+
this.updateToolbar();
|
|
286
|
+
if (before !== JSON.stringify(this.selection))
|
|
287
|
+
this.notify('selection-changed', { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath, selection: this.selection });
|
|
288
|
+
}
|
|
289
|
+
clearMixedSelection() {
|
|
290
|
+
this.selectedLineIds.clear();
|
|
291
|
+
this.selectedNoteIds.clear();
|
|
292
|
+
this.graphSelection = false;
|
|
193
293
|
}
|
|
194
294
|
get headingLevel() { const level = Number(this.getAttribute("heading-level") ?? 2); return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 2; }
|
|
195
295
|
set headingLevel(value) { this.setAttribute("heading-level", String(value)); }
|
|
@@ -213,8 +313,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
213
313
|
this.refresh();
|
|
214
314
|
}
|
|
215
315
|
set document(value) {
|
|
316
|
+
this.localEchoRevision++;
|
|
317
|
+
const rename = this.pendingLocalRename;
|
|
216
318
|
this.documentValue = value;
|
|
217
319
|
this.refresh();
|
|
320
|
+
// One explicit echo settles this intent, including a rejected unchanged echo.
|
|
321
|
+
if (rename === this.pendingLocalRename && rename && this.localEchoRevision > rename.echoRevision)
|
|
322
|
+
this.pendingLocalRename = undefined;
|
|
218
323
|
}
|
|
219
324
|
get model() {
|
|
220
325
|
return this.modelValue;
|
|
@@ -259,6 +364,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
259
364
|
}
|
|
260
365
|
set locked(value) {
|
|
261
366
|
this.toggleAttribute("locked", value);
|
|
367
|
+
if (value)
|
|
368
|
+
this.cancelPalettePointer();
|
|
262
369
|
}
|
|
263
370
|
get snapToGrid() {
|
|
264
371
|
return this.hasAttribute("snap-to-grid");
|
|
@@ -280,7 +387,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
280
387
|
this.refresh();
|
|
281
388
|
}
|
|
282
389
|
disconnectedCallback() {
|
|
390
|
+
this.layoutEditSession++;
|
|
283
391
|
this.resizeObserver?.disconnect();
|
|
392
|
+
this.clearClipboard();
|
|
393
|
+
this.cancelPalettePointer();
|
|
284
394
|
this.closeDrawer();
|
|
285
395
|
const chooser = this.shadowRoot?.querySelector("[part=insert-chooser]");
|
|
286
396
|
if (chooser)
|
|
@@ -312,8 +422,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
312
422
|
}
|
|
313
423
|
if (id !== null && !this.projection.boxes.some((box) => box.id === id))
|
|
314
424
|
return;
|
|
315
|
-
if (id === this.selectedId && this.selectedIds.size === (id ? 1 : 0) && !this.selectedLineId)
|
|
425
|
+
if (id === this.selectedId && this.selectedIds.size === (id ? 1 : 0) && !this.selectedLineId && !this.graphSelection)
|
|
316
426
|
return;
|
|
427
|
+
this.clearMixedSelection();
|
|
317
428
|
this.selectedId = id;
|
|
318
429
|
this.selectedIds = new Set(id ? [id] : []);
|
|
319
430
|
this.selectedLineId = null;
|
|
@@ -321,18 +432,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
321
432
|
if (this.isRendered)
|
|
322
433
|
this.updateToolbar();
|
|
323
434
|
if (notify)
|
|
324
|
-
|
|
435
|
+
this.notify("selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath, ...(this.graphSelection ? { selection: this.selection } : {}) });
|
|
325
436
|
}
|
|
326
437
|
selectLine(id) {
|
|
327
438
|
if (id !== null && !this.projection.lines.some(line => line.id === id))
|
|
328
439
|
return;
|
|
329
|
-
if (this.selectedLineId === id && !this.selectedIds.size)
|
|
440
|
+
if (!this.graphSelection && this.selectedLineId === id && !this.selectedIds.size)
|
|
330
441
|
return;
|
|
442
|
+
this.clearMixedSelection();
|
|
443
|
+
this.selectedLineIds = new Set(id ? [id] : []);
|
|
331
444
|
this.selectedLineId = id;
|
|
332
445
|
this.selectedId = null;
|
|
333
446
|
this.selectedIds.clear();
|
|
334
447
|
this.refresh();
|
|
335
|
-
|
|
448
|
+
this.notify('selection-changed', { box: null, boxes: [], line: this.selectedLine, path: null });
|
|
336
449
|
}
|
|
337
450
|
setValidation(checks) {
|
|
338
451
|
this.checks = checks;
|
|
@@ -341,21 +454,100 @@ export class ProcessModeler extends BaseElement {
|
|
|
341
454
|
setValidationAtPath(message, path) {
|
|
342
455
|
this.setValidation(message ? [{ message, path }] : []);
|
|
343
456
|
}
|
|
457
|
+
connectionProblem(edit) {
|
|
458
|
+
if (!this.model.connectionProblem || this.documentValue === undefined || !['connect', 'reattach'].includes(edit.type))
|
|
459
|
+
return null;
|
|
460
|
+
if (!this.isRendered) {
|
|
461
|
+
this.projection = this.model.project(this.documentValue);
|
|
462
|
+
validateProjection(this.projection);
|
|
463
|
+
}
|
|
464
|
+
const line = edit.type === 'reattach' ? this.projection.lines.find(line => line.id === edit.lineId) : undefined;
|
|
465
|
+
const from = edit.from ?? line?.from;
|
|
466
|
+
const to = edit.to ?? line?.to;
|
|
467
|
+
if (!from || !to)
|
|
468
|
+
return null;
|
|
469
|
+
return this.model.connectionProblem(this.documentValue, { from, to, ...(line ? { ignoreLineId: line.id } : {}) }, this.projection) || null;
|
|
470
|
+
}
|
|
344
471
|
requestEdit(edit) {
|
|
345
472
|
if (this.locked || (this.disableConnections && (edit.type === "connect" || edit.type === "disconnect" || edit.type === 'reattach')))
|
|
346
473
|
return;
|
|
474
|
+
if (edit.type === 'delete-selection' && (!edit.selection?.length || edit.selection.some(item => !['box', 'line', 'note'].includes(item.type) || typeof item.id !== 'string' || !item.id))) {
|
|
475
|
+
this.setStatus('The selected graph items are invalid', true);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (edit.selection)
|
|
479
|
+
edit = { ...edit, selection: Object.freeze(edit.selection.map(item => Object.freeze({ type: item.type, id: item.id }))) };
|
|
480
|
+
if (this.disableConnections && edit.type === 'delete-selection' && edit.selection?.some(item => item.type === 'line')) {
|
|
481
|
+
this.setStatus('Deleting selected lines is disabled');
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const placement = edit.placement;
|
|
485
|
+
if (placement) {
|
|
486
|
+
const source = placement.source;
|
|
487
|
+
let normalized;
|
|
488
|
+
if (edit.type === 'add' || edit.type === 'insert') {
|
|
489
|
+
if (source === 'next')
|
|
490
|
+
normalized = { source };
|
|
491
|
+
else if (source === 'direction') {
|
|
492
|
+
const side = placement.side;
|
|
493
|
+
if (['north', 'east', 'south', 'west'].includes(side))
|
|
494
|
+
normalized = { source, side };
|
|
495
|
+
}
|
|
496
|
+
else if (source === 'point' || source === 'line') {
|
|
497
|
+
const { x, y } = placement.center ?? {};
|
|
498
|
+
if (Number.isFinite(x) && Number.isFinite(y))
|
|
499
|
+
normalized = { source, center: { x, y } };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (!normalized) {
|
|
503
|
+
this.setStatus('The insertion placement is invalid', true);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
edit = { ...edit, placement: normalized };
|
|
507
|
+
}
|
|
508
|
+
const problem = this.connectionProblem(edit);
|
|
509
|
+
if (problem) {
|
|
510
|
+
this.setStatus(problem, true);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const request = { ...edit, accept: this.editAcceptance(edit) };
|
|
514
|
+
if (edit.type !== 'delete-selection') {
|
|
515
|
+
emit(this, 'process-edit-request', request);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// A synchronous host echo can prune selected lines before accept records
|
|
519
|
+
// history. Keep those observers behind the complete atomic transaction.
|
|
520
|
+
this.acceptanceNotifications++;
|
|
521
|
+
try {
|
|
522
|
+
emit(this, 'process-edit-request', request);
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
this.acceptanceNotifications--;
|
|
526
|
+
this.flushAcceptanceNotifications();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
editAcceptance(edit) {
|
|
347
530
|
const before = this.layout;
|
|
531
|
+
const beforeSelection = [...this.selectedIds];
|
|
532
|
+
const beforeItems = this.selection;
|
|
348
533
|
const previousIds = new Set(this.projection.boxes.map((box) => box.id));
|
|
534
|
+
const session = this.layoutEditSession;
|
|
349
535
|
let accepted = false;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
536
|
+
return (command) => {
|
|
537
|
+
if (accepted || session !== this.layoutEditSession)
|
|
538
|
+
return;
|
|
539
|
+
accepted = true;
|
|
540
|
+
// Projection/readability/selection observers may synchronously edit or
|
|
541
|
+
// replace the document. Publish only after the whole history entry exists.
|
|
542
|
+
this.acceptanceNotifications++;
|
|
543
|
+
try {
|
|
356
544
|
if (command.layout)
|
|
357
545
|
this.layoutValue = structuredClone(command.layout);
|
|
546
|
+
if (edit.type === 'reset-line' && edit.lineId)
|
|
547
|
+
delete this.layoutValue.lines?.[edit.lineId];
|
|
358
548
|
this.refresh();
|
|
549
|
+
if (session !== this.layoutEditSession)
|
|
550
|
+
return;
|
|
359
551
|
const inserted = edit.type === 'insert' && !edit.boxId && !command.layout
|
|
360
552
|
? this.projection.boxes.find(box => !previousIds.has(box.id)) : undefined;
|
|
361
553
|
const reflowed = inserted ? this.reflowStraightInsert(edit, inserted.id, before) : false;
|
|
@@ -368,7 +560,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
368
560
|
if (added)
|
|
369
561
|
this.layoutValue = this.translatedLayout(added.id, edit.position.x, edit.position.y);
|
|
370
562
|
}
|
|
563
|
+
if (session !== this.layoutEditSession)
|
|
564
|
+
return;
|
|
371
565
|
const after = this.layout;
|
|
566
|
+
const acceptedItems = command.selection ? command.selection.map(item => ({ type: item.type, id: item.id })) : undefined;
|
|
567
|
+
const acceptedSelection = command.selectionIds ? [...command.selectionIds] : undefined;
|
|
372
568
|
const restore = (layout) => {
|
|
373
569
|
this.layoutValue = structuredClone(layout);
|
|
374
570
|
this.refresh();
|
|
@@ -378,19 +574,34 @@ export class ProcessModeler extends BaseElement {
|
|
|
378
574
|
undo: () => {
|
|
379
575
|
command.undo();
|
|
380
576
|
restore(before);
|
|
577
|
+
if (acceptedItems)
|
|
578
|
+
this.selectItems(beforeItems);
|
|
579
|
+
else if (acceptedSelection)
|
|
580
|
+
this.selectMany(beforeSelection);
|
|
381
581
|
},
|
|
382
582
|
redo: () => {
|
|
383
583
|
command.redo();
|
|
384
584
|
restore(after);
|
|
585
|
+
if (acceptedItems)
|
|
586
|
+
this.selectItems(acceptedItems);
|
|
587
|
+
else if (acceptedSelection)
|
|
588
|
+
this.selectMany(acceptedSelection);
|
|
385
589
|
},
|
|
386
590
|
});
|
|
591
|
+
if (acceptedItems)
|
|
592
|
+
this.selectItems(acceptedItems);
|
|
593
|
+
else if (acceptedSelection)
|
|
594
|
+
this.selectMany(acceptedSelection);
|
|
387
595
|
this.refresh();
|
|
388
596
|
if (JSON.stringify(before) !== JSON.stringify(after))
|
|
389
597
|
this.emitPositions();
|
|
390
598
|
announce("Process updated", "polite", this.ownerDocument);
|
|
391
|
-
}
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
this.acceptanceNotifications--;
|
|
602
|
+
this.flushAcceptanceNotifications();
|
|
603
|
+
}
|
|
392
604
|
};
|
|
393
|
-
emit(this, "process-edit-request", request);
|
|
394
605
|
}
|
|
395
606
|
undo() {
|
|
396
607
|
if (this.locked)
|
|
@@ -404,6 +615,53 @@ export class ProcessModeler extends BaseElement {
|
|
|
404
615
|
this.history.redo();
|
|
405
616
|
this.refresh();
|
|
406
617
|
}
|
|
618
|
+
requestLayoutEdit(type, sourceIds, title, onAccepted) {
|
|
619
|
+
const capability = this.model.layoutEdit;
|
|
620
|
+
if (!capability || this.documentValue === undefined)
|
|
621
|
+
return false;
|
|
622
|
+
if (this.locked)
|
|
623
|
+
return true;
|
|
624
|
+
if (!this.isRendered) {
|
|
625
|
+
this.projection = this.model.project(this.documentValue);
|
|
626
|
+
validateProjection(this.projection);
|
|
627
|
+
}
|
|
628
|
+
const session = this.layoutEditSession;
|
|
629
|
+
const accept = this.editAcceptance({ type });
|
|
630
|
+
let active = true, settled = false, acceptedLayout = false;
|
|
631
|
+
const valid = () => active && !settled && session === this.layoutEditSession && !this.locked;
|
|
632
|
+
const request = {
|
|
633
|
+
type, sourceIds: sourceIds ? Object.freeze([...sourceIds]) : null, ...(title !== undefined ? { title } : {}), layout: this.layout,
|
|
634
|
+
accept: command => {
|
|
635
|
+
if (!valid())
|
|
636
|
+
return;
|
|
637
|
+
settled = true;
|
|
638
|
+
if (!command.layout) {
|
|
639
|
+
this.setStatus('The host must supply the complete accepted layout', true);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
accept(command);
|
|
643
|
+
acceptedLayout = session === this.layoutEditSession;
|
|
644
|
+
},
|
|
645
|
+
refuse: message => { if (!valid())
|
|
646
|
+
return; settled = true; this.setStatus(message ?? 'The host refused this layout edit', true); },
|
|
647
|
+
};
|
|
648
|
+
// Hosts may assign a fresh document before accepting the request. Its
|
|
649
|
+
// notifications must follow the same atomic history boundary.
|
|
650
|
+
this.acceptanceNotifications++;
|
|
651
|
+
try {
|
|
652
|
+
capability.call(this.model, this.documentValue, request, this.projection);
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
active = false;
|
|
656
|
+
this.acceptanceNotifications--;
|
|
657
|
+
this.flushAcceptanceNotifications();
|
|
658
|
+
}
|
|
659
|
+
if (acceptedLayout && session === this.layoutEditSession)
|
|
660
|
+
onAccepted?.();
|
|
661
|
+
if (!settled && session === this.layoutEditSession)
|
|
662
|
+
this.setStatus('This layout edit was not accepted by the host', true);
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
407
665
|
commitLayout(next) {
|
|
408
666
|
const before = this.layout;
|
|
409
667
|
const after = structuredClone(next);
|
|
@@ -432,8 +690,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
432
690
|
this.commitLayout(next);
|
|
433
691
|
}
|
|
434
692
|
emitPositions() {
|
|
435
|
-
|
|
436
|
-
|
|
693
|
+
this.notify("layout-changed", { layout: this.layout });
|
|
694
|
+
this.notify("positions-changed", { positions: this.positions, version: this.versionValue });
|
|
437
695
|
}
|
|
438
696
|
translatedLayout(id, x, y) {
|
|
439
697
|
const snap = (n) => (this.snapToGrid ? Math.round(n / 16) * 16 : n);
|
|
@@ -499,6 +757,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
499
757
|
tidy() {
|
|
500
758
|
if (this.locked)
|
|
501
759
|
return;
|
|
760
|
+
if (this.requestLayoutEdit('tidy', null, undefined, () => { if (this.isRendered)
|
|
761
|
+
this.fitTo(this.narrowValue ? 0.55 : 0.7); }))
|
|
762
|
+
return;
|
|
502
763
|
const before = this.layout.boxes;
|
|
503
764
|
this.commitLayout(this.arrangedLayout({ ...this.layout, boxes: {} }));
|
|
504
765
|
if (!globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
|
|
@@ -541,6 +802,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
541
802
|
});
|
|
542
803
|
}
|
|
543
804
|
resetLine(id) {
|
|
805
|
+
const line = this.projection.lines.find(line => line.id === id);
|
|
806
|
+
if (this.locked || !line)
|
|
807
|
+
return;
|
|
808
|
+
if (line.fromSide || line.toSide || line.points?.length) {
|
|
809
|
+
this.requestEdit({ type: 'reset-line', lineId: id });
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
544
812
|
if (!this.layoutValue.lines?.[id])
|
|
545
813
|
return;
|
|
546
814
|
const next = this.layout;
|
|
@@ -763,7 +1031,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
763
1031
|
palette.prepend(skip, paletteHeading);
|
|
764
1032
|
const hint = document.createElement("p");
|
|
765
1033
|
hint.setAttribute("part", "palette-hint");
|
|
766
|
-
hint.textContent = "Drag onto the canvas, a
|
|
1034
|
+
hint.textContent = "Drag onto the canvas, onto a line to insert, or into a frame. Choosing one with a step selected adds it after that step. Or click a dot beside any step to add one that way.";
|
|
767
1035
|
palette.append(hint);
|
|
768
1036
|
for (const [part, label] of [["palette", "Building blocks"], ["inspector", "Process details"]]) {
|
|
769
1037
|
const pane = root.querySelector(`[part=${part}]`);
|
|
@@ -864,6 +1132,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
864
1132
|
controls.setAttribute("role", "toolbar");
|
|
865
1133
|
controls.setAttribute("aria-label", "Canvas view controls");
|
|
866
1134
|
for (const command of ["zoom-out", "reset", "zoom-in", "fit", "snap", "lock"]) {
|
|
1135
|
+
if (command === 'snap') {
|
|
1136
|
+
const divider = document.createElement('span');
|
|
1137
|
+
divider.setAttribute('part', 'controls-divider');
|
|
1138
|
+
divider.setAttribute('aria-hidden', 'true');
|
|
1139
|
+
controls.append(divider);
|
|
1140
|
+
}
|
|
867
1141
|
const button = root.querySelector(`[data-command=${command}]`);
|
|
868
1142
|
if (button)
|
|
869
1143
|
controls.append(button);
|
|
@@ -896,6 +1170,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
896
1170
|
this.narrowValue = narrow;
|
|
897
1171
|
this.toggleAttribute("data-narrow", narrow);
|
|
898
1172
|
this.toggleAttribute('data-phone', entries[0].contentRect.width < 560);
|
|
1173
|
+
this.toggleAttribute('data-hide-tidy', entries[0].contentRect.width <= 560);
|
|
899
1174
|
this.renderSelectionToolbar();
|
|
900
1175
|
this.positionSelectionToolbar();
|
|
901
1176
|
if (!narrow)
|
|
@@ -968,6 +1243,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
968
1243
|
});
|
|
969
1244
|
const content = root.querySelector('[part=pane-content]');
|
|
970
1245
|
content.hidden = Boolean(selected);
|
|
1246
|
+
content.dataset.pane = this.activePane;
|
|
971
1247
|
content.setAttribute("aria-labelledby", `process-tab-${this.activePane.toLowerCase()}`);
|
|
972
1248
|
const checks = root.querySelector('[part=checks]');
|
|
973
1249
|
checks.hidden = this.activePane !== "Checks";
|
|
@@ -1026,29 +1302,50 @@ export class ProcessModeler extends BaseElement {
|
|
|
1026
1302
|
content.append(checks);
|
|
1027
1303
|
}
|
|
1028
1304
|
else if (this.activePane === "Variables") {
|
|
1029
|
-
|
|
1030
|
-
|
|
1305
|
+
const intro = document.createElement('p');
|
|
1306
|
+
intro.setAttribute('part', 'variables-intro');
|
|
1307
|
+
intro.textContent = 'Values the process keeps while it runs. Decisions and step inputs read them by name, written in CEL.';
|
|
1308
|
+
content.append(intro);
|
|
1309
|
+
const scopes = document.createElement('dl');
|
|
1310
|
+
scopes.setAttribute('part', 'variable-scopes');
|
|
1311
|
+
for (const [name, meaning] of [['Iteration', 'Starts fresh each time a test user runs the process.'], ['Process', 'Set once per test user and kept across their iterations.'], ['Step', 'Only inside one step or frame. Add those in the step’s details.']]) {
|
|
1312
|
+
const term = document.createElement('dt');
|
|
1313
|
+
term.textContent = name;
|
|
1314
|
+
const definition = document.createElement('dd');
|
|
1315
|
+
definition.textContent = meaning;
|
|
1316
|
+
scopes.append(term, definition);
|
|
1317
|
+
}
|
|
1318
|
+
content.append(scopes);
|
|
1319
|
+
const collection = document.createElement('div');
|
|
1320
|
+
collection.setAttribute('part', 'variable-list');
|
|
1321
|
+
content.append(collection);
|
|
1322
|
+
if (!this.variables.length) {
|
|
1323
|
+
const empty = document.createElement('p');
|
|
1324
|
+
empty.textContent = 'None yet.';
|
|
1325
|
+
collection.append(empty);
|
|
1326
|
+
}
|
|
1031
1327
|
for (const [index, variable] of this.variables.entries()) {
|
|
1032
1328
|
const row = document.createElement('div');
|
|
1033
1329
|
row.setAttribute('part', 'variable-row');
|
|
1034
1330
|
const identify = (control, key) => { control.dataset.variable = variable.name; control.dataset.variableKey = key; control.dataset.variableIndex = String(index); };
|
|
1035
1331
|
if (this.variablesEditable) {
|
|
1036
|
-
const label =
|
|
1037
|
-
label.textContent = 'Name';
|
|
1332
|
+
const label = variableLabel('Name');
|
|
1038
1333
|
const name = document.createElement('input');
|
|
1039
1334
|
name.type = 'text';
|
|
1335
|
+
name.placeholder = 'name';
|
|
1336
|
+
name.spellcheck = false;
|
|
1337
|
+
name.autocomplete = 'off';
|
|
1040
1338
|
name.value = variable.name;
|
|
1041
1339
|
name.disabled = this.locked;
|
|
1042
1340
|
identify(name, 'name');
|
|
1043
1341
|
name.onchange = () => this.requestVariableEdit({ type: 'rename', name: variable.name, value: name.value });
|
|
1044
1342
|
label.append(name);
|
|
1045
1343
|
row.append(label);
|
|
1046
|
-
const scopeLabel =
|
|
1047
|
-
scopeLabel.textContent = 'Scope';
|
|
1344
|
+
const scopeLabel = variableLabel('Kept for');
|
|
1048
1345
|
const scope = document.createElement('select');
|
|
1049
1346
|
identify(scope, 'scope');
|
|
1050
1347
|
scope.disabled = this.locked;
|
|
1051
|
-
for (const [value, text] of [['iteration', '
|
|
1348
|
+
for (const [value, text] of [['iteration', 'Iteration'], ['process', 'Process']]) {
|
|
1052
1349
|
const option = document.createElement('option');
|
|
1053
1350
|
option.value = value;
|
|
1054
1351
|
option.textContent = text;
|
|
@@ -1070,16 +1367,32 @@ export class ProcessModeler extends BaseElement {
|
|
|
1070
1367
|
row.append(scope);
|
|
1071
1368
|
}
|
|
1072
1369
|
}
|
|
1073
|
-
if (
|
|
1370
|
+
if (this.variablesEditable) {
|
|
1371
|
+
const label = variableLabel('What it’s for');
|
|
1372
|
+
const about = document.createElement('input');
|
|
1373
|
+
about.type = 'text';
|
|
1374
|
+
about.placeholder = 'What it’s for';
|
|
1375
|
+
about.value = variable.description ?? '';
|
|
1376
|
+
about.disabled = this.locked;
|
|
1377
|
+
identify(about, 'description');
|
|
1378
|
+
about.oninput = () => this.requestVariableEdit({ type: 'description', name: variable.name, value: about.value });
|
|
1379
|
+
label.append(about);
|
|
1380
|
+
row.append(label);
|
|
1381
|
+
}
|
|
1382
|
+
else if (variable.description) {
|
|
1074
1383
|
const description = document.createElement('p');
|
|
1075
1384
|
description.textContent = variable.description;
|
|
1076
1385
|
row.append(description);
|
|
1077
1386
|
}
|
|
1078
1387
|
if (variable.startingValue !== undefined) {
|
|
1079
|
-
const label = document.createElement('label');
|
|
1080
|
-
|
|
1388
|
+
const label = this.variablesEditable ? variableLabel('Starts as') : document.createElement('label');
|
|
1389
|
+
if (!this.variablesEditable)
|
|
1390
|
+
label.textContent = 'Starting value';
|
|
1081
1391
|
const input = document.createElement('input');
|
|
1082
1392
|
input.type = 'text';
|
|
1393
|
+
input.placeholder = 'starts as, like 0';
|
|
1394
|
+
input.spellcheck = false;
|
|
1395
|
+
input.autocomplete = 'off';
|
|
1083
1396
|
identify(input, 'value');
|
|
1084
1397
|
input.value = variable.startingValue;
|
|
1085
1398
|
input.disabled = this.locked;
|
|
@@ -1102,13 +1415,26 @@ export class ProcessModeler extends BaseElement {
|
|
|
1102
1415
|
remove.type = 'button';
|
|
1103
1416
|
remove.setAttribute('part', 'variable-remove');
|
|
1104
1417
|
remove.setAttribute('aria-label', `Remove ${variable.name || 'this variable'}`);
|
|
1105
|
-
remove.
|
|
1418
|
+
remove.append(variableGlyph());
|
|
1106
1419
|
remove.disabled = this.locked;
|
|
1107
1420
|
identify(remove, 'remove');
|
|
1108
1421
|
remove.onclick = () => this.requestVariableEdit({ type: 'remove', name: variable.name });
|
|
1109
1422
|
row.append(remove);
|
|
1110
1423
|
}
|
|
1111
|
-
|
|
1424
|
+
if (this.variablesEditable) {
|
|
1425
|
+
// The grid's visual ordering must also be the keyboard/read order.
|
|
1426
|
+
for (const key of ['name', 'value', 'remove', 'description', 'scope']) {
|
|
1427
|
+
const control = row.querySelector(`[data-variable-key=${key}]`);
|
|
1428
|
+
if (control)
|
|
1429
|
+
row.append(control.closest('label') ?? control);
|
|
1430
|
+
}
|
|
1431
|
+
const problem = row.querySelector('[part=field-problem]');
|
|
1432
|
+
if (problem)
|
|
1433
|
+
row.append(problem);
|
|
1434
|
+
}
|
|
1435
|
+
row.setAttribute('role', 'group');
|
|
1436
|
+
row.setAttribute('aria-label', `Variable ${index + 1}`);
|
|
1437
|
+
collection.append(row);
|
|
1112
1438
|
}
|
|
1113
1439
|
if (this.variablesEditable) {
|
|
1114
1440
|
const add = document.createElement('button');
|
|
@@ -1116,11 +1442,45 @@ export class ProcessModeler extends BaseElement {
|
|
|
1116
1442
|
add.setAttribute('part', 'variable-add');
|
|
1117
1443
|
add.dataset.variable = '@add';
|
|
1118
1444
|
add.dataset.variableKey = 'add';
|
|
1119
|
-
add.
|
|
1445
|
+
add.append(variableGlyph(true), document.createTextNode('Add a variable'));
|
|
1120
1446
|
add.disabled = this.locked;
|
|
1121
1447
|
add.onclick = () => this.requestVariableEdit({ type: 'add' });
|
|
1122
|
-
|
|
1448
|
+
const actions = document.createElement('div');
|
|
1449
|
+
actions.setAttribute('part', 'variable-actions');
|
|
1450
|
+
actions.append(add);
|
|
1451
|
+
content.append(actions);
|
|
1123
1452
|
}
|
|
1453
|
+
const localRows = this.projection.boxes.flatMap(box => (box.localVariables ?? []).filter(variable => variable.name).map(variable => ({ name: variable.name, connective: 'only in', title: box.title })));
|
|
1454
|
+
const savedRows = this.projection.boxes.filter(box => box.savedResult).map(box => ({ name: box.savedResult, connective: 'from', title: box.title }));
|
|
1455
|
+
const summary = (part, heading, rows, empty) => {
|
|
1456
|
+
if (!rows.length && !empty)
|
|
1457
|
+
return;
|
|
1458
|
+
const section = document.createElement('section');
|
|
1459
|
+
section.setAttribute('part', part);
|
|
1460
|
+
const title = document.createElement('h3');
|
|
1461
|
+
title.textContent = heading;
|
|
1462
|
+
section.append(title);
|
|
1463
|
+
if (rows.length) {
|
|
1464
|
+
const list = document.createElement('ul');
|
|
1465
|
+
for (const item of rows) {
|
|
1466
|
+
const row = document.createElement('li'), text = document.createElement('span'), name = document.createElement('code'), connective = document.createElement('em');
|
|
1467
|
+
name.textContent = item.name;
|
|
1468
|
+
connective.textContent = item.connective;
|
|
1469
|
+
text.append(name, document.createTextNode(' '), connective, document.createTextNode(` ${item.title}`));
|
|
1470
|
+
row.append(text);
|
|
1471
|
+
list.append(row);
|
|
1472
|
+
}
|
|
1473
|
+
section.append(list);
|
|
1474
|
+
}
|
|
1475
|
+
else {
|
|
1476
|
+
const help = document.createElement('p');
|
|
1477
|
+
help.textContent = empty;
|
|
1478
|
+
section.append(help);
|
|
1479
|
+
}
|
|
1480
|
+
content.append(section);
|
|
1481
|
+
};
|
|
1482
|
+
summary('local-variable-summary', 'Only in one step', localRows);
|
|
1483
|
+
summary('saved-result-summary', 'Saved by steps', savedRows, 'No step saves its result yet. Set “Save the result as” on a step.');
|
|
1124
1484
|
}
|
|
1125
1485
|
else if (this.activePane === "Connections") {
|
|
1126
1486
|
if (!this.connections.length)
|
|
@@ -1162,6 +1522,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
1162
1522
|
if (this.locked)
|
|
1163
1523
|
return;
|
|
1164
1524
|
const boxes = this.selectedBoxes;
|
|
1525
|
+
if (command === "delete-many" && this.graphSelection) {
|
|
1526
|
+
if (this.disableConnections && this.selectedLineIds.size) {
|
|
1527
|
+
this.setStatus('Deleting selected lines is disabled');
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
this.requestEdit({ type: 'delete-selection', selection: this.selection });
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1165
1533
|
if (command === "delete-many") {
|
|
1166
1534
|
for (const box of boxes)
|
|
1167
1535
|
this.requestEdit({ type: "delete", boxId: box.id });
|
|
@@ -1179,6 +1547,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
1179
1547
|
tidySelection() {
|
|
1180
1548
|
if (this.locked || this.selectedBoxes.length < 2)
|
|
1181
1549
|
return;
|
|
1550
|
+
if (this.requestLayoutEdit('tidy', [...this.selectedIds]))
|
|
1551
|
+
return;
|
|
1182
1552
|
const next = this.layout;
|
|
1183
1553
|
const roots = this.selectedBoxes.filter(box => {
|
|
1184
1554
|
let parent = box.parentId;
|
|
@@ -1218,11 +1588,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
1218
1588
|
const arrangedY = Math.min(...group.map(box => arranged.boxes[box.id]?.y ?? Infinity));
|
|
1219
1589
|
if (![arrangedX, arrangedY].every(Number.isFinite))
|
|
1220
1590
|
continue;
|
|
1591
|
+
const dx = Math.round((startX - arrangedX) / 16) * 16;
|
|
1592
|
+
const dy = Math.round((startY - arrangedY) / 16) * 16;
|
|
1221
1593
|
for (const box of subset.boxes) {
|
|
1222
1594
|
const position = arranged.boxes[box.id];
|
|
1223
1595
|
if (!position || !next.boxes[box.id])
|
|
1224
1596
|
continue;
|
|
1225
|
-
next.boxes[box.id] = { ...next.boxes[box.id], x:
|
|
1597
|
+
next.boxes[box.id] = { ...next.boxes[box.id], x: position.x + dx, y: position.y + dy };
|
|
1226
1598
|
}
|
|
1227
1599
|
}
|
|
1228
1600
|
this.commitLayout(next);
|
|
@@ -1315,7 +1687,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
1315
1687
|
this.commitLayout(next);
|
|
1316
1688
|
}
|
|
1317
1689
|
makeSection(title = 'New section') {
|
|
1318
|
-
if (this.locked || this.selectedBoxes.length
|
|
1690
|
+
if (this.locked || !this.selectedBoxes.length)
|
|
1691
|
+
return;
|
|
1692
|
+
if (this.requestLayoutEdit('make-section', [...this.selectedIds], title))
|
|
1693
|
+
return;
|
|
1694
|
+
if (this.selectedBoxes.length < 2)
|
|
1319
1695
|
return;
|
|
1320
1696
|
const positions = this.selectedBoxes.map(box => this.layoutValue.boxes[box.id]);
|
|
1321
1697
|
const x = Math.min(...positions.map(position => position.x)) - 24;
|
|
@@ -1326,6 +1702,16 @@ export class ProcessModeler extends BaseElement {
|
|
|
1326
1702
|
this.setSection({ id, title, x, y, width: right - x, height: bottom - y });
|
|
1327
1703
|
}
|
|
1328
1704
|
setupListeners() {
|
|
1705
|
+
this.addEventListener('pointermove', event => this.movePalettePointer(event));
|
|
1706
|
+
this.addEventListener('pointerup', event => this.finishPalettePointer(event));
|
|
1707
|
+
this.addEventListener('pointercancel', event => { if (event.pointerId === this.palettePointer?.id)
|
|
1708
|
+
this.cancelPalettePointer(); });
|
|
1709
|
+
this.addEventListener('lostpointercapture', event => { if (event.pointerId === this.palettePointer?.id)
|
|
1710
|
+
this.cancelPalettePointer(); });
|
|
1711
|
+
this.shadowRoot.addEventListener('keydown', event => { if (event.key === 'Escape' && this.palettePointer) {
|
|
1712
|
+
this.cancelPalettePointer();
|
|
1713
|
+
event.preventDefault();
|
|
1714
|
+
} }, true);
|
|
1329
1715
|
this.shadowRoot.addEventListener("click", (event) => {
|
|
1330
1716
|
const command = event.target.closest("button")?.dataset.command;
|
|
1331
1717
|
if (command === "zoom-in")
|
|
@@ -1455,6 +1841,93 @@ export class ProcessModeler extends BaseElement {
|
|
|
1455
1841
|
this.paintViewport();
|
|
1456
1842
|
});
|
|
1457
1843
|
}
|
|
1844
|
+
clearClipboard() {
|
|
1845
|
+
const previous = this.clipboard;
|
|
1846
|
+
this.clipboard = null;
|
|
1847
|
+
this.clipboardSession++;
|
|
1848
|
+
previous?.dispose?.();
|
|
1849
|
+
}
|
|
1850
|
+
copySelection() {
|
|
1851
|
+
this.clearClipboard();
|
|
1852
|
+
const session = this.clipboardSession;
|
|
1853
|
+
let active = true;
|
|
1854
|
+
let claimed = false;
|
|
1855
|
+
const request = {
|
|
1856
|
+
sourceIds: Object.freeze([...this.selectedIds]),
|
|
1857
|
+
selection: Object.freeze(this.selection.map(item => Object.freeze(item))),
|
|
1858
|
+
capture: clipboard => {
|
|
1859
|
+
if (!active || claimed || session !== this.clipboardSession || !this.isConnected)
|
|
1860
|
+
return;
|
|
1861
|
+
claimed = true;
|
|
1862
|
+
if (!Number.isInteger(clipboard?.itemCount) || clipboard.itemCount < 1 || typeof clipboard.paste !== 'function' || (clipboard.dispose !== undefined && typeof clipboard.dispose !== 'function')) {
|
|
1863
|
+
this.setStatus('The host did not supply a valid clipboard');
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
this.clipboard = Object.freeze({ itemCount: clipboard.itemCount, paste: clipboard.paste.bind(clipboard), ...(clipboard.dispose ? { dispose: clipboard.dispose.bind(clipboard) } : {}) });
|
|
1867
|
+
this.setStatus(`${clipboard.itemCount} steps copied`);
|
|
1868
|
+
},
|
|
1869
|
+
refuse: message => { if (!active || claimed || session !== this.clipboardSession || !this.isConnected)
|
|
1870
|
+
return; claimed = true; this.setStatus(message ?? 'Copy is not supported by this host'); },
|
|
1871
|
+
};
|
|
1872
|
+
try {
|
|
1873
|
+
emit(this, this.graphSelection && (this.selectedLineIds.size || this.selectedNoteIds.size) ? 'process-selection-copy-request' : 'process-copy-request', request);
|
|
1874
|
+
}
|
|
1875
|
+
finally {
|
|
1876
|
+
active = false;
|
|
1877
|
+
}
|
|
1878
|
+
if (!claimed && session === this.clipboardSession && this.isConnected)
|
|
1879
|
+
this.setStatus('Copy is not supported by this host');
|
|
1880
|
+
return claimed;
|
|
1881
|
+
}
|
|
1882
|
+
pasteClipboard() {
|
|
1883
|
+
const clipboard = this.clipboard;
|
|
1884
|
+
if (!clipboard || this.locked)
|
|
1885
|
+
return;
|
|
1886
|
+
const session = this.clipboardSession;
|
|
1887
|
+
const accept = this.editAcceptance({ type: 'duplicate' });
|
|
1888
|
+
let active = true;
|
|
1889
|
+
let settled = false;
|
|
1890
|
+
const request = {
|
|
1891
|
+
offset: Object.freeze({ x: 32, y: 32 }),
|
|
1892
|
+
accept: command => {
|
|
1893
|
+
if (!active || settled || session !== this.clipboardSession || this.locked)
|
|
1894
|
+
return;
|
|
1895
|
+
settled = true;
|
|
1896
|
+
accept(command);
|
|
1897
|
+
},
|
|
1898
|
+
refuse: message => { if (!active || settled || session !== this.clipboardSession || !this.isConnected)
|
|
1899
|
+
return; settled = true; this.setStatus(message ?? 'Paste was refused by this host'); },
|
|
1900
|
+
};
|
|
1901
|
+
try {
|
|
1902
|
+
clipboard.paste(request);
|
|
1903
|
+
}
|
|
1904
|
+
finally {
|
|
1905
|
+
active = false;
|
|
1906
|
+
}
|
|
1907
|
+
if (!settled && session === this.clipboardSession)
|
|
1908
|
+
this.setStatus('Paste was not accepted by this host');
|
|
1909
|
+
}
|
|
1910
|
+
duplicateBoxes(ids, distance) {
|
|
1911
|
+
const selected = new Set(ids);
|
|
1912
|
+
if (!selected.size || [...selected].some(id => !this.projection.boxes.some(box => box.id === id) || !this.layoutValue.boxes[id]))
|
|
1913
|
+
return;
|
|
1914
|
+
const sources = [...selected].filter(id => {
|
|
1915
|
+
let parent = this.projection.boxes.find(box => box.id === id)?.parentId;
|
|
1916
|
+
while (parent) {
|
|
1917
|
+
if (selected.has(parent))
|
|
1918
|
+
return false;
|
|
1919
|
+
parent = this.projection.boxes.find(box => box.id === parent)?.parentId;
|
|
1920
|
+
}
|
|
1921
|
+
return true;
|
|
1922
|
+
});
|
|
1923
|
+
if (sources.length === 1) {
|
|
1924
|
+
const position = this.layoutValue.boxes[sources[0]];
|
|
1925
|
+
this.requestEdit({ type: 'duplicate', sourceId: sources[0], position: { x: position.x + distance, y: position.y + distance } });
|
|
1926
|
+
}
|
|
1927
|
+
else {
|
|
1928
|
+
this.requestEdit({ type: 'duplicate', sourceIds: sources, offset: { x: distance, y: distance } });
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1458
1931
|
onKey(event) {
|
|
1459
1932
|
if (event.defaultPrevented)
|
|
1460
1933
|
return;
|
|
@@ -1478,25 +1951,27 @@ export class ProcessModeler extends BaseElement {
|
|
|
1478
1951
|
}
|
|
1479
1952
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
|
|
1480
1953
|
event.preventDefault();
|
|
1481
|
-
this.
|
|
1482
|
-
|
|
1954
|
+
this.selectItems([
|
|
1955
|
+
...this.projection.boxes.filter(box => box.kind !== 'section').map(box => ({ type: 'box', id: box.id })),
|
|
1956
|
+
...this.projection.lines.map(line => ({ type: 'line', id: line.id })),
|
|
1957
|
+
...(this.layoutValue.notes ?? []).map(note => ({ type: 'note', id: note.id })),
|
|
1958
|
+
]);
|
|
1959
|
+
this.setStatus(`${this.selection.length} items selected`);
|
|
1483
1960
|
return;
|
|
1484
1961
|
}
|
|
1485
|
-
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c' && this.selectedIds.size) {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
this.setStatus(`${this.clipboardIds.length} steps copied`);
|
|
1962
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c' && (this.selectedIds.size || this.selectedNoteIds.size)) {
|
|
1963
|
+
if (this.copySelection())
|
|
1964
|
+
event.preventDefault();
|
|
1489
1965
|
return;
|
|
1490
1966
|
}
|
|
1491
|
-
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v' && this.
|
|
1967
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v' && this.clipboard && !this.locked) {
|
|
1492
1968
|
event.preventDefault();
|
|
1493
|
-
this.
|
|
1494
|
-
this.requestEdit({ type: 'duplicate', sourceId: id, position: { x: position.x + 32 * (index + 1), y: position.y + 32 * (index + 1) } }); });
|
|
1969
|
+
this.pasteClipboard();
|
|
1495
1970
|
return;
|
|
1496
1971
|
}
|
|
1497
1972
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'd' && this.selectedIds.size && !this.locked) {
|
|
1498
1973
|
event.preventDefault();
|
|
1499
|
-
[...this.selectedIds]
|
|
1974
|
+
this.duplicateBoxes([...this.selectedIds], 48);
|
|
1500
1975
|
return;
|
|
1501
1976
|
}
|
|
1502
1977
|
if (event.key === "Escape") {
|
|
@@ -1506,6 +1981,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1506
1981
|
if (this.selectedLineId)
|
|
1507
1982
|
this.selectLine(null);
|
|
1508
1983
|
this.portDrag = undefined;
|
|
1984
|
+
this.endDrag = undefined;
|
|
1509
1985
|
this.marquee = undefined;
|
|
1510
1986
|
this.segmentDrag = undefined;
|
|
1511
1987
|
this.drag = undefined;
|
|
@@ -1521,6 +1997,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
1521
1997
|
this.setStatus("Whole process fitted to the canvas");
|
|
1522
1998
|
return;
|
|
1523
1999
|
}
|
|
2000
|
+
if ((event.key === 'Delete' || event.key === 'Backspace') && this.graphSelection && this.selection.length && !this.locked) {
|
|
2001
|
+
event.preventDefault();
|
|
2002
|
+
this.selectionCommand('delete-many');
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
1524
2005
|
const box = this.selected;
|
|
1525
2006
|
if (this.selectedLine && (event.key === 'Delete' || event.key === 'Backspace') && !this.locked && !this.disableConnections) {
|
|
1526
2007
|
event.preventDefault();
|
|
@@ -1532,6 +2013,44 @@ export class ProcessModeler extends BaseElement {
|
|
|
1532
2013
|
this.openKeyboardChooser(box);
|
|
1533
2014
|
return;
|
|
1534
2015
|
}
|
|
2016
|
+
const direction = {
|
|
2017
|
+
ArrowLeft: [-1, 0],
|
|
2018
|
+
ArrowRight: [1, 0],
|
|
2019
|
+
ArrowUp: [0, -1],
|
|
2020
|
+
ArrowDown: [0, 1],
|
|
2021
|
+
}[event.key];
|
|
2022
|
+
if (direction && this.graphSelection && !this.locked && !event.altKey && !event.ctrlKey && !event.metaKey && (this.selectedIds.size || this.selectedNoteIds.size)) {
|
|
2023
|
+
event.preventDefault();
|
|
2024
|
+
const distance = event.shiftKey ? 64 : 16, dx = direction[0] * distance, dy = direction[1] * distance;
|
|
2025
|
+
const next = this.layout, moved = new Set(this.selectedIds);
|
|
2026
|
+
for (let changed = true; changed;) {
|
|
2027
|
+
changed = false;
|
|
2028
|
+
for (const box of this.projection.boxes)
|
|
2029
|
+
if (box.parentId && moved.has(box.parentId) && !moved.has(box.id)) {
|
|
2030
|
+
moved.add(box.id);
|
|
2031
|
+
changed = true;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
for (const id of moved) {
|
|
2035
|
+
const p = next.boxes[id];
|
|
2036
|
+
if (p)
|
|
2037
|
+
next.boxes[id] = { ...p, x: p.x + dx, y: p.y + dy };
|
|
2038
|
+
}
|
|
2039
|
+
next.notes = next.notes?.map(note => this.selectedNoteIds.has(note.id) ? { ...note, x: note.x + dx, y: note.y + dy } : note);
|
|
2040
|
+
const primary = this.selectedId;
|
|
2041
|
+
const allowed = primary
|
|
2042
|
+
? emit(this, 'move-request', { boxId: primary, position: next.boxes[primary], boxIds: [...moved], positions: next.boxes, noteIds: [...this.selectedNoteIds], notes: next.notes ?? [] })
|
|
2043
|
+
: emit(this, 'note-move-request', { noteIds: [...this.selectedNoteIds], notes: next.notes ?? [] });
|
|
2044
|
+
if (allowed)
|
|
2045
|
+
this.commitLayout(next);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
const canInsertDirection = event.ctrlKey && event.altKey && box && !this.locked && this.catalog.length && !this.disableConnections;
|
|
2049
|
+
if (direction && !canInsertDirection && (event.altKey || !box || this.locked)) {
|
|
2050
|
+
event.preventDefault();
|
|
2051
|
+
this.navigateSelection(direction);
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
1535
2054
|
if (!box || this.locked)
|
|
1536
2055
|
return;
|
|
1537
2056
|
if (event.key === "Enter") {
|
|
@@ -1560,12 +2079,6 @@ export class ProcessModeler extends BaseElement {
|
|
|
1560
2079
|
event.preventDefault();
|
|
1561
2080
|
this.selectionCommand("delete-many");
|
|
1562
2081
|
}
|
|
1563
|
-
const direction = {
|
|
1564
|
-
ArrowLeft: [-1, 0],
|
|
1565
|
-
ArrowRight: [1, 0],
|
|
1566
|
-
ArrowUp: [0, -1],
|
|
1567
|
-
ArrowDown: [0, 1],
|
|
1568
|
-
}[event.key];
|
|
1569
2082
|
if (direction) {
|
|
1570
2083
|
event.preventDefault();
|
|
1571
2084
|
if (event.ctrlKey && event.altKey && this.catalog.length && !this.disableConnections) {
|
|
@@ -1578,31 +2091,52 @@ export class ProcessModeler extends BaseElement {
|
|
|
1578
2091
|
this.openAnchoredChooser(this.nextEdit(box, side, { x: pos.x + direction[0] * 320, y: pos.y + direction[1] * 170 }), this.nextChooserTitle(box, side), anchor, returnFocus);
|
|
1579
2092
|
return;
|
|
1580
2093
|
}
|
|
1581
|
-
if (event.altKey) {
|
|
1582
|
-
const origin = this.layoutValue.boxes[box.id];
|
|
1583
|
-
const center = { x: origin.x + (origin.width ?? 224) / 2, y: origin.y + (origin.height ?? 64) / 2 };
|
|
1584
|
-
const candidates = this.projection.boxes.filter(candidate => candidate.id !== box.id).map(candidate => {
|
|
1585
|
-
const position = this.layoutValue.boxes[candidate.id];
|
|
1586
|
-
const dx = position.x + (position.width ?? 224) / 2 - center.x;
|
|
1587
|
-
const dy = position.y + (position.height ?? 64) / 2 - center.y;
|
|
1588
|
-
const forward = dx * direction[0] + dy * direction[1];
|
|
1589
|
-
const sideways = Math.abs(dx * direction[1] - dy * direction[0]);
|
|
1590
|
-
return { candidate, forward, score: forward + sideways * 2 };
|
|
1591
|
-
}).filter(item => item.forward > 0).sort((a, b) => a.score - b.score || a.candidate.id.localeCompare(b.candidate.id));
|
|
1592
|
-
const next = candidates[0]?.candidate;
|
|
1593
|
-
if (next) {
|
|
1594
|
-
this.select(next.id);
|
|
1595
|
-
this.focusBox(next.id);
|
|
1596
|
-
this.setStatus(`${next.title} selected`);
|
|
1597
|
-
}
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
2094
|
const pos = this.layoutValue.boxes[box.id];
|
|
1601
2095
|
const distance = event.shiftKey ? 64 : 16;
|
|
1602
2096
|
this.move(box.id, pos.x + direction[0] * distance, pos.y + direction[1] * distance);
|
|
1603
2097
|
this.focusBox(box.id);
|
|
1604
2098
|
}
|
|
1605
2099
|
}
|
|
2100
|
+
navigateSelection(direction) {
|
|
2101
|
+
const canvas = this.shadowRoot.querySelector('[part=canvas]');
|
|
2102
|
+
const box = this.selected;
|
|
2103
|
+
const origin = box ? this.layoutValue.boxes[box.id] : undefined;
|
|
2104
|
+
const originSize = box ? this.selectionDimensions(box) : undefined;
|
|
2105
|
+
const center = origin && originSize
|
|
2106
|
+
? { x: origin.x + originSize.width / 2, y: origin.y + originSize.height / 2 }
|
|
2107
|
+
: { x: (canvas.clientWidth / 2 - this.viewport.x) / this.viewport.zoom, y: (canvas.clientHeight / 2 - this.viewport.y) / this.viewport.zoom };
|
|
2108
|
+
const candidates = this.projection.boxes.filter(candidate => candidate.id !== box?.id && candidate.kind !== 'section').map(candidate => {
|
|
2109
|
+
const position = this.layoutValue.boxes[candidate.id];
|
|
2110
|
+
const size = this.selectionDimensions(candidate);
|
|
2111
|
+
const dx = position.x + size.width / 2 - center.x;
|
|
2112
|
+
const dy = position.y + size.height / 2 - center.y;
|
|
2113
|
+
const forward = dx * direction[0] + dy * direction[1];
|
|
2114
|
+
const sideways = Math.abs(dx * direction[1] - dy * direction[0]);
|
|
2115
|
+
return { candidate, forward, score: forward + sideways * 2.5 };
|
|
2116
|
+
}).filter(item => item.forward > 4).sort((a, b) => a.score - b.score);
|
|
2117
|
+
const next = candidates[0]?.candidate;
|
|
2118
|
+
if (!next) {
|
|
2119
|
+
this.setStatus('Nothing further that way');
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
this.select(next.id);
|
|
2123
|
+
this.revealBox(next);
|
|
2124
|
+
this.focusBox(next.id);
|
|
2125
|
+
this.setStatus(`${next.title} selected`);
|
|
2126
|
+
}
|
|
2127
|
+
revealBox(box) {
|
|
2128
|
+
const canvas = this.shadowRoot.querySelector('[part=canvas]');
|
|
2129
|
+
if (!canvas.clientWidth || !canvas.clientHeight)
|
|
2130
|
+
return;
|
|
2131
|
+
const position = this.layoutValue.boxes[box.id];
|
|
2132
|
+
const size = this.selectionDimensions(box);
|
|
2133
|
+
const left = position.x * this.viewport.zoom + this.viewport.x;
|
|
2134
|
+
const top = position.y * this.viewport.zoom + this.viewport.y;
|
|
2135
|
+
if (left > 48 && top > 48 && left + size.width * this.viewport.zoom < canvas.clientWidth - 48 && top + size.height * this.viewport.zoom < canvas.clientHeight - 48)
|
|
2136
|
+
return;
|
|
2137
|
+
this.setView({ x: canvas.clientWidth / 2 - (position.x + size.width / 2) * this.viewport.zoom,
|
|
2138
|
+
y: canvas.clientHeight / 2 - (position.y + size.height / 2) * this.viewport.zoom, zoom: this.viewport.zoom });
|
|
2139
|
+
}
|
|
1606
2140
|
pointerDown(event) {
|
|
1607
2141
|
if (event.button !== 0)
|
|
1608
2142
|
return;
|
|
@@ -1706,10 +2240,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
1706
2240
|
const start = this.portDrag.start;
|
|
1707
2241
|
ghost.setAttribute("points", `${start.x},${start.y} ${point.x},${start.y} ${point.x},${point.y}`);
|
|
1708
2242
|
const target = this.boxAt(point, this.portDrag.id);
|
|
1709
|
-
const
|
|
1710
|
-
|
|
2243
|
+
const problem = target ? this.connectionProblem({ type: 'connect', from: this.portDrag.id, to: target.id }) : null;
|
|
2244
|
+
const pinned = target && !problem ? this.sideAt(target, point) : undefined;
|
|
2245
|
+
ghost.dataset.invalid = String(Boolean(problem));
|
|
2246
|
+
this.shadowRoot.querySelectorAll('[data-box-id]').forEach(box => { box.dataset.connectTarget = String(box.dataset.boxId === target?.id); box.dataset.connectInvalid = String(box.dataset.boxId === target?.id && Boolean(problem)); });
|
|
1711
2247
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { port.dataset.hot = String(port.dataset.owner === target?.id && port.dataset.side === pinned); });
|
|
1712
|
-
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Connect to ${target.title}` : 'Drop here to add a connected step');
|
|
2248
|
+
this.showConnectTooltip(point, problem ?? (target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Connect to ${target.title}` : 'Drop here to add a connected step'), Boolean(problem));
|
|
1713
2249
|
return;
|
|
1714
2250
|
}
|
|
1715
2251
|
if (this.endDrag) {
|
|
@@ -1725,11 +2261,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
1725
2261
|
if (fixed)
|
|
1726
2262
|
ghost.setAttribute('points', `${fixed.x},${fixed.y} ${point.x},${fixed.y} ${point.x},${point.y}`);
|
|
1727
2263
|
const target = this.boxAt(point);
|
|
1728
|
-
const
|
|
1729
|
-
|
|
2264
|
+
const problem = target ? this.connectionProblem({ type: 'reattach', lineId: this.endDrag.lineId, [this.endDrag.end]: target.id }) : null;
|
|
2265
|
+
const pinned = target && !problem ? this.sideAt(target, point) : undefined;
|
|
2266
|
+
ghost.dataset.invalid = String(Boolean(problem) || !target);
|
|
2267
|
+
this.shadowRoot.querySelectorAll('[data-box-id]').forEach(box => { box.dataset.connectTarget = String(box.dataset.boxId === target?.id); box.dataset.connectInvalid = String(box.dataset.boxId === target?.id && Boolean(problem)); });
|
|
1730
2268
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { port.dataset.hot = String(port.dataset.owner === target?.id && port.dataset.side === pinned); });
|
|
1731
|
-
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Attach to ${target.title}` : 'Drop on a step to reattach', !target);
|
|
1732
|
-
this.setStatus(target ? `Release to attach the connection to ${target.title}` : 'Move over a step to attach this connection');
|
|
2269
|
+
this.showConnectTooltip(point, problem ?? (target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Attach to ${target.title}` : 'Drop on a step to reattach'), Boolean(problem) || !target);
|
|
2270
|
+
this.setStatus(problem ?? (target ? `Release to attach the connection to ${target.title}` : 'Move over a step to attach this connection'));
|
|
1733
2271
|
return;
|
|
1734
2272
|
}
|
|
1735
2273
|
if (this.segmentDrag) {
|
|
@@ -1828,7 +2366,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1828
2366
|
this.portDrag = undefined;
|
|
1829
2367
|
this.shadowRoot.querySelector('[part=connection-preview]')?.remove();
|
|
1830
2368
|
this.shadowRoot.querySelector('[part=connect-tooltip]')?.remove();
|
|
1831
|
-
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; });
|
|
2369
|
+
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; delete box.dataset.connectInvalid; });
|
|
1832
2370
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { delete port.dataset.hot; });
|
|
1833
2371
|
if (commit) {
|
|
1834
2372
|
if (Math.hypot(point.x - port.start.x, point.y - port.start.y) < 8) {
|
|
@@ -1849,7 +2387,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1849
2387
|
if ((frame?.id ?? null) !== (source.parentId ?? null))
|
|
1850
2388
|
this.setStatus('Drop inside the same frame to add a connected step');
|
|
1851
2389
|
else
|
|
1852
|
-
this.openChooserAtPoint({ type: 'add', from: port.id, fromSide: port.side, parentId: source.parentId, position: point }, `Add after ${source.title}`, point);
|
|
2390
|
+
this.openChooserAtPoint({ type: 'add', from: port.id, fromSide: port.side, parentId: source.parentId, position: point, placement: { source: 'point', center: { ...point } } }, `Add after ${source.title}`, point);
|
|
1853
2391
|
}
|
|
1854
2392
|
}
|
|
1855
2393
|
}
|
|
@@ -1860,7 +2398,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1860
2398
|
this.endDrag = undefined;
|
|
1861
2399
|
this.shadowRoot.querySelector('[part=connection-preview]')?.remove();
|
|
1862
2400
|
this.shadowRoot.querySelector('[part=connect-tooltip]')?.remove();
|
|
1863
|
-
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; });
|
|
2401
|
+
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; delete box.dataset.connectInvalid; });
|
|
1864
2402
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { delete port.dataset.hot; });
|
|
1865
2403
|
if (commit) {
|
|
1866
2404
|
if (Math.hypot(point.x - drag.start.x, point.y - drag.start.y) < 8) {
|
|
@@ -1909,8 +2447,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
1909
2447
|
const position = this.alignedPoint(drag.id, drag.position.x + dx / this.viewport.zoom, drag.position.y + dy / this.viewport.zoom);
|
|
1910
2448
|
const line = this.lineAt(point, drag.id);
|
|
1911
2449
|
const frame = this.boxAt(point, drag.id, true);
|
|
1912
|
-
if (line)
|
|
1913
|
-
this.
|
|
2450
|
+
if (line) {
|
|
2451
|
+
const box = this.projection.boxes.find(box => box.id === drag.id);
|
|
2452
|
+
const { width, height } = this.selectionDimensions(box);
|
|
2453
|
+
this.requestEdit({ type: "insert", boxId: drag.id, lineId: line.id, from: line.from, to: line.to, position, placement: { source: 'point', center: { x: position.x + width / 2, y: position.y + height / 2 } } });
|
|
2454
|
+
}
|
|
1914
2455
|
else if (this.projection.boxes.find(box => box.id === drag.id)?.parentId !== frame?.id)
|
|
1915
2456
|
this.requestEdit({ type: "reparent", boxId: drag.id, parentId: frame?.id, position });
|
|
1916
2457
|
else if (this.selectedIds.size > 1 && this.selectedIds.has(drag.id)) {
|
|
@@ -1976,6 +2517,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
1976
2517
|
}
|
|
1977
2518
|
markDropLine(point, except) {
|
|
1978
2519
|
this.dropLine = point ? this.lineAt(point, except)?.id : undefined;
|
|
2520
|
+
this.paintDropLine();
|
|
2521
|
+
}
|
|
2522
|
+
paintDropLine() {
|
|
1979
2523
|
this.shadowRoot.querySelectorAll('[part=line]').forEach(path => { const active = path.dataset.lineId === this.dropLine; path.dataset.drop = String(active); path.setAttribute('marker-end', `url(#${active || path.dataset.selected === 'true' ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`); });
|
|
1980
2524
|
}
|
|
1981
2525
|
alignedPoint(id, x, y) {
|
|
@@ -2032,9 +2576,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
2032
2576
|
nextEdit(box, side, position) {
|
|
2033
2577
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape;
|
|
2034
2578
|
const outgoing = this.projection.lines.filter(line => line.from === box.id);
|
|
2579
|
+
const placement = side ? { source: 'direction', side } : { source: 'next' };
|
|
2035
2580
|
if ((!side || side === 'east') && shape !== 'gateway' && outgoing.length === 1)
|
|
2036
|
-
return { type: 'insert', lineId: outgoing[0].id, from: box.id, to: outgoing[0].to };
|
|
2037
|
-
return { type: 'add', from: box.id, ...(side ? { fromSide: side } : {}), parentId: box.parentId, ...(position ? { position } : {}) };
|
|
2581
|
+
return { type: 'insert', lineId: outgoing[0].id, from: box.id, to: outgoing[0].to, placement };
|
|
2582
|
+
return { type: 'add', from: box.id, placement, ...(side ? { fromSide: side } : {}), parentId: box.parentId, ...(position ? { position } : {}) };
|
|
2038
2583
|
}
|
|
2039
2584
|
nextChooserTitle(box, side) {
|
|
2040
2585
|
const edit = this.nextEdit(box, side);
|
|
@@ -2060,7 +2605,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2060
2605
|
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
2061
2606
|
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
2062
2607
|
const points = this.routedLines.get(line.id) ?? routeProcessLine(line, this.layoutValue, this.projection);
|
|
2063
|
-
this.openChooserAtPoint({ type: 'insert', lineId: line.id, from: line.from, to: line.to }, `Insert between ${source?.title ?? line.from} and ${target?.title ?? line.to}`, lineMidpoint(points), returnFocus);
|
|
2608
|
+
this.openChooserAtPoint({ type: 'insert', lineId: line.id, from: line.from, to: line.to, placement: { source: 'line', center: lineMidpoint(points) } }, `Insert between ${source?.title ?? line.from} and ${target?.title ?? line.to}`, lineMidpoint(points), returnFocus);
|
|
2064
2609
|
}
|
|
2065
2610
|
closeKeyboardChooser(returnFocus = false) {
|
|
2066
2611
|
const chooser = this.shadowRoot?.querySelector('[part=keyboard-chooser]');
|
|
@@ -2088,8 +2633,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
2088
2633
|
const anchor = Array.from(this.shadowRoot.querySelectorAll('[data-box-id]'))
|
|
2089
2634
|
.find(element => element.dataset.boxId === box.id);
|
|
2090
2635
|
this.openAnchoredChooser(insert
|
|
2091
|
-
? { type: 'insert', lineId: outgoing[0].id, from: box.id, to: target.id }
|
|
2092
|
-
: { type: 'add', from: box.id }, insert ? `Insert between ${box.title} and ${target.title}`
|
|
2636
|
+
? { type: 'insert', lineId: outgoing[0].id, from: box.id, to: target.id, placement: { source: 'next' } }
|
|
2637
|
+
: { type: 'add', from: box.id, placement: { source: 'next' } }, insert ? `Insert between ${box.title} and ${target.title}`
|
|
2093
2638
|
: shape === 'gateway' ? `Add a path from ${box.title}` : `Add after ${box.title}`, anchor);
|
|
2094
2639
|
}
|
|
2095
2640
|
openAnchoredChooser(edit, title, anchor, returnFocus) {
|
|
@@ -2138,7 +2683,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2138
2683
|
tooltip.style.top = `${point.y + 16}px`;
|
|
2139
2684
|
}
|
|
2140
2685
|
update() {
|
|
2141
|
-
const selectionBefore = JSON.stringify({
|
|
2686
|
+
const selectionBefore = JSON.stringify({ items: this.selection, path: this.selectedPath });
|
|
2142
2687
|
if (this.documentValue !== undefined) {
|
|
2143
2688
|
this.projection = this.model.project(this.documentValue);
|
|
2144
2689
|
validateProjection(this.projection);
|
|
@@ -2153,10 +2698,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
2153
2698
|
if (this.selectedLineId && !this.selectedLine)
|
|
2154
2699
|
this.selectedLineId = null;
|
|
2155
2700
|
this.selectedIds = new Set([...this.selectedIds].filter(id => this.projection.boxes.some(box => box.id === id)));
|
|
2701
|
+
this.selectedLineIds = new Set([...this.selectedLineIds].filter(id => this.projection.lines.some(line => line.id === id)));
|
|
2702
|
+
this.selectedNoteIds = new Set([...this.selectedNoteIds].filter(id => this.layoutValue.notes?.some(note => note.id === id)));
|
|
2156
2703
|
if (!this.selectedId)
|
|
2157
2704
|
this.selectedId = this.selectedIds.values().next().value ?? null;
|
|
2158
2705
|
const serialized = JSON.stringify({
|
|
2159
|
-
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 })),
|
|
2706
|
+
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 })),
|
|
2160
2707
|
lines: this.projection.lines,
|
|
2161
2708
|
});
|
|
2162
2709
|
if (this.documentValue !== undefined && (serialized !== this.lastProjection || this.documentValue !== this.lastDocument)) {
|
|
@@ -2164,7 +2711,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2164
2711
|
this.versionValue = typeof this.versionValue === "number" ? this.versionValue + 1 : `${this.versionValue}+1`;
|
|
2165
2712
|
this.lastProjection = serialized;
|
|
2166
2713
|
this.lastDocument = this.documentValue;
|
|
2167
|
-
|
|
2714
|
+
this.notify("projection-changed", { projection: snapshotProcessProjection(this.projection), version: this.versionValue, checks: this.allChecks });
|
|
2168
2715
|
}
|
|
2169
2716
|
// Riptide or another host owns conversion. It supplies validate() and
|
|
2170
2717
|
// receives this event only when that conversion can read the drawing.
|
|
@@ -2174,7 +2721,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2174
2721
|
if (this.documentValue !== undefined && !this.allChecks.length && readableKey !== this.lastReadableProjection) {
|
|
2175
2722
|
this.lastReadableProjection = readableKey;
|
|
2176
2723
|
this.readableValue = snapshotProcessProjection(this.projection);
|
|
2177
|
-
|
|
2724
|
+
this.notify("readable-projection-changed", { projection: snapshotProcessProjection(this.readableValue), version: this.versionValue });
|
|
2178
2725
|
}
|
|
2179
2726
|
if (this.documentValue === undefined) {
|
|
2180
2727
|
this.lastProjection = "";
|
|
@@ -2182,18 +2729,29 @@ export class ProcessModeler extends BaseElement {
|
|
|
2182
2729
|
this.readableValue = null;
|
|
2183
2730
|
this.lastDocument = undefined;
|
|
2184
2731
|
}
|
|
2185
|
-
const
|
|
2186
|
-
|
|
2732
|
+
const active = this.shadowRoot.activeElement;
|
|
2733
|
+
const focused = active?.dataset.boxId, focusedNote = active?.dataset.noteId;
|
|
2187
2734
|
this.renderWorld();
|
|
2188
2735
|
this.renderPalette();
|
|
2189
2736
|
this.renderChecks();
|
|
2190
2737
|
this.renderSelection();
|
|
2191
2738
|
this.paintViewport();
|
|
2192
2739
|
this.updateToolbar();
|
|
2193
|
-
if (selectionBefore !== JSON.stringify({
|
|
2194
|
-
|
|
2195
|
-
if (focused)
|
|
2196
|
-
this.
|
|
2740
|
+
if (selectionBefore !== JSON.stringify({ items: this.selection, path: this.selectedPath }))
|
|
2741
|
+
this.notify("selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath, ...(this.graphSelection ? { selection: this.selection } : {}) });
|
|
2742
|
+
if (focused) {
|
|
2743
|
+
this.restoringBoxFocus = true;
|
|
2744
|
+
try {
|
|
2745
|
+
this.focusBox(focused);
|
|
2746
|
+
}
|
|
2747
|
+
finally {
|
|
2748
|
+
this.restoringBoxFocus = false;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
else if (focusedNote) {
|
|
2752
|
+
const note = Array.from(this.shadowRoot.querySelectorAll('[data-note-id]')).find(note => note.dataset.noteId === focusedNote);
|
|
2753
|
+
(note ?? this.shadowRoot.querySelector('[part=canvas]'))?.focus({ preventScroll: true });
|
|
2754
|
+
}
|
|
2197
2755
|
}
|
|
2198
2756
|
get allChecks() { return this.computedChecks; }
|
|
2199
2757
|
collectChecks() {
|
|
@@ -2222,6 +2780,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
2222
2780
|
for (const box of [...this.projection.boxes].sort((a, b) => Number(Boolean(b.frame)) - Number(Boolean(a.frame)))) {
|
|
2223
2781
|
const element = document.createElement("div");
|
|
2224
2782
|
element.tabIndex = 0;
|
|
2783
|
+
element.addEventListener('focus', () => {
|
|
2784
|
+
if (!this.restoringBoxFocus && !this.pointers.size && !this.portDrag && !this.endDrag && !this.segmentDrag && !this.marquee && !this.frameResize) {
|
|
2785
|
+
this.select(box.id);
|
|
2786
|
+
if (box.kind !== 'section')
|
|
2787
|
+
this.revealBox(box);
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2225
2790
|
element.setAttribute("role", "group");
|
|
2226
2791
|
element.setAttribute("part", box.frame ? "frame" : "box");
|
|
2227
2792
|
element.dataset.boxId = box.id;
|
|
@@ -2233,7 +2798,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2233
2798
|
const problem = this.allChecks.find(check => check.boxId === box.id || (check.path && JSON.stringify(check.path) === JSON.stringify(box.path)));
|
|
2234
2799
|
const incoming = this.projection.lines.filter(line => line.to === box.id).length;
|
|
2235
2800
|
const outgoing = this.projection.lines.filter(line => line.from === box.id).length;
|
|
2236
|
-
element.setAttribute("aria-label", [`${kind?.label ?? box.kind}: ${box.title}. ${incoming} in, ${outgoing} out`, problem ? `Needs attention: ${problem.message}` : ""]
|
|
2801
|
+
element.setAttribute("aria-label", [`${kind?.label ?? box.kind}: ${box.title}. ${incoming} in, ${outgoing} out`, problem ? `Needs attention: ${problem.title || problem.message}` : ""]
|
|
2237
2802
|
.filter(Boolean)
|
|
2238
2803
|
.join(". "));
|
|
2239
2804
|
element.setAttribute("aria-current", String(this.selectedIds.has(box.id)));
|
|
@@ -2350,7 +2915,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2350
2915
|
if (problem) {
|
|
2351
2916
|
const message = document.createElement("span");
|
|
2352
2917
|
message.setAttribute("part", "problem");
|
|
2353
|
-
message.append(checkGlyph(), document.createTextNode(problem.title
|
|
2918
|
+
message.append(checkGlyph(), document.createTextNode(problem.title || problem.message));
|
|
2354
2919
|
element.append(message);
|
|
2355
2920
|
}
|
|
2356
2921
|
element.addEventListener("keydown", event => {
|
|
@@ -2394,6 +2959,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2394
2959
|
for (const side of ["north", "east", "south", "west"]) {
|
|
2395
2960
|
const port = document.createElement("button");
|
|
2396
2961
|
port.type = "button";
|
|
2962
|
+
port.tabIndex = -1;
|
|
2397
2963
|
port.setAttribute("part", "port");
|
|
2398
2964
|
port.dataset.side = side;
|
|
2399
2965
|
port.dataset.owner = box.id;
|
|
@@ -2458,8 +3024,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
2458
3024
|
const path = svgElement("path");
|
|
2459
3025
|
path.setAttribute("part", "line");
|
|
2460
3026
|
path.dataset.lineId = line.id;
|
|
2461
|
-
path.dataset.selected = String(this.
|
|
2462
|
-
path.setAttribute('marker-end', `url(#${this.
|
|
3027
|
+
path.dataset.selected = String(this.selectedLineIds.has(line.id));
|
|
3028
|
+
path.setAttribute('marker-end', `url(#${this.selectedLineIds.has(line.id) ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`);
|
|
2463
3029
|
path.setAttribute("vector-effect", "non-scaling-stroke");
|
|
2464
3030
|
path.setAttribute("d", roundedProcessPath(points));
|
|
2465
3031
|
if (line.dashed)
|
|
@@ -2558,6 +3124,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2558
3124
|
lineId: line.id,
|
|
2559
3125
|
from: line.from,
|
|
2560
3126
|
to: line.to,
|
|
3127
|
+
...(type === 'insert' ? { placement: { source: 'line', center: lineMidpoint(points) } } : {}),
|
|
2561
3128
|
};
|
|
2562
3129
|
if (type === "insert" && this.catalog.length) {
|
|
2563
3130
|
this.openLineChooser(line, button);
|
|
@@ -2587,6 +3154,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
2587
3154
|
const element = document.createElement("div");
|
|
2588
3155
|
element.setAttribute("part", "note");
|
|
2589
3156
|
element.textContent = note.text;
|
|
3157
|
+
element.dataset.noteId = note.id;
|
|
3158
|
+
element.addEventListener('pointerdown', event => event.stopPropagation());
|
|
3159
|
+
element.tabIndex = 0;
|
|
3160
|
+
element.setAttribute('role', 'button');
|
|
3161
|
+
element.setAttribute('aria-label', note.text || 'Diagram note');
|
|
3162
|
+
element.addEventListener('click', event => { const items = event.shiftKey ? this.selection.filter(item => !(item.type === 'note' && item.id === note.id)) : []; if (!event.shiftKey || !this.selectedNoteIds.has(note.id))
|
|
3163
|
+
items.push({ type: 'note', id: note.id }); this.selectItems(items); });
|
|
3164
|
+
element.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') {
|
|
3165
|
+
event.preventDefault();
|
|
3166
|
+
event.stopPropagation();
|
|
3167
|
+
this.selectItems([{ type: 'note', id: note.id }]);
|
|
3168
|
+
} });
|
|
2590
3169
|
this.place(element, { ...note, width: 208, height: 80 });
|
|
2591
3170
|
world.append(element);
|
|
2592
3171
|
}
|
|
@@ -2599,6 +3178,149 @@ export class ProcessModeler extends BaseElement {
|
|
|
2599
3178
|
height: `${position.height ?? 64}px`,
|
|
2600
3179
|
});
|
|
2601
3180
|
}
|
|
3181
|
+
activatePaletteKind(kind) {
|
|
3182
|
+
if (this.locked)
|
|
3183
|
+
return;
|
|
3184
|
+
if (this.narrowValue) {
|
|
3185
|
+
this.closeDrawer();
|
|
3186
|
+
this.shadowRoot.querySelector('[part=canvas]').focus({ preventScroll: true });
|
|
3187
|
+
}
|
|
3188
|
+
if (kind.placement) {
|
|
3189
|
+
this.addLayoutKind(kind);
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
const selected = this.selectedIds.size === 1 ? this.selected : null;
|
|
3193
|
+
const from = selected && !selected.frame && selected.kind !== 'section' && !['end', 'finish'].includes(selected.kind) ? selected.id : undefined;
|
|
3194
|
+
if (from && selected)
|
|
3195
|
+
this.requestEdit({ ...this.nextEdit(selected), kind });
|
|
3196
|
+
else {
|
|
3197
|
+
const canvas = this.shadowRoot.querySelector('[part=canvas]');
|
|
3198
|
+
const center = { x: (canvas.clientWidth / 2 - this.viewport.x) / this.viewport.zoom, y: (canvas.clientHeight / 2 - this.viewport.y) / this.viewport.zoom };
|
|
3199
|
+
this.requestEdit({ type: 'add', kind, placement: { source: 'point', center } });
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
paletteLineAt(point) {
|
|
3203
|
+
let nearest;
|
|
3204
|
+
let best = 20 / this.viewport.zoom;
|
|
3205
|
+
for (const line of this.projection.lines) {
|
|
3206
|
+
const points = this.routedLines.get(line.id) ?? [];
|
|
3207
|
+
for (let i = 1; i < points.length; i++) {
|
|
3208
|
+
const a = points[i - 1], b = points[i], dx = b.x - a.x, dy = b.y - a.y, length = dx * dx + dy * dy;
|
|
3209
|
+
const t = length ? Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / length)) : 0;
|
|
3210
|
+
const distance = Math.hypot(point.x - a.x - t * dx, point.y - a.y - t * dy);
|
|
3211
|
+
if (distance < best) {
|
|
3212
|
+
best = distance;
|
|
3213
|
+
nearest = line;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return nearest;
|
|
3218
|
+
}
|
|
3219
|
+
paletteFrameAt(point) {
|
|
3220
|
+
let nearest;
|
|
3221
|
+
let depth = -1;
|
|
3222
|
+
for (const box of this.projection.boxes) {
|
|
3223
|
+
if (!box.frame)
|
|
3224
|
+
continue;
|
|
3225
|
+
const p = this.layoutValue.boxes[box.id];
|
|
3226
|
+
if (!p)
|
|
3227
|
+
continue;
|
|
3228
|
+
const { width, height } = this.selectionDimensions(box);
|
|
3229
|
+
if (point.x < p.x || point.x > p.x + width || point.y < p.y || point.y > p.y + height)
|
|
3230
|
+
continue;
|
|
3231
|
+
let level = 0, parent = box.parentId;
|
|
3232
|
+
while (parent) {
|
|
3233
|
+
level++;
|
|
3234
|
+
parent = this.projection.boxes.find(box => box.id === parent)?.parentId;
|
|
3235
|
+
}
|
|
3236
|
+
if (level > depth) {
|
|
3237
|
+
depth = level;
|
|
3238
|
+
nearest = box;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
return nearest;
|
|
3242
|
+
}
|
|
3243
|
+
markPaletteDrop(point) {
|
|
3244
|
+
this.dropLine = point ? this.paletteLineAt(point)?.id : undefined;
|
|
3245
|
+
const frame = point && !this.dropLine ? this.paletteFrameAt(point) : undefined;
|
|
3246
|
+
this.shadowRoot.querySelectorAll('[part=frame]').forEach(element => { element.dataset.paletteDrop = String(element.dataset.boxId === frame?.id); });
|
|
3247
|
+
this.paintDropLine();
|
|
3248
|
+
}
|
|
3249
|
+
paletteCanvasPoint(event) {
|
|
3250
|
+
const rect = this.shadowRoot.querySelector('[part=canvas]').getBoundingClientRect();
|
|
3251
|
+
if (event.clientX <= rect.left || event.clientX >= rect.right || event.clientY <= rect.top || event.clientY >= rect.bottom)
|
|
3252
|
+
return;
|
|
3253
|
+
return this.canvasPoint(event);
|
|
3254
|
+
}
|
|
3255
|
+
movePalettePointer(event) {
|
|
3256
|
+
const drag = this.palettePointer;
|
|
3257
|
+
if (!drag || drag.id !== event.pointerId)
|
|
3258
|
+
return;
|
|
3259
|
+
if (this.locked) {
|
|
3260
|
+
this.cancelPalettePointer();
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
if (!drag.moved && Math.hypot(event.clientX - drag.x, event.clientY - drag.y) < 5)
|
|
3264
|
+
return;
|
|
3265
|
+
if (!drag.moved) {
|
|
3266
|
+
drag.moved = true;
|
|
3267
|
+
if (this.narrowValue)
|
|
3268
|
+
this.closeDrawer();
|
|
3269
|
+
}
|
|
3270
|
+
let ghost = this.shadowRoot.querySelector('[part=palette-ghost]');
|
|
3271
|
+
if (!ghost) {
|
|
3272
|
+
ghost = document.createElement('div');
|
|
3273
|
+
ghost.setAttribute('part', 'palette-ghost');
|
|
3274
|
+
ghost.dataset.pointer = 'true';
|
|
3275
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
3276
|
+
if (drag.kind.icon)
|
|
3277
|
+
ghost.append(drag.kind.icon());
|
|
3278
|
+
ghost.append(document.createTextNode(drag.kind.label));
|
|
3279
|
+
this.shadowRoot.append(ghost);
|
|
3280
|
+
}
|
|
3281
|
+
ghost.style.left = `${event.clientX}px`;
|
|
3282
|
+
ghost.style.top = `${event.clientY}px`;
|
|
3283
|
+
this.markPaletteDrop(!drag.kind.placement && drag.kind.kind !== 'end' ? this.paletteCanvasPoint(event) : undefined);
|
|
3284
|
+
event.preventDefault();
|
|
3285
|
+
}
|
|
3286
|
+
cancelPalettePointer() {
|
|
3287
|
+
const drag = this.palettePointer;
|
|
3288
|
+
this.palettePointer = undefined;
|
|
3289
|
+
this.ownerDocument.removeEventListener('keydown', this.escapePalettePointer, true);
|
|
3290
|
+
if (!drag)
|
|
3291
|
+
return;
|
|
3292
|
+
this.suppressPaletteClick = true;
|
|
3293
|
+
this.shadowRoot?.querySelector('[part=palette-ghost]')?.remove();
|
|
3294
|
+
this.markPaletteDrop(undefined);
|
|
3295
|
+
try {
|
|
3296
|
+
this.releasePointerCapture(drag.id);
|
|
3297
|
+
}
|
|
3298
|
+
catch { /* Capture may already have been lost. */ }
|
|
3299
|
+
}
|
|
3300
|
+
finishPalettePointer(event) {
|
|
3301
|
+
const drag = this.palettePointer;
|
|
3302
|
+
if (!drag || drag.id !== event.pointerId)
|
|
3303
|
+
return;
|
|
3304
|
+
const point = this.paletteCanvasPoint(event);
|
|
3305
|
+
this.cancelPalettePointer();
|
|
3306
|
+
if (this.locked)
|
|
3307
|
+
return;
|
|
3308
|
+
if (!drag.moved) {
|
|
3309
|
+
this.activatePaletteKind(drag.kind);
|
|
3310
|
+
if (this.narrowValue)
|
|
3311
|
+
this.closeDrawer();
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
if (!point)
|
|
3315
|
+
return;
|
|
3316
|
+
if (drag.kind.placement) {
|
|
3317
|
+
this.addLayoutKind(drag.kind, point);
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
const line = drag.kind.kind === 'end' ? undefined : this.paletteLineAt(point);
|
|
3321
|
+
const frame = line ? undefined : this.paletteFrameAt(point);
|
|
3322
|
+
this.requestEdit({ type: line ? 'insert' : 'add', kind: drag.kind, lineId: line?.id, from: line?.from, to: line?.to, parentId: frame?.id, position: point, placement: { source: 'point', center: { ...point } } });
|
|
3323
|
+
}
|
|
2602
3324
|
renderPalette() {
|
|
2603
3325
|
const choices = this.shadowRoot.querySelector("[part=choices]");
|
|
2604
3326
|
const query = this.shadowRoot.querySelector("[part=search]").value.toLowerCase();
|
|
@@ -2631,19 +3353,30 @@ export class ProcessModeler extends BaseElement {
|
|
|
2631
3353
|
description.textContent = kind.description;
|
|
2632
3354
|
button.append(icon, name, description);
|
|
2633
3355
|
button.disabled = this.locked;
|
|
2634
|
-
button.draggable =
|
|
2635
|
-
button.addEventListener(
|
|
2636
|
-
if (
|
|
2637
|
-
|
|
3356
|
+
button.draggable = false;
|
|
3357
|
+
button.addEventListener('pointerdown', event => {
|
|
3358
|
+
if (this.locked || event.button !== 0 || event.isPrimary === false)
|
|
3359
|
+
return;
|
|
3360
|
+
this.cancelPalettePointer();
|
|
3361
|
+
this.suppressPaletteClick = false;
|
|
3362
|
+
this.palettePointer = { kind, id: event.pointerId, x: event.clientX, y: event.clientY, moved: false };
|
|
3363
|
+
this.ownerDocument.addEventListener('keydown', this.escapePalettePointer, true);
|
|
3364
|
+
try {
|
|
3365
|
+
this.setPointerCapture(event.pointerId);
|
|
3366
|
+
}
|
|
3367
|
+
catch { /* Detached synthetic pointers cannot be captured. */ }
|
|
3368
|
+
event.preventDefault();
|
|
3369
|
+
event.stopPropagation();
|
|
3370
|
+
});
|
|
3371
|
+
button.addEventListener('click', event => {
|
|
3372
|
+
if (event.detail > 0 && this.suppressPaletteClick) {
|
|
3373
|
+
this.suppressPaletteClick = false;
|
|
2638
3374
|
return;
|
|
2639
3375
|
}
|
|
2640
|
-
|
|
2641
|
-
const outgoing = from ? this.projection.lines.filter(line => line.from === from) : [];
|
|
2642
|
-
const line = outgoing.length === 1 ? outgoing[0] : undefined;
|
|
2643
|
-
this.requestEdit(line ? { type: "insert", kind, lineId: line.id, from: line.from, to: line.to } : { type: "add", kind, ...(from ? { from } : {}) });
|
|
3376
|
+
this.activatePaletteKind(kind);
|
|
2644
3377
|
});
|
|
2645
|
-
button.addEventListener(
|
|
2646
|
-
button.addEventListener(
|
|
3378
|
+
button.addEventListener('dragstart', event => { this.paletteDragKind = kind; event.dataTransfer?.setData('application/boe-process-kind', kind.kind); });
|
|
3379
|
+
button.addEventListener('dragend', () => { this.paletteDragKind = null; this.shadowRoot?.querySelector('[part=palette-ghost]')?.remove(); this.markDropLine(undefined); });
|
|
2647
3380
|
choices.append(button);
|
|
2648
3381
|
}
|
|
2649
3382
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
@@ -2685,7 +3418,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2685
3418
|
type: line ? "insert" : "add",
|
|
2686
3419
|
kind,
|
|
2687
3420
|
lineId: line?.id, from: line?.from, to: line?.to, parentId: frame?.id,
|
|
2688
|
-
position,
|
|
3421
|
+
position, placement: { source: 'point', center: { ...position } },
|
|
2689
3422
|
});
|
|
2690
3423
|
this.markDropLine(undefined);
|
|
2691
3424
|
};
|
|
@@ -2937,19 +3670,48 @@ export class ProcessModeler extends BaseElement {
|
|
|
2937
3670
|
return row;
|
|
2938
3671
|
}
|
|
2939
3672
|
renderSelection() {
|
|
3673
|
+
this.shadowRoot?.querySelectorAll('[part=note]').forEach(note => { note.dataset.selected = String(this.selectedNoteIds.has(note.dataset.noteId)); note.setAttribute('aria-pressed', note.dataset.selected); });
|
|
2940
3674
|
if (!this.isRendered)
|
|
2941
3675
|
return;
|
|
2942
3676
|
const focusedControl = this.shadowRoot.activeElement;
|
|
2943
|
-
const focusedKey = focusedControl?.dataset.field ?? focusedControl?.dataset.variable;
|
|
3677
|
+
const focusedKey = focusedControl?.dataset.field ?? focusedControl?.dataset.variable ?? focusedControl?.dataset.localVariable;
|
|
2944
3678
|
const variableKey = focusedControl?.dataset.variableKey;
|
|
2945
|
-
const
|
|
3679
|
+
const localKey = focusedControl?.dataset.localKey, localName = focusedControl?.dataset.localName;
|
|
3680
|
+
const focusedType = focusedControl?.dataset.field !== undefined ? 'field' : focusedControl?.dataset.variable !== undefined ? 'variable' : focusedControl?.dataset.localVariable !== undefined ? 'localVariable' : null;
|
|
2946
3681
|
const caret = focusedControl && 'selectionStart' in focusedControl ? focusedControl.selectionStart : null;
|
|
2947
3682
|
const restoreControlFocus = () => {
|
|
2948
3683
|
if (focusedKey === undefined || !focusedType)
|
|
2949
3684
|
return;
|
|
2950
3685
|
const controls = Array.from(this.shadowRoot.querySelectorAll('input,textarea,select,button'));
|
|
2951
|
-
const control = controls.find(item => item.dataset[focusedType] === focusedKey && (focusedType !== 'variable' || item.dataset.variableKey === variableKey))
|
|
3686
|
+
const control = controls.find(item => item.dataset[focusedType] === focusedKey && (focusedType !== 'variable' || item.dataset.variableKey === variableKey) && focusedType !== 'localVariable')
|
|
2952
3687
|
?? (focusedType === 'variable' ? controls.find(item => this.pendingVariableRename?.name === focusedKey && item.dataset.variable === this.pendingVariableRename.value && item.dataset.variableKey === variableKey) ?? controls.find(item => item.dataset.variableKey === 'add') : undefined);
|
|
3688
|
+
if (focusedType === 'localVariable') {
|
|
3689
|
+
if (focusedControl?.dataset.localAmbiguous === 'true') {
|
|
3690
|
+
focusedControl.blur();
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
const names = [localName];
|
|
3694
|
+
if (this.pendingLocalRename?.boxId === focusedKey && this.pendingLocalRename.name === localName && !this.pendingLocalRename.ambiguous)
|
|
3695
|
+
names.push(this.pendingLocalRename.value);
|
|
3696
|
+
let localControl = undefined;
|
|
3697
|
+
for (const name of names) {
|
|
3698
|
+
const matches = controls.filter(item => item.dataset.localVariable === focusedKey && item.dataset.localKey === localKey && item.dataset.localName === name);
|
|
3699
|
+
if (matches.length === 1) {
|
|
3700
|
+
localControl = matches[0];
|
|
3701
|
+
break;
|
|
3702
|
+
}
|
|
3703
|
+
if (matches.length > 1)
|
|
3704
|
+
break;
|
|
3705
|
+
}
|
|
3706
|
+
if (!localControl) {
|
|
3707
|
+
focusedControl?.blur();
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
localControl.focus();
|
|
3711
|
+
if (caret !== null && localControl instanceof HTMLInputElement)
|
|
3712
|
+
localControl.setSelectionRange(caret, caret);
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
2953
3715
|
if (!control)
|
|
2954
3716
|
return;
|
|
2955
3717
|
control.focus();
|
|
@@ -2957,7 +3719,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2957
3719
|
control.setSelectionRange(caret, caret);
|
|
2958
3720
|
};
|
|
2959
3721
|
this.shadowRoot.querySelectorAll('[part=minimap] [data-map-box]').forEach(rect => rect.dataset.selected = String(this.selectedIds.has(rect.dataset.mapBox)));
|
|
2960
|
-
this.shadowRoot.querySelectorAll('[part=line]').forEach(line => { const selected = line.dataset.lineId
|
|
3722
|
+
this.shadowRoot.querySelectorAll('[part=line]').forEach(line => { const selected = this.selectedLineIds.has(line.dataset.lineId); line.dataset.selected = String(selected); line.setAttribute('marker-end', `url(#${selected ? 'boe-process-arrow-brand' : 'boe-process-arrow'})`); });
|
|
2961
3723
|
this.shadowRoot.querySelectorAll("[data-box-id]").forEach((element) => {
|
|
2962
3724
|
element.setAttribute("aria-current", String(this.selectedIds.has(element.dataset.boxId)));
|
|
2963
3725
|
const box = this.projection.boxes.find((box) => box.id === element.dataset.boxId);
|
|
@@ -2967,9 +3729,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
2967
3729
|
});
|
|
2968
3730
|
const editor = this.shadowRoot.querySelector("[part=editor]");
|
|
2969
3731
|
const selected = this.selected;
|
|
2970
|
-
const controlsKey = `${[...this.selectedIds].join(',')}|${this.locked}|${this.disableConnections}|${this.showLastRunValue}|${selected?.runMetrics}|${JSON.stringify(selected ? this.fieldsValue[selected.id] ?? [] : [])}`;
|
|
3732
|
+
const controlsKey = `${[...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] ?? [] : [])}`;
|
|
2971
3733
|
this.shadowRoot.querySelector("[part=palette]").hidden = false;
|
|
2972
|
-
this.shadowRoot.querySelector('[part=selection-toolbar]').hidden = this.
|
|
3734
|
+
this.shadowRoot.querySelector('[part=selection-toolbar]').hidden = this.selection.length === 0;
|
|
2973
3735
|
this.renderSelectionToolbar();
|
|
2974
3736
|
this.renderPane();
|
|
2975
3737
|
this.positionSelectionToolbar();
|
|
@@ -2978,6 +3740,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
2978
3740
|
this.inspectedControls === controlsKey &&
|
|
2979
3741
|
editor.querySelector(`[part=inspector-heading]`)?.tagName === `H${this.headingLevel}`) {
|
|
2980
3742
|
editor.querySelector("[part=inspector-heading]").textContent = selected?.title ?? "";
|
|
3743
|
+
const locals = editor.querySelector('[part=local-variables]');
|
|
3744
|
+
if (selected && locals && locals.dataset.localEchoRevision !== String(this.localEchoRevision)) {
|
|
3745
|
+
// Reconcile controlled local drafts without disposing the host's custom
|
|
3746
|
+
// inspector or unrelated focused controls on unchanged document echoes.
|
|
3747
|
+
locals.replaceWith(this.renderLocalVariables(selected));
|
|
3748
|
+
}
|
|
2981
3749
|
restoreControlFocus();
|
|
2982
3750
|
return;
|
|
2983
3751
|
}
|
|
@@ -3031,6 +3799,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
3031
3799
|
}
|
|
3032
3800
|
for (const field of this.fieldsValue[selected.id] ?? [])
|
|
3033
3801
|
editor.append(this.renderField(selected, field));
|
|
3802
|
+
if (selected.localVariables !== undefined)
|
|
3803
|
+
editor.append(this.renderLocalVariables(selected));
|
|
3034
3804
|
this.cleanupInspector =
|
|
3035
3805
|
this.renderer?.(selected.node, editor) || undefined;
|
|
3036
3806
|
const outgoing = this.projection.lines.filter(line => line.from === selected.id);
|
|
@@ -3106,7 +3876,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3106
3876
|
duplicate.type = 'button';
|
|
3107
3877
|
duplicate.textContent = 'Duplicate';
|
|
3108
3878
|
duplicate.disabled = this.locked;
|
|
3109
|
-
duplicate.onclick = () => this.
|
|
3879
|
+
duplicate.onclick = () => this.duplicateBoxes([selected.id], 48);
|
|
3110
3880
|
const remove = document.createElement('button');
|
|
3111
3881
|
remove.type = 'button';
|
|
3112
3882
|
remove.textContent = 'Delete';
|
|
@@ -3117,14 +3887,118 @@ export class ProcessModeler extends BaseElement {
|
|
|
3117
3887
|
}
|
|
3118
3888
|
restoreControlFocus();
|
|
3119
3889
|
}
|
|
3890
|
+
renderLocalVariables(box) {
|
|
3891
|
+
const section = document.createElement('section');
|
|
3892
|
+
section.setAttribute('part', 'local-variables');
|
|
3893
|
+
section.dataset.localEchoRevision = String(this.localEchoRevision);
|
|
3894
|
+
const heading = document.createElement('h3');
|
|
3895
|
+
heading.textContent = box.frame ? 'Variables for the steps inside' : 'Variables for this step';
|
|
3896
|
+
const help = document.createElement('p');
|
|
3897
|
+
help.textContent = box.frame ? 'Every step inside can read these; they’re gone once the frame is done.' : 'Only this step’s inputs can read these; they’re gone once it’s done.';
|
|
3898
|
+
const collection = document.createElement('div');
|
|
3899
|
+
collection.setAttribute('part', 'local-variable-list');
|
|
3900
|
+
section.append(heading, help, collection);
|
|
3901
|
+
for (const [index, variable] of (box.localVariables ?? []).entries()) {
|
|
3902
|
+
const row = document.createElement('div');
|
|
3903
|
+
row.setAttribute('part', 'local-variable-row');
|
|
3904
|
+
row.setAttribute('role', 'group');
|
|
3905
|
+
row.setAttribute('aria-label', `Step variable ${index + 1}`);
|
|
3906
|
+
const identify = (control, key) => { control.dataset.localVariable = box.id; control.dataset.localIndex = String(index); control.dataset.localKey = key; control.dataset.localName = variable.name; control.dataset.localAmbiguous = String(box.localVariables?.filter(item => item.name === variable.name).length !== 1); };
|
|
3907
|
+
for (const [key, label, value] of [['rename', 'Name', variable.name], ['starting-value', 'Starts as', variable.startingValue ?? '']]) {
|
|
3908
|
+
const field = variableLabel(label);
|
|
3909
|
+
const input = document.createElement('input');
|
|
3910
|
+
input.type = 'text';
|
|
3911
|
+
input.placeholder = key === 'rename' ? 'name' : 'starts as';
|
|
3912
|
+
input.spellcheck = false;
|
|
3913
|
+
input.autocomplete = 'off';
|
|
3914
|
+
input.value = value;
|
|
3915
|
+
input.disabled = this.locked || !box.localVariablesEditable;
|
|
3916
|
+
identify(input, key);
|
|
3917
|
+
if (variable.problem) {
|
|
3918
|
+
input.setAttribute('aria-invalid', 'true');
|
|
3919
|
+
input.setAttribute('aria-describedby', `process-local-problem-${encodeURIComponent(box.id)}-${index}`);
|
|
3920
|
+
}
|
|
3921
|
+
input.oninput = () => {
|
|
3922
|
+
if (key === 'rename')
|
|
3923
|
+
this.pendingLocalRename = { boxId: box.id, name: variable.name, value: input.value, echoRevision: this.localEchoRevision, ambiguous: input.dataset.localAmbiguous === 'true' };
|
|
3924
|
+
this.requestLocalVariableEdit(box, { type: key, index, name: variable.name, value: input.value });
|
|
3925
|
+
};
|
|
3926
|
+
field.append(input);
|
|
3927
|
+
row.append(field);
|
|
3928
|
+
}
|
|
3929
|
+
if (box.localVariablesEditable) {
|
|
3930
|
+
const remove = document.createElement('button');
|
|
3931
|
+
remove.type = 'button';
|
|
3932
|
+
remove.append(variableGlyph());
|
|
3933
|
+
remove.setAttribute('aria-label', `Remove ${variable.name || 'this variable'}`);
|
|
3934
|
+
remove.disabled = this.locked;
|
|
3935
|
+
identify(remove, 'remove');
|
|
3936
|
+
remove.onclick = () => this.requestLocalVariableEdit(box, { type: 'remove', index, name: variable.name });
|
|
3937
|
+
row.append(remove);
|
|
3938
|
+
}
|
|
3939
|
+
if (variable.problem) {
|
|
3940
|
+
const problem = document.createElement('small');
|
|
3941
|
+
problem.setAttribute('part', 'field-problem');
|
|
3942
|
+
problem.id = `process-local-problem-${encodeURIComponent(box.id)}-${index}`;
|
|
3943
|
+
problem.textContent = variable.problem;
|
|
3944
|
+
row.append(problem);
|
|
3945
|
+
}
|
|
3946
|
+
collection.append(row);
|
|
3947
|
+
}
|
|
3948
|
+
if (box.localVariablesEditable) {
|
|
3949
|
+
const add = document.createElement('button');
|
|
3950
|
+
add.type = 'button';
|
|
3951
|
+
add.append(variableGlyph(true), document.createTextNode('Add a variable'));
|
|
3952
|
+
add.disabled = this.locked;
|
|
3953
|
+
add.dataset.localVariable = box.id;
|
|
3954
|
+
add.dataset.localKey = 'add';
|
|
3955
|
+
add.onclick = () => this.requestLocalVariableEdit(box, { type: 'add' });
|
|
3956
|
+
const actions = document.createElement('div');
|
|
3957
|
+
actions.setAttribute('part', 'local-variable-actions');
|
|
3958
|
+
actions.append(add);
|
|
3959
|
+
section.append(actions);
|
|
3960
|
+
}
|
|
3961
|
+
return section;
|
|
3962
|
+
}
|
|
3963
|
+
requestLocalVariableEdit(box, edit) {
|
|
3964
|
+
const owner = this.selected;
|
|
3965
|
+
if (this.locked || !owner?.localVariablesEditable || owner.id !== box.id)
|
|
3966
|
+
return;
|
|
3967
|
+
if (edit.type !== 'add' && owner.localVariables?.[edit.index]?.name !== edit.name)
|
|
3968
|
+
return;
|
|
3969
|
+
const request = { boxId: owner.id, ...(owner.path ? { path: [...owner.path] } : {}), edit };
|
|
3970
|
+
const session = this.layoutEditSession, beforeNames = (owner.localVariables ?? []).map(variable => variable.name), beforeCount = beforeNames.length;
|
|
3971
|
+
emit(this, 'process-local-variable-edit-request', request);
|
|
3972
|
+
if (session !== this.layoutEditSession || this.selected?.id !== box.id)
|
|
3973
|
+
return;
|
|
3974
|
+
const count = this.selected.localVariables?.length ?? 0;
|
|
3975
|
+
if (edit.type === 'add' && count > beforeCount) {
|
|
3976
|
+
const remaining = [...beforeNames];
|
|
3977
|
+
const added = (this.selected.localVariables ?? []).filter(variable => {
|
|
3978
|
+
const index = remaining.indexOf(variable.name);
|
|
3979
|
+
if (index < 0)
|
|
3980
|
+
return true;
|
|
3981
|
+
remaining.splice(index, 1);
|
|
3982
|
+
return false;
|
|
3983
|
+
});
|
|
3984
|
+
if (added.length === 1 && this.selected.localVariables?.filter(variable => variable.name === added[0].name).length === 1) {
|
|
3985
|
+
const name = Array.from(this.shadowRoot.querySelectorAll('[data-local-key=rename]')).find(input => input.dataset.localVariable === box.id && input.dataset.localName === added[0].name);
|
|
3986
|
+
name?.focus();
|
|
3987
|
+
name?.select();
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
else if (edit.type === 'remove' && count < beforeCount) {
|
|
3991
|
+
Array.from(this.shadowRoot.querySelectorAll('[data-local-key=add]')).find(button => button.dataset.localVariable === box.id)?.focus();
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3120
3994
|
renderSelectionToolbar() {
|
|
3121
3995
|
const toolbar = this.shadowRoot.querySelector('[part=selection-toolbar]');
|
|
3122
|
-
const key = `${
|
|
3996
|
+
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(',')}`;
|
|
3123
3997
|
if (key === this.selectionToolbarKey)
|
|
3124
3998
|
return;
|
|
3125
3999
|
this.selectionToolbarKey = key;
|
|
3126
4000
|
toolbar.replaceChildren();
|
|
3127
|
-
if (!this.
|
|
4001
|
+
if (!this.selection.length)
|
|
3128
4002
|
return;
|
|
3129
4003
|
const add = (label, command, action, withPlus = false) => {
|
|
3130
4004
|
const button = document.createElement('button');
|
|
@@ -3145,10 +4019,15 @@ export class ProcessModeler extends BaseElement {
|
|
|
3145
4019
|
button.append(text);
|
|
3146
4020
|
toolbar.append(button);
|
|
3147
4021
|
};
|
|
3148
|
-
if (this.
|
|
4022
|
+
if (this.graphSelection && (this.selectedNoteIds.size || this.selectedLineIds.size > 1 || this.selectedLineIds.size && this.selectedIds.size)) {
|
|
4023
|
+
if (this.selectedIds.size > 1)
|
|
4024
|
+
add('Line up', 'align', () => this.selectionCommand('align'));
|
|
4025
|
+
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
4026
|
+
}
|
|
4027
|
+
else if (this.selectedLine) {
|
|
3149
4028
|
const line = this.selectedLine;
|
|
3150
4029
|
add('Insert a step', 'insert', () => this.openLineChooser(line, toolbar.querySelector('[data-selection-command=insert]') ?? undefined), true);
|
|
3151
|
-
if (this.layoutValue.lines?.[line.id])
|
|
4030
|
+
if (this.layoutValue.lines?.[line.id] || line.fromSide || line.toSide || line.points?.length)
|
|
3152
4031
|
add('Reset line', 'reset-line', () => this.resetLine(line.id));
|
|
3153
4032
|
if (!this.disableConnections)
|
|
3154
4033
|
add('Delete', 'delete-line', () => this.requestEdit({ type: 'disconnect', lineId: line.id }));
|
|
@@ -3161,9 +4040,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
3161
4040
|
if (!['end', 'finish', 'section', 'note'].includes(box.kind))
|
|
3162
4041
|
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);
|
|
3163
4042
|
if (failurePath)
|
|
3164
|
-
add('If it fails', 'add-failure', () => this.openAnchoredChooser({ type: 'add', from: box.id, routeLabel: 'If it fails', dashed: true }, `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);
|
|
4043
|
+
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);
|
|
3165
4044
|
if (!['start', 'timer', 'section'].includes(box.kind))
|
|
3166
|
-
add('Duplicate', 'duplicate', () =>
|
|
4045
|
+
add('Duplicate', 'duplicate', () => this.duplicateBoxes([box.id], 48));
|
|
3167
4046
|
if (!['start', 'timer'].includes(box.kind))
|
|
3168
4047
|
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
3169
4048
|
}
|
|
@@ -3197,6 +4076,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
3197
4076
|
paintViewport() {
|
|
3198
4077
|
const world = this.shadowRoot.querySelector("[part=world]");
|
|
3199
4078
|
world.style.setProperty('--boe-process-inverse-zoom', String(1 / this.viewport.zoom));
|
|
4079
|
+
// A small screen-space margin keeps targets above 24px after fractional
|
|
4080
|
+
// layout rounding without assuming a browser's CSS-pixel quantum.
|
|
4081
|
+
world.style.setProperty('--boe-process-hit-target', `${24.1 / this.viewport.zoom}px`);
|
|
3200
4082
|
world.style.transform =
|
|
3201
4083
|
`translate(${this.viewport.x}px,${this.viewport.y}px) scale(${this.viewport.zoom})`;
|
|
3202
4084
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
@@ -3244,17 +4126,27 @@ export class ProcessModeler extends BaseElement {
|
|
|
3244
4126
|
minimap.append(viewport);
|
|
3245
4127
|
}
|
|
3246
4128
|
positionSelectionToolbar() {
|
|
3247
|
-
if (!this.isRendered ||
|
|
4129
|
+
if (!this.isRendered || !this.selection.length)
|
|
3248
4130
|
return;
|
|
3249
4131
|
const canvas = this.shadowRoot.querySelector('[part=canvas]');
|
|
3250
4132
|
const toolbar = this.shadowRoot.querySelector('[part=selection-toolbar]');
|
|
3251
4133
|
const positions = [...this.selectedIds].map(id => this.layoutValue.boxes[id]).filter(Boolean);
|
|
4134
|
+
positions.push(...(this.layoutValue.notes ?? []).filter(note => this.selectedNoteIds.has(note.id)).map(note => ({ ...note, width: 208, height: 80 })));
|
|
3252
4135
|
if (!positions.length && this.selectedLine) {
|
|
3253
4136
|
const point = lineMidpoint(this.routedLines.get(this.selectedLine.id) ?? []);
|
|
3254
4137
|
toolbar.style.left = `${Math.max(8, Math.min(point.x * this.viewport.zoom + this.viewport.x, Math.max(8, canvas.clientWidth - toolbar.offsetWidth - 8)))}px`;
|
|
3255
4138
|
toolbar.style.top = `${Math.max(8, point.y * this.viewport.zoom + this.viewport.y - 52)}px`;
|
|
3256
4139
|
return;
|
|
3257
4140
|
}
|
|
4141
|
+
if (!positions.length) {
|
|
4142
|
+
for (const id of this.selectedLineIds) {
|
|
4143
|
+
const points = this.routedLines.get(id) ?? [];
|
|
4144
|
+
if (!points.length)
|
|
4145
|
+
continue;
|
|
4146
|
+
const x = Math.min(...points.map(point => point.x)), y = Math.min(...points.map(point => point.y));
|
|
4147
|
+
positions.push({ x, y, width: Math.max(...points.map(point => point.x)) - x, height: Math.max(...points.map(point => point.y)) - y });
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
3258
4150
|
if (!positions.length)
|
|
3259
4151
|
return;
|
|
3260
4152
|
const left = Math.min(...positions.map(position => position.x * this.viewport.zoom + this.viewport.x));
|
|
@@ -3266,9 +4158,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
3266
4158
|
focusBox(id) {
|
|
3267
4159
|
Array.from(this.shadowRoot.querySelectorAll("[data-box-id]"))
|
|
3268
4160
|
.find((box) => box.dataset.boxId === id)
|
|
3269
|
-
?.focus();
|
|
4161
|
+
?.focus({ preventScroll: true });
|
|
3270
4162
|
}
|
|
3271
4163
|
setStatus(message, urgent = false) {
|
|
4164
|
+
if (!this.isRendered)
|
|
4165
|
+
return;
|
|
3272
4166
|
this.shadowRoot.querySelector(urgent ? "[part=status]" : "[part=urgent-status]").textContent = "";
|
|
3273
4167
|
this.shadowRoot.querySelector(urgent ? "[part=urgent-status]" : "[part=status]").textContent = message;
|
|
3274
4168
|
}
|