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 ?
};
}
}