import { DelegateMixin, SlotMixin } from '@lion/core'; import { LionLitElement } from '@lion/core/src/LionLitElement.js'; import { ElementMixin } from '@lion/core/src/ElementMixin.js'; import { CssClassMixin } from '@lion/core/src/CssClassMixin.js'; import { ObserverMixin } from '@lion/core/src/ObserverMixin.js'; import { ValidateMixin } from '@lion/validate'; import { FormControlMixin } from './FormControlMixin.js'; import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies FocusMixin import { FormatMixin } from './FormatMixin.js'; import { FocusMixin } from './FocusMixin.js'; /** * LionField: wraps components input, textarea and select and potentially others * (checkbox group, radio group) * Also it would follow a nice hierarchy: lion-form -> lion-fieldset -> lion-field * * * * * * * Note: We do not support placeholders, because we have a helper text and * placeholders confuse the user with accessibility needs. * * @customElement */ // TODO: Consider exporting as FieldMixin // eslint-disable-next-line max-len, no-unused-vars export class LionField extends FormControlMixin( InteractionStateMixin( FocusMixin( FormatMixin( ValidateMixin( CssClassMixin(ElementMixin(DelegateMixin(SlotMixin(ObserverMixin(LionLitElement))))), ), ), ), ), ) { get delegations() { return { ...super.delegations, target: () => this.inputElement, properties: [ ...super.delegations.properties, 'name', 'type', 'disabled', 'selectionStart', 'selectionEnd', ], attributes: [...super.delegations.attributes, 'name', 'type', 'disabled'], }; } static get properties() { return { ...super.properties, submitted: { // make sure validation can be triggered based on observer type: Boolean, }, }; } static get asyncObservers() { return { ...super.asyncObservers, _setDisabledClass: ['disabled'], }; } // We don't delegate, because we want to 'preprocess' via _setValueAndPreserveCaret set value(value) { // if not yet connected to dom can't change the value if (this.inputElement) { this._setValueAndPreserveCaret(value); } this._onValueChanged({ value }); } get value() { return (this.inputElement && this.inputElement.value) || ''; } _setDisabledClass() { this.classList[this.disabled ? 'add' : 'remove']('state-disabled'); } resetInteractionState() { if (super.resetInteractionState) super.resetInteractionState(); // TODO: add submitted prop to InteractionStateMixin ? this.submitted = false; } /* * * * * * * * Lifecycle */ connectedCallback() { super.connectedCallback(); this._onChange = this._onChange.bind(this); this.inputElement.addEventListener('change', this._onChange); this._delegateInitialValueAttr(); // TODO: find a better way to do this this._setDisabledClass(); this.classList.add('form-field'); } disconnectedCallback() { super.disconnectedCallback(); if (this.__parentFormGroup) { const event = new CustomEvent('form-element-unregister', { detail: { element: this }, bubbles: true, }); this.__parentFormGroup.dispatchEvent(event); } this.inputElement.removeEventListener('change', this._onChange); } /** * This is not done via 'get delegations', because this.inputElement.setAttribute('value') * does not trigger a value change */ _delegateInitialValueAttr() { const valueAttr = this.getAttribute('value'); if (valueAttr !== null) { this.value = valueAttr; } } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Public Methods (also notice delegated methods that are available on host) */ clear() { // Let validationMixin and interactionStateMixin clear their invalid and dirty/touched states // respectively if (super.clear) super.clear(); this.value = ''; // can't set null here, because IE11 treats it as a string } /* * * * * * * * * * Event Handlers */ _onChange() { if (super._onChange) super._onChange(); this.dispatchEvent( new CustomEvent('user-input-changed', { bubbles: true, }), ); this.modelValue = this.parser(this.value); } /* * * * * * * * * * * * Observer Handlers */ _onValueChanged({ value }) { if (super._onValueChanged) super._onValueChanged(); // For styling purposes, make it known the input field is not empty this.classList[value ? 'add' : 'remove']('state-filled'); } /** * Copied from Polymer team. TODO: add license * Restores the cursor to its original position after updating the value. * @param {string} newValue The value that should be saved. */ _setValueAndPreserveCaret(newValue) { // Only preserve caret if focused (changing selectionStart will move focus in Safari) if (this.focused) { // Not all elements might have selection, and even if they have the // right properties, accessing them might throw an exception (like for // ) try { const start = this.inputElement.selectionStart; this.inputElement.value = newValue; // The cursor automatically jumps to the end after re-setting the value, // so restore it to its original position. this.inputElement.selectionStart = start; this.inputElement.selectionEnd = start; } catch (error) { // Just set the value and give up on the caret. this.inputElement.value = newValue; } } else { this.inputElement.value = newValue; } } // eslint-disable-next-line class-methods-use-this __isRequired(modelValue) { return { required: (typeof modelValue === 'string' && modelValue !== '') || (typeof modelValue !== 'string' && typeof modelValue !== 'undefined'), // TODO: && modelValue !== null ? }; } }