elementUI和iview中select选择框都有属性配置:是否将弹层放置于 body 内,它将不受父级样式影响,从而达到更好的效果。若是父级弹框设置了overflow: hidden,弹层也能正常展现而且超出父级弹框。
但项目开发中常常会遇到须要自定义重写select的状况,也须要支持这种效果。javascript
<div class="select-model" > <!-- 匹配列表 使用visibility才能获取隐藏元素宽高 --> <div class="select-option-wrap" @click.stop="isFocus = true"> <ul class="select-drop-list" v-show="nowList.length > 0"> <li class="each-item">{{item.label}}</li> </ul> </div> </div>
.select-option-wrap{ position: absolute; min-width: 200px; max-height: 200px; min-height: 32px; left: 0; z-index: 1500; visibility: hidden; }
computed: { matchDom () { // 匹配框,须要相对于body return this.$el.querySelector('.select-option-wrap') }, matchParent () { // 匹配框父级 return this.$el.querySelector('.select-model') } }
mounted () { this.$nextTick(() => { const body = document.querySelector('body') // 将匹配DOM添加到body中 if (body.append) { // 在IE11中 document.appendChild会报错: javascript runtime error:HierarchyRequestError body.append(this.matchDom) } else { body.appendChild(this.matchDom) } }) },
checkTransfer () { if (this.isFocus) { // 聚焦时,须要计算当前匹配DOM的位置 let bodyHeight = document.documentElement.clientHeight // body 可视区域高度 let matchHeight = this.matchDom.clientHeight // 匹配DOM的高度 let rect = this.matchParent.getBoundingClientRect() // 取出匹配父级DOM的矩形对象 // getBoundingClientRect.bottom为元素下边与页面上边的距离,因此元素下边与页面下边距离 = 页面高度 - getBoundingClientRect.bottom let bottom = bodyHeight - rect.bottom this.matchDom.style.visibility = 'visible' this.matchDom.style.left = rect.left + 'px' // 匹配DOM的left与父级一致 if (bottom >= matchHeight) { // 父级距离页面下边的高度大于等于匹配DOM的高度,则往下展现 this.matchDom.style.bottom = 'auto' this.matchDom.style.top = (rect.top + rect.height) + 'px' // 匹配DOM的top = 父级矩形对象top + 父级的高度 } else { // 父级距离页面下边的高度小玉匹配DOM的高度,则往上展现 this.matchDom.style.top = 'auto' this.matchDom.style.bottom = (bottom + rect.height) + 'px' // 匹配DOM的bottom = 父级矩形对象bottom + 父级的高度 } } else { // 不聚焦则直接隐藏 this.matchDom.style.visibility = 'hidden' } }
watch: { isFocus () { this.checkTransfer() } }
mounted () { // 组件监听页面resize只能用addEventListener,不然不会生效 window.addEventListener('resize', this.checkTransfer, false) // 监听scroll事件的事件传递必须使用捕获阶段,让外部元素事件先触发 document.addEventListener('scroll', this.checkTransfer, true) }
beforeDestroy () { // 当DOM元素与事件拥有不一样的生命周期时,假若不remove掉eventListener就有可能致使内存泄漏 window.removeEventListener('resize', this.checkTransfer, false) document.removeEventListener('scroll', this.checkTransfer, true) },
<template> <!-- 季节选择框 --> <div class="quater-select" :style="{width: selectWidth}" v-click-outside="blurSelect"> <div class="quater-model" @click.stop="focusSelect" > <div class="select-content" :class="isFocus ? 'select-focus' : ''"> <div class="select-show" v-if="nowChosData.label">{{nowChosData.label}}</div> <div class="default-show" v-else>{{placeholder}}</div> <span class="ivu-input-suffix"><i class="ivu-icon ivu-icon-ios-calendar-outline"></i></span> </div> <!-- 匹配列表 v-show="isFocus" --> <div class="select-quater-wrap" @click.stop="isFocus = true"> <div class="year-header"> <!-- :class="nowChosYear <= 2020 ? 'disabled-next' : ''" --> <span @click="prevYear" :class="nowChosYear <= 2020 ? 'disabled-next' : ''" class="ivu-picker-panel-icon-btn ivu-date-picker-prev-btn ivu-date-picker-prev-btn-arrow-double"><i class="ivu-icon ivu-icon-ios-arrow-back"></i></span> {{nowChosYear}} <span @click="nextYear" :class="nowChosYear >= defaultYear ? 'disabled-next' : ''" class="ivu-picker-panel-icon-btn ivu-date-picker-next-btn ivu-date-picker-next-btn-arrow-double"><i class="ivu-icon ivu-icon-ios-arrow-forward"></i></span> </div> <!-- flex布局的父元素设置visibility隐藏时,子元素会延迟隐藏,因此这里使用opacity来控制即时隐藏效果 --> <div class="quarter-content" :style="isFocus ? 'opacity: 1' : 'opacity: 0'" v-if="quatLst && quatLst.length > 0"> <div class="each-quarter" :class="{'disabled': item.quaFlag === '0', 'active' : activeIndex === index}" v-for="(item, index) of quatLst" :key="index"> <span class="quarter-val" @click.stop="chosItem(item, index)">{{item.quaLabel}}</span> </div> </div> </div> </div> </div> </template> <script> export default { name: '', props: { dateValue: { // 父页面值 type: String, default: '' }, selectWidth: { // 选择框宽度 type: String, default: '200px' }, position: { // 弹出框位置 type: String, default: 'bottom' }, minDate: { // 最小日期 type: String, default: '' }, maxDate: { // 最大日期 type: String, default: '' } }, data () { return { isFocus: false, // 是否聚焦选择框 quatLst: [], // 季度列表 activeIndex: '', // 当前高亮索引, placeholder: '请选择', nowChosData: {}, // 当前选择数据 defaultYear: Number(new Date().format('yyyy')), // 默认年份 nowChosYear: Number(new Date().format('yyyy')), // 当前选择的年份 defaultLst: [ // 默认季度数组 {quaLabel: '第一季度', quaVal: '03', quaFlag: '1', quaYear: ''}, {quaLabel: '第二季度', quaVal: '06', quaFlag: '1', quaYear: ''}, {quaLabel: '第三季度', quaVal: '09', quaFlag: '1', quaYear: ''}, {quaLabel: '第四季度', quaVal: '12', quaFlag: '1', quaYear: ''} ] } }, mounted () { // 组件监听页面resize只能用addEventListener,不然不会生效 window.addEventListener('resize', this.checkTransfer, false) // 监听scroll事件的事件传递必须使用捕获阶段,让外部元素事件先触发 document.addEventListener('scroll', this.checkTransfer, true) this.$nextTick(() => { const body = document.querySelector('body') // 将匹配DOM添加到body中 if (body.append) { body.append(this.matchDom) } else { body.appendChild(this.matchDom) } }) this.initData() }, beforeDestroy () { // 当DOM元素与事件拥有不一样的生命周期时,假若不remove掉eventListener就有可能致使内存泄漏 window.removeEventListener('resize', this.checkTransfer, false) document.removeEventListener('scroll', this.checkTransfer, true) }, watch: { isFocus () { this.checkTransfer() }, dateValue (val) { // 监听表单重置 this.initData() } }, computed: { matchDom () { // 匹配框,须要相对于body return this.$el.querySelector('.select-quater-wrap') }, matchParent () { // 匹配框父级 return this.$el.querySelector('.quater-model') } }, methods: { initData () { // 初始化数据 if (this.dateValue) { // 有初始化的时间 // 拆分年和月份值 let quaYear = this.dateValue.substr(0, 4) if (Number(quaYear) <= this.defaultYear) { // 初始化年份小于等于默认年份,才能够跳转选择至对应年份和季度 this.nowChosYear = Number(quaYear) let quaVal = '' let quaLabel = '' let numVal = Number(this.dateValue.substr(4)) if (numVal < 4) { // 判断季度 quaVal = '03' quaLabel = '第一季度' } else if (numVal < 7) { quaVal = '06' quaLabel = '第二季度' } else if (numVal < 10) { quaVal = '09' quaLabel = '第三季度' } else { quaVal = '12' quaLabel = '第四季度' } this.nowChosData = { // 赋值为组件识别的数据结构 label: quaYear + '年' + quaLabel, value: quaYear + '' + quaVal } } } else { this.nowChosData = { // 赋值为组件识别的数据结构 label: '', value: '' } } this.checkNowYear() }, prevYear () { // 选择上一年 if (this.nowChosYear <= 2020) return // 选择年为2020,不容许往前选择年份(暂不限制) this.nowChosYear -= 1 this.checkNowYear() }, nextYear () { // 选择下一年 if (this.nowChosYear >= this.defaultYear) return // 选择年为当前年,不容许日后选择年份 this.nowChosYear += 1 this.checkNowYear() }, checkNowYear () { // 切换年份,须要重置季度列表,而且判断新季度列表是否存在已选择季度 this.activeIndex = '' // 季度高亮索引置空 this.quatLst = this.defaultLst.map((item, index) => { item.quaYear = this.nowChosYear.toString() // 同步年份 let nowVal = item.quaYear + '' + item.quaVal // 当前值 if (nowVal === this.nowChosData.value) { // 当前值是否与选择的值相等 this.activeIndex = index } if (this.minDate && Number(nowVal) < this.minDate) { // 最小日期存在而且小于最小日期 item.quaFlag = '0' } if (this.maxDate && Number(nowVal) > this.maxDate) { // 最大日期存在而且大于最大日期 item.quaFlag = '0' } return item }) }, chosItem (item, index) { // 选择季度 if (item.quaFlag === '0') return this.nowChosData = { label: item.quaYear + '年' + item.quaLabel, value: item.quaYear + '' + item.quaVal } this.activeIndex = index // 高亮索引 this.$emit('update:dateValue', this.nowChosData.value) this.$emit('on-change', this.nowChosData) this.isFocus = false }, focusSelect () { this.isFocus = !this.isFocus }, blurSelect () { this.isFocus = false }, checkTransfer () { if (this.isFocus) { // 聚焦时,须要计算当前匹配DOM的位置 let bodyHeight = document.documentElement.clientHeight // body 可视区域高度 let matchHeight = this.matchDom.clientHeight // 匹配DOM的高度 let rect = this.matchParent.getBoundingClientRect() // 取出匹配父级DOM的矩形对象 // getBoundingClientRect.bottom为元素下边与页面上边的距离,因此元素下边与页面下边距离 = 页面高度 - getBoundingClientRect.bottom let bottom = bodyHeight - rect.bottom this.matchDom.style.visibility = 'visible' this.matchDom.style.left = rect.left + 'px' // 匹配DOM的left与父级一致 if (bottom >= matchHeight) { // 父级距离页面下边的高度大于等于匹配DOM的高度,则往下展现 this.matchDom.style.bottom = 'auto' this.matchDom.style.top = (rect.top + rect.height) + 'px' // 匹配DOM的top = 父级矩形对象top + 父级的高度 } else { // 父级距离页面下边的高度小玉匹配DOM的高度,则往上展现 this.matchDom.style.top = 'auto' this.matchDom.style.bottom = (bottom + rect.height) + 'px' // 匹配DOM的bottom = 父级矩形对象bottom + 父级的高度 } } else { // 不聚焦则直接隐藏 this.matchDom.style.visibility = 'hidden' } } } } </script> <style lang="less" scoped> @import '../../assets/css/var.less'; .quater-select{ width: 200px; display: inline-block; height: 32px; color: #666; } .quater-model{ position: relative; display: inline-block; width: 100%; box-sizing: border-box; vertical-align: middle; color: #666; font-size: 14px; line-height: normal; height: 100%; .select-content{ display: block; box-sizing: border-box; outline: 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; cursor: pointer; position: relative; background-color: #fff; border-radius: 4px; border: 1px solid #dcdee2; transition: all .2s ease-in-out; padding: 0 24px 0 4px; height: 100%; line-height: 32px; &.select-focus, &:hover{ border-color:#f56752; } &.selected{ -webkit-box-shadow: 0 0 5px #f56752; box-shadow: 0 0 5px #f56752; outline: 0; } .default-show { font-size: 12px; color: #ccc; } } } .select-quater-wrap{ position: absolute; will-change: top, left; transform-origin: center top; left: 0; max-height: 200px; width: 200px; overflow: hidden; margin: 5px 0; padding: 5px 0; background-color: #fff; box-sizing: border-box; border-radius: 4px; box-shadow: 0 1px 6px rgba(0,0,0,.2); z-index: 1500; visibility: hidden; .year-header{ height: 32px; line-height: 32px; text-align: center; border-bottom: 1px solid #e8eaec; visibility: inherit; } .quarter-content{ padding: 8px 0; .flex-base(@flex-flow: row wrap;); visibility: inherit; .each-quarter{ flex: 0 0 50%; text-align: center; font-size: 14px; line-height: 30px; margin: 10px 0; &.active, &:hover{ .quarter-val{ color: #fff; background-color: #E84831; } } &.disabled{ .quarter-val{ cursor: not-allowed; color: #C5C8CE; background-color: transparent; &:hover{ color: #C5C8CE; background-color: #F7F7F7; } } } .quarter-val{ display: inline-block; padding: 0 10px; cursor: pointer; border-radius: 4px; transition: all .2s ease-in-out; } } } } .default-txt{ display: inline-block; line-height: 30px; color: #BFBFBF; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .disabled-next{ cursor: not-allowed; } </style>