框架:Vue+Element

问题:

element ui下拉框格式化 element下拉菜单_elementui


当点击select,弹出popper后,再滚动外层容器区域,popper的scroll监听会不停的变动popper的位置。这是正常现象。

但是,由于我们的DOM结构嵌套太多,布局太复杂。出现了一个特别的现象:

element ui下拉框格式化 element下拉菜单_javascript_02

popper没有正常的跟随select的位置变化定位。

如果发生这种情况,比较好处理的是直接可以加一下select 的 popper-append-to-body 属性 并设置为false值(whether to append the popper menu to body. If the positioning of the popper is wrong, you can try to set this prop to false——element官网对此属性的解释)

但是由于我们奇葩的布局结构,我们容器外一层容器的overflowhidden。便导致了如下问题。

element ui下拉框格式化 element下拉菜单_vue.js_03


导致直观看上去,下拉菜单的popper显示不全。

这种问题比上面只是position错位的问题更加严重。

产品提出的方案是,scroll的时候,select的下拉菜单消失

好了,问题就是这么个问题,开发如果刚一点,怼一顿设计/产品就能解决。

我虽然刚,但是我本着尽职尽责(人渣代码不能渣

问题简单想,首先我们知道,select的定位是根据scroll的监听事件去计算出来并赋值给style的。

1/ 找到原监听scroll的方法
2/ 判断是否需要页面滚动popper消失
3/ 控制popper消失

找到原监听scroll的方法

这个只要去element源码中,全局搜索"scroll",或者'scroll';大概看看代码中使用监听的方式为addEventListener('scroll';继续全局搜索addEventListener('scroll',范围就缩小到2位数的文件了。

巧妙的就会找到src/utils/popper.js 这个文件中的Popper.prototype._setupEventListeners这个方法。当然第一次看到这个方法,其实也是一头雾水,最好的方式就是当前方法变量全log出来看看都是什么东西。然后再看看_setupEventListeners这个方法那里用到了。结合良好的注释,你就会知道,只要点击select,下拉菜单弹出来,这个scroll监听就已经发生了。

然而他原本的监听就是为了更改定位。我们不研究,我们则需要在同时,加一个监听方法,去执行popper消失的代码。

判断是否需要页面滚动popper消失

毕竟通过popper-apend-to-body这种官方的解决方案不满足的情况还是少之又少。所以这个判断需要在select本身的属性上扩展一个字段,我拙劣的起名为:scroll-hidden-popper。类型为布尔值。现在就要研究,我们如何通过select的props传给到这个Poper文件内。

这时在之前的console变量就发挥了用武之地。可以发现在this._options中,我们看到的东西比较像Dom的props参数。所以在本文件查找这个this._options.是如何赋值的。

function Popper(reference, popper, options) {
  this._options = Object.assign({}, DEFAULTS, options);
}

截取的代码段如上。所以我们看到,是Popper方法传进来的。那我们去找一下Popper方法哪里用到了。搜索一下这个文件的路径:/popper,就找到了这个文件:src/utils/vue-popper.js

import Vue from 'vue';
const PopperJS = Vue.prototype.$isServer ? function() {} : require('./popper');

export default {
  props: {
    popperOptions: {
      type: Object,
      default() {
        return {
          gpuAcceleration: false
        };
      }
    }
  },

  data() {
    return {
      showPopper: false,
      currentPlacement: ''
    };
  },

  watch: {
    value: {
      immediate: true,
      handler(val) {
        this.showPopper = val;
        this.$emit('input', val);
      }
    },

    showPopper(val) {
      if (this.disabled) return;
      val ? this.updatePopper() : this.destroyPopper();
      this.$emit('input', val);
    }
  },

  methods: {
    createPopper() {
      const options = this.popperOptions;
      this.popperJS = new PopperJS(reference, popper, options);
    }
  }
}

关键代码截取如上,popper初始化注册了new PopperJS对象,传入了popperOptions属性值,既我们要找的options哪里来的。

所以我们需要在select 的Dom结构中,找到popper使用的dom结构,传入popperOptions。

控制popper消失

让popper消失的方法有很多种思路
一开始找到了hidden,但是发现,hidden一经设置值,还需要再设置回去,操作繁琐。pass
display: none——同上,pass
转念一想,input的blur事件

思路全通了,直接上代码

packages/select/src/select.vue

  • select 的popperDom中增加popper-options属性
<el-select-menu
  ref="popper"
  :append-to-body="popperAppendToBody"
  :popper-options="popperOptions"
  v-show="visible && emptyText !== false">
...
</el-select-menu>
  • Props中接受参数: scrollHiddenPopper
props: {
  scrollHiddenPopper: {
    type: Boolean,
    default: false
  }
}
  • data中初始化参数:popperOptions
data() {
  popperOptions: {
    scrollHiddenPopper: false
  }
}
  • mounted方法中处理:popperOptions,这里定义一个callback函数,在Popper的scroll监听事件中调用
mounted() {
  if (this.scrollHiddenPopper) {
    this.popperOptions = {
      scrollHiddenPopper: this.scrollHiddenPopper,
      hiddenCallback: () => {
        this.blur();
      }
    };
  }
}
  • method方法中原有的blur()方法
blur() {
  this.visible = false;
  this.$refs.reference.blur();
}

src/utils/popper.js

Popper.prototype._setupEventListeners = function() {
  if (this._options.boundariesElement !== 'window') {
    var target = getScrollParent(this._reference);
    // 滚动popper自动隐藏
    function scrollHiddenPopperFun (target, popper, fun) {
      // 这里来判断参数scrollHiddenPopper
      if (popper._options.scrollHiddenPopper) {
     	// 调用callback(即blur事件)
        popper._options.hiddenCallback();
        // 在监听内,结束任务后,立即移除监听
        target.removeEventListener('scroll', fun);
      }
    }
    var fun = function () {
      scrollHiddenPopperFun(target, this, fun)
    }.bind(this)
    // 添加监听事件
    target.addEventListener('scroll', fun);
  }
};

回顾代码,发现问题,如果当select放在body层,直接出发scroll,且需要 hidden popper的话,需要把scroll的监听移动到if (this._options.boundariesElement !== 'window')外部。

以上解决问题

附加解决的问题是:如果popper移动到当前可视的view外,会导致部分显示的问题,且由于popper的z-index很大,会覆盖别的层,有一些情况就无法允许了。

这些就是今天解决组件库问题的思路记载,希望能解决本问题的同时,也能提供给大家一些解决问题的思路。如果有更好的解决办法和建议,不要吝啬在评论里叨逼叨。

感谢大家~