autocomplete

4 阅读5分钟
<template>
  <!-- 使用transition组件实现下拉框的显示/隐藏动画效果 -->
  <!-- 动画名称为el-zoom-in-top,表示从上方缩放进入 -->
  <!-- @after-leave="doDestroy":当元素完全隐藏(离开动画结束后),触发doDestroy方法清理浮层 -->
  <transition name="el-zoom-in-top" @after-leave="doDestroy">
    <!-- 根元素div作为自动补全建议列表的容器 -->
    <!-- v-show="showPopper":控制下拉建议框的显隐,由父组件控制 -->
    <!-- class绑定基础类名 el-autocomplete-suggestion 和 el-popper(浮层通用样式) -->
    <!-- :class 动态绑定是否处于加载状态:当parent.hideLoading为false且parent.loading为true时,添加is-loading类 -->
    <!-- :style="{ width: dropdownWidth }":动态设置下拉框宽度,与输入框保持一致 -->
    <!-- role="region":ARIA属性,辅助技术识别该区域为一个独立的界面区域 -->
    <div
      v-show="showPopper"
      class="el-autocomplete-suggestion el-popper"
      :class="{ 'is-loading': !parent.hideLoading && parent.loading }"
      :style="{ width: dropdownWidth }"
      role="region"
    >
      <!-- 使用ElScrollbar组件包裹建议列表,支持滚动 -->
      <!-- tag="ul":将滚动容器渲染为ul标签语义 -->
      <!-- wrap-class 设置滚动外层包裹容器的类名 -->
      <!-- view-class 设置滚动内容区的类名 -->
      <el-scrollbar
        tag="ul"
        wrap-class="el-autocomplete-suggestion__wrap"
        view-class="el-autocomplete-suggestion__list"
      >
        <!-- 加载状态提示:当未隐藏加载图标且父组件loading为true时显示loading图标 -->
        <li v-if="!parent.hideLoading && parent.loading">
          <i class="el-icon-loading"></i>
        </li>
        <!-- 非加载状态下,插槽用于接收外部传入的建议项内容(通常是li列表项) -->
        <!-- slot标签已被废弃,此处保留兼容性写法,实际会被Vue编译为默认插槽内容 -->
        <slot v-else></slot>
      </el-scrollbar>
    </div>
  </transition>
</template>

<script>
// 导入 Popper 工具库,用于处理浮层定位(如弹出层、下拉菜单等)
// 实现基于 popper.js 的定位逻辑,确保下拉框能正确对齐输入框
import Popper from 'element-ui/src/utils/vue-popper';

// 导入 Emitter 混入对象,提供 dispatch 和 broadcast 方法
// 用于向上派发事件或向下广播事件,在组件树中进行通信
import Emitter from 'element-ui/src/mixins/emitter';

// 导入滚动条组件 ElScrollbar,用于在建议过多时提供滚动能力
import ElScrollbar from 'element-ui/packages/scrollbar';

export default {
  // 注册使用的子组件
  components: { ElScrollbar },

  // 使用混入(mixins)复用功能
  // Popper 提供浮层定位能力
  // Emitter 提供跨层级组件通信能力
  mixins: [Popper, Emitter],

  // 组件名称,用于调试和递归组件调用(非必须但推荐)
  componentName: 'ElAutocompleteSuggestions',

  // 响应式数据定义
  data() {
    return {
      // 引用父组件实例,便于直接访问其属性和方法
      parent: this.$parent,
      // 下拉框宽度,初始化为空字符串,后续根据输入框宽度动态设置
      dropdownWidth: ''
    };
  },

  // 接收外部传入的属性(props)
  props: {
    // Popper.js 的配置选项,默认关闭GPU加速(避免某些浏览器渲染问题)
    options: {
      default() {
        return {
          gpuAcceleration: false
        };
      }
    },
    // 为建议列表分配唯一ID,用于无障碍访问(ARIA)中的关联
    id: String
  },

  // 方法集合
  methods: {
    /**
     * 处理建议项被选中的逻辑
     * @param {Object} item - 被点击的建议项数据
     * 通过 dispatch 向上派发事件到 ElAutocomplete 组件,通知其处理选中行为
     */
    select(item) {
      this.dispatch('ElAutocomplete', 'item-click', item);
    }
  },

  // 生命周期钩子:组件更新后调用
  // 确保 DOM 已重新渲染后,更新浮层位置(防止错位)
  updated() {
    this.$nextTick(() => {
      // 判断 popperJS 实例是否存在,存在则调用 updatePopper 更新位置
      if (this.popperJS) {
        this.updatePopper();
      }
    });
  },

  // 生命周期钩子:组件挂载完成后调用
  // 初始化浮层相关元素引用,并设置必要的DOM属性
  mounted() {
    // 将当前组件的根元素 $el 赋值给父组件的 popperElm 和自身的 popperElm
    // 以便 Popper 混入可以获取浮层元素
    this.$parent.popperElm = this.popperElm = this.$el;

    // 获取参考元素(即输入框),优先取 input 或 textarea 元素
    this.referenceElm =
      this.$parent.$refs.input.$refs.input || this.$parent.$refs.input.$refs.textarea;

    // 获取建议列表的滚动内容区域
    this.referenceList = this.$el.querySelector('.el-autocomplete-suggestion__list');

    // 为列表添加 ARIA 语义化角色 listbox,提升可访问性
    this.referenceList.setAttribute('role', 'listbox');

    // 为列表设置唯一的 ID,与输入框的 aria-owns 关联
    this.referenceList.setAttribute('id', this.id);
  },

  // 生命周期钩子:组件创建完成时调用
  // 监听来自父组件的 'visible' 事件,控制下拉框显隐和宽度
  created() {
    // 使用 $on 监听事件总线上的 'visible' 事件
    // 参数 val 表示是否显示,inputWidth 是输入框宽度
    this.$on('visible', (val, inputWidth) => {
      // 动态设置下拉框宽度,加上 'px' 单位
      this.dropdownWidth = inputWidth + 'px';
      // 控制浮层显隐
      this.showPopper = val;
    });
  }
};
</script>
<template>
  <!-- 使用 transition 组件实现下拉框的显示/隐藏动画效果,名称为 el-zoom-in-top -->
  <!-- @after-leave:动画结束后触发 doDestroy 方法,用于销毁 Popper 实例 -->
  <transition name="el-zoom-in-top" @after-leave="doDestroy">
    <!-- v-show 控制下拉建议框的显隐状态 -->
    <!-- class 固定为 el-autocomplete-suggestion 和 el-popper,基础样式类 -->
    <!-- 动态 class:当父组件未隐藏加载图标且 loading 状态为 true 时,添加 is-loading 类 -->
    <!-- :style 动态绑定宽度,确保下拉框与输入框宽度一致 -->
    <!-- role="region" 提高可访问性,表示这是一个页面区域 -->
    <div
      v-show="showPopper"
      class="el-autocomplete-suggestion el-popper"
      :class="{ 'is-loading': !parent.hideLoading && parent.loading }"
      :style="{ width: dropdownWidth }"
      role="region"
    >
      <!-- 使用 ElScrollbar 滚动组件包裹列表内容,支持滚动 -->
      <!-- tag="ul" 表示最外层渲染为 ul 标签 -->
      <!-- wrap-class 设置滚动容器的类名 -->
      <!-- view-class 设置滚动视图的类名 -->
      <el-scrollbar
        tag="ul"
        wrap-class="el-autocomplete-suggestion__wrap"
        view-class="el-autocomplete-suggestion__list"
      >
        <!-- 加载状态:若父组件不隐藏加载图标且处于 loading,则显示加载动画 -->
        <li v-if="!parent.hideLoading && parent.loading">
          <i class="el-icon-loading"></i>
        </li>
        <!-- 非加载状态下插槽内容(即实际的建议项列表) -->
        <!-- slot 默认插槽,由 autocomplete.vue 中的 scoped slot 提供具体选项渲染逻辑 -->
        <slot v-else></slot>
      </el-scrollbar>
    </div>
  </transition>
</template>

<script>
// 导入依赖模块:
// Popper:提供下拉定位能力,基于 popper.js 实现元素定位
// Emitter:事件通信混入,支持向上派发事件(dispatch)
// ElScrollbar:Element UI 的滚动条组件,用于美化滚动区域
import Popper from 'element-ui/src/utils/vue-popper';
import Emitter from 'element-ui/src/mixins/emitter';
import ElScrollbar from 'element-ui/packages/scrollbar';

export default {
  // 注册子组件
  components: { ElScrollbar },

  // 使用混入(mixins)复用代码逻辑
  // Popper 提供弹出层定位功能
  // Emitter 提供父子组件间事件通信能力
  mixins: [Popper, Emitter],

  // 当前组件的名称,用于调试和递归引用
  componentName: 'ElAutocompleteSuggestions',

  // 响应式数据定义
  data() {
    return {
      // 引用父组件(即 autocomplete.vue 实例),便于直接访问其属性和方法
      parent: this.$parent,
      // 下拉框宽度,初始为空,后续通过监听事件动态设置
      dropdownWidth: ''
    };
  },

  // 接收外部传入的属性(props)
  props: {
    // Popper.js 的配置选项,默认关闭 GPU 加速(避免某些浏览器渲染问题)
    options: {
      default() {
        return {
          gpuAcceleration: false
        };
      }
    },
    // 下拉列表的唯一标识 id,用于 ARIA 可访问性支持
    id: String
  },

  // 方法集合
  methods: {
    /**
     * 处理建议项被选中的逻辑
     * 调用 dispatch 向上级组件(ElAutocomplete)发送 'item-click' 事件
     * 并传递当前选中的 item 数据
     * @param {Object} item - 当前点击的建议项数据
     */
    select(item) {
      this.dispatch('ElAutocomplete', 'item-click', item);
    }
  },

  // 生命周期钩子:组件更新后调用
  // 确保 DOM 已重新渲染后,更新 Popper 定位以应对位置变化
  updated() {
    this.$nextTick(() => {
      // 判断 popperJS 实例是否存在,存在则调用 update 方法刷新定位
      if (this.popperJS) {
        this.updatePopper();
      }
    });
  },

  // 生命周期钩子:组件挂载完成后执行
  // 初始化 Popper 相关的 DOM 元素引用,并设置语义化标签
  mounted() {
    // 将当前组件的根元素 $el 赋值给父组件的 popperElm 和自身的 popperElm
    this.$parent.popperElm = this.popperElm = this.$el;

    // 获取父组件中 input 或 textarea 的原生 DOM 引用作为 reference 元素(定位参考)
    this.referenceElm =
      this.$parent.$refs.input.$refs.input || this.$parent.$refs.input.$refs.textarea;

    // 获取建议列表 DOM 节点,用于后续属性设置
    this.referenceList = this.$el.querySelector('.el-autocomplete-suggestion__list');

    // 为列表添加 ARIA 语义化角色 listbox,提升无障碍访问体验
    this.referenceList.setAttribute('role', 'listbox');

    // 设置列表的 id 属性,与输入框 aria-owns 对应,建立关联
    this.referenceList.setAttribute('id', this.id);
  },

  // 生命周期钩子:组件创建完成时执行
  // 监听来自父组件的 'visible' 事件,控制下拉框显隐和宽度
  created() {
    this.$on('visible', (val, inputWidth) => {
      // 根据事件参数设置下拉框宽度(像素字符串格式)
      this.dropdownWidth = inputWidth + 'px';
      // 控制 Popper 显示/隐藏状态
      this.showPopper = val;
    });
  }
};
</script>