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

聖光之護
发布: 2025-11-14 08:33:16
原创
282人浏览过

解决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事件会从该子元素冒泡到其父元素,直到文档根部。

立即学习前端免费学习笔记(深入)”;

Motiff妙多
Motiff妙多

Motiff妙多是一款AI驱动的界面设计工具,定位为“AI时代设计工具”

Motiff妙多 22
查看详情 Motiff妙多

利用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的妙用的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
热门推荐
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号