UNPKG

@egjs/infinitegrid

Version:

A module used to arrange elements including content infinitely according to grid type. With this module, you can implement various grids composed of different card elements whose sizes vary. It guarantees performance by maintaining the number of DOMs the

529 lines (455 loc) 13.7 kB
import { camelize, withClassMethods } from "@cfcs/core"; import Grid, { GRID_PROPERTY_TYPES } from "@egjs/grid"; import { diff } from "@egjs/list-differ"; import { GROUP_TYPE, IGNORE_PROPERITES_MAP, INFINITEGRID_METHODS, ITEM_INFO_PROPERTIES, ITEM_TYPE } from "./consts"; import { GroupManagerStatus, InfiniteGridGroupStatus } from "./GroupManager"; import InfiniteGrid from "./InfiniteGrid"; import { InfiniteGridItem, InfiniteGridItemStatus } from "./InfiniteGridItem"; import { CategorizedGroup, InfiniteGridGroup, InfiniteGridInsertedItems, InfiniteGridItemInfo, RenderingOptions, } from "./types"; export function isWindow(el: Window | Element): el is Window { return el === window; } export function isNumber(val: any): val is number { return typeof val === "number"; } export function isString(val: any): val is string { return typeof val === "string"; } export function isObject(val: any): val is object { return typeof val === "object"; } export function flat<T>(arr: T[][]): T[] { return arr.reduce((prev, cur) => { return [...prev, ...cur]; }, []); } export function splitOptions(options: Record<string, any>) { const { gridOptions, ...otherOptions } = options; return { ...splitGridOptions(gridOptions), ...otherOptions, }; } export function splitGridOptions(options: Record<string, any>) { const nextOptions: Record<string, any> = {}; const gridOptions: Record<string, any> = {}; const defaultOptions = Grid.defaultOptions; for (const name in options) { const value = options[name]; if (!(name in IGNORE_PROPERITES_MAP)) { gridOptions[name] = value; } if (name in defaultOptions) { nextOptions[name] = value; } } return { ...nextOptions, gridOptions, }; } export function categorize<Item extends InfiniteGridItemInfo = InfiniteGridItem>(items: Item[]) { const groups: Array<CategorizedGroup<Item>> = []; const groupKeys: Record<string | number, CategorizedGroup<Item>> = {}; const registeredGroupKeys: Record<string | number, boolean> = {}; items.filter((item) => item.groupKey != null).forEach(({ groupKey }) => { registeredGroupKeys[groupKey!] = true; }); let generatedGroupKey: number | string; let isContinuousGroupKey = false; items.forEach((item, i) => { if (item.groupKey != null) { isContinuousGroupKey = false; } else if (!item.inserted && items[i - 1]) { // In case of framework, inserted is false. // If groupKey is not set, the group key of the previous item is followed. item.groupKey = items[i - 1].groupKey!; isContinuousGroupKey = false; } else { if (!isContinuousGroupKey) { generatedGroupKey = makeKey(registeredGroupKeys); isContinuousGroupKey = true; registeredGroupKeys[generatedGroupKey] = true; } item.groupKey = generatedGroupKey; } const groupKey = item.groupKey; let group = groupKeys[groupKey]; if (!group) { group = { groupKey, items: [], }; groupKeys[groupKey] = group; groups.push(group); } group.items.push(item); }); return groups; } export function getNextCursors( prevKeys: Array<string | number>, nextKeys: Array<string | number>, prevStartCursor: number, prevEndCursor: number, ) { const result = diff(prevKeys, nextKeys, (key) => key); let nextStartCursor = -1; let nextEndCursor = -1; // sync cursors result.maintained.forEach(([prevIndex, nextIndex]) => { if (prevStartCursor <= prevIndex && prevIndex <= prevEndCursor) { if (nextStartCursor === -1) { nextStartCursor = nextIndex; nextEndCursor = nextIndex; } else { nextStartCursor = Math.min(nextStartCursor, nextIndex); nextEndCursor = Math.max(nextEndCursor, nextIndex); } } }); return { startCursor: nextStartCursor, endCursor: nextEndCursor, }; } export function splitVirtualGroups<Group extends { type: GROUP_TYPE, groupKey: string | number }>( groups: Group[], direction: "start" | "end", nextGroups: CategorizedGroup<InfiniteGridItemStatus>[], ) { let virtualGroups: Group[] = []; if (direction === "start") { const index = findIndex(groups, (group) => group.type === GROUP_TYPE.NORMAL); if (index === -1) { return []; } // Get the virtual group maintained in the group from the next group. const endMaintainedIndex = findIndex(groups, (group) => { return findIndex(nextGroups, (nextGroup) => nextGroup.groupKey === group.groupKey) >= 0; }); const endIndex = endMaintainedIndex >= 0 ? Math.min(index, endMaintainedIndex) : index; virtualGroups = groups.slice(0, endIndex); } else { const index = findLastIndex(groups, (group) => group.type === GROUP_TYPE.NORMAL); if (index === -1) { return []; } const startMaintainedIndex = findLastIndex(groups, (group) => { return findIndex(nextGroups, (nextGroup) => nextGroup.groupKey === group.groupKey) >= 0; }); const startIndex = startMaintainedIndex >= 0 ? Math.max(index, startMaintainedIndex) : index; virtualGroups = groups.slice(startIndex + 1); } return virtualGroups; } export function getFirstRenderingItems( nextItems: InfiniteGridItemStatus[], horizontal: boolean, ) { const groups = categorize(nextItems); if (!groups[0]) { return []; } return groups[0].items.map((item) => { return new InfiniteGridItem(horizontal, { ...item, }); }); } export function getRenderingItemsByStatus( groupManagerStatus: GroupManagerStatus, nextItems: InfiniteGridItemStatus[], usePlaceholder: boolean, horizontal: boolean, ) { const prevGroups = groupManagerStatus.groups; const groups = categorize(nextItems); const startVirtualGroups = splitVirtualGroups(prevGroups, "start", groups); const endVirtualGroups = splitVirtualGroups(prevGroups, "end", groups); const nextGroups = [ ...startVirtualGroups, ...groups, ...endVirtualGroups, ] as Array<InfiniteGridGroupStatus | CategorizedGroup<InfiniteGridItemStatus>>; const { startCursor, endCursor, } = getNextCursors( prevGroups.map((group) => group.groupKey), nextGroups.map((group) => group.groupKey), groupManagerStatus.cursors[0], groupManagerStatus.cursors[1], ); let nextVisibleItems = flat(nextGroups.slice(startCursor, endCursor + 1).map((group) => { return group.items.map((item) => { return new InfiniteGridItem(horizontal, { ...item }); }); })); if (!usePlaceholder) { nextVisibleItems = nextVisibleItems.filter((item) => { return item.type !== ITEM_TYPE.VIRTUAL; }); } return nextVisibleItems; } export function mountRenderingItems(items: InfiniteGridItemInfo[], options: RenderingOptions) { const { grid, usePlaceholder, useLoading, useFirstRender, status, } = options; if (!grid) { return; } if (usePlaceholder) { grid.setPlaceholder({}); } if (useLoading) { grid.setLoading({}); } if (status) { grid.setStatus(status, true); } grid.syncItems(items); if (useFirstRender && !status && grid.getGroups().length) { grid.setCursors(0, 0, true); } } export function getRenderingItems(items: InfiniteGridItemInfo[], options: RenderingOptions) { const { status, usePlaceholder, useLoading, horizontal, useFirstRender, grid, } = options; let visibleItems: InfiniteGridItem[] = []; if (grid) { grid.setPlaceholder(usePlaceholder ? {} : null); grid.setLoading(useLoading ? {} : null); grid.syncItems(items); visibleItems = grid.getRenderingItems(); } else if (status) { visibleItems = getRenderingItemsByStatus(status.groupManager, items, !!usePlaceholder, !!horizontal); } else if (useFirstRender) { visibleItems = getFirstRenderingItems(items, !!horizontal); } return visibleItems; } /* Class Decorator */ export function InfiniteGridGetterSetter(component: { prototype: InfiniteGrid<any>, propertyTypes: typeof GRID_PROPERTY_TYPES, infinitegridTypes: any, }) { const { prototype, propertyTypes, infinitegridTypes, } = component; for (const name in infinitegridTypes) { const attributes: Record<string, any> = { enumerable: true, configurable: true, get: function get(this: InfiniteGrid) { const options = this.options; return options[name]; }, set: function set(this: InfiniteGrid, value: any) { const setterName = `_${camelize(`set ${name}`)}`; if (this[setterName]) { this[setterName](value); } else { this.options[name] = value; } }, }; Object.defineProperty(prototype, name, attributes); } for (const name in propertyTypes) { const attributes: Record<string, any> = { enumerable: true, configurable: true, get(this: InfiniteGrid) { const options = this.groupManager.options; if (name in options) { return options[name]; } else { return options.gridOptions[name]; } }, set(this: InfiniteGrid, value: any) { const prevValue = this.groupManager[name]; if (prevValue === value) { return; } this.groupManager.gridOptions = { [name]: value, }; }, }; Object.defineProperty(prototype, name, attributes); } } export function makeKey( registeredKeys: Record<string, any>, prefix = "", ) { let index = 0; // eslint-disable-next-line no-constant-condition while (true) { const key = `infinitegrid_${prefix}${index++}`; if (!(key in registeredKeys)) { return key; } } } export function convertHTMLtoElement(html: string) { const dummy = document.createElement("div"); dummy.innerHTML = html; return toArray(dummy.children); } export function convertInsertedItems( items: InfiniteGridInsertedItems, groupKey?: string | number, ): InfiniteGridItemInfo[] { let insertedItems: Array<string | HTMLElement | InfiniteGridItemInfo>; if (isString(items)) { insertedItems = convertHTMLtoElement(items); } else { insertedItems = items; } return insertedItems.map((item) => { let element!: HTMLElement; let html = ""; let key!: string | number; if (isString(item)) { html = item; } else if ("parentNode" in item) { element = item; html = item.outerHTML; } else { // inserted is true when adding via a method. return { groupKey, inserted: true, ...item }; } // inserted is true when adding via a method. return { key, groupKey, html, element, inserted: true, }; }); } export function toArray(nodes: HTMLCollection): HTMLElement[]; export function toArray<T>(nodes: { length: number, [key: number]: T }): T[]; export function toArray<T>(nodes: { length: number, [key: number]: T }): T[] { const array: T[] = []; if (nodes) { const length = nodes.length; for (let i = 0; i < length; i++) { array.push(nodes[i]); } } return array; } export function find<T>(arr: T[], callback: (value: T, index: number) => boolean): T | null { const length = arr.length; for (let i = 0; i < length; ++i) { const value = arr[i]; if (callback(value, i)) { return value; } } return null; } export function findIndex<T>(arr: T[], callback: (value: T, index: number) => boolean) { const length = arr.length; for (let i = 0; i < length; ++i) { if (callback(arr[i], i)) { return i; } } return -1; } export function findLastIndex<T>(arr: T[], callback: (value: T, index: number) => boolean) { const length = arr.length; for (let i = length - 1; i >= 0; --i) { if (callback(arr[i], i)) { return i; } } return -1; } export function getItemInfo(info: InfiniteGridItemInfo) { const nextInfo: InfiniteGridItemInfo = {}; for (const name in info) { if (name in ITEM_INFO_PROPERTIES) { nextInfo[name] = info[name]; } } return nextInfo; } export function setPlaceholder(item: InfiniteGridItem, info: InfiniteGridItemStatus) { for (const name in info) { const value = info[name]; if (isObject(value)) { item[name] = { ...item[name], ...value, }; } else { item[name] = info[name]; } } } export function isFlatOutline(start: number[], end: number[]) { return start.length === end.length && start.every((pos, i) => end[i] === pos); } export function range(length: number): number[] { const arr: number[] = []; for (let i = 0; i < length; ++i) { arr.push(i); } return arr; } export function flatGroups(groups: InfiniteGridGroup[]) { return flat(groups.map(({ grid }) => grid.getItems() as InfiniteGridItem[])); } export function filterVirtuals<T extends InfiniteGridItem | InfiniteGridGroup>( items: T[], includePlaceholders?: boolean ): T[] { if (includePlaceholders) { return [...items]; } else { return items.filter((item) => item.type !== ITEM_TYPE.VIRTUAL); } } /** * Decorator that makes the method of InfiniteGrid available in the framework. * @ko 프레임워크에서 InfiniteGrid의 메소드를 사용할 수 있게 하는 데코레이터. * @private * @example * ```js * import { withInfiniteGridMethods } from "@egjs/infinitegrid"; * * class Grid extends React.Component<Partial<InfiniteGridProps & InfiniteGridOptions>> { * &#64;withInfiniteGridMethods * private grid: NativeGrid; * } * ``` */ export const withInfiniteGridMethods = withClassMethods(INFINITEGRID_METHODS);