import type AirDatepickerCtor from 'air-datepicker';
import type { AirDatepickerOptions } from 'air-datepicker';
import { Component, BaseOptions, InitElements, MElement } from '../../src/component';
import { loadPeer } from '../../src/peer-loader';
export interface AirDatepickerFieldOptions extends BaseOptions {
locale?: unknown;
}
const _defaults: AirDatepickerFieldOptions = {};
let activeAirDatepickerField: AirDatepickerField | undefined;
// @implement /Users/kzarshenas/Sites/CrazyProject/CrazyPHP/src/Front/Library/Utility/Form/Date.ts
// Enhances ``
// with the air-datepicker calendar/range picker - deliberately separate from
// kmaterialize's own native `components/datepicker/` (a different, already
// existing class-based component); this one is opted into per-input via the
// `data-date-picker` attribute so it never collides with `.datepicker`
// elements. The `multiple` attribute switches the picker into range mode,
// `data-date-format`/`data-date-lang` (only "fr-FR" ported so far) mirror
// CrazyPHP's own dataset convention.
//
// air-datepicker is an optional peerDependency, loaded on demand via
// peer-loader. Its own base stylesheet (air-datepicker/air-datepicker.css)
// must be imported separately by the consumer - see the docs page.
export class AirDatepickerField extends Component {
declare el: HTMLInputElement;
picker: AirDatepickerCtor | undefined;
ready: Promise;
private _triggers: HTMLElement[];
private _handleOutsideClick = (e: MouseEvent) => {
const calendarEl = document.querySelector('.air-datepicker');
const clickedTrigger = this._triggers.some(trigger => trigger.contains(e.target as Node));
if (
calendarEl instanceof HTMLElement &&
!calendarEl.contains(e.target as Node) &&
!this.el.contains(e.target as Node) &&
!clickedTrigger
) {
this.picker?.hide();
}
};
constructor(el: HTMLInputElement, options: Partial) {
super(el, options, AirDatepickerField);
(this.el as any).M_AirDatepickerField = this;
this.options = {
...AirDatepickerField.defaults,
...options
};
const labels = Array.from(document.querySelectorAll('label')).filter(
label => label.htmlFor === this.el.id
);
const icons = Array.from(
this.el.parentElement?.querySelectorAll('.prefix, .suffix') ?? []
);
this._triggers = [...labels, ...icons];
this._triggers.forEach(trigger => trigger.addEventListener('click', this._handleTriggerClick));
this.ready = this._setup();
}
static get defaults(): AirDatepickerFieldOptions {
return _defaults;
}
static init(el: HTMLInputElement, options?: Partial): AirDatepickerField;
static init(
els: InitElements,
options?: Partial
): AirDatepickerField[];
static init(
els: HTMLInputElement | InitElements,
options: Partial = {}
): AirDatepickerField | AirDatepickerField[] {
return super.init(els, options, AirDatepickerField);
}
static getInstance(el: HTMLInputElement): AirDatepickerField {
return (el as any).M_AirDatepickerField;
}
destroy() {
document.removeEventListener('click', this._handleOutsideClick);
this._triggers.forEach(trigger => trigger.removeEventListener('click', this._handleTriggerClick));
this.picker?.destroy();
if (activeAirDatepickerField === this) activeAirDatepickerField = undefined;
(this.el as any).M_AirDatepickerField = undefined;
}
private _handleTriggerClick = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
void this.ready.then(() => this.picker?.show());
};
async _setup(): Promise {
const AirDatepicker = await loadPeer(
{
specifier: 'air-datepicker',
globalName: 'AirDatepicker',
feature: 'Date input (air-datepicker) enhancement',
cdnHint:
'\n' +
' \n' +
' (self-hosted - copy from node_modules/air-datepicker/, or a CDN of your choice)'
},
() => import('air-datepicker')
);
const dataset = this.el.dataset;
const builtInLocale = dataset.dateLang === 'fr-FR'
? (await import('air-datepicker/locale/fr.js')).default
: (await import('air-datepicker/locale/en.js')).default;
const pickerOptions: AirDatepickerOptions = {
dateFormat: dataset.dateFormat || 'yyyy-MM-dd',
autoClose: dataset.dateAutoClose !== 'false',
locale: (this.options.locale ?? builtInLocale) as AirDatepickerOptions['locale'],
onShow: isAnimationComplete => {
if (isAnimationComplete) return;
if (activeAirDatepickerField && activeAirDatepickerField !== this) {
activeAirDatepickerField.picker?.hide();
}
activeAirDatepickerField = this;
},
onHide: isAnimationComplete => {
if (isAnimationComplete && activeAirDatepickerField === this) {
activeAirDatepickerField = undefined;
}
}
};
let multiple = false;
if (this.el.multiple || dataset.dateRange === 'true') {
pickerOptions.range = true;
pickerOptions.multipleDatesSeparator = ' - ';
multiple = true;
}
const initialValue = this.el.getAttribute('value')?.trim();
if (initialValue) {
pickerOptions.selectedDates = multiple
? initialValue.split(' - ').map(value => value.trim())
: [initialValue];
}
if (dataset.dateTimepicker === 'true') {
pickerOptions.timepicker = true;
if (dataset.dateTimeFormat) pickerOptions.timeFormat = dataset.dateTimeFormat;
}
const supportedViews = ['days', 'months', 'years'];
if (supportedViews.includes(dataset.dateView ?? '')) {
pickerOptions.view = dataset.dateView as AirDatepickerOptions['view'];
}
if (supportedViews.includes(dataset.dateMinView ?? '')) {
pickerOptions.minView = dataset.dateMinView as AirDatepickerOptions['minView'];
}
if (dataset.dateMobile === 'true') pickerOptions.isMobile = true;
if (dataset.datePosition) {
pickerOptions.position = dataset.datePosition as AirDatepickerOptions['position'];
}
if (dataset.dateButtons) {
const supportedButtons = ['today', 'clear'];
pickerOptions.buttons = dataset.dateButtons
.split(',')
.map(button => button.trim())
.filter(button => supportedButtons.includes(button)) as AirDatepickerOptions['buttons'];
}
if (this.el.hasAttribute('min') && this.el.getAttribute('min')) {
pickerOptions.minDate = new Date(this.el.getAttribute('min') as string);
}
if (this.el.hasAttribute('max') && this.el.getAttribute('max')) {
pickerOptions.maxDate = new Date(this.el.getAttribute('max') as string);
}
if (!this.el.required && !pickerOptions.buttons) {
pickerOptions.buttons = ['clear'];
}
pickerOptions.onSelect = ({ date }: any) => {
if (multiple && Array.isArray(date) && date.length === 1) return;
this.el.dispatchEvent(new Event('input', { bubbles: true }));
this.el.dispatchEvent(new Event('change', { bubbles: true }));
};
this.picker = new AirDatepicker(this.el, pickerOptions);
document.addEventListener('click', this._handleOutsideClick);
}
}