新闻中心

解决Vue自定义多选组件中Blur事件失效问题:理解Focusout的妙用

2025-11-14
浏览次数:
返回列表

解决Vue自定义多选组件中Blur事件失效问题:理解Focusout的妙用

本文探讨了vue自定义多选组件中`blur`事件未能按预期触发的问题。由于`blur`事件不冒泡,当焦点在组件内部元素间转移时,外部`div`无法感知焦点离开。解决方案是使用`focusout`事件,它能够正确捕获组件内部或外部的焦点转移,从而实现选项列表的精确控制。

在构建复杂的自定义UI组件,特别是像多选下拉框这类涉及焦点管理和交互的组件时,正确处理焦点事件至关重要。开发者常遇到的一个挑战是,当用户在组件内部的输入框与选项列表之间切换,或点击组件外部时,如何准确地关闭选项列表。

理解 blur 事件的局限性

在Web开发中,blur事件用于表示元素失去焦点。然而,blur事件有一个重要的特性,那就是它不冒泡。这意味着当一个子元素失去焦点时,其父元素不会收到blur事件的通知。

考虑一个自定义多选组件,其结构通常包含一个外层容器div、一个输入框以及一个选项列表。如果我们在外层div上监听@blur事件以关闭选项列表,当焦点从输入框转移到选项列表中的某个li元素时(这两个元素都在外层div内部),外层div并不会触发blur事件。这是因为焦点仍在div的子元素之间移动,div本身并未失去焦点。只有当焦点完全移出这个div及其所有子元素时,blur事件才可能在外层div上被触发,但这并非通过事件冒泡机制实现,而是因为div本身失去了焦点。这种行为导致了在组件内部元素之间切换时,选项列表无法按预期关闭的问题。

focusout 事件的优势

为了解决blur事件不冒泡的问题,我们可以使用focusout事件。与blur事件不同,focusout事件会冒泡。这意味着当一个子元素失去焦点时,focusout事件会从该子元素冒泡到其父元素,直到文档根部。

Tanka Tanka

具备AI长期记忆的下一代团队协作沟通工具

Tanka 146 查看详情 Tanka

利用focusout的冒泡特性,我们可以在外层容器div上监听@focusout事件。当焦点从组件内部的任何元素(如输入框)转移到组件外部,或者从一个内部元素转移到另一个内部元素时,focusout事件都会被触发并冒泡到外层div。这样,我们就可以在外层div上统一处理焦点离开的逻辑,例如关闭选项列表。

实现 focusout 事件

将组件的外层容器上监听的@blur事件替换为@focusout即可解决问题。

<template>
  <div class="flex flex-col relative w-full">
    <span v-if="label" class="font-jost-medium mb-2">{{ label }}</span>
    <div>
      <!-- 将 @blur 替换为 @focusout -->
      <div @focusout="showOptions = false" :tabindex="tabIndex">
        <div
          class="border border-[#EAEAEA] bg-white rounded-md flex flex-col w-full"
        >
          <div
            v-if="selectedOptions.length"
            class="flex flex-wrap px-4 py-2 border-b gap-2"
          >
            <div
              v-for="option in selectedOptions"
              class="border bg-secondary rounded-full py-1 px-2 flex items-center"
            >
              <span>{{ option.text }}</span>
              <vue-feather
                type="x"
                class="h-3 w-3 ml-1.5 cursor-pointer"
                @click="onDeleteOption(option)"
              />
            </div>
          </div>
          <div
            class="flex flex-row justify-end items-center px-4 cursor-pointer"
            :class="selectedOptions.length ? 'py-2' : 'p-4'"
            @click="showOptions = !showOptions"
          >
            <MagnifyingGlassIcon class="h-5 w-5 mr-2" />
            <input
              class="focus:outline-0 w-full"
              type="text"
              v-model="searchInput"
            />
            <vue-feather type="chevron-down" class="h-5 w-5" />
          </div>
        </div>
        <div v-if="showOptions && optionsMap.length" class="options-list">
          <ul role="listbox" class="w-full overflow-auto">
            <li
              class="hover:bg-primary-light px-4 py-2 rounded-md cursor-pointer"
              role="option"
              v-for="option in optionsMap"
              @mousedown="onOptionClick(option)"
            >
              {{ option.text }}
            </li>
          </ul>
        </div>
        <div
          id="not-found"
          class="absolute w-full italic text-center text-inactive-grey"
          v-else-if="!optionsMap.length"
        >
          No records found
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType, ref, watch } from "vue";
import { IconNameTypes } from "@/types/enums/IconNameTypes";
import { AppIcon } from "@/components/base/index";
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";

export default defineComponent({
  name: "AppAutocomplete",
  components: {
    AppIcon,
    MagnifyingGlassIcon,
  },
  props: {
    modelValue: {
      type: String,
    },
    label: {
      type: String,
      default: "",
    },
    tabIndex: {
      type: Number,
      default: 0,
    },
    options: {
      type: Array as PropType<{ text: string; value: string }[]>,
      required: true,
    },
  },
  setup(props, { emit }) {
    const showOptions = ref(false);

    const optionsMap = ref(props.options);
    const selectedOptions = ref<{ text: string; value: string }[]>([]);
    const searchInput = ref("");
    watch(searchInput, () => {
      optionsMap.value = props.options.filter((option1) => {
        return (
          !selectedOptions.value.some((option2) => {
            return option1.text === option2.text;
          }) &&
          option1.text.toLowerCase().includes(searchInput.value.toLowerCase())
        );
      });
      sortOptionsMapList();
    });

    const onOptionClick = (option: { text: string; value: string }) => {
      searchInput.value = "";
      selectedOptions.value.push(option);
      optionsMap.value = optionsMap.value.filter((option1) => {
        return !selectedOptions.value.some((option2) => {
          return option1.text === option2.text;
        });
      });
      sortOptionsMapList();
      emit("update:modelValue", option.value);
    };

    const onDeleteOption = (option: { text: string; value: string }) => {
      selectedOptions.value = selectedOptions.value.filter((option2) => {
        return option2.text !== option.text;
      });
      optionsMap.value.push(option);
      sortOptionsMapList();
    };

    const sortOptionsMapList = () => {
      optionsMap.value.sort(function (a, b) {
        return a.text.localeCompare(b.text);
      });
    };
    sortOptionsMapList();

    // 移除不必要的全局事件监听,因为它可能与组件的焦点管理逻辑冲突
    // document.addEventListener("click", () => {
    //   console.log(document.activeElement);
    // });

    return {
      showOptions,
      optionsMap,
      searchInput,
      selectedOptions,
      IconNameTypes,
      onOptionClick,
      onDeleteOption,
    };
  },
});
</script>

<style scoped lang="scss">
.options-list,
#not-found {
  box-shadow: 0 0 50px 0 rgb(19 19 28 / 12%);

  @apply border border-[#EAEAEA] rounded-md p-4 mt-1 absolute bg-white z-10 w-full;
}
ul {
  @apply max-h-52 #{!important};
}
</style>

注意事项

  1. tabindex 的重要性: 确保你的组件外层容器具有tabindex属性,这样它才能接收焦点,并因此能够触发focusout事件。在示例代码中,tabindex被设置为0,这使得该元素可以通过键盘导航(Tab键)获取焦点。
  2. mousedown 与 click: 在选项列表的li元素上,使用@mousedown而非@click事件来选择选项是一个常见的技巧。这是因为mousedown事件在blur/focusout事件之前触发,这样可以确保在选项列表关闭之前,用户选择的选项已经被处理。如果使用@click,在某些情况下,focusout可能会先触发并关闭列表,导致click事件无法作用于已消失的元素。
  3. 事件处理逻辑: focusout事件会在焦点离开组件内部任何元素时触发,这包括离开输入框、离开选项列表等。确保你的@focusout处理函数(例如showOptions = false)能够正确响应这些场景,并根据业务逻辑决定是否需要添加额外的条件判断来精确控制选项列表的开闭。
  4. 避免全局事件监听: 原代码中存在一个document.addEventListener("click", ...)的全局监听器。在组件内部处理焦点和点击事件时,应尽量避免这种全局监听,因为它可能与组件自身的事件处理逻辑冲突,导致难以调试的问题。如果确实需要全局点击来关闭组件,可以考虑在组件挂载时添加,并在卸载时移除,并添加逻辑判断点击是否发生在组件外部。然而,对于焦点管理,focusout通常是更优雅的解决方案。

总结

在开发自定义Vue组件时,正确理解和运用DOM事件的特性至关重要。blur事件不冒泡的特性使其不适用于管理复杂组件内部的焦点转移。通过使用focusout事件,我们可以利用其冒泡机制,在外层容器上统一监听焦点离开事件,从而实现更健壮、更符合预期的焦点管理逻辑,有效控制组件(如多选下拉框)选项列表的显示与隐藏。

以上就是解决Vue自定义多选组件中Blur事件失效问题:理解Focusout的妙用的详细内容,更多请关注其它相关文章!


# 这是因为  # 完美网站建设全能资源  # 网站空间选择优化的特点  # 陵园网站建设价格表  # 咖啡产品网站建设原则  # 如何选择营销推广对象  # 静安搜索关键词排名多少钱  # 东莞原创网站建设公司  # 轴承行业网站建设平台  # 孝感企业网站推广外包  # 衡阳资讯网站建设优化  # 移除  # 至关重要  # 转移到  # 能与  # css  # 因为它  # 解决问题  # 输入框  # 多选  # 自定义  # red  # overflow  # 点击事件  # vue组件  # 事件冒泡  # v-if  # app  # vue 


相关栏目: 【 科技资讯46185 】 【 网络学院92790


相关推荐: 中兴Axon42Ultra怎样在文件App筛图_iPhone中兴Axon42Ultra文件App筛图【图片筛选】  php源码怎么在电脑上测试_电脑测试php源码方法步骤【教程】  J*a应用程序首次运行自动创建文件与目录的最佳实践  漫蛙官网正版漫画入口 漫蛙2官方网页登录地址  Python实时数据流中的动态最值查找策略  React Router 嵌套组件中 URL 重定向问题的解决方案  微博网页版官方账号登录 微博网页版内容浏览使用指南  AWS EC2实例间SQL Server连接超时:安全组配置与故障排除指南  Golang如何通过reflect获取匿名字段方法_Golang reflect匿名字段方法访问技巧  电脑安装程序提示“错误1722”怎么办_Windows Installer服务问题解决【教程】  React/Next.js中实现列表项的动态选择与移动  steam官方网页快速访问 steam账号注册全流程  C++如何连接MySQL数据库_C++使用Connector/C++操作MySQL数据库教程  C++如何比较两个字符串_C++ string compare函数与操作符对比  Odoo 16:在表单视图中基于当前记录动态修改Tree视图属性  抖音商城签到领现金是真的吗_抖音商城签到奖励与提现说明  Android Studio计算器C键逻辑错误排查与修复:条件判断优化指南  J*a TimerTask中HashMap意外清空的深层原因与解决方案  R星幕后开发视频泄露 包含《GTA6》等多款大作  TikTok国际版官网直达_TikTok国际版官网直达进入在线观看  sublime怎么预览Markdown渲染效果_Markdown Preview插件 for sublime教程  在FastAPI中利用lifespan与依赖注入高效管理Redis连接池  css滚动区域卡顿如何改善_css滚动问题用will-change优化渲染  构建轻量级网站内部消息系统:Formspree 集成指南  解决Django多数据库/多Schema环境下外键迁移问题  Angular Material 垂直步进器:实现底部到顶部排序的教程  PySpark中高效提取字符串右侧可变长度数字:使用regexp_extract  Typer应用中灵活处理命令行参数的令牌化与解析  C++如何实现一个装饰器模式_C++设计模式之动态地给对象添加额外职责  Adobe PDF表单中利用J*aScript解析与格式化日期组件的教程  如何将HTML表格多行数据保存到Google Sheets  C++如何打印当前代码行号与文件名_C++预定义宏FILE与LINE的使用  css子元素高度不一致导致布局错位怎么办_使用align-items:stretch解决高度差异  PySpark中从现有列右侧提取可变长度字符创建新列的教程  微信网页版官方快速登录入口 微信网页版网页版账号直达  Safari浏览器输入栏卡顿如何解决 Safari搜索建议与缓存清理  12306怎么选座位选到安静区_12306选座安静区域选择策略  解决移动端滚动问题的overflow属性应用指南  Win11怎么开启卓越性能模式 Win11电源选项启用高性能释放硬件潜力【方法】  Web Components中自定义开关组件状态同步的常见陷阱与解决方案  Win11 BitLocker密码忘了怎么办 Win11找回BitLocker恢复密钥方法【解决】  Word2013如何插入视频和音频媒体_Word2013媒体插入的多媒体支持  在React函数组件中利用原生HTML5进行邮箱地址验证  在J*a中如何使用BigDecimal进行高精度计算_BigDecimal类应用指南  Python模块化编程:有效管理依赖与避免循环引用  小红书怎么解除第三方平台绑定_小红书多平台登录解绑方法介绍  Win10系统服务哪些可以禁用 Win10安全优化服务列表【干货】  TikTok评论显示延迟如何处理 TikTok评论刷新优化方法  铁路12306改签能改到更早的车次吗_铁路12306改签提前车次规则  漫蛙2漫画入口 漫蛙正版网页漫画直达网址 

搜索