import produce from 'immer'; import iterateItems from './iterateItems'; import { LayoutItem, LayoutItemDirection } from './types'; function isLastVisible(itemIndex: number, item: LayoutItem, parent: LayoutItem) { if (item.visible === false) return false; for (let i = parent.children.length - 1; i >= 0; i--) { const child = parent.children[i]; if (child && child.visible !== false) return i === itemIndex; } return false; } function updateItemSize(itemIndex: number, itemDraft: LayoutItem, parent: LayoutItem) { if (!parent) return; // If a container has only one child, this child should not // have a width and height, and simply fill up the container if (parent.children.length === 1) { delete itemDraft.width; delete itemDraft.height; } // If all children of a container have a fixed width, the // latest visible child should have a flexible width (i.e. no "width" // property), so that it fills up the remaining space if (isLastVisible(itemIndex, itemDraft, parent)) { let allChildrenAreSized = true; for (const child of parent.children) { if (child.visible === false) continue; if (parent.direction === LayoutItemDirection.Row) { if (!child.width) { allChildrenAreSized = false; break; } } else { if (!child.height) { allChildrenAreSized = false; break; } } } if (allChildrenAreSized) { if (parent.direction === LayoutItemDirection.Row) { delete itemDraft.width; } else { delete itemDraft.height; } } } } // All items should be resizable, except for the root and the latest visible child // of a container. function updateResizeRules(itemIndex: number, itemDraft: LayoutItem, parent: LayoutItem) { if (!parent) return; const isLastVisibleChild = isLastVisible(itemIndex, itemDraft, parent); itemDraft.resizableRight = parent.direction === LayoutItemDirection.Row && !isLastVisibleChild; itemDraft.resizableBottom = parent.direction === LayoutItemDirection.Column && !isLastVisibleChild; } // Container direction should alternate between row (for the root) and // columns, then rows again. function updateDirection(_itemIndex: number, itemDraft: LayoutItem, parent: LayoutItem) { if (!parent) { itemDraft.direction = LayoutItemDirection.Row; } else { itemDraft.direction = parent.direction === LayoutItemDirection.Row ? LayoutItemDirection.Column : LayoutItemDirection.Row; } } function itemShouldBeVisible(item: LayoutItem): boolean { if (!item.children) return item.visible !== false; let oneIsVisible = false; for (const child of item.children) { if (itemShouldBeVisible(child)) { oneIsVisible = true; break; } } return oneIsVisible; } // If all children of a container are hidden, the container should be // hidden too. A container visiblity cannot be changed by the user. function updateContainerVisibility(_itemIndex: number, itemDraft: LayoutItem, _parent: LayoutItem) { if (itemDraft.children) { itemDraft.visible = itemShouldBeVisible(itemDraft); } else { itemDraft.visible = itemDraft.visible !== false; } } export default function validateLayout(layout: LayoutItem): LayoutItem { if (!layout) throw new Error('Layout is null'); if (!layout.children || !layout.children.length) throw new Error('Root does not have children'); return produce(layout, (draft: LayoutItem) => { draft.isRoot = true; iterateItems(draft, (itemIndex: number, itemDraft: LayoutItem, parent: LayoutItem) => { updateItemSize(itemIndex, itemDraft, parent); updateResizeRules(itemIndex, itemDraft, parent); updateDirection(itemIndex, itemDraft, parent); updateContainerVisibility(itemIndex, itemDraft, parent); return true; }); }); }