/* eslint-disable import/no-extraneous-dependencies */ import { LionCalendar } from '@lion/ui/calendar.js'; import { ScopedElementsMixin } from '@open-wc/scoped-elements'; import { html, css } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; import { LionInputDate } from '@lion/ui/input-date.js'; import { OverlayMixin, withBottomSheetConfig, withModalDialogConfig, ArrowMixin, } from '@lion/ui/overlays.js'; import { LocalizeMixin } from '@lion/ui/localize-no-side-effects.js'; import { localizeNamespaceLoader } from './localizeNamespaceLoader.js'; /** * @typedef {import('../../form-core/src/validate/Validator.js').Validator} Validator * @typedef {import('lit').RenderOptions} RenderOptions */ /** * @customElement lion-input-datepicker */ export class LionInputDatepicker extends ScopedElementsMixin( ArrowMixin(OverlayMixin(LocalizeMixin(LionInputDate))), ) { static get scopedElements() { return { ...super.scopedElements, 'lion-calendar': LionCalendar, }; } static get styles() { return [ ...super.styles, css` .calendar__overlay-frame { display: inline-block; background: white; position: relative; } .calendar-overlay__header { display: flex; } .calendar-overlay__heading { padding: 16px 16px 8px; flex: 1; } .calendar-overlay__heading > .calendar-overlay__close-button { flex: none; } .calendar-overlay__close-button { min-width: 40px; min-height: 32px; border-width: 0; padding: 0; font-size: 24px; } `, ]; } /** @type {any} */ static get properties() { return { /** * The heading to be added on top of the calendar overlay. * Naming chosen from an Application Developer perspective. * For a Subclasser 'calendarOverlayHeading' would be more appropriate. */ calendarHeading: { type: String, attribute: 'calendar-heading', }, /** * The slot to put the invoker button in. Can be 'prefix', 'suffix', 'before' and 'after'. * Default will be 'suffix'. */ _calendarInvokerSlot: { attribute: false, }, __calendarMinDate: { attribute: false, }, __calendarMaxDate: { attribute: false, }, __calendarDisableDates: { attribute: false, }, }; } get slots() { return { ...super.slots, [this._calendarInvokerSlot]: () => this._invokerTemplate(), }; } static get localizeNamespaces() { return [{ 'lion-input-datepicker': localizeNamespaceLoader }, ...super.localizeNamespaces]; } /** * @protected */ get _invokerNode() { return /** @type {HTMLElement} */ (this.querySelector(`#${this.__invokerId}`)); } /** * @type {LionCalendar} * @protected */ get _calendarNode() { return /** @type {LionCalendar} */ ( this._overlayCtrl.contentNode.querySelector('[slot="content"]') ); } constructor() { super(); /** @private */ this.__invokerId = this.__createUniqueIdForA11y(); /** @protected */ this._calendarInvokerSlot = 'suffix'; // Configuration flags for subclassers /** @protected */ this._focusCentralDateOnCalendarOpen = true; /** @protected */ this._hideOnUserSelect = true; /** @protected */ this._syncOnUserSelect = true; /** @protected */ this._isHandlingCalendarUserInput = false; /** @private */ this.__openCalendarOverlay = this.__openCalendarOverlay.bind(this); /** @protected */ this._onCalendarUserSelectedChanged = this._onCalendarUserSelectedChanged.bind(this); } /** @private */ __createUniqueIdForA11y() { return `${this.localName}-${Math.random().toString(36).substr(2, 10)}`; } /** * @param {string} [name] * @param {unknown} [oldValue] * @param {import('lit').PropertyDeclaration} [options] * @returns {void} */ requestUpdate(name, oldValue, options) { super.requestUpdate(name, oldValue, options); if (name === 'disabled' || name === 'readOnly') { this.__toggleInvokerDisabled(); } } /** @private */ __toggleInvokerDisabled() { if (this._invokerNode) { const invokerNode = /** @type {HTMLElement & {disabled: boolean}} */ (this._invokerNode); invokerNode.disabled = this.disabled || this.readOnly; } } /** @param {import('lit').PropertyValues } changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.__toggleInvokerDisabled(); } /** @param {import('lit').PropertyValues } changedProperties */ updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('validators')) { const validators = [...(this.validators || [])]; this.__syncDisabledDates(validators); } if (changedProperties.has('label')) { this.calendarHeading = this.calendarHeading || this.label; } } /** * Defining this overlay as a templates from OverlayMixin * this is our source to give as .contentNode to OverlayController. * Important: do not change the name of this method. * @protected */ _overlayTemplate() { // TODO: add performance optimization to only render the calendar if needed // When not opened (usually on init), it does not need to be rendered. // This would make first paint quicker return html`
${this._overlayFrameTemplate()} ${this._arrowNodeTemplate()}
`; } _overlayFrameTemplate() { return html`

${this.calendarHeading}

${this._calendarTemplate()}
`; } render() { return html`
${this._groupOneTemplate()}
${this._groupTwoTemplate()} ${this._overlayTemplate()}
`; } /** * Subclassers can replace this with their custom extension of * LionCalendar, like `` */ // eslint-disable-next-line class-methods-use-this _calendarTemplate() { return html` `; } /** * Subclassers can replace this with their custom extension invoker, * like `` */ // eslint-disable-next-line class-methods-use-this _invokerTemplate() { return html` `; } _setupOverlayCtrl() { super._setupOverlayCtrl(); this.__datepickerBeforeShow = () => { this._overlayCtrl.updateConfig(this._defineOverlayConfig()); }; this._overlayCtrl.addEventListener('before-show', this.__datepickerBeforeShow); } /** * @override Configures OverlayMixin * @desc overrides default configuration options for this component * @returns {Object} */ // eslint-disable-next-line class-methods-use-this _defineOverlayConfig() { if (window.innerWidth >= 600) { this.hasArrow = true; return { ...withModalDialogConfig(), hidesOnOutsideClick: true, visibilityTriggerFunction: undefined, ...super._defineOverlayConfig(), popperConfig: { ...super._defineOverlayConfig().popperConfig, placement: 'bottom', }, }; } this.hasArrow = false; return withBottomSheetConfig(); } async __openCalendarOverlay() { await this._overlayCtrl.show(); await Promise.all([ /** @type {import('lit').LitElement} */ (this._overlayCtrl.contentNode).updateComplete, this._calendarNode.updateComplete, ]); this._onCalendarOverlayOpened(); } /** * Lifecycle callback for subclassers * @overridable */ _onCalendarOverlayOpened() { if (this._focusCentralDateOnCalendarOpen) { this._calendarNode.initCentralDate(); } } /** * @param {{ target: { selectedDate: Date }}} opts */ _onCalendarUserSelectedChanged({ target: { selectedDate } }) { if (this._hideOnUserSelect) { this._overlayCtrl.hide(); } if (this._syncOnUserSelect) { // Synchronize new selectedDate value to input this._isHandlingUserInput = true; this._isHandlingCalendarUserInput = true; this.modelValue = selectedDate; this._isHandlingUserInput = false; this._isHandlingCalendarUserInput = false; } } /** * @enhance FormatMixin: sync to view value after handling calendar user input * @protected */ _reflectBackOn() { return super._reflectBackOn() || this._isHandlingCalendarUserInput; } /** * The LionCalendar shouldn't know anything about the modelValue; * it can't handle Unparseable dates, but does handle 'undefined' * @param {?} modelValue * @returns {Date|undefined} a 'guarded' modelValue */ static __getSyncDownValue(modelValue) { return modelValue instanceof Date ? modelValue : undefined; } /** * Validators contain the information to synchronize the input with * the min, max and enabled dates of the calendar. * @param {Validator[]} validators - errorValidators or warningValidators array */ __syncDisabledDates(validators) { // On every validator change, synchronize disabled dates: this means // we need to extract minDate, maxDate, minMaxDate and disabledDates validators validators.forEach(v => { const vctor = /** @type {typeof import('@lion/ui/form-core.js').Validator} */ (v.constructor); if (vctor.validatorName === 'MinDate') { this.__calendarMinDate = v.param; } else if (vctor.validatorName === 'MaxDate') { this.__calendarMaxDate = v.param; } else if (vctor.validatorName === 'MinMaxDate') { this.__calendarMinDate = v.param.min; this.__calendarMaxDate = v.param.max; } else if (vctor.validatorName === 'IsDateDisabled') { this.__calendarDisableDates = v.param; } }); } /** * Responsible for listening param change event and * sync the calendar dates with the updated validator params * @param {Event|CustomEvent} e * @param {{validator: Validator}} metaData * @protected */ // @ts-ignore Binding element 'any' implicitly has an 'any' type. _onValidatorUpdated(e, metaData) { // @ts-ignore super._onValidatorUpdated(e, metaData); if (e.type === 'param-changed') { this.__syncDisabledDates([metaData.validator]); } } /** * @override Configures OverlayMixin */ get _overlayInvokerNode() { return this._invokerNode; } /** * @override Configures OverlayMixin */ get _overlayContentNode() { if (this._cachedOverlayContentNode) { return this._cachedOverlayContentNode; } this._cachedOverlayContentNode = /** @type {HTMLElement} */ ( /** @type {ShadowRoot} */ (this.shadowRoot).querySelector('.calendar__overlay-frame') ); return this._cachedOverlayContentNode; } }