@gitlab/ui
Version:
GitLab UI Components
484 lines (469 loc) • 17 kB
JavaScript
import { isObject, isBoolean, uniqueId, isPlainObject, get, isNull, isUndefined } from 'lodash-es';
import { formInputWidths } from '../../../../utils/constants';
import { isVisible } from '../../../../utils/utils';
import { SafeHtmlDirective } from '../../../../directives/safe_html/safe_html';
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithHoles(r) {
if (Array.isArray(r)) return r;
}
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = !0,
o = !1;
try {
if (i = (t = t.call(r)).next, 0 === l) {
if (Object(t) !== t) return;
f = !1;
} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = !0, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _objectWithoutProperties(e, t) {
if (null == e) return {};
var o,
r,
i = _objectWithoutPropertiesLoose(e, t);
if (Object.getOwnPropertySymbols) {
var n = Object.getOwnPropertySymbols(e);
for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
}
return i;
}
function _objectWithoutPropertiesLoose(r, e) {
if (null == r) return {};
var t = {};
for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
if (-1 !== e.indexOf(n)) continue;
t[n] = r[n];
}
return t;
}
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
const _excluded = ["default"];
var script = {
name: 'GlFormSelect',
directives: {
SafeHtml: SafeHtmlDirective
},
inheritAttrs: false,
props: {
/**
* Current value of the select. Should be set to an array when the 'multiple' prop is set
*/
value: {
// Value can be any type!
type: undefined,
required: false,
default: undefined
},
/**
* Array of items to render in the component
*/
options: {
type: [Array, Object],
required: false,
default: undefined
},
/**
* When set to a number larger than 0, will set the number of display option rows. Note not all browser will respect this setting
* @deprecated Only useful with `multiple`, which is also deprecated.
*/
selectSize: {
type: Number,
required: false,
default: 0
},
/**
* 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
},
/**
* Render the form control in plain mode, rather than custom styled mode
* @deprecated Use a native <select> element instead.
*/
plain: {
type: Boolean,
required: false,
default: false
},
/**
* Set the size of the component's appearance. 'sm', 'md' (default), or 'lg'
* @deprecated Has no effect. Use the `width` prop instead.
*/
size: {
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
},
/**
* Controls the validation state appearance of the component. `true` for valid, `false` for invalid, or `null` for no validation state
*/
state: {
// Tri-state prop: true, false, null (or undefined)
type: Boolean,
required: false,
default: null
},
/**
* Field name in the `options` array that should be used for the value
*/
valueField: {
type: String,
required: false,
default: 'value'
},
/**
* The key to use from the option object to get the label
*/
labelField: {
type: String,
required: false,
default: 'label'
},
/**
* The key to use from the option object to get the options
*/
optionsField: {
type: String,
required: false,
default: 'options'
},
/**
* Field name in the `options` array that should be used for the text label
*/
textField: {
type: String,
required: false,
default: 'text'
},
/**
* Field name in the `options` array that should be used for the disabled state
*/
disabledField: {
type: String,
required: false,
default: 'disabled'
},
/**
* Field name in the `options` array that should be used for the html label instead of text field
* @deprecated Using the `.html` field (whatever its name) is potentially unsafe.
*/
htmlField: {
type: String,
required: false,
default: 'html'
},
/**
* When set, allows multiple options to be selected (multi-select)
* @deprecated Use GlCollapsibleListbox instead.
*/
multiple: {
type: Boolean,
required: false,
default: false
},
/**
* 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
},
/**
* Maximum width of the Select
*/
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));
}
},
/**
* Additional CSS class(es) to apply to the select element.
*/
selectClass: {
type: [String, Object, Array],
required: false,
default: ''
}
},
data() {
return {
localValue: this.value,
localId: null
};
},
computed: {
computedId() {
return this.id || this.localId;
},
computedSelectSize() {
// Custom selects with a size of zero causes the arrows to be hidden,
// so dont render the size attribute in this case
return !this.plain && this.selectSize === 0 ? null : this.selectSize;
},
computedAriaInvalid() {
const ariaInvalid = this.ariaInvalid;
if (ariaInvalid === true || ariaInvalid === 'true' || ariaInvalid === '') {
return 'true';
}
return this.computedState === false ? 'true' : ariaInvalid;
},
computedState() {
// If not a boolean, ensure that value is null
return isBoolean(this.state) ? this.state : null;
},
cssClasses() {
if (this.width === null) {
return [];
}
if (isObject(this.width)) {
const _this$width = this.width,
defaultWidth = _this$width.default,
nonDefaultWidths = _objectWithoutProperties(_this$width, _excluded);
return [
// eslint-disable-next-line @gitlab/tailwind-no-interpolation -- Not a CSS utility
...(defaultWidth ? [`gl-form-select-${defaultWidth}`] : []), ...Object.entries(nonDefaultWidths).map(
// eslint-disable-next-line @gitlab/tailwind-no-interpolation -- Not a CSS utility
_ref => {
let _ref2 = _slicedToArray(_ref, 2),
breakpoint = _ref2[0],
width = _ref2[1];
return `gl-${breakpoint}-form-select-${width}`;
})];
}
// eslint-disable-next-line @gitlab/tailwind-no-interpolation -- Not a CSS utility
return [`gl-form-select-${this.width}`];
},
stateClass() {
if (this.computedState === true) return 'is-valid';
if (this.computedState === false) return 'is-invalid';
return null;
},
inputClass() {
return ['gl-form-select', this.plain ? 'form-control' : 'custom-select', this.size && this.plain ? `form-control-${this.size}` : null, this.size && !this.plain ? `custom-select-${this.size}` : null, this.stateClass, this.selectClass];
},
normalizedOptions() {
return this.normalizeOptions(this.options);
}
},
watch: {
value(newValue) {
this.localValue = newValue;
},
localValue(newValue) {
/**
* Emitted when the select value changes
*
* @event input
* @type {any}
*/
this.$emit('input', newValue);
}
},
activated() {
this.handleAutofocus();
},
async mounted() {
this.handleAutofocus();
await this.$nextTick();
// Update DOM with auto-generated ID after mount
// to prevent SSR hydration errors
this.localId = uniqueId('gl-form-select-');
},
methods: {
focus() {
var _this$$refs$select;
(_this$$refs$select = this.$refs.select) === null || _this$$refs$select === void 0 ? void 0 : _this$$refs$select.focus();
},
blur() {
var _this$$refs$select2;
(_this$$refs$select2 = this.$refs.select) === null || _this$$refs$select2 === void 0 ? void 0 : _this$$refs$select2.blur();
},
normalizeOptions(options) {
// Normalize the given options array
if (Array.isArray(options)) return options.map(option => this.normalizeOption(option));
if (isPlainObject(options)) {
if (process.env.NODE_ENV !== 'production') {
// Deprecate the object options format
// eslint-disable-next-line no-console
console.warn('[@gitlab/ui] GlFormSelect - Setting prop "options" to an object is deprecated. Use the array format instead.');
}
// Normalize a `options` object to an array of options
return Object.keys(options).map(key => this.normalizeOption(options[key] || {}, key));
}
// If not an array or object, return an empty array
return [];
},
normalizeOption(option) {
let key = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
// When the option is an object, normalize it
if (isPlainObject(option)) {
const value = get(option, this.valueField);
const text = get(option, this.textField);
const options = get(option, this.optionsField, null);
// When it has options, create an <optgroup> object
if (!isNull(options)) {
return {
label: String(get(option, this.labelField) || text),
options: this.normalizeOptions(options)
};
}
// Otherwise create an <option> object
return {
value: isUndefined(value) ? key || text : value,
text: String(isUndefined(text) ? key : text),
html: get(option, this.htmlField),
disabled: Boolean(get(option, this.disabledField))
};
}
// Otherwise create an <option> object from the given value
return {
value: key || option,
text: String(option),
disabled: false
};
},
isOptionGroup(option) {
return Array.isArray(option.options);
},
async onChange(event) {
const target = event.target;
const selectedValue = Array.from(target.options).filter(option => option.selected)
// NOTE: the _value property is set up by both Vue 2 and 3 explicitly for the
// purpose of handling non-string values on form elements.
//
// See:
// - https://github.com/vuejs/vue/blob/v2.7.16/src/platforms/web/runtime/modules/dom-props.ts#L42-L44
// - https://github.com/vuejs/vue/blob/v2.7.16/test/unit/modules/vdom/modules/dom-props.spec.ts#L38
// - https://github.com/vuejs/core/blob/v3.4.27/packages/runtime-dom/src/modules/props.ts#L48-L50
// - https://github.com/vuejs/core/blob/v3.4.27/packages/runtime-dom/__tests__/patchProps.spec.ts#L25
// eslint-disable-next-line no-underscore-dangle
.map(option => '_value' in option ? option._value : option.value);
this.localValue = target.multiple ? selectedValue : selectedValue[0];
await this.$nextTick();
/**
* Emitted when the select value changes via user interaction
*
* @event change
* @type {any}
*/
this.$emit('change', this.localValue);
},
async handleAutofocus() {
await this.$nextTick();
window.requestAnimationFrame(() => {
if (this.autofocus && isVisible(this.$refs.select)) this.focus();
});
}
}
};
/* 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('span',{staticClass:"gl-form-select-wrapper",class:_vm.cssClasses},[_c('select',_vm._b({directives:[{name:"model",rawName:"v-model",value:(_vm.localValue),expression:"localValue"}],ref:"select",class:_vm.inputClass,attrs:{"id":_vm.computedId,"aria-invalid":_vm.computedAriaInvalid,"aria-required":_vm.required ? 'true' : null,"disabled":_vm.disabled,"form":_vm.form,"multiple":_vm.multiple,"name":_vm.name,"required":_vm.required,"size":_vm.computedSelectSize},on:{"change":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return val}); _vm.localValue=$event.target.multiple ? $$selectedVal : $$selectedVal[0];},_vm.onChange]}},'select',_vm.$attrs,false),[_vm._t("first"),_vm._v(" "),_vm._l((_vm.normalizedOptions),function(option,index){return [(_vm.isOptionGroup(option))?[_c('optgroup',{key:index,attrs:{"label":option.label}},[_vm._l((option.options),function(childOption,childIndex){return [(childOption.html)?_c('option',{directives:[{name:"safe-html",rawName:"v-safe-html",value:(childOption.html),expression:"childOption.html"}],key:(childIndex + "-html"),attrs:{"disabled":childOption.disabled},domProps:{"value":childOption.value}}):_c('option',{key:childIndex,attrs:{"disabled":childOption.disabled},domProps:{"value":childOption.value}},[_vm._v("\n "+_vm._s(childOption.text)+"\n ")])]})],2)]:[(option.html)?_c('option',{directives:[{name:"safe-html",rawName:"v-safe-html",value:(option.html),expression:"option.html"}],key:(index + "-html"),attrs:{"disabled":option.disabled},domProps:{"value":option.value}}):_c('option',{key:index,attrs:{"disabled":option.disabled},domProps:{"value":option.value}},[_vm._v("\n "+_vm._s(option.text)+"\n ")])]]}),_vm._v(" "),_vm._t("default")],2)])};
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 };