Source: src/layouts/GridLayout.ts

import { ALIGN } from "../consts";
import { getStyleNames, assignOptions, fill, cloneItems } from "../utils";
import { ILayout, IAlign, IRectlProperties, IInfiniteGridItem, IInfiniteGridGroup } from "../types";

// ALIGN
const { START, CENTER, END, JUSTIFY } = ALIGN;

/**
 * @classdesc The GridLayout is a layout that stacks cards with the same width as a stack of bricks. Adjust the width of all images to the same size, find the lowest height column, and insert a new card.
 * @ko GridLayout는 벽돌을 쌓아 올린 모양처럼 동일한 너비를 가진 카드를 쌓는 레이아웃이다. 모든 이미지의 너비를 동일한 크기로 조정하고, 가장 높이가 낮은 열을 찾아 새로운 이미지를 삽입한다. 따라서 배치된 카드 사이에 빈 공간이 생기지는 않지만 배치된 레이아웃의 아래쪽은 울퉁불퉁해진다.
 * @class eg.InfiniteGrid.GridLayout
 * @param {Object} [options] The option object of eg.InfiniteGrid.GridLayout module <ko>eg.InfiniteGrid.GridLayout 모듈의 옵션 객체</ko>
 * @param {String} [options.margin=0] Margin used to create space around items <ko>아이템들 사이의 공간</ko>
 * @param {Boolean} [options.horizontal=false] Direction of the scroll movement (false: vertical, true: horizontal) <ko>스크롤 이동 방향 (false: 세로방향, true: 가로방향)</ko>
 * @param {Boolean} [options.align=START] Align of the position of the items (START, CENTER, END, JUSTIFY) <ko>아이템들의 위치의 정렬 (START, CENTER, END, JUSTIFY)</ko>
 * @param {Boolean} [options.itemSize=0] The size of the items. If it is 0, it is calculated as the size of the first item in items. <ko> 아이템의 사이즈. 만약 아이템 사이즈가 0이면, 아이템들의 첫번째 아이템의 사이즈로 계산이 된다. </ko>
 * @example
```
<script>
var ig = new eg.InfiniteGrid("#grid". {
  horizontal: true,
});

ig.setLayout(eg.InfiniteGrid.GridLayout, {
  margin: 10,
  align: "start",
  itemSize: 200
});

// or

var layout = new eg.InfiniteGrid.GridLayout({
  margin: 10,
  align: "center",
  itemSize: 200,
  horizontal: true,
});

</script>
```
 **/
class GridLayout implements ILayout {
	public options: {
		horizontal: boolean,
		margin: number,
		align: IAlign[keyof IAlign],
		itemSize: number,
	};
	private _size: number;
	private _columnSize: number;
	private _columnLength: number;
	private _style: IRectlProperties;
	constructor(options: Partial<GridLayout["options"]> = {}) {
		this.options = assignOptions({
			margin: 0,
			horizontal: false,
			align: START,
			itemSize: 0,
		}, options);
		this._size = 0;
		this._columnSize = 0;
		this._columnLength = 0;
		this._style = getStyleNames(this.options.horizontal);
	}
	/**
	 * Adds items at the bottom of a outline.
	 * @ko 아이템들을 아웃라인 아래에 추가한다.
	 * @method eg.InfiniteGrid.GridLayout#append
	 * @param {Array} items Array of items to be layouted <ko>레이아웃할 아이템들의 배열</ko>
	 * @param {Array} [outline=[]] Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
	 * @return {Object} Layouted items and outline of start and end <ko> 레이아웃이 된 아이템과 시작과 끝의 아웃라인이 담긴 정보</ko>
	 * @example
	 * layout.prepend(items, [100, 200, 300, 400]);
	 */
	public append(items: IInfiniteGridItem[], outline?: number[], cache?: boolean) {
		return this._insert(items, outline, true, cache);
	}
	/**
	 * Adds items at the top of a outline.
	 * @ko 아이템을 아웃라인 위에 추가한다.
	 * @method eg.InfiniteGrid.GridLayout#prepend
	 * @param {Array} items Array of items to be layouted <ko>레이아웃할 아이템들의 배열</ko>
	 * @param {Array} [outline=[]] Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
	 * @return {Object} Layouted items and outline of start and end <ko> 레이아웃이 된 아이템과 시작과 끝의 아웃라인이 담긴 정보</ko>
	 * @example
	 * layout.prepend(items, [100, 200, 300, 400]);
	 */
	public prepend(items: IInfiniteGridItem[], outline?: number[], cache?: boolean) {
		return this._insert(items, outline, false, cache);
	}
	/**
	 * Adds items of groups at the bottom of a outline.
	 * @ko 그룹들의 아이템들을 아웃라인 아래에 추가한다.
	 * @method eg.InfiniteGrid.GridLayout#layout
	 * @param {Array} groups Array of groups to be layouted <ko>레이아웃할 그룹들의 배열</ko>
	 * @param {Array} outline Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
	 * @return {eg.InfiniteGrid.GridLayout} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
	 * @example
	 * layout.layout(groups, [100, 200, 300, 400]);
	 */
	public layout(groups: IInfiniteGridGroup[] = [], outline: number[] = []) {
		const firstItem = (groups.length && groups[0].items.length && groups[0].items[0]) as IInfiniteGridItem;

		this.checkColumn(firstItem);

		// if outlines' length and columns' length are now same, re-caculate outlines.
		let startOutline: number[];

		if (outline.length !== this._columnLength) {
			const pos = outline.length === 0 ? 0 : Math.min(...outline);

			// re-layout items.
			startOutline = fill(new Array(this._columnLength), pos);
		} else {
			startOutline = outline.slice();
		}
		groups.forEach(group => {
			const items = group.items;
			const result = this._layout(items, startOutline, true);

			group.outlines = result;
			startOutline = result.end;
		});

		return this;
	}
	/**
	 * Set the viewport size of the layout.
	 * @ko 레이아웃의 가시 사이즈를 설정한다.
	 * @method eg.InfiniteGrid.GridLayout#setSize
	 * @param {Number} size The viewport size of container area where items are added to a layout <ko>레이아웃에 아이템을 추가하는 컨테이너 영역의 가시 사이즈</ko>
	 * @return {eg.InfiniteGrid.GridLayout} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
	 * @example
	 * layout.setSize(800);
	 */
	public setSize(size: number) {
		this._size = size;
		return this;
	}
	private checkColumn(item: IInfiniteGridItem) {
		const { itemSize, margin, horizontal } = this.options;
		const sizeName = horizontal ? "height" : "width";
		const columnSize = Math.floor(itemSize || (item && item.size![sizeName]) || 0) || 0;

		this._columnSize = columnSize;
		if (!columnSize) {
			this._columnLength = 1;
			return;
		}
		this._columnLength = Math.max(Math.floor((this._size + margin) / (columnSize + margin)), 1);
	}
	private _layout(items: IInfiniteGridItem[], outline: number[], isAppend?: boolean) {
		const length = items.length;
		const margin = this.options.margin;
		const align = this.options.align;
		const style = this._style;

		const size1Name = style.size1;
		const size2Name = style.size2;
		const pos1Name = style.startPos1;
		const pos2Name = style.startPos2;
		const columnSize = this._columnSize;
		const columnLength = this._columnLength;

		const size = this._size;
		const viewDist = (size - (columnSize + margin) * columnLength + margin);

		const pointCaculateName = isAppend ? "min" : "max";
		const indexCaculateName = isAppend ? "indexOf" : "lastIndexOf";
		const startOutline = outline.slice();
		const endOutline = outline.slice();

		for (let i = 0; i < length; ++i) {
			const point = Math[pointCaculateName](...endOutline) || 0;
			let index = endOutline[indexCaculateName](point);
			const item = items[isAppend ? i : length - 1 - i];
			const itemSize = item.size;

			if (!itemSize) {
				continue;
			}
			const size1 = itemSize[size1Name];
			const size2 = itemSize[size2Name];
			const pos1 = isAppend ? point : point - margin - size1;
			const endPos1 = pos1 + size1 + margin;

			if (index === -1) {
				index = 0;
			}
			let pos2 = (columnSize + margin) * index;

			// ALIGN
			if (align === CENTER) {
				pos2 += viewDist / 2;
			} else if (align === END) {
				pos2 += viewDist + columnSize - size2;
			} else if (align === JUSTIFY) {
				if (columnLength <= 1) {
					pos2 += viewDist / 2;
				} else {
					pos2 = (size - columnSize) / (columnLength - 1) * index;
				}
			}
			// tetris
			item.rect = {
				[pos1Name as "top"]: pos1,
				[pos2Name as "left"]: pos2,
			};
			item.column = index;
			endOutline[index] = isAppend ? endPos1 : pos1;
		}
		if (!isAppend) {
			items.sort((a, b) => {
				const item1pos1 = a.rect[pos1Name];
				const item1pos2 = a.rect[pos2Name];
				const item2pos1 = b.rect[pos1Name];
				const item2pos2 = b.rect[pos2Name];

				if (item1pos1 - item2pos1) {
					return item1pos1 - item2pos1;
				}
				return item1pos2 - item2pos2;
			});
		}
		// if append items, startOutline is low, endOutline is high
		// if prepend items, startOutline is high, endOutline is low
		return {
			start: isAppend ? startOutline : endOutline,
			end: isAppend ? endOutline : startOutline,
		};
	}
	private _insert(
		items: IInfiniteGridItem[] = [],
		outline: number[] = [],
		isAppend?: boolean,
		cache?: boolean,
	) {
		const clone = cache ? items : cloneItems(items);

		let startOutline = outline;

		if (!this._columnLength) {
			this.checkColumn(items[0]);
		}
		if (outline.length !== this._columnLength) {
			startOutline = fill(new Array(this._columnLength), outline.length ? (Math[isAppend ? "min" : "max"](...outline) || 0) : 0);
		}

		const result = this._layout(clone, startOutline, isAppend);

		return {
			items: clone,
			outlines: result,
		};
	}
}

export default GridLayout;
comments powered by Disqus