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>