@cntrl-site/sdk-nextjs 1.9.14 → 1.9.16
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/ItemGeometry/GenericGeometryController.js +43 -0
- package/lib/ItemGeometry/ItemGeometryContext.js +6 -0
- package/lib/ItemGeometry/ItemGeometryController.js +2 -0
- package/lib/ItemGeometry/ItemGeometryService.js +34 -0
- package/lib/ItemGeometry/RichTextGeometryController.js +135 -0
- package/lib/ItemGeometry/useItemGeometry.js +25 -0
- package/lib/components/Page.js +4 -1
- package/lib/components/items/CodeEmbedItem/CodeEmbedItem.js +2 -0
- package/lib/components/items/ComponentItem/ComponentItem.js +2 -0
- package/lib/components/items/CompoundItem/CompoundItem.js +17 -0
- package/lib/components/items/CustomItem/CustomItem.js +2 -0
- package/lib/components/items/EmbedVideoItem/VimeoEmbed.js +2 -0
- package/lib/components/items/EmbedVideoItem/YoutubeEmbed.js +2 -0
- package/lib/components/items/FileItem/ImageItem.js +2 -0
- package/lib/components/items/FileItem/VideoItem.js +2 -0
- package/lib/components/items/GroupItem/GroupItem.js +17 -0
- package/lib/components/items/Item.js +8 -0
- package/lib/components/items/RectangleItem/RectangleItem.js +2 -0
- package/lib/components/items/RichTextItem/RichTextItem.js +5 -1
- package/lib/components/items/useRichTextItemValues.js +2 -1
- package/lib/interactions/InteractionsRegistry.js +113 -12
- package/lib/interactions/ItemInteractionCtrl.js +1 -1
- package/lib/interactions/getTransition.js +1 -1
- package/lib/provider/InteractionsContext.js +4 -2
- package/lib/utils/CSSNumeric.js +19 -0
- package/lib/utils/RichTextConverter/RichTextConverter.js +4 -4
- package/lib/utils/domDfs.js +15 -0
- package/package.json +2 -2
- package/src/ItemGeometry/GenericGeometryController.ts +59 -0
- package/src/ItemGeometry/ItemGeometryContext.ts +4 -0
- package/src/ItemGeometry/ItemGeometryController.ts +22 -0
- package/src/ItemGeometry/ItemGeometryService.ts +34 -0
- package/src/ItemGeometry/RichTextGeometryController.ts +166 -0
- package/src/ItemGeometry/useItemGeometry.ts +25 -0
- package/src/components/Page.tsx +8 -3
- package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +2 -0
- package/src/components/items/ComponentItem/ComponentItem.tsx +2 -0
- package/src/components/items/CompoundItem/CompoundItem.tsx +18 -1
- package/src/components/items/CustomItem/CustomItem.tsx +2 -0
- package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +2 -0
- package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +2 -0
- package/src/components/items/FileItem/ImageItem.tsx +2 -0
- package/src/components/items/FileItem/VideoItem.tsx +2 -0
- package/src/components/items/GroupItem/GroupItem.tsx +18 -1
- package/src/components/items/Item.tsx +11 -3
- package/src/components/items/RectangleItem/RectangleItem.tsx +3 -1
- package/src/components/items/RichTextItem/RichTextItem.tsx +6 -1
- package/src/components/items/useRichTextItemValues.ts +2 -1
- package/src/interactions/InteractionsRegistry.ts +120 -15
- package/src/interactions/ItemInteractionCtrl.ts +2 -2
- package/src/interactions/getTransition.ts +1 -1
- package/src/interactions/types.ts +2 -2
- package/src/provider/InteractionsContext.tsx +4 -2
- package/src/utils/CSSNumeric.ts +19 -0
- package/src/utils/RichTextConverter/RichTextConverter.tsx +20 -4
- package/src/utils/domDfs.ts +11 -0
- package/cntrl-site-sdk-nextjs-1.9.14-2.tgz +0 -0
- /package/lib/{interactions → ItemGeometry}/CSSPropertyNameMap.js +0 -0
- /package/src/{interactions → ItemGeometry}/CSSPropertyNameMap.ts +0 -0
|
@@ -7,6 +7,7 @@ import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
|
7
7
|
import { useItemAngle } from '../useItemAngle';
|
|
8
8
|
import { LinkWrapper } from '../LinkWrapper';
|
|
9
9
|
import { useCodeEmbedItem } from './useCodeEmbedItem';
|
|
10
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
10
11
|
|
|
11
12
|
const stylesMap = {
|
|
12
13
|
[AreaAnchor.TopLeft]: {},
|
|
@@ -31,6 +32,7 @@ export const CodeEmbedItem: FC<ItemProps<TCodeEmbedItem>> = ({ item, sectionId,
|
|
|
31
32
|
const html = decodeBase64(item.commonParams.html);
|
|
32
33
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
33
34
|
useRegisterResize(ref, onResize);
|
|
35
|
+
useItemGeometry(item.id, ref);
|
|
34
36
|
const pos = stylesMap[anchor];
|
|
35
37
|
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
|
|
36
38
|
const stateParams = interactionCtrl?.getState<number>(['angle', 'blur', 'opacity']);
|
|
@@ -8,6 +8,7 @@ import { useItemAngle } from '../useItemAngle';
|
|
|
8
8
|
import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
9
9
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
10
10
|
import { useLayoutContext } from '../../useLayoutContext';
|
|
11
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
11
12
|
|
|
12
13
|
export const ComponentItem: FC<ItemProps<TComponentItem>> = ({ item, sectionId, onResize, interactionCtrl }) => {
|
|
13
14
|
const sdk = useCntrlContext();
|
|
@@ -19,6 +20,7 @@ export const ComponentItem: FC<ItemProps<TComponentItem>> = ({ item, sectionId,
|
|
|
19
20
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
20
21
|
const { opacity: itemOpacity, blur: itemBlur } = useComponentItem(item, sectionId);
|
|
21
22
|
useRegisterResize(ref, onResize);
|
|
23
|
+
useItemGeometry(item.id, ref);
|
|
22
24
|
const stateParams = interactionCtrl?.getState<number>(['opacity', 'angle', 'blur']);
|
|
23
25
|
const angle = getStyleFromItemStateAndParams(stateParams?.styles?.angle, itemAngle);
|
|
24
26
|
const opacity = getStyleFromItemStateAndParams(stateParams?.styles?.opacity, itemOpacity);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { FC, useEffect, useId, useState } from 'react';
|
|
1
|
+
import React, { FC, useContext, useEffect, useId, useState } from 'react';
|
|
2
2
|
import { useItemAngle } from '../useItemAngle';
|
|
3
3
|
import { useCntrlContext } from '../../../provider/useCntrlContext';
|
|
4
4
|
import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
@@ -9,16 +9,21 @@ import JSXStyle from 'styled-jsx/style';
|
|
|
9
9
|
import { getLayoutStyles, CompoundItem as TCompoundItem } from '@cntrl-site/sdk';
|
|
10
10
|
import { CompoundChild } from './CompoundChild';
|
|
11
11
|
import { useCompoundItem } from './useCompoundItem';
|
|
12
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
13
|
+
import { ItemGeometryContext } from '../../../ItemGeometry/ItemGeometryContext';
|
|
12
14
|
|
|
13
15
|
export const CompoundItem: FC<ItemProps<TCompoundItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
14
16
|
const id = useId();
|
|
15
17
|
const { items } = item;
|
|
18
|
+
const itemIds = items ? items.map(item => item.id) : [];
|
|
16
19
|
const itemAngle = useItemAngle(item, sectionId);
|
|
17
20
|
const { layouts } = useCntrlContext();
|
|
18
21
|
const { opacity: itemOpacity } = useCompoundItem(item, sectionId);
|
|
19
22
|
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
|
|
20
23
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
21
24
|
useRegisterResize(ref, onResize);
|
|
25
|
+
useItemGeometry(item.id, ref);
|
|
26
|
+
const itemGeometryService = useContext(ItemGeometryContext);
|
|
22
27
|
const stateParams = interactionCtrl?.getState<number>(['opacity', 'angle']);
|
|
23
28
|
const angle = getStyleFromItemStateAndParams(stateParams?.styles?.angle, itemAngle);
|
|
24
29
|
const opacity = getStyleFromItemStateAndParams(stateParams?.styles?.opacity, itemOpacity);
|
|
@@ -26,6 +31,18 @@ export const CompoundItem: FC<ItemProps<TCompoundItem>> = ({ item, sectionId, on
|
|
|
26
31
|
useEffect(() => {
|
|
27
32
|
onVisibilityChange?.(isInteractive);
|
|
28
33
|
}, [isInteractive, onVisibilityChange]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// because on first render controllers not yet initialized
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
for (const id of itemIds) {
|
|
38
|
+
const hasItem = itemGeometryService.hasItem(id);
|
|
39
|
+
if (hasItem) {
|
|
40
|
+
const cntrl = itemGeometryService.getControllerById(id);
|
|
41
|
+
cntrl.setParentId(item.id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, 0);
|
|
45
|
+
}, [itemIds, item.id]);
|
|
29
46
|
return (
|
|
30
47
|
<LinkWrapper url={item.link?.url} target={item.link?.target}>
|
|
31
48
|
<>
|
|
@@ -5,6 +5,7 @@ import { ItemProps } from '../Item';
|
|
|
5
5
|
import JSXStyle from 'styled-jsx/style';
|
|
6
6
|
import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
7
7
|
import { useItemAngle } from '../useItemAngle';
|
|
8
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
8
9
|
|
|
9
10
|
export const CustomItem: FC<ItemProps<TCustomItem>> = ({ item, onResize, sectionId, interactionCtrl }) => {
|
|
10
11
|
const sdk = useCntrlContext();
|
|
@@ -14,6 +15,7 @@ export const CustomItem: FC<ItemProps<TCustomItem>> = ({ item, onResize, section
|
|
|
14
15
|
const layoutValues: Record<string, any>[] = [item.area];
|
|
15
16
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
16
17
|
useRegisterResize(ref, onResize);
|
|
18
|
+
useItemGeometry(item.id, ref);
|
|
17
19
|
const stateParams = interactionCtrl?.getState<number>(['angle']);
|
|
18
20
|
const angle = stateParams?.styles?.angle ?? itemAngle;
|
|
19
21
|
if (!component) return null;
|
|
@@ -12,6 +12,7 @@ import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemS
|
|
|
12
12
|
import { useEmbedVideoItem } from './useEmbedVideoItem';
|
|
13
13
|
import { LinkWrapper } from '../LinkWrapper';
|
|
14
14
|
import { useLayoutContext } from '../../useLayoutContext';
|
|
15
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
15
16
|
|
|
16
17
|
export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
17
18
|
const id = useId();
|
|
@@ -38,6 +39,7 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
|
|
|
38
39
|
const opacity = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.opacity, itemOpacity);
|
|
39
40
|
const radius = getStyleFromItemStateAndParams(frameStateParams?.styles?.radius, itemRadius);
|
|
40
41
|
useRegisterResize(ref, onResize);
|
|
42
|
+
useItemGeometry(item.id, ref);
|
|
41
43
|
const validUrl = useMemo(() => {
|
|
42
44
|
if (!layoutParams) return url;
|
|
43
45
|
const validURL = new URL(url);
|
|
@@ -12,6 +12,7 @@ import { YTPlayer } from '../../../utils/Youtube/YoutubeIframeApi';
|
|
|
12
12
|
import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
13
13
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
14
14
|
import { useLayoutContext } from '../../useLayoutContext';
|
|
15
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
15
16
|
|
|
16
17
|
export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
17
18
|
const id = useId();
|
|
@@ -38,6 +39,7 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
|
|
|
38
39
|
onVisibilityChange?.(isInteractive);
|
|
39
40
|
}, [isInteractive, onVisibilityChange]);
|
|
40
41
|
useRegisterResize(div, onResize);
|
|
42
|
+
useItemGeometry(item.id, div);
|
|
41
43
|
useEffect(() => {
|
|
42
44
|
const newUrl = new URL(url);
|
|
43
45
|
const videoId = getYoutubeId(newUrl);
|
|
@@ -13,6 +13,7 @@ import { useLayoutContext } from '../../useLayoutContext';
|
|
|
13
13
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
14
14
|
import { useItemFXData } from '../../../common/useItemFXData';
|
|
15
15
|
import { getFill } from '../../../utils/getFill';
|
|
16
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
16
17
|
|
|
17
18
|
export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
18
19
|
const id = useId();
|
|
@@ -28,6 +29,7 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
|
|
|
28
29
|
const itemAngle = useItemAngle(item, sectionId);
|
|
29
30
|
const [wrapperRef, setWrapperRef] = useState<HTMLDivElement | null>(null);
|
|
30
31
|
useRegisterResize(wrapperRef, onResize);
|
|
32
|
+
useItemGeometry(item.id, wrapperRef);
|
|
31
33
|
const { url, hasGLEffect } = item.commonParams;
|
|
32
34
|
const fxCanvas = useRef<HTMLCanvasElement | null>(null);
|
|
33
35
|
const isInitialRef = useRef(true);
|
|
@@ -14,6 +14,7 @@ import { useVideoFx } from '../../../utils/effects/useVideoFx';
|
|
|
14
14
|
import { useElementRect } from '../../../utils/useElementRect';
|
|
15
15
|
import { useItemFXData } from '../../../common/useItemFXData';
|
|
16
16
|
import { getFill } from '../../../utils/getFill';
|
|
17
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
17
18
|
|
|
18
19
|
export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
19
20
|
const id = useId();
|
|
@@ -74,6 +75,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
74
75
|
height
|
|
75
76
|
);
|
|
76
77
|
useRegisterResize(ref, onResize);
|
|
78
|
+
useItemGeometry(item.id, ref);
|
|
77
79
|
const inlineStyles = {
|
|
78
80
|
...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}),
|
|
79
81
|
...(strokeWidth !== undefined ? {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { FC, useEffect, useId, useState } from 'react';
|
|
1
|
+
import React, { FC, useContext, useEffect, useId, useState } from 'react';
|
|
2
2
|
import { Item, ItemProps } from '../Item';
|
|
3
3
|
import JSXStyle from 'styled-jsx/style';
|
|
4
4
|
import { getLayoutStyles, GroupItem as TGroupItem } from '@cntrl-site/sdk';
|
|
@@ -9,16 +9,21 @@ import { useItemAngle } from '../useItemAngle';
|
|
|
9
9
|
import { useGroupItem } from './useGroupItem';
|
|
10
10
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
11
11
|
import { CompoundChild } from '../CompoundItem/CompoundChild';
|
|
12
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
13
|
+
import { ItemGeometryContext } from '../../../ItemGeometry/ItemGeometryContext';
|
|
12
14
|
|
|
13
15
|
export const GroupItem: FC<ItemProps<TGroupItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange, isInCompound }) => {
|
|
14
16
|
const id = useId();
|
|
15
17
|
const { items } = item;
|
|
18
|
+
const itemIds = items ? items.map(item => item.id) : [];
|
|
16
19
|
const itemAngle = useItemAngle(item, sectionId);
|
|
17
20
|
const { layouts } = useCntrlContext();
|
|
18
21
|
const { opacity: itemOpacity, blur: itemBlur } = useGroupItem(item, sectionId);
|
|
19
22
|
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
|
|
20
23
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
21
24
|
useRegisterResize(ref, onResize);
|
|
25
|
+
useItemGeometry(item.id, ref);
|
|
26
|
+
const itemGeometryService = useContext(ItemGeometryContext);
|
|
22
27
|
const stateParams = interactionCtrl?.getState<number>(['opacity', 'angle', 'blur']);
|
|
23
28
|
const angle = getStyleFromItemStateAndParams(stateParams?.styles?.angle, itemAngle);
|
|
24
29
|
const opacity = getStyleFromItemStateAndParams(stateParams?.styles?.opacity, itemOpacity);
|
|
@@ -27,6 +32,18 @@ export const GroupItem: FC<ItemProps<TGroupItem>> = ({ item, sectionId, onResize
|
|
|
27
32
|
useEffect(() => {
|
|
28
33
|
onVisibilityChange?.(isInteractive);
|
|
29
34
|
}, [isInteractive, onVisibilityChange]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
// because on first render controllers not yet initialized
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
for (const id of itemIds) {
|
|
39
|
+
const hasItem = itemGeometryService.hasItem(id);
|
|
40
|
+
if (hasItem) {
|
|
41
|
+
const cntrl = itemGeometryService.getControllerById(id);
|
|
42
|
+
cntrl.setParentId(item.id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}, 0);
|
|
46
|
+
}, [itemIds, item.id]);
|
|
30
47
|
return (
|
|
31
48
|
<LinkWrapper url={item.link?.url} target={item.link?.target}>
|
|
32
49
|
<>
|
|
@@ -35,6 +35,7 @@ import { parseSizing, useSizing } from './useSizing';
|
|
|
35
35
|
import { useItemPointerEvents } from './useItemPointerEvents';
|
|
36
36
|
import { useItemArea } from './useItemArea';
|
|
37
37
|
import { useDraggable } from './useDraggable';
|
|
38
|
+
import { ItemGeometryContext } from '../../ItemGeometry/ItemGeometryContext';
|
|
38
39
|
|
|
39
40
|
export interface ItemProps<I extends ItemAny> {
|
|
40
41
|
item: I;
|
|
@@ -84,6 +85,13 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
|
|
|
84
85
|
const [isDraggingActive, setIsDraggingActive] = useState(false);
|
|
85
86
|
const wrapperStateProps = interactionCtrl?.getState<number>(['top', 'left']);
|
|
86
87
|
const innerStateProps = interactionCtrl?.getState<number>(['width', 'height', 'scale']);
|
|
88
|
+
const geometryService = useContext(ItemGeometryContext);
|
|
89
|
+
const triggerZone = geometryService.getItemBoundary(item.id);
|
|
90
|
+
const hasItem = geometryService.hasItem(item.id);
|
|
91
|
+
const geometryController = useMemo(() => hasItem ? geometryService.getControllerById(item.id) : undefined, [hasItem]);
|
|
92
|
+
if (geometryController) {
|
|
93
|
+
geometryController.setScale(itemScale!);
|
|
94
|
+
}
|
|
87
95
|
const { width, height, top, left } = useItemArea(item, sectionId, {
|
|
88
96
|
top: wrapperStateProps?.styles?.top as number,
|
|
89
97
|
left: wrapperStateProps?.styles?.left as number,
|
|
@@ -228,8 +236,8 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
|
|
|
228
236
|
}
|
|
229
237
|
.item-${item.id}-inner {
|
|
230
238
|
width: ${sizingAxis.x === 'manual'
|
|
231
|
-
|
|
232
|
-
|
|
239
|
+
? `${area.width * 100}vw`
|
|
240
|
+
: 'max-content'};
|
|
233
241
|
height: ${sizingAxis.y === 'manual' ? `${area.height * 100}vw` : 'unset'};
|
|
234
242
|
transform-origin: ${ScaleAnchorMap[scaleAnchor]};
|
|
235
243
|
transform: scale(${area.scale});
|
|
@@ -245,7 +253,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
|
|
|
245
253
|
left: ${area.left * 100}vw;
|
|
246
254
|
}
|
|
247
255
|
`);
|
|
248
|
-
|
|
256
|
+
})}
|
|
249
257
|
`}
|
|
250
258
|
</JSXStyle>
|
|
251
259
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FC, useEffect, useId, useState } from 'react';
|
|
1
|
+
import { FC, useContext, useEffect, useId, useState } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
3
|
import { RectangleItem as TRectangleItem, getLayoutStyles, FillLayer } from '@cntrl-site/sdk';
|
|
4
4
|
import { CntrlColor } from '@cntrl-site/color';
|
|
@@ -11,6 +11,7 @@ import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
|
11
11
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
12
12
|
import { getFill } from '../../../utils/getFill';
|
|
13
13
|
import { areFillsVisible } from '../../../utils/areFillsVisible/areFillsVisible';
|
|
14
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
14
15
|
|
|
15
16
|
export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
16
17
|
const id = useId();
|
|
@@ -32,6 +33,7 @@ export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId,
|
|
|
32
33
|
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
|
|
33
34
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
34
35
|
useRegisterResize(ref, onResize);
|
|
36
|
+
useItemGeometry(item.id, ref);
|
|
35
37
|
const backdropBlur = getStyleFromItemStateAndParams(styles?.backdropBlur, itemBackdropBlur);
|
|
36
38
|
const radius = getStyleFromItemStateAndParams(styles?.radius, itemRadius);
|
|
37
39
|
const strokeWidth = getStyleFromItemStateAndParams(styles?.strokeWidth, itemStrokeWidth);
|
|
@@ -12,6 +12,8 @@ import { useExemplary } from '../../../common/useExemplary';
|
|
|
12
12
|
import { useItemAngle } from '../useItemAngle';
|
|
13
13
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
14
14
|
import { useCurrentLayout } from '../../../common/useCurrentLayout';
|
|
15
|
+
import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
|
|
16
|
+
import { RichTextGeometryController } from '../../../ItemGeometry/RichTextGeometryController';
|
|
15
17
|
|
|
16
18
|
export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
17
19
|
const id = useId();
|
|
@@ -24,12 +26,15 @@ export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, on
|
|
|
24
26
|
letterSpacing: itemLetterSpacing,
|
|
25
27
|
color: itemColor,
|
|
26
28
|
fontSize,
|
|
27
|
-
lineHeight
|
|
29
|
+
lineHeight,
|
|
30
|
+
xSizing
|
|
28
31
|
} = useRichTextItemValues(item, sectionId);
|
|
29
32
|
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
|
|
30
33
|
const exemplary = useExemplary();
|
|
31
34
|
const { layoutId } = useCurrentLayout();
|
|
32
35
|
useRegisterResize(ref, onResize);
|
|
36
|
+
const geometry = useItemGeometry(item.id, ref, RichTextGeometryController, { xSizing });
|
|
37
|
+
geometry?.setAngle(itemAngle!);
|
|
33
38
|
const stateParams = interactionCtrl?.getState<number | string>(['angle', 'blur', 'letterSpacing', 'wordSpacing', 'color']);
|
|
34
39
|
const stateStyles = stateParams?.styles ?? {};
|
|
35
40
|
const transition = stateParams?.transition ?? 'none';
|
|
@@ -60,6 +60,7 @@ export const useRichTextItemValues = (item: RichTextItem, sectionId: string) =>
|
|
|
60
60
|
|
|
61
61
|
const fontSize = layoutId ? item.layoutParams[layoutId].fontSize : undefined;
|
|
62
62
|
const lineHeight = layoutId ? item.layoutParams[layoutId].lineHeight : undefined;
|
|
63
|
+
const xSizing = layoutId ? item.layoutParams[layoutId].sizing : undefined;
|
|
63
64
|
|
|
64
|
-
return { blur, letterSpacing, wordSpacing, color, fontSize, lineHeight };
|
|
65
|
+
return { blur, letterSpacing, wordSpacing, color, fontSize, lineHeight, xSizing };
|
|
65
66
|
};
|
|
@@ -7,18 +7,23 @@ import {
|
|
|
7
7
|
ItemAny,
|
|
8
8
|
} from '@cntrl-site/sdk';
|
|
9
9
|
import { isItemType } from '../utils/isItemType';
|
|
10
|
+
import { InteractionItemScrollTrigger, InteractionScrollTrigger } from '@cntrl-site/sdk/lib/types/article/Interaction';
|
|
11
|
+
import { ItemGeometryService } from '../ItemGeometry/ItemGeometryService';
|
|
10
12
|
|
|
11
13
|
export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
12
14
|
private ctrls: Map<ItemId, ItemInteractionCtrl> = new Map();
|
|
13
15
|
private items: ItemAny[];
|
|
16
|
+
private itemGeometryService: ItemGeometryService;
|
|
14
17
|
private interactions: Interaction[];
|
|
15
18
|
private stateItemsIdsMap: StateItemsIdsMap;
|
|
16
19
|
private interactionStateMap: InteractionStateMap;
|
|
17
20
|
private itemsStages: ItemStages;
|
|
18
21
|
private activeStateIdInteractionIdMap: Record<StateId, InteractionId>;
|
|
22
|
+
private itemScrollPositionAnimationMap: Map<string, number> = new Map();
|
|
19
23
|
|
|
20
|
-
constructor(article: Article, layoutId: string) {
|
|
24
|
+
constructor(article: Article, layoutId: string, itemGeometryService: ItemGeometryService) {
|
|
21
25
|
this.items = this.unpackItems(article);
|
|
26
|
+
this.itemGeometryService = itemGeometryService;
|
|
22
27
|
const interactions = article.interactions[layoutId] ?? [];
|
|
23
28
|
const activeStatesIds = interactions.reduce<StateId[]>((map, inter) => {
|
|
24
29
|
const activeStateId = inter.states.find((state) => state.id !== inter.startStateId)?.id;
|
|
@@ -54,6 +59,7 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
54
59
|
this.interactionStateMap = interactionStateMap;
|
|
55
60
|
const itemStages = this.getDefaultItemStages();
|
|
56
61
|
this.itemsStages = itemStages;
|
|
62
|
+
this.itemScrollPositionAnimationMap = new Map();
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
|
|
@@ -96,28 +102,69 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
96
102
|
return itemStyles;
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['
|
|
100
|
-
const available = new Set<InteractionItemTrigger['
|
|
105
|
+
getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['triggerEvent']> {
|
|
106
|
+
const available = new Set<InteractionItemTrigger['triggerEvent']>();
|
|
101
107
|
const activeStates = Object.values(this.interactionStateMap);
|
|
102
108
|
for (const interaction of this.interactions) {
|
|
103
109
|
const { triggers } = interaction;
|
|
104
110
|
for (const trigger of triggers) {
|
|
105
|
-
if (!('
|
|
111
|
+
if (!('triggerEvent' in trigger)) continue;
|
|
106
112
|
if (trigger.itemId !== itemId) continue;
|
|
107
113
|
if (activeStates.includes(trigger.from)) {
|
|
108
|
-
available.add(trigger.
|
|
114
|
+
available.add(trigger.triggerEvent);
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
return available;
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
getTriggerPositionForItemScrollTrigger(trigger: InteractionItemScrollTrigger): number {
|
|
122
|
+
const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
|
|
123
|
+
let triggerPosition = 0;
|
|
124
|
+
switch (trigger.screenPosition) {
|
|
125
|
+
case 'top':
|
|
126
|
+
triggerPosition = itemRect.top;
|
|
127
|
+
break;
|
|
128
|
+
case 'center':
|
|
129
|
+
triggerPosition = itemRect.top - window.innerHeight / 2;
|
|
130
|
+
break;
|
|
131
|
+
case 'bottom':
|
|
132
|
+
triggerPosition = itemRect.top - window.innerHeight;
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
triggerPosition = itemRect.top;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
switch (trigger.itemPosition) {
|
|
139
|
+
case 'top':
|
|
140
|
+
triggerPosition += 0;
|
|
141
|
+
break;
|
|
142
|
+
case 'center':
|
|
143
|
+
triggerPosition += itemRect.height / 2;
|
|
144
|
+
break;
|
|
145
|
+
case 'bottom':
|
|
146
|
+
triggerPosition += itemRect.height;
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
triggerPosition += 0;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
triggerPosition += trigger.offset;
|
|
153
|
+
return triggerPosition;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
isItemScrollTriggerOnStartScreen(trigger: InteractionItemScrollTrigger): boolean {
|
|
157
|
+
const triggerPosition = this.getTriggerPositionForItemScrollTrigger(trigger);
|
|
158
|
+
return triggerPosition < 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
115
161
|
notifyLoad() {
|
|
116
162
|
const timestamp = Date.now();
|
|
117
163
|
for (const interaction of this.interactions) {
|
|
118
164
|
const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
|
|
119
165
|
const matchingTrigger = interaction.triggers.find(trigger =>
|
|
120
|
-
'position' in trigger && trigger.position === 0 && trigger.from === currentStateId
|
|
166
|
+
('position' in trigger && trigger.position === 0 && trigger.from === currentStateId)
|
|
167
|
+
|| (trigger.type === 'item-scroll-position' && this.items.find((item) => item.id === trigger.itemId) && trigger.from === currentStateId && this.isItemScrollTriggerOnStartScreen(trigger))
|
|
121
168
|
);
|
|
122
169
|
if (!matchingTrigger) continue;
|
|
123
170
|
const activeStateId = this.getActiveInteractionState(interaction.id);
|
|
@@ -155,22 +202,80 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
155
202
|
}
|
|
156
203
|
}
|
|
157
204
|
|
|
205
|
+
scrollPositionNotifyScroll(trigger: InteractionScrollTrigger, position: number): boolean {
|
|
206
|
+
if (trigger.position === 0) return false;
|
|
207
|
+
const triggerPosition = trigger.position * window.innerWidth;
|
|
208
|
+
return triggerPosition < position;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
scrollItemPositionNotifyScroll(trigger: InteractionItemScrollTrigger, position: number, interaction: Interaction): boolean {
|
|
212
|
+
let triggerPosition;
|
|
213
|
+
let isScrolledPastTrigger = false;
|
|
214
|
+
const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
|
|
215
|
+
if (!itemRect) return false;
|
|
216
|
+
switch (trigger.screenPosition) {
|
|
217
|
+
case 'top':
|
|
218
|
+
triggerPosition = 0;
|
|
219
|
+
break;
|
|
220
|
+
case 'center':
|
|
221
|
+
triggerPosition = window.innerHeight / 2;
|
|
222
|
+
break;
|
|
223
|
+
case 'bottom':
|
|
224
|
+
triggerPosition = window.innerHeight;
|
|
225
|
+
break;
|
|
226
|
+
default:
|
|
227
|
+
triggerPosition = 0;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
triggerPosition -= trigger.offset;
|
|
231
|
+
switch (trigger.itemPosition) {
|
|
232
|
+
case 'top':
|
|
233
|
+
isScrolledPastTrigger = itemRect.y < triggerPosition;
|
|
234
|
+
break;
|
|
235
|
+
case 'center':
|
|
236
|
+
isScrolledPastTrigger = itemRect.y + itemRect.height / 2 < triggerPosition;
|
|
237
|
+
break;
|
|
238
|
+
case 'bottom':
|
|
239
|
+
isScrolledPastTrigger = itemRect.bottom < triggerPosition;
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
isScrolledPastTrigger = itemRect.y < triggerPosition;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
if (isScrolledPastTrigger && !this.itemScrollPositionAnimationMap.get(interaction.id)) {
|
|
246
|
+
this.itemScrollPositionAnimationMap.set(interaction.id, position + itemRect.y - triggerPosition);
|
|
247
|
+
}
|
|
248
|
+
if (this.itemScrollPositionAnimationMap.get(interaction.id)! >= position) isScrolledPastTrigger = false;
|
|
249
|
+
return isScrolledPastTrigger;
|
|
250
|
+
}
|
|
251
|
+
|
|
158
252
|
notifyScroll(position: number) {
|
|
159
253
|
const timestamp = Date.now();
|
|
160
254
|
for (const interaction of this.interactions) {
|
|
161
255
|
const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
|
|
162
256
|
const activeStateId = interaction.states.find((state) => state.id !== interaction.startStateId)?.id;
|
|
163
257
|
const matchingTrigger = interaction.triggers.find((trigger) => {
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
258
|
+
if (trigger.type !== 'scroll-position' && trigger.type !== 'item-scroll-position') return false;
|
|
259
|
+
let isScrolledPastTrigger = false;
|
|
260
|
+
if (trigger.type === 'scroll-position') {
|
|
261
|
+
isScrolledPastTrigger = this.scrollPositionNotifyScroll(trigger, position);
|
|
262
|
+
}
|
|
263
|
+
if (trigger.type === 'item-scroll-position') {
|
|
264
|
+
isScrolledPastTrigger = this.scrollItemPositionNotifyScroll(trigger, position, interaction);
|
|
265
|
+
}
|
|
167
266
|
if (!isScrolledPastTrigger && !trigger.isReverse) return false;
|
|
168
267
|
const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
|
|
169
268
|
return stateId === currentStateId;
|
|
170
269
|
});
|
|
171
|
-
if (!matchingTrigger ||
|
|
172
|
-
|
|
173
|
-
|
|
270
|
+
if (!matchingTrigger || (matchingTrigger.type !== 'scroll-position' && matchingTrigger.type !== 'item-scroll-position') || !activeStateId) continue;
|
|
271
|
+
let isScrolledPastTrigger = false;
|
|
272
|
+
if (matchingTrigger.type === 'scroll-position') {
|
|
273
|
+
const triggerPosition = matchingTrigger.position * window.innerWidth;
|
|
274
|
+
isScrolledPastTrigger = triggerPosition < position;
|
|
275
|
+
}
|
|
276
|
+
if (matchingTrigger.type === 'item-scroll-position') {
|
|
277
|
+
isScrolledPastTrigger = this.itemScrollPositionAnimationMap.get(interaction.id)! < position; // scroll down
|
|
278
|
+
}
|
|
174
279
|
const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
|
|
175
280
|
this.setCurrentStateForInteraction(interaction.id, targetStateId);
|
|
176
281
|
const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
|
|
@@ -210,10 +315,10 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
|
|
|
210
315
|
for (const interaction of this.interactions) {
|
|
211
316
|
const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
|
|
212
317
|
const matchingTrigger = interaction.triggers.find((trigger) =>
|
|
213
|
-
'
|
|
318
|
+
'triggerEvent' in trigger
|
|
214
319
|
&& trigger.itemId === itemId
|
|
215
320
|
&& trigger.from === currentStateId
|
|
216
|
-
&& trigger.
|
|
321
|
+
&& trigger.triggerEvent === triggerType
|
|
217
322
|
);
|
|
218
323
|
if (!matchingTrigger) continue;
|
|
219
324
|
const activeStateId = this.getActiveInteractionState(interaction.id);
|
|
@@ -366,7 +471,7 @@ type TransitioningStage = {
|
|
|
366
471
|
type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
|
|
367
472
|
type InteractionStateMap = Record<InteractionId, StateId>;
|
|
368
473
|
type StateItemsIdsMap = Record<StateId, ItemId[]>;
|
|
369
|
-
type TriggerType = InteractionItemTrigger['
|
|
474
|
+
type TriggerType = InteractionItemTrigger['triggerEvent'];
|
|
370
475
|
type InteractionId = string;
|
|
371
476
|
type StateId = string;
|
|
372
477
|
type ItemId = string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
|
|
2
2
|
import { getTransition } from './getTransition';
|
|
3
|
-
import { getStyleKeysFromCSSProperty } from '
|
|
3
|
+
import { getStyleKeysFromCSSProperty } from '../ItemGeometry/CSSPropertyNameMap';
|
|
4
4
|
import { FillLayer, InteractionItemTrigger } from '@cntrl-site/sdk';
|
|
5
5
|
|
|
6
6
|
export class ItemInteractionController implements ItemInteractionCtrl {
|
|
@@ -30,7 +30,7 @@ export class ItemInteractionController implements ItemInteractionCtrl {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['
|
|
33
|
+
getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['triggerEvent']): boolean {
|
|
34
34
|
const triggers = this.registry.getItemAvailableTriggers(itemId);
|
|
35
35
|
return triggers.has(triggerType);
|
|
36
36
|
}
|
|
@@ -2,7 +2,7 @@ import { ArticleItemType, FillLayer, InteractionItemTrigger, ItemState } from '@
|
|
|
2
2
|
|
|
3
3
|
export interface ItemInteractionCtrl {
|
|
4
4
|
getState<T>(keys: string[]): StateCSSInfo<T>;
|
|
5
|
-
getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['
|
|
5
|
+
getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['triggerEvent']): boolean;
|
|
6
6
|
sendTrigger(type: 'click' | 'hover-in' | 'hover-out'): void;
|
|
7
7
|
handleTransitionEnd?: (styleKey: string) => void;
|
|
8
8
|
handleTransitionStart?: (styleKeys: string[]) => void;
|
|
@@ -14,7 +14,7 @@ export interface ItemInteractionCtrl {
|
|
|
14
14
|
export interface InteractionsRegistryPort {
|
|
15
15
|
register(itemId: string, ctrl: ItemInteractionCtrl): void;
|
|
16
16
|
getStatePropsForItem(itemId: string): StateProps<unknown>;
|
|
17
|
-
getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['
|
|
17
|
+
getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['triggerEvent']>;
|
|
18
18
|
notifyItemTrigger(itemId: string, type: TriggerType): void;
|
|
19
19
|
notifyTransitionEnd(itemId: string): void;
|
|
20
20
|
}
|
|
@@ -3,6 +3,7 @@ import { InteractionsRegistry } from '../interactions/InteractionsRegistry';
|
|
|
3
3
|
import { Article } from '@cntrl-site/sdk';
|
|
4
4
|
import { useCurrentLayout } from '../common/useCurrentLayout';
|
|
5
5
|
import { ArticleRectContext } from './ArticleRectContext';
|
|
6
|
+
import { ItemGeometryContext } from '../ItemGeometry/ItemGeometryContext';
|
|
6
7
|
|
|
7
8
|
export const InteractionsContext = createContext<InteractionsRegistry | undefined>(undefined);
|
|
8
9
|
|
|
@@ -13,10 +14,11 @@ interface Props {
|
|
|
13
14
|
export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ article, children }) => {
|
|
14
15
|
const { layoutId } = useCurrentLayout();
|
|
15
16
|
const articleRectObserver = useContext(ArticleRectContext);
|
|
17
|
+
const itemGeometryService = useContext(ItemGeometryContext);
|
|
16
18
|
const registry = useMemo(() => {
|
|
17
19
|
if (!layoutId) return;
|
|
18
|
-
return new InteractionsRegistry(article, layoutId);
|
|
19
|
-
}, [layoutId]);
|
|
20
|
+
return new InteractionsRegistry(article, layoutId, itemGeometryService);
|
|
21
|
+
}, [layoutId, itemGeometryService]);
|
|
20
22
|
|
|
21
23
|
useEffect(() => {
|
|
22
24
|
if (!registry || !articleRectObserver) return;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const reCSSNumeric = /([+-]?(?:\d*\.\d|\d)\d*)(\w+)/;
|
|
2
|
+
|
|
3
|
+
interface CSSNumberValue {
|
|
4
|
+
value: number;
|
|
5
|
+
unit?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseCSSNumber(input: string): CSSNumberValue {
|
|
9
|
+
const match = reCSSNumeric.exec(input);
|
|
10
|
+
if (!match) return { value: Number.NaN };
|
|
11
|
+
return {
|
|
12
|
+
value: Number.parseFloat(match[1]),
|
|
13
|
+
unit: match[2]
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatCSSNumber(input: CSSNumberValue): string {
|
|
18
|
+
return `${input.value}${input.unit ?? ''}`;
|
|
19
|
+
}
|