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>
相关文章
相关标签/搜索