@legendapp/list 0.1.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/.DS_Store +0 -0
- package/CHANGELOG.md +0 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/index.d.mts +37 -0
- package/index.d.ts +37 -0
- package/index.js +393 -0
- package/index.mjs +372 -0
- package/package.json +38 -0
package/.DS_Store
ADDED
|
Binary file
|
package/CHANGELOG.md
ADDED
|
File without changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Moo.do LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Legend List
|
|
2
|
+
|
|
3
|
+
Legend List aims to be a drop-in replacement for FlatList with much better performance and supporting dynamically sized items.
|
|
4
|
+
|
|
5
|
+
## Caution: Experimental
|
|
6
|
+
|
|
7
|
+
This is an early release to test and gather feedback. It's not used in production yet and needs more work to reach parity with FlatList features.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
In addition to normal FlatList features:
|
|
12
|
+
|
|
13
|
+
- Dynamic layouts supported. Just use the `estimatedItemLength` prop to give a close estimate so that layouts aren't too far off, and positions will adjust while rendering.
|
|
14
|
+
- `autoScrollToBottom`: If true and scroll is within `autoScrollToBottomThreshold * screen height` then changing items or heights will scroll to the bottom. This can be useful for chat interfaces.
|
|
15
|
+
- `recycleItems` prop enables toggling recycling of list items. If enabled it will reuse item components for improved performance, but it will reuse any local state in items. So if you have local state in items you likely want this disabled.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Props
|
|
20
|
+
|
|
21
|
+
We suggest using all of the required props and additionally `keyExtractor` to improve performance when adding/removing items.
|
|
22
|
+
|
|
23
|
+
#### Required
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
interface PropsRequired {
|
|
27
|
+
data: ArrayLike<any> & T[];
|
|
28
|
+
renderItem: (props: LegendListRenderItemInfo<T>) => ReactNode;
|
|
29
|
+
estimatedItemLength: (index: number) => number;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### Optional
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
interface PropsOptional {
|
|
37
|
+
initialScrollOffset?: number;
|
|
38
|
+
initialScrollIndex?: number;
|
|
39
|
+
drawDistance?: number;
|
|
40
|
+
initialContainers?: number;
|
|
41
|
+
recycleItems?: boolean;
|
|
42
|
+
onEndReachedThreshold?: number | null | undefined;
|
|
43
|
+
autoScrollToBottom?: boolean;
|
|
44
|
+
autoScrollToBottomThreshold?: number;
|
|
45
|
+
onEndReached?: ((info: { distanceFromEnd: number }) => void) | null | undefined;
|
|
46
|
+
keyExtractor?: (item: T, index: number) => string;
|
|
47
|
+
onViewableRangeChanged?: (range: ViewableRange<T>) => void;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How to build
|
|
52
|
+
|
|
53
|
+
`npm run build` will build the package to the `dist` folder.
|
|
54
|
+
|
|
55
|
+
## How to run example
|
|
56
|
+
|
|
57
|
+
1. `cd example`
|
|
58
|
+
2. `npm i`
|
|
59
|
+
3. `npm run bootstrap-start`
|
|
60
|
+
|
|
61
|
+
## PRs gladly accepted!
|
|
62
|
+
|
|
63
|
+
There's not a lot of code here so hopefully it's easy to contribute. If you want to add a missing feature or fix a bug please post an issue to see if development is already in progress so we can make sure to not duplicate work 😀.
|
|
64
|
+
|
|
65
|
+
## TODO list
|
|
66
|
+
|
|
67
|
+
- onViewableItemsChanged
|
|
68
|
+
- Adjust scroll when item heights change above the viewable area so they don't jump
|
|
69
|
+
- A prop to start with items at the bottom like a chat interface, just needs to pad the top with screen height - items height
|
|
70
|
+
- Other important missing features from FlatList or other lists libraries
|
|
71
|
+
- Optimizations:
|
|
72
|
+
- Loop over only potentially changed items when adjusting heights rather than data array
|
package/index.d.mts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ComponentProps, ReactNode } from 'react';
|
|
3
|
+
import { ScrollView } from 'react-native';
|
|
4
|
+
|
|
5
|
+
type LegendListProps<T> = Omit<ComponentProps<typeof ScrollView>, 'contentOffset'> & {
|
|
6
|
+
data: ArrayLike<any> & T[];
|
|
7
|
+
initialScrollOffset?: number;
|
|
8
|
+
initialScrollIndex?: number;
|
|
9
|
+
drawDistance?: number;
|
|
10
|
+
initialContainers?: number;
|
|
11
|
+
recycleItems?: boolean;
|
|
12
|
+
onEndReachedThreshold?: number | null | undefined;
|
|
13
|
+
autoScrollToBottom?: boolean;
|
|
14
|
+
autoScrollToBottomThreshold?: number;
|
|
15
|
+
estimatedItemLength: (index: number) => number;
|
|
16
|
+
onEndReached?: ((info: {
|
|
17
|
+
distanceFromEnd: number;
|
|
18
|
+
}) => void) | null | undefined;
|
|
19
|
+
keyExtractor?: (item: T, index: number) => string;
|
|
20
|
+
renderItem?: (props: LegendListRenderItemInfo<T>) => ReactNode;
|
|
21
|
+
onViewableRangeChanged?: (range: ViewableRange<T>) => void;
|
|
22
|
+
};
|
|
23
|
+
interface ViewableRange<T> {
|
|
24
|
+
startBuffered: number;
|
|
25
|
+
start: number;
|
|
26
|
+
endBuffered: number;
|
|
27
|
+
end: number;
|
|
28
|
+
items: T[];
|
|
29
|
+
}
|
|
30
|
+
interface LegendListRenderItemInfo<ItemT> {
|
|
31
|
+
item: ItemT;
|
|
32
|
+
index: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare function LegendList<T>(props: LegendListProps<T>): React.JSX.Element;
|
|
36
|
+
|
|
37
|
+
export { LegendList, type LegendListProps, type LegendListRenderItemInfo, type ViewableRange };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ComponentProps, ReactNode } from 'react';
|
|
3
|
+
import { ScrollView } from 'react-native';
|
|
4
|
+
|
|
5
|
+
type LegendListProps<T> = Omit<ComponentProps<typeof ScrollView>, 'contentOffset'> & {
|
|
6
|
+
data: ArrayLike<any> & T[];
|
|
7
|
+
initialScrollOffset?: number;
|
|
8
|
+
initialScrollIndex?: number;
|
|
9
|
+
drawDistance?: number;
|
|
10
|
+
initialContainers?: number;
|
|
11
|
+
recycleItems?: boolean;
|
|
12
|
+
onEndReachedThreshold?: number | null | undefined;
|
|
13
|
+
autoScrollToBottom?: boolean;
|
|
14
|
+
autoScrollToBottomThreshold?: number;
|
|
15
|
+
estimatedItemLength: (index: number) => number;
|
|
16
|
+
onEndReached?: ((info: {
|
|
17
|
+
distanceFromEnd: number;
|
|
18
|
+
}) => void) | null | undefined;
|
|
19
|
+
keyExtractor?: (item: T, index: number) => string;
|
|
20
|
+
renderItem?: (props: LegendListRenderItemInfo<T>) => ReactNode;
|
|
21
|
+
onViewableRangeChanged?: (range: ViewableRange<T>) => void;
|
|
22
|
+
};
|
|
23
|
+
interface ViewableRange<T> {
|
|
24
|
+
startBuffered: number;
|
|
25
|
+
start: number;
|
|
26
|
+
endBuffered: number;
|
|
27
|
+
end: number;
|
|
28
|
+
items: T[];
|
|
29
|
+
}
|
|
30
|
+
interface LegendListRenderItemInfo<ItemT> {
|
|
31
|
+
item: ItemT;
|
|
32
|
+
index: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare function LegendList<T>(props: LegendListProps<T>): React.JSX.Element;
|
|
36
|
+
|
|
37
|
+
export { LegendList, type LegendListProps, type LegendListRenderItemInfo, type ViewableRange };
|
package/index.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React2 = require('react');
|
|
4
|
+
var state = require('@legendapp/state');
|
|
5
|
+
var enableReactNativeComponents = require('@legendapp/state/config/enableReactNativeComponents');
|
|
6
|
+
var react = require('@legendapp/state/react');
|
|
7
|
+
var reactNative = require('react-native');
|
|
8
|
+
|
|
9
|
+
function _interopNamespace(e) {
|
|
10
|
+
if (e && e.__esModule) return e;
|
|
11
|
+
var n = Object.create(null);
|
|
12
|
+
if (e) {
|
|
13
|
+
Object.keys(e).forEach(function (k) {
|
|
14
|
+
if (k !== 'default') {
|
|
15
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
16
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () { return e[k]; }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
n.default = e;
|
|
24
|
+
return Object.freeze(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
|
|
28
|
+
|
|
29
|
+
// src/LegendList.tsx
|
|
30
|
+
enableReactNativeComponents.enableReactNativeComponents();
|
|
31
|
+
var Container = ({
|
|
32
|
+
$container,
|
|
33
|
+
recycleItems,
|
|
34
|
+
listProps,
|
|
35
|
+
getRenderedItem,
|
|
36
|
+
onLayout
|
|
37
|
+
}) => {
|
|
38
|
+
const { horizontal } = listProps;
|
|
39
|
+
const { id } = $container.peek();
|
|
40
|
+
const itemIndex = react.use$($container.itemIndex);
|
|
41
|
+
const key = recycleItems ? void 0 : itemIndex;
|
|
42
|
+
const createStyle = () => horizontal ? {
|
|
43
|
+
position: "absolute",
|
|
44
|
+
top: 0,
|
|
45
|
+
bottom: 0,
|
|
46
|
+
left: $container.position.get(),
|
|
47
|
+
opacity: $container.position.get() < 0 ? 0 : 1
|
|
48
|
+
} : {
|
|
49
|
+
position: "absolute",
|
|
50
|
+
left: 0,
|
|
51
|
+
right: 0,
|
|
52
|
+
top: $container.position.get(),
|
|
53
|
+
opacity: $container.position.get() < 0 ? 0 : 1
|
|
54
|
+
};
|
|
55
|
+
return itemIndex < 0 ? null : /* @__PURE__ */ React2__namespace.createElement(
|
|
56
|
+
react.Reactive.View,
|
|
57
|
+
{
|
|
58
|
+
key: id,
|
|
59
|
+
$style: createStyle,
|
|
60
|
+
onLayout: (event) => {
|
|
61
|
+
const index = $container.itemIndex.peek();
|
|
62
|
+
const length = Math.round(event.nativeEvent.layout[horizontal ? "width" : "height"]);
|
|
63
|
+
onLayout(index, length);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
/* @__PURE__ */ React2__namespace.createElement(reactNative.View, { key }, getRenderedItem(itemIndex))
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/LegendList.tsx
|
|
71
|
+
enableReactNativeComponents.enableReactNativeComponents();
|
|
72
|
+
var DEFAULT_SCROLL_BUFFER = 0;
|
|
73
|
+
var POSITION_OUT_OF_VIEW = -1e4;
|
|
74
|
+
function LegendList(props) {
|
|
75
|
+
const {
|
|
76
|
+
data,
|
|
77
|
+
initialScrollIndex,
|
|
78
|
+
initialScrollOffset,
|
|
79
|
+
horizontal,
|
|
80
|
+
style,
|
|
81
|
+
contentContainerStyle,
|
|
82
|
+
initialContainers,
|
|
83
|
+
drawDistance,
|
|
84
|
+
recycleItems = true,
|
|
85
|
+
onEndReachedThreshold,
|
|
86
|
+
autoScrollToBottom = false,
|
|
87
|
+
autoScrollToBottomThreshold = 0.1,
|
|
88
|
+
keyExtractor,
|
|
89
|
+
renderItem,
|
|
90
|
+
estimatedItemLength,
|
|
91
|
+
onEndReached,
|
|
92
|
+
onViewableRangeChanged,
|
|
93
|
+
...rest
|
|
94
|
+
} = props;
|
|
95
|
+
const refScroller = React2.useRef(null);
|
|
96
|
+
const containers$ = react.useObservable(() => []);
|
|
97
|
+
const visibleRange$ = react.useObservable(() => ({
|
|
98
|
+
start: 0,
|
|
99
|
+
end: 0,
|
|
100
|
+
totalLength: 0,
|
|
101
|
+
scroll: 0,
|
|
102
|
+
topPad: 0
|
|
103
|
+
}));
|
|
104
|
+
const scrollBuffer = drawDistance != null ? drawDistance : DEFAULT_SCROLL_BUFFER;
|
|
105
|
+
const refPositions = React2.useRef();
|
|
106
|
+
const getId = (index) => {
|
|
107
|
+
var _a;
|
|
108
|
+
const data2 = (_a = refPositions.current) == null ? void 0 : _a.data;
|
|
109
|
+
if (!data2) {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
const ret = index < data2.length ? keyExtractor ? keyExtractor(data2[index], index) : index : null;
|
|
113
|
+
return ret + "";
|
|
114
|
+
};
|
|
115
|
+
if (!refPositions.current) {
|
|
116
|
+
refPositions.current = {
|
|
117
|
+
lengths: /* @__PURE__ */ new Map(),
|
|
118
|
+
positions: /* @__PURE__ */ new Map(),
|
|
119
|
+
pendingAdjust: 0,
|
|
120
|
+
animFrame: null,
|
|
121
|
+
isStartReached: false,
|
|
122
|
+
isEndReached: false,
|
|
123
|
+
isAtBottom: false,
|
|
124
|
+
data,
|
|
125
|
+
idsInFirstRender: void 0,
|
|
126
|
+
hasScrolled: false
|
|
127
|
+
};
|
|
128
|
+
refPositions.current.idsInFirstRender = new Set(data.map((_, i) => getId(i)));
|
|
129
|
+
}
|
|
130
|
+
refPositions.current.data = data;
|
|
131
|
+
const SCREEN_LENGTH = reactNative.Dimensions.get("window")[horizontal ? "width" : "height"];
|
|
132
|
+
const initialContentOffset = initialScrollOffset != null ? initialScrollOffset : initialScrollIndex ? initialScrollIndex * estimatedItemLength(initialScrollIndex) : void 0;
|
|
133
|
+
const allocateContainers = React2.useCallback(() => {
|
|
134
|
+
const numContainers = initialContainers || Math.ceil((SCREEN_LENGTH + scrollBuffer * 2) / estimatedItemLength(0)) + 4;
|
|
135
|
+
const containers2 = [];
|
|
136
|
+
for (let i = 0; i < numContainers; i++) {
|
|
137
|
+
containers2.push({
|
|
138
|
+
id: i,
|
|
139
|
+
itemIndex: -1,
|
|
140
|
+
position: POSITION_OUT_OF_VIEW
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
containers$.set(containers2);
|
|
144
|
+
}, []);
|
|
145
|
+
const getRenderedItem = React2.useCallback(
|
|
146
|
+
(index) => {
|
|
147
|
+
var _a;
|
|
148
|
+
const data2 = (_a = refPositions.current) == null ? void 0 : _a.data;
|
|
149
|
+
if (!data2) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const renderedItem = renderItem == null ? void 0 : renderItem({
|
|
153
|
+
item: data2[index],
|
|
154
|
+
index
|
|
155
|
+
});
|
|
156
|
+
return renderedItem;
|
|
157
|
+
},
|
|
158
|
+
[renderItem]
|
|
159
|
+
);
|
|
160
|
+
const calculateItemsInView = React2.useCallback(() => {
|
|
161
|
+
var _a, _b;
|
|
162
|
+
const data2 = refPositions.current.data;
|
|
163
|
+
if (!data2) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const scroll = visibleRange$.scroll.peek() - visibleRange$.topPad.peek();
|
|
167
|
+
const containers2 = containers$.peek();
|
|
168
|
+
const { lengths, positions } = refPositions.current;
|
|
169
|
+
let top = 0;
|
|
170
|
+
let startNoBuffer = null;
|
|
171
|
+
let startBuffered = null;
|
|
172
|
+
let endNoBuffer = null;
|
|
173
|
+
let endBuffered = null;
|
|
174
|
+
const prevRange = onViewableRangeChanged ? { ...visibleRange$.peek() } : void 0;
|
|
175
|
+
for (let i = 0; i < data2.length; i++) {
|
|
176
|
+
const id = getId(i);
|
|
177
|
+
const length = (_a = lengths.get(id)) != null ? _a : estimatedItemLength(i);
|
|
178
|
+
if (positions.get(id) !== top) {
|
|
179
|
+
positions.set(id, top);
|
|
180
|
+
}
|
|
181
|
+
if (startNoBuffer === null && top + length > scroll) {
|
|
182
|
+
startNoBuffer = i;
|
|
183
|
+
}
|
|
184
|
+
if (startBuffered === null && top + length > scroll - scrollBuffer) {
|
|
185
|
+
startBuffered = i;
|
|
186
|
+
}
|
|
187
|
+
if (startNoBuffer !== null) {
|
|
188
|
+
if (top <= scroll + SCREEN_LENGTH) {
|
|
189
|
+
endNoBuffer = i;
|
|
190
|
+
}
|
|
191
|
+
if (top <= scroll + SCREEN_LENGTH + scrollBuffer) {
|
|
192
|
+
endBuffered = i;
|
|
193
|
+
} else {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
top += length;
|
|
198
|
+
}
|
|
199
|
+
visibleRange$.assign({
|
|
200
|
+
startBuffered,
|
|
201
|
+
startNoBuffer,
|
|
202
|
+
endBuffered,
|
|
203
|
+
endNoBuffer
|
|
204
|
+
});
|
|
205
|
+
state.beginBatch();
|
|
206
|
+
if (startBuffered !== null && endBuffered !== null) {
|
|
207
|
+
for (let i = startBuffered; i <= endBuffered; i++) {
|
|
208
|
+
let isContained = false;
|
|
209
|
+
for (let j = 0; j < containers2.length; j++) {
|
|
210
|
+
const container = containers2[j];
|
|
211
|
+
if (container.itemIndex === i) {
|
|
212
|
+
isContained = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!isContained) {
|
|
217
|
+
let didRecycle = false;
|
|
218
|
+
for (let u = 0; u < containers2.length; u++) {
|
|
219
|
+
const container = containers2[u];
|
|
220
|
+
if (container.itemIndex < startBuffered || container.itemIndex > endBuffered) {
|
|
221
|
+
containers$[u].itemIndex.set(i);
|
|
222
|
+
didRecycle = true;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!didRecycle) {
|
|
227
|
+
if (__DEV__) {
|
|
228
|
+
console.warn(
|
|
229
|
+
"[legend-list] No container to recycle, consider increasing initialContainers or estimatedItemLength",
|
|
230
|
+
i
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
containers$.push({
|
|
234
|
+
id: containers$.peek().length,
|
|
235
|
+
itemIndex: i,
|
|
236
|
+
position: POSITION_OUT_OF_VIEW
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (let i = 0; i < containers2.length; i++) {
|
|
242
|
+
const container = containers2[i];
|
|
243
|
+
const item = data2[container.itemIndex];
|
|
244
|
+
if (item) {
|
|
245
|
+
const id = getId(container.itemIndex);
|
|
246
|
+
if (container.itemIndex < startBuffered || container.itemIndex > endBuffered) {
|
|
247
|
+
containers$[i].position.set(POSITION_OUT_OF_VIEW);
|
|
248
|
+
} else {
|
|
249
|
+
const pos = (_b = positions.get(id)) != null ? _b : -1;
|
|
250
|
+
if (pos >= 0 && pos !== containers$[i].position.peek()) {
|
|
251
|
+
containers$[i].position.set(pos);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (onViewableRangeChanged) {
|
|
257
|
+
if (startNoBuffer !== (prevRange == null ? void 0 : prevRange.startNoBuffer) || startBuffered !== (prevRange == null ? void 0 : prevRange.startBuffered) || endNoBuffer !== (prevRange == null ? void 0 : prevRange.endNoBuffer) || endBuffered !== (prevRange == null ? void 0 : prevRange.endBuffered)) {
|
|
258
|
+
onViewableRangeChanged({
|
|
259
|
+
start: startNoBuffer,
|
|
260
|
+
startBuffered,
|
|
261
|
+
end: endNoBuffer,
|
|
262
|
+
endBuffered,
|
|
263
|
+
items: data2.slice(startNoBuffer, endNoBuffer + 1)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
state.endBatch();
|
|
269
|
+
}, [data]);
|
|
270
|
+
React2.useMemo(() => {
|
|
271
|
+
var _a, _b;
|
|
272
|
+
allocateContainers();
|
|
273
|
+
calculateItemsInView();
|
|
274
|
+
const lengths = (_a = refPositions.current) == null ? void 0 : _a.lengths;
|
|
275
|
+
let totalLength = 0;
|
|
276
|
+
for (let i = 0; i < data.length; i++) {
|
|
277
|
+
const id = getId(i);
|
|
278
|
+
totalLength += (_b = lengths.get(id)) != null ? _b : estimatedItemLength(i);
|
|
279
|
+
}
|
|
280
|
+
visibleRange$.totalLength.set(totalLength);
|
|
281
|
+
}, []);
|
|
282
|
+
React2.useMemo(() => {
|
|
283
|
+
if (refPositions.current) {
|
|
284
|
+
refPositions.current.isEndReached = false;
|
|
285
|
+
}
|
|
286
|
+
}, [data]);
|
|
287
|
+
const containers = react.use$(containers$, { shallow: true });
|
|
288
|
+
const updateItemLength = React2.useCallback((index, length) => {
|
|
289
|
+
var _a, _b, _c, _d, _e;
|
|
290
|
+
const data2 = (_a = refPositions.current) == null ? void 0 : _a.data;
|
|
291
|
+
if (!data2) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const lengths = (_b = refPositions.current) == null ? void 0 : _b.lengths;
|
|
295
|
+
const id = getId(index);
|
|
296
|
+
const wasInFirstRender = (_c = refPositions.current) == null ? void 0 : _c.idsInFirstRender.has(id);
|
|
297
|
+
const prevLength = lengths.get(id) || (wasInFirstRender ? estimatedItemLength(index) : 0);
|
|
298
|
+
if (!prevLength || prevLength !== length) {
|
|
299
|
+
state.beginBatch();
|
|
300
|
+
lengths.set(id, length);
|
|
301
|
+
visibleRange$.totalLength.set((prevTotal) => prevTotal + (length - prevLength));
|
|
302
|
+
if (((_d = refPositions.current) == null ? void 0 : _d.isAtBottom) && autoScrollToBottom) {
|
|
303
|
+
requestAnimationFrame(() => {
|
|
304
|
+
var _a2;
|
|
305
|
+
(_a2 = refScroller.current) == null ? void 0 : _a2.scrollToEnd({
|
|
306
|
+
animated: true
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (!((_e = refPositions.current) == null ? void 0 : _e.animFrame)) {
|
|
311
|
+
calculateItemsInView();
|
|
312
|
+
}
|
|
313
|
+
state.endBatch();
|
|
314
|
+
}
|
|
315
|
+
}, []);
|
|
316
|
+
const handleScrollDebounced = React2.useCallback(() => {
|
|
317
|
+
var _a;
|
|
318
|
+
const newScroll = visibleRange$.scroll.peek();
|
|
319
|
+
calculateItemsInView();
|
|
320
|
+
const distanceFromEnd = visibleRange$.totalLength.peek() - newScroll - SCREEN_LENGTH;
|
|
321
|
+
if (refPositions.current) {
|
|
322
|
+
refPositions.current.isAtBottom = distanceFromEnd < SCREEN_LENGTH * autoScrollToBottomThreshold;
|
|
323
|
+
}
|
|
324
|
+
if (onEndReached && !((_a = refPositions.current) == null ? void 0 : _a.isEndReached)) {
|
|
325
|
+
if (distanceFromEnd < (onEndReachedThreshold || 0.5) * SCREEN_LENGTH) {
|
|
326
|
+
if (refPositions.current) {
|
|
327
|
+
refPositions.current.isEndReached = true;
|
|
328
|
+
}
|
|
329
|
+
onEndReached({ distanceFromEnd });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (refPositions.current) {
|
|
333
|
+
refPositions.current.animFrame = null;
|
|
334
|
+
}
|
|
335
|
+
}, []);
|
|
336
|
+
const handleScroll = React2.useCallback((event) => {
|
|
337
|
+
refPositions.current.hasScrolled = true;
|
|
338
|
+
const newScroll = event.nativeEvent.contentOffset[horizontal ? "x" : "y"];
|
|
339
|
+
visibleRange$.scroll.set(newScroll);
|
|
340
|
+
if (refPositions.current && !refPositions.current.animFrame) {
|
|
341
|
+
refPositions.current.animFrame = requestAnimationFrame(handleScrollDebounced);
|
|
342
|
+
}
|
|
343
|
+
}, []);
|
|
344
|
+
React2.useEffect(() => {
|
|
345
|
+
if (initialContentOffset) {
|
|
346
|
+
handleScroll({
|
|
347
|
+
nativeEvent: { contentOffset: { y: initialContentOffset } }
|
|
348
|
+
});
|
|
349
|
+
calculateItemsInView();
|
|
350
|
+
}
|
|
351
|
+
}, []);
|
|
352
|
+
return /* @__PURE__ */ React2__namespace.createElement(
|
|
353
|
+
react.Reactive.ScrollView,
|
|
354
|
+
{
|
|
355
|
+
style,
|
|
356
|
+
contentContainerStyle: [
|
|
357
|
+
contentContainerStyle,
|
|
358
|
+
horizontal ? {
|
|
359
|
+
height: "100%"
|
|
360
|
+
} : {}
|
|
361
|
+
],
|
|
362
|
+
onScroll: handleScroll,
|
|
363
|
+
scrollEventThrottle: 32,
|
|
364
|
+
horizontal,
|
|
365
|
+
contentOffset: initialContentOffset ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0,
|
|
366
|
+
...rest,
|
|
367
|
+
ref: refScroller
|
|
368
|
+
},
|
|
369
|
+
/* @__PURE__ */ React2__namespace.createElement(
|
|
370
|
+
react.Reactive.View,
|
|
371
|
+
{
|
|
372
|
+
$style: () => horizontal ? {
|
|
373
|
+
width: visibleRange$.totalLength.get()
|
|
374
|
+
} : {
|
|
375
|
+
height: visibleRange$.totalLength.get()
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
containers.map((container, i) => /* @__PURE__ */ React2__namespace.createElement(
|
|
379
|
+
Container,
|
|
380
|
+
{
|
|
381
|
+
key: container.id,
|
|
382
|
+
recycleItems,
|
|
383
|
+
$container: containers$[i],
|
|
384
|
+
listProps: props,
|
|
385
|
+
getRenderedItem,
|
|
386
|
+
onLayout: updateItemLength
|
|
387
|
+
}
|
|
388
|
+
))
|
|
389
|
+
)
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
exports.LegendList = LegendList;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import * as React2 from 'react';
|
|
2
|
+
import { useRef, useCallback, useMemo, useEffect } from 'react';
|
|
3
|
+
import { beginBatch, endBatch } from '@legendapp/state';
|
|
4
|
+
import { enableReactNativeComponents } from '@legendapp/state/config/enableReactNativeComponents';
|
|
5
|
+
import { useObservable, use$, Reactive } from '@legendapp/state/react';
|
|
6
|
+
import { Dimensions, View } from 'react-native';
|
|
7
|
+
|
|
8
|
+
// src/LegendList.tsx
|
|
9
|
+
enableReactNativeComponents();
|
|
10
|
+
var Container = ({
|
|
11
|
+
$container,
|
|
12
|
+
recycleItems,
|
|
13
|
+
listProps,
|
|
14
|
+
getRenderedItem,
|
|
15
|
+
onLayout
|
|
16
|
+
}) => {
|
|
17
|
+
const { horizontal } = listProps;
|
|
18
|
+
const { id } = $container.peek();
|
|
19
|
+
const itemIndex = use$($container.itemIndex);
|
|
20
|
+
const key = recycleItems ? void 0 : itemIndex;
|
|
21
|
+
const createStyle = () => horizontal ? {
|
|
22
|
+
position: "absolute",
|
|
23
|
+
top: 0,
|
|
24
|
+
bottom: 0,
|
|
25
|
+
left: $container.position.get(),
|
|
26
|
+
opacity: $container.position.get() < 0 ? 0 : 1
|
|
27
|
+
} : {
|
|
28
|
+
position: "absolute",
|
|
29
|
+
left: 0,
|
|
30
|
+
right: 0,
|
|
31
|
+
top: $container.position.get(),
|
|
32
|
+
opacity: $container.position.get() < 0 ? 0 : 1
|
|
33
|
+
};
|
|
34
|
+
return itemIndex < 0 ? null : /* @__PURE__ */ React2.createElement(
|
|
35
|
+
Reactive.View,
|
|
36
|
+
{
|
|
37
|
+
key: id,
|
|
38
|
+
$style: createStyle,
|
|
39
|
+
onLayout: (event) => {
|
|
40
|
+
const index = $container.itemIndex.peek();
|
|
41
|
+
const length = Math.round(event.nativeEvent.layout[horizontal ? "width" : "height"]);
|
|
42
|
+
onLayout(index, length);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
/* @__PURE__ */ React2.createElement(View, { key }, getRenderedItem(itemIndex))
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/LegendList.tsx
|
|
50
|
+
enableReactNativeComponents();
|
|
51
|
+
var DEFAULT_SCROLL_BUFFER = 0;
|
|
52
|
+
var POSITION_OUT_OF_VIEW = -1e4;
|
|
53
|
+
function LegendList(props) {
|
|
54
|
+
const {
|
|
55
|
+
data,
|
|
56
|
+
initialScrollIndex,
|
|
57
|
+
initialScrollOffset,
|
|
58
|
+
horizontal,
|
|
59
|
+
style,
|
|
60
|
+
contentContainerStyle,
|
|
61
|
+
initialContainers,
|
|
62
|
+
drawDistance,
|
|
63
|
+
recycleItems = true,
|
|
64
|
+
onEndReachedThreshold,
|
|
65
|
+
autoScrollToBottom = false,
|
|
66
|
+
autoScrollToBottomThreshold = 0.1,
|
|
67
|
+
keyExtractor,
|
|
68
|
+
renderItem,
|
|
69
|
+
estimatedItemLength,
|
|
70
|
+
onEndReached,
|
|
71
|
+
onViewableRangeChanged,
|
|
72
|
+
...rest
|
|
73
|
+
} = props;
|
|
74
|
+
const refScroller = useRef(null);
|
|
75
|
+
const containers$ = useObservable(() => []);
|
|
76
|
+
const visibleRange$ = useObservable(() => ({
|
|
77
|
+
start: 0,
|
|
78
|
+
end: 0,
|
|
79
|
+
totalLength: 0,
|
|
80
|
+
scroll: 0,
|
|
81
|
+
topPad: 0
|
|
82
|
+
}));
|
|
83
|
+
const scrollBuffer = drawDistance != null ? drawDistance : DEFAULT_SCROLL_BUFFER;
|
|
84
|
+
const refPositions = useRef();
|
|
85
|
+
const getId = (index) => {
|
|
86
|
+
var _a;
|
|
87
|
+
const data2 = (_a = refPositions.current) == null ? void 0 : _a.data;
|
|
88
|
+
if (!data2) {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
const ret = index < data2.length ? keyExtractor ? keyExtractor(data2[index], index) : index : null;
|
|
92
|
+
return ret + "";
|
|
93
|
+
};
|
|
94
|
+
if (!refPositions.current) {
|
|
95
|
+
refPositions.current = {
|
|
96
|
+
lengths: /* @__PURE__ */ new Map(),
|
|
97
|
+
positions: /* @__PURE__ */ new Map(),
|
|
98
|
+
pendingAdjust: 0,
|
|
99
|
+
animFrame: null,
|
|
100
|
+
isStartReached: false,
|
|
101
|
+
isEndReached: false,
|
|
102
|
+
isAtBottom: false,
|
|
103
|
+
data,
|
|
104
|
+
idsInFirstRender: void 0,
|
|
105
|
+
hasScrolled: false
|
|
106
|
+
};
|
|
107
|
+
refPositions.current.idsInFirstRender = new Set(data.map((_, i) => getId(i)));
|
|
108
|
+
}
|
|
109
|
+
refPositions.current.data = data;
|
|
110
|
+
const SCREEN_LENGTH = Dimensions.get("window")[horizontal ? "width" : "height"];
|
|
111
|
+
const initialContentOffset = initialScrollOffset != null ? initialScrollOffset : initialScrollIndex ? initialScrollIndex * estimatedItemLength(initialScrollIndex) : void 0;
|
|
112
|
+
const allocateContainers = useCallback(() => {
|
|
113
|
+
const numContainers = initialContainers || Math.ceil((SCREEN_LENGTH + scrollBuffer * 2) / estimatedItemLength(0)) + 4;
|
|
114
|
+
const containers2 = [];
|
|
115
|
+
for (let i = 0; i < numContainers; i++) {
|
|
116
|
+
containers2.push({
|
|
117
|
+
id: i,
|
|
118
|
+
itemIndex: -1,
|
|
119
|
+
position: POSITION_OUT_OF_VIEW
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
containers$.set(containers2);
|
|
123
|
+
}, []);
|
|
124
|
+
const getRenderedItem = useCallback(
|
|
125
|
+
(index) => {
|
|
126
|
+
var _a;
|
|
127
|
+
const data2 = (_a = refPositions.current) == null ? void 0 : _a.data;
|
|
128
|
+
if (!data2) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const renderedItem = renderItem == null ? void 0 : renderItem({
|
|
132
|
+
item: data2[index],
|
|
133
|
+
index
|
|
134
|
+
});
|
|
135
|
+
return renderedItem;
|
|
136
|
+
},
|
|
137
|
+
[renderItem]
|
|
138
|
+
);
|
|
139
|
+
const calculateItemsInView = useCallback(() => {
|
|
140
|
+
var _a, _b;
|
|
141
|
+
const data2 = refPositions.current.data;
|
|
142
|
+
if (!data2) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const scroll = visibleRange$.scroll.peek() - visibleRange$.topPad.peek();
|
|
146
|
+
const containers2 = containers$.peek();
|
|
147
|
+
const { lengths, positions } = refPositions.current;
|
|
148
|
+
let top = 0;
|
|
149
|
+
let startNoBuffer = null;
|
|
150
|
+
let startBuffered = null;
|
|
151
|
+
let endNoBuffer = null;
|
|
152
|
+
let endBuffered = null;
|
|
153
|
+
const prevRange = onViewableRangeChanged ? { ...visibleRange$.peek() } : void 0;
|
|
154
|
+
for (let i = 0; i < data2.length; i++) {
|
|
155
|
+
const id = getId(i);
|
|
156
|
+
const length = (_a = lengths.get(id)) != null ? _a : estimatedItemLength(i);
|
|
157
|
+
if (positions.get(id) !== top) {
|
|
158
|
+
positions.set(id, top);
|
|
159
|
+
}
|
|
160
|
+
if (startNoBuffer === null && top + length > scroll) {
|
|
161
|
+
startNoBuffer = i;
|
|
162
|
+
}
|
|
163
|
+
if (startBuffered === null && top + length > scroll - scrollBuffer) {
|
|
164
|
+
startBuffered = i;
|
|
165
|
+
}
|
|
166
|
+
if (startNoBuffer !== null) {
|
|
167
|
+
if (top <= scroll + SCREEN_LENGTH) {
|
|
168
|
+
endNoBuffer = i;
|
|
169
|
+
}
|
|
170
|
+
if (top <= scroll + SCREEN_LENGTH + scrollBuffer) {
|
|
171
|
+
endBuffered = i;
|
|
172
|
+
} else {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
top += length;
|
|
177
|
+
}
|
|
178
|
+
visibleRange$.assign({
|
|
179
|
+
startBuffered,
|
|
180
|
+
startNoBuffer,
|
|
181
|
+
endBuffered,
|
|
182
|
+
endNoBuffer
|
|
183
|
+
});
|
|
184
|
+
beginBatch();
|
|
185
|
+
if (startBuffered !== null && endBuffered !== null) {
|
|
186
|
+
for (let i = startBuffered; i <= endBuffered; i++) {
|
|
187
|
+
let isContained = false;
|
|
188
|
+
for (let j = 0; j < containers2.length; j++) {
|
|
189
|
+
const container = containers2[j];
|
|
190
|
+
if (container.itemIndex === i) {
|
|
191
|
+
isContained = true;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!isContained) {
|
|
196
|
+
let didRecycle = false;
|
|
197
|
+
for (let u = 0; u < containers2.length; u++) {
|
|
198
|
+
const container = containers2[u];
|
|
199
|
+
if (container.itemIndex < startBuffered || container.itemIndex > endBuffered) {
|
|
200
|
+
containers$[u].itemIndex.set(i);
|
|
201
|
+
didRecycle = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!didRecycle) {
|
|
206
|
+
if (__DEV__) {
|
|
207
|
+
console.warn(
|
|
208
|
+
"[legend-list] No container to recycle, consider increasing initialContainers or estimatedItemLength",
|
|
209
|
+
i
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
containers$.push({
|
|
213
|
+
id: containers$.peek().length,
|
|
214
|
+
itemIndex: i,
|
|
215
|
+
position: POSITION_OUT_OF_VIEW
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (let i = 0; i < containers2.length; i++) {
|
|
221
|
+
const container = containers2[i];
|
|
222
|
+
const item = data2[container.itemIndex];
|
|
223
|
+
if (item) {
|
|
224
|
+
const id = getId(container.itemIndex);
|
|
225
|
+
if (container.itemIndex < startBuffered || container.itemIndex > endBuffered) {
|
|
226
|
+
containers$[i].position.set(POSITION_OUT_OF_VIEW);
|
|
227
|
+
} else {
|
|
228
|
+
const pos = (_b = positions.get(id)) != null ? _b : -1;
|
|
229
|
+
if (pos >= 0 && pos !== containers$[i].position.peek()) {
|
|
230
|
+
containers$[i].position.set(pos);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (onViewableRangeChanged) {
|
|
236
|
+
if (startNoBuffer !== (prevRange == null ? void 0 : prevRange.startNoBuffer) || startBuffered !== (prevRange == null ? void 0 : prevRange.startBuffered) || endNoBuffer !== (prevRange == null ? void 0 : prevRange.endNoBuffer) || endBuffered !== (prevRange == null ? void 0 : prevRange.endBuffered)) {
|
|
237
|
+
onViewableRangeChanged({
|
|
238
|
+
start: startNoBuffer,
|
|
239
|
+
startBuffered,
|
|
240
|
+
end: endNoBuffer,
|
|
241
|
+
endBuffered,
|
|
242
|
+
items: data2.slice(startNoBuffer, endNoBuffer + 1)
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
endBatch();
|
|
248
|
+
}, [data]);
|
|
249
|
+
useMemo(() => {
|
|
250
|
+
var _a, _b;
|
|
251
|
+
allocateContainers();
|
|
252
|
+
calculateItemsInView();
|
|
253
|
+
const lengths = (_a = refPositions.current) == null ? void 0 : _a.lengths;
|
|
254
|
+
let totalLength = 0;
|
|
255
|
+
for (let i = 0; i < data.length; i++) {
|
|
256
|
+
const id = getId(i);
|
|
257
|
+
totalLength += (_b = lengths.get(id)) != null ? _b : estimatedItemLength(i);
|
|
258
|
+
}
|
|
259
|
+
visibleRange$.totalLength.set(totalLength);
|
|
260
|
+
}, []);
|
|
261
|
+
useMemo(() => {
|
|
262
|
+
if (refPositions.current) {
|
|
263
|
+
refPositions.current.isEndReached = false;
|
|
264
|
+
}
|
|
265
|
+
}, [data]);
|
|
266
|
+
const containers = use$(containers$, { shallow: true });
|
|
267
|
+
const updateItemLength = useCallback((index, length) => {
|
|
268
|
+
var _a, _b, _c, _d, _e;
|
|
269
|
+
const data2 = (_a = refPositions.current) == null ? void 0 : _a.data;
|
|
270
|
+
if (!data2) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const lengths = (_b = refPositions.current) == null ? void 0 : _b.lengths;
|
|
274
|
+
const id = getId(index);
|
|
275
|
+
const wasInFirstRender = (_c = refPositions.current) == null ? void 0 : _c.idsInFirstRender.has(id);
|
|
276
|
+
const prevLength = lengths.get(id) || (wasInFirstRender ? estimatedItemLength(index) : 0);
|
|
277
|
+
if (!prevLength || prevLength !== length) {
|
|
278
|
+
beginBatch();
|
|
279
|
+
lengths.set(id, length);
|
|
280
|
+
visibleRange$.totalLength.set((prevTotal) => prevTotal + (length - prevLength));
|
|
281
|
+
if (((_d = refPositions.current) == null ? void 0 : _d.isAtBottom) && autoScrollToBottom) {
|
|
282
|
+
requestAnimationFrame(() => {
|
|
283
|
+
var _a2;
|
|
284
|
+
(_a2 = refScroller.current) == null ? void 0 : _a2.scrollToEnd({
|
|
285
|
+
animated: true
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (!((_e = refPositions.current) == null ? void 0 : _e.animFrame)) {
|
|
290
|
+
calculateItemsInView();
|
|
291
|
+
}
|
|
292
|
+
endBatch();
|
|
293
|
+
}
|
|
294
|
+
}, []);
|
|
295
|
+
const handleScrollDebounced = useCallback(() => {
|
|
296
|
+
var _a;
|
|
297
|
+
const newScroll = visibleRange$.scroll.peek();
|
|
298
|
+
calculateItemsInView();
|
|
299
|
+
const distanceFromEnd = visibleRange$.totalLength.peek() - newScroll - SCREEN_LENGTH;
|
|
300
|
+
if (refPositions.current) {
|
|
301
|
+
refPositions.current.isAtBottom = distanceFromEnd < SCREEN_LENGTH * autoScrollToBottomThreshold;
|
|
302
|
+
}
|
|
303
|
+
if (onEndReached && !((_a = refPositions.current) == null ? void 0 : _a.isEndReached)) {
|
|
304
|
+
if (distanceFromEnd < (onEndReachedThreshold || 0.5) * SCREEN_LENGTH) {
|
|
305
|
+
if (refPositions.current) {
|
|
306
|
+
refPositions.current.isEndReached = true;
|
|
307
|
+
}
|
|
308
|
+
onEndReached({ distanceFromEnd });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (refPositions.current) {
|
|
312
|
+
refPositions.current.animFrame = null;
|
|
313
|
+
}
|
|
314
|
+
}, []);
|
|
315
|
+
const handleScroll = useCallback((event) => {
|
|
316
|
+
refPositions.current.hasScrolled = true;
|
|
317
|
+
const newScroll = event.nativeEvent.contentOffset[horizontal ? "x" : "y"];
|
|
318
|
+
visibleRange$.scroll.set(newScroll);
|
|
319
|
+
if (refPositions.current && !refPositions.current.animFrame) {
|
|
320
|
+
refPositions.current.animFrame = requestAnimationFrame(handleScrollDebounced);
|
|
321
|
+
}
|
|
322
|
+
}, []);
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (initialContentOffset) {
|
|
325
|
+
handleScroll({
|
|
326
|
+
nativeEvent: { contentOffset: { y: initialContentOffset } }
|
|
327
|
+
});
|
|
328
|
+
calculateItemsInView();
|
|
329
|
+
}
|
|
330
|
+
}, []);
|
|
331
|
+
return /* @__PURE__ */ React2.createElement(
|
|
332
|
+
Reactive.ScrollView,
|
|
333
|
+
{
|
|
334
|
+
style,
|
|
335
|
+
contentContainerStyle: [
|
|
336
|
+
contentContainerStyle,
|
|
337
|
+
horizontal ? {
|
|
338
|
+
height: "100%"
|
|
339
|
+
} : {}
|
|
340
|
+
],
|
|
341
|
+
onScroll: handleScroll,
|
|
342
|
+
scrollEventThrottle: 32,
|
|
343
|
+
horizontal,
|
|
344
|
+
contentOffset: initialContentOffset ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0,
|
|
345
|
+
...rest,
|
|
346
|
+
ref: refScroller
|
|
347
|
+
},
|
|
348
|
+
/* @__PURE__ */ React2.createElement(
|
|
349
|
+
Reactive.View,
|
|
350
|
+
{
|
|
351
|
+
$style: () => horizontal ? {
|
|
352
|
+
width: visibleRange$.totalLength.get()
|
|
353
|
+
} : {
|
|
354
|
+
height: visibleRange$.totalLength.get()
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
containers.map((container, i) => /* @__PURE__ */ React2.createElement(
|
|
358
|
+
Container,
|
|
359
|
+
{
|
|
360
|
+
key: container.id,
|
|
361
|
+
recycleItems,
|
|
362
|
+
$container: containers$[i],
|
|
363
|
+
listProps: props,
|
|
364
|
+
getRenderedItem,
|
|
365
|
+
onLayout: updateItemLength
|
|
366
|
+
}
|
|
367
|
+
))
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export { LegendList };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@legendapp/list",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "legend-list",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "./index.js",
|
|
8
|
+
"module": "./index.mjs",
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"**"
|
|
12
|
+
],
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"react": "*",
|
|
15
|
+
"react-native": "*"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@legendapp/state": "^3.0.0-beta.19"
|
|
19
|
+
},
|
|
20
|
+
"author": "Legend <contact@legendapp.com> (https://github.com/LegendApp)",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"react",
|
|
23
|
+
"react-native",
|
|
24
|
+
"list"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/LegendApp/legend-list.git"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/LegendApp/legend-list/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/LegendApp/legend-list#readme",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
}
|
|
38
|
+
}
|