UNPKG

vxe-pc-ui

Version:
1,447 lines (1,348 loc) 60 kB
import { h, Teleport, PropType, ref, inject, computed, provide, onUnmounted, reactive, nextTick, watch, onMounted } from 'vue' import { defineVxeComponent } from '../../ui/src/comp' import XEUtils from 'xe-utils' import { VxeUI, getConfig, getIcon, getI18n, globalEvents, GLOBAL_EVENT_KEYS, createEvent, useSize, renderEmptyElement } from '../../ui' import { getEventTargetNode, toCssUnit, updatePanelPlacement } from '../../ui/src/dom' import { getLastZIndex, nextZIndex, getFuncText, eqEmptyValue } from '../../ui/src/utils' import { getSlotVNs } from '../../ui/src/vn' import { errLog } from '../../ui/src/log' import VxeInputComponent from '../../input/src/input' import VxeButtonComponent from '../../button/src/button' import type { VxeSelectPropTypes, VxeSelectConstructor, SelectInternalData, SelectReactData, VxeSelectDefines, VxeButtonEvents, ValueOf, VxeSelectEmits, VxeComponentSlotType, VxeInputConstructor, SelectMethods, SelectPrivateRef, VxeSelectMethods, VxeOptionProps, VxeDrawerConstructor, VxeDrawerMethods, VxeFormDefines, VxeFormConstructor, VxeFormPrivateMethods, VxeModalConstructor, VxeModalMethods, VxeInputEvents, VxeComponentStyleType } from '../../../types' import type { VxeTableConstructor, VxeTablePrivateMethods } from '../../../types/components/table' function isOptionVisible (option: any) { return option.visible !== false } function getOptUniqueId () { return XEUtils.uniqueId('opt_') } function createInternalData (): SelectInternalData { return { // isLoaded: false, synchData: [], fullData: [], afterVisibleList: [], optAddMaps: {}, optGroupKeyMaps: {}, optFullValMaps: {}, remoteValMaps: {}, lastScrollLeft: 0, lastScrollTop: 0, scrollYStore: { startIndex: 0, endIndex: 0, visibleSize: 0, offsetSize: 0, rowHeight: 0 }, lastScrollTime: 0, hpTimeout: undefined } } export default defineVxeComponent({ name: 'VxeSelect', props: { modelValue: [String, Number, Boolean, Array] as PropType<VxeSelectPropTypes.ModelValue>, defaultConfig: Object as PropType<VxeSelectPropTypes.DefaultConfig>, clearable: Boolean as PropType<VxeSelectPropTypes.Clearable>, placeholder: String as PropType<VxeSelectPropTypes.Placeholder>, readonly: { type: Boolean as PropType<VxeSelectPropTypes.Readonly>, default: null }, loading: Boolean as PropType<VxeSelectPropTypes.Loading>, disabled: { type: Boolean as PropType<VxeSelectPropTypes.Disabled>, default: null }, multiple: Boolean as PropType<VxeSelectPropTypes.Multiple>, multiCharOverflow: { type: [Number, String] as PropType<VxeSelectPropTypes.MultiCharOverflow>, default: () => getConfig().select.multiCharOverflow }, prefixIcon: String as PropType<VxeSelectPropTypes.PrefixIcon>, allowCreate: { type: Boolean as PropType<VxeSelectPropTypes.AllowCreate>, default: () => getConfig().select.allowCreate }, placement: String as PropType<VxeSelectPropTypes.Placement>, lazyOptions: Array as PropType<VxeSelectPropTypes.LazyOptions>, options: Array as PropType<VxeSelectPropTypes.Options>, optionProps: Object as PropType<VxeSelectPropTypes.OptionProps>, optionGroups: Array as PropType<VxeSelectPropTypes.OptionGroups>, optionGroupProps: Object as PropType<VxeSelectPropTypes.OptionGroupProps>, optionConfig: Object as PropType<VxeSelectPropTypes.OptionConfig>, className: [String, Function] as PropType<VxeSelectPropTypes.ClassName>, /** * 已废弃,请使用 popupConfig.className * @deprecated */ popupClassName: [String, Function] as PropType<VxeSelectPropTypes.PopupClassName>, max: { type: [String, Number] as PropType<VxeSelectPropTypes.Max>, default: null }, /** * 已废弃,请使用 popupConfig.zIndex * @deprecated */ zIndex: Number as PropType<VxeSelectPropTypes.ZIndex>, size: { type: String as PropType<VxeSelectPropTypes.Size>, default: () => getConfig().select.size || getConfig().size }, filterable: Boolean as PropType<VxeSelectPropTypes.Filterable>, /** * 已废弃,被 filter-config.filterMethod 替换 * @deprecated */ filterMethod: Function as PropType<VxeSelectPropTypes.FilterMethod>, filterConfig: Object as PropType<VxeSelectPropTypes.FilterConfig>, remote: Boolean as PropType<VxeSelectPropTypes.Remote>, remoteConfig: Object as PropType<VxeSelectPropTypes.RemoteConfig>, emptyText: String as PropType<VxeSelectPropTypes.EmptyText>, showTotalButoon: { type: Boolean as PropType<VxeSelectPropTypes.ShowTotalButoon>, default: () => getConfig().select.showTotalButoon }, showCheckedButoon: { type: Boolean as PropType<VxeSelectPropTypes.ShowCheckedButoon>, default: () => getConfig().select.showCheckedButoon }, showClearButton: { type: Boolean as PropType<VxeSelectPropTypes.ShowClearButton>, default: () => getConfig().select.showClearButton }, transfer: { type: Boolean as PropType<VxeSelectPropTypes.Transfer>, default: null }, popupConfig: Object as PropType<VxeSelectPropTypes.PopupConfig>, virtualYConfig: Object as PropType<VxeSelectPropTypes.VirtualYConfig>, scrollY: Object as PropType<VxeSelectPropTypes.ScrollY>, /** * 已废弃,被 remote-config.queryMethod 替换 * @deprecated */ remoteMethod: Function as PropType<VxeSelectPropTypes.RemoteMethod>, /** * 已废弃,被 option-config.keyField 替换 * @deprecated */ optionId: { type: String as PropType<VxeSelectPropTypes.OptionId>, default: () => getConfig().select.optionId }, /** * 已废弃,被 option-config.useKey 替换 * @deprecated */ optionKey: Boolean as PropType<VxeSelectPropTypes.OptionKey> }, emits: [ 'update:modelValue', 'change', 'default-change', 'all-change', 'clear', 'blur', 'focus', 'click', 'scroll', 'visible-change' ] as VxeSelectEmits, setup (props, context) { const { slots, emit } = context const $xeModal = inject<(VxeModalConstructor & VxeModalMethods)| null>('$xeModal', null) const $xeDrawer = inject<(VxeDrawerConstructor & VxeDrawerMethods) | null>('$xeDrawer', null) const $xeTable = inject<(VxeTableConstructor & VxeTablePrivateMethods) | null>('$xeTable', null) const $xeForm = inject<(VxeFormConstructor & VxeFormPrivateMethods)| null>('$xeForm', null) const formItemInfo = inject<VxeFormDefines.ProvideItemInfo | null>('xeFormItemInfo', null) const xID = XEUtils.uniqueId() const refElem = ref<HTMLDivElement>() const refInput = ref<VxeInputConstructor>() const refInpSearch = ref<VxeInputConstructor>() const refVirtualWrapper = ref<HTMLDivElement>() const refOptionPanel = ref<HTMLDivElement>() const refVirtualBody = ref<HTMLDivElement>() const { computeSize } = useSize(props) const reactData = reactive<SelectReactData>({ initialized: false, scrollYLoad: false, bodyHeight: 0, topSpaceHeight: 0, optList: [], staticOptions: [], reactFlag: 0, currentOption: null, searchValue: '', searchLoading: false, panelIndex: 0, panelStyle: {}, panelPlacement: null, triggerFocusPanel: false, visiblePanel: false, isAniVisible: false, isActivated: false }) const internalData = createInternalData() const refMaps: SelectPrivateRef = { refElem } const $xeSelect = { xID, props, context, reactData, internalData, getRefMaps: () => refMaps } as unknown as VxeSelectConstructor & VxeSelectMethods const computeFormReadonly = computed(() => { const { readonly } = props if (readonly === null) { if ($xeForm) { return $xeForm.props.readonly } return false } return readonly }) const computeIsDisabled = computed(() => { const { disabled } = props if (disabled === null) { if ($xeForm) { return $xeForm.props.disabled } return false } return disabled }) const computeBtnTransfer = computed(() => { const { transfer } = props const popupOpts = computePopupOpts.value if (XEUtils.isBoolean(popupOpts.transfer)) { return popupOpts.transfer } if (transfer === null) { const globalTransfer = getConfig().select.transfer if (XEUtils.isBoolean(globalTransfer)) { return globalTransfer } if ($xeTable || $xeModal || $xeDrawer || $xeForm) { return true } } return transfer }) const computeInpPlaceholder = computed(() => { const { placeholder } = props if (placeholder) { return getFuncText(placeholder) } const globalPlaceholder = getConfig().select.placeholder if (globalPlaceholder) { return getFuncText(globalPlaceholder) } return getI18n('vxe.base.pleaseSelect') }) const computeDefaultOpts = computed(() => { return Object.assign({}, props.defaultConfig) }) const computePropsOpts = computed(() => { return Object.assign({}, props.optionProps) }) const computeGroupPropsOpts = computed(() => { return Object.assign({}, props.optionGroupProps) }) const computeLabelField = computed(() => { const propsOpts = computePropsOpts.value return propsOpts.label || 'label' }) const computeValueField = computed(() => { const propsOpts = computePropsOpts.value return propsOpts.value || 'value' }) const computeGroupLabelField = computed(() => { const groupPropsOpts = computeGroupPropsOpts.value return groupPropsOpts.label || 'label' }) const computeGroupOptionsField = computed(() => { const groupPropsOpts = computeGroupPropsOpts.value return groupPropsOpts.options || 'options' }) const computeIsMaximize = computed(() => { const selectVals = computeSelectVals.value return checkMaxLimit(selectVals) }) const computePopupOpts = computed(() => { return Object.assign({}, getConfig().select.popupConfig, props.popupConfig) }) const computeVirtualYOpts = computed(() => { return Object.assign({} as { gt: number }, getConfig().select.virtualYConfig || getConfig().select.scrollY, props.virtualYConfig || props.scrollY) }) const computeRemoteOpts = computed(() => { return Object.assign({}, getConfig().select.remoteConfig, props.remoteConfig) }) const computeFilterOpts = computed(() => { return Object.assign({}, getConfig().select.filterConfig, props.filterConfig) }) const computeOptionOpts = computed(() => { return Object.assign({}, getConfig().select.optionConfig, props.optionConfig) }) const computeMultiMaxCharNum = computed(() => { return XEUtils.toNumber(props.multiCharOverflow) }) const computePopupWrapperStyle = computed(() => { const popupOpts = computePopupOpts.value const { height, width } = popupOpts const stys: VxeComponentStyleType = {} if (width) { stys.width = toCssUnit(width) } if (height) { stys.height = toCssUnit(height) stys.maxHeight = toCssUnit(height) } return stys }) const computeSelectVals = computed(() => { const { modelValue, multiple } = props let vals: (string | number | boolean)[] = [] if (XEUtils.isArray(modelValue)) { vals = modelValue } else { if (multiple) { if (!eqEmptyValue(modelValue)) { vals = `${modelValue}`.indexOf(',') > -1 ? `${modelValue}`.split(',') : [modelValue] as any[] } } else { vals = modelValue === null || modelValue === undefined ? [] : [modelValue] } } return vals }) const computeFullLabel = computed(() => { const { remote } = props const { reactFlag } = reactData const selectVals = computeSelectVals.value if (remote && reactFlag) { return selectVals.map(val => getRemoteSelectLabel(val)).join(', ') } return selectVals.map((val) => getSelectLabel(val)).join(', ') }) const computeSelectLabel = computed(() => { const { remote, multiple } = props const { reactFlag } = reactData const multiMaxCharNum = computeMultiMaxCharNum.value const selectVals = computeSelectVals.value if (remote && reactFlag) { return selectVals.map(val => getRemoteSelectLabel(val)).join(', ') } const labels = selectVals.map((val) => getSelectLabel(val)) if (multiple && multiMaxCharNum > 0 && labels.length > multiMaxCharNum) { return `${labels.slice(0, multiMaxCharNum)}...` } return labels.join(', ') }) const callSlot = <T>(slotFunc: ((params: T) => VxeComponentSlotType | VxeComponentSlotType[]) | string | null, params: T) => { if (slotFunc) { if (XEUtils.isString(slotFunc)) { slotFunc = slots[slotFunc] || null } if (XEUtils.isFunction(slotFunc)) { return getSlotVNs(slotFunc(params)) } } return [] } const dispatchEvent = (type: ValueOf<VxeSelectEmits>, params: Record<string, any>, evnt: Event | null) => { emit(type, createEvent(evnt, { $select: $xeSelect }, params)) } const emitModel = (value: any) => { emit('update:modelValue', value) } const emitDefaultValue = (value: any) => { emitModel(value) dispatchEvent('default-change', { value }, null) } const getOptKey = () => { const optionOpts = computeOptionOpts.value return optionOpts.keyField || props.optionId || '_X_OPTION_KEY' } const getOptId = (option: any) => { const optid = option[getOptKey()] return optid ? encodeURIComponent(optid) : '' } const checkMaxLimit = (selectVals: VxeSelectPropTypes.ModelValue[]) => { const { multiple, max } = props if (multiple && max) { return selectVals.length >= XEUtils.toNumber(max) } return false } const getRemoteSelectLabel = (value: any) => { const { lazyOptions } = props const { remoteValMaps, optFullValMaps } = internalData const valueField = computeValueField.value const labelField = computeLabelField.value const remoteItem = remoteValMaps[value] || optFullValMaps[value] const item = remoteItem ? remoteItem.item : null if (item) { return XEUtils.toValueString(item[labelField]) } if (lazyOptions) { const lazyItem = lazyOptions.find(item => item[valueField] === value) if (lazyItem) { return lazyItem[labelField] } } return value } const getSelectLabel = (value: any) => { const { lazyOptions } = props const { optFullValMaps } = internalData const valueField = computeValueField.value const labelField = computeLabelField.value const cacheItem = reactData.reactFlag ? optFullValMaps[value] : null if (cacheItem) { return cacheItem.item[labelField as 'label'] } if (lazyOptions) { const lazyItem = lazyOptions.find(item => item[valueField] === value) if (lazyItem) { return lazyItem[labelField] } } return value } const cacheItemMap = (datas: any[]) => { const groupOptionsField = computeGroupOptionsField.value const valueField = computeValueField.value const key = getOptKey() const groupKeyMaps: Record<string, any> = {} const fullKeyMaps: Record<string, VxeSelectDefines.OptCacheItem> = {} const list: any[] = [] const handleOptItem = (item: any) => { list.push(item) let optid = getOptId(item) if (!optid) { optid = getOptUniqueId() item[key] = optid } fullKeyMaps[item[valueField]] = { key: optid, item, _index: -1 } } datas.forEach((group: any) => { handleOptItem(group) if (group[groupOptionsField]) { groupKeyMaps[group[key]] = group group[groupOptionsField].forEach(handleOptItem) } }) internalData.fullData = list internalData.optGroupKeyMaps = groupKeyMaps internalData.optFullValMaps = fullKeyMaps reactData.reactFlag++ handleOption() } /** * 处理选项,当选项被动态显示/隐藏时可能会用到 */ const handleOption = () => { const { remote, modelValue, filterable } = props const { searchValue } = reactData const { fullData, optFullValMaps } = internalData const labelField = computeLabelField.value const valueField = computeValueField.value const filterOpts = computeFilterOpts.value const frMethod = filterOpts.filterMethod || props.filterMethod const searchStr = `${searchValue || ''}`.toLowerCase() let avList: any[] = [] if (remote) { avList = fullData.filter(isOptionVisible) } else { if (filterable && frMethod) { avList = fullData.filter(option => isOptionVisible(option) && frMethod({ $select: $xeSelect, group: null, option, searchValue, value: modelValue })) } else if (filterable) { avList = fullData.filter(option => isOptionVisible(option) && (!searchStr || `${option[labelField] || option[valueField]}`.toLowerCase().indexOf(searchStr) > -1)) } else { avList = fullData.filter(isOptionVisible) } } avList.forEach((item, index) => { const cacheItem = optFullValMaps[item[valueField]] if (cacheItem) { cacheItem._index = index } }) internalData.afterVisibleList = avList return nextTick() } const setCurrentOption = (option: any) => { if (option) { reactData.currentOption = option } } const updateZIndex = () => { const popupOpts = computePopupOpts.value const customZIndex = popupOpts.zIndex || props.zIndex if (customZIndex) { reactData.panelIndex = XEUtils.toNumber(customZIndex) } else if (reactData.panelIndex < getLastZIndex()) { reactData.panelIndex = nextZIndex() } } const updatePlacement = () => { const { placement } = props const { panelIndex } = reactData const targetElem = refElem.value const panelElem = refOptionPanel.value const btnTransfer = computeBtnTransfer.value const popupOpts = computePopupOpts.value const handleStyle = () => { const ppObj = updatePanelPlacement(targetElem, panelElem, { placement: popupOpts.placement || placement, defaultPlacement: popupOpts.defaultPlacement, teleportTo: btnTransfer }) const panelStyle: { [key: string]: string | number } = Object.assign(ppObj.style, { zIndex: panelIndex }) reactData.panelStyle = panelStyle reactData.panelPlacement = ppObj.placement } handleStyle() return nextTick().then(handleStyle) } const handleScrollSelect = () => { nextTick(() => { const { isAniVisible, visiblePanel } = reactData const { optFullValMaps } = internalData const selectVals = computeSelectVals.value if (selectVals.length && isAniVisible && visiblePanel) { const cacheItem = reactData.reactFlag ? optFullValMaps[`${selectVals[0]}`] : null if (cacheItem) { handleScrollToOption(cacheItem.item) } } }) } const showOptionPanel = () => { const { loading, filterable, remote } = props const { fullData, hpTimeout } = internalData const isDisabled = computeIsDisabled.value const remoteOpts = computeRemoteOpts.value if (!loading && !isDisabled) { if (hpTimeout) { clearTimeout(hpTimeout) internalData.hpTimeout = undefined } if (!reactData.initialized) { reactData.initialized = true } reactData.isActivated = true reactData.isAniVisible = true if (filterable) { if (remote && remoteOpts.enabled && ((remoteOpts.autoLoad && !fullData.length) || (fullData.length && remoteOpts.clearOnClose))) { handleSearchEvent() } else { handleOption() updateYData() } } setTimeout(() => { reactData.visiblePanel = true handleFocusSearch() recalculate().then(() => { handleScrollSelect() refreshScroll() }) updatePlacement() }, 10) setTimeout(() => { recalculate().then(() => refreshScroll()) }, 100) updateZIndex() updatePlacement() dispatchEvent('visible-change', { visible: true }, null) } } const hideOptionPanel = () => { const { filterable, remote } = props const filterOpts = computeFilterOpts.value const remoteOpts = computeRemoteOpts.value if (remote) { if (remoteOpts.clearOnClose) { reactData.searchValue = '' } } else if (filterable) { if (filterOpts.clearOnClose) { reactData.searchValue = '' } } else { reactData.searchValue = '' } reactData.searchLoading = false reactData.visiblePanel = false internalData.hpTimeout = setTimeout(() => { reactData.isAniVisible = false }, 350) dispatchEvent('visible-change', { visible: false }, null) } const changeEvent = (evnt: Event, selectValue: any, option: any) => { emitModel(selectValue) if (selectValue !== props.modelValue) { dispatchEvent('change', { value: selectValue, option }, evnt) // 自动更新校验状态 if ($xeForm && formItemInfo) { $xeForm.triggerItemEvent(evnt, formItemInfo.itemConfig.field, selectValue) } } } const clearValueEvent = (evnt: Event, selectValue: any) => { internalData.remoteValMaps = {} changeEvent(evnt, selectValue, null) dispatchEvent('clear', { value: selectValue }, evnt) } const clearEvent: VxeInputEvents.Clear = (params) => { const { $event } = params clearValueEvent($event, null) hideOptionPanel() } const allCheckedPanelEvent: VxeButtonEvents.Click = (params) => { const { $event } = params const { multiple, max } = props const { optList } = reactData const valueField = computeValueField.value if (multiple) { const selectVals = computeSelectVals.value const currVlas = selectVals.slice(0) for (let i = 0; i < optList.length; i++) { const option = optList[i] const selectValue = option[valueField] // 检测是否超过最大可选数量 if (checkMaxLimit(currVlas)) { if (VxeUI) { VxeUI.modal.message({ content: getI18n('vxe.select.overSizeErr', [max]), status: 'warning' }) } break } if (!currVlas.some(val => val === selectValue)) { currVlas.push(selectValue) } } changeEvent($event, currVlas, optList[0]) dispatchEvent('all-change', { value: currVlas }, $event) } } const clearCheckedPanelEvent: VxeButtonEvents.Click = (params) => { const { $event } = params clearValueEvent($event, null) hideOptionPanel() } const changeOptionEvent = (evnt: Event, option: any) => { const { multiple } = props const { remoteValMaps } = internalData const valueField = computeValueField.value const selectValue = option[valueField] const remoteItem = remoteValMaps[selectValue] if (!reactData.visiblePanel) { return } if (remoteItem) { remoteItem.item = option } else { remoteValMaps[selectValue] = { key: getOptId(option), item: option, _index: -1 } } if (multiple) { let multipleValue: any[] = [] const selectVals = computeSelectVals.value const index = XEUtils.findIndexOf(selectVals, val => val === selectValue) if (index === -1) { multipleValue = selectVals.concat([selectValue]) } else { multipleValue = selectVals.filter((val) => val !== selectValue) } changeEvent(evnt, multipleValue, option) } else { changeEvent(evnt, selectValue, option) hideOptionPanel() } reactData.reactFlag++ } const handleGlobalMousewheelEvent = (evnt: MouseEvent) => { const { visiblePanel } = reactData const isDisabled = computeIsDisabled.value if (!isDisabled) { if (visiblePanel) { const panelElem = refOptionPanel.value if (getEventTargetNode(evnt, panelElem).flag) { updatePlacement() } else { hideOptionPanel() } } } } const handleGlobalMousedownEvent = (evnt: MouseEvent) => { const { visiblePanel } = reactData const isDisabled = computeIsDisabled.value const popupOpts = computePopupOpts.value const { trigger } = popupOpts if (!isDisabled) { const el = refElem.value const panelElem = refOptionPanel.value reactData.isActivated = getEventTargetNode(evnt, el).flag || getEventTargetNode(evnt, panelElem).flag if (visiblePanel && !reactData.isActivated) { if (trigger !== 'manual') { hideOptionPanel() } } } } const validOffsetOption = (option: any) => { const isDisabled = option.disabled const optid = getOptId(option) if (!isDisabled && !hasOptGroupById(optid)) { return true } return false } const findOffsetOption = (option: any, isDwArrow: boolean) => { const { allowCreate } = props const { optList } = reactData const { optFullValMaps, optAddMaps, afterVisibleList } = internalData const valueField = computeValueField.value let fullList = afterVisibleList let offsetAddIndex = 0 if (allowCreate && optList.length) { const firstItem = optList[0] const optid = getOptId(firstItem) if (optAddMaps[optid]) { offsetAddIndex = 1 fullList = [optAddMaps[optid]].concat(fullList) } } if (!option) { if (isDwArrow) { for (let i = 0; i < fullList.length; i++) { const item = fullList[i] if (validOffsetOption(item)) { return item } } } else { for (let len = fullList.length - 1; len >= 0; len--) { const item = fullList[len] if (validOffsetOption(item)) { return item } } } } let avIndex = 0 const cacheItem = option ? optFullValMaps[option[valueField]] : null if (cacheItem) { avIndex = cacheItem._index + offsetAddIndex } if (avIndex > -1) { if (isDwArrow) { for (let i = avIndex + 1; i <= fullList.length - 1; i++) { const item = fullList[i] if (validOffsetOption(item)) { return item } } } else { if (avIndex > 0) { for (let len = avIndex - 1; len >= 0; len--) { const item = fullList[len] if (validOffsetOption(item)) { return item } } } } } return null } const handleGlobalKeydownEvent = (evnt: KeyboardEvent) => { const { clearable } = props const { visiblePanel, currentOption } = reactData const isDisabled = computeIsDisabled.value const popupOpts = computePopupOpts.value const { trigger } = popupOpts if (!isDisabled) { const isTab = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.TAB) const isEnter = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ENTER) const isEsc = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ESCAPE) const isUpArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_UP) const isDwArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_DOWN) const isDel = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.DELETE) if (isTab) { reactData.isActivated = false } if (visiblePanel) { if (isEsc || isTab) { if (trigger !== 'manual') { hideOptionPanel() } } else if (isEnter) { if (currentOption) { evnt.preventDefault() evnt.stopPropagation() changeOptionEvent(evnt, currentOption) } } else if (isUpArrow || isDwArrow) { evnt.preventDefault() let offsetOption = findOffsetOption(currentOption, isDwArrow) // 如果不匹配,默认最接近一个 if (!offsetOption) { offsetOption = findOffsetOption(null, isDwArrow) } if (offsetOption) { setCurrentOption(offsetOption) handleScrollToOption(offsetOption, isDwArrow) } } } else if ((isUpArrow || isDwArrow || isEnter) && reactData.isActivated) { evnt.preventDefault() showOptionPanel() } if (reactData.isActivated) { if (isDel && clearable) { clearValueEvent(evnt, null) } } } } const handleGlobalBlurEvent = () => { const { visiblePanel, isActivated } = reactData const popupOpts = computePopupOpts.value const { trigger } = popupOpts if (visiblePanel) { if (trigger !== 'manual') { hideOptionPanel() } } if (isActivated) { reactData.isActivated = false } if (visiblePanel || isActivated) { const $input = refInput.value if ($input) { $input.blur() } } } const handleGlobalResizeEvent = () => { const { visiblePanel } = reactData if (visiblePanel) { updatePlacement() } } const handleFocusSearch = () => { if (props.filterable) { nextTick(() => { const inpSearch = refInpSearch.value if (inpSearch) { inpSearch.focus() } }) } } const focusEvent = (evnt: FocusEvent) => { const isDisabled = computeIsDisabled.value const popupOpts = computePopupOpts.value const { trigger } = popupOpts if (!isDisabled) { if (!reactData.visiblePanel) { if (!trigger || trigger === 'default') { reactData.triggerFocusPanel = true showOptionPanel() setTimeout(() => { reactData.triggerFocusPanel = false }, 500) } } } dispatchEvent('focus', {}, evnt) } const clickEvent = (evnt: MouseEvent) => { const popupOpts = computePopupOpts.value const { trigger } = popupOpts if (!trigger || trigger === 'default') { togglePanelEvent(evnt) } dispatchEvent('click', { triggerButton: false, visible: reactData.visiblePanel }, evnt) } const blurEvent = (evnt: FocusEvent) => { reactData.isActivated = false dispatchEvent('blur', {}, evnt) } const suffixClickEvent = (evnt: MouseEvent) => { const popupOpts = computePopupOpts.value const { trigger } = popupOpts if (!trigger || trigger === 'default' || trigger === 'icon') { togglePanelEvent(evnt) } dispatchEvent('click', { triggerButton: true, visible: reactData.visiblePanel }, evnt) } const modelSearchEvent = (value: string) => { reactData.searchValue = value } const focusSearchEvent = () => { reactData.isActivated = true } const handleSearchEvent = () => { const { modelValue, remote } = props const { searchValue } = reactData const remoteOpts = computeRemoteOpts.value const qyMethod = remoteOpts.queryMethod || props.remoteMethod if (remote && qyMethod && remoteOpts.enabled) { reactData.searchLoading = true Promise.resolve( qyMethod({ $select: $xeSelect, searchValue, value: modelValue }) ).then(() => nextTick()) .catch(() => nextTick()) .finally(() => { reactData.searchLoading = false handleOption() updateYData() }) } else { handleOption() updateYData() } } const triggerSearchEvent = XEUtils.debounce(handleSearchEvent, 350, { trailing: true }) const togglePanelEvent = (params: any) => { const { $event } = params $event.preventDefault() if (reactData.triggerFocusPanel) { reactData.triggerFocusPanel = false } else { if (reactData.visiblePanel) { hideOptionPanel() } else { showOptionPanel() } } } const checkOptionDisabled = (isSelected: any, option: VxeOptionProps) => { if (option.disabled) { return true } const isMaximize = computeIsMaximize.value if (isMaximize && !isSelected) { return true } return false } const updateYSpace = () => { const { scrollYLoad } = reactData const { scrollYStore, afterVisibleList } = internalData reactData.bodyHeight = scrollYLoad ? afterVisibleList.length * scrollYStore.rowHeight : 0 reactData.topSpaceHeight = scrollYLoad ? Math.max(scrollYStore.startIndex * scrollYStore.rowHeight, 0) : 0 } const handleData = () => { const { filterable, allowCreate } = props const { scrollYLoad, searchValue } = reactData const { optAddMaps, scrollYStore, afterVisibleList } = internalData const labelField = computeLabelField.value const valueField = computeValueField.value const restList = scrollYLoad ? afterVisibleList.slice(scrollYStore.startIndex, scrollYStore.endIndex) : afterVisibleList.slice(0) if (filterable && allowCreate && searchValue) { if (!restList.some(option => option[labelField] === searchValue)) { const addItem = optAddMaps[searchValue] || reactive({ [getOptKey()]: searchValue, [labelField]: searchValue, [valueField]: searchValue }) optAddMaps[searchValue] = addItem restList.unshift(addItem) } } reactData.optList = restList return nextTick() } const updateYData = () => { handleData() updateYSpace() } const computeScrollLoad = () => { return nextTick().then(() => { const { scrollYLoad } = reactData const { scrollYStore } = internalData const virtualBodyElem = refVirtualBody.value const virtualYOpts = computeVirtualYOpts.value let rowHeight = 0 let firstItemElem: HTMLElement | undefined if (virtualBodyElem) { if (!firstItemElem) { firstItemElem = virtualBodyElem.children[0] as HTMLElement } } if (firstItemElem) { rowHeight = firstItemElem.offsetHeight } rowHeight = Math.max(20, rowHeight) scrollYStore.rowHeight = rowHeight // 计算 Y 逻辑 if (scrollYLoad) { const scrollBodyElem = refVirtualWrapper.value const visibleYSize = Math.max(8, scrollBodyElem ? Math.ceil(scrollBodyElem.clientHeight / rowHeight) : 0) const offsetYSize = Math.max(0, Math.min(2, XEUtils.toNumber(virtualYOpts.oSize))) scrollYStore.offsetSize = offsetYSize scrollYStore.visibleSize = visibleYSize scrollYStore.endIndex = Math.max(scrollYStore.startIndex, visibleYSize + offsetYSize, scrollYStore.endIndex) updateYData() } else { updateYSpace() } }) } const handleScrollToOption = (option: any, isDwArrow?: boolean) => { const { scrollYLoad } = reactData const { optFullValMaps, scrollYStore } = internalData const valueField = computeValueField.value const cacheItem = optFullValMaps[option[valueField]] if (cacheItem) { const optid = cacheItem.key const avIndex = cacheItem._index if (avIndex > -1) { const optWrapperElem = refVirtualWrapper.value const panelElem = refOptionPanel.value if (!panelElem) { return } const optElem = panelElem.querySelector(`[optid='${optid}']`) as HTMLElement if (optWrapperElem) { if (optElem) { const wrapperHeight = optWrapperElem.offsetHeight const offsetPadding = 1 if (isDwArrow) { if (optElem.offsetTop + optElem.offsetHeight - optWrapperElem.scrollTop > wrapperHeight) { optWrapperElem.scrollTop = optElem.offsetTop + optElem.offsetHeight - wrapperHeight } else if (optElem.offsetTop + offsetPadding < optWrapperElem.scrollTop || optElem.offsetTop + offsetPadding > optWrapperElem.scrollTop + optWrapperElem.clientHeight) { optWrapperElem.scrollTop = optElem.offsetTop - offsetPadding } } else { if (optElem.offsetTop + offsetPadding < optWrapperElem.scrollTop || optElem.offsetTop + offsetPadding > optWrapperElem.scrollTop + optWrapperElem.clientHeight) { optWrapperElem.scrollTop = optElem.offsetTop - offsetPadding } else if (optElem.offsetTop + optElem.offsetHeight - optWrapperElem.scrollTop > wrapperHeight) { optWrapperElem.scrollTop = optElem.offsetTop + optElem.offsetHeight - wrapperHeight } } } else if (scrollYLoad) { if (isDwArrow) { optWrapperElem.scrollTop = avIndex * scrollYStore.rowHeight - optWrapperElem.clientHeight + scrollYStore.rowHeight } else { optWrapperElem.scrollTop = avIndex * scrollYStore.rowHeight } } } } } } /** * 如果有滚动条,则滚动到对应的位置 * @param {Number} scrollLeft 左距离 * @param {Number} scrollTop 上距离 */ const scrollTo = (scrollLeft: number | null, scrollTop?: number | null) => { const scrollBodyElem = refVirtualWrapper.value if (scrollBodyElem) { if (XEUtils.isNumber(scrollLeft)) { scrollBodyElem.scrollLeft = scrollLeft } if (XEUtils.isNumber(scrollTop)) { scrollBodyElem.scrollTop = scrollTop } } if (reactData.scrollYLoad) { return new Promise<void>(resolve => { setTimeout(() => { nextTick(() => { resolve() }) }, 50) }) } return nextTick() } /** * 刷新滚动条 */ const refreshScroll = () => { const { lastScrollLeft, lastScrollTop } = internalData return clearScroll().then(() => { if (lastScrollLeft || lastScrollTop) { internalData.lastScrollLeft = 0 internalData.lastScrollTop = 0 return scrollTo(lastScrollLeft, lastScrollTop) } }) } /** * 重新计算列表 */ const recalculate = () => { const el = refElem.value if (el && el.clientWidth && el.clientHeight) { return computeScrollLoad() } return Promise.resolve() } const loadYData = (evnt: Event) => { const { scrollYStore } = internalData const { startIndex, endIndex, visibleSize, offsetSize, rowHeight } = scrollYStore const scrollBodyElem = evnt.target as HTMLDivElement const scrollTop = scrollBodyElem.scrollTop const toVisibleIndex = Math.floor(scrollTop / rowHeight) const offsetStartIndex = Math.max(0, toVisibleIndex - 1 - offsetSize) const offsetEndIndex = toVisibleIndex + visibleSize + offsetSize if (toVisibleIndex <= startIndex || toVisibleIndex >= endIndex - visibleSize - 1) { if (startIndex !== offsetStartIndex || endIndex !== offsetEndIndex) { scrollYStore.startIndex = offsetStartIndex scrollYStore.endIndex = offsetEndIndex updateYData() } } } // 滚动、拖动过程中不需要触发 const isVMScrollProcess = () => { const delayHover = 250 const { lastScrollTime } = internalData return !!(lastScrollTime && Date.now() < lastScrollTime + delayHover) } const scrollEvent = (evnt: Event) => { const scrollBodyElem = evnt.target as HTMLDivElement const scrollTop = scrollBodyElem.scrollTop const scrollLeft = scrollBodyElem.scrollLeft const isX = scrollLeft !== internalData.lastScrollLeft const isY = scrollTop !== internalData.lastScrollTop internalData.lastScrollTop = scrollTop internalData.lastScrollLeft = scrollLeft if (reactData.scrollYLoad) { loadYData(evnt) } internalData.lastScrollTime = Date.now() dispatchEvent('scroll', { scrollLeft, scrollTop, isX, isY }, evnt) } /** * 加载数据 * @param {Array} datas 数据 */ const loadData = (datas: any[]) => { cacheItemMap(datas || []) const { multiple } = props const { isLoaded, fullData, scrollYStore } = internalData const defaultOpts = computeDefaultOpts.value const virtualYOpts = computeVirtualYOpts.value const valueField = computeValueField.value Object.assign(scrollYStore, { startIndex: 0, endIndex: 1, visibleSize: 0 }) internalData.synchData = datas || [] // 如果gt为0,则总是启用 reactData.scrollYLoad = !!virtualYOpts.enabled && virtualYOpts.gt > -1 && (virtualYOpts.gt === 0 || virtualYOpts.gt <= fullData.length) handleData() if (!isLoaded) { const { selectMode } = defaultOpts if (datas.length > 0 && XEUtils.eqNull(props.modelValue)) { if (selectMode === 'all') { if (multiple) { nextTick(() => { emitDefaultValue(datas.map(item => item[valueField])) }) } else { errLog('vxe.error.notConflictProp', ['default-config.selectMode=all', 'multiple=true']) } } else if (selectMode === 'first' || selectMode === 'last') { const selectItem = XEUtils[selectMode](datas) if (selectItem) { nextTick(() => { if (XEUtils.eqNull(props.modelValue)) { emitDefaultValue(selectItem[valueField]) } }) } } internalData.isLoaded = true } } return computeScrollLoad().then(() => { refreshScroll() }) } const clearScroll = () => { const scrollBodyElem = refVirtualWrapper.value if (scrollBodyElem) { scrollBodyElem.scrollTop = 0 scrollBodyElem.scrollLeft = 0 } internalData.lastScrollTop = 0 internalData.lastScrollLeft = 0 return nextTick() } const hasOptGroupById = (optid: any) => { const { optGroupKeyMaps } = internalData return !!optGroupKeyMaps[optid] } const selectMethods: SelectMethods = { dispatchEvent, loadData, reloadData (datas) { internalData.isLoaded = false clearScroll() return loadData(datas) }, isPanelVisible () { return reactData.visiblePanel }, togglePanel () { if (reactData.visiblePanel) { hideOptionPanel() } else { showOptionPanel() } return nextTick() }, hidePanel () { if (reactData.visiblePanel) { hideOptionPanel() } return nextTick() }, showPanel () { if (!reactData.visiblePanel) { showOptionPanel() } return nextTick() }, refreshOption () { handleOption() updateYData() return nextTick() }, focus () { const $input = refInput.value if ($input) { $input.blur() } reactData.isActivated = true return nextTick() }, blur () { const $input = refInput.value if ($input) { $input.blur() } reactData.isActivated = false return nextTick() }, recalculate, clearScroll } Object.assign($xeSelect, selectMethods) const renderOption = (list: VxeOptionProps[]) => { const { allowCreate, optionKey } = props const { currentOption } = reactData const { optAddMaps } = internalData const optionOpts = computeOptionOpts.value const labelField = computeLabelField.value const valueField = computeValueField.value const groupLabelField = computeGroupLabelField.value const selectVals = computeSelectVals.value const { useKey, height } = optionOpts const optionSlot = slots.option return list.map((option, cIndex) => { const { slots, className } = option const optid = getOptId(option) const optionValue = option[valueField as 'value'] const isOptGroup = hasOptGroupById(optid) const isAdd = !!(allowCreate && optAddMaps[optid]) const isSelected = !isAdd && selectVals.indexOf(optionValue) > -1 const isVisible = isAdd || (!isOptGroup || isOptionVisible(option)) const isDisabled = !isAdd && checkOptionDisabled(isSelected, option) const defaultSlot = slots ? slots.default : null const optParams = { option, group: isOptGroup ? option : null, $select: $xeSelect } let optLabel = '' let optVNs: string | VxeComponentSlotType[] = [] if (optionSlot) { optVNs = callSlot(optionSlot, optParams) } else if (defaultSlot) { optVNs = callSlot(defaultSlot, optParams) } else { optLabel = getFuncText(option[(isOptGroup ? groupLabelField : labelField) as 'label'] || optionValue) optVNs = optLabel } return isVisible ? h('div', { key: useKey || optionKey ? optid : cIndex, class: ['vxe-select-option', className ? (XEUtils.isFunction(className) ? className(optParams) : className) : '', { 'vxe-select-optgroup': isOptGroup, 'is--disabled': isDisabled, 'is--selected': isSelected, 'is--add': isAdd, 'is--hover': currentOption && getOptId(currentOption) === optid }], optid: optid, title: optLabel || null, style: height ? { height: toCssUnit(height) } : undefined, onMousedown: (evnt: MouseEvent) => { const isLeftBtn = evnt.button === 0 if (isLeftBtn) { evnt.stopPropagation() } }, onClick: (evnt: MouseEvent) => { if (!isDisabled && !isOptGroup) { changeOptionEvent(evnt, option) } }, onMouseenter: () => { if (!isDisabled && !isOptGroup && !isVMScrollProcess()) { setCurrentOption(option) } } }, allowCreate ? [ h('span', { key: 1, class: 'vxe-select-option--label' }, optVNs), isAdd ? h('span', { key: 2, class: 'vxe-select-option--add-icon' }, [ h('i', { class: getIcon().SELECT_ADD_OPTION }) ]) : renderEmptyElement($xeSelect) ] : optVNs) : renderEmptyElement($xeSelect) }) } const renderOpts = () => { const { optList, searchLoad