@cntrl-site/sdk-nextjs 1.9.0 → 1.9.2

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 (130) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +4 -4
  3. package/TODO.md +1 -1
  4. package/cntrl-site-sdk-nextjs-1.8.40.tgz +0 -0
  5. package/eslint.config.mjs +35 -35
  6. package/jest.config.js +6 -6
  7. package/lib/common/useItemFXData.js +7 -7
  8. package/lib/components/Article.js +5 -5
  9. package/lib/components/LayoutStyle.js +4 -4
  10. package/lib/components/Section/Section.js +27 -27
  11. package/lib/components/items/CodeEmbedItem/CodeEmbedItem.js +36 -36
  12. package/lib/components/items/ComponentItem/ComponentItem.js +13 -13
  13. package/lib/components/items/CompoundItem/CompoundChild.js +20 -20
  14. package/lib/components/items/CompoundItem/CompoundItem.js +14 -14
  15. package/lib/components/items/CustomItem/CustomItem.js +9 -9
  16. package/lib/components/items/EmbedVideoItem/VimeoEmbed.js +24 -24
  17. package/lib/components/items/EmbedVideoItem/YoutubeEmbed.js +27 -27
  18. package/lib/components/items/FileItem/ImageItem.js +52 -52
  19. package/lib/components/items/FileItem/VideoItem.js +76 -76
  20. package/lib/components/items/GroupItem/GroupItem.js +14 -14
  21. package/lib/components/items/Item.js +30 -30
  22. package/lib/components/items/RectangleItem/RectangleItem.js +39 -39
  23. package/lib/components/items/RichTextItem/RichTextItem.js +22 -22
  24. package/lib/utils/RichTextConverter/RichTextConverter.js +21 -21
  25. package/package.json +58 -57
  26. package/src/common/useCurrentLayout.ts +52 -52
  27. package/src/common/useExemplary.ts +9 -9
  28. package/src/common/useItemFXData.ts +47 -47
  29. package/src/common/useKeyframeValue.ts +85 -85
  30. package/src/common/useRegisterResize.ts +16 -16
  31. package/src/components/Article.tsx +73 -73
  32. package/src/components/ArticleWrapper.tsx +21 -21
  33. package/src/components/Head.tsx +56 -56
  34. package/src/components/LayoutStyle.tsx +21 -21
  35. package/src/components/Page.tsx +33 -33
  36. package/src/components/ScrollPlaybackVideo.tsx +56 -56
  37. package/src/components/Section/Section.tsx +127 -127
  38. package/src/components/Section/SectionImage.tsx +48 -48
  39. package/src/components/Section/SectionVideo.tsx +144 -144
  40. package/src/components/Section/useSectionColor.ts +19 -19
  41. package/src/components/Section/useSectionHeightMap.ts +19 -19
  42. package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +145 -145
  43. package/src/components/items/CodeEmbedItem/useCodeEmbedItem.ts +36 -36
  44. package/src/components/items/ComponentItem/ComponentItem.tsx +67 -67
  45. package/src/components/items/ComponentItem/useComponentItem.ts +35 -35
  46. package/src/components/items/CompoundItem/CompoundChild.tsx +150 -150
  47. package/src/components/items/CompoundItem/CompoundItem.tsx +70 -70
  48. package/src/components/items/CompoundItem/useCompoundItem.ts +21 -21
  49. package/src/components/items/CustomItem/CustomItem.tsx +48 -48
  50. package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +206 -206
  51. package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +193 -193
  52. package/src/components/items/EmbedVideoItem/useEmbedVideoItem.ts +46 -46
  53. package/src/components/items/FileItem/ImageItem.tsx +211 -211
  54. package/src/components/items/FileItem/VideoItem.tsx +328 -328
  55. package/src/components/items/FileItem/useFileItem.ts +81 -81
  56. package/src/components/items/GroupItem/GroupItem.tsx +81 -81
  57. package/src/components/items/GroupItem/useGroupItem.ts +34 -34
  58. package/src/components/items/Item.tsx +251 -251
  59. package/src/components/items/LinkWrapper.tsx +39 -39
  60. package/src/components/items/RectangleItem/RectangleItem.tsx +199 -199
  61. package/src/components/items/RectangleItem/useRectangleItem.ts +89 -89
  62. package/src/components/items/RichTextItem/RichTextItem.tsx +107 -107
  63. package/src/components/items/RichTextItem/useRichTextItem.ts +12 -12
  64. package/src/components/items/RichTextWrapper.tsx +15 -15
  65. package/src/components/items/itemsMap.ts +28 -28
  66. package/src/components/items/useDraggable.ts +252 -252
  67. package/src/components/items/useItemAngle.ts +13 -13
  68. package/src/components/items/useItemArea.ts +35 -35
  69. package/src/components/items/useItemPointerEvents.ts +23 -23
  70. package/src/components/items/useItemScale.ts +17 -17
  71. package/src/components/items/useItemTriggers.ts +15 -15
  72. package/src/components/items/useRichTextItemValues.ts +65 -65
  73. package/src/components/items/useSizing.ts +25 -25
  74. package/src/components/items/useStickyItemTop.ts +21 -21
  75. package/src/components/useLayoutContext.ts +7 -7
  76. package/src/index.ts +24 -24
  77. package/src/interactions/CSSPropertyNameMap.ts +37 -37
  78. package/src/interactions/InteractionsRegistry.ts +380 -380
  79. package/src/interactions/ItemInteractionCtrl.ts +78 -78
  80. package/src/interactions/getTransition.ts +27 -27
  81. package/src/interactions/types.ts +36 -36
  82. package/src/interactions/useItemInteractionCtrl.ts +18 -18
  83. package/src/provider/ArticleRectContext.ts +4 -4
  84. package/src/provider/CntrlContext.ts +5 -5
  85. package/src/provider/CntrlProvider.tsx +11 -11
  86. package/src/provider/CntrlSdkContext.ts +83 -83
  87. package/src/provider/CustomItemRegistry.ts +14 -14
  88. package/src/provider/CustomItemTypes.ts +3 -3
  89. package/src/provider/CustomSectionRegistry.ts +25 -25
  90. package/src/provider/InteractionsContext.tsx +59 -59
  91. package/src/provider/Keyframes.ts +11 -11
  92. package/src/provider/KeyframesContext.ts +5 -5
  93. package/src/provider/LayoutContext.ts +3 -3
  94. package/src/provider/WebGLContextManagerContext.tsx +4 -4
  95. package/src/provider/defaultContext.ts +8 -8
  96. package/src/provider/useCntrlContext.ts +8 -8
  97. package/src/styled-jsx.d.ts +1 -1
  98. package/src/utils/Animator/Animator.ts +571 -571
  99. package/src/utils/ArticleRectManager/ArticleRectObserver.ts +93 -93
  100. package/src/utils/ArticleRectManager/useArticleRectObserver.ts +17 -17
  101. package/src/utils/ArticleRectManager/useSectionRegistry.ts +11 -11
  102. package/src/utils/EventEmitter.ts +41 -41
  103. package/src/utils/RichTextConverter/RichTextConverter.tsx +326 -326
  104. package/src/utils/ScaleAnchorMap.ts +13 -13
  105. package/src/utils/StickyManager/StickyManager.ts +23 -23
  106. package/src/utils/Youtube/YouTubeIframeApiLoader.ts +58 -58
  107. package/src/utils/Youtube/YoutubeIframeApi.ts +101 -101
  108. package/src/utils/Youtube/useYouTubeIframeApi.ts +12 -12
  109. package/src/utils/areFillsVisible/areFillsVisible.test.ts +63 -63
  110. package/src/utils/areFillsVisible/areFillsVisible.ts +37 -37
  111. package/src/utils/binSearchInsertAt.ts +33 -33
  112. package/src/utils/castObject.ts +10 -10
  113. package/src/utils/checkOverflowClipSupport.ts +13 -13
  114. package/src/utils/effects/useImageFx.ts +156 -156
  115. package/src/utils/effects/useVideoFx.ts +158 -158
  116. package/src/utils/getAnchoredItemTop.ts +12 -12
  117. package/src/utils/getCompoundBoundaryStyles.ts +60 -60
  118. package/src/utils/getFill.ts +46 -46
  119. package/src/utils/getFontFamilyValue.ts +3 -3
  120. package/src/utils/getInvertedRanges.ts +43 -43
  121. package/src/utils/getItemTopStyle.ts +14 -14
  122. package/src/utils/getStyleFromItemStateAndParams.ts +8 -8
  123. package/src/utils/getValidYoutubeUrl.ts +29 -29
  124. package/src/utils/isItemType.ts +5 -5
  125. package/src/utils/rangeMap.ts +13 -13
  126. package/src/utils/useElementRect.ts +23 -23
  127. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  128. package/.idea/modules.xml +0 -8
  129. package/.idea/sdk-nextjs.iml +0 -12
  130. package/.idea/vcs.xml +0 -6
@@ -1,380 +1,380 @@
1
- import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
2
- import {
3
- Article,
4
- ArticleItemType,
5
- Interaction,
6
- InteractionItemTrigger,
7
- ItemAny,
8
- } from '@cntrl-site/sdk';
9
- import { isItemType } from '../utils/isItemType';
10
-
11
- export class InteractionsRegistry implements InteractionsRegistryPort {
12
- private ctrls: Map<ItemId, ItemInteractionCtrl> = new Map();
13
- private items: ItemAny[];
14
- private interactions: Interaction[];
15
- private stateItemsIdsMap: StateItemsIdsMap;
16
- private interactionStateMap: InteractionStateMap;
17
- private itemsStages: ItemStages;
18
- private activeStateIdInteractionIdMap: Record<StateId, InteractionId>;
19
-
20
- constructor(article: Article, layoutId: string) {
21
- this.items = this.unpackItems(article);
22
- const interactions = article.interactions[layoutId] ?? [];
23
- const activeStatesIds = interactions.reduce<StateId[]>((map, inter) => {
24
- const activeStateId = inter.states.find((state) => state.id !== inter.startStateId)?.id;
25
- if (!activeStateId) {
26
- throw new Error(`Failed to find active state for interaction w/ id="${inter.id}"`);
27
- }
28
- map.push(activeStateId);
29
- return map;
30
- }, []);
31
- const interactionStateMap = interactions.reduce<InteractionStateMap>((map, { id, startStateId }) => {
32
- map[id] = startStateId;
33
- return map;
34
- }, {});
35
- this.activeStateIdInteractionIdMap = interactions.reduce<Record<StateId, InteractionId>>((map, interaction) => {
36
- const activeState = interaction.states.find((state) => state.id !== interaction.startStateId);
37
- if (activeState) {
38
- map[activeState.id] = interaction.id;
39
- }
40
- return map;
41
- }, {});
42
- const stateItemsIdsMap = activeStatesIds.reduce<StateItemsIdsMap>((map, stateId) => {
43
- map[stateId] = this.items
44
- .filter((item) => {
45
- const state = item.state[stateId] ?? {};
46
- const hasKeys = Object.keys(state).length !== 0;
47
- return hasKeys;
48
- })
49
- .map((item) => item.id);
50
- return map;
51
- }, {});
52
- this.interactions = interactions;
53
- this.stateItemsIdsMap = stateItemsIdsMap;
54
- this.interactionStateMap = interactionStateMap;
55
- const itemStages = this.getDefaultItemStages();
56
- this.itemsStages = itemStages;
57
- }
58
-
59
- register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
60
- this.ctrls.set(itemId, ctrl);
61
- }
62
-
63
- getStatePropsForItem(itemId: string) {
64
- const { items } = this;
65
- const item = items.find((item) => item.id === itemId)!;
66
- const itemStages = this.itemsStages.filter((stage) => stage.itemId === itemId);
67
- itemStages.sort((a, b) => a.updated - b.updated);
68
- const itemStyles: StateProps = {};
69
- for (const stage of itemStages) {
70
- if (stage.type === 'active') {
71
- if (stage.isStartState) continue;
72
- const params = item.state[stage.stateId!] ?? {};
73
- for (const [key, stateDetails] of Object.entries(params)) {
74
- itemStyles[key] = {
75
- value: stateDetails.value
76
- };
77
- }
78
- }
79
- if (stage.type === 'transitioning') {
80
- const activeStateId = stage.direction === 'in' ? stage.to : stage.from;
81
- const params = item.state[activeStateId] ?? {};
82
- for (const [key, stateDetails] of Object.entries(params)) {
83
- const transitionDetails = stateDetails[stage.direction];
84
- if (!transitionDetails) continue;
85
- itemStyles[key] = {
86
- value: stage.direction === 'in' ? stateDetails.value : itemStyles[key]?.value,
87
- transition: {
88
- timing: transitionDetails.timing,
89
- duration: transitionDetails.duration,
90
- delay: transitionDetails.delay
91
- }
92
- };
93
- }
94
- }
95
- }
96
- return itemStyles;
97
- }
98
-
99
- getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['type']> {
100
- const available = new Set<InteractionItemTrigger['type']>();
101
- const activeStates = Object.values(this.interactionStateMap);
102
- for (const interaction of this.interactions) {
103
- const { triggers } = interaction;
104
- for (const trigger of triggers) {
105
- if (!('itemId' in trigger)) continue;
106
- if (trigger.itemId !== itemId) continue;
107
- if (activeStates.includes(trigger.from)) {
108
- available.add(trigger.type);
109
- }
110
- }
111
- }
112
- return available;
113
- }
114
-
115
- notifyLoad() {
116
- const timestamp = Date.now();
117
- for (const interaction of this.interactions) {
118
- const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
119
- const matchingTrigger = interaction.triggers.find(trigger =>
120
- 'position' in trigger && trigger.position === 0 && trigger.from === currentStateId
121
- );
122
- if (!matchingTrigger) continue;
123
- const activeStateId = this.getActiveInteractionState(interaction.id);
124
- const isNewStateActive = matchingTrigger.to === activeStateId;
125
- this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
126
- const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
127
- const state = interaction.states.find((state) => state.id === matchingTrigger.to);
128
- const actions = state?.actions ?? [];
129
- for (const action of actions) {
130
- const ctrl = this.ctrls.get(action.itemId);
131
- if (!ctrl) continue;
132
- ctrl.receiveAction(action.type);
133
- }
134
-
135
- this.itemsStages = this.itemsStages.map((stage) => {
136
- if (stage.interactionId !== interaction.id) return stage;
137
- return {
138
- itemId: stage.itemId,
139
- interactionId: stage.interactionId,
140
- type: 'transitioning',
141
- from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
142
- to: matchingTrigger.to,
143
- direction: isNewStateActive ? 'in' : 'out',
144
- updated: timestamp
145
- };
146
- });
147
-
148
- const itemsToNotify = new Set<ItemId>(transitioningItems);
149
- for (const trigger of interaction.triggers) {
150
- if (!('itemId' in trigger)) continue;
151
- itemsToNotify.add(trigger.itemId);
152
- }
153
- this.notifyItemCtrlsChange(Array.from(itemsToNotify));
154
- this.notifyTransitionStartForItems(transitioningItems, activeStateId);
155
- }
156
- }
157
-
158
- notifyScroll(position: number) {
159
- const timestamp = Date.now();
160
- for (const interaction of this.interactions) {
161
- const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
162
- const activeStateId = interaction.states.find((state) => state.id !== interaction.startStateId)?.id;
163
- const matchingTrigger = interaction.triggers.find((trigger) => {
164
- if (!('position' in trigger) || trigger.position === 0) return false;
165
- const triggerPosition = trigger.position * window.innerWidth;
166
- const isScrolledPastTrigger = triggerPosition < position;
167
- if (!isScrolledPastTrigger && !trigger.isReverse) return false;
168
- const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
169
- return stateId === currentStateId;
170
- });
171
- if (!matchingTrigger || !('position' in matchingTrigger) || !activeStateId) continue;
172
- const triggerPosition = matchingTrigger.position * window.innerWidth;
173
- const isScrolledPastTrigger = triggerPosition < position;
174
- const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
175
- this.setCurrentStateForInteraction(interaction.id, targetStateId);
176
- const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
177
- const state = interaction.states.find((state) => state.id === targetStateId);
178
- const actions = state?.actions ?? [];
179
- for (const action of actions) {
180
- const ctrl = this.ctrls.get(action.itemId);
181
- if (!ctrl) continue;
182
- ctrl.receiveAction(action.type);
183
- }
184
- const itemsStages = this.itemsStages.map((stage) => {
185
- if (stage.interactionId !== interaction.id) return stage;
186
- const newStage = {
187
- itemId: stage.itemId,
188
- interactionId: stage.interactionId,
189
- type: 'transitioning' as const,
190
- from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
191
- to: targetStateId,
192
- direction: targetStateId === activeStateId ? 'in' as const : 'out' as const,
193
- updated: timestamp
194
- };
195
- return newStage;
196
- });
197
- this.itemsStages = itemsStages;
198
- const itemsToNotify = new Set<ItemId>(transitioningItems);
199
- for (const trigger of interaction.triggers) {
200
- if (!('itemId' in trigger)) continue;
201
- itemsToNotify.add(trigger.itemId);
202
- }
203
- this.notifyItemCtrlsChange(Array.from(itemsToNotify));
204
- this.notifyTransitionStartForItems(transitioningItems, activeStateId);
205
- }
206
- }
207
-
208
- notifyItemTrigger(itemId: string, triggerType: TriggerType): void {
209
- const timestamp = Date.now();
210
- for (const interaction of this.interactions) {
211
- const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
212
- const matchingTrigger = interaction.triggers.find((trigger) =>
213
- 'itemId' in trigger
214
- && trigger.itemId === itemId
215
- && trigger.from === currentStateId
216
- && trigger.type === triggerType
217
- );
218
- if (!matchingTrigger) continue;
219
- const activeStateId = this.getActiveInteractionState(interaction.id);
220
- const isNewStateActive = matchingTrigger.to === activeStateId;
221
- this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
222
- const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
223
- const state = interaction.states.find((state) => state.id === matchingTrigger.to);
224
- const actions = state?.actions ?? [];
225
- for (const action of actions) {
226
- const ctrl = this.ctrls.get(action.itemId);
227
- if (!ctrl) continue;
228
- ctrl.receiveAction(action.type);
229
- }
230
- this.itemsStages = this.itemsStages.map((stage) => {
231
- if (stage.interactionId !== interaction.id) return stage;
232
- return {
233
- itemId: stage.itemId,
234
- interactionId: stage.interactionId,
235
- type: 'transitioning',
236
- from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
237
- to: matchingTrigger.to,
238
- direction: isNewStateActive ? 'in' : 'out',
239
- updated: timestamp
240
- };
241
- });
242
- const itemsToNotify = new Set<ItemId>(transitioningItems);
243
- for (const trigger of interaction.triggers) {
244
- if (!('itemId' in trigger)) continue;
245
- itemsToNotify.add(trigger.itemId);
246
- }
247
- this.notifyItemCtrlsChange(Array.from(itemsToNotify));
248
- this.notifyTransitionStartForItems(transitioningItems, activeStateId);
249
- }
250
- }
251
-
252
- notifyTransitionStartForItems(itemsIds: string[], activeStateId: string) {
253
- for (const itemId of itemsIds) {
254
- const ctrl = this.ctrls.get(itemId);
255
- const item = this.items.find((item) => item.id === itemId)!;
256
- const keys = Object.keys(item.state[activeStateId] ?? {});
257
- ctrl?.handleTransitionStart?.(keys);
258
- }
259
- }
260
-
261
- notifyTransitionEnd(itemId: string): void {
262
- const timestamp = Date.now();
263
- this.itemsStages = this.itemsStages.map((stage) => {
264
- if (stage.itemId !== itemId || stage.type !== 'transitioning') return stage;
265
- return {
266
- itemId,
267
- interactionId: stage.interactionId,
268
- type: 'active',
269
- stateId: stage.to,
270
- isStartState: stage.direction === 'out',
271
- updated: timestamp
272
- };
273
- });
274
- this.ctrls.get(itemId)?.receiveChange();
275
- }
276
-
277
- private getCurrentStateByInteractionId(id: InteractionId): string {
278
- let state;
279
- for (const interactionId of Object.keys(this.interactionStateMap)) {
280
- if (id !== interactionId) continue;
281
- state = this.interactionStateMap[interactionId];
282
- }
283
- if (!state) throw new Error(`Failed to find current state for interaction w/ id="${id}"`);
284
- return state;
285
- }
286
-
287
- private setCurrentStateForInteraction(interactionId: InteractionId, stateId: StateId) {
288
- this.interactionStateMap = {
289
- ...this.interactionStateMap,
290
- [interactionId]: stateId
291
- };
292
- }
293
-
294
- private getActiveInteractionState(interactionId: InteractionId): string {
295
- const { interactions } = this;
296
- const interaction = interactions.find((interaction) => interaction.id === interactionId)!;
297
- const activeStateId = interaction.states.find(state => state.id !== interaction.startStateId)?.id;
298
- if (!activeStateId) {
299
- throw new Error(`Failed to find active state for interaction w/ id="${interactionId}"`);
300
- }
301
- return activeStateId;
302
- }
303
-
304
- private notifyItemCtrlsChange(itemsIds: string[]) {
305
- for (const itemId of itemsIds) {
306
- this.ctrls.get(itemId)?.receiveChange();
307
- }
308
- }
309
-
310
- private unpackItems(article: Article): ItemAny[] {
311
- const itemsArr = [];
312
- for (const section of article.sections) {
313
- const items = this.getNestedItems(section.items);
314
- itemsArr.push(...items);
315
- }
316
- return itemsArr;
317
- }
318
-
319
- private getNestedItems(items: ItemAny[]): ItemAny[] {
320
- const allItems: ItemAny[] = [];
321
- for (const item of items) {
322
- if (isItemType(item, ArticleItemType.Group) || isItemType(item, ArticleItemType.Compound)) {
323
- const groupChildren = item?.items ?? [];
324
- const nestedItems = this.getNestedItems(groupChildren);
325
- allItems.push(...nestedItems);
326
- }
327
- allItems.push(item);
328
- }
329
- return allItems;
330
- }
331
-
332
- private getDefaultItemStages(): ItemStages {
333
- const timestamp = Date.now();
334
- const { items } = this;
335
- const stages: ItemStages = [];
336
- for (const item of items) {
337
- const itemStatesMap = item.state;
338
- if (!itemStatesMap) continue;
339
- for (const stateId of Object.keys(itemStatesMap)) {
340
- const interactionId = this.activeStateIdInteractionIdMap[stateId];
341
- if (!interactionId) continue;
342
- stages.push({
343
- itemId: item.id,
344
- interactionId,
345
- type: 'active',
346
- isStartState: true,
347
- updated: timestamp,
348
- stateId: this.interactions.find((interaction) => interaction.id === interactionId)?.startStateId
349
- });
350
- }
351
- }
352
- return stages;
353
- }
354
- }
355
-
356
- type ItemStages = (TransitioningStage | ActiveStage)[];
357
- type TransitioningStage = {
358
- itemId: string;
359
- interactionId: string;
360
- type: 'transitioning';
361
- from: StateId;
362
- to: StateId;
363
- direction: 'in' | 'out';
364
- updated: number;
365
- };
366
- type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
367
- type InteractionStateMap = Record<InteractionId, StateId>;
368
- type StateItemsIdsMap = Record<StateId, ItemId[]>;
369
- type TriggerType = InteractionItemTrigger['type'];
370
- type InteractionId = string;
371
- type StateId = string;
372
- type ItemId = string;
373
- type StateProps = Record<string, {
374
- value?: string | number;
375
- transition?: {
376
- timing: string;
377
- duration: number;
378
- delay: number;
379
- };
380
- }>;
1
+ import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
2
+ import {
3
+ Article,
4
+ ArticleItemType,
5
+ Interaction,
6
+ InteractionItemTrigger,
7
+ ItemAny,
8
+ } from '@cntrl-site/sdk';
9
+ import { isItemType } from '../utils/isItemType';
10
+
11
+ export class InteractionsRegistry implements InteractionsRegistryPort {
12
+ private ctrls: Map<ItemId, ItemInteractionCtrl> = new Map();
13
+ private items: ItemAny[];
14
+ private interactions: Interaction[];
15
+ private stateItemsIdsMap: StateItemsIdsMap;
16
+ private interactionStateMap: InteractionStateMap;
17
+ private itemsStages: ItemStages;
18
+ private activeStateIdInteractionIdMap: Record<StateId, InteractionId>;
19
+
20
+ constructor(article: Article, layoutId: string) {
21
+ this.items = this.unpackItems(article);
22
+ const interactions = article.interactions[layoutId] ?? [];
23
+ const activeStatesIds = interactions.reduce<StateId[]>((map, inter) => {
24
+ const activeStateId = inter.states.find((state) => state.id !== inter.startStateId)?.id;
25
+ if (!activeStateId) {
26
+ throw new Error(`Failed to find active state for interaction w/ id="${inter.id}"`);
27
+ }
28
+ map.push(activeStateId);
29
+ return map;
30
+ }, []);
31
+ const interactionStateMap = interactions.reduce<InteractionStateMap>((map, { id, startStateId }) => {
32
+ map[id] = startStateId;
33
+ return map;
34
+ }, {});
35
+ this.activeStateIdInteractionIdMap = interactions.reduce<Record<StateId, InteractionId>>((map, interaction) => {
36
+ const activeState = interaction.states.find((state) => state.id !== interaction.startStateId);
37
+ if (activeState) {
38
+ map[activeState.id] = interaction.id;
39
+ }
40
+ return map;
41
+ }, {});
42
+ const stateItemsIdsMap = activeStatesIds.reduce<StateItemsIdsMap>((map, stateId) => {
43
+ map[stateId] = this.items
44
+ .filter((item) => {
45
+ const state = item.state[stateId] ?? {};
46
+ const hasKeys = Object.keys(state).length !== 0;
47
+ return hasKeys;
48
+ })
49
+ .map((item) => item.id);
50
+ return map;
51
+ }, {});
52
+ this.interactions = interactions;
53
+ this.stateItemsIdsMap = stateItemsIdsMap;
54
+ this.interactionStateMap = interactionStateMap;
55
+ const itemStages = this.getDefaultItemStages();
56
+ this.itemsStages = itemStages;
57
+ }
58
+
59
+ register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
60
+ this.ctrls.set(itemId, ctrl);
61
+ }
62
+
63
+ getStatePropsForItem(itemId: string) {
64
+ const { items } = this;
65
+ const item = items.find((item) => item.id === itemId)!;
66
+ const itemStages = this.itemsStages.filter((stage) => stage.itemId === itemId);
67
+ itemStages.sort((a, b) => a.updated - b.updated);
68
+ const itemStyles: StateProps = {};
69
+ for (const stage of itemStages) {
70
+ if (stage.type === 'active') {
71
+ if (stage.isStartState) continue;
72
+ const params = item.state[stage.stateId!] ?? {};
73
+ for (const [key, stateDetails] of Object.entries(params)) {
74
+ itemStyles[key] = {
75
+ value: stateDetails.value
76
+ };
77
+ }
78
+ }
79
+ if (stage.type === 'transitioning') {
80
+ const activeStateId = stage.direction === 'in' ? stage.to : stage.from;
81
+ const params = item.state[activeStateId] ?? {};
82
+ for (const [key, stateDetails] of Object.entries(params)) {
83
+ const transitionDetails = stateDetails[stage.direction];
84
+ if (!transitionDetails) continue;
85
+ itemStyles[key] = {
86
+ value: stage.direction === 'in' ? stateDetails.value : itemStyles[key]?.value,
87
+ transition: {
88
+ timing: transitionDetails.timing,
89
+ duration: transitionDetails.duration,
90
+ delay: transitionDetails.delay
91
+ }
92
+ };
93
+ }
94
+ }
95
+ }
96
+ return itemStyles;
97
+ }
98
+
99
+ getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['type']> {
100
+ const available = new Set<InteractionItemTrigger['type']>();
101
+ const activeStates = Object.values(this.interactionStateMap);
102
+ for (const interaction of this.interactions) {
103
+ const { triggers } = interaction;
104
+ for (const trigger of triggers) {
105
+ if (!('itemId' in trigger)) continue;
106
+ if (trigger.itemId !== itemId) continue;
107
+ if (activeStates.includes(trigger.from)) {
108
+ available.add(trigger.type);
109
+ }
110
+ }
111
+ }
112
+ return available;
113
+ }
114
+
115
+ notifyLoad() {
116
+ const timestamp = Date.now();
117
+ for (const interaction of this.interactions) {
118
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
119
+ const matchingTrigger = interaction.triggers.find(trigger =>
120
+ 'position' in trigger && trigger.position === 0 && trigger.from === currentStateId
121
+ );
122
+ if (!matchingTrigger) continue;
123
+ const activeStateId = this.getActiveInteractionState(interaction.id);
124
+ const isNewStateActive = matchingTrigger.to === activeStateId;
125
+ this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
126
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
127
+ const state = interaction.states.find((state) => state.id === matchingTrigger.to);
128
+ const actions = state?.actions ?? [];
129
+ for (const action of actions) {
130
+ const ctrl = this.ctrls.get(action.itemId);
131
+ if (!ctrl) continue;
132
+ ctrl.receiveAction(action.type);
133
+ }
134
+
135
+ this.itemsStages = this.itemsStages.map((stage) => {
136
+ if (stage.interactionId !== interaction.id) return stage;
137
+ return {
138
+ itemId: stage.itemId,
139
+ interactionId: stage.interactionId,
140
+ type: 'transitioning',
141
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
142
+ to: matchingTrigger.to,
143
+ direction: isNewStateActive ? 'in' : 'out',
144
+ updated: timestamp
145
+ };
146
+ });
147
+
148
+ const itemsToNotify = new Set<ItemId>(transitioningItems);
149
+ for (const trigger of interaction.triggers) {
150
+ if (!('itemId' in trigger)) continue;
151
+ itemsToNotify.add(trigger.itemId);
152
+ }
153
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
154
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
155
+ }
156
+ }
157
+
158
+ notifyScroll(position: number) {
159
+ const timestamp = Date.now();
160
+ for (const interaction of this.interactions) {
161
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
162
+ const activeStateId = interaction.states.find((state) => state.id !== interaction.startStateId)?.id;
163
+ const matchingTrigger = interaction.triggers.find((trigger) => {
164
+ if (!('position' in trigger) || trigger.position === 0) return false;
165
+ const triggerPosition = trigger.position * window.innerWidth;
166
+ const isScrolledPastTrigger = triggerPosition < position;
167
+ if (!isScrolledPastTrigger && !trigger.isReverse) return false;
168
+ const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
169
+ return stateId === currentStateId;
170
+ });
171
+ if (!matchingTrigger || !('position' in matchingTrigger) || !activeStateId) continue;
172
+ const triggerPosition = matchingTrigger.position * window.innerWidth;
173
+ const isScrolledPastTrigger = triggerPosition < position;
174
+ const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
175
+ this.setCurrentStateForInteraction(interaction.id, targetStateId);
176
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
177
+ const state = interaction.states.find((state) => state.id === targetStateId);
178
+ const actions = state?.actions ?? [];
179
+ for (const action of actions) {
180
+ const ctrl = this.ctrls.get(action.itemId);
181
+ if (!ctrl) continue;
182
+ ctrl.receiveAction(action.type);
183
+ }
184
+ const itemsStages = this.itemsStages.map((stage) => {
185
+ if (stage.interactionId !== interaction.id) return stage;
186
+ const newStage = {
187
+ itemId: stage.itemId,
188
+ interactionId: stage.interactionId,
189
+ type: 'transitioning' as const,
190
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
191
+ to: targetStateId,
192
+ direction: targetStateId === activeStateId ? 'in' as const : 'out' as const,
193
+ updated: timestamp
194
+ };
195
+ return newStage;
196
+ });
197
+ this.itemsStages = itemsStages;
198
+ const itemsToNotify = new Set<ItemId>(transitioningItems);
199
+ for (const trigger of interaction.triggers) {
200
+ if (!('itemId' in trigger)) continue;
201
+ itemsToNotify.add(trigger.itemId);
202
+ }
203
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
204
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
205
+ }
206
+ }
207
+
208
+ notifyItemTrigger(itemId: string, triggerType: TriggerType): void {
209
+ const timestamp = Date.now();
210
+ for (const interaction of this.interactions) {
211
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
212
+ const matchingTrigger = interaction.triggers.find((trigger) =>
213
+ 'itemId' in trigger
214
+ && trigger.itemId === itemId
215
+ && trigger.from === currentStateId
216
+ && trigger.type === triggerType
217
+ );
218
+ if (!matchingTrigger) continue;
219
+ const activeStateId = this.getActiveInteractionState(interaction.id);
220
+ const isNewStateActive = matchingTrigger.to === activeStateId;
221
+ this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
222
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
223
+ const state = interaction.states.find((state) => state.id === matchingTrigger.to);
224
+ const actions = state?.actions ?? [];
225
+ for (const action of actions) {
226
+ const ctrl = this.ctrls.get(action.itemId);
227
+ if (!ctrl) continue;
228
+ ctrl.receiveAction(action.type);
229
+ }
230
+ this.itemsStages = this.itemsStages.map((stage) => {
231
+ if (stage.interactionId !== interaction.id) return stage;
232
+ return {
233
+ itemId: stage.itemId,
234
+ interactionId: stage.interactionId,
235
+ type: 'transitioning',
236
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
237
+ to: matchingTrigger.to,
238
+ direction: isNewStateActive ? 'in' : 'out',
239
+ updated: timestamp
240
+ };
241
+ });
242
+ const itemsToNotify = new Set<ItemId>(transitioningItems);
243
+ for (const trigger of interaction.triggers) {
244
+ if (!('itemId' in trigger)) continue;
245
+ itemsToNotify.add(trigger.itemId);
246
+ }
247
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
248
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
249
+ }
250
+ }
251
+
252
+ notifyTransitionStartForItems(itemsIds: string[], activeStateId: string) {
253
+ for (const itemId of itemsIds) {
254
+ const ctrl = this.ctrls.get(itemId);
255
+ const item = this.items.find((item) => item.id === itemId)!;
256
+ const keys = Object.keys(item.state[activeStateId] ?? {});
257
+ ctrl?.handleTransitionStart?.(keys);
258
+ }
259
+ }
260
+
261
+ notifyTransitionEnd(itemId: string): void {
262
+ const timestamp = Date.now();
263
+ this.itemsStages = this.itemsStages.map((stage) => {
264
+ if (stage.itemId !== itemId || stage.type !== 'transitioning') return stage;
265
+ return {
266
+ itemId,
267
+ interactionId: stage.interactionId,
268
+ type: 'active',
269
+ stateId: stage.to,
270
+ isStartState: stage.direction === 'out',
271
+ updated: timestamp
272
+ };
273
+ });
274
+ this.ctrls.get(itemId)?.receiveChange();
275
+ }
276
+
277
+ private getCurrentStateByInteractionId(id: InteractionId): string {
278
+ let state;
279
+ for (const interactionId of Object.keys(this.interactionStateMap)) {
280
+ if (id !== interactionId) continue;
281
+ state = this.interactionStateMap[interactionId];
282
+ }
283
+ if (!state) throw new Error(`Failed to find current state for interaction w/ id="${id}"`);
284
+ return state;
285
+ }
286
+
287
+ private setCurrentStateForInteraction(interactionId: InteractionId, stateId: StateId) {
288
+ this.interactionStateMap = {
289
+ ...this.interactionStateMap,
290
+ [interactionId]: stateId
291
+ };
292
+ }
293
+
294
+ private getActiveInteractionState(interactionId: InteractionId): string {
295
+ const { interactions } = this;
296
+ const interaction = interactions.find((interaction) => interaction.id === interactionId)!;
297
+ const activeStateId = interaction.states.find(state => state.id !== interaction.startStateId)?.id;
298
+ if (!activeStateId) {
299
+ throw new Error(`Failed to find active state for interaction w/ id="${interactionId}"`);
300
+ }
301
+ return activeStateId;
302
+ }
303
+
304
+ private notifyItemCtrlsChange(itemsIds: string[]) {
305
+ for (const itemId of itemsIds) {
306
+ this.ctrls.get(itemId)?.receiveChange();
307
+ }
308
+ }
309
+
310
+ private unpackItems(article: Article): ItemAny[] {
311
+ const itemsArr = [];
312
+ for (const section of article.sections) {
313
+ const items = this.getNestedItems(section.items);
314
+ itemsArr.push(...items);
315
+ }
316
+ return itemsArr;
317
+ }
318
+
319
+ private getNestedItems(items: ItemAny[]): ItemAny[] {
320
+ const allItems: ItemAny[] = [];
321
+ for (const item of items) {
322
+ if (isItemType(item, ArticleItemType.Group) || isItemType(item, ArticleItemType.Compound)) {
323
+ const groupChildren = item?.items ?? [];
324
+ const nestedItems = this.getNestedItems(groupChildren);
325
+ allItems.push(...nestedItems);
326
+ }
327
+ allItems.push(item);
328
+ }
329
+ return allItems;
330
+ }
331
+
332
+ private getDefaultItemStages(): ItemStages {
333
+ const timestamp = Date.now();
334
+ const { items } = this;
335
+ const stages: ItemStages = [];
336
+ for (const item of items) {
337
+ const itemStatesMap = item.state;
338
+ if (!itemStatesMap) continue;
339
+ for (const stateId of Object.keys(itemStatesMap)) {
340
+ const interactionId = this.activeStateIdInteractionIdMap[stateId];
341
+ if (!interactionId) continue;
342
+ stages.push({
343
+ itemId: item.id,
344
+ interactionId,
345
+ type: 'active',
346
+ isStartState: true,
347
+ updated: timestamp,
348
+ stateId: this.interactions.find((interaction) => interaction.id === interactionId)?.startStateId
349
+ });
350
+ }
351
+ }
352
+ return stages;
353
+ }
354
+ }
355
+
356
+ type ItemStages = (TransitioningStage | ActiveStage)[];
357
+ type TransitioningStage = {
358
+ itemId: string;
359
+ interactionId: string;
360
+ type: 'transitioning';
361
+ from: StateId;
362
+ to: StateId;
363
+ direction: 'in' | 'out';
364
+ updated: number;
365
+ };
366
+ type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
367
+ type InteractionStateMap = Record<InteractionId, StateId>;
368
+ type StateItemsIdsMap = Record<StateId, ItemId[]>;
369
+ type TriggerType = InteractionItemTrigger['type'];
370
+ type InteractionId = string;
371
+ type StateId = string;
372
+ type ItemId = string;
373
+ type StateProps = Record<string, {
374
+ value?: string | number;
375
+ transition?: {
376
+ timing: string;
377
+ duration: number;
378
+ delay: number;
379
+ };
380
+ }>;