@teleporthq/teleport-plugin-next-workflows 0.43.69 → 0.43.71

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 (72) hide show
  1. package/__tests__/chapter-reached-trigger.test.ts +141 -0
  2. package/__tests__/element-video-control.test.ts +102 -0
  3. package/__tests__/element-visible-settings.test.ts +128 -0
  4. package/__tests__/element-visible-trigger-scoping.test.ts +22 -0
  5. package/__tests__/scrolled-past-trigger.test.ts +189 -0
  6. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  7. package/dist/cjs/api-route-generator.js +1 -0
  8. package/dist/cjs/api-route-generator.js.map +1 -1
  9. package/dist/cjs/element-visible-trigger.d.ts +32 -0
  10. package/dist/cjs/element-visible-trigger.d.ts.map +1 -0
  11. package/dist/cjs/element-visible-trigger.js +42 -0
  12. package/dist/cjs/element-visible-trigger.js.map +1 -0
  13. package/dist/cjs/index.d.ts +1 -0
  14. package/dist/cjs/index.d.ts.map +1 -1
  15. package/dist/cjs/index.js +5 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/nodes/element/element-video-control.d.ts +3 -0
  18. package/dist/cjs/nodes/element/element-video-control.d.ts.map +1 -0
  19. package/dist/cjs/nodes/element/element-video-control.js +94 -0
  20. package/dist/cjs/nodes/element/element-video-control.js.map +1 -0
  21. package/dist/cjs/nodes/index.d.ts.map +1 -1
  22. package/dist/cjs/nodes/index.js +2 -1
  23. package/dist/cjs/nodes/index.js.map +1 -1
  24. package/dist/cjs/scroll-points.d.ts +14 -0
  25. package/dist/cjs/scroll-points.d.ts.map +1 -0
  26. package/dist/cjs/scroll-points.js +23 -0
  27. package/dist/cjs/scroll-points.js.map +1 -0
  28. package/dist/cjs/trigger-generator.d.ts.map +1 -1
  29. package/dist/cjs/trigger-generator.js +3 -15
  30. package/dist/cjs/trigger-generator.js.map +1 -1
  31. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  32. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  33. package/dist/cjs/workflow-component-plugin.js +121 -13
  34. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  35. package/dist/esm/api-route-generator.d.ts.map +1 -1
  36. package/dist/esm/api-route-generator.js +1 -0
  37. package/dist/esm/api-route-generator.js.map +1 -1
  38. package/dist/esm/element-visible-trigger.d.ts +32 -0
  39. package/dist/esm/element-visible-trigger.d.ts.map +1 -0
  40. package/dist/esm/element-visible-trigger.js +37 -0
  41. package/dist/esm/element-visible-trigger.js.map +1 -0
  42. package/dist/esm/index.d.ts +1 -0
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js +1 -0
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/nodes/element/element-video-control.d.ts +3 -0
  47. package/dist/esm/nodes/element/element-video-control.d.ts.map +1 -0
  48. package/dist/esm/nodes/element/element-video-control.js +91 -0
  49. package/dist/esm/nodes/element/element-video-control.js.map +1 -0
  50. package/dist/esm/nodes/index.d.ts.map +1 -1
  51. package/dist/esm/nodes/index.js +2 -1
  52. package/dist/esm/nodes/index.js.map +1 -1
  53. package/dist/esm/scroll-points.d.ts +14 -0
  54. package/dist/esm/scroll-points.d.ts.map +1 -0
  55. package/dist/esm/scroll-points.js +19 -0
  56. package/dist/esm/scroll-points.js.map +1 -0
  57. package/dist/esm/trigger-generator.d.ts.map +1 -1
  58. package/dist/esm/trigger-generator.js +3 -15
  59. package/dist/esm/trigger-generator.js.map +1 -1
  60. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  61. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  62. package/dist/esm/workflow-component-plugin.js +121 -13
  63. package/dist/esm/workflow-component-plugin.js.map +1 -1
  64. package/package.json +4 -4
  65. package/src/api-route-generator.ts +1 -0
  66. package/src/element-visible-trigger.ts +63 -0
  67. package/src/index.ts +1 -0
  68. package/src/nodes/element/element-video-control.ts +95 -0
  69. package/src/nodes/index.ts +2 -0
  70. package/src/scroll-points.ts +21 -0
  71. package/src/trigger-generator.ts +7 -16
  72. package/src/workflow-component-plugin.ts +135 -13
@@ -0,0 +1,95 @@
1
+ import { NodeHandlerGenerator, handlerToString } from '../types'
2
+
3
+ /**
4
+ * "Control a Video": play / pause / play-or-pause / restart / mute / unmute
5
+ * the bound Video element (or the first <video> inside the bound element).
6
+ * A play the browser blocks for sound (autoplay policy: no user gesture yet,
7
+ * e.g. a chapter-reached trigger) is retried muted, so the video still
8
+ * starts, and the result says so. Client-only: it needs the DOM.
9
+ */
10
+ async function element_video_control(config: any, context: Record<string, unknown>) {
11
+ const action = typeof config.action === 'string' ? config.action : 'play'
12
+ const host =
13
+ document.getElementById(config.nodeId) ||
14
+ (config.elementHtmlId ? document.getElementById(config.elementHtmlId) : null)
15
+ const video = host
16
+ ? host.tagName === 'VIDEO'
17
+ ? (host as HTMLVideoElement)
18
+ : (host.querySelector('video') as HTMLVideoElement | null)
19
+ : null
20
+ if (!video) {
21
+ return { success: false, action, reason: 'video-not-found' }
22
+ }
23
+
24
+ const state = (extra?: Record<string, unknown>) => {
25
+ const result: Record<string, unknown> = {
26
+ success: true,
27
+ action,
28
+ playing: !video.paused && !video.ended,
29
+ currentTime: video.currentTime,
30
+ muted: video.muted,
31
+ }
32
+ if (extra) {
33
+ Object.keys(extra).forEach((key) => {
34
+ result[key] = extra[key]
35
+ })
36
+ }
37
+ return result
38
+ }
39
+
40
+ const play = async () => {
41
+ try {
42
+ await video.play()
43
+ return null
44
+ } catch (error) {
45
+ if (video.muted) {
46
+ return 'blocked'
47
+ }
48
+ video.muted = true
49
+ try {
50
+ await video.play()
51
+ return 'muted'
52
+ } catch (retryError) {
53
+ return 'blocked'
54
+ }
55
+ }
56
+ }
57
+
58
+ const playAndReport = async () => {
59
+ const outcome = await play()
60
+ if (outcome === 'blocked') {
61
+ return state({ success: false, reason: 'autoplay-blocked' })
62
+ }
63
+ return outcome === 'muted' ? state({ mutedFallback: true }) : state()
64
+ }
65
+
66
+ if (action === 'pause') {
67
+ video.pause()
68
+ return state()
69
+ }
70
+ if (action === 'mute') {
71
+ video.muted = true
72
+ return state()
73
+ }
74
+ if (action === 'unmute') {
75
+ video.muted = false
76
+ return state()
77
+ }
78
+ if (action === 'restart') {
79
+ video.currentTime = 0
80
+ return playAndReport()
81
+ }
82
+ if (action === 'toggle' && !video.paused && !video.ended) {
83
+ video.pause()
84
+ return state()
85
+ }
86
+ return playAndReport()
87
+ }
88
+
89
+ export const elementVideoControl: NodeHandlerGenerator = {
90
+ nodeType: 'element-video-control',
91
+ executionEnv: 'client',
92
+ generateHandler(): string {
93
+ return handlerToString(element_video_control)
94
+ },
95
+ }
@@ -69,6 +69,7 @@ import { elementGetInputValue } from './element/element-get-input-value'
69
69
  import { elementHide } from './element/element-hide'
70
70
  import { elementRemoveClass } from './element/element-remove-class'
71
71
  import { elementScrollTo } from './element/element-scroll-to'
72
+ import { elementVideoControl } from './element/element-video-control'
72
73
  import { elementSetAttribute } from './element/element-set-attribute'
73
74
  import { elementSetText } from './element/element-set-text'
74
75
  import { elementShow } from './element/element-show'
@@ -241,6 +242,7 @@ export const nodeRegistry: Record<string, NodeHandlerGenerator> = {
241
242
  'element-hide': elementHide,
242
243
  'element-remove-class': elementRemoveClass,
243
244
  'element-scroll-to': elementScrollTo,
245
+ 'element-video-control': elementVideoControl,
244
246
  'element-set-attribute': elementSetAttribute,
245
247
  'element-set-text': elementSetText,
246
248
  'element-show': elementShow,
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The plain choices of the "scrolled past a point" triggers and the progress
3
+ * (0..1) each stands for. The published Scroll Scene runtime carries the same
4
+ * list (scenePointList in teleport-project-generator-next, pinned by a test);
5
+ * the page trigger reads it here directly.
6
+ */
7
+ export const SCROLL_POINT_PROGRESS: Readonly<Record<string, number>> = {
8
+ quarter: 0.25,
9
+ half: 0.5,
10
+ 'three-quarters': 0.75,
11
+ end: 1,
12
+ }
13
+
14
+ export const DEFAULT_SCROLL_POINT = 'half'
15
+
16
+ export const resolveScrollPoint = (value: unknown): { key: string; at: number; rank: number } => {
17
+ const keys = Object.keys(SCROLL_POINT_PROGRESS)
18
+ const key =
19
+ typeof value === 'string' && value in SCROLL_POINT_PROGRESS ? value : DEFAULT_SCROLL_POINT
20
+ return { key, at: SCROLL_POINT_PROGRESS[key], rank: keys.indexOf(key) + 1 }
21
+ }
@@ -1,4 +1,5 @@
1
1
  import { UIDLWorkflow } from '@teleporthq/teleport-types'
2
+ import { elementVisibleObserverCode } from './element-visible-trigger'
2
3
 
3
4
  /**
4
5
  * For DOM events whose `target` is a form control (select, input, textarea,
@@ -171,27 +172,17 @@ const generateElementVisibleTrigger = (
171
172
  workflowId: string
172
173
  ): string => {
173
174
  const elementId = (config.elementHtmlId || config.nodeId) as string
174
- const threshold = (config.threshold as number) || 0
175
- const once = config.once as boolean
176
-
177
175
  const safeId = workflowId.replace(/[^a-zA-Z0-9]/g, '_')
176
+ const observer = elementVisibleObserverCode(
177
+ `obs_${safeId}`,
178
+ config,
179
+ `const triggerContext = { elementId: '${elementId}', timestamp: Date.now(), intersectionRatio: entry.intersectionRatio };\n ${executionCall};`
180
+ )
178
181
  return `
179
182
  // Workflow trigger: element visible (${workflowId})
180
183
  const el_${safeId} = document.getElementById('${elementId}');
181
184
  if (el_${safeId}) {
182
- const obs_${safeId} = new IntersectionObserver(function(entries) {
183
- entries.forEach(function(entry) {
184
- if (entry.isIntersecting) {
185
- const triggerContext = {
186
- elementId: '${elementId}',
187
- timestamp: Date.now(),
188
- intersectionRatio: entry.intersectionRatio
189
- };
190
- ${executionCall};
191
- ${once ? `obs_${safeId}.disconnect();` : ''}
192
- }
193
- });
194
- }, { threshold: ${threshold} });
185
+ ${observer.trimEnd().replace(/^/gm, ' ')}
195
186
  obs_${safeId}.observe(el_${safeId});
196
187
  return function() { obs_${safeId}.disconnect(); };
197
188
  }`
@@ -30,7 +30,9 @@ import { getAPIRouteFileName, hasStreamingAINode } from './api-route-generator'
30
30
  import { REALTIME_TRIGGER_TYPES, REALTIME_NODE_TYPES } from './graph-utils'
31
31
  import { neutraliseIsLoggedInGates } from './is-logged-in-gate'
32
32
  import { formControlPropertyReads } from './trigger-generator'
33
+ import { elementVisibleObserverCode } from './element-visible-trigger'
33
34
  import { restoreControlledSelectValue } from './controlled-select'
35
+ import { resolveScrollPoint } from './scroll-points'
34
36
  import { findFormStateKeyForHydration } from './admin-form-hydration'
35
37
 
36
38
  interface WorkflowPluginConfig {
@@ -96,7 +98,11 @@ const resolveLifecycleTriggerElementId = (
96
98
  triggerType: string,
97
99
  config: Record<string, unknown>
98
100
  ): string | null => {
99
- if (triggerType !== 'event-element-visible') {
101
+ if (
102
+ triggerType !== 'event-element-visible' &&
103
+ triggerType !== 'event-chapter-reached' &&
104
+ triggerType !== 'event-scene-scrolled-past'
105
+ ) {
100
106
  return null
101
107
  }
102
108
  return ((config.elementHtmlId || config.nodeId) as string) || null
@@ -2198,27 +2204,143 @@ const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2198
2204
  // so the observer was never constructed and the cookie-consent banner
2199
2205
  // could never appear on any page (run a15472af: 408 dead lookups).
2200
2206
  const elementId = (config.elementHtmlId || config.nodeId) as string
2201
- const threshold = (config.threshold as number) || 0
2202
- const once = config.once as boolean
2207
+ const observer = elementVisibleObserverCode(
2208
+ `__obs_${safeId}`,
2209
+ config,
2210
+ `const triggerContext = { elementId: '${elementId}', timestamp: Date.now(), intersectionRatio: entry.intersectionRatio };\n ${execCall};`
2211
+ )
2203
2212
  return (
2204
2213
  ` // Element visible (${wf.name || wf.id})\n` +
2205
2214
  ` const __visEl_${safeId} = document.getElementById('${elementId}');\n` +
2206
2215
  ` if (__visEl_${safeId}) {\n` +
2207
- ` const __obs_${safeId} = new IntersectionObserver(function(entries) {\n` +
2208
- ` entries.forEach(function(entry) {\n` +
2209
- ` if (entry.isIntersecting) {\n` +
2210
- ` const triggerContext = { elementId: '${elementId}', timestamp: Date.now(), intersectionRatio: entry.intersectionRatio };\n` +
2211
- ` ${execCall};\n` +
2212
- (once ? ` __obs_${safeId}.disconnect();\n` : '') +
2213
- ` }\n` +
2214
- ` });\n` +
2215
- ` }, { threshold: ${threshold} });\n` +
2216
- ` __obs_${safeId}.observe(__visEl_${safeId});\n` +
2216
+ observer.trimEnd().replace(/^/gm, ' ') +
2217
+ `\n __obs_${safeId}.observe(__visEl_${safeId});\n` +
2217
2218
  ` cleanups.push(function() { __obs_${safeId}.disconnect(); });\n` +
2218
2219
  ` }`
2219
2220
  )
2220
2221
  }
2221
2222
 
2223
+ case 'event-chapter-reached': {
2224
+ // The published TqScrollScene announces the chapter on stage as a
2225
+ // `tq-chapter-reached` event on the CHAPTER element (a direct child of the
2226
+ // scene) and stamps `data-chapter-active` / `data-chapter-count` on it.
2227
+ // The bound element is resolved to its chapter root, so a descendant the
2228
+ // author picked still listens where the event lands; events bubbling up
2229
+ // from a scene nested inside the chapter are ignored; and a listener that
2230
+ // attaches after the announcement reads the stamp instead of waiting for
2231
+ // a change that already happened.
2232
+ const elementId = resolveLifecycleTriggerElementId(trigger.type, config) ?? ''
2233
+ const once = config.once !== false
2234
+ return (
2235
+ ` // Chapter reached (${wf.name || wf.id})\n` +
2236
+ ` const __chEl_${safeId} = document.getElementById('${elementId}');\n` +
2237
+ ` if (__chEl_${safeId}) {\n` +
2238
+ ` const __chRoot_${safeId} = __chEl_${safeId}.closest('[data-scene-stage] > *, [data-scene-track] > *') || __chEl_${safeId};\n` +
2239
+ ` const __chFire_${safeId} = function(detail) {\n` +
2240
+ ` const triggerContext = { elementId: '${elementId}', chapterIndex: detail.chapterIndex, chapterCount: detail.chapterCount, progress: detail.progress, direction: detail.direction, timestamp: Date.now() };\n` +
2241
+ ` ${execCall};\n` +
2242
+ ` };\n` +
2243
+ ` const __chH_${safeId} = function(event) {\n` +
2244
+ ` if (event.target !== __chRoot_${safeId}) { return; }\n` +
2245
+ (once
2246
+ ? ` __chRoot_${safeId}.removeEventListener('tq-chapter-reached', __chH_${safeId});\n`
2247
+ : '') +
2248
+ ` __chFire_${safeId}(event.detail || {});\n` +
2249
+ ` };\n` +
2250
+ ` const __chStamp_${safeId} = __chRoot_${safeId}.getAttribute('data-chapter-active');\n` +
2251
+ ` if (__chStamp_${safeId}) {\n` +
2252
+ ` __chFire_${safeId}({ chapterIndex: Number(__chStamp_${safeId}), chapterCount: Number(__chRoot_${safeId}.getAttribute('data-chapter-count')) || undefined });\n` +
2253
+ ` }\n` +
2254
+ (once ? ` if (!__chStamp_${safeId}) {\n` : ` {\n`) +
2255
+ ` __chRoot_${safeId}.addEventListener('tq-chapter-reached', __chH_${safeId});\n` +
2256
+ ` cleanups.push(function() { __chRoot_${safeId}.removeEventListener('tq-chapter-reached', __chH_${safeId}); });\n` +
2257
+ ` }\n` +
2258
+ ` }`
2259
+ )
2260
+ }
2261
+
2262
+ case 'event-scene-scrolled-past': {
2263
+ // The published TqScrollScene announces every point passed on the way
2264
+ // down as `tq-scene-point-passed` on its track (the element with the
2265
+ // scene's id) and stamps the furthest point passed as `data-scene-point`.
2266
+ // The bound element is resolved to its track; announcements bubbling up
2267
+ // from a scene nested inside it are ignored; a listener that attaches
2268
+ // after the point was passed fires from the stamp instead of waiting.
2269
+ const elementId = resolveLifecycleTriggerElementId(trigger.type, config) ?? ''
2270
+ const point = resolveScrollPoint(config.point)
2271
+ const once = config.once !== false
2272
+ return (
2273
+ ` // Scene scrolled past (${wf.name || wf.id})\n` +
2274
+ ` const __spEl_${safeId} = document.getElementById('${elementId}');\n` +
2275
+ ` if (__spEl_${safeId}) {\n` +
2276
+ ` const __spRoot_${safeId} = __spEl_${safeId}.closest('[data-scene-track]') || __spEl_${safeId};\n` +
2277
+ ` const __spFire_${safeId} = function(progress) {\n` +
2278
+ ` const triggerContext = { elementId: '${elementId}', point: '${point.key}', progress: progress, timestamp: Date.now() };\n` +
2279
+ ` ${execCall};\n` +
2280
+ ` };\n` +
2281
+ ` const __spH_${safeId} = function(event) {\n` +
2282
+ ` if (event.target !== __spRoot_${safeId}) { return; }\n` +
2283
+ ` const detail = event.detail || {};\n` +
2284
+ ` if (detail.point !== '${point.key}') { return; }\n` +
2285
+ (once
2286
+ ? ` __spRoot_${safeId}.removeEventListener('tq-scene-point-passed', __spH_${safeId});\n`
2287
+ : '') +
2288
+ ` __spFire_${safeId}(detail.progress);\n` +
2289
+ ` };\n` +
2290
+ ` const __spStamp_${safeId} = ['quarter', 'half', 'three-quarters', 'end'].indexOf(__spRoot_${safeId}.getAttribute('data-scene-point') || '') + 1;\n` +
2291
+ ` if (__spStamp_${safeId} >= ${point.rank}) {\n` +
2292
+ ` __spFire_${safeId}(${point.at});\n` +
2293
+ ` }\n` +
2294
+ (once ? ` if (__spStamp_${safeId} < ${point.rank}) {\n` : ` {\n`) +
2295
+ ` __spRoot_${safeId}.addEventListener('tq-scene-point-passed', __spH_${safeId});\n` +
2296
+ ` cleanups.push(function() { __spRoot_${safeId}.removeEventListener('tq-scene-point-passed', __spH_${safeId}); });\n` +
2297
+ ` }\n` +
2298
+ ` }`
2299
+ )
2300
+ }
2301
+
2302
+ case 'event-page-scrolled-past': {
2303
+ // Page progress = scrollY over the scrollable height; a page that cannot
2304
+ // scroll is entirely in view, so every point counts as passed. Checked
2305
+ // once on attach (an anchor link or a restored scroll may open the page
2306
+ // already past the point), then on scroll and resize, one check per
2307
+ // frame. Going back above the point re-arms it; `once` detaches after
2308
+ // the first pass.
2309
+ const point = resolveScrollPoint(config.point)
2310
+ const once = config.once !== false
2311
+ return (
2312
+ ` // Page scrolled past (${wf.name || wf.id})\n` +
2313
+ ` const __ppProgress_${safeId} = function() {\n` +
2314
+ ` const max = document.documentElement.scrollHeight - window.innerHeight;\n` +
2315
+ ` return max <= 0 ? 1 : Math.min(1, Math.max(0, window.scrollY / max));\n` +
2316
+ ` };\n` +
2317
+ ` let __ppPassed_${safeId} = false;\n` +
2318
+ ` let __ppTick_${safeId} = false;\n` +
2319
+ ` const __ppOnScroll_${safeId} = function() {\n` +
2320
+ ` if (__ppTick_${safeId}) { return; }\n` +
2321
+ ` __ppTick_${safeId} = true;\n` +
2322
+ ` requestAnimationFrame(function() { __ppTick_${safeId} = false; __ppCheck_${safeId}(); });\n` +
2323
+ ` };\n` +
2324
+ ` const __ppStop_${safeId} = function() {\n` +
2325
+ ` window.removeEventListener('scroll', __ppOnScroll_${safeId});\n` +
2326
+ ` window.removeEventListener('resize', __ppOnScroll_${safeId});\n` +
2327
+ ` };\n` +
2328
+ ` const __ppCheck_${safeId} = function() {\n` +
2329
+ ` const progress = __ppProgress_${safeId}();\n` +
2330
+ ` if (progress < ${point.at} - 0.000001) { __ppPassed_${safeId} = false; return; }\n` +
2331
+ ` if (__ppPassed_${safeId}) { return; }\n` +
2332
+ ` __ppPassed_${safeId} = true;\n` +
2333
+ (once ? ` __ppStop_${safeId}();\n` : '') +
2334
+ ` const triggerContext = { point: '${point.key}', progress: progress, url: window.location.href, timestamp: Date.now() };\n` +
2335
+ ` ${execCall};\n` +
2336
+ ` };\n` +
2337
+ ` window.addEventListener('scroll', __ppOnScroll_${safeId}, { passive: true });\n` +
2338
+ ` window.addEventListener('resize', __ppOnScroll_${safeId});\n` +
2339
+ ` cleanups.push(__ppStop_${safeId});\n` +
2340
+ ` __ppCheck_${safeId}();`
2341
+ )
2342
+ }
2343
+
2222
2344
  case 'event-user-logged-in':
2223
2345
  case 'event-user-logged-out': {
2224
2346
  const eventName =