@nyaruka/temba-components 0.142.2 → 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.
Files changed (40) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/temba-components.js +266 -192
  3. package/dist/temba-components.js.map +1 -1
  4. package/out-tsc/src/flow/CanvasMenu.js +8 -3
  5. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  6. package/out-tsc/src/flow/CanvasNode.js +158 -9
  7. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  8. package/out-tsc/src/flow/Editor.js +473 -17
  9. package/out-tsc/src/flow/Editor.js.map +1 -1
  10. package/out-tsc/src/flow/NodeEditor.js +8 -8
  11. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  12. package/out-tsc/src/flow/Plumber.js +89 -27
  13. package/out-tsc/src/flow/Plumber.js.map +1 -1
  14. package/out-tsc/src/flow/StickyNote.js +63 -3
  15. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  16. package/out-tsc/src/flow/actions/send_msg.js +11 -8
  17. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  18. package/out-tsc/src/flow/nodes/split_by_subflow.js +3 -1
  19. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  20. package/out-tsc/src/flow/utils.js +1 -3
  21. package/out-tsc/src/flow/utils.js.map +1 -1
  22. package/out-tsc/src/list/SortableList.js +104 -43
  23. package/out-tsc/src/list/SortableList.js.map +1 -1
  24. package/out-tsc/src/simulator/Simulator.js +5 -1
  25. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  26. package/package.json +1 -1
  27. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  28. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  29. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  30. package/src/flow/CanvasMenu.ts +12 -4
  31. package/src/flow/CanvasNode.ts +185 -9
  32. package/src/flow/Editor.ts +552 -19
  33. package/src/flow/NodeEditor.ts +12 -12
  34. package/src/flow/Plumber.ts +101 -36
  35. package/src/flow/StickyNote.ts +76 -4
  36. package/src/flow/actions/send_msg.ts +11 -8
  37. package/src/flow/nodes/split_by_subflow.ts +3 -1
  38. package/src/flow/utils.ts +1 -3
  39. package/src/list/SortableList.ts +117 -47
  40. package/src/simulator/Simulator.ts +5 -1
@@ -69,15 +69,19 @@ export class CanvasMenu extends RapidElement {
69
69
  }
70
70
  firstUpdated(_changedProperties) {
71
71
  super.firstUpdated(_changedProperties);
72
- // Close menu when clicking outside — use mousedown instead of click
73
- // to avoid being triggered by the click synthesized from a drag-and-drop
74
- // (mousedown on exit + mouseup on canvas = click on common ancestor)
72
+ // Close menu when clicking/tapping outside — use mousedown instead of
73
+ // click to avoid being triggered by the click synthesized from a
74
+ // drag-and-drop (mousedown on exit + mouseup on canvas = click on
75
+ // common ancestor). Also listen for touchstart for touch devices.
75
76
  const handleClickOutside = (e) => {
76
77
  if (this.open && !this.contains(e.target)) {
77
78
  this.close();
78
79
  }
79
80
  };
80
81
  document.addEventListener('mousedown', handleClickOutside);
82
+ document.addEventListener('touchstart', handleClickOutside, {
83
+ passive: true
84
+ });
81
85
  // Store cleanup function
82
86
  this._clickOutsideHandler = handleClickOutside;
83
87
  }
@@ -85,6 +89,7 @@ export class CanvasMenu extends RapidElement {
85
89
  super.disconnectedCallback();
86
90
  if (this._clickOutsideHandler) {
87
91
  document.removeEventListener('mousedown', this._clickOutsideHandler);
92
+ document.removeEventListener('touchstart', this._clickOutsideHandler);
88
93
  }
89
94
  }
90
95
  show(x, y, clickPosition, showStickyNote = true, showReflow = false, showWaitForResponse = true) {
@@ -1 +1 @@
1
- {"version":3,"file":"CanvasMenu.js","sourceRoot":"","sources":["../../../src/flow/CanvasMenu.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, PropertyValueMap, TemplateResult } from 'lit';\nimport { property, state } from 'lit/decorators.js';\nimport { RapidElement } from '../RapidElement';\nimport { CustomEventType } from '../interfaces';\n\n/**\n * Event detail for canvas menu selection\n */\nexport interface CanvasMenuSelection {\n action:\n | 'sticky'\n | 'action'\n | 'split'\n | 'send_msg'\n | 'wait_for_response'\n | 'reflow';\n position: { x: number; y: number };\n}\n\n/**\n * CanvasMenu - A popup menu for adding items to the flow canvas\n * Displayed when double-clicking on empty canvas space\n */\nexport class CanvasMenu extends RapidElement {\n static get styles() {\n return css`\n :host {\n position: fixed;\n z-index: 10000;\n display: block;\n pointer-events: none;\n }\n\n .menu {\n position: fixed;\n background: white;\n border-radius: var(--curvature);\n box-shadow: var(--dropdown-shadow);\n padding: 0.5em 0;\n width: 265px;\n pointer-events: auto;\n }\n\n .menu-item {\n padding: 0.75em 1.5em;\n cursor: pointer;\n display: flex;\n align-items: flex-start;\n gap: 0.75em;\n transition: background-color 0.15s ease;\n }\n\n .menu-item:hover {\n background-color: rgba(0, 0, 0, 0.05);\n }\n\n .menu-item temba-icon {\n --icon-color: var(--color-text);\n }\n\n .menu-item-title {\n font-weight: 500;\n font-size: 1rem;\n color: var(--color-text-dark);\n }\n\n .divider {\n height: 1px;\n background: rgba(0, 0, 0, 0.1);\n margin: 0.5em 0;\n }\n `;\n }\n\n @property({ type: Number })\n public x = 0;\n\n @property({ type: Number })\n public y = 0;\n\n @property({ type: Boolean })\n public open = false;\n\n @property({ type: Boolean })\n public showStickyNote = true;\n\n @property({ type: Boolean })\n public showWaitForResponse = true;\n\n @property({ type: Boolean })\n public showReflow = false;\n\n @state()\n private clickPosition = { x: 0, y: 0 };\n\n protected firstUpdated(\n _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.firstUpdated(_changedProperties);\n\n // Close menu when clicking outside — use mousedown instead of click\n // to avoid being triggered by the click synthesized from a drag-and-drop\n // (mousedown on exit + mouseup on canvas = click on common ancestor)\n const handleClickOutside = (e: MouseEvent) => {\n if (this.open && !this.contains(e.target as Node)) {\n this.close();\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n\n // Store cleanup function\n (this as any)._clickOutsideHandler = handleClickOutside;\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n if ((this as any)._clickOutsideHandler) {\n document.removeEventListener(\n 'mousedown',\n (this as any)._clickOutsideHandler\n );\n }\n }\n\n public show(\n x: number,\n y: number,\n clickPosition: { x: number; y: number },\n showStickyNote: boolean = true,\n showReflow: boolean = false,\n showWaitForResponse: boolean = true\n ) {\n this.x = x;\n this.y = y;\n this.clickPosition = clickPosition;\n this.showStickyNote = showStickyNote;\n this.showReflow = showReflow;\n this.showWaitForResponse = showWaitForResponse;\n this.open = true;\n\n // Adjust position after menu renders to ensure it fits on screen\n requestAnimationFrame(() => {\n this.adjustPosition();\n });\n }\n\n private adjustPosition(): void {\n const menuElement = this.shadowRoot?.querySelector('.menu') as HTMLElement;\n if (!menuElement) return;\n\n const menuRect = menuElement.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const margin = 10; // margin from viewport edges\n\n let adjustedX = this.x;\n let adjustedY = this.y;\n\n // Check if menu goes off the right edge\n if (this.x + menuRect.width + margin > viewportWidth) {\n adjustedX = viewportWidth - menuRect.width - margin;\n }\n\n // Check if menu goes off the bottom edge\n if (this.y + menuRect.height + margin > viewportHeight) {\n adjustedY = viewportHeight - menuRect.height - margin;\n }\n\n // Update position if needed\n if (adjustedX !== this.x || adjustedY !== this.y) {\n this.x = adjustedX;\n this.y = adjustedY;\n }\n }\n\n public close(fireCanceledEvent: boolean = true) {\n if (this.open) {\n this.open = false;\n // Fire close event so parent can clean up, but only if not from a selection\n if (fireCanceledEvent) {\n this.fireCustomEvent(CustomEventType.Canceled, {});\n }\n }\n }\n\n private handleMenuItemClick(action: CanvasMenuSelection['action']) {\n this.fireCustomEvent(CustomEventType.Selection, {\n action,\n position: this.clickPosition\n } as CanvasMenuSelection);\n // Close without firing canceled event since we made a selection\n this.close(false);\n }\n\n public render(): TemplateResult {\n if (!this.open) {\n return html``;\n }\n\n return html`\n <div class=\"menu\" style=\"left: ${this.x}px; top: ${this.y}px;\">\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('send_msg')}\n >\n <temba-icon name=\"send\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Send Message</div>\n </div>\n\n ${this.showWaitForResponse\n ? html`\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('wait_for_response')}\n >\n <temba-icon name=\"message\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Wait for Response</div>\n </div>\n `\n : ''}\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('action')}\n >\n <temba-icon name=\"action\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Add Action</div>\n </div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('split')}\n >\n <temba-icon name=\"split\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Add Split</div>\n </div>\n\n ${this.showStickyNote\n ? html`\n <div class=\"divider\"></div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('sticky')}\n >\n <temba-icon name=\"note\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Add Sticky Note</div>\n </div>\n `\n : ''}\n ${this.showReflow\n ? html`\n <div class=\"divider\"></div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('reflow')}\n >\n <temba-icon name=\"flow\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Reflow</div>\n </div>\n `\n : ''}\n </div>\n `;\n }\n}\n"]}
1
+ {"version":3,"file":"CanvasMenu.js","sourceRoot":"","sources":["../../../src/flow/CanvasMenu.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, PropertyValueMap, TemplateResult } from 'lit';\nimport { property, state } from 'lit/decorators.js';\nimport { RapidElement } from '../RapidElement';\nimport { CustomEventType } from '../interfaces';\n\n/**\n * Event detail for canvas menu selection\n */\nexport interface CanvasMenuSelection {\n action:\n | 'sticky'\n | 'action'\n | 'split'\n | 'send_msg'\n | 'wait_for_response'\n | 'reflow';\n position: { x: number; y: number };\n}\n\n/**\n * CanvasMenu - A popup menu for adding items to the flow canvas\n * Displayed when double-clicking on empty canvas space\n */\nexport class CanvasMenu extends RapidElement {\n static get styles() {\n return css`\n :host {\n position: fixed;\n z-index: 10000;\n display: block;\n pointer-events: none;\n }\n\n .menu {\n position: fixed;\n background: white;\n border-radius: var(--curvature);\n box-shadow: var(--dropdown-shadow);\n padding: 0.5em 0;\n width: 265px;\n pointer-events: auto;\n }\n\n .menu-item {\n padding: 0.75em 1.5em;\n cursor: pointer;\n display: flex;\n align-items: flex-start;\n gap: 0.75em;\n transition: background-color 0.15s ease;\n }\n\n .menu-item:hover {\n background-color: rgba(0, 0, 0, 0.05);\n }\n\n .menu-item temba-icon {\n --icon-color: var(--color-text);\n }\n\n .menu-item-title {\n font-weight: 500;\n font-size: 1rem;\n color: var(--color-text-dark);\n }\n\n .divider {\n height: 1px;\n background: rgba(0, 0, 0, 0.1);\n margin: 0.5em 0;\n }\n `;\n }\n\n @property({ type: Number })\n public x = 0;\n\n @property({ type: Number })\n public y = 0;\n\n @property({ type: Boolean })\n public open = false;\n\n @property({ type: Boolean })\n public showStickyNote = true;\n\n @property({ type: Boolean })\n public showWaitForResponse = true;\n\n @property({ type: Boolean })\n public showReflow = false;\n\n @state()\n private clickPosition = { x: 0, y: 0 };\n\n protected firstUpdated(\n _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.firstUpdated(_changedProperties);\n\n // Close menu when clicking/tapping outside — use mousedown instead of\n // click to avoid being triggered by the click synthesized from a\n // drag-and-drop (mousedown on exit + mouseup on canvas = click on\n // common ancestor). Also listen for touchstart for touch devices.\n const handleClickOutside = (e: MouseEvent | TouchEvent) => {\n if (this.open && !this.contains(e.target as Node)) {\n this.close();\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n document.addEventListener('touchstart', handleClickOutside, {\n passive: true\n });\n\n // Store cleanup function\n (this as any)._clickOutsideHandler = handleClickOutside;\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n if ((this as any)._clickOutsideHandler) {\n document.removeEventListener(\n 'mousedown',\n (this as any)._clickOutsideHandler\n );\n document.removeEventListener(\n 'touchstart',\n (this as any)._clickOutsideHandler\n );\n }\n }\n\n public show(\n x: number,\n y: number,\n clickPosition: { x: number; y: number },\n showStickyNote: boolean = true,\n showReflow: boolean = false,\n showWaitForResponse: boolean = true\n ) {\n this.x = x;\n this.y = y;\n this.clickPosition = clickPosition;\n this.showStickyNote = showStickyNote;\n this.showReflow = showReflow;\n this.showWaitForResponse = showWaitForResponse;\n this.open = true;\n\n // Adjust position after menu renders to ensure it fits on screen\n requestAnimationFrame(() => {\n this.adjustPosition();\n });\n }\n\n private adjustPosition(): void {\n const menuElement = this.shadowRoot?.querySelector('.menu') as HTMLElement;\n if (!menuElement) return;\n\n const menuRect = menuElement.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const margin = 10; // margin from viewport edges\n\n let adjustedX = this.x;\n let adjustedY = this.y;\n\n // Check if menu goes off the right edge\n if (this.x + menuRect.width + margin > viewportWidth) {\n adjustedX = viewportWidth - menuRect.width - margin;\n }\n\n // Check if menu goes off the bottom edge\n if (this.y + menuRect.height + margin > viewportHeight) {\n adjustedY = viewportHeight - menuRect.height - margin;\n }\n\n // Update position if needed\n if (adjustedX !== this.x || adjustedY !== this.y) {\n this.x = adjustedX;\n this.y = adjustedY;\n }\n }\n\n public close(fireCanceledEvent: boolean = true) {\n if (this.open) {\n this.open = false;\n // Fire close event so parent can clean up, but only if not from a selection\n if (fireCanceledEvent) {\n this.fireCustomEvent(CustomEventType.Canceled, {});\n }\n }\n }\n\n private handleMenuItemClick(action: CanvasMenuSelection['action']) {\n this.fireCustomEvent(CustomEventType.Selection, {\n action,\n position: this.clickPosition\n } as CanvasMenuSelection);\n // Close without firing canceled event since we made a selection\n this.close(false);\n }\n\n public render(): TemplateResult {\n if (!this.open) {\n return html``;\n }\n\n return html`\n <div class=\"menu\" style=\"left: ${this.x}px; top: ${this.y}px;\">\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('send_msg')}\n >\n <temba-icon name=\"send\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Send Message</div>\n </div>\n\n ${this.showWaitForResponse\n ? html`\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('wait_for_response')}\n >\n <temba-icon name=\"message\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Wait for Response</div>\n </div>\n `\n : ''}\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('action')}\n >\n <temba-icon name=\"action\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Add Action</div>\n </div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('split')}\n >\n <temba-icon name=\"split\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Add Split</div>\n </div>\n\n ${this.showStickyNote\n ? html`\n <div class=\"divider\"></div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('sticky')}\n >\n <temba-icon name=\"note\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Add Sticky Note</div>\n </div>\n `\n : ''}\n ${this.showReflow\n ? html`\n <div class=\"divider\"></div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('reflow')}\n >\n <temba-icon name=\"flow\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-title\">Reflow</div>\n </div>\n `\n : ''}\n </div>\n `;\n }\n}\n"]}
@@ -171,24 +171,20 @@ export class CanvasNode extends RapidElement {
171
171
  }
172
172
  .title-spacer {
173
173
  width: 1.8em;
174
-
175
174
  }
176
175
 
177
176
  .action:hover .drag-handle {
178
177
  visibility: visible;
179
178
  opacity: 0.7;
180
-
181
-
182
- }
183
-
184
- strong {
185
- font-weight: 500;
186
179
  }
187
180
 
188
181
  .action .drag-handle:hover {
189
182
  visibility: visible;
190
183
  opacity: 1;
191
-
184
+ }
185
+
186
+ strong {
187
+ font-weight: 500;
192
188
  }
193
189
 
194
190
  .action .cn-title,
@@ -215,6 +211,8 @@ export class CanvasNode extends RapidElement {
215
211
 
216
212
  .quick-replies {
217
213
  margin-top: 0.5em;
214
+ display: flex;
215
+ flex-wrap: wrap;
218
216
  }
219
217
 
220
218
  .quick-reply {
@@ -222,9 +220,14 @@ export class CanvasNode extends RapidElement {
222
220
  border: 1px solid #e0e0e0;
223
221
  border-radius: calc(var(--curvature) * 1.5);
224
222
  padding: 0.2em 1em;
225
- display: inline-block;
226
223
  font-size: 0.8em;
227
224
  margin: 0.2em;
225
+ flex: 0 1 auto;
226
+ min-width: 0;
227
+ max-width: 100%;
228
+ overflow: hidden;
229
+ text-overflow: ellipsis;
230
+ white-space: nowrap;
228
231
  }
229
232
 
230
233
  .router-section {
@@ -409,6 +412,22 @@ export class CanvasNode extends RapidElement {
409
412
  color: #9ca3af;
410
413
  font-size: 0.9em;
411
414
  }
415
+
416
+ /* On touch devices, always show interactive controls.
417
+ The .touch-device class is added to the editor on first touch. */
418
+ .touch-device .remove-button {
419
+ visibility: visible !important;
420
+ opacity: 0.7;
421
+ }
422
+
423
+ .touch-device .action .drag-handle {
424
+ visibility: visible !important;
425
+ opacity: 0.7;
426
+ }
427
+
428
+ .touch-device .add-action-button {
429
+ opacity: 0.8 !important;
430
+ }
412
431
  }`;
413
432
  }
414
433
  constructor() {
@@ -850,6 +869,61 @@ export class CanvasNode extends RapidElement {
850
869
  this.actionClickStartPos = null;
851
870
  this.pendingActionClick = null;
852
871
  }
872
+ /* c8 ignore start -- touch-only handlers untestable in headless Chromium */
873
+ handleActionTouchStart(event, action) {
874
+ const target = event.target;
875
+ if (target.closest('.remove-button') ||
876
+ target.closest('.drag-handle') ||
877
+ target.closest('.linked-name') ||
878
+ this.actionRemovingState.has(action.uuid)) {
879
+ return;
880
+ }
881
+ const touch = event.touches[0];
882
+ if (!touch)
883
+ return;
884
+ this.actionClickStartPos = { x: touch.clientX, y: touch.clientY };
885
+ this.pendingActionClick = { action, event: event };
886
+ }
887
+ handleActionTouchEnd(event, action) {
888
+ if (!this.pendingActionClick ||
889
+ this.pendingActionClick.action.uuid !== action.uuid) {
890
+ this.actionClickStartPos = null;
891
+ this.pendingActionClick = null;
892
+ return;
893
+ }
894
+ const target = event.target;
895
+ if (target.closest('.remove-button') ||
896
+ target.closest('.drag-handle') ||
897
+ target.closest('.linked-name') ||
898
+ this.actionRemovingState.has(action.uuid)) {
899
+ this.actionClickStartPos = null;
900
+ this.pendingActionClick = null;
901
+ return;
902
+ }
903
+ const touch = event.changedTouches[0];
904
+ if (this.actionClickStartPos && touch) {
905
+ const deltaX = touch.clientX - this.actionClickStartPos.x;
906
+ const deltaY = touch.clientY - this.actionClickStartPos.y;
907
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
908
+ const editor = this.closest('temba-flow-editor');
909
+ const editorWasDragging = editor === null || editor === void 0 ? void 0 : editor.dragging;
910
+ if (distance <= DRAG_THRESHOLD && (!editor || !editorWasDragging)) {
911
+ const actionEl = event.currentTarget;
912
+ const origin = actionEl
913
+ ? this.getTopCenter(actionEl)
914
+ : { x: touch.clientX, y: touch.clientY };
915
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
916
+ action,
917
+ nodeUuid: this.node.uuid,
918
+ originX: origin.x,
919
+ originY: origin.y
920
+ });
921
+ }
922
+ }
923
+ this.actionClickStartPos = null;
924
+ this.pendingActionClick = null;
925
+ }
926
+ /* c8 ignore stop */
853
927
  handleActionClick(event, action) {
854
928
  // This method is kept for backward compatibility but should not be used
855
929
  // The new mousedown/mouseup approach handles click vs drag properly
@@ -972,6 +1046,73 @@ export class CanvasNode extends RapidElement {
972
1046
  this.nodeClickStartPos = null;
973
1047
  this.pendingNodeClick = null;
974
1048
  }
1049
+ /* c8 ignore start -- touch-only handlers */
1050
+ handleNodeTouchStart(event) {
1051
+ const target = event.target;
1052
+ if (target.closest('.remove-button') ||
1053
+ target.closest('.exit') ||
1054
+ target.closest('.exit-wrapper') ||
1055
+ target.closest('.drag-handle') ||
1056
+ target.closest('.linked-name') ||
1057
+ this.actionRemovingState.has(this.node.uuid)) {
1058
+ return;
1059
+ }
1060
+ const touch = event.touches[0];
1061
+ if (!touch)
1062
+ return;
1063
+ this.nodeClickStartPos = { x: touch.clientX, y: touch.clientY };
1064
+ this.pendingNodeClick = { event: event };
1065
+ }
1066
+ handleNodeTouchEnd(event) {
1067
+ if (!this.pendingNodeClick) {
1068
+ this.nodeClickStartPos = null;
1069
+ this.pendingNodeClick = null;
1070
+ return;
1071
+ }
1072
+ const target = event.target;
1073
+ if (target.closest('.remove-button') ||
1074
+ target.closest('.exit') ||
1075
+ target.closest('.exit-wrapper') ||
1076
+ target.closest('.drag-handle') ||
1077
+ target.closest('.linked-name') ||
1078
+ this.actionRemovingState.has(this.node.uuid)) {
1079
+ this.nodeClickStartPos = null;
1080
+ this.pendingNodeClick = null;
1081
+ return;
1082
+ }
1083
+ const touch = event.changedTouches[0];
1084
+ if (this.nodeClickStartPos && touch) {
1085
+ const deltaX = touch.clientX - this.nodeClickStartPos.x;
1086
+ const deltaY = touch.clientY - this.nodeClickStartPos.y;
1087
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
1088
+ const editor = this.closest('temba-flow-editor');
1089
+ const editorWasDragging = editor === null || editor === void 0 ? void 0 : editor.dragging;
1090
+ if (distance <= 5 && (!editor || !editorWasDragging)) {
1091
+ if (this.node.router) {
1092
+ const origin = this.getTopCenter(this);
1093
+ if (this.node.actions && this.node.actions.length === 1) {
1094
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
1095
+ action: this.node.actions[0],
1096
+ nodeUuid: this.node.uuid,
1097
+ originX: origin.x,
1098
+ originY: origin.y
1099
+ });
1100
+ }
1101
+ else {
1102
+ this.fireCustomEvent(CustomEventType.NodeEditRequested, {
1103
+ node: this.node,
1104
+ nodeUI: this.ui,
1105
+ originX: origin.x,
1106
+ originY: origin.y
1107
+ });
1108
+ }
1109
+ }
1110
+ }
1111
+ }
1112
+ this.nodeClickStartPos = null;
1113
+ this.pendingNodeClick = null;
1114
+ }
1115
+ /* c8 ignore stop */
975
1116
  handleAddActionClick(event) {
976
1117
  event.preventDefault();
977
1118
  event.stopPropagation();
@@ -1230,6 +1371,8 @@ export class CanvasNode extends RapidElement {
1230
1371
  class="action-content ${hasIssues ? 'has-issues' : ''}"
1231
1372
  @mousedown=${(e) => !isDisabled && this.handleActionMouseDown(e, action)}
1232
1373
  @mouseup=${(e) => !isDisabled && this.handleActionMouseUp(e, action)}
1374
+ @touchstart=${(e) => !isDisabled && this.handleActionTouchStart(e, action)}
1375
+ @touchend=${(e) => !isDisabled && this.handleActionTouchEnd(e, action)}
1233
1376
  style="cursor: ${isDisabled ? 'not-allowed' : 'pointer'}"
1234
1377
  >
1235
1378
  ${this.renderTitle(config, action, index, isRemoving)}
@@ -1283,6 +1426,8 @@ export class CanvasNode extends RapidElement {
1283
1426
  class="body"
1284
1427
  @mousedown=${(e) => this.handleNodeMouseDown(e)}
1285
1428
  @mouseup=${(e) => this.handleNodeMouseUp(e)}
1429
+ @touchstart=${(e) => this.handleNodeTouchStart(e)}
1430
+ @touchend=${(e) => this.handleNodeTouchEnd(e)}
1286
1431
  style="cursor: pointer;"
1287
1432
  >
1288
1433
  ${renderClamped(html `Save as
@@ -1332,6 +1477,8 @@ export class CanvasNode extends RapidElement {
1332
1477
  })}
1333
1478
  @mousedown=${(e) => this.handleNodeMouseDown(e)}
1334
1479
  @mouseup=${(e) => this.handleNodeMouseUp(e)}
1480
+ @touchstart=${(e) => this.handleNodeTouchStart(e)}
1481
+ @touchend=${(e) => this.handleNodeTouchEnd(e)}
1335
1482
  style="cursor: pointer;"
1336
1483
  >
1337
1484
  <div class="cn-title" title="${displayName}">${displayName}</div>
@@ -1396,6 +1543,8 @@ export class CanvasNode extends RapidElement {
1396
1543
  <div
1397
1544
  @mousedown=${(e) => this.handleNodeMouseDown(e)}
1398
1545
  @mouseup=${(e) => this.handleNodeMouseUp(e)}
1546
+ @touchstart=${(e) => this.handleNodeTouchStart(e)}
1547
+ @touchend=${(e) => this.handleNodeTouchEnd(e)}
1399
1548
  style="cursor: pointer;"
1400
1549
  >
1401
1550
  ${this.renderNodeTitle(nodeConfig, this.node, this.ui, this.actionRemovingState.has(this.node.uuid))}