@cntrl-site/sdk-nextjs 0.12.9 → 0.14.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/lib/components/Item.js +15 -4
- package/lib/components/items/CustomItem.js +18 -1
- package/lib/components/items/ImageItem.js +13 -0
- package/lib/components/items/RectangleItem.js +13 -0
- package/lib/components/items/RichTextItem.js +16 -2
- package/lib/components/items/VideoItem.js +13 -0
- package/lib/components/items/VimeoEmbed.js +42 -17
- package/lib/components/items/YoutubeEmbed.js +58 -13
- package/lib/components/useItemPosition.js +3 -17
- package/lib/utils/HoverStyles/HoverStyles.js +67 -0
- package/lib/utils/Youtube/YouTubeIframeApiLoader.js +53 -0
- package/lib/utils/Youtube/YoutubeIframeApi.js +12 -0
- package/lib/utils/Youtube/useYouTubeIframeApi.js +14 -0
- package/lib/utils/getItemTopStyle.js +18 -0
- package/package.json +4 -2
- package/src/components/Item.tsx +15 -5
- package/src/components/items/CustomItem.tsx +20 -2
- package/src/components/items/ImageItem.tsx +16 -2
- package/src/components/items/RectangleItem.tsx +14 -1
- package/src/components/items/RichTextItem.tsx +18 -4
- package/src/components/items/VideoItem.tsx +14 -1
- package/src/components/items/VimeoEmbed.tsx +58 -31
- package/src/components/items/YoutubeEmbed.tsx +59 -21
- package/src/components/useItemPosition.ts +1 -13
- package/src/utils/HoverStyles/HoverStyles.ts +74 -0
- package/src/utils/Youtube/YouTubeIframeApiLoader.ts +58 -0
- package/src/utils/Youtube/YoutubeIframeApi.ts +101 -0
- package/src/utils/Youtube/useYouTubeIframeApi.ts +12 -0
- package/src/utils/getItemTopStyle.ts +14 -0
- package/.idea/inspectionProfiles/Project_Default.xml +0 -15
package/src/components/Item.tsx
CHANGED
|
@@ -18,12 +18,14 @@ import { VimeoEmbedItem } from './items/VimeoEmbed';
|
|
|
18
18
|
import { YoutubeEmbedItem } from './items/YoutubeEmbed';
|
|
19
19
|
import { CustomItem } from './items/CustomItem';
|
|
20
20
|
import { useCntrlContext } from '../provider/useCntrlContext';
|
|
21
|
-
import {
|
|
21
|
+
import { useItemPosition } from './useItemPosition';
|
|
22
22
|
import { useItemDimensions } from './useItemDimensions';
|
|
23
23
|
import { useCurrentLayout } from '../common/useCurrentLayout';
|
|
24
24
|
import { useItemScale } from './useItemScale';
|
|
25
25
|
import { ScaleAnchorMap } from '../utils/ScaleAnchorMap';
|
|
26
26
|
import { useSectionHeightData } from './useSectionHeightMap';
|
|
27
|
+
import { getHoverStyles, getTransitions } from '../utils/HoverStyles/HoverStyles';
|
|
28
|
+
import { getItemTopStyle } from '../utils/getItemTopStyle';
|
|
27
29
|
|
|
28
30
|
export interface ItemProps<I extends TArticleItemAny> {
|
|
29
31
|
item: I;
|
|
@@ -52,7 +54,7 @@ export const Item: FC<ItemProps<TArticleItemAny>> = ({ item, sectionId}) => {
|
|
|
52
54
|
const sectionHeight = useSectionHeightData(sectionId);
|
|
53
55
|
const layout = useCurrentLayout();
|
|
54
56
|
const { width, height } = useItemDimensions(item, sectionId);
|
|
55
|
-
const layoutValues: Record<string, any>[] = [item.area, item.hidden];
|
|
57
|
+
const layoutValues: Record<string, any>[] = [item.area, item.hidden, item.state.hover];
|
|
56
58
|
const isInitialRef = useRef(true);
|
|
57
59
|
layoutValues.push(item.sticky);
|
|
58
60
|
layoutValues.push(sectionHeight);
|
|
@@ -100,12 +102,11 @@ export const Item: FC<ItemProps<TArticleItemAny>> = ({ item, sectionId}) => {
|
|
|
100
102
|
<ItemComponent item={item} sectionId={sectionId} onResize={handleItemResize} />
|
|
101
103
|
</div>
|
|
102
104
|
<JSXStyle id={id}>{`
|
|
103
|
-
${getLayoutStyles(layouts, layoutValues, ([area, hidden, sticky, sectionHeight, layoutParams]) => {
|
|
105
|
+
${getLayoutStyles(layouts, layoutValues, ([area, hidden, hoverParams, sticky, sectionHeight, layoutParams]) => {
|
|
104
106
|
const sizingAxis = parseSizing(layoutParams.sizing);
|
|
105
|
-
|
|
106
107
|
return (`
|
|
107
108
|
.item-${item.id} {
|
|
108
|
-
position: ${sticky ? 'sticky' : 'absolute'};
|
|
109
|
+
position: ${sticky ? 'sticky' : 'absolute'};
|
|
109
110
|
width: ${sizingAxis.x === SizingType.Manual ? `${area.width * 100}vw` : 'max-content'};
|
|
110
111
|
height: ${sizingAxis.y === SizingType.Manual ? `w${area.height * 100}vw` : 'unset'};
|
|
111
112
|
transform: scale(${scale});
|
|
@@ -113,7 +114,9 @@ export const Item: FC<ItemProps<TArticleItemAny>> = ({ item, sectionId}) => {
|
|
|
113
114
|
transform-origin: ${ScaleAnchorMap[scaleAnchor]};
|
|
114
115
|
pointer-events: auto;
|
|
115
116
|
--webkit-backface-visibility: hidden;
|
|
117
|
+
cursor: ${hoverParams ? 'pointer' : 'default'};
|
|
116
118
|
visibility: ${hidden ? 'hidden' : 'visible'};
|
|
119
|
+
transition: ${getTransitions(['width', 'height', 'scale'], hoverParams)};
|
|
117
120
|
}
|
|
118
121
|
.item-wrapper-${item.id} {
|
|
119
122
|
position: absolute;
|
|
@@ -124,6 +127,13 @@ export const Item: FC<ItemProps<TArticleItemAny>> = ({ item, sectionId}) => {
|
|
|
124
127
|
top: ${getItemTopStyle(area.top, area.anchorSide)};
|
|
125
128
|
left: ${area.left * 100}vw;
|
|
126
129
|
height: ${sticky ? `${getStickyItemWrapperHeight(sticky, area.height) * 100}vw` : 'unset'};
|
|
130
|
+
transition: ${getTransitions(['left', 'top'], hoverParams)};
|
|
131
|
+
}
|
|
132
|
+
.item-${item.id}:hover {
|
|
133
|
+
${getHoverStyles(['width', 'height', 'scale'], hoverParams)}
|
|
134
|
+
}
|
|
135
|
+
.item-wrapper-${item.id}:hover {
|
|
136
|
+
${getHoverStyles(['left', 'top'], hoverParams, area.anchorSide)}
|
|
127
137
|
}
|
|
128
138
|
`);
|
|
129
139
|
})}
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
-
import { TCustomItem } from '@cntrl-site/sdk';
|
|
1
|
+
import { ArticleItemType, getLayoutStyles, TCustomItem } from '@cntrl-site/sdk';
|
|
2
2
|
import { FC } from 'react';
|
|
3
3
|
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
4
4
|
import { ItemProps } from '../Item';
|
|
5
|
+
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
6
|
+
import JSXStyle from 'styled-jsx/style';
|
|
5
7
|
|
|
6
8
|
export const CustomItem: FC<ItemProps<TCustomItem>> = ({ item }) => {
|
|
7
9
|
const sdk = useCntrlContext();
|
|
10
|
+
const { layouts } = useCntrlContext();
|
|
8
11
|
const component = sdk.customItems.get(item.commonParams.name);
|
|
9
12
|
if (!component) return null;
|
|
10
|
-
return
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<div className={`custom-component-${item.id}`}>{component({})}</div>
|
|
16
|
+
<JSXStyle id={item.id}>
|
|
17
|
+
{`${getLayoutStyles(layouts, [item.state.hover], ([hoverParams]) => {
|
|
18
|
+
return (`
|
|
19
|
+
.custom-component-${item.id} {
|
|
20
|
+
transition: ${getTransitions<ArticleItemType.Custom>(['angle'], hoverParams)};
|
|
21
|
+
}
|
|
22
|
+
.custom-component-${item.id}:hover {
|
|
23
|
+
${getHoverStyles<ArticleItemType.Custom>(['angle'], hoverParams)}
|
|
24
|
+
}
|
|
25
|
+
`);
|
|
26
|
+
})}`}
|
|
27
|
+
</JSXStyle>
|
|
28
|
+
</>);
|
|
11
29
|
};
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { FC, useId, useMemo } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
|
-
import { CntrlColor, TImageItem } from '@cntrl-site/sdk';
|
|
3
|
+
import { ArticleItemType, CntrlColor, getLayoutStyles, TImageItem } from '@cntrl-site/sdk';
|
|
4
4
|
import { ItemProps } from '../Item';
|
|
5
5
|
import { LinkWrapper } from '../LinkWrapper';
|
|
6
6
|
import { useFileItem } from './useFileItem';
|
|
7
7
|
import { useItemAngle } from '../useItemAngle';
|
|
8
|
+
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
9
|
+
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
8
10
|
|
|
9
11
|
export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId }) => {
|
|
10
12
|
const id = useId();
|
|
13
|
+
const { layouts } = useCntrlContext();
|
|
11
14
|
const { radius, strokeWidth, opacity, strokeColor } = useFileItem(item, sectionId);
|
|
12
15
|
const angle = useItemAngle(item, sectionId);
|
|
13
16
|
const borderColor = useMemo(() => CntrlColor.parse(strokeColor), [strokeColor]);
|
|
14
17
|
return (
|
|
15
18
|
<LinkWrapper url={item.link?.url}>
|
|
16
19
|
<>
|
|
17
|
-
<div
|
|
20
|
+
<div
|
|
21
|
+
className={`image-wrapper-${item.id}`}
|
|
18
22
|
style={{
|
|
19
23
|
borderRadius: `${radius * 100}vw`,
|
|
20
24
|
borderWidth: `${strokeWidth * 100}vw`,
|
|
@@ -47,6 +51,16 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId }) => {
|
|
|
47
51
|
object-fit: cover;
|
|
48
52
|
pointer-events: none;
|
|
49
53
|
}
|
|
54
|
+
${getLayoutStyles(layouts, [item.state.hover], ([hoverParams]) => {
|
|
55
|
+
return (`
|
|
56
|
+
.image-wrapper-${item.id} {
|
|
57
|
+
transition: ${getTransitions<ArticleItemType.Image>(['angle', 'strokeWidth', 'radius', 'strokeColor', 'opacity'], hoverParams)};
|
|
58
|
+
}
|
|
59
|
+
.image-wrapper-${item.id}:hover {
|
|
60
|
+
${getHoverStyles<ArticleItemType.Image>(['angle', 'strokeWidth', 'radius', 'strokeColor', 'opacity'], hoverParams)}
|
|
61
|
+
}
|
|
62
|
+
`);
|
|
63
|
+
})}
|
|
50
64
|
`}</JSXStyle>
|
|
51
65
|
</>
|
|
52
66
|
</LinkWrapper>
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { FC, useId, useMemo } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
|
-
import { TRectangleItem, CntrlColor } from '@cntrl-site/sdk';
|
|
3
|
+
import { TRectangleItem, CntrlColor, getLayoutStyles, ArticleItemType } from '@cntrl-site/sdk';
|
|
4
4
|
import { ItemProps } from '../Item';
|
|
5
5
|
import { LinkWrapper } from '../LinkWrapper';
|
|
6
6
|
import { useRectangleItem } from './useRectangleItem';
|
|
7
7
|
import { useItemAngle } from '../useItemAngle';
|
|
8
|
+
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
9
|
+
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
8
10
|
|
|
9
11
|
export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId }) => {
|
|
10
12
|
const id = useId();
|
|
13
|
+
const { layouts } = useCntrlContext();
|
|
11
14
|
const { fillColor, radius, strokeWidth, strokeColor } = useRectangleItem(item, sectionId);
|
|
12
15
|
const angle = useItemAngle(item, sectionId);
|
|
13
16
|
const backgroundColor = useMemo(() => CntrlColor.parse(fillColor), [fillColor]);
|
|
@@ -39,6 +42,16 @@ export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId }
|
|
|
39
42
|
border-style: solid;
|
|
40
43
|
box-sizing: border-box;
|
|
41
44
|
}
|
|
45
|
+
${getLayoutStyles(layouts, [item.state.hover], ([hoverParams]) => {
|
|
46
|
+
return (`
|
|
47
|
+
.rectangle-${item.id} {
|
|
48
|
+
transition: ${getTransitions<ArticleItemType.Rectangle>(['angle', 'fillColor', 'strokeWidth', 'radius', 'strokeColor'], hoverParams)};
|
|
49
|
+
}
|
|
50
|
+
.rectangle-${item.id}:hover {
|
|
51
|
+
${getHoverStyles<ArticleItemType.Rectangle>(['angle', 'fillColor', 'strokeWidth', 'radius', 'strokeColor'], hoverParams)}
|
|
52
|
+
}
|
|
53
|
+
`);
|
|
54
|
+
})}
|
|
42
55
|
`}</JSXStyle>
|
|
43
56
|
</>
|
|
44
57
|
</LinkWrapper>
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { FC, useEffect, useState } from 'react';
|
|
2
|
-
import { TRichTextItem } from '@cntrl-site/sdk';
|
|
1
|
+
import { FC, useEffect, useId, useState } from 'react';
|
|
2
|
+
import { ArticleItemType, getLayoutStyles, TRichTextItem } from '@cntrl-site/sdk';
|
|
3
3
|
import JSXStyle from 'styled-jsx/style';
|
|
4
4
|
import { ItemProps } from '../Item';
|
|
5
5
|
import { useRichTextItem } from './useRichTextItem';
|
|
6
6
|
import { useItemAngle } from '../useItemAngle';
|
|
7
|
+
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
8
|
+
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
7
9
|
|
|
8
10
|
export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, onResize }) => {
|
|
9
11
|
const [content, styles, preset] = useRichTextItem(item);
|
|
12
|
+
const id = useId();
|
|
10
13
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
11
14
|
const angle = useItemAngle(item, sectionId);
|
|
15
|
+
const { layouts } = useCntrlContext();
|
|
12
16
|
const className = preset ? `cntrl-preset-${preset.id}` : undefined;
|
|
13
17
|
|
|
14
18
|
useEffect(() => {
|
|
@@ -27,15 +31,25 @@ export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, on
|
|
|
27
31
|
<>
|
|
28
32
|
<div
|
|
29
33
|
ref={setRef}
|
|
30
|
-
className={className}
|
|
34
|
+
className={`${className} rich-text-wrapper-${item.id}`}
|
|
31
35
|
style={{
|
|
32
36
|
transform: `rotate(${angle}deg)`
|
|
33
37
|
}}
|
|
34
38
|
>
|
|
35
39
|
{content}
|
|
36
40
|
</div>
|
|
37
|
-
<JSXStyle id={
|
|
41
|
+
<JSXStyle id={id}>
|
|
38
42
|
{styles}
|
|
43
|
+
{`${getLayoutStyles(layouts, [item.state.hover], ([hoverParams]) => {
|
|
44
|
+
return (`
|
|
45
|
+
.rich-text-wrapper-${item.id} {
|
|
46
|
+
transition: ${getTransitions<ArticleItemType.RichText>(['angle'], hoverParams)};
|
|
47
|
+
}
|
|
48
|
+
.rich-text-wrapper-${item.id}:hover {
|
|
49
|
+
${getHoverStyles<ArticleItemType.RichText>(['angle'], hoverParams)}
|
|
50
|
+
}
|
|
51
|
+
`);
|
|
52
|
+
})}`}
|
|
39
53
|
</JSXStyle>
|
|
40
54
|
</>
|
|
41
55
|
);
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { FC, useId, useMemo } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
|
-
import { CntrlColor, TVideoItem } from '@cntrl-site/sdk';
|
|
3
|
+
import { ArticleItemType, CntrlColor, getLayoutStyles, TVideoItem } from '@cntrl-site/sdk';
|
|
4
4
|
import { ItemProps } from '../Item';
|
|
5
5
|
import { LinkWrapper } from '../LinkWrapper';
|
|
6
6
|
import { useFileItem } from './useFileItem';
|
|
7
7
|
import { useItemAngle } from '../useItemAngle';
|
|
8
|
+
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
9
|
+
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
8
10
|
|
|
9
11
|
export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId }) => {
|
|
10
12
|
const id = useId();
|
|
13
|
+
const { layouts } = useCntrlContext();
|
|
11
14
|
const { radius, strokeWidth, strokeColor, opacity } = useFileItem(item, sectionId);
|
|
12
15
|
const angle = useItemAngle(item, sectionId);
|
|
13
16
|
const borderColor = useMemo(() => CntrlColor.parse(strokeColor), [strokeColor]);
|
|
@@ -50,6 +53,16 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId }) => {
|
|
|
50
53
|
object-fit: cover;
|
|
51
54
|
pointer-events: none;
|
|
52
55
|
}
|
|
56
|
+
${getLayoutStyles(layouts, [item.state.hover], ([hoverParams]) => {
|
|
57
|
+
return (`
|
|
58
|
+
.video-wrapper-${item.id} {
|
|
59
|
+
transition: ${getTransitions<ArticleItemType.Video>(['angle', 'strokeWidth', 'radius', 'strokeColor', 'opacity'], hoverParams)};
|
|
60
|
+
}
|
|
61
|
+
.video-wrapper-${item.id}:hover {
|
|
62
|
+
${getHoverStyles<ArticleItemType.Video>(['angle', 'strokeWidth', 'radius', 'strokeColor', 'opacity'], hoverParams)}
|
|
63
|
+
}
|
|
64
|
+
`);
|
|
65
|
+
})}
|
|
53
66
|
`}</JSXStyle>
|
|
54
67
|
</LinkWrapper>
|
|
55
68
|
);
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import { FC, useId } from 'react';
|
|
1
|
+
import { FC, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import Player from '@vimeo/player';
|
|
2
3
|
import JSXStyle from 'styled-jsx/style';
|
|
3
4
|
import { TVimeoEmbedItem } from '@cntrl-site/core';
|
|
4
5
|
import { ItemProps } from '../Item';
|
|
5
6
|
import { LinkWrapper } from '../LinkWrapper';
|
|
6
7
|
import { useEmbedVideoItem } from './useEmbedVideoItem';
|
|
7
8
|
import { useItemAngle } from '../useItemAngle';
|
|
9
|
+
import { ArticleItemType, getLayoutStyles } from '@cntrl-site/sdk';
|
|
10
|
+
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
11
|
+
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
12
|
+
import { useCurrentLayout } from '../../common/useCurrentLayout';
|
|
8
13
|
|
|
9
14
|
export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId }) => {
|
|
10
15
|
const id = useId();
|
|
16
|
+
const { layouts } = useCntrlContext();
|
|
11
17
|
const { radius } = useEmbedVideoItem(item, sectionId);
|
|
18
|
+
const [iframeRef, setIframeRef] = useState<HTMLIFrameElement | null>(null);
|
|
19
|
+
const vimeoPlayer = useMemo(() => iframeRef ? new Player(iframeRef) : undefined, [iframeRef]);
|
|
12
20
|
const angle = useItemAngle(item, sectionId);
|
|
13
|
-
const {
|
|
21
|
+
const { play, controls, loop, muted, pictureInPicture, url } = item.commonParams;
|
|
14
22
|
const getValidVimeoUrl = (url: string): string => {
|
|
15
23
|
const validURL = new URL(url);
|
|
16
24
|
validURL.searchParams.append('controls', String(controls));
|
|
17
|
-
validURL.searchParams.append('autoplay',
|
|
18
|
-
validURL.searchParams.append('muted', String(muted));
|
|
25
|
+
validURL.searchParams.append('autoplay', String(play === 'auto'));
|
|
26
|
+
validURL.searchParams.append('muted', String(play !== 'on-click' ? true : muted));
|
|
19
27
|
validURL.searchParams.append('loop', String(loop));
|
|
20
28
|
validURL.searchParams.append('pip', String(pictureInPicture));
|
|
21
29
|
validURL.searchParams.append('title', '0');
|
|
@@ -29,33 +37,52 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
|
|
|
29
37
|
|
|
30
38
|
return (
|
|
31
39
|
<LinkWrapper url={item.link?.url}>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
40
|
+
<div className={`embed-video-wrapper-${item.id}`}
|
|
41
|
+
style={{
|
|
42
|
+
borderRadius: `${radius * 100}vw`,
|
|
43
|
+
transform: `rotate(${angle}deg)`
|
|
44
|
+
}}
|
|
45
|
+
onMouseEnter={() => {
|
|
46
|
+
if (!vimeoPlayer || play !== 'on-hover') return;
|
|
47
|
+
vimeoPlayer.play();
|
|
48
|
+
}}
|
|
49
|
+
onMouseLeave={() =>{
|
|
50
|
+
if (!vimeoPlayer || play !== 'on-hover') return;
|
|
51
|
+
vimeoPlayer.pause();
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<iframe
|
|
55
|
+
ref={setIframeRef}
|
|
56
|
+
className="embedVideo"
|
|
57
|
+
src={validUrl || ''}
|
|
58
|
+
allow="autoplay; fullscreen; picture-in-picture;"
|
|
59
|
+
allowFullScreen
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<JSXStyle id={id}>{`
|
|
63
|
+
.embed-video-wrapper-${item.id} {
|
|
64
|
+
position: absolute;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
}
|
|
69
|
+
.embedVideo {
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: 100%;
|
|
72
|
+
z-index: 1;
|
|
73
|
+
border: none;
|
|
74
|
+
}
|
|
75
|
+
${getLayoutStyles(layouts, [item.state.hover], ([hoverParams]) => {
|
|
76
|
+
return (`
|
|
77
|
+
.embed-video-wrapper-${item.id} {
|
|
78
|
+
transition: ${getTransitions<ArticleItemType.VimeoEmbed>(['angle', 'radius'], hoverParams)};
|
|
79
|
+
}
|
|
80
|
+
.embed-video-wrapper-${item.id}:hover {
|
|
81
|
+
${getHoverStyles<ArticleItemType.VimeoEmbed>(['angle', 'radius'], hoverParams)}
|
|
82
|
+
}
|
|
83
|
+
`);
|
|
84
|
+
})}
|
|
85
|
+
`}</JSXStyle>
|
|
59
86
|
</LinkWrapper>
|
|
60
87
|
);
|
|
61
88
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FC, useId } from 'react';
|
|
1
|
+
import { FC, useEffect, useId, useState } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
3
|
import { TYoutubeEmbedItem } from '@cntrl-site/core';
|
|
4
4
|
import { ItemProps } from '../Item';
|
|
@@ -6,40 +6,68 @@ import { LinkWrapper } from '../LinkWrapper';
|
|
|
6
6
|
import { getYoutubeId } from '../../utils/getValidYoutubeUrl';
|
|
7
7
|
import { useEmbedVideoItem } from './useEmbedVideoItem';
|
|
8
8
|
import { useItemAngle } from '../useItemAngle';
|
|
9
|
+
import { ArticleItemType, getLayoutStyles } from '@cntrl-site/sdk';
|
|
10
|
+
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
11
|
+
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
12
|
+
import { useYouTubeIframeApi } from '../../utils/Youtube/useYouTubeIframeApi';
|
|
13
|
+
import { YTPlayer } from '../../utils/Youtube/YoutubeIframeApi';
|
|
9
14
|
|
|
10
15
|
export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, sectionId }) => {
|
|
11
16
|
const id = useId();
|
|
12
|
-
const {
|
|
17
|
+
const { layouts } = useCntrlContext();
|
|
18
|
+
const { play, controls, url } = item.commonParams;
|
|
13
19
|
const { radius } = useEmbedVideoItem(item, sectionId);
|
|
14
20
|
const angle = useItemAngle(item, sectionId);
|
|
21
|
+
const YT = useYouTubeIframeApi();
|
|
22
|
+
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
|
23
|
+
const [player, setPlayer] = useState<YTPlayer | undefined>(undefined);
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
useEffect(() => {
|
|
17
26
|
const newUrl = new URL(url);
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const videoId = getYoutubeId(newUrl);
|
|
28
|
+
if (!YT || !videoId || !div) return;
|
|
29
|
+
const divRect = div.getBoundingClientRect();
|
|
30
|
+
const placeholder = document.createElement('div');
|
|
31
|
+
div.appendChild(placeholder);
|
|
32
|
+
const player = new YT.Player(placeholder, {
|
|
33
|
+
videoId,
|
|
34
|
+
playerVars: {
|
|
35
|
+
autoplay: play === 'auto' ? '1' : '0',
|
|
36
|
+
controls: controls ? '1' : '0'
|
|
37
|
+
},
|
|
38
|
+
events: {
|
|
39
|
+
onReady: (event) => {
|
|
40
|
+
setPlayer(event.target);
|
|
41
|
+
if (play !== 'on-click') {
|
|
42
|
+
player.mute();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
setPlayer(undefined);
|
|
49
|
+
player.destroy();
|
|
50
|
+
placeholder.parentElement?.removeChild(placeholder);
|
|
51
|
+
};
|
|
52
|
+
}, [YT, div]);
|
|
27
53
|
|
|
28
54
|
return (
|
|
29
55
|
<LinkWrapper url={item.link?.url}>
|
|
30
56
|
<div className={`embed-youtube-video-wrapper-${item.id}`}
|
|
57
|
+
onMouseEnter={() => {
|
|
58
|
+
if (!player || play !== 'on-hover') return;
|
|
59
|
+
player.playVideo();
|
|
60
|
+
}}
|
|
61
|
+
onMouseLeave={() => {
|
|
62
|
+
if (!player || play !== 'on-hover') return;
|
|
63
|
+
player.pauseVideo();
|
|
64
|
+
}}
|
|
65
|
+
ref={setDiv}
|
|
31
66
|
style={{
|
|
32
67
|
borderRadius: `${radius * 100}vw`,
|
|
33
68
|
transform: `rotate(${angle}deg)`
|
|
34
69
|
}}
|
|
35
|
-
>
|
|
36
|
-
<iframe
|
|
37
|
-
className="embedYoutubeVideo"
|
|
38
|
-
src={validUrl || ''}
|
|
39
|
-
allow="accelerometer; autoplay; allowfullscreen;"
|
|
40
|
-
allowFullScreen
|
|
41
|
-
/>
|
|
42
|
-
</div>
|
|
70
|
+
></div>
|
|
43
71
|
<JSXStyle id={id}>{`
|
|
44
72
|
.embed-youtube-video-wrapper-${item.id} {
|
|
45
73
|
position: absolute;
|
|
@@ -47,12 +75,22 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
|
|
|
47
75
|
width: 100%;
|
|
48
76
|
height: 100%;
|
|
49
77
|
}
|
|
50
|
-
.
|
|
78
|
+
.embed-youtube-video-wrapper-${item.id} iframe {
|
|
51
79
|
width: 100%;
|
|
52
80
|
height: 100%;
|
|
53
81
|
z-index: 1;
|
|
54
82
|
border: none;
|
|
55
83
|
}
|
|
84
|
+
${getLayoutStyles(layouts, [item.state.hover], ([hoverParams]) => {
|
|
85
|
+
return (`
|
|
86
|
+
.embed-youtube-video-wrapper-${item.id} {
|
|
87
|
+
transition: ${getTransitions<ArticleItemType.YoutubeEmbed>(['angle', 'radius'], hoverParams)};
|
|
88
|
+
}
|
|
89
|
+
.embed-youtube-video-wrapper-${item.id}:hover {
|
|
90
|
+
${getHoverStyles<ArticleItemType.YoutubeEmbed>(['angle', 'radius'], hoverParams)}
|
|
91
|
+
}
|
|
92
|
+
`);
|
|
93
|
+
})}
|
|
56
94
|
`}</JSXStyle>
|
|
57
95
|
</LinkWrapper>
|
|
58
96
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnchorSide, TArticleItemAny } from '@cntrl-site/sdk';
|
|
2
2
|
import { useKeyframeValue } from '../common/useKeyframeValue';
|
|
3
3
|
import { useCurrentLayout } from '../common/useCurrentLayout';
|
|
4
|
+
import { getItemTopStyle } from '../utils/getItemTopStyle';
|
|
4
5
|
|
|
5
6
|
export const useItemPosition = (item: TArticleItemAny, sectionId: string) => {
|
|
6
7
|
const layoutId = useCurrentLayout();
|
|
@@ -17,17 +18,4 @@ export const useItemPosition = (item: TArticleItemAny, sectionId: string) => {
|
|
|
17
18
|
};
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
export function getItemTopStyle(top: number, anchorSide?: AnchorSide) {
|
|
21
|
-
const defaultValue = `${top * 100}vw`;
|
|
22
|
-
if (!anchorSide) return defaultValue;
|
|
23
|
-
switch (anchorSide) {
|
|
24
|
-
case AnchorSide.Top:
|
|
25
|
-
return defaultValue;
|
|
26
|
-
case AnchorSide.Center:
|
|
27
|
-
return `calc(50% + ${top * 100}vw)`;
|
|
28
|
-
case AnchorSide.Bottom:
|
|
29
|
-
return `calc(100% + ${top * 100}vw)`;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
21
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { THoverParams, ArticleItemType, CntrlColor, AnchorSide, TItemHoverState } from '@cntrl-site/sdk';
|
|
2
|
+
import { getItemTopStyle } from '../getItemTopStyle';
|
|
3
|
+
|
|
4
|
+
type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends (arg: infer I) => void ? I : never;
|
|
5
|
+
type HoverParamsGetter = (value: any, anchorSide?: AnchorSide) => string;
|
|
6
|
+
type ItemHoverParams = Omit<UnionToIntersection<TItemHoverState>, 'autoplay'>;
|
|
7
|
+
|
|
8
|
+
const hoverTransformationMap: Record<keyof ItemHoverParams, HoverParamsGetter> = {
|
|
9
|
+
'width': (width: number) => `width: ${width * 100}vw !important;`,
|
|
10
|
+
'height': (height: number) => `height: ${height * 100}vw !important;`,
|
|
11
|
+
'top': (top: number, anchorSide?: AnchorSide) => `top: ${getItemTopStyle(top, anchorSide)} !important;`,
|
|
12
|
+
'left': (left: number) => `left: ${left * 100}vw !important;`,
|
|
13
|
+
'scale': (scale: number) => `transform: scale(${scale}) !important;`,
|
|
14
|
+
'angle': (angle: number) => `transform: rotate(${angle}deg) !important;`,
|
|
15
|
+
'opacity': (opacity: number) => `opacity: ${opacity} !important;`,
|
|
16
|
+
'radius': (radius: number) => `border-radius: ${radius * 100}vw !important;`,
|
|
17
|
+
'strokeWidth': (strokeWidth: number) => `border-width: ${strokeWidth * 100}vw !important;`,
|
|
18
|
+
'strokeColor': (strokeColor: string) => `border-color: ${CntrlColor.parse(strokeColor).toCss()} !important;`,
|
|
19
|
+
'fillColor': (fillColor: string) => `background-color: ${CntrlColor.parse(fillColor).toCss()} !important;`,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CSSPropertyNameMap: Record<keyof ItemHoverParams, string> = {
|
|
23
|
+
'width': 'width',
|
|
24
|
+
'height': 'height',
|
|
25
|
+
'top': 'top',
|
|
26
|
+
'left': 'left',
|
|
27
|
+
'scale': 'transform',
|
|
28
|
+
'angle': 'transform',
|
|
29
|
+
'opacity': 'opacity',
|
|
30
|
+
'radius': 'border-radius',
|
|
31
|
+
'strokeWidth': 'border-width',
|
|
32
|
+
'strokeColor': 'border-color',
|
|
33
|
+
'fillColor': 'background-color'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function getTransitions<T extends ArticleItemType>(
|
|
37
|
+
values: Array<keyof ItemHoverParams>,
|
|
38
|
+
hover?: ItemHoverParams
|
|
39
|
+
): string {
|
|
40
|
+
if (!hover) return 'unset';
|
|
41
|
+
const transitionValues = values.reduce<string[]>((acc, valueName) => {
|
|
42
|
+
if (valueName in hover && hover[valueName] !== undefined) {
|
|
43
|
+
const hoverProperties = hover[valueName] as THoverParams<string | number>;
|
|
44
|
+
return [
|
|
45
|
+
...acc,
|
|
46
|
+
`${CSSPropertyNameMap[valueName]} ${hoverProperties!.duration}ms ${hoverProperties!.timing} ${hoverProperties!.delay}ms`
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
return acc;
|
|
50
|
+
}, []);
|
|
51
|
+
if (!transitionValues.length) return 'unset';
|
|
52
|
+
return transitionValues.join(', ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getHoverStyles<T extends ArticleItemType>(
|
|
56
|
+
values: Array<keyof ItemHoverParams>,
|
|
57
|
+
hover?: ItemHoverParams,
|
|
58
|
+
anchorSide?: AnchorSide
|
|
59
|
+
): string {
|
|
60
|
+
if (!hover) return '';
|
|
61
|
+
const hoverValues = values.reduce<string[]>((acc, valueName) => {
|
|
62
|
+
if (valueName in hover && hover[valueName] !== undefined) {
|
|
63
|
+
const hoverProperties = hover[valueName] as THoverParams<string | number>;
|
|
64
|
+
return [
|
|
65
|
+
...acc,
|
|
66
|
+
hoverTransformationMap[valueName](hoverProperties.value, anchorSide)
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
return acc;
|
|
70
|
+
}, []);
|
|
71
|
+
if (!hoverValues.length) return '';
|
|
72
|
+
return hoverValues.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { YT } from './YoutubeIframeApi';
|
|
2
|
+
|
|
3
|
+
const YT_IFRAME_API_URL = 'https://www.youtube.com/iframe_api';
|
|
4
|
+
|
|
5
|
+
export class YouTubeIframeApiLoader {
|
|
6
|
+
private static instance: YouTubeIframeApiLoader | undefined;
|
|
7
|
+
|
|
8
|
+
public static create(): YouTubeIframeApiLoader {
|
|
9
|
+
if (!YouTubeIframeApiLoader.instance) {
|
|
10
|
+
YouTubeIframeApiLoader.instance = new YouTubeIframeApiLoader();
|
|
11
|
+
}
|
|
12
|
+
return YouTubeIframeApiLoader.instance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private loadingPromise: Promise<void>;
|
|
16
|
+
|
|
17
|
+
private constructor() {
|
|
18
|
+
this.loadingPromise = this.loadApi();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getApi(): Promise<YT> {
|
|
22
|
+
await this.loadingPromise;
|
|
23
|
+
if (!window.YT) {
|
|
24
|
+
throw new Error('YouTube IFrame API is not loaded');
|
|
25
|
+
}
|
|
26
|
+
return window.YT;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private loadApi(): Promise<void> {
|
|
30
|
+
return new Promise<void>((resolve, reject) => {
|
|
31
|
+
const script = document.createElement('script');
|
|
32
|
+
script.src = YT_IFRAME_API_URL;
|
|
33
|
+
const loadError = new Error('YouTube IFrame API loading failed');
|
|
34
|
+
captureStackTrace(loadError);
|
|
35
|
+
script.addEventListener('abort', () => {
|
|
36
|
+
reject(loadError);
|
|
37
|
+
});
|
|
38
|
+
script.addEventListener('error', () => {
|
|
39
|
+
reject(loadError);
|
|
40
|
+
});
|
|
41
|
+
window.onYouTubeIframeAPIReady = () => {
|
|
42
|
+
window.onYouTubeIframeAPIReady = undefined;
|
|
43
|
+
resolve();
|
|
44
|
+
};
|
|
45
|
+
document.head.appendChild(script);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function captureStackTrace(error: Error): void {
|
|
51
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
52
|
+
Error.captureStackTrace(error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
throw error;
|
|
57
|
+
} catch (errWithStack) {}
|
|
58
|
+
}
|