vue組件開發——將彈層放置於 body 內,不受父級元素影響,在瀏覽器窗口改變或滾動時,依然跟隨目標元素

問題描述

elementUI和iview中select選擇框都有屬性配置:是否將彈層放置於 body 內,它將不受父級樣式影響,從而達到更好的效果。若是父級彈框設置了overflow: hidden,彈層也能正常展現而且超出父級彈框。
但項目開發中常常會遇到須要自定義重寫select的狀況,也須要支持這種效果。javascript

1.png

1.將組件中須要動態展現的DOM添加到body中
<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)
      }
    })
  },
2.計算當前匹配DOM相對於body的位置
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()
    }
  }
3.監聽瀏覽器窗口大小改變和滾動事件,動態改變匹配DOM的位置
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)
  },
效果

2.png


附上完整代碼
<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>
相關文章
相關標籤/搜索