UNPKG

9.98 kBPlain TextView Raw
1import Component from "@egjs/component";
2import { diff } from "@egjs/list-differ";
3import { DIRECTION } from "./consts";
4import { findIndex, getNextCursors, isFlatOutline } from "./utils";
5
6export interface OnInfiniteRequestAppend {
7 key?: string | number | undefined;
8 nextKey?: string | number | undefined;
9 isVirtual: boolean;
10}
11
12export interface OnInfiniteRequestPrepend {
13 key?: string | number;
14 nextKey?: string | number;
15 isVirtual: boolean;
16}
17
18export interface OnInfiniteChange {
19 prevStartCursor: number;
20 prevEndCursor: number;
21 nextStartCursor: number;
22 nextEndCursor: number;
23}
24
25export interface InfiniteEvents {
26 requestAppend: OnInfiniteRequestAppend;
27 requestPrepend: OnInfiniteRequestPrepend;
28 change: OnInfiniteChange;
29}
30
31export interface InfiniteOptions {
32 useRecycle?: boolean;
33 threshold?: number;
34 defaultDirection?: "start" | "end";
35}
36
37export interface InfiniteItem {
38 key: string | number;
39 startOutline: number[];
40 endOutline: number[];
41 isVirtual?: boolean;
42}
43
44export 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 * @private
157 * Call the requestAppend or requestPrepend event to fill the virtual items.
158 * @ko virtual item을 채우기 위해 requestAppend 또는 requestPrepend 이벤트를 호출합니다.
159 * @return - Whether the event is called. <ko>이벤트를 호출했는지 여부.</ko>
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 // Fill the placeholder with the original item.
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 // sync items between cursors
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}