♻️(component): [tooltip] 重构 tooltip

*使用 script setup 语法重构
*新增 trigger,enterable属性
This commit is contained in:
sight 2022-09-10 05:19:58 +08:00
parent 36d7f20b5b
commit 18b35e0449
4 changed files with 255 additions and 177 deletions

View File

@ -19,6 +19,7 @@ import {
Fragment,
cloneVNode,
useAttrs,
StyleValue,
} from "vue";
import {
computed,
@ -69,6 +70,8 @@ export interface LayDropdownProps {
// ,
alignPoint?: boolean;
popupContainer?: string | undefined;
contentClass?: string | Array<string | object> | object;
contentStyle?: StyleValue;
}
const props = withDefaults(defineProps<LayDropdownProps>(), {
@ -664,8 +667,13 @@ defineExpose({ show, hide, toggle });
<div
v-if="openState"
ref="contentRef"
class="layui-dropdown-content layui-anim layui-anim-upbit"
:style="contentStyle"
:class="[
'layui-dropdown-content',
'layui-anim',
'layui-anim-upbit',
props.contentClass,
]"
:style="[contentStyle, props.contentStyle ?? '']"
@mouseenter="handleMouseEnterWithContext"
@mouseleave="handleMouseLeaveWithContext"
>

View File

@ -2,10 +2,12 @@
<teleport to="body" v-if="isExist">
<transition v-show="innerVisible">
<div
ref="popper"
:class="['layui-popper', { 'layui-dark': isDark }]"
:style="style"
ref="popperRefEl"
:class="['layui-popper', { 'layui-dark': isDark }, props.popperClass]"
:style="[style, props.popperStyle ?? '']"
:position="position"
@mouseenter="handlerPopperMouseEnter"
@mouseleave="handlerPopperMouseLeave"
>
<slot>{{ content }}</slot>
<div class="layui-popper-arrow"></div>
@ -28,25 +30,30 @@ import {
ref,
watch,
onMounted,
watchEffect,
nextTick,
onBeforeUnmount,
useSlots,
shallowRef,
computed,
toRef,
StyleValue,
} from "vue";
import { on } from "../../utils/domUtil";
import { useThrottleFn } from "@vueuse/core";
import { onClickOutside, useEventListener, useThrottleFn } from "@vueuse/core";
export type PopperTrigger = "click" | "hover" | "focus" | "contextMenu";
export interface LayPopperProps {
el: any;
el: HTMLElement;
content?: string | Number;
position?: string;
trigger?: string;
trigger?: PopperTrigger | PopperTrigger[];
enterable?: boolean;
isDark?: boolean;
disabled?: boolean;
isCanHide?: boolean;
isAutoShow?: boolean;
visible?: boolean;
popperClass?: string | Array<string | object> | object;
popperStyle?: StyleValue;
}
const props = withDefaults(defineProps<LayPopperProps>(), {
@ -61,50 +68,18 @@ const props = withDefaults(defineProps<LayPopperProps>(), {
});
const slots = useSlots();
const EVENT_MAP: any = {
hover: ["mouseenter", null, "mouseleave", false],
click: ["click", document, "click", true],
};
const triggerArr = EVENT_MAP[props.trigger];
if (!triggerArr) {
console.error(`${NAME} render error!cause: 'Trigger' must be 'hover/click' `);
}
const style = ref<CSSProperties>({ top: -window.innerHeight + "px", left: 0 });
const checkTarget = ref(false);
const popper = ref<HTMLDivElement>({} as HTMLDivElement);
const triggerRefEl = toRef(props, "el");
const popperRefEl = shallowRef<HTMLDivElement>({} as HTMLDivElement);
const innnerPosition = ref(props.position);
const innerVisible = ref(props.visible);
const isExist = ref(props.visible);
let scrollElements: HTMLElement[] | undefined;
watch(
() => props.visible,
(val) => {
if (val) {
doShow();
} else {
doHidden();
}
}
const triggerMethods = computed(() =>
([] as Array<PopperTrigger>).concat(props.trigger)
);
watch(innerVisible, (val) => {
invokeShowPosistion();
});
watch(popper, (val) => {
if (props.trigger === "hover" && props.enterable) {
on(popper.value, EVENT_MAP["hover"][0], doShow);
on(popper.value, EVENT_MAP["hover"][2], doHidden);
}
});
watch([() => props.content, () => slots.content && slots?.content()], (val) => {
innerVisible.value && invokeShowPosistion();
});
const doShow = function () {
if (!props.disabled) {
if (!isExist.value) {
@ -119,10 +94,6 @@ const doShow = function () {
};
const doHidden = function (e?: MouseEvent) {
if (checkTarget.value && props.el.contains(e?.target)) {
return;
}
// isCanHide
if (props.isCanHide === false) {
return;
@ -132,36 +103,132 @@ const doHidden = function (e?: MouseEvent) {
style.value = { top: -window.innerHeight + "px", left: 0 };
};
//
const showPosistion = function () {
const calcPosistion = function () {
postionFns[props.position] &&
(style.value = postionFns[props.position](
props.el,
popper.value,
triggerRefEl.value,
popperRefEl.value,
innnerPosition
));
};
const invokeShowPosistion = function () {
const updatePosistion = function () {
if (innerVisible.value) {
popper.value.offsetWidth === 0
? nextTick(() => showPosistion())
: showPosistion();
popperRefEl.value.offsetWidth === 0
? nextTick(() => calcPosistion())
: calcPosistion();
nextTick(() => {
showPosistion();
calcPosistion();
});
}
};
let scrollElements: HTMLElement[] | undefined;
const handlerPopperMouseEnter = function () {
if (triggerMethods.value.includes("hover") && props.enterable) {
doShow();
}
};
const isScrollElement = (element: HTMLElement) => {
const handlerPopperMouseLeave = function () {
if (triggerMethods.value.includes("hover") && props.enterable) {
doHidden();
}
};
const handlerTriggerClick = function () {
if (!triggerMethods.value.includes("click")) return;
if (innerVisible.value) {
doHidden();
} else {
doShow();
}
};
const handleTriggerContextMenu = function () {
if (!triggerMethods.value.includes("contextMenu")) return;
if (innerVisible.value) {
doHidden();
} else {
doShow();
}
};
const handlerTriggerMouseEnter = function () {
if (!triggerMethods.value.includes("hover")) return;
doShow();
};
const handlerTriggerMouseLeave = function () {
if (!triggerMethods.value.includes("hover")) return;
doHidden();
};
const handleTriggerFocusin = function () {
if (triggerMethods.value.includes("focus") && props.enterable) {
doShow();
}
};
const handleTriggerFocusout = function () {
if (triggerMethods.value.includes("focus") && props.enterable) {
doHidden();
}
};
const handlerTriggerEventRegist = function () {
useEventListener(triggerRefEl.value, "click", handlerTriggerClick);
useEventListener(triggerRefEl.value, "contextmenu", handleTriggerContextMenu);
useEventListener(triggerRefEl.value, "mouseenter", handlerTriggerMouseEnter);
useEventListener(triggerRefEl.value, "mouseleave", handlerTriggerMouseLeave);
useEventListener(triggerRefEl.value, "focusin", handleTriggerFocusin);
useEventListener(triggerRefEl.value, "focusout", handleTriggerFocusout);
};
const handleScroll = useThrottleFn(() => {
if (innerVisible.value) {
updatePosistion();
}
}, 15);
onClickOutside(
triggerRefEl.value,
(e) => {
if (
!innerVisible.value ||
triggerRefEl.value.contains(e.target as HTMLElement) ||
popperRefEl.value.contains(e.target as HTMLElement)
) {
return;
}
doHidden();
},
{
ignore: [popperRefEl.value],
}
);
watch(
() => props.visible,
(isShow) => (isShow ? doShow() : doHidden())
);
watch(innerVisible, (val) => {
updatePosistion();
});
watch([() => props.content, () => slots.content && slots?.content()], (val) => {
innerVisible.value && updatePosistion();
});
const isScrollElement = function (element: HTMLElement) {
return (
element.scrollHeight > element.offsetHeight ||
element.scrollWidth > element.offsetWidth
);
};
const getScrollElements = (container: HTMLElement | undefined) => {
const getScrollElements = function (container: HTMLElement | undefined) {
const scrollElements: HTMLElement[] = [];
let element: HTMLElement | undefined = container;
while (element && element !== document.documentElement) {
@ -173,33 +240,13 @@ const getScrollElements = (container: HTMLElement | undefined) => {
return scrollElements;
};
const handleScroll = useThrottleFn(() => {
if (innerVisible.value) {
invokeShowPosistion();
}
}, 15);
//
on(props.el, triggerArr[0], doShow);
on(triggerArr[1] ?? props.el, triggerArr[2], doHidden);
checkTarget.value = triggerArr[3];
onMounted(() => {
invokeShowPosistion();
scrollElements = getScrollElements(props.el);
updatePosistion();
scrollElements = getScrollElements(triggerRefEl.value);
for (const item of scrollElements) {
item.addEventListener("scroll", handleScroll);
useEventListener(item, "scroll", handleScroll);
}
window.addEventListener("resize", handleScroll);
});
onBeforeUnmount(() => {
if (scrollElements) {
for (const item of scrollElements) {
item.removeEventListener("scroll", handleScroll);
}
scrollElements = undefined;
}
window.removeEventListener("resize", handleScroll);
useEventListener("resize", handleScroll);
handlerTriggerEventRegist();
});
</script>

View File

@ -1,93 +1,114 @@
<script lang="ts">
export default {
name: "LayTooltip",
inheritAttrs: false,
};
</script>
<script lang="ts" setup>
import "./index.less";
import LayPopper from "../popper/index.vue";
import {
computed,
getCurrentInstance,
nextTick,
onMounted,
PropType,
ref,
shallowRef,
StyleValue,
} from "vue";
import { useEventListener } from "@vueuse/core";
export type PopperTrigger = "click" | "hover" | "focus" | "contextMenu";
const props = defineProps({
content: {
type: [Number, String],
required: false,
},
position: {
type: String,
default: "top",
},
isDark: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
isCanHide: {
type: Boolean,
default: true,
},
isAutoShow: {
type: Boolean,
default: false,
},
visible: {
type: Boolean,
default: false,
},
trigger: {
type: String as PropType<PopperTrigger | PopperTrigger[]>,
default: "hover",
},
enterable: {
type: Boolean,
default: true,
},
popperClass: {
type: [String, Array, Object],
},
popperStyle: {
type: Object as PropType<StyleValue>,
},
});
const vm = getCurrentInstance()!;
const isMounted = ref(false);
const tooltipRefEl = shallowRef<HTMLElement | undefined>(undefined);
const innerProps = computed(() => {
return { el: vm.proxy!.$el.nextElementSibling, ...vm.proxy!.$props };
});
const setEllipsis = function () {
if (tooltipRefEl.value) {
let tooltipHtml = tooltipRefEl.value;
if (
tooltipHtml.offsetWidth >=
(tooltipHtml.firstChild as HTMLElement)?.offsetWidth
) {
isMounted.value = false;
} else {
isMounted.value = true;
}
} else {
isMounted.value = true;
}
};
onMounted(() => {
if (props.isAutoShow) {
useEventListener("resize", () => {
setEllipsis();
});
}
nextTick(() => {
setEllipsis();
});
});
</script>
<template>
<div v-if="isAutoShow" class="lay-tooltip-content" ref="refTooltip">
<span><slot></slot></span>
<div ref="tooltipRefEl" v-if="isAutoShow" class="lay-tooltip-content">
<span>
<slot></slot>
</span>
</div>
<slot v-else></slot>
<lay-popper v-if="isMounted" v-bind="innerProps">
<slot name="content"></slot>
</lay-popper>
</template>
<script lang="ts">
import "./index.less";
import LayPopper from "../popper/index.vue";
import { defineComponent, PropType, ref } from "vue";
import { useEventListener } from "@vueuse/core";
export default defineComponent({
name: "LayTooltip",
components: {
LayPopper,
},
props: {
content: {
type: [Number, String],
required: false,
},
position: {
type: String,
default: "top",
},
isDark: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
isCanHide: {
type: Boolean,
default: true,
},
isAutoShow: {
type: Boolean,
default: false,
},
visible: {
type: Boolean,
default: false,
},
trigger: {
type: String as PropType<"click" | "hover">,
default: "hover",
},
},
setup() {
const isMounted = ref(false);
const refTooltip = ref<any>(null);
return {
isMounted,
refTooltip,
};
},
computed: {
innerProps() {
return { el: this.$el.nextElementSibling, ...this.$props };
},
},
mounted() {
if (this.isAutoShow) {
useEventListener("resize", () => {
this.setEllipsis();
});
}
this.$nextTick(() => {
this.setEllipsis();
});
},
methods: {
setEllipsis() {
if (this.refTooltip) {
let tooltipHtml = this.refTooltip;
if (tooltipHtml.offsetWidth >= tooltipHtml.firstChild.offsetWidth) {
this.isMounted = false;
} else {
this.isMounted = true;
}
} else {
this.isMounted = true;
}
},
},
});
</script>

View File

@ -57,7 +57,7 @@ setup() {
<template>
<lay-button @click="visible = !visible">{{visible ? '显示' : '隐藏'}}</lay-button>
<div style="padding: 100px">
<lay-tooltip position="right" content="时光都淡了,我还伴着你。" :visible="visible">
<lay-tooltip position="right" content="假装这里有文字提示" :visible="visible">
<lay-button>tooltip</lay-button>
</lay-tooltip>
</div>
@ -189,7 +189,9 @@ export default {
| disabled | 是否禁用 | `false`(默认值)、`true`(禁用) |
| isCanHide | 控制是否可以隐藏,可参考`lay-slider`组件 | `true`(默认值)、`false` |
| isAutoShow | 控制超出文本 `...` 时自动展示, 没有 `...` 时不展示 | `false`(默认值)、`true` |
| visible | 控制显示/隐藏| `true` `false`|
| visible | 控制显示/隐藏| `true` `false`(默认值)|
| enterable | 鼠标是否能进入 tooltip 中 | `true`(默认值) `false`|
| trigger | 触发方式| `click` `hover`(默认值) `contextmenu` `focus` `trigger[]`|
:::