@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
@@ -1,4 +1,15 @@
1
1
  import { isRightClick } from './utils';
2
+ /** Extract clientX/clientY from a MouseEvent or the first touch of a TouchEvent. */
3
+ function getClientCoords(e) {
4
+ if ('touches' in e) {
5
+ const touch = e.touches[0] || e.changedTouches[0];
6
+ return { clientX: touch.clientX, clientY: touch.clientY };
7
+ }
8
+ return {
9
+ clientX: e.clientX,
10
+ clientY: e.clientY
11
+ };
12
+ }
2
13
  // Shared arrow/drag constants used by both Plumber and Editor
3
14
  export const ARROW_LENGTH = 13;
4
15
  export const ARROW_HALF_WIDTH = 6.5;
@@ -162,45 +173,62 @@ export class Plumber {
162
173
  }
163
174
  let pendingDrag = null;
164
175
  const DRAG_THRESHOLD = 5;
165
- const onMouseDown = (e) => {
166
- if (isRightClick(e))
176
+ const beginPendingDrag = (e) => {
177
+ if ('button' in e && isRightClick(e))
167
178
  return;
168
179
  // Don't start drag from exit if it already has a connection —
169
180
  // existing connections are picked up from the arrowhead instead
170
181
  if (this.connections.has(exitId))
171
182
  return;
172
- const startX = e.clientX;
173
- const startY = e.clientY;
183
+ const isTouch = 'touches' in e;
184
+ if (isTouch)
185
+ e.preventDefault();
186
+ const { clientX: startX, clientY: startY } = getClientCoords(e);
174
187
  const nodeEl = element.closest('temba-flow-node');
175
188
  const scope = (nodeEl === null || nodeEl === void 0 ? void 0 : nodeEl.getAttribute('uuid')) || '';
176
189
  const originalTargetId = null;
177
190
  const onMove = (me) => {
178
- const dx = me.clientX - startX;
179
- const dy = me.clientY - startY;
191
+ const { clientX, clientY } = getClientCoords(me);
192
+ const dx = clientX - startX;
193
+ const dy = clientY - startY;
180
194
  if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
181
195
  // Exceeded threshold — start actual drag
182
- document.removeEventListener('mousemove', onMove);
183
- document.removeEventListener('mouseup', onUp);
184
- pendingDrag = null;
196
+ removePendingListeners();
185
197
  this.startDrag(exitId, scope, originalTargetId, me);
186
198
  }
187
199
  };
188
200
  const onUp = () => {
189
- // Mouse released without dragging — let click handler fire
201
+ // Released without dragging — let click handler fire
202
+ removePendingListeners();
203
+ };
204
+ const removePendingListeners = () => {
190
205
  document.removeEventListener('mousemove', onMove);
191
206
  document.removeEventListener('mouseup', onUp);
207
+ document.removeEventListener('touchmove', onMove);
208
+ document.removeEventListener('touchend', onUp);
209
+ document.removeEventListener('touchcancel', onUp);
192
210
  pendingDrag = null;
193
211
  };
194
212
  document.addEventListener('mousemove', onMove);
195
213
  document.addEventListener('mouseup', onUp);
196
- pendingDrag = { startX, startY, onMove, onUp };
214
+ document.addEventListener('touchmove', onMove, { passive: false });
215
+ document.addEventListener('touchend', onUp);
216
+ document.addEventListener('touchcancel', onUp);
217
+ pendingDrag = { startX, startY, onMove, onUp, isTouch };
197
218
  };
198
- element.addEventListener('mousedown', onMouseDown);
219
+ element.addEventListener('mousedown', beginPendingDrag);
220
+ element.addEventListener('touchstart', beginPendingDrag, {
221
+ passive: false
222
+ });
199
223
  this.sources.set(exitId, () => {
200
- element.removeEventListener('mousedown', onMouseDown);
224
+ element.removeEventListener('mousedown', beginPendingDrag);
225
+ element.removeEventListener('touchstart', beginPendingDrag);
201
226
  if (pendingDrag) {
202
227
  document.removeEventListener('mousemove', pendingDrag.onMove);
203
228
  document.removeEventListener('mouseup', pendingDrag.onUp);
229
+ document.removeEventListener('touchmove', pendingDrag.onMove);
230
+ document.removeEventListener('touchend', pendingDrag.onUp);
231
+ document.removeEventListener('touchcancel', pendingDrag.onUp);
204
232
  pendingDrag = null;
205
233
  }
206
234
  });
@@ -489,29 +517,43 @@ export class Plumber {
489
517
  });
490
518
  // Make arrowhead draggable for picking up existing connections
491
519
  const DRAG_THRESHOLD = 5;
492
- const onArrowMouseDown = (e) => {
493
- if (isRightClick(e))
520
+ const onArrowDown = (e) => {
521
+ if ('button' in e && isRightClick(e))
494
522
  return;
495
523
  e.stopPropagation();
496
- const startX = e.clientX;
497
- const startY = e.clientY;
524
+ if ('touches' in e)
525
+ e.preventDefault();
526
+ const { clientX: startX, clientY: startY } = getClientCoords(e);
498
527
  const onMove = (me) => {
499
- const dx = me.clientX - startX;
500
- const dy = me.clientY - startY;
528
+ const { clientX, clientY } = getClientCoords(me);
529
+ const dx = clientX - startX;
530
+ const dy = clientY - startY;
501
531
  if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
502
532
  document.removeEventListener('mousemove', onMove);
503
533
  document.removeEventListener('mouseup', onUp);
534
+ document.removeEventListener('touchmove', onMove);
535
+ document.removeEventListener('touchend', onUp);
536
+ document.removeEventListener('touchcancel', onUp);
504
537
  this.startDrag(exitId, scope, toId, me);
505
538
  }
506
539
  };
507
540
  const onUp = () => {
508
541
  document.removeEventListener('mousemove', onMove);
509
542
  document.removeEventListener('mouseup', onUp);
543
+ document.removeEventListener('touchmove', onMove);
544
+ document.removeEventListener('touchend', onUp);
545
+ document.removeEventListener('touchcancel', onUp);
510
546
  };
511
547
  document.addEventListener('mousemove', onMove);
512
548
  document.addEventListener('mouseup', onUp);
549
+ document.addEventListener('touchmove', onMove, { passive: false });
550
+ document.addEventListener('touchend', onUp);
551
+ document.addEventListener('touchcancel', onUp);
513
552
  };
514
- arrowEl.addEventListener('mousedown', onArrowMouseDown);
553
+ arrowEl.addEventListener('mousedown', onArrowDown);
554
+ arrowEl.addEventListener('touchstart', onArrowDown, { passive: false });
555
+ pathEl.addEventListener('mousedown', onArrowDown);
556
+ pathEl.addEventListener('touchstart', onArrowDown, { passive: false });
515
557
  // Mark the exit element as connected
516
558
  const exitEl = document.getElementById(exitId);
517
559
  if (exitEl) {
@@ -891,8 +933,17 @@ export class Plumber {
891
933
  }
892
934
  // --- Drag-and-drop ---
893
935
  startDrag(exitId, scope, originalTargetId, e) {
894
- // Remove existing connection SVG for this exit (the connection is being dragged away)
895
- this.removeConnectionSVG(exitId);
936
+ // Hide (don't remove) the existing connection SVG while dragging.
937
+ // On iOS Safari, removing the element that received the original
938
+ // touchstart can trigger touchcancel, prematurely ending the drag.
939
+ // We defer removal until the drag ends.
940
+ const oldConn = this.connections.get(exitId);
941
+ if (oldConn) {
942
+ oldConn.svgEl.style.display = 'none';
943
+ const overlay = this.overlays.get(exitId);
944
+ if (overlay)
945
+ overlay.style.display = 'none';
946
+ }
896
947
  const { svgEl, pathEl, arrowEl } = this.createSVGElement();
897
948
  svgEl.classList.add('dragging');
898
949
  // Ensure the drag SVG never intercepts mouse events (e.g. hover detection on nodes)
@@ -945,22 +996,30 @@ export class Plumber {
945
996
  }
946
997
  };
947
998
  // Initial path to cursor (convert viewport to canvas coordinates)
948
- const cursorX = this.toCanvas(e.clientX - canvasRect.left);
949
- const cursorY = this.toCanvas(e.clientY - canvasRect.top);
999
+ const { clientX: initX, clientY: initY } = getClientCoords(e);
1000
+ const cursorX = this.toCanvas(initX - canvasRect.left);
1001
+ const cursorY = this.toCanvas(initY - canvasRect.top);
950
1002
  updateDragPath(cursorX, cursorY);
951
1003
  this.connectionDragging = true;
952
1004
  const onMove = (me) => {
1005
+ if ('touches' in me)
1006
+ me.preventDefault();
953
1007
  // Re-read canvasRect each move since scroll may have changed
954
1008
  const rect = this.canvas.getBoundingClientRect();
955
- const cx = this.toCanvas(me.clientX - rect.left);
956
- const cy = this.toCanvas(me.clientY - rect.top);
1009
+ const { clientX, clientY } = getClientCoords(me);
1010
+ const cx = this.toCanvas(clientX - rect.left);
1011
+ const cy = this.toCanvas(clientY - rect.top);
957
1012
  updateDragPath(cx, cy);
958
1013
  };
959
1014
  const onUp = (_me) => {
960
1015
  document.removeEventListener('mousemove', onMove);
961
1016
  document.removeEventListener('mouseup', onUp);
962
- // Remove the drag SVG
1017
+ document.removeEventListener('touchmove', onMove);
1018
+ document.removeEventListener('touchend', onUp);
1019
+ document.removeEventListener('touchcancel', onUp);
1020
+ // Remove the drag SVG and the hidden old connection SVG
963
1021
  svgEl.remove();
1022
+ this.removeConnectionSVG(exitId);
964
1023
  this.connectionDragging = false;
965
1024
  this.dragState = null;
966
1025
  // Fire abort event so Editor can handle connection logic
@@ -973,6 +1032,9 @@ export class Plumber {
973
1032
  };
974
1033
  document.addEventListener('mousemove', onMove);
975
1034
  document.addEventListener('mouseup', onUp);
1035
+ document.addEventListener('touchmove', onMove, { passive: false });
1036
+ document.addEventListener('touchend', onUp);
1037
+ document.addEventListener('touchcancel', onUp);
976
1038
  this.dragState = {
977
1039
  sourceId: exitId,
978
1040
  scope,