@cntrl-site/sdk-nextjs 1.8.18-alpha.2 → 1.8.19-alpha.0
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/cntrl-site-sdk-nextjs-1.8.18.tgz +0 -0
- package/lib/components/Section/Section.js +7 -51
- package/lib/components/Section/SectionImage.js +28 -0
- package/lib/components/Section/SectionVideo.js +80 -0
- package/lib/interactions/InteractionsRegistry.js +5 -111
- package/lib/interactions/ItemInteractionCtrl.js +1 -1
- package/lib/provider/InteractionsContext.js +0 -36
- package/package.json +2 -2
- package/src/components/ScrollPlaybackVideo.tsx +1 -1
- package/src/components/Section/Section.tsx +10 -63
- package/src/components/Section/SectionImage.tsx +40 -0
- package/src/components/Section/SectionVideo.tsx +116 -0
- package/src/interactions/InteractionsRegistry.ts +9 -106
- package/src/interactions/ItemInteractionCtrl.ts +3 -4
- package/src/interactions/types.ts +4 -4
- package/src/provider/InteractionsContext.tsx +1 -40
|
Binary file
|
|
@@ -12,22 +12,19 @@ const useCntrlContext_1 = require("../../provider/useCntrlContext");
|
|
|
12
12
|
const useSectionRegistry_1 = require("../../utils/ArticleRectManager/useSectionRegistry");
|
|
13
13
|
const color_1 = require("@cntrl-site/color");
|
|
14
14
|
const useLayoutContext_1 = require("../useLayoutContext");
|
|
15
|
+
const SectionVideo_1 = require("./SectionVideo");
|
|
16
|
+
const SectionImage_1 = require("./SectionImage");
|
|
15
17
|
const DEFAULT_COLOR = 'rgba(0, 0, 0, 0)';
|
|
16
|
-
function isVideoUrl(url) {
|
|
17
|
-
const videoExtensions = ['.mp4', '.webm', '.ogg'];
|
|
18
|
-
return videoExtensions.some(ext => url.toLowerCase().endsWith(ext));
|
|
19
|
-
}
|
|
20
18
|
const Section = ({ section, data, children }) => {
|
|
21
|
-
var _a
|
|
19
|
+
var _a;
|
|
22
20
|
const id = (0, react_1.useId)();
|
|
23
21
|
const sectionRef = (0, react_1.useRef)(null);
|
|
24
22
|
const { layouts, customSections } = (0, useCntrlContext_1.useCntrlContext)();
|
|
25
23
|
const layout = (0, useLayoutContext_1.useLayoutContext)();
|
|
26
24
|
const layoutValues = [section.height, section.color, (_a = section.media) !== null && _a !== void 0 ? _a : {}];
|
|
27
25
|
const SectionComponent = section.name ? customSections.getComponent(section.name) : undefined;
|
|
28
|
-
const isVideo = layout && ((_c = (_b = section.media) === null || _b === void 0 ? void 0 : _b[layout]) === null || _c === void 0 ? void 0 : _c.url) ? isVideoUrl(String((_e = (_d = section.media) === null || _d === void 0 ? void 0 : _d[layout]) === null || _e === void 0 ? void 0 : _e.url)) : false;
|
|
29
|
-
const videoRef = (0, react_1.useRef)(null);
|
|
30
26
|
(0, useSectionRegistry_1.useSectionRegistry)(section.id, sectionRef.current);
|
|
27
|
+
const layoutMedia = layout && section.media && section.media[layout] ? section.media[layout] : undefined;
|
|
31
28
|
const getSectionVisibilityStyles = () => {
|
|
32
29
|
return layouts
|
|
33
30
|
.sort((a, b) => a.startsWith - b.startsWith)
|
|
@@ -42,34 +39,10 @@ const Section = ({ section, data, children }) => {
|
|
|
42
39
|
}`;
|
|
43
40
|
}, '');
|
|
44
41
|
};
|
|
45
|
-
(0, react_1.useEffect)(() => {
|
|
46
|
-
if (!isVideo || !sectionRef.current || !videoRef.current)
|
|
47
|
-
return;
|
|
48
|
-
const video = videoRef.current;
|
|
49
|
-
const section = sectionRef.current;
|
|
50
|
-
const observer = new IntersectionObserver(([entry]) => {
|
|
51
|
-
if (entry.isIntersecting) {
|
|
52
|
-
video.style.display = 'block';
|
|
53
|
-
video.play().catch(() => { });
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
video.style.display = 'none';
|
|
57
|
-
video.pause();
|
|
58
|
-
}
|
|
59
|
-
}, {
|
|
60
|
-
root: null,
|
|
61
|
-
rootMargin: '50px',
|
|
62
|
-
threshold: 0
|
|
63
|
-
});
|
|
64
|
-
observer.observe(section);
|
|
65
|
-
return () => observer.disconnect();
|
|
66
|
-
}, [isVideo]);
|
|
67
42
|
if (SectionComponent)
|
|
68
43
|
return (0, jsx_runtime_1.jsx)("div", { ref: sectionRef, children: (0, jsx_runtime_1.jsx)(SectionComponent, { data: data, children: children }) });
|
|
69
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: `section-${section.id}`, id: section.name, ref: sectionRef, children: [
|
|
70
|
-
${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([height, color, media]) =>
|
|
71
|
-
var _a;
|
|
72
|
-
return (`
|
|
44
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: `section-${section.id}`, id: section.name, ref: sectionRef, children: [layoutMedia && layoutMedia.size !== 'none' && sectionRef.current && ((0, jsx_runtime_1.jsx)("div", { className: `section-background-overlay-${section.id}`, children: (0, jsx_runtime_1.jsxs)("div", { className: `section-background-wrapper-${section.id}`, children: [layoutMedia.type === 'video' && ((0, jsx_runtime_1.jsx)(SectionVideo_1.SectionVideo, { container: sectionRef.current, sectionId: section.id, media: layoutMedia })), layoutMedia.type === 'image' && ((0, jsx_runtime_1.jsx)(SectionImage_1.SectionImage, { media: layoutMedia, sectionId: section.id }))] }, `section-background-wrapper-${section.id}`) })), children] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
|
|
45
|
+
${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([height, color, media]) => (`
|
|
73
46
|
.section-${section.id} {
|
|
74
47
|
height: ${getSectionHeight(height)};
|
|
75
48
|
position: relative;
|
|
@@ -87,24 +60,7 @@ const Section = ({ section, data, children }) => {
|
|
|
87
60
|
height: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? `calc(${getSectionHeight(height)} + 200vh)` : getSectionHeight(height)};
|
|
88
61
|
width: 100%;
|
|
89
62
|
}
|
|
90
|
-
|
|
91
|
-
object-fit: ${(_a = media === null || media === void 0 ? void 0 : media.size) !== null && _a !== void 0 ? _a : 'cover'};
|
|
92
|
-
width: ${(media === null || media === void 0 ? void 0 : media.offsetX) === null || (media === null || media === void 0 ? void 0 : media.size) === 'cover' ? '100%' : 'auto'};
|
|
93
|
-
height: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? '100vh' : '100%'};
|
|
94
|
-
position: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? 'sticky' : 'relative'};
|
|
95
|
-
top: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? '100vh' : 'unset'};
|
|
96
|
-
${media && media.offsetX !== null && media.size !== 'cover' ? `margin-left: ${media.offsetX * 100}vw;` : ''}
|
|
97
|
-
}
|
|
98
|
-
.image-background-${section.id} {
|
|
99
|
-
object-fit: ${media === null || media === void 0 ? void 0 : media.size};
|
|
100
|
-
width: ${(media === null || media === void 0 ? void 0 : media.offsetX) === null ? '100%' : 'auto'};
|
|
101
|
-
height: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? '100vh' : '100%'};
|
|
102
|
-
position: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? 'sticky' : 'relative'};
|
|
103
|
-
top: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? '100vh' : 'unset'};
|
|
104
|
-
${media && media.offsetX !== null && media.size !== 'cover' ? `margin-left: ${media.offsetX * 100}vw;` : ''}
|
|
105
|
-
}
|
|
106
|
-
`);
|
|
107
|
-
})}
|
|
63
|
+
`))}
|
|
108
64
|
${getSectionVisibilityStyles()}
|
|
109
65
|
` })] }));
|
|
110
66
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SectionImage = void 0;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
const style_1 = __importDefault(require("styled-jsx/style"));
|
|
10
|
+
const SectionImage = ({ media, sectionId }) => {
|
|
11
|
+
const id = (0, react_1.useId)();
|
|
12
|
+
const { url, size, position, offsetX } = media;
|
|
13
|
+
const isContainHeight = size === 'contain-height';
|
|
14
|
+
const hasOffsetX = offsetX !== null && size === 'contain';
|
|
15
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("img", { src: url, className: `image-background-${sectionId}` }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
|
|
16
|
+
.image-background-${sectionId} {
|
|
17
|
+
object-fit: ${isContainHeight ? 'unset' : size !== null && size !== void 0 ? size : 'cover'};
|
|
18
|
+
width: ${isContainHeight || hasOffsetX ? 'auto' : '100%'};
|
|
19
|
+
height: ${position === 'fixed' ? '100vh' : '100%'};
|
|
20
|
+
transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
|
|
21
|
+
position: ${position === 'fixed' ? 'sticky' : 'relative'};
|
|
22
|
+
top: ${position === 'fixed' ? '100vh' : 'unset'};
|
|
23
|
+
margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
|
|
24
|
+
${offsetX ? 'max-width: 100vw;' : ''}
|
|
25
|
+
}
|
|
26
|
+
` })] }));
|
|
27
|
+
};
|
|
28
|
+
exports.SectionImage = SectionImage;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SectionVideo = void 0;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
const style_1 = __importDefault(require("styled-jsx/style"));
|
|
10
|
+
const SectionVideo = ({ container, sectionId, media }) => {
|
|
11
|
+
const [video, setVideo] = (0, react_1.useState)(null);
|
|
12
|
+
const { url, size, position, offsetX, coverUrl, play } = media;
|
|
13
|
+
const id = (0, react_1.useId)();
|
|
14
|
+
const [isPlaying, setIsPlaying] = (0, react_1.useState)(false);
|
|
15
|
+
const handleCoverClick = () => {
|
|
16
|
+
if (!video || play !== 'on-click')
|
|
17
|
+
return;
|
|
18
|
+
if (isPlaying) {
|
|
19
|
+
video.pause();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
video.play();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
(0, react_1.useEffect)(() => {
|
|
26
|
+
if (!video || play !== 'auto')
|
|
27
|
+
return;
|
|
28
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
29
|
+
if (entry.isIntersecting) {
|
|
30
|
+
video.style.display = 'block';
|
|
31
|
+
video.play().catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
video.style.display = 'none';
|
|
35
|
+
video.pause();
|
|
36
|
+
}
|
|
37
|
+
}, {
|
|
38
|
+
root: null,
|
|
39
|
+
rootMargin: '50px',
|
|
40
|
+
threshold: 0
|
|
41
|
+
});
|
|
42
|
+
observer.observe(container);
|
|
43
|
+
return () => observer.disconnect();
|
|
44
|
+
}, [container, play]);
|
|
45
|
+
(0, react_1.useEffect)(() => {
|
|
46
|
+
if (!video || play !== 'on-click')
|
|
47
|
+
return;
|
|
48
|
+
video.currentTime = 0.01;
|
|
49
|
+
video.pause();
|
|
50
|
+
}, [play]);
|
|
51
|
+
const isContainHeight = size === 'contain-height';
|
|
52
|
+
const hasOffsetX = offsetX !== null && size === 'contain';
|
|
53
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("video", { ref: setVideo, autoPlay: play === 'auto', loop: true, controls: false, muted: play === 'auto', playsInline: true, preload: "auto", className: `video-background-${sectionId}`, onPlay: () => setIsPlaying(true), onPause: () => setIsPlaying(false), children: (0, jsx_runtime_1.jsx)("source", { src: `${url}#t=0.001` }) }), (0, jsx_runtime_1.jsx)("div", { className: `video-background-${sectionId}-cover-container`, onClick: handleCoverClick, children: coverUrl && play === 'on-click' && ((0, jsx_runtime_1.jsx)("img", { src: coverUrl, alt: "Video cover", className: `video-background-${sectionId}-cover`, style: { opacity: isPlaying ? 0 : 1 } })) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
|
|
54
|
+
.video-background-${sectionId}-cover-container {
|
|
55
|
+
position: ${position === 'fixed' ? 'sticky' : 'absolute'};
|
|
56
|
+
pointer-events: ${play === 'on-click' ? 'auto' : 'none'};
|
|
57
|
+
left: 0;
|
|
58
|
+
width: 100vw;
|
|
59
|
+
height: 100vh;
|
|
60
|
+
top: ${position === 'fixed' ? '100vh' : '0'};
|
|
61
|
+
}
|
|
62
|
+
.video-background-${sectionId}-cover {
|
|
63
|
+
width: 100%;
|
|
64
|
+
transition: opacity 0.1s ease-in-out;
|
|
65
|
+
height: 100%;
|
|
66
|
+
object-fit: cover;
|
|
67
|
+
}
|
|
68
|
+
.video-background-${sectionId} {
|
|
69
|
+
object-fit: ${isContainHeight ? 'unset' : size !== null && size !== void 0 ? size : 'cover'};
|
|
70
|
+
width: ${isContainHeight || hasOffsetX ? 'auto' : '100%'};
|
|
71
|
+
height: ${position === 'fixed' ? '100vh' : '100%'};
|
|
72
|
+
transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
|
|
73
|
+
position: ${position === 'fixed' ? 'sticky' : 'relative'};
|
|
74
|
+
top: ${position === 'fixed' ? '100vh' : 'unset'};
|
|
75
|
+
margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
|
|
76
|
+
${offsetX ? 'max-width: 100vw;' : ''}
|
|
77
|
+
}
|
|
78
|
+
` })] }));
|
|
79
|
+
};
|
|
80
|
+
exports.SectionVideo = SectionVideo;
|
|
@@ -29,6 +29,7 @@ class InteractionsRegistry {
|
|
|
29
29
|
}
|
|
30
30
|
return map;
|
|
31
31
|
}, {});
|
|
32
|
+
const itemStages = this.getDefaultItemStages();
|
|
32
33
|
const stateItemsIdsMap = activeStatesIds.reduce((map, stateId) => {
|
|
33
34
|
map[stateId] = this.items
|
|
34
35
|
.filter((item) => {
|
|
@@ -41,10 +42,9 @@ class InteractionsRegistry {
|
|
|
41
42
|
return map;
|
|
42
43
|
}, {});
|
|
43
44
|
this.interactions = interactions;
|
|
45
|
+
this.itemsStages = itemStages;
|
|
44
46
|
this.stateItemsIdsMap = stateItemsIdsMap;
|
|
45
47
|
this.interactionStateMap = interactionStateMap;
|
|
46
|
-
const itemStages = this.getDefaultItemStages();
|
|
47
|
-
this.itemsStages = itemStages;
|
|
48
48
|
}
|
|
49
49
|
register(itemId, ctrl) {
|
|
50
50
|
this.ctrls.set(itemId, ctrl);
|
|
@@ -93,8 +93,6 @@ class InteractionsRegistry {
|
|
|
93
93
|
for (const interaction of this.interactions) {
|
|
94
94
|
const { triggers } = interaction;
|
|
95
95
|
for (const trigger of triggers) {
|
|
96
|
-
if (!('itemId' in trigger))
|
|
97
|
-
continue;
|
|
98
96
|
if (trigger.itemId !== itemId)
|
|
99
97
|
continue;
|
|
100
98
|
if (activeStates.includes(trigger.from)) {
|
|
@@ -104,112 +102,12 @@ class InteractionsRegistry {
|
|
|
104
102
|
}
|
|
105
103
|
return available;
|
|
106
104
|
}
|
|
107
|
-
|
|
108
|
-
var _a, _b;
|
|
109
|
-
const timestamp = Date.now();
|
|
110
|
-
for (const interaction of this.interactions) {
|
|
111
|
-
const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
|
|
112
|
-
const matchingTrigger = interaction.triggers.find(trigger => 'position' in trigger && trigger.position === 0 && trigger.from === currentStateId);
|
|
113
|
-
if (!matchingTrigger)
|
|
114
|
-
continue;
|
|
115
|
-
const activeStateId = this.getActiveInteractionState(interaction.id);
|
|
116
|
-
const isNewStateActive = matchingTrigger.to === activeStateId;
|
|
117
|
-
this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
|
|
118
|
-
const transitioningItems = (_a = this.stateItemsIdsMap[activeStateId]) !== null && _a !== void 0 ? _a : [];
|
|
119
|
-
const state = interaction.states.find((state) => state.id === matchingTrigger.to);
|
|
120
|
-
const actions = (_b = state === null || state === void 0 ? void 0 : state.actions) !== null && _b !== void 0 ? _b : [];
|
|
121
|
-
for (const action of actions) {
|
|
122
|
-
const ctrl = this.ctrls.get(action.itemId);
|
|
123
|
-
if (!ctrl)
|
|
124
|
-
continue;
|
|
125
|
-
ctrl.receiveAction(action.type);
|
|
126
|
-
}
|
|
127
|
-
this.itemsStages = this.itemsStages.map((stage) => {
|
|
128
|
-
if (stage.interactionId !== interaction.id)
|
|
129
|
-
return stage;
|
|
130
|
-
return {
|
|
131
|
-
itemId: stage.itemId,
|
|
132
|
-
interactionId: stage.interactionId,
|
|
133
|
-
type: 'transitioning',
|
|
134
|
-
from: stage.type === 'transitioning' ? stage.to : stage.stateId,
|
|
135
|
-
to: matchingTrigger.to,
|
|
136
|
-
direction: isNewStateActive ? 'in' : 'out',
|
|
137
|
-
updated: timestamp
|
|
138
|
-
};
|
|
139
|
-
});
|
|
140
|
-
const itemsToNotify = new Set(transitioningItems);
|
|
141
|
-
for (const trigger of interaction.triggers) {
|
|
142
|
-
if (!('itemId' in trigger))
|
|
143
|
-
continue;
|
|
144
|
-
itemsToNotify.add(trigger.itemId);
|
|
145
|
-
}
|
|
146
|
-
this.notifyItemCtrlsChange(Array.from(itemsToNotify));
|
|
147
|
-
this.notifyTransitionStartForItems(transitioningItems, activeStateId);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
notifyScroll(position) {
|
|
151
|
-
var _a, _b, _c;
|
|
152
|
-
const timestamp = Date.now();
|
|
153
|
-
for (const interaction of this.interactions) {
|
|
154
|
-
const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
|
|
155
|
-
const activeStateId = (_a = interaction.states.find((state) => state.id !== interaction.startStateId)) === null || _a === void 0 ? void 0 : _a.id;
|
|
156
|
-
const matchingTrigger = interaction.triggers.find((trigger) => {
|
|
157
|
-
if (!('position' in trigger) || trigger.position === 0)
|
|
158
|
-
return false;
|
|
159
|
-
const triggerPosition = trigger.position * window.innerWidth;
|
|
160
|
-
const isScrolledPastTrigger = triggerPosition < position;
|
|
161
|
-
if (!isScrolledPastTrigger && !trigger.isReverse)
|
|
162
|
-
return false;
|
|
163
|
-
const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
|
|
164
|
-
return stateId === currentStateId;
|
|
165
|
-
});
|
|
166
|
-
if (!matchingTrigger || !('position' in matchingTrigger) || !activeStateId)
|
|
167
|
-
continue;
|
|
168
|
-
const triggerPosition = matchingTrigger.position * window.innerWidth;
|
|
169
|
-
const isScrolledPastTrigger = triggerPosition < position;
|
|
170
|
-
const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
|
|
171
|
-
this.setCurrentStateForInteraction(interaction.id, targetStateId);
|
|
172
|
-
const transitioningItems = (_b = this.stateItemsIdsMap[activeStateId]) !== null && _b !== void 0 ? _b : [];
|
|
173
|
-
const state = interaction.states.find((state) => state.id === targetStateId);
|
|
174
|
-
const actions = (_c = state === null || state === void 0 ? void 0 : state.actions) !== null && _c !== void 0 ? _c : [];
|
|
175
|
-
for (const action of actions) {
|
|
176
|
-
const ctrl = this.ctrls.get(action.itemId);
|
|
177
|
-
if (!ctrl)
|
|
178
|
-
continue;
|
|
179
|
-
ctrl.receiveAction(action.type);
|
|
180
|
-
}
|
|
181
|
-
const itemsStages = this.itemsStages.map((stage) => {
|
|
182
|
-
if (stage.interactionId !== interaction.id)
|
|
183
|
-
return stage;
|
|
184
|
-
const newStage = {
|
|
185
|
-
itemId: stage.itemId,
|
|
186
|
-
interactionId: stage.interactionId,
|
|
187
|
-
type: 'transitioning',
|
|
188
|
-
from: stage.type === 'transitioning' ? stage.to : stage.stateId,
|
|
189
|
-
to: targetStateId,
|
|
190
|
-
direction: targetStateId === activeStateId ? 'in' : 'out',
|
|
191
|
-
updated: timestamp
|
|
192
|
-
};
|
|
193
|
-
return newStage;
|
|
194
|
-
});
|
|
195
|
-
this.itemsStages = itemsStages;
|
|
196
|
-
const itemsToNotify = new Set(transitioningItems);
|
|
197
|
-
for (const trigger of interaction.triggers) {
|
|
198
|
-
if (!('itemId' in trigger))
|
|
199
|
-
continue;
|
|
200
|
-
itemsToNotify.add(trigger.itemId);
|
|
201
|
-
}
|
|
202
|
-
this.notifyItemCtrlsChange(Array.from(itemsToNotify));
|
|
203
|
-
this.notifyTransitionStartForItems(transitioningItems, activeStateId);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
notifyItemTrigger(itemId, triggerType) {
|
|
105
|
+
notifyTrigger(itemId, triggerType) {
|
|
207
106
|
var _a, _b;
|
|
208
107
|
const timestamp = Date.now();
|
|
209
108
|
for (const interaction of this.interactions) {
|
|
210
109
|
const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
|
|
211
|
-
const matchingTrigger = interaction.triggers.find((trigger) =>
|
|
212
|
-
&& trigger.itemId === itemId
|
|
110
|
+
const matchingTrigger = interaction.triggers.find((trigger) => trigger.itemId === itemId
|
|
213
111
|
&& trigger.from === currentStateId
|
|
214
112
|
&& trigger.type === triggerType);
|
|
215
113
|
if (!matchingTrigger)
|
|
@@ -241,8 +139,6 @@ class InteractionsRegistry {
|
|
|
241
139
|
});
|
|
242
140
|
const itemsToNotify = new Set(transitioningItems);
|
|
243
141
|
for (const trigger of interaction.triggers) {
|
|
244
|
-
if (!('itemId' in trigger))
|
|
245
|
-
continue;
|
|
246
142
|
itemsToNotify.add(trigger.itemId);
|
|
247
143
|
}
|
|
248
144
|
this.notifyItemCtrlsChange(Array.from(itemsToNotify));
|
|
@@ -327,7 +223,6 @@ class InteractionsRegistry {
|
|
|
327
223
|
return allItems;
|
|
328
224
|
}
|
|
329
225
|
getDefaultItemStages() {
|
|
330
|
-
var _a;
|
|
331
226
|
const timestamp = Date.now();
|
|
332
227
|
const { items } = this;
|
|
333
228
|
const stages = [];
|
|
@@ -344,8 +239,7 @@ class InteractionsRegistry {
|
|
|
344
239
|
interactionId,
|
|
345
240
|
type: 'active',
|
|
346
241
|
isStartState: true,
|
|
347
|
-
updated: timestamp
|
|
348
|
-
stateId: (_a = this.interactions.find((interaction) => interaction.id === interactionId)) === null || _a === void 0 ? void 0 : _a.startStateId
|
|
242
|
+
updated: timestamp
|
|
349
243
|
});
|
|
350
244
|
}
|
|
351
245
|
}
|
|
@@ -5,50 +5,14 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const InteractionsRegistry_1 = require("../interactions/InteractionsRegistry");
|
|
7
7
|
const useCurrentLayout_1 = require("../common/useCurrentLayout");
|
|
8
|
-
const ArticleRectContext_1 = require("./ArticleRectContext");
|
|
9
8
|
exports.InteractionsContext = (0, react_1.createContext)(undefined);
|
|
10
9
|
const InteractionsProvider = ({ article, children }) => {
|
|
11
10
|
const { layoutId } = (0, useCurrentLayout_1.useCurrentLayout)();
|
|
12
|
-
const articleRectObserver = (0, react_1.useContext)(ArticleRectContext_1.ArticleRectContext);
|
|
13
11
|
const registry = (0, react_1.useMemo)(() => {
|
|
14
12
|
if (!layoutId)
|
|
15
13
|
return;
|
|
16
14
|
return new InteractionsRegistry_1.InteractionsRegistry(article, layoutId);
|
|
17
15
|
}, [layoutId]);
|
|
18
|
-
(0, react_1.useEffect)(() => {
|
|
19
|
-
if (!registry || !articleRectObserver)
|
|
20
|
-
return;
|
|
21
|
-
const handleScroll = () => {
|
|
22
|
-
const scrollY = window.scrollY;
|
|
23
|
-
registry.notifyScroll(scrollY);
|
|
24
|
-
};
|
|
25
|
-
return articleRectObserver.on('scroll', handleScroll);
|
|
26
|
-
}, [registry, articleRectObserver]);
|
|
27
|
-
const notifyLoad = (0, react_1.useCallback)(() => {
|
|
28
|
-
if (!registry)
|
|
29
|
-
return;
|
|
30
|
-
requestAnimationFrame(() => {
|
|
31
|
-
setTimeout(() => {
|
|
32
|
-
registry.notifyLoad();
|
|
33
|
-
}, 0);
|
|
34
|
-
});
|
|
35
|
-
}, [registry]);
|
|
36
|
-
(0, react_1.useEffect)(() => {
|
|
37
|
-
if (document.readyState === 'complete') {
|
|
38
|
-
notifyLoad();
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
window.addEventListener('load', notifyLoad);
|
|
42
|
-
}
|
|
43
|
-
return () => window.removeEventListener('load', notifyLoad);
|
|
44
|
-
}, [notifyLoad]);
|
|
45
|
-
(0, react_1.useEffect)(() => {
|
|
46
|
-
const log = () => {
|
|
47
|
-
console.log('load');
|
|
48
|
-
};
|
|
49
|
-
window.addEventListener('load', log);
|
|
50
|
-
return () => window.removeEventListener('load', log);
|
|
51
|
-
}, []);
|
|
52
16
|
return ((0, jsx_runtime_1.jsx)(exports.InteractionsContext.Provider, { value: registry, children: children }));
|
|
53
17
|
};
|
|
54
18
|
exports.InteractionsProvider = InteractionsProvider;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntrl-site/sdk-nextjs",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.19-alpha.0",
|
|
4
4
|
"description": "SDK for Next.js",
|
|
5
5
|
"author": "arsen@momdesign.nyc",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@antfu/eslint-config": "^3.8.0",
|
|
32
32
|
"@cntrl-site/color": "^1.0.0",
|
|
33
33
|
"@cntrl-site/effects": "^1.4.0",
|
|
34
|
-
"@cntrl-site/sdk": "^1.22.
|
|
34
|
+
"@cntrl-site/sdk": "^1.22.18-alpha.0",
|
|
35
35
|
"@types/vimeo__player": "^2.18.0",
|
|
36
36
|
"@vimeo/player": "^2.25.0",
|
|
37
37
|
"html-react-parser": "^3.0.1",
|
|
@@ -24,7 +24,7 @@ export const ScrollPlaybackVideo: FC<Props> = ({ sectionId, src, playbackParams,
|
|
|
24
24
|
const scrollPos = articleRectObserver.getSectionScroll(sectionId);
|
|
25
25
|
const time = rangeMap(scrollPos, playbackParams.from, playbackParams.to, 0, 1, true);
|
|
26
26
|
setTime(toFixed(time));
|
|
27
|
-
})
|
|
27
|
+
})
|
|
28
28
|
}, [playbackParams?.from, playbackParams?.to, time]);
|
|
29
29
|
|
|
30
30
|
const scrollVideoManager = useMemo<ScrollPlaybackVideoManager | null>(() => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FC, ReactElement,
|
|
1
|
+
import { FC, ReactElement, useId, useRef } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
3
|
import {
|
|
4
4
|
getLayoutMediaQuery,
|
|
@@ -11,6 +11,8 @@ import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
|
11
11
|
import { useSectionRegistry } from '../../utils/ArticleRectManager/useSectionRegistry';
|
|
12
12
|
import { CntrlColor } from '@cntrl-site/color';
|
|
13
13
|
import { useLayoutContext } from '../useLayoutContext';
|
|
14
|
+
import { SectionVideo } from './SectionVideo';
|
|
15
|
+
import { SectionImage } from './SectionImage';
|
|
14
16
|
|
|
15
17
|
type SectionChild = ReactElement<any, any>;
|
|
16
18
|
const DEFAULT_COLOR = 'rgba(0, 0, 0, 0)';
|
|
@@ -21,11 +23,6 @@ interface Props {
|
|
|
21
23
|
data?: any;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
function isVideoUrl(url: string): boolean {
|
|
25
|
-
const videoExtensions = ['.mp4', '.webm', '.ogg'];
|
|
26
|
-
return videoExtensions.some(ext => url.toLowerCase().endsWith(ext));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
26
|
export const Section: FC<Props> = ({ section, data, children }) => {
|
|
30
27
|
const id = useId();
|
|
31
28
|
const sectionRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -33,9 +30,8 @@ export const Section: FC<Props> = ({ section, data, children }) => {
|
|
|
33
30
|
const layout = useLayoutContext();
|
|
34
31
|
const layoutValues: Record<string, any>[] = [section.height, section.color, section.media ?? {}];
|
|
35
32
|
const SectionComponent = section.name ? customSections.getComponent(section.name) : undefined;
|
|
36
|
-
const isVideo = layout && section.media?.[layout]?.url ? isVideoUrl(String(section.media?.[layout]?.url)) : false;
|
|
37
|
-
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
38
33
|
useSectionRegistry(section.id, sectionRef.current);
|
|
34
|
+
const layoutMedia = layout && section.media && section.media[layout] ? section.media[layout] : undefined;
|
|
39
35
|
|
|
40
36
|
const getSectionVisibilityStyles = () => {
|
|
41
37
|
return layouts
|
|
@@ -52,30 +48,6 @@ export const Section: FC<Props> = ({ section, data, children }) => {
|
|
|
52
48
|
}, '');
|
|
53
49
|
};
|
|
54
50
|
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
if (!isVideo || !sectionRef.current || !videoRef.current) return;
|
|
57
|
-
const video = videoRef.current;
|
|
58
|
-
const section = sectionRef.current;
|
|
59
|
-
const observer = new IntersectionObserver(
|
|
60
|
-
([entry]) => {
|
|
61
|
-
if (entry.isIntersecting) {
|
|
62
|
-
video.style.display = 'block';
|
|
63
|
-
video.play().catch(() => {});
|
|
64
|
-
} else {
|
|
65
|
-
video.style.display = 'none';
|
|
66
|
-
video.pause();
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
root: null,
|
|
71
|
-
rootMargin: '50px',
|
|
72
|
-
threshold: 0
|
|
73
|
-
}
|
|
74
|
-
);
|
|
75
|
-
observer.observe(section);
|
|
76
|
-
return () => observer.disconnect();
|
|
77
|
-
}, [isVideo]);
|
|
78
|
-
|
|
79
51
|
if (SectionComponent) return <div ref={sectionRef}><SectionComponent data={data}>{children}</SectionComponent></div>;
|
|
80
52
|
|
|
81
53
|
return (
|
|
@@ -85,26 +57,17 @@ export const Section: FC<Props> = ({ section, data, children }) => {
|
|
|
85
57
|
id={section.name}
|
|
86
58
|
ref={sectionRef}
|
|
87
59
|
>
|
|
88
|
-
{
|
|
60
|
+
{layoutMedia && layoutMedia.size !== 'none' && sectionRef.current && (
|
|
89
61
|
<div className={`section-background-overlay-${section.id}`}>
|
|
90
62
|
<div
|
|
91
63
|
key={`section-background-wrapper-${section.id}`}
|
|
92
64
|
className={`section-background-wrapper-${section.id}`}
|
|
93
65
|
>
|
|
94
|
-
{
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
muted
|
|
100
|
-
playsInline
|
|
101
|
-
preload="auto"
|
|
102
|
-
className={`video-background-${section.id}`}
|
|
103
|
-
>
|
|
104
|
-
<source src={section.media?.[layout]?.url ?? ''} />
|
|
105
|
-
</video>
|
|
106
|
-
) : (
|
|
107
|
-
<img src={section.media?.[layout]?.url ?? ''} className={`image-background-${section.id}`} />
|
|
66
|
+
{layoutMedia.type === 'video' && (
|
|
67
|
+
<SectionVideo container={sectionRef.current} sectionId={section.id} media={layoutMedia} />
|
|
68
|
+
)}
|
|
69
|
+
{layoutMedia.type === 'image' && (
|
|
70
|
+
<SectionImage media={layoutMedia} sectionId={section.id} />
|
|
108
71
|
)}
|
|
109
72
|
</div>
|
|
110
73
|
</div>
|
|
@@ -131,22 +94,6 @@ export const Section: FC<Props> = ({ section, data, children }) => {
|
|
|
131
94
|
height: ${media?.position === 'fixed' ? `calc(${getSectionHeight(height)} + 200vh)` : getSectionHeight(height)};
|
|
132
95
|
width: 100%;
|
|
133
96
|
}
|
|
134
|
-
.video-background-${section.id} {
|
|
135
|
-
object-fit: ${media?.size ?? 'cover'};
|
|
136
|
-
width: ${media?.offsetX === null || media?.size === 'cover' ? '100%' : 'auto'};
|
|
137
|
-
height: ${media?.position === 'fixed' ? '100vh' : '100%'};
|
|
138
|
-
position: ${media?.position === 'fixed' ? 'sticky' : 'relative'};
|
|
139
|
-
top: ${media?.position === 'fixed' ? '100vh' : 'unset'};
|
|
140
|
-
${media && media.offsetX !== null && media.size !== 'cover' ? `margin-left: ${media.offsetX * 100}vw;` : ''}
|
|
141
|
-
}
|
|
142
|
-
.image-background-${section.id} {
|
|
143
|
-
object-fit: ${media?.size};
|
|
144
|
-
width: ${media?.offsetX === null ? '100%' : 'auto'};
|
|
145
|
-
height: ${media?.position === 'fixed' ? '100vh' : '100%'};
|
|
146
|
-
position: ${media?.position === 'fixed' ? 'sticky' : 'relative'};
|
|
147
|
-
top: ${media?.position === 'fixed' ? '100vh' : 'unset'};
|
|
148
|
-
${media && media.offsetX !== null && media.size !== 'cover' ? `margin-left: ${media.offsetX * 100}vw;` : ''}
|
|
149
|
-
}
|
|
150
97
|
`
|
|
151
98
|
))
|
|
152
99
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { FC, useId } from 'react';
|
|
2
|
+
import JSXStyle from 'styled-jsx/style';
|
|
3
|
+
|
|
4
|
+
export type TSectionImage = {
|
|
5
|
+
url: string;
|
|
6
|
+
type: 'image';
|
|
7
|
+
size: string;
|
|
8
|
+
position: string;
|
|
9
|
+
offsetX: number | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
media: TSectionImage;
|
|
14
|
+
sectionId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const SectionImage: FC<Props> = ({ media, sectionId }) => {
|
|
18
|
+
const id = useId();
|
|
19
|
+
const { url, size, position, offsetX } = media;
|
|
20
|
+
const isContainHeight = size === 'contain-height';
|
|
21
|
+
const hasOffsetX = offsetX !== null && size === 'contain';
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<img src={url} className={`image-background-${sectionId}`} />
|
|
25
|
+
<JSXStyle id={id}>{`
|
|
26
|
+
.image-background-${sectionId} {
|
|
27
|
+
object-fit: ${isContainHeight ? 'unset' : size ?? 'cover'};
|
|
28
|
+
width: ${isContainHeight || hasOffsetX ? 'auto' : '100%'};
|
|
29
|
+
height: ${position === 'fixed' ? '100vh' : '100%'};
|
|
30
|
+
transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
|
|
31
|
+
position: ${position === 'fixed' ? 'sticky' : 'relative'};
|
|
32
|
+
top: ${position === 'fixed' ? '100vh' : 'unset'};
|
|
33
|
+
margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
|
|
34
|
+
${offsetX ? 'max-width: 100vw;' : ''}
|
|
35
|
+
}
|
|
36
|
+
`}
|
|
37
|
+
</JSXStyle>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { FC, useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import JSXStyle from 'styled-jsx/style';
|
|
3
|
+
|
|
4
|
+
export type TSectionVideo = {
|
|
5
|
+
url: string;
|
|
6
|
+
size: string;
|
|
7
|
+
type: 'video';
|
|
8
|
+
play: 'on-click' | 'auto';
|
|
9
|
+
position: string;
|
|
10
|
+
coverUrl: string | null;
|
|
11
|
+
offsetX: number | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
container: HTMLDivElement;
|
|
16
|
+
sectionId: string;
|
|
17
|
+
media: TSectionVideo;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SectionVideo: FC<Props> = ({ container, sectionId, media }) => {
|
|
21
|
+
const [video, setVideo] = useState<HTMLVideoElement | null>(null);
|
|
22
|
+
const { url, size, position, offsetX, coverUrl, play } = media;
|
|
23
|
+
const id = useId();
|
|
24
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
25
|
+
|
|
26
|
+
const handleCoverClick = () => {
|
|
27
|
+
if (!video || play !== 'on-click') return;
|
|
28
|
+
if (isPlaying) {
|
|
29
|
+
video.pause();
|
|
30
|
+
} else {
|
|
31
|
+
video.play();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!video || play !== 'auto') return;
|
|
37
|
+
const observer = new IntersectionObserver(
|
|
38
|
+
([entry]) => {
|
|
39
|
+
if (entry.isIntersecting) {
|
|
40
|
+
video.style.display = 'block';
|
|
41
|
+
video.play().catch(() => {});
|
|
42
|
+
} else {
|
|
43
|
+
video.style.display = 'none';
|
|
44
|
+
video.pause();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
root: null,
|
|
49
|
+
rootMargin: '50px',
|
|
50
|
+
threshold: 0
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
observer.observe(container);
|
|
54
|
+
return () => observer.disconnect();
|
|
55
|
+
}, [container, play]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!video || play !== 'on-click') return;
|
|
59
|
+
video.currentTime = 0.01;
|
|
60
|
+
video.pause();
|
|
61
|
+
}, [play]);
|
|
62
|
+
|
|
63
|
+
const isContainHeight = size === 'contain-height';
|
|
64
|
+
const hasOffsetX = offsetX !== null && size === 'contain';
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<video
|
|
69
|
+
ref={setVideo}
|
|
70
|
+
autoPlay={play === 'auto'}
|
|
71
|
+
loop
|
|
72
|
+
controls={false}
|
|
73
|
+
muted={play === 'auto'}
|
|
74
|
+
playsInline
|
|
75
|
+
preload="auto"
|
|
76
|
+
className={`video-background-${sectionId}`}
|
|
77
|
+
onPlay={() => setIsPlaying(true)}
|
|
78
|
+
onPause={() => setIsPlaying(false)}
|
|
79
|
+
>
|
|
80
|
+
<source src={`${url}#t=0.001`} />
|
|
81
|
+
</video>
|
|
82
|
+
<div className={`video-background-${sectionId}-cover-container`} onClick={handleCoverClick}>
|
|
83
|
+
{coverUrl && play === 'on-click' && (
|
|
84
|
+
<img src={coverUrl} alt="Video cover" className={`video-background-${sectionId}-cover`} style={{ opacity: isPlaying ? 0 : 1 }} />
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
<JSXStyle id={id}>{`
|
|
88
|
+
.video-background-${sectionId}-cover-container {
|
|
89
|
+
position: ${position === 'fixed' ? 'sticky' : 'absolute'};
|
|
90
|
+
pointer-events: ${play === 'on-click' ? 'auto' : 'none'};
|
|
91
|
+
left: 0;
|
|
92
|
+
width: 100vw;
|
|
93
|
+
height: 100vh;
|
|
94
|
+
top: ${position === 'fixed' ? '100vh' : '0'};
|
|
95
|
+
}
|
|
96
|
+
.video-background-${sectionId}-cover {
|
|
97
|
+
width: 100%;
|
|
98
|
+
transition: opacity 0.1s ease-in-out;
|
|
99
|
+
height: 100%;
|
|
100
|
+
object-fit: cover;
|
|
101
|
+
}
|
|
102
|
+
.video-background-${sectionId} {
|
|
103
|
+
object-fit: ${isContainHeight ? 'unset' : size ?? 'cover'};
|
|
104
|
+
width: ${isContainHeight || hasOffsetX ? 'auto' : '100%'};
|
|
105
|
+
height: ${position === 'fixed' ? '100vh' : '100%'};
|
|
106
|
+
transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
|
|
107
|
+
position: ${position === 'fixed' ? 'sticky' : 'relative'};
|
|
108
|
+
top: ${position === 'fixed' ? '100vh' : 'unset'};
|
|
109
|
+
margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
|
|
110
|
+
${offsetX ? 'max-width: 100vw;' : ''}
|
|
111
|
+
}
|
|
112
|
+
`}
|
|
113
|
+
</JSXStyle>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
Article,
|
|
4
4
|
ArticleItemType,
|
|
5
5
|
Interaction,
|
|
6
|
-
|
|
6
|
+
InteractionTrigger,
|
|
7
7
|
ItemAny,
|
|
8
8
|
} from '@cntrl-site/sdk';
|
|
9
9
|
import { isItemType } from '../utils/isItemType';
|
|
@@ -39,6 +39,7 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
39
39
|
}
|
|
40
40
|
return map;
|
|
41
41
|
}, {});
|
|
42
|
+
const itemStages = this.getDefaultItemStages();
|
|
42
43
|
const stateItemsIdsMap = activeStatesIds.reduce<StateItemsIdsMap>((map, stateId) => {
|
|
43
44
|
map[stateId] = this.items
|
|
44
45
|
.filter((item) => {
|
|
@@ -50,10 +51,9 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
50
51
|
return map;
|
|
51
52
|
}, {});
|
|
52
53
|
this.interactions = interactions;
|
|
54
|
+
this.itemsStages = itemStages;
|
|
53
55
|
this.stateItemsIdsMap = stateItemsIdsMap;
|
|
54
56
|
this.interactionStateMap = interactionStateMap;
|
|
55
|
-
const itemStages = this.getDefaultItemStages();
|
|
56
|
-
this.itemsStages = itemStages;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
|
|
@@ -96,13 +96,12 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
96
96
|
return itemStyles;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
getItemAvailableTriggers(itemId: string): Set<
|
|
100
|
-
const available = new Set<
|
|
99
|
+
getItemAvailableTriggers(itemId: string): Set<InteractionTrigger['type']> {
|
|
100
|
+
const available = new Set<InteractionTrigger['type']>();
|
|
101
101
|
const activeStates = Object.values(this.interactionStateMap);
|
|
102
102
|
for (const interaction of this.interactions) {
|
|
103
103
|
const { triggers } = interaction;
|
|
104
104
|
for (const trigger of triggers) {
|
|
105
|
-
if (!('itemId' in trigger)) continue;
|
|
106
105
|
if (trigger.itemId !== itemId) continue;
|
|
107
106
|
if (activeStates.includes(trigger.from)) {
|
|
108
107
|
available.add(trigger.type);
|
|
@@ -112,106 +111,12 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
112
111
|
return available;
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
|
|
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 {
|
|
114
|
+
notifyTrigger(itemId: string, triggerType: TriggerType): void {
|
|
209
115
|
const timestamp = Date.now();
|
|
210
116
|
for (const interaction of this.interactions) {
|
|
211
117
|
const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
|
|
212
118
|
const matchingTrigger = interaction.triggers.find((trigger) =>
|
|
213
|
-
|
|
214
|
-
&& trigger.itemId === itemId
|
|
119
|
+
trigger.itemId === itemId
|
|
215
120
|
&& trigger.from === currentStateId
|
|
216
121
|
&& trigger.type === triggerType
|
|
217
122
|
);
|
|
@@ -241,7 +146,6 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
241
146
|
});
|
|
242
147
|
const itemsToNotify = new Set<ItemId>(transitioningItems);
|
|
243
148
|
for (const trigger of interaction.triggers) {
|
|
244
|
-
if (!('itemId' in trigger)) continue;
|
|
245
149
|
itemsToNotify.add(trigger.itemId);
|
|
246
150
|
}
|
|
247
151
|
this.notifyItemCtrlsChange(Array.from(itemsToNotify));
|
|
@@ -344,8 +248,7 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
344
248
|
interactionId,
|
|
345
249
|
type: 'active',
|
|
346
250
|
isStartState: true,
|
|
347
|
-
updated: timestamp
|
|
348
|
-
stateId: this.interactions.find((interaction) => interaction.id === interactionId)?.startStateId
|
|
251
|
+
updated: timestamp
|
|
349
252
|
});
|
|
350
253
|
}
|
|
351
254
|
}
|
|
@@ -366,7 +269,7 @@ type TransitioningStage = {
|
|
|
366
269
|
type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
|
|
367
270
|
type InteractionStateMap = Record<InteractionId, StateId>;
|
|
368
271
|
type StateItemsIdsMap = Record<StateId, ItemId[]>;
|
|
369
|
-
type TriggerType =
|
|
272
|
+
type TriggerType = InteractionTrigger['type'];
|
|
370
273
|
type InteractionId = string;
|
|
371
274
|
type StateId = string;
|
|
372
275
|
type ItemId = string;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
|
|
2
2
|
import { getTransition } from './getTransition';
|
|
3
3
|
import { getStyleKeysFromCSSProperty } from './CSSPropertyNameMap';
|
|
4
|
-
import {
|
|
4
|
+
import { InteractionTrigger } from '@cntrl-site/sdk';
|
|
5
5
|
|
|
6
6
|
export class ItemInteractionController implements ItemInteractionCtrl {
|
|
7
7
|
private transitionsInProgress: Set<string> = new Set();
|
|
8
8
|
private actionReceiver: ((type: 'play' | 'pause') => void) | undefined;
|
|
9
|
-
|
|
10
9
|
constructor(
|
|
11
10
|
private itemId: string,
|
|
12
11
|
private registry: InteractionsRegistryPort,
|
|
@@ -30,13 +29,13 @@ export class ItemInteractionController implements ItemInteractionCtrl {
|
|
|
30
29
|
};
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
getHasTrigger(itemId: string, triggerType:
|
|
32
|
+
getHasTrigger(itemId: string, triggerType: InteractionTrigger['type']): boolean {
|
|
34
33
|
const triggers = this.registry.getItemAvailableTriggers(itemId);
|
|
35
34
|
return triggers.has(triggerType);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
sendTrigger(type: 'click' | 'hover-in' | 'hover-out') {
|
|
39
|
-
this.registry.
|
|
38
|
+
this.registry.notifyTrigger(this.itemId, type);
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
receiveAction(type: 'play' | 'pause') {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ArticleItemType,
|
|
1
|
+
import { ArticleItemType, InteractionTrigger, ItemState } from '@cntrl-site/sdk';
|
|
2
2
|
|
|
3
3
|
export interface ItemInteractionCtrl {
|
|
4
4
|
getState(keys: string[]): StateCSSInfo;
|
|
5
|
-
getHasTrigger(itemId: string, triggerType:
|
|
5
|
+
getHasTrigger(itemId: string, triggerType: InteractionTrigger['type']): boolean;
|
|
6
6
|
sendTrigger(type: 'click' | 'hover-in' | 'hover-out'): void;
|
|
7
7
|
handleTransitionEnd?: (styleKey: string) => void;
|
|
8
8
|
handleTransitionStart?: (styleKeys: string[]) => void;
|
|
@@ -14,8 +14,8 @@ export interface ItemInteractionCtrl {
|
|
|
14
14
|
export interface InteractionsRegistryPort {
|
|
15
15
|
register(itemId: string, ctrl: ItemInteractionCtrl): void;
|
|
16
16
|
getStatePropsForItem(itemId: string): StateProps;
|
|
17
|
-
getItemAvailableTriggers(itemId: string): Set<
|
|
18
|
-
|
|
17
|
+
getItemAvailableTriggers(itemId: string): Set<InteractionTrigger['type']>;
|
|
18
|
+
notifyTrigger(itemId: string, type: TriggerType): void;
|
|
19
19
|
notifyTransitionEnd(itemId: string): void;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { createContext, FC, PropsWithChildren,
|
|
1
|
+
import { createContext, FC, PropsWithChildren, useContext, useMemo } from 'react';
|
|
2
2
|
import { InteractionsRegistry } from '../interactions/InteractionsRegistry';
|
|
3
3
|
import { Article } from '@cntrl-site/sdk';
|
|
4
4
|
import { useCurrentLayout } from '../common/useCurrentLayout';
|
|
5
|
-
import { ArticleRectContext } from './ArticleRectContext';
|
|
6
5
|
|
|
7
6
|
export const InteractionsContext = createContext<InteractionsRegistry | undefined>(undefined);
|
|
8
7
|
|
|
@@ -12,48 +11,10 @@ interface Props {
|
|
|
12
11
|
|
|
13
12
|
export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ article, children }) => {
|
|
14
13
|
const { layoutId } = useCurrentLayout();
|
|
15
|
-
const articleRectObserver = useContext(ArticleRectContext);
|
|
16
14
|
const registry = useMemo(() => {
|
|
17
15
|
if (!layoutId) return;
|
|
18
16
|
return new InteractionsRegistry(article, layoutId);
|
|
19
17
|
}, [layoutId]);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!registry || !articleRectObserver) return;
|
|
23
|
-
const handleScroll = () => {
|
|
24
|
-
const scrollY = window.scrollY;
|
|
25
|
-
registry.notifyScroll(scrollY);
|
|
26
|
-
};
|
|
27
|
-
return articleRectObserver.on('scroll', handleScroll);
|
|
28
|
-
}, [registry, articleRectObserver]);
|
|
29
|
-
|
|
30
|
-
const notifyLoad = useCallback(() => {
|
|
31
|
-
if (!registry) return;
|
|
32
|
-
requestAnimationFrame(() => {
|
|
33
|
-
setTimeout(() => {
|
|
34
|
-
registry.notifyLoad();
|
|
35
|
-
}, 0);
|
|
36
|
-
});
|
|
37
|
-
}, [registry]);
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (document.readyState === 'complete') {
|
|
41
|
-
notifyLoad();
|
|
42
|
-
} else {
|
|
43
|
-
window.addEventListener('load', notifyLoad);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return () => window.removeEventListener('load', notifyLoad);
|
|
47
|
-
}, [notifyLoad]);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const log = () => {
|
|
51
|
-
console.log('load');
|
|
52
|
-
};
|
|
53
|
-
window.addEventListener('load', log);
|
|
54
|
-
return () => window.removeEventListener('load', log);
|
|
55
|
-
}, []);
|
|
56
|
-
|
|
57
18
|
return (
|
|
58
19
|
<InteractionsContext.Provider value={registry}>
|
|
59
20
|
{children}
|