@nyaruka/temba-components 0.142.1 → 0.142.3
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/CHANGELOG.md +19 -0
- package/dist/temba-components.js +953 -708
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +1 -0
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +38 -38
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +171 -17
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +491 -22
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +346 -10
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/NodeTypeSelector.js +2 -0
- package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +92 -28
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +63 -3
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js +2 -6
- package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
- package/out-tsc/src/flow/actions/enter_flow.js +2 -2
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +2 -1
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/send_broadcast.js +2 -6
- package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
- package/out-tsc/src/flow/actions/send_email.js +2 -6
- package/out-tsc/src/flow/actions/send_email.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +55 -35
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_field.js +4 -5
- package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_language.js +3 -3
- package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +3 -3
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/actions/start_session.js +2 -2
- package/out-tsc/src/flow/actions/start_session.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm.js +4 -5
- package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_resthook.js +3 -8
- package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_subflow.js +4 -2
- package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_webhook.js +25 -33
- package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +1 -0
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +66 -0
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +17 -2
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +104 -43
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +6 -2
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/test/temba-canvas-menu.test.js +13 -9
- package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
- package/out-tsc/test/temba-flow-reflow.test.js.map +1 -1
- package/out-tsc/test/temba-node-editor.test.js +9 -10
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +3 -3
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +2 -2
- package/out-tsc/test/temba-simulator.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
- package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
- package/screenshots/truth/canvas-menu/open.png +0 -0
- package/screenshots/truth/node-type-selector/action-mode.png +0 -0
- package/screenshots/truth/node-type-selector/split-mode.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/src/Icons.ts +1 -0
- package/src/flow/CanvasMenu.ts +50 -43
- package/src/flow/CanvasNode.ts +201 -17
- package/src/flow/Editor.ts +585 -25
- package/src/flow/NodeEditor.ts +373 -10
- package/src/flow/NodeTypeSelector.ts +2 -0
- package/src/flow/Plumber.ts +104 -37
- package/src/flow/StickyNote.ts +76 -4
- package/src/flow/actions/add_contact_urn.ts +5 -6
- package/src/flow/actions/enter_flow.ts +2 -2
- package/src/flow/actions/say_msg.ts +2 -1
- package/src/flow/actions/send_broadcast.ts +2 -6
- package/src/flow/actions/send_email.ts +2 -6
- package/src/flow/actions/send_msg.ts +59 -38
- package/src/flow/actions/set_contact_channel.ts +5 -1
- package/src/flow/actions/set_contact_field.ts +10 -5
- package/src/flow/actions/set_contact_language.ts +6 -3
- package/src/flow/actions/set_contact_name.ts +5 -1
- package/src/flow/actions/set_contact_status.ts +5 -1
- package/src/flow/actions/set_run_result.ts +6 -3
- package/src/flow/actions/start_session.ts +2 -2
- package/src/flow/nodes/split_by_llm.ts +5 -5
- package/src/flow/nodes/split_by_resthook.ts +3 -8
- package/src/flow/nodes/split_by_subflow.ts +4 -2
- package/src/flow/nodes/split_by_webhook.ts +26 -34
- package/src/flow/nodes/wait_for_response.ts +1 -0
- package/src/flow/types.ts +25 -2
- package/src/flow/utils.ts +79 -1
- package/src/form/FieldRenderer.ts +32 -3
- package/src/interfaces.ts +1 -0
- package/src/list/SortableList.ts +117 -47
- package/src/simulator/Simulator.ts +6 -2
- package/test/temba-canvas-menu.test.ts +13 -9
- package/test/temba-flow-reflow.test.ts +4 -2
- package/test/temba-node-editor.test.ts +9 -10
- package/test/temba-node-type-selector.test.ts +3 -3
- package/test/temba-simulator.test.ts +2 -2
package/src/flow/Editor.ts
CHANGED
|
@@ -214,7 +214,7 @@ export class Editor extends RapidElement {
|
|
|
214
214
|
private autoScrollAnimationId: number | null = null;
|
|
215
215
|
private autoScrollDeltaX = 0;
|
|
216
216
|
private autoScrollDeltaY = 0;
|
|
217
|
-
private
|
|
217
|
+
private lastPointerPos: { clientX: number; clientY: number } | null = null;
|
|
218
218
|
|
|
219
219
|
// Selection state
|
|
220
220
|
@state()
|
|
@@ -226,6 +226,16 @@ export class Editor extends RapidElement {
|
|
|
226
226
|
@state()
|
|
227
227
|
private selectionBox: SelectionBox | null = null;
|
|
228
228
|
|
|
229
|
+
// Touch device state
|
|
230
|
+
private isTouchDevice = false;
|
|
231
|
+
private isTwoFingerPanning = false;
|
|
232
|
+
private twoFingerDidPan = false;
|
|
233
|
+
private twoFingerStartMidX = 0;
|
|
234
|
+
private twoFingerStartMidY = 0;
|
|
235
|
+
private twoFingerOnCanvas = false;
|
|
236
|
+
private lastPanX = 0;
|
|
237
|
+
private lastPanY = 0;
|
|
238
|
+
|
|
229
239
|
@state()
|
|
230
240
|
private targetId: string | null = null;
|
|
231
241
|
|
|
@@ -410,6 +420,10 @@ export class Editor extends RapidElement {
|
|
|
410
420
|
private boundKeyDown = this.handleKeyDown.bind(this);
|
|
411
421
|
private boundCanvasContextMenu = this.handleCanvasContextMenu.bind(this);
|
|
412
422
|
private boundWheel = this.handleWheel.bind(this);
|
|
423
|
+
private boundTouchMove = this.handleTouchMove.bind(this);
|
|
424
|
+
private boundTouchEnd = this.handleTouchEnd.bind(this);
|
|
425
|
+
private boundTouchCancel = this.handleTouchCancel.bind(this);
|
|
426
|
+
private boundCanvasTouchStart = this.handleCanvasTouchStart.bind(this);
|
|
413
427
|
|
|
414
428
|
static get styles() {
|
|
415
429
|
return css`
|
|
@@ -426,6 +440,29 @@ export class Editor extends RapidElement {
|
|
|
426
440
|
-webkit-font-smoothing: antialiased;
|
|
427
441
|
}
|
|
428
442
|
|
|
443
|
+
/* On touch devices, disable native scroll-by-touch so canvas
|
|
444
|
+
drag draws a selection rectangle. Users scroll via scrollbars. */
|
|
445
|
+
#editor.touch-device {
|
|
446
|
+
touch-action: none;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#editor.touch-device::-webkit-scrollbar {
|
|
450
|
+
-webkit-appearance: none;
|
|
451
|
+
width: 12px;
|
|
452
|
+
height: 12px;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#editor.touch-device::-webkit-scrollbar-thumb {
|
|
456
|
+
background: rgba(0, 0, 0, 0.3);
|
|
457
|
+
border-radius: 6px;
|
|
458
|
+
border: 2px solid transparent;
|
|
459
|
+
background-clip: padding-box;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#editor.touch-device::-webkit-scrollbar-track {
|
|
463
|
+
background: rgba(0, 0, 0, 0.05);
|
|
464
|
+
}
|
|
465
|
+
|
|
429
466
|
temba-floating-tab {
|
|
430
467
|
--floating-tab-right: 15px;
|
|
431
468
|
}
|
|
@@ -456,6 +493,7 @@ export class Editor extends RapidElement {
|
|
|
456
493
|
#canvas > .draggable {
|
|
457
494
|
position: absolute;
|
|
458
495
|
z-index: 100;
|
|
496
|
+
touch-action: none;
|
|
459
497
|
}
|
|
460
498
|
|
|
461
499
|
#canvas > .dragging {
|
|
@@ -955,7 +993,7 @@ export class Editor extends RapidElement {
|
|
|
955
993
|
top: 8px;
|
|
956
994
|
right: 240px;
|
|
957
995
|
padding: 6px 10px;
|
|
958
|
-
z-index:
|
|
996
|
+
z-index: 4999;
|
|
959
997
|
pointer-events: none;
|
|
960
998
|
opacity: 0;
|
|
961
999
|
transition: opacity 0.15s ease-in-out;
|
|
@@ -1106,6 +1144,12 @@ export class Editor extends RapidElement {
|
|
|
1106
1144
|
super.firstUpdated(changes);
|
|
1107
1145
|
this.plumber = new Plumber(this.querySelector('#canvas'), this);
|
|
1108
1146
|
this.setupGlobalEventListeners();
|
|
1147
|
+
|
|
1148
|
+
// Eagerly detect touch capability so hover-only controls are visible
|
|
1149
|
+
// from the start and scrollbar/touch-action CSS is applied immediately.
|
|
1150
|
+
if (navigator.maxTouchPoints > 0) {
|
|
1151
|
+
this.markTouchDevice();
|
|
1152
|
+
}
|
|
1109
1153
|
this.updateZoomControlPositioning();
|
|
1110
1154
|
if (changes.has('flow')) {
|
|
1111
1155
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
@@ -1188,8 +1232,10 @@ export class Editor extends RapidElement {
|
|
|
1188
1232
|
x: snappedPosition.left,
|
|
1189
1233
|
y: snappedPosition.top
|
|
1190
1234
|
},
|
|
1191
|
-
false
|
|
1192
|
-
|
|
1235
|
+
false, // Don't show sticky note option for connection drops
|
|
1236
|
+
false,
|
|
1237
|
+
this.flowType === 'message'
|
|
1238
|
+
);
|
|
1193
1239
|
}
|
|
1194
1240
|
}
|
|
1195
1241
|
|
|
@@ -1506,10 +1552,14 @@ export class Editor extends RapidElement {
|
|
|
1506
1552
|
document.removeEventListener('mouseup', this.boundMouseUp);
|
|
1507
1553
|
document.removeEventListener('mousedown', this.boundGlobalMouseDown);
|
|
1508
1554
|
document.removeEventListener('keydown', this.boundKeyDown);
|
|
1555
|
+
document.removeEventListener('touchmove', this.boundTouchMove);
|
|
1556
|
+
document.removeEventListener('touchend', this.boundTouchEnd);
|
|
1557
|
+
document.removeEventListener('touchcancel', this.boundTouchCancel);
|
|
1509
1558
|
|
|
1510
1559
|
const canvas = this.querySelector('#canvas');
|
|
1511
1560
|
if (canvas) {
|
|
1512
1561
|
canvas.removeEventListener('contextmenu', this.boundCanvasContextMenu);
|
|
1562
|
+
canvas.removeEventListener('touchstart', this.boundCanvasTouchStart);
|
|
1513
1563
|
}
|
|
1514
1564
|
|
|
1515
1565
|
const editor = this.querySelector('#editor');
|
|
@@ -1527,10 +1577,26 @@ export class Editor extends RapidElement {
|
|
|
1527
1577
|
document.addEventListener('mouseup', this.boundMouseUp);
|
|
1528
1578
|
document.addEventListener('mousedown', this.boundGlobalMouseDown);
|
|
1529
1579
|
document.addEventListener('keydown', this.boundKeyDown);
|
|
1580
|
+
document.addEventListener('touchmove', this.boundTouchMove, {
|
|
1581
|
+
passive: false
|
|
1582
|
+
});
|
|
1583
|
+
document.addEventListener('touchend', this.boundTouchEnd);
|
|
1584
|
+
document.addEventListener('touchcancel', this.boundTouchCancel);
|
|
1585
|
+
|
|
1586
|
+
// Fallback: on first touch, mark as touch device in case
|
|
1587
|
+
// navigator.maxTouchPoints wasn't detected in firstUpdated.
|
|
1588
|
+
const markTouchOnce = () => {
|
|
1589
|
+
this.markTouchDevice();
|
|
1590
|
+
document.removeEventListener('touchstart', markTouchOnce);
|
|
1591
|
+
};
|
|
1592
|
+
document.addEventListener('touchstart', markTouchOnce);
|
|
1530
1593
|
|
|
1531
1594
|
const canvas = this.querySelector('#canvas');
|
|
1532
1595
|
if (canvas) {
|
|
1533
1596
|
canvas.addEventListener('contextmenu', this.boundCanvasContextMenu);
|
|
1597
|
+
canvas.addEventListener('touchstart', this.boundCanvasTouchStart, {
|
|
1598
|
+
passive: false
|
|
1599
|
+
});
|
|
1534
1600
|
}
|
|
1535
1601
|
|
|
1536
1602
|
const editor = this.querySelector('#editor');
|
|
@@ -1617,7 +1683,11 @@ export class Editor extends RapidElement {
|
|
|
1617
1683
|
const element = event.currentTarget as HTMLElement;
|
|
1618
1684
|
// Only start dragging if clicking on the element itself, not on exits or other interactive elements
|
|
1619
1685
|
const target = event.target as HTMLElement;
|
|
1620
|
-
if (
|
|
1686
|
+
if (
|
|
1687
|
+
target.classList.contains('exit') ||
|
|
1688
|
+
target.closest('.exit') ||
|
|
1689
|
+
target.closest('.linked-name')
|
|
1690
|
+
) {
|
|
1621
1691
|
return;
|
|
1622
1692
|
}
|
|
1623
1693
|
|
|
@@ -1652,6 +1722,71 @@ export class Editor extends RapidElement {
|
|
|
1652
1722
|
event.stopPropagation();
|
|
1653
1723
|
}
|
|
1654
1724
|
|
|
1725
|
+
/**
|
|
1726
|
+
* Mirror of handleMouseDown for touch devices.
|
|
1727
|
+
* Sets up the same drag state so handleTouchMove/End can drive the drag.
|
|
1728
|
+
*/
|
|
1729
|
+
/* c8 ignore start -- touch-only handlers untestable in headless Chromium */
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Mark the editor as a touch device — adds classes to #canvas and
|
|
1733
|
+
* #editor so touch-specific CSS activates (visible controls,
|
|
1734
|
+
* always-on scrollbars, touch-action: none).
|
|
1735
|
+
*/
|
|
1736
|
+
private markTouchDevice(): void {
|
|
1737
|
+
if (this.isTouchDevice) return;
|
|
1738
|
+
this.isTouchDevice = true;
|
|
1739
|
+
this.querySelector('#canvas')?.classList.add('touch-device');
|
|
1740
|
+
this.querySelector('#editor')?.classList.add('touch-device');
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
private handleItemTouchStart(event: TouchEvent): void {
|
|
1744
|
+
this.markTouchDevice();
|
|
1745
|
+
|
|
1746
|
+
if (this.isReadOnly()) return;
|
|
1747
|
+
this.blurActiveContentEditable();
|
|
1748
|
+
|
|
1749
|
+
const touch = event.touches[0];
|
|
1750
|
+
if (!touch) return;
|
|
1751
|
+
|
|
1752
|
+
const element = event.currentTarget as HTMLElement;
|
|
1753
|
+
const target = event.target as HTMLElement;
|
|
1754
|
+
if (
|
|
1755
|
+
target.classList.contains('exit') ||
|
|
1756
|
+
target.closest('.exit') ||
|
|
1757
|
+
target.closest('.linked-name')
|
|
1758
|
+
) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
const uuid = element.getAttribute('uuid');
|
|
1763
|
+
const type = element.tagName === 'TEMBA-FLOW-NODE' ? 'node' : 'sticky';
|
|
1764
|
+
|
|
1765
|
+
const position = this.getPosition(uuid, type);
|
|
1766
|
+
if (!position) return;
|
|
1767
|
+
|
|
1768
|
+
// Touch doesn't support Ctrl/Cmd selection — just clear
|
|
1769
|
+
if (!this.selectedItems.has(uuid)) {
|
|
1770
|
+
this.selectedItems.clear();
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
this.isMouseDown = true;
|
|
1774
|
+
this.dragStartPos = { x: touch.clientX, y: touch.clientY };
|
|
1775
|
+
this.startPos = { left: position.left, top: position.top };
|
|
1776
|
+
this.currentDragItem = {
|
|
1777
|
+
uuid,
|
|
1778
|
+
position,
|
|
1779
|
+
element,
|
|
1780
|
+
type
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
// Don't preventDefault here — allow the threshold check in touchmove
|
|
1784
|
+
// to decide whether this is a drag or a tap
|
|
1785
|
+
event.stopPropagation();
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/* c8 ignore stop */
|
|
1789
|
+
|
|
1655
1790
|
private handleGlobalMouseDown(event: MouseEvent): void {
|
|
1656
1791
|
if (isRightClick(event)) return;
|
|
1657
1792
|
|
|
@@ -2370,6 +2505,80 @@ export class Editor extends RapidElement {
|
|
|
2370
2505
|
}
|
|
2371
2506
|
}
|
|
2372
2507
|
|
|
2508
|
+
/* c8 ignore start -- touch-only handlers */
|
|
2509
|
+
|
|
2510
|
+
/**
|
|
2511
|
+
* Find the temba-flow-node element at the given viewport coordinates.
|
|
2512
|
+
* Uses elementFromPoint which works for both mouse and touch input.
|
|
2513
|
+
*/
|
|
2514
|
+
private findTargetNodeAt(clientX: number, clientY: number): Element | null {
|
|
2515
|
+
const el = document.elementFromPoint(clientX, clientY);
|
|
2516
|
+
return el?.closest('temba-flow-node') ?? null;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
/**
|
|
2520
|
+
* Handle touchstart on the canvas element. Mirrors handleGlobalMouseDown
|
|
2521
|
+
* + handleCanvasMouseDown for touch: starts selection on empty canvas,
|
|
2522
|
+
* and detects double-tap to show the context menu.
|
|
2523
|
+
*/
|
|
2524
|
+
private handleCanvasTouchStart(event: TouchEvent): void {
|
|
2525
|
+
this.markTouchDevice();
|
|
2526
|
+
|
|
2527
|
+
const touch = event.touches[0];
|
|
2528
|
+
if (!touch) return;
|
|
2529
|
+
|
|
2530
|
+
// Only handle touches directly on canvas/grid (not on nodes)
|
|
2531
|
+
const target = event.target as HTMLElement;
|
|
2532
|
+
if (target.closest('.draggable')) return;
|
|
2533
|
+
if (target.id !== 'canvas' && target.id !== 'grid') return;
|
|
2534
|
+
|
|
2535
|
+
// Two-finger touch on canvas — record start position and enter the
|
|
2536
|
+
// two-finger state immediately (even before any touchmove). If the
|
|
2537
|
+
// fingers lift without panning, we show the context menu (handleTouchEnd).
|
|
2538
|
+
if (event.touches.length >= 2) {
|
|
2539
|
+
// Cancel any single-finger selection that the first touch started
|
|
2540
|
+
this.canvasMouseDown = false;
|
|
2541
|
+
this.isSelecting = false;
|
|
2542
|
+
this.selectionBox = null;
|
|
2543
|
+
|
|
2544
|
+
this.isTwoFingerPanning = true;
|
|
2545
|
+
this.twoFingerOnCanvas = true;
|
|
2546
|
+
this.twoFingerDidPan = false;
|
|
2547
|
+
this.twoFingerStartMidX =
|
|
2548
|
+
(event.touches[0].clientX + event.touches[1].clientX) / 2;
|
|
2549
|
+
this.twoFingerStartMidY =
|
|
2550
|
+
(event.touches[0].clientY + event.touches[1].clientY) / 2;
|
|
2551
|
+
this.lastPanX = this.twoFingerStartMidX;
|
|
2552
|
+
this.lastPanY = this.twoFingerStartMidY;
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// Start selection box (mirrors handleCanvasMouseDown)
|
|
2557
|
+
if (this.isReadOnly()) return;
|
|
2558
|
+
|
|
2559
|
+
this.canvasMouseDown = true;
|
|
2560
|
+
this.dragStartPos = { x: touch.clientX, y: touch.clientY };
|
|
2561
|
+
|
|
2562
|
+
const canvasRect = this.querySelector('#canvas')?.getBoundingClientRect();
|
|
2563
|
+
if (canvasRect) {
|
|
2564
|
+
this.selectedItems.clear();
|
|
2565
|
+
|
|
2566
|
+
const relativeX = (touch.clientX - canvasRect.left) / this.zoom;
|
|
2567
|
+
const relativeY = (touch.clientY - canvasRect.top) / this.zoom;
|
|
2568
|
+
|
|
2569
|
+
this.selectionBox = {
|
|
2570
|
+
startX: relativeX,
|
|
2571
|
+
startY: relativeY,
|
|
2572
|
+
endX: relativeX,
|
|
2573
|
+
endY: relativeY
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
event.preventDefault();
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
/* c8 ignore stop */
|
|
2581
|
+
|
|
2373
2582
|
private handleMouseMove(event: MouseEvent): void {
|
|
2374
2583
|
// Handle selection box drawing
|
|
2375
2584
|
if (this.canvasMouseDown && !this.isMouseDown) {
|
|
@@ -2455,7 +2664,7 @@ export class Editor extends RapidElement {
|
|
|
2455
2664
|
// Handle item dragging
|
|
2456
2665
|
if (!this.isMouseDown || !this.currentDragItem) return;
|
|
2457
2666
|
|
|
2458
|
-
this.
|
|
2667
|
+
this.lastPointerPos = { clientX: event.clientX, clientY: event.clientY };
|
|
2459
2668
|
|
|
2460
2669
|
const deltaX = event.clientX - this.dragStartPos.x + this.autoScrollDeltaX;
|
|
2461
2670
|
const deltaY = event.clientY - this.dragStartPos.y + this.autoScrollDeltaY;
|
|
@@ -2474,16 +2683,16 @@ export class Editor extends RapidElement {
|
|
|
2474
2683
|
}
|
|
2475
2684
|
|
|
2476
2685
|
private updateDragPositions(): void {
|
|
2477
|
-
if (!this.currentDragItem || !this.
|
|
2686
|
+
if (!this.currentDragItem || !this.lastPointerPos) return;
|
|
2478
2687
|
|
|
2479
2688
|
// Convert screen + scroll delta to canvas delta
|
|
2480
2689
|
const deltaX =
|
|
2481
|
-
(this.
|
|
2690
|
+
(this.lastPointerPos.clientX -
|
|
2482
2691
|
this.dragStartPos.x +
|
|
2483
2692
|
this.autoScrollDeltaX) /
|
|
2484
2693
|
this.zoom;
|
|
2485
2694
|
const deltaY =
|
|
2486
|
-
(this.
|
|
2695
|
+
(this.lastPointerPos.clientY -
|
|
2487
2696
|
this.dragStartPos.y +
|
|
2488
2697
|
this.autoScrollDeltaY) /
|
|
2489
2698
|
this.zoom;
|
|
@@ -2518,14 +2727,14 @@ export class Editor extends RapidElement {
|
|
|
2518
2727
|
if (!editor) return;
|
|
2519
2728
|
|
|
2520
2729
|
const tick = () => {
|
|
2521
|
-
if (!this.isDragging || !this.
|
|
2730
|
+
if (!this.isDragging || !this.lastPointerPos) {
|
|
2522
2731
|
this.autoScrollAnimationId = null;
|
|
2523
2732
|
return;
|
|
2524
2733
|
}
|
|
2525
2734
|
|
|
2526
2735
|
const editorRect = editor.getBoundingClientRect();
|
|
2527
|
-
const mouseX = this.
|
|
2528
|
-
const mouseY = this.
|
|
2736
|
+
const mouseX = this.lastPointerPos.clientX;
|
|
2737
|
+
const mouseY = this.lastPointerPos.clientY;
|
|
2529
2738
|
|
|
2530
2739
|
let scrollDx = 0;
|
|
2531
2740
|
let scrollDy = 0;
|
|
@@ -2699,9 +2908,332 @@ export class Editor extends RapidElement {
|
|
|
2699
2908
|
this.canvasMouseDown = false;
|
|
2700
2909
|
this.autoScrollDeltaX = 0;
|
|
2701
2910
|
this.autoScrollDeltaY = 0;
|
|
2702
|
-
this.
|
|
2911
|
+
this.lastPointerPos = null;
|
|
2703
2912
|
}
|
|
2704
2913
|
|
|
2914
|
+
/* c8 ignore start -- touch-only handlers */
|
|
2915
|
+
|
|
2916
|
+
/**
|
|
2917
|
+
* Handle touch move on the document — mirrors handleMouseMove for
|
|
2918
|
+
* both connection dragging and node/sticky dragging on touch devices.
|
|
2919
|
+
*/
|
|
2920
|
+
private handleTouchMove(event: TouchEvent): void {
|
|
2921
|
+
// --- Two-finger panning ---
|
|
2922
|
+
if (event.touches.length >= 2) {
|
|
2923
|
+
event.preventDefault();
|
|
2924
|
+
const midX = (event.touches[0].clientX + event.touches[1].clientX) / 2;
|
|
2925
|
+
const midY = (event.touches[0].clientY + event.touches[1].clientY) / 2;
|
|
2926
|
+
|
|
2927
|
+
if (this.isTwoFingerPanning) {
|
|
2928
|
+
const dx = this.lastPanX - midX;
|
|
2929
|
+
const dy = this.lastPanY - midY;
|
|
2930
|
+
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
|
|
2931
|
+
this.twoFingerDidPan = true;
|
|
2932
|
+
}
|
|
2933
|
+
const editor = this.querySelector('#editor') as HTMLElement;
|
|
2934
|
+
if (editor) {
|
|
2935
|
+
editor.scrollBy(dx, dy);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// Cancel any in-progress single-finger actions
|
|
2940
|
+
this.canvasMouseDown = false;
|
|
2941
|
+
this.isSelecting = false;
|
|
2942
|
+
this.selectionBox = null;
|
|
2943
|
+
|
|
2944
|
+
this.isTwoFingerPanning = true;
|
|
2945
|
+
this.lastPanX = midX;
|
|
2946
|
+
this.lastPanY = midY;
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
const touch = event.touches[0];
|
|
2951
|
+
if (!touch) return;
|
|
2952
|
+
|
|
2953
|
+
// --- Selection box drawing ---
|
|
2954
|
+
if (this.canvasMouseDown && !this.isMouseDown) {
|
|
2955
|
+
event.preventDefault();
|
|
2956
|
+
this.isSelecting = true;
|
|
2957
|
+
|
|
2958
|
+
const canvasRect = this.querySelector('#canvas')?.getBoundingClientRect();
|
|
2959
|
+
if (canvasRect && this.selectionBox) {
|
|
2960
|
+
this.selectionBox = {
|
|
2961
|
+
...this.selectionBox,
|
|
2962
|
+
endX: (touch.clientX - canvasRect.left) / this.zoom,
|
|
2963
|
+
endY: (touch.clientY - canvasRect.top) / this.zoom
|
|
2964
|
+
};
|
|
2965
|
+
this.updateSelectedItemsFromBox();
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
this.requestUpdate();
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
// --- Connection dragging ---
|
|
2973
|
+
if (this.plumber.connectionDragging) {
|
|
2974
|
+
event.preventDefault();
|
|
2975
|
+
|
|
2976
|
+
const targetNode = this.findTargetNodeAt(touch.clientX, touch.clientY);
|
|
2977
|
+
|
|
2978
|
+
// Clear previous target styles
|
|
2979
|
+
document.querySelectorAll('temba-flow-node').forEach((node) => {
|
|
2980
|
+
node.classList.remove(
|
|
2981
|
+
'connection-target-valid',
|
|
2982
|
+
'connection-target-invalid'
|
|
2983
|
+
);
|
|
2984
|
+
});
|
|
2985
|
+
|
|
2986
|
+
if (targetNode) {
|
|
2987
|
+
this.targetId = targetNode.getAttribute('uuid');
|
|
2988
|
+
this.isValidTarget = this.targetId !== this.dragFromNodeId;
|
|
2989
|
+
|
|
2990
|
+
if (this.isValidTarget) {
|
|
2991
|
+
targetNode.classList.add('connection-target-valid');
|
|
2992
|
+
} else {
|
|
2993
|
+
targetNode.classList.add('connection-target-invalid');
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
this.connectionPlaceholder = null;
|
|
2997
|
+
} else {
|
|
2998
|
+
this.targetId = null;
|
|
2999
|
+
this.isValidTarget = true;
|
|
3000
|
+
|
|
3001
|
+
const canvas = this.querySelector('#canvas');
|
|
3002
|
+
if (canvas) {
|
|
3003
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
3004
|
+
const relativeX = (touch.clientX - canvasRect.left) / this.zoom;
|
|
3005
|
+
const relativeY = (touch.clientY - canvasRect.top) / this.zoom;
|
|
3006
|
+
|
|
3007
|
+
const placeholderWidth = 200;
|
|
3008
|
+
const placeholderHeight = 64;
|
|
3009
|
+
const arrowLength = ARROW_LENGTH;
|
|
3010
|
+
const cursorGap = CURSOR_GAP;
|
|
3011
|
+
|
|
3012
|
+
const dragUp =
|
|
3013
|
+
this.connectionSourceY != null
|
|
3014
|
+
? relativeY < this.connectionSourceY
|
|
3015
|
+
: false;
|
|
3016
|
+
|
|
3017
|
+
let top: number;
|
|
3018
|
+
if (dragUp) {
|
|
3019
|
+
top = relativeY + cursorGap - placeholderHeight;
|
|
3020
|
+
} else {
|
|
3021
|
+
top = relativeY - cursorGap + arrowLength;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
this.connectionPlaceholder = {
|
|
3025
|
+
position: {
|
|
3026
|
+
left: relativeX - placeholderWidth / 2,
|
|
3027
|
+
top
|
|
3028
|
+
},
|
|
3029
|
+
visible: true,
|
|
3030
|
+
dragUp
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
this.requestUpdate();
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// --- Node/sticky dragging ---
|
|
3040
|
+
if (!this.isMouseDown || !this.currentDragItem) return;
|
|
3041
|
+
|
|
3042
|
+
this.lastPointerPos = { clientX: touch.clientX, clientY: touch.clientY };
|
|
3043
|
+
|
|
3044
|
+
const deltaX = touch.clientX - this.dragStartPos.x + this.autoScrollDeltaX;
|
|
3045
|
+
const deltaY = touch.clientY - this.dragStartPos.y + this.autoScrollDeltaY;
|
|
3046
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
3047
|
+
|
|
3048
|
+
if (!this.isDragging && distance > DRAG_THRESHOLD) {
|
|
3049
|
+
this.isDragging = true;
|
|
3050
|
+
this.startAutoScroll();
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// Only prevent default scrolling once we're actually dragging.
|
|
3054
|
+
// Before the threshold, allow the browser to fire synthetic click
|
|
3055
|
+
// events for taps on buttons (remove, add-action, etc.).
|
|
3056
|
+
if (this.isDragging) {
|
|
3057
|
+
event.preventDefault();
|
|
3058
|
+
this.updateDragPositions();
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
/**
|
|
3063
|
+
* Handle touch end on the document — mirrors handleMouseUp for
|
|
3064
|
+
* both connection dragging and node/sticky dragging on touch devices.
|
|
3065
|
+
*/
|
|
3066
|
+
private handleTouchEnd(event: TouchEvent): void {
|
|
3067
|
+
// --- Two-finger gesture end ---
|
|
3068
|
+
if (this.isTwoFingerPanning) {
|
|
3069
|
+
if (event.touches.length === 0) {
|
|
3070
|
+
const didPan = this.twoFingerDidPan;
|
|
3071
|
+
const onCanvas = this.twoFingerOnCanvas;
|
|
3072
|
+
const midX = this.twoFingerStartMidX;
|
|
3073
|
+
const midY = this.twoFingerStartMidY;
|
|
3074
|
+
|
|
3075
|
+
// Reset state
|
|
3076
|
+
this.isTwoFingerPanning = false;
|
|
3077
|
+
this.twoFingerOnCanvas = false;
|
|
3078
|
+
this.twoFingerDidPan = false;
|
|
3079
|
+
|
|
3080
|
+
// Two-finger tap (no pan) on canvas → show context menu
|
|
3081
|
+
if (!didPan && onCanvas) {
|
|
3082
|
+
this.showContextMenuAt(midX, midY);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
const touch = event.changedTouches[0];
|
|
3089
|
+
|
|
3090
|
+
// --- Selection box completion ---
|
|
3091
|
+
if (this.canvasMouseDown && this.isSelecting) {
|
|
3092
|
+
this.isSelecting = false;
|
|
3093
|
+
this.selectionBox = null;
|
|
3094
|
+
this.canvasMouseDown = false;
|
|
3095
|
+
this.requestUpdate();
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
// --- Canvas tap (no drag) — clear selection ---
|
|
3100
|
+
if (this.canvasMouseDown && !this.isSelecting) {
|
|
3101
|
+
this.canvasMouseDown = false;
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// --- Connection dragging ---
|
|
3106
|
+
if (this.plumber.connectionDragging) {
|
|
3107
|
+
if (touch) {
|
|
3108
|
+
const targetNode = this.findTargetNodeAt(touch.clientX, touch.clientY);
|
|
3109
|
+
if (targetNode) {
|
|
3110
|
+
this.targetId = targetNode.getAttribute('uuid');
|
|
3111
|
+
this.isValidTarget = this.targetId !== this.dragFromNodeId;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// --- Node/sticky dragging ---
|
|
3118
|
+
if (!this.isMouseDown || !this.currentDragItem) return;
|
|
3119
|
+
|
|
3120
|
+
this.stopAutoScroll();
|
|
3121
|
+
|
|
3122
|
+
if (this.isDragging && touch) {
|
|
3123
|
+
const deltaX =
|
|
3124
|
+
(touch.clientX - this.dragStartPos.x + this.autoScrollDeltaX) /
|
|
3125
|
+
this.zoom;
|
|
3126
|
+
const deltaY =
|
|
3127
|
+
(touch.clientY - this.dragStartPos.y + this.autoScrollDeltaY) /
|
|
3128
|
+
this.zoom;
|
|
3129
|
+
|
|
3130
|
+
const itemsToMove =
|
|
3131
|
+
this.selectedItems.has(this.currentDragItem.uuid) &&
|
|
3132
|
+
this.selectedItems.size > 1
|
|
3133
|
+
? Array.from(this.selectedItems)
|
|
3134
|
+
: [this.currentDragItem.uuid];
|
|
3135
|
+
|
|
3136
|
+
const newPositions: { [uuid: string]: FlowPosition } = {};
|
|
3137
|
+
|
|
3138
|
+
itemsToMove.forEach((uuid) => {
|
|
3139
|
+
const type = this.definition.nodes.find((node) => node.uuid === uuid)
|
|
3140
|
+
? 'node'
|
|
3141
|
+
: 'sticky';
|
|
3142
|
+
const position = this.getPosition(uuid, type);
|
|
3143
|
+
|
|
3144
|
+
if (position) {
|
|
3145
|
+
const newLeft = position.left + deltaX;
|
|
3146
|
+
const newTop = position.top + deltaY;
|
|
3147
|
+
const snappedLeft = snapToGrid(newLeft);
|
|
3148
|
+
const snappedTop = snapToGrid(newTop);
|
|
3149
|
+
|
|
3150
|
+
newPositions[uuid] = { left: snappedLeft, top: snappedTop };
|
|
3151
|
+
|
|
3152
|
+
const element = this.querySelector(`[uuid="${uuid}"]`) as HTMLElement;
|
|
3153
|
+
if (element) {
|
|
3154
|
+
element.classList.remove('dragging');
|
|
3155
|
+
element.style.left = `${snappedLeft}px`;
|
|
3156
|
+
element.style.top = `${snappedTop}px`;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
});
|
|
3160
|
+
|
|
3161
|
+
if (Object.keys(newPositions).length > 0) {
|
|
3162
|
+
getStore().getState().updateCanvasPositions(newPositions);
|
|
3163
|
+
|
|
3164
|
+
const nodeUuids = itemsToMove.filter((uuid) =>
|
|
3165
|
+
this.definition.nodes.find((node) => node.uuid === uuid)
|
|
3166
|
+
);
|
|
3167
|
+
|
|
3168
|
+
if (nodeUuids.length > 0) {
|
|
3169
|
+
setTimeout(() => {
|
|
3170
|
+
this.checkCollisionsAndReflow(nodeUuids);
|
|
3171
|
+
}, 0);
|
|
3172
|
+
} else {
|
|
3173
|
+
setTimeout(() => {
|
|
3174
|
+
this.plumber.repaintEverything();
|
|
3175
|
+
}, 0);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
this.selectedItems.clear();
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// Reset all drag state
|
|
3183
|
+
this.isDragging = false;
|
|
3184
|
+
this.isMouseDown = false;
|
|
3185
|
+
this.currentDragItem = null;
|
|
3186
|
+
this.canvasMouseDown = false;
|
|
3187
|
+
this.autoScrollDeltaX = 0;
|
|
3188
|
+
this.autoScrollDeltaY = 0;
|
|
3189
|
+
this.lastPointerPos = null;
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
/**
|
|
3193
|
+
* Handle touchcancel — reset all touch-related state so the editor
|
|
3194
|
+
* doesn't get stuck in a partial drag/selection mode.
|
|
3195
|
+
*/
|
|
3196
|
+
private handleTouchCancel(): void {
|
|
3197
|
+
this.isTwoFingerPanning = false;
|
|
3198
|
+
this.isSelecting = false;
|
|
3199
|
+
this.selectionBox = null;
|
|
3200
|
+
this.canvasMouseDown = false;
|
|
3201
|
+
|
|
3202
|
+
if (this.isDragging && this.currentDragItem) {
|
|
3203
|
+
// Remove dragging class from all moved items
|
|
3204
|
+
const itemsToReset =
|
|
3205
|
+
this.selectedItems.has(this.currentDragItem.uuid) &&
|
|
3206
|
+
this.selectedItems.size > 1
|
|
3207
|
+
? Array.from(this.selectedItems)
|
|
3208
|
+
: [this.currentDragItem.uuid];
|
|
3209
|
+
itemsToReset.forEach((uuid) => {
|
|
3210
|
+
const el = this.querySelector(`[uuid="${uuid}"]`) as HTMLElement;
|
|
3211
|
+
if (el) el.classList.remove('dragging');
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
this.stopAutoScroll();
|
|
3216
|
+
this.isDragging = false;
|
|
3217
|
+
this.isMouseDown = false;
|
|
3218
|
+
this.currentDragItem = null;
|
|
3219
|
+
this.autoScrollDeltaX = 0;
|
|
3220
|
+
this.autoScrollDeltaY = 0;
|
|
3221
|
+
this.lastPointerPos = null;
|
|
3222
|
+
|
|
3223
|
+
// Clear connection drag visual state
|
|
3224
|
+
document.querySelectorAll('temba-flow-node').forEach((node) => {
|
|
3225
|
+
node.classList.remove(
|
|
3226
|
+
'connection-target-valid',
|
|
3227
|
+
'connection-target-invalid'
|
|
3228
|
+
);
|
|
3229
|
+
});
|
|
3230
|
+
this.connectionPlaceholder = null;
|
|
3231
|
+
|
|
3232
|
+
this.requestUpdate();
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
/* c8 ignore stop */
|
|
3236
|
+
|
|
2705
3237
|
private updateCanvasSize(): void {
|
|
2706
3238
|
if (!this.definition) return;
|
|
2707
3239
|
|
|
@@ -2777,33 +3309,39 @@ export class Editor extends RapidElement {
|
|
|
2777
3309
|
event.preventDefault();
|
|
2778
3310
|
event.stopPropagation();
|
|
2779
3311
|
|
|
2780
|
-
|
|
3312
|
+
this.showContextMenuAt(event.clientX, event.clientY);
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
/**
|
|
3316
|
+
* Show the canvas context menu at the given viewport coordinates.
|
|
3317
|
+
* Shared by right-click (mouse) and double-tap (touch).
|
|
3318
|
+
*/
|
|
3319
|
+
private showContextMenuAt(clientX: number, clientY: number): void {
|
|
3320
|
+
if (this.isReadOnly()) return;
|
|
3321
|
+
|
|
2781
3322
|
const canvas = this.querySelector('#canvas');
|
|
2782
|
-
if (!canvas)
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
3323
|
+
if (!canvas) return;
|
|
2785
3324
|
|
|
2786
3325
|
const canvasRect = canvas.getBoundingClientRect();
|
|
2787
|
-
const relativeX = (
|
|
2788
|
-
const relativeY = (
|
|
3326
|
+
const relativeX = (clientX - canvasRect.left) / this.zoom - 10;
|
|
3327
|
+
const relativeY = (clientY - canvasRect.top) / this.zoom - 10;
|
|
2789
3328
|
|
|
2790
|
-
// Snap position to grid
|
|
2791
3329
|
const snappedLeft = snapToGrid(relativeX);
|
|
2792
3330
|
const snappedTop = snapToGrid(relativeY);
|
|
2793
3331
|
|
|
2794
|
-
// Show the canvas menu at the mouse position (use viewport coordinates)
|
|
2795
3332
|
const canvasMenu = this.querySelector('temba-canvas-menu') as CanvasMenu;
|
|
2796
3333
|
if (canvasMenu) {
|
|
2797
3334
|
const hasNodes = this.definition && this.definition.nodes.length > 0;
|
|
2798
3335
|
canvasMenu.show(
|
|
2799
|
-
|
|
2800
|
-
|
|
3336
|
+
clientX,
|
|
3337
|
+
clientY,
|
|
2801
3338
|
{
|
|
2802
3339
|
x: snappedLeft,
|
|
2803
3340
|
y: snappedTop
|
|
2804
3341
|
},
|
|
2805
3342
|
true,
|
|
2806
|
-
hasNodes
|
|
3343
|
+
hasNodes,
|
|
3344
|
+
this.flowType === 'message'
|
|
2807
3345
|
);
|
|
2808
3346
|
}
|
|
2809
3347
|
}
|
|
@@ -2826,7 +3364,14 @@ export class Editor extends RapidElement {
|
|
|
2826
3364
|
const menuWidth = 265;
|
|
2827
3365
|
const menuX = rect.left + rect.width / 2 - menuWidth / 2;
|
|
2828
3366
|
const menuY = rect.bottom + 8;
|
|
2829
|
-
canvasMenu.show(
|
|
3367
|
+
canvasMenu.show(
|
|
3368
|
+
menuX,
|
|
3369
|
+
menuY,
|
|
3370
|
+
{ x: nodeLeft, y: nodeTop },
|
|
3371
|
+
false,
|
|
3372
|
+
false,
|
|
3373
|
+
this.flowType === 'message'
|
|
3374
|
+
);
|
|
2830
3375
|
}
|
|
2831
3376
|
}
|
|
2832
3377
|
|
|
@@ -2852,6 +3397,19 @@ export class Editor extends RapidElement {
|
|
|
2852
3397
|
this.connectionSourceX = null;
|
|
2853
3398
|
this.connectionSourceY = null;
|
|
2854
3399
|
this.dragFromNodeId = null;
|
|
3400
|
+
} else if (
|
|
3401
|
+
selection.action === 'send_msg' ||
|
|
3402
|
+
selection.action === 'wait_for_response'
|
|
3403
|
+
) {
|
|
3404
|
+
// Go directly to the node editor (skip node type selector)
|
|
3405
|
+
this.handleNodeTypeSelection(
|
|
3406
|
+
new CustomEvent(CustomEventType.Selection, {
|
|
3407
|
+
detail: {
|
|
3408
|
+
nodeType: selection.action,
|
|
3409
|
+
position: selection.position
|
|
3410
|
+
} as NodeTypeSelection
|
|
3411
|
+
})
|
|
3412
|
+
);
|
|
2855
3413
|
} else {
|
|
2856
3414
|
// Show node type selector
|
|
2857
3415
|
const selector = this.querySelector(
|
|
@@ -4716,6 +5274,7 @@ export class Editor extends RapidElement {
|
|
|
4716
5274
|
? 'flow-start'
|
|
4717
5275
|
: ''}"
|
|
4718
5276
|
@mousedown=${this.handleMouseDown.bind(this)}
|
|
5277
|
+
@touchstart=${this.handleItemTouchStart.bind(this)}
|
|
4719
5278
|
uuid=${node.uuid}
|
|
4720
5279
|
data-node-uuid=${node.uuid}
|
|
4721
5280
|
style="left:${position.left}px; top:${position.top}px;transition: all 0.2s ease-in-out;"
|
|
@@ -4742,6 +5301,7 @@ export class Editor extends RapidElement {
|
|
|
4742
5301
|
? 'selected'
|
|
4743
5302
|
: ''}"
|
|
4744
5303
|
@mousedown=${this.handleMouseDown.bind(this)}
|
|
5304
|
+
@touchstart=${this.handleItemTouchStart.bind(this)}
|
|
4745
5305
|
style="left:${position.left}px; top:${position.top}px;"
|
|
4746
5306
|
uuid=${uuid}
|
|
4747
5307
|
.data=${sticky}
|