vxe-pc-ui
Version:
A vue based PC component library
650 lines (606 loc) • 22 kB
text/typescript
import { defineComponent, h, Teleport, ref, Ref, inject, reactive, nextTick, provide, watch, PropType, onMounted, onUnmounted, computed } from 'vue'
import XEUtils from 'xe-utils'
import { useSize, getIcon, getConfig, getI18n, globalEvents, GLOBAL_EVENT_KEYS, createEvent, renderEmptyElement } from '../../ui'
import { getLastZIndex, nextZIndex, getFuncText } from '../../ui/src/utils'
import { getDomNode, toCssUnit } from '../../ui/src/dom'
import { getSlotVNs } from '../../ui/src/vn'
import VxeButtonComponent from '../../button/src/button'
import VxeLoadingComponent from '../../loading/index'
import type { VxeDrawerPropTypes, DrawerReactData, VxeDrawerEmits, DrawerPrivateRef, DrawerMethods, DrawerPrivateMethods, VxeDrawerPrivateComputed, VxeDrawerConstructor, VxeDrawerMethods, VxeButtonInstance, DrawerEventTypes, ValueOf, VxeModalConstructor, VxeModalMethods, VxeFormConstructor, VxeFormPrivateMethods } from '../../../types'
import type { VxeTableConstructor, VxeTablePrivateMethods } from '../../../types/components/table'
export const allActiveDrawers: VxeDrawerConstructor[] = []
export default defineComponent({
name: 'VxeDrawer',
props: {
modelValue: Boolean as PropType<VxeDrawerPropTypes.ModelValue>,
id: String as PropType<VxeDrawerPropTypes.ID>,
title: String as PropType<VxeDrawerPropTypes.Title>,
loading: {
type: Boolean as PropType<VxeDrawerPropTypes.Loading>,
default: null
},
className: String as PropType<VxeDrawerPropTypes.ClassName>,
position: {
type: [String, Object] as PropType<VxeDrawerPropTypes.Position>,
default: () => getConfig().drawer.position
},
lockView: {
type: Boolean as PropType<VxeDrawerPropTypes.LockView>,
default: () => getConfig().drawer.lockView
},
lockScroll: Boolean as PropType<VxeDrawerPropTypes.LockScroll>,
mask: {
type: Boolean as PropType<VxeDrawerPropTypes.Mask>,
default: () => getConfig().drawer.mask
},
maskClosable: {
type: Boolean as PropType<VxeDrawerPropTypes.MaskClosable>,
default: () => getConfig().drawer.maskClosable
},
escClosable: {
type: Boolean as PropType<VxeDrawerPropTypes.EscClosable>,
default: () => getConfig().drawer.escClosable
},
cancelClosable: {
type: Boolean as PropType<VxeDrawerPropTypes.CancelClosable>,
default: () => getConfig().drawer.cancelClosable
},
confirmClosable: {
type: Boolean as PropType<VxeDrawerPropTypes.ConfirmClosable>,
default: () => getConfig().drawer.confirmClosable
},
showHeader: {
type: Boolean as PropType<VxeDrawerPropTypes.ShowHeader>,
default: () => getConfig().drawer.showHeader
},
showFooter: {
type: Boolean as PropType<VxeDrawerPropTypes.ShowFooter>,
default: () => getConfig().drawer.showFooter
},
showClose: {
type: Boolean as PropType<VxeDrawerPropTypes.ShowClose>,
default: () => getConfig().drawer.showClose
},
content: [Number, String] as PropType<VxeDrawerPropTypes.Content>,
showCancelButton: {
type: Boolean as PropType<VxeDrawerPropTypes.ShowCancelButton>,
default: null
},
cancelButtonText: {
type: String as PropType<VxeDrawerPropTypes.CancelButtonText>,
default: () => getConfig().drawer.cancelButtonText
},
showConfirmButton: {
type: Boolean as PropType<VxeDrawerPropTypes.ShowConfirmButton>,
default: () => getConfig().drawer.showConfirmButton
},
confirmButtonText: {
type: String as PropType<VxeDrawerPropTypes.ConfirmButtonText>,
default: () => getConfig().drawer.confirmButtonText
},
destroyOnClose: {
type: Boolean as PropType<VxeDrawerPropTypes.DestroyOnClose>,
default: () => getConfig().drawer.destroyOnClose
},
showTitleOverflow: {
type: Boolean as PropType<VxeDrawerPropTypes.ShowTitleOverflow>,
default: () => getConfig().drawer.showTitleOverflow
},
width: [Number, String] as PropType<VxeDrawerPropTypes.Width>,
height: [Number, String] as PropType<VxeDrawerPropTypes.Height>,
resize: {
type: Boolean as PropType<VxeDrawerPropTypes.Resize>,
default: () => getConfig().drawer.resize
},
zIndex: Number as PropType<VxeDrawerPropTypes.ZIndex>,
transfer: {
type: Boolean as PropType<VxeDrawerPropTypes.Transfer>,
default: () => getConfig().drawer.transfer
},
padding: {
type: Boolean as PropType<VxeDrawerPropTypes.Padding>,
default: () => getConfig().drawer.padding
},
size: {
type: String as PropType<VxeDrawerPropTypes.Size>,
default: () => getConfig().drawer.size || getConfig().size
},
beforeHideMethod: {
type: Function as PropType<VxeDrawerPropTypes.BeforeHideMethod>,
default: () => getConfig().drawer.beforeHideMethod
},
slots: Object as PropType<VxeDrawerPropTypes.Slots>
},
emits: [
'update:modelValue',
'show',
'hide',
'before-hide',
'close',
'confirm',
'cancel',
'resize'
] as VxeDrawerEmits,
setup (props, context) {
const { slots, emit } = context
const xID = XEUtils.uniqueId()
const $xeModal = inject<(VxeModalConstructor & VxeModalMethods)| null>('$xeModal', null)
const $xeParentDrawer = inject<(VxeDrawerConstructor & VxeDrawerMethods) | null>('$xeDrawer', null)
const $xeTable = inject<(VxeTableConstructor & VxeTablePrivateMethods) | null>('$xeTable', null)
const $xeForm = inject<(VxeFormConstructor & VxeFormPrivateMethods)| null>('$xeForm', null)
const { computeSize } = useSize(props)
const refElem = ref<HTMLDivElement>()
const refDrawerBox = ref() as Ref<HTMLDivElement>
const refConfirmBtn = ref<VxeButtonInstance>()
const refCancelBtn = ref<VxeButtonInstance>()
const reactData = reactive<DrawerReactData>({
initialized: false,
visible: false,
contentVisible: false,
drawerZIndex: 0,
resizeFlag: 1
})
const refMaps: DrawerPrivateRef = {
refElem
}
const computeBtnTransfer = computed(() => {
const { transfer } = props
if (transfer === null) {
const globalTransfer = getConfig().modal.transfer
if (XEUtils.isBoolean(globalTransfer)) {
return globalTransfer
}
if ($xeTable || $xeModal || $xeParentDrawer || $xeForm) {
return true
}
}
return transfer
})
const computeDragType = computed(() => {
switch (props.position) {
case 'top':
return 'sb'
case 'bottom':
return 'st'
case 'left':
return 'wr'
}
return 'wl'
})
const computeMaps: VxeDrawerPrivateComputed = {
}
const $xeDrawer = {
xID,
props,
context,
reactData,
getRefMaps: () => refMaps,
getComputeMaps: () => computeMaps
} as unknown as VxeDrawerConstructor & VxeDrawerMethods
const getBox = () => {
const boxElem = refDrawerBox.value
return boxElem
}
const recalculate = () => {
const { width, height } = props
const boxElem = getBox()
if (boxElem) {
boxElem.style.width = toCssUnit(width)
boxElem.style.height = toCssUnit(height)
}
return nextTick()
}
const updateZindex = () => {
const { zIndex } = props
const { drawerZIndex } = reactData
if (zIndex) {
reactData.drawerZIndex = zIndex
} else if (drawerZIndex < getLastZIndex()) {
reactData.drawerZIndex = nextZIndex()
}
}
const closeDrawer = (type: DrawerEventTypes) => {
const { beforeHideMethod } = props
const { visible } = reactData
const params = { type }
if (visible) {
Promise.resolve(beforeHideMethod ? beforeHideMethod(params) : null).then((rest) => {
if (!XEUtils.isError(rest)) {
reactData.contentVisible = false
XEUtils.remove(allActiveDrawers, item => item === $xeDrawer)
dispatchEvent('before-hide', params, null)
setTimeout(() => {
reactData.visible = false
emit('update:modelValue', false)
dispatchEvent('hide', params, null)
}, 200)
}
}).catch(e => e)
}
return nextTick()
}
const closeEvent = (evnt: Event) => {
const type = 'close'
dispatchEvent(type, { type }, evnt)
closeDrawer(type)
}
const confirmEvent = (evnt: Event) => {
const { confirmClosable } = props
const type = 'confirm'
dispatchEvent(type, { type }, evnt)
if (confirmClosable) {
closeDrawer(type)
}
}
const cancelEvent = (evnt: Event) => {
const { cancelClosable } = props
const type = 'cancel'
dispatchEvent(type, { type }, evnt)
if (cancelClosable) {
closeDrawer(type)
}
}
const openDrawer = () => {
const { showFooter } = props
const { initialized, visible } = reactData
if (!initialized) {
reactData.initialized = true
}
if (!visible) {
reactData.visible = true
reactData.contentVisible = false
updateZindex()
allActiveDrawers.push($xeDrawer)
setTimeout(() => {
recalculate()
reactData.contentVisible = true
nextTick(() => {
if (showFooter) {
const confirmBtn = refConfirmBtn.value
const cancelBtn = refCancelBtn.value
const operBtn = confirmBtn || cancelBtn
if (operBtn) {
operBtn.focus()
}
}
const type = ''
const params = { type }
emit('update:modelValue', true)
dispatchEvent('show', params, null)
})
}, 10)
}
return nextTick()
}
const dispatchEvent = (type: ValueOf<VxeDrawerEmits>, params: Record<string, any>, evnt: Event | null) => {
emit(type, createEvent(evnt, { $drawer: $xeDrawer }, params))
}
const drawerMethods: DrawerMethods = {
dispatchEvent,
open: openDrawer,
close () {
return closeDrawer('close')
},
getBox
}
const selfClickEvent = (evnt: Event) => {
const el = refElem.value
if (props.maskClosable && evnt.target === el) {
const type = 'mask'
closeDrawer(type)
}
}
const handleGlobalKeydownEvent = (evnt: KeyboardEvent) => {
const isEsc = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ESCAPE)
if (isEsc) {
const lastDrawer = XEUtils.max(allActiveDrawers, (item) => item.reactData.drawerZIndex)
// 多个时,只关掉最上层的窗口
if (lastDrawer) {
setTimeout(() => {
if (lastDrawer === $xeDrawer && lastDrawer.props.escClosable) {
const type = 'exit'
dispatchEvent('close', { type }, evnt)
closeDrawer(type)
}
}, 10)
}
}
}
const boxMousedownEvent = () => {
const { drawerZIndex } = reactData
if (allActiveDrawers.some(comp => comp.reactData.visible && comp.reactData.drawerZIndex > drawerZIndex)) {
updateZindex()
}
}
const dragEvent = (evnt: MouseEvent) => {
evnt.preventDefault()
const { visibleHeight, visibleWidth } = getDomNode()
const marginSize = 0
const targetElem = evnt.target as HTMLSpanElement
const type = targetElem.getAttribute('type')
const minWidth = 0
const minHeight = 0
const maxWidth = visibleWidth
const maxHeight = visibleHeight
const boxElem = getBox()
const clientWidth = boxElem.clientWidth
const clientHeight = boxElem.clientHeight
const disX = evnt.clientX
const disY = evnt.clientY
const offsetTop = boxElem.offsetTop
const offsetLeft = boxElem.offsetLeft
const params = { type: 'resize' }
document.onmousemove = evnt => {
evnt.preventDefault()
let dragLeft
let dragTop
let width
let height
switch (type) {
case 'wl':
dragLeft = disX - evnt.clientX
width = dragLeft + clientWidth
if (offsetLeft - dragLeft > marginSize) {
if (width > minWidth) {
boxElem.style.width = `${width < maxWidth ? width : maxWidth}px`
}
}
break
case 'st':
dragTop = disY - evnt.clientY
height = clientHeight + dragTop
if (offsetTop - dragTop > marginSize) {
if (height > minHeight) {
boxElem.style.height = `${height < maxHeight ? height : maxHeight}px`
}
}
break
case 'wr':
dragLeft = evnt.clientX - disX
width = dragLeft + clientWidth
if (offsetLeft + width + marginSize < visibleWidth) {
if (width > minWidth) {
boxElem.style.width = `${width < maxWidth ? width : maxWidth}px`
}
}
break
case 'sb':
dragTop = evnt.clientY - disY
height = dragTop + clientHeight
if (offsetTop + height + marginSize < visibleHeight) {
if (height > minHeight) {
boxElem.style.height = `${height < maxHeight ? height : maxHeight}px`
}
}
break
}
boxElem.className = boxElem.className.replace(/\s?is--drag/, '') + ' is--drag'
dispatchEvent('resize', params, evnt)
reactData.resizeFlag++
}
document.onmouseup = () => {
document.onmousemove = null
document.onmouseup = null
reactData.resizeFlag++
setTimeout(() => {
boxElem.className = boxElem.className.replace(/\s?is--drag/, '')
}, 50)
}
}
const formDesignPrivateMethods: DrawerPrivateMethods = {}
Object.assign($xeDrawer, drawerMethods, formDesignPrivateMethods)
const renderTitles = () => {
const { slots: propSlots = {}, showClose, title } = props
const titleSlot = slots.title || propSlots.title
const cornerSlot = slots.corner || propSlots.corner
return [
h('div', {
class: 'vxe-drawer--header-title'
}, titleSlot ? getSlotVNs(titleSlot({ $drawer: $xeDrawer })) : (title ? getFuncText(title) : getI18n('vxe.alert.title'))),
h('div', {
class: 'vxe-drawer--header-right'
}, [
cornerSlot
? h('div', {
class: 'vxe-drawer--corner-wrapper'
}, getSlotVNs(cornerSlot({ $drawer: $xeDrawer })))
: renderEmptyElement($xeDrawer),
showClose
? h('div', {
class: ['vxe-drawer--close-btn', 'trigger--btn'],
title: getI18n('vxe.drawer.close'),
onClick: closeEvent
}, [
h('i', {
class: getIcon().DRAWER_CLOSE
})
])
: renderEmptyElement($xeDrawer)
])
]
}
const renderHeader = () => {
const { slots: propSlots = {}, showTitleOverflow } = props
const headerSlot = slots.header || propSlots.header
if (props.showHeader) {
return h('div', {
class: ['vxe-drawer--header', {
'is--ellipsis': showTitleOverflow
}]
}, headerSlot ? getSlotVNs(headerSlot({ $drawer: $xeDrawer })) : renderTitles())
}
return renderEmptyElement($xeDrawer)
}
const renderBody = () => {
const { slots: propSlots = {}, content } = props
const defaultSlot = slots.default || propSlots.default
const leftSlot = slots.left || propSlots.left
const rightSlot = slots.right || propSlots.right
return h('div', {
class: 'vxe-drawer--body'
}, [
leftSlot
? h('div', {
class: 'vxe-drawer--body-left'
}, getSlotVNs(leftSlot({ $drawer: $xeDrawer })))
: renderEmptyElement($xeDrawer),
h('div', {
class: 'vxe-drawer--body-default'
}, [
h('div', {
class: 'vxe-drawer--content'
}, defaultSlot ? getSlotVNs(defaultSlot({ $drawer: $xeDrawer })) : getFuncText(content))
]),
rightSlot
? h('div', {
class: 'vxe-drawer--body-right'
}, getSlotVNs(rightSlot({ $drawer: $xeDrawer })))
: renderEmptyElement($xeDrawer),
h(VxeLoadingComponent, {
class: 'vxe-drawer--loading',
modelValue: props.loading
})
])
}
const renderDefaultFooter = () => {
const { slots: propSlots = {}, showCancelButton, showConfirmButton, loading } = props
const lfSlot = slots.leftfoot || propSlots.leftfoot
const rfSlot = slots.rightfoot || propSlots.rightfoot
const btnVNs = []
if (showCancelButton) {
btnVNs.push(
h(VxeButtonComponent, {
key: 1,
ref: refCancelBtn,
content: props.cancelButtonText || getI18n('vxe.button.cancel'),
onClick: cancelEvent
})
)
}
if (showConfirmButton) {
btnVNs.push(
h(VxeButtonComponent, {
key: 2,
ref: refConfirmBtn,
loading: loading,
status: 'primary',
content: props.confirmButtonText || getI18n('vxe.button.confirm'),
onClick: confirmEvent
})
)
}
return h('div', {
class: 'vxe-drawer--footer-wrapper'
}, [
h('div', {
class: 'vxe-drawer--footer-left'
}, lfSlot ? getSlotVNs(lfSlot({ $drawer: $xeDrawer })) : []),
h('div', {
class: 'vxe-drawer--footer-right'
}, rfSlot ? getSlotVNs(rfSlot({ $drawer: $xeDrawer })) : btnVNs)
])
}
const renderFooter = () => {
const { slots: propSlots = {} } = props
const footerSlot = slots.footer || propSlots.footer
if (props.showFooter) {
return h('div', {
class: 'vxe-drawer--footer'
}, footerSlot ? getSlotVNs(footerSlot({ $drawer: $xeDrawer })) : [renderDefaultFooter()])
}
return renderEmptyElement($xeDrawer)
}
const renderVN = () => {
const { slots: propSlots = {}, className, position, loading, lockScroll, padding, lockView, mask, resize, destroyOnClose } = props
const { initialized, contentVisible, visible } = reactData
const asideSlot = slots.aside || propSlots.aside
const vSize = computeSize.value
const dragType = computeDragType.value
const btnTransfer = computeBtnTransfer.value
return h(Teleport, {
to: 'body',
disabled: btnTransfer ? !initialized : true
}, [
h('div', {
ref: refElem,
class: ['vxe-drawer--wrapper', `pos--${position}`, className || '', {
[`size--${vSize}`]: vSize,
'is--padding': padding,
'lock--scroll': lockScroll,
'lock--view': lockView,
'is--resize': resize,
'is--mask': mask,
'is--visible': contentVisible,
'is--active': visible,
'is--loading': loading
}],
style: {
zIndex: reactData.drawerZIndex
},
onClick: selfClickEvent
}, [
h('div', {
ref: refDrawerBox,
class: 'vxe-drawer--box',
onMousedown: boxMousedownEvent
}, [
asideSlot
? h('div', {
class: 'vxe-drawer--aside'
}, getSlotVNs(asideSlot({ $drawer: $xeDrawer })))
: renderEmptyElement($xeDrawer),
h('div', {
class: 'vxe-drawer--container'
}, !reactData.initialized || (destroyOnClose && !reactData.visible)
? []
: [
renderHeader(),
renderBody(),
renderFooter(),
resize
? h('span', {
class: 'vxe-drawer--resize'
}, [
h('span', {
class: `${dragType}-resize`,
type: dragType,
onMousedown: dragEvent
})
])
: renderEmptyElement($xeDrawer)
])
])
])
])
}
watch(() => props.width, recalculate)
watch(() => props.height, recalculate)
watch(() => props.modelValue, (value) => {
if (value) {
openDrawer()
} else {
closeDrawer('model')
}
})
onMounted(() => {
nextTick(() => {
if (props.modelValue) {
openDrawer()
}
recalculate()
})
if (props.escClosable) {
globalEvents.on($xeDrawer, 'keydown', handleGlobalKeydownEvent)
}
})
onUnmounted(() => {
globalEvents.off($xeDrawer, 'keydown')
})
provide('$xeDrawer', $xeDrawer)
$xeDrawer.renderVN = renderVN
return $xeDrawer
},
render () {
return this.renderVN()
}
})