首页 > 其他分享 >popper/tooltip 组件

popper/tooltip 组件

时间:2023-12-15 17:14:53浏览次数:32  
标签:triggerRef const value popper tooltip 组件 van border

在 element-plus 中,popper 组件是 tooltip、select、date-picker 等触发式弹出层组件的基础,有了它就可以封装各种类似功能的组件了。

popper 组件依赖于 floating-ui,是对 floating-ui 的高级封装。

最终效果展示

03.gif

今天的完整代码放在 play/src/components/popper 里了

popper 组件封装

准备工作

首先要明白 popper 组件的两个要素

  1. 触发器:trggier
  2. 弹出层内容:popper content

02.png

element-plus 把所有的 popper 弹出内容都统一放到了一个 el-popper-container-xxx 的容器中,因此要先把这个容器创建出来

// play/src/components/popper/popper.ts
let cachedContainer: HTMLElement;
const selector = `van-popper-container-1996`;

type PopperContainerType = {
  container: HTMLElement;
  selector: string;
};

export const usePopperContainer = (): PopperContainerType => {
  if (!cachedContainer && !document.querySelector(`#${selector}`)) {
    const container = document.createElement("div");
    container.id = selector;
    cachedContainer = container;
    document.body.appendChild(container);
  }

  return {
    container: cachedContainer,
    selector,
  };
};
  • popper 组件的 props
const props = defineProps({
  effect: {
    type: String as PropType<"light" | "dark">,
    default: "dark",
  },
  content: {
    type: String,
  },
  transitionName: {
    type: String,
    default: "van-popper-fade",
  },
  placement: {
    type: String as PropType<Placement>,
    default: "bottom-start",
  },
  strategy: {
    type: String as PropType<Strategy>,
    default: "absolute",
  },
  showArrow: {
    // 是否显示箭头
    type: Boolean,
    default: true,
  },
  popperClass: {
    type: String,
    default: "",
  },
});

floating-ui 的简单使用

安装依赖

pnpm add @floating-ui/dom

一个简单的 popper

<template>
  <div class="container">
    <span ref="triggerRef" class="demo-btn" @click.stop="handleTrigger"
      >Trigger</span
    >
    <div
      v-if="visible"
      ref="contentRef"
      class="popper"
      :style="contentStyle"
      @mousedown.stop
    >
      My Popper
    </div>
  </div>
</template>

<script lang="ts" setup>
  import { computed, ref, watchEffect } from "vue";
  import { computePosition, flip, shift, offset } from "@floating-ui/dom";

  const triggerRef = ref();
  const contentRef = ref();
  const visible = ref(false);

  function handleTrigger() {
    visible.value = true;

    document.addEventListener(
      "mousedown",
      () => {
        visible.value = false;
      },
      { once: true }
    );
  }

  const middleware = [shift(), flip(), offset(10)];
  const x = ref(0);
  const y = ref(0);
  // 弹出框样式
  const contentStyle = computed(() => ({
    left: x.value + "px",
    top: y.value + "px",
  }));
  watchEffect(() => {
    if (triggerRef.value && contentRef.value) {
      computePosition(triggerRef.value, contentRef.value, { middleware }).then(
        (data) => {
          x.value = data.x;
          y.value = data.y;
        }
      );
    }
  });
</script>

<style lang="less">
  .popper {
    position: absolute;
    background: #222;
    color: white;
    font-weight: bold;
    padding: 5px;
    border-radius: 4px;
    font-size: 90%;
  }
</style>

解析:

  1. 首先要准备两个元素,触发器(trigger)和弹出层内容(popper)
  2. 当触发器和弹出层都挂载后执行 floating-ui 的 computePosition 函数传入需要的参数
  3. 该函数会返回位置信息和中间件信息
interface IData {
  middlewareData: Object; // 中间件信息
  placement: string; // 方位
  strategy: "absolute" | "fixed"; // 浮动策略
  x: number; // x坐标
  y: number; // y坐标
}

几个中间件介绍

  • offset 弹出层离触发器的偏移量
  • shift 保持元素在可视范围内
  • flip 切换到其他位置,以确保元素的可见性
  • arrow 箭头(后面有具体使用)

05.gif

接下来正式进入 Popper 组件的封装。。。

触发器

触发器是使用者插槽传递过来的,而必须要获取到触发器的 dom 为其注册触发事件,那么要注意以下两种情况

<span
  v-if="noWrap"
  ref="triggerRef"
  class="van-popper__trigger"
  v-bind="$attrs"
>
  <slot />
</span>
<component v-else :ref="setTriggerRef" :is="$slots.default" v-bind="$attrs" />
  1. 纯文本:noWrap 为 true 表示插槽为纯文本,使用了 span 标签包了一层
  2. 标签元素:如果是标签元素使用 component 渲染默认插槽
export function useTrigger() {
  const triggerRef = ref();
  const visible = ref(false);
  // 获取默认插槽
  const trggierSlot = useSlots!().default!();
  // 如果是纯文本
  const noWrap = computed(() => trggierSlot[0].patchFlag === 0);
  const open = () => (visible.value = true);
  const close = () => (visible.value = false);
  // 设置触发器dom
  const setTriggerRef = (el: HTMLElement | null) => {
    triggerRef.value = (el && el.nextElementSibling) || null;
  };
  // 点击触发器事件
  const onClick = (e: MouseEvent) => {
    e.stopPropagation();
    open();
    setTimeout(() => {
      document.addEventListener("mousedown", onDocumentMousedown, {
        once: true,
      });
    });
  };

  // 点击其它区域
  const onDocumentMousedown = () => {
    close();
  };
  onMounted(() => {
    if (!triggerRef.value) return;
    triggerRef.value.addEventListener("click", onClick);
  });

  onBeforeUnmount(() => {
    triggerRef.value!.removeEventListener("click", onClick);
  });

  return {
    setTriggerRef,
    triggerRef,
    visible,
    noWrap,
    open,
    close,
  };
}

// 使用时
const { setTriggerRef, triggerRef, visible, noWrap, trggierSlot } =
  useTrigger();

解析:

  1. useTrigger 的主要功能就是,拿到触发器的 dom 元素为其注册事件(这里只注册了点击事件)
  2. 获取触发器 dom 元素使用了两种方式,只会有一种生效。如果是纯文本 triggerRef 挂载后会自动赋值。否则 setTriggerRef 生效为 triggerRef 赋值
  3. 提供 open 和 close 方法控制弹出内容的显示与隐藏,visible 弹出内容的显示与隐藏,后面会用到

弹出内容

<Teleport :to="`#${selector}`">
  <Transition :name="transitionName">
    <div
      v-if="visible"
      ref="contentRef"
      :class="['van-popper', `is-${effect}`, popperClass]"
      :style="contentStyle"
      :data-side="side"
      @mousedown.stop
    >
      <slot name="content">{{ content }}</slot>
      <span
        v-if="showArrow"
        ref="arrowRef"
        class="van-popper__arrow"
        :style="arrowStyle"
      >
      </span>
    </div>
  </Transition>
</Teleport>
import {
  computed,
  ref,
  onMounted,
  watch,
  unref,
  getCurrentInstance,
} from "vue";
import { usePopperContainer, useFloating, useTrigger } from "./popper.ts";
import type { Placement, Strategy } from "@floating-ui/dom";
import { offset, arrow, shift, flip } from "@floating-ui/dom";

const { selector } = usePopperContainer();
const arrowRef = ref();
const middleware = computed(() => {
  const mds = [shift(), flip(), offset(10)];
  if (props.showArrow) {
    mds.push(arrow({ element: arrowRef.value }));
  }
  return mds;
});

const placement = ref(props.placement);
const strategy = ref(props.strategy);

const { x, y, contentRef, middlewareData, update } = useFloating(
  { middleware, placement, strategy },
  triggerRef
);

const side = computed(() => {
  return placement.value.split("-")[0];
});
// 弹出框样式
const contentStyle = computed(() => ({
  left: x.value + "px",
  top: y.value + "px",
}));
// 箭头样式
const arrowStyle = computed(() => {
  if (!props.showArrow) return {};
  const { arrow } = unref(middlewareData);
  return {
    left: arrow?.x + "px",
    top: arrow?.y + "px",
  };
});

解析:

  1. 调用刚刚写好的 usePopperContainer 返回的 selector 塞给 Teleport
  2. 接着准备 floating-ui 要使用到的数据,middleware 中间件,placement、strategy
  3. 核心部分 useFloating 这个 hook 里
type UseFloatingProps = ToRefs<{
  middleware: Array<Middleware>;
  placement: Placement;
  strategy: Strategy;
}>;
export const useFloating = (
  { middleware, placement, strategy }: UseFloatingProps,
  triggerRef: Ref<HTMLElement | null>
) => {
  const contentRef = ref();
  // popper位置信息
  const x = ref<number>();
  const y = ref<number>();
  // floating-ui 中间件数据
  const middlewareData = ref<ComputePositionReturn["middlewareData"]>({});
  const states = {
    x,
    y,
    placement,
    strategy,
    middlewareData,
  } as const;
  async function update() {
    if (!triggerRef.value || !contentRef.value) return;
    // floating-ui 的 computePosition计算位置
    const data: any = await computePosition(
      triggerRef.value,
      contentRef.value,
      {
        middleware: unref(middleware),
        placement: unref(placement),
        strategy: unref(strategy),
      }
    );

    Object.keys(states).forEach((key) => {
      (states as any)[key].value = data[key];
    });
  }

  onMounted(() => {
    watchEffect(() => {
      update();
    });
  });
  return {
    ...states,
    update,
    contentRef,
  };
};

主要使用 floating-ui 的 computePosition 根据传入的 触发器、内容 dom 和配置参数计算最新的位置信息和中间件信息。

  • xy:分别存储弹出内容的横向和纵向位置。
  • middlewareData:用于存储 floating-ui 中间件的计算数据。
  • states:用它的目的是循环赋值。
  • update 函数:用于更新弹出内容的位置。computePosition 计算新的位置数据。这个函数可用于 resize 窗口自适应的场景(这里并没有实现)

样式

样式不复杂,要考虑的主要是 arrow 箭头的不同表现

<style lang="less">
  @sides: {
    top: bottom;
    bottom: top;
    left: right;
    right: left;
  };
  @N: .van-popper;
  @{N} {
    --van-arrow-size: 10px;
    --van-popper-content-bg: #fff;
    --van-popper-border: 1px solid #ebedf0;
    border-radius: 4px;
    color: #000;
    background-color: var(--van-popper-content-bg);
    padding: 10px 12px;
    border: var(--van-popper-border);
    position: absolute;
    white-space: nowrap;
    transition: opacity 0.3s;
    font-size: 13px;
    z-index: 1000;
    width: max-content;

    &__arrow {
      position: absolute;
      width: 10px;
      height: 10px;
      background: var(--van-popper-content-bg);
      transform: rotate(45deg);
    }

    each(@sides, {
    &[data-side^='@{value}'] {
      @{N}__arrow {
        @{key}: -5px;
      }
      @{N}__arrow {
        border-@{key}: var(--van-popper-border);
      }
      @{N}__arrow when (@value =top) {
        border-right: var(--van-popper-border);
      }
      @{N}__arrow when (@value =bottom) {
        border-left: var(--van-popper-border);
      }
      @{N}__arrow when (@value =left) {
        border-top: var(--van-popper-border);
      }
      @{N}__arrow when (@value =right) {
        border-bottom: var(--van-popper-border);
      }
    }
  });

    &.is-dark {
      --van-popper-content-bg: #000;
      color: #fff;
      border: none;

      @{N}__arrow {
        border-color: transparent;
      }
    }
  }

  .van-popper-fade-enter-from,
  .van-popper-fade-leave-to {
    opacity: 0;
  }
</style>

以上就是 popper 的基本封装,还有很多可扩展的地方,比如:触发的方式还可以有 mouseenter/mouseleave、mousedown/mouseup、focus/blur 等

使用

<VanPopper effect="dark" content="你好呀!"> noWrap </VanPopper>
<VanPopper effect="dark" content="你也好呀!">
  <span class="demo-btn">wrap</span>
</VanPopper>

04.gif

标签:triggerRef,const,value,popper,tooltip,组件,van,border
From: https://www.cnblogs.com/wp-leonard/p/17903746.html

相关文章

  • 开源组件DockerFIle老是Build失败,如何解决
    推荐把外网地址替换为国内高速镜像#替换源地址http://dl-cdn.alpinelinux.org/alpine===https://mirrors.aliyun.com/alpinehttps://repo1.maven.org/maven2=====https://maven.aliyun.com/repository/public#goinstall代理proxy.golang.orggoenv-wGO11......
  • C++ Qt开发:DateTime日期时间组件
    Qt是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍QDateTime日期与时间组件的常用方法及灵活运用。在Qt中,日期和时间的处理通常使用QDateTime类。......
  • 日期选择器(date-picker)组件
    在工作中,经常需要使用日期选择器来让用户方便地选择日期。实现一个日期选择器组件。先来看看今天要实现的效果基础布局popper和日期选择器对应的元素触发器:也就是输入框弹出层:日期面板看看下面的基础布局,为了简化对时间的操作,这里使用dayjsdate-picker.ts//date-pi......
  • Sortable 拖拽排序组件实现原理
    如果想要实现拖拽排序功能,有很多现成的库可以供使用,比如Sortable.js(vuedraggable)、dnd-kit(react-dnd)等可以轻松帮助实现这一功能。本文的目标不是介绍如何使用这些库,而是手动实现一个简单版的Sortable组件。通过本文的阅读,您将深入了解拖拽排序的核心原理。使用模板使用Sor......
  • vue3Cron表达式组件
    npm安装no-vue3-cron引入报错,就直接把代码拿来自己改了no-vue3-cron仓库地址:https://github.com/wuchuanpeng/no-vue3-cronvue-cron.vue<stylelang="scss">.no-vue3-cron-div{.language{position:absolute;right:25px;z-index:1;}.el-tabs{......
  • 适配器模式揭秘:让不兼容的组件完美协同
    前言从这篇文章开始来盘一盘结构型设计模式,在开始之前先来简单回顾一下创建型的设计模式有哪些,如果有兴趣,就来一起学习吧:设计模式之简单工厂模式工厂方法模式:改变你对软件开发的认知_凡夫编程的技术博客_51CTO博客抽象工厂模式:角色解析与应用探索_凡夫编程的技术博客_51CTO博客设计......
  • Flutter 自带的搜索组件
    效果如下官方需要重写四个关键方法classsearchBarDelegateextendsSearchDelegate<String>{/*这个方法返回一个控件列表,显示为搜索框右边的图标按钮,这里设置为一个清除按钮,并且在搜索内容为空的时候显示建议搜索内容,使用的是showSuggestions(context)方法:*/@overrid......
  • vue2子组件拷贝父组件传递的参数
    在Vue2中,父组件向子组件传递参数时,如果参数是对象或数组(即非基本数据类型),那么子组件可以直接修改这个参数,这会影响到父组件的数据。如果你想避免这种情况,你可以让子组件对父组件的传参进行深度拷贝。这样,子组件就可以自由修改拷贝后的参数,而不会影响到父组件的数据。这是一个例......
  • C++ Qt开发:ProgressBar进度条组件
    Qt是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍ProgressBar进度条组件的常用方法及灵活运用。ProgressBar(进度条)是在Qt中常用的用户界面组件之一......
  • 界面组件DevExpress VCL v23.2新功能预览 - 支持RAD Studio 12.0
    本文即将发布DevExpressVCL 下一个主要更新(v23.2),在之前的文章中(点击这里回顾>>)我们为大家介绍了新的工具提示、图表空间中的标签重叠等,本文将主要介绍DevExpressVCLv23.2中将支持的RADStudio12.0、增强的图像选择器、字体和自定义图标包等。新版即将发布,敬请期待哦~获取De......