@cntrl-site/sdk-nextjs 1.9.78 → 1.9.79

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 (145) hide show
  1. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/sdk-nextjs.iml +12 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/LICENSE +21 -21
  6. package/README.md +4 -4
  7. package/TODO.md +1 -1
  8. package/eslint.config.mjs +35 -35
  9. package/jest.config.js +6 -6
  10. package/lib/common/useItemFXData.js +5 -5
  11. package/lib/components/Article.js +5 -5
  12. package/lib/components/LayoutStyle.js +4 -4
  13. package/lib/components/Section/Section.js +27 -27
  14. package/lib/components/StructuredBlockItem/StructuredBlockItem.js +13 -13
  15. package/lib/components/StructuredBlockItem/StructuredComponent/StructuredComponent.js +12 -12
  16. package/lib/components/StructuredContent/StructuredContent.js +11 -11
  17. package/lib/components/items/CodeEmbedItem/CodeEmbedItem.js +36 -36
  18. package/lib/components/items/ComponentItem/ComponentItem.js +13 -13
  19. package/lib/components/items/CompoundItem/CompoundChild.js +20 -20
  20. package/lib/components/items/CompoundItem/CompoundItem.js +14 -14
  21. package/lib/components/items/CustomItem/CustomItem.js +9 -9
  22. package/lib/components/items/EmbedVideoItem/VimeoEmbed.js +24 -24
  23. package/lib/components/items/EmbedVideoItem/YoutubeEmbed.js +27 -27
  24. package/lib/components/items/FileItem/ImageItem.js +40 -40
  25. package/lib/components/items/FileItem/VideoItem.js +64 -64
  26. package/lib/components/items/GroupItem/GroupItem.js +14 -14
  27. package/lib/components/items/Item.js +30 -30
  28. package/lib/components/items/RectangleItem/RectangleItem.js +27 -27
  29. package/lib/components/items/RichTextItem/RichTextItem.js +22 -22
  30. package/lib/utils/RichTextConverter/RichTextConverter.js +21 -21
  31. package/package.json +62 -62
  32. package/src/ItemGeometry/CSSPropertyNameMap.ts +37 -37
  33. package/src/ItemGeometry/GenericGeometryController.ts +59 -59
  34. package/src/ItemGeometry/ItemGeometryContext.ts +4 -4
  35. package/src/ItemGeometry/ItemGeometryController.ts +22 -22
  36. package/src/ItemGeometry/ItemGeometryService.ts +34 -34
  37. package/src/ItemGeometry/RichTextGeometryController.ts +166 -166
  38. package/src/ItemGeometry/useItemGeometry.ts +25 -25
  39. package/src/common/useCurrentLayout.ts +52 -52
  40. package/src/common/useExemplary.ts +9 -9
  41. package/src/common/useItemFXData.ts +63 -63
  42. package/src/common/useKeyframeValue.ts +85 -85
  43. package/src/common/useRegisterResize.ts +16 -16
  44. package/src/components/Article.tsx +69 -69
  45. package/src/components/ArticleWrapper.tsx +21 -21
  46. package/src/components/Head.tsx +95 -95
  47. package/src/components/LayoutStyle.tsx +21 -21
  48. package/src/components/Page.tsx +47 -47
  49. package/src/components/ScrollPlaybackVideo.tsx +75 -75
  50. package/src/components/Section/Section.tsx +162 -162
  51. package/src/components/Section/SectionImage.tsx +48 -48
  52. package/src/components/Section/SectionVideo.tsx +144 -144
  53. package/src/components/Section/useSectionColor.ts +19 -19
  54. package/src/components/Section/useSectionHeightMap.ts +19 -19
  55. package/src/components/StructuredBlockItem/StructuredBlockItem.tsx +69 -69
  56. package/src/components/StructuredBlockItem/StructuredComponent/StructuredComponent.tsx +66 -66
  57. package/src/components/StructuredContent/StructuredContent.tsx +42 -42
  58. package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +148 -148
  59. package/src/components/items/CodeEmbedItem/useCodeEmbedItem.ts +36 -36
  60. package/src/components/items/ComponentItem/ComponentItem.tsx +85 -85
  61. package/src/components/items/ComponentItem/useComponentItem.ts +35 -35
  62. package/src/components/items/CompoundItem/CompoundChild.tsx +151 -151
  63. package/src/components/items/CompoundItem/CompoundItem.tsx +88 -88
  64. package/src/components/items/CompoundItem/useCompoundItem.ts +21 -21
  65. package/src/components/items/CustomItem/CustomItem.tsx +50 -50
  66. package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +217 -217
  67. package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +201 -201
  68. package/src/components/items/EmbedVideoItem/useEmbedVideoItem.ts +46 -46
  69. package/src/components/items/FileItem/ImageItem.tsx +166 -166
  70. package/src/components/items/FileItem/VideoItem.tsx +287 -287
  71. package/src/components/items/FileItem/useFileItem.ts +81 -81
  72. package/src/components/items/GroupItem/GroupItem.tsx +99 -99
  73. package/src/components/items/GroupItem/useGroupItem.ts +34 -34
  74. package/src/components/items/Item.tsx +268 -268
  75. package/src/components/items/LinkWrapper.tsx +39 -39
  76. package/src/components/items/RectangleItem/RectangleItem.tsx +177 -177
  77. package/src/components/items/RectangleItem/useRectangleItem.ts +89 -89
  78. package/src/components/items/RichTextItem/RichTextItem.tsx +113 -113
  79. package/src/components/items/RichTextItem/useRichTextItem.ts +12 -12
  80. package/src/components/items/RichTextWrapper.tsx +15 -15
  81. package/src/components/items/itemsMap.ts +28 -28
  82. package/src/components/items/useDraggable.ts +252 -252
  83. package/src/components/items/useItemAngle.ts +13 -13
  84. package/src/components/items/useItemArea.ts +35 -35
  85. package/src/components/items/useItemPointerEvents.ts +23 -23
  86. package/src/components/items/useItemScale.ts +17 -17
  87. package/src/components/items/useItemTriggers.ts +15 -15
  88. package/src/components/items/useRichTextItemValues.ts +66 -66
  89. package/src/components/items/useSizing.ts +25 -25
  90. package/src/components/items/useStickyItemTop.ts +21 -21
  91. package/src/components/useLayoutContext.ts +7 -7
  92. package/src/index.ts +25 -25
  93. package/src/interactions/InteractionsRegistry.ts +513 -513
  94. package/src/interactions/ItemInteractionCtrl.ts +78 -78
  95. package/src/interactions/getTransition.ts +27 -27
  96. package/src/interactions/types.ts +36 -36
  97. package/src/interactions/useItemInteractionCtrl.ts +18 -18
  98. package/src/provider/ArticleRectContext.ts +4 -4
  99. package/src/provider/CntrlContext.ts +5 -5
  100. package/src/provider/CntrlProvider.tsx +11 -11
  101. package/src/provider/CntrlSdkContext.ts +113 -113
  102. package/src/provider/CustomItemRegistry.ts +14 -14
  103. package/src/provider/CustomItemTypes.ts +3 -3
  104. package/src/provider/CustomSectionRegistry.ts +25 -25
  105. package/src/provider/InteractionsContext.tsx +73 -73
  106. package/src/provider/Keyframes.ts +11 -11
  107. package/src/provider/KeyframesContext.ts +5 -5
  108. package/src/provider/LayoutContext.ts +3 -3
  109. package/src/provider/WebGLContextManagerContext.tsx +4 -4
  110. package/src/provider/defaultContext.ts +8 -8
  111. package/src/provider/useCntrlContext.ts +8 -8
  112. package/src/styled-jsx.d.ts +1 -1
  113. package/src/types/components-utils.d.ts +7 -7
  114. package/src/utils/Animator/Animator.ts +570 -570
  115. package/src/utils/ArticleRectManager/ArticleRectObserver.ts +93 -93
  116. package/src/utils/ArticleRectManager/useArticleRectObserver.ts +17 -17
  117. package/src/utils/ArticleRectManager/useSectionRegistry.ts +11 -11
  118. package/src/utils/CSSNumeric.ts +19 -19
  119. package/src/utils/EventEmitter.ts +41 -41
  120. package/src/utils/RichTextConverter/RichTextConverter.tsx +342 -342
  121. package/src/utils/ScaleAnchorMap.ts +13 -13
  122. package/src/utils/StickyManager/StickyManager.ts +23 -23
  123. package/src/utils/Youtube/YouTubeIframeApiLoader.ts +58 -58
  124. package/src/utils/Youtube/YoutubeIframeApi.ts +101 -101
  125. package/src/utils/Youtube/useYouTubeIframeApi.ts +12 -12
  126. package/src/utils/areFillsVisible/areFillsVisible.test.ts +63 -63
  127. package/src/utils/areFillsVisible/areFillsVisible.ts +37 -37
  128. package/src/utils/binSearchInsertAt.ts +33 -33
  129. package/src/utils/castObject.ts +10 -10
  130. package/src/utils/checkOverflowClipSupport.ts +13 -13
  131. package/src/utils/domDfs.ts +11 -11
  132. package/src/utils/effects/useImageFx.ts +154 -154
  133. package/src/utils/effects/useVideoFx.ts +156 -156
  134. package/src/utils/evaluateComponentBundle.ts +83 -83
  135. package/src/utils/getAnchoredItemTop.ts +12 -12
  136. package/src/utils/getCompoundBoundaryStyles.ts +60 -60
  137. package/src/utils/getFill.ts +46 -46
  138. package/src/utils/getFontFamilyValue.ts +3 -3
  139. package/src/utils/getInvertedRanges.ts +43 -43
  140. package/src/utils/getItemTopStyle.ts +39 -39
  141. package/src/utils/getStyleFromItemStateAndParams.ts +8 -8
  142. package/src/utils/getValidYoutubeUrl.ts +29 -29
  143. package/src/utils/isItemType.ts +5 -5
  144. package/src/utils/rangeMap.ts +13 -13
  145. package/src/utils/useElementRect.ts +23 -23
@@ -1,513 +1,513 @@
1
- import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
2
- import {
3
- Article,
4
- ArticleItemType,
5
- Interaction,
6
- InteractionItemTrigger,
7
- ItemAny,
8
- InteractionItemScrollTrigger,
9
- InteractionScrollTrigger,
10
- } from '@cntrl-site/sdk';
11
- import { isItemType } from '../utils/isItemType';
12
- import { ItemGeometryService } from '../ItemGeometry/ItemGeometryService';
13
-
14
- export class InteractionsRegistry implements InteractionsRegistryPort {
15
- private ctrls: Map<ItemId, ItemInteractionCtrl> = new Map();
16
- private items: ItemAny[];
17
- private itemGeometryService: ItemGeometryService;
18
- private interactions: Interaction[];
19
- private stateItemsIdsMap: StateItemsIdsMap;
20
- private interactionStateMap: InteractionStateMap;
21
- private itemsStages: ItemStages;
22
- private activeStateIdInteractionIdMap: Record<StateId, InteractionId>;
23
- private itemScrollPositionAnimationMap: Map<string, number> = new Map();
24
-
25
- constructor(article: Article, layoutId: string, itemGeometryService: ItemGeometryService) {
26
- this.items = this.unpackItems(article);
27
- this.itemGeometryService = itemGeometryService;
28
- const interactions = article.interactions[layoutId] ?? [];
29
- const activeStatesIds = interactions.reduce<StateId[]>((map, inter) => {
30
- const activeStateId = inter.states.find((state) => state.id !== inter.startStateId)?.id;
31
- if (!activeStateId) {
32
- throw new Error(`Failed to find active state for interaction w/ id="${inter.id}"`);
33
- }
34
- map.push(activeStateId);
35
- return map;
36
- }, []);
37
- const interactionStateMap = interactions.reduce<InteractionStateMap>((map, { id, startStateId }) => {
38
- map[id] = startStateId;
39
- return map;
40
- }, {});
41
- this.activeStateIdInteractionIdMap = interactions.reduce<Record<StateId, InteractionId>>((map, interaction) => {
42
- const activeState = interaction.states.find((state) => state.id !== interaction.startStateId);
43
- if (activeState) {
44
- map[activeState.id] = interaction.id;
45
- }
46
- return map;
47
- }, {});
48
- const stateItemsIdsMap = activeStatesIds.reduce<StateItemsIdsMap>((map, stateId) => {
49
- map[stateId] = this.items
50
- .filter((item) => {
51
- const state = item.state[stateId] ?? {};
52
- const hasKeys = Object.keys(state).length !== 0;
53
- return hasKeys;
54
- })
55
- .map((item) => item.id);
56
- return map;
57
- }, {});
58
- this.interactions = interactions;
59
- this.stateItemsIdsMap = stateItemsIdsMap;
60
- this.interactionStateMap = interactionStateMap;
61
- const itemStages = this.getDefaultItemStages();
62
- this.itemsStages = itemStages;
63
- this.itemScrollPositionAnimationMap = new Map();
64
- }
65
-
66
- register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
67
- this.ctrls.set(itemId, ctrl);
68
- }
69
-
70
- getStatePropsForItem(itemId: string) {
71
- const { items } = this;
72
- const item = items.find((item) => item.id === itemId)!;
73
- const itemStages = this.itemsStages.filter((stage) => stage.itemId === itemId);
74
- itemStages.sort((a, b) => a.updated - b.updated);
75
- const itemStyles: StateProps = {};
76
- for (const stage of itemStages) {
77
- if (stage.type === 'active') {
78
- if (stage.isStartState) continue;
79
- const params = item.state[stage.stateId!] ?? {};
80
- for (const [key, stateDetails] of Object.entries(params)) {
81
- itemStyles[key] = {
82
- value: stateDetails.value
83
- };
84
- }
85
- }
86
- if (stage.type === 'transitioning') {
87
- const activeStateId = stage.direction === 'in' ? stage.to : stage.from;
88
- const params = item.state[activeStateId] ?? {};
89
- for (const [key, stateDetails] of Object.entries(params)) {
90
- const transitionDetails = stateDetails[stage.direction];
91
- if (!transitionDetails) continue;
92
- itemStyles[key] = {
93
- value: stage.direction === 'in' ? stateDetails.value : itemStyles[key]?.value,
94
- transition: {
95
- timing: transitionDetails.timing,
96
- duration: transitionDetails.duration,
97
- delay: transitionDetails.delay
98
- }
99
- };
100
- }
101
- }
102
- }
103
- return itemStyles;
104
- }
105
-
106
- isItemScrollBasedTrigger(itemId: string): boolean {
107
- let isElementHasScrollBasedTrigger = false;
108
- for (const interaction of this.interactions) {
109
- const hasInteractionItemScrollBasedTrigger = interaction.triggers.find((tr) => (tr.type === 'item-scroll-position' && tr.itemId === itemId)) !== undefined;
110
- const hasInteractionScrollBasedTrigger = interaction.triggers.find((tr) => tr.type === 'scroll-position') !== undefined;
111
- if (hasInteractionScrollBasedTrigger) {
112
- for (const trigger of interaction.triggers) {
113
- if (trigger.type === 'item' && trigger.itemId === itemId) {
114
- isElementHasScrollBasedTrigger = true;
115
- return true;
116
- }
117
- }
118
- }
119
- if (hasInteractionItemScrollBasedTrigger) {
120
- isElementHasScrollBasedTrigger = true;
121
- }
122
- }
123
- return isElementHasScrollBasedTrigger;
124
- }
125
-
126
- getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['triggerEvent']> {
127
- const available = new Set<InteractionItemTrigger['triggerEvent']>();
128
- const activeStates = Object.values(this.interactionStateMap);
129
- if (this.isItemScrollBasedTrigger(itemId)) return available;
130
- for (const interaction of this.interactions) {
131
- const { triggers } = interaction;
132
- for (const trigger of triggers) {
133
- if (!('triggerEvent' in trigger)) continue;
134
- if (trigger.itemId !== itemId) continue;
135
- if (activeStates.includes(trigger.from)) {
136
- available.add(trigger.triggerEvent);
137
- }
138
- }
139
- }
140
- return available;
141
- }
142
-
143
- getTriggerPositionForItemScrollTrigger(trigger: InteractionItemScrollTrigger): number {
144
- const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
145
- let triggerPosition = 0;
146
- switch (trigger.screenPosition) {
147
- case 'top':
148
- triggerPosition = itemRect.top;
149
- break;
150
- case 'center':
151
- triggerPosition = itemRect.top - window.innerHeight / 2;
152
- break;
153
- case 'bottom':
154
- triggerPosition = itemRect.top - window.innerHeight;
155
- break;
156
- default:
157
- triggerPosition = itemRect.top;
158
- break;
159
- }
160
- switch (trigger.itemPosition) {
161
- case 'top':
162
- triggerPosition += 0;
163
- break;
164
- case 'center':
165
- triggerPosition += itemRect.height / 2;
166
- break;
167
- case 'bottom':
168
- triggerPosition += itemRect.height;
169
- break;
170
- default:
171
- triggerPosition += 0;
172
- break;
173
- }
174
- triggerPosition += trigger.offset;
175
- return triggerPosition;
176
- }
177
-
178
- isItemScrollTriggerOnStartScreen(trigger: InteractionItemScrollTrigger): boolean {
179
- const triggerPosition = this.getTriggerPositionForItemScrollTrigger(trigger);
180
- return triggerPosition < 0;
181
- }
182
-
183
- notifyLoad() {
184
- const timestamp = Date.now();
185
- for (const interaction of this.interactions) {
186
- const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
187
- const matchingTrigger = interaction.triggers.find(trigger =>
188
- ('position' in trigger && trigger.position === 0 && trigger.from === currentStateId)
189
- || (trigger.type === 'item-scroll-position' && this.items.find((item) => item.id === trigger.itemId) && trigger.from === currentStateId && this.isItemScrollTriggerOnStartScreen(trigger))
190
- );
191
- if (!matchingTrigger) continue;
192
- const activeStateId = this.getActiveInteractionState(interaction.id);
193
- const isNewStateActive = matchingTrigger.to === activeStateId;
194
- this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
195
- const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
196
- const state = interaction.states.find((state) => state.id === matchingTrigger.to);
197
- const actions = state?.actions ?? [];
198
- for (const action of actions) {
199
- const ctrl = this.ctrls.get(action.itemId);
200
- if (!ctrl) continue;
201
- ctrl.receiveAction(action.type);
202
- }
203
-
204
- this.itemsStages = this.itemsStages.map((stage) => {
205
- if (stage.interactionId !== interaction.id) return stage;
206
- return {
207
- itemId: stage.itemId,
208
- interactionId: stage.interactionId,
209
- type: 'transitioning',
210
- from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
211
- to: matchingTrigger.to,
212
- direction: isNewStateActive ? 'in' : 'out',
213
- updated: timestamp
214
- };
215
- });
216
-
217
- const itemsToNotify = new Set<ItemId>(transitioningItems);
218
- for (const trigger of interaction.triggers) {
219
- if (!('itemId' in trigger)) continue;
220
- itemsToNotify.add(trigger.itemId);
221
- }
222
- this.notifyItemCtrlsChange(Array.from(itemsToNotify));
223
- this.notifyTransitionStartForItems(transitioningItems, activeStateId);
224
- }
225
- }
226
-
227
- scrollPositionNotifyScroll(trigger: InteractionScrollTrigger, position: number): boolean {
228
- if (trigger.position === 0) return false;
229
- const triggerPosition = trigger.position * window.innerWidth;
230
- return triggerPosition < position;
231
- }
232
-
233
- scrollItemPositionNotifyScroll(trigger: InteractionItemScrollTrigger, position: number, interaction: Interaction): boolean {
234
- let triggerPosition;
235
- let isScrolledPastTrigger = false;
236
- const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
237
- if (!itemRect) return false;
238
- switch (trigger.screenPosition) {
239
- case 'top':
240
- triggerPosition = 0;
241
- break;
242
- case 'center':
243
- triggerPosition = window.innerHeight / 2;
244
- break;
245
- case 'bottom':
246
- triggerPosition = window.innerHeight;
247
- break;
248
- default:
249
- triggerPosition = 0;
250
- break;
251
- }
252
- triggerPosition -= trigger.offset;
253
- switch (trigger.itemPosition) {
254
- case 'top':
255
- isScrolledPastTrigger = itemRect.y < triggerPosition;
256
- break;
257
- case 'center':
258
- isScrolledPastTrigger = itemRect.y + itemRect.height / 2 < triggerPosition;
259
- break;
260
- case 'bottom':
261
- isScrolledPastTrigger = itemRect.bottom < triggerPosition;
262
- break;
263
- default:
264
- isScrolledPastTrigger = itemRect.y < triggerPosition;
265
- break;
266
- }
267
- if (isScrolledPastTrigger && !this.itemScrollPositionAnimationMap.get(interaction.id)) {
268
- this.itemScrollPositionAnimationMap.set(interaction.id, position + itemRect.y - triggerPosition);
269
- }
270
- if (this.itemScrollPositionAnimationMap.get(interaction.id)! >= position) isScrolledPastTrigger = false;
271
- return isScrolledPastTrigger;
272
- }
273
-
274
- notifyScroll(position: number) {
275
- const timestamp = Date.now();
276
- for (const interaction of this.interactions) {
277
- const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
278
- const activeStateId = interaction.states.find((state) => state.id !== interaction.startStateId)?.id;
279
- const matchingTrigger = interaction.triggers.find((trigger) => {
280
- if (trigger.type !== 'scroll-position' && trigger.type !== 'item-scroll-position') return false;
281
- let isScrolledPastTrigger = false;
282
- if (trigger.type === 'scroll-position') {
283
- isScrolledPastTrigger = this.scrollPositionNotifyScroll(trigger, position);
284
- }
285
- if (trigger.type === 'item-scroll-position') {
286
- isScrolledPastTrigger = this.scrollItemPositionNotifyScroll(trigger, position, interaction);
287
- }
288
- if (!isScrolledPastTrigger && !trigger.isReverse) return false;
289
- const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
290
- return stateId === currentStateId;
291
- });
292
- if (!matchingTrigger || (matchingTrigger.type !== 'scroll-position' && matchingTrigger.type !== 'item-scroll-position') || !activeStateId) continue;
293
- let isScrolledPastTrigger = false;
294
- if (matchingTrigger.type === 'scroll-position') {
295
- const triggerPosition = matchingTrigger.position * window.innerWidth;
296
- isScrolledPastTrigger = triggerPosition < position;
297
- }
298
- if (matchingTrigger.type === 'item-scroll-position') {
299
- isScrolledPastTrigger = this.itemScrollPositionAnimationMap.get(interaction.id)! < position; // scroll down
300
- }
301
- const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
302
- this.setCurrentStateForInteraction(interaction.id, targetStateId);
303
- const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
304
- const state = interaction.states.find((state) => state.id === targetStateId);
305
- const actions = state?.actions ?? [];
306
- for (const action of actions) {
307
- const ctrl = this.ctrls.get(action.itemId);
308
- if (!ctrl) continue;
309
- ctrl.receiveAction(action.type);
310
- }
311
- const itemsStages = this.itemsStages.map((stage) => {
312
- if (stage.interactionId !== interaction.id) return stage;
313
- const newStage = {
314
- itemId: stage.itemId,
315
- interactionId: stage.interactionId,
316
- type: 'transitioning' as const,
317
- from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
318
- to: targetStateId,
319
- direction: targetStateId === activeStateId ? 'in' as const : 'out' as const,
320
- updated: timestamp
321
- };
322
- return newStage;
323
- });
324
- this.itemsStages = itemsStages;
325
- const itemsToNotify = new Set<ItemId>(transitioningItems);
326
- for (const trigger of interaction.triggers) {
327
- if (!('itemId' in trigger)) continue;
328
- itemsToNotify.add(trigger.itemId);
329
- }
330
- this.notifyItemCtrlsChange(Array.from(itemsToNotify));
331
- this.notifyTransitionStartForItems(transitioningItems, activeStateId);
332
- }
333
- }
334
-
335
- notifyItemTrigger(itemId: string, triggerType: TriggerType): void {
336
- const timestamp = Date.now();
337
- if (this.isItemScrollBasedTrigger(itemId)) return;
338
- for (const interaction of this.interactions) {
339
- const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
340
- const hasScrollTriggers = interaction.triggers.find((trigger) => trigger.type === 'item-scroll-position' || trigger.type === 'scroll-position');
341
- const matchingTrigger = interaction.triggers.find((trigger) =>
342
- 'triggerEvent' in trigger
343
- && trigger.itemId === itemId
344
- && trigger.from === currentStateId
345
- && trigger.triggerEvent === triggerType
346
- );
347
- if (!matchingTrigger || hasScrollTriggers) continue;
348
- const activeStateId = this.getActiveInteractionState(interaction.id);
349
- const isNewStateActive = matchingTrigger.to === activeStateId;
350
- this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
351
- const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
352
- const state = interaction.states.find((state) => state.id === matchingTrigger.to);
353
- const actions = state?.actions ?? [];
354
- for (const action of actions) {
355
- const ctrl = this.ctrls.get(action.itemId);
356
- if (!ctrl) continue;
357
- ctrl.receiveAction(action.type);
358
- }
359
- this.itemsStages = this.itemsStages.map((stage) => {
360
- if (stage.interactionId !== interaction.id) return stage;
361
- return {
362
- itemId: stage.itemId,
363
- interactionId: stage.interactionId,
364
- type: 'transitioning',
365
- from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
366
- to: matchingTrigger.to,
367
- direction: isNewStateActive ? 'in' : 'out',
368
- updated: timestamp
369
- };
370
- });
371
- const itemsToNotify = new Set<ItemId>(transitioningItems);
372
- for (const trigger of interaction.triggers) {
373
- if (!('itemId' in trigger)) continue;
374
- itemsToNotify.add(trigger.itemId);
375
- }
376
- this.notifyItemCtrlsChange(Array.from(itemsToNotify));
377
- this.notifyTransitionStartForItems(transitioningItems, activeStateId);
378
- }
379
- }
380
-
381
- notifyTransitionStartForItems(itemsIds: string[], activeStateId: string) {
382
- for (const itemId of itemsIds) {
383
- const ctrl = this.ctrls.get(itemId);
384
- const item = this.items.find((item) => item.id === itemId)!;
385
- const keys = Object.keys(item.state[activeStateId] ?? {});
386
- ctrl?.handleTransitionStart?.(keys);
387
- }
388
- }
389
-
390
- notifyTransitionEnd(itemId: string): void {
391
- const timestamp = Date.now();
392
- this.itemsStages = this.itemsStages.map((stage) => {
393
- if (stage.itemId !== itemId || stage.type !== 'transitioning') return stage;
394
- return {
395
- itemId,
396
- interactionId: stage.interactionId,
397
- type: 'active',
398
- stateId: stage.to,
399
- isStartState: stage.direction === 'out',
400
- updated: timestamp
401
- };
402
- });
403
- this.ctrls.get(itemId)?.receiveChange();
404
- }
405
-
406
- notifyPageOverlay(itemId: string): void {
407
- this.notifyItemTrigger(itemId, 'hover-out');
408
- }
409
-
410
- private getCurrentStateByInteractionId(id: InteractionId): string {
411
- let state;
412
- for (const interactionId of Object.keys(this.interactionStateMap)) {
413
- if (id !== interactionId) continue;
414
- state = this.interactionStateMap[interactionId];
415
- }
416
- if (!state) throw new Error(`Failed to find current state for interaction w/ id="${id}"`);
417
- return state;
418
- }
419
-
420
- private setCurrentStateForInteraction(interactionId: InteractionId, stateId: StateId) {
421
- this.interactionStateMap = {
422
- ...this.interactionStateMap,
423
- [interactionId]: stateId
424
- };
425
- }
426
-
427
- private getActiveInteractionState(interactionId: InteractionId): string {
428
- const { interactions } = this;
429
- const interaction = interactions.find((interaction) => interaction.id === interactionId)!;
430
- const activeStateId = interaction.states.find(state => state.id !== interaction.startStateId)?.id;
431
- if (!activeStateId) {
432
- throw new Error(`Failed to find active state for interaction w/ id="${interactionId}"`);
433
- }
434
- return activeStateId;
435
- }
436
-
437
- private notifyItemCtrlsChange(itemsIds: string[]) {
438
- for (const itemId of itemsIds) {
439
- this.ctrls.get(itemId)?.receiveChange();
440
- }
441
- }
442
-
443
- private unpackItems(article: Article): ItemAny[] {
444
- const itemsArr = [];
445
- for (const section of article.sections) {
446
- const items = this.getNestedItems(section.items);
447
- itemsArr.push(...items);
448
- }
449
- return itemsArr;
450
- }
451
-
452
- private getNestedItems(items: ItemAny[]): ItemAny[] {
453
- const allItems: ItemAny[] = [];
454
- for (const item of items) {
455
- if (isItemType(item, ArticleItemType.Group) || isItemType(item, ArticleItemType.Compound)) {
456
- const groupChildren = item?.items ?? [];
457
- const nestedItems = this.getNestedItems(groupChildren);
458
- allItems.push(...nestedItems);
459
- }
460
- allItems.push(item);
461
- }
462
- return allItems;
463
- }
464
-
465
- private getDefaultItemStages(): ItemStages {
466
- const timestamp = Date.now();
467
- const { items } = this;
468
- const stages: ItemStages = [];
469
- for (const item of items) {
470
- const itemStatesMap = item.state;
471
- if (!itemStatesMap) continue;
472
- for (const stateId of Object.keys(itemStatesMap)) {
473
- const interactionId = this.activeStateIdInteractionIdMap[stateId];
474
- if (!interactionId) continue;
475
- stages.push({
476
- itemId: item.id,
477
- interactionId,
478
- type: 'active',
479
- isStartState: true,
480
- updated: timestamp,
481
- stateId: this.interactions.find((interaction) => interaction.id === interactionId)?.startStateId
482
- });
483
- }
484
- }
485
- return stages;
486
- }
487
- }
488
-
489
- type ItemStages = (TransitioningStage | ActiveStage)[];
490
- type TransitioningStage = {
491
- itemId: string;
492
- interactionId: string;
493
- type: 'transitioning';
494
- from: StateId;
495
- to: StateId;
496
- direction: 'in' | 'out';
497
- updated: number;
498
- };
499
- type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
500
- type InteractionStateMap = Record<InteractionId, StateId>;
501
- type StateItemsIdsMap = Record<StateId, ItemId[]>;
502
- type TriggerType = InteractionItemTrigger['triggerEvent'];
503
- type InteractionId = string;
504
- type StateId = string;
505
- type ItemId = string;
506
- type StateProps = Record<string, {
507
- value?: string | number;
508
- transition?: {
509
- timing: string;
510
- duration: number;
511
- delay: number;
512
- };
513
- }>;
1
+ import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
2
+ import {
3
+ Article,
4
+ ArticleItemType,
5
+ Interaction,
6
+ InteractionItemTrigger,
7
+ ItemAny,
8
+ InteractionItemScrollTrigger,
9
+ InteractionScrollTrigger,
10
+ } from '@cntrl-site/sdk';
11
+ import { isItemType } from '../utils/isItemType';
12
+ import { ItemGeometryService } from '../ItemGeometry/ItemGeometryService';
13
+
14
+ export class InteractionsRegistry implements InteractionsRegistryPort {
15
+ private ctrls: Map<ItemId, ItemInteractionCtrl> = new Map();
16
+ private items: ItemAny[];
17
+ private itemGeometryService: ItemGeometryService;
18
+ private interactions: Interaction[];
19
+ private stateItemsIdsMap: StateItemsIdsMap;
20
+ private interactionStateMap: InteractionStateMap;
21
+ private itemsStages: ItemStages;
22
+ private activeStateIdInteractionIdMap: Record<StateId, InteractionId>;
23
+ private itemScrollPositionAnimationMap: Map<string, number> = new Map();
24
+
25
+ constructor(article: Article, layoutId: string, itemGeometryService: ItemGeometryService) {
26
+ this.items = this.unpackItems(article);
27
+ this.itemGeometryService = itemGeometryService;
28
+ const interactions = article.interactions[layoutId] ?? [];
29
+ const activeStatesIds = interactions.reduce<StateId[]>((map, inter) => {
30
+ const activeStateId = inter.states.find((state) => state.id !== inter.startStateId)?.id;
31
+ if (!activeStateId) {
32
+ throw new Error(`Failed to find active state for interaction w/ id="${inter.id}"`);
33
+ }
34
+ map.push(activeStateId);
35
+ return map;
36
+ }, []);
37
+ const interactionStateMap = interactions.reduce<InteractionStateMap>((map, { id, startStateId }) => {
38
+ map[id] = startStateId;
39
+ return map;
40
+ }, {});
41
+ this.activeStateIdInteractionIdMap = interactions.reduce<Record<StateId, InteractionId>>((map, interaction) => {
42
+ const activeState = interaction.states.find((state) => state.id !== interaction.startStateId);
43
+ if (activeState) {
44
+ map[activeState.id] = interaction.id;
45
+ }
46
+ return map;
47
+ }, {});
48
+ const stateItemsIdsMap = activeStatesIds.reduce<StateItemsIdsMap>((map, stateId) => {
49
+ map[stateId] = this.items
50
+ .filter((item) => {
51
+ const state = item.state[stateId] ?? {};
52
+ const hasKeys = Object.keys(state).length !== 0;
53
+ return hasKeys;
54
+ })
55
+ .map((item) => item.id);
56
+ return map;
57
+ }, {});
58
+ this.interactions = interactions;
59
+ this.stateItemsIdsMap = stateItemsIdsMap;
60
+ this.interactionStateMap = interactionStateMap;
61
+ const itemStages = this.getDefaultItemStages();
62
+ this.itemsStages = itemStages;
63
+ this.itemScrollPositionAnimationMap = new Map();
64
+ }
65
+
66
+ register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
67
+ this.ctrls.set(itemId, ctrl);
68
+ }
69
+
70
+ getStatePropsForItem(itemId: string) {
71
+ const { items } = this;
72
+ const item = items.find((item) => item.id === itemId)!;
73
+ const itemStages = this.itemsStages.filter((stage) => stage.itemId === itemId);
74
+ itemStages.sort((a, b) => a.updated - b.updated);
75
+ const itemStyles: StateProps = {};
76
+ for (const stage of itemStages) {
77
+ if (stage.type === 'active') {
78
+ if (stage.isStartState) continue;
79
+ const params = item.state[stage.stateId!] ?? {};
80
+ for (const [key, stateDetails] of Object.entries(params)) {
81
+ itemStyles[key] = {
82
+ value: stateDetails.value
83
+ };
84
+ }
85
+ }
86
+ if (stage.type === 'transitioning') {
87
+ const activeStateId = stage.direction === 'in' ? stage.to : stage.from;
88
+ const params = item.state[activeStateId] ?? {};
89
+ for (const [key, stateDetails] of Object.entries(params)) {
90
+ const transitionDetails = stateDetails[stage.direction];
91
+ if (!transitionDetails) continue;
92
+ itemStyles[key] = {
93
+ value: stage.direction === 'in' ? stateDetails.value : itemStyles[key]?.value,
94
+ transition: {
95
+ timing: transitionDetails.timing,
96
+ duration: transitionDetails.duration,
97
+ delay: transitionDetails.delay
98
+ }
99
+ };
100
+ }
101
+ }
102
+ }
103
+ return itemStyles;
104
+ }
105
+
106
+ isItemScrollBasedTrigger(itemId: string): boolean {
107
+ let isElementHasScrollBasedTrigger = false;
108
+ for (const interaction of this.interactions) {
109
+ const hasInteractionItemScrollBasedTrigger = interaction.triggers.find((tr) => (tr.type === 'item-scroll-position' && tr.itemId === itemId)) !== undefined;
110
+ const hasInteractionScrollBasedTrigger = interaction.triggers.find((tr) => tr.type === 'scroll-position') !== undefined;
111
+ if (hasInteractionScrollBasedTrigger) {
112
+ for (const trigger of interaction.triggers) {
113
+ if (trigger.type === 'item' && trigger.itemId === itemId) {
114
+ isElementHasScrollBasedTrigger = true;
115
+ return true;
116
+ }
117
+ }
118
+ }
119
+ if (hasInteractionItemScrollBasedTrigger) {
120
+ isElementHasScrollBasedTrigger = true;
121
+ }
122
+ }
123
+ return isElementHasScrollBasedTrigger;
124
+ }
125
+
126
+ getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['triggerEvent']> {
127
+ const available = new Set<InteractionItemTrigger['triggerEvent']>();
128
+ const activeStates = Object.values(this.interactionStateMap);
129
+ if (this.isItemScrollBasedTrigger(itemId)) return available;
130
+ for (const interaction of this.interactions) {
131
+ const { triggers } = interaction;
132
+ for (const trigger of triggers) {
133
+ if (!('triggerEvent' in trigger)) continue;
134
+ if (trigger.itemId !== itemId) continue;
135
+ if (activeStates.includes(trigger.from)) {
136
+ available.add(trigger.triggerEvent);
137
+ }
138
+ }
139
+ }
140
+ return available;
141
+ }
142
+
143
+ getTriggerPositionForItemScrollTrigger(trigger: InteractionItemScrollTrigger): number {
144
+ const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
145
+ let triggerPosition = 0;
146
+ switch (trigger.screenPosition) {
147
+ case 'top':
148
+ triggerPosition = itemRect.top;
149
+ break;
150
+ case 'center':
151
+ triggerPosition = itemRect.top - window.innerHeight / 2;
152
+ break;
153
+ case 'bottom':
154
+ triggerPosition = itemRect.top - window.innerHeight;
155
+ break;
156
+ default:
157
+ triggerPosition = itemRect.top;
158
+ break;
159
+ }
160
+ switch (trigger.itemPosition) {
161
+ case 'top':
162
+ triggerPosition += 0;
163
+ break;
164
+ case 'center':
165
+ triggerPosition += itemRect.height / 2;
166
+ break;
167
+ case 'bottom':
168
+ triggerPosition += itemRect.height;
169
+ break;
170
+ default:
171
+ triggerPosition += 0;
172
+ break;
173
+ }
174
+ triggerPosition += trigger.offset;
175
+ return triggerPosition;
176
+ }
177
+
178
+ isItemScrollTriggerOnStartScreen(trigger: InteractionItemScrollTrigger): boolean {
179
+ const triggerPosition = this.getTriggerPositionForItemScrollTrigger(trigger);
180
+ return triggerPosition < 0;
181
+ }
182
+
183
+ notifyLoad() {
184
+ const timestamp = Date.now();
185
+ for (const interaction of this.interactions) {
186
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
187
+ const matchingTrigger = interaction.triggers.find(trigger =>
188
+ ('position' in trigger && trigger.position === 0 && trigger.from === currentStateId)
189
+ || (trigger.type === 'item-scroll-position' && this.items.find((item) => item.id === trigger.itemId) && trigger.from === currentStateId && this.isItemScrollTriggerOnStartScreen(trigger))
190
+ );
191
+ if (!matchingTrigger) continue;
192
+ const activeStateId = this.getActiveInteractionState(interaction.id);
193
+ const isNewStateActive = matchingTrigger.to === activeStateId;
194
+ this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
195
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
196
+ const state = interaction.states.find((state) => state.id === matchingTrigger.to);
197
+ const actions = state?.actions ?? [];
198
+ for (const action of actions) {
199
+ const ctrl = this.ctrls.get(action.itemId);
200
+ if (!ctrl) continue;
201
+ ctrl.receiveAction(action.type);
202
+ }
203
+
204
+ this.itemsStages = this.itemsStages.map((stage) => {
205
+ if (stage.interactionId !== interaction.id) return stage;
206
+ return {
207
+ itemId: stage.itemId,
208
+ interactionId: stage.interactionId,
209
+ type: 'transitioning',
210
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
211
+ to: matchingTrigger.to,
212
+ direction: isNewStateActive ? 'in' : 'out',
213
+ updated: timestamp
214
+ };
215
+ });
216
+
217
+ const itemsToNotify = new Set<ItemId>(transitioningItems);
218
+ for (const trigger of interaction.triggers) {
219
+ if (!('itemId' in trigger)) continue;
220
+ itemsToNotify.add(trigger.itemId);
221
+ }
222
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
223
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
224
+ }
225
+ }
226
+
227
+ scrollPositionNotifyScroll(trigger: InteractionScrollTrigger, position: number): boolean {
228
+ if (trigger.position === 0) return false;
229
+ const triggerPosition = trigger.position * window.innerWidth;
230
+ return triggerPosition < position;
231
+ }
232
+
233
+ scrollItemPositionNotifyScroll(trigger: InteractionItemScrollTrigger, position: number, interaction: Interaction): boolean {
234
+ let triggerPosition;
235
+ let isScrolledPastTrigger = false;
236
+ const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
237
+ if (!itemRect) return false;
238
+ switch (trigger.screenPosition) {
239
+ case 'top':
240
+ triggerPosition = 0;
241
+ break;
242
+ case 'center':
243
+ triggerPosition = window.innerHeight / 2;
244
+ break;
245
+ case 'bottom':
246
+ triggerPosition = window.innerHeight;
247
+ break;
248
+ default:
249
+ triggerPosition = 0;
250
+ break;
251
+ }
252
+ triggerPosition -= trigger.offset;
253
+ switch (trigger.itemPosition) {
254
+ case 'top':
255
+ isScrolledPastTrigger = itemRect.y < triggerPosition;
256
+ break;
257
+ case 'center':
258
+ isScrolledPastTrigger = itemRect.y + itemRect.height / 2 < triggerPosition;
259
+ break;
260
+ case 'bottom':
261
+ isScrolledPastTrigger = itemRect.bottom < triggerPosition;
262
+ break;
263
+ default:
264
+ isScrolledPastTrigger = itemRect.y < triggerPosition;
265
+ break;
266
+ }
267
+ if (isScrolledPastTrigger && !this.itemScrollPositionAnimationMap.get(interaction.id)) {
268
+ this.itemScrollPositionAnimationMap.set(interaction.id, position + itemRect.y - triggerPosition);
269
+ }
270
+ if (this.itemScrollPositionAnimationMap.get(interaction.id)! >= position) isScrolledPastTrigger = false;
271
+ return isScrolledPastTrigger;
272
+ }
273
+
274
+ notifyScroll(position: number) {
275
+ const timestamp = Date.now();
276
+ for (const interaction of this.interactions) {
277
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
278
+ const activeStateId = interaction.states.find((state) => state.id !== interaction.startStateId)?.id;
279
+ const matchingTrigger = interaction.triggers.find((trigger) => {
280
+ if (trigger.type !== 'scroll-position' && trigger.type !== 'item-scroll-position') return false;
281
+ let isScrolledPastTrigger = false;
282
+ if (trigger.type === 'scroll-position') {
283
+ isScrolledPastTrigger = this.scrollPositionNotifyScroll(trigger, position);
284
+ }
285
+ if (trigger.type === 'item-scroll-position') {
286
+ isScrolledPastTrigger = this.scrollItemPositionNotifyScroll(trigger, position, interaction);
287
+ }
288
+ if (!isScrolledPastTrigger && !trigger.isReverse) return false;
289
+ const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
290
+ return stateId === currentStateId;
291
+ });
292
+ if (!matchingTrigger || (matchingTrigger.type !== 'scroll-position' && matchingTrigger.type !== 'item-scroll-position') || !activeStateId) continue;
293
+ let isScrolledPastTrigger = false;
294
+ if (matchingTrigger.type === 'scroll-position') {
295
+ const triggerPosition = matchingTrigger.position * window.innerWidth;
296
+ isScrolledPastTrigger = triggerPosition < position;
297
+ }
298
+ if (matchingTrigger.type === 'item-scroll-position') {
299
+ isScrolledPastTrigger = this.itemScrollPositionAnimationMap.get(interaction.id)! < position; // scroll down
300
+ }
301
+ const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
302
+ this.setCurrentStateForInteraction(interaction.id, targetStateId);
303
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
304
+ const state = interaction.states.find((state) => state.id === targetStateId);
305
+ const actions = state?.actions ?? [];
306
+ for (const action of actions) {
307
+ const ctrl = this.ctrls.get(action.itemId);
308
+ if (!ctrl) continue;
309
+ ctrl.receiveAction(action.type);
310
+ }
311
+ const itemsStages = this.itemsStages.map((stage) => {
312
+ if (stage.interactionId !== interaction.id) return stage;
313
+ const newStage = {
314
+ itemId: stage.itemId,
315
+ interactionId: stage.interactionId,
316
+ type: 'transitioning' as const,
317
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
318
+ to: targetStateId,
319
+ direction: targetStateId === activeStateId ? 'in' as const : 'out' as const,
320
+ updated: timestamp
321
+ };
322
+ return newStage;
323
+ });
324
+ this.itemsStages = itemsStages;
325
+ const itemsToNotify = new Set<ItemId>(transitioningItems);
326
+ for (const trigger of interaction.triggers) {
327
+ if (!('itemId' in trigger)) continue;
328
+ itemsToNotify.add(trigger.itemId);
329
+ }
330
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
331
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
332
+ }
333
+ }
334
+
335
+ notifyItemTrigger(itemId: string, triggerType: TriggerType): void {
336
+ const timestamp = Date.now();
337
+ if (this.isItemScrollBasedTrigger(itemId)) return;
338
+ for (const interaction of this.interactions) {
339
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
340
+ const hasScrollTriggers = interaction.triggers.find((trigger) => trigger.type === 'item-scroll-position' || trigger.type === 'scroll-position');
341
+ const matchingTrigger = interaction.triggers.find((trigger) =>
342
+ 'triggerEvent' in trigger
343
+ && trigger.itemId === itemId
344
+ && trigger.from === currentStateId
345
+ && trigger.triggerEvent === triggerType
346
+ );
347
+ if (!matchingTrigger || hasScrollTriggers) continue;
348
+ const activeStateId = this.getActiveInteractionState(interaction.id);
349
+ const isNewStateActive = matchingTrigger.to === activeStateId;
350
+ this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
351
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
352
+ const state = interaction.states.find((state) => state.id === matchingTrigger.to);
353
+ const actions = state?.actions ?? [];
354
+ for (const action of actions) {
355
+ const ctrl = this.ctrls.get(action.itemId);
356
+ if (!ctrl) continue;
357
+ ctrl.receiveAction(action.type);
358
+ }
359
+ this.itemsStages = this.itemsStages.map((stage) => {
360
+ if (stage.interactionId !== interaction.id) return stage;
361
+ return {
362
+ itemId: stage.itemId,
363
+ interactionId: stage.interactionId,
364
+ type: 'transitioning',
365
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
366
+ to: matchingTrigger.to,
367
+ direction: isNewStateActive ? 'in' : 'out',
368
+ updated: timestamp
369
+ };
370
+ });
371
+ const itemsToNotify = new Set<ItemId>(transitioningItems);
372
+ for (const trigger of interaction.triggers) {
373
+ if (!('itemId' in trigger)) continue;
374
+ itemsToNotify.add(trigger.itemId);
375
+ }
376
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
377
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
378
+ }
379
+ }
380
+
381
+ notifyTransitionStartForItems(itemsIds: string[], activeStateId: string) {
382
+ for (const itemId of itemsIds) {
383
+ const ctrl = this.ctrls.get(itemId);
384
+ const item = this.items.find((item) => item.id === itemId)!;
385
+ const keys = Object.keys(item.state[activeStateId] ?? {});
386
+ ctrl?.handleTransitionStart?.(keys);
387
+ }
388
+ }
389
+
390
+ notifyTransitionEnd(itemId: string): void {
391
+ const timestamp = Date.now();
392
+ this.itemsStages = this.itemsStages.map((stage) => {
393
+ if (stage.itemId !== itemId || stage.type !== 'transitioning') return stage;
394
+ return {
395
+ itemId,
396
+ interactionId: stage.interactionId,
397
+ type: 'active',
398
+ stateId: stage.to,
399
+ isStartState: stage.direction === 'out',
400
+ updated: timestamp
401
+ };
402
+ });
403
+ this.ctrls.get(itemId)?.receiveChange();
404
+ }
405
+
406
+ notifyPageOverlay(itemId: string): void {
407
+ this.notifyItemTrigger(itemId, 'hover-out');
408
+ }
409
+
410
+ private getCurrentStateByInteractionId(id: InteractionId): string {
411
+ let state;
412
+ for (const interactionId of Object.keys(this.interactionStateMap)) {
413
+ if (id !== interactionId) continue;
414
+ state = this.interactionStateMap[interactionId];
415
+ }
416
+ if (!state) throw new Error(`Failed to find current state for interaction w/ id="${id}"`);
417
+ return state;
418
+ }
419
+
420
+ private setCurrentStateForInteraction(interactionId: InteractionId, stateId: StateId) {
421
+ this.interactionStateMap = {
422
+ ...this.interactionStateMap,
423
+ [interactionId]: stateId
424
+ };
425
+ }
426
+
427
+ private getActiveInteractionState(interactionId: InteractionId): string {
428
+ const { interactions } = this;
429
+ const interaction = interactions.find((interaction) => interaction.id === interactionId)!;
430
+ const activeStateId = interaction.states.find(state => state.id !== interaction.startStateId)?.id;
431
+ if (!activeStateId) {
432
+ throw new Error(`Failed to find active state for interaction w/ id="${interactionId}"`);
433
+ }
434
+ return activeStateId;
435
+ }
436
+
437
+ private notifyItemCtrlsChange(itemsIds: string[]) {
438
+ for (const itemId of itemsIds) {
439
+ this.ctrls.get(itemId)?.receiveChange();
440
+ }
441
+ }
442
+
443
+ private unpackItems(article: Article): ItemAny[] {
444
+ const itemsArr = [];
445
+ for (const section of article.sections) {
446
+ const items = this.getNestedItems(section.items);
447
+ itemsArr.push(...items);
448
+ }
449
+ return itemsArr;
450
+ }
451
+
452
+ private getNestedItems(items: ItemAny[]): ItemAny[] {
453
+ const allItems: ItemAny[] = [];
454
+ for (const item of items) {
455
+ if (isItemType(item, ArticleItemType.Group) || isItemType(item, ArticleItemType.Compound)) {
456
+ const groupChildren = item?.items ?? [];
457
+ const nestedItems = this.getNestedItems(groupChildren);
458
+ allItems.push(...nestedItems);
459
+ }
460
+ allItems.push(item);
461
+ }
462
+ return allItems;
463
+ }
464
+
465
+ private getDefaultItemStages(): ItemStages {
466
+ const timestamp = Date.now();
467
+ const { items } = this;
468
+ const stages: ItemStages = [];
469
+ for (const item of items) {
470
+ const itemStatesMap = item.state;
471
+ if (!itemStatesMap) continue;
472
+ for (const stateId of Object.keys(itemStatesMap)) {
473
+ const interactionId = this.activeStateIdInteractionIdMap[stateId];
474
+ if (!interactionId) continue;
475
+ stages.push({
476
+ itemId: item.id,
477
+ interactionId,
478
+ type: 'active',
479
+ isStartState: true,
480
+ updated: timestamp,
481
+ stateId: this.interactions.find((interaction) => interaction.id === interactionId)?.startStateId
482
+ });
483
+ }
484
+ }
485
+ return stages;
486
+ }
487
+ }
488
+
489
+ type ItemStages = (TransitioningStage | ActiveStage)[];
490
+ type TransitioningStage = {
491
+ itemId: string;
492
+ interactionId: string;
493
+ type: 'transitioning';
494
+ from: StateId;
495
+ to: StateId;
496
+ direction: 'in' | 'out';
497
+ updated: number;
498
+ };
499
+ type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
500
+ type InteractionStateMap = Record<InteractionId, StateId>;
501
+ type StateItemsIdsMap = Record<StateId, ItemId[]>;
502
+ type TriggerType = InteractionItemTrigger['triggerEvent'];
503
+ type InteractionId = string;
504
+ type StateId = string;
505
+ type ItemId = string;
506
+ type StateProps = Record<string, {
507
+ value?: string | number;
508
+ transition?: {
509
+ timing: string;
510
+ duration: number;
511
+ delay: number;
512
+ };
513
+ }>;