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