返回

Vue-Multiselect溢出终极解决(Vue 2.6适用)

vue.js

Vue-Multiselect (2.1.6) 下拉菜单 "溢出" 问题终极解决大法 (Vue 2.6 适用)

在使用 @shentao/vue-multiselect 2.1.6 版本的时候,碰到了一个让人头疼的问题:下拉菜单会被父容器限制住,显示不全!就像这样,被无情地“截肢”了:

(此处本应有图片,下拉菜单被截断的情况,用户可以自行脑补)

这个老版本的多选框组件没有 appendToBody 这个属性,所以才出了这档子事。 为了解决这个问题, 我可是费了不少劲! 下面, 我就来好好说道说道.

一、 问题出在哪儿?

根本原因就是 Vue-Multiselect 2.1.6 版本的 dropdown 元素是直接渲染在组件内部的。 当组件所在的容器设置了 overflow: hidden 或者高度限制时,下拉菜单超出容器的部分就会被隐藏。

就像这样:

<div style="overflow: hidden; height: 100px;">
  <multiselect ...></multiselect> 
  <!-- 下拉菜单会被限制在 div 里面 -->
</div>

二、 解决方案大放送!

下面提供几种解决方案, 各有优缺点, 大家按需选用。

1. 暴力拆解法:直接修改源码 (不推荐, 但可学习)

原理:

直接修改 vue-multiselect 的源码,将 dropdown 元素在组件挂载时,直接移动到 body 下。

操作步骤:

  1. 找到 node_modules/@shentao/vue-multiselect/dist/vue-multiselect.min.js 文件。

  2. 搜索 $mount 方法。

  3. 找到相关代码片段(例如: 创建 dropdown 的 div 元素的部分)。

  4. 修改创建后的挂载逻辑, 类似这样的:

    // 找到类似于这样的代码(具体可能不一样):
    // this.$refs.dropdown = document.createElement('div')
    // ...
    
    // 修改成:
    this.$refs.dropdown = document.createElement('div');
    this.$refs.dropdown.classList.add('multiselect__content-wrapper'); //示例: 保持样式
    // ...
    document.body.appendChild(this.$refs.dropdown); // 挂载到 body
    
  5. 添加$destroy生命周期中删除body中节点的逻辑:

       $destroy() {
          if (this.$refs.dropdown && this.$refs.dropdown.parentNode === document.body) {
            document.body.removeChild(this.$refs.dropdown);
          }
    	}
    

6.调整dropdown的位置计算, 实现dropdown相对input框的正确定位, 具体代码需要自行定位分析(比较复杂)。

代码示例 (仅为部分关键修改示意):

// vue-multiselect.min.js (修改后的部分)
// ...
$mount: function() {
    // ... 原有代码
    this.$refs.dropdown = document.createElement('div');
    this.$refs.dropdown.classList.add('multiselect__content-wrapper');
    // ... 其他属性设置

    document.body.appendChild(this.$refs.dropdown); // 移动到 body

    // ... 原有代码, 需要调整位置计算逻辑

      //在destroyed 生命周期里移除节点
      if (!this.$isServer) {
        // ... 其他监听
        window.addEventListener('beforeunload', this.deactivate);
      }
},
    $destroy() {
       if (this.$refs.dropdown && this.$refs.dropdown.parentNode === document.body) {
         document.body.removeChild(this.$refs.dropdown);
       }
		// ...
	},
// ...

优点:

  • 最彻底的解决方式,从根源上避免了 dropdown 被限制。

缺点:

  • 不推荐! 直接修改 node_modules 里的代码,项目重新 npm install 后修改会丢失。
  • 需要对组件源码非常熟悉,修改难度大。
  • 可能会引入新的问题, 需要自行处理。
  • 升级组件版本后需要重新修改。

进阶提示 : 可以考虑将修改后的文件单独保存,使用 patch-package 工具来管理对第三方库的修改。

2. 传送门大法:使用 Portal (强烈推荐)

原理:

使用 portal-vue 库(或者其他类似的库)将 dropdown 元素 “传送” 到 body 下。

操作步骤:

  1. 安装 portal-vue:

    npm install portal-vue --save
    
  2. 在 Vue 实例中注册 portal-vue:

    import PortalVue from 'portal-vue';
    
    Vue.use(PortalVue);
    
  3. 修改你的 multiselect 组件,用 <portal> 包裹住 dropdown 部分的 DOM(需要对组件模板进行调整,获取到 dropdown 对应的 DOM)。 考虑到我们拿不到源码,这里提供一种基于 ref 和 nextTick 的 hack 思路(假设我们在原组件外套了一个wrapper组件):

代码示例:

<template>
  <div>
    <multiselect
      ref="multiselect"
      v-bind="$attrs"
      v-on="$listeners"
      @open="handleOpen"
       @close="handleClose"
    />
    <portal to="multiselect-dropdown" :disabled="!isOpen">
      <div v-if="isOpen" ref="dropdownContent" class="multiselect__content-wrapper">
        </div>
    </portal>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isOpen: false,
        dropdownEl: null,
    };
  },
  mounted() {
    // 给body加一个div容器用于挂载dropdown
    const el = document.createElement("div")
    el.id="multiselect-dropdown"
    document.body.appendChild(el);
  },
  methods: {
      handleOpen() {
      this.isOpen = true;

      this.$nextTick(() => {
         //查找dom结构, 根据类名拿到 dropdown 元素
         this.dropdownEl = this.$refs.multiselect.$el.querySelector('.multiselect__content-wrapper');
          // 把 dropdown 元素搬到新的容器里面
         if(this.dropdownEl){
              this.$refs.dropdownContent.appendChild(this.dropdownEl);
          }

        this.positionDropdown(); // 调整位置
      });
        this.$emit('open')
    },
     handleClose(){
          this.isOpen = false;
          //还原原始结构
          if (this.dropdownEl && this.$refs.multiselect.$el) {
            this.$refs.multiselect.$el.appendChild(this.dropdownEl);
             this.dropdownEl = null;
          }

        this.$emit('close');

     },

    positionDropdown() {

      if (!this.$refs.dropdownContent || !this.$refs.multiselect) return;

      const inputRect = this.$refs.multiselect.$el.getBoundingClientRect();
      const dropdownContent = this.$refs.dropdownContent;
      // 示例, 需要微调 top left 的值,达到精准对其的目的
      dropdownContent.style.position = 'absolute';
      dropdownContent.style.left = `${inputRect.left}px`;
      dropdownContent.style.top = `${inputRect.bottom + window.scrollY}px`; //加上滚动高度
        dropdownContent.style.width = `${inputRect.width}px`
      // 其他样式调整,如最大高度等...

    },

  },

    beforeDestroy() {
    // 移除dom
    const el = document.getElementById("multiselect-dropdown")
    if(el){
     document.body.removeChild(el);
    }
  },
    //监听滚动, 输入框移动等变化事件, 重算位置.
  watch: {
        isOpen(val){
            if(val){
              window.addEventListener('resize', this.positionDropdown);
               window.addEventListener('scroll', this.positionDropdown, true);
            }else{
                window.removeEventListener('resize', this.positionDropdown);
                 window.removeEventListener('scroll', this.positionDropdown, true);
            }

        }
  },
};
</script>
<style scoped>
    /* 这里需要注意!  原组件内 .multiselect__content-wrapper 如果有position: absolute的设置需要通过important 覆盖! */
</style>

优点:

  • 推荐! 不需要修改 vue-multiselect 源码。
  • portal-vue 专门用于处理这类问题,使用方便。
  • 代码清晰,易于维护。

缺点:

  • 需要安装额外的库。
  • 需要获取到原组件下拉菜单对应的 DOM, 并根据类名找到对应的元素, 有一定风险。
  • 需要监听窗口变化(resize scroll), 计算 dropdown 元素的位置, 代码复杂度略有增加。

3. CSS 障眼法 (有限制, 场景特定)

原理:

通过调整 z-indexposition 属性,让 dropdown 元素在视觉上“浮”在其他元素之上。 但该方案有局限性.

操作步骤:

需要调整vue-multiselect 组件外层容器的 position 属性, 使其成为一个定位上下文, 然后在通过修改.multiselect__content-wrapper类的positionz-index让其浮动出来。

代码示例:

/*  组件的父容器 */
.multiselect-container {
  position: relative; /* 或者 absolute, fixed 等 */
  z-index: 1;
}

/* 在你的 CSS 文件或者 style 标签中添加 (注意样式优先级, 或许需要使用 !important) */
.multiselect__content-wrapper {
  position: absolute !important; /* 确保覆盖原有样式 */
  z-index: 9999 !important; /* 确保在最上层 */
      /* 其他可能需要的样式微调 */
  left: 0;
   /*  top,width 一般不用动,除非原组件计算出来的位置有问题*/
}

优点:

  • 简单快捷,不需要修改 JS 代码。

缺点:

  • 非常有限制!
    只有在 dropdown 不会被父容器以外的元素遮挡时才有效。
  • 可能存在兼容性问题,不同的浏览器表现可能不一致.

进阶说明:
这个方法适用一些比较简单的场景, 如果外层有复杂的定位或层级关系,此方法很可能会失效!

4. 造轮子大法: 自己动手, 丰衣足食(按需考虑, 实现成本高)

原理 :

从零开始,基于 Vue 2.6 实现一个具有 append to body 功能的多选组件。

优点:

  • 完全掌控,可以根据自己的需求定制功能。

缺点:

  • 开发成本高,需要花费大量时间和精力。
  • 需要测试, 处理各种兼容问题。

这里就不提供代码示例了, 因为从零开发需要较长篇幅。

三、安全建议

使用 portal-vue 或手动将元素添加到 body 时,要注意在组件销毁时将元素移除,避免内存泄漏。 (参考方法二beforeDestroy的逻辑)。

总结

在解决这个问题时,综合考虑了多种方法。 最终,我更推荐 “传送门大法”(使用 Portal) , 它既不需要修改源码,又相对简单可靠。 当然,“CSS 障眼法” 在一些简单场景下也可以使用。 如果实在想锻炼技术,“造轮子大法” 也是可以的,但要有心理准备,这会是一场“持久战”!