UNPKG

vxe-table

Version:

一个基于 vue 的 PC 端表格组件,支持增删改查、虚拟滚动、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、虚拟列表、模态窗口、自定义模板、渲染器、贼灵活的配置项、扩展接口等...

694 lines (682 loc) 21.9 kB
import XEUtils from 'xe-utils/ctor' import VxeInput from '../../input/src/input' import GlobalConfig from '../../conf' import vSize from '../../mixins/size' import { UtilTools, DomTools, GlobalEvent } from '../../tools' function isOptionVisible (option) { return option.visible !== false } function getOptUniqueId () { return XEUtils.uniqueId('opt_') } function getOptkey (_vm) { return _vm.optionId || '_XID' } 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 getSelectLabel (_vm, value) { const item = findOption(_vm, value) return XEUtils.toString(item ? item[_vm.labelField] : value) } export function renderOption (h, _vm, list, group) { const { isGroup, labelField, valueField, optionKey, value, multiple, currentValue } = _vm return list.map((option, cIndex) => { const isVisible = !isGroup || isOptionVisible(option) const isDisabled = (group && group.disabled) || option.disabled const optionValue = option[valueField] const optid = getOptid(_vm, option) return isVisible ? h('div', { key: optionKey ? optid : cIndex, class: ['vxe-select-option', { 'is--disabled': isDisabled, 'is--selected': multiple ? (value && value.indexOf(optionValue) > -1) : value === optionValue, 'is--hover': currentValue === optionValue }], attrs: { 'data-optid': optid }, on: { click: (evnt) => { if (!isDisabled) { _vm.changeOptionEvent(evnt, optionValue) } }, mouseenter: () => { if (!isDisabled) { _vm.setCurrentOption(option) } } } }, UtilTools.formatText(UtilTools.getFuncText(option[labelField]))) : null }) } export function renderOptgroup (h, _vm) { const { optionKey, visibleGroupList, groupLabelField, groupOptionsField } = _vm return visibleGroupList.map((group, gIndex) => { const optid = getOptid(_vm, group) const isGroupDisabled = group.disabled return h('div', { key: optionKey ? optid : gIndex, class: ['vxe-optgroup', { 'is--disabled': isGroupDisabled }], attrs: { 'data-optid': optid } }, [ h('div', { class: 'vxe-optgroup--title' }, UtilTools.getFuncText(group[groupLabelField])), h('div', { class: 'vxe-optgroup--wrapper' }, renderOption(h, _vm, group[groupOptionsField], group)) ]) }) } function renderOpts (h, _vm) { const { isGroup, visibleGroupList, visibleOptionList } = _vm 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, disabled: Boolean, multiple: Boolean, multiCharOverflow: { type: [Number, String], default: () => GlobalConfig.select.multiCharOverflow }, prefixIcon: String, placement: String, options: Array, optionProps: Object, optionGroups: Array, optionGroupProps: Object, size: { type: String, default: () => GlobalConfig.select.size || GlobalConfig.size }, emptyText: String, optionId: { type: String, default: () => GlobalConfig.select.optionId }, optionKey: Boolean, transfer: { type: Boolean, default: () => GlobalConfig.select.transfer } }, components: { VxeInput }, provide () { return { $xeselect: this } }, data () { return { inited: false, collectOption: [], fullGroupList: [], fullOptionList: [], visibleGroupList: [], visibleOptionList: [], panelIndex: 0, panelStyle: null, panelPlacement: null, currentValue: null, visiblePanel: false, animatVisible: false, isActivated: 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' }, isGroup () { return this.fullGroupList.some(item => item.options && item.options.length) }, multiMaxCharNum () { return XEUtils.toNumber(this.multiCharOverflow) }, selectLabel () { const { value, multiple, multiMaxCharNum } = this if (value && multiple) { return value.map(val => { const label = getSelectLabel(this, val) if (multiMaxCharNum > 0 && label.length > multiMaxCharNum) { return `${label.substring(0, multiMaxCharNum)}...` } return label }).join(', ') } return getSelectLabel(this, value) } }, watch: { collectOption (value) { if (value.some(item => item.options && item.options.length)) { this.fullOptionList = [] this.fullGroupList = value } else { this.fullGroupList = [] this.fullOptionList = value } this.updateCache() }, options (value) { this.fullGroupList = [] this.fullOptionList = value this.updateCache() }, optionGroups (value) { this.fullOptionList = [] this.fullGroupList = value this.updateCache() } }, created () { const { options, optionGroups } = this if (optionGroups) { this.fullGroupList = optionGroups } else if (options) { this.fullOptionList = options } this.updateCache() 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 { vSize, inited, isActivated, disabled, visiblePanel } = this return h('div', { class: ['vxe-select', { [`size--${vSize}`]: vSize, 'is--visivle': visiblePanel, 'is--disabled': disabled, '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: 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 } }), h('div', { ref: 'panel', class: ['vxe-table--ignore-clear vxe-select--panel', { [`size--${vSize}`]: vSize, 'is--transfer': this.transfer, 'animat--leave': this.animatVisible, 'animat--enter': visiblePanel }], attrs: { 'data-placement': this.panelPlacement }, style: this.panelStyle }, inited ? [ h('div', { ref: 'optWrapper', class: 'vxe-select-option--wrapper' }, renderOpts(h, this)) ] : null) ]) }, methods: { updateCache () { 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 } = this if (isGroup) { this.visibleGroupList = fullGroupList.filter(isOptionVisible) } else { this.visibleOptionList = fullOptionList.filter(isOptionVisible) } return this.$nextTick() }, setCurrentOption (option) { if (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(`[data-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.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 }) } }, changeOptionEvent (evnt, selectValue) { const { value, multiple } = 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] } this.changeEvent(evnt, multipleValue) } else { 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, 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) } 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() } }, focusEvent () { if (!this.disabled) { this.isActivated = true } }, blurEvent () { this.isActivated = false }, 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 () { if (!this.disabled) { clearTimeout(this.hidePanelTimeout) if (!this.inited) { this.inited = true if (this.transfer) { document.body.appendChild(this.$refs.panel) } } this.isActivated = true this.animatVisible = true 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) } }, 10) this.updateZindex() this.updatePlacement() } }, hideOptionPanel () { this.visiblePanel = false this.hidePanelTimeout = setTimeout(() => { this.animatVisible = false }, 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() } } }