react-native-divkit 1.6.4 → 1.7.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/README.md +3 -1
- package/dist/DivKit.d.ts +2 -2
- package/dist/DivKit.d.ts.map +1 -1
- package/dist/DivKit.js +17 -13
- package/dist/DivKit.js.map +1 -1
- package/dist/components/DivComponent.d.ts.map +1 -1
- package/dist/components/DivComponent.js +6 -2
- package/dist/components/DivComponent.js.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/indicator/DivIndicator.d.ts +19 -0
- package/dist/components/indicator/DivIndicator.d.ts.map +1 -0
- package/dist/components/indicator/DivIndicator.js +112 -0
- package/dist/components/indicator/DivIndicator.js.map +1 -0
- package/dist/components/indicator/index.d.ts +3 -0
- package/dist/components/indicator/index.d.ts.map +1 -0
- package/dist/components/indicator/index.js +2 -0
- package/dist/components/indicator/index.js.map +1 -0
- package/dist/components/indicator/utils.d.ts +61 -0
- package/dist/components/indicator/utils.d.ts.map +1 -0
- package/dist/components/indicator/utils.js +104 -0
- package/dist/components/indicator/utils.js.map +1 -0
- package/dist/components/pager/DivPager.d.ts +22 -0
- package/dist/components/pager/DivPager.d.ts.map +1 -0
- package/dist/components/pager/DivPager.js +269 -0
- package/dist/components/pager/DivPager.js.map +1 -0
- package/dist/components/pager/index.d.ts +3 -0
- package/dist/components/pager/index.d.ts.map +1 -0
- package/dist/components/pager/index.js +2 -0
- package/dist/components/pager/index.js.map +1 -0
- package/dist/components/pager/utils.d.ts +96 -0
- package/dist/components/pager/utils.d.ts.map +1 -0
- package/dist/components/pager/utils.js +142 -0
- package/dist/components/pager/utils.js.map +1 -0
- package/dist/components/utilities/Outer.d.ts.map +1 -1
- package/dist/components/utilities/Outer.js +4 -3
- package/dist/components/utilities/Outer.js.map +1 -1
- package/dist/context/PagerContext.d.ts +30 -0
- package/dist/context/PagerContext.d.ts.map +1 -0
- package/dist/context/PagerContext.js +76 -0
- package/dist/context/PagerContext.js.map +1 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -0
- package/dist/context/index.js.map +1 -1
- package/package.json +2 -1
- package/src/DivKit.tsx +19 -16
- package/src/components/DivComponent.tsx +8 -2
- package/src/components/README.md +59 -5
- package/src/components/index.ts +4 -0
- package/src/components/indicator/DivIndicator.tsx +175 -0
- package/src/components/indicator/index.ts +2 -0
- package/src/components/indicator/utils.ts +149 -0
- package/src/components/pager/DivPager.tsx +393 -0
- package/src/components/pager/index.ts +2 -0
- package/src/components/pager/utils.ts +214 -0
- package/src/components/utilities/Outer.tsx +4 -2
- package/src/context/PagerContext.tsx +108 -0
- package/src/context/index.ts +8 -0
- package/src/types/indicator.d.ts +32 -0
- package/src/types/pager.d.ts +36 -0
- package/src/types/shape.d.ts +26 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for DivIndicator. Extracted for unit testing.
|
|
3
|
+
*/
|
|
4
|
+
import { correctColor } from '../../utils/correctColor';
|
|
5
|
+
|
|
6
|
+
export interface DotStyle {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
borderRadius: number;
|
|
10
|
+
background: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_ACTIVE: DotStyle = {
|
|
14
|
+
width: 13,
|
|
15
|
+
height: 13,
|
|
16
|
+
borderRadius: 6.5,
|
|
17
|
+
background: '#ffdc60'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_INACTIVE: DotStyle = {
|
|
21
|
+
width: 10,
|
|
22
|
+
height: 10,
|
|
23
|
+
borderRadius: 5,
|
|
24
|
+
background: '#33919cb5'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface ShapeLike {
|
|
28
|
+
type?: string;
|
|
29
|
+
item_width?: { value?: number };
|
|
30
|
+
item_height?: { value?: number };
|
|
31
|
+
corner_radius?: { value?: number };
|
|
32
|
+
radius?: { value?: number };
|
|
33
|
+
background_color?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert a Shape (rounded_rectangle | circle) into a DotStyle.
|
|
38
|
+
* Falls back to `base` when the shape is missing or unsupported.
|
|
39
|
+
*/
|
|
40
|
+
export function shapeToDot(shape: unknown, fallbackColor: string, base: DotStyle): DotStyle {
|
|
41
|
+
if (!shape) return base;
|
|
42
|
+
const s = shape as ShapeLike;
|
|
43
|
+
if (s.type === 'rounded_rectangle') {
|
|
44
|
+
const w = s.item_width?.value ?? base.width;
|
|
45
|
+
const h = s.item_height?.value ?? base.height;
|
|
46
|
+
const r = s.corner_radius?.value ?? Math.min(w, h) / 2;
|
|
47
|
+
return {
|
|
48
|
+
width: w,
|
|
49
|
+
height: h,
|
|
50
|
+
borderRadius: r,
|
|
51
|
+
background: correctColor(s.background_color, 1, fallbackColor)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (s.type === 'circle') {
|
|
55
|
+
const r = s.radius?.value ?? base.width / 2;
|
|
56
|
+
return {
|
|
57
|
+
width: r * 2,
|
|
58
|
+
height: r * 2,
|
|
59
|
+
borderRadius: r,
|
|
60
|
+
background: correctColor(s.background_color, 1, fallbackColor)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return base;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BuildDotStylesArgs {
|
|
67
|
+
activeShape?: unknown;
|
|
68
|
+
inactiveShape?: unknown;
|
|
69
|
+
legacyShape?: unknown;
|
|
70
|
+
activeColor?: string;
|
|
71
|
+
inactiveColor?: string;
|
|
72
|
+
activeItemSize?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compute final active/inactive dot styles. Mirrors Indicator.svelte:
|
|
77
|
+
* - explicit active_shape/inactive_shape take precedence
|
|
78
|
+
* - else legacy `shape` + `active_item_size` + colors generates both
|
|
79
|
+
* - else defaults
|
|
80
|
+
*/
|
|
81
|
+
export function buildDotStyles(args: BuildDotStylesArgs): { active: DotStyle; inactive: DotStyle } {
|
|
82
|
+
const { activeShape, inactiveShape, legacyShape, activeColor, inactiveColor, activeItemSize } = args;
|
|
83
|
+
let inactive: DotStyle = { ...DEFAULT_INACTIVE };
|
|
84
|
+
let active: DotStyle = { ...DEFAULT_ACTIVE };
|
|
85
|
+
|
|
86
|
+
if (activeShape) {
|
|
87
|
+
active = shapeToDot(activeShape, active.background, active);
|
|
88
|
+
}
|
|
89
|
+
if (inactiveShape) {
|
|
90
|
+
inactive = shapeToDot(inactiveShape, inactive.background, inactive);
|
|
91
|
+
}
|
|
92
|
+
if (!activeShape && !inactiveShape && legacyShape) {
|
|
93
|
+
const sizeMul = typeof activeItemSize === 'number' && activeItemSize > 0 ? activeItemSize : 1.3;
|
|
94
|
+
inactive = shapeToDot(legacyShape, inactive.background, inactive);
|
|
95
|
+
inactive.background = correctColor(inactiveColor, 1, inactive.background);
|
|
96
|
+
const activeBg = correctColor(activeColor, 1, active.background);
|
|
97
|
+
active = {
|
|
98
|
+
width: inactive.width * sizeMul,
|
|
99
|
+
height: inactive.height * sizeMul,
|
|
100
|
+
borderRadius: inactive.borderRadius * sizeMul,
|
|
101
|
+
background: activeBg
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { active, inactive };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type IndicatorPlacement = 'default' | 'stretch';
|
|
108
|
+
|
|
109
|
+
export interface ResolvePlacementArgs {
|
|
110
|
+
itemsPlacement?: { type?: string; space_between_centers?: { value?: number }; item_spacing?: { value?: number }; max_visible_items?: number };
|
|
111
|
+
spaceBetweenCenters?: { value?: number };
|
|
112
|
+
inactiveWidth: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ResolvedPlacement {
|
|
116
|
+
placement: IndicatorPlacement;
|
|
117
|
+
gap: number;
|
|
118
|
+
stretchSpacing: number;
|
|
119
|
+
maxVisible: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve which placement mode to use and the spacing parameters.
|
|
124
|
+
* - stretch: equal spacing across the full width, item_spacing px between dots.
|
|
125
|
+
* - default: gap = space_between_centers - inactiveWidth.
|
|
126
|
+
*/
|
|
127
|
+
export function resolvePlacement(args: ResolvePlacementArgs): ResolvedPlacement {
|
|
128
|
+
const { itemsPlacement, spaceBetweenCenters, inactiveWidth } = args;
|
|
129
|
+
|
|
130
|
+
if (itemsPlacement && itemsPlacement.type === 'stretch') {
|
|
131
|
+
return {
|
|
132
|
+
placement: 'stretch',
|
|
133
|
+
gap: 0,
|
|
134
|
+
stretchSpacing: itemsPlacement.item_spacing?.value ?? 5,
|
|
135
|
+
maxVisible: itemsPlacement.max_visible_items ?? 10
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
let center = spaceBetweenCenters?.value;
|
|
139
|
+
if (itemsPlacement && itemsPlacement.type === 'default') {
|
|
140
|
+
center = itemsPlacement.space_between_centers?.value ?? center;
|
|
141
|
+
}
|
|
142
|
+
const c = typeof center === 'number' && center >= 0 ? center : 15;
|
|
143
|
+
return {
|
|
144
|
+
placement: 'default',
|
|
145
|
+
gap: Math.max(0, c - inactiveWidth),
|
|
146
|
+
stretchSpacing: 0,
|
|
147
|
+
maxVisible: 10
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
ScrollView,
|
|
5
|
+
NativeScrollEvent,
|
|
6
|
+
NativeSyntheticEvent,
|
|
7
|
+
LayoutChangeEvent,
|
|
8
|
+
StyleSheet
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import type { ComponentContext } from '../../types/componentContext';
|
|
11
|
+
import type { DivPagerData, PagerOrientation } from '../../types/pager';
|
|
12
|
+
import type { PagerData, PagerRegisterData } from '../../types/componentContext';
|
|
13
|
+
import type { EdgeInsets } from '../../types/edgeInserts';
|
|
14
|
+
import { Outer } from '../utilities/Outer';
|
|
15
|
+
import { DivComponent } from '../DivComponent';
|
|
16
|
+
import { useDerivedFromVarsSimple } from '../../hooks/useDerivedFromVars';
|
|
17
|
+
import { useDivKitContext } from '../../context/DivKitContext';
|
|
18
|
+
import { usePagerContextOptional } from '../../context/PagerContext';
|
|
19
|
+
import { wrapError } from '../../utils/wrapError';
|
|
20
|
+
import {
|
|
21
|
+
DUPLICATES_IN_INFINITE,
|
|
22
|
+
buildRenderedItems,
|
|
23
|
+
computeContentPad,
|
|
24
|
+
computePageSize,
|
|
25
|
+
isInDuplicateRegion as isInDuplicateRegionFn,
|
|
26
|
+
isInfiniteEnabled,
|
|
27
|
+
offsetToPosition,
|
|
28
|
+
positionToReal as positionToRealFn,
|
|
29
|
+
realToPosition as realToPositionFn,
|
|
30
|
+
type ScrollAxisAlignment
|
|
31
|
+
} from './utils';
|
|
32
|
+
|
|
33
|
+
export interface DivPagerProps {
|
|
34
|
+
componentContext: ComponentContext<DivPagerData>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DUPLICATES = DUPLICATES_IN_INFINITE;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* DivPager — horizontal/vertical pager with snap-to-page scrolling.
|
|
41
|
+
*
|
|
42
|
+
* Based on Web Pager.svelte. Adapted for React Native:
|
|
43
|
+
* - Uses ScrollView with snapToInterval for paging behaviour.
|
|
44
|
+
* - Computes per-page size from layout_mode (fixed neighbour, percentage,
|
|
45
|
+
* wrap_content) once container size is known.
|
|
46
|
+
* - infinite_scroll: renders DUPLICATES extra items at each end; when the user
|
|
47
|
+
* lands on a duplicate the scroll position is silently snapped to the
|
|
48
|
+
* matching real item (no animation), giving a seamless loop.
|
|
49
|
+
* - Exposes its current page/size to indicators via PagerContext using the
|
|
50
|
+
* same registerPager/listenPager contract as Web. The "currentItem" reported
|
|
51
|
+
* to indicators is always the real index in [0, items.length).
|
|
52
|
+
*/
|
|
53
|
+
export function DivPager({ componentContext }: DivPagerProps) {
|
|
54
|
+
const { genId } = useDivKitContext();
|
|
55
|
+
const pagerCtx = usePagerContextOptional();
|
|
56
|
+
const { json, variables } = componentContext;
|
|
57
|
+
|
|
58
|
+
const orientation = useDerivedFromVarsSimple<PagerOrientation>(
|
|
59
|
+
(json.orientation as PagerOrientation) || 'horizontal',
|
|
60
|
+
variables || new Map()
|
|
61
|
+
);
|
|
62
|
+
const layoutMode = useDerivedFromVarsSimple(
|
|
63
|
+
json.layout_mode,
|
|
64
|
+
variables || new Map()
|
|
65
|
+
);
|
|
66
|
+
const itemSpacingObj = useDerivedFromVarsSimple(json.item_spacing, variables || new Map());
|
|
67
|
+
const paddings = useDerivedFromVarsSimple(json.paddings, variables || new Map());
|
|
68
|
+
const scrollAxisAlignment = useDerivedFromVarsSimple(
|
|
69
|
+
json.scroll_axis_alignment || 'center',
|
|
70
|
+
variables || new Map()
|
|
71
|
+
);
|
|
72
|
+
const defaultItem = useDerivedFromVarsSimple<number>(
|
|
73
|
+
typeof json.default_item === 'number' ? json.default_item : 0,
|
|
74
|
+
variables || new Map()
|
|
75
|
+
);
|
|
76
|
+
const infiniteScroll = useDerivedFromVarsSimple<number | boolean | undefined>(
|
|
77
|
+
json.infinite_scroll,
|
|
78
|
+
variables || new Map()
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const isHorizontal = orientation !== 'vertical';
|
|
82
|
+
const itemSpacing = (itemSpacingObj as { value?: number } | undefined)?.value ?? 0;
|
|
83
|
+
|
|
84
|
+
const items = useMemo(() => {
|
|
85
|
+
return Array.isArray(json.items) ? json.items : [];
|
|
86
|
+
}, [json.items]);
|
|
87
|
+
|
|
88
|
+
const isInfinite = useMemo(
|
|
89
|
+
() => isInfiniteEnabled(infiniteScroll, items.length),
|
|
90
|
+
[infiniteScroll, items.length]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Pager paddings — applied on the inner ScrollView so we can also use them
|
|
94
|
+
// for snap math. Outer should NOT also apply them, so we strip them from
|
|
95
|
+
// the json passed into Outer below.
|
|
96
|
+
const innerPadStart = useMemo(() => {
|
|
97
|
+
const p = (paddings as EdgeInsets | undefined) || {};
|
|
98
|
+
if (isHorizontal) {
|
|
99
|
+
return Number(p.start ?? p.left ?? 0) || 0;
|
|
100
|
+
}
|
|
101
|
+
return Number(p.top ?? 0) || 0;
|
|
102
|
+
}, [paddings, isHorizontal]);
|
|
103
|
+
const innerPadEnd = useMemo(() => {
|
|
104
|
+
const p = (paddings as EdgeInsets | undefined) || {};
|
|
105
|
+
if (isHorizontal) {
|
|
106
|
+
return Number(p.end ?? p.right ?? 0) || 0;
|
|
107
|
+
}
|
|
108
|
+
return Number(p.bottom ?? 0) || 0;
|
|
109
|
+
}, [paddings, isHorizontal]);
|
|
110
|
+
|
|
111
|
+
const [containerSize, setContainerSize] = useState(0);
|
|
112
|
+
const scrollRef = useRef<ScrollView>(null);
|
|
113
|
+
const currentItemRef = useRef(0); // real index, always in [0, items.length)
|
|
114
|
+
const initialScrollDone = useRef(false);
|
|
115
|
+
const registerDataRef = useRef<PagerRegisterData | null>(null);
|
|
116
|
+
const pagerInstId = useRef<string>(genId('pager'));
|
|
117
|
+
const scrollToItemRef = useRef<((realIndex: number, animated: boolean) => void) | null>(null);
|
|
118
|
+
|
|
119
|
+
const pageSize = useMemo(
|
|
120
|
+
() =>
|
|
121
|
+
computePageSize({
|
|
122
|
+
containerSize,
|
|
123
|
+
layoutMode,
|
|
124
|
+
scrollAxisAlignment: scrollAxisAlignment as ScrollAxisAlignment,
|
|
125
|
+
itemSpacing,
|
|
126
|
+
innerPadStart,
|
|
127
|
+
innerPadEnd
|
|
128
|
+
}),
|
|
129
|
+
[containerSize, innerPadStart, innerPadEnd, layoutMode, scrollAxisAlignment, itemSpacing]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const snapInterval = pageSize > 0 ? pageSize + itemSpacing : 0;
|
|
133
|
+
|
|
134
|
+
const contentPad = useMemo(
|
|
135
|
+
() =>
|
|
136
|
+
computeContentPad({
|
|
137
|
+
containerSize,
|
|
138
|
+
pageSize,
|
|
139
|
+
innerPadStart,
|
|
140
|
+
innerPadEnd,
|
|
141
|
+
layoutMode,
|
|
142
|
+
scrollAxisAlignment: scrollAxisAlignment as ScrollAxisAlignment,
|
|
143
|
+
itemSpacing,
|
|
144
|
+
isInfinite
|
|
145
|
+
}),
|
|
146
|
+
[
|
|
147
|
+
containerSize,
|
|
148
|
+
pageSize,
|
|
149
|
+
innerPadStart,
|
|
150
|
+
innerPadEnd,
|
|
151
|
+
layoutMode,
|
|
152
|
+
scrollAxisAlignment,
|
|
153
|
+
itemSpacing,
|
|
154
|
+
isInfinite
|
|
155
|
+
]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const realToPosition = useCallback(
|
|
159
|
+
(realIdx: number) => realToPositionFn(realIdx, isInfinite, DUPLICATES),
|
|
160
|
+
[isInfinite]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const positionToReal = useCallback(
|
|
164
|
+
(pos: number) => positionToRealFn(pos, isInfinite, items.length, DUPLICATES),
|
|
165
|
+
[isInfinite, items.length]
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const isInDuplicateRegion = useCallback(
|
|
169
|
+
(pos: number) => isInDuplicateRegionFn(pos, isInfinite, items.length, DUPLICATES),
|
|
170
|
+
[isInfinite, items.length]
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const runSelectedActions = useCallback(
|
|
174
|
+
(index: number) => {
|
|
175
|
+
const item = items[index] as any;
|
|
176
|
+
const actions = item?.selected_actions;
|
|
177
|
+
if (Array.isArray(actions) && actions.length > 0) {
|
|
178
|
+
componentContext.execAnyActions(actions);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
[items, componentContext]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const pushPagerState = useCallback(
|
|
185
|
+
(item: number) => {
|
|
186
|
+
const reg = registerDataRef.current;
|
|
187
|
+
if (!reg) return;
|
|
188
|
+
const data: PagerData = {
|
|
189
|
+
instId: pagerInstId.current,
|
|
190
|
+
size: items.length,
|
|
191
|
+
currentItem: item,
|
|
192
|
+
scrollToPagerItem: (index: number) => scrollToItemRef.current?.(index, true)
|
|
193
|
+
};
|
|
194
|
+
reg.update(data);
|
|
195
|
+
},
|
|
196
|
+
[items.length]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const scrollToItem = useCallback(
|
|
200
|
+
(realIndex: number, animated: boolean) => {
|
|
201
|
+
const node = scrollRef.current;
|
|
202
|
+
if (!node || snapInterval <= 0 || items.length === 0) return;
|
|
203
|
+
const clampedReal = isInfinite
|
|
204
|
+
? ((realIndex % items.length) + items.length) % items.length
|
|
205
|
+
: Math.max(0, Math.min(items.length - 1, realIndex));
|
|
206
|
+
const pos = realToPosition(clampedReal);
|
|
207
|
+
const offset = pos * snapInterval;
|
|
208
|
+
if (isHorizontal) {
|
|
209
|
+
node.scrollTo({ x: offset, y: 0, animated });
|
|
210
|
+
} else {
|
|
211
|
+
node.scrollTo({ x: 0, y: offset, animated });
|
|
212
|
+
}
|
|
213
|
+
if (clampedReal !== currentItemRef.current) {
|
|
214
|
+
currentItemRef.current = clampedReal;
|
|
215
|
+
pushPagerState(clampedReal);
|
|
216
|
+
runSelectedActions(clampedReal);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
[
|
|
220
|
+
items.length,
|
|
221
|
+
snapInterval,
|
|
222
|
+
isHorizontal,
|
|
223
|
+
isInfinite,
|
|
224
|
+
realToPosition,
|
|
225
|
+
pushPagerState,
|
|
226
|
+
runSelectedActions
|
|
227
|
+
]
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
scrollToItemRef.current = scrollToItem;
|
|
232
|
+
}, [scrollToItem]);
|
|
233
|
+
|
|
234
|
+
// Register pager in context (so indicators can find it)
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!pagerCtx) return;
|
|
237
|
+
const pagerId = json.id;
|
|
238
|
+
const reg = pagerCtx.registerPager(pagerId);
|
|
239
|
+
registerDataRef.current = reg;
|
|
240
|
+
pushPagerState(currentItemRef.current);
|
|
241
|
+
return () => {
|
|
242
|
+
reg.destroy();
|
|
243
|
+
registerDataRef.current = null;
|
|
244
|
+
};
|
|
245
|
+
}, [pagerCtx, json.id, pushPagerState]);
|
|
246
|
+
|
|
247
|
+
// Re-broadcast on items length / scrollToItem changes
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
pushPagerState(currentItemRef.current);
|
|
250
|
+
}, [pushPagerState]);
|
|
251
|
+
|
|
252
|
+
// Initial scroll to default_item once we know page size
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (initialScrollDone.current) return;
|
|
255
|
+
if (snapInterval <= 0) return;
|
|
256
|
+
const initial = Math.max(0, Math.min(items.length - 1, defaultItem ?? 0));
|
|
257
|
+
currentItemRef.current = initial;
|
|
258
|
+
const id = setTimeout(() => {
|
|
259
|
+
scrollToItem(initial, false);
|
|
260
|
+
initialScrollDone.current = true;
|
|
261
|
+
pushPagerState(initial);
|
|
262
|
+
}, 0);
|
|
263
|
+
return () => clearTimeout(id);
|
|
264
|
+
}, [snapInterval, defaultItem, items.length, scrollToItem, pushPagerState]);
|
|
265
|
+
|
|
266
|
+
const onScrollEnd = useCallback(
|
|
267
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
268
|
+
if (snapInterval <= 0) return;
|
|
269
|
+
const { contentOffset } = event.nativeEvent;
|
|
270
|
+
const offset = isHorizontal ? contentOffset.x : contentOffset.y;
|
|
271
|
+
const pos = offsetToPosition(offset, snapInterval);
|
|
272
|
+
const realIdx = positionToReal(pos);
|
|
273
|
+
|
|
274
|
+
// In infinite mode: silently snap from a duplicate back to the
|
|
275
|
+
// matching real item without animation.
|
|
276
|
+
if (isInfinite && isInDuplicateRegion(pos)) {
|
|
277
|
+
const realPos = realToPosition(realIdx);
|
|
278
|
+
const node = scrollRef.current;
|
|
279
|
+
if (node) {
|
|
280
|
+
const realOffset = realPos * snapInterval;
|
|
281
|
+
if (isHorizontal) {
|
|
282
|
+
node.scrollTo({ x: realOffset, y: 0, animated: false });
|
|
283
|
+
} else {
|
|
284
|
+
node.scrollTo({ x: 0, y: realOffset, animated: false });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (realIdx !== currentItemRef.current) {
|
|
290
|
+
currentItemRef.current = realIdx;
|
|
291
|
+
pushPagerState(realIdx);
|
|
292
|
+
runSelectedActions(realIdx);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
[
|
|
296
|
+
snapInterval,
|
|
297
|
+
isHorizontal,
|
|
298
|
+
positionToReal,
|
|
299
|
+
isInDuplicateRegion,
|
|
300
|
+
isInfinite,
|
|
301
|
+
realToPosition,
|
|
302
|
+
pushPagerState,
|
|
303
|
+
runSelectedActions
|
|
304
|
+
]
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const onLayout = useCallback(
|
|
308
|
+
(e: LayoutChangeEvent) => {
|
|
309
|
+
const size = isHorizontal ? e.nativeEvent.layout.width : e.nativeEvent.layout.height;
|
|
310
|
+
if (size && Math.abs(size - containerSize) > 0.5) {
|
|
311
|
+
setContainerSize(size);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
[containerSize, isHorizontal]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Strip paddings from Outer — we apply them on the ScrollView ourselves.
|
|
318
|
+
const outerContext = useMemo(() => {
|
|
319
|
+
const restJson = { ...json };
|
|
320
|
+
delete restJson.paddings;
|
|
321
|
+
return { ...componentContext, json: restJson } as ComponentContext<DivPagerData>;
|
|
322
|
+
}, [componentContext, json]);
|
|
323
|
+
|
|
324
|
+
const renderedItems = useMemo(
|
|
325
|
+
() => buildRenderedItems(items, isInfinite, DUPLICATES),
|
|
326
|
+
[items, isInfinite]
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
if (!json.layout_mode) {
|
|
330
|
+
componentContext.logError(
|
|
331
|
+
wrapError(new Error('Empty "layout_mode" prop for div "pager"'))
|
|
332
|
+
);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const renderItems = () => {
|
|
337
|
+
if (!renderedItems.length || pageSize <= 0) return null;
|
|
338
|
+
return renderedItems.map((entry, posIndex) => {
|
|
339
|
+
const childContext = componentContext.produceChildContext(entry.item, {
|
|
340
|
+
path: posIndex
|
|
341
|
+
});
|
|
342
|
+
const isLast = posIndex === renderedItems.length - 1;
|
|
343
|
+
const itemStyle = isHorizontal
|
|
344
|
+
? { width: pageSize, marginRight: isLast ? 0 : itemSpacing }
|
|
345
|
+
: { height: pageSize, marginBottom: isLast ? 0 : itemSpacing };
|
|
346
|
+
return (
|
|
347
|
+
<View key={entry.key} style={[styles.itemWrapper, itemStyle]}>
|
|
348
|
+
<DivComponent componentContext={childContext} />
|
|
349
|
+
</View>
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<Outer componentContext={outerContext}>
|
|
356
|
+
<View style={styles.fill} onLayout={onLayout}>
|
|
357
|
+
{pageSize > 0 ? (
|
|
358
|
+
<ScrollView
|
|
359
|
+
ref={scrollRef}
|
|
360
|
+
horizontal={isHorizontal}
|
|
361
|
+
showsHorizontalScrollIndicator={false}
|
|
362
|
+
showsVerticalScrollIndicator={false}
|
|
363
|
+
decelerationRate="fast"
|
|
364
|
+
snapToInterval={snapInterval}
|
|
365
|
+
snapToAlignment="start"
|
|
366
|
+
disableIntervalMomentum
|
|
367
|
+
onMomentumScrollEnd={onScrollEnd}
|
|
368
|
+
onScrollEndDrag={onScrollEnd}
|
|
369
|
+
scrollEventThrottle={16}
|
|
370
|
+
contentContainerStyle={
|
|
371
|
+
isHorizontal
|
|
372
|
+
? { paddingLeft: contentPad.start, paddingRight: contentPad.end }
|
|
373
|
+
: { paddingTop: contentPad.start, paddingBottom: contentPad.end }
|
|
374
|
+
}
|
|
375
|
+
style={styles.fill}
|
|
376
|
+
>
|
|
377
|
+
{renderItems()}
|
|
378
|
+
</ScrollView>
|
|
379
|
+
) : null}
|
|
380
|
+
</View>
|
|
381
|
+
</Outer>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const styles = StyleSheet.create({
|
|
386
|
+
fill: {
|
|
387
|
+
flex: 1,
|
|
388
|
+
alignSelf: 'stretch'
|
|
389
|
+
},
|
|
390
|
+
itemWrapper: {
|
|
391
|
+
overflow: 'hidden'
|
|
392
|
+
}
|
|
393
|
+
});
|