UNPKG

angular-split

Version:

Angular UI library to split views and allow dragging to resize areas using CSS grid layout.

1 lines 89.3 kB
{"version":3,"file":"angular-split.mjs","sources":["../../../projects/angular-split/src/lib/angular-split-config.token.ts","../../../projects/angular-split/src/lib/gutter/split-gutter.directive.ts","../../../projects/angular-split/src/lib/gutter/gutter-num-token.ts","../../../projects/angular-split/src/lib/gutter/split-gutter-drag-handle.directive.ts","../../../projects/angular-split/src/lib/gutter/split-gutter-exclude-from-drag.directive.ts","../../../projects/angular-split/src/lib/utils.ts","../../../projects/angular-split/src/lib/split-custom-events-behavior.directive.ts","../../../projects/angular-split/src/lib/validations.ts","../../../projects/angular-split/src/lib/gutter/split-gutter-dynamic-injector.directive.ts","../../../projects/angular-split/src/lib/split/split.component.ts","../../../projects/angular-split/src/lib/split/split.component.html","../../../projects/angular-split/src/lib/models.ts","../../../projects/angular-split/src/lib/split-area/split-area.component.ts","../../../projects/angular-split/src/lib/split-area/split-area.component.html","../../../projects/angular-split/src/lib/split-module.module.ts","../../../projects/angular-split/src/public_api.ts","../../../projects/angular-split/src/angular-split.ts"],"sourcesContent":["import { InjectionToken, Provider, inject } from '@angular/core'\nimport { SplitDir, SplitDirection, SplitUnit } from './models'\n\nexport interface AngularSplitDefaultOptions {\n dir: SplitDir\n direction: SplitDirection\n disabled: boolean\n gutterDblClickDuration: number\n gutterSize: number\n gutterStep: number\n gutterClickDeltaPx: number\n restrictMove: boolean\n unit: SplitUnit\n useTransition: boolean\n}\n\nconst defaultOptions: AngularSplitDefaultOptions = {\n dir: 'ltr',\n direction: 'horizontal',\n disabled: false,\n gutterDblClickDuration: 0,\n gutterSize: 11,\n gutterStep: 1,\n gutterClickDeltaPx: 2,\n restrictMove: false,\n unit: 'percent',\n useTransition: false,\n}\n\nexport const ANGULAR_SPLIT_DEFAULT_OPTIONS = new InjectionToken<AngularSplitDefaultOptions>(\n 'angular-split-global-config',\n { providedIn: 'root', factory: () => defaultOptions },\n)\n\n/**\n * Provides default options for angular split. The options object has hierarchical inheritance\n * which means only the declared properties will be overridden\n */\nexport function provideAngularSplitOptions(options: Partial<AngularSplitDefaultOptions>): Provider {\n return {\n provide: ANGULAR_SPLIT_DEFAULT_OPTIONS,\n useFactory: (): AngularSplitDefaultOptions => ({\n ...inject(ANGULAR_SPLIT_DEFAULT_OPTIONS, { skipSelf: true }),\n ...options,\n }),\n }\n}\n","import { Directive, ElementRef, inject, TemplateRef } from '@angular/core'\nimport { SplitAreaComponent } from '../split-area/split-area.component'\n\nexport interface SplitGutterTemplateContext {\n /**\n * The area before the gutter.\n * In RTL the right area and in LTR the left area\n */\n areaBefore: SplitAreaComponent\n /**\n * The area after the gutter.\n * In RTL the left area and in LTR the right area\n */\n areaAfter: SplitAreaComponent\n /**\n * The absolute number of the gutter based on direction (RTL and LTR).\n * First gutter is 1, second is 2, etc...\n */\n gutterNum: number\n /**\n * Whether this is the first gutter.\n * In RTL the most right area and in LTR the most left area\n */\n first: boolean\n /**\n * Whether this is the last gutter.\n * In RTL the most left area and in LTR the most right area\n */\n last: boolean\n /**\n * Whether the gutter is being dragged now\n */\n isDragged: boolean\n}\n\n@Directive({\n selector: '[asSplitGutter]',\n standalone: true,\n})\nexport class SplitGutterDirective {\n readonly template = inject<TemplateRef<SplitGutterTemplateContext>>(TemplateRef)\n\n /**\n * The map holds reference to the drag handle elements inside instances\n * of the provided template.\n *\n * @internal\n */\n readonly _gutterToHandleElementMap = new Map<number, ElementRef<HTMLElement>[]>()\n /**\n * The map holds reference to the excluded drag elements inside instances\n * of the provided template.\n *\n * @internal\n */\n readonly _gutterToExcludeDragElementMap = new Map<number, ElementRef<HTMLElement>[]>()\n\n /**\n * @internal\n */\n _canStartDragging(originElement: HTMLElement, gutterNum: number) {\n if (this._gutterToExcludeDragElementMap.has(gutterNum)) {\n const isInsideExclude = this._gutterToExcludeDragElementMap\n .get(gutterNum)\n .some((gutterExcludeElement) => gutterExcludeElement.nativeElement.contains(originElement))\n\n if (isInsideExclude) {\n return false\n }\n }\n\n if (this._gutterToHandleElementMap.has(gutterNum)) {\n return this._gutterToHandleElementMap\n .get(gutterNum)\n .some((gutterHandleElement) => gutterHandleElement.nativeElement.contains(originElement))\n }\n\n return true\n }\n\n /**\n * @internal\n */\n _addToMap(map: Map<number, ElementRef<HTMLElement>[]>, gutterNum: number, elementRef: ElementRef<HTMLElement>) {\n if (map.has(gutterNum)) {\n map.get(gutterNum).push(elementRef)\n } else {\n map.set(gutterNum, [elementRef])\n }\n }\n\n /**\n * @internal\n */\n _removedFromMap(map: Map<number, ElementRef<HTMLElement>[]>, gutterNum: number, elementRef: ElementRef<HTMLElement>) {\n const elements = map.get(gutterNum)\n elements.splice(elements.indexOf(elementRef), 1)\n\n if (elements.length === 0) {\n map.delete(gutterNum)\n }\n }\n\n static ngTemplateContextGuard(_dir: SplitGutterDirective, ctx: unknown): ctx is SplitGutterTemplateContext {\n return true\n }\n}\n","import { InjectionToken } from '@angular/core'\n\n/**\n * Identifies the gutter by number through DI\n * to allow SplitGutterDragHandleDirective and SplitGutterExcludeFromDragDirective to know\n * the gutter template context without inputs\n */\nexport const GUTTER_NUM_TOKEN = new InjectionToken<number>('Gutter num')\n","import { Directive, OnDestroy, ElementRef, inject } from '@angular/core'\nimport { SplitGutterDirective } from './split-gutter.directive'\nimport { GUTTER_NUM_TOKEN } from './gutter-num-token'\n\n@Directive({\n selector: '[asSplitGutterDragHandle]',\n standalone: true,\n})\nexport class SplitGutterDragHandleDirective implements OnDestroy {\n private readonly gutterNum = inject(GUTTER_NUM_TOKEN)\n private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef)\n private readonly gutterDir = inject(SplitGutterDirective)\n\n constructor() {\n this.gutterDir._addToMap(this.gutterDir._gutterToHandleElementMap, this.gutterNum, this.elementRef)\n }\n\n ngOnDestroy(): void {\n this.gutterDir._removedFromMap(this.gutterDir._gutterToHandleElementMap, this.gutterNum, this.elementRef)\n }\n}\n","import { Directive, OnDestroy, ElementRef, inject } from '@angular/core'\nimport { SplitGutterDirective } from './split-gutter.directive'\nimport { GUTTER_NUM_TOKEN } from './gutter-num-token'\n\n@Directive({\n selector: '[asSplitGutterExcludeFromDrag]',\n standalone: true,\n})\nexport class SplitGutterExcludeFromDragDirective implements OnDestroy {\n private readonly gutterNum = inject(GUTTER_NUM_TOKEN)\n private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef)\n private readonly gutterDir = inject(SplitGutterDirective)\n\n constructor() {\n this.gutterDir._addToMap(this.gutterDir._gutterToExcludeDragElementMap, this.gutterNum, this.elementRef)\n }\n\n ngOnDestroy(): void {\n this.gutterDir._removedFromMap(this.gutterDir._gutterToExcludeDragElementMap, this.gutterNum, this.elementRef)\n }\n}\n","import { NgZone, Signal, computed, inject, numberAttribute, signal, untracked } from '@angular/core'\nimport { Observable, filter, fromEvent, merge } from 'rxjs'\n\nexport interface ClientPoint {\n x: number\n y: number\n}\n\n/**\n * Only supporting a single {@link TouchEvent} point\n */\nexport function getPointFromEvent(event: MouseEvent | TouchEvent | KeyboardEvent): ClientPoint {\n // NOTE: In firefox TouchEvent is only defined for touch capable devices\n const isTouchEvent = (e: typeof event): e is TouchEvent => window.TouchEvent && event instanceof TouchEvent\n\n if (isTouchEvent(event)) {\n if (event.changedTouches.length === 0) {\n return undefined\n }\n\n const { clientX, clientY } = event.changedTouches[0]\n\n return {\n x: clientX,\n y: clientY,\n }\n }\n\n if (event instanceof KeyboardEvent) {\n const target = event.target as HTMLElement\n\n // Calculate element midpoint\n return {\n x: target.offsetLeft + target.offsetWidth / 2,\n y: target.offsetTop + target.offsetHeight / 2,\n }\n }\n\n return {\n x: event.clientX,\n y: event.clientY,\n }\n}\n\nexport function gutterEventsEqualWithDelta(\n startEvent: MouseEvent | TouchEvent,\n endEvent: MouseEvent | TouchEvent,\n deltaInPx: number,\n gutterElement: HTMLElement,\n) {\n if (\n !gutterElement.contains(startEvent.target as HTMLElement) ||\n !gutterElement.contains(endEvent.target as HTMLElement)\n ) {\n return false\n }\n\n const startPoint = getPointFromEvent(startEvent)\n const endPoint = getPointFromEvent(endEvent)\n\n return Math.abs(endPoint.x - startPoint.x) <= deltaInPx && Math.abs(endPoint.y - startPoint.y) <= deltaInPx\n}\n\nexport function fromMouseDownEvent(target: HTMLElement | Document) {\n return merge(\n fromEvent<MouseEvent>(target, 'mousedown').pipe(filter((e) => e.button === 0)),\n // We must prevent default here so we declare it as non passive explicitly\n fromEvent<TouchEvent>(target, 'touchstart', { passive: false }),\n )\n}\n\nexport function fromMouseMoveEvent(target: HTMLElement | Document) {\n return merge(fromEvent<MouseEvent>(target, 'mousemove'), fromEvent<TouchEvent>(target, 'touchmove'))\n}\n\nexport function fromMouseUpEvent(target: HTMLElement | Document, includeTouchCancel = false) {\n const withoutTouchCancel = merge(fromEvent<MouseEvent>(target, 'mouseup'), fromEvent<TouchEvent>(target, 'touchend'))\n\n return includeTouchCancel\n ? merge(withoutTouchCancel, fromEvent<TouchEvent>(target, 'touchcancel'))\n : withoutTouchCancel\n}\n\nexport function sum<T>(array: T[] | readonly T[], fn: (item: T) => number) {\n return (array as T[]).reduce((sum, item) => sum + fn(item), 0)\n}\n\nexport function toRecord<TItem, TKey extends string, TValue>(\n array: TItem[] | readonly TItem[],\n fn: (item: TItem, index: number) => [TKey, TValue],\n): Record<TKey, TValue> {\n return (array as TItem[]).reduce<Record<TKey, TValue>>(\n (record, item, index) => {\n const [key, value] = fn(item, index)\n record[key] = value\n return record\n },\n {} as Record<TKey, TValue>,\n )\n}\n\nexport function createClassesString(classesRecord: Record<string, boolean>) {\n return Object.entries(classesRecord)\n .filter(([, value]) => value)\n .map(([key]) => key)\n .join(' ')\n}\n\nexport interface MirrorSignal<T> {\n (): T\n set(value: T): void\n reset(): void\n}\n\n/**\n * Creates a semi signal which allows writes but is based on an existing signal\n * Whenever the original signal changes the mirror signal gets aligned\n * overriding the current value inside.\n */\nexport function mirrorSignal<T>(outer: Signal<T>): MirrorSignal<T> {\n const inner = computed(() => signal(outer()))\n const mirror: MirrorSignal<T> = () => inner()()\n mirror.set = (value: T) => untracked(inner).set(value)\n mirror.reset = () => untracked(() => inner().set(outer()))\n return mirror\n}\n\nexport function leaveNgZone<T>() {\n return (source: Observable<T>) =>\n new Observable<T>((observer) => inject(NgZone).runOutsideAngular(() => source.subscribe(observer)))\n}\n\nexport const numberAttributeWithFallback = (fallback: number) => (value: unknown) => numberAttribute(value, fallback)\n\nexport const assertUnreachable = (value: never, name: string) => {\n throw new Error(`as-split: unknown value \"${value}\" for \"${name}\"`)\n}\n","/* eslint-disable @angular-eslint/no-output-native */\n/* eslint-disable @angular-eslint/no-output-rename */\n/* eslint-disable @angular-eslint/no-input-rename */\nimport { Directive, ElementRef, inject, input, output } from '@angular/core'\nimport {\n gutterEventsEqualWithDelta,\n fromMouseDownEvent,\n fromMouseMoveEvent,\n fromMouseUpEvent,\n leaveNgZone,\n} from './utils'\nimport {\n delay,\n filter,\n fromEvent,\n map,\n mergeMap,\n of,\n repeat,\n scan,\n switchMap,\n take,\n takeUntil,\n tap,\n timeInterval,\n} from 'rxjs'\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop'\nimport { DOCUMENT } from '@angular/common'\n\n/**\n * Emits mousedown, click, double click and keydown out of zone\n *\n * Emulates browser behavior of click and double click with new features:\n * 1. Supports touch events (tap and double tap)\n * 2. Ignores the first click in a double click with the side effect of a bit slower emission of the click event\n * 3. Allow customizing the delay after mouse down to count another mouse down as a double click\n */\n@Directive({\n selector: '[asSplitCustomEventsBehavior]',\n standalone: true,\n})\nexport class SplitCustomEventsBehaviorDirective {\n private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef)\n private readonly document = inject(DOCUMENT)\n\n readonly multiClickThreshold = input.required<number>({ alias: 'asSplitCustomMultiClickThreshold' })\n readonly deltaInPx = input.required<number>({ alias: 'asSplitCustomClickDeltaInPx' })\n readonly mouseDown = output<MouseEvent | TouchEvent>({ alias: 'asSplitCustomMouseDown' })\n readonly click = output({ alias: 'asSplitCustomClick' })\n readonly dblClick = output({ alias: 'asSplitCustomDblClick' })\n readonly keyDown = output<KeyboardEvent>({ alias: 'asSplitCustomKeyDown' })\n\n constructor() {\n fromEvent<KeyboardEvent>(this.elementRef.nativeElement, 'keydown')\n .pipe(leaveNgZone(), takeUntilDestroyed())\n .subscribe((e) => this.keyDown.emit(e))\n\n // We just need to know when drag start to cancel all click related interactions\n const dragStarted$ = fromMouseDownEvent(this.elementRef.nativeElement).pipe(\n switchMap((mouseDownEvent) =>\n fromMouseMoveEvent(this.document).pipe(\n filter(\n (e) => !gutterEventsEqualWithDelta(mouseDownEvent, e, this.deltaInPx(), this.elementRef.nativeElement),\n ),\n take(1),\n map(() => true),\n takeUntil(fromMouseUpEvent(this.document)),\n ),\n ),\n )\n\n fromMouseDownEvent(this.elementRef.nativeElement)\n .pipe(\n tap((e) => this.mouseDown.emit(e)),\n // Gather mousedown events intervals to identify whether it is a single double or more click\n timeInterval(),\n // We only count a click as part of a multi click if the multiClickThreshold wasn't reached\n scan((sum, { interval }) => (interval >= this.multiClickThreshold() ? 1 : sum + 1), 0),\n // As mouseup always comes after mousedown if the delayed mouseup has yet to come\n // but a new mousedown arrived we can discard the older mouseup as we are part of a multi click\n switchMap((numOfConsecutiveClicks) =>\n // In case of a double click we directly emit as we don't care about more than two consecutive clicks\n // so we don't have to wait compared to a single click that might be followed by another for a double.\n // In case of a mouse up that was too long after the mouse down\n // we don't have to wait as we know it won't be a multi click but a single click\n fromMouseUpEvent(this.elementRef.nativeElement).pipe(\n timeInterval(),\n take(1),\n numOfConsecutiveClicks === 2\n ? map(() => numOfConsecutiveClicks)\n : mergeMap(({ interval }) =>\n interval >= this.multiClickThreshold()\n ? of(numOfConsecutiveClicks)\n : of(numOfConsecutiveClicks).pipe(delay(this.multiClickThreshold() - interval)),\n ),\n ),\n ),\n // Discard everything once drag started and listen again (repeat) to mouse down\n takeUntil(dragStarted$),\n repeat(),\n leaveNgZone(),\n takeUntilDestroyed(),\n )\n .subscribe((amount) => {\n if (amount === 1) {\n this.click.emit()\n } else if (amount === 2) {\n this.dblClick.emit()\n }\n })\n }\n}\n","import { SplitAreaSize, SplitUnit } from './models'\nimport { SplitAreaComponent } from './split-area/split-area.component'\nimport { sum } from './utils'\n\nexport function areAreasValid(areas: readonly SplitAreaComponent[], unit: SplitUnit, logWarnings: boolean): boolean {\n if (areas.length === 0) {\n return true\n }\n\n const areaSizes = areas.map((area): SplitAreaSize => {\n const size = area.size()\n return size === 'auto' ? '*' : size\n })\n\n const wildcardAreas = areaSizes.filter((areaSize) => areaSize === '*')\n\n if (wildcardAreas.length > 1) {\n if (logWarnings) {\n console.warn('as-split: Maximum one * area is allowed')\n }\n\n return false\n }\n\n if (unit === 'pixel') {\n if (wildcardAreas.length === 1) {\n return true\n }\n\n if (logWarnings) {\n console.warn('as-split: Pixel mode must have exactly one * area')\n }\n\n return false\n }\n\n const sumPercent = sum(areaSizes, (areaSize) => (areaSize === '*' ? 0 : areaSize))\n\n // As percent calculation isn't perfect we allow for a small margin of error\n if (wildcardAreas.length === 1) {\n if (sumPercent <= 100.1) {\n return true\n }\n\n if (logWarnings) {\n console.warn(`as-split: Percent areas must total 100%`)\n }\n\n return false\n }\n\n if (sumPercent < 99.9 || sumPercent > 100.1) {\n if (logWarnings) {\n console.warn('as-split: Percent areas must total 100%')\n }\n\n return false\n }\n\n return true\n}\n","import { Injector, Directive, ViewContainerRef, TemplateRef, input, effect, inject } from '@angular/core'\nimport { GUTTER_NUM_TOKEN } from './gutter-num-token'\n\ninterface SplitGutterDynamicInjectorTemplateContext {\n $implicit: Injector\n}\n\n/**\n * This directive allows creating a dynamic injector inside ngFor\n * with dynamic gutter num and expose the injector for ngTemplateOutlet usage\n */\n@Directive({\n selector: '[asSplitGutterDynamicInjector]',\n standalone: true,\n})\nexport class SplitGutterDynamicInjectorDirective {\n private readonly vcr = inject(ViewContainerRef)\n private readonly templateRef = inject<TemplateRef<SplitGutterDynamicInjectorTemplateContext>>(TemplateRef)\n\n protected readonly gutterNum = input.required<number>({ alias: 'asSplitGutterDynamicInjector' })\n\n constructor() {\n effect(() => {\n this.vcr.clear()\n\n const injector = Injector.create({\n providers: [\n {\n provide: GUTTER_NUM_TOKEN,\n useValue: this.gutterNum(),\n },\n ],\n parent: this.vcr.injector,\n })\n\n this.vcr.createEmbeddedView(this.templateRef, { $implicit: injector })\n })\n }\n\n static ngTemplateContextGuard(\n _dir: SplitGutterDynamicInjectorDirective,\n ctx: unknown,\n ): ctx is SplitGutterDynamicInjectorTemplateContext {\n return true\n }\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n HostBinding,\n InjectionToken,\n NgZone,\n Renderer2,\n booleanAttribute,\n computed,\n contentChild,\n contentChildren,\n effect,\n inject,\n input,\n isDevMode,\n output,\n signal,\n} from '@angular/core'\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop'\nimport type { SplitAreaComponent } from '../split-area/split-area.component'\nimport { Subject, filter, fromEvent, map, pairwise, skipWhile, startWith, switchMap, take, takeUntil, tap } from 'rxjs'\nimport {\n ClientPoint,\n createClassesString,\n gutterEventsEqualWithDelta,\n fromMouseMoveEvent,\n fromMouseUpEvent,\n getPointFromEvent,\n leaveNgZone,\n numberAttributeWithFallback,\n sum,\n toRecord,\n assertUnreachable,\n} from '../utils'\nimport { DOCUMENT, NgStyle, NgTemplateOutlet } from '@angular/common'\nimport { SplitGutterInteractionEvent, SplitAreaSize } from '../models'\nimport { SplitCustomEventsBehaviorDirective } from '../split-custom-events-behavior.directive'\nimport { areAreasValid } from '../validations'\nimport { SplitGutterDirective } from '../gutter/split-gutter.directive'\nimport { SplitGutterDynamicInjectorDirective } from '../gutter/split-gutter-dynamic-injector.directive'\nimport { ANGULAR_SPLIT_DEFAULT_OPTIONS } from '../angular-split-config.token'\n\ninterface MouseDownContext {\n mouseDownEvent: MouseEvent | TouchEvent\n gutterIndex: number\n gutterElement: HTMLElement\n areaBeforeGutterIndex: number\n areaAfterGutterIndex: number\n}\n\ninterface AreaBoundary {\n min: number\n max: number\n}\n\ninterface DragStartContext {\n startEvent: MouseEvent | TouchEvent | KeyboardEvent\n areasPixelSizes: number[]\n totalAreasPixelSize: number\n areaIndexToBoundaries: Record<number, AreaBoundary>\n areaBeforeGutterIndex: number\n areaAfterGutterIndex: number\n}\n\nexport const SPLIT_AREA_CONTRACT = new InjectionToken<SplitAreaComponent>('Split Area Contract')\n\n@Component({\n selector: 'as-split',\n imports: [NgStyle, SplitCustomEventsBehaviorDirective, SplitGutterDynamicInjectorDirective, NgTemplateOutlet],\n exportAs: 'asSplit',\n templateUrl: './split.component.html',\n styleUrl: './split.component.css',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SplitComponent {\n private readonly document = inject(DOCUMENT)\n private readonly renderer = inject(Renderer2)\n private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef)\n private readonly ngZone = inject(NgZone)\n private readonly defaultOptions = inject(ANGULAR_SPLIT_DEFAULT_OPTIONS)\n\n private readonly gutterMouseDownSubject = new Subject<MouseDownContext>()\n private readonly dragProgressSubject = new Subject<SplitGutterInteractionEvent>()\n\n /**\n * @internal\n */\n readonly _areas = contentChildren(SPLIT_AREA_CONTRACT)\n protected readonly customGutter = contentChild(SplitGutterDirective)\n readonly gutterSize = input(this.defaultOptions.gutterSize, {\n transform: numberAttributeWithFallback(this.defaultOptions.gutterSize),\n })\n readonly gutterStep = input(this.defaultOptions.gutterStep, {\n transform: numberAttributeWithFallback(this.defaultOptions.gutterStep),\n })\n readonly disabled = input(this.defaultOptions.disabled, { transform: booleanAttribute })\n readonly gutterClickDeltaPx = input(this.defaultOptions.gutterClickDeltaPx, {\n transform: numberAttributeWithFallback(this.defaultOptions.gutterClickDeltaPx),\n })\n readonly direction = input(this.defaultOptions.direction)\n readonly dir = input(this.defaultOptions.dir)\n readonly unit = input(this.defaultOptions.unit)\n readonly gutterAriaLabel = input<string>()\n readonly restrictMove = input(this.defaultOptions.restrictMove, { transform: booleanAttribute })\n readonly useTransition = input(this.defaultOptions.useTransition, { transform: booleanAttribute })\n readonly gutterDblClickDuration = input(this.defaultOptions.gutterDblClickDuration, {\n transform: numberAttributeWithFallback(this.defaultOptions.gutterDblClickDuration),\n })\n readonly gutterClick = output<SplitGutterInteractionEvent>()\n readonly gutterDblClick = output<SplitGutterInteractionEvent>()\n readonly dragStart = output<SplitGutterInteractionEvent>()\n readonly dragEnd = output<SplitGutterInteractionEvent>()\n readonly transitionEnd = output<SplitAreaSize[]>()\n\n readonly dragProgress$ = this.dragProgressSubject.asObservable()\n\n /**\n * @internal\n */\n readonly _visibleAreas = computed(() => this._areas().filter((area) => area.visible()))\n private readonly gridTemplateColumnsStyle = computed(() => this.createGridTemplateColumnsStyle())\n private readonly hostClasses = computed(() =>\n createClassesString({\n [`as-${this.direction()}`]: true,\n [`as-${this.unit()}`]: true,\n ['as-disabled']: this.disabled(),\n ['as-dragging']: this._isDragging(),\n ['as-transition']: this.useTransition() && !this._isDragging(),\n }),\n )\n protected readonly draggedGutterIndex = signal<number>(undefined)\n /**\n * @internal\n */\n readonly _isDragging = computed(() => this.draggedGutterIndex() !== undefined)\n /**\n * @internal\n * Should only be used by {@link SplitAreaComponent._internalSize}\n */\n readonly _alignedVisibleAreasSizes = computed(() => this.createAlignedVisibleAreasSize())\n\n @HostBinding('class') protected get hostClassesBinding() {\n return this.hostClasses()\n }\n\n @HostBinding('dir') protected get hostDirBinding() {\n return this.dir()\n }\n\n constructor() {\n if (isDevMode()) {\n // Logs warnings to console when the provided areas sizes are invalid\n effect(() => {\n // Special mode when no size input was declared which is a valid mode\n if (this.unit() === 'percent' && this._visibleAreas().every((area) => area.size() === 'auto')) {\n return\n }\n\n areAreasValid(this._visibleAreas(), this.unit(), true)\n })\n }\n\n // Responsible for updating grid template style. Must be this way and not based on HostBinding\n // as change detection for host binding is bound to the parent component and this style\n // is updated on every mouse move. Doing it this way will prevent change detection cycles in parent.\n effect(() => {\n const gridTemplateColumnsStyle = this.gridTemplateColumnsStyle()\n this.renderer.setStyle(this.elementRef.nativeElement, 'grid-template', gridTemplateColumnsStyle)\n })\n\n this.gutterMouseDownSubject\n .pipe(\n filter(\n (context) =>\n !this.customGutter() ||\n this.customGutter()._canStartDragging(\n context.mouseDownEvent.target as HTMLElement,\n context.gutterIndex + 1,\n ),\n ),\n switchMap((mouseDownContext) =>\n // As we have gutterClickDeltaPx we can't just start the drag but need to make sure\n // we are out of the delta pixels. As the delta can be any number we make sure\n // we always start the drag if we go out of the gutter (delta based on mouse position is larger than gutter).\n // As moving can start inside the drag and end outside of it we always keep track of the previous event\n // so once the current is out of the delta size we use the previous one as the drag start baseline.\n fromMouseMoveEvent(this.document).pipe(\n startWith(mouseDownContext.mouseDownEvent),\n pairwise(),\n skipWhile(([, currMoveEvent]) =>\n gutterEventsEqualWithDelta(\n mouseDownContext.mouseDownEvent,\n currMoveEvent,\n this.gutterClickDeltaPx(),\n mouseDownContext.gutterElement,\n ),\n ),\n take(1),\n takeUntil(fromMouseUpEvent(this.document, true)),\n tap(() => {\n this.ngZone.run(() => {\n this.dragStart.emit(this.createDragInteractionEvent(mouseDownContext.gutterIndex))\n this.draggedGutterIndex.set(mouseDownContext.gutterIndex)\n })\n }),\n map(([prevMouseEvent]) =>\n this.createDragStartContext(\n prevMouseEvent,\n mouseDownContext.areaBeforeGutterIndex,\n mouseDownContext.areaAfterGutterIndex,\n ),\n ),\n switchMap((dragStartContext) =>\n fromMouseMoveEvent(this.document).pipe(\n tap((moveEvent) => this.mouseDragMove(moveEvent, dragStartContext)),\n takeUntil(fromMouseUpEvent(this.document, true)),\n tap({\n complete: () =>\n this.ngZone.run(() => {\n this.dragEnd.emit(this.createDragInteractionEvent(this.draggedGutterIndex()))\n this.draggedGutterIndex.set(undefined)\n }),\n }),\n ),\n ),\n ),\n ),\n takeUntilDestroyed(),\n )\n .subscribe()\n\n fromEvent<TransitionEvent>(this.elementRef.nativeElement, 'transitionend')\n .pipe(\n filter((e) => e.propertyName.startsWith('grid-template')),\n leaveNgZone(),\n takeUntilDestroyed(),\n )\n .subscribe(() => this.ngZone.run(() => this.transitionEnd.emit(this.createAreaSizes())))\n }\n\n protected gutterClicked(gutterIndex: number) {\n this.ngZone.run(() => this.gutterClick.emit(this.createDragInteractionEvent(gutterIndex)))\n }\n\n protected gutterDoubleClicked(gutterIndex: number) {\n this.ngZone.run(() => this.gutterDblClick.emit(this.createDragInteractionEvent(gutterIndex)))\n }\n\n protected gutterMouseDown(\n e: MouseEvent | TouchEvent,\n gutterElement: HTMLElement,\n gutterIndex: number,\n areaBeforeGutterIndex: number,\n areaAfterGutterIndex: number,\n ) {\n if (this.disabled()) {\n return\n }\n\n e.preventDefault()\n e.stopPropagation()\n\n this.gutterMouseDownSubject.next({\n mouseDownEvent: e,\n gutterElement,\n gutterIndex,\n areaBeforeGutterIndex,\n areaAfterGutterIndex,\n })\n }\n\n protected gutterKeyDown(\n e: KeyboardEvent,\n gutterIndex: number,\n areaBeforeGutterIndex: number,\n areaAfterGutterIndex: number,\n ) {\n if (this.disabled()) {\n return\n }\n\n const pixelsToMove = 50\n const pageMoveMultiplier = 10\n\n let xPointOffset = 0\n let yPointOffset = 0\n\n if (this.direction() === 'horizontal') {\n // Even though we are going in the x axis we support page up and down\n switch (e.key) {\n case 'ArrowLeft':\n xPointOffset -= pixelsToMove\n break\n case 'ArrowRight':\n xPointOffset += pixelsToMove\n break\n case 'PageUp':\n if (this.dir() === 'rtl') {\n xPointOffset -= pixelsToMove * pageMoveMultiplier\n } else {\n xPointOffset += pixelsToMove * pageMoveMultiplier\n }\n break\n case 'PageDown':\n if (this.dir() === 'rtl') {\n xPointOffset += pixelsToMove * pageMoveMultiplier\n } else {\n xPointOffset -= pixelsToMove * pageMoveMultiplier\n }\n break\n default:\n return\n }\n } else {\n switch (e.key) {\n case 'ArrowUp':\n yPointOffset -= pixelsToMove\n break\n case 'ArrowDown':\n yPointOffset += pixelsToMove\n break\n case 'PageUp':\n yPointOffset -= pixelsToMove * pageMoveMultiplier\n break\n case 'PageDown':\n yPointOffset += pixelsToMove * pageMoveMultiplier\n break\n default:\n return\n }\n }\n\n e.preventDefault()\n e.stopPropagation()\n\n const gutterMidPoint = getPointFromEvent(e)\n const dragStartContext = this.createDragStartContext(e, areaBeforeGutterIndex, areaAfterGutterIndex)\n\n this.ngZone.run(() => {\n this.dragStart.emit(this.createDragInteractionEvent(gutterIndex))\n this.draggedGutterIndex.set(gutterIndex)\n })\n\n this.dragMoveToPoint({ x: gutterMidPoint.x + xPointOffset, y: gutterMidPoint.y + yPointOffset }, dragStartContext)\n\n this.ngZone.run(() => {\n this.dragEnd.emit(this.createDragInteractionEvent(gutterIndex))\n this.draggedGutterIndex.set(undefined)\n })\n }\n\n protected getGutterGridStyle(nextAreaIndex: number) {\n const gutterNum = nextAreaIndex * 2\n const style = `${gutterNum} / ${gutterNum}`\n\n return {\n ['grid-column']: this.direction() === 'horizontal' ? style : '1',\n ['grid-row']: this.direction() === 'vertical' ? style : '1',\n }\n }\n\n protected getAriaAreaSizeText(area: SplitAreaComponent): string {\n const size = area._internalSize()\n\n if (size === '*') {\n return undefined\n }\n\n return `${size.toFixed(0)} ${this.unit()}`\n }\n\n protected getAriaValue(size: SplitAreaSize) {\n return size === '*' ? undefined : size\n }\n\n private createDragInteractionEvent(gutterIndex: number): SplitGutterInteractionEvent {\n return {\n gutterNum: gutterIndex + 1,\n sizes: this.createAreaSizes(),\n }\n }\n\n private createAreaSizes() {\n return this._visibleAreas().map((area) => area._internalSize())\n }\n\n private createDragStartContext(\n startEvent: MouseEvent | TouchEvent | KeyboardEvent,\n areaBeforeGutterIndex: number,\n areaAfterGutterIndex: number,\n ): DragStartContext {\n const splitBoundingRect = this.elementRef.nativeElement.getBoundingClientRect()\n const splitSize = this.direction() === 'horizontal' ? splitBoundingRect.width : splitBoundingRect.height\n const totalAreasPixelSize = splitSize - (this._visibleAreas().length - 1) * this.gutterSize()\n // Use the internal size and split size to calculate the pixel size from wildcard and percent areas\n const areaPixelSizesWithWildcard = this._areas().map((area) => {\n if (this.unit() === 'pixel') {\n return area._internalSize()\n } else {\n const size = area._internalSize()\n\n if (size === '*') {\n return size\n }\n\n return (size / 100) * totalAreasPixelSize\n }\n })\n const remainingSize = Math.max(\n 0,\n totalAreasPixelSize - sum(areaPixelSizesWithWildcard, (size) => (size === '*' ? 0 : size)),\n )\n const areasPixelSizes = areaPixelSizesWithWildcard.map((size) => (size === '*' ? remainingSize : size))\n\n return {\n startEvent,\n areaBeforeGutterIndex,\n areaAfterGutterIndex,\n areasPixelSizes,\n totalAreasPixelSize,\n areaIndexToBoundaries: toRecord(this._areas(), (area, index) => {\n const percentToPixels = (percent: number) => (percent / 100) * totalAreasPixelSize\n\n const value: AreaBoundary =\n this.unit() === 'pixel'\n ? {\n min: area._normalizedMinSize(),\n max: area._normalizedMaxSize(),\n }\n : {\n min: percentToPixels(area._normalizedMinSize()),\n max: percentToPixels(area._normalizedMaxSize()),\n }\n\n return [index.toString(), value]\n }),\n }\n }\n\n private mouseDragMove(moveEvent: MouseEvent | TouchEvent, dragStartContext: DragStartContext) {\n moveEvent.preventDefault()\n moveEvent.stopPropagation()\n\n const endPoint = getPointFromEvent(moveEvent)\n\n this.dragMoveToPoint(endPoint, dragStartContext)\n }\n\n private dragMoveToPoint(endPoint: ClientPoint, dragStartContext: DragStartContext) {\n const startPoint = getPointFromEvent(dragStartContext.startEvent)\n const preDirOffset = this.direction() === 'horizontal' ? endPoint.x - startPoint.x : endPoint.y - startPoint.y\n const offset = this.direction() === 'horizontal' && this.dir() === 'rtl' ? -preDirOffset : preDirOffset\n const isDraggingForward = offset > 0\n // Align offset with gutter step and abs it as we need absolute pixels movement\n const absSteppedOffset = Math.abs(Math.round(offset / this.gutterStep()) * this.gutterStep())\n // Copy as we don't want to edit the original array\n const tempAreasPixelSizes = [...dragStartContext.areasPixelSizes]\n // As we are going to shuffle the areas order for easier iterations we should work with area indices array\n // instead of actual area sizes array.\n const areasIndices = tempAreasPixelSizes.map((_, index) => index)\n // The two variables below are ordered for iterations with real area indices inside.\n // We must also remove the invisible ones as we can't expand or shrink them.\n const areasIndicesBeforeGutter = this.restrictMove()\n ? [dragStartContext.areaBeforeGutterIndex]\n : areasIndices\n .slice(0, dragStartContext.areaBeforeGutterIndex + 1)\n .filter((index) => this._areas()[index].visible())\n .reverse()\n const areasIndicesAfterGutter = this.restrictMove()\n ? [dragStartContext.areaAfterGutterIndex]\n : areasIndices.slice(dragStartContext.areaAfterGutterIndex).filter((index) => this._areas()[index].visible())\n // Based on direction we need to decide which areas are expanding and which are shrinking\n const potentialAreasIndicesArrToShrink = isDraggingForward ? areasIndicesAfterGutter : areasIndicesBeforeGutter\n const potentialAreasIndicesArrToExpand = isDraggingForward ? areasIndicesBeforeGutter : areasIndicesAfterGutter\n\n let remainingPixels = absSteppedOffset\n let potentialShrinkArrIndex = 0\n let potentialExpandArrIndex = 0\n\n // We gradually run in both expand and shrink direction transferring pixels from the offset.\n // We stop once no pixels are left or we reached the end of either the expanding areas or the shrinking areas\n while (\n remainingPixels !== 0 &&\n potentialShrinkArrIndex < potentialAreasIndicesArrToShrink.length &&\n potentialExpandArrIndex < potentialAreasIndicesArrToExpand.length\n ) {\n const areaIndexToShrink = potentialAreasIndicesArrToShrink[potentialShrinkArrIndex]\n const areaIndexToExpand = potentialAreasIndicesArrToExpand[potentialExpandArrIndex]\n const areaToShrinkSize = tempAreasPixelSizes[areaIndexToShrink]\n const areaToExpandSize = tempAreasPixelSizes[areaIndexToExpand]\n const areaToShrinkMinSize = dragStartContext.areaIndexToBoundaries[areaIndexToShrink].min\n const areaToExpandMaxSize = dragStartContext.areaIndexToBoundaries[areaIndexToExpand].max\n // We can only transfer pixels based on the shrinking area min size and the expanding area max size\n // to avoid overflow. If any pixels left they will be handled by the next area in the next `while` iteration\n const maxPixelsToShrink = areaToShrinkSize - areaToShrinkMinSize\n const maxPixelsToExpand = areaToExpandMaxSize - areaToExpandSize\n const pixelsToTransfer = Math.min(maxPixelsToShrink, maxPixelsToExpand, remainingPixels)\n\n // Actual pixels transfer\n tempAreasPixelSizes[areaIndexToShrink] -= pixelsToTransfer\n tempAreasPixelSizes[areaIndexToExpand] += pixelsToTransfer\n remainingPixels -= pixelsToTransfer\n\n // Once min threshold reached we need to move to the next area in turn\n if (tempAreasPixelSizes[areaIndexToShrink] === areaToShrinkMinSize) {\n potentialShrinkArrIndex++\n }\n\n // Once max threshold reached we need to move to the next area in turn\n if (tempAreasPixelSizes[areaIndexToExpand] === areaToExpandMaxSize) {\n potentialExpandArrIndex++\n }\n }\n\n this._areas().forEach((area, index) => {\n // No need to update wildcard size\n if (area._internalSize() === '*') {\n return\n }\n\n if (this.unit() === 'pixel') {\n area._internalSize.set(tempAreasPixelSizes[index])\n } else {\n const percentSize = (tempAreasPixelSizes[index] / dragStartContext.totalAreasPixelSize) * 100\n // Fix javascript only working with float numbers which are inaccurate compared to decimals\n area._internalSize.set(parseFloat(percentSize.toFixed(10)))\n }\n })\n\n this.dragProgressSubject.next(this.createDragInteractionEvent(this.draggedGutterIndex()))\n }\n\n private createGridTemplateColumnsStyle(): string {\n const columns: string[] = []\n const sumNonWildcardSizes = sum(this._visibleAreas(), (area) => {\n const size = area._internalSize()\n return size === '*' ? 0 : size\n })\n const visibleAreasCount = this._visibleAreas().length\n\n let visitedVisibleAreas = 0\n\n this._areas().forEach((area, index, areas) => {\n const unit = this.unit()\n const areaSize = area._internalSize()\n\n // Add area size column\n if (!area.visible()) {\n columns.push(unit === 'percent' || areaSize === '*' ? '0fr' : '0px')\n } else {\n if (unit === 'pixel') {\n const columnValue = areaSize === '*' ? '1fr' : `${areaSize}px`\n columns.push(columnValue)\n } else {\n const percentSize = areaSize === '*' ? 100 - sumNonWildcardSizes : areaSize\n const columnValue = `${percentSize}fr`\n columns.push(columnValue)\n }\n\n visitedVisibleAreas++\n }\n\n const isLastArea = index === areas.length - 1\n\n if (isLastArea) {\n return\n }\n\n const remainingVisibleAreas = visibleAreasCount - visitedVisibleAreas\n\n // Only add gutter with size if this area is visible and there are more visible areas after this one\n // to avoid ghost gutters\n if (area.visible() && remainingVisibleAreas > 0) {\n columns.push(`${this.gutterSize()}px`)\n } else {\n columns.push('0px')\n }\n })\n\n return this.direction() === 'horizontal' ? `1fr / ${columns.join(' ')}` : `${columns.join(' ')} / 1fr`\n }\n\n private createAlignedVisibleAreasSize(): SplitAreaSize[] {\n const visibleAreasSizes = this._visibleAreas().map((area): SplitAreaSize => {\n const size = area.size()\n return size === 'auto' ? '*' : size\n })\n const isValid = areAreasValid(this._visibleAreas(), this.unit(), false)\n\n if (isValid) {\n return visibleAreasSizes\n }\n\n const unit = this.unit()\n\n if (unit === 'percent') {\n // Distribute sizes equally\n const defaultPercentSize = 100 / visibleAreasSizes.length\n return visibleAreasSizes.map(() => defaultPercentSize)\n }\n\n if (unit === 'pixel') {\n // Make sure only one wildcard area\n const wildcardAreas = visibleAreasSizes.filter((areaSize) => areaSize === '*')\n\n if (wildcardAreas.length === 0) {\n return ['*', ...visibleAreasSizes.slice(1)]\n } else {\n const firstWildcardIndex = visibleAreasSizes.findIndex((areaSize) => areaSize === '*')\n const defaultPxSize = 100\n\n return visibleAreasSizes.map((areaSize, index) =>\n index === firstWildcardIndex || areaSize !== '*' ? areaSize : defaultPxSize,\n )\n }\n }\n\n return assertUnreachable(unit, 'SplitUnit')\n }\n}\n","<ng-content></ng-content>\n@for (area of _areas(); track area) {\n @if (!$last) {\n <div\n #gutter\n class=\"as-split-gutter\"\n role=\"separator\"\n tabindex=\"0\"\n [attr.aria-label]=\"gutterAriaLabel()\"\n [attr.aria-orientation]=\"direction()\"\n [attr.aria-valuemin]=\"getAriaValue(area.minSize())\"\n [attr.aria-valuemax]=\"getAriaValue(area.maxSize())\"\n [attr.aria-valuenow]=\"getAriaValue(area._internalSize())\"\n [attr.aria-valuetext]=\"getAriaAreaSizeText(area)\"\n [ngStyle]=\"getGutterGridStyle($index + 1)\"\n [class.as-dragged]=\"draggedGutterIndex() === $index\"\n asSplitCustomEventsBehavior\n [asSplitCustomMultiClickThreshold]=\"gutterDblClickDuration()\"\n [asSplitCustomClickDeltaInPx]=\"gutterClickDeltaPx()\"\n (asSplitCustomClick)=\"gutterClicked($index)\"\n (asSplitCustomDblClick)=\"gutterDoubleClicked($index)\"\n (asSplitCustomMouseDown)=\"gutterMouseDown($event, gutter, $index, $index, $index + 1)\"\n (asSplitCustomKeyDown)=\"gutterKeyDown($event, $index, $index, $index + 1)\"\n >\n @if (customGutter()?.template) {\n <ng-container *asSplitGutterDynamicInjector=\"$index + 1; let injector\">\n <ng-container\n *ngTemplateOutlet=\"\n customGutter().template;\n context: {\n areaBefore: area,\n areaAfter: _areas()[$index + 1],\n gutterNum: $index + 1,\n first: $first,\n last: $index === _areas().length - 2,\n isDragged: draggedGutterIndex() === $index\n };\n injector: injector\n \"\n ></ng-container>\n </ng-container>\n } @else {\n <div class=\"as-split-gutter-icon\"></div>\n }\n </div>\n }\n}\n","export type SplitAreaSize = number | '*'\n\nexport type SplitAreaSizeInput = SplitAreaSize | `${number}` | undefined | null\n\nconst internalAreaSizeTransform = (areaSize: SplitAreaSizeInput): SplitAreaSize =>\n areaSize === undefined || areaSize === null || areaSize === '*' ? '*' : +areaSize\n\nexport const areaSizeTransform = (areaSize: SplitAreaSizeInput): SplitAreaSize | 'auto' =>\n internalAreaSizeTransform(areaSize)\n\nexport const boundaryAreaSizeTransform = (areaSize: SplitAreaSizeInput): SplitAreaSize =>\n internalAreaSizeTransform(areaSize)\n\nexport type SplitDirection = 'horizontal' | 'vertical'\n\nexport type SplitDir = 'ltr' | 'rtl'\n\nexport type SplitUnit = 'pixel' | 'percent'\n\nexport interface SplitGutterInteractionEvent {\n gutterNum: number\n sizes: SplitAreaSize[]\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n HostBinding,\n Signal,\n booleanAttribute,\n computed,\n inject,\n input,\n isDevMode,\n} from '@angular/core'\nimport { SPLIT_AREA_CONTRACT, SplitComponent } from '../split/split.component'\nimport { createClassesString, mirrorSignal } from '../utils'\nimport { SplitAreaSize, areaSizeTransform, boundaryAreaSizeTransform } from '../models'\n\n@Component({\n selector: 'as-split-area',\n standalone: true,\n exportAs: 'asSplitArea',\n templateUrl: './split-area.component.html',\n styleUrl: './split-area.component.css',\n providers: [\n {\n provide: SPLIT_AREA_CONTRACT,\n useExisting: SplitAreaComponent,\n },\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SplitAreaComponent {\n protected readonly split = inject(SplitComponent)\n\n readonly size = input('auto', { transform: areaSizeTransform })\n readonly minSize = input('*', { transform: boundaryAreaSizeTransform })\n readonly maxSize = input('*', { transform: boundaryAreaSizeTransform })\n readonly lockSize = input(false, { transform: booleanAttribute })\n readonly visible = input(true, { transform: booleanAttribute })\n\n /**\n * @internal\n */\n readonly _internalSize = mirrorSignal(\n // As size is an input and we can change the size without the outside\n // listening to the change we need an intermediate writeable signal\n computed((): SplitAreaSize => {\n if (!this.visible()) {\n return 0\n }\n\n const visibleIndex = this.split._visibleAreas().findIndex((area) => area === this)\n\n return this.split._alignedVisibleAreasSizes()[visibleIndex]\n }),\n )\n /**\n * @internal\n */\n readonly _normalizedMinSize = computed(() => this.normalizeMinSize())\n /**\n * @internal\n */\n readonly _normalizedMaxSize = computed(() => this.normalizeMaxSize())\n private readonly index = computed(() => this.split._areas().findIndex((area) => area === this))\n private readonly gridAreaNum = computed(() => this.index() * 2 + 1)\n private readonly hostClasses = computed(() =>\n createClassesString({\n ['as-split-area']: true,\n ['as-min']: this.visible() && this._internalSize() === this._normalizedMinSize(),\n ['as-max']: this.visible() && this._internalSize() === this._normalizedMaxSize(),\n ['as-hidden']: !this.visible(),\n }),\n )\n\n @HostBinding('class') protected get hostClassesBinding() {\n return this.hostClasses()\n }\n @HostBinding('style.grid-column') protected get hostGridColumnStyleBinding() {\n return this.split.direction() === 'horizontal' ? `${this.gridAreaNum()} / ${this.gridAreaNum()}` : undefined\n }\n @HostBinding('style.grid-row') protected get hostGridRowStyleBinding() {\n return this.split.direction() === 'vertical' ? `${this.gridAreaNum()} / ${this.gridAreaNum()}` : undefined\n }\n @HostBinding('style.position') protected get hostPositionStyleBinding() {\n return this.split._isDragging() ? 'relative' : undefined\n }\n\n private normalizeMinSize() {\n const defaultMinSize = 0\n\n if (!this.visible()) {\n return defaultMinSize\n }\n\n const minSize = this.normalizeSizeBoundary(this.minSize, defaultMinSize)\n const size = this.size()\n\n if (size !== '*' && size !== 'auto' && size < minSize) {\n if (isDevMode()) {\n console.warn('as-split: size cannot be smaller than minSize')\n }\n\n return defaultMinSize\n }\n\n return minSize\n }\n\n private normalizeMaxSize() {\n const defaultMaxSize = Infinity\n\n if (!this.visible()) {\n return defaultMaxSize\n }\n\n const maxSize = this.normalizeSizeBoundary(this.maxSize, defaultMaxSize)\n const size = this.size()\n\n if (size !== '*' && size !== 'auto' && size > maxSize) {\n if (isDevMode()) {\n console.warn('as-split: size cannot be larger than maxSize')\n }\n\n return defaultMaxSize\n }\n\n return maxSize\n }\n\n private normalizeSizeBoundary(sizeBoundarySignal: Signal<SplitAreaSize>, defaultBoundarySize: number): number {\n const size = this.size()\n const lockSize = this.lockSize()\n const boundarySize = sizeBoundarySignal()\n\n if (lockSize) {\n if (isDevMode() && boundarySize !== '*') {\n console.warn('as-split: lockSize overwrites maxSize/minSize')\n }\n\n if (size ===