import { defineComponent, h, ref, Ref, computed, Teleport, VNode, onUnmounted, reactive, nextTick, PropType, onMounted } from 'vue' import XEUtils from 'xe-utils' import GlobalConfig from '../../v-x-e-table/src/conf' import { useSize } from '../../hooks/size' import { getAbsolutePos, getEventTargetNode } from '../../tools/dom' import { getFuncText, getLastZIndex, nextZIndex } from '../../tools/utils' import { GlobalEvent } from '../../tools/event' import { VxeButtonConstructor, VxeButtonPropTypes, VxeButtonEmits, ButtonReactData, ButtonMethods, ButtonPrivateRef, ButtonInternalData } from '../../../types/all' export default defineComponent({ name: 'VxeButton', props: { /** * 按钮类型 */ type: String as PropType, className: String as PropType, /** * 按钮尺寸 */ size: { type: String as PropType, default: () => GlobalConfig.button.size || GlobalConfig.size }, /** * 用来标识这一项 */ name: [String, Number] as PropType, /** * 按钮内容 */ content: String as PropType, /** * 固定显示下拉面板的方向 */ placement: String as PropType, /** * 按钮状态 */ status: String as PropType, /** * 按钮的图标 */ icon: String as PropType, /** * 圆角边框 */ round: Boolean as PropType, /** * 圆角按钮 */ circle: Boolean as PropType, /** * 是否禁用 */ disabled: Boolean as PropType, /** * 是否加载中 */ loading: Boolean as PropType, /** * 在下拉面板关闭时销毁内容 */ destroyOnClose: Boolean as PropType, /** * 是否将弹框容器插入于 body 内 */ transfer: { type: Boolean as PropType, default: () => GlobalConfig.button.transfer } }, emits: [ 'click', 'dropdown-click' ] as VxeButtonEmits, setup (props, context) { const { slots, emit } = context const xID = XEUtils.uniqueId() const computeSize = useSize(props) const reactData = reactive({ inited: false, showPanel: false, animatVisible: false, panelIndex: 0, panelStyle: {}, panelPlacement: '' }) const internalData: ButtonInternalData = { showTime: null } const refElem = ref() as Ref const refButton = ref() as Ref const refBtnPanel = ref() as Ref const refMaps: ButtonPrivateRef = { refElem } const $xebutton = { xID, props, context, reactData, internalData, getRefMaps: () => refMaps } as unknown as VxeButtonConstructor let buttonMethods = {} as ButtonMethods const computeIsFormBtn = computed(() => { const { type } = props if (type) { return ['submit', 'reset', 'button'].indexOf(type) > -1 } return false }) const computeBtnType = computed(() => { const { type } = props return type && type === 'text' ? type : 'button' }) const updateZindex = () => { if (reactData.panelIndex < getLastZIndex()) { reactData.panelIndex = nextZIndex() } } const updatePlacement = () => { return nextTick().then(() => { const { transfer, placement } = props const { panelIndex } = reactData const targetElem = refButton.value const panelElem = refBtnPanel.value if (panelElem && targetElem) { const targetHeight = targetElem.offsetHeight const targetWidth = targetElem.offsetWidth const panelHeight = panelElem.offsetHeight const panelWidth = panelElem.offsetWidth const marginSize = 5 const panelStyle: { [key: string]: string | number } = { zIndex: panelIndex } const { top, left, boundingTop, visibleHeight, visibleWidth } = getAbsolutePos(targetElem) let panelPlacement = 'bottom' if (transfer) { let btnLeft = left + targetWidth - panelWidth let btnTop = top + targetHeight if (placement === 'top') { panelPlacement = 'top' btnTop = top - panelHeight } else if (!placement) { // 如果下面不够放,则向上 if (boundingTop + targetHeight + panelHeight + marginSize > visibleHeight) { panelPlacement = 'top' btnTop = top - panelHeight } // 如果上面不够放,则向下(优先) if (btnTop < marginSize) { panelPlacement = 'bottom' btnTop = top + targetHeight } } // 如果溢出右边 if (btnLeft + panelWidth + marginSize > visibleWidth) { btnLeft -= btnLeft + panelWidth + marginSize - visibleWidth } // 如果溢出左边 if (btnLeft < marginSize) { btnLeft = marginSize } Object.assign(panelStyle, { left: `${btnLeft}px`, right: 'auto', top: `${btnTop}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` } } } } reactData.panelStyle = panelStyle reactData.panelPlacement = panelPlacement return nextTick() } }) } const clickEvent = (evnt: Event) => { buttonMethods.dispatchEvent('click', { $event: evnt }, evnt) } const mousedownDropdownEvent = (evnt: MouseEvent) => { const isLeftBtn = evnt.button === 0 if (isLeftBtn) { evnt.stopPropagation() } } const clickDropdownEvent = (evnt: Event) => { const dropdownElem = evnt.currentTarget const panelElem = refBtnPanel.value const { flag, targetElem } = getEventTargetNode(evnt, dropdownElem, 'vxe-button') if (flag) { if (panelElem) { panelElem.dataset.active = 'N' } reactData.showPanel = false setTimeout(() => { if (!panelElem || panelElem.dataset.active !== 'Y') { reactData.animatVisible = false } }, 350) buttonMethods.dispatchEvent('dropdown-click', { name: targetElem.getAttribute('name'), $event: evnt }, evnt) } } const mouseenterEvent = () => { const panelElem = refBtnPanel.value if (panelElem) { panelElem.dataset.active = 'Y' reactData.animatVisible = true setTimeout(() => { if (panelElem.dataset.active === 'Y') { reactData.showPanel = true updateZindex() updatePlacement() setTimeout(() => { if (reactData.showPanel) { updatePlacement() } }, 50) } }, 20) } } const mouseenterTargetEvent = () => { const panelElem = refBtnPanel.value if (panelElem) { panelElem.dataset.active = 'Y' if (!reactData.inited) { reactData.inited = true } internalData.showTime = setTimeout(() => { if (panelElem.dataset.active === 'Y') { mouseenterEvent() } else { reactData.animatVisible = false } }, 250) } } const closePanel = () => { const panelElem = refBtnPanel.value clearTimeout(internalData.showTime) if (panelElem) { panelElem.dataset.active = 'N' setTimeout(() => { if (panelElem.dataset.active !== 'Y') { reactData.showPanel = false setTimeout(() => { if (panelElem.dataset.active !== 'Y') { reactData.animatVisible = false } }, 350) } }, 100) } else { reactData.animatVisible = false reactData.showPanel = false } } const mouseleaveEvent = () => { closePanel() } const renderContent = () => { const { content, icon, loading } = props const contVNs: VNode[] = [] if (loading) { contVNs.push( h('i', { class: ['vxe-button--loading-icon', GlobalConfig.icon.BUTTON_LOADING] }) ) } else if (slots.icon) { contVNs.push( h('span', { class: 'vxe-button--custom-icon' }, slots.icon({})) ) } else if (icon) { contVNs.push( h('i', { class: ['vxe-button--icon', icon] }) ) } if (slots.default) { contVNs.push( h('span', { class: 'vxe-button--content' }, slots.default({})) ) } else if (content) { contVNs.push( h('span', { class: 'vxe-button--content' }, getFuncText(content)) ) } return contVNs } buttonMethods = { dispatchEvent (type, params, evnt) { emit(type, Object.assign({ $button: $xebutton, $event: evnt }, params)) }, focus () { const btnElem = refButton.value btnElem.focus() return nextTick() }, blur () { const btnElem = refButton.value btnElem.blur() return nextTick() } } Object.assign($xebutton, buttonMethods) onMounted(() => { GlobalEvent.on($xebutton, 'mousewheel', (evnt: Event) => { const panelElem = refBtnPanel.value if (reactData.showPanel && !getEventTargetNode(evnt, panelElem).flag) { closePanel() } }) }) onUnmounted(() => { GlobalEvent.off($xebutton, 'mousewheel') }) const renderVN = () => { const { className, transfer, type, round, circle, destroyOnClose, status, name, disabled, loading } = props const { inited, showPanel } = reactData const isFormBtn = computeIsFormBtn.value const btnType = computeBtnType.value const vSize = computeSize.value if (slots.dropdowns) { return h('div', { ref: refElem, class: ['vxe-button--dropdown', className, { [`size--${vSize}`]: vSize, 'is--active': showPanel }] }, [ h('button', { ref: refButton, class: ['vxe-button', `type--${btnType}`, { [`size--${vSize}`]: vSize, [`theme--${status}`]: status, 'is--round': round, 'is--circle': circle, 'is--disabled': disabled || loading, 'is--loading': loading }], name, type: isFormBtn ? type : 'button', disabled: disabled || loading, onMouseenter: mouseenterTargetEvent, onMouseleave: mouseleaveEvent, onClick: clickEvent }, renderContent().concat([ h('i', { class: `vxe-button--dropdown-arrow ${GlobalConfig.icon.BUTTON_DROPDOWN}` }) ])), h(Teleport, { to: 'body', disabled: transfer ? !inited : true }, [ h('div', { ref: refBtnPanel, class: ['vxe-button--dropdown-panel', { [`size--${vSize}`]: vSize, 'animat--leave': reactData.animatVisible, 'animat--enter': showPanel }], placement: reactData.panelPlacement, style: reactData.panelStyle }, inited ? [ h('div', { class: 'vxe-button--dropdown-wrapper', onMousedown: mousedownDropdownEvent, onClick: clickDropdownEvent, onMouseenter: mouseenterEvent, onMouseleave: mouseleaveEvent }, destroyOnClose && !showPanel ? [] : slots.dropdowns({})) ] : []) ]) ]) } return h('button', { ref: refButton, class: ['vxe-button', `type--${btnType}`, { [`size--${vSize}`]: vSize, [`theme--${status}`]: status, 'is--round': round, 'is--circle': circle, 'is--disabled': disabled || loading, 'is--loading': loading }], name, type: isFormBtn ? type : 'button', disabled: disabled || loading, onClick: clickEvent }, renderContent()) } $xebutton.renderVN = renderVN return $xebutton }, render () { return this.renderVN() } })