vxe-table
Version:
一个基于 vue 的 PC 端表格组件,支持增删改查、虚拟滚动、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、虚拟列表、模态窗口、自定义模板、渲染器、贼灵活的配置项、扩展接口等...
694 lines (682 loc) • 21.9 kB
JavaScript
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()
}
}
}