type Optional = T | undefined; export type DimensionModes = 'px' | '%' | 'fr' | 'fitContent' | 'gridUnit' | 'fillParent'; export type Dimension = { value: number; mode: Modes; }; interface DimensionCache { get(key: string): Type | undefined; set(key: string, dimension: Type): void; clear(): void; } // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Dimension { let enableCaching = false; let cache: DimensionCache>; export const configure = { enableCaching: (c: DimensionCache>) => { enableCaching = true; cache = c; } }; export function build(dimension: Dimension): Dimension; export function build(value: number, mode: Mode): Dimension; export function build(valueOrDimension: number | Dimension, mode_?: Mode): Dimension { const [value, mode] = typeof valueOrDimension === 'number' ? [valueOrDimension, mode_!] : [valueOrDimension.value, valueOrDimension.mode]; if (!enableCaching) { return { value, mode } as Dimension; } const key = `${value}-${mode}`; let dimension = cache.get(key) as Dimension; if (!dimension) { dimension = Object.freeze({ value, mode }) as Dimension; cache.set(key, dimension); } return dimension; } export const gridUnit = (value: number): Dimension<'gridUnit'> => build(value, 'gridUnit'); export const fitContent = (value: number): Dimension<'fitContent'> => build(value, 'fitContent'); export const fillParent = (value: number): Dimension<'fillParent'> => build(value, 'fillParent'); export const px = (value: number): Dimension<'px'> => build(value, 'px'); export const percent = (value: number): Dimension<'%'> => build(value, '%'); interface DimensionBuilder { asFirst: () => Dimension; asSecond: () => Dimension; as(mode: Mode1 | Mode2): Dimension; value: number; } // note: return.value is represented in px if any of the inputs are in px const mixedOperationHelper = ( d: Dimension, d2: Dimension, parentSpace = 12, op: (a: number, b: number) => number ): DimensionBuilder => { const d1Px = d.mode === 'px' ? d.value : toPx(d as Dimension<'gridUnit'>, parentSpace).value; const d2Px = d2.mode === 'px' ? d2.value : toPx(d2 as Dimension<'gridUnit'>, parentSpace).value; const valuePx = op(d1Px, d2Px); const valueGU = valuePx / parentSpace; return { asFirst: () => build(d.mode === 'px' ? valuePx : valueGU, d.mode), asSecond: () => build(d2.mode === 'px' ? valuePx : valueGU, d2.mode), as: (mode: T1 | T2) => build(mode === 'px' ? valuePx : valueGU, mode), value: d.mode === 'px' || d2.mode === 'px' ? valuePx : valueGU }; }; // Overloads for 'add' export function add(d: Dimension<'px'>, d2: Dimension<'px'>): DimensionBuilder<'px', 'px'>; export function add( d: Dimension, d2: Dimension ): DimensionBuilder; export function add< M1 extends 'gridUnit' | 'fitContent' | 'fillParent' | 'px', M2 extends 'gridUnit' | 'fitContent' | 'fillParent' | 'px' >(d: Dimension, d2: Dimension, parentSpace: number): DimensionBuilder; export function add( d: Dimension, d2: Dimension, parentSpace?: number ): DimensionBuilder { return mixedOperationHelper(d, d2, parentSpace, (a, b) => a + b); } // Overloads for 'minus' export function minus(d: Dimension<'px'>, d2: Dimension<'px'>): DimensionBuilder<'px', 'px'>; export function minus( d: Dimension, d2: Dimension ): DimensionBuilder; export function minus< M1 extends 'gridUnit' | 'fitContent' | 'fillParent' | 'px', M2 extends 'gridUnit' | 'fitContent' | 'fillParent' | 'px' >(d: Dimension, d2: Dimension, parentSpace: number): DimensionBuilder; export function minus( d: Dimension, d2: Dimension, parentSpace?: number ): DimensionBuilder { return mixedOperationHelper(d, d2, parentSpace, (a, b) => a - b); } // Overloads for 'min' export function min(d: Dimension<'px'>, d2: Dimension<'px'>): DimensionBuilder<'px', 'px'>; export function min( d: Dimension, d2: Dimension ): DimensionBuilder; export function min< M1 extends 'gridUnit' | 'fitContent' | 'fillParent' | 'px', M2 extends 'gridUnit' | 'fitContent' | 'fillParent' | 'px' >(d: Dimension, d2: Dimension, parentSpace: number): DimensionBuilder; export function min( d: Dimension, d2: Dimension, parentSpace?: number ): DimensionBuilder { return mixedOperationHelper(d, d2, parentSpace, (a, b) => Math.min(a, b)); } // Overloads for 'max' export function max(d: Dimension<'px'>, d2: Dimension<'px'>): DimensionBuilder<'px', 'px'>; export function max( d: Dimension, d2: Dimension ): DimensionBuilder; export function max< M1 extends 'gridUnit' | 'fitContent' | 'fillParent' | 'px', M2 extends 'gridUnit' | 'fitContent' | 'fillParent' | 'px' >(d: Dimension, d2: Dimension, parentSpace: number): DimensionBuilder; export function max( d: Dimension, d2: Dimension, parentSpace?: number ): DimensionBuilder { return mixedOperationHelper(d, d2, parentSpace, (a, b) => Math.max(a, b)); } export const toPx = | undefined>( d: T, parentSpace: number ): T extends Dimension<'gridUnit' | 'px' | 'fitContent' | 'fillParent'> ? Dimension<'px'> : Optional> => { if (d === undefined) { return d as never; } if (d.mode === 'px') { return d as never; } return build(d.value * parentSpace, 'px') as never; }; export const toGridUnit = | undefined>( d: T, parentSpace: number ): GridUnitBuilder => new GridUnitBuilder(d, parentSpace); } class GridUnitBuilder< Input extends Dimension<'gridUnit' | 'fitContent' | 'px' | 'fillParent'> | undefined, Output = Input extends Dimension<'gridUnit' | 'fitContent' | 'px' | 'fillParent'> ? Dimension<'gridUnit' | 'fitContent' | 'fillParent'> : Optional> > { private dimension: Input; private parentSpace: number; constructor(dimension: Input, parentSpace: number) { this.dimension = { ...dimension }; this.parentSpace = parentSpace; } roundDown(): Output { if (this.dimension === undefined) { return this.dimension as never; } if (this.dimension.mode === 'gridUnit' || this.dimension.mode === 'fitContent' || this.dimension.mode === 'fillParent') { return this.dimension as never; } return Dimension.gridUnit(Math.floor(this.dimension.value / this.parentSpace)) as never; } roundUp(): Output { if (this.dimension === undefined) { return this.dimension as never; } if (this.dimension.mode === 'gridUnit' || this.dimension.mode === 'fitContent' || this.dimension.mode === 'fillParent') { return this.dimension as never; } return Dimension.gridUnit(Math.ceil(this.dimension.value / this.parentSpace)) as never; } raw(): Output { if (this.dimension === undefined) { return this.dimension as never; } if (this.dimension.mode === 'gridUnit' || this.dimension.mode === 'fitContent' || this.dimension.mode === 'fillParent') { return this.dimension as never; } return Dimension.gridUnit(this.dimension.value / this.parentSpace) as never; } }