@cntrl-site/sdk-nextjs 1.8.19 → 1.8.20

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.
Binary file
Binary file
@@ -12,22 +12,27 @@ 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");
17
+ const checkOverflowClipSupport_1 = require("../../utils/checkOverflowClipSupport");
15
18
  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
19
  const Section = ({ section, data, children }) => {
21
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
20
+ var _a;
22
21
  const id = (0, react_1.useId)();
23
22
  const sectionRef = (0, react_1.useRef)(null);
24
23
  const { layouts, customSections } = (0, useCntrlContext_1.useCntrlContext)();
25
24
  const layout = (0, useLayoutContext_1.useLayoutContext)();
26
25
  const layoutValues = [section.height, section.color, (_a = section.media) !== null && _a !== void 0 ? _a : {}];
27
26
  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
27
  (0, useSectionRegistry_1.useSectionRegistry)(section.id, sectionRef.current);
28
+ const sectionHeight = layout && section.height[layout] ? section.height[layout] : undefined;
29
+ const layoutMedia = layout && section.media && section.media[layout] ? section.media[layout] : undefined;
30
+ const media = (0, react_1.useMemo)(() => {
31
+ if (layoutMedia && !(0, checkOverflowClipSupport_1.isOverflowClipSupported)()) {
32
+ return Object.assign(Object.assign({}, layoutMedia), { position: 'local' });
33
+ }
34
+ return layoutMedia;
35
+ }, [layoutMedia]);
31
36
  const getSectionVisibilityStyles = () => {
32
37
  return layouts
33
38
  .sort((a, b) => a.startsWith - b.startsWith)
@@ -42,34 +47,10 @@ const Section = ({ section, data, children }) => {
42
47
  }`;
43
48
  }, '');
44
49
  };
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
50
  if (SectionComponent)
68
51
  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: [layout && ((_g = (_f = section.media) === null || _f === void 0 ? void 0 : _f[layout]) === null || _g === void 0 ? void 0 : _g.url) && ((_j = (_h = section.media) === null || _h === void 0 ? void 0 : _h[layout]) === null || _j === void 0 ? void 0 : _j.size) !== 'none' && sectionRef.current && ((0, jsx_runtime_1.jsx)("div", { className: `section-background-overlay-${section.id}`, children: (0, jsx_runtime_1.jsx)("div", { className: `section-background-wrapper-${section.id}`, children: isVideo ? ((0, jsx_runtime_1.jsx)("video", { ref: videoRef, autoPlay: true, loop: true, muted: true, playsInline: true, preload: "auto", className: `video-background-${section.id}`, children: (0, jsx_runtime_1.jsx)("source", { src: (_m = (_l = (_k = section.media) === null || _k === void 0 ? void 0 : _k[layout]) === null || _l === void 0 ? void 0 : _l.url) !== null && _m !== void 0 ? _m : '' }) })) : ((0, jsx_runtime_1.jsx)("img", { src: (_q = (_p = (_o = section.media) === null || _o === void 0 ? void 0 : _o[layout]) === null || _p === void 0 ? void 0 : _p.url) !== null && _q !== void 0 ? _q : '', className: `image-background-${section.id}` })) }, `section-background-wrapper-${section.id}`) })), children] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
70
- ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([height, color, media]) => {
71
- var _a;
72
- return (`
52
+ 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: [media && media.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}`, style: Object.assign({ transform: media.position === 'fixed' ? 'translateY(-100vh)' : 'unset' }, (sectionHeight && { height: media.position === 'fixed' ? `calc(${getSectionHeight(sectionHeight)} + 200vh)` : getSectionHeight(sectionHeight) })), children: [media.type === 'video' && ((0, jsx_runtime_1.jsx)(SectionVideo_1.SectionVideo, { container: sectionRef.current, sectionId: section.id, media: media })), media.type === 'image' && ((0, jsx_runtime_1.jsx)(SectionImage_1.SectionImage, { media: media, sectionId: section.id }))] }, `section-background-wrapper-${section.id}`) })), children] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
53
+ ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([height, color, media]) => (`
73
54
  .section-${section.id} {
74
55
  height: ${getSectionHeight(height)};
75
56
  position: relative;
@@ -87,24 +68,7 @@ const Section = ({ section, data, children }) => {
87
68
  height: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? `calc(${getSectionHeight(height)} + 200vh)` : getSectionHeight(height)};
88
69
  width: 100%;
89
70
  }
90
- .video-background-${section.id} {
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
- })}
71
+ `))}
108
72
  ${getSectionVisibilityStyles()}
109
73
  ` })] }));
110
74
  };
@@ -0,0 +1,34 @@
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)("div", { className: `section-image-wrapper-${sectionId}`, 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
+ .section-image-wrapper-${sectionId} {
17
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
18
+ height: ${position === 'fixed' ? '100vh' : '100%'};
19
+ top: ${position === 'fixed' ? '100vh' : '0'};
20
+ width: 100%;
21
+ overflow: hidden;
22
+ }
23
+ .image-background-${sectionId} {
24
+ object-fit: ${isContainHeight ? 'unset' : size !== null && size !== void 0 ? size : 'cover'};
25
+ width: ${isContainHeight || hasOffsetX ? 'auto' : '100%'};
26
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
27
+ position: relative;
28
+ left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
29
+ height: 100%;
30
+ ${offsetX ? 'max-width: 100vw;' : ''}
31
+ }
32
+ ` })] }));
33
+ };
34
+ exports.SectionImage = SectionImage;
@@ -0,0 +1,84 @@
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 [userPaused, setUserPaused] = (0, react_1.useState)(false);
16
+ const [isClickedOnCover, setIsClickedOnCover] = (0, react_1.useState)(false);
17
+ const handleCoverClick = () => {
18
+ if (!video || play !== 'on-click')
19
+ return;
20
+ setIsClickedOnCover(true);
21
+ if (isPlaying) {
22
+ video.pause();
23
+ setUserPaused(true);
24
+ }
25
+ else {
26
+ video.play();
27
+ setUserPaused(false);
28
+ }
29
+ };
30
+ (0, react_1.useEffect)(() => {
31
+ if (!video || play !== 'on-click')
32
+ return;
33
+ const observer = new IntersectionObserver(([entry]) => {
34
+ if (userPaused || !isClickedOnCover)
35
+ return;
36
+ if (entry.isIntersecting) {
37
+ video.play();
38
+ }
39
+ else {
40
+ video.pause();
41
+ }
42
+ });
43
+ observer.observe(container);
44
+ return () => observer.disconnect();
45
+ }, [container, play, userPaused, isClickedOnCover]);
46
+ const isContainHeight = size === 'contain-height';
47
+ const hasOffsetX = offsetX !== null && size === 'contain';
48
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: `section-video-wrapper-${sectionId}`, children: [(0, jsx_runtime_1.jsx)("video", { ref: setVideo, autoPlay: play === 'auto', loop: true, style: { opacity: !isPlaying && play === 'on-click' && coverUrl ? 0 : 1 }, 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: `
49
+ .section-video-wrapper-${sectionId} {
50
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
51
+ height: ${position === 'fixed' ? '100vh' : '100%'};
52
+ top: ${position === 'fixed' ? '100vh' : '0'};
53
+ width: 100%;
54
+ overflow: hidden;
55
+ }
56
+ .video-background-${sectionId}-cover-container {
57
+ position: absolute;
58
+ pointer-events: ${play === 'on-click' ? 'auto' : 'none'};
59
+ left: 0;
60
+ width: 100%;
61
+ height: 100%;
62
+ top: 0;
63
+ }
64
+ .video-background-${sectionId}-cover {
65
+ position: relative;
66
+ left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
67
+ width: ${isContainHeight ? 'auto' : '100%'};
68
+ height: 100%;
69
+ object-fit: ${isContainHeight ? 'unset' : size !== null && size !== void 0 ? size : 'cover'};
70
+ transition: opacity 0.1s ease-in-out;
71
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
72
+ ${hasOffsetX ? 'max-width: 100vw;' : ''}
73
+ }
74
+ .video-background-${sectionId} {
75
+ object-fit: ${isContainHeight ? 'unset' : size !== null && size !== void 0 ? size : 'cover'};
76
+ width: ${isContainHeight ? 'auto' : '100%'};
77
+ height: 100%;
78
+ position: relative;
79
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
80
+ left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
81
+ }
82
+ ` })] }));
83
+ };
84
+ exports.SectionVideo = SectionVideo;
@@ -20,7 +20,6 @@ const getStyleFromItemStateAndParams_1 = require("../../../utils/getStyleFromIte
20
20
  const useVideoFx_1 = require("../../../utils/effects/useVideoFx");
21
21
  const useElementRect_1 = require("../../../utils/useElementRect");
22
22
  const useItemFXData_1 = require("../../../common/useItemFXData");
23
- const PlayIcon_1 = require("./PlayIcon");
24
23
  const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
25
24
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
26
25
  const id = (0, react_1.useId)();
@@ -31,8 +30,6 @@ const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
31
30
  const [ref, setRef] = (0, react_1.useState)(null);
32
31
  const videoRef = (0, react_1.useRef)(null);
33
32
  const fxCanvas = (0, react_1.useRef)(null);
34
- const [isPlaying, setIsPlaying] = (0, react_1.useState)(false);
35
- const [isEventTriggered, setIsEventTriggered] = (0, react_1.useState)(false);
36
33
  const { url, hasGLEffect } = item.commonParams;
37
34
  const isInitialRef = (0, react_1.useRef)(true);
38
35
  const area = layoutId ? item.area[layoutId] : null;
@@ -72,45 +69,11 @@ const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
72
69
  (0, react_1.useEffect)(() => {
73
70
  onVisibilityChange === null || onVisibilityChange === void 0 ? void 0 : onVisibilityChange(isInteractive);
74
71
  }, [isInteractive, onVisibilityChange]);
75
- const playVideo = () => {
76
- if (videoRef.current) {
77
- videoRef.current.play();
78
- setIsEventTriggered(true);
79
- }
80
- };
81
- (0, react_1.useEffect)(() => {
82
- if (!isEventTriggered || !ref)
83
- return;
84
- const intersectionObserver = new IntersectionObserver((entries) => {
85
- entries.forEach((entry) => {
86
- if (!videoRef.current)
87
- return;
88
- if (entry.isIntersecting) {
89
- videoRef.current.play();
90
- }
91
- else {
92
- videoRef.current.pause();
93
- }
94
- });
95
- });
96
- intersectionObserver.observe(ref);
97
- return () => {
98
- intersectionObserver.disconnect();
99
- };
100
- }, [isEventTriggered, ref]);
101
- return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: (_l = item.link) === null || _l === void 0 ? void 0 : _l.url, target: (_m = item.link) === null || _m === void 0 ? void 0 : _m.target, children: [(0, jsx_runtime_1.jsxs)("div", { className: `video-wrapper-${item.id}`, ref: setRef, style: Object.assign(Object.assign(Object.assign(Object.assign({}, (opacity !== undefined ? { opacity } : {})), (angle !== undefined ? { transform: `rotate(${angle}deg) translateZ(0)` } : {})), (blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {})), { transition: (_o = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.transition) !== null && _o !== void 0 ? _o : 'none' }), children: [!isPlaying && ((0, jsx_runtime_1.jsx)("div", { className: `play-btn-wrapper-${item.id}`, children: (0, jsx_runtime_1.jsx)(PlayIcon_1.PlayIcon, { className: `play-btn-${item.id}`, onClick: playVideo }) })), hasScrollPlayback
102
- ? ((0, jsx_runtime_1.jsx)(ScrollPlaybackVideo_1.ScrollPlaybackVideo, { sectionId: sectionId, src: item.commonParams.url, playbackParams: scrollPlayback, style: inlineStyles, className: `video video-playback-wrapper video-${item.id}` }))
103
- : ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: hasGLEffect && isFXAllowed
104
- ? ((0, jsx_runtime_1.jsx)("canvas", { style: inlineStyles, ref: fxCanvas, className: `video-canvas video-${item.id}`, width: rectWidth, height: rectHeight }))
105
- : ((0, jsx_runtime_1.jsx)("video", { poster: (_p = item.commonParams.coverUrl) !== null && _p !== void 0 ? _p : '', ref: videoRef, controls: true, onPlay: () => {
106
- setIsPlaying(true);
107
- console.log('play');
108
- }, onPause: () => {
109
- setIsPlaying(false);
110
- console.log('pause');
111
- }, onEnded: () => {
112
- console.log('ended');
113
- }, loop: true, playsInline: true, className: `video video-${item.id}`, style: inlineStyles, children: (0, jsx_runtime_1.jsx)("source", { src: item.commonParams.url }) })) }))] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
72
+ return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: (_l = item.link) === null || _l === void 0 ? void 0 : _l.url, target: (_m = item.link) === null || _m === void 0 ? void 0 : _m.target, children: [(0, jsx_runtime_1.jsx)("div", { className: `video-wrapper-${item.id}`, ref: setRef, style: Object.assign(Object.assign(Object.assign(Object.assign({}, (opacity !== undefined ? { opacity } : {})), (angle !== undefined ? { transform: `rotate(${angle}deg) translateZ(0)` } : {})), (blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {})), { transition: (_o = wrapperStateParams === null || wrapperStateParams === void 0 ? void 0 : wrapperStateParams.transition) !== null && _o !== void 0 ? _o : 'none' }), children: hasScrollPlayback
73
+ ? ((0, jsx_runtime_1.jsx)(ScrollPlaybackVideo_1.ScrollPlaybackVideo, { sectionId: sectionId, src: item.commonParams.url, playbackParams: scrollPlayback, style: inlineStyles, className: `video video-playback-wrapper video-${item.id}` }))
74
+ : ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: hasGLEffect && isFXAllowed
75
+ ? ((0, jsx_runtime_1.jsx)("canvas", { style: inlineStyles, ref: fxCanvas, className: `video-canvas video-${item.id}`, width: rectWidth, height: rectHeight }))
76
+ : ((0, jsx_runtime_1.jsx)("video", { poster: (_p = item.commonParams.coverUrl) !== null && _p !== void 0 ? _p : '', ref: videoRef, autoPlay: true, muted: true, loop: true, playsInline: true, className: `video video-${item.id}`, style: inlineStyles, children: (0, jsx_runtime_1.jsx)("source", { src: item.commonParams.url }) })) })) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
114
77
  .video-wrapper-${item.id} {
115
78
  position: absolute;
116
79
  overflow: hidden;
@@ -119,23 +82,6 @@ const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
119
82
  box-sizing: border-box;
120
83
  opacity: ${opacity};
121
84
  }
122
- .play-btn-wrapper-${item.id} {
123
- position: absolute;
124
- top: 0;
125
- left: 0;
126
- width: 100%;
127
- display: flex;
128
- justify-content: center;
129
- align-items: center;
130
- height: 100%;
131
- }
132
- .play-btn-${item.id} {
133
- width: 100px;
134
- position: relative;
135
- z-index: 1;
136
- pointer-events: auto;
137
- height: 100px;
138
- }
139
85
  .video {
140
86
  width: 100%;
141
87
  height: 100%;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isOverflowClipSupported = void 0;
4
+ function isOverflowClipSupported() {
5
+ if (typeof window === 'undefined')
6
+ return false;
7
+ if (typeof CSS !== 'undefined' && CSS.supports) {
8
+ return CSS.supports('overflow', 'clip');
9
+ }
10
+ try {
11
+ const testElement = document.createElement('div');
12
+ testElement.style.overflow = 'clip';
13
+ return testElement.style.overflow === 'clip';
14
+ }
15
+ catch (_a) {
16
+ return false;
17
+ }
18
+ }
19
+ exports.isOverflowClipSupported = isOverflowClipSupported;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "1.8.19",
3
+ "version": "1.8.20",
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.16",
34
+ "@cntrl-site/sdk": "^1.22.18",
35
35
  "@types/vimeo__player": "^2.18.0",
36
36
  "@vimeo/player": "^2.25.0",
37
37
  "html-react-parser": "^3.0.1",
@@ -4,7 +4,7 @@ import { LayoutContext } from '../provider/LayoutContext';
4
4
 
5
5
  export const ArticleWrapper: FC<PropsWithChildren<{}>> = ({ children }) => {
6
6
  const { layoutId, layoutDeviation } = useCurrentLayout();
7
- const layoutDeviationStyle = {'--layout-deviation': layoutDeviation} as CSSProperties;
7
+ const layoutDeviationStyle = { '--layout-deviation': layoutDeviation } as CSSProperties;
8
8
 
9
9
  return (
10
10
  <LayoutContext.Provider value={layoutId}>
@@ -1,4 +1,4 @@
1
- import { FC, ReactElement, useEffect, useId, useRef, useState, useContext } from 'react';
1
+ import React, { FC, ReactElement, useId, useRef, useMemo } from 'react';
2
2
  import JSXStyle from 'styled-jsx/style';
3
3
  import {
4
4
  getLayoutMediaQuery,
@@ -11,6 +11,9 @@ 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';
16
+ import { isOverflowClipSupported } from '../../utils/checkOverflowClipSupport';
14
17
 
15
18
  type SectionChild = ReactElement<any, any>;
16
19
  const DEFAULT_COLOR = 'rgba(0, 0, 0, 0)';
@@ -21,11 +24,6 @@ interface Props {
21
24
  data?: any;
22
25
  }
23
26
 
24
- function isVideoUrl(url: string): boolean {
25
- const videoExtensions = ['.mp4', '.webm', '.ogg'];
26
- return videoExtensions.some(ext => url.toLowerCase().endsWith(ext));
27
- }
28
-
29
27
  export const Section: FC<Props> = ({ section, data, children }) => {
30
28
  const id = useId();
31
29
  const sectionRef = useRef<HTMLDivElement | null>(null);
@@ -33,9 +31,19 @@ export const Section: FC<Props> = ({ section, data, children }) => {
33
31
  const layout = useLayoutContext();
34
32
  const layoutValues: Record<string, any>[] = [section.height, section.color, section.media ?? {}];
35
33
  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
34
  useSectionRegistry(section.id, sectionRef.current);
35
+ const sectionHeight = layout && section.height[layout] ? section.height[layout] : undefined;
36
+ const layoutMedia = layout && section.media && section.media[layout] ? section.media[layout] : undefined;
37
+
38
+ const media = useMemo(() => {
39
+ if (layoutMedia && !isOverflowClipSupported()) {
40
+ return {
41
+ ...layoutMedia,
42
+ position: 'local'
43
+ };
44
+ }
45
+ return layoutMedia;
46
+ }, [layoutMedia]);
39
47
 
40
48
  const getSectionVisibilityStyles = () => {
41
49
  return layouts
@@ -52,30 +60,6 @@ export const Section: FC<Props> = ({ section, data, children }) => {
52
60
  }, '');
53
61
  };
54
62
 
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
63
  if (SectionComponent) return <div ref={sectionRef}><SectionComponent data={data}>{children}</SectionComponent></div>;
80
64
 
81
65
  return (
@@ -85,26 +69,21 @@ export const Section: FC<Props> = ({ section, data, children }) => {
85
69
  id={section.name}
86
70
  ref={sectionRef}
87
71
  >
88
- {layout && section.media?.[layout]?.url && section.media?.[layout]?.size !== 'none' && sectionRef.current && (
72
+ {media && media.size !== 'none' && sectionRef.current && (
89
73
  <div className={`section-background-overlay-${section.id}`}>
90
74
  <div
91
75
  key={`section-background-wrapper-${section.id}`}
92
76
  className={`section-background-wrapper-${section.id}`}
77
+ style={{
78
+ transform: media.position === 'fixed' ? 'translateY(-100vh)' : 'unset',
79
+ ...(sectionHeight && { height: media.position === 'fixed' ? `calc(${getSectionHeight(sectionHeight)} + 200vh)` : getSectionHeight(sectionHeight) })
80
+ }}
93
81
  >
94
- {isVideo ? (
95
- <video
96
- ref={videoRef}
97
- autoPlay
98
- loop
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}`} />
82
+ {media.type === 'video' && (
83
+ <SectionVideo container={sectionRef.current} sectionId={section.id} media={media} />
84
+ )}
85
+ {media.type === 'image' && (
86
+ <SectionImage media={media} sectionId={section.id} />
108
87
  )}
109
88
  </div>
110
89
  </div>
@@ -131,22 +110,6 @@ export const Section: FC<Props> = ({ section, data, children }) => {
131
110
  height: ${media?.position === 'fixed' ? `calc(${getSectionHeight(height)} + 200vh)` : getSectionHeight(height)};
132
111
  width: 100%;
133
112
  }
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
113
  `
151
114
  ))
152
115
  }
@@ -0,0 +1,48 @@
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
+ <div className={`section-image-wrapper-${sectionId}`}>
25
+ <img src={url} className={`image-background-${sectionId}`} />
26
+ </div>
27
+ <JSXStyle id={id}>{`
28
+ .section-image-wrapper-${sectionId} {
29
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
30
+ height: ${position === 'fixed' ? '100vh' : '100%'};
31
+ top: ${position === 'fixed' ? '100vh' : '0'};
32
+ width: 100%;
33
+ overflow: hidden;
34
+ }
35
+ .image-background-${sectionId} {
36
+ object-fit: ${isContainHeight ? 'unset' : size ?? 'cover'};
37
+ width: ${isContainHeight || hasOffsetX ? 'auto' : '100%'};
38
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
39
+ position: relative;
40
+ left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
41
+ height: 100%;
42
+ ${offsetX ? 'max-width: 100vw;' : ''}
43
+ }
44
+ `}
45
+ </JSXStyle>
46
+ </>
47
+ );
48
+ };
@@ -0,0 +1,121 @@
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
+ const [userPaused, setUserPaused] = useState(false);
26
+ const [isClickedOnCover, setIsClickedOnCover] = useState(false);
27
+
28
+ const handleCoverClick = () => {
29
+ if (!video || play !== 'on-click') return;
30
+ setIsClickedOnCover(true);
31
+ if (isPlaying) {
32
+ video.pause();
33
+ setUserPaused(true);
34
+ } else {
35
+ video.play();
36
+ setUserPaused(false);
37
+ }
38
+ };
39
+
40
+ useEffect(() => {
41
+ if (!video || play !== 'on-click') return;
42
+ const observer = new IntersectionObserver(
43
+ ([entry]) => {
44
+ if (userPaused || !isClickedOnCover) return;
45
+ if (entry.isIntersecting) {
46
+ video.play();
47
+ } else {
48
+ video.pause();
49
+ }
50
+ }
51
+ );
52
+ observer.observe(container);
53
+ return () => observer.disconnect();
54
+ }, [container, play, userPaused, isClickedOnCover]);
55
+
56
+ const isContainHeight = size === 'contain-height';
57
+ const hasOffsetX = offsetX !== null && size === 'contain';
58
+
59
+ return (
60
+ <>
61
+ <div className={`section-video-wrapper-${sectionId}`}>
62
+ <video
63
+ ref={setVideo}
64
+ autoPlay={play === 'auto'}
65
+ loop
66
+ style={{ opacity: !isPlaying && play === 'on-click' && coverUrl ? 0 : 1 }}
67
+ controls={false}
68
+ muted={play === 'auto'}
69
+ playsInline
70
+ preload="auto"
71
+ className={`video-background-${sectionId}`}
72
+ onPlay={() => setIsPlaying(true)}
73
+ onPause={() => setIsPlaying(false)}
74
+ >
75
+ <source src={`${url}#t=0.001`} />
76
+ </video>
77
+ <div className={`video-background-${sectionId}-cover-container`} onClick={handleCoverClick}>
78
+ {coverUrl && play === 'on-click' && (
79
+ <img src={coverUrl} alt="Video cover" className={`video-background-${sectionId}-cover`} style={{ opacity: isPlaying ? 0 : 1 }} />
80
+ )}
81
+ </div>
82
+ </div>
83
+ <JSXStyle id={id}>{`
84
+ .section-video-wrapper-${sectionId} {
85
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
86
+ height: ${position === 'fixed' ? '100vh' : '100%'};
87
+ top: ${position === 'fixed' ? '100vh' : '0'};
88
+ width: 100%;
89
+ overflow: hidden;
90
+ }
91
+ .video-background-${sectionId}-cover-container {
92
+ position: absolute;
93
+ pointer-events: ${play === 'on-click' ? 'auto' : 'none'};
94
+ left: 0;
95
+ width: 100%;
96
+ height: 100%;
97
+ top: 0;
98
+ }
99
+ .video-background-${sectionId}-cover {
100
+ position: relative;
101
+ left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
102
+ width: ${isContainHeight ? 'auto' : '100%'};
103
+ height: 100%;
104
+ object-fit: ${isContainHeight ? 'unset' : size ?? 'cover'};
105
+ transition: opacity 0.1s ease-in-out;
106
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
107
+ ${hasOffsetX ? 'max-width: 100vw;' : ''}
108
+ }
109
+ .video-background-${sectionId} {
110
+ object-fit: ${isContainHeight ? 'unset' : size ?? 'cover'};
111
+ width: ${isContainHeight ? 'auto' : '100%'};
112
+ height: 100%;
113
+ position: relative;
114
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
115
+ left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
116
+ }
117
+ `}
118
+ </JSXStyle>
119
+ </>
120
+ );
121
+ };
@@ -14,7 +14,6 @@ import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemS
14
14
  import { useVideoFx } from '../../../utils/effects/useVideoFx';
15
15
  import { useElementRect } from '../../../utils/useElementRect';
16
16
  import { useItemFXData } from '../../../common/useItemFXData';
17
- import { PlayIcon } from './PlayIcon';
18
17
 
19
18
  export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
20
19
  const id = useId();
@@ -31,8 +30,6 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
31
30
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
32
31
  const videoRef = useRef<HTMLVideoElement | null>(null);
33
32
  const fxCanvas = useRef<HTMLCanvasElement | null>(null);
34
- const [isPlaying, setIsPlaying] = useState(false);
35
- const [isEventTriggered, setIsEventTriggered] = useState(false);
36
33
  const { url, hasGLEffect } = item.commonParams;
37
34
  const isInitialRef = useRef(true);
38
35
  const area = layoutId ? item.area[layoutId] : null;
@@ -83,31 +80,6 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
83
80
  onVisibilityChange?.(isInteractive);
84
81
  }, [isInteractive, onVisibilityChange]);
85
82
 
86
- const playVideo = () => {
87
- if (videoRef.current) {
88
- videoRef.current.play();
89
- setIsEventTriggered(true);
90
- }
91
- };
92
-
93
- useEffect(() => {
94
- if (!isEventTriggered || !ref) return;
95
- const intersectionObserver = new IntersectionObserver((entries) => {
96
- entries.forEach((entry) => {
97
- if (!videoRef.current) return;
98
- if (entry.isIntersecting) {
99
- videoRef.current.play();
100
- } else {
101
- videoRef.current.pause();
102
- }
103
- });
104
- });
105
- intersectionObserver.observe(ref);
106
- return () => {
107
- intersectionObserver.disconnect();
108
- };
109
- }, [isEventTriggered, ref]);
110
-
111
83
  return (
112
84
  <LinkWrapper url={item.link?.url} target={item.link?.target}>
113
85
  <div
@@ -120,11 +92,6 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
120
92
  transition: wrapperStateParams?.transition ?? 'none'
121
93
  }}
122
94
  >
123
- {!isPlaying && (
124
- <div className={`play-btn-wrapper-${item.id}`}>
125
- <PlayIcon className={`play-btn-${item.id}`} onClick={playVideo} />
126
- </div>
127
- )}
128
95
  {hasScrollPlayback
129
96
  ? (
130
97
  <ScrollPlaybackVideo
@@ -151,18 +118,8 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
151
118
  <video
152
119
  poster={item.commonParams.coverUrl ?? ''}
153
120
  ref={videoRef}
154
- controls={true}
155
- onPlay={() => {
156
- setIsPlaying(true);
157
- console.log('play');
158
- }}
159
- onPause={() => {
160
- setIsPlaying(false);
161
- console.log('pause');
162
- }}
163
- onEnded={() => {
164
- console.log('ended');
165
- }}
121
+ autoPlay
122
+ muted
166
123
  loop
167
124
  playsInline
168
125
  className={`video video-${item.id}`}
@@ -184,23 +141,6 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
184
141
  box-sizing: border-box;
185
142
  opacity: ${opacity};
186
143
  }
187
- .play-btn-wrapper-${item.id} {
188
- position: absolute;
189
- top: 0;
190
- left: 0;
191
- width: 100%;
192
- display: flex;
193
- justify-content: center;
194
- align-items: center;
195
- height: 100%;
196
- }
197
- .play-btn-${item.id} {
198
- width: 100px;
199
- position: relative;
200
- z-index: 1;
201
- pointer-events: auto;
202
- height: 100px;
203
- }
204
144
  .video {
205
145
  width: 100%;
206
146
  height: 100%;
@@ -0,0 +1,13 @@
1
+ export function isOverflowClipSupported(): boolean {
2
+ if (typeof window === 'undefined') return false;
3
+ if (typeof CSS !== 'undefined' && CSS.supports) {
4
+ return CSS.supports('overflow', 'clip');
5
+ }
6
+ try {
7
+ const testElement = document.createElement('div');
8
+ testElement.style.overflow = 'clip';
9
+ return testElement.style.overflow === 'clip';
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
Binary file
@@ -1,8 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PlayIcon = void 0;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- const PlayIcon = ({ className, onClick }) => {
6
- return ((0, jsx_runtime_1.jsx)("svg", { height: "32px", version: "1.1", viewBox: "0 0 32 32", width: "32px", className: className, onClick: onClick, children: (0, jsx_runtime_1.jsx)("g", { id: "play_x5F_alt", children: (0, jsx_runtime_1.jsx)("path", { fill: "#4E4E50", d: "M16,0C7.164,0,0,7.164,0,16s7.164,16,16,16s16-7.164,16-16S24.836,0,16,0z M10,24V8l16.008,8L10,24z " }) }) }));
7
- };
8
- exports.PlayIcon = PlayIcon;
@@ -1,17 +0,0 @@
1
- import { FC } from 'react';
2
-
3
- interface Props {
4
- className?: string;
5
- onClick?: () => void;
6
- }
7
-
8
- export const PlayIcon: FC<Props> = ({ className, onClick }) => {
9
- return (
10
- <svg height="32px" version="1.1" viewBox="0 0 32 32" width="32px" className={className} onClick={onClick}>
11
- <g id="play_x5F_alt">
12
- <path fill="#4E4E50" d="M16,0C7.164,0,0,7.164,0,16s7.164,16,16,16s16-7.164,16-16S24.836,0,16,0z M10,24V8l16.008,8L10,24z " />
13
- </g>
14
- </svg>
15
- );
16
- };
17
-