Source: src/grids/JustifiedGrid.ts

/**
 * egjs-grid
 * Copyright (c) 2021-present NAVER Corp.
 * MIT license
 */
import Grid from "../Grid";
import { MOUNT_STATE, PROPERTY_TYPE } from "../consts";
import { GridOptions, GridOutlines, Properties } from "../types";
import { between, getRangeCost, GetterSetter, isNumber, isObject, sum, throttle } from "../utils";
import { find_path } from "./lib/dijkstra";
import { GridItem } from "../GridItem";


interface Link {
  path: number[];
  cost: number;
  length: number;
  currentNode: number;
  isOver?: boolean;
}

function splitItems(items: GridItem[], path: string[]) {
  const length = path.length;
  const groups: GridItem[][] = [];

  for (let i = 0; i < length - 1; ++i) {
    const path1 = parseInt(path[i], 10);
    const path2 = parseInt(path[i + 1], 10);

    groups.push(items.slice(path1, path2));
  }
  return groups;
}

function parseStretchSize(inlineSize: number, size: number | string) {
  if (isNumber(size)) {
    return size;
  }
  const signText = size.charAt(0);
  const sign = signText === "+" ? 1 : (signText === "-" ? -1 : 0);
  let nextSize = parseFloat(size);

  if (size.match(/%$/g)) {
    nextSize *= inlineSize / 100;
  }
  if (sign) {
    return inlineSize + nextSize;
  }
  return nextSize;
}

function getExpectedItemInlineSize(item: GridItem, rowSize: number) {
  const inlineSize = item.orgInlineSize;
  const contentSize = item.orgContentSize;
  const inlineOffset = item.gridData.inlineOffset || 0;
  const contentOffset = item.gridData.contentOffset || 0;

  if (!inlineSize || !contentSize) {
    return rowSize;
  }

  const ratio = contentSize <= contentOffset ? 1 : (inlineSize - inlineOffset) / (contentSize - contentOffset);

  return ratio * (rowSize - contentOffset) + inlineOffset;
}

/**
 * @typedef
 * @memberof Grid.JustifiedGrid
 * @extends Grid.GridOptions
 */
export interface JustifiedGridOptions extends GridOptions {
  /**
   * The minimum and maximum number of items per line.
   * <ko> 한 줄에 들어가는 아이템의 최소, 최대 개수.</ko>
   * @default [1, 8]
   */
  columnRange?: number | number[];
  /**
   * The minimum and maximum number of rows in a group, 0 is not set.
   * <ko> 한 그룹에 들어가는 행의 최소, 최대 개수, 0은 미설정이다.</ko>
   * @default 0
   */
  rowRange?: number | number[];
  /**
   * The minimum and maximum size by which the item is adjusted. If it is not calculated, it may deviate from the minimum and maximum sizes.
   * <ko>아이템이 조정되는 최소, 최대 사이즈. 계산이 되지 않는 경우 최소, 최대 사이즈를 벗어날 수 있다.</ko>
   * @default [0, Infinity]
   */
  sizeRange?: number | number[];
  /**
   * Maximum number of rows to be counted for container size. You can hide it on the screen by setting overflow: hidden. -1 is not set.
   * <ko>컨테이너 크기에 계산될 최대 row 개수. overflow: hidden을 설정하면 화면에 가릴 수 있다. -1은 미설정이다.</ko>
   * @default -1
   */
  displayedRow?: number;
  /**
   * Whether to crop when the row size is out of sizeRange. If set to true, this ratio can be broken.
   * <ko>row사이즈가 sizeRange에 벗어나면 크롭할지 여부. true로 설정하면 비율이 깨질 수 있다.</ko>
   * @default false
   */
  isCroppedSize?: boolean;
  /**
   * The ratio is maintained except for the offset value in the inline direction. If 'data-grid-inline-offset' is set in the element of each item, it will be applied first.
   * <ko>inline 방향의 offset 수치 만큼 제외하고 비율을 유지한다. 각 아이템의 element에 'data-grid-inline-offset' 을 설정하면 우선 적용한다.</ko>
   * @default 0
   */
  inlineOffset?: number;
  /**
   * The ratio is maintained except for the offset value in the content direction. If 'data-grid-content-offset' is set in the element or JSX of each item, it will be applied first.
   * <ko>content 방향의 offset 수치 만큼 제외하고 비율을 유지한다. 각 아이템의 Element 또는 JSX에 'data-grid-content-offset' 을 설정하면 우선 적용한다.</ko>
   * @default 0
   */
  contentOffset?: number;
  /**
   * it is possible to basically break the proportion of the item and stretch the inline size to fill the container.
   * If you set the `sizeRange` range narrowly, you can stretch well.
   * <ko>기본적으로 아이템의 비율을 깨서 inline size를 stretch하여 container를 꽉 채우게 가능하다. sizeRange의 범위를 좁게 설정하면 stretch가 잘 될 수 있다. </ko>
   * @default false
   */
  stretch?: boolean;
  /**
   * If `-`, `+`, or `%` are added as a string value, it is a relative value to the original size. If it is a number value, the stretch range can be set as an absolute value.
   * If `data-grid-min-stretch` and `data-grid-max-stretch` are set in the Element or JSX of each item, they will be applied first.
   * <ko>string 값으로 `-`, `+`, `%`이 붙으면 원본 크기에 대한 상대값이며 number 값으로 들어오면 절대 값으로 stretch 범위를 설정할 수 있습니다.
   * 각 아이템의 Element 또는 JSX에 `data-grid-min-stretch`, `data-grid-max-stretch`을 설정하면 우선 적용한다.</ko>
   * @
   * @default ["-10%", "+10%"]
   */
  stretchRange?: Array<string | number>;
  /**
   * Items placed in the last row are not stretched and are drawn maintaining their proportions. When using InfiniteGrid, it is calculated and re-rendered as follows:
   * <ko>마지막 row에 배치되는 아이템들 경우 stretch되지 않고 비율유지한채로 그려진다. InfiniteGrid를 사용하는 경우 다음 그룹과 같이 계산되어 재렌더링한다.</ko>
   */
  passUnstretchRow?: boolean;
}

/**
 * 'justified' is a printing term with the meaning that 'it fits in one row wide'. JustifiedGrid is a grid that the item is filled up on the basis of a line given a size.
 * If 'data-grid-inline-offset' or 'data-grid-content-offset' are set for item element, the ratio is maintained except for the offset value.
 * If 'data-grid-maintained-target' is set for an element whose ratio is to be maintained, the item is rendered while maintaining the ratio of the element.
 * @ko 'justified'는 '1행의 너비에 맞게 꼭 들어찬'이라는 의미를 가진 인쇄 용어다. JustifiedGrid는 용어의 의미대로 너비가 주어진 사이즈를 기준으로 아이템가 가득 차도록 배치하는 Grid다.
 * 아이템 엘리먼트에 'data-grid-inline-offset' 또는 'data-grid-content-offset'를 설정하면 offset 값을 제외하고 비율을 유지한다.
 * 비율을 유지하고 싶은 엘리먼트에 'data-grid-maintained-target'을 설정한다면 해당 엘리먼트의 비율을 유지하면서 아이템이 렌더링이 된다.
 * @memberof Grid
 * @param {HTMLElement | string} container - A base element for a module <ko>모듈을 적용할 기준 엘리먼트</ko>
 * @param {Grid.JustifiedGrid.JustifiedGridOptions} options - The option object of the JustifiedGrid module <ko>JustifiedGrid 모듈의 옵션 객체</ko>
 */
@GetterSetter
export class JustifiedGrid extends Grid<JustifiedGridOptions> {
  public static propertyTypes = {
    ...Grid.propertyTypes,
    columnRange: PROPERTY_TYPE.RENDER_PROPERTY,
    rowRange: PROPERTY_TYPE.RENDER_PROPERTY,
    sizeRange: PROPERTY_TYPE.RENDER_PROPERTY,
    isCroppedSize: PROPERTY_TYPE.RENDER_PROPERTY,
    displayedRow: PROPERTY_TYPE.RENDER_PROPERTY,
    stretch: PROPERTY_TYPE.RENDER_PROPERTY,
    stretchRange: PROPERTY_TYPE.RENDER_PROPERTY,
    passUnstretchRow: PROPERTY_TYPE.RENDER_PROPERTY,
    inlineMargin: PROPERTY_TYPE.RENDER_PROPERTY,
    contentMargin: PROPERTY_TYPE.RENDER_PROPERTY,
    inlineOffset: PROPERTY_TYPE.RENDER_PROPERTY,
    contentOffset: PROPERTY_TYPE.RENDER_PROPERTY,
  };
  public static defaultOptions: Required<JustifiedGridOptions> = {
    ...Grid.defaultOptions,
    columnRange: [1, 8],
    rowRange: 0,
    sizeRange: [0, Infinity],
    displayedRow: -1,
    isCroppedSize: false,
    stretch: false,
    passUnstretchRow: true,
    stretchRange: ["-20%", "+20%"],
    inlineOffset: 0,
    contentOffset: 0,
  };
  public applyGrid(items: GridItem[], direction: "start" | "end", outline: number[]): GridOutlines {
    const {
      attributePrefix,
      horizontal,
    } = this.options;

    items.forEach((item) => {
      if (!item.isUpdating) {
        return;
      }
      const element = item.element;
      const attributes = item.attributes;
      const gridData = item.gridData;
      let inlineOffset = parseFloat(attributes.inlineOffset);
      let contentOffset = parseFloat(attributes.contentOffset);
      // let contentMargin = parseFloat(attributes.contentMargin);

      if (isNaN(inlineOffset)) {
        inlineOffset = this.inlineOffset || gridData.inlineOffset || 0;
      }
      if (isNaN(contentOffset)) {
        contentOffset = this.contentOffset || gridData.contentOffset | 0;
      }
      // if (isNaN(contentMargin)) {
      //   contentMargin = this.contentMargin || gridData.contentMargin | 0;
      // }

      if (
        element && !("inlineOffset" in attributes) && !("contentOffset" in attributes)
        && item.mountState === MOUNT_STATE.MOUNTED
      ) {
        const maintainedTarget = element.querySelector<HTMLImageElement>(`[${attributePrefix}maintained-target]`);

        if (maintainedTarget) {
          const widthOffset = element.offsetWidth - element.clientWidth
            + element.scrollWidth - maintainedTarget.clientWidth;
          const heightOffset = element.offsetHeight - element.clientHeight
            + element.scrollHeight - maintainedTarget.clientHeight;

          if (horizontal) {
            inlineOffset = heightOffset;
            contentOffset = widthOffset;
          } else {
            inlineOffset = widthOffset;
            contentOffset = heightOffset;
          }
        }
      }
      gridData.inlineOffset = inlineOffset;
      gridData.contentOffset = contentOffset;
      // gridData.contentMargin = contentMargin;
    });
    const rowRange = this.options.rowRange;
    let path: string[] = [];

    const isEndDirection = direction === "end";

    if (items.length) {
      path = rowRange ? this._getRowPath(items, isEndDirection) : this._getPath(items, isEndDirection);
    }

    return this._setStyle(items, path, outline, direction === "end");
  }
  private _getRowPath(items: GridItem[], isEndDirection: boolean) {
    const columnRange = this._getColumnRange();
    const rowRange = this._getRowRange();

    const pathLink = this._getRowLink(items, {
      path: [0],
      cost: 0,
      length: 0,
      currentNode: 0,
    }, columnRange, rowRange, isEndDirection);

    return pathLink?.path.map((node) => `${node}`) ?? [];
  }
  private _getRowLink(
    items: GridItem[],
    currentLink: Link,
    columnRange: number[],
    rowRange: number[],
    isEndDirection: boolean,
  ): Link {
    const [minColumn] = columnRange;
    const [minRow, maxRow] = rowRange;
    const lastNode = items.length;
    const {
      path,
      length: pathLength,
      cost,
      currentNode,
    } = currentLink;

    // not reached lastNode but path is exceed or the number of remaining nodes is less than minColumn.
    if (currentNode < lastNode && (maxRow <= pathLength || currentNode + minColumn > lastNode)) {
      const rangeCost = getRangeCost(lastNode - currentNode, columnRange);
      const lastCost = rangeCost * Math.abs(this._getCost(items, currentNode, lastNode, isEndDirection));

      return {
        ...currentLink,
        length: pathLength + 1,
        path: [...path, lastNode],
        currentNode: lastNode,
        cost: cost + lastCost,
        isOver: true,
      };
    } else if (currentNode >= lastNode) {
      return {
        ...currentLink,
        currentNode: lastNode,
        isOver: minRow > pathLength || maxRow < pathLength,
      };
    } else {
      return this._searchRowLink(items, currentLink, lastNode, columnRange, rowRange, isEndDirection);
    }

  }
  private _searchRowLink(
    items: GridItem[],
    currentLink: Link,
    lastNode: number,
    columnRange: number[],
    rowRange: number[],
    isEndDirection: boolean,
  ) {
    const [minColumn, maxColumn] = columnRange;
    const {
      currentNode,
      path,
      length: pathLength,
      cost,
    } = currentLink;
    const length = Math.min(lastNode, currentNode + maxColumn);
    const links: Link[] = [];

    for (let nextNode = currentNode + minColumn; nextNode <= length; ++nextNode) {
      if (nextNode === currentNode) {
        continue;
      }
      const nextCost = Math.abs(this._getCost(items, currentNode, nextNode, isEndDirection));
      const nextLink = this._getRowLink(items, {
        path: [...path, nextNode],
        length: pathLength + 1,
        cost: cost + nextCost,
        currentNode: nextNode,
      }, columnRange, rowRange, isEndDirection);

      if (nextLink) {
        links.push(nextLink);
      }
    }
    links.sort((a, b) => {
      const aIsOver = a.isOver;
      const bIsOver = b.isOver;

      if (aIsOver !== bIsOver) {
        // If it is over, the cost is high.
        return aIsOver ? 1 : -1;
      }
      const aRangeCost = getRangeCost(a.length, rowRange);
      const bRangeCost = getRangeCost(b.length, rowRange);

      return aRangeCost - bRangeCost || a.cost - b.cost;
    });

    // It returns the lowest cost link.
    return links[0];
  }
  private _getExpectedRowSize(items: GridItem[], forceStretch?: boolean) {
    const containerInlineSize = this.getContainerInlineSize()! - this.getInlineGap() * (items.length - 1);
    let fixedContainerInsize = containerInlineSize;
    let ratioSum = 0;
    let inlineSum = 0;

    items.forEach((item) => {
      const inlineSize = item.orgInlineSize;
      const contentSize = item.orgContentSize;

      if (!inlineSize || !contentSize) {
        ratioSum += 1;
        return;
      }
      // sum((expect - offset) * ratio) = container inline size
      const inlineOffset = item.gridData.inlineOffset || 0;
      const contentOffset = item.gridData.contentOffset || 0;
      // const contentMargin = item.gridData.contentMargin || 0;

      const maintainedRatio = contentSize <= contentOffset ? 1
        : (inlineSize - inlineOffset) / (contentSize - contentOffset);

      ratioSum += maintainedRatio;
      // inlineSum += (contentOffset + contentMargin) * maintainedRatio;
      inlineSum += contentOffset * maintainedRatio;
      fixedContainerInsize -= inlineOffset;
    });

    if (ratioSum) {
      const nextRowSize = (fixedContainerInsize + inlineSum) / ratioSum;

      if (this.stretch) {
        const [minRowSize, maxRowSize] = this._getSizeRange();
        const stretchRowSize = between(nextRowSize, minRowSize, maxRowSize);

        if (forceStretch) {
          return stretchRowSize;
        }
        const stretchRange = this.stretchRange;
        const inlineSizes = items.map((item) => {
          return getExpectedItemInlineSize(item, stretchRowSize);
        });
        const minInlineSize = inlineSizes.reduce((prev, itemInlineSize, i) => {
          return prev + parseStretchSize(itemInlineSize, items[i].attributes.minStretch || stretchRange[0]);
        }, 0);
        const maxInlineSize = inlineSizes.reduce((prev, itemInlineSize, i) => {
          return prev + parseStretchSize(itemInlineSize, items[i].attributes.maxStretch || stretchRange[1]);
        }, 0);

        // for stretch
        if (minInlineSize <= containerInlineSize && containerInlineSize <= maxInlineSize) {
          return stretchRowSize;
        }
      }

      return nextRowSize;
    }
    return 0;
  }

  private _getExpectedInlineSizes(items: GridItem[], rowSize: number) {
    const {
      stretch,
      stretchRange,
    } = this.options;
    return items.map((item) => {
      const minInlineSize = stretch
        ? parseStretchSize(item.orgInlineSize, item.attributes.minStretch || stretchRange[0])
        : -Infinity;
      const maxInlineSize = stretch
        ? parseStretchSize(item.orgInlineSize, item.attributes.maxStretch || stretchRange[1])
        : Infinity;

      const itemInlineSize = getExpectedItemInlineSize(item, rowSize);
      let isMax = false;
      let isMin = false;
      if (itemInlineSize >= maxInlineSize) {
        isMax = true;
      } else if (itemInlineSize <= minInlineSize) {
        isMin = true;
      }

      return {
        minSize: minInlineSize,
        maxSize: maxInlineSize,
        size: between(itemInlineSize, minInlineSize, maxInlineSize),
        originalSize: itemInlineSize,
        isMax,
        isMin,
      };
    });
  }
  private _getStretchItemInfos(items: GridItem[], rowSize: number) {
    const itemsLength = items.length;
    const containerInlineSize = this.getContainerInlineSize() - this.getInlineGap() * (Math.max(1, itemsLength) - 1);
    const itemInfos = this._getExpectedInlineSizes(items, rowSize);
    const firstItemsSize = sum(itemInfos.map((info) => info.size));
    const distSize = containerInlineSize - firstItemsSize;
    const firstScale = containerInlineSize / sum(itemInfos.map((info) => info.originalSize));
    const costInfos = itemInfos.map((info) => {
      return {
        ...info,
        passed: false,
        size: info.originalSize * firstScale,
      };
    });

    if (distSize === 0) {
      return {
        infos: costInfos,
        cost: 0,
      };
    }
    // increase
    const isIncrease = distSize > 0;
    const costInfosLength = costInfos.length;

    for (let i = 0; i < costInfosLength; ++i) {
      const passedItemsSize = sum(costInfos.map((info) => info.passed ? info.size : 0));
      const restItemsSize = sum(costInfos.map((info) => info.passed ? 0 : info.originalSize));
      let distScale = (containerInlineSize - passedItemsSize) / restItemsSize;
      // minimize or maximize
      costInfos.forEach((info) => {
        if (info.passed) {
          return;
        }

        if (isIncrease) {
          if (info.size > info.maxSize) {
            distScale = Math.min(distScale, info.maxSize / info.originalSize);
          }
        } else {
          if (info.size < info.minSize) {
            distScale = Math.max(distScale, info.minSize / info.originalSize);
          }
        }
      });

      costInfos.forEach((info) => {
        if (!info.passed) {
          info.size = between(info.originalSize * distScale, info.minSize, info.maxSize);

          if (
            (isIncrease && !throttle(info.size - info.maxSize, 0.001))
            || (!isIncrease && !throttle(info.size - info.minSize, 0.001))
          ) {
            info.passed = true;
          }
        }
      });

      if (costInfos.every((info) => info.passed)) {
        break;
      }
    }
    const lastDistScale = containerInlineSize / sum(costInfos.map((info) => info.size));

    // last
    if (throttle(lastDistScale - 1, 0.001)) {
      costInfos.forEach((info) => {
        info.size *= lastDistScale;
      });
    }


    return {
      infos: costInfos,
      cost: sum(costInfos.map((info) => {
        let costRatio = 1;

        if (info.size > info.maxSize || info.size < info.minSize) {
          costRatio = 2;
        }
        let originalSize = info.originalSize;

        if (isIncrease) {
          originalSize = Math.max(originalSize, info.minSize);
        } else {
          originalSize = Math.min(originalSize, info.maxSize);
        }
        return Math.abs(info.size - originalSize) * costRatio;
      })),
    };
  }
  private _getExpectedInlineSize(items: GridItem[], rowSize: number) {
    const inlineGap = this.getInlineGap();
    const itemInfos = this._getExpectedInlineSizes(items, rowSize);

    return itemInfos.length ? sum(itemInfos.map((info) => info.size)) + inlineGap * (items.length - 1) : 0;
  }
  private _getCost(
    items: GridItem[],
    i: number,
    j: number,
    isEndDirection: boolean,
  ) {
    const lineItems = items.slice(i, j);
    const containerInlineSize = this.getContainerInlineSize();
    let rowSize = this._getExpectedRowSize(lineItems);
    const [minSize, maxSize] = this._getSizeRange();

    if (this.isCroppedSize) {
      if (minSize <= rowSize && rowSize <= maxSize) {
        return 0;
      }
      const expectedInlineSize = this._getExpectedInlineSize(
        lineItems,
        rowSize < minSize ? minSize : maxSize,
      );

      return Math.pow(expectedInlineSize - containerInlineSize, 2);
    }
    let extraCost = 0;

    if (this.stretch) {
      if (rowSize < minSize) {
        rowSize = minSize;
      } else if (rowSize > maxSize) {
        rowSize = maxSize;
      }
      const sizeCost = Math.abs(rowSize - minSize);

      const expectedInlineSize = this._getExpectedInlineSize(
        lineItems,
        rowSize,
      );

      if (
        !this.passUnstretchRow
        || (isEndDirection ? j !== items.length : i !== 0)
        || expectedInlineSize >= containerInlineSize
      ) {
        const res = this._getStretchItemInfos(lineItems, rowSize);

        extraCost = res.cost;
      }

      return extraCost + sizeCost;
    }

    if (isFinite(maxSize)) {
      // if this size is not in range, the cost increases sharply.
      if (rowSize < minSize) {
        return Math.pow(rowSize - minSize, 2) + Math.pow(maxSize, 2) + extraCost;
      } else if (rowSize > maxSize) {
        return Math.pow(rowSize - maxSize, 2) + Math.pow(maxSize, 2) + extraCost;
      }
    } else if (rowSize < minSize) {
      return Math.max(Math.pow(minSize, 2), Math.pow(rowSize, 2)) + Math.pow(maxSize, 2) + extraCost;
    }
    // if this size in range, the cost is row
    return rowSize - minSize + extraCost;
  }
  private _getPath(items: GridItem[], isEndDirection: boolean) {
    const lastNode = items.length;
    const columnRangeOption = this.options.columnRange;
    const [minColumn, maxColumn]: number[] = isObject(columnRangeOption)
      ? columnRangeOption
      : [columnRangeOption, columnRangeOption];

    const graph = (nodeKey: string) => {
      const results: { [key: string]: number } = {};
      const currentNode = parseInt(nodeKey, 10);

      for (let nextNode = Math.min(currentNode + minColumn, lastNode); nextNode <= lastNode; ++nextNode) {
        if (nextNode - currentNode > maxColumn) {
          break;
        }
        let cost = this._getCost(
          items,
          currentNode,
          nextNode,
          isEndDirection,
        );

        if (cost < 0 && nextNode === lastNode) {
          cost = 0;
        }
        results[`${nextNode}`] = Math.pow(cost, 2);
      }
      return results;
    };
    // shortest path for items' total height.
    return find_path(graph, "0", `${lastNode}`);
  }
  private _setStyle(
    items: GridItem[],
    path: string[],
    outline: number[] = [],
    isEndDirection: boolean,
  ) {
    const {
      isCroppedSize,
      displayedRow,
      stretch,
      passUnstretchRow,
    } = this.options;
    const itemsLength = items.length;
    const sizeRange = this._getSizeRange();
    const startPoint = outline[0] || 0;
    const containerInlineSize = this.getContainerInlineSize();
    const inlineGap = this.getInlineGap();
    const contentGap = this.getContentGap();
    const groups = splitItems(items, path);
    let passedItems!: number[];
    const groupsLength = groups.length;
    let contentPos = startPoint;
    let displayedSize = 0;
    let passedPoint!: number[];

    groups.forEach((groupItems, rowIndex) => {
      const groupItemslength = groupItems.length;
      let rowSize = this._getExpectedRowSize(groupItems, true);

      if (isCroppedSize) {
        rowSize = Math.max(sizeRange[0], Math.min(rowSize, sizeRange[1]));
      }
      const allGap = inlineGap * (length - 1);
      const itemInfos = groupItems.map((item, index) => {
        const itemInlineSize = getExpectedItemInlineSize(item, rowSize);

        return {
          index,
          item,
          inlineSize: itemInlineSize,
          orgInlineSize: itemInlineSize,
          maxInlineSize: itemInlineSize,
          minInlineSize: itemInlineSize,
        };
      });
      const expectedInlineSize = this._getExpectedInlineSize(groupItems, rowSize);
      const scale = (containerInlineSize - allGap) / (expectedInlineSize - allGap);
      const noGapExpectedContainerInlineSize = expectedInlineSize - allGap;
      const noGapContainerInlineSize = containerInlineSize - allGap;

      if (stretch && expectedInlineSize && noGapContainerInlineSize !== noGapExpectedContainerInlineSize) {
        // passed이고 마지막 그룹의 경우 stretchSize가 containerSize보다 작으면 pass!
        if (
          passUnstretchRow && noGapExpectedContainerInlineSize < noGapContainerInlineSize
          && (isEndDirection ? rowIndex === groupsLength - 1 : rowIndex === 0)
        ) {
          passedPoint = [contentPos];
          passedItems = groupItems.map((_, i) => itemsLength - groupItemslength + i);

          const inlineSizes = this._getExpectedInlineSizes(groupItems, rowSize);

          itemInfos.forEach((info, i) => {
            info.minInlineSize = inlineSizes[i].minSize;
            info.maxInlineSize = inlineSizes[i].maxSize;
            info.inlineSize = between(info.inlineSize, info.minInlineSize, info.maxInlineSize);

          });
        } else {
          const { infos } = this._getStretchItemInfos(groupItems, rowSize);

          itemInfos.forEach((info, i) => {
            info.inlineSize = infos[i].size;
            info.minInlineSize = infos[i].minSize;
            info.maxInlineSize = infos[i].maxSize;
          });
        }
      }

      itemInfos.forEach((info, i) => {
        const {
          item,
          inlineSize,

        } = info;
        let nextInlineSize = inlineSize;
        const prevItem = groupItems[i - 1];
        const inlinePos = prevItem
          ? prevItem.cssInlinePos! + prevItem.cssInlineSize! + inlineGap
          : 0;

        if (isCroppedSize) {
          nextInlineSize *= scale;
        }


        const gridData = item.gridData;

        gridData.orgInlineSize = info.orgInlineSize;
        gridData.orgContentSize = rowSize;
        gridData.minInlineSize = info.minInlineSize;
        gridData.maxInlineSize = info.maxInlineSize;

        item.setCSSGridRect({
          inlinePos,
          contentPos,
          inlineSize: nextInlineSize,
          contentSize: rowSize,
        });
      });
      contentPos += contentGap + rowSize;
      if (displayedRow < 0 || rowIndex < displayedRow) {
        displayedSize = contentPos;
      }
    });

    if (isEndDirection) {
      // previous group's end outline is current group's start outline
      return {
        start: [startPoint],
        end: [displayedSize],
        passedItems,
        passed: passedPoint,
      };
    }
    // always start is lower than end.
    // contentPos is endPoinnt
    const height = contentPos - startPoint;

    items.forEach((item) => {
      item.cssContentPos! -= height;
    });
    return {
      passedItems,
      passed: passedPoint ? [passedPoint[0] - height] : null,
      start: [startPoint - height],
      end: [startPoint], // endPoint - height = startPoint
    };
  }
  public getComputedOutlineLength() {
    return 1;
  }
  public getComputedOutlineSize() {
    return this.getContainerInlineSize();
  }
  private _getRowRange() {
    const rowRange = this.rowRange;
    return isObject(rowRange) ? rowRange : [rowRange, rowRange];
  }
  private _getColumnRange() {
    const columnRange = this.columnRange;
    return isObject(columnRange) ? columnRange : [columnRange, columnRange];
  }
  private _getSizeRange() {
    const sizeRange = this.sizeRange;
    return isObject(sizeRange) ? sizeRange : [sizeRange, sizeRange];
  }
}

export interface JustifiedGrid extends Properties<typeof JustifiedGrid> {
}


/**
 * The minimum and maximum number of items per line.
 * @ko 한 줄에 들어가는 아이템의 최소, 최대 개수.
 * @name Grid.JustifiedGrid#columnRange
 * @type {$ts:Grid.JustifiedGrid.JustifiedGridOptions["columnRange"]}
 * @default [1, 8]
 * @example
 * ```js
 * import { JustifiedGrid } from "@egjs/grid";
 *
 * const grid = new JustifiedGrid(container, {
 *   columnRange: [1, 8],
 * });
 *
 * grid.columnRange = [3, 6];
 * ```
 */


/**
 * The minimum and maximum number of rows in a group, 0 is not set.
 * @ko 한 그룹에 들어가는 행의 최소, 최대 개수, 0은 미설정이다.
 * @name Grid.JustifiedGrid#rowRange
 * @type {$ts:Grid.JustifiedGrid.JustifiedGridOptions["rowRange"]}
 * @default 0
 * @example
 * ```js
 * import { JustifiedGrid } from "@egjs/grid";
 *
 * const grid = new JustifiedGrid(container, {
 *   rowRange: 0,
 * });
 *
 * grid.rowRange = [3, 4];
 * ```
 */

/**
 * The minimum and maximum size by which the item is adjusted. If it is not calculated, it may deviate from the minimum and maximum sizes.
 * @ko 아이템이 조정되는 최소, 최대 사이즈. 계산이 되지 않는 경우 최소, 최대 사이즈를 벗어날 수 있다.
 * @name Grid.JustifiedGrid#sizeRange
 * @type {$ts:Grid.JustifiedGrid.JustifiedGridOptions["sizeRange"]}
 * @default [0, Infinity]
 * @example
 * ```js
 * import { JustifiedGrid } from "@egjs/grid";
 *
 * const grid = new JustifiedGrid(container, {
 *   sizeRange: [0, Infinity],
 * });
 *
 * grid.sizeRange = [200, 800];
 * ```
 */

/**
 * Maximum number of rows to be counted for container size. You can hide it on the screen by setting overflow: hidden. -1 is not set.
 * @ko - 컨테이너 크기에 계산될 최대 row 개수. overflow: hidden을 설정하면 화면에 가릴 수 있다. -1은 미설정이다.
 * @name Grid.JustifiedGrid#displayedRow
 * @type {$ts:Grid.JustifiedGrid.JustifiedGridOptions["displayedRow"]}
 * @default -1
 * @example
 * ```js
 * import { JustifiedGrid } from "@egjs/grid";
 *
 * const grid = new JustifiedGrid(container, {
 *   displayedRow: -1,
 * });
 *
 * grid.displayedRow = 3;
 * ```
 */

/**
 * Whether to crop when the row size is out of sizeRange. If set to true, this ratio can be broken.
 * @ko - row 사이즈가 sizeRange에 벗어나면 크롭할지 여부. true로 설정하면 비율이 깨질 수 있다.
 * @name Grid.JustifiedGrid#isCroppedSize
 * @type {$ts:Grid.JustifiedGrid.JustifiedGridOptions["isCroppedSize"]}
 * @default false
 * @example
 * ```js
 * import { JustifiedGrid } from "@egjs/grid";
 *
 * const grid = new JustifiedGrid(container, {
 *   sizeRange: [200, 250],
 *   isCroppedSize: false,
 * });
 *
 * grid.isCroppedSize = true;
 * ```
 */
comments powered by Disqus