import isObject from 'lodash/isObject';
import uniqueId from 'lodash/uniqueId';
import isBoolean from 'lodash/isBoolean';
import toInteger from 'lodash/toInteger';
import toString from 'lodash/toString';
import { toFloat } from '../../../../utils/number_utils';
import { stopEvent, isVisible } from '../../../../utils/utils';
import { formInputWidths } from '../../../../utils/constants';
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';

// Valid supported input types
const TYPES = ['text', 'password', 'email', 'number', 'url', 'tel', 'search', 'range', 'color', 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'];
const MODEL_PROP = 'value';
const MODEL_EVENT = 'input';
var script = {
  name: 'GlFormInput',
  model: {
    prop: MODEL_PROP,
    event: MODEL_EVENT
  },
  props: {
    /**
     * The current value of the input. Result will always be a string, except when the `number` prop is used
     */
    value: {
      type: [Number, String],
      required: false,
      default: ''
    },
    /**
     * The type of input to render.
     */
    type: {
      type: String,
      required: false,
      default: 'text',
      validator: value => TYPES.includes(value)
    },
    /**
     * Maximum width of the input
     */
    width: {
      type: [String, Object],
      required: false,
      default: null,
      validator: value => {
        const widths = isObject(value) ? Object.values(value) : [value];
        return widths.every(width => Object.values(formInputWidths).includes(width));
      }
    },
    /**
     * Used to set the `id` attribute on the rendered content, and used as the base to generate any additional element IDs as needed
     */
    id: {
      type: String,
      required: false,
      default: undefined
    },
    /**
     * When set to `true`, attempts to auto-focus the control when it is mounted, or re-activated when in a keep-alive. Does not set the `autofocus` attribute on the control
     */
    autofocus: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * When set to `true`, disables the component's functionality and places it in a disabled state
     */
    disabled: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * ID of the form that the form control belongs to. Sets the `form` attribute on the control
     */
    form: {
      type: String,
      required: false,
      default: undefined
    },
    /**
     * Sets the value of the `name` attribute on the form control
     */
    name: {
      type: String,
      required: false,
      default: undefined
    },
    /**
     * Adds the `required` attribute to the form control
     */
    required: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * Controls the validation state appearance of the component. `true` for valid, `false` for invalid, or `null` for no validation state
     */
    state: {
      type: Boolean,
      required: false,
      default: null
    },
    /**
     * Sets the `placeholder` attribute value on the form control
     */
    placeholder: {
      type: String,
      required: false,
      default: undefined
    },
    /**
     * Optional value to set for the 'aria-invalid' attribute. Supported values are 'true' and 'false'. If not set, the 'state' prop will dictate the value
     */
    ariaInvalid: {
      type: [Boolean, String],
      required: false,
      default: false
    },
    /**
     * Sets the 'autocomplete' attribute value on the form control
     */
    autocomplete: {
      type: String,
      required: false,
      default: undefined
    },
    /**
     * When set to a number of milliseconds greater than zero, will debounce the user input. Has no effect if prop 'lazy' is set
     */
    debounce: {
      type: [Number, String],
      required: false,
      default: undefined
    },
    /**
     * Reference to a function for formatting the input
     */
    formatter: {
      type: Function,
      required: false,
      default: undefined
    },
    /**
     * When set, updates the v-model on 'change'/'blur' events instead of 'input'. Emulates the Vue '.lazy' v-model modifier
     */
    lazy: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * When set, the input is formatted on blur instead of each keystroke (if there is a formatter specified)
     */
    lazyFormatter: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * When set attempts to convert the input value to a native number. Emulates the Vue '.number' v-model modifier
     */
    number: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * Set the form control as readonly and renders the control to look like plain text (no borders)
     */
    plaintext: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * Sets the `readonly` attribute on the form control
     */
    readonly: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * When set, trims any leading and trailing white space from the input value. Emulates the Vue '.trim' v-model modifier
     */
    trim: {
      type: Boolean,
      required: false,
      default: false
    },
    /**
     * The ID of the associated datalist element or component
     */
    list: {
      type: String,
      required: false,
      default: undefined
    },
    /**
     * Value to set in the 'max' attribute on the input. Used by number-like inputs
     */
    max: {
      type: [Number, String],
      required: false,
      default: undefined
    },
    /**
     * Value to set in the 'min' attribute on the input. Used by number-like inputs
     */
    min: {
      type: [Number, String],
      required: false,
      default: undefined
    },
    /**
     * Value to set in the 'step' attribute on the input. Used by number-like inputs
     */
    step: {
      type: [Number, String],
      required: false,
      default: undefined
    }
  },
  data() {
    return {
      localValue: toString(this.value),
      vModelValue: this.modifyValue(this.value),
      localId: null
    };
  },
  computed: {
    computedId() {
      return this.id || this.localId;
    },
    localType() {
      // We only allow certain types
      const {
        type
      } = this;
      return TYPES.includes(type) ? type : 'text';
    },
    computedAriaInvalid() {
      const {
        ariaInvalid
      } = this;
      if (ariaInvalid === true || ariaInvalid === 'true' || ariaInvalid === '') {
        return 'true';
      }
      return this.computedState === false ? 'true' : ariaInvalid;
    },
    computedAttrs() {
      const {
        localType: type,
        name,
        form,
        disabled,
        placeholder,
        required,
        min,
        max,
        step
      } = this;
      return {
        id: this.computedId,
        name,
        form,
        type,
        disabled,
        placeholder,
        required,
        autocomplete: this.autocomplete || null,
        readonly: this.readonly || this.plaintext,
        min,
        max,
        step,
        list: type !== 'password' ? this.list : null,
        'aria-required': required ? 'true' : null,
        'aria-invalid': this.computedAriaInvalid
      };
    },
    computedState() {
      // If not a boolean, ensure that value is null
      return isBoolean(this.state) ? this.state : null;
    },
    stateClass() {
      if (this.computedState === true) return 'is-valid';
      if (this.computedState === false) return 'is-invalid';
      return null;
    },
    widthClasses() {
      if (this.width === null) {
        return [];
      }
      if (isObject(this.width)) {
        const {
          default: defaultWidth,
          ...nonDefaultWidths
        } = this.width;
        return [
        // eslint-disable-next-line @gitlab/tailwind-no-interpolation -- Not a CSS utility
        ...(defaultWidth ? [`gl-form-input-${defaultWidth}`] : []), ...Object.entries(nonDefaultWidths).map(
        // eslint-disable-next-line @gitlab/tailwind-no-interpolation -- Not a CSS utility
        _ref => {
          let [breakpoint, width] = _ref;
          return `gl-${breakpoint}-form-input-${width}`;
        })];
      }

      // eslint-disable-next-line @gitlab/tailwind-no-interpolation -- Not a CSS utility
      return [`gl-form-input-${this.width}`];
    },
    computedClass() {
      const {
        plaintext,
        type
      } = this;
      const isRange = type === 'range';
      const isColor = type === 'color';
      return [...this.widthClasses, {
        // Range input needs class `custom-range`
        'custom-range': isRange,
        // `plaintext` not supported by `type="range"` or `type="color"`
        'form-control-plaintext': plaintext && !isRange && !isColor,
        // `form-control` not used by `type="range"` or `plaintext`
        // Always used by `type="color"`
        'form-control': isColor || !plaintext && !isRange
      }, this.stateClass];
    },
    computedListeners() {
      return {
        ...this.$listeners,
        input: this.onInput,
        change: this.onChange,
        blur: this.onBlur
      };
    },
    computedDebounce() {
      // Ensure we have a positive number equal to or greater than 0
      return Math.max(toInteger(this.debounce), 0);
    },
    hasFormatter() {
      return typeof this.formatter === 'function';
    },
    noWheel() {
      return this.type === 'number';
    },
    selectionStart: {
      // Expose selectionStart for formatters, etc
      cache: false,
      get() {
        return this.$refs.input.selectionStart;
      },
      set(val) {
        this.$refs.input.selectionStart = val;
      }
    },
    selectionEnd: {
      // Expose selectionEnd for formatters, etc
      cache: false,
      get() {
        return this.$refs.input.selectionEnd;
      },
      set(val) {
        this.$refs.input.selectionEnd = val;
      }
    },
    selectionDirection: {
      // Expose selectionDirection for formatters, etc
      cache: false,
      get() {
        return this.$refs.input.selectionDirection;
      },
      set(val) {
        this.$refs.input.selectionDirection = val;
      }
    },
    validity: {
      // Expose validity property
      cache: false,
      get() {
        return this.$refs.input.validity;
      }
    },
    validationMessage: {
      // Expose validationMessage property
      cache: false,
      get() {
        return this.$refs.input.validationMessage;
      }
    },
    willValidate: {
      // Expose willValidate property
      cache: false,
      get() {
        return this.$refs.input.willValidate;
      }
    }
  },
  watch: {
    value(newValue) {
      const stringifyValue = toString(newValue);
      const modifiedValue = this.modifyValue(newValue);
      if (stringifyValue !== this.localValue || modifiedValue !== this.vModelValue) {
        // Clear any pending debounce timeout, as we are overwriting the user input
        this.clearDebounce();
        // Update the local values
        this.localValue = stringifyValue;
        this.vModelValue = modifiedValue;
      }
    },
    noWheel(newValue) {
      this.setWheelStopper(newValue);
    }
  },
  created() {
    // Create private non-reactive props
    this.$_inputDebounceTimer = null;
  },
  mounted() {
    this.setWheelStopper(this.noWheel);
    this.handleAutofocus();
    this.$nextTick(() => {
      // Update DOM with auto-generated ID after mount
      // to prevent SSR hydration errors
      this.localId = uniqueId('gl-form-input-');
    });
  },
  deactivated() {
    // Turn off listeners when keep-alive component deactivated
    this.setWheelStopper(false);
  },
  activated() {
    // Turn on listeners (if no-wheel) when keep-alive component activated
    this.setWheelStopper(this.noWheel);
    this.handleAutofocus();
  },
  beforeDestroy() {
    this.setWheelStopper(false);
    this.clearDebounce();
  },
  methods: {
    focus() {
      if (!this.disabled) {
        var _this$$refs$input;
        (_this$$refs$input = this.$refs.input) === null || _this$$refs$input === void 0 ? void 0 : _this$$refs$input.focus();
      }
    },
    blur() {
      if (!this.disabled) {
        var _this$$refs$input2;
        (_this$$refs$input2 = this.$refs.input) === null || _this$$refs$input2 === void 0 ? void 0 : _this$$refs$input2.blur();
      }
    },
    clearDebounce() {
      clearTimeout(this.$_inputDebounceTimer);
      this.$_inputDebounceTimer = null;
    },
    formatValue(value, event) {
      let force = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
      let newValue = toString(value);
      if (this.hasFormatter && (!this.lazyFormatter || force)) {
        newValue = this.formatter(value, event);
      }
      return newValue;
    },
    modifyValue(value) {
      let newValue = toString(value);
      // Emulate `.trim` modifier behaviour
      if (this.trim) {
        newValue = newValue.trim();
      }
      // Emulate `.number` modifier behaviour
      if (this.number) {
        newValue = toFloat(newValue, newValue);
      }
      return newValue;
    },
    updateValue(value) {
      let force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
      const {
        lazy
      } = this;
      if (lazy && !force) {
        return;
      }
      // Make sure to always clear the debounce when `updateValue()`
      // is called, even when the v-model hasn't changed
      this.clearDebounce();
      // Define the shared update logic in a method to be able to use
      // it for immediate and debounced value changes
      const doUpdate = () => {
        const newValue = this.modifyValue(value);
        if (newValue !== this.vModelValue) {
          this.vModelValue = newValue;
          this.$emit(MODEL_EVENT, newValue);
        } else if (this.hasFormatter) {
          // When the `vModelValue` hasn't changed but the actual input value
          // is out of sync, make sure to change it to the given one
          // Usually caused by browser autocomplete and how it triggers the
          // change or input event, or depending on the formatter function
          // https://github.com/bootstrap-vue/bootstrap-vue/issues/2657
          // https://github.com/bootstrap-vue/bootstrap-vue/issues/3498
          const $input = this.$refs.input;
          if ($input && newValue !== $input.value) {
            $input.value = newValue;
          }
        }
      };
      // Only debounce the value update when a value greater than `0`
      // is set and we are not in lazy mode or this is a forced update
      const debounce = this.computedDebounce;
      if (debounce > 0 && !lazy && !force) {
        this.$_inputDebounceTimer = setTimeout(doUpdate, debounce);
      } else {
        // Immediately update the v-model
        doUpdate();
      }
    },
    onInput(event) {
      // `event.target.composing` is set by Vue
      // https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/directives/model.js
      // TODO: Is this needed now with the latest Vue?
      if (event.target.composing) {
        return;
      }
      const {
        value
      } = event.target;
      const formattedValue = this.formatValue(value, event);
      // Exit when the `formatter` function strictly returned `false`
      // or prevented the input event
      if (formattedValue === false || event.defaultPrevented) {
        stopEvent(event, {
          propagation: false
        });
        return;
      }
      this.localValue = formattedValue;
      this.updateValue(formattedValue);
      /**
       * The `input` and `update` events are swapped
       * see https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1628
       */
      this.$emit('update', formattedValue);
    },
    onChange(event) {
      const {
        value
      } = event.target;
      const formattedValue = this.formatValue(value, event);
      // Exit when the `formatter` function strictly returned `false`
      // or prevented the input event
      if (formattedValue === false || event.defaultPrevented) {
        stopEvent(event, {
          propagation: false
        });
        return;
      }
      this.localValue = formattedValue;
      this.updateValue(formattedValue, true);
      this.$emit('change', formattedValue);
    },
    onBlur(event) {
      // Apply the `localValue` on blur to prevent cursor jumps
      // on mobile browsers (e.g. caused by autocomplete)
      const {
        value
      } = event.target;
      const formattedValue = this.formatValue(value, event, true);
      if (formattedValue !== false) {
        // We need to use the modified value here to apply the
        // `.trim` and `.number` modifiers properly
        this.localValue = toString(this.modifyValue(formattedValue));
        // We pass the formatted value here since the `updateValue` method
        // handles the modifiers itself
        this.updateValue(formattedValue, true);
      }
      // Emit native blur event
      this.$emit('blur', event);
    },
    setWheelStopper(on) {
      const {
        input
      } = this.$refs;
      // We use native events, so that we don't interfere with propagation
      if (on) {
        input.addEventListener('focus', this.onWheelFocus);
        input.addEventListener('blur', this.onWheelBlur);
      } else {
        input.removeEventListener('focus', this.onWheelFocus);
        input.removeEventListener('blur', this.onWheelBlur);
        document.removeEventListener('wheel', this.stopWheel);
      }
    },
    onWheelFocus() {
      document.addEventListener('wheel', this.stopWheel);
    },
    onWheelBlur() {
      document.removeEventListener('wheel', this.stopWheel);
    },
    stopWheel(event) {
      stopEvent(event, {
        propagation: false
      });
      this.blur();
    },
    handleAutofocus() {
      this.$nextTick(() => {
        window.requestAnimationFrame(() => {
          if (this.autofocus && isVisible(this.$refs.input)) this.focus();
        });
      });
    },
    select() {
      for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
        args[_key] = arguments[_key];
      }
      // For external handler that may want a select() method
      this.$refs.input.select(args);
    },
    setSelectionRange() {
      for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }
      // For external handler that may want a setSelectionRange(a,b,c) method
      this.$refs.input.setSelectionRange(args);
    },
    setRangeText() {
      for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
        args[_key3] = arguments[_key3];
      }
      // For external handler that may want a setRangeText(a,b,c) method
      this.$refs.input.setRangeText(args);
    },
    setCustomValidity() {
      for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
        args[_key4] = arguments[_key4];
      }
      // For external handler that may want a setCustomValidity(...) method
      return this.$refs.input.setCustomValidity(args);
    },
    checkValidity() {
      for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
        args[_key5] = arguments[_key5];
      }
      // For external handler that may want a checkValidity(...) method
      return this.$refs.input.checkValidity(args);
    },
    reportValidity() {
      for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
        args[_key6] = arguments[_key6];
      }
      // For external handler that may want a reportValidity(...) method
      return this.$refs.input.reportValidity(args);
    }
  }
};

/* script */
const __vue_script__ = script;

/* template */
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('input',_vm._g(_vm._b({ref:"input",staticClass:"gl-form-input",class:_vm.computedClass,domProps:{"value":_vm.localValue}},'input',_vm.computedAttrs,false),_vm.computedListeners))};
var __vue_staticRenderFns__ = [];

  /* style */
  const __vue_inject_styles__ = undefined;
  /* scoped */
  const __vue_scope_id__ = undefined;
  /* module identifier */
  const __vue_module_identifier__ = undefined;
  /* functional template */
  const __vue_is_functional_template__ = false;
  /* style inject */
  
  /* style inject SSR */
  
  /* style inject shadow dom */
  

  
  const __vue_component__ = /*#__PURE__*/__vue_normalize__(
    { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
    __vue_inject_styles__,
    __vue_script__,
    __vue_scope_id__,
    __vue_is_functional_template__,
    __vue_module_identifier__,
    false,
    undefined,
    undefined,
    undefined
  );

export { __vue_component__ as default };
