@cntrl-site/sdk-nextjs 1.0.19-alpha.2 → 1.0.19-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Article.js +1 -1
- package/lib/components/Item.js +31 -36
- package/lib/components/items/CodeEmbedItem.js +11 -15
- package/lib/components/items/CustomItem.js +7 -12
- package/lib/components/items/GroupItem.js +11 -15
- package/lib/components/items/ImageItem.js +21 -24
- package/lib/components/items/RectangleItem.js +24 -18
- package/lib/components/items/RichTextItem.js +19 -14
- package/lib/components/items/VideoItem.js +22 -23
- package/lib/components/items/VimeoEmbed.js +16 -22
- package/lib/components/items/YoutubeEmbed.js +16 -23
- package/lib/components/useItemPosition.js +10 -5
- package/lib/interactions/CSSPropertyNameMap.js +38 -0
- package/lib/interactions/InteractionsRegistry.js +158 -0
- package/lib/interactions/ItemInteractionCtrl.js +55 -0
- package/lib/interactions/getTransition.js +21 -0
- package/lib/interactions/types.js +2 -0
- package/lib/interactions/useItemInteractionCtrl.js +17 -0
- package/lib/provider/InteractionsContext.js +17 -39
- package/lib/utils/getStyleFromItemStateAndParams.js +9 -0
- package/package.json +1 -1
- package/src/components/Article.tsx +2 -2
- package/src/components/Item.tsx +40 -34
- package/src/components/items/CodeEmbedItem.tsx +11 -14
- package/src/components/items/CustomItem.tsx +8 -13
- package/src/components/items/GroupItem.tsx +12 -14
- package/src/components/items/ImageItem.tsx +29 -25
- package/src/components/items/RectangleItem.tsx +30 -16
- package/src/components/items/RichTextItem.tsx +26 -20
- package/src/components/items/VideoItem.tsx +31 -25
- package/src/components/items/VimeoEmbed.tsx +21 -21
- package/src/components/items/YoutubeEmbed.tsx +17 -23
- package/src/components/useItemPosition.ts +12 -5
- package/src/interactions/CSSPropertyNameMap.ts +38 -0
- package/src/interactions/InteractionsRegistry.ts +193 -0
- package/src/interactions/ItemInteractionCtrl.ts +56 -0
- package/src/interactions/getTransition.ts +27 -0
- package/src/interactions/types.ts +32 -0
- package/src/interactions/useItemInteractionCtrl.ts +18 -0
- package/src/provider/InteractionsContext.old.tsx +65 -0
- package/src/provider/InteractionsContext.test.tsx +7 -7
- package/src/provider/InteractionsContext.tsx +17 -54
- package/src/utils/getStyleFromItemStateAndParams.ts +8 -0
- package/lib/components/useStatesClassNames.js +0 -18
- package/lib/components/useStatesTransitions.js +0 -89
- package/lib/utils/StateStyles/StateStyles.js +0 -89
- package/lib/utils/getStatesCSS.js +0 -16
- package/src/components/useStatesClassNames.ts +0 -23
- package/src/components/useStatesTransitions.ts +0 -95
- package/src/utils/StateStyles/StateStyles.ts +0 -99
- package/src/utils/getStatesCSS.ts +0 -24
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
|
|
2
|
+
import {
|
|
3
|
+
Article,
|
|
4
|
+
ArticleItemType,
|
|
5
|
+
Interaction,
|
|
6
|
+
InteractionTrigger,
|
|
7
|
+
ItemAny,
|
|
8
|
+
ItemState,
|
|
9
|
+
StateParams
|
|
10
|
+
} from '@cntrl-site/sdk';
|
|
11
|
+
|
|
12
|
+
export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
13
|
+
private ctrls: Map<ItemId, ItemInteractionCtrl> = new Map();
|
|
14
|
+
private items: ItemAny[];
|
|
15
|
+
private interactions: Interaction[];
|
|
16
|
+
private stateItemsIdsMap: StateItemsIdsMap;
|
|
17
|
+
private interactionStateMap: InteractionStateMap;
|
|
18
|
+
private itemStageMap: ItemStageMap;
|
|
19
|
+
|
|
20
|
+
constructor(article: Article, private layoutId: string) {
|
|
21
|
+
const { interactions } = article;
|
|
22
|
+
const items = article.sections.flatMap((section) => section.items);
|
|
23
|
+
const activeStatesIds = interactions.reduce<StateId[]>((map, inter) => {
|
|
24
|
+
const activeStateId = inter.states.find((state) => state.id !== inter.startStateId)?.id!;
|
|
25
|
+
map.push(activeStateId);
|
|
26
|
+
return map;
|
|
27
|
+
}, []);
|
|
28
|
+
const interactionStateMap = interactions.reduce<InteractionStateMap>((map, { id, startStateId }) => {
|
|
29
|
+
map[id] = startStateId;
|
|
30
|
+
return map;
|
|
31
|
+
}, {});
|
|
32
|
+
const itemStageMap = items.reduce<ItemStageMap>((map, item) => {
|
|
33
|
+
map[item.id] = { type: 'active', isStartState: true };
|
|
34
|
+
return map;
|
|
35
|
+
}, {});
|
|
36
|
+
const stateItemsIdsMap = activeStatesIds.reduce<StateItemsIdsMap>((map, stateId) => {
|
|
37
|
+
map[stateId] = items
|
|
38
|
+
.filter((item) => {
|
|
39
|
+
const layoutStates = item.state[layoutId] ?? {};
|
|
40
|
+
const state = layoutStates?.[stateId] ?? {};
|
|
41
|
+
const hasKeys = Object.keys(state).length !== 0;
|
|
42
|
+
return hasKeys;
|
|
43
|
+
})
|
|
44
|
+
.map((item) => item.id);
|
|
45
|
+
return map;
|
|
46
|
+
}, {});
|
|
47
|
+
this.items = items;
|
|
48
|
+
this.interactions = interactions;
|
|
49
|
+
this.itemStageMap = itemStageMap;
|
|
50
|
+
this.stateItemsIdsMap = stateItemsIdsMap;
|
|
51
|
+
this.interactionStateMap = interactionStateMap;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
|
|
55
|
+
this.ctrls.set(itemId, ctrl);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getStatePropsForItem(itemId: string) {
|
|
59
|
+
const { items, layoutId } = this;
|
|
60
|
+
const item = items.find((item) => item.id === itemId)!;
|
|
61
|
+
const stage = this.itemStageMap[itemId];
|
|
62
|
+
if (!stage) return {};
|
|
63
|
+
// handle "active" stage
|
|
64
|
+
if (stage.type === 'active') {
|
|
65
|
+
if (stage.isStartState) return {};
|
|
66
|
+
const params = item.state[layoutId]?.[stage.stateId!];
|
|
67
|
+
const stateProps = Object.entries(params).reduce<StateProps>((map, [key, stateDetails]) => {
|
|
68
|
+
const style = key as keyof ItemState<ArticleItemType>;
|
|
69
|
+
map[style] = {
|
|
70
|
+
value: stateDetails.value
|
|
71
|
+
};
|
|
72
|
+
return map;
|
|
73
|
+
}, {});
|
|
74
|
+
return stateProps;
|
|
75
|
+
}
|
|
76
|
+
// handle "transitioning" stage
|
|
77
|
+
const activeStateId = stage.direction === 'in' ? stage.to : stage.from;
|
|
78
|
+
const params = item.state[layoutId]?.[activeStateId];
|
|
79
|
+
return Object.entries(params).reduce<StateProps>((map, [key, stateDetails]) => {
|
|
80
|
+
const style = key as keyof ItemState<ArticleItemType>;
|
|
81
|
+
const details = stateDetails as StateParams<string | number>;
|
|
82
|
+
map[style] = {
|
|
83
|
+
value: stage.direction === 'in' ? details.value : undefined,
|
|
84
|
+
transition: {
|
|
85
|
+
timing: details[stage.direction].timing,
|
|
86
|
+
duration: details[stage.direction].duration,
|
|
87
|
+
delay: details[stage.direction].delay
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return map;
|
|
91
|
+
}, {});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
notifyTrigger(itemId: string, triggerType: TriggerType): void {
|
|
95
|
+
for (const interaction of this.interactions) {
|
|
96
|
+
const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
|
|
97
|
+
const matchingTrigger = interaction.triggers.find((trigger) =>
|
|
98
|
+
trigger.itemId === itemId
|
|
99
|
+
&& trigger.from === currentStateId
|
|
100
|
+
&& trigger.type === triggerType
|
|
101
|
+
);
|
|
102
|
+
if (!matchingTrigger) continue;
|
|
103
|
+
const activeStateId = this.getActiveInteractionState(interaction.id);
|
|
104
|
+
const isNewStateActive = matchingTrigger.to === activeStateId;
|
|
105
|
+
this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
|
|
106
|
+
const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
|
|
107
|
+
for (const [itemId, stage] of Object.entries(this.itemStageMap)) {
|
|
108
|
+
if (!transitioningItems.includes(itemId)) continue;
|
|
109
|
+
this.itemStageMap[itemId] = {
|
|
110
|
+
type: 'transitioning',
|
|
111
|
+
from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
|
|
112
|
+
to: matchingTrigger.to,
|
|
113
|
+
direction: isNewStateActive ? 'in' : 'out'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
this.notifyItemCtrlsChange(transitioningItems);
|
|
117
|
+
this.notifyTransitionStartForItems(transitioningItems, activeStateId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
notifyTransitionStartForItems(itemsIds: string[], activeStateId: string) {
|
|
122
|
+
for (const itemId of itemsIds) {
|
|
123
|
+
const ctrl = this.ctrls.get(itemId);
|
|
124
|
+
const item = this.items.find((item) => item.id === itemId)!;
|
|
125
|
+
const keys = Object.keys(item.state[this.layoutId]?.[activeStateId] ?? {});
|
|
126
|
+
ctrl?.handleTransitionStart?.(keys);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
notifyTransitionEnd(itemId: string): void {
|
|
131
|
+
const prevState = this.itemStageMap[itemId];
|
|
132
|
+
if (prevState.type !== 'transitioning') return; // throw?
|
|
133
|
+
this.itemStageMap[itemId] = {
|
|
134
|
+
type: 'active',
|
|
135
|
+
stateId: prevState.to,
|
|
136
|
+
isStartState: prevState.direction === 'out'
|
|
137
|
+
};
|
|
138
|
+
this.ctrls.get(itemId)?.receiveChange();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
private getCurrentStateByInteractionId(id: InteractionId): string {
|
|
143
|
+
let state;
|
|
144
|
+
for (const interactionId of Object.keys(this.interactionStateMap)) {
|
|
145
|
+
if (id !== interactionId) continue;
|
|
146
|
+
state = this.interactionStateMap[interactionId];
|
|
147
|
+
}
|
|
148
|
+
if (!state) throw new Error(`Failed to find current state for interaction w/ id="${id}"`);
|
|
149
|
+
return state;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private setCurrentStateForInteraction(interactionId: InteractionId, stateId: StateId) {
|
|
153
|
+
this.interactionStateMap = {
|
|
154
|
+
...this.interactionStateMap,
|
|
155
|
+
[interactionId]: stateId
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private getActiveInteractionState(interactionId: InteractionId): string {
|
|
160
|
+
const { interactions } = this;
|
|
161
|
+
const interaction = interactions.find((interaction) => interaction.id === interactionId)!;
|
|
162
|
+
return interaction.states.find(state => state.id !== interaction.startStateId)?.id!;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private notifyItemCtrlsChange(itemsIds: string[]) {
|
|
166
|
+
for (const itemId of itemsIds) {
|
|
167
|
+
this.ctrls.get(itemId)?.receiveChange();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type ItemStageMap = Record<ItemId, TransitioningStage | ActiveStage>;
|
|
173
|
+
type TransitioningStage = {
|
|
174
|
+
type: 'transitioning';
|
|
175
|
+
from: StateId;
|
|
176
|
+
to: StateId;
|
|
177
|
+
direction: 'in' | 'out';
|
|
178
|
+
};
|
|
179
|
+
type ActiveStage = { type: 'active'; stateId?: string; isStartState: boolean; };
|
|
180
|
+
type InteractionStateMap = Record<InteractionId, StateId>;
|
|
181
|
+
type StateItemsIdsMap = Record<StateId, ItemId[]>;
|
|
182
|
+
type TriggerType = InteractionTrigger['type'];
|
|
183
|
+
type InteractionId = string;
|
|
184
|
+
type StateId = string;
|
|
185
|
+
type ItemId = string;
|
|
186
|
+
type StateProps = Record<string, {
|
|
187
|
+
value?: string | number;
|
|
188
|
+
transition?: {
|
|
189
|
+
timing: string;
|
|
190
|
+
duration: number;
|
|
191
|
+
delay: number;
|
|
192
|
+
};
|
|
193
|
+
}>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
|
|
2
|
+
import { getTransition } from './getTransition';
|
|
3
|
+
import { getStyleKeysFromCSSProperty } from './CSSPropertyNameMap';
|
|
4
|
+
|
|
5
|
+
export class ItemInteractionController implements ItemInteractionCtrl {
|
|
6
|
+
private transitionsInProgress: Set<string> = new Set();
|
|
7
|
+
constructor(
|
|
8
|
+
private itemId: string,
|
|
9
|
+
private registry: InteractionsRegistryPort,
|
|
10
|
+
private onChange: () => void
|
|
11
|
+
) {
|
|
12
|
+
this.registry.register(itemId, this);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getState(keys: string[]) {
|
|
16
|
+
const stateProps = this.registry.getStatePropsForItem(this.itemId);
|
|
17
|
+
const styles = keys.reduce<Record<string, string | number>>((map, styleKey) => {
|
|
18
|
+
const prop = stateProps[styleKey];
|
|
19
|
+
if (prop?.value === undefined) return map;
|
|
20
|
+
map[styleKey] = prop.value;
|
|
21
|
+
return map;
|
|
22
|
+
}, {});
|
|
23
|
+
const transition = getTransition(stateProps, keys);
|
|
24
|
+
return {
|
|
25
|
+
styles,
|
|
26
|
+
transition
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
sendTrigger(type: 'click' | 'hover-in' | 'hover-out') {
|
|
31
|
+
this.registry.notifyTrigger(this.itemId, type);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
handleTransitionStart = (types: string[]) => {
|
|
35
|
+
this.transitionsInProgress.clear();
|
|
36
|
+
for (const type of types) {
|
|
37
|
+
this.transitionsInProgress.add(type);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
handleTransitionEnd = (cssPropKey: string) => {
|
|
42
|
+
const styleKeys = getStyleKeysFromCSSProperty(cssPropKey);
|
|
43
|
+
for (const key of styleKeys) {
|
|
44
|
+
const found = this.transitionsInProgress.has(key);
|
|
45
|
+
if (!found) continue;
|
|
46
|
+
this.transitionsInProgress.delete(key);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (this.transitionsInProgress.size !== 0) return;
|
|
50
|
+
this.registry.notifyTransitionEnd(this.itemId);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
receiveChange() {
|
|
54
|
+
this.onChange();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CSSPropertyNameMap } from './CSSPropertyNameMap';
|
|
2
|
+
|
|
3
|
+
export function getTransition(
|
|
4
|
+
state: Record<string, StateInfo>,
|
|
5
|
+
keys: string[]
|
|
6
|
+
): string {
|
|
7
|
+
if (Object.keys(state).length === 0) return 'none';
|
|
8
|
+
const transitions = [];
|
|
9
|
+
for (const [key, params] of Object.entries(state)) {
|
|
10
|
+
if (!keys.includes(key) || !params.transition) continue;
|
|
11
|
+
const { transition: { duration, delay, timing } } = params;
|
|
12
|
+
const cssKey = CSSPropertyNameMap[key]!;
|
|
13
|
+
const nonZeroDuration = Math.max(duration, 0.01);
|
|
14
|
+
transitions.push(`${cssKey} ${nonZeroDuration}ms ${timing} ${delay}ms`);
|
|
15
|
+
}
|
|
16
|
+
return transitions.length > 0
|
|
17
|
+
? transitions.join(', ')
|
|
18
|
+
: 'none';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type StateInfo = {
|
|
22
|
+
transition?: {
|
|
23
|
+
duration: number;
|
|
24
|
+
delay: number;
|
|
25
|
+
timing: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ArticleItemType, ItemState } from '@cntrl-site/sdk';
|
|
2
|
+
|
|
3
|
+
export interface ItemInteractionCtrl {
|
|
4
|
+
getState(keys: string[]): StateCSSInfo;
|
|
5
|
+
sendTrigger(type: 'click' | 'hover-in' | 'hover-out'): void;
|
|
6
|
+
handleTransitionEnd?: (styleKey: string) => void;
|
|
7
|
+
handleTransitionStart?: (styleKeys: string[]) => void;
|
|
8
|
+
receiveChange: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface InteractionsRegistryPort {
|
|
12
|
+
register(itemId: string, ctrl: ItemInteractionCtrl): void;
|
|
13
|
+
getStatePropsForItem(itemId: string): StateProps;
|
|
14
|
+
notifyTrigger(itemId: string, type: TriggerType): void;
|
|
15
|
+
notifyTransitionEnd(itemId: string): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type StateCSSInfo = {
|
|
19
|
+
styles: Partial<Record<string, string | number>>;
|
|
20
|
+
transition?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type StateProps = Record<keyof ItemState<ArticleItemType>, {
|
|
24
|
+
value?: string | number;
|
|
25
|
+
transition?: {
|
|
26
|
+
timing: string;
|
|
27
|
+
duration: number;
|
|
28
|
+
delay: number;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
|
|
32
|
+
type TriggerType = 'click' | 'hover-in' | 'hover-out';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { ItemInteractionController } from './ItemInteractionCtrl';
|
|
3
|
+
import { ItemInteractionCtrl } from './types';
|
|
4
|
+
import { useInteractionsRegistry } from '../provider/InteractionsContext';
|
|
5
|
+
|
|
6
|
+
export function useItemInteractionCtrl(itemId: string): ItemInteractionCtrl | undefined {
|
|
7
|
+
const [_, triggerRender] = useState(0);
|
|
8
|
+
const registry = useInteractionsRegistry();
|
|
9
|
+
const ctrl = useMemo(() => {
|
|
10
|
+
if (!registry) return;
|
|
11
|
+
return new ItemInteractionController(
|
|
12
|
+
itemId,
|
|
13
|
+
registry,
|
|
14
|
+
() => triggerRender(prev => prev + 1)
|
|
15
|
+
);
|
|
16
|
+
}, [itemId, registry]);
|
|
17
|
+
return ctrl;
|
|
18
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createContext, FC, PropsWithChildren, useState } from 'react';
|
|
2
|
+
import { Interaction, InteractionTrigger } from '@cntrl-site/sdk';
|
|
3
|
+
|
|
4
|
+
const defaultState = {
|
|
5
|
+
interactionsStatesMap: {},
|
|
6
|
+
interactions: [],
|
|
7
|
+
transitionTo: () => {},
|
|
8
|
+
getItemTrigger: () => null
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const InteractionsContextOld = createContext<{
|
|
12
|
+
interactionsStatesMap: StatesMap,
|
|
13
|
+
interactions: Interaction[],
|
|
14
|
+
transitionTo: (interactionId: string, stateId: string) => void,
|
|
15
|
+
getItemTrigger: (itemId: string, triggerType: TriggerType) => Trigger | null
|
|
16
|
+
}>(defaultState);
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
interactions: Interaction[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ interactions, children }) => {
|
|
23
|
+
const defaultStatesMap = interactions.reduce<Record<string, string>>((map, { id, startStateId }) => {
|
|
24
|
+
map[id] = startStateId;
|
|
25
|
+
return map;
|
|
26
|
+
}, {});
|
|
27
|
+
const [interactionsStatesMap, setInteractionsStatesMap] = useState(defaultStatesMap);
|
|
28
|
+
const transitionTo = (interactionId: string, stateId: string) => {
|
|
29
|
+
setInteractionsStatesMap((map) => ({ ...map, [interactionId]: stateId }));
|
|
30
|
+
};
|
|
31
|
+
const getItemTrigger = (itemId: string, triggerType: TriggerType): Trigger | null => {
|
|
32
|
+
for (const interaction of interactions) {
|
|
33
|
+
const activeStateId = interactionsStatesMap[interaction.id];
|
|
34
|
+
const matchingTrigger = interaction.triggers.find((trigger) =>
|
|
35
|
+
trigger.itemId === itemId &&
|
|
36
|
+
trigger.from === activeStateId &&
|
|
37
|
+
trigger.type === triggerType
|
|
38
|
+
);
|
|
39
|
+
if (matchingTrigger) {
|
|
40
|
+
return {
|
|
41
|
+
id: interaction.id,
|
|
42
|
+
from: matchingTrigger.from,
|
|
43
|
+
to: matchingTrigger.to,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
return (
|
|
50
|
+
<InteractionsContextOld.Provider value={{
|
|
51
|
+
transitionTo,
|
|
52
|
+
interactionsStatesMap,
|
|
53
|
+
interactions,
|
|
54
|
+
getItemTrigger
|
|
55
|
+
}}>
|
|
56
|
+
{children}
|
|
57
|
+
</InteractionsContextOld.Provider>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type StatesMap = Record<InteractionId, StateId>;
|
|
62
|
+
type Trigger = { id: InteractionId, from: StateId, to: StateId };
|
|
63
|
+
type TriggerType = InteractionTrigger['type'];
|
|
64
|
+
type InteractionId = string;
|
|
65
|
+
type StateId = string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, waitFor } from '@testing-library/react';
|
|
3
|
-
import { InteractionsProvider,
|
|
3
|
+
import { InteractionsProvider, InteractionsContextOld } from './InteractionsContext.old';
|
|
4
4
|
import { Interaction } from '@cntrl-site/sdk';
|
|
5
5
|
|
|
6
6
|
describe('InteractionsProvider', () => {
|
|
@@ -28,12 +28,12 @@ describe('InteractionsProvider', () => {
|
|
|
28
28
|
|
|
29
29
|
render(
|
|
30
30
|
<InteractionsProvider interactions={interactions}>
|
|
31
|
-
<
|
|
31
|
+
<InteractionsContextOld.Consumer>
|
|
32
32
|
{value => {
|
|
33
33
|
contextValue = value;
|
|
34
34
|
return null;
|
|
35
35
|
}}
|
|
36
|
-
</
|
|
36
|
+
</InteractionsContextOld.Consumer>
|
|
37
37
|
</InteractionsProvider>
|
|
38
38
|
);
|
|
39
39
|
|
|
@@ -49,12 +49,12 @@ describe('InteractionsProvider', () => {
|
|
|
49
49
|
|
|
50
50
|
render(
|
|
51
51
|
<InteractionsProvider interactions={interactions}>
|
|
52
|
-
<
|
|
52
|
+
<InteractionsContextOld.Consumer>
|
|
53
53
|
{value => {
|
|
54
54
|
contextValue = value;
|
|
55
55
|
return null;
|
|
56
56
|
}}
|
|
57
|
-
</
|
|
57
|
+
</InteractionsContextOld.Consumer>
|
|
58
58
|
</InteractionsProvider>
|
|
59
59
|
);
|
|
60
60
|
|
|
@@ -71,12 +71,12 @@ describe('InteractionsProvider', () => {
|
|
|
71
71
|
|
|
72
72
|
render(
|
|
73
73
|
<InteractionsProvider interactions={interactions}>
|
|
74
|
-
<
|
|
74
|
+
<InteractionsContextOld.Consumer>
|
|
75
75
|
{value => {
|
|
76
76
|
contextValue = value;
|
|
77
77
|
return null;
|
|
78
78
|
}}
|
|
79
|
-
</
|
|
79
|
+
</InteractionsContextOld.Consumer>
|
|
80
80
|
</InteractionsProvider>
|
|
81
81
|
);
|
|
82
82
|
|
|
@@ -1,65 +1,28 @@
|
|
|
1
|
-
import { createContext, FC, PropsWithChildren,
|
|
2
|
-
import {
|
|
1
|
+
import { createContext, FC, PropsWithChildren, useContext, useMemo } from 'react';
|
|
2
|
+
import { InteractionsRegistry } from '../interactions/InteractionsRegistry';
|
|
3
|
+
import { Article } from '@cntrl-site/sdk';
|
|
4
|
+
import { useCurrentLayout } from '../common/useCurrentLayout';
|
|
3
5
|
|
|
4
|
-
const
|
|
5
|
-
interactionsStatesMap: {},
|
|
6
|
-
interactions: [],
|
|
7
|
-
transitionTo: () => {},
|
|
8
|
-
getItemTrigger: () => null
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const InteractionsContext = createContext<{
|
|
12
|
-
interactionsStatesMap: StatesMap,
|
|
13
|
-
interactions: Interaction[],
|
|
14
|
-
transitionTo: (interactionId: string, stateId: string) => void,
|
|
15
|
-
getItemTrigger: (itemId: string, triggerType: TriggerType) => Trigger | null
|
|
16
|
-
}>(defaultState);
|
|
6
|
+
export const InteractionsContext = createContext<InteractionsRegistry | undefined>(undefined);
|
|
17
7
|
|
|
18
8
|
interface Props {
|
|
19
|
-
|
|
9
|
+
article: Article;
|
|
20
10
|
}
|
|
21
11
|
|
|
22
|
-
export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const transitionTo = (interactionId: string, stateId: string) => {
|
|
29
|
-
setInteractionsStatesMap((map) => ({ ...map, [interactionId]: stateId }));
|
|
30
|
-
};
|
|
31
|
-
const getItemTrigger = (itemId: string, triggerType: TriggerType): Trigger | null => {
|
|
32
|
-
for (const interaction of interactions) {
|
|
33
|
-
const activeStateId = interactionsStatesMap[interaction.id];
|
|
34
|
-
const matchingTrigger = interaction.triggers.find((trigger) =>
|
|
35
|
-
trigger.itemId === itemId &&
|
|
36
|
-
trigger.from === activeStateId &&
|
|
37
|
-
trigger.type === triggerType
|
|
38
|
-
);
|
|
39
|
-
if (matchingTrigger) {
|
|
40
|
-
return {
|
|
41
|
-
id: interaction.id,
|
|
42
|
-
from: matchingTrigger.from,
|
|
43
|
-
to: matchingTrigger.to,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
};
|
|
12
|
+
export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ article, children }) => {
|
|
13
|
+
const { layoutId } = useCurrentLayout();
|
|
14
|
+
const registry = useMemo(() => {
|
|
15
|
+
if (!layoutId) return;
|
|
16
|
+
return new InteractionsRegistry(article, layoutId);
|
|
17
|
+
}, [layoutId]);
|
|
49
18
|
return (
|
|
50
|
-
<InteractionsContext.Provider value={
|
|
51
|
-
transitionTo,
|
|
52
|
-
interactionsStatesMap,
|
|
53
|
-
interactions,
|
|
54
|
-
getItemTrigger
|
|
55
|
-
}}>
|
|
19
|
+
<InteractionsContext.Provider value={registry}>
|
|
56
20
|
{children}
|
|
57
21
|
</InteractionsContext.Provider>
|
|
58
22
|
);
|
|
59
23
|
};
|
|
60
24
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
type StateId = string;
|
|
25
|
+
export function useInteractionsRegistry(): InteractionsRegistry | undefined {
|
|
26
|
+
const registry = useContext(InteractionsContext);
|
|
27
|
+
return registry;
|
|
28
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useStatesClassNames = void 0;
|
|
4
|
-
const react_1 = require("react");
|
|
5
|
-
const InteractionsContext_1 = require("../provider/InteractionsContext");
|
|
6
|
-
const useCurrentLayout_1 = require("../common/useCurrentLayout");
|
|
7
|
-
function useStatesClassNames(itemId, itemStates, uniquePrefix) {
|
|
8
|
-
const { interactionsStatesMap } = (0, react_1.useContext)(InteractionsContext_1.InteractionsContext);
|
|
9
|
-
const { layoutId } = (0, useCurrentLayout_1.useCurrentLayout)();
|
|
10
|
-
const activeStates = Object.values(interactionsStatesMap);
|
|
11
|
-
const statesForLayout = layoutId ? itemStates[layoutId] : {};
|
|
12
|
-
const stateClassNames = Object.keys(statesForLayout !== null && statesForLayout !== void 0 ? statesForLayout : {})
|
|
13
|
-
.filter((stateId) => activeStates.includes(stateId))
|
|
14
|
-
.map((stateId) => `${uniquePrefix}-${itemId}-state-${stateId}`)
|
|
15
|
-
.join(' ');
|
|
16
|
-
return stateClassNames;
|
|
17
|
-
}
|
|
18
|
-
exports.useStatesClassNames = useStatesClassNames;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useStatesTransitions = void 0;
|
|
4
|
-
const react_1 = require("react");
|
|
5
|
-
const InteractionsContext_1 = require("../provider/InteractionsContext");
|
|
6
|
-
const useCurrentLayout_1 = require("../common/useCurrentLayout");
|
|
7
|
-
function useStatesTransitions(el, state, values) {
|
|
8
|
-
const { interactionsStatesMap } = (0, react_1.useContext)(InteractionsContext_1.InteractionsContext);
|
|
9
|
-
const { layoutId } = (0, useCurrentLayout_1.useCurrentLayout)();
|
|
10
|
-
const activeStates = Object.values(interactionsStatesMap);
|
|
11
|
-
const statesForLayout = (0, react_1.useMemo)(() => layoutId ? state[layoutId] : {}, [state, layoutId]);
|
|
12
|
-
const itemStatesIds = statesForLayout ? Object.keys(statesForLayout) : [];
|
|
13
|
-
const itemActiveStateId = activeStates.find((stateId) => itemStatesIds.includes(stateId));
|
|
14
|
-
(0, react_1.useEffect)(() => {
|
|
15
|
-
if (!itemActiveStateId || !el)
|
|
16
|
-
return;
|
|
17
|
-
const state = statesForLayout[itemActiveStateId];
|
|
18
|
-
const transitionStr = getTransition(state, 'in', values);
|
|
19
|
-
const slowestProp = getSlowestProperty(state, 'in', values);
|
|
20
|
-
if (!transitionStr)
|
|
21
|
-
return;
|
|
22
|
-
el.style.transition = transitionStr;
|
|
23
|
-
el.ontransitionend = (e) => {
|
|
24
|
-
e.stopPropagation();
|
|
25
|
-
if (e.target !== el || e.propertyName !== slowestProp)
|
|
26
|
-
return;
|
|
27
|
-
el.style.transition = 'none';
|
|
28
|
-
};
|
|
29
|
-
return () => {
|
|
30
|
-
const transitionStr = getTransition(state, 'out', values);
|
|
31
|
-
const slowestProp = getSlowestProperty(state, 'out', values);
|
|
32
|
-
if (!transitionStr)
|
|
33
|
-
return;
|
|
34
|
-
el.style.transition = transitionStr;
|
|
35
|
-
el.ontransitionend = (e) => {
|
|
36
|
-
e.stopPropagation();
|
|
37
|
-
if (e.target !== el || e.propertyName !== slowestProp)
|
|
38
|
-
return;
|
|
39
|
-
el.style.transition = 'none';
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
}, [itemActiveStateId, statesForLayout, el]);
|
|
43
|
-
}
|
|
44
|
-
exports.useStatesTransitions = useStatesTransitions;
|
|
45
|
-
const CSSPropertyNameMap = {
|
|
46
|
-
'width': 'width',
|
|
47
|
-
'height': 'height',
|
|
48
|
-
'top': 'top',
|
|
49
|
-
'left': 'left',
|
|
50
|
-
'scale': 'transform',
|
|
51
|
-
'angle': 'transform',
|
|
52
|
-
'opacity': 'opacity',
|
|
53
|
-
'radius': 'border-radius',
|
|
54
|
-
'strokeWidth': 'border-width',
|
|
55
|
-
'strokeColor': 'border-color',
|
|
56
|
-
'fillColor': 'background-color',
|
|
57
|
-
'blur': 'filter',
|
|
58
|
-
'backdropBlur': 'backdrop-filter',
|
|
59
|
-
'letterSpacing': 'letter-spacing',
|
|
60
|
-
'wordSpacing': 'word-spacing',
|
|
61
|
-
'color': 'color'
|
|
62
|
-
};
|
|
63
|
-
function getTransition(state, direction, values) {
|
|
64
|
-
return Object.entries(state)
|
|
65
|
-
.filter(([key]) => values.includes(key))
|
|
66
|
-
.map(([key, params]) => {
|
|
67
|
-
const cssKey = CSSPropertyNameMap[key];
|
|
68
|
-
if (!cssKey) {
|
|
69
|
-
throw new Error(`Cannot translate "${key}" to a CSS property.`);
|
|
70
|
-
}
|
|
71
|
-
return `${cssKey} ${params[direction].duration}ms ${params[direction].timing} ${params[direction].delay}ms`;
|
|
72
|
-
}, [])
|
|
73
|
-
.join(', ');
|
|
74
|
-
}
|
|
75
|
-
function getSlowestProperty(state, direction, values) {
|
|
76
|
-
const mappedEntries = Object.entries(state)
|
|
77
|
-
.filter(([key]) => values.includes(key))
|
|
78
|
-
.map(([key, params]) => {
|
|
79
|
-
const transitionParams = params[direction];
|
|
80
|
-
return {
|
|
81
|
-
key,
|
|
82
|
-
time: transitionParams.duration + transitionParams.delay
|
|
83
|
-
};
|
|
84
|
-
});
|
|
85
|
-
if (mappedEntries.length === 0)
|
|
86
|
-
return '';
|
|
87
|
-
const { key } = mappedEntries.reduce((slowest, current) => current.time > slowest.time ? current : slowest);
|
|
88
|
-
return CSSPropertyNameMap[key];
|
|
89
|
-
}
|