quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
1,599 lines (1,332 loc) • 46 kB
JavaScript
import { h, ref, computed, watch, onBeforeUpdate, onUpdated, onBeforeUnmount, nextTick, getCurrentInstance } from 'vue'
import QField from '../field/QField.js'
import QIcon from '../icon/QIcon.js'
import QChip from '../chip/QChip.js'
import QItem from '../item/QItem.js'
import QItemSection from '../item/QItemSection.js'
import QItemLabel from '../item/QItemLabel.js'
import QMenu from '../menu/QMenu.js'
import QDialog from '../dialog/QDialog.js'
import useField, { useFieldState, useFieldProps, useFieldEmits, fieldValueIsFilled } from '../../composables/private.use-field/use-field.js'
import { useVirtualScroll, useVirtualScrollProps } from '../virtual-scroll/use-virtual-scroll.js'
import { useFormProps, useFormInputNameAttr } from '../../composables/use-form/private.use-form.js'
import useKeyComposition from '../../composables/private.use-key-composition/use-key-composition.js'
import { createComponent } from '../../utils/private.create/create.js'
import { isDeepEqual } from '../../utils/is/is.js'
import { stop, prevent, stopAndPrevent } from '../../utils/event/event.js'
import { normalizeToInterval } from '../../utils/format/format.js'
import { shouldIgnoreKey, isKeyCode } from '../../utils/private.keyboard/key-composition.js'
import { hMergeSlot } from '../../utils/private.render/render.js'
const validateNewValueMode = v => [ 'add', 'add-unique', 'toggle' ].includes(v)
const reEscapeList = '.*+?^${}()|[]\\'
const fieldPropsList = Object.keys(useFieldProps)
export default createComponent({
name: 'QSelect',
inheritAttrs: false,
props: {
...useVirtualScrollProps,
...useFormProps,
...useFieldProps,
// override of useFieldProps > modelValue
modelValue: {
required: true
},
multiple: Boolean,
displayValue: [ String, Number ],
displayValueHtml: Boolean,
dropdownIcon: String,
options: {
type: Array,
default: () => []
},
optionValue: [ Function, String ],
optionLabel: [ Function, String ],
optionDisable: [ Function, String ],
hideSelected: Boolean,
hideDropdownIcon: Boolean,
fillInput: Boolean,
maxValues: [ Number, String ],
optionsDense: Boolean,
optionsDark: {
type: Boolean,
default: null
},
optionsSelectedClass: String,
optionsHtml: Boolean,
optionsCover: Boolean,
menuShrink: Boolean,
menuAnchor: String,
menuSelf: String,
menuOffset: Array,
popupContentClass: String,
popupContentStyle: [ String, Array, Object ],
popupNoRouteDismiss: Boolean,
useInput: Boolean,
useChips: Boolean,
newValueMode: {
type: String,
validator: validateNewValueMode
},
mapOptions: Boolean,
emitValue: Boolean,
inputDebounce: {
type: [ Number, String ],
default: 500
},
inputClass: [ Array, String, Object ],
inputStyle: [ Array, String, Object ],
tabindex: {
type: [ String, Number ],
default: 0
},
autocomplete: String,
transitionShow: {},
transitionHide: {},
transitionDuration: {},
behavior: {
type: String,
validator: v => [ 'default', 'menu', 'dialog' ].includes(v),
default: 'default'
},
// override of useVirtualScrollProps > virtualScrollItemSize (no default)
virtualScrollItemSize: useVirtualScrollProps.virtualScrollItemSize.type,
onNewValue: Function,
onFilter: Function
},
emits: [
...useFieldEmits,
'add', 'remove', 'inputValue',
'keyup', 'keypress', 'keydown',
'popupShow', 'popupHide',
'filterAbort'
],
setup (props, { slots, emit }) {
const { proxy } = getCurrentInstance()
const { $q } = proxy
const menu = ref(false)
const dialog = ref(false)
const optionIndex = ref(-1)
const inputValue = ref('')
const dialogFieldFocused = ref(false)
const innerLoadingIndicator = ref(false)
let filterTimer = null, inputValueTimer = null,
innerValueCache,
hasDialog, userInputValue, filterId = null, defaultInputValue,
transitionShowComputed, searchBuffer, searchBufferExp
const inputRef = ref(null)
const targetRef = ref(null)
const menuRef = ref(null)
const dialogRef = ref(null)
const menuContentRef = ref(null)
const nameProp = useFormInputNameAttr(props)
const onComposition = useKeyComposition(onInput)
const virtualScrollLength = computed(() => (
Array.isArray(props.options)
? props.options.length
: 0
))
const virtualScrollItemSizeComputed = computed(() => (
props.virtualScrollItemSize === void 0
? (props.optionsDense === true ? 24 : 48)
: props.virtualScrollItemSize
))
const {
virtualScrollSliceRange,
virtualScrollSliceSizeComputed,
localResetVirtualScroll,
padVirtualScroll,
onVirtualScrollEvt,
scrollTo,
setVirtualScrollSize
} = useVirtualScroll({
virtualScrollLength, getVirtualScrollTarget, getVirtualScrollEl,
virtualScrollItemSizeComputed
})
const state = useFieldState()
const innerValue = computed(() => {
const
mapNull = props.mapOptions === true && props.multiple !== true,
val = props.modelValue !== void 0 && (props.modelValue !== null || mapNull === true)
? (props.multiple === true && Array.isArray(props.modelValue) ? props.modelValue : [ props.modelValue ])
: []
if (props.mapOptions === true && Array.isArray(props.options) === true) {
const cache = props.mapOptions === true && innerValueCache !== void 0
? innerValueCache
: []
const values = val.map(v => getOption(v, cache))
return props.modelValue === null && mapNull === true
? values.filter(v => v !== null)
: values
}
return val
})
const innerFieldProps = computed(() => {
const acc = {}
fieldPropsList.forEach(key => {
const val = props[ key ]
if (val !== void 0) {
acc[ key ] = val
}
})
return acc
})
const isOptionsDark = computed(() => (
props.optionsDark === null
? state.isDark.value
: props.optionsDark
))
const hasValue = computed(() => fieldValueIsFilled(innerValue.value))
const computedInputClass = computed(() => {
let cls = 'q-field__input q-placeholder col'
if (props.hideSelected === true || innerValue.value.length === 0) {
return [ cls, props.inputClass ]
}
cls += ' q-field__input--padding'
return props.inputClass === void 0
? cls
: [ cls, props.inputClass ]
})
const menuContentClass = computed(() =>
(props.virtualScrollHorizontal === true ? 'q-virtual-scroll--horizontal' : '')
+ (props.popupContentClass ? ' ' + props.popupContentClass : '')
)
const noOptions = computed(() => virtualScrollLength.value === 0)
const selectedString = computed(() =>
innerValue.value
.map(opt => getOptionLabel.value(opt))
.join(', ')
)
const ariaCurrentValue = computed(() => (props.displayValue !== void 0
? props.displayValue
: selectedString.value
))
const needsHtmlFn = computed(() => (
props.optionsHtml === true
? () => true
: opt => opt !== void 0 && opt !== null && opt.html === true
))
const valueAsHtml = computed(() => (
props.displayValueHtml === true || (
props.displayValue === void 0 && (
props.optionsHtml === true
|| innerValue.value.some(needsHtmlFn.value)
)
)
))
const tabindex = computed(() => (state.focused.value === true ? props.tabindex : -1))
const comboboxAttrs = computed(() => {
const attrs = {
tabindex: props.tabindex,
role: 'combobox',
'aria-label': props.label,
'aria-readonly': props.readonly === true ? 'true' : 'false',
'aria-autocomplete': props.useInput === true ? 'list' : 'none',
'aria-expanded': menu.value === true ? 'true' : 'false',
'aria-controls': `${ state.targetUid.value }_lb`
}
if (optionIndex.value >= 0) {
attrs[ 'aria-activedescendant' ] = `${ state.targetUid.value }_${ optionIndex.value }`
}
return attrs
})
const listboxAttrs = computed(() => ({
id: `${ state.targetUid.value }_lb`,
role: 'listbox',
'aria-multiselectable': props.multiple === true ? 'true' : 'false'
}))
const selectedScope = computed(() => {
return innerValue.value.map((opt, i) => ({
index: i,
opt,
html: needsHtmlFn.value(opt),
selected: true,
removeAtIndex: removeAtIndexAndFocus,
toggleOption,
tabindex: tabindex.value
}))
})
const optionScope = computed(() => {
if (virtualScrollLength.value === 0) {
return []
}
const { from, to } = virtualScrollSliceRange.value
return props.options.slice(from, to).map((opt, i) => {
const disable = isOptionDisabled.value(opt) === true
const active = isOptionSelected(opt) === true
const index = from + i
const itemProps = {
clickable: true,
active,
activeClass: computedOptionsSelectedClass.value,
manualFocus: true,
focused: false,
disable,
tabindex: -1,
dense: props.optionsDense,
dark: isOptionsDark.value,
role: 'option',
'aria-selected': active === true ? 'true' : 'false',
id: `${ state.targetUid.value }_${ index }`,
onClick: () => { toggleOption(opt) }
}
if (disable !== true) {
optionIndex.value === index && (itemProps.focused = true)
if ($q.platform.is.desktop === true) {
itemProps.onMousemove = () => { menu.value === true && setOptionIndex(index) }
}
}
return {
index,
opt,
html: needsHtmlFn.value(opt),
label: getOptionLabel.value(opt),
selected: itemProps.active,
focused: itemProps.focused,
toggleOption,
setOptionIndex,
itemProps
}
})
})
const dropdownArrowIcon = computed(() => (
props.dropdownIcon !== void 0
? props.dropdownIcon
: $q.iconSet.arrow.dropdown
))
const squaredMenu = computed(() =>
props.optionsCover === false
&& props.outlined !== true
&& props.standout !== true
&& props.borderless !== true
&& props.rounded !== true
)
const computedOptionsSelectedClass = computed(() => (
props.optionsSelectedClass !== void 0
? props.optionsSelectedClass
: (props.color !== void 0 ? `text-${ props.color }` : '')
))
// returns method to get value of an option;
// takes into account 'option-value' prop
const getOptionValue = computed(() => getPropValueFn(props.optionValue, 'value'))
// returns method to get label of an option;
// takes into account 'option-label' prop
const getOptionLabel = computed(() => getPropValueFn(props.optionLabel, 'label'))
// returns method to tell if an option is disabled;
// takes into account 'option-disable' prop
const isOptionDisabled = computed(() => getPropValueFn(props.optionDisable, 'disable'))
const innerOptionsValue = computed(() => innerValue.value.map(opt => getOptionValue.value(opt)))
const inputControlEvents = computed(() => {
const evt = {
onInput,
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
onChange: onComposition,
onKeydown: onTargetKeydown,
onKeyup: onTargetAutocomplete,
onKeypress: onTargetKeypress,
onFocus: selectInputText,
onClick (e) { hasDialog === true && stop(e) }
}
evt.onCompositionstart = evt.onCompositionupdate = evt.onCompositionend = onComposition
return evt
})
watch(innerValue, val => {
innerValueCache = val
if (
props.useInput === true
&& props.fillInput === true
&& props.multiple !== true
// Prevent re-entering in filter while filtering
// Also prevent clearing inputValue while filtering
&& state.innerLoading.value !== true
&& ((dialog.value !== true && menu.value !== true) || hasValue.value !== true)
) {
userInputValue !== true && resetInputValue()
if (dialog.value === true || menu.value === true) {
filter('')
}
}
}, { immediate: true })
watch(() => props.fillInput, resetInputValue)
watch(menu, updateMenu)
watch(virtualScrollLength, rerenderMenu)
function getEmittingOptionValue (opt) {
return props.emitValue === true
? getOptionValue.value(opt)
: opt
}
function removeAtIndex (index) {
if (index !== -1 && index < innerValue.value.length) {
if (props.multiple === true) {
const model = props.modelValue.slice()
emit('remove', { index, value: model.splice(index, 1)[ 0 ] })
emit('update:modelValue', model)
}
else {
emit('update:modelValue', null)
}
}
}
function removeAtIndexAndFocus (index) {
removeAtIndex(index)
state.focus()
}
function add (opt, unique) {
const val = getEmittingOptionValue(opt)
if (props.multiple !== true) {
props.fillInput === true && updateInputValue(
getOptionLabel.value(opt),
true,
true
)
emit('update:modelValue', val)
return
}
if (innerValue.value.length === 0) {
emit('add', { index: 0, value: val })
emit('update:modelValue', props.multiple === true ? [ val ] : val)
return
}
if (unique === true && isOptionSelected(opt) === true) {
return
}
if (props.maxValues !== void 0 && props.modelValue.length >= props.maxValues) {
return
}
const model = props.modelValue.slice()
emit('add', { index: model.length, value: val })
model.push(val)
emit('update:modelValue', model)
}
function toggleOption (opt, keepOpen) {
if (state.editable.value !== true || opt === void 0 || isOptionDisabled.value(opt) === true) {
return
}
const optValue = getOptionValue.value(opt)
if (props.multiple !== true) {
if (keepOpen !== true) {
updateInputValue(
props.fillInput === true ? getOptionLabel.value(opt) : '',
true,
true
)
hidePopup()
}
targetRef.value !== null && targetRef.value.focus()
if (
innerValue.value.length === 0
|| isDeepEqual(getOptionValue.value(innerValue.value[ 0 ]), optValue) !== true
) {
emit('update:modelValue', props.emitValue === true ? optValue : opt)
}
return
}
(hasDialog !== true || dialogFieldFocused.value === true) && state.focus()
selectInputText()
if (innerValue.value.length === 0) {
const val = props.emitValue === true ? optValue : opt
emit('add', { index: 0, value: val })
emit('update:modelValue', props.multiple === true ? [ val ] : val)
return
}
const
model = props.modelValue.slice(),
index = innerOptionsValue.value.findIndex(v => isDeepEqual(v, optValue))
if (index !== -1) {
emit('remove', { index, value: model.splice(index, 1)[ 0 ] })
}
else {
if (props.maxValues !== void 0 && model.length >= props.maxValues) {
return
}
const val = props.emitValue === true ? optValue : opt
emit('add', { index: model.length, value: val })
model.push(val)
}
emit('update:modelValue', model)
}
function setOptionIndex (index) {
if ($q.platform.is.desktop !== true) return
const val = index !== -1 && index < virtualScrollLength.value
? index
: -1
if (optionIndex.value !== val) {
optionIndex.value = val
}
}
function moveOptionSelection (offset = 1, skipInputValue) {
if (menu.value === true) {
let index = optionIndex.value
do {
index = normalizeToInterval(
index + offset,
-1,
virtualScrollLength.value - 1
)
}
while (index !== -1 && index !== optionIndex.value && isOptionDisabled.value(props.options[ index ]) === true)
if (optionIndex.value !== index) {
setOptionIndex(index)
scrollTo(index)
if (skipInputValue !== true && props.useInput === true && props.fillInput === true) {
setInputValue(
index >= 0
? getOptionLabel.value(props.options[ index ])
: defaultInputValue,
true
)
}
}
}
}
function getOption (value, valueCache) {
const fn = opt => isDeepEqual(getOptionValue.value(opt), value)
return props.options.find(fn) || valueCache.find(fn) || value
}
function getPropValueFn (propValue, defaultVal) {
const val = propValue !== void 0
? propValue
: defaultVal
return typeof val === 'function'
? val
: opt => (opt !== null && typeof opt === 'object' && val in opt ? opt[ val ] : opt)
}
function isOptionSelected (opt) {
const val = getOptionValue.value(opt)
return innerOptionsValue.value.find(v => isDeepEqual(v, val)) !== void 0
}
function selectInputText (e) {
if (
props.useInput === true
&& targetRef.value !== null
&& (e === void 0 || (targetRef.value === e.target && e.target.value === selectedString.value))
) {
targetRef.value.select()
}
}
function onTargetKeyup (e) {
// if ESC and we have an opened menu
// then stop propagation (might be caught by a QDialog
// and so it will also close the QDialog, which is wrong)
if (isKeyCode(e, 27) === true && menu.value === true) {
stop(e)
// on ESC we need to close the dialog also
hidePopup()
resetInputValue()
}
emit('keyup', e)
}
function onTargetAutocomplete (e) {
const { value } = e.target
if (e.keyCode !== void 0) {
onTargetKeyup(e)
return
}
e.target.value = ''
if (filterTimer !== null) {
clearTimeout(filterTimer)
filterTimer = null
}
if (inputValueTimer !== null) {
clearTimeout(inputValueTimer)
inputValueTimer = null
}
resetInputValue()
if (typeof value === 'string' && value.length !== 0) {
const needle = value.toLocaleLowerCase()
const findFn = extractFn => {
const option = props.options.find(opt => extractFn.value(opt).toLocaleLowerCase() === needle)
if (option === void 0) {
return false
}
if (innerValue.value.indexOf(option) === -1) {
toggleOption(option)
}
else {
hidePopup()
}
return true
}
const fillFn = afterFilter => {
if (findFn(getOptionValue) === true) {
return
}
if (findFn(getOptionLabel) === true || afterFilter === true) {
return
}
filter(value, true, () => fillFn(true))
}
fillFn()
}
else {
state.clearValue(e)
}
}
function onTargetKeypress (e) {
emit('keypress', e)
}
function onTargetKeydown (e) {
emit('keydown', e)
if (shouldIgnoreKey(e) === true) {
return
}
const newValueModeValid = inputValue.value.length !== 0
&& (props.newValueMode !== void 0 || props.onNewValue !== void 0)
const tabShouldSelect = e.shiftKey !== true
&& props.multiple !== true
&& (optionIndex.value !== -1 || newValueModeValid === true)
// escape
if (e.keyCode === 27) {
prevent(e) // prevent clearing the inputValue
return
}
// tab
if (e.keyCode === 9 && tabShouldSelect === false) {
closeMenu()
return
}
if (
e.target === void 0
|| e.target.id !== state.targetUid.value
|| state.editable.value !== true
) return
// down
if (
e.keyCode === 40
&& state.innerLoading.value !== true
&& menu.value === false
) {
stopAndPrevent(e)
showPopup()
return
}
// backspace
if (
e.keyCode === 8
&& (
props.useChips === true
|| props.clearable === true
)
&& props.hideSelected !== true
&& inputValue.value.length === 0
) {
if (props.multiple === true && Array.isArray(props.modelValue) === true) {
removeAtIndex(props.modelValue.length - 1)
}
else if (props.multiple !== true && props.modelValue !== null) {
emit('update:modelValue', null)
}
return
}
// home, end - 36, 35
if (
(e.keyCode === 35 || e.keyCode === 36)
&& (typeof inputValue.value !== 'string' || inputValue.value.length === 0)
) {
stopAndPrevent(e)
optionIndex.value = -1
moveOptionSelection(e.keyCode === 36 ? 1 : -1, props.multiple)
}
// pg up, pg down - 33, 34
if (
(e.keyCode === 33 || e.keyCode === 34)
&& virtualScrollSliceSizeComputed.value !== void 0
) {
stopAndPrevent(e)
optionIndex.value = Math.max(
-1,
Math.min(
virtualScrollLength.value,
optionIndex.value + (e.keyCode === 33 ? -1 : 1) * virtualScrollSliceSizeComputed.value.view
)
)
moveOptionSelection(e.keyCode === 33 ? 1 : -1, props.multiple)
}
// up, down
if (e.keyCode === 38 || e.keyCode === 40) {
stopAndPrevent(e)
moveOptionSelection(e.keyCode === 38 ? -1 : 1, props.multiple)
}
const optionsLength = virtualScrollLength.value
// clear search buffer if expired
if (searchBuffer === void 0 || searchBufferExp < Date.now()) {
searchBuffer = ''
}
// keyboard search when not having use-input
if (
optionsLength > 0
&& props.useInput !== true
&& e.key !== void 0
&& e.key.length === 1 // printable char
&& e.altKey === false // not kbd shortcut
&& e.ctrlKey === false // not kbd shortcut
&& e.metaKey === false // not kbd shortcut, especially on macOS with Command key
&& (e.keyCode !== 32 || searchBuffer.length !== 0) // space in middle of search
) {
menu.value !== true && showPopup(e)
const
char = e.key.toLocaleLowerCase(),
keyRepeat = searchBuffer.length === 1 && searchBuffer[ 0 ] === char
searchBufferExp = Date.now() + 1500
if (keyRepeat === false) {
stopAndPrevent(e)
searchBuffer += char
}
const searchRe = new RegExp('^' + searchBuffer.split('').map(l => (reEscapeList.indexOf(l) !== -1 ? '\\' + l : l)).join('.*'), 'i')
let index = optionIndex.value
if (keyRepeat === true || index < 0 || searchRe.test(getOptionLabel.value(props.options[ index ])) !== true) {
do {
index = normalizeToInterval(index + 1, -1, optionsLength - 1)
}
while (index !== optionIndex.value && (
isOptionDisabled.value(props.options[ index ]) === true
|| searchRe.test(getOptionLabel.value(props.options[ index ])) !== true
))
}
if (optionIndex.value !== index) {
nextTick(() => {
setOptionIndex(index)
scrollTo(index)
if (index >= 0 && props.useInput === true && props.fillInput === true) {
setInputValue(getOptionLabel.value(props.options[ index ]), true)
}
})
}
return
}
// enter, space (when not using use-input and not in search), or tab (when not using multiple and option selected)
// same target is checked above
if (
e.keyCode !== 13
&& (e.keyCode !== 32 || props.useInput === true || searchBuffer !== '')
&& (e.keyCode !== 9 || tabShouldSelect === false)
) return
e.keyCode !== 9 && stopAndPrevent(e)
if (optionIndex.value !== -1 && optionIndex.value < optionsLength) {
toggleOption(props.options[ optionIndex.value ])
return
}
if (newValueModeValid === true) {
const done = (val, mode) => {
if (mode) {
if (validateNewValueMode(mode) !== true) {
return
}
}
else {
mode = props.newValueMode
}
updateInputValue('', props.multiple !== true, true)
if (val === void 0 || val === null) {
return
}
const fn = mode === 'toggle' ? toggleOption : add
fn(val, mode === 'add-unique')
if (props.multiple !== true) {
targetRef.value !== null && targetRef.value.focus()
hidePopup()
}
}
if (props.onNewValue !== void 0) {
emit('newValue', inputValue.value, done)
}
else {
done(inputValue.value)
}
if (props.multiple !== true) {
return
}
}
if (menu.value === true) {
closeMenu()
}
else if (state.innerLoading.value !== true) {
showPopup()
}
}
function getVirtualScrollEl () {
return hasDialog === true
? menuContentRef.value
: (
menuRef.value !== null && menuRef.value.contentEl !== null
? menuRef.value.contentEl
: void 0
)
}
function getVirtualScrollTarget () {
return getVirtualScrollEl()
}
function getSelection () {
if (props.hideSelected === true) {
return []
}
if (slots[ 'selected-item' ] !== void 0) {
return selectedScope.value.map(scope => slots[ 'selected-item' ](scope)).slice()
}
if (slots.selected !== void 0) {
return [].concat(slots.selected())
}
if (props.useChips === true) {
return selectedScope.value.map((scope, i) => h(QChip, {
key: 'option-' + i,
removable: state.editable.value === true && isOptionDisabled.value(scope.opt) !== true,
dense: true,
textColor: props.color,
tabindex: tabindex.value,
onRemove () { scope.removeAtIndex(i) }
}, () => h('span', {
class: 'ellipsis',
[ scope.html === true ? 'innerHTML' : 'textContent' ]: getOptionLabel.value(scope.opt)
})))
}
return [
h('span', {
[ valueAsHtml.value === true ? 'innerHTML' : 'textContent' ]: ariaCurrentValue.value
})
]
}
function getAllOptions () {
if (noOptions.value === true) {
return slots[ 'no-option' ] !== void 0
? slots[ 'no-option' ]({ inputValue: inputValue.value })
: void 0
}
const fn = slots.option !== void 0
? slots.option
: scope => {
return h(QItem, {
key: scope.index,
...scope.itemProps
}, () => {
return h(
QItemSection,
() => h(
QItemLabel,
() => h('span', {
[ scope.html === true ? 'innerHTML' : 'textContent' ]: scope.label
})
)
)
})
}
let options = padVirtualScroll('div', optionScope.value.map(fn))
if (slots[ 'before-options' ] !== void 0) {
options = slots[ 'before-options' ]().concat(options)
}
return hMergeSlot(slots[ 'after-options' ], options)
}
function getInput (fromDialog, isTarget) {
const attrs = isTarget === true ? { ...comboboxAttrs.value, ...state.splitAttrs.attributes.value } : void 0
const data = {
ref: isTarget === true ? targetRef : void 0,
key: 'i_t',
class: computedInputClass.value,
style: props.inputStyle,
value: inputValue.value !== void 0 ? inputValue.value : '',
// required for Android in order to show ENTER key when in form
type: 'search',
...attrs,
id: isTarget === true ? state.targetUid.value : void 0,
maxlength: props.maxlength,
autocomplete: props.autocomplete,
'data-autofocus': fromDialog === true || props.autofocus === true || void 0,
disabled: props.disable === true,
readonly: props.readonly === true,
...inputControlEvents.value
}
if (fromDialog !== true && hasDialog === true) {
if (Array.isArray(data.class) === true) {
data.class = [ ...data.class, 'no-pointer-events' ]
}
else {
data.class += ' no-pointer-events'
}
}
return h('input', data)
}
function onInput (e) {
if (filterTimer !== null) {
clearTimeout(filterTimer)
filterTimer = null
}
if (inputValueTimer !== null) {
clearTimeout(inputValueTimer)
inputValueTimer = null
}
if (e && e.target && e.target.qComposing === true) {
return
}
setInputValue(e.target.value || '')
// mark it here as user input so that if updateInputValue is called
// before filter is called the indicator is reset
userInputValue = true
defaultInputValue = inputValue.value
if (
state.focused.value !== true
&& (hasDialog !== true || dialogFieldFocused.value === true)
) {
state.focus()
}
if (props.onFilter !== void 0) {
filterTimer = setTimeout(() => {
filterTimer = null
filter(inputValue.value)
}, props.inputDebounce)
}
}
function setInputValue (val, emitImmediately) {
if (inputValue.value !== val) {
inputValue.value = val
if (emitImmediately === true || props.inputDebounce === 0 || props.inputDebounce === '0') {
emit('inputValue', val)
}
else {
inputValueTimer = setTimeout(() => {
inputValueTimer = null
emit('inputValue', val)
}, props.inputDebounce)
}
}
}
function updateInputValue (val, noFiltering, internal) {
userInputValue = internal !== true
if (props.useInput === true) {
setInputValue(val, true)
if (noFiltering === true || internal !== true) {
defaultInputValue = val
}
noFiltering !== true && filter(val)
}
}
function filter (val, keepClosed, afterUpdateFn) {
if (props.onFilter === void 0 || (keepClosed !== true && state.focused.value !== true)) {
return
}
if (state.innerLoading.value === true) {
emit('filterAbort')
}
else {
state.innerLoading.value = true
innerLoadingIndicator.value = true
}
if (
val !== ''
&& props.multiple !== true
&& innerValue.value.length !== 0
&& userInputValue !== true
&& val === getOptionLabel.value(innerValue.value[ 0 ])
) {
val = ''
}
const localFilterId = setTimeout(() => {
menu.value === true && (menu.value = false)
}, 10)
filterId !== null && clearTimeout(filterId)
filterId = localFilterId
emit(
'filter',
val,
(fn, afterFn) => {
if ((keepClosed === true || state.focused.value === true) && filterId === localFilterId) {
clearTimeout(filterId)
typeof fn === 'function' && fn()
// hide indicator to allow arrow to animate
innerLoadingIndicator.value = false
nextTick(() => {
state.innerLoading.value = false
if (state.editable.value === true) {
if (keepClosed === true) {
menu.value === true && hidePopup()
}
else if (menu.value === true) {
updateMenu(true)
}
else {
menu.value = true
}
}
typeof afterFn === 'function' && nextTick(() => { afterFn(proxy) })
typeof afterUpdateFn === 'function' && nextTick(() => { afterUpdateFn(proxy) })
})
}
},
() => {
if (state.focused.value === true && filterId === localFilterId) {
clearTimeout(filterId)
state.innerLoading.value = false
innerLoadingIndicator.value = false
}
menu.value === true && (menu.value = false)
}
)
}
function getMenu () {
return h(QMenu, {
ref: menuRef,
class: menuContentClass.value,
style: props.popupContentStyle,
modelValue: menu.value,
fit: props.menuShrink !== true,
cover: props.optionsCover === true && noOptions.value !== true && props.useInput !== true,
anchor: props.menuAnchor,
self: props.menuSelf,
offset: props.menuOffset,
dark: isOptionsDark.value,
noParentEvent: true,
noRefocus: true,
noFocus: true,
noRouteDismiss: props.popupNoRouteDismiss,
square: squaredMenu.value,
transitionShow: props.transitionShow,
transitionHide: props.transitionHide,
transitionDuration: props.transitionDuration,
separateClosePopup: true,
...listboxAttrs.value,
onScrollPassive: onVirtualScrollEvt,
onBeforeShow: onControlPopupShow,
onBeforeHide: onMenuBeforeHide,
onShow: onMenuShow
}, getAllOptions)
}
function onMenuBeforeHide (e) {
onControlPopupHide(e)
closeMenu()
}
function onMenuShow () {
setVirtualScrollSize()
}
function onDialogFieldFocus (e) {
stop(e)
targetRef.value !== null && targetRef.value.focus()
dialogFieldFocused.value = true
window.scrollTo(window.pageXOffset || window.scrollX || document.body.scrollLeft || 0, 0)
}
function onDialogFieldBlur (e) {
stop(e)
nextTick(() => {
dialogFieldFocused.value = false
})
}
function getDialog () {
const content = [
h(QField, {
class: `col-auto ${ state.fieldClass.value }`,
...innerFieldProps.value,
for: state.targetUid.value,
dark: isOptionsDark.value,
square: true,
loading: innerLoadingIndicator.value,
itemAligned: false,
filled: true,
stackLabel: inputValue.value.length !== 0,
...state.splitAttrs.listeners.value,
onFocus: onDialogFieldFocus,
onBlur: onDialogFieldBlur
}, {
...slots,
rawControl: () => state.getControl(true),
before: void 0,
after: void 0
})
]
menu.value === true && content.push(
h('div', {
ref: menuContentRef,
class: menuContentClass.value + ' scroll',
style: props.popupContentStyle,
...listboxAttrs.value,
onClick: prevent,
onScrollPassive: onVirtualScrollEvt
}, getAllOptions())
)
return h(QDialog, {
ref: dialogRef,
modelValue: dialog.value,
position: props.useInput === true ? 'top' : void 0,
transitionShow: transitionShowComputed,
transitionHide: props.transitionHide,
transitionDuration: props.transitionDuration,
noRouteDismiss: props.popupNoRouteDismiss,
onBeforeShow: onControlPopupShow,
onBeforeHide: onDialogBeforeHide,
onHide: onDialogHide,
onShow: onDialogShow
}, () => h('div', {
class: 'q-select__dialog'
+ (isOptionsDark.value === true ? ' q-select__dialog--dark q-dark' : '')
+ (dialogFieldFocused.value === true ? ' q-select__dialog--focused' : '')
}, content))
}
function onDialogBeforeHide (e) {
onControlPopupHide(e)
if (dialogRef.value !== null) {
dialogRef.value.__updateRefocusTarget(
state.rootRef.value.querySelector('.q-field__native > [tabindex]:last-child')
)
}
state.focused.value = false
}
function onDialogHide (e) {
hidePopup()
state.focused.value === false && emit('blur', e)
resetInputValue()
}
function onDialogShow () {
const el = document.activeElement
if (
(el === null || el.id !== state.targetUid.value)
&& targetRef.value !== null
&& targetRef.value !== el
) {
targetRef.value.focus()
}
setVirtualScrollSize()
}
function closeMenu () {
if (dialog.value === true) {
return
}
optionIndex.value = -1
if (menu.value === true) {
menu.value = false
}
if (state.focused.value === false) {
if (filterId !== null) {
clearTimeout(filterId)
filterId = null
}
if (state.innerLoading.value === true) {
emit('filterAbort')
state.innerLoading.value = false
innerLoadingIndicator.value = false
}
}
}
function showPopup (e) {
if (state.editable.value !== true) {
return
}
if (hasDialog === true) {
state.onControlFocusin(e)
dialog.value = true
nextTick(() => {
state.focus()
})
}
else {
state.focus()
}
if (props.onFilter !== void 0) {
filter(inputValue.value)
}
else if (noOptions.value !== true || slots[ 'no-option' ] !== void 0) {
menu.value = true
}
}
function hidePopup () {
dialog.value = false
closeMenu()
}
function resetInputValue () {
props.useInput === true && updateInputValue(
props.multiple !== true && props.fillInput === true && innerValue.value.length !== 0
? getOptionLabel.value(innerValue.value[ 0 ]) || ''
: '',
true,
true
)
}
function updateMenu (show) {
let optionIndex = -1
if (show === true) {
if (innerValue.value.length !== 0) {
const val = getOptionValue.value(innerValue.value[ 0 ])
optionIndex = props.options.findIndex(v => isDeepEqual(getOptionValue.value(v), val))
}
localResetVirtualScroll(optionIndex)
}
setOptionIndex(optionIndex)
}
function rerenderMenu (newLength, oldLength) {
if (menu.value === true && state.innerLoading.value === false) {
localResetVirtualScroll(-1, true)
nextTick(() => {
if (menu.value === true && state.innerLoading.value === false) {
if (newLength > oldLength) {
localResetVirtualScroll()
}
else {
updateMenu(true)
}
}
})
}
}
function updateMenuPosition () {
if (dialog.value === false && menuRef.value !== null) {
menuRef.value.updatePosition()
}
}
function onControlPopupShow (e) {
e !== void 0 && stop(e)
emit('popupShow', e)
state.hasPopupOpen = true
state.onControlFocusin(e)
}
function onControlPopupHide (e) {
e !== void 0 && stop(e)
emit('popupHide', e)
state.hasPopupOpen = false
state.onControlFocusout(e)
}
function updatePreState () {
hasDialog = $q.platform.is.mobile !== true && props.behavior !== 'dialog'
? false
: props.behavior !== 'menu' && (
props.useInput === true
? slots[ 'no-option' ] !== void 0 || props.onFilter !== void 0 || noOptions.value === false
: true
)
transitionShowComputed = $q.platform.is.ios === true && hasDialog === true && props.useInput === true
? 'fade'
: props.transitionShow
}
onBeforeUpdate(updatePreState)
onUpdated(updateMenuPosition)
updatePreState()
onBeforeUnmount(() => {
filterTimer !== null && clearTimeout(filterTimer)
inputValueTimer !== null && clearTimeout(inputValueTimer)
})
// expose public methods
Object.assign(proxy, {
showPopup, hidePopup,
removeAtIndex, add, toggleOption,
getOptionIndex: () => optionIndex.value,
setOptionIndex, moveOptionSelection,
filter, updateMenuPosition, updateInputValue,
isOptionSelected,
getEmittingOptionValue,
isOptionDisabled: (...args) => isOptionDisabled.value.apply(null, args) === true,
getOptionValue: (...args) => getOptionValue.value.apply(null, args),
getOptionLabel: (...args) => getOptionLabel.value.apply(null, args)
})
Object.assign(state, {
innerValue,
fieldClass: computed(() =>
`q-select q-field--auto-height q-select--with${ props.useInput !== true ? 'out' : '' }-input`
+ ` q-select--with${ props.useChips !== true ? 'out' : '' }-chips`
+ ` q-select--${ props.multiple === true ? 'multiple' : 'single' }`
),
inputRef,
targetRef,
hasValue,
showPopup,
floatingLabel: computed(() =>
(props.hideSelected !== true && hasValue.value === true)
|| typeof inputValue.value === 'number'
|| inputValue.value.length !== 0
|| fieldValueIsFilled(props.displayValue)
),
getControlChild: () => {
if (
state.editable.value !== false && (
dialog.value === true // dialog always has menu displayed, so need to render it
|| noOptions.value !== true
|| slots[ 'no-option' ] !== void 0
)
) {
return hasDialog === true ? getDialog() : getMenu()
}
else if (state.hasPopupOpen === true) {
// explicitly set it otherwise TAB will not blur component
state.hasPopupOpen = false
}
},
controlEvents: {
onFocusin (e) { state.onControlFocusin(e) },
onFocusout (e) {
state.onControlFocusout(e, () => {
resetInputValue()
closeMenu()
})
},
onClick (e) {
// label from QField will propagate click on the input
prevent(e)
if (hasDialog !== true && menu.value === true) {
closeMenu()
targetRef.value !== null && targetRef.value.focus()
return
}
showPopup(e)
}
},
getControl: fromDialog => {
const child = getSelection()
const isTarget = fromDialog === true || dialog.value !== true || hasDialog !== true
if (props.useInput === true) {
child.push(getInput(fromDialog, isTarget))
}
// there can be only one (when dialog is opened the control in dialog should be target)
else if (state.editable.value === true) {
const attrs = isTarget === true ? comboboxAttrs.value : void 0
child.push(
h('input', {
ref: isTarget === true ? targetRef : void 0,
key: 'd_t',
class: 'q-select__focus-target',
id: isTarget === true ? state.targetUid.value : void 0,
value: ariaCurrentValue.value,
readonly: true,
'data-autofocus': fromDialog === true || props.autofocus === true || void 0,
...attrs,
onKeydown: onTargetKeydown,
onKeyup: onTargetKeyup,
onKeypress: onTargetKeypress
})
)
if (isTarget === true && typeof props.autocomplete === 'string' && props.autocomplete.length !== 0) {
child.push(
h('input', {
class: 'q-select__autocomplete-input',
autocomplete: props.autocomplete,
tabindex: -1,
onKeyup: onTargetAutocomplete
})
)
}
}
if (nameProp.value !== void 0 && props.disable !== true && innerOptionsValue.value.length !== 0) {
const opts = innerOptionsValue.value.map(value => h('option', { value, selected: true }))
child.push(
h('select', {
class: 'hidden',
name: nameProp.value,
multiple: props.multiple
}, opts)
)
}
const attrs = props.useInput === true || isTarget !== true ? void 0 : state.splitAttrs.attributes.value
return h('div', {
class: 'q-field__native row items-center',
...attrs,
...state.splitAttrs.listeners.value
}, child)
},
getInnerAppend: () => (
props.loading !== true && innerLoadingIndicator.value !== true && props.hideDropdownIcon !== true
? [
h(QIcon, {
class: 'q-select__dropdown-icon' + (menu.value === true ? ' rotate-180' : ''),
name: dropdownArrowIcon.value
})
]
: null
)
})
return useField(state)
}
})