All Downloads are FREE. Search and download functionalities are using the official Maven repository.

custom-fieldpackage.src.vaadin-custom-field-mixin.js Maven / Gradle / Ivy

The newest version!
/**
 * @license
 * Copyright (c) 2019 - 2024 Vaadin Ltd.
 * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
 */
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
import { getFlattenedElements } from '@vaadin/component-base/src/dom-utils.js';
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';

/**
 * Default implementation of the parse function that creates individual field
 * values from the single component value.
 * @param value
 * @returns {*}
 */
const defaultParseValue = (value) => {
  return value.split('\t');
};

/**
 * Default implementation of the format function that creates a single component
 * value from individual field values.
 * @param inputValues
 * @returns {*}
 */
const defaultFormatValue = (inputValues) => {
  return inputValues.join('\t');
};

/**
 * @polymerMixin
 * @mixes FieldMixin
 * @mixes FocusMixin
 * @mixes KeyboardMixin
 */
export const CustomFieldMixin = (superClass) =>
  class CustomFieldMixin extends FieldMixin(FocusMixin(KeyboardMixin(superClass))) {
    static get properties() {
      return {
        /**
         * The name of the control, which is submitted with the form data.
         */
        name: String,

        /**
         * The value of the field. When wrapping several inputs, it will contain `\t`
         * (Tab character) as a delimiter indicating parts intended to be used as the
         * corresponding inputs values.
         * Use the [`formatValue`](#/elements/vaadin-custom-field#property-formatValue)
         * and [`parseValue`](#/elements/vaadin-custom-field#property-parseValue)
         * properties to customize this behavior.
         */
        value: {
          type: String,
          observer: '__valueChanged',
          notify: true,
        },

        /**
         * Array of available input nodes
         * @type {!Array | undefined}
         */
        inputs: {
          type: Array,
          readOnly: true,
          observer: '__inputsChanged',
        },

        /**
         * A function to format the values of the individual fields contained by
         * the custom field into a single component value. The function receives
         * an array of all values of the individual fields in the order of their
         * presence in the DOM, and must return a single component value.
         * This function is called each time a value of an internal field is
         * changed.
         *
         * Example:
         * ```js
         * customField.formatValue = (fieldValues) => {
         *   return fieldValues.join("-");
         * }
         * ```
         * @type {!CustomFieldFormatValueFn | undefined}
         */
        formatValue: {
          type: Function,
        },

        /**
         * A function to parse the component value into values for the individual
         * fields contained by the custom field. The function receives the
         * component value, and must return an array of values for the individual
         * fields in the order of their presence in the DOM.
         * The function is called each time the value of the component changes.
         *
         * Example:
         * ```js
         * customField.parseValue = (componentValue) => {
         *   return componentValue.split("-");
         * }
         * ```
         * @type {!CustomFieldParseValueFn | undefined}
         */
        parseValue: {
          type: Function,
        },
      };
    }

    /** @protected */
    ready() {
      super.ready();

      // See https://github.com/vaadin/vaadin-web-components/issues/94
      this.setAttribute('role', 'group');

      this.ariaTarget = this;

      this.__childrenObserver = new MutationObserver(() => {
        this.__setInputsFromSlot();
      });

      this.__setInputsFromSlot();
      this.$.slot.addEventListener('slotchange', () => {
        this.__setInputsFromSlot();

        // Observe changes to any children except inputs
        // to allow wrapping `` with `
` etc. getFlattenedElements(this.$.slot) .filter((el) => !this.__isInput(el)) .forEach((el) => { this.__childrenObserver.observe(el, { childList: true }); }); }); this._tooltipController = new TooltipController(this); this.addController(this._tooltipController); this._tooltipController.setShouldShow((target) => { const inputs = target.inputs || []; return !inputs.some((el) => el.opened); }); } /** @protected */ focus() { if (this.inputs && this.inputs[0]) { this.inputs[0].focus(); } } /** * Override method inherited from `FocusMixin` to validate on blur. * @param {boolean} focused * @protected */ _setFocused(focused) { super._setFocused(focused); if (!focused) { this.validate(); } } /** * Override method inherited from `FocusMixin` to not remove focused * state when focus moves to another input in the custom field. * @param {FocusEvent} event * @return {boolean} * @protected */ _shouldRemoveFocus(event) { const { relatedTarget } = event; return !this.inputs || !this.inputs.some((el) => relatedTarget === (el.focusElement || el)); } /** * Returns true if the current inputs values satisfy all constraints (if any). * * @return {boolean} */ checkValidity() { const hasInvalidFields = this.inputs && this.inputs.some((input) => !(input.validate || input.checkValidity).call(input)); if (hasInvalidFields || (this.required && !(this.value && this.value.trim()))) { // Either 1. one of the input fields is invalid or // 2. the custom field itself is required but doesn't have a value return false; } return true; } /** * Override an observer from `FieldMixin` * to validate when required is removed. * * @protected * @override */ _requiredChanged(required) { super._requiredChanged(required); if (required === false) { this.validate(); } } /** * @param {KeyboardEvent} e * @protected * @override */ _onKeyDown(e) { if (e.key === 'Tab') { const inputs = this.inputs || []; if ( (inputs.indexOf(e.target) < inputs.length - 1 && !e.shiftKey) || (inputs.indexOf(e.target) > 0 && e.shiftKey) ) { this.dispatchEvent(new CustomEvent('internal-tab')); } else { // FIXME(yuriy): remove this workaround when value should not be updated before focusout this.__setValue(); } } } /** @protected */ _onInputChange(event) { // Stop native change events event.stopPropagation(); this.__setValue(); this.validate(); this.dispatchEvent( new CustomEvent('change', { bubbles: true, cancelable: false, detail: { value: this.value, }, }), ); } /** @private */ __setValue() { this.__settingValue = true; const formatFn = this.formatValue || defaultFormatValue; this.value = formatFn.apply(this, [this.inputs.map((input) => input.value)]); this.__settingValue = false; } /** @private */ __isInput(node) { const isSlottedInput = node.getAttribute('slot') === 'input' || node.getAttribute('slot') === 'textarea'; return !isSlottedInput && (node.validate || node.checkValidity); } /** @private */ __getInputsFromSlot() { return getFlattenedElements(this.$.slot).filter((node) => this.__isInput(node)); } /** @private */ __setInputsFromSlot() { this._setInputs(this.__getInputsFromSlot()); } /** @private */ __inputsChanged(inputs, oldInputs) { if (inputs.length === 0) { return; } // When inputs are first initialized, apply value set with property. if (this.value && this.value !== '\t' && (!oldInputs || oldInputs.length === 0)) { this.__applyInputsValue(this.value); } else { this.__setValue(); } } /** @private */ __toggleHasValue(value) { this.toggleAttribute('has-value', value !== null && value.trim() !== ''); } /** @private */ __valueChanged(value, oldValue) { this.__toggleHasValue(value); if (this.__settingValue || !this.inputs) { return; } this.__applyInputsValue(value || '\t'); if (oldValue !== undefined) { this.validate(); } } /** @private */ __applyInputsValue(value) { const parseFn = this.parseValue || defaultParseValue; const valuesArray = parseFn.apply(this, [value]); if (!valuesArray || valuesArray.length === 0) { console.warn('Value parser has not provided values array'); return; } this.inputs.forEach((input, idx) => { input.value = valuesArray[idx]; }); } };




© 2015 - 2024 Weber Informatics LLC | Privacy Policy