♻️(component): [tooltip] 重构 tooltip
*使用 script setup 语法重构 *新增 trigger,enterable属性
This commit is contained in:
parent
36d7f20b5b
commit
18b35e0449
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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[]`|
|
||||
|
||||
:::
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user