@tarojs/components-advanced 4.1.12-beta.8 → 4.2.0-beta.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/dist/components/index.js +1 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/list/index.d.ts +3 -65
- package/dist/components/list/index.js +111 -1050
- package/dist/components/list/index.js.map +1 -1
- package/dist/components/virtual-list/vue/list.d.ts +2 -2
- package/dist/components/virtual-waterfall/vue/waterfall.d.ts +1 -1
- package/dist/components/water-flow/interface.d.ts +2 -14
- package/dist/components/water-flow/root.d.ts +1 -3
- package/dist/components/water-flow/root.js +9 -10
- package/dist/components/water-flow/root.js.map +1 -1
- package/dist/components/water-flow/utils.d.ts +0 -2
- package/dist/components/water-flow/utils.js +1 -3
- package/dist/components/water-flow/utils.js.map +1 -1
- package/dist/components/water-flow/water-flow.d.ts +1 -1
- package/dist/components/water-flow/water-flow.js +24 -151
- package/dist/components/water-flow/water-flow.js.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/package.json +8 -9
- package/dist/components/list/NoMore.d.ts +0 -30
- package/dist/components/list/NoMore.js +0 -10
- package/dist/components/list/NoMore.js.map +0 -1
- package/dist/components/list/hooks/useItemSizeCache.d.ts +0 -13
- package/dist/components/list/hooks/useItemSizeCache.js +0 -40
- package/dist/components/list/hooks/useItemSizeCache.js.map +0 -1
- package/dist/components/list/hooks/useMeasureStartOffset.d.ts +0 -10
- package/dist/components/list/hooks/useMeasureStartOffset.js +0 -43
- package/dist/components/list/hooks/useMeasureStartOffset.js.map +0 -1
- package/dist/components/list/hooks/useRefresher.d.ts +0 -74
- package/dist/components/list/hooks/useRefresher.js +0 -503
- package/dist/components/list/hooks/useRefresher.js.map +0 -1
- package/dist/components/list/hooks/useResizeObserver.d.ts +0 -26
- package/dist/components/list/hooks/useResizeObserver.js +0 -152
- package/dist/components/list/hooks/useResizeObserver.js.map +0 -1
- package/dist/components/list/hooks/useScrollCorrection.d.ts +0 -19
- package/dist/components/list/hooks/useScrollCorrection.js +0 -73
- package/dist/components/list/hooks/useScrollCorrection.js.map +0 -1
- package/dist/components/list/utils.d.ts +0 -16
- package/dist/components/list/utils.js +0 -19
- package/dist/components/list/utils.js.map +0 -1
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import { jsx, jsxs
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import { View, ScrollView } from '@tarojs/components';
|
|
3
|
-
import
|
|
4
|
-
import React, { useContext } from 'react';
|
|
5
|
-
import { useItemSizeCache } from './hooks/useItemSizeCache.js';
|
|
6
|
-
import { useMeasureStartOffset } from './hooks/useMeasureStartOffset.js';
|
|
7
|
-
import { useRefresher, DEFAULT_REFRESHER_HEIGHT } from './hooks/useRefresher.js';
|
|
8
|
-
import { useResizeObserver } from './hooks/useResizeObserver.js';
|
|
9
|
-
import { useScrollCorrection } from './hooks/useScrollCorrection.js';
|
|
3
|
+
import React from 'react';
|
|
10
4
|
import { ListItem } from './ListItem.js';
|
|
11
|
-
import { NoMore } from './NoMore.js';
|
|
12
5
|
import { StickyHeader } from './StickyHeader.js';
|
|
13
6
|
import { StickySection } from './StickySection.js';
|
|
14
|
-
import { isH5, isWeapp, supportsNativeRefresher } from './utils.js';
|
|
15
7
|
|
|
16
8
|
// 工具:累加数组
|
|
17
9
|
function accumulate(arr) {
|
|
@@ -36,278 +28,25 @@ function isShaking(diffList) {
|
|
|
36
28
|
// 如果交替次数过多,认为是抖动
|
|
37
29
|
return alternations >= 2;
|
|
38
30
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return !measuredSet.has(index);
|
|
43
|
-
}
|
|
44
|
-
// 小程序端暂不外抛 onItemSizeChange,避免父层重渲染导致 List remount 引发回顶或空白
|
|
45
|
-
function weappDeferItemSizeChange(_index, _size, _onItemSizeChange) { }
|
|
46
|
-
const EMPTY_SCROLL_REF = { current: null };
|
|
47
|
-
/** 任务 4.1/4.2:List 内嵌 WaterFlow/List 时,向子组件提供 scrollRef、containerHeight、startOffset、reportNestedHeightChange */
|
|
48
|
-
const ListScrollElementContext = React.createContext(null);
|
|
49
|
-
/** 抽离 scrollElement 模式逻辑,降低 InnerList 圈复杂度 */
|
|
50
|
-
function useListScrollElement(listType, scrollElement,
|
|
51
|
-
/** 无 Context 时自动测量的 startOffset(上方内容高度),与下方 DOM stacking 对称 */
|
|
52
|
-
measuredStartOffset) {
|
|
53
|
-
var _a, _b;
|
|
54
|
-
const listScrollCtx = useContext(ListScrollElementContext);
|
|
55
|
-
const effectiveScrollElement = scrollElement !== null && scrollElement !== void 0 ? scrollElement : listScrollCtx === null || listScrollCtx === void 0 ? void 0 : listScrollCtx.scrollRef;
|
|
56
|
-
const effectiveStartOffset = (_b = (_a = listScrollCtx === null || listScrollCtx === void 0 ? void 0 : listScrollCtx.startOffset) !== null && _a !== void 0 ? _a : measuredStartOffset) !== null && _b !== void 0 ? _b : 0;
|
|
57
|
-
const useScrollElementMode = listType === 'nested' && !!(effectiveScrollElement && isH5);
|
|
58
|
-
if (listType === 'nested' && !effectiveScrollElement && isH5) {
|
|
59
|
-
// eslint-disable-next-line no-console
|
|
60
|
-
console.warn('[List] type=nested 但无 scrollElement(props 或 Context),回退为 default');
|
|
61
|
-
}
|
|
62
|
-
return { effectiveScrollElement, effectiveStartOffset, useScrollElementMode, hasListScrollCtx: !!listScrollCtx };
|
|
63
|
-
}
|
|
64
|
-
const InnerList = (props, ref) => {
|
|
65
|
-
var _a, _b, _c, _d;
|
|
66
|
-
const { stickyHeader = false, space = 0, height = 400, width = '100%', showScrollbar = true, scrollTop: controlledScrollTop, scrollX = false, scrollY = true, onScroll, onScrollToUpper, onScrollToLower, onScrollStart, onScrollEnd, upperThreshold = 50, lowerThreshold = 50, cacheCount = 2, cacheExtent, enableBackToTop, className, style, children, type: listType = 'default', scrollElement, scrollRef: scrollRefProp, } = props;
|
|
31
|
+
const List = (props) => {
|
|
32
|
+
const isH5 = process.env.TARO_ENV === 'h5';
|
|
33
|
+
const { stickyHeader = false, space = 0, height = 400, width = '100%', showScrollbar = true, scrollTop: controlledScrollTop, scrollX = false, scrollY = true, onScroll, onScrollToUpper, onScrollToLower, upperThreshold = 0, lowerThreshold = 0, cacheCount = 2, style, children, } = props;
|
|
67
34
|
const isHorizontal = scrollX === true;
|
|
68
|
-
const contentWrapperRef = React.useRef(null);
|
|
69
|
-
const result1 = useListScrollElement(listType, scrollElement, 0);
|
|
70
|
-
const measuredStartOffset = useMeasureStartOffset((_a = result1.effectiveScrollElement) !== null && _a !== void 0 ? _a : EMPTY_SCROLL_REF, contentWrapperRef, {
|
|
71
|
-
enabled: result1.useScrollElementMode && !result1.hasListScrollCtx && !!result1.effectiveScrollElement && isH5,
|
|
72
|
-
isHorizontal,
|
|
73
|
-
});
|
|
74
|
-
const { effectiveScrollElement, effectiveStartOffset, useScrollElementMode } = useListScrollElement(listType, scrollElement, measuredStartOffset);
|
|
75
35
|
const DEFAULT_ITEM_WIDTH = 120;
|
|
76
36
|
const DEFAULT_ITEM_HEIGHT = 40;
|
|
77
37
|
// 滚动状态管理
|
|
78
38
|
const containerRef = React.useRef(null);
|
|
79
|
-
// 生成唯一 List ID(用于小程序 ResizeObserver)
|
|
80
|
-
const listId = React.useMemo(() => `list-${Math.random().toString(36).slice(2, 11)}`, []);
|
|
81
39
|
// 渲染偏移量 - 用于计算应该渲染哪些元素
|
|
82
40
|
const [renderOffset, setRenderOffset] = React.useState(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
83
|
-
//
|
|
41
|
+
// 滚动视图偏移量 - 只在滚动结束或明确请求时更新到ScrollView
|
|
84
42
|
const [scrollViewOffset, setScrollViewOffset] = React.useState(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
85
|
-
|
|
86
|
-
const [isUserScrolling, setIsUserScrolling] = React.useState(false);
|
|
87
|
-
// isUserScrolling 的 ref 镜像,供异步上下文读取最新值
|
|
88
|
-
const isUserScrollingRef = React.useRef(false);
|
|
89
|
-
const initialContainerLength = typeof (isHorizontal ? width : height) === 'number' ? (isHorizontal ? width : height) : 400;
|
|
90
|
-
const [containerLength, setContainerLength] = React.useState(initialContainerLength);
|
|
91
|
-
// 用容器实际尺寸更新视口长度,避免 props 与 CSS 不一致导致底部空白
|
|
92
|
-
React.useEffect(() => {
|
|
93
|
-
const el = containerRef.current;
|
|
94
|
-
if (!el || typeof ResizeObserver === 'undefined')
|
|
95
|
-
return;
|
|
96
|
-
const ro = new ResizeObserver((entries) => {
|
|
97
|
-
for (const entry of entries) {
|
|
98
|
-
const { contentRect } = entry;
|
|
99
|
-
const measured = isHorizontal ? contentRect.width : contentRect.height;
|
|
100
|
-
if (measured > 0)
|
|
101
|
-
setContainerLength(measured);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
ro.observe(el);
|
|
105
|
-
return () => ro.disconnect();
|
|
106
|
-
}, [isHorizontal]);
|
|
43
|
+
const [containerLength] = React.useState(typeof (isHorizontal ? width : height) === 'number' ? (isHorizontal ? width : height) : 400);
|
|
107
44
|
// 滚动追踪相关refs
|
|
108
45
|
const isScrollingRef = React.useRef(false);
|
|
109
46
|
const lastScrollTopRef = React.useRef(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
110
47
|
const scrollDiffListRef = React.useRef([0, 0, 0]);
|
|
111
48
|
const scrollTimeoutRef = React.useRef(null);
|
|
112
|
-
// H5:仅程序性滚动时写回 DOM scrollTop,用户滑动结束后不写回避免卡顿
|
|
113
|
-
const programmaticScrollRef = React.useRef(false);
|
|
114
|
-
// 小程序端程序性滚动冷却期:handleScroll 只更新 renderOffset,避免 scrollIntoView 后被原生回调拉回
|
|
115
|
-
const programmaticCooldownRef = React.useRef(false);
|
|
116
|
-
const programmaticCooldownTimerRef = React.useRef(null);
|
|
117
|
-
const scrollViewOffsetRef = React.useRef(0);
|
|
118
|
-
// 处理渲染偏移量更新。
|
|
119
|
-
// syncToScrollView=true:程序性滚动,立即同步 scrollViewOffset。
|
|
120
|
-
// syncToScrollView=false:用户滑动,仅更新 renderOffset。weapp 采用 recycle-view 策略:不把用户滑动位置同步到 scrollViewOffset,避免「传滞后值拉回」和「从有到无归顶」。
|
|
121
|
-
const updateRenderOffset = React.useCallback((newOffset, syncToScrollView, source) => {
|
|
122
|
-
lastScrollTopRef.current = newOffset;
|
|
123
|
-
isScrollingRef.current = true;
|
|
124
|
-
if (scrollTimeoutRef.current) {
|
|
125
|
-
clearTimeout(scrollTimeoutRef.current);
|
|
126
|
-
}
|
|
127
|
-
setRenderOffset(newOffset); // 始终更新虚拟列表用到的偏移
|
|
128
|
-
if (syncToScrollView) {
|
|
129
|
-
isUserScrollingRef.current = false;
|
|
130
|
-
setIsUserScrolling(false);
|
|
131
|
-
// 小程序:target===sv 时 ScrollView 认为无变化不滚动→白屏;imperative/scrollIntoView 时先传中间值再 RAF 传 target 强制触发
|
|
132
|
-
// target=0 用 +0.01;target>0 用 -0.01(统一 +0.01 到底部会被 clamp 成同值无效)
|
|
133
|
-
const same = isWeapp && Math.abs(scrollViewOffsetRef.current - newOffset) < 1;
|
|
134
|
-
const needForce = same && (source === 'imperative' || source === 'scrollIntoView');
|
|
135
|
-
if (needForce) {
|
|
136
|
-
const intermediate = newOffset > 0 ? newOffset - 0.01 : 0.01;
|
|
137
|
-
setScrollViewOffset(intermediate);
|
|
138
|
-
requestAnimationFrame(() => {
|
|
139
|
-
setScrollViewOffset(newOffset);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
setScrollViewOffset(newOffset);
|
|
144
|
-
}
|
|
145
|
-
programmaticScrollRef.current = true;
|
|
146
|
-
if (isWeapp) {
|
|
147
|
-
programmaticCooldownRef.current = true;
|
|
148
|
-
if (programmaticCooldownTimerRef.current) {
|
|
149
|
-
clearTimeout(programmaticCooldownTimerRef.current);
|
|
150
|
-
}
|
|
151
|
-
programmaticCooldownTimerRef.current = setTimeout(() => {
|
|
152
|
-
programmaticCooldownRef.current = false;
|
|
153
|
-
}, 500);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
isUserScrollingRef.current = true;
|
|
158
|
-
setIsUserScrolling(true);
|
|
159
|
-
}
|
|
160
|
-
scrollTimeoutRef.current = setTimeout(() => {
|
|
161
|
-
isScrollingRef.current = false;
|
|
162
|
-
if (!syncToScrollView) {
|
|
163
|
-
isUserScrollingRef.current = false;
|
|
164
|
-
setIsUserScrolling(false);
|
|
165
|
-
// weapp recycle-view 策略:用户滑动结束后不同步 scrollViewOffset,保持 pass 的值不变,避免 从有到无 归顶
|
|
166
|
-
if (!isWeapp) {
|
|
167
|
-
setScrollViewOffset(lastScrollTopRef.current);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}, isWeapp ? 200 : 150);
|
|
171
|
-
}, []);
|
|
172
|
-
// 暴露给外部的实例方法:通过 ref.scroll({ top / left }) 进行程序性滚动
|
|
173
|
-
React.useImperativeHandle(ref, () => ({
|
|
174
|
-
scroll(options) {
|
|
175
|
-
const opts = options !== null && options !== void 0 ? options : {};
|
|
176
|
-
const isHorizontal = scrollX === true;
|
|
177
|
-
const top = typeof opts.top === 'number' ? opts.top : undefined;
|
|
178
|
-
const left = typeof opts.left === 'number' ? opts.left : undefined;
|
|
179
|
-
let targetOffset = 0;
|
|
180
|
-
if (isHorizontal) {
|
|
181
|
-
if (typeof left === 'number') {
|
|
182
|
-
targetOffset = left;
|
|
183
|
-
}
|
|
184
|
-
else if (typeof top === 'number') {
|
|
185
|
-
targetOffset = top;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
if (typeof top === 'number') {
|
|
190
|
-
targetOffset = top;
|
|
191
|
-
}
|
|
192
|
-
else if (typeof left === 'number') {
|
|
193
|
-
targetOffset = left;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (!Number.isFinite(targetOffset)) {
|
|
197
|
-
targetOffset = 0;
|
|
198
|
-
}
|
|
199
|
-
// 任务 2.2:scrollElement 模式下直接操作外部容器(内嵌时需加上 startOffset)
|
|
200
|
-
const el = effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current;
|
|
201
|
-
if (el && isH5) {
|
|
202
|
-
const scrollTarget = targetOffset + effectiveStartOffset;
|
|
203
|
-
if (isHorizontal) {
|
|
204
|
-
el.scrollTo({ left: scrollTarget });
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
el.scrollTo({ top: scrollTarget });
|
|
208
|
-
}
|
|
209
|
-
updateRenderOffset(targetOffset, false, 'scrollElement');
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
updateRenderOffset(targetOffset, true, 'imperative');
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
}), [scrollX, effectiveScrollElement, effectiveStartOffset, isH5, isHorizontal, updateRenderOffset]);
|
|
216
|
-
// 提取 Refresher 配置(List 属性为 base,Refresher 子组件覆盖)
|
|
217
|
-
const refresherConfig = React.useMemo(() => {
|
|
218
|
-
const listRefresherEnabled = props.refresherEnabled !== false && (props.refresherEnabled === true || props.onRefresherRefresh != null);
|
|
219
|
-
const baseFromList = listRefresherEnabled
|
|
220
|
-
? {
|
|
221
|
-
refresherEnabled: props.refresherEnabled,
|
|
222
|
-
refresherThreshold: props.refresherThreshold,
|
|
223
|
-
refresherDefaultStyle: props.refresherDefaultStyle,
|
|
224
|
-
refresherBackground: props.refresherBackground,
|
|
225
|
-
refresherTriggered: props.refresherTriggered,
|
|
226
|
-
onRefresherPulling: props.onRefresherPulling,
|
|
227
|
-
onRefresherRefresh: props.onRefresherRefresh,
|
|
228
|
-
onRefresherRestore: props.onRefresherRestore,
|
|
229
|
-
onRefresherAbort: props.onRefresherAbort,
|
|
230
|
-
onRefresherWillRefresh: props.onRefresherWillRefresh,
|
|
231
|
-
onRefresherStatusChange: props.onRefresherStatusChange,
|
|
232
|
-
}
|
|
233
|
-
: null;
|
|
234
|
-
const isRefresherComponent = (child) => {
|
|
235
|
-
const type = child.type;
|
|
236
|
-
return (type === null || type === void 0 ? void 0 : type.displayName) === 'Refresher' || (type === null || type === void 0 ? void 0 : type.name) === 'Refresher';
|
|
237
|
-
};
|
|
238
|
-
let refresherChildProps = null;
|
|
239
|
-
let refresherCount = 0;
|
|
240
|
-
React.Children.forEach(children, (child) => {
|
|
241
|
-
if (React.isValidElement(child) && isRefresherComponent(child)) {
|
|
242
|
-
refresherCount++;
|
|
243
|
-
if (refresherCount > 1) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
refresherChildProps = child.props;
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
if (refresherChildProps != null) {
|
|
250
|
-
const base = baseFromList !== null && baseFromList !== void 0 ? baseFromList : {};
|
|
251
|
-
// Refresher 子组件的配置覆盖 List 的配置
|
|
252
|
-
return Object.assign(Object.assign({}, base), refresherChildProps);
|
|
253
|
-
}
|
|
254
|
-
return baseFromList;
|
|
255
|
-
}, [
|
|
256
|
-
children,
|
|
257
|
-
props.refresherEnabled,
|
|
258
|
-
props.refresherThreshold,
|
|
259
|
-
props.refresherDefaultStyle,
|
|
260
|
-
props.refresherBackground,
|
|
261
|
-
props.refresherTriggered,
|
|
262
|
-
props.onRefresherPulling,
|
|
263
|
-
props.onRefresherRefresh,
|
|
264
|
-
props.onRefresherRestore,
|
|
265
|
-
props.onRefresherAbort,
|
|
266
|
-
props.onRefresherWillRefresh,
|
|
267
|
-
props.onRefresherStatusChange,
|
|
268
|
-
]);
|
|
269
|
-
// 提取 NoMore 配置
|
|
270
|
-
const noMoreConfig = React.useMemo(() => {
|
|
271
|
-
var _a;
|
|
272
|
-
let config = null;
|
|
273
|
-
let noMoreCount = 0;
|
|
274
|
-
// 从子组件中提取 NoMore
|
|
275
|
-
React.Children.forEach(children, (child) => {
|
|
276
|
-
if (React.isValidElement(child) && child.type === NoMore) {
|
|
277
|
-
noMoreCount++;
|
|
278
|
-
if (noMoreCount > 1) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
const childProps = child.props;
|
|
282
|
-
config = Object.assign(Object.assign({}, childProps), { visible: childProps.visible !== false });
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
// Props 方式转换为配置(优先级低于子组件)
|
|
286
|
-
if (props.showNoMore && !config) {
|
|
287
|
-
config = {
|
|
288
|
-
visible: true,
|
|
289
|
-
text: props.noMoreText,
|
|
290
|
-
style: props.noMoreStyle,
|
|
291
|
-
children: (_a = props.renderNoMore) === null || _a === void 0 ? void 0 : _a.call(props)
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
return config;
|
|
295
|
-
}, [children, props.showNoMore, props.noMoreText, props.noMoreStyle, props.renderNoMore]);
|
|
296
|
-
// Refresher 平台适配:H5 用 addImperativeTouchListeners
|
|
297
|
-
const { scrollViewRefresherProps, scrollViewRefresherHandlers, h5RefresherProps, addImperativeTouchListeners, renderRefresherContent, } = useRefresher(refresherConfig, isH5 && refresherConfig && !supportsNativeRefresher ? 0 : renderOffset, useScrollElementMode
|
|
298
|
-
? (ev) => { var _a, _b; return (_b = (ev.target != null && ((_a = contentWrapperRef.current) === null || _a === void 0 ? void 0 : _a.contains(ev.target)))) !== null && _b !== void 0 ? _b : false; }
|
|
299
|
-
: undefined);
|
|
300
|
-
const refresherTeardownRef = React.useRef(null);
|
|
301
|
-
const addImperativeTouchListenersRef = React.useRef(addImperativeTouchListeners);
|
|
302
|
-
addImperativeTouchListenersRef.current = addImperativeTouchListeners;
|
|
303
|
-
React.useEffect(() => () => {
|
|
304
|
-
if (refresherTeardownRef.current)
|
|
305
|
-
refresherTeardownRef.current();
|
|
306
|
-
}, []);
|
|
307
|
-
// H5 下拉刷新顶栏高度(默认 50px,来自 useRefresher)
|
|
308
|
-
const refresherHeightForH5 = (isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false) ? DEFAULT_REFRESHER_HEIGHT : 0;
|
|
309
49
|
// 解析分组结构,只支持 StickySection 和 ListItem 作为直接子组件
|
|
310
|
-
// 过滤掉 Refresher 和 NoMore 组件
|
|
311
50
|
const sections = React.useMemo(() => {
|
|
312
51
|
const result = [];
|
|
313
52
|
const defaultItems = [];
|
|
@@ -329,109 +68,14 @@ const InnerList = (props, ref) => {
|
|
|
329
68
|
// 普通 ListItem
|
|
330
69
|
defaultItems.push(child);
|
|
331
70
|
}
|
|
332
|
-
// 忽略 Refresher 和 NoMore 组件(已在上面提取配置)
|
|
333
71
|
});
|
|
334
72
|
if (defaultItems.length > 0) {
|
|
335
73
|
result.push({ header: null, items: defaultItems, key: 'default' });
|
|
336
74
|
}
|
|
337
75
|
return result;
|
|
338
76
|
}, [children]);
|
|
339
|
-
//
|
|
340
|
-
const
|
|
341
|
-
const estimatedSize = (_b = props.estimatedItemSize) !== null && _b !== void 0 ? _b : defaultEstimatedSize;
|
|
342
|
-
// 计算总 item 数量(跨所有 section)
|
|
343
|
-
const totalItemCount = React.useMemo(() => {
|
|
344
|
-
return sections.reduce((sum, section) => sum + section.items.length, 0);
|
|
345
|
-
}, [sections]);
|
|
346
|
-
// 存储元素引用(用于 ResizeObserver)
|
|
347
|
-
const itemRefsRef = React.useRef(new Map());
|
|
348
|
-
// 存储 header 元素引用(用于 ResizeObserver)
|
|
349
|
-
const headerRefsRef = React.useRef(new Map());
|
|
350
|
-
// 小程序端:已完成 SelectorQuery 测量的 item 索引集,避免 refCallback 重复触发测量导致无限循环
|
|
351
|
-
const weappMeasuredItemsRef = React.useRef(new Set());
|
|
352
|
-
// 动态尺寸缓存更新版本:setItemSize 后递增,用于驱动 sectionOffsets/totalLength 与 item 定位重算
|
|
353
|
-
const [sizeCacheVersion, setSizeCacheVersion] = React.useState(0);
|
|
354
|
-
const sizeCacheRafRef = React.useRef(null);
|
|
355
|
-
// 小程序端:测量触发 re-render 时,reflow 帧传 eff 并同步 scrollViewOffset,避免下一帧传 0 导致跳变
|
|
356
|
-
const measureScrollProtectRef = React.useRef(false);
|
|
357
|
-
// 动态尺寸缓存
|
|
358
|
-
const sizeCache = useItemSizeCache({
|
|
359
|
-
isHorizontal,
|
|
360
|
-
estimatedItemSize: estimatedSize,
|
|
361
|
-
itemCount: totalItemCount
|
|
362
|
-
});
|
|
363
|
-
// header 动态尺寸缓存(sectionIndex -> size)
|
|
364
|
-
const headerSizeCacheRef = React.useRef(new Map());
|
|
365
|
-
// 滚动修正的可见起始索引(后续更新)
|
|
366
|
-
const visibleStartIndexRef = React.useRef(0);
|
|
367
|
-
// ScrollTop 修正(仅 H5):动高时尺寸变化自动修正 scrollTop
|
|
368
|
-
const scrollCorrectionEnabled = !isWeapp && props.useResizeObserver === true;
|
|
369
|
-
const scrollCorrection = useScrollCorrection({
|
|
370
|
-
enabled: scrollCorrectionEnabled,
|
|
371
|
-
visibleStartIndexRef,
|
|
372
|
-
setScrollOffset: (offsetOrUpdater) => {
|
|
373
|
-
const newOffset = typeof offsetOrUpdater === 'function'
|
|
374
|
-
? offsetOrUpdater(renderOffset)
|
|
375
|
-
: offsetOrUpdater;
|
|
376
|
-
updateRenderOffset(newOffset, true, 'scrollCorrection'); // 程序性修正需同步到 ScrollView
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
const scrollCorrectionRef = React.useRef(scrollCorrection);
|
|
380
|
-
scrollCorrectionRef.current = scrollCorrection;
|
|
381
|
-
const onScrollRef = React.useRef(onScroll);
|
|
382
|
-
onScrollRef.current = onScroll;
|
|
383
|
-
const onScrollToUpperRef = React.useRef(onScrollToUpper);
|
|
384
|
-
onScrollToUpperRef.current = onScrollToUpper;
|
|
385
|
-
const onScrollToLowerRef = React.useRef(onScrollToLower);
|
|
386
|
-
onScrollToLowerRef.current = onScrollToLower;
|
|
387
|
-
const thresholdRef = React.useRef({ upper: upperThreshold, lower: lowerThreshold });
|
|
388
|
-
thresholdRef.current = { upper: upperThreshold, lower: lowerThreshold };
|
|
389
|
-
// 小程序 + 动高(virtual-list 风格):测量变化后同帧重排,不做程序性 scrollTop 回拉
|
|
390
|
-
const scheduleWeappDynamicReflow = React.useCallback(() => {
|
|
391
|
-
if (!isWeapp || props.useResizeObserver !== true)
|
|
392
|
-
return;
|
|
393
|
-
if (sizeCacheRafRef.current != null)
|
|
394
|
-
return;
|
|
395
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
396
|
-
sizeCacheRafRef.current = null;
|
|
397
|
-
measureScrollProtectRef.current = true;
|
|
398
|
-
setSizeCacheVersion((v) => v + 1);
|
|
399
|
-
});
|
|
400
|
-
}, [isWeapp, props.useResizeObserver]);
|
|
401
|
-
// ResizeObserver(当启用动态测量时)
|
|
402
|
-
const resizeObserver = useResizeObserver({
|
|
403
|
-
enabled: props.useResizeObserver === true,
|
|
404
|
-
isHorizontal,
|
|
405
|
-
listId,
|
|
406
|
-
onResize: (index, size) => {
|
|
407
|
-
var _a;
|
|
408
|
-
const oldSize = sizeCache.getItemSize(index);
|
|
409
|
-
sizeCache.setItemSize(index, size);
|
|
410
|
-
if (Math.abs(oldSize - size) >= 1) {
|
|
411
|
-
if (isWeapp && props.useResizeObserver === true) {
|
|
412
|
-
scheduleWeappDynamicReflow();
|
|
413
|
-
}
|
|
414
|
-
else if (sizeCacheRafRef.current == null) {
|
|
415
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
416
|
-
sizeCacheRafRef.current = null;
|
|
417
|
-
setSizeCacheVersion((v) => v + 1);
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
// 触发 ScrollTop 修正
|
|
422
|
-
scrollCorrection.recordSizeChange(index, oldSize, size);
|
|
423
|
-
// 小程序:延迟 onItemSizeChange,避免父组件重渲染导致 List remount
|
|
424
|
-
// H5:直接回调
|
|
425
|
-
if (isWeapp) {
|
|
426
|
-
weappDeferItemSizeChange(index, size, props.onItemSizeChange);
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
(_a = props.onItemSizeChange) === null || _a === void 0 ? void 0 : _a.call(props, index, size);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
// 工具:获取 header 默认/估算尺寸
|
|
434
|
-
const getDefaultHeaderSize = () => {
|
|
77
|
+
// 工具:获取 header 尺寸,确保所有 header 相关逻辑一致
|
|
78
|
+
const getHeaderSize = () => {
|
|
435
79
|
if (isHorizontal) {
|
|
436
80
|
if (typeof props.headerWidth === 'number')
|
|
437
81
|
return props.headerWidth;
|
|
@@ -455,24 +99,8 @@ const InnerList = (props, ref) => {
|
|
|
455
99
|
return DEFAULT_ITEM_HEIGHT;
|
|
456
100
|
}
|
|
457
101
|
};
|
|
458
|
-
// 工具:获取
|
|
459
|
-
const
|
|
460
|
-
// 如果启用动态测量,优先从缓存读取
|
|
461
|
-
if (props.useResizeObserver === true) {
|
|
462
|
-
const cached = headerSizeCacheRef.current.get(sectionIndex);
|
|
463
|
-
if (cached != null && cached > 0)
|
|
464
|
-
return cached;
|
|
465
|
-
}
|
|
466
|
-
// 否则返回默认尺寸
|
|
467
|
-
return getDefaultHeaderSize();
|
|
468
|
-
}, [props.useResizeObserver, props.headerHeight, props.headerWidth, props.itemHeight, props.itemWidth, props.itemSize, props.itemData, isHorizontal]);
|
|
469
|
-
// 工具:获取 item 尺寸,支持函数/props/默认值/动态测量
|
|
470
|
-
const getItemSize = React.useCallback((index) => {
|
|
471
|
-
// 优先级1:如果启用动态测量,从缓存读取
|
|
472
|
-
if (props.useResizeObserver === true) {
|
|
473
|
-
return sizeCache.getItemSize(index);
|
|
474
|
-
}
|
|
475
|
-
// 优先级2:固定尺寸或函数计算
|
|
102
|
+
// 工具:获取 item 尺寸,支持函数/props/默认值
|
|
103
|
+
const getItemSize = (index) => {
|
|
476
104
|
if (isHorizontal) {
|
|
477
105
|
if (typeof props.itemWidth === 'number')
|
|
478
106
|
return props.itemWidth;
|
|
@@ -491,24 +119,20 @@ const InnerList = (props, ref) => {
|
|
|
491
119
|
return props.itemSize(index, props.itemData) || DEFAULT_ITEM_HEIGHT;
|
|
492
120
|
return DEFAULT_ITEM_HEIGHT;
|
|
493
121
|
}
|
|
494
|
-
}
|
|
495
|
-
//
|
|
122
|
+
};
|
|
123
|
+
// 计算分组累积高度/宽度
|
|
496
124
|
const sectionOffsets = React.useMemo(() => {
|
|
497
125
|
const offsets = [0];
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
// 使用全局索引计算每个 item 的尺寸
|
|
502
|
-
const itemSizes = section.items.map((_, localIdx) => getItemSize(globalItemIndex + localIdx));
|
|
126
|
+
sections.forEach((section) => {
|
|
127
|
+
const headerSize = getHeaderSize();
|
|
128
|
+
const itemSizes = section.items.map((_, i) => getItemSize(i));
|
|
503
129
|
const groupSize = (section.header ? headerSize : 0) +
|
|
504
130
|
itemSizes.reduce((a, b) => a + b, 0) +
|
|
505
131
|
Math.max(0, section.items.length) * space;
|
|
506
132
|
offsets.push(offsets[offsets.length - 1] + groupSize);
|
|
507
|
-
// 累加当前 section 的 item 数量
|
|
508
|
-
globalItemIndex += section.items.length;
|
|
509
133
|
});
|
|
510
134
|
return offsets;
|
|
511
|
-
}, [sections, space, isHorizontal, props.headerHeight, props.headerWidth, props.itemHeight, props.itemWidth, props.itemSize, props.itemData
|
|
135
|
+
}, [sections, space, isHorizontal, props.headerHeight, props.headerWidth, props.itemHeight, props.itemWidth, props.itemSize, props.itemData]);
|
|
512
136
|
// 外层虚拟滚动:可见分组
|
|
513
137
|
const [startSection, endSection] = React.useMemo(() => {
|
|
514
138
|
let start = 0;
|
|
@@ -527,351 +151,119 @@ const InnerList = (props, ref) => {
|
|
|
527
151
|
}
|
|
528
152
|
return [start, end];
|
|
529
153
|
}, [renderOffset, containerLength, sectionOffsets, sections.length, cacheCount]);
|
|
530
|
-
//
|
|
531
|
-
const [visibleStartItem, visibleEndItem] = React.useMemo(() => {
|
|
532
|
-
const viewportTop = renderOffset;
|
|
533
|
-
const viewportBottom = renderOffset + containerLength;
|
|
534
|
-
let firstVisible = -1;
|
|
535
|
-
let lastVisible = -1;
|
|
536
|
-
let globalIndex = 0;
|
|
537
|
-
for (let s = 0; s < sections.length; s++) {
|
|
538
|
-
const section = sections[s];
|
|
539
|
-
const headerSize = getHeaderSize(s);
|
|
540
|
-
const sectionStart = sectionOffsets[s] + (section.header ? headerSize : 0);
|
|
541
|
-
let itemTop = sectionStart;
|
|
542
|
-
for (let i = 0; i < section.items.length; i++) {
|
|
543
|
-
const itemSize = getItemSize(globalIndex);
|
|
544
|
-
const itemBottom = itemTop + itemSize;
|
|
545
|
-
// 判断 item 本身(不含 space)是否与视口相交
|
|
546
|
-
if (itemBottom > viewportTop && itemTop < viewportBottom) {
|
|
547
|
-
if (firstVisible < 0)
|
|
548
|
-
firstVisible = globalIndex;
|
|
549
|
-
lastVisible = globalIndex;
|
|
550
|
-
}
|
|
551
|
-
// 下一个 item 的起始位置 = 当前 item 结束 + space
|
|
552
|
-
itemTop = itemBottom + space;
|
|
553
|
-
globalIndex++;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
if (firstVisible < 0 || lastVisible < 0)
|
|
557
|
-
return [0, 0];
|
|
558
|
-
return [firstVisible, lastVisible];
|
|
559
|
-
}, [renderOffset, containerLength, sections, sectionOffsets, getHeaderSize, getItemSize, space]);
|
|
560
|
-
// 触发 onScrollIndex 回调(带防重复)
|
|
561
|
-
const lastVisibleRangeRef = React.useRef({ start: -1, end: -1 });
|
|
154
|
+
// 触顶/触底事件
|
|
562
155
|
React.useEffect(() => {
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
if (lastVisibleRangeRef.current.start !== visibleStartItem ||
|
|
566
|
-
lastVisibleRangeRef.current.end !== visibleEndItem) {
|
|
567
|
-
lastVisibleRangeRef.current = { start: visibleStartItem, end: visibleEndItem };
|
|
568
|
-
props.onScrollIndex(visibleStartItem, visibleEndItem);
|
|
569
|
-
}
|
|
156
|
+
if (onScrollToUpper && renderOffset <= (upperThreshold > 0 ? sectionOffsets[upperThreshold] : 0)) {
|
|
157
|
+
onScrollToUpper();
|
|
570
158
|
}
|
|
571
|
-
|
|
159
|
+
if (onScrollToLower && renderOffset + containerLength >= sectionOffsets[sectionOffsets.length - 1] - (lowerThreshold > 0 ? sectionOffsets[sectionOffsets.length - 1] - sectionOffsets[sections.length - lowerThreshold] : 0)) {
|
|
160
|
+
onScrollToLower();
|
|
161
|
+
}
|
|
162
|
+
}, [renderOffset, containerLength, sectionOffsets, sections.length, upperThreshold, lowerThreshold, onScrollToUpper, onScrollToLower]);
|
|
163
|
+
// 处理渲染偏移量更新
|
|
164
|
+
const updateRenderOffset = React.useCallback((newOffset) => {
|
|
165
|
+
lastScrollTopRef.current = newOffset;
|
|
166
|
+
isScrollingRef.current = true;
|
|
167
|
+
if (scrollTimeoutRef.current) {
|
|
168
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
169
|
+
}
|
|
170
|
+
setRenderOffset(newOffset); // 立即更新渲染偏移量
|
|
171
|
+
// 平台适配:微信小程序使用延时,其他平台立即更新
|
|
172
|
+
const isWeapp = process.env.TARO_ENV === 'weapp';
|
|
173
|
+
if (isWeapp) {
|
|
174
|
+
// 微信小程序:使用延时避免抖动
|
|
175
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
176
|
+
isScrollingRef.current = false;
|
|
177
|
+
setScrollViewOffset(newOffset); // 滚动结束后,同步滚动视图偏移量
|
|
178
|
+
}, 200);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// 其他平台:立即更新以获得更好的响应性
|
|
182
|
+
setScrollViewOffset(newOffset); // 立即更新滚动视图偏移量
|
|
183
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
184
|
+
isScrollingRef.current = false;
|
|
185
|
+
}, 200);
|
|
186
|
+
}
|
|
187
|
+
}, []);
|
|
188
|
+
// 智能滚动处理函数
|
|
572
189
|
const handleScroll = React.useCallback((e) => {
|
|
190
|
+
// 兼容Stencil版本和React版本的事件结构
|
|
573
191
|
let newOffset;
|
|
574
192
|
if (e.detail) {
|
|
193
|
+
// React版本的事件结构
|
|
575
194
|
newOffset = isHorizontal ? e.detail.scrollLeft : e.detail.scrollTop;
|
|
576
195
|
}
|
|
577
196
|
else {
|
|
197
|
+
// Stencil版本的事件结构
|
|
578
198
|
newOffset = isHorizontal ? e.scrollLeft : e.scrollTop;
|
|
579
199
|
}
|
|
580
|
-
const
|
|
581
|
-
const diff = effectiveOffset - lastScrollTopRef.current;
|
|
200
|
+
const diff = newOffset - lastScrollTopRef.current;
|
|
582
201
|
scrollDiffListRef.current.shift();
|
|
583
202
|
scrollDiffListRef.current.push(diff);
|
|
584
|
-
|
|
585
|
-
if (
|
|
586
|
-
return;
|
|
587
|
-
if (programmaticCooldownRef.current) {
|
|
588
|
-
lastScrollTopRef.current = effectiveOffset;
|
|
589
|
-
setRenderOffset(effectiveOffset);
|
|
590
|
-
onScroll === null || onScroll === void 0 ? void 0 : onScroll({
|
|
591
|
-
scrollTop: isHorizontal ? 0 : newOffset,
|
|
592
|
-
scrollLeft: isHorizontal ? newOffset : 0
|
|
593
|
-
});
|
|
203
|
+
// 只保留抖动检测,移除方向检测
|
|
204
|
+
if (isScrollingRef.current && isShaking(scrollDiffListRef.current)) {
|
|
594
205
|
return;
|
|
595
206
|
}
|
|
596
|
-
|
|
597
|
-
updateRenderOffset(effectiveOffset, false, 'onScroll');
|
|
207
|
+
updateRenderOffset(newOffset); // 直接更新渲染偏移量
|
|
598
208
|
onScroll === null || onScroll === void 0 ? void 0 : onScroll({
|
|
599
209
|
scrollTop: isHorizontal ? 0 : newOffset,
|
|
600
210
|
scrollLeft: isHorizontal ? newOffset : 0
|
|
601
211
|
});
|
|
602
|
-
}, [isHorizontal, onScroll, updateRenderOffset,
|
|
603
|
-
//
|
|
604
|
-
const handleNativeScrollEnd = React.useCallback(() => {
|
|
605
|
-
if (isWeapp) {
|
|
606
|
-
if (scrollTimeoutRef.current) {
|
|
607
|
-
clearTimeout(scrollTimeoutRef.current);
|
|
608
|
-
scrollTimeoutRef.current = null;
|
|
609
|
-
}
|
|
610
|
-
if (isUserScrollingRef.current) {
|
|
611
|
-
isScrollingRef.current = false;
|
|
612
|
-
isUserScrollingRef.current = false;
|
|
613
|
-
setIsUserScrolling(false);
|
|
614
|
-
// recycle-view 策略:不把用户滑动位置同步到 scrollViewOffset
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
onScrollEnd === null || onScrollEnd === void 0 ? void 0 : onScrollEnd();
|
|
618
|
-
}, [isWeapp, onScrollEnd]);
|
|
619
|
-
// scrollElement 模式:containerLength 从 scrollElement 获取(任务 1.6)
|
|
620
|
-
React.useEffect(() => {
|
|
621
|
-
if (!effectiveScrollElement || !isH5)
|
|
622
|
-
return;
|
|
623
|
-
const el = effectiveScrollElement.current;
|
|
624
|
-
if (!el || typeof ResizeObserver === 'undefined')
|
|
625
|
-
return;
|
|
626
|
-
const update = () => {
|
|
627
|
-
const measured = isHorizontal ? el.clientWidth : el.clientHeight;
|
|
628
|
-
if (measured > 0) {
|
|
629
|
-
setContainerLength(measured);
|
|
630
|
-
}
|
|
631
|
-
// scrollElement 模式下 useLayoutEffect 可能早于 ref 挂载,ResizeObserver 作为兜底确保 scrollRef 正确
|
|
632
|
-
if (scrollRefProp)
|
|
633
|
-
scrollRefProp.current = el;
|
|
634
|
-
};
|
|
635
|
-
update();
|
|
636
|
-
const ro = new ResizeObserver(update);
|
|
637
|
-
ro.observe(el);
|
|
638
|
-
return () => ro.disconnect();
|
|
639
|
-
}, [effectiveScrollElement, isH5, isHorizontal]);
|
|
640
|
-
// scrollElement 模式:监听外部滚动,驱动 renderOffset
|
|
641
|
-
// 使用 ref 保存 scrollCorrection/onScroll,避免每次渲染 deps 变化导致 effect 反复执行形成循环
|
|
642
|
-
const effectiveStartOffsetRef = React.useRef(effectiveStartOffset);
|
|
643
|
-
effectiveStartOffsetRef.current = effectiveStartOffset;
|
|
644
|
-
React.useEffect(() => {
|
|
645
|
-
if (!effectiveScrollElement || !isH5)
|
|
646
|
-
return;
|
|
647
|
-
const el = effectiveScrollElement.current;
|
|
648
|
-
if (!el)
|
|
649
|
-
return;
|
|
650
|
-
const handler = () => {
|
|
651
|
-
var _a, _b, _c;
|
|
652
|
-
const scrollPos = isHorizontal ? el.scrollLeft : el.scrollTop;
|
|
653
|
-
const adjustedPos = scrollPos - effectiveStartOffsetRef.current;
|
|
654
|
-
const clientSize = isHorizontal ? el.clientWidth : el.clientHeight;
|
|
655
|
-
const scrollSize = isHorizontal ? el.scrollWidth : el.scrollHeight;
|
|
656
|
-
scrollCorrectionRef.current.markUserScrolling();
|
|
657
|
-
updateRenderOffset(adjustedPos, false, 'scrollElement');
|
|
658
|
-
(_a = onScrollRef.current) === null || _a === void 0 ? void 0 : _a.call(onScrollRef, {
|
|
659
|
-
scrollTop: isHorizontal ? 0 : scrollPos,
|
|
660
|
-
scrollLeft: isHorizontal ? scrollPos : 0
|
|
661
|
-
});
|
|
662
|
-
// 任务 2.1:onScrollToUpper / onScrollToLower(scrollElement 模式用 adjustedPos,使有上方混排时以 List 顶为基准)
|
|
663
|
-
const { upper, lower } = thresholdRef.current;
|
|
664
|
-
const effectiveAdjusted = Math.max(0, adjustedPos);
|
|
665
|
-
if (effectiveAdjusted <= upper) {
|
|
666
|
-
(_b = onScrollToUpperRef.current) === null || _b === void 0 ? void 0 : _b.call(onScrollToUpperRef);
|
|
667
|
-
}
|
|
668
|
-
if (scrollPos + clientSize >= scrollSize - lower) {
|
|
669
|
-
(_c = onScrollToLowerRef.current) === null || _c === void 0 ? void 0 : _c.call(onScrollToLowerRef);
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
// 同步初始 scroll 位置(内嵌时需减去 startOffset)
|
|
673
|
-
const initialScroll = isHorizontal ? el.scrollLeft : el.scrollTop;
|
|
674
|
-
updateRenderOffset(initialScroll - effectiveStartOffsetRef.current, false, 'scrollElement');
|
|
675
|
-
el.addEventListener('scroll', handler, { passive: true });
|
|
676
|
-
return () => el.removeEventListener('scroll', handler);
|
|
677
|
-
}, [effectiveScrollElement, isH5, isHorizontal, updateRenderOffset]);
|
|
678
|
-
// 任务 2.3:暴露 scrollRef 给父组件(供内层 List/WaterFlow 传入 scrollElement)
|
|
679
|
-
// 收敛到此处统一赋值,不再在 ResizeObserver 中重复
|
|
680
|
-
React.useLayoutEffect(() => {
|
|
681
|
-
if (!scrollRefProp)
|
|
682
|
-
return;
|
|
683
|
-
const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
|
|
684
|
-
if (el)
|
|
685
|
-
scrollRefProp.current = el;
|
|
686
|
-
}, [scrollRefProp, useScrollElementMode, effectiveScrollElement]);
|
|
687
|
-
// controlledScrollTop 变化时同步到 ScrollView
|
|
212
|
+
}, [isHorizontal, onScroll, updateRenderOffset, containerLength]);
|
|
213
|
+
// 初始化后的延迟同步 - 确保ScrollView正确设置初始位置
|
|
688
214
|
React.useEffect(() => {
|
|
689
215
|
if (typeof controlledScrollTop === 'number') {
|
|
690
|
-
|
|
691
|
-
const same = isWeapp && Math.abs(sv - controlledScrollTop) < 1;
|
|
692
|
-
if (same) {
|
|
693
|
-
const intermediate = controlledScrollTop > 0 ? controlledScrollTop - 0.01 : 0.01;
|
|
694
|
-
setRenderOffset(intermediate);
|
|
695
|
-
setScrollViewOffset(intermediate);
|
|
696
|
-
requestAnimationFrame(() => {
|
|
697
|
-
setRenderOffset(controlledScrollTop);
|
|
698
|
-
setScrollViewOffset(controlledScrollTop);
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
setRenderOffset(controlledScrollTop);
|
|
703
|
-
setScrollViewOffset(controlledScrollTop);
|
|
704
|
-
}
|
|
216
|
+
setScrollViewOffset(controlledScrollTop);
|
|
705
217
|
lastScrollTopRef.current = controlledScrollTop;
|
|
706
|
-
programmaticScrollRef.current = true;
|
|
707
218
|
}
|
|
708
219
|
}, [controlledScrollTop]);
|
|
709
|
-
//
|
|
220
|
+
// 清理定时器
|
|
710
221
|
React.useEffect(() => {
|
|
711
222
|
return () => {
|
|
712
223
|
if (scrollTimeoutRef.current) {
|
|
713
224
|
clearTimeout(scrollTimeoutRef.current);
|
|
714
225
|
}
|
|
715
|
-
// 小程序端冷却期定时器清理
|
|
716
|
-
if (isWeapp && programmaticCooldownTimerRef.current) {
|
|
717
|
-
clearTimeout(programmaticCooldownTimerRef.current);
|
|
718
|
-
}
|
|
719
|
-
if (sizeCacheRafRef.current != null) {
|
|
720
|
-
cancelAnimationFrame(sizeCacheRafRef.current);
|
|
721
|
-
sizeCacheRafRef.current = null;
|
|
722
|
-
}
|
|
723
|
-
resizeObserver.disconnect();
|
|
724
|
-
scrollCorrection.clearQueue();
|
|
725
226
|
};
|
|
726
227
|
}, []);
|
|
727
|
-
//
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
const targetId = props.scrollIntoView;
|
|
731
|
-
if (!targetId) {
|
|
732
|
-
lastScrollIntoViewRef.current = null;
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
if (lastScrollIntoViewRef.current === targetId)
|
|
736
|
-
return;
|
|
737
|
-
lastScrollIntoViewRef.current = targetId;
|
|
738
|
-
let targetIndex = -1;
|
|
739
|
-
if (targetId.startsWith('list-item-')) {
|
|
740
|
-
targetIndex = parseInt(targetId.replace('list-item-', ''), 10);
|
|
741
|
-
}
|
|
742
|
-
if (targetIndex >= 0 && targetIndex < totalItemCount) {
|
|
743
|
-
let targetOffset = 0;
|
|
744
|
-
let currentGlobalIndex = 0;
|
|
745
|
-
for (let s = 0; s < sections.length; s++) {
|
|
746
|
-
const section = sections[s];
|
|
747
|
-
const headerSize = section.header ? getHeaderSize(s) : 0;
|
|
748
|
-
if (currentGlobalIndex + section.items.length > targetIndex) {
|
|
749
|
-
targetOffset += headerSize;
|
|
750
|
-
for (let i = 0; i < targetIndex - currentGlobalIndex; i++) {
|
|
751
|
-
targetOffset += getItemSize(currentGlobalIndex + i) + space;
|
|
752
|
-
}
|
|
753
|
-
break;
|
|
754
|
-
}
|
|
755
|
-
else {
|
|
756
|
-
targetOffset += headerSize;
|
|
757
|
-
for (let i = 0; i < section.items.length; i++) {
|
|
758
|
-
targetOffset += getItemSize(currentGlobalIndex + i) + space;
|
|
759
|
-
}
|
|
760
|
-
currentGlobalIndex += section.items.length;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
764
|
-
// scrollElement 模式下需显式滚动外部容器,否则仅 updateRenderOffset 不会滚动 div
|
|
765
|
-
if (useScrollElementMode && (effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current) && isH5) {
|
|
766
|
-
const el = effectiveScrollElement.current;
|
|
767
|
-
const scrollTarget = targetOffset + effectiveStartOffset;
|
|
768
|
-
if (isHorizontal) {
|
|
769
|
-
el.scrollTo({ left: scrollTarget });
|
|
770
|
-
}
|
|
771
|
-
else {
|
|
772
|
-
el.scrollTo({ top: scrollTarget });
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}, [props.scrollIntoView, totalItemCount, sections, getHeaderSize, getItemSize, space, updateRenderOffset, useScrollElementMode, effectiveScrollElement, effectiveStartOffset, isH5, isHorizontal]);
|
|
777
|
-
// 容器样式;H5 刷新中禁止滚动
|
|
778
|
-
const containerStyle = Object.assign(Object.assign({ position: 'relative', boxSizing: 'border-box', height,
|
|
779
|
-
width }, style), (isH5 && refresherConfig && !supportsNativeRefresher && h5RefresherProps.isRefreshing
|
|
780
|
-
? { overflow: 'hidden' }
|
|
781
|
-
: {}));
|
|
228
|
+
// 容器样式
|
|
229
|
+
const containerStyle = Object.assign({ position: 'relative', boxSizing: 'border-box', height: isHorizontal ? width : height, width: isHorizontal ? height : width }, style);
|
|
230
|
+
// 修改ScrollView组件的props,添加data-testid属性
|
|
782
231
|
// ScrollView 属性
|
|
783
|
-
const scrollViewProps =
|
|
784
|
-
|
|
785
|
-
|
|
232
|
+
const scrollViewProps = {
|
|
233
|
+
scrollY: !scrollX && scrollY,
|
|
234
|
+
scrollX: scrollX,
|
|
235
|
+
style: containerStyle,
|
|
236
|
+
enhanced: true,
|
|
237
|
+
showScrollbar: showScrollbar,
|
|
238
|
+
onScroll: handleScroll,
|
|
239
|
+
onScrollToUpper,
|
|
786
240
|
onScrollToLower,
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}, [h5RefresherProps.isRefreshing, isH5, refresherConfig, supportsNativeRefresher, updateRenderOffset]);
|
|
794
|
-
// H5 下拉刷新:ref 存 addImperativeTouchListeners,避免 config 变化导致 effect 循环
|
|
795
|
-
React.useLayoutEffect(() => {
|
|
796
|
-
const attach = addImperativeTouchListenersRef.current;
|
|
797
|
-
if (!attach)
|
|
798
|
-
return;
|
|
799
|
-
let teardown = null;
|
|
800
|
-
const tryAttach = () => {
|
|
801
|
-
const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
|
|
802
|
-
if (el && !refresherTeardownRef.current) {
|
|
803
|
-
teardown = attach(el);
|
|
804
|
-
refresherTeardownRef.current = teardown;
|
|
805
|
-
}
|
|
806
|
-
};
|
|
807
|
-
tryAttach();
|
|
808
|
-
const rafId = requestAnimationFrame(tryAttach);
|
|
809
|
-
return () => {
|
|
810
|
-
cancelAnimationFrame(rafId);
|
|
811
|
-
if (teardown) {
|
|
812
|
-
teardown();
|
|
813
|
-
}
|
|
814
|
-
refresherTeardownRef.current = null;
|
|
815
|
-
};
|
|
816
|
-
}, [useScrollElementMode, effectiveScrollElement]);
|
|
817
|
-
scrollViewOffsetRef.current = scrollViewOffset;
|
|
818
|
-
// scrollTop:weapp 传 scrollViewOffset(reflow 帧传 eff 防跳变);H5 仅非用户滑动时传
|
|
819
|
-
if (isWeapp) {
|
|
820
|
-
if (measureScrollProtectRef.current) {
|
|
821
|
-
measureScrollProtectRef.current = false;
|
|
822
|
-
const eff = lastScrollTopRef.current;
|
|
823
|
-
setScrollViewOffset(eff);
|
|
824
|
-
if (isHorizontal) {
|
|
825
|
-
scrollViewProps.scrollLeft = eff;
|
|
826
|
-
}
|
|
827
|
-
else {
|
|
828
|
-
scrollViewProps.scrollTop = eff;
|
|
829
|
-
}
|
|
830
|
-
programmaticScrollRef.current = false;
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
|
-
const sv = scrollViewOffset;
|
|
834
|
-
if (isHorizontal) {
|
|
835
|
-
scrollViewProps.scrollLeft = sv;
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
scrollViewProps.scrollTop = sv;
|
|
839
|
-
}
|
|
840
|
-
programmaticScrollRef.current = false;
|
|
841
|
-
}
|
|
241
|
+
'data-testid': 'taro-list-container'
|
|
242
|
+
};
|
|
243
|
+
// 设置ScrollView的滚动位置 - 同时兼容React版本和Stencil版本
|
|
244
|
+
if (isHorizontal) {
|
|
245
|
+
scrollViewProps.scrollLeft = scrollViewOffset; // React版本
|
|
246
|
+
scrollViewProps.mpScrollLeft = scrollViewOffset; // Stencil版本
|
|
842
247
|
}
|
|
843
248
|
else {
|
|
844
|
-
//
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
249
|
+
scrollViewProps.scrollTop = scrollViewOffset; // React版本
|
|
250
|
+
scrollViewProps.mpScrollTop = scrollViewOffset; // Stencil版本
|
|
251
|
+
}
|
|
252
|
+
// H5上额外使用DOM直接操作确保滚动位置正确
|
|
253
|
+
if (isH5) {
|
|
254
|
+
React.useEffect(() => {
|
|
255
|
+
if (containerRef.current && typeof scrollViewOffset === 'number') {
|
|
256
|
+
if (isHorizontal) {
|
|
257
|
+
containerRef.current.scrollLeft = scrollViewOffset;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
containerRef.current.scrollTop = scrollViewOffset;
|
|
261
|
+
}
|
|
851
262
|
}
|
|
852
|
-
}
|
|
263
|
+
}, [scrollViewOffset, isHorizontal]);
|
|
853
264
|
}
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
if (!isH5)
|
|
857
|
-
return;
|
|
858
|
-
if (isUserScrolling || !containerRef.current || typeof scrollViewOffset !== 'number')
|
|
859
|
-
return;
|
|
860
|
-
if (!programmaticScrollRef.current)
|
|
861
|
-
return;
|
|
862
|
-
const scrollValue = scrollViewOffset;
|
|
863
|
-
if (isHorizontal) {
|
|
864
|
-
containerRef.current.scrollLeft = scrollValue;
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
containerRef.current.scrollTop = scrollValue;
|
|
868
|
-
}
|
|
869
|
-
programmaticScrollRef.current = false;
|
|
870
|
-
}, [isH5, scrollViewOffset, isHorizontal, isUserScrolling]);
|
|
871
|
-
// 总高度/宽度(含 NoMore)
|
|
872
|
-
const noMoreHeight = (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible) ? (noMoreConfig.height || 60) : 0;
|
|
873
|
-
const listContentLength = sectionOffsets[sectionOffsets.length - 1] + noMoreHeight;
|
|
874
|
-
const totalLength = listContentLength;
|
|
265
|
+
// 总高度/宽度
|
|
266
|
+
const totalLength = sectionOffsets[sectionOffsets.length - 1];
|
|
875
267
|
// 吸顶/吸左 header
|
|
876
268
|
const stickyHeaderNode = React.useMemo(() => {
|
|
877
269
|
if (!stickyHeader)
|
|
@@ -880,18 +272,9 @@ const InnerList = (props, ref) => {
|
|
|
880
272
|
if (sectionOffsets[i] <= renderOffset && renderOffset < sectionOffsets[i + 1]) {
|
|
881
273
|
const section = sections[i];
|
|
882
274
|
if (section.header) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
top: 0,
|
|
887
|
-
left: 0,
|
|
888
|
-
zIndex: 100,
|
|
889
|
-
background: '#fff',
|
|
890
|
-
boxSizing: 'border-box',
|
|
891
|
-
minHeight: 20,
|
|
892
|
-
overflow: 'hidden',
|
|
893
|
-
lineHeight: 1
|
|
894
|
-
};
|
|
275
|
+
const headerSize = getHeaderSize();
|
|
276
|
+
// 内联样式替代className
|
|
277
|
+
const stickyHeaderStyle = Object.assign({ position: 'sticky', top: 0, left: 0, zIndex: 100, background: '#fff', boxSizing: 'border-box', minHeight: '20px', overflow: 'hidden', lineHeight: 1 }, (isHorizontal ? { width: headerSize } : { height: headerSize }));
|
|
895
278
|
return (jsx(View, { style: stickyHeaderStyle, children: section.header }));
|
|
896
279
|
}
|
|
897
280
|
}
|
|
@@ -902,105 +285,20 @@ const InnerList = (props, ref) => {
|
|
|
902
285
|
const renderSections = () => {
|
|
903
286
|
const nodes = [];
|
|
904
287
|
let offset = sectionOffsets[startSection];
|
|
905
|
-
let globalItemIndex = 0; // 全局 item 索引(跨 section)
|
|
906
|
-
// 计算起始 section 之前的所有 item 数量
|
|
907
|
-
for (let s = 0; s < startSection; s++) {
|
|
908
|
-
globalItemIndex += sections[s].items.length;
|
|
909
|
-
}
|
|
910
288
|
for (let s = startSection; s <= endSection; s++) {
|
|
911
289
|
const section = sections[s];
|
|
912
|
-
const headerSize = getHeaderSize(
|
|
913
|
-
const itemSizes = section.items.map((_, i) => getItemSize(
|
|
290
|
+
const headerSize = getHeaderSize();
|
|
291
|
+
const itemSizes = section.items.map((_, i) => getItemSize(i));
|
|
914
292
|
// header
|
|
915
293
|
if (section.header) {
|
|
916
|
-
|
|
917
|
-
// 动态测量时外层定位容器不设固定高度,由内层撑开
|
|
294
|
+
// 内联样式替代className
|
|
918
295
|
const sectionHeaderStyle = Object.assign({ position: 'absolute', zIndex: 2, boxSizing: 'border-box', width: '100%', minHeight: '20px', overflow: 'hidden', lineHeight: 1 }, (isHorizontal
|
|
919
|
-
? { top: 0, height: '100%', left: offset, width:
|
|
920
|
-
: { top: offset, height:
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
resizeObserver.observe(el, -sectionIndex - 1); // 用负数索引区分 header 和 item
|
|
926
|
-
const measureAndUpdateHeader = (measured) => {
|
|
927
|
-
var _a;
|
|
928
|
-
if (measured > 0) {
|
|
929
|
-
const oldSize = (_a = headerSizeCacheRef.current.get(sectionIndex)) !== null && _a !== void 0 ? _a : getDefaultHeaderSize();
|
|
930
|
-
if (Math.abs(oldSize - measured) >= 1) {
|
|
931
|
-
headerSizeCacheRef.current.set(sectionIndex, measured);
|
|
932
|
-
if (isWeapp && props.useResizeObserver === true) {
|
|
933
|
-
scheduleWeappDynamicReflow();
|
|
934
|
-
}
|
|
935
|
-
else if (sizeCacheRafRef.current == null) {
|
|
936
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
937
|
-
sizeCacheRafRef.current = null;
|
|
938
|
-
setSizeCacheVersion((v) => v + 1);
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
};
|
|
944
|
-
if (isH5) {
|
|
945
|
-
requestAnimationFrame(() => {
|
|
946
|
-
if (!headerRefsRef.current.has(sectionIndex))
|
|
947
|
-
return;
|
|
948
|
-
const rect = el.getBoundingClientRect();
|
|
949
|
-
measureAndUpdateHeader(isHorizontal ? rect.width : rect.height);
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
else if (isWeapp) {
|
|
953
|
-
Taro.nextTick(() => {
|
|
954
|
-
if (!headerRefsRef.current.has(sectionIndex))
|
|
955
|
-
return;
|
|
956
|
-
Taro.createSelectorQuery()
|
|
957
|
-
.select(`#${listId}-list-header-inner-${sectionIndex}`)
|
|
958
|
-
.boundingClientRect((rect) => {
|
|
959
|
-
if (rect) {
|
|
960
|
-
measureAndUpdateHeader(isHorizontal ? rect.width : rect.height);
|
|
961
|
-
}
|
|
962
|
-
})
|
|
963
|
-
.exec();
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
else {
|
|
968
|
-
const oldEl = headerRefsRef.current.get(sectionIndex);
|
|
969
|
-
if (oldEl) {
|
|
970
|
-
resizeObserver.unobserve(oldEl);
|
|
971
|
-
headerRefsRef.current.delete(sectionIndex);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
} : undefined;
|
|
975
|
-
// 动态尺寸时:外层定位,内层撑开以便测量
|
|
976
|
-
const headerContentStyle = props.useResizeObserver
|
|
977
|
-
? (isHorizontal
|
|
978
|
-
// weapp 下 max-content 兼容性不稳定,改用 inline-flex 收缩包裹以测得真实宽度
|
|
979
|
-
? (isWeapp
|
|
980
|
-
? { boxSizing: 'border-box', display: 'inline-flex', height: '100%' }
|
|
981
|
-
: { boxSizing: 'border-box', width: 'max-content', height: '100%' })
|
|
982
|
-
: { boxSizing: 'border-box', width: '100%' })
|
|
983
|
-
: {};
|
|
984
|
-
if (props.useResizeObserver) {
|
|
985
|
-
const headerInnerProps = {
|
|
986
|
-
ref: headerRefCallback,
|
|
987
|
-
style: headerContentStyle,
|
|
988
|
-
'data-index': String(-sectionIndex - 1), // 用于 unobserve 获取 index;与 observe(el, -sectionIndex-1) 对应
|
|
989
|
-
};
|
|
990
|
-
if (isWeapp) {
|
|
991
|
-
headerInnerProps.id = `${listId}-list-header-inner-${sectionIndex}`;
|
|
992
|
-
}
|
|
993
|
-
nodes.push(React.createElement(View, {
|
|
994
|
-
key: section.key + '-header',
|
|
995
|
-
style: sectionHeaderStyle,
|
|
996
|
-
}, React.createElement(View, headerInnerProps, section.header)));
|
|
997
|
-
}
|
|
998
|
-
else {
|
|
999
|
-
nodes.push(React.createElement(View, {
|
|
1000
|
-
key: section.key + '-header',
|
|
1001
|
-
style: sectionHeaderStyle,
|
|
1002
|
-
}, section.header));
|
|
1003
|
-
}
|
|
296
|
+
? { top: 0, height: '100%', left: offset, width: headerSize }
|
|
297
|
+
: { top: offset, height: headerSize }));
|
|
298
|
+
nodes.push(React.createElement(View, {
|
|
299
|
+
key: section.key + '-header',
|
|
300
|
+
style: sectionHeaderStyle,
|
|
301
|
+
}, section.header));
|
|
1004
302
|
offset += headerSize;
|
|
1005
303
|
}
|
|
1006
304
|
// item offsets
|
|
@@ -1020,17 +318,9 @@ const InnerList = (props, ref) => {
|
|
|
1020
318
|
break;
|
|
1021
319
|
}
|
|
1022
320
|
}
|
|
1023
|
-
// 更新可见起始索引(用于 ScrollCorrection)
|
|
1024
|
-
if (s === startSection && startItem === 0) {
|
|
1025
|
-
visibleStartIndexRef.current = globalItemIndex;
|
|
1026
|
-
}
|
|
1027
|
-
else if (s === startSection) {
|
|
1028
|
-
visibleStartIndexRef.current = globalItemIndex + startItem;
|
|
1029
|
-
}
|
|
1030
321
|
// 渲染可见item
|
|
1031
322
|
for (let i = startItem; i <= endItem; i++) {
|
|
1032
|
-
|
|
1033
|
-
const itemId = `list-item-${currentGlobalIndex}`;
|
|
323
|
+
// 内联样式替代className
|
|
1034
324
|
const sectionItemStyle = Object.assign({ position: 'absolute', zIndex: 1, boxSizing: 'border-box', width: '100%', minHeight: '20px', overflow: 'hidden', lineHeight: 1 }, (isHorizontal
|
|
1035
325
|
? {
|
|
1036
326
|
top: 0,
|
|
@@ -1044,246 +334,17 @@ const InnerList = (props, ref) => {
|
|
|
1044
334
|
height: itemSizes[i],
|
|
1045
335
|
marginBottom: space
|
|
1046
336
|
}));
|
|
1047
|
-
|
|
1048
|
-
const refCallback = (el) => {
|
|
1049
|
-
if (el) {
|
|
1050
|
-
const capturedIndex = currentGlobalIndex;
|
|
1051
|
-
itemRefsRef.current.set(capturedIndex, el);
|
|
1052
|
-
resizeObserver.observe(el, capturedIndex);
|
|
1053
|
-
// H5:使用 getBoundingClientRect 进行 fallback 测量
|
|
1054
|
-
// 小程序:使用 SelectorQuery 进行 fallback 测量(小程序没有 getBoundingClientRect)
|
|
1055
|
-
const measureAndUpdate = (measured) => {
|
|
1056
|
-
var _a;
|
|
1057
|
-
if (measured > 0) {
|
|
1058
|
-
const oldSize = sizeCache.getItemSize(capturedIndex);
|
|
1059
|
-
if (Math.abs(oldSize - measured) < 1)
|
|
1060
|
-
return;
|
|
1061
|
-
sizeCache.setItemSize(capturedIndex, measured);
|
|
1062
|
-
scrollCorrection.recordSizeChange(capturedIndex, oldSize, measured);
|
|
1063
|
-
if (isWeapp && props.useResizeObserver === true) {
|
|
1064
|
-
scheduleWeappDynamicReflow();
|
|
1065
|
-
}
|
|
1066
|
-
else if (sizeCacheRafRef.current == null) {
|
|
1067
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
1068
|
-
sizeCacheRafRef.current = null;
|
|
1069
|
-
setSizeCacheVersion((v) => v + 1);
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
// 小程序:延迟 onItemSizeChange,避免父组件重渲染导致 List remount
|
|
1073
|
-
if (isWeapp) {
|
|
1074
|
-
weappDeferItemSizeChange(capturedIndex, measured, props.onItemSizeChange);
|
|
1075
|
-
}
|
|
1076
|
-
else {
|
|
1077
|
-
(_a = props.onItemSizeChange) === null || _a === void 0 ? void 0 : _a.call(props, capturedIndex, measured);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
};
|
|
1081
|
-
if (isH5) {
|
|
1082
|
-
requestAnimationFrame(() => {
|
|
1083
|
-
if (!itemRefsRef.current.has(capturedIndex))
|
|
1084
|
-
return;
|
|
1085
|
-
const rect = el.getBoundingClientRect();
|
|
1086
|
-
measureAndUpdate(isHorizontal ? rect.width : rect.height);
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
-
else if (isWeapp) {
|
|
1090
|
-
// 小程序端:使用 SelectorQuery 测量内层内容容器的实际尺寸
|
|
1091
|
-
// 注意:必须选 inner 容器(无固定高度,由内容撑开),不能选 outer(有虚拟列表设置的固定高度)
|
|
1092
|
-
// 已测量过的 item 跳过,避免 refCallback 重复触发导致无限循环
|
|
1093
|
-
// 滚动期间跳过 SelectorQuery(不加入 weappMeasuredItemsRef),避免异步查询干扰 scroll-view
|
|
1094
|
-
// SelectorQuery 是只读查询,滚动期间执行安全
|
|
1095
|
-
if (shouldMeasureWeappItem(capturedIndex, weappMeasuredItemsRef.current)) {
|
|
1096
|
-
weappMeasuredItemsRef.current.add(capturedIndex);
|
|
1097
|
-
// 使用 Taro.nextTick 代替 setTimeout(50):等待下一帧渲染完成后再测量
|
|
1098
|
-
// 比固定延时更快(减少"预估→实测"闪烁)且更可靠(保证 DOM 已更新)
|
|
1099
|
-
Taro.nextTick(() => {
|
|
1100
|
-
if (!itemRefsRef.current.has(capturedIndex))
|
|
1101
|
-
return;
|
|
1102
|
-
Taro.createSelectorQuery()
|
|
1103
|
-
// 页面上可能同时存在多个 List,inner id 必须带 listId 前缀避免跨列表误命中
|
|
1104
|
-
.select(`#${listId}-list-item-inner-${capturedIndex}`)
|
|
1105
|
-
.boundingClientRect((rect) => {
|
|
1106
|
-
if (rect) {
|
|
1107
|
-
measureAndUpdate(isHorizontal ? rect.width : rect.height);
|
|
1108
|
-
}
|
|
1109
|
-
})
|
|
1110
|
-
.exec();
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
else {
|
|
1116
|
-
const oldEl = itemRefsRef.current.get(currentGlobalIndex);
|
|
1117
|
-
if (oldEl) {
|
|
1118
|
-
resizeObserver.unobserve(oldEl);
|
|
1119
|
-
itemRefsRef.current.delete(currentGlobalIndex);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
};
|
|
1123
|
-
// 动态尺寸时:外层负责定位,内层由内容撑开以便测量真实尺寸。
|
|
1124
|
-
// 纵向:内层 width:100% 无 height,由内容撑开,测到的是内容高度。
|
|
1125
|
-
// 横向:width:max-content + height:100%:
|
|
1126
|
-
// - width:max-content 便于测量真实宽度;
|
|
1127
|
-
// - height:100% 让测量层与外层 slot 条带高度一致,避免「外层条带 180px、内层 wrapper 只有内容高度」导致的内外高度差。
|
|
1128
|
-
const contentWrapperStyle = props.useResizeObserver
|
|
1129
|
-
? (isHorizontal
|
|
1130
|
-
// weapp 下 max-content 兼容性不稳定,改用 inline-flex 收缩包裹以测得真实宽度
|
|
1131
|
-
? (isWeapp
|
|
1132
|
-
? { boxSizing: 'border-box', display: 'inline-flex', height: '100%' }
|
|
1133
|
-
: { boxSizing: 'border-box', width: 'max-content', height: '100%' })
|
|
1134
|
-
: { boxSizing: 'border-box', width: '100%' })
|
|
1135
|
-
: {};
|
|
1136
|
-
const outerItemProps = {
|
|
337
|
+
nodes.push(React.createElement(View, {
|
|
1137
338
|
key: section.key + '-item-' + i,
|
|
1138
|
-
id: itemId,
|
|
1139
339
|
style: sectionItemStyle,
|
|
1140
|
-
};
|
|
1141
|
-
if (props.useResizeObserver) {
|
|
1142
|
-
const innerProps = {
|
|
1143
|
-
ref: refCallback,
|
|
1144
|
-
style: contentWrapperStyle,
|
|
1145
|
-
};
|
|
1146
|
-
// 小程序端需要 id 用于 SelectorQuery 测量内容真实尺寸;H5 端不需要(用 getBoundingClientRect)
|
|
1147
|
-
if (isWeapp) {
|
|
1148
|
-
// 页面内多 List 并存时避免 id 冲突(例如 demo 同页含多个 List)
|
|
1149
|
-
innerProps.id = `${listId}-list-item-inner-${currentGlobalIndex}`;
|
|
1150
|
-
}
|
|
1151
|
-
innerProps['data-index'] = String(currentGlobalIndex);
|
|
1152
|
-
const itemNode = React.createElement(View, outerItemProps, React.createElement(View, innerProps, section.items[i]));
|
|
1153
|
-
// 任务 4.1:当 List 暴露 scrollRef 时,为每个 item 提供 Context,供内层 WaterFlow 使用
|
|
1154
|
-
const itemWithContext = scrollRefProp && !useScrollElementMode
|
|
1155
|
-
? React.createElement(ListScrollElementContext.Provider, {
|
|
1156
|
-
key: outerItemProps.key,
|
|
1157
|
-
value: {
|
|
1158
|
-
scrollRef: scrollRefProp,
|
|
1159
|
-
containerHeight: containerLength,
|
|
1160
|
-
startOffset: offset + itemOffsets[i],
|
|
1161
|
-
reportNestedHeightChange: props.useResizeObserver
|
|
1162
|
-
? (height) => {
|
|
1163
|
-
if (height <= 0)
|
|
1164
|
-
return;
|
|
1165
|
-
const idx = currentGlobalIndex;
|
|
1166
|
-
const estimatedHeader = estimatedSize * 0.5;
|
|
1167
|
-
const fullHeight = height + estimatedHeader;
|
|
1168
|
-
const oldSize = sizeCache.getItemSize(idx);
|
|
1169
|
-
if (Math.abs(oldSize - fullHeight) < 1)
|
|
1170
|
-
return;
|
|
1171
|
-
sizeCache.setItemSize(idx, fullHeight);
|
|
1172
|
-
scrollCorrection.recordSizeChange(idx, oldSize, fullHeight);
|
|
1173
|
-
if (isWeapp && props.useResizeObserver === true) {
|
|
1174
|
-
scheduleWeappDynamicReflow();
|
|
1175
|
-
}
|
|
1176
|
-
else if (sizeCacheRafRef.current == null) {
|
|
1177
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
1178
|
-
sizeCacheRafRef.current = null;
|
|
1179
|
-
setSizeCacheVersion((v) => v + 1);
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
: undefined,
|
|
1184
|
-
},
|
|
1185
|
-
}, itemNode)
|
|
1186
|
-
: itemNode;
|
|
1187
|
-
nodes.push(itemWithContext);
|
|
1188
|
-
}
|
|
1189
|
-
else {
|
|
1190
|
-
const itemNode = React.createElement(View, outerItemProps, section.items[i]);
|
|
1191
|
-
const itemWithContext = scrollRefProp && !useScrollElementMode
|
|
1192
|
-
? React.createElement(ListScrollElementContext.Provider, {
|
|
1193
|
-
key: outerItemProps.key,
|
|
1194
|
-
value: {
|
|
1195
|
-
scrollRef: scrollRefProp,
|
|
1196
|
-
containerHeight: containerLength,
|
|
1197
|
-
startOffset: offset + itemOffsets[i],
|
|
1198
|
-
reportNestedHeightChange: props.useResizeObserver
|
|
1199
|
-
? (height) => {
|
|
1200
|
-
if (height <= 0)
|
|
1201
|
-
return;
|
|
1202
|
-
const idx = currentGlobalIndex;
|
|
1203
|
-
const estimatedHeader = estimatedSize * 0.5;
|
|
1204
|
-
const fullHeight = height + estimatedHeader;
|
|
1205
|
-
const oldSize = sizeCache.getItemSize(idx);
|
|
1206
|
-
if (Math.abs(oldSize - fullHeight) < 1)
|
|
1207
|
-
return;
|
|
1208
|
-
sizeCache.setItemSize(idx, fullHeight);
|
|
1209
|
-
scrollCorrection.recordSizeChange(idx, oldSize, fullHeight);
|
|
1210
|
-
if (isWeapp && props.useResizeObserver === true) {
|
|
1211
|
-
scheduleWeappDynamicReflow();
|
|
1212
|
-
}
|
|
1213
|
-
else if (sizeCacheRafRef.current == null) {
|
|
1214
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
1215
|
-
sizeCacheRafRef.current = null;
|
|
1216
|
-
setSizeCacheVersion((v) => v + 1);
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
: undefined,
|
|
1221
|
-
},
|
|
1222
|
-
}, itemNode)
|
|
1223
|
-
: itemNode;
|
|
1224
|
-
nodes.push(itemWithContext);
|
|
1225
|
-
}
|
|
340
|
+
}, section.items[i]));
|
|
1226
341
|
}
|
|
1227
|
-
globalItemIndex += section.items.length;
|
|
1228
342
|
offset += itemOffsets[itemOffsets.length - 1];
|
|
1229
343
|
}
|
|
1230
344
|
return nodes;
|
|
1231
345
|
};
|
|
1232
|
-
|
|
1233
|
-
const renderNoMoreContent = () => {
|
|
1234
|
-
if (!noMoreConfig || !noMoreConfig.visible)
|
|
1235
|
-
return null;
|
|
1236
|
-
const noMoreHeightValue = noMoreConfig.height || 60;
|
|
1237
|
-
const listContentEnd = sectionOffsets[sectionOffsets.length - 1];
|
|
1238
|
-
const defaultStyle = Object.assign(Object.assign(Object.assign({ position: 'absolute' }, (isHorizontal
|
|
1239
|
-
? { left: listContentEnd, top: 0, width: noMoreHeightValue, height: '100%' }
|
|
1240
|
-
: { top: listContentEnd, left: 0, width: '100%', height: noMoreHeightValue })), { display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', color: '#999', fontSize: '14px', boxSizing: 'border-box' }), noMoreConfig.style);
|
|
1241
|
-
return (jsx(View, { style: defaultStyle, children: noMoreConfig.children || noMoreConfig.text || '没有更多了' }));
|
|
1242
|
-
};
|
|
1243
|
-
// 空列表场景:仅显示 NoMore
|
|
1244
|
-
if (sections.length === 0 && (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible)) {
|
|
1245
|
-
return (jsx(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { children: jsx(View, { style: Object.assign({ minHeight: containerLength, display: 'flex', alignItems: 'center', justifyContent: 'center' }, containerStyle), children: renderNoMoreContent() }) })));
|
|
1246
|
-
}
|
|
1247
|
-
// 可滚区域总尺寸
|
|
1248
|
-
// H5 refresher 用负 translateY 隐藏,有上方混排时可能延伸到 sibling 区域;overflow:hidden 裁剪避免重叠
|
|
1249
|
-
const needsRefresherClip = refresherHeightForH5 > 0;
|
|
1250
|
-
const contentWrapperStyle = isHorizontal
|
|
1251
|
-
? Object.assign({ width: totalLength, position: 'relative', height: '100%' }, (needsRefresherClip && { overflow: 'hidden' })) : Object.assign({ height: totalLength, position: 'relative', width: '100%' }, (needsRefresherClip && { overflow: 'hidden' }));
|
|
1252
|
-
const listWrapperStyle = isHorizontal
|
|
1253
|
-
? { width: listContentLength, position: 'relative', height: '100%' }
|
|
1254
|
-
: { height: listContentLength, position: 'relative', width: '100%' };
|
|
1255
|
-
const pullTranslate = refresherHeightForH5 > 0 && h5RefresherProps.pullDistance !== 0
|
|
1256
|
-
? { transform: `translateY(${h5RefresherProps.pullDistance}px)` }
|
|
1257
|
-
: {};
|
|
1258
|
-
const h5RefresherTranslateY = -refresherHeightForH5 + h5RefresherProps.pullDistance;
|
|
1259
|
-
// 内容区域渲染
|
|
1260
|
-
const renderContentArea = () => {
|
|
1261
|
-
var _a, _b;
|
|
1262
|
-
return (jsx(View, { style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
|
|
1263
|
-
position: 'absolute',
|
|
1264
|
-
top: 0,
|
|
1265
|
-
left: 0,
|
|
1266
|
-
right: 0,
|
|
1267
|
-
height: refresherHeightForH5,
|
|
1268
|
-
zIndex: 0,
|
|
1269
|
-
transform: `translateY(${h5RefresherTranslateY}px)`,
|
|
1270
|
-
}, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (_b = (_a = style === null || style === void 0 ? void 0 : style.background) !== null && _a !== void 0 ? _a : style === null || style === void 0 ? void 0 : style.backgroundColor) !== null && _b !== void 0 ? _b : '#fff' }), children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })] })) : (jsxs(Fragment, { children: [!supportsNativeRefresher && renderRefresherContent(), stickyHeaderNode, renderSections(), renderNoMoreContent()] })) }));
|
|
1271
|
-
};
|
|
1272
|
-
if (useScrollElementMode) {
|
|
1273
|
-
// 任务 2.4:恢复 refresher DOM 结构(refresher 层 + listWrapperStyle)
|
|
1274
|
-
return (jsx(View, { ref: contentWrapperRef, style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
|
|
1275
|
-
position: 'absolute',
|
|
1276
|
-
top: 0,
|
|
1277
|
-
left: 0,
|
|
1278
|
-
right: 0,
|
|
1279
|
-
height: refresherHeightForH5,
|
|
1280
|
-
zIndex: 0,
|
|
1281
|
-
transform: `translateY(${h5RefresherTranslateY}px)`,
|
|
1282
|
-
}, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (_d = (_c = style === null || style === void 0 ? void 0 : style.background) !== null && _c !== void 0 ? _c : style === null || style === void 0 ? void 0 : style.backgroundColor) !== null && _d !== void 0 ? _d : '#fff' }), children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })] })) : (jsxs(Fragment, { children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })) }));
|
|
1283
|
-
}
|
|
1284
|
-
return (jsxs(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { id: listId, children: [supportsNativeRefresher && renderRefresherContent(), renderContentArea()] })));
|
|
346
|
+
return (jsx(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { children: jsxs(View, { style: isHorizontal ? { width: totalLength, position: 'relative', height: '100%' } : { height: totalLength, position: 'relative', width: '100%' }, children: [stickyHeaderNode, renderSections()] }) })));
|
|
1285
347
|
};
|
|
1286
|
-
const List = React.forwardRef(InnerList);
|
|
1287
348
|
|
|
1288
|
-
export { List, ListItem,
|
|
349
|
+
export { List, ListItem, StickyHeader, StickySection, accumulate, List as default, isShaking };
|
|
1289
350
|
//# sourceMappingURL=index.js.map
|