UNPKG

vxe-pc-ui

Version:
617 lines (616 loc) 26.6 kB
import { defineComponent, ref, computed, h, nextTick, inject, provide, reactive, Teleport, onMounted, onUnmounted, watch } from 'vue'; import { getConfig, getI18n, getIcon, globalEvents, createEvent, useSize, renderEmptyElement } from '../../ui'; import { getEventTargetNode, getAbsolutePos, toCssUnit } from '../../ui/src/dom'; import { getLastZIndex, nextZIndex } from '../../ui/src/utils'; import { errLog } from '../../ui/src/log'; import XEUtils from 'xe-utils'; import VxeInputComponent from '../../input/src/input'; import VxeTreeComponent from '../../tree/src/tree'; function getOptUniqueId() { return XEUtils.uniqueId('node_'); } export default defineComponent({ name: 'VxeTreeSelect', props: { modelValue: [String, Number, Array], clearable: Boolean, placeholder: { type: String, default: () => XEUtils.eqNull(getConfig().treeSelect.placeholder) ? getI18n('vxe.base.pleaseSelect') : getConfig().treeSelect.placeholder }, readonly: { type: Boolean, default: null }, loading: Boolean, disabled: { type: Boolean, default: null }, multiple: Boolean, className: [String, Function], popupClassName: [String, Function], prefixIcon: String, placement: String, options: Array, optionProps: Object, size: { type: String, default: () => getConfig().select.size || getConfig().size }, remote: Boolean, remoteMethod: Function, popupConfig: Object, treeConfig: Object, transfer: { type: Boolean, default: null } }, emits: [ 'update:modelValue', 'change', 'clear', 'blur', 'focus', 'click', 'node-click' ], setup(props, context) { const { emit, slots } = context; const $xeModal = inject('$xeModal', null); const $xeDrawer = inject('$xeDrawer', null); const $xeTable = inject('$xeTable', null); const $xeForm = inject('$xeForm', null); const formItemInfo = inject('xeFormItemInfo', null); const xID = XEUtils.uniqueId(); const { computeSize } = useSize(props); const refElem = ref(); const refInput = ref(); const refTreeWrapper = ref(); const refOptionPanel = ref(); const reactData = reactive({ initialized: false, fullOptionList: [], fullNodeMaps: {}, panelIndex: 0, panelStyle: {}, panelPlacement: null, triggerFocusPanel: false, visiblePanel: false, isAniVisible: false, isActivated: false }); const internalData = { hpTimeout: undefined }; const refMaps = { refElem }; const computeFormReadonly = computed(() => { const { readonly } = props; if (readonly === null) { if ($xeForm) { return $xeForm.props.readonly; } return false; } return readonly; }); const computeIsDisabled = computed(() => { const { disabled } = props; if (disabled === null) { if ($xeForm) { return $xeForm.props.disabled; } return false; } return disabled; }); const computeBtnTransfer = computed(() => { const { transfer } = props; if (transfer === null) { const globalTransfer = getConfig().select.transfer; if (XEUtils.isBoolean(globalTransfer)) { return globalTransfer; } if ($xeTable || $xeModal || $xeDrawer || $xeForm) { return true; } } return transfer; }); const computePopupOpts = computed(() => { return Object.assign({}, getConfig().treeSelect.popupConfig, props.popupConfig); }); const computeTreeOpts = computed(() => { return Object.assign({}, getConfig().treeSelect.treeConfig, props.treeConfig, { data: undefined }); }); const computeTreeNodeOpts = computed(() => { const treeOpts = computeTreeOpts.value; return Object.assign({ isHover: true }, treeOpts.nodeConfig); }); const computeTreeCheckboxOpts = computed(() => { const treeOpts = computeTreeOpts.value; return Object.assign({ showIcon: !!treeOpts.showCheckbox }, treeOpts.checkboxConfig, { trigger: 'node' }); }); const computeTreeRadioOpts = computed(() => { const treeOpts = computeTreeOpts.value; return Object.assign({ showIcon: !!treeOpts.showRadio }, treeOpts.radioConfig, { trigger: 'node' }); }); const computePropsOpts = computed(() => { return props.optionProps || {}; }); const computeNodeKeyField = computed(() => { const treeOpts = computeTreeOpts.value; return treeOpts.keyField || 'id'; }); const computeLabelField = computed(() => { const propsOpts = computePropsOpts.value; return propsOpts.label || 'label'; }); const computeValueField = computed(() => { const propsOpts = computePropsOpts.value; return propsOpts.value || 'value'; }); const computeChildrenField = computed(() => { const propsOpts = computePropsOpts.value; return propsOpts.children || 'children'; }); const computeParentField = computed(() => { const propsOpts = computePropsOpts.value; return propsOpts.parent || 'parentField'; }); const computeHasChildField = computed(() => { const propsOpts = computePropsOpts.value; return propsOpts.hasChild || 'hasChild'; }); const computeSelectLabel = computed(() => { const { modelValue } = props; const { fullNodeMaps } = reactData; const labelField = computeLabelField.value; return (XEUtils.isArray(modelValue) ? modelValue : [modelValue]).map(val => { const cacheItem = fullNodeMaps[val]; return cacheItem ? cacheItem.item[labelField] : val; }).join(', '); }); const computePopupWrapperStyle = computed(() => { const popupOpts = computePopupOpts.value; const { height, width } = popupOpts; const stys = {}; if (width) { stys.width = toCssUnit(width); } if (height) { stys.height = toCssUnit(height); stys.maxHeight = toCssUnit(height); } return stys; }); const computeMaps = {}; const $xeTreeSelect = { xID, props, context, reactData, internalData, getRefMaps: () => refMaps, getComputeMaps: () => computeMaps }; const dispatchEvent = (type, params, evnt) => { emit(type, createEvent(evnt, { $treeSelect: $xeTreeSelect }, params)); }; const emitModel = (value) => { emit('update:modelValue', value); }; const treeSelectMethods = { dispatchEvent }; const getNodeid = (option) => { const nodeKeyField = computeNodeKeyField.value; const nodeid = option[nodeKeyField]; return nodeid ? encodeURIComponent(nodeid) : ''; }; const cacheDataMap = () => { const { options } = props; const nodeKeyField = computeNodeKeyField.value; const childrenField = computeChildrenField.value; const valueField = computeValueField.value; const nodeMaps = {}; const keyMaps = {}; XEUtils.eachTree(options, (item, index, items, path, parent, nodes) => { let nodeid = getNodeid(item); if (!nodeid) { nodeid = getOptUniqueId(); } if (keyMaps[nodeid]) { errLog('vxe.error.repeatKey', [nodeKeyField, nodeid]); } keyMaps[nodeid] = true; const value = item[valueField]; if (nodeMaps[value]) { errLog('vxe.error.repeatKey', [valueField, value]); } nodeMaps[value] = { item, index, items, parent, nodes }; }, { children: childrenField }); reactData.fullOptionList = options || []; reactData.fullNodeMaps = nodeMaps; }; const updateZindex = () => { if (reactData.panelIndex < getLastZIndex()) { reactData.panelIndex = nextZIndex(); } }; const updatePlacement = () => { return nextTick().then(() => { const { placement } = props; const { panelIndex } = reactData; const el = refElem.value; const panelElem = refOptionPanel.value; const btnTransfer = computeBtnTransfer.value; if (panelElem && el) { const targetHeight = el.offsetHeight; const targetWidth = el.offsetWidth; const panelHeight = panelElem.offsetHeight; const panelWidth = panelElem.offsetWidth; const marginSize = 5; const panelStyle = { zIndex: panelIndex }; const { boundingTop, boundingLeft, visibleHeight, visibleWidth } = getAbsolutePos(el); let panelPlacement = 'bottom'; if (btnTransfer) { 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`; } } } } reactData.panelStyle = panelStyle; reactData.panelPlacement = panelPlacement; return nextTick(); } }); }; const showOptionPanel = () => { const { loading } = props; const isDisabled = computeIsDisabled.value; if (!loading && !isDisabled) { clearTimeout(internalData.hpTimeout); if (!reactData.initialized) { reactData.initialized = true; } reactData.isActivated = true; reactData.isAniVisible = true; setTimeout(() => { reactData.visiblePanel = true; }, 10); updateZindex(); updatePlacement(); } }; const hideOptionPanel = () => { reactData.visiblePanel = false; internalData.hpTimeout = setTimeout(() => { reactData.isAniVisible = false; }, 350); }; const changeEvent = (evnt, selectValue) => { const { fullNodeMaps } = reactData; emitModel(selectValue); if (selectValue !== props.modelValue) { const cacheItem = fullNodeMaps[selectValue]; dispatchEvent('change', { value: selectValue, option: cacheItem ? cacheItem.item : null }, evnt); // 自动更新校验状态 if ($xeForm && formItemInfo) { $xeForm.triggerItemEvent(evnt, formItemInfo.itemConfig.field, selectValue); } } }; const clearValueEvent = (evnt, selectValue) => { changeEvent(evnt, selectValue); dispatchEvent('clear', { value: selectValue }, evnt); }; const clearEvent = (params, evnt) => { clearValueEvent(evnt, null); hideOptionPanel(); }; const handleGlobalMousewheelEvent = (evnt) => { const { visiblePanel } = reactData; const isDisabled = computeIsDisabled.value; if (!isDisabled) { if (visiblePanel) { const panelElem = refOptionPanel.value; if (getEventTargetNode(evnt, panelElem).flag) { updatePlacement(); } else { hideOptionPanel(); } } } }; const handleGlobalMousedownEvent = (evnt) => { const { visiblePanel } = reactData; const isDisabled = computeIsDisabled.value; if (!isDisabled) { const el = refElem.value; const panelElem = refOptionPanel.value; reactData.isActivated = getEventTargetNode(evnt, el).flag || getEventTargetNode(evnt, panelElem).flag; if (visiblePanel && !reactData.isActivated) { hideOptionPanel(); } } }; const handleGlobalBlurEvent = () => { hideOptionPanel(); }; const focusEvent = (evnt) => { const isDisabled = computeIsDisabled.value; if (!isDisabled) { if (!reactData.visiblePanel) { reactData.triggerFocusPanel = true; showOptionPanel(); setTimeout(() => { reactData.triggerFocusPanel = false; }, 150); } } dispatchEvent('focus', {}, evnt); }; const clickEvent = (evnt) => { togglePanelEvent(evnt); dispatchEvent('click', {}, evnt); }; const blurEvent = (evnt) => { reactData.isActivated = false; dispatchEvent('blur', {}, evnt); }; const togglePanelEvent = (params) => { const { $event } = params; $event.preventDefault(); if (reactData.triggerFocusPanel) { reactData.triggerFocusPanel = false; } else { if (reactData.visiblePanel) { hideOptionPanel(); } else { showOptionPanel(); } } }; const nodeClickEvent = (params) => { const { $event } = params; dispatchEvent('node-click', params, $event); }; const radioChangeEvent = (params) => { const { value, $event } = params; changeEvent($event, value); hideOptionPanel(); }; const checkboxChangeEvent = (params) => { const { value, $event } = params; changeEvent($event, value); }; const loadSuccessEvent = () => { cacheDataMap(); }; const treeSelectPrivateMethods = {}; Object.assign($xeTreeSelect, treeSelectMethods, treeSelectPrivateMethods); const renderVN = () => { const { className, modelValue, multiple, options, loading } = props; const { initialized, isActivated, isAniVisible, visiblePanel } = reactData; const vSize = computeSize.value; const isDisabled = computeIsDisabled.value; const selectLabel = computeSelectLabel.value; const btnTransfer = computeBtnTransfer.value; const formReadonly = computeFormReadonly.value; const popupWrapperStyle = computePopupWrapperStyle.value; const headerSlot = slots.header; const footerSlot = slots.footer; const prefixSlot = slots.prefix; const popupOpts = computePopupOpts.value; const popupClassName = popupOpts.className || props.popupClassName; const treeOpts = computeTreeOpts.value; const treeNodeOpts = computeTreeNodeOpts.value; const treeCheckboxOpts = computeTreeCheckboxOpts.value; const treeRadioOpts = computeTreeRadioOpts.value; const nodeKeyField = computeNodeKeyField.value; const labelField = computeLabelField.value; const valueField = computeValueField.value; const childrenField = computeChildrenField.value; const parentField = computeParentField.value; const hasChildField = computeHasChildField.value; if (formReadonly) { return h('div', { ref: refElem, class: ['vxe-tree-select--readonly', className] }, [ h('span', { class: 'vxe-tree-select-label' }, selectLabel) ]); } return h('div', { ref: refElem, class: ['vxe-tree-select', className ? (XEUtils.isFunction(className) ? className({ $treeSelect: $xeTreeSelect }) : className) : '', { [`size--${vSize}`]: vSize, 'is--visible': visiblePanel, 'is--disabled': isDisabled, 'is--loading': loading, 'is--active': isActivated }] }, [ h(VxeInputComponent, { ref: refInput, clearable: props.clearable, placeholder: loading ? getI18n('vxe.select.loadingText') : props.placeholder, readonly: true, disabled: isDisabled, type: 'text', prefixIcon: props.prefixIcon, suffixIcon: loading ? getIcon().TREE_SELECT_LOADED : (visiblePanel ? getIcon().TREE_SELECT_OPEN : getIcon().TREE_SELECT_CLOSE), modelValue: loading ? '' : selectLabel, onClear: clearEvent, onClick: clickEvent, onFocus: focusEvent, onBlur: blurEvent, onSuffixClick: togglePanelEvent }, prefixSlot ? { prefix: () => prefixSlot({}) } : {}), h(Teleport, { to: 'body', disabled: btnTransfer ? !initialized : true }, [ h('div', { ref: refOptionPanel, class: ['vxe-table--ignore-clear vxe-tree-select--panel', popupClassName ? (XEUtils.isFunction(popupClassName) ? popupClassName({ $treeSelect: $xeTreeSelect }) : popupClassName) : '', { [`size--${vSize}`]: vSize, 'is--transfer': btnTransfer, 'ani--leave': !loading && isAniVisible, 'ani--enter': !loading && visiblePanel }], placement: reactData.panelPlacement, style: reactData.panelStyle }, initialized ? [ h('div', { class: 'vxe-tree-select--panel-wrapper' }, [ headerSlot ? h('div', { class: 'vxe-tree-select--panel-header' }, headerSlot({})) : renderEmptyElement($xeTreeSelect), h('div', { class: 'vxe-tree-select--panel-body' }, [ h('div', { ref: refTreeWrapper, class: 'vxe-tree-select-tree--wrapper', style: popupWrapperStyle }, [ h(VxeTreeComponent, { class: 'vxe-tree-select--tree', data: options, indent: treeOpts.indent, showRadio: !multiple, radioConfig: treeRadioOpts, checkNodeKey: multiple ? null : modelValue, showCheckbox: !!multiple, checkNodeKeys: multiple ? modelValue : null, checkboxConfig: treeCheckboxOpts, titleField: labelField, valueField: valueField, keyField: nodeKeyField, childrenField: treeOpts.childrenField || childrenField, parentField: treeOpts.parentField || parentField, hasChildField: treeOpts.hasChildField || hasChildField, accordion: treeOpts.accordion, expandAll: treeOpts.expandAll, nodeConfig: treeNodeOpts, lazy: treeOpts.lazy, loadMethod: treeOpts.loadMethod, toggleMethod: treeOpts.toggleMethod, transform: treeOpts.transform, trigger: treeOpts.trigger, showIcon: treeOpts.showIcon, showLine: treeOpts.showLine, iconOpen: treeOpts.iconOpen, iconLoaded: treeOpts.iconLoaded, iconClose: treeOpts.iconClose, onNodeClick: nodeClickEvent, onRadioChange: radioChangeEvent, onCheckboxChange: checkboxChangeEvent, onLoadSuccess: loadSuccessEvent }) ]) ]), footerSlot ? h('div', { class: 'vxe-tree-select--panel-footer' }, footerSlot({})) : renderEmptyElement($xeTreeSelect) ]) ] : []) ]) ]); }; watch(() => props.options, () => { cacheDataMap(); }); cacheDataMap(); onMounted(() => { globalEvents.on($xeTreeSelect, 'mousewheel', handleGlobalMousewheelEvent); globalEvents.on($xeTreeSelect, 'mousedown', handleGlobalMousedownEvent); globalEvents.on($xeTreeSelect, 'blur', handleGlobalBlurEvent); }); onUnmounted(() => { globalEvents.off($xeTreeSelect, 'mousewheel'); globalEvents.off($xeTreeSelect, 'mousedown'); globalEvents.off($xeTreeSelect, 'blur'); }); provide('$xeTreeSelect', $xeTreeSelect); $xeTreeSelect.renderVN = renderVN; return $xeTreeSelect; }, render() { return this.renderVN(); } });