@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.
Files changed (30) hide show
  1. package/lib/components/Item.js +15 -4
  2. package/lib/components/items/CustomItem.js +18 -1
  3. package/lib/components/items/ImageItem.js +13 -0
  4. package/lib/components/items/RectangleItem.js +13 -0
  5. package/lib/components/items/RichTextItem.js +16 -2
  6. package/lib/components/items/VideoItem.js +13 -0
  7. package/lib/components/items/VimeoEmbed.js +42 -17
  8. package/lib/components/items/YoutubeEmbed.js +58 -13
  9. package/lib/components/useItemPosition.js +3 -17
  10. package/lib/utils/HoverStyles/HoverStyles.js +67 -0
  11. package/lib/utils/Youtube/YouTubeIframeApiLoader.js +53 -0
  12. package/lib/utils/Youtube/YoutubeIframeApi.js +12 -0
  13. package/lib/utils/Youtube/useYouTubeIframeApi.js +14 -0
  14. package/lib/utils/getItemTopStyle.js +18 -0
  15. package/package.json +4 -2
  16. package/src/components/Item.tsx +15 -5
  17. package/src/components/items/CustomItem.tsx +20 -2
  18. package/src/components/items/ImageItem.tsx +16 -2
  19. package/src/components/items/RectangleItem.tsx +14 -1
  20. package/src/components/items/RichTextItem.tsx +18 -4
  21. package/src/components/items/VideoItem.tsx +14 -1
  22. package/src/components/items/VimeoEmbed.tsx +58 -31
  23. package/src/components/items/YoutubeEmbed.tsx +59 -21
  24. package/src/components/useItemPosition.ts +1 -13
  25. package/src/utils/HoverStyles/HoverStyles.ts +74 -0
  26. package/src/utils/Youtube/YouTubeIframeApiLoader.ts +58 -0
  27. package/src/utils/Youtube/YoutubeIframeApi.ts +101 -0
  28. package/src/utils/Youtube/useYouTubeIframeApi.ts +12 -0
  29. package/src/utils/getItemTopStyle.ts +14 -0
  30. package/.idea/inspectionProfiles/Project_Default.xml +0 -15
@@ -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 { getItemTopStyle, useItemPosition } from './useItemPosition';
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'};xw
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 component({});
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 className={`image-wrapper-${item.id}`}
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={item.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 { autoplay, controls, loop, muted, pictureInPicture, url } = item.commonParams;
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', !controls ? 'true' : String(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
- <div className={`embed-video-wrapper-${item.id}`}
33
- style={{
34
- borderRadius: `${radius * 100}vw`,
35
- transform: `rotate(${angle}deg)`
36
- }}
37
- >
38
- <iframe
39
- className="embedVideo"
40
- src={validUrl || ''}
41
- allow="autoplay; fullscreen; picture-in-picture;"
42
- allowFullScreen
43
- />
44
- </div>
45
- <JSXStyle id={id}>{`
46
- .embed-video-wrapper-${item.id} {
47
- position: absolute;
48
- overflow: hidden;
49
- width: 100%;
50
- height: 100%;
51
- }
52
- .embedVideo {
53
- width: 100%;
54
- height: 100%;
55
- z-index: 1;
56
- border: none;
57
- }
58
- `}</JSXStyle>
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 { autoplay, controls, url } = item.commonParams;
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
- const getValidYoutubeUrl = (url: string): string => {
25
+ useEffect(() => {
17
26
  const newUrl = new URL(url);
18
- const id = getYoutubeId(newUrl);
19
- const validUrl = new URL(`https://www.youtube.com/embed/${id}`);
20
- validUrl.searchParams.append('controls', `${ Number(controls) }`);
21
- validUrl.searchParams.append('autoplay', !controls ? 'true' : `${ Number(autoplay) }`);
22
- validUrl.searchParams.append('mute', `${ Number(autoplay) }`);
23
-
24
- return validUrl.href;
25
- }
26
- const validUrl = getValidYoutubeUrl(url);
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
- .embedYoutubeVideo {
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
+ }