@unofficialbox/box-open-elements 0.31.1 → 0.32.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 +1 -0
- package/dist/patterns/process-modeler/design.d.ts +1 -1
- package/dist/patterns/process-modeler/design.js +15 -6
- package/dist/patterns/process-modeler/model.d.ts +42 -1
- package/dist/patterns/process-modeler/process-modeler.d.ts +24 -1
- package/dist/patterns/process-modeler/process-modeler.js +520 -118
- package/package.json +1 -1
|
@@ -74,7 +74,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
74
74
|
activeDrawer;
|
|
75
75
|
drawerReturn;
|
|
76
76
|
selectedIds = new Set();
|
|
77
|
-
|
|
77
|
+
clipboard = null;
|
|
78
|
+
clipboardSession = 0;
|
|
79
|
+
restoringBoxFocus = false;
|
|
78
80
|
selectionToolbarKey = "";
|
|
79
81
|
sectionSequence = 0;
|
|
80
82
|
versionValue = 0;
|
|
@@ -99,6 +101,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
99
101
|
spacePressed = false;
|
|
100
102
|
dropLine;
|
|
101
103
|
paletteDragKind = null;
|
|
104
|
+
palettePointer;
|
|
105
|
+
suppressPaletteClick = false;
|
|
106
|
+
escapePalettePointer = (event) => {
|
|
107
|
+
if (event.key !== 'Escape' || !this.palettePointer)
|
|
108
|
+
return;
|
|
109
|
+
this.cancelPalettePointer();
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
};
|
|
102
112
|
narrowValue = false;
|
|
103
113
|
suppressClick = false;
|
|
104
114
|
resizeObserver;
|
|
@@ -157,6 +167,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
157
167
|
};
|
|
158
168
|
}
|
|
159
169
|
load(document, options = {}) {
|
|
170
|
+
this.cancelPalettePointer();
|
|
160
171
|
const projection = this.model.project(document);
|
|
161
172
|
validateProjection(projection);
|
|
162
173
|
this.documentValue = document;
|
|
@@ -170,7 +181,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
170
181
|
this.selectedId = null;
|
|
171
182
|
this.selectedLineId = null;
|
|
172
183
|
this.selectedIds.clear();
|
|
173
|
-
this.
|
|
184
|
+
this.clearClipboard();
|
|
174
185
|
this.refresh();
|
|
175
186
|
if (options.selectedPath)
|
|
176
187
|
this.selectedPath = options.selectedPath;
|
|
@@ -259,6 +270,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
259
270
|
}
|
|
260
271
|
set locked(value) {
|
|
261
272
|
this.toggleAttribute("locked", value);
|
|
273
|
+
if (value)
|
|
274
|
+
this.cancelPalettePointer();
|
|
262
275
|
}
|
|
263
276
|
get snapToGrid() {
|
|
264
277
|
return this.hasAttribute("snap-to-grid");
|
|
@@ -281,6 +294,8 @@ export class ProcessModeler extends BaseElement {
|
|
|
281
294
|
}
|
|
282
295
|
disconnectedCallback() {
|
|
283
296
|
this.resizeObserver?.disconnect();
|
|
297
|
+
this.clearClipboard();
|
|
298
|
+
this.cancelPalettePointer();
|
|
284
299
|
this.closeDrawer();
|
|
285
300
|
const chooser = this.shadowRoot?.querySelector("[part=insert-chooser]");
|
|
286
301
|
if (chooser)
|
|
@@ -341,56 +356,85 @@ export class ProcessModeler extends BaseElement {
|
|
|
341
356
|
setValidationAtPath(message, path) {
|
|
342
357
|
this.setValidation(message ? [{ message, path }] : []);
|
|
343
358
|
}
|
|
359
|
+
connectionProblem(edit) {
|
|
360
|
+
if (!this.model.connectionProblem || this.documentValue === undefined || !['connect', 'reattach'].includes(edit.type))
|
|
361
|
+
return null;
|
|
362
|
+
if (!this.isRendered) {
|
|
363
|
+
this.projection = this.model.project(this.documentValue);
|
|
364
|
+
validateProjection(this.projection);
|
|
365
|
+
}
|
|
366
|
+
const line = edit.type === 'reattach' ? this.projection.lines.find(line => line.id === edit.lineId) : undefined;
|
|
367
|
+
const from = edit.from ?? line?.from;
|
|
368
|
+
const to = edit.to ?? line?.to;
|
|
369
|
+
if (!from || !to)
|
|
370
|
+
return null;
|
|
371
|
+
return this.model.connectionProblem(this.documentValue, { from, to, ...(line ? { ignoreLineId: line.id } : {}) }, this.projection) || null;
|
|
372
|
+
}
|
|
344
373
|
requestEdit(edit) {
|
|
345
374
|
if (this.locked || (this.disableConnections && (edit.type === "connect" || edit.type === "disconnect" || edit.type === 'reattach')))
|
|
346
375
|
return;
|
|
376
|
+
const problem = this.connectionProblem(edit);
|
|
377
|
+
if (problem) {
|
|
378
|
+
this.setStatus(problem, true);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const request = { ...edit, accept: this.editAcceptance(edit) };
|
|
382
|
+
emit(this, "process-edit-request", request);
|
|
383
|
+
}
|
|
384
|
+
editAcceptance(edit) {
|
|
347
385
|
const before = this.layout;
|
|
386
|
+
const beforeSelection = [...this.selectedIds];
|
|
348
387
|
const previousIds = new Set(this.projection.boxes.map((box) => box.id));
|
|
349
388
|
let accepted = false;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
this.emitPositions();
|
|
376
|
-
};
|
|
377
|
-
this.history.record({
|
|
378
|
-
undo: () => {
|
|
379
|
-
command.undo();
|
|
380
|
-
restore(before);
|
|
381
|
-
},
|
|
382
|
-
redo: () => {
|
|
383
|
-
command.redo();
|
|
384
|
-
restore(after);
|
|
385
|
-
},
|
|
386
|
-
});
|
|
389
|
+
return (command) => {
|
|
390
|
+
if (accepted)
|
|
391
|
+
return;
|
|
392
|
+
accepted = true;
|
|
393
|
+
if (command.layout)
|
|
394
|
+
this.layoutValue = structuredClone(command.layout);
|
|
395
|
+
if (edit.type === 'reset-line' && edit.lineId)
|
|
396
|
+
delete this.layoutValue.lines?.[edit.lineId];
|
|
397
|
+
this.refresh();
|
|
398
|
+
const inserted = edit.type === 'insert' && !edit.boxId && !command.layout
|
|
399
|
+
? this.projection.boxes.find(box => !previousIds.has(box.id)) : undefined;
|
|
400
|
+
const reflowed = inserted ? this.reflowStraightInsert(edit, inserted.id, before) : false;
|
|
401
|
+
if (!reflowed &&
|
|
402
|
+
!command.layout &&
|
|
403
|
+
edit.position &&
|
|
404
|
+
Number.isFinite(edit.position.x) &&
|
|
405
|
+
Number.isFinite(edit.position.y)) {
|
|
406
|
+
const added = this.projection.boxes.find(box => box.id === edit.boxId) ?? this.projection.boxes.find((box) => !previousIds.has(box.id));
|
|
407
|
+
if (added)
|
|
408
|
+
this.layoutValue = this.translatedLayout(added.id, edit.position.x, edit.position.y);
|
|
409
|
+
}
|
|
410
|
+
const after = this.layout;
|
|
411
|
+
const acceptedSelection = command.selectionIds ? [...command.selectionIds] : undefined;
|
|
412
|
+
const restore = (layout) => {
|
|
413
|
+
this.layoutValue = structuredClone(layout);
|
|
387
414
|
this.refresh();
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
415
|
+
this.emitPositions();
|
|
416
|
+
};
|
|
417
|
+
this.history.record({
|
|
418
|
+
undo: () => {
|
|
419
|
+
command.undo();
|
|
420
|
+
restore(before);
|
|
421
|
+
if (acceptedSelection)
|
|
422
|
+
this.selectMany(beforeSelection);
|
|
423
|
+
},
|
|
424
|
+
redo: () => {
|
|
425
|
+
command.redo();
|
|
426
|
+
restore(after);
|
|
427
|
+
if (acceptedSelection)
|
|
428
|
+
this.selectMany(acceptedSelection);
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
if (acceptedSelection)
|
|
432
|
+
this.selectMany(acceptedSelection);
|
|
433
|
+
this.refresh();
|
|
434
|
+
if (JSON.stringify(before) !== JSON.stringify(after))
|
|
435
|
+
this.emitPositions();
|
|
436
|
+
announce("Process updated", "polite", this.ownerDocument);
|
|
392
437
|
};
|
|
393
|
-
emit(this, "process-edit-request", request);
|
|
394
438
|
}
|
|
395
439
|
undo() {
|
|
396
440
|
if (this.locked)
|
|
@@ -541,6 +585,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
541
585
|
});
|
|
542
586
|
}
|
|
543
587
|
resetLine(id) {
|
|
588
|
+
const line = this.projection.lines.find(line => line.id === id);
|
|
589
|
+
if (this.locked || !line)
|
|
590
|
+
return;
|
|
591
|
+
if (line.fromSide || line.toSide || line.points?.length) {
|
|
592
|
+
this.requestEdit({ type: 'reset-line', lineId: id });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
544
595
|
if (!this.layoutValue.lines?.[id])
|
|
545
596
|
return;
|
|
546
597
|
const next = this.layout;
|
|
@@ -763,7 +814,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
763
814
|
palette.prepend(skip, paletteHeading);
|
|
764
815
|
const hint = document.createElement("p");
|
|
765
816
|
hint.setAttribute("part", "palette-hint");
|
|
766
|
-
hint.textContent = "Drag onto the canvas, a
|
|
817
|
+
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
818
|
palette.append(hint);
|
|
768
819
|
for (const [part, label] of [["palette", "Building blocks"], ["inspector", "Process details"]]) {
|
|
769
820
|
const pane = root.querySelector(`[part=${part}]`);
|
|
@@ -864,6 +915,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
864
915
|
controls.setAttribute("role", "toolbar");
|
|
865
916
|
controls.setAttribute("aria-label", "Canvas view controls");
|
|
866
917
|
for (const command of ["zoom-out", "reset", "zoom-in", "fit", "snap", "lock"]) {
|
|
918
|
+
if (command === 'snap') {
|
|
919
|
+
const divider = document.createElement('span');
|
|
920
|
+
divider.setAttribute('part', 'controls-divider');
|
|
921
|
+
divider.setAttribute('aria-hidden', 'true');
|
|
922
|
+
controls.append(divider);
|
|
923
|
+
}
|
|
867
924
|
const button = root.querySelector(`[data-command=${command}]`);
|
|
868
925
|
if (button)
|
|
869
926
|
controls.append(button);
|
|
@@ -896,6 +953,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
896
953
|
this.narrowValue = narrow;
|
|
897
954
|
this.toggleAttribute("data-narrow", narrow);
|
|
898
955
|
this.toggleAttribute('data-phone', entries[0].contentRect.width < 560);
|
|
956
|
+
this.toggleAttribute('data-hide-tidy', entries[0].contentRect.width <= 560);
|
|
899
957
|
this.renderSelectionToolbar();
|
|
900
958
|
this.positionSelectionToolbar();
|
|
901
959
|
if (!narrow)
|
|
@@ -1152,6 +1210,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
1152
1210
|
emit(this, 'process-variable-edit-request', edit);
|
|
1153
1211
|
}
|
|
1154
1212
|
}
|
|
1213
|
+
selectionDimensions(box) {
|
|
1214
|
+
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape;
|
|
1215
|
+
const compact = shape === 'gateway' || shape === 'event';
|
|
1216
|
+
const position = this.layoutValue.boxes[box.id];
|
|
1217
|
+
return { width: position.width ?? (compact ? 56 : 224), height: position.height ?? (compact ? 56 : 64) };
|
|
1218
|
+
}
|
|
1155
1219
|
selectionCommand(command) {
|
|
1156
1220
|
if (this.locked)
|
|
1157
1221
|
return;
|
|
@@ -1165,7 +1229,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
1165
1229
|
this.tidySelection();
|
|
1166
1230
|
return;
|
|
1167
1231
|
}
|
|
1168
|
-
this.
|
|
1232
|
+
const rectangles = boxes.map(box => ({ ...this.layoutValue.boxes[box.id], ...this.selectionDimensions(box) }));
|
|
1233
|
+
const width = Math.max(...rectangles.map(r => r.x + r.width)) - Math.min(...rectangles.map(r => r.x));
|
|
1234
|
+
const height = Math.max(...rectangles.map(r => r.y + r.height)) - Math.min(...rectangles.map(r => r.y));
|
|
1235
|
+
this.arrangeSelection(width >= height ? 'middle' : 'center');
|
|
1169
1236
|
}
|
|
1170
1237
|
tidySelection() {
|
|
1171
1238
|
if (this.locked || this.selectedBoxes.length < 2)
|
|
@@ -1234,20 +1301,62 @@ export class ProcessModeler extends BaseElement {
|
|
|
1234
1301
|
});
|
|
1235
1302
|
if (sorted.length < 2)
|
|
1236
1303
|
return;
|
|
1237
|
-
const width = (box) =>
|
|
1238
|
-
const height = (box) =>
|
|
1304
|
+
const width = (box) => this.selectionDimensions(box).width;
|
|
1305
|
+
const height = (box) => this.selectionDimensions(box).height;
|
|
1239
1306
|
const minX = Math.min(...sorted.map(box => next.boxes[box.id].x));
|
|
1240
1307
|
const maxX = Math.max(...sorted.map(box => next.boxes[box.id].x + width(box)));
|
|
1241
1308
|
const minY = Math.min(...sorted.map(box => next.boxes[box.id].y));
|
|
1242
1309
|
const maxY = Math.max(...sorted.map(box => next.boxes[box.id].y + height(box)));
|
|
1243
|
-
const
|
|
1244
|
-
const
|
|
1310
|
+
const snap = (value) => Math.round(value / 16) * 16;
|
|
1311
|
+
const targets = new Map(sorted.map(box => [box.id, { x: next.boxes[box.id].x, y: next.boxes[box.id].y }]));
|
|
1312
|
+
const vertical = action === 'distribute-vertical';
|
|
1313
|
+
if (action === 'distribute-horizontal' || vertical) {
|
|
1314
|
+
if (sorted.length < 3)
|
|
1315
|
+
return;
|
|
1316
|
+
const axis = vertical ? 'y' : 'x';
|
|
1317
|
+
const size = vertical ? height : width;
|
|
1318
|
+
const ordered = [...sorted].sort((a, b) => targets.get(a.id)[axis] - targets.get(b.id)[axis]);
|
|
1319
|
+
const first = targets.get(ordered[0].id)[axis];
|
|
1320
|
+
const last = targets.get(ordered.at(-1).id)[axis] + size(ordered.at(-1));
|
|
1321
|
+
const gap = (last - first - ordered.reduce((total, box) => total + size(box), 0)) / (ordered.length - 1);
|
|
1322
|
+
let position = first + size(ordered[0]) + gap;
|
|
1323
|
+
for (const box of ordered.slice(1, -1)) {
|
|
1324
|
+
targets.get(box.id)[axis] = position;
|
|
1325
|
+
position += size(box) + gap;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
for (const box of sorted) {
|
|
1330
|
+
const target = targets.get(box.id);
|
|
1331
|
+
if (action === 'left')
|
|
1332
|
+
target.x = minX;
|
|
1333
|
+
if (action === 'center')
|
|
1334
|
+
target.x = snap((minX + maxX) / 2) - width(box) / 2;
|
|
1335
|
+
if (action === 'right')
|
|
1336
|
+
target.x = maxX - width(box);
|
|
1337
|
+
if (action === 'top')
|
|
1338
|
+
target.y = minY;
|
|
1339
|
+
if (action === 'middle')
|
|
1340
|
+
target.y = snap((minY + maxY) / 2) - height(box) / 2;
|
|
1341
|
+
if (action === 'bottom')
|
|
1342
|
+
target.y = maxY - height(box);
|
|
1343
|
+
}
|
|
1344
|
+
if (action === 'middle' || action === 'center') {
|
|
1345
|
+
const axis = action === 'middle' ? 'x' : 'y';
|
|
1346
|
+
const size = action === 'middle' ? width : height;
|
|
1347
|
+
const ordered = [...sorted].sort((a, b) => targets.get(a.id)[axis] - targets.get(b.id)[axis]);
|
|
1348
|
+
for (let index = 1; index < ordered.length; index++) {
|
|
1349
|
+
const previous = ordered[index - 1], box = ordered[index];
|
|
1350
|
+
const minimum = targets.get(previous.id)[axis] + size(previous) + 48;
|
|
1351
|
+
const target = targets.get(box.id);
|
|
1352
|
+
if (target[axis] < minimum)
|
|
1353
|
+
target[axis] += snap(minimum - target[axis] + 8);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1245
1357
|
for (const box of sorted) {
|
|
1246
|
-
const position = next.boxes[box.id];
|
|
1247
|
-
const
|
|
1248
|
-
const x = action === 'left' ? minX : action === 'center' ? (minX + maxX - width(box)) / 2 : action === 'right' ? maxX - width(box) : action === 'distribute-horizontal' ? starts[0] + index * (starts.at(-1) - starts[0]) / (ordered.length - 1) : position.x;
|
|
1249
|
-
const y = action === 'top' ? minY : action === 'middle' ? (minY + maxY - height(box)) / 2 : action === 'bottom' ? maxY - height(box) : action === 'distribute-vertical' ? starts[0] + index * (starts.at(-1) - starts[0]) / (ordered.length - 1) : position.y;
|
|
1250
|
-
const dx = x - position.x, dy = y - position.y;
|
|
1358
|
+
const position = next.boxes[box.id], target = targets.get(box.id);
|
|
1359
|
+
const dx = target.x - position.x, dy = target.y - position.y;
|
|
1251
1360
|
const group = new Set([box.id]);
|
|
1252
1361
|
for (let changed = true; changed;) {
|
|
1253
1362
|
changed = false;
|
|
@@ -1275,6 +1384,16 @@ export class ProcessModeler extends BaseElement {
|
|
|
1275
1384
|
this.setSection({ id, title, x, y, width: right - x, height: bottom - y });
|
|
1276
1385
|
}
|
|
1277
1386
|
setupListeners() {
|
|
1387
|
+
this.addEventListener('pointermove', event => this.movePalettePointer(event));
|
|
1388
|
+
this.addEventListener('pointerup', event => this.finishPalettePointer(event));
|
|
1389
|
+
this.addEventListener('pointercancel', event => { if (event.pointerId === this.palettePointer?.id)
|
|
1390
|
+
this.cancelPalettePointer(); });
|
|
1391
|
+
this.addEventListener('lostpointercapture', event => { if (event.pointerId === this.palettePointer?.id)
|
|
1392
|
+
this.cancelPalettePointer(); });
|
|
1393
|
+
this.shadowRoot.addEventListener('keydown', event => { if (event.key === 'Escape' && this.palettePointer) {
|
|
1394
|
+
this.cancelPalettePointer();
|
|
1395
|
+
event.preventDefault();
|
|
1396
|
+
} }, true);
|
|
1278
1397
|
this.shadowRoot.addEventListener("click", (event) => {
|
|
1279
1398
|
const command = event.target.closest("button")?.dataset.command;
|
|
1280
1399
|
if (command === "zoom-in")
|
|
@@ -1404,6 +1523,92 @@ export class ProcessModeler extends BaseElement {
|
|
|
1404
1523
|
this.paintViewport();
|
|
1405
1524
|
});
|
|
1406
1525
|
}
|
|
1526
|
+
clearClipboard() {
|
|
1527
|
+
const previous = this.clipboard;
|
|
1528
|
+
this.clipboard = null;
|
|
1529
|
+
this.clipboardSession++;
|
|
1530
|
+
previous?.dispose?.();
|
|
1531
|
+
}
|
|
1532
|
+
copySelection() {
|
|
1533
|
+
this.clearClipboard();
|
|
1534
|
+
const session = this.clipboardSession;
|
|
1535
|
+
let active = true;
|
|
1536
|
+
let claimed = false;
|
|
1537
|
+
const request = {
|
|
1538
|
+
sourceIds: Object.freeze([...this.selectedIds]),
|
|
1539
|
+
capture: clipboard => {
|
|
1540
|
+
if (!active || claimed || session !== this.clipboardSession || !this.isConnected)
|
|
1541
|
+
return;
|
|
1542
|
+
claimed = true;
|
|
1543
|
+
if (!Number.isInteger(clipboard?.itemCount) || clipboard.itemCount < 1 || typeof clipboard.paste !== 'function' || (clipboard.dispose !== undefined && typeof clipboard.dispose !== 'function')) {
|
|
1544
|
+
this.setStatus('The host did not supply a valid clipboard');
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
this.clipboard = Object.freeze({ itemCount: clipboard.itemCount, paste: clipboard.paste.bind(clipboard), ...(clipboard.dispose ? { dispose: clipboard.dispose.bind(clipboard) } : {}) });
|
|
1548
|
+
this.setStatus(`${clipboard.itemCount} steps copied`);
|
|
1549
|
+
},
|
|
1550
|
+
refuse: message => { if (!active || claimed || session !== this.clipboardSession || !this.isConnected)
|
|
1551
|
+
return; claimed = true; this.setStatus(message ?? 'Copy is not supported by this host'); },
|
|
1552
|
+
};
|
|
1553
|
+
try {
|
|
1554
|
+
emit(this, 'process-copy-request', request);
|
|
1555
|
+
}
|
|
1556
|
+
finally {
|
|
1557
|
+
active = false;
|
|
1558
|
+
}
|
|
1559
|
+
if (!claimed && session === this.clipboardSession && this.isConnected)
|
|
1560
|
+
this.setStatus('Copy is not supported by this host');
|
|
1561
|
+
return claimed;
|
|
1562
|
+
}
|
|
1563
|
+
pasteClipboard() {
|
|
1564
|
+
const clipboard = this.clipboard;
|
|
1565
|
+
if (!clipboard || this.locked)
|
|
1566
|
+
return;
|
|
1567
|
+
const session = this.clipboardSession;
|
|
1568
|
+
const accept = this.editAcceptance({ type: 'duplicate' });
|
|
1569
|
+
let active = true;
|
|
1570
|
+
let settled = false;
|
|
1571
|
+
const request = {
|
|
1572
|
+
offset: Object.freeze({ x: 32, y: 32 }),
|
|
1573
|
+
accept: command => {
|
|
1574
|
+
if (!active || settled || session !== this.clipboardSession || this.locked)
|
|
1575
|
+
return;
|
|
1576
|
+
settled = true;
|
|
1577
|
+
accept(command);
|
|
1578
|
+
},
|
|
1579
|
+
refuse: message => { if (!active || settled || session !== this.clipboardSession || !this.isConnected)
|
|
1580
|
+
return; settled = true; this.setStatus(message ?? 'Paste was refused by this host'); },
|
|
1581
|
+
};
|
|
1582
|
+
try {
|
|
1583
|
+
clipboard.paste(request);
|
|
1584
|
+
}
|
|
1585
|
+
finally {
|
|
1586
|
+
active = false;
|
|
1587
|
+
}
|
|
1588
|
+
if (!settled && session === this.clipboardSession)
|
|
1589
|
+
this.setStatus('Paste was not accepted by this host');
|
|
1590
|
+
}
|
|
1591
|
+
duplicateBoxes(ids, distance) {
|
|
1592
|
+
const selected = new Set(ids);
|
|
1593
|
+
if (!selected.size || [...selected].some(id => !this.projection.boxes.some(box => box.id === id) || !this.layoutValue.boxes[id]))
|
|
1594
|
+
return;
|
|
1595
|
+
const sources = [...selected].filter(id => {
|
|
1596
|
+
let parent = this.projection.boxes.find(box => box.id === id)?.parentId;
|
|
1597
|
+
while (parent) {
|
|
1598
|
+
if (selected.has(parent))
|
|
1599
|
+
return false;
|
|
1600
|
+
parent = this.projection.boxes.find(box => box.id === parent)?.parentId;
|
|
1601
|
+
}
|
|
1602
|
+
return true;
|
|
1603
|
+
});
|
|
1604
|
+
if (sources.length === 1) {
|
|
1605
|
+
const position = this.layoutValue.boxes[sources[0]];
|
|
1606
|
+
this.requestEdit({ type: 'duplicate', sourceId: sources[0], position: { x: position.x + distance, y: position.y + distance } });
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
this.requestEdit({ type: 'duplicate', sourceIds: sources, offset: { x: distance, y: distance } });
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1407
1612
|
onKey(event) {
|
|
1408
1613
|
if (event.defaultPrevented)
|
|
1409
1614
|
return;
|
|
@@ -1432,20 +1637,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
1432
1637
|
return;
|
|
1433
1638
|
}
|
|
1434
1639
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c' && this.selectedIds.size) {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
this.setStatus(`${this.clipboardIds.length} steps copied`);
|
|
1640
|
+
if (this.copySelection())
|
|
1641
|
+
event.preventDefault();
|
|
1438
1642
|
return;
|
|
1439
1643
|
}
|
|
1440
|
-
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v' && this.
|
|
1644
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v' && this.clipboard && !this.locked) {
|
|
1441
1645
|
event.preventDefault();
|
|
1442
|
-
this.
|
|
1443
|
-
this.requestEdit({ type: 'duplicate', sourceId: id, position: { x: position.x + 32 * (index + 1), y: position.y + 32 * (index + 1) } }); });
|
|
1646
|
+
this.pasteClipboard();
|
|
1444
1647
|
return;
|
|
1445
1648
|
}
|
|
1446
1649
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'd' && this.selectedIds.size && !this.locked) {
|
|
1447
1650
|
event.preventDefault();
|
|
1448
|
-
[...this.selectedIds]
|
|
1651
|
+
this.duplicateBoxes([...this.selectedIds], 48);
|
|
1449
1652
|
return;
|
|
1450
1653
|
}
|
|
1451
1654
|
if (event.key === "Escape") {
|
|
@@ -1455,6 +1658,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1455
1658
|
if (this.selectedLineId)
|
|
1456
1659
|
this.selectLine(null);
|
|
1457
1660
|
this.portDrag = undefined;
|
|
1661
|
+
this.endDrag = undefined;
|
|
1458
1662
|
this.marquee = undefined;
|
|
1459
1663
|
this.segmentDrag = undefined;
|
|
1460
1664
|
this.drag = undefined;
|
|
@@ -1481,6 +1685,18 @@ export class ProcessModeler extends BaseElement {
|
|
|
1481
1685
|
this.openKeyboardChooser(box);
|
|
1482
1686
|
return;
|
|
1483
1687
|
}
|
|
1688
|
+
const direction = {
|
|
1689
|
+
ArrowLeft: [-1, 0],
|
|
1690
|
+
ArrowRight: [1, 0],
|
|
1691
|
+
ArrowUp: [0, -1],
|
|
1692
|
+
ArrowDown: [0, 1],
|
|
1693
|
+
}[event.key];
|
|
1694
|
+
const canInsertDirection = event.ctrlKey && event.altKey && box && !this.locked && this.catalog.length && !this.disableConnections;
|
|
1695
|
+
if (direction && !canInsertDirection && (event.altKey || !box || this.locked)) {
|
|
1696
|
+
event.preventDefault();
|
|
1697
|
+
this.navigateSelection(direction);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1484
1700
|
if (!box || this.locked)
|
|
1485
1701
|
return;
|
|
1486
1702
|
if (event.key === "Enter") {
|
|
@@ -1509,12 +1725,6 @@ export class ProcessModeler extends BaseElement {
|
|
|
1509
1725
|
event.preventDefault();
|
|
1510
1726
|
this.selectionCommand("delete-many");
|
|
1511
1727
|
}
|
|
1512
|
-
const direction = {
|
|
1513
|
-
ArrowLeft: [-1, 0],
|
|
1514
|
-
ArrowRight: [1, 0],
|
|
1515
|
-
ArrowUp: [0, -1],
|
|
1516
|
-
ArrowDown: [0, 1],
|
|
1517
|
-
}[event.key];
|
|
1518
1728
|
if (direction) {
|
|
1519
1729
|
event.preventDefault();
|
|
1520
1730
|
if (event.ctrlKey && event.altKey && this.catalog.length && !this.disableConnections) {
|
|
@@ -1527,31 +1737,52 @@ export class ProcessModeler extends BaseElement {
|
|
|
1527
1737
|
this.openAnchoredChooser(this.nextEdit(box, side, { x: pos.x + direction[0] * 320, y: pos.y + direction[1] * 170 }), this.nextChooserTitle(box, side), anchor, returnFocus);
|
|
1528
1738
|
return;
|
|
1529
1739
|
}
|
|
1530
|
-
if (event.altKey) {
|
|
1531
|
-
const origin = this.layoutValue.boxes[box.id];
|
|
1532
|
-
const center = { x: origin.x + (origin.width ?? 224) / 2, y: origin.y + (origin.height ?? 64) / 2 };
|
|
1533
|
-
const candidates = this.projection.boxes.filter(candidate => candidate.id !== box.id).map(candidate => {
|
|
1534
|
-
const position = this.layoutValue.boxes[candidate.id];
|
|
1535
|
-
const dx = position.x + (position.width ?? 224) / 2 - center.x;
|
|
1536
|
-
const dy = position.y + (position.height ?? 64) / 2 - center.y;
|
|
1537
|
-
const forward = dx * direction[0] + dy * direction[1];
|
|
1538
|
-
const sideways = Math.abs(dx * direction[1] - dy * direction[0]);
|
|
1539
|
-
return { candidate, forward, score: forward + sideways * 2 };
|
|
1540
|
-
}).filter(item => item.forward > 0).sort((a, b) => a.score - b.score || a.candidate.id.localeCompare(b.candidate.id));
|
|
1541
|
-
const next = candidates[0]?.candidate;
|
|
1542
|
-
if (next) {
|
|
1543
|
-
this.select(next.id);
|
|
1544
|
-
this.focusBox(next.id);
|
|
1545
|
-
this.setStatus(`${next.title} selected`);
|
|
1546
|
-
}
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
1740
|
const pos = this.layoutValue.boxes[box.id];
|
|
1550
1741
|
const distance = event.shiftKey ? 64 : 16;
|
|
1551
1742
|
this.move(box.id, pos.x + direction[0] * distance, pos.y + direction[1] * distance);
|
|
1552
1743
|
this.focusBox(box.id);
|
|
1553
1744
|
}
|
|
1554
1745
|
}
|
|
1746
|
+
navigateSelection(direction) {
|
|
1747
|
+
const canvas = this.shadowRoot.querySelector('[part=canvas]');
|
|
1748
|
+
const box = this.selected;
|
|
1749
|
+
const origin = box ? this.layoutValue.boxes[box.id] : undefined;
|
|
1750
|
+
const originSize = box ? this.selectionDimensions(box) : undefined;
|
|
1751
|
+
const center = origin && originSize
|
|
1752
|
+
? { x: origin.x + originSize.width / 2, y: origin.y + originSize.height / 2 }
|
|
1753
|
+
: { x: (canvas.clientWidth / 2 - this.viewport.x) / this.viewport.zoom, y: (canvas.clientHeight / 2 - this.viewport.y) / this.viewport.zoom };
|
|
1754
|
+
const candidates = this.projection.boxes.filter(candidate => candidate.id !== box?.id && candidate.kind !== 'section').map(candidate => {
|
|
1755
|
+
const position = this.layoutValue.boxes[candidate.id];
|
|
1756
|
+
const size = this.selectionDimensions(candidate);
|
|
1757
|
+
const dx = position.x + size.width / 2 - center.x;
|
|
1758
|
+
const dy = position.y + size.height / 2 - center.y;
|
|
1759
|
+
const forward = dx * direction[0] + dy * direction[1];
|
|
1760
|
+
const sideways = Math.abs(dx * direction[1] - dy * direction[0]);
|
|
1761
|
+
return { candidate, forward, score: forward + sideways * 2.5 };
|
|
1762
|
+
}).filter(item => item.forward > 4).sort((a, b) => a.score - b.score);
|
|
1763
|
+
const next = candidates[0]?.candidate;
|
|
1764
|
+
if (!next) {
|
|
1765
|
+
this.setStatus('Nothing further that way');
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
this.select(next.id);
|
|
1769
|
+
this.revealBox(next);
|
|
1770
|
+
this.focusBox(next.id);
|
|
1771
|
+
this.setStatus(`${next.title} selected`);
|
|
1772
|
+
}
|
|
1773
|
+
revealBox(box) {
|
|
1774
|
+
const canvas = this.shadowRoot.querySelector('[part=canvas]');
|
|
1775
|
+
if (!canvas.clientWidth || !canvas.clientHeight)
|
|
1776
|
+
return;
|
|
1777
|
+
const position = this.layoutValue.boxes[box.id];
|
|
1778
|
+
const size = this.selectionDimensions(box);
|
|
1779
|
+
const left = position.x * this.viewport.zoom + this.viewport.x;
|
|
1780
|
+
const top = position.y * this.viewport.zoom + this.viewport.y;
|
|
1781
|
+
if (left > 48 && top > 48 && left + size.width * this.viewport.zoom < canvas.clientWidth - 48 && top + size.height * this.viewport.zoom < canvas.clientHeight - 48)
|
|
1782
|
+
return;
|
|
1783
|
+
this.setView({ x: canvas.clientWidth / 2 - (position.x + size.width / 2) * this.viewport.zoom,
|
|
1784
|
+
y: canvas.clientHeight / 2 - (position.y + size.height / 2) * this.viewport.zoom, zoom: this.viewport.zoom });
|
|
1785
|
+
}
|
|
1555
1786
|
pointerDown(event) {
|
|
1556
1787
|
if (event.button !== 0)
|
|
1557
1788
|
return;
|
|
@@ -1655,10 +1886,12 @@ export class ProcessModeler extends BaseElement {
|
|
|
1655
1886
|
const start = this.portDrag.start;
|
|
1656
1887
|
ghost.setAttribute("points", `${start.x},${start.y} ${point.x},${start.y} ${point.x},${point.y}`);
|
|
1657
1888
|
const target = this.boxAt(point, this.portDrag.id);
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1889
|
+
const problem = target ? this.connectionProblem({ type: 'connect', from: this.portDrag.id, to: target.id }) : null;
|
|
1890
|
+
const pinned = target && !problem ? this.sideAt(target, point) : undefined;
|
|
1891
|
+
ghost.dataset.invalid = String(Boolean(problem));
|
|
1892
|
+
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)); });
|
|
1660
1893
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { port.dataset.hot = String(port.dataset.owner === target?.id && port.dataset.side === pinned); });
|
|
1661
|
-
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Connect to ${target.title}` : 'Drop here to add a connected step');
|
|
1894
|
+
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));
|
|
1662
1895
|
return;
|
|
1663
1896
|
}
|
|
1664
1897
|
if (this.endDrag) {
|
|
@@ -1674,11 +1907,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
1674
1907
|
if (fixed)
|
|
1675
1908
|
ghost.setAttribute('points', `${fixed.x},${fixed.y} ${point.x},${fixed.y} ${point.x},${point.y}`);
|
|
1676
1909
|
const target = this.boxAt(point);
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1910
|
+
const problem = target ? this.connectionProblem({ type: 'reattach', lineId: this.endDrag.lineId, [this.endDrag.end]: target.id }) : null;
|
|
1911
|
+
const pinned = target && !problem ? this.sideAt(target, point) : undefined;
|
|
1912
|
+
ghost.dataset.invalid = String(Boolean(problem) || !target);
|
|
1913
|
+
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)); });
|
|
1679
1914
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { port.dataset.hot = String(port.dataset.owner === target?.id && port.dataset.side === pinned); });
|
|
1680
|
-
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Attach to ${target.title}` : 'Drop on a step to reattach', !target);
|
|
1681
|
-
this.setStatus(target ? `Release to attach the connection to ${target.title}` : 'Move over a step to attach this connection');
|
|
1915
|
+
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);
|
|
1916
|
+
this.setStatus(problem ?? (target ? `Release to attach the connection to ${target.title}` : 'Move over a step to attach this connection'));
|
|
1682
1917
|
return;
|
|
1683
1918
|
}
|
|
1684
1919
|
if (this.segmentDrag) {
|
|
@@ -1777,7 +2012,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1777
2012
|
this.portDrag = undefined;
|
|
1778
2013
|
this.shadowRoot.querySelector('[part=connection-preview]')?.remove();
|
|
1779
2014
|
this.shadowRoot.querySelector('[part=connect-tooltip]')?.remove();
|
|
1780
|
-
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; });
|
|
2015
|
+
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; delete box.dataset.connectInvalid; });
|
|
1781
2016
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { delete port.dataset.hot; });
|
|
1782
2017
|
if (commit) {
|
|
1783
2018
|
if (Math.hypot(point.x - port.start.x, point.y - port.start.y) < 8) {
|
|
@@ -1809,7 +2044,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1809
2044
|
this.endDrag = undefined;
|
|
1810
2045
|
this.shadowRoot.querySelector('[part=connection-preview]')?.remove();
|
|
1811
2046
|
this.shadowRoot.querySelector('[part=connect-tooltip]')?.remove();
|
|
1812
|
-
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; });
|
|
2047
|
+
this.shadowRoot.querySelectorAll('[data-connect-target]').forEach(box => { delete box.dataset.connectTarget; delete box.dataset.connectInvalid; });
|
|
1813
2048
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { delete port.dataset.hot; });
|
|
1814
2049
|
if (commit) {
|
|
1815
2050
|
if (Math.hypot(point.x - drag.start.x, point.y - drag.start.y) < 8) {
|
|
@@ -1925,6 +2160,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
1925
2160
|
}
|
|
1926
2161
|
markDropLine(point, except) {
|
|
1927
2162
|
this.dropLine = point ? this.lineAt(point, except)?.id : undefined;
|
|
2163
|
+
this.paintDropLine();
|
|
2164
|
+
}
|
|
2165
|
+
paintDropLine() {
|
|
1928
2166
|
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'})`); });
|
|
1929
2167
|
}
|
|
1930
2168
|
alignedPoint(id, x, y) {
|
|
@@ -2141,8 +2379,15 @@ export class ProcessModeler extends BaseElement {
|
|
|
2141
2379
|
this.updateToolbar();
|
|
2142
2380
|
if (selectionBefore !== JSON.stringify({ ids: [...this.selectedIds], path: this.selectedPath }))
|
|
2143
2381
|
emit(this, "selection-changed", { box: this.selected, boxes: this.selectedBoxes, path: this.selectedPath });
|
|
2144
|
-
if (focused)
|
|
2145
|
-
this.
|
|
2382
|
+
if (focused) {
|
|
2383
|
+
this.restoringBoxFocus = true;
|
|
2384
|
+
try {
|
|
2385
|
+
this.focusBox(focused);
|
|
2386
|
+
}
|
|
2387
|
+
finally {
|
|
2388
|
+
this.restoringBoxFocus = false;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2146
2391
|
}
|
|
2147
2392
|
get allChecks() { return this.computedChecks; }
|
|
2148
2393
|
collectChecks() {
|
|
@@ -2171,6 +2416,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
2171
2416
|
for (const box of [...this.projection.boxes].sort((a, b) => Number(Boolean(b.frame)) - Number(Boolean(a.frame)))) {
|
|
2172
2417
|
const element = document.createElement("div");
|
|
2173
2418
|
element.tabIndex = 0;
|
|
2419
|
+
element.addEventListener('focus', () => {
|
|
2420
|
+
if (!this.restoringBoxFocus && !this.pointers.size && !this.portDrag && !this.endDrag && !this.segmentDrag && !this.marquee && !this.frameResize) {
|
|
2421
|
+
this.select(box.id);
|
|
2422
|
+
if (box.kind !== 'section')
|
|
2423
|
+
this.revealBox(box);
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2174
2426
|
element.setAttribute("role", "group");
|
|
2175
2427
|
element.setAttribute("part", box.frame ? "frame" : "box");
|
|
2176
2428
|
element.dataset.boxId = box.id;
|
|
@@ -2305,13 +2557,10 @@ export class ProcessModeler extends BaseElement {
|
|
|
2305
2557
|
element.addEventListener("keydown", event => {
|
|
2306
2558
|
if (event.target !== element)
|
|
2307
2559
|
return;
|
|
2308
|
-
|
|
2309
|
-
event.preventDefault();
|
|
2310
|
-
element.click();
|
|
2311
|
-
}
|
|
2560
|
+
// Space reaches the canvas handler so holding it pans from a focused step.
|
|
2312
2561
|
// Let Enter reach the canvas handler after selecting the focused step.
|
|
2313
2562
|
// Cancelling it here prevents the editor from opening and taking focus.
|
|
2314
|
-
|
|
2563
|
+
if (event.key === "Enter") {
|
|
2315
2564
|
// Connection completion consumes Enter; it must not also edit the target.
|
|
2316
2565
|
if (this.connecting || this.pendingReattach)
|
|
2317
2566
|
event.preventDefault();
|
|
@@ -2346,6 +2595,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2346
2595
|
for (const side of ["north", "east", "south", "west"]) {
|
|
2347
2596
|
const port = document.createElement("button");
|
|
2348
2597
|
port.type = "button";
|
|
2598
|
+
port.tabIndex = -1;
|
|
2349
2599
|
port.setAttribute("part", "port");
|
|
2350
2600
|
port.dataset.side = side;
|
|
2351
2601
|
port.dataset.owner = box.id;
|
|
@@ -2551,6 +2801,145 @@ export class ProcessModeler extends BaseElement {
|
|
|
2551
2801
|
height: `${position.height ?? 64}px`,
|
|
2552
2802
|
});
|
|
2553
2803
|
}
|
|
2804
|
+
activatePaletteKind(kind) {
|
|
2805
|
+
if (this.locked)
|
|
2806
|
+
return;
|
|
2807
|
+
if (this.narrowValue) {
|
|
2808
|
+
this.closeDrawer();
|
|
2809
|
+
this.shadowRoot.querySelector('[part=canvas]').focus({ preventScroll: true });
|
|
2810
|
+
}
|
|
2811
|
+
if (kind.placement) {
|
|
2812
|
+
this.addLayoutKind(kind);
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
const selected = this.selectedIds.size === 1 ? this.selected : null;
|
|
2816
|
+
const from = selected && !selected.frame && selected.kind !== 'section' && !['end', 'finish'].includes(selected.kind) ? selected.id : undefined;
|
|
2817
|
+
const outgoing = from ? this.projection.lines.filter(line => line.from === from) : [];
|
|
2818
|
+
const line = outgoing.length === 1 ? outgoing[0] : undefined;
|
|
2819
|
+
this.requestEdit(line ? { type: 'insert', kind, lineId: line.id, from: line.from, to: line.to } : { type: 'add', kind, ...(from ? { from } : {}) });
|
|
2820
|
+
}
|
|
2821
|
+
paletteLineAt(point) {
|
|
2822
|
+
let nearest;
|
|
2823
|
+
let best = 20 / this.viewport.zoom;
|
|
2824
|
+
for (const line of this.projection.lines) {
|
|
2825
|
+
const points = this.routedLines.get(line.id) ?? [];
|
|
2826
|
+
for (let i = 1; i < points.length; i++) {
|
|
2827
|
+
const a = points[i - 1], b = points[i], dx = b.x - a.x, dy = b.y - a.y, length = dx * dx + dy * dy;
|
|
2828
|
+
const t = length ? Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / length)) : 0;
|
|
2829
|
+
const distance = Math.hypot(point.x - a.x - t * dx, point.y - a.y - t * dy);
|
|
2830
|
+
if (distance < best) {
|
|
2831
|
+
best = distance;
|
|
2832
|
+
nearest = line;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return nearest;
|
|
2837
|
+
}
|
|
2838
|
+
paletteFrameAt(point) {
|
|
2839
|
+
let nearest;
|
|
2840
|
+
let depth = -1;
|
|
2841
|
+
for (const box of this.projection.boxes) {
|
|
2842
|
+
if (!box.frame)
|
|
2843
|
+
continue;
|
|
2844
|
+
const p = this.layoutValue.boxes[box.id];
|
|
2845
|
+
if (!p)
|
|
2846
|
+
continue;
|
|
2847
|
+
const { width, height } = this.selectionDimensions(box);
|
|
2848
|
+
if (point.x < p.x || point.x > p.x + width || point.y < p.y || point.y > p.y + height)
|
|
2849
|
+
continue;
|
|
2850
|
+
let level = 0, parent = box.parentId;
|
|
2851
|
+
while (parent) {
|
|
2852
|
+
level++;
|
|
2853
|
+
parent = this.projection.boxes.find(box => box.id === parent)?.parentId;
|
|
2854
|
+
}
|
|
2855
|
+
if (level > depth) {
|
|
2856
|
+
depth = level;
|
|
2857
|
+
nearest = box;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
return nearest;
|
|
2861
|
+
}
|
|
2862
|
+
markPaletteDrop(point) {
|
|
2863
|
+
this.dropLine = point ? this.paletteLineAt(point)?.id : undefined;
|
|
2864
|
+
const frame = point && !this.dropLine ? this.paletteFrameAt(point) : undefined;
|
|
2865
|
+
this.shadowRoot.querySelectorAll('[part=frame]').forEach(element => { element.dataset.paletteDrop = String(element.dataset.boxId === frame?.id); });
|
|
2866
|
+
this.paintDropLine();
|
|
2867
|
+
}
|
|
2868
|
+
paletteCanvasPoint(event) {
|
|
2869
|
+
const rect = this.shadowRoot.querySelector('[part=canvas]').getBoundingClientRect();
|
|
2870
|
+
if (event.clientX <= rect.left || event.clientX >= rect.right || event.clientY <= rect.top || event.clientY >= rect.bottom)
|
|
2871
|
+
return;
|
|
2872
|
+
return this.canvasPoint(event);
|
|
2873
|
+
}
|
|
2874
|
+
movePalettePointer(event) {
|
|
2875
|
+
const drag = this.palettePointer;
|
|
2876
|
+
if (!drag || drag.id !== event.pointerId)
|
|
2877
|
+
return;
|
|
2878
|
+
if (this.locked) {
|
|
2879
|
+
this.cancelPalettePointer();
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
if (!drag.moved && Math.hypot(event.clientX - drag.x, event.clientY - drag.y) < 5)
|
|
2883
|
+
return;
|
|
2884
|
+
if (!drag.moved) {
|
|
2885
|
+
drag.moved = true;
|
|
2886
|
+
if (this.narrowValue)
|
|
2887
|
+
this.closeDrawer();
|
|
2888
|
+
}
|
|
2889
|
+
let ghost = this.shadowRoot.querySelector('[part=palette-ghost]');
|
|
2890
|
+
if (!ghost) {
|
|
2891
|
+
ghost = document.createElement('div');
|
|
2892
|
+
ghost.setAttribute('part', 'palette-ghost');
|
|
2893
|
+
ghost.dataset.pointer = 'true';
|
|
2894
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
2895
|
+
if (drag.kind.icon)
|
|
2896
|
+
ghost.append(drag.kind.icon());
|
|
2897
|
+
ghost.append(document.createTextNode(drag.kind.label));
|
|
2898
|
+
this.shadowRoot.append(ghost);
|
|
2899
|
+
}
|
|
2900
|
+
ghost.style.left = `${event.clientX}px`;
|
|
2901
|
+
ghost.style.top = `${event.clientY}px`;
|
|
2902
|
+
this.markPaletteDrop(!drag.kind.placement && drag.kind.kind !== 'end' ? this.paletteCanvasPoint(event) : undefined);
|
|
2903
|
+
event.preventDefault();
|
|
2904
|
+
}
|
|
2905
|
+
cancelPalettePointer() {
|
|
2906
|
+
const drag = this.palettePointer;
|
|
2907
|
+
this.palettePointer = undefined;
|
|
2908
|
+
this.ownerDocument.removeEventListener('keydown', this.escapePalettePointer, true);
|
|
2909
|
+
if (!drag)
|
|
2910
|
+
return;
|
|
2911
|
+
this.suppressPaletteClick = true;
|
|
2912
|
+
this.shadowRoot?.querySelector('[part=palette-ghost]')?.remove();
|
|
2913
|
+
this.markPaletteDrop(undefined);
|
|
2914
|
+
try {
|
|
2915
|
+
this.releasePointerCapture(drag.id);
|
|
2916
|
+
}
|
|
2917
|
+
catch { /* Capture may already have been lost. */ }
|
|
2918
|
+
}
|
|
2919
|
+
finishPalettePointer(event) {
|
|
2920
|
+
const drag = this.palettePointer;
|
|
2921
|
+
if (!drag || drag.id !== event.pointerId)
|
|
2922
|
+
return;
|
|
2923
|
+
const point = this.paletteCanvasPoint(event);
|
|
2924
|
+
this.cancelPalettePointer();
|
|
2925
|
+
if (this.locked)
|
|
2926
|
+
return;
|
|
2927
|
+
if (!drag.moved) {
|
|
2928
|
+
this.activatePaletteKind(drag.kind);
|
|
2929
|
+
if (this.narrowValue)
|
|
2930
|
+
this.closeDrawer();
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
if (!point)
|
|
2934
|
+
return;
|
|
2935
|
+
if (drag.kind.placement) {
|
|
2936
|
+
this.addLayoutKind(drag.kind, point);
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
const line = drag.kind.kind === 'end' ? undefined : this.paletteLineAt(point);
|
|
2940
|
+
const frame = line ? undefined : this.paletteFrameAt(point);
|
|
2941
|
+
this.requestEdit({ type: line ? 'insert' : 'add', kind: drag.kind, lineId: line?.id, from: line?.from, to: line?.to, parentId: frame?.id, position: point });
|
|
2942
|
+
}
|
|
2554
2943
|
renderPalette() {
|
|
2555
2944
|
const choices = this.shadowRoot.querySelector("[part=choices]");
|
|
2556
2945
|
const query = this.shadowRoot.querySelector("[part=search]").value.toLowerCase();
|
|
@@ -2583,19 +2972,30 @@ export class ProcessModeler extends BaseElement {
|
|
|
2583
2972
|
description.textContent = kind.description;
|
|
2584
2973
|
button.append(icon, name, description);
|
|
2585
2974
|
button.disabled = this.locked;
|
|
2586
|
-
button.draggable =
|
|
2587
|
-
button.addEventListener(
|
|
2588
|
-
if (
|
|
2589
|
-
this.addLayoutKind(kind);
|
|
2975
|
+
button.draggable = false;
|
|
2976
|
+
button.addEventListener('pointerdown', event => {
|
|
2977
|
+
if (this.locked || event.button !== 0 || event.isPrimary === false)
|
|
2590
2978
|
return;
|
|
2979
|
+
this.cancelPalettePointer();
|
|
2980
|
+
this.suppressPaletteClick = false;
|
|
2981
|
+
this.palettePointer = { kind, id: event.pointerId, x: event.clientX, y: event.clientY, moved: false };
|
|
2982
|
+
this.ownerDocument.addEventListener('keydown', this.escapePalettePointer, true);
|
|
2983
|
+
try {
|
|
2984
|
+
this.setPointerCapture(event.pointerId);
|
|
2591
2985
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
this.requestEdit(line ? { type: "insert", kind, lineId: line.id, from: line.from, to: line.to } : { type: "add", kind, ...(from ? { from } : {}) });
|
|
2986
|
+
catch { /* Detached synthetic pointers cannot be captured. */ }
|
|
2987
|
+
event.preventDefault();
|
|
2988
|
+
event.stopPropagation();
|
|
2596
2989
|
});
|
|
2597
|
-
button.addEventListener(
|
|
2598
|
-
|
|
2990
|
+
button.addEventListener('click', event => {
|
|
2991
|
+
if (event.detail > 0 && this.suppressPaletteClick) {
|
|
2992
|
+
this.suppressPaletteClick = false;
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
this.activatePaletteKind(kind);
|
|
2996
|
+
});
|
|
2997
|
+
button.addEventListener('dragstart', event => { this.paletteDragKind = kind; event.dataTransfer?.setData('application/boe-process-kind', kind.kind); });
|
|
2998
|
+
button.addEventListener('dragend', () => { this.paletteDragKind = null; this.shadowRoot?.querySelector('[part=palette-ghost]')?.remove(); this.markDropLine(undefined); });
|
|
2599
2999
|
choices.append(button);
|
|
2600
3000
|
}
|
|
2601
3001
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
@@ -3058,7 +3458,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3058
3458
|
duplicate.type = 'button';
|
|
3059
3459
|
duplicate.textContent = 'Duplicate';
|
|
3060
3460
|
duplicate.disabled = this.locked;
|
|
3061
|
-
duplicate.onclick = () => this.
|
|
3461
|
+
duplicate.onclick = () => this.duplicateBoxes([selected.id], 48);
|
|
3062
3462
|
const remove = document.createElement('button');
|
|
3063
3463
|
remove.type = 'button';
|
|
3064
3464
|
remove.textContent = 'Delete';
|
|
@@ -3071,7 +3471,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3071
3471
|
}
|
|
3072
3472
|
renderSelectionToolbar() {
|
|
3073
3473
|
const toolbar = this.shadowRoot.querySelector('[part=selection-toolbar]');
|
|
3074
|
-
const key = `${[...this.selectedIds].join(',')}|${this.selected?.kind}|${this.selected?.shape}|${this.selectedLineId}|${Boolean(this.
|
|
3474
|
+
const key = `${[...this.selectedIds].join(',')}|${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(',')}`;
|
|
3075
3475
|
if (key === this.selectionToolbarKey)
|
|
3076
3476
|
return;
|
|
3077
3477
|
this.selectionToolbarKey = key;
|
|
@@ -3100,7 +3500,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3100
3500
|
if (this.selectedLine) {
|
|
3101
3501
|
const line = this.selectedLine;
|
|
3102
3502
|
add('Insert a step', 'insert', () => this.openLineChooser(line, toolbar.querySelector('[data-selection-command=insert]') ?? undefined), true);
|
|
3103
|
-
if (this.layoutValue.lines?.[line.id])
|
|
3503
|
+
if (this.layoutValue.lines?.[line.id] || line.fromSide || line.toSide || line.points?.length)
|
|
3104
3504
|
add('Reset line', 'reset-line', () => this.resetLine(line.id));
|
|
3105
3505
|
if (!this.disableConnections)
|
|
3106
3506
|
add('Delete', 'delete-line', () => this.requestEdit({ type: 'disconnect', lineId: line.id }));
|
|
@@ -3115,7 +3515,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
3115
3515
|
if (failurePath)
|
|
3116
3516
|
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);
|
|
3117
3517
|
if (!['start', 'timer', 'section'].includes(box.kind))
|
|
3118
|
-
add('Duplicate', 'duplicate', () =>
|
|
3518
|
+
add('Duplicate', 'duplicate', () => this.duplicateBoxes([box.id], 48));
|
|
3119
3519
|
if (!['start', 'timer'].includes(box.kind))
|
|
3120
3520
|
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
3121
3521
|
}
|
|
@@ -3218,9 +3618,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
3218
3618
|
focusBox(id) {
|
|
3219
3619
|
Array.from(this.shadowRoot.querySelectorAll("[data-box-id]"))
|
|
3220
3620
|
.find((box) => box.dataset.boxId === id)
|
|
3221
|
-
?.focus();
|
|
3621
|
+
?.focus({ preventScroll: true });
|
|
3222
3622
|
}
|
|
3223
3623
|
setStatus(message, urgent = false) {
|
|
3624
|
+
if (!this.isRendered)
|
|
3625
|
+
return;
|
|
3224
3626
|
this.shadowRoot.querySelector(urgent ? "[part=status]" : "[part=urgent-status]").textContent = "";
|
|
3225
3627
|
this.shadowRoot.querySelector(urgent ? "[part=urgent-status]" : "[part=status]").textContent = message;
|
|
3226
3628
|
}
|