@mpxjs/webpack-plugin 2.10.21 → 2.10.24-beta.1
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/index.js +4 -1
- package/lib/loader.js +2 -0
- package/lib/react/index.js +3 -1
- package/lib/react/processTemplate.js +3 -1
- package/lib/resolver/AddEnvPlugin.js +1 -1
- package/lib/resolver/AddModePlugin.js +1 -1
- package/lib/resolver/ExtendComponentsPlugin.js +91 -0
- package/lib/runtime/components/extends/section-list.mpx +14 -0
- package/lib/runtime/components/react/dist/mpx-canvas/utils.d.ts +3 -1
- package/lib/runtime/components/react/dist/mpx-section-list.d.ts +56 -0
- package/lib/runtime/components/react/dist/mpx-section-list.jsx +484 -0
- package/lib/runtime/components/react/dist/mpx-simple-text.jsx +12 -12
- package/lib/runtime/components/react/dist/mpx-simple-view.d.ts +0 -1
- package/lib/runtime/components/react/dist/mpx-simple-view.jsx +12 -12
- package/lib/runtime/components/react/dist/mpx-text.jsx +15 -15
- package/lib/runtime/components/react/dist/mpx-view.jsx +15 -15
- package/lib/runtime/components/react/dist/mpx-web-view.d.ts +2 -1
- package/lib/runtime/components/react/dist/utils.d.ts +1 -0
- package/lib/runtime/components/react/dist/utils.jsx +4 -1
- package/lib/runtime/components/react/mpx-canvas/index.tsx +4 -2
- package/lib/runtime/components/react/mpx-canvas/utils.tsx +3 -1
- package/lib/runtime/components/react/mpx-section-list.tsx +734 -0
- package/lib/runtime/components/react/mpx-simple-text.tsx +12 -12
- package/lib/runtime/components/react/mpx-simple-view.tsx +12 -12
- package/lib/runtime/components/react/mpx-text.tsx +15 -15
- package/lib/runtime/components/react/mpx-view.tsx +15 -15
- package/lib/runtime/components/react/mpx-web-view.tsx +4 -3
- package/lib/runtime/components/react/utils.tsx +5 -1
- package/lib/template-compiler/compiler.js +5 -1
- package/lib/utils/source-location.js +9 -13
- package/lib/web/index.js +3 -1
- package/lib/web/processTemplate.js +3 -1
- package/package.json +6 -4
- package/LICENSE +0 -433
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { forwardRef, useRef, useMemo, createElement, useImperativeHandle, useEffect } from 'react'
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
import { SectionList, RefreshControl, NativeSyntheticEvent, NativeScrollEvent } from 'react-native'
|
|
4
|
+
import type { SectionListData, SectionListProps as RNSectionListProps } from 'react-native'
|
|
5
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
|
6
|
+
import { hasOwn, error } from '@mpxjs/utils'
|
|
7
|
+
import useInnerProps, { getCustomEvent } from './getInnerListeners'
|
|
8
|
+
import { extendObject, useLayout, useTransformStyle, GestureHandler, flatGesture } from './utils'
|
|
9
|
+
interface ListItem {
|
|
10
|
+
isSectionHeader?: boolean;
|
|
11
|
+
isSectionFooter?: boolean;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SectionExtra {
|
|
16
|
+
headerData: ListItem | null;
|
|
17
|
+
footerData: ListItem | null;
|
|
18
|
+
hasSectionHeader?: boolean;
|
|
19
|
+
hasSectionFooter?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Section extends SectionExtra {
|
|
23
|
+
data: ListItem[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type RNSection = SectionListData<ListItem, SectionExtra>
|
|
27
|
+
|
|
28
|
+
const TypedSectionList = SectionList as unknown as ComponentType<RNSectionListProps<ListItem, SectionExtra>>
|
|
29
|
+
|
|
30
|
+
interface ItemHeightType {
|
|
31
|
+
value?: number;
|
|
32
|
+
getter?: (item: any, index: number) => number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ItemLayoutInfo {
|
|
36
|
+
length: number;
|
|
37
|
+
offset: number;
|
|
38
|
+
index: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ItemExposureLayout {
|
|
42
|
+
offset: number;
|
|
43
|
+
length: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ItemExposureDetail {
|
|
47
|
+
index: number;
|
|
48
|
+
itemData: ListItem | null;
|
|
49
|
+
layout: ItemExposureLayout;
|
|
50
|
+
threshold: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ItemExposureViewToken {
|
|
54
|
+
item: Section | ListItem;
|
|
55
|
+
key: string;
|
|
56
|
+
index: number | null;
|
|
57
|
+
isViewable: boolean;
|
|
58
|
+
section?: any;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ItemExposureViewabilityConfig {
|
|
62
|
+
itemVisiblePercentThreshold: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ItemExposureViewabilityPair {
|
|
66
|
+
viewabilityConfig: ItemExposureViewabilityConfig;
|
|
67
|
+
onViewableItemsChanged: (info: {
|
|
68
|
+
viewableItems: ItemExposureViewToken[];
|
|
69
|
+
changed: ItemExposureViewToken[];
|
|
70
|
+
}) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ItemExposureInfo {
|
|
74
|
+
index: number;
|
|
75
|
+
itemData: ListItem | null;
|
|
76
|
+
layout: ItemExposureLayout;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type ItemExposureType = 'header' | 'footer' | 'item'
|
|
80
|
+
|
|
81
|
+
interface ItemExposureIndexInfo {
|
|
82
|
+
index: number;
|
|
83
|
+
type: ItemExposureType;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface MpxSectionListProps {
|
|
87
|
+
enhanced?: boolean;
|
|
88
|
+
bounces?: boolean;
|
|
89
|
+
height?: number | string;
|
|
90
|
+
width?: number | string;
|
|
91
|
+
generichash?: string;
|
|
92
|
+
style?: Record<string, any>;
|
|
93
|
+
'scroll-event-throttle'?: number;
|
|
94
|
+
'list-data'?: ListItem[];
|
|
95
|
+
'item-height'?: ItemHeightType;
|
|
96
|
+
'section-header-height'?: ItemHeightType;
|
|
97
|
+
'section-footer-height'?: ItemHeightType;
|
|
98
|
+
'list-header-data'?: any;
|
|
99
|
+
'list-header-height'?: number;
|
|
100
|
+
'use-list-header'?: boolean;
|
|
101
|
+
'list-footer-data'?: any;
|
|
102
|
+
'use-list-footer'?: boolean;
|
|
103
|
+
'genericrecycle-item'?: string;
|
|
104
|
+
'genericsection-header'?: string;
|
|
105
|
+
'genericsection-footer'?: string;
|
|
106
|
+
'genericlist-header'?: string;
|
|
107
|
+
'genericlist-footer'?: string;
|
|
108
|
+
'enable-var'?: boolean;
|
|
109
|
+
'external-var-context'?: any;
|
|
110
|
+
'parent-font-size'?: number;
|
|
111
|
+
'parent-width'?: number;
|
|
112
|
+
'parent-height'?: number;
|
|
113
|
+
'enable-sticky'?: boolean;
|
|
114
|
+
'enable-back-to-top'?: boolean;
|
|
115
|
+
'end-reached-threshold'?: number;
|
|
116
|
+
'refresher-enabled'?: boolean;
|
|
117
|
+
'show-scrollbar'?: boolean;
|
|
118
|
+
'refresher-triggered'?: boolean;
|
|
119
|
+
'enable-item-exposure'?: boolean;
|
|
120
|
+
'item-exposure-threshold'?: number;
|
|
121
|
+
'wait-for'?: Array<GestureHandler>;
|
|
122
|
+
'simultaneous-handlers'?: Array<GestureHandler>;
|
|
123
|
+
bindrefresherrefresh?: (event: any) => void;
|
|
124
|
+
bindscrolltolower?: (event: any) => void;
|
|
125
|
+
bindscroll?: (event: any) => void;
|
|
126
|
+
binditemexposure?: (event: any) => void;
|
|
127
|
+
[key: string]: any;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ScrollPositionParams {
|
|
131
|
+
index: number;
|
|
132
|
+
animated?: boolean;
|
|
133
|
+
viewOffset?: number;
|
|
134
|
+
viewPosition?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const getGeneric = (generichash: string, generickey: string) => {
|
|
138
|
+
if (!generichash || !generickey) return null
|
|
139
|
+
return global.__mpxGenericsMap?.[generichash]?.[generickey]?.() || null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const getExposurePercentThreshold = (threshold = 0) => {
|
|
143
|
+
return Math.max(0, Math.min(100, threshold))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const _SectionList = forwardRef<any, MpxSectionListProps>((props = {}, ref) => {
|
|
147
|
+
const {
|
|
148
|
+
enhanced = false,
|
|
149
|
+
bounces = true,
|
|
150
|
+
height,
|
|
151
|
+
width,
|
|
152
|
+
generichash,
|
|
153
|
+
style = {},
|
|
154
|
+
'list-data': listData,
|
|
155
|
+
'scroll-event-throttle': scrollEventThrottle = 0,
|
|
156
|
+
'item-height': itemHeight = {},
|
|
157
|
+
'section-header-height': sectionHeaderHeight = {},
|
|
158
|
+
'section-footer-height': sectionFooterHeight = {},
|
|
159
|
+
'list-header-height': listHeaderHeight = 0,
|
|
160
|
+
'list-header-data': listHeaderData = null,
|
|
161
|
+
'use-list-header': useListHeader = false,
|
|
162
|
+
'list-footer-data': listFooterData = null,
|
|
163
|
+
'use-list-footer': useListFooter = false,
|
|
164
|
+
'genericrecycle-item': genericrecycleItem,
|
|
165
|
+
'genericsection-header': genericsectionHeader,
|
|
166
|
+
'genericsection-footer': genericsectionFooter,
|
|
167
|
+
'genericlist-header': genericListHeader,
|
|
168
|
+
'genericlist-footer': genericListFooter,
|
|
169
|
+
'enable-var': enableVar,
|
|
170
|
+
'external-var-context': externalVarContext,
|
|
171
|
+
'parent-font-size': parentFontSize,
|
|
172
|
+
'parent-width': parentWidth,
|
|
173
|
+
'parent-height': parentHeight,
|
|
174
|
+
'enable-sticky': enableSticky = false,
|
|
175
|
+
'enable-back-to-top': enableBackToTop = false,
|
|
176
|
+
'end-reached-threshold': onEndReachedThreshold = 0.1,
|
|
177
|
+
'refresher-enabled': refresherEnabled,
|
|
178
|
+
'show-scrollbar': showScrollbar = true,
|
|
179
|
+
'refresher-triggered': refresherTriggered,
|
|
180
|
+
'enable-item-exposure': enableItemExposure = false,
|
|
181
|
+
'item-exposure-threshold': itemExposureThreshold = 0,
|
|
182
|
+
'simultaneous-handlers': originSimultaneousHandlers,
|
|
183
|
+
'wait-for': waitFor,
|
|
184
|
+
binditemexposure
|
|
185
|
+
} = props
|
|
186
|
+
|
|
187
|
+
const refreshing = !!refresherTriggered
|
|
188
|
+
|
|
189
|
+
const scrollViewRef = useRef<any>(null)
|
|
190
|
+
const sectionListGestureRef = useRef<any>()
|
|
191
|
+
|
|
192
|
+
const indexMap = useRef<{ [key: string]: string }>({})
|
|
193
|
+
|
|
194
|
+
const reverseIndexMap = useRef<{ [key: string]: number }>({})
|
|
195
|
+
const exposureIndexMap = useRef<{ [key: number]: ItemExposureIndexInfo }>({})
|
|
196
|
+
const itemLayoutsRef = useRef<ItemLayoutInfo[]>([])
|
|
197
|
+
const itemExposureState = useRef<{ [key: string]: boolean }>({})
|
|
198
|
+
const enableStickyRef = useRef(enableSticky)
|
|
199
|
+
const bindItemExposureRef = useRef<typeof binditemexposure>()
|
|
200
|
+
const propsRef = useRef(props)
|
|
201
|
+
const initialEnableItemExposureRef = useRef(enableItemExposure)
|
|
202
|
+
const itemExposureViewabilityConfig = useRef<ItemExposureViewabilityConfig>()
|
|
203
|
+
if (!itemExposureViewabilityConfig.current) {
|
|
204
|
+
itemExposureViewabilityConfig.current = {
|
|
205
|
+
itemVisiblePercentThreshold: getExposurePercentThreshold(itemExposureThreshold)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const itemExposureViewabilityConfigValue = itemExposureViewabilityConfig.current as ItemExposureViewabilityConfig
|
|
209
|
+
|
|
210
|
+
const {
|
|
211
|
+
hasSelfPercent,
|
|
212
|
+
setWidth,
|
|
213
|
+
setHeight
|
|
214
|
+
} = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight })
|
|
215
|
+
|
|
216
|
+
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef })
|
|
217
|
+
|
|
218
|
+
if (initialEnableItemExposureRef.current !== enableItemExposure) {
|
|
219
|
+
error('[Mpx runtime error]: [enable-item-exposure] cannot be toggled at runtime. Set its value once at component initialization.')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
enableStickyRef.current = enableSticky
|
|
223
|
+
bindItemExposureRef.current = binditemexposure
|
|
224
|
+
propsRef.current = props
|
|
225
|
+
|
|
226
|
+
const onRefresh = () => {
|
|
227
|
+
const { bindrefresherrefresh } = props
|
|
228
|
+
bindrefresherrefresh &&
|
|
229
|
+
bindrefresherrefresh(
|
|
230
|
+
getCustomEvent('refresherrefresh', {}, { layoutRef }, props)
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const onEndReached = () => {
|
|
235
|
+
const { bindscrolltolower } = props
|
|
236
|
+
bindscrolltolower &&
|
|
237
|
+
bindscrolltolower(
|
|
238
|
+
getCustomEvent('scrolltolower', {}, { layoutRef }, props)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
243
|
+
const { bindscroll } = props
|
|
244
|
+
bindscroll &&
|
|
245
|
+
bindscroll(
|
|
246
|
+
getCustomEvent('scroll', event.nativeEvent, { layoutRef }, props)
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const itemExposureViewabilityPairs = useRef<ItemExposureViewabilityPair[]>()
|
|
251
|
+
if (!itemExposureViewabilityPairs.current) {
|
|
252
|
+
itemExposureViewabilityPairs.current = [
|
|
253
|
+
{
|
|
254
|
+
viewabilityConfig: itemExposureViewabilityConfigValue,
|
|
255
|
+
onViewableItemsChanged: ({ changed }) => {
|
|
256
|
+
const bindItemExposure = bindItemExposureRef.current
|
|
257
|
+
if (!bindItemExposure) return
|
|
258
|
+
|
|
259
|
+
const exposedItems: ItemExposureDetail[] = []
|
|
260
|
+
changed.forEach((viewToken) => {
|
|
261
|
+
const exposureInfo = getItemExposureInfo(viewToken)
|
|
262
|
+
if (!exposureInfo) return
|
|
263
|
+
|
|
264
|
+
const { index, itemData, layout } = exposureInfo
|
|
265
|
+
const key = `${index}`
|
|
266
|
+
|
|
267
|
+
if (!viewToken.isViewable) {
|
|
268
|
+
delete itemExposureState.current[key]
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!itemExposureState.current[key]) {
|
|
273
|
+
itemExposureState.current[key] = true
|
|
274
|
+
exposedItems.push({
|
|
275
|
+
index,
|
|
276
|
+
itemData,
|
|
277
|
+
layout,
|
|
278
|
+
threshold: itemExposureViewabilityConfigValue.itemVisiblePercentThreshold
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
if (exposedItems.length) {
|
|
284
|
+
bindItemExposure(
|
|
285
|
+
getCustomEvent('itemexposure', {}, {
|
|
286
|
+
detail: {
|
|
287
|
+
items: exposedItems,
|
|
288
|
+
time: Date.now()
|
|
289
|
+
},
|
|
290
|
+
layoutRef
|
|
291
|
+
}, propsRef.current)
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 通过sectionIndex和rowIndex获取原始索引
|
|
300
|
+
const getOriginalIndex = (sectionIndex: number, rowIndex: number | 'header' | 'footer'): number => {
|
|
301
|
+
const key = `${sectionIndex}_${rowIndex}`
|
|
302
|
+
return reverseIndexMap.current[key] ?? -1 // 如果找不到,返回-1
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const getItemExposureInfo = (viewToken: ItemExposureViewToken): ItemExposureInfo | null => {
|
|
306
|
+
const item = viewToken.item
|
|
307
|
+
if (!item) return null
|
|
308
|
+
|
|
309
|
+
const viewIndex = viewToken.index
|
|
310
|
+
if (viewIndex == null) return null
|
|
311
|
+
const indexInfo = exposureIndexMap.current[viewIndex]
|
|
312
|
+
if (!indexInfo) return null
|
|
313
|
+
if (indexInfo.type === 'header' && enableStickyRef.current) return null
|
|
314
|
+
const layoutInfo = itemLayoutsRef.current[viewIndex]
|
|
315
|
+
const layout = {
|
|
316
|
+
offset: layoutInfo ? layoutInfo.offset : 0,
|
|
317
|
+
length: layoutInfo ? layoutInfo.length : 0
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (indexInfo.type === 'header') {
|
|
321
|
+
const section = item as Section
|
|
322
|
+
return {
|
|
323
|
+
index: indexInfo.index,
|
|
324
|
+
itemData: section.headerData || null,
|
|
325
|
+
layout
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (indexInfo.type === 'footer') {
|
|
330
|
+
const section = item as Section
|
|
331
|
+
return {
|
|
332
|
+
index: indexInfo.index,
|
|
333
|
+
itemData: section.footerData || null,
|
|
334
|
+
layout
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
index: indexInfo.index,
|
|
340
|
+
itemData: item as ListItem,
|
|
341
|
+
layout
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const scrollToIndex = ({ index, animated, viewOffset = 0, viewPosition = 0 }: ScrollPositionParams) => {
|
|
346
|
+
if (!scrollViewRef.current) return
|
|
347
|
+
// 通过索引映射表快速定位位置
|
|
348
|
+
const position = indexMap.current[index]
|
|
349
|
+
if (!position) return
|
|
350
|
+
const [sectionIndex, itemIndex] = position.split('_')
|
|
351
|
+
const targetSectionIndex = Number(sectionIndex) || 0
|
|
352
|
+
const targetItemIndex = itemIndex === 'header'
|
|
353
|
+
? 0
|
|
354
|
+
: itemIndex === 'footer'
|
|
355
|
+
? convertedListData[targetSectionIndex].data.length + 1
|
|
356
|
+
: Number(itemIndex) + 1
|
|
357
|
+
scrollViewRef.current.scrollToLocation?.({
|
|
358
|
+
itemIndex: targetItemIndex,
|
|
359
|
+
sectionIndex: targetSectionIndex,
|
|
360
|
+
animated,
|
|
361
|
+
viewOffset,
|
|
362
|
+
viewPosition
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const getItemHeight = ({ sectionIndex, rowIndex }: { sectionIndex: number, rowIndex: number }) => {
|
|
367
|
+
if (!itemHeight) {
|
|
368
|
+
return 0
|
|
369
|
+
}
|
|
370
|
+
if ((itemHeight as ItemHeightType).getter) {
|
|
371
|
+
const item = convertedListData[sectionIndex].data[rowIndex]
|
|
372
|
+
return (itemHeight as ItemHeightType).getter?.(item, getOriginalIndex(sectionIndex, rowIndex)) || 0
|
|
373
|
+
} else {
|
|
374
|
+
return (itemHeight as ItemHeightType).value || 0
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const getSectionExtraHeight = ({ sectionIndex, type }: { sectionIndex: number, type: 'header' | 'footer' }) => {
|
|
379
|
+
const item = convertedListData[sectionIndex]
|
|
380
|
+
const isHeader = type === 'header'
|
|
381
|
+
if (!(isHeader ? item.hasSectionHeader : item.hasSectionFooter)) return 0
|
|
382
|
+
const sectionExtraHeight = (isHeader ? sectionHeaderHeight : sectionFooterHeight) as ItemHeightType
|
|
383
|
+
if (sectionExtraHeight.getter) {
|
|
384
|
+
const sectionExtraData = isHeader ? item.headerData : item.footerData
|
|
385
|
+
return sectionExtraHeight.getter?.(sectionExtraData, getOriginalIndex(sectionIndex, type)) || 0
|
|
386
|
+
}
|
|
387
|
+
return sectionExtraHeight.value || 0
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const convertedListData = useMemo(() => {
|
|
391
|
+
const sections: Section[] = []
|
|
392
|
+
let currentSection: Section | null = null
|
|
393
|
+
// 清空之前的索引映射
|
|
394
|
+
indexMap.current = {}
|
|
395
|
+
// 清空反向索引映射
|
|
396
|
+
reverseIndexMap.current = {}
|
|
397
|
+
exposureIndexMap.current = {}
|
|
398
|
+
|
|
399
|
+
// 处理 listData 为空的情况
|
|
400
|
+
if (!listData || !listData.length) {
|
|
401
|
+
return sections
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let exposureIndex = 0
|
|
405
|
+
// 需与 RN SectionList 和 getItemLayout 的扁平顺序保持一致:每个 section 都是 [header, ...items, footer]
|
|
406
|
+
const createSection = (headerInfo?: { data: ListItem, index: number }): Section => {
|
|
407
|
+
const sectionIndex = sections.length
|
|
408
|
+
const section = {
|
|
409
|
+
headerData: headerInfo?.data || null,
|
|
410
|
+
footerData: null,
|
|
411
|
+
data: [],
|
|
412
|
+
hasSectionHeader: !!headerInfo,
|
|
413
|
+
hasSectionFooter: false
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (headerInfo) {
|
|
417
|
+
const { index } = headerInfo
|
|
418
|
+
indexMap.current[index] = `${sectionIndex}_header`
|
|
419
|
+
reverseIndexMap.current[`${sectionIndex}_header`] = index
|
|
420
|
+
exposureIndexMap.current[exposureIndex] = {
|
|
421
|
+
index,
|
|
422
|
+
type: 'header'
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
exposureIndex++
|
|
426
|
+
|
|
427
|
+
return section
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const closeSection = () => {
|
|
431
|
+
const section = currentSection
|
|
432
|
+
if (!section) return
|
|
433
|
+
|
|
434
|
+
if (section.hasSectionFooter) {
|
|
435
|
+
const sectionIndex = sections.length
|
|
436
|
+
const index = getOriginalIndex(sectionIndex, 'footer')
|
|
437
|
+
if (index > -1) {
|
|
438
|
+
exposureIndexMap.current[exposureIndex] = {
|
|
439
|
+
index,
|
|
440
|
+
type: 'footer'
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// RN SectionList 即使没有真实 footer,也会为每个 section 保留 footer slot
|
|
445
|
+
exposureIndex++
|
|
446
|
+
|
|
447
|
+
sections.push(section)
|
|
448
|
+
currentSection = null
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
listData.forEach((item: ListItem, index: number) => {
|
|
452
|
+
if (item.isSectionHeader) {
|
|
453
|
+
// 如果已经存在一个 section,先把它添加到 sections 中
|
|
454
|
+
if (currentSection) {
|
|
455
|
+
closeSection()
|
|
456
|
+
}
|
|
457
|
+
currentSection = createSection({ data: item, index })
|
|
458
|
+
} else if (item.isSectionFooter) {
|
|
459
|
+
// 如果没有当前 section,创建一个默认的
|
|
460
|
+
if (!currentSection) {
|
|
461
|
+
// 创建默认section (无header的section)
|
|
462
|
+
currentSection = createSection()
|
|
463
|
+
}
|
|
464
|
+
const sectionIndex = sections.length
|
|
465
|
+
currentSection.footerData = item
|
|
466
|
+
currentSection.hasSectionFooter = true
|
|
467
|
+
indexMap.current[index] = `${sectionIndex}_footer`
|
|
468
|
+
// 添加反向索引映射
|
|
469
|
+
reverseIndexMap.current[`${sectionIndex}_footer`] = index
|
|
470
|
+
closeSection()
|
|
471
|
+
} else {
|
|
472
|
+
// 如果没有当前 section,创建一个默认的
|
|
473
|
+
if (!currentSection) {
|
|
474
|
+
// 创建默认section (无header的section)
|
|
475
|
+
currentSection = createSection()
|
|
476
|
+
}
|
|
477
|
+
// 将 item 添加到当前 section 的 data 中
|
|
478
|
+
const itemIndex = currentSection.data.length
|
|
479
|
+
currentSection.data.push(item)
|
|
480
|
+
const sectionIndex = sections.length
|
|
481
|
+
// 为 item 添加索引映射 - 存储格式为: "sectionIndex_itemIndex"
|
|
482
|
+
indexMap.current[index] = `${sectionIndex}_${itemIndex}`
|
|
483
|
+
// 添加反向索引映射
|
|
484
|
+
reverseIndexMap.current[`${sectionIndex}_${itemIndex}`] = index
|
|
485
|
+
exposureIndexMap.current[exposureIndex] = {
|
|
486
|
+
index,
|
|
487
|
+
type: 'item'
|
|
488
|
+
}
|
|
489
|
+
exposureIndex++
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
// 添加最后一个 section
|
|
493
|
+
if (currentSection) {
|
|
494
|
+
closeSection()
|
|
495
|
+
}
|
|
496
|
+
return sections
|
|
497
|
+
}, [listData])
|
|
498
|
+
|
|
499
|
+
const { getItemLayout } = useMemo(() => {
|
|
500
|
+
const layouts: ItemLayoutInfo[] = []
|
|
501
|
+
let offset = 0
|
|
502
|
+
|
|
503
|
+
if (useListHeader) {
|
|
504
|
+
// 计算列表头部的高度
|
|
505
|
+
offset += listHeaderHeight
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 遍历所有 sections
|
|
509
|
+
convertedListData.forEach((section: Section, sectionIndex: number) => {
|
|
510
|
+
// 添加 section header 的位置信息
|
|
511
|
+
const headerHeight = getSectionExtraHeight({ sectionIndex, type: 'header' })
|
|
512
|
+
layouts.push({
|
|
513
|
+
length: headerHeight,
|
|
514
|
+
offset,
|
|
515
|
+
index: layouts.length
|
|
516
|
+
})
|
|
517
|
+
offset += headerHeight
|
|
518
|
+
|
|
519
|
+
// 添加该 section 中所有 items 的位置信息
|
|
520
|
+
section.data.forEach((item: ListItem, itemIndex: number) => {
|
|
521
|
+
const contentHeight = getItemHeight({ sectionIndex, rowIndex: itemIndex })
|
|
522
|
+
layouts.push({
|
|
523
|
+
length: contentHeight,
|
|
524
|
+
offset,
|
|
525
|
+
index: layouts.length
|
|
526
|
+
})
|
|
527
|
+
offset += contentHeight
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
// 添加该 section 尾部位置信息
|
|
531
|
+
// 因为即使 sectionList 没传 renderSectionFooter,getItemLayout 中的 index 的计算也会包含尾部节点
|
|
532
|
+
const footerHeight = getSectionExtraHeight({ sectionIndex, type: 'footer' })
|
|
533
|
+
layouts.push({
|
|
534
|
+
length: footerHeight,
|
|
535
|
+
offset,
|
|
536
|
+
index: layouts.length
|
|
537
|
+
})
|
|
538
|
+
offset += footerHeight
|
|
539
|
+
})
|
|
540
|
+
itemLayoutsRef.current = layouts
|
|
541
|
+
return {
|
|
542
|
+
getItemLayout: (data: any, index: number) => layouts[index]
|
|
543
|
+
}
|
|
544
|
+
}, [convertedListData, useListHeader, itemHeight.value, itemHeight.getter, sectionHeaderHeight.value, sectionHeaderHeight.getter, sectionFooterHeight.value, sectionFooterHeight.getter, listHeaderHeight])
|
|
545
|
+
|
|
546
|
+
useEffect(() => {
|
|
547
|
+
itemExposureState.current = {}
|
|
548
|
+
}, [convertedListData, enableSticky])
|
|
549
|
+
|
|
550
|
+
const scrollAdditionalProps = extendObject(
|
|
551
|
+
{
|
|
552
|
+
style: [
|
|
553
|
+
hasOwn(style, 'flex') || hasOwn(style, 'flexGrow') ? null : { flexGrow: 0 },
|
|
554
|
+
{ height, width },
|
|
555
|
+
style,
|
|
556
|
+
layoutStyle
|
|
557
|
+
],
|
|
558
|
+
alwaysBounceVertical: false,
|
|
559
|
+
alwaysBounceHorizontal: false,
|
|
560
|
+
scrollEventThrottle: scrollEventThrottle,
|
|
561
|
+
scrollsToTop: enableBackToTop,
|
|
562
|
+
showsHorizontalScrollIndicator: showScrollbar,
|
|
563
|
+
onEndReachedThreshold,
|
|
564
|
+
ref: scrollViewRef,
|
|
565
|
+
bounces: enhanced ? bounces : false,
|
|
566
|
+
stickySectionHeadersEnabled: enableSticky,
|
|
567
|
+
onScroll: onScroll,
|
|
568
|
+
onEndReached: onEndReached
|
|
569
|
+
},
|
|
570
|
+
refresherEnabled ? { refreshing } : null,
|
|
571
|
+
layoutProps
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
if (initialEnableItemExposureRef.current) {
|
|
575
|
+
extendObject(scrollAdditionalProps, {
|
|
576
|
+
viewabilityConfigCallbackPairs: itemExposureViewabilityPairs.current
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const nativeGesture = useMemo(() => {
|
|
581
|
+
const simultaneousHandlers = flatGesture(originSimultaneousHandlers)
|
|
582
|
+
const waitForHandlers = flatGesture(waitFor)
|
|
583
|
+
const gesture = Gesture.Native().withRef(sectionListGestureRef as any)
|
|
584
|
+
if (simultaneousHandlers && simultaneousHandlers.length) {
|
|
585
|
+
gesture.simultaneousWithExternalGesture(...simultaneousHandlers)
|
|
586
|
+
}
|
|
587
|
+
if (waitForHandlers && waitForHandlers.length) {
|
|
588
|
+
gesture.requireExternalGestureToFail(...waitForHandlers)
|
|
589
|
+
}
|
|
590
|
+
return gesture
|
|
591
|
+
}, [originSimultaneousHandlers, waitFor])
|
|
592
|
+
|
|
593
|
+
useImperativeHandle(ref, () => {
|
|
594
|
+
return {
|
|
595
|
+
gestureRef: sectionListGestureRef,
|
|
596
|
+
scrollToIndex
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
const innerProps = useInnerProps(extendObject({}, props, scrollAdditionalProps), [
|
|
601
|
+
'id',
|
|
602
|
+
'enhanced',
|
|
603
|
+
'height',
|
|
604
|
+
'width',
|
|
605
|
+
'list-data',
|
|
606
|
+
'item-height',
|
|
607
|
+
'section-header-height',
|
|
608
|
+
'section-footer-height',
|
|
609
|
+
'list-header-height',
|
|
610
|
+
'list-header-data',
|
|
611
|
+
'use-list-header',
|
|
612
|
+
'list-footer-data',
|
|
613
|
+
'use-list-footer',
|
|
614
|
+
'genericrecycle-item',
|
|
615
|
+
'genericsection-header',
|
|
616
|
+
'genericsection-footer',
|
|
617
|
+
'genericlist-header',
|
|
618
|
+
'genericlist-footer',
|
|
619
|
+
'show-scrollbar',
|
|
620
|
+
'lower-threshold',
|
|
621
|
+
'scroll-event-throttle',
|
|
622
|
+
'enable-sticky',
|
|
623
|
+
'enable-back-to-top',
|
|
624
|
+
'end-reached-threshold',
|
|
625
|
+
'refresher-triggered',
|
|
626
|
+
'refresher-enabled',
|
|
627
|
+
'enable-item-exposure',
|
|
628
|
+
'item-exposure-threshold',
|
|
629
|
+
'bindrefresherrefresh',
|
|
630
|
+
'bindscrolltolower',
|
|
631
|
+
'bindscroll',
|
|
632
|
+
'binditemexposure',
|
|
633
|
+
'simultaneous-handlers',
|
|
634
|
+
'wait-for'
|
|
635
|
+
], { layoutRef })
|
|
636
|
+
|
|
637
|
+
// 使用 useMemo 获取 GenericComponent 并创建渲染函数,避免每次组件更新都重新创建函数引用导致不必要的重新渲染
|
|
638
|
+
const renderItem = useMemo(
|
|
639
|
+
() => {
|
|
640
|
+
const ItemComponent = getGeneric(generichash, genericrecycleItem)
|
|
641
|
+
if (!ItemComponent) return undefined
|
|
642
|
+
return ({ item }: { item: ListItem }) => createElement(ItemComponent, { itemData: item })
|
|
643
|
+
},
|
|
644
|
+
[generichash, genericrecycleItem]
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
const renderSectionHeader = useMemo(
|
|
648
|
+
() => {
|
|
649
|
+
const SectionHeaderComponent = getGeneric(generichash, genericsectionHeader)
|
|
650
|
+
if (!SectionHeaderComponent) return undefined
|
|
651
|
+
return (sectionData: { section: RNSection }) => {
|
|
652
|
+
if (!sectionData.section.hasSectionHeader) return null
|
|
653
|
+
return createElement(SectionHeaderComponent, { itemData: sectionData.section.headerData })
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
[generichash, genericsectionHeader]
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
const renderSectionFooter = useMemo(
|
|
660
|
+
() => {
|
|
661
|
+
const SectionFooterComponent = getGeneric(generichash, genericsectionFooter)
|
|
662
|
+
if (!SectionFooterComponent) return undefined
|
|
663
|
+
return (sectionData: { section: RNSection }) => {
|
|
664
|
+
if (!sectionData.section.hasSectionFooter) return null
|
|
665
|
+
return createElement(SectionFooterComponent, { itemData: sectionData.section.footerData })
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
[generichash, genericsectionFooter]
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
const ListHeaderGenericComponent = useMemo(
|
|
672
|
+
() => {
|
|
673
|
+
if (!useListHeader) return null
|
|
674
|
+
return getGeneric(generichash, genericListHeader)
|
|
675
|
+
},
|
|
676
|
+
[useListHeader, generichash, genericListHeader]
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
const ListFooterGenericComponent = useMemo(
|
|
680
|
+
() => {
|
|
681
|
+
if (!useListFooter) return null
|
|
682
|
+
return getGeneric(generichash, genericListFooter)
|
|
683
|
+
},
|
|
684
|
+
[useListFooter, generichash, genericListFooter]
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
const ListHeaderComponent = useMemo(
|
|
688
|
+
() => {
|
|
689
|
+
if (!ListHeaderGenericComponent) return null
|
|
690
|
+
return createElement(ListHeaderGenericComponent, { listHeaderData })
|
|
691
|
+
},
|
|
692
|
+
[ListHeaderGenericComponent, listHeaderData]
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
const ListFooterComponent = useMemo(
|
|
696
|
+
() => {
|
|
697
|
+
if (!ListFooterGenericComponent) return null
|
|
698
|
+
return createElement(ListFooterGenericComponent, { listFooterData })
|
|
699
|
+
},
|
|
700
|
+
[ListFooterGenericComponent, listFooterData]
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
const sectionListProps: RNSectionListProps<ListItem, SectionExtra> = extendObject(
|
|
704
|
+
{
|
|
705
|
+
sections: convertedListData,
|
|
706
|
+
renderItem: renderItem,
|
|
707
|
+
getItemLayout: getItemLayout,
|
|
708
|
+
ListHeaderComponent: useListHeader ? ListHeaderComponent : null,
|
|
709
|
+
ListFooterComponent: useListFooter ? ListFooterComponent : null,
|
|
710
|
+
renderSectionHeader: renderSectionHeader,
|
|
711
|
+
renderSectionFooter: renderSectionFooter,
|
|
712
|
+
refreshControl: refresherEnabled
|
|
713
|
+
? createElement(RefreshControl, {
|
|
714
|
+
onRefresh: onRefresh,
|
|
715
|
+
refreshing: refreshing
|
|
716
|
+
})
|
|
717
|
+
: undefined
|
|
718
|
+
},
|
|
719
|
+
innerProps
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
return createElement(
|
|
723
|
+
GestureDetector,
|
|
724
|
+
{ gesture: nativeGesture },
|
|
725
|
+
createElement(
|
|
726
|
+
TypedSectionList,
|
|
727
|
+
sectionListProps
|
|
728
|
+
)
|
|
729
|
+
)
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
_SectionList.displayName = 'MpxSectionList'
|
|
733
|
+
|
|
734
|
+
export default _SectionList
|