UNPKG

11.9 kBPlain TextView Raw
1import Grid, { GRID_PROPERTY_TYPES, withMethods } from "@egjs/grid";
2import { diff } from "@egjs/list-differ";
3import { GROUP_TYPE, IGNORE_PROPERITES_MAP, INFINITEGRID_METHODS, ITEM_INFO_PROPERTIES, ITEM_TYPE } from "./consts";
4import { GroupManagerStatus, InfiniteGridGroupStatus } from "./GroupManager";
5import InfiniteGrid from "./InfiniteGrid";
6import { InfiniteGridItem, InfiniteGridItemStatus } from "./InfiniteGridItem";
7import {
8 CategorizedGroup, InfiniteGridGroup, InfiniteGridInsertedItems,
9 InfiniteGridItemInfo,
10 RenderingOptions,
11} from "./types";
12
13export function isWindow(el: Window | Element): el is Window {
14 return el === window;
15}
16
17export function isNumber(val: any): val is number {
18 return typeof val === "number";
19}
20
21export function isString(val: any): val is string {
22 return typeof val === "string";
23}
24export function isObject(val: any): val is object {
25 return typeof val === "object";
26}
27
28export function flat<T>(arr: T[][]): T[] {
29 return arr.reduce((prev, cur) => {
30 return [...prev, ...cur];
31 }, []);
32}
33export function splitOptions(options: Record<string, any>) {
34 const {
35 gridOptions,
36 ...otherOptions
37 } = options;
38
39 return {
40 ...splitGridOptions(gridOptions),
41 ...otherOptions,
42 };
43}
44export function splitGridOptions(options: Record<string, any>) {
45 const nextOptions: Record<string, any> = {};
46 const gridOptions: Record<string, any> = {};
47 const defaultOptions = Grid.defaultOptions;
48
49 for (const name in options) {
50 const value = options[name];
51
52 if (!(name in IGNORE_PROPERITES_MAP)) {
53 gridOptions[name] = value;
54 }
55
56 if (name in defaultOptions) {
57 nextOptions[name] = value;
58 }
59 }
60 return {
61 ...nextOptions,
62 gridOptions,
63 };
64}
65
66export function categorize<Item extends InfiniteGridItemInfo = InfiniteGridItem>(items: Item[]) {
67 const groups: Array<CategorizedGroup<Item>> = [];
68 const groupKeys: Record<string | number, CategorizedGroup<Item>> = {};
69 const registeredGroupKeys: Record<string | number, boolean> = {};
70
71 items.filter((item) => item.groupKey != null).forEach(({ groupKey }) => {
72 registeredGroupKeys[groupKey!] = true;
73 });
74
75 let generatedGroupKey: number | string;
76 let isContinuousGroupKey = false;
77
78 items.forEach((item) => {
79 if (item.groupKey != null) {
80 isContinuousGroupKey = false;
81 } else {
82 if (!isContinuousGroupKey) {
83 generatedGroupKey = makeKey(registeredGroupKeys);
84 isContinuousGroupKey = true;
85 registeredGroupKeys[generatedGroupKey] = true;
86 }
87 item.groupKey = generatedGroupKey;
88 }
89
90 const groupKey = item.groupKey;
91 let group = groupKeys[groupKey];
92
93 if (!group) {
94 group = {
95 groupKey,
96 items: [],
97 };
98 groupKeys[groupKey] = group;
99 groups.push(group);
100 }
101
102 group.items.push(item);
103 });
104 return groups;
105}
106
107export function getNextCursors(
108 prevKeys: Array<string | number>,
109 nextKeys: Array<string | number>,
110 prevStartCursor: number,
111 prevEndCursor: number,
112) {
113 const result = diff(prevKeys, nextKeys, (key) => key);
114 let nextStartCursor = -1;
115 let nextEndCursor = -1;
116
117 // sync cursors
118 result.maintained.forEach(([prevIndex, nextIndex]) => {
119 if (prevStartCursor <= prevIndex && prevIndex <= prevEndCursor) {
120 if (nextStartCursor === -1) {
121 nextStartCursor = nextIndex;
122 nextEndCursor = nextIndex;
123 } else {
124 nextStartCursor = Math.min(nextStartCursor, nextIndex);
125 nextEndCursor = Math.max(nextEndCursor, nextIndex);
126 }
127 }
128 });
129 return {
130 startCursor: nextStartCursor,
131 endCursor: nextEndCursor,
132 };
133}
134export function splitVirtualGroups<Group extends { type: GROUP_TYPE, groupKey: string | number }>(
135 groups: Group[],
136 direction: "start" | "end",
137 nextGroups: CategorizedGroup<InfiniteGridItemStatus>[],
138) {
139 let virtualGroups: Group[] = [];
140
141 if (direction === "start") {
142 const index = findIndex(groups, (group) => group.type === GROUP_TYPE.NORMAL);
143
144 if (index === -1) {
145 return [];
146 }
147 virtualGroups = groups.slice(0, index);
148 } else {
149 const index = findLastIndex(groups, (group) => group.type === GROUP_TYPE.NORMAL);
150
151 if (index === -1) {
152 return [];
153 }
154 virtualGroups = groups.slice(index + 1);
155 }
156
157 const nextVirtualGroups = diff<{ groupKey: string | number }>(
158 virtualGroups, nextGroups, ({ groupKey }) => groupKey,
159 ).removed.map((index) => {
160 return virtualGroups[index];
161 }).reverse();
162
163 return nextVirtualGroups;
164}
165
166export function getFirstRenderingItems(
167 nextItems: InfiniteGridItemStatus[],
168 horizontal: boolean,
169) {
170 const groups = categorize(nextItems);
171
172 if (!groups[0]) {
173 return [];
174 }
175 return groups[0].items.map((item) => {
176 return new InfiniteGridItem(horizontal, {
177 ...item,
178 });
179 });
180}
181export function getRenderingItemsByStatus(
182 groupManagerStatus: GroupManagerStatus,
183 nextItems: InfiniteGridItemStatus[],
184 usePlaceholder: boolean,
185 horizontal: boolean,
186) {
187 const prevGroups = groupManagerStatus.groups;
188 const groups = categorize(nextItems);
189
190 const startVirtualGroups = splitVirtualGroups(prevGroups, "start", groups);
191 const endVirtualGroups = splitVirtualGroups(prevGroups, "end", groups);
192 const nextGroups = [
193 ...startVirtualGroups,
194 ...groups,
195 ...endVirtualGroups,
196 ] as Array<InfiniteGridGroupStatus | CategorizedGroup<InfiniteGridItemStatus>>;
197 const {
198 startCursor,
199 endCursor,
200 } = getNextCursors(
201 prevGroups.map((group) => group.groupKey),
202 nextGroups.map((group) => group.groupKey),
203 groupManagerStatus.cursors[0],
204 groupManagerStatus.cursors[1],
205 );
206
207 let nextVisibleItems = flat(nextGroups.slice(startCursor, endCursor + 1).map((group) => {
208 return group.items.map((item) => {
209 return new InfiniteGridItem(horizontal, { ...item });
210 });
211 }));
212
213 if (!usePlaceholder) {
214 nextVisibleItems = nextVisibleItems.filter((item) => {
215 return item.type !== ITEM_TYPE.VIRTUAL;
216 });
217 }
218
219 return nextVisibleItems;
220}
221
222export function mountRenderingItems(items: InfiniteGridItemInfo[], options: RenderingOptions) {
223 const {
224 grid,
225 usePlaceholder,
226 useLoading,
227 useFirstRender,
228 status,
229 } = options;
230 if (!grid) {
231 return;
232 }
233 if (usePlaceholder) {
234 grid.setPlaceholder({});
235 }
236 if (useLoading) {
237 grid.setLoading({});
238 }
239 if (status) {
240 grid.setStatus(status, true);
241 }
242
243 grid.syncItems(items);
244
245 if (useFirstRender && !status && grid.getGroups().length) {
246 grid.setCursors(0, 0, true);
247 }
248}
249export function getRenderingItems(items: InfiniteGridItemInfo[], options: RenderingOptions) {
250 const {
251 status,
252 usePlaceholder,
253 useLoading,
254 horizontal,
255 useFirstRender,
256 grid,
257 } = options;
258 let visibleItems: InfiniteGridItem[] = [];
259
260 if (grid) {
261 grid.setPlaceholder(usePlaceholder ? {} : null);
262 grid.setLoading(useLoading ? {} : null);
263 grid.syncItems(items);
264
265 visibleItems = grid.getRenderingItems();
266 } else if (status) {
267 visibleItems = getRenderingItemsByStatus(status.groupManager, items, !!usePlaceholder, !!horizontal);
268 } else if (useFirstRender) {
269 visibleItems = getFirstRenderingItems(items, !!horizontal);
270 }
271
272 return visibleItems;
273}
274
275/* Class Decorator */
276export function InfiniteGridGetterSetter(component: {
277 prototype: InfiniteGrid<any>,
278 propertyTypes: typeof GRID_PROPERTY_TYPES,
279}) {
280 const {
281 prototype,
282 propertyTypes,
283 } = component;
284 for (const name in propertyTypes) {
285 const attributes: Record<string, any> = {
286 enumerable: true,
287 configurable: true,
288 get(this: InfiniteGrid) {
289 const options = this.groupManager.options;
290 if (name in options) {
291 return options[name];
292 } else {
293 return options.gridOptions[name];
294 }
295 },
296 set(this: InfiniteGrid, value: any) {
297 const prevValue = this.groupManager[name];
298
299 if (prevValue === value) {
300 return;
301 }
302 this.groupManager.gridOptions = {
303 [name]: value,
304 };
305 },
306 };
307 Object.defineProperty(prototype, name, attributes);
308 }
309}
310
311export function makeKey(registeredKeys: Record<string, any>) {
312 // eslint-disable-next-line no-constant-condition
313 while (true) {
314 const key = new Date().getTime() + Math.floor(Math.random() * 1000);
315
316 if (!(key in registeredKeys)) {
317 return key;
318 }
319 }
320}
321
322export function convertHTMLtoElement(html: string) {
323 const dummy = document.createElement("div");
324
325 dummy.innerHTML = html;
326 return toArray(dummy.children);
327}
328
329export function convertInsertedItems(
330 items: InfiniteGridInsertedItems,
331 groupKey?: string | number,
332): InfiniteGridItemInfo[] {
333 let insertedItems: Array<string | HTMLElement | InfiniteGridItemInfo>;
334
335 if (isString(items)) {
336 insertedItems = convertHTMLtoElement(items);
337 } else {
338 insertedItems = items;
339 }
340 return insertedItems.map((item) => {
341 let element!: HTMLElement;
342 let html = "";
343 let key!: string | number;
344
345 if (isString(item)) {
346 html = item;
347 } else if ("parentNode" in item) {
348 element = item;
349 html = item.outerHTML;
350 } else {
351 return { groupKey, ...item };
352 }
353
354 return {
355 key,
356 groupKey,
357 html,
358 element,
359 };
360 });
361}
362export function toArray(nodes: HTMLCollection): HTMLElement[];
363export function toArray<T>(nodes: { length: number, [key: number]: T }): T[];
364export function toArray<T>(nodes: { length: number, [key: number]: T }): T[] {
365 const array: T[] = [];
366
367 if (nodes) {
368 const length = nodes.length;
369
370 for (let i = 0; i < length; i++) {
371 array.push(nodes[i]);
372 }
373 }
374 return array;
375}
376
377
378export function findIndex<T>(arr: T[], callback: (value: T, index: number) => boolean) {
379 const length = arr.length;
380 for (let i = 0; i < length; ++i) {
381 if (callback(arr[i], i)) {
382 return i;
383 }
384 }
385
386 return -1;
387}
388
389export function findLastIndex<T>(arr: T[], callback: (value: T, index: number) => boolean) {
390 const length = arr.length;
391 for (let i = length - 1; i >= 0; --i) {
392 if (callback(arr[i], i)) {
393 return i;
394 }
395 }
396
397 return -1;
398}
399
400export function getItemInfo(info: InfiniteGridItemInfo) {
401 const nextInfo: InfiniteGridItemInfo = {};
402
403 for (const name in info) {
404 if (name in ITEM_INFO_PROPERTIES) {
405 nextInfo[name] = info[name];
406 }
407 }
408
409 return nextInfo;
410}
411
412export function setPlaceholder(item: InfiniteGridItem, info: InfiniteGridItemStatus) {
413 for (const name in info) {
414 const value = info[name];
415
416 if (isObject(value)) {
417 item[name] = {
418 ...item[name],
419 ...value,
420 };
421 } else {
422 item[name] = info[name];
423 }
424 }
425}
426
427export function isFlatOutline(start: number[], end: number[]) {
428 return start.length === end.length && start.every((pos, i) => end[i] === pos);
429}
430
431export function range(length: number): number[] {
432 const arr: number[] = [];
433 for (let i = 0; i < length; ++i) {
434 arr.push(i);
435 }
436 return arr;
437}
438
439export function flatGroups(groups: InfiniteGridGroup[]) {
440 return flat(groups.map(({ grid }) => grid.getItems() as InfiniteGridItem[]));
441}
442
443
444export function filterVirtuals<T extends InfiniteGridItem | InfiniteGridGroup>(
445 items: T[],
446 includePlaceholders?: boolean
447): T[] {
448 if (includePlaceholders) {
449 return items;
450 } else {
451 return items.filter((item) => item.type !== ITEM_TYPE.VIRTUAL);
452 }
453}
454
455/**
456 * Decorator that makes the method of InfiniteGrid available in the framework.
457 * @ko 프레임워크에서 InfiniteGrid의 메소드를 사용할 수 있게 하는 데코레이터.
458 * @private
459 * @example
460 * ```js
461 * import { withInfiniteGridMethods } from "@egjs/infinitegrid";
462 *
463 * class Grid extends React.Component<Partial<InfiniteGridProps & InfiniteGridOptions>> {
464 * &#64;withInfiniteGridMethods
465 * private grid: NativeGrid;
466 * }
467 * ```
468 */
469export const withInfiniteGridMethods = withMethods(INFINITEGRID_METHODS);
470