1 | import Grid, { GRID_PROPERTY_TYPES, withMethods } from "@egjs/grid";
|
2 | import { diff } from "@egjs/list-differ";
|
3 | import { GROUP_TYPE, IGNORE_PROPERITES_MAP, INFINITEGRID_METHODS, ITEM_INFO_PROPERTIES, ITEM_TYPE } from "./consts";
|
4 | import { GroupManagerStatus, InfiniteGridGroupStatus } from "./GroupManager";
|
5 | import InfiniteGrid from "./InfiniteGrid";
|
6 | import { InfiniteGridItem, InfiniteGridItemStatus } from "./InfiniteGridItem";
|
7 | import {
|
8 | CategorizedGroup, InfiniteGridGroup, InfiniteGridInsertedItems,
|
9 | InfiniteGridItemInfo,
|
10 | RenderingOptions,
|
11 | } from "./types";
|
12 |
|
13 | export function isWindow(el: Window | Element): el is Window {
|
14 | return el === window;
|
15 | }
|
16 |
|
17 | export function isNumber(val: any): val is number {
|
18 | return typeof val === "number";
|
19 | }
|
20 |
|
21 | export function isString(val: any): val is string {
|
22 | return typeof val === "string";
|
23 | }
|
24 | export function isObject(val: any): val is object {
|
25 | return typeof val === "object";
|
26 | }
|
27 |
|
28 | export function flat<T>(arr: T[][]): T[] {
|
29 | return arr.reduce((prev, cur) => {
|
30 | return [...prev, ...cur];
|
31 | }, []);
|
32 | }
|
33 | export function splitOptions(options: Record<string, any>) {
|
34 | const {
|
35 | gridOptions,
|
36 | ...otherOptions
|
37 | } = options;
|
38 |
|
39 | return {
|
40 | ...splitGridOptions(gridOptions),
|
41 | ...otherOptions,
|
42 | };
|
43 | }
|
44 | export 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 |
|
66 | export 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 |
|
107 | export 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 |
|
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 | }
|
134 | export 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 |
|
166 | export 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 | }
|
181 | export 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 |
|
222 | export 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 | }
|
249 | export 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 |
|
276 | export 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 |
|
311 | export function makeKey(registeredKeys: Record<string, any>) {
|
312 |
|
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 |
|
322 | export function convertHTMLtoElement(html: string) {
|
323 | const dummy = document.createElement("div");
|
324 |
|
325 | dummy.innerHTML = html;
|
326 | return toArray(dummy.children);
|
327 | }
|
328 |
|
329 | export 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 | }
|
362 | export function toArray(nodes: HTMLCollection): HTMLElement[];
|
363 | export function toArray<T>(nodes: { length: number, [key: number]: T }): T[];
|
364 | export 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 |
|
378 | export 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 |
|
389 | export 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 |
|
400 | export 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 |
|
412 | export 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 |
|
427 | export function isFlatOutline(start: number[], end: number[]) {
|
428 | return start.length === end.length && start.every((pos, i) => end[i] === pos);
|
429 | }
|
430 |
|
431 | export 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 |
|
439 | export function flatGroups(groups: InfiniteGridGroup[]) {
|
440 | return flat(groups.map(({ grid }) => grid.getItems() as InfiniteGridItem[]));
|
441 | }
|
442 |
|
443 |
|
444 | export 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 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 |
|
462 |
|
463 |
|
464 |
|
465 |
|
466 |
|
467 |
|
468 |
|
469 | export const withInfiniteGridMethods = withMethods(INFINITEGRID_METHODS);
|
470 |
|