@gitlab/ui
Version:
GitLab UI Components
700 lines (688 loc) • 21.1 kB
JavaScript
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 };