UNPKG

vue-fantable

Version:
717 lines (637 loc) 18.9 kB
import clickOutside from '../../src/directives/clickoutside.js' import VeCheckbox from '@P/ve-checkbox/ve-checkbox.js' import VeRadio from '@P/ve-radio/ve-radio.js' import { COMPS_NAME } from './util/constant' import { clsName } from './util/index' import { isFunction, isBoolean } from '../../src/utils/index' import { getRandomId } from '../../src/utils/random' import { getViewportOffset, getViewportOffsetWithinContainer } from '../../src/utils/dom' import { h } from 'vue' export default { name: COMPS_NAME.VE_DROPDOWN, directives: { 'click-outside': clickOutside, }, props: { // 如果是 select 组件将特殊处理 isSelect: { type: Boolean, default: false, }, showOperation: { type: Boolean, default: false, }, width: { type: Number, default: 90, }, // select 的最大宽度(超出隐藏) maxWidth: { type: Number, default: 0, }, // max height maxHeight: { type: Number, default: 1000, }, // 如果为true 会包含 checkbox isMultiple: { type: Boolean, default: false, }, // 用户传入 v-model 的值 [{value/label/selected}] modelValue: { type: [Array], default: null, }, // 文本居中方式 left|center|right textAlign: { type: String, default: 'left', }, // 是否支持输入input isInput: { type: Boolean, default: false, }, // confirm filter text confirmFilterText: { type: String, default: '', }, // confirm filter text resetFilterText: { type: String, default: '', }, // hide by single selection item click hideByItemClick: { type: Boolean, default: false, }, // is show radio when single selection showRadio: { type: Boolean, default: false, }, // 当 isControlled=true ,visible 生效 visible: { type: Boolean, default: false, }, // is controlled isControlled: { type: Boolean, default: false, }, // is custom content isCustomContent: { type: Boolean, default: false, }, // instance between dropdown items and trigger element defaultInstance: { type: Number, default: 5, }, // popper append to element popperAppendTo: { type: [String, HTMLElement], default: function () { return document.body }, }, /* before visible change 如果返回false 则阻止显示或者关闭 */ beforeVisibleChange: { type: Function, default: null, }, }, emits: ['update:modelValue', 'dropdownVisibleChange', 'filterConfirm', 'filterReset', 'itemSelectChange'], data() { return { internalVisible: false, internalOptions: [], inputValue: '', // 是否显示触发器被点击了(被点击将忽略 clickOutside 事件) isDropdownShowTriggerClicked: false, // root id rootId: '', // dropdown items panel id dropdownItemsPanelId: '', // 弹出被添加到的目标元素 popperAppendToEl: null, // 弹出被添加到的目标元素标签名称 appendToElTagName: null, } }, computed: { // is dropdown visible isDropdownVisible() { return this.isControlled ? this.visible : this.internalVisible }, // 获取最大宽度(不设置则是无穷大) getMaxWidth() { let result = Infinity const maxWidth = this.maxWidth const width = this.width if (maxWidth && maxWidth > 0 && maxWidth > width) { result = maxWidth } return result }, // selected labels selectedLabels() { return this.internalOptions .filter((x) => x.selected) .map((x) => { if (x.selected) { return x.label } return null }) }, // operation buttons class operationFilterClass() { let result = null result = { [clsName('filter-disable')]: this.selectedLabels.length === 0, } return result }, // dropdown items class dropdownItemsClass() { const ddShow = clsName('dd-show') return { [clsName('dd')]: true, [ddShow]: this.isDropdownVisible } }, }, watch: { modelValue() { this.initModel() }, visible: { handler(visible) { const { isControlled, showDropDown, hideDropDown } = this // deal after mounted hook setTimeout(() => { if (isControlled) { if (visible) { showDropDown() } else { hideDropDown() } } }) }, immediate: true, }, }, created() { this.initModel() }, mounted() { this.addRootElementToElement() this.$nextTick(() => { const targetEl = this.appendToElTagName === 'BODY' ? document : this.popperAppendToEl targetEl.addEventListener( 'scroll', this.changDropdownPanelPosition, ) }) window.addEventListener('resize', this.changDropdownPanelPosition) }, unmounted() { this.removeOrEmptyRootPanel() this.$nextTick(() => { const targetEl = this.appendToElTagName === 'BODY' ? document : this.popperAppendToEl if (!targetEl) return targetEl.removeEventListener( 'scroll', this.changDropdownPanelPosition, ) }) window.removeEventListener('resize', this.changDropdownPanelPosition) }, methods: { // 初始化 initModel() { this.internalOptions = Object.assign([], this.modelValue) if (this.isInput) { this.setInputValue() } }, // operation filter confirm confirm() { // 使用户传入的v-model 生效 this.$emit('update:modelValue', this.internalOptions) // this.$emit(EMIT_EVENTS.FILTER_CONFIRM, this.internalOptions) this.$emit('filterConfirm', this.internalOptions) this.hideDropDown() }, // operation filter reset reset() { if (this.internalOptions.some((x) => x.selected)) { this.internalOptions.map((x) => { if (x.selected) { x.selected = false } return x }) // 使用户传入的v-model 生效 this.$emit('update:modelValue', this.internalOptions) // this.$emit(EMIT_EVENTS.FILTER_RESET, this.internalOptions) this.$emit('filterReset', this.internalOptions) } this.hideDropDown() }, // show dropdown showDropDown() { const { rootId, dropdownItemsPanelId } = this const nextVisible = true const allowChange = this.beforeVisibleChangeCallback(nextVisible) if (isBoolean(allowChange) && !allowChange) { return false } const rootEl = document.querySelector(`#${rootId}`) if (rootEl) { // remove first rootEl.innerHTML = '' rootEl.appendChild(this.$refs[dropdownItemsPanelId]) rootEl.style.position = 'absolute' rootEl.classList.add(clsName('popper')) this.changDropdownPanelPosition() } this.internalVisible = true // this.$emit(EMIT_EVENTS.VISIBLE_CHANGE, nextVisible) this.$emit('dropdownVisibleChange', nextVisible) }, // hide dropdown hideDropDown() { const nextVisible = false const allowChange = this.beforeVisibleChangeCallback(nextVisible) if (isBoolean(allowChange) && !allowChange) { return false } // this.$emit(EMIT_EVENTS.VISIBLE_CHANGE, nextVisible) this.$emit('dropdownVisibleChange', nextVisible) setTimeout(() => { this.internalVisible = false this.removeOrEmptyRootPanel() }, 150) }, // before visible change callback beforeVisibleChangeCallback(nextVisible) { const { beforeVisibleChange, isDropdownVisible } = this if ( nextVisible !== isDropdownVisible && isFunction(beforeVisibleChange) ) { // next visible return beforeVisibleChange({ nextVisible, }) } }, // remove or empty root panel removeOrEmptyRootPanel() { const { rootId } = this const rootEl = document.querySelector(`#${rootId}`) if (rootEl) { rootEl.innerHTML = '' } }, // change dropdown panel position changDropdownPanelPosition() { const { defaultInstance, rootId, popperAppendToEl, appendToElTagName, } = this const rootEl = document.querySelector(`#${rootId}`) if (rootEl) { const { width: currentPanelWidth, height: currentPanelHeight } = rootEl.getBoundingClientRect() const triggerEl = this.$el.querySelector('.ve-dropdown-dt') const { height: triggerElHeight } = triggerEl.getBoundingClientRect() if (!popperAppendToEl) { return false } // is append to body const isAppendToBody = appendToElTagName === 'BODY' const { offsetLeft: triggerElLeft, offsetTop: triggerElTop, right: triggerElRight, bottom: triggerElBottom, } = isAppendToBody ? getViewportOffset(triggerEl) : getViewportOffsetWithinContainer( triggerEl, popperAppendToEl, ) let panelX = 0 let panelY = 0 // 如果不是添加到body 需要考虑外层容器滚动调的影响 let scrollLeft = 0 let scrollTop = 0 if (!isAppendToBody) { scrollLeft = popperAppendToEl.scrollLeft scrollTop = popperAppendToEl.scrollTop } // 右方宽度够显示 if (triggerElRight >= currentPanelWidth) { panelX = triggerElLeft + scrollLeft } else { // 右方宽度不够显示在鼠标点击左方 panelX = triggerElLeft - currentPanelWidth + scrollLeft } // 下方高度够显示 if (triggerElBottom >= currentPanelHeight) { panelY = triggerElTop + triggerElHeight + defaultInstance + scrollTop } else { // 下方高度不够显示在鼠标点击上方 panelY = triggerElTop - currentPanelHeight - defaultInstance + scrollTop } rootEl.style.left = panelX + 'px' rootEl.style.top = panelY + 'px' } }, // 设置文本框的值 setInputValue() { let result = null const labels = this.selectedLabels if (Array.isArray(labels) && labels.length > 0) { result = labels.join() } this.inputValue = result }, // dropdown panel click dropdownPanelClick() { this.isDropdownShowTriggerClicked = true this.dropdownShowToggle() }, // dropdown show toggle dropdownShowToggle() { if (this.isDropdownVisible) { this.hideDropDown() } else { this.showDropDown() } }, // single select option click singleSelectOptionClick(e, item) { this.internalOptions = this.internalOptions.map((x) => { if (item.label === x.label) { x.selected = true } else { x.selected = false } return x }) if (this.hideByItemClick) { this.hideDropDown() } if (this.isInput) { this.setInputValue() } // 使用户传入的 v-model 生效 this.$emit('update:modelValue', this.internalOptions) this.$emit('itemSelectChange', this.internalOptions) }, // 获取样式名称 getTextAlignClass() { return clsName(`items-li-a-${this.textAlign}`) }, // dropdown click outSide dropdownClickOutside() { /* 是否显示触发器被点击了(被点击将忽略 clickOutside 事件) */ setTimeout(() => { if (this.isDropdownShowTriggerClicked) { this.isDropdownShowTriggerClicked = false } else { this.hideDropDown() } }) }, // checkbox 受控属性管理 checkedChangeControl(item, isChecked) { this.internalOptions = this.internalOptions.map((i) => { if (i.label === item.label) { i.selected = isChecked } return i }) // this.$emit(EMIT_EVENTS.ITEM_SELECT_CHANGE, this.internalOptions) this.$emit('itemSelectChange', this.internalOptions) }, // get random id getRandomIdWithPrefix() { return clsName(getRandomId()) }, /* add root element to element 如果不指定则添加到 body */ addRootElementToElement() { const { popperAppendTo } = this this.rootId = this.getRandomIdWithPrefix() this.dropdownItemsPanelId = this.getRandomIdWithPrefix() const rootEl = document.querySelector(`#${this.rootId}`) if (rootEl) { return false } else { // fixed unit test error: [Vue warn]: Error in v-on handler: "TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'." this.$nextTick(() => { const containerEl = document.createElement('div') containerEl.setAttribute('id', this.rootId) if ( typeof popperAppendTo === 'string' && popperAppendTo.length > 0 ) { this.popperAppendToEl = document.querySelector(popperAppendTo) } else { this.popperAppendToEl = popperAppendTo } this.appendToElTagName = this.popperAppendToEl.tagName this.popperAppendToEl.appendChild(containerEl) }) } }, }, render() { const { isMultiple, getTextAlignClass, internalOptions, isSelect, width, maxHeight, dropdownPanelClick, getMaxWidth, reset, singleSelectOptionClick, showOperation, isCustomContent, dropdownItemsClass, dropdownItemsPanelId, } = this let content = '' if (isMultiple) { content = internalOptions.map((item, index) => { const checkboxProps = { key: item.label, isControlled: true, label: item.label, showLine: item.showLine, isSelected: item.selected, onCheckedChange: (isChecked) => this.checkedChangeControl(item, isChecked), } return ( <li key={index} class={[ clsName('items-multiple'), clsName('items-li'), getTextAlignClass(), ]} > <VeCheckbox {...checkboxProps} /> </li> ) }) } else { content = internalOptions.map((item, index) => { const radioProps = { isControlled: true, isSelected: item.selected, onRadioChange: () => { }, } return ( <li key={index} class={[ clsName('items-li'), item.selected ? 'active' : '', ]} onClick={(e) => singleSelectOptionClick(e, item)} > <a class={[clsName('items-li-a'), getTextAlignClass()]} href="javascript:void(0);" > {this.showRadio ? ( <VeRadio {...radioProps}>{item.label}</VeRadio> ) : ( item.label )} </a> </li> ) }) } const dropdownProps = { class: ['ve-dropdown'], } const dropdownItemsProps = { ref: dropdownItemsPanelId, class: dropdownItemsClass, // 'v-click-outside': this.dropdownClickOutside, // directives: [ // { // name: 'click-outside', // value: this.dropdownClickOutside, // }, // ], } return ( <dl {...dropdownProps}> <dt class="ve-dropdown-dt" onClick={dropdownPanelClick}> <a class={[isSelect ? clsName('dt-selected') : '']} style={{ width: width + 'px' }} > {h(this.$slots.default)} </a> </dt> <div style={{ display: 'none' }}> <dd {...dropdownItemsProps} v-click-outside={this.dropdownClickOutside}> <ul class={clsName('items')} style={{ 'min-width': width + 'px', 'max-width': getMaxWidth + 'px', }} > {/* custom content */} {isCustomContent && h(this.$slots['custom-content'])} {/* not custom content */} {!isCustomContent && ( <div> <div style={{ 'max-height': maxHeight + 'px', }} class={clsName('items-wrapper')} > {content} </div> {showOperation && ( <li class={clsName('operation')}> <a class={[ clsName('operation-item'), this.operationFilterClass, ]} href="javascript:void(0)" onClick={reset} > {this.resetFilterText} </a> <a class={clsName( 'operation-item', )} href="javascript:void(0)" onClick={this.confirm} > {this.confirmFilterText} </a> </li> )} </div> )} </ul> </dd> </div> </dl> ) }, }