UNPKG

vxe-table-select-area

Version:

一个基于 vxe-table 的可区域选中复制、粘贴的组件

896 lines (882 loc) 28.5 kB
import XEUtils from 'xe-utils' import VxeInput from '../../input/src/input' import GlobalConfig from '../../v-x-e-table/src/conf' import vSize from '../../mixins/size' import UtilTools, { getFuncText } from '../../tools/utils' import DomTools from '../../tools/dom' import { GlobalEvent, hasEventKey, EVENT_KEYS } from '../../tools/event' import { getSlotVNs } from '../../tools/vn' function isOptionVisible (option) { return option.visible !== false } function getOptUniqueId () { return XEUtils.uniqueId('opt_') } function getOptkey (_vm) { const { optionOpts } = _vm return optionOpts.keyField || _vm.optionId || '_X_OPTION_KEY' } function getOptid (_vm, option) { const optid = option[getOptkey(_vm)] return optid ? encodeURIComponent(optid) : '' } function findOffsetOption (_vm, optionValue, isUpArrow) { const { isGroup, visibleOptionList, visibleGroupList, valueField, groupOptionsField } = _vm let firstOption let prevOption let nextOption let currOption if (isGroup) { for (let gIndex = 0; gIndex < visibleGroupList.length; gIndex++) { const group = visibleGroupList[gIndex] const groupOptionList = group[groupOptionsField] const isGroupDisabled = group.disabled if (groupOptionList) { for (let index = 0; index < groupOptionList.length; index++) { const option = groupOptionList[index] const isVisible = isOptionVisible(option) const isDisabled = isGroupDisabled || option.disabled if (!firstOption && !isDisabled) { firstOption = option } if (currOption) { if (isVisible && !isDisabled) { nextOption = option if (!isUpArrow) { return { offsetOption: nextOption } } } } if (optionValue === option[valueField]) { currOption = option if (isUpArrow) { return { offsetOption: prevOption } } } else { if (isVisible && !isDisabled) { prevOption = option } } } } } } else { for (let index = 0; index < visibleOptionList.length; index++) { const option = visibleOptionList[index] const isDisabled = option.disabled if (!firstOption && !isDisabled) { firstOption = option } if (currOption) { if (!isDisabled) { nextOption = option if (!isUpArrow) { return { offsetOption: nextOption } } } } if (optionValue === option[valueField]) { currOption = option if (isUpArrow) { return { offsetOption: prevOption } } } else { if (!isDisabled) { prevOption = option } } } } return { firstOption } } function findOption (_vm, optionValue) { const { isGroup, fullOptionList, fullGroupList, valueField } = _vm if (isGroup) { for (let gIndex = 0; gIndex < fullGroupList.length; gIndex++) { const group = fullGroupList[gIndex] if (group.options) { for (let index = 0; index < group.options.length; index++) { const option = group.options[index] if (optionValue === option[valueField]) { return option } } } } } return fullOptionList.find(item => optionValue === item[valueField]) } function getRemoteSelectLabel (_vm, value) { const { remoteValueList } = _vm const remoteItem = remoteValueList.find(item => value === item.key) const item = remoteItem ? remoteItem.result : null return XEUtils.toValueString(item ? item[_vm.labelField] : value) } function getSelectLabel (_vm, value) { const item = findOption(_vm, value) return XEUtils.toValueString(item ? item[_vm.labelField] : value) } function checkOptionDisabled (_vm, isSelected, option, group) { if (option.disabled) { return true } if (group && group.disabled) { return true } if (_vm.isMaximize && !isSelected) { return true } return false } export function renderOption (h, _vm, list, group) { const { isGroup, labelField, valueField, optionKey, value, multiple, currentValue, optionOpts } = _vm const { useKey } = optionOpts return list.map((option, cIndex) => { const { slots } = option const optionValue = option[valueField] const isSelected = multiple ? (value && value.indexOf(optionValue) > -1) : value === optionValue const isVisible = !isGroup || isOptionVisible(option) const isDisabled = checkOptionDisabled(_vm, isSelected, option, group) const optid = getOptid(_vm, option) const defaultSlot = slots ? slots.default : null return isVisible ? h('div', { key: useKey || optionKey ? optid : cIndex, class: ['vxe-select-option', option.className, { 'is--disabled': isDisabled, 'is--selected': isSelected, 'is--hover': currentValue === optionValue }], attrs: { optid: optid }, on: { mousedown: _vm.mousedownOptionEvent, click: (evnt) => { if (!isDisabled) { _vm.changeOptionEvent(evnt, optionValue, option) } }, mouseenter: () => { if (!isDisabled) { _vm.setCurrentOption(option) } } } }, defaultSlot ? _vm.callSlot(defaultSlot, { option, $select: _vm }, h) : UtilTools.formatText(getFuncText(option[labelField]))) : null }) } export function renderOptgroup (h, _vm) { const { optionKey, visibleGroupList, groupLabelField, groupOptionsField, optionOpts } = _vm const { useKey } = optionOpts return visibleGroupList.map((group, gIndex) => { const { slots } = group const optid = getOptid(_vm, group) const isGroupDisabled = group.disabled const defaultSlot = slots ? slots.default : null return h('div', { key: useKey || optionKey ? optid : gIndex, class: ['vxe-optgroup', group.className, { 'is--disabled': isGroupDisabled }], attrs: { optid: optid } }, [ h('div', { class: 'vxe-optgroup--title' }, defaultSlot ? _vm.callSlot(defaultSlot, { option: group, $select: _vm }, h) : getFuncText(group[groupLabelField])), h('div', { class: 'vxe-optgroup--wrapper' }, renderOption(h, _vm, group[groupOptionsField], group)) ]) }) } function renderOpts (h, _vm) { const { isGroup, visibleGroupList, visibleOptionList, searchLoading } = _vm if (searchLoading) { return [ h('div', { class: 'vxe-select--search-loading' }, [ h('i', { class: ['vxe-select--search-icon', GlobalConfig.icon.SELECT_LOADED] }), h('span', { class: 'vxe-select--search-text' }, GlobalConfig.i18n('vxe.select.loadingText')) ]) ] } if (isGroup) { if (visibleGroupList.length) { return renderOptgroup(h, _vm) } } else { if (visibleOptionList.length) { return renderOption(h, _vm, visibleOptionList) } } return [ h('div', { class: 'vxe-select--empty-placeholder' }, _vm.emptyText || GlobalConfig.i18n('vxe.select.emptyText')) ] } export default { name: 'VxeSelect', mixins: [vSize], props: { value: null, clearable: Boolean, placeholder: String, loading: Boolean, disabled: Boolean, multiple: Boolean, multiCharOverflow: { type: [Number, String], default: () => GlobalConfig.select.multiCharOverflow }, prefixIcon: String, placement: String, options: Array, optionProps: Object, optionGroups: Array, optionGroupProps: Object, optionConfig: Object, className: [String, Function], max: [String, Number], size: { type: String, default: () => GlobalConfig.select.size || GlobalConfig.size }, filterable: Boolean, filterMethod: Function, remote: Boolean, remoteMethod: Function, emptyText: String, // 已废弃,被 option-config.keyField 替换 optionId: { type: String, default: () => GlobalConfig.select.optionId }, // 已废弃,被 option-config.useKey 替换 optionKey: Boolean, transfer: { type: Boolean, default: () => GlobalConfig.select.transfer } }, components: { VxeInput }, inject: { $xeform: { default: null }, $xeformiteminfo: { default: null } }, provide () { return { $xeselect: this } }, data () { return { inited: false, collectOption: [], fullGroupList: [], fullOptionList: [], visibleGroupList: [], visibleOptionList: [], remoteValueList: [], panelIndex: 0, panelStyle: null, panelPlacement: null, currentOption: null, currentValue: null, visiblePanel: false, animatVisible: false, isActivated: false, searchValue: '', searchLoading: false } }, computed: { propsOpts () { return this.optionProps || {} }, groupPropsOpts () { return this.optionGroupProps || {} }, labelField () { return this.propsOpts.label || 'label' }, valueField () { return this.propsOpts.value || 'value' }, groupLabelField () { return this.groupPropsOpts.label || 'label' }, groupOptionsField () { return this.groupPropsOpts.options || 'options' }, optionOpts () { return Object.assign({}, GlobalConfig.select.optionConfig, this.optionConfig) }, isGroup () { return this.fullGroupList.some(item => item.options && item.options.length) }, multiMaxCharNum () { return XEUtils.toNumber(this.multiCharOverflow) }, selectLabel () { const { value, multiple, remote, multiMaxCharNum } = this if (value && multiple) { const vals = (XEUtils.isArray(value) ? value : [value]) if (remote) { return vals.map(val => getRemoteSelectLabel(this, val)).join(', ') } return vals.map(val => { const label = getSelectLabel(this, val) if (multiMaxCharNum > 0 && label.length > multiMaxCharNum) { return `${label.substring(0, multiMaxCharNum)}...` } return label }).join(', ') } if (remote) { return getRemoteSelectLabel(this, value) } return getSelectLabel(this, value) }, isMaximize () { const { value, multiple, max } = this if (multiple && max) { return (value ? value.length : 0) >= XEUtils.toNumber(max) } return false } }, watch: { collectOption (value) { if (value.some(item => item.options && item.options.length)) { this.fullOptionList = [] this.fullGroupList = value } else { this.fullGroupList = [] this.fullOptionList = value } this.cacheItemMap() }, options (value) { this.fullGroupList = [] this.fullOptionList = value this.cacheItemMap() }, optionGroups (value) { this.fullOptionList = [] this.fullGroupList = value this.cacheItemMap() } }, created () { const { options, optionGroups } = this if (optionGroups) { this.fullGroupList = optionGroups } else if (options) { this.fullOptionList = options } this.cacheItemMap() GlobalEvent.on(this, 'mousewheel', this.handleGlobalMousewheelEvent) GlobalEvent.on(this, 'mousedown', this.handleGlobalMousedownEvent) GlobalEvent.on(this, 'keydown', this.handleGlobalKeydownEvent) GlobalEvent.on(this, 'blur', this.handleGlobalBlurEvent) }, beforeDestroy () { const panelElem = this.$refs.panel if (panelElem && panelElem.parentNode) { panelElem.parentNode.removeChild(panelElem) } }, destroyed () { GlobalEvent.off(this, 'mousewheel') GlobalEvent.off(this, 'mousedown') GlobalEvent.off(this, 'keydown') GlobalEvent.off(this, 'blur') }, render (h) { const { _e, $scopedSlots, vSize, className, inited, isActivated, loading, disabled, visiblePanel, filterable } = this const prefixSlot = $scopedSlots.prefix return h('div', { class: ['vxe-select', className ? (XEUtils.isFunction(className) ? className({ $select: this }) : className) : '', { [`size--${vSize}`]: vSize, 'is--visivle': visiblePanel, 'is--disabled': disabled, 'is--filter': filterable, 'is--loading': loading, 'is--active': isActivated }] }, [ h('div', { class: 'vxe-select-slots', ref: 'hideOption' }, this.$slots.default), h('vxe-input', { ref: 'input', props: { clearable: this.clearable, placeholder: this.placeholder, readonly: true, disabled: disabled, type: 'text', prefixIcon: this.prefixIcon, suffixIcon: loading ? GlobalConfig.icon.SELECT_LOADED : (visiblePanel ? GlobalConfig.icon.SELECT_OPEN : GlobalConfig.icon.SELECT_CLOSE), value: this.selectLabel }, on: { clear: this.clearEvent, click: this.togglePanelEvent, focus: this.focusEvent, blur: this.blurEvent, 'suffix-click': this.togglePanelEvent }, scopedSlots: prefixSlot ? { prefix: () => prefixSlot({}) } : {} }), h('div', { ref: 'panel', class: ['vxe-table--ignore-clear vxe-select--panel', { [`size--${vSize}`]: vSize, 'is--transfer': this.transfer, 'animat--leave': !loading && this.animatVisible, 'animat--enter': !loading && visiblePanel }], attrs: { placement: this.panelPlacement }, style: this.panelStyle }, inited ? [ filterable ? h('div', { class: 'vxe-select-filter--wrapper' }, [ h('vxe-input', { ref: 'inpSearch', class: 'vxe-select-filter--input', props: { value: this.searchValue, type: 'text', clearable: true, placeholder: GlobalConfig.i18n('vxe.select.search'), prefixIcon: GlobalConfig.icon.INPUT_SEARCH }, on: { modelValue: this.modelSearchEvent, focus: this.focusSearchEvent, keydown: this.keydownSearchEvent, change: this.triggerSearchEvent, search: this.triggerSearchEvent } }) ]) : _e(), h('div', { ref: 'optWrapper', class: 'vxe-select-option--wrapper' }, renderOpts(h, this)) ] : null) ]) }, methods: { callSlot (slotFunc, params, h) { if (slotFunc) { const { $scopedSlots } = this if (XEUtils.isString(slotFunc)) { slotFunc = $scopedSlots[slotFunc] || null } if (XEUtils.isFunction(slotFunc)) { return getSlotVNs(slotFunc.call(this, params, h)) } } return [] }, cacheItemMap () { const { fullOptionList, fullGroupList, groupOptionsField } = this const optkey = getOptkey(this) const handleOptis = (item) => { if (!getOptid(this, item)) { item[optkey] = getOptUniqueId() } } if (fullGroupList.length) { fullGroupList.forEach(group => { handleOptis(group) if (group[groupOptionsField]) { group[groupOptionsField].forEach(handleOptis) } }) } else if (fullOptionList.length) { fullOptionList.forEach(handleOptis) } this.refreshOption() }, /** * 刷新选项,当选项被搜索、动态显示/隐藏时可能会用到 */ refreshOption () { const { isGroup, fullOptionList, fullGroupList, filterable, filterMethod, searchValue, labelField, groupLabelField } = this if (isGroup) { if (filterable && filterMethod) { this.visibleGroupList = fullGroupList.filter(group => isOptionVisible(group) && filterMethod({ group, option: null, searchValue })) } else if (filterable) { this.visibleGroupList = fullGroupList.filter(group => isOptionVisible(group) && (!searchValue || `${group[groupLabelField]}`.indexOf(searchValue) > -1)) } else { this.visibleGroupList = fullGroupList.filter(isOptionVisible) } } else { if (filterable && filterMethod) { this.visibleOptionList = fullOptionList.filter(option => isOptionVisible(option) && filterMethod({ group: null, option, searchValue })) } else if (filterable) { this.visibleOptionList = fullOptionList.filter(option => isOptionVisible(option) && (!searchValue || `${option[labelField]}`.indexOf(searchValue) > -1)) } else { this.visibleOptionList = fullOptionList.filter(isOptionVisible) } } return this.$nextTick() }, setCurrentOption (option) { if (option) { this.currentOption = option this.currentValue = option[this.valueField] } }, scrollToOption (option, isAlignBottom) { return this.$nextTick().then(() => { if (option) { const { $refs } = this const optWrapperElem = $refs.optWrapper const optElem = $refs.panel.querySelector(`[optid='${getOptid(this, option)}']`) if (optWrapperElem && optElem) { const wrapperHeight = optWrapperElem.offsetHeight const offsetPadding = 5 if (isAlignBottom) { 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 } } } } }) }, clearEvent (params, evnt) { this.clearValueEvent(evnt, null) this.hideOptionPanel() }, clearValueEvent (evnt, selectValue) { this.remoteValueList = [] this.changeEvent(evnt, selectValue) this.$emit('clear', { value: selectValue, $event: evnt }) }, changeEvent (evnt, selectValue) { if (selectValue !== this.value) { this.$emit('input', selectValue) this.$emit('change', { value: selectValue, $event: evnt }) // 自动更新校验状态 if (this.$xeform && this.$xeformiteminfo) { this.$xeform.triggerItemEvent(evnt, this.$xeformiteminfo.itemConfig.field, selectValue) } } }, mousedownOptionEvent (evnt) { const isLeftBtn = evnt.button === 0 if (isLeftBtn) { evnt.stopPropagation() } }, changeOptionEvent (evnt, selectValue, option) { const { value, multiple, remoteValueList } = this if (multiple) { let multipleValue if (value) { if (value.indexOf(selectValue) === -1) { multipleValue = value.concat([selectValue]) } else { multipleValue = value.filter(val => val !== selectValue) } } else { multipleValue = [selectValue] } const remoteItem = remoteValueList.find(item => item.key === selectValue) if (remoteItem) { remoteItem.result = option } else { remoteValueList.push({ key: selectValue, result: option }) } this.changeEvent(evnt, multipleValue) } else { this.remoteValueList = [{ key: selectValue, result: option }] this.changeEvent(evnt, selectValue) this.hideOptionPanel() } }, handleGlobalMousewheelEvent (evnt) { const { $refs, disabled, visiblePanel } = this if (!disabled) { if (visiblePanel) { if (DomTools.getEventTargetNode(evnt, $refs.panel).flag) { this.updatePlacement() } else { this.hideOptionPanel() } } } }, handleGlobalMousedownEvent (evnt) { const { $refs, $el, disabled, visiblePanel } = this if (!disabled) { this.isActivated = DomTools.getEventTargetNode(evnt, $el).flag || DomTools.getEventTargetNode(evnt, $refs.panel).flag if (visiblePanel && !this.isActivated) { this.hideOptionPanel() } } }, handleGlobalKeydownEvent (evnt) { const { visiblePanel, currentValue, currentOption, clearable, disabled } = this if (!disabled) { const keyCode = evnt.keyCode const isTab = keyCode === 9 const isEnter = keyCode === 13 const isEsc = keyCode === 27 const isUpArrow = keyCode === 38 const isDwArrow = keyCode === 40 const isDel = keyCode === 46 const isSpacebar = keyCode === 32 if (isTab) { this.isActivated = false } if (visiblePanel) { if (isEsc || isTab) { this.hideOptionPanel() } else if (isEnter) { evnt.preventDefault() evnt.stopPropagation() this.changeOptionEvent(evnt, currentValue, currentOption) } else if (isUpArrow || isDwArrow) { evnt.preventDefault() let { firstOption, offsetOption } = findOffsetOption(this, currentValue, isUpArrow) if (!offsetOption && !findOption(this, currentValue)) { offsetOption = firstOption } this.setCurrentOption(offsetOption) this.scrollToOption(offsetOption, isDwArrow) } else if (isSpacebar) { evnt.preventDefault() } } else if ((isUpArrow || isDwArrow || isEnter || isSpacebar) && this.isActivated) { evnt.preventDefault() this.showOptionPanel() } if (this.isActivated) { if (isDel && clearable) { this.clearValueEvent(evnt, null) } } } }, handleGlobalBlurEvent () { this.hideOptionPanel() }, updateZindex () { if (this.panelIndex < UtilTools.getLastZIndex()) { this.panelIndex = UtilTools.nextZIndex() } }, handleFocusSearch () { if (this.filterable) { this.$nextTick(() => { if (this.$refs.inpSearch) { this.$refs.inpSearch.focus() } }) } }, focusEvent (evnt) { if (!this.disabled) { this.isActivated = true } this.$emit('focus', { $event: evnt }) }, blurEvent (evnt) { this.isActivated = false this.$emit('blur', { $event: evnt }) }, modelSearchEvent (value) { this.searchValue = value }, focusSearchEvent () { this.isActivated = true }, keydownSearchEvent (params) { const { $event } = params const isEnter = hasEventKey($event, EVENT_KEYS.ENTER) if (isEnter) { $event.preventDefault() $event.stopPropagation() } }, triggerSearchEvent: XEUtils.debounce(function () { const { remote, remoteMethod, searchValue } = this if (remote && remoteMethod) { this.searchLoading = true Promise.resolve(remoteMethod({ searchValue })).then(() => this.$nextTick()).catch(() => this.$nextTick()).finally(() => { this.searchLoading = false this.refreshOption() }) } else { this.refreshOption() } }, 350, { trailing: true }), isPanelVisible () { return this.visiblePanel }, togglePanel () { if (this.visiblePanel) { this.hideOptionPanel() } else { this.showOptionPanel() } this.$nextTick() }, hidePanel () { if (this.visiblePanel) { this.hideOptionPanel() } this.$nextTick() }, showPanel () { if (!this.visiblePanel) { this.showOptionPanel() } this.$nextTick() }, togglePanelEvent (params) { const { $event } = params $event.preventDefault() if (this.visiblePanel) { this.hideOptionPanel() } else { this.showOptionPanel() } }, showOptionPanel () { const { loading, disabled, filterable } = this if (!loading && !disabled) { this.searchList = this.option clearTimeout(this.hidePanelTimeout) if (!this.inited) { this.inited = true if (this.transfer) { document.body.appendChild(this.$refs.panel) } } this.isActivated = true this.animatVisible = true if (filterable) { this.refreshOption() } setTimeout(() => { const { value, multiple } = this const currOption = findOption(this, multiple && value ? value[0] : value) this.visiblePanel = true if (currOption) { this.setCurrentOption(currOption) this.scrollToOption(currOption) } this.handleFocusSearch() }, 10) this.updateZindex() this.updatePlacement() } }, hideOptionPanel () { this.searchValue = '' this.searchLoading = false this.visiblePanel = false this.hidePanelTimeout = setTimeout(() => { this.animatVisible = false this.searchValue = '' }, 350) }, updatePlacement () { return this.$nextTick().then(() => { const { $refs, transfer, placement, panelIndex } = this const targetElem = $refs.input.$el const panelElem = $refs.panel if (panelElem && targetElem) { const targetHeight = targetElem.offsetHeight const targetWidth = targetElem.offsetWidth const panelHeight = panelElem.offsetHeight const panelWidth = panelElem.offsetWidth const marginSize = 5 const panelStyle = { zIndex: panelIndex } const { boundingTop, boundingLeft, visibleHeight, visibleWidth } = DomTools.getAbsolutePos(targetElem) let panelPlacement = 'bottom' if (transfer) { let left = boundingLeft let top = boundingTop + targetHeight if (placement === 'top') { panelPlacement = 'top' top = boundingTop - panelHeight } else if (!placement) { // 如果下面不够放,则向上 if (top + panelHeight + marginSize > visibleHeight) { panelPlacement = 'top' top = boundingTop - panelHeight } // 如果上面不够放,则向下(优先) if (top < marginSize) { panelPlacement = 'bottom' top = boundingTop + targetHeight } } // 如果溢出右边 if (left + panelWidth + marginSize > visibleWidth) { left -= left + panelWidth + marginSize - visibleWidth } // 如果溢出左边 if (left < marginSize) { left = marginSize } Object.assign(panelStyle, { left: `${left}px`, top: `${top}px`, minWidth: `${targetWidth}px` }) } else { if (placement === 'top') { panelPlacement = 'top' panelStyle.bottom = `${targetHeight}px` } else if (!placement) { // 如果下面不够放,则向上 if (boundingTop + targetHeight + panelHeight > visibleHeight) { // 如果上面不够放,则向下(优先) if (boundingTop - targetHeight - panelHeight > marginSize) { panelPlacement = 'top' panelStyle.bottom = `${targetHeight}px` } } } } this.panelStyle = panelStyle this.panelPlacement = panelPlacement return this.$nextTick() } }) }, focus () { this.isActivated = true this.$refs.input.focus() return this.$nextTick() }, blur () { this.hideOptionPanel() this.$refs.input.blur() return this.$nextTick() } } }