1 | import Component from "@egjs/component";
|
2 | import { diff } from "@egjs/list-differ";
|
3 | import { DIRECTION } from "./consts";
|
4 | import { findIndex, getNextCursors, isFlatOutline } from "./utils";
|
5 |
|
6 | export interface OnInfiniteRequestAppend {
|
7 | key?: string | number | undefined;
|
8 | nextKey?: string | number | undefined;
|
9 | isVirtual: boolean;
|
10 | }
|
11 |
|
12 | export interface OnInfiniteRequestPrepend {
|
13 | key?: string | number;
|
14 | nextKey?: string | number;
|
15 | isVirtual: boolean;
|
16 | }
|
17 |
|
18 | export interface OnInfiniteChange {
|
19 | prevStartCursor: number;
|
20 | prevEndCursor: number;
|
21 | nextStartCursor: number;
|
22 | nextEndCursor: number;
|
23 | }
|
24 |
|
25 | export interface InfiniteEvents {
|
26 | requestAppend: OnInfiniteRequestAppend;
|
27 | requestPrepend: OnInfiniteRequestPrepend;
|
28 | change: OnInfiniteChange;
|
29 | }
|
30 |
|
31 | export interface InfiniteOptions {
|
32 | useRecycle?: boolean;
|
33 | threshold?: number;
|
34 | defaultDirection?: "start" | "end";
|
35 | }
|
36 |
|
37 | export interface InfiniteItem {
|
38 | key: string | number;
|
39 | startOutline: number[];
|
40 | endOutline: number[];
|
41 | isVirtual?: boolean;
|
42 | }
|
43 |
|
44 | export class Infinite extends Component<InfiniteEvents> {
|
45 | public options: Required<InfiniteOptions>;
|
46 | protected startCursor = -1;
|
47 | protected endCursor = -1;
|
48 | protected size = 0;
|
49 | protected items: InfiniteItem[] = [];
|
50 | protected itemKeys: Record<string | number, InfiniteItem> = {};
|
51 | constructor(options: InfiniteOptions) {
|
52 | super();
|
53 | this.options = {
|
54 | threshold: 0,
|
55 | useRecycle: true,
|
56 | defaultDirection: "end",
|
57 | ...options,
|
58 | };
|
59 | }
|
60 | public scroll(scrollPos: number) {
|
61 | const prevStartCursor = this.startCursor;
|
62 | const prevEndCursor = this.endCursor;
|
63 | const items = this.items;
|
64 | const length = items.length;
|
65 | const size = this.size;
|
66 | const {
|
67 | defaultDirection,
|
68 | threshold,
|
69 | useRecycle,
|
70 | } = this.options;
|
71 | const isDirectionEnd = defaultDirection === "end";
|
72 |
|
73 | if (!length) {
|
74 | this.trigger(isDirectionEnd ? "requestAppend" : "requestPrepend", {
|
75 | key: undefined,
|
76 | isVirtual: false,
|
77 | });
|
78 | return;
|
79 | } else if (prevStartCursor === -1 || prevEndCursor === -1) {
|
80 | const nextCursor = isDirectionEnd ? 0 : length - 1;
|
81 | this.trigger("change", {
|
82 | prevStartCursor,
|
83 | prevEndCursor,
|
84 | nextStartCursor: nextCursor,
|
85 | nextEndCursor: nextCursor,
|
86 | });
|
87 | return;
|
88 | }
|
89 |
|
90 | const endScrollPos = scrollPos + size;
|
91 | const startEdgePos = Math.max(...items[prevStartCursor].startOutline);
|
92 | const endEdgePos = Math.min(...items[prevEndCursor].endOutline);
|
93 | const visibles = items.map((item) => {
|
94 | const {
|
95 | startOutline,
|
96 | endOutline,
|
97 | } = item;
|
98 |
|
99 | if (!startOutline.length || !endOutline.length) {
|
100 | return false;
|
101 | }
|
102 | const startPos = Math.min(...startOutline);
|
103 | const endPos = Math.max(...endOutline);
|
104 |
|
105 | if (startPos - threshold <= endScrollPos && scrollPos <= endPos + threshold) {
|
106 | return true;
|
107 | }
|
108 | return false;
|
109 | });
|
110 | const hasStartItems = 0 < prevStartCursor;
|
111 | const hasEndItems = prevEndCursor < length - 1;
|
112 | const isStart = scrollPos <= startEdgePos + threshold;
|
113 | const isEnd = endScrollPos >= endEdgePos - threshold;
|
114 | let nextStartCursor = visibles.indexOf(true);
|
115 | let nextEndCursor = visibles.lastIndexOf(true);
|
116 |
|
117 | if (nextStartCursor === -1) {
|
118 | nextStartCursor = prevStartCursor;
|
119 | nextEndCursor = prevEndCursor;
|
120 | }
|
121 |
|
122 | if (!useRecycle) {
|
123 | nextStartCursor = Math.min(nextStartCursor, prevStartCursor);
|
124 | nextEndCursor = Math.max(nextEndCursor, prevEndCursor);
|
125 | }
|
126 | if (nextStartCursor === prevStartCursor && hasStartItems && isStart) {
|
127 | nextStartCursor -= 1;
|
128 | }
|
129 | if (nextEndCursor === prevEndCursor && hasEndItems && isEnd) {
|
130 | nextEndCursor += 1;
|
131 | }
|
132 | if (prevStartCursor !== nextStartCursor || prevEndCursor !== nextEndCursor) {
|
133 | this.trigger("change", {
|
134 | prevStartCursor,
|
135 | prevEndCursor,
|
136 | nextStartCursor,
|
137 | nextEndCursor,
|
138 | });
|
139 | return;
|
140 | } else if (this._requestVirtualItems()) {
|
141 | return;
|
142 | } else if ((!isDirectionEnd || !isEnd) && isStart) {
|
143 | this.trigger("requestPrepend", {
|
144 | key: items[prevStartCursor].key,
|
145 | isVirtual: false,
|
146 | });
|
147 | } else if ((isDirectionEnd || !isStart) && isEnd) {
|
148 | this.trigger("requestAppend", {
|
149 | key: items[prevEndCursor].key,
|
150 | isVirtual: false,
|
151 | });
|
152 | }
|
153 | }
|
154 |
|
155 | |
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 | public _requestVirtualItems() {
|
162 | const isDirectionEnd = this.options.defaultDirection === "end";
|
163 | const items = this.items;
|
164 | const totalVisibleItems = this.getVisibleItems();
|
165 | const visibleItems = totalVisibleItems.filter((item) => !item.isVirtual);
|
166 | const totalVisibleLength = totalVisibleItems.length;
|
167 | const visibleLength = visibleItems.length;
|
168 | const startCursor = this.getStartCursor();
|
169 | const endCursor = this.getEndCursor();
|
170 |
|
171 |
|
172 | if (visibleLength === totalVisibleLength) {
|
173 | return false;
|
174 | } else if (visibleLength) {
|
175 | const startKey = visibleItems[0].key;
|
176 | const endKey = visibleItems[visibleLength - 1].key;
|
177 | const startIndex = findIndex(items, (item) => item.key === startKey) - 1;
|
178 | const endIndex = findIndex(items, (item) => item.key === endKey) + 1;
|
179 |
|
180 | const isEnd = endIndex <= endCursor;
|
181 | const isStart = startIndex >= startCursor;
|
182 |
|
183 |
|
184 | if ((isDirectionEnd || !isStart) && isEnd) {
|
185 | this.trigger("requestAppend", {
|
186 | key: endKey,
|
187 | nextKey: items[endIndex].key,
|
188 | isVirtual: true,
|
189 | });
|
190 | return true;
|
191 | } else if ((!isDirectionEnd || !isEnd) && isStart) {
|
192 | this.trigger("requestPrepend", {
|
193 | key: startKey,
|
194 | nextKey: items[startIndex].key,
|
195 | isVirtual: true,
|
196 | });
|
197 | return true;
|
198 | }
|
199 | } else if (totalVisibleLength) {
|
200 | const lastItem = totalVisibleItems[totalVisibleLength - 1];
|
201 |
|
202 | if (isDirectionEnd) {
|
203 | this.trigger("requestAppend", {
|
204 | nextKey: totalVisibleItems[0].key,
|
205 | isVirtual: true,
|
206 | });
|
207 | } else {
|
208 | this.trigger("requestPrepend", {
|
209 | nextKey: lastItem.key,
|
210 | isVirtual: true,
|
211 | });
|
212 | }
|
213 | return true;
|
214 | }
|
215 | return false;
|
216 | }
|
217 | public setCursors(startCursor: number, endCursor: number) {
|
218 | this.startCursor = startCursor;
|
219 | this.endCursor = endCursor;
|
220 | }
|
221 | public setSize(size: number) {
|
222 | this.size = size;
|
223 | }
|
224 | public getStartCursor() {
|
225 | return this.startCursor;
|
226 | }
|
227 | public getEndCursor() {
|
228 | return this.endCursor;
|
229 | }
|
230 | public isLoading(direction: "start" | "end") {
|
231 | const startCursor = this.startCursor;
|
232 | const endCursor = this.endCursor;
|
233 | const items = this.items;
|
234 | const firstItem = items[startCursor]!;
|
235 | const lastItem = items[endCursor]!;
|
236 | const length = items.length;
|
237 |
|
238 | if (
|
239 | direction === DIRECTION.END
|
240 | && endCursor > -1
|
241 | && endCursor < length - 1
|
242 | && !lastItem.isVirtual
|
243 | && !isFlatOutline(lastItem.startOutline, lastItem.endOutline)
|
244 | ) {
|
245 | return false;
|
246 | }
|
247 | if (
|
248 | direction === DIRECTION.START
|
249 | && startCursor > 0
|
250 | && !firstItem.isVirtual
|
251 | && !isFlatOutline(firstItem.startOutline, firstItem.endOutline)
|
252 | ) {
|
253 | return false;
|
254 | }
|
255 | return true;
|
256 | }
|
257 | public setItems(nextItems: InfiniteItem[]) {
|
258 | this.items = nextItems;
|
259 |
|
260 | const itemKeys: Record<string | number, InfiniteItem> = {};
|
261 |
|
262 | nextItems.forEach((item) => {
|
263 | itemKeys[item.key] = item;
|
264 | });
|
265 | this.itemKeys = itemKeys;
|
266 | }
|
267 | public syncItems(nextItems: InfiniteItem[]) {
|
268 | const prevItems = this.items;
|
269 | const prevStartCursor = this.startCursor;
|
270 | const prevEndCursor = this.endCursor;
|
271 | const {
|
272 | startCursor: nextStartCursor,
|
273 | endCursor: nextEndCursor,
|
274 | } = getNextCursors(
|
275 | this.items.map((item) => item.key),
|
276 | nextItems.map((item) => item.key),
|
277 | prevStartCursor,
|
278 | prevEndCursor,
|
279 | );
|
280 |
|
281 | let isChange = nextEndCursor - nextStartCursor !== prevEndCursor - prevStartCursor
|
282 | || (prevStartCursor === -1 || nextStartCursor === -1);
|
283 |
|
284 | if (!isChange) {
|
285 | const prevVisibleItems = prevItems.slice(prevStartCursor, prevEndCursor + 1);
|
286 | const nextVisibleItems = nextItems.slice(nextStartCursor, nextEndCursor + 1);
|
287 | const visibleResult = diff(prevVisibleItems, nextVisibleItems, (item) => item.key);
|
288 |
|
289 | isChange = visibleResult.added.length > 0
|
290 | || visibleResult.removed.length > 0
|
291 | || visibleResult.changed.length > 0;
|
292 | }
|
293 | this.setItems(nextItems);
|
294 | this.setCursors(nextStartCursor, nextEndCursor);
|
295 | return isChange;
|
296 | }
|
297 | public getItems() {
|
298 | return this.items;
|
299 | }
|
300 | public getVisibleItems() {
|
301 | const startCursor = this.startCursor;
|
302 | const endCursor = this.endCursor;
|
303 |
|
304 | if (startCursor === -1) {
|
305 | return [];
|
306 | }
|
307 | return this.items.slice(startCursor, endCursor + 1);
|
308 | }
|
309 | public getItemByKey(key: string | number) {
|
310 | return this.itemKeys[key];
|
311 | }
|
312 | public getRenderedVisibleItems() {
|
313 | const items = this.getVisibleItems();
|
314 | const rendered = items.map(({ startOutline, endOutline }) => {
|
315 | const length = startOutline.length;
|
316 |
|
317 | if (length === 0 || length !== endOutline.length) {
|
318 | return false;
|
319 | }
|
320 | return startOutline.some((pos, i) => endOutline[i] !== pos);
|
321 | });
|
322 | const startIndex = rendered.indexOf(true);
|
323 | const endIndex = rendered.lastIndexOf(true);
|
324 |
|
325 | return endIndex === -1 ? [] : items.slice(startIndex, endIndex + 1);
|
326 | }
|
327 | public destroy() {
|
328 | this.off();
|
329 | this.startCursor = -1;
|
330 | this.endCursor = -1;
|
331 | this.items = [];
|
332 | this.size = 0;
|
333 | }
|
334 | }
|