UNPKG

quasar-framework

Version:

Build responsive SPA, SSR, PWA, Hybrid Mobile Apps and Electron apps, all simultaneously using the same codebase

505 lines (490 loc) 14 kB
import QSearch from '../search/QSearch.js' import QPopover from '../popover/QPopover.js' import QList from '../list/QList.js' import QItemWrapper from '../list/QItemWrapper.js' import QCheckbox from '../checkbox/QCheckbox.js' import QRadio from '../radio/QRadio.js' import QToggle from '../toggle/QToggle.js' import QIcon from '../icon/QIcon.js' import QInputFrame from '../input-frame/QInputFrame.js' import QChip from '../chip/QChip.js' import FrameMixin from '../../mixins/input-frame.js' import KeyboardSelectionMixin from '../../mixins/keyboard-selection.js' function defaultFilterFn (terms, obj) { return obj.label.toLowerCase().indexOf(terms) > -1 } export default { name: 'QSelect', mixins: [FrameMixin, KeyboardSelectionMixin], props: { filter: [Function, Boolean], filterPlaceholder: String, radio: Boolean, placeholder: String, separator: Boolean, value: { required: true }, multiple: Boolean, toggle: Boolean, chips: Boolean, options: { type: Array, required: true, validator: v => v.every(o => 'label' in o && 'value' in o) }, chipsColor: String, chipsBgColor: String, displayValue: String, popupMaxHeight: String, popupCover: { type: Boolean, default: true } }, data () { return { model: this.multiple && Array.isArray(this.value) ? this.value.slice() : this.value, terms: '', focused: false } }, watch: { value (val) { this.model = this.multiple && Array.isArray(val) ? val.slice() : val }, visibleOptions () { this.__keyboardCalcIndex() } }, computed: { optModel () { if (this.multiple) { return this.model.length > 0 ? this.options.map(opt => this.model.includes(opt.value)) : this.options.map(opt => false) } }, visibleOptions () { let opts = this.options.map((opt, index) => Object.assign({}, opt, { index })) if (this.filter && this.terms.length) { const lowerTerms = this.terms.toLowerCase() opts = opts.filter(opt => this.filterFn(lowerTerms, opt)) } return opts }, keyboardMaxIndex () { return this.visibleOptions.length - 1 }, filterFn () { return typeof this.filter === 'boolean' ? defaultFilterFn : this.filter }, actualValue () { if (this.displayValue) { return this.displayValue } if (!this.multiple) { const opt = this.options.find(opt => opt.value === this.model) return opt ? opt.label : '' } const opt = this.selectedOptions.map(opt => opt.label) return opt.length ? opt.join(', ') : '' }, computedClearValue () { return this.clearValue === void 0 ? (this.multiple ? [] : null) : this.clearValue }, isClearable () { return this.editable && this.clearable && JSON.stringify(this.computedClearValue) !== JSON.stringify(this.model) }, selectedOptions () { if (this.multiple) { return this.length > 0 ? this.options.filter(opt => this.model.includes(opt.value)) : [] } }, hasChips () { return this.multiple && this.chips && this.length > 0 }, length () { return this.multiple ? this.model.length : ([null, undefined, ''].includes(this.model) ? 0 : 1) }, additionalLength () { return this.displayValue && this.displayValue.length > 0 } }, methods: { togglePopup () { this.$refs.popover && this[this.$refs.popover.showing ? 'hide' : 'show']() }, show () { this.__keyboardCalcIndex() if (this.$refs.popover) { return this.$refs.popover.show() } }, hide () { return this.$refs.popover ? this.$refs.popover.hide() : Promise.resolve() }, reposition () { const popover = this.$refs.popover if (popover && popover.showing) { this.$nextTick(() => popover && popover.reposition()) } }, __keyboardCalcIndex () { this.keyboardIndex = -1 const sel = this.multiple ? this.selectedOptions.map(o => o.value) : [this.model] this.$nextTick(() => { const index = sel === void 0 ? -1 : Math.max(-1, this.visibleOptions.findIndex(opt => sel.includes(opt.value))) if (index > -1) { this.keyboardMoveDirection = true setTimeout(() => { this.keyboardMoveDirection = false }, 500) this.__keyboardShow(index) } }) }, __keyboardCustomKeyHandle (key, e) { switch (key) { case 27: // ESCAPE if (this.$refs.popover.showing) { this.hide() } break case 13: // ENTER key case 32: // SPACE key if (!this.$refs.popover.showing) { this.show() } break } }, __keyboardShowTrigger () { this.show() }, __keyboardSetSelection (index) { const opt = this.visibleOptions[index] if (this.multiple) { this.__toggleMultiple(opt.value, opt.disable) } else { this.__singleSelect(opt.value, opt.disable) } }, __keyboardIsSelectableIndex (index) { return index > -1 && index < this.visibleOptions.length && !this.visibleOptions[index].disable }, __mouseEnterHandler (e, index) { if (!this.keyboardMoveDirection) { this.keyboardIndex = index } }, __onFocus () { if (this.disable || this.focused) { return } this.focused = true this.$emit('focus') }, __onShow () { if (this.disable) { return } this.__onFocus() if (this.filter && this.$refs.filter) { this.$refs.filter.focus() this.reposition() } }, __onBlur (e) { if (!this.focused) { return } setTimeout(() => { const el = document.activeElement if ( !this.$refs.popover || !this.$refs.popover.showing || (el !== document.body && !this.$refs.popover.$el.contains(el)) ) { this.__onClose() this.hide() } }, 1) }, __onClose (keepFocus) { this.$nextTick(() => { if (JSON.stringify(this.model) !== JSON.stringify(this.value)) { this.$emit('change', this.model) } }) this.terms = '' if (!this.focused) { return } if (keepFocus) { this.$refs.input && this.$refs.input.$el && this.$refs.input.$el.focus() return } this.focused = false this.$emit('blur') }, __singleSelect (val, disable) { if (disable) { return } this.__emit(val) this.hide() }, __toggleMultiple (value, disable) { if (disable) { return } const model = this.model, index = model.indexOf(value) if (index > -1) { this.$emit('remove', { index, value: model.splice(index, 1) }) } else { this.$emit('add', { index: model.length, value }) model.push(value) } this.$emit('input', model) }, __emit (value) { this.$emit('input', value) this.$nextTick(() => { if (JSON.stringify(value) !== JSON.stringify(this.value)) { this.$emit('change', value) } }) }, __setModel (val, forceUpdate) { this.model = val || (this.multiple ? [] : null) this.$emit('input', this.model) if (forceUpdate || !this.$refs.popover || !this.$refs.popover.showing) { this.__onClose(forceUpdate) } }, __getChipTextColor (optColor) { if (this.chipsColor) { return this.chipsColor } if (this.isInvertedLight) { return this.invertedLight ? optColor || this.color : 'white' } if (this.isInverted) { return optColor || (this.invertedLight ? 'grey-10' : this.color) } return this.dark ? optColor || this.color : 'white' }, __getChipBgColor (optColor) { if (this.chipsBgColor) { return this.chipsBgColor } if (this.isInvertedLight) { return this.invertedLight ? 'grey-10' : optColor || this.color } if (this.isInverted) { return this.invertedLight ? this.color : 'white' } return this.dark ? 'white' : optColor || this.color } }, render (h) { const child = [] if (this.hasChips) { const el = h('div', { staticClass: 'col row items-center q-input-chips', 'class': this.alignClass }, this.selectedOptions.map((opt, index) => { return h(QChip, { key: index, props: { small: true, closable: this.editable && !opt.disable, color: this.__getChipBgColor(opt.color), textColor: this.__getChipTextColor(opt.color), icon: opt.icon, iconRight: opt.rightIcon, avatar: opt.avatar }, on: { hide: () => { this.__toggleMultiple(opt.value, this.disable || opt.disable) } }, nativeOn: { click: e => { e.stopPropagation() } } }, [ h('div', { domProps: { innerHTML: opt.label } }) ]) })) child.push(el) } else { const el = h('div', { staticClass: 'col q-input-target ellipsis', 'class': this.fakeInputClasses, domProps: { innerHTML: this.fakeInputValue } }) child.push(el) } child.push(h(QPopover, { ref: 'popover', staticClass: 'column no-wrap', 'class': this.dark ? 'bg-dark' : null, props: { cover: this.popupCover, keepOnScreen: true, disable: !this.editable, anchorClick: false, maxHeight: this.popupMaxHeight }, slot: 'after', on: { show: this.__onShow, hide: () => { this.__onClose(true) } }, nativeOn: { keydown: this.__keyboardHandleKey } }, [ (this.filter && h(QSearch, { ref: 'filter', staticClass: 'col-auto', style: 'padding: 10px;', props: { value: this.terms, placeholder: this.filterPlaceholder || this.$q.i18n.label.filter, debounce: 100, color: this.color, dark: this.dark, noParentField: true, noIcon: true }, on: { input: val => { this.terms = val this.reposition() } } })) || void 0, (this.visibleOptions.length && h(QList, { staticClass: 'no-border scroll', props: { separator: this.separator, dark: this.dark } }, this.visibleOptions.map((opt, index) => { return h(QItemWrapper, { key: index, 'class': [ opt.disable ? 'text-faded' : 'cursor-pointer', index === this.keyboardIndex ? 'q-select-highlight' : '', opt.disable ? '' : 'cursor-pointer', opt.className || '' ], props: { cfg: opt, slotReplace: true, active: this.multiple ? void 0 : this.value === opt.value }, nativeOn: { '!click': () => { const action = this.multiple ? '__toggleMultiple' : '__singleSelect' this[action](opt.value, opt.disable) }, mouseenter: e => { !opt.disable && this.__mouseEnterHandler(e, index) } } }, [ this.multiple ? h(this.toggle ? QToggle : QCheckbox, { slot: this.toggle ? 'right' : 'left', props: { keepColor: true, color: opt.color || this.color, dark: this.dark, value: this.optModel[opt.index], disable: opt.disable, noFocus: true } }) : (this.radio && h(QRadio, { slot: 'left', props: { keepColor: true, color: opt.color || this.color, dark: this.dark, value: this.value, val: opt.value, disable: opt.disable, noFocus: true } })) || void 0 ]) }))) || void 0 ])) if (this.isClearable) { child.push(h(QIcon, { slot: 'after', staticClass: 'q-if-control', props: { name: this.$q.icon.input[`clear${this.isInverted ? 'Inverted' : ''}`] }, nativeOn: { click: this.clear } })) } child.push( h(QIcon, this.readonly ? { slot: 'after' } : { slot: 'after', staticClass: 'q-if-control', props: { name: this.$q.icon.input.dropdown } }) ) return h(QInputFrame, { ref: 'input', staticClass: 'q-select', props: { prefix: this.prefix, suffix: this.suffix, stackLabel: this.stackLabel, floatLabel: this.floatLabel, error: this.error, warning: this.warning, disable: this.disable, readonly: this.readonly, inverted: this.inverted, invertedLight: this.invertedLight, dark: this.dark, hideUnderline: this.hideUnderline, before: this.before, after: this.after, color: this.color, noParentField: this.noParentField, focused: this.focused, focusable: true, length: this.length, additionalLength: this.additionalLength }, nativeOn: { click: this.togglePopup, focus: this.__onFocus, blur: this.__onBlur, keydown: this.__keyboardHandleKey } }, child) } }