(dropdown): 支持渲染到 body

This commit is contained in:
sight 2022-07-03 00:55:13 +08:00
parent 18edd93618
commit 7ed837fe7d
3 changed files with 179 additions and 59 deletions

View File

@ -3,8 +3,8 @@
display: inline-block;
}
.layui-dropdown-content,
.layui-dropdown dl {
display: none;
position: absolute;
z-index: 899;
background-color: #fff;
@ -14,6 +14,7 @@
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
}
.layui-dropdown-content > .layui-dropdown-menu,
.layui-dropdown dl > .layui-dropdown-menu {
border-radius: var(--global-border-radius);
}
@ -22,16 +23,20 @@
display: block;
}
.layui-dropdown-content .layui-menu,
.layui-dropdown .layui-menu {
position: relative;
background-color: #fff;
}
.layui-dropdown-content .layui-menu li,
.layui-dropdown-content .layui-menu-body-title a,
.layui-dropdown .layui-menu li,
.layui-dropdown .layui-menu-body-title a {
padding: 5px 15px;
}
.layui-dropdown-content .layui-menu li,
.layui-dropdown .layui-menu li {
position: relative;
display: flex;
@ -42,10 +47,12 @@
cursor: pointer;
}
.layui-dropdown-content .layui-menu li:hover,
.layui-dropdown .layui-menu li:hover {
background-color: var(--global-neutral-color-2);
}
.layui-dropdown-content .layui-menu-body-title,
.layui-dropdown .layui-menu-body-title {
white-space: nowrap;
overflow: hidden;
@ -59,6 +66,7 @@
margin-left: 15px;
}
.layui-dropdown-content .layui-line-horizontal,
.layui-dropdown .layui-line-horizontal{
margin: 0px;
border-color: #EEEEEE;

View File

@ -6,7 +6,7 @@ export default {
<script setup lang="ts">
import "./index.less";
import type { CSSProperties } from "vue";
import { CSSProperties, inject, reactive, Ref } from "vue";
import {
computed,
nextTick,
@ -24,7 +24,12 @@ import {
useThrottleFn,
useWindowSize,
} from "@vueuse/core";
import type { DropdownPlacement } from "./interface";
import {
dropdownInjectionKey,
DropdownPlacement,
ElementScrollRect,
DropdownContext,
} from "./interface";
export type DropdownTrigger = "click" | "hover" | "focus" | "contextMenu";
@ -47,6 +52,7 @@ export interface LayDropdownProps {
mouseLeaveDelay?: number;
focusDelay?: number;
alignPoint?: boolean;
renderToBody?: boolean;
}
const props = withDefaults(defineProps<LayDropdownProps>(), {
@ -67,8 +73,11 @@ const props = withDefaults(defineProps<LayDropdownProps>(), {
mouseLeaveDelay: 150,
focusDelay: 150,
alignPoint: false,
renderToBody: false,
});
const childrenRefs = new Set<Ref<HTMLElement>>();
const dropdownCtx = inject<DropdownContext>(dropdownInjectionKey, undefined);
const dropdownRef = shallowRef<HTMLElement | undefined>();
const contentRef = shallowRef<HTMLElement | undefined>();
const contentStyle = ref<CSSProperties>({});
@ -133,12 +142,34 @@ const changeVisible = (visible: boolean, delay?: number) => {
}
};
const containerRef = computed(() =>
props.renderToBody ? document.body : dropdownRef.value
);
const getElementScrollRect = (element: HTMLElement, containerRect: DOMRect) => {
const rect = element.getBoundingClientRect();
return {
top: rect.top,
bottom: rect.bottom,
left: rect.left,
right: rect.right,
width: rect.width,
height: rect.height,
scrollTop: rect.top - containerRect.top,
scrollBottom: rect.bottom - containerRect.top,
scrollLeft: rect.left - containerRect.left,
scrollRight: rect.right - containerRect.left,
};
};
const updateContentStyle = () => {
if (!dropdownRef.value || !contentRef.value) {
if (!containerRef.value || !dropdownRef.value || !contentRef.value) {
return;
}
const triggerRect = dropdownRef.value!.getBoundingClientRect();
const contentRect = contentRef.value.getBoundingClientRect();
const containerRect = containerRef.value.getBoundingClientRect();
const triggerRect = getElementScrollRect(dropdownRef.value, containerRect);
const contentRect = getElementScrollRect(contentRef.value, containerRect);
const { style } = getContentStyle(
props.placement,
triggerRect,
@ -156,8 +187,14 @@ const updateContentStyle = () => {
if (props.autoFitPosition) {
nextTick(() => {
const triggerRect = dropdownRef.value!.getBoundingClientRect();
const contentRect = contentRef.value!.getBoundingClientRect();
const triggerRect = getElementScrollRect(
dropdownRef.value as HTMLElement,
containerRect
);
const contentRect = getElementScrollRect(
contentRef.value as HTMLElement,
containerRect
);
let { top, left } = style;
top = Number(top.toString().replace("px", ""));
left = Number(left.toString().replace("px", ""));
@ -179,8 +216,8 @@ const updateContentStyle = () => {
const getContentStyle = (
placement: DropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect,
triggerRect: ElementScrollRect,
contentRect: ElementScrollRect,
isAlignPoint?: boolean,
{
customStyle = {},
@ -224,19 +261,19 @@ const getFitPlacement = (
top: number,
left: number,
placement: DropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect
triggerRect: ElementScrollRect,
contentRect: ElementScrollRect
) => {
// FIXME
const position = getPosition(placement);
if (["top", "bottom"].includes(position)) {
//
if (contentRect.bottom > windowHeight.value) {
top = -contentRect.height - props.contentOffset;
top = triggerRect.scrollTop - contentRect.height - props.contentOffset;
}
//
if (contentRect.top < 0) {
top = triggerRect.height + props.contentOffset;
top = triggerRect.scrollBottom + props.contentOffset;
}
//
if (contentRect.left < 0) {
@ -258,11 +295,11 @@ const getFitPlacement = (
}
//
if (contentRect.left < 0) {
left = triggerRect.width + props.contentOffset;
left = triggerRect.scrollRight + props.contentOffset;
}
//
if (contentRect.right > windowWidth.value) {
left = -(contentRect.width + props.contentOffset);
left = triggerRect.scrollLeft - contentRect.width - props.contentOffset;
}
}
@ -274,8 +311,8 @@ const getFitPlacement = (
const getContentOffset = (
placement: DropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect,
triggerRect: ElementScrollRect,
contentRect: ElementScrollRect,
isAlignPoint?: boolean
) => {
if (isAlignPoint) {
@ -287,63 +324,71 @@ const getContentOffset = (
switch (placement) {
case "top":
return {
top: -contentRect.height - props.contentOffset,
left: -(contentRect.width - triggerRect.width) / 2,
top: triggerRect.scrollTop - contentRect.height - props.contentOffset,
left:
triggerRect.scrollLeft +
Math.round((triggerRect.width - contentRect.width) / 2),
};
case "top-left":
return {
top: -contentRect.height - props.contentOffset,
left: 0,
top: triggerRect.scrollTop - contentRect.height - props.contentOffset,
left: triggerRect.scrollLeft,
};
case "top-right":
return {
top: -contentRect.height - props.contentOffset,
left: -(contentRect.width - triggerRect.width),
top: triggerRect.scrollTop - contentRect.height - props.contentOffset,
left: triggerRect.scrollRight - contentRect.width,
};
case "bottom":
return {
top: triggerRect.height + props.contentOffset,
left: -(contentRect.width - triggerRect.width) / 2,
top: triggerRect.scrollBottom + props.contentOffset,
left:
triggerRect.scrollLeft +
Math.round((triggerRect.width - contentRect.width) / 2),
};
case "bottom-left":
return {
top: triggerRect.height + props.contentOffset,
left: 0,
top: triggerRect.scrollBottom + props.contentOffset,
left: triggerRect.scrollLeft,
};
case "bottom-right":
return {
top: triggerRect.height + props.contentOffset,
left: -(contentRect.width - triggerRect.width),
top: triggerRect.scrollBottom + props.contentOffset,
left: triggerRect.scrollRight - contentRect.width,
};
case "right":
return {
top: -(contentRect.height - triggerRect.height) / 2,
left: triggerRect.width + props.contentOffset,
top:
triggerRect.scrollTop +
Math.round((triggerRect.height - contentRect.height) / 2),
left: triggerRect.scrollRight + props.contentOffset,
};
case "right-top":
return {
top: 0,
left: triggerRect.width + props.contentOffset,
top: triggerRect.scrollTop,
left: triggerRect.scrollRight + props.contentOffset,
};
case "right-bottom":
return {
top: -(contentRect.height - triggerRect.height),
left: triggerRect.width + props.contentOffset,
top: triggerRect.scrollBottom - contentRect.height,
left: triggerRect.scrollRight + props.contentOffset,
};
case "left":
return {
top: -(contentRect.height - triggerRect.height) / 2,
left: -(contentRect.width + props.contentOffset),
top:
triggerRect.scrollTop +
Math.round((triggerRect.height - contentRect.height) / 2),
left: triggerRect.scrollLeft - contentRect.width - props.contentOffset,
};
case "left-top":
return {
top: 0,
left: -(contentRect.width + props.contentOffset),
top: triggerRect.scrollTop,
left: triggerRect.scrollLeft - contentRect.width - props.contentOffset,
};
case "left-bottom":
return {
top: -(contentRect.height - triggerRect.height),
left: -(contentRect.width + props.contentOffset),
top: triggerRect.scrollBottom - contentRect.height,
left: triggerRect.scrollLeft - contentRect.width - props.contentOffset,
};
default:
return {
@ -376,7 +421,7 @@ const handleScroll = useThrottleFn(() => {
if (openState.value) {
updateContentStyle();
}
}, 250);
}, 10);
const handleClick = () => {
if (props.disabled || (openState.value && !props.clickToClose)) {
@ -387,7 +432,7 @@ const handleClick = () => {
}
};
const handleContextMenuClick = (e: Event) => {
const handleContextMenuClick = (e: MouseEvent) => {
if (props.disabled || (openState.value && !props.clickToClose)) {
return;
}
@ -400,20 +445,30 @@ const handleContextMenuClick = (e: Event) => {
}
};
const handleMouseEnter = () => {
const handleMouseEnter = (e: MouseEvent) => {
if (props.disabled || !triggerMethods.value.includes("hover")) {
return;
}
open(props.mouseEnterDelay);
};
const handleMouseLeave = () => {
const handleMouseEnterWithContext = (e: MouseEvent) => {
dropdownCtx?.onMouseenter(e);
handleMouseEnter(e);
};
const handleMouseLeave = (e: MouseEvent) => {
if (props.disabled || !triggerMethods.value.includes("hover")) {
return;
}
hide(props.mouseLeaveDelay);
};
const handleMouseLeaveWithContext = (e: MouseEvent) => {
dropdownCtx?.onMouseleave(e);
handleMouseLeave(e);
};
const handleFocusin = () => {
if (props.disabled || !triggerMethods.value.includes("focus")) {
return;
@ -431,6 +486,27 @@ const handleFocusout = () => {
hide();
};
const addChildRef = (ref: any) => {
childrenRefs.add(ref);
dropdownCtx?.addChildRef(ref);
};
const removeChildRef = (ref: any) => {
childrenRefs.delete(ref);
dropdownCtx?.removeChildRef(ref);
};
provide(
dropdownInjectionKey,
reactive({
onMouseenter: handleMouseEnterWithContext,
onMouseleave: handleMouseLeaveWithContext,
addChildRef,
removeChildRef,
})
);
dropdownCtx?.addChildRef(contentRef);
const { stop: removeContentResizeObserver } = useResizeObserver(
contentRef,
() => {
@ -449,14 +525,22 @@ const { stop: removeTriggerResizeObserver } = useResizeObserver(
}
);
onClickOutside(dropdownRef, () => {
if (props.clickOutsideToClose) {
hide();
onClickOutside(dropdownRef, (e) => {
if (
!props.clickOutsideToClose ||
dropdownRef.value?.contains(e.target as HTMLElement) ||
contentRef.value?.contains(e.target as HTMLElement)
)
return;
for (const item of childrenRefs) {
if (item.value?.contains(e.target as HTMLElement)) {
return;
}
}
hide();
});
let scrollElements: HTMLElement[] | undefined;
onMounted(() => {
if (props.updateAtScroll) {
scrollElements = getScrollElements(dropdownRef.value);
@ -467,6 +551,7 @@ onMounted(() => {
});
onBeforeUnmount(() => {
dropdownCtx?.removeChildRef(contentRef);
if (scrollElements) {
for (const item of scrollElements) {
item.removeEventListener("scroll", handleScroll);
@ -509,8 +594,8 @@ defineExpose({ open, hide, toggle });
<div
ref="dropdownRef"
class="layui-dropdown"
@mouseenter="handleMouseEnter()"
@mouseleave="handleMouseLeave()"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@focusin="handleFocusin()"
@focusout="handleFocusout()"
:class="{ 'layui-dropdown-up': openState }"
@ -518,12 +603,17 @@ defineExpose({ open, hide, toggle });
<div @click="handleClick()" @contextmenu="handleContextMenuClick">
<slot></slot>
</div>
<dl
ref="contentRef"
class="layui-anim layui-anim-upbit"
:style="contentStyle"
>
<slot name="content"></slot>
</dl>
<Teleport :to="containerRef" :disabled="!renderToBody">
<dl
v-show="openState"
ref="contentRef"
class="layui-dropdown-content layui-anim layui-anim-upbit"
:style="contentStyle"
@mouseenter="handleMouseEnterWithContext"
@mouseleave="handleMouseLeaveWithContext"
>
<slot name="content"></slot>
</dl>
</Teleport>
</div>
</template>

View File

@ -12,3 +12,25 @@ export type DropdownPlacement =
| "left"
| "left-top"
| "left-bottom";
export interface ElementScrollRect {
top: number;
bottom: number;
left: number;
right: number;
scrollTop: number;
scrollBottom: number;
scrollLeft: number;
scrollRight: number;
width: number;
height: number;
}
export interface DropdownContext {
onMouseenter: Function;
onMouseleave: Function;
addChildRef: Function;
removeChildRef: Function;
}
export const dropdownInjectionKey = Symbol("dropdownInjectKey");