import type { BooleanInput } from '@angular/cdk/coercion'; import { booleanAttribute, ChangeDetectionStrategy, Component, computed, contentChild, forwardRef, input, linkedSignal, output, signal, untracked, viewChild, } from '@angular/core'; import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { type BrnDatePickerBase, BrnDatePickerTriggerToken, provideBrnDatePicker, } from '@spartan-ng/brain/date-picker'; import { BrnFieldControl, provideBrnLabelable } from '@spartan-ng/brain/field'; import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms'; import type { BrnOverlayState } from '@spartan-ng/brain/overlay'; import { BrnPopover, type BrnPopoverAlign } from '@spartan-ng/brain/popover'; import { HlmCalendarRange } from '@spartan-ng/styles/calendar'; import { HlmPopoverImports } from '@spartan-ng/styles/popover'; import { injectHlmDateRangePickerConfig } from './hlm-date-range-picker.token'; export const HLM_DATE_RANGE_PICKER_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HlmDateRangePicker), multi: true, }; @Component({ selector: 'hlm-date-range-picker', imports: [HlmPopoverImports, HlmCalendarRange], providers: [ HLM_DATE_RANGE_PICKER_VALUE_ACCESSOR, provideBrnDatePicker(HlmDateRangePicker), provideBrnLabelable(HlmDateRangePicker), ], changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [BrnFieldControl], host: { class: 'block' }, template: ` `, }) export class HlmDateRangePicker implements BrnDatePickerBase<[T, T]>, ControlValueAccessor { private readonly _config = injectHlmDateRangePickerConfig(); public readonly popover = viewChild.required(BrnPopover); private readonly _trigger = contentChild(BrnDatePickerTriggerToken); public readonly align = input('center'); /** Show dropdowns to navigate between months or years. */ public readonly captionLayout = input< 'dropdown' | 'label' | 'dropdown-months' | 'dropdown-years' >('label'); /** The minimum date that can be selected.*/ public readonly minDate = input(); /** The maximum date that can be selected. */ public readonly maxDate = input(); /** Determine if the date picker is disabled. */ public readonly disabled = input(false, { transform: booleanAttribute, }); /** The selected value. */ public readonly date = input<[T, T]>(); protected readonly _mutableDate = linkedSignal(this.date); protected readonly _start = linkedSignal(() => this._mutableDate()?.[0]); protected readonly _end = linkedSignal(() => this._mutableDate()?.[1]); /** If true, the date picker will close when the end date is selected */ public readonly autoCloseOnEndSelection = input( this._config.autoCloseOnEndSelection, { transform: booleanAttribute, }, ); /** Defines how the date should be displayed in the UI. */ public readonly formatDates = input<(dates: [T | null, T | null]) => string>( this._config.formatDates, ); /** Defines how the date should be transformed before saving to model/form. */ public readonly transformDates = input<(date: [T, T]) => [T, T]>(this._config.transformDates); protected readonly _popoverState = signal(null); protected readonly _disabled = linkedSignal(this.disabled); /** @internal The disabled state as a readonly signal */ public readonly disabledState = this._disabled.asReadonly(); public readonly formattedDate = computed(() => { const start = this._start(); const end = this._end(); return start || end ? this.formatDates()([start ?? null, end ?? null]) : undefined; }); public readonly dateChange = output<[T, T] | null>(); public readonly labelableId = computed(() => this._trigger()?.triggerId()); public readonly hasDate = computed(() => !!this._start() || !!this._end()); /** @internal The current raw value, used by inputs to reformat on focus. */ public readonly value = computed(() => this._mutableDate() ?? null); protected _onChange?: ChangeFn<[T, T] | null>; protected _onTouched?: TouchFn; protected _onStateChange(state: BrnOverlayState) { this._popoverState.set(state); if (state === 'closed') { this._onClose(); this._onTouched?.(); } } protected _handleStartDayChange(value: T | undefined) { this._start.set(value); } protected _handleEndDateChange(value: T | undefined): void { this._end.set(value); if (this._disabled()) return; const start = this._start(); if (start && value) { const transformedDates = this.transformDates()([start, value]); this._mutableDate.set(transformedDates); this.dateChange.emit(transformedDates); this._onChange?.(transformedDates); if (this.autoCloseOnEndSelection()) { this._popoverState.set('closed'); } } } /** * Commit a range to the picker. Updates the internal model, notifies form * controls, and emits `dateChange`. Intended to be called from a text input * that parses user-entered values. Pass `null` to clear the range. */ public updateDate(value: [T, T] | null) { if (this._disabled()) return; if (!value) { this._mutableDate.set(undefined); this._start.set(undefined); this._end.set(undefined); this._onChange?.(null); this.dateChange.emit(null); return; } const transformedDates = this.transformDates()(value); this._mutableDate.set(transformedDates); this._start.set(transformedDates[0]); this._end.set(transformedDates[1]); this._onChange?.(transformedDates); this.dateChange.emit(transformedDates); } public touched(): void { this._onTouched?.(); } /** CONTROL VALUE ACCESSOR */ public writeValue(value: [T, T] | null): void { untracked(() => { if (!value) { this._mutableDate.set(undefined); } else { this._mutableDate.set(this.transformDates()(value)); } }); } public registerOnChange(fn: ChangeFn<[T, T] | null>): void { this._onChange = fn; } public registerOnTouched(fn: TouchFn): void { this._onTouched = fn; } public setDisabledState(isDisabled: boolean): void { this._disabled.set(isDisabled); } public open() { this._popoverState.set('open'); } public close() { this._popoverState.set('closed'); } public reset() { this._mutableDate.set(undefined); this._start.set(undefined); this._end.set(undefined); this._onChange?.(null); this.dateChange.emit(null); } protected _onClose(): void { const dates = this._mutableDate(); if (this._start() && !this._end() && dates) { this._start.set(dates[0]); this._end.set(dates[1]); } } }