import type { BooleanInput, NumberInput } from '@angular/cdk/coercion';
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
contentChild,
forwardRef,
input,
linkedSignal,
numberAttribute,
output,
signal,
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 { HlmCalendarMulti } from '@spartan-ng/styles/calendar';
import { HlmPopoverImports } from '@spartan-ng/styles/popover';
import { injectHlmDatePickerMultiConfig } from './hlm-date-picker-multi.token';
export const HLM_DATE_PICKER_MUTLI_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => HlmDatePickerMulti),
multi: true,
};
@Component({
selector: 'hlm-date-picker-multi',
imports: [HlmPopoverImports, HlmCalendarMulti],
providers: [
HLM_DATE_PICKER_MUTLI_VALUE_ACCESSOR,
provideBrnDatePicker(HlmDatePickerMulti),
provideBrnLabelable(HlmDatePickerMulti),
],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [BrnFieldControl],
host: { class: 'block' },
template: `
`,
})
export class HlmDatePickerMulti implements BrnDatePickerBase, ControlValueAccessor {
private readonly _config = injectHlmDatePickerMultiConfig();
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();
/** The minimum selectable dates. */
public readonly minSelection = input(undefined, {
transform: numberAttribute,
});
/** The maximum selectable dates. */
public readonly maxSelection = input(undefined, {
transform: numberAttribute,
});
/** Determine if the date picker is disabled. */
public readonly disabled = input(false, {
transform: booleanAttribute,
});
/** The selected value. */
public readonly date = input();
protected readonly _mutableDate = linkedSignal(this.date);
/** If true, the date picker will close when the max selection of dates is reached. */
public readonly autoCloseOnMaxSelection = input(
this._config.autoCloseOnMaxSelection,
{
transform: booleanAttribute,
},
);
/** Defines how the date should be displayed in the UI. */
public readonly formatDates = input<(date: T[]) => string>(this._config.formatDates);
/** Defines how the date should be transformed before saving to model/form. */
public readonly transformDates = input<(date: 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 dates = this._mutableDate();
return dates ? this.formatDates()(dates) : undefined;
});
public readonly dateChange = output();
public readonly labelableId = computed(() => this._trigger()?.triggerId());
public readonly hasDate = computed(() => !!this._mutableDate()?.length);
/** @internal The current raw value, used by inputs to reformat on focus. */
public readonly value = computed(() => this._mutableDate() ?? null);
protected _onChange?: ChangeFn;
protected _onTouched?: TouchFn;
protected _onStateChange(state: BrnOverlayState) {
this._popoverState.set(state);
if (state === 'closed') this._onTouched?.();
}
protected _handleChange(value: T[] | undefined) {
if (value === undefined) return;
if (this._disabled()) return;
const transformedDate = value !== undefined ? this.transformDates()(value) : value;
this._mutableDate.set(transformedDate);
this._onChange?.(transformedDate);
this.dateChange.emit(transformedDate);
if (this.autoCloseOnMaxSelection() && this._mutableDate()?.length === this.maxSelection()) {
this._popoverState.set('closed');
}
}
/**
* Commit dates 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 selection.
*/
public updateDate(value: T[] | null) {
if (this._disabled()) return;
const transformedDate = value ? this.transformDates()(value) : undefined;
this._mutableDate.set(transformedDate);
this._onChange?.(transformedDate ?? []);
this.dateChange.emit(transformedDate ?? []);
}
public touched(): void {
this._onTouched?.();
}
/** CONTROL VALUE ACCESSOR */
public writeValue(value: T[] | null): void {
this._mutableDate.set(value ? this.transformDates()(value) : undefined);
}
public registerOnChange(fn: ChangeFn): 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._onChange?.([]);
this.dateChange.emit([]);
}
}