@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/Plumber.ts
CHANGED
|
@@ -2,6 +2,21 @@ import { isRightClick } from './utils';
|
|
|
2
2
|
|
|
3
3
|
export type TargetFace = 'top' | 'left' | 'right';
|
|
4
4
|
|
|
5
|
+
/** Extract clientX/clientY from a MouseEvent or the first touch of a TouchEvent. */
|
|
6
|
+
function getClientCoords(e: MouseEvent | TouchEvent): {
|
|
7
|
+
clientX: number;
|
|
8
|
+
clientY: number;
|
|
9
|
+
} {
|
|
10
|
+
if ('touches' in e) {
|
|
11
|
+
const touch = e.touches[0] || (e as TouchEvent).changedTouches[0];
|
|
12
|
+
return { clientX: touch.clientX, clientY: touch.clientY };
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
clientX: (e as MouseEvent).clientX,
|
|
16
|
+
clientY: (e as MouseEvent).clientY
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
// Shared arrow/drag constants used by both Plumber and Editor
|
|
6
21
|
export const ARROW_LENGTH = 13;
|
|
7
22
|
export const ARROW_HALF_WIDTH = 6.5;
|
|
@@ -33,8 +48,8 @@ interface DragState {
|
|
|
33
48
|
svgEl: SVGSVGElement;
|
|
34
49
|
pathEl: SVGPathElement;
|
|
35
50
|
arrowEl: SVGPolygonElement;
|
|
36
|
-
onMove: (e: MouseEvent) => void;
|
|
37
|
-
onUp: (e: MouseEvent) => void;
|
|
51
|
+
onMove: (e: MouseEvent | TouchEvent) => void;
|
|
52
|
+
onUp: (e: MouseEvent | TouchEvent) => void;
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
/**
|
|
@@ -242,56 +257,75 @@ export class Plumber {
|
|
|
242
257
|
let pendingDrag: {
|
|
243
258
|
startX: number;
|
|
244
259
|
startY: number;
|
|
245
|
-
onMove: (e: MouseEvent) => void;
|
|
246
|
-
onUp: (e: MouseEvent) => void;
|
|
260
|
+
onMove: (e: MouseEvent | TouchEvent) => void;
|
|
261
|
+
onUp: (e: MouseEvent | TouchEvent) => void;
|
|
262
|
+
isTouch: boolean;
|
|
247
263
|
} | null = null;
|
|
248
264
|
|
|
249
265
|
const DRAG_THRESHOLD = 5;
|
|
250
266
|
|
|
251
|
-
const
|
|
252
|
-
if (isRightClick(e)) return;
|
|
267
|
+
const beginPendingDrag = (e: MouseEvent | TouchEvent) => {
|
|
268
|
+
if ('button' in e && isRightClick(e)) return;
|
|
253
269
|
|
|
254
270
|
// Don't start drag from exit if it already has a connection —
|
|
255
271
|
// existing connections are picked up from the arrowhead instead
|
|
256
272
|
if (this.connections.has(exitId)) return;
|
|
257
273
|
|
|
258
|
-
const
|
|
259
|
-
|
|
274
|
+
const isTouch = 'touches' in e;
|
|
275
|
+
if (isTouch) e.preventDefault();
|
|
276
|
+
|
|
277
|
+
const { clientX: startX, clientY: startY } = getClientCoords(e);
|
|
260
278
|
|
|
261
279
|
const nodeEl = element.closest('temba-flow-node');
|
|
262
280
|
const scope = nodeEl?.getAttribute('uuid') || '';
|
|
263
281
|
const originalTargetId: string | null = null;
|
|
264
282
|
|
|
265
|
-
const onMove = (me: MouseEvent) => {
|
|
266
|
-
const
|
|
267
|
-
const
|
|
283
|
+
const onMove = (me: MouseEvent | TouchEvent) => {
|
|
284
|
+
const { clientX, clientY } = getClientCoords(me);
|
|
285
|
+
const dx = clientX - startX;
|
|
286
|
+
const dy = clientY - startY;
|
|
268
287
|
if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
|
|
269
288
|
// Exceeded threshold — start actual drag
|
|
270
|
-
|
|
271
|
-
document.removeEventListener('mouseup', onUp);
|
|
272
|
-
pendingDrag = null;
|
|
289
|
+
removePendingListeners();
|
|
273
290
|
this.startDrag(exitId, scope, originalTargetId, me);
|
|
274
291
|
}
|
|
275
292
|
};
|
|
276
293
|
|
|
277
294
|
const onUp = () => {
|
|
278
|
-
//
|
|
295
|
+
// Released without dragging — let click handler fire
|
|
296
|
+
removePendingListeners();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const removePendingListeners = () => {
|
|
279
300
|
document.removeEventListener('mousemove', onMove);
|
|
280
301
|
document.removeEventListener('mouseup', onUp);
|
|
302
|
+
document.removeEventListener('touchmove', onMove);
|
|
303
|
+
document.removeEventListener('touchend', onUp);
|
|
304
|
+
document.removeEventListener('touchcancel', onUp);
|
|
281
305
|
pendingDrag = null;
|
|
282
306
|
};
|
|
283
307
|
|
|
284
308
|
document.addEventListener('mousemove', onMove);
|
|
285
309
|
document.addEventListener('mouseup', onUp);
|
|
286
|
-
|
|
310
|
+
document.addEventListener('touchmove', onMove, { passive: false });
|
|
311
|
+
document.addEventListener('touchend', onUp);
|
|
312
|
+
document.addEventListener('touchcancel', onUp);
|
|
313
|
+
pendingDrag = { startX, startY, onMove, onUp, isTouch };
|
|
287
314
|
};
|
|
288
315
|
|
|
289
|
-
element.addEventListener('mousedown',
|
|
316
|
+
element.addEventListener('mousedown', beginPendingDrag);
|
|
317
|
+
element.addEventListener('touchstart', beginPendingDrag, {
|
|
318
|
+
passive: false
|
|
319
|
+
});
|
|
290
320
|
this.sources.set(exitId, () => {
|
|
291
|
-
element.removeEventListener('mousedown',
|
|
321
|
+
element.removeEventListener('mousedown', beginPendingDrag);
|
|
322
|
+
element.removeEventListener('touchstart', beginPendingDrag);
|
|
292
323
|
if (pendingDrag) {
|
|
293
324
|
document.removeEventListener('mousemove', pendingDrag.onMove);
|
|
294
325
|
document.removeEventListener('mouseup', pendingDrag.onUp);
|
|
326
|
+
document.removeEventListener('touchmove', pendingDrag.onMove);
|
|
327
|
+
document.removeEventListener('touchend', pendingDrag.onUp);
|
|
328
|
+
document.removeEventListener('touchcancel', pendingDrag.onUp);
|
|
295
329
|
pendingDrag = null;
|
|
296
330
|
}
|
|
297
331
|
});
|
|
@@ -714,19 +748,23 @@ export class Plumber {
|
|
|
714
748
|
|
|
715
749
|
// Make arrowhead draggable for picking up existing connections
|
|
716
750
|
const DRAG_THRESHOLD = 5;
|
|
717
|
-
const
|
|
718
|
-
if (isRightClick(e)) return;
|
|
751
|
+
const onArrowDown = (e: MouseEvent | TouchEvent) => {
|
|
752
|
+
if ('button' in e && isRightClick(e)) return;
|
|
719
753
|
e.stopPropagation();
|
|
754
|
+
if ('touches' in e) e.preventDefault();
|
|
720
755
|
|
|
721
|
-
const startX = e
|
|
722
|
-
const startY = e.clientY;
|
|
756
|
+
const { clientX: startX, clientY: startY } = getClientCoords(e);
|
|
723
757
|
|
|
724
|
-
const onMove = (me: MouseEvent) => {
|
|
725
|
-
const
|
|
726
|
-
const
|
|
758
|
+
const onMove = (me: MouseEvent | TouchEvent) => {
|
|
759
|
+
const { clientX, clientY } = getClientCoords(me);
|
|
760
|
+
const dx = clientX - startX;
|
|
761
|
+
const dy = clientY - startY;
|
|
727
762
|
if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
|
|
728
763
|
document.removeEventListener('mousemove', onMove);
|
|
729
764
|
document.removeEventListener('mouseup', onUp);
|
|
765
|
+
document.removeEventListener('touchmove', onMove);
|
|
766
|
+
document.removeEventListener('touchend', onUp);
|
|
767
|
+
document.removeEventListener('touchcancel', onUp);
|
|
730
768
|
this.startDrag(exitId, scope, toId, me);
|
|
731
769
|
}
|
|
732
770
|
};
|
|
@@ -734,12 +772,21 @@ export class Plumber {
|
|
|
734
772
|
const onUp = () => {
|
|
735
773
|
document.removeEventListener('mousemove', onMove);
|
|
736
774
|
document.removeEventListener('mouseup', onUp);
|
|
775
|
+
document.removeEventListener('touchmove', onMove);
|
|
776
|
+
document.removeEventListener('touchend', onUp);
|
|
777
|
+
document.removeEventListener('touchcancel', onUp);
|
|
737
778
|
};
|
|
738
779
|
|
|
739
780
|
document.addEventListener('mousemove', onMove);
|
|
740
781
|
document.addEventListener('mouseup', onUp);
|
|
782
|
+
document.addEventListener('touchmove', onMove, { passive: false });
|
|
783
|
+
document.addEventListener('touchend', onUp);
|
|
784
|
+
document.addEventListener('touchcancel', onUp);
|
|
741
785
|
};
|
|
742
|
-
arrowEl.addEventListener('mousedown',
|
|
786
|
+
arrowEl.addEventListener('mousedown', onArrowDown);
|
|
787
|
+
arrowEl.addEventListener('touchstart', onArrowDown, { passive: false });
|
|
788
|
+
pathEl.addEventListener('mousedown', onArrowDown);
|
|
789
|
+
pathEl.addEventListener('touchstart', onArrowDown, { passive: false });
|
|
743
790
|
|
|
744
791
|
// Mark the exit element as connected
|
|
745
792
|
const exitEl = document.getElementById(exitId);
|
|
@@ -1087,7 +1134,9 @@ export class Plumber {
|
|
|
1087
1134
|
const contactUuid = target.getAttribute('data-uuid');
|
|
1088
1135
|
if (contactUuid) {
|
|
1089
1136
|
this.editor.fireCustomEvent('temba-contact-clicked', {
|
|
1090
|
-
uuid: contactUuid
|
|
1137
|
+
uuid: contactUuid,
|
|
1138
|
+
metaKey: e.metaKey,
|
|
1139
|
+
ctrlKey: e.ctrlKey
|
|
1091
1140
|
});
|
|
1092
1141
|
}
|
|
1093
1142
|
}
|
|
@@ -1200,10 +1249,18 @@ export class Plumber {
|
|
|
1200
1249
|
exitId: string,
|
|
1201
1250
|
scope: string,
|
|
1202
1251
|
originalTargetId: string | null,
|
|
1203
|
-
e: MouseEvent
|
|
1252
|
+
e: MouseEvent | TouchEvent
|
|
1204
1253
|
) {
|
|
1205
|
-
//
|
|
1206
|
-
|
|
1254
|
+
// Hide (don't remove) the existing connection SVG while dragging.
|
|
1255
|
+
// On iOS Safari, removing the element that received the original
|
|
1256
|
+
// touchstart can trigger touchcancel, prematurely ending the drag.
|
|
1257
|
+
// We defer removal until the drag ends.
|
|
1258
|
+
const oldConn = this.connections.get(exitId);
|
|
1259
|
+
if (oldConn) {
|
|
1260
|
+
oldConn.svgEl.style.display = 'none';
|
|
1261
|
+
const overlay = this.overlays.get(exitId);
|
|
1262
|
+
if (overlay) overlay.style.display = 'none';
|
|
1263
|
+
}
|
|
1207
1264
|
|
|
1208
1265
|
const { svgEl, pathEl, arrowEl } = this.createSVGElement();
|
|
1209
1266
|
svgEl.classList.add('dragging');
|
|
@@ -1281,26 +1338,33 @@ export class Plumber {
|
|
|
1281
1338
|
};
|
|
1282
1339
|
|
|
1283
1340
|
// Initial path to cursor (convert viewport to canvas coordinates)
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1341
|
+
const { clientX: initX, clientY: initY } = getClientCoords(e);
|
|
1342
|
+
const cursorX = this.toCanvas(initX - canvasRect.left);
|
|
1343
|
+
const cursorY = this.toCanvas(initY - canvasRect.top);
|
|
1286
1344
|
updateDragPath(cursorX, cursorY);
|
|
1287
1345
|
|
|
1288
1346
|
this.connectionDragging = true;
|
|
1289
1347
|
|
|
1290
|
-
const onMove = (me: MouseEvent) => {
|
|
1348
|
+
const onMove = (me: MouseEvent | TouchEvent) => {
|
|
1349
|
+
if ('touches' in me) me.preventDefault();
|
|
1291
1350
|
// Re-read canvasRect each move since scroll may have changed
|
|
1292
1351
|
const rect = this.canvas.getBoundingClientRect();
|
|
1293
|
-
const
|
|
1294
|
-
const
|
|
1352
|
+
const { clientX, clientY } = getClientCoords(me);
|
|
1353
|
+
const cx = this.toCanvas(clientX - rect.left);
|
|
1354
|
+
const cy = this.toCanvas(clientY - rect.top);
|
|
1295
1355
|
updateDragPath(cx, cy);
|
|
1296
1356
|
};
|
|
1297
1357
|
|
|
1298
|
-
const onUp = (_me: MouseEvent) => {
|
|
1358
|
+
const onUp = (_me: MouseEvent | TouchEvent) => {
|
|
1299
1359
|
document.removeEventListener('mousemove', onMove);
|
|
1300
1360
|
document.removeEventListener('mouseup', onUp);
|
|
1361
|
+
document.removeEventListener('touchmove', onMove);
|
|
1362
|
+
document.removeEventListener('touchend', onUp);
|
|
1363
|
+
document.removeEventListener('touchcancel', onUp);
|
|
1301
1364
|
|
|
1302
|
-
// Remove the drag SVG
|
|
1365
|
+
// Remove the drag SVG and the hidden old connection SVG
|
|
1303
1366
|
svgEl.remove();
|
|
1367
|
+
this.removeConnectionSVG(exitId);
|
|
1304
1368
|
this.connectionDragging = false;
|
|
1305
1369
|
this.dragState = null;
|
|
1306
1370
|
|
|
@@ -1315,6 +1379,9 @@ export class Plumber {
|
|
|
1315
1379
|
|
|
1316
1380
|
document.addEventListener('mousemove', onMove);
|
|
1317
1381
|
document.addEventListener('mouseup', onUp);
|
|
1382
|
+
document.addEventListener('touchmove', onMove, { passive: false });
|
|
1383
|
+
document.addEventListener('touchend', onUp);
|
|
1384
|
+
document.addEventListener('touchcancel', onUp);
|
|
1318
1385
|
|
|
1319
1386
|
this.dragState = {
|
|
1320
1387
|
sourceId: exitId,
|
package/src/flow/StickyNote.ts
CHANGED
|
@@ -21,6 +21,11 @@ export class StickyNote extends RapidElement {
|
|
|
21
21
|
@property({ type: Boolean })
|
|
22
22
|
private colorPickerExpanded = false;
|
|
23
23
|
|
|
24
|
+
// On touch devices, contenteditable starts false to prevent Apple Pencil
|
|
25
|
+
// Scribble from hijacking touches. It is set to true on explicit tap.
|
|
26
|
+
private isTouchDevice = navigator.maxTouchPoints > 0;
|
|
27
|
+
private editingField: HTMLElement | null = null;
|
|
28
|
+
|
|
24
29
|
@fromStore(zustand, (state: AppState) => state.isTranslating)
|
|
25
30
|
private isTranslating!: boolean;
|
|
26
31
|
|
|
@@ -289,6 +294,7 @@ export class StickyNote extends RapidElement {
|
|
|
289
294
|
}
|
|
290
295
|
|
|
291
296
|
private handleTitleBlur(event: FocusEvent): void {
|
|
297
|
+
this.handleContentBlurForTouch(event);
|
|
292
298
|
const target = event.target as HTMLElement;
|
|
293
299
|
const newTitle = target.textContent || '';
|
|
294
300
|
|
|
@@ -304,6 +310,7 @@ export class StickyNote extends RapidElement {
|
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
private handleBodyBlur(event: FocusEvent): void {
|
|
313
|
+
this.handleContentBlurForTouch(event);
|
|
307
314
|
const target = event.target as HTMLElement;
|
|
308
315
|
const newBody = target.innerText || '';
|
|
309
316
|
|
|
@@ -318,6 +325,46 @@ export class StickyNote extends RapidElement {
|
|
|
318
325
|
this.requestUpdate();
|
|
319
326
|
}
|
|
320
327
|
|
|
328
|
+
/* c8 ignore start -- touch-only handlers untestable in headless Chromium */
|
|
329
|
+
private handleDragHandleTouchStart(event: TouchEvent): void {
|
|
330
|
+
// Prevent Apple Pencil Scribble from activating on the adjacent
|
|
331
|
+
// contenteditable fields when touching/dragging the handle.
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* On touch devices, contenteditable is off by default. A tap on the
|
|
337
|
+
* title or body enables it and focuses the element for editing.
|
|
338
|
+
*/
|
|
339
|
+
private handleContentTap(event: TouchEvent): void {
|
|
340
|
+
if (!this.isTouchDevice) return;
|
|
341
|
+
const target = event.target as HTMLElement;
|
|
342
|
+
if (
|
|
343
|
+
!target.classList.contains('sticky-title') &&
|
|
344
|
+
!target.classList.contains('sticky-body')
|
|
345
|
+
)
|
|
346
|
+
return;
|
|
347
|
+
|
|
348
|
+
// Enable editing and focus
|
|
349
|
+
target.setAttribute('contenteditable', 'true');
|
|
350
|
+
this.editingField = target;
|
|
351
|
+
target.focus();
|
|
352
|
+
event.stopPropagation();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* When a contenteditable field loses focus on a touch device,
|
|
357
|
+
* disable contenteditable again to prevent Scribble.
|
|
358
|
+
*/
|
|
359
|
+
private handleContentBlurForTouch(event: FocusEvent): void {
|
|
360
|
+
const target = event.target as HTMLElement;
|
|
361
|
+
if (this.isTouchDevice && this.editingField === target) {
|
|
362
|
+
target.setAttribute('contenteditable', 'false');
|
|
363
|
+
this.editingField = null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/* c8 ignore stop */
|
|
367
|
+
|
|
321
368
|
private handleContentMouseDown(event: MouseEvent): void {
|
|
322
369
|
// If this sticky note is selected, don't stop propagation
|
|
323
370
|
// so that group dragging can work
|
|
@@ -356,11 +403,19 @@ export class StickyNote extends RapidElement {
|
|
|
356
403
|
this.colorPickerExpanded = false;
|
|
357
404
|
}
|
|
358
405
|
|
|
406
|
+
/* c8 ignore next 5 -- touch-only */
|
|
407
|
+
private handleColorPickerTap(event: TouchEvent): void {
|
|
408
|
+
event.stopPropagation();
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
this.colorPickerExpanded = !this.colorPickerExpanded;
|
|
411
|
+
}
|
|
412
|
+
|
|
359
413
|
private handleColorOptionClick(
|
|
360
|
-
event: MouseEvent,
|
|
414
|
+
event: MouseEvent | TouchEvent,
|
|
361
415
|
color: 'yellow' | 'blue' | 'pink' | 'green' | 'gray'
|
|
362
416
|
): void {
|
|
363
417
|
event.stopPropagation();
|
|
418
|
+
event.preventDefault();
|
|
364
419
|
|
|
365
420
|
if (this.data && color !== this.data.color) {
|
|
366
421
|
getStore()
|
|
@@ -391,23 +446,29 @@ export class StickyNote extends RapidElement {
|
|
|
391
446
|
data-uuid="${this.uuid}"
|
|
392
447
|
>
|
|
393
448
|
<div class="sticky-title-container">
|
|
394
|
-
<temba-icon
|
|
449
|
+
<temba-icon
|
|
450
|
+
name="drag"
|
|
451
|
+
class="drag-handle"
|
|
452
|
+
@touchstart=${this.handleDragHandleTouchStart}
|
|
453
|
+
></temba-icon>
|
|
395
454
|
<div
|
|
396
455
|
class="sticky-title"
|
|
397
|
-
contenteditable="${!this.isTranslating}"
|
|
456
|
+
contenteditable="${!this.isTranslating && !this.isTouchDevice}"
|
|
398
457
|
@blur="${this.handleTitleBlur}"
|
|
399
458
|
@keydown="${this.handleTitleKeyDown}"
|
|
400
459
|
@mousedown="${this.handleContentMouseDown}"
|
|
460
|
+
@touchend="${this.handleContentTap}"
|
|
401
461
|
.textContent="${this.data.title}"
|
|
402
462
|
></div>
|
|
403
463
|
</div>
|
|
404
464
|
<div class="sticky-body-container">
|
|
405
465
|
<div
|
|
406
466
|
class="sticky-body"
|
|
407
|
-
contenteditable="${!this.isTranslating}"
|
|
467
|
+
contenteditable="${!this.isTranslating && !this.isTouchDevice}"
|
|
408
468
|
@blur="${this.handleBodyBlur}"
|
|
409
469
|
@keydown="${this.handleBodyKeyDown}"
|
|
410
470
|
@mousedown="${this.handleContentMouseDown}"
|
|
471
|
+
@touchend="${this.handleContentTap}"
|
|
411
472
|
.textContent="${this.data.body}"
|
|
412
473
|
></div>
|
|
413
474
|
${!this.isTranslating
|
|
@@ -419,6 +480,7 @@ export class StickyNote extends RapidElement {
|
|
|
419
480
|
class="color-picker"
|
|
420
481
|
@mouseenter="${this.handleColorPickerMouseEnter}"
|
|
421
482
|
@mouseleave="${this.handleColorPickerMouseLeave}"
|
|
483
|
+
@touchend="${this.handleColorPickerTap}"
|
|
422
484
|
>
|
|
423
485
|
<div
|
|
424
486
|
class="color-options ${this.colorPickerExpanded
|
|
@@ -429,26 +491,36 @@ export class StickyNote extends RapidElement {
|
|
|
429
491
|
class="color-option yellow"
|
|
430
492
|
@click="${(e: MouseEvent) =>
|
|
431
493
|
this.handleColorOptionClick(e, 'yellow')}"
|
|
494
|
+
@touchend="${(e: TouchEvent) =>
|
|
495
|
+
this.handleColorOptionClick(e, 'yellow')}"
|
|
432
496
|
></div>
|
|
433
497
|
<div
|
|
434
498
|
class="color-option blue"
|
|
435
499
|
@click="${(e: MouseEvent) =>
|
|
436
500
|
this.handleColorOptionClick(e, 'blue')}"
|
|
501
|
+
@touchend="${(e: TouchEvent) =>
|
|
502
|
+
this.handleColorOptionClick(e, 'blue')}"
|
|
437
503
|
></div>
|
|
438
504
|
<div
|
|
439
505
|
class="color-option pink"
|
|
440
506
|
@click="${(e: MouseEvent) =>
|
|
441
507
|
this.handleColorOptionClick(e, 'pink')}"
|
|
508
|
+
@touchend="${(e: TouchEvent) =>
|
|
509
|
+
this.handleColorOptionClick(e, 'pink')}"
|
|
442
510
|
></div>
|
|
443
511
|
<div
|
|
444
512
|
class="color-option green"
|
|
445
513
|
@click="${(e: MouseEvent) =>
|
|
446
514
|
this.handleColorOptionClick(e, 'green')}"
|
|
515
|
+
@touchend="${(e: TouchEvent) =>
|
|
516
|
+
this.handleColorOptionClick(e, 'green')}"
|
|
447
517
|
></div>
|
|
448
518
|
<div
|
|
449
519
|
class="color-option gray"
|
|
450
520
|
@click="${(e: MouseEvent) =>
|
|
451
521
|
this.handleColorOptionClick(e, 'gray')}"
|
|
522
|
+
@touchend="${(e: TouchEvent) =>
|
|
523
|
+
this.handleColorOptionClick(e, 'gray')}"
|
|
452
524
|
></div>
|
|
453
525
|
</div>
|
|
454
526
|
</div>
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
FlowTypes
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { Node, AddContactUrn } from '../../store/flow-definition';
|
|
10
|
-
import { SCHEMES } from '../utils';
|
|
10
|
+
import { SCHEMES, renderClamped } from '../utils';
|
|
11
11
|
|
|
12
12
|
export const add_contact_urn: ActionConfig = {
|
|
13
13
|
name: 'Add URN',
|
|
@@ -16,11 +16,10 @@ export const add_contact_urn: ActionConfig = {
|
|
|
16
16
|
render: (_node: Node, action: AddContactUrn) => {
|
|
17
17
|
const schemeObj = SCHEMES.find((s) => s.scheme === action.scheme);
|
|
18
18
|
const friendlyScheme = schemeObj?.path || action.scheme;
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</div>`;
|
|
19
|
+
return renderClamped(
|
|
20
|
+
html`Add ${friendlyScheme} <strong>${action.path}</strong>`,
|
|
21
|
+
`Add ${friendlyScheme} ${action.path}`
|
|
22
|
+
);
|
|
24
23
|
},
|
|
25
24
|
|
|
26
25
|
toFormData: (action: AddContactUrn) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FlowTypes } from '../types';
|
|
3
3
|
import { Node, EnterFlow } from '../../store/flow-definition';
|
|
4
|
-
import {
|
|
4
|
+
import { renderFlowLinks } from '../utils';
|
|
5
5
|
|
|
6
6
|
export const enter_flow: ActionConfig = {
|
|
7
7
|
name: 'Enter a Flow',
|
|
@@ -9,7 +9,7 @@ export const enter_flow: ActionConfig = {
|
|
|
9
9
|
hideFromActions: true,
|
|
10
10
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
11
11
|
render: (_node: Node, action: EnterFlow) => {
|
|
12
|
-
return html`${
|
|
12
|
+
return html`${renderFlowLinks([action.flow], 'flow')}`;
|
|
13
13
|
},
|
|
14
14
|
toFormData: (action: EnterFlow) => {
|
|
15
15
|
return {
|
|
@@ -3,6 +3,7 @@ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
|
|
3
3
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
4
4
|
import { Node, SayMsg } from '../../store/flow-definition';
|
|
5
5
|
import { renderAudioPlayer } from './audio-player';
|
|
6
|
+
import { renderClamped } from '../utils';
|
|
6
7
|
|
|
7
8
|
export const say_msg: ActionConfig = {
|
|
8
9
|
name: 'Say Message',
|
|
@@ -11,7 +12,7 @@ export const say_msg: ActionConfig = {
|
|
|
11
12
|
render: (_node: Node, action: SayMsg) => {
|
|
12
13
|
const text = (action.text || '').replace(/\n/g, '<br>');
|
|
13
14
|
return html`
|
|
14
|
-
${unsafeHTML(text)}
|
|
15
|
+
${renderClamped(html`${unsafeHTML(text)}`, action.text || '')}
|
|
15
16
|
${action.audio_url
|
|
16
17
|
? html`<div style="margin-top: 0.5em;">
|
|
17
18
|
${renderAudioPlayer(action.audio_url)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
3
3
|
import { Node, SendBroadcast } from '../../store/flow-definition';
|
|
4
|
-
import { renderStringList } from '../utils';
|
|
4
|
+
import { renderStringList, renderClamped } from '../utils';
|
|
5
5
|
import { Icon } from '../../Icons';
|
|
6
6
|
|
|
7
7
|
export const send_broadcast: ActionConfig = {
|
|
@@ -17,11 +17,7 @@ export const send_broadcast: ActionConfig = {
|
|
|
17
17
|
return html`<div>
|
|
18
18
|
<div>${renderStringList(recipients, Icon.contacts)}</div>
|
|
19
19
|
<div style="margin-top: 0.5em">
|
|
20
|
-
|
|
21
|
-
style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
|
|
22
|
-
>
|
|
23
|
-
${action.text}
|
|
24
|
-
</div>
|
|
20
|
+
${renderClamped(action.text, action.text)}
|
|
25
21
|
</div>
|
|
26
22
|
</div>`;
|
|
27
23
|
},
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
FlowTypes
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { Node, SendEmail } from '../../store/flow-definition';
|
|
10
|
-
import { renderStringList } from '../utils';
|
|
10
|
+
import { renderStringList, renderClamped } from '../utils';
|
|
11
11
|
import { Icon } from '../../Icons';
|
|
12
12
|
|
|
13
13
|
export const send_email: ActionConfig = {
|
|
@@ -18,11 +18,7 @@ export const send_email: ActionConfig = {
|
|
|
18
18
|
return html`<div>
|
|
19
19
|
<div>${renderStringList(action.addresses, Icon.email)}</div>
|
|
20
20
|
<div style="margin-top: 0.5em">
|
|
21
|
-
|
|
22
|
-
style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
|
|
23
|
-
>
|
|
24
|
-
${action.subject}
|
|
25
|
-
</div>
|
|
21
|
+
${renderClamped(action.subject, action.subject)}
|
|
26
22
|
</div>
|
|
27
23
|
</div>`;
|
|
28
24
|
},
|