(component): [dropdown]下拉面板位置自适应

This commit is contained in:
sight 2022-06-22 13:08:48 +08:00
parent aed6091707
commit 753f0f545b
6 changed files with 247 additions and 27 deletions

View File

@ -42,6 +42,6 @@
background-color: var(--global-checked-color);
color: white;
}
.layui-cascader.layui-dropdown-up > dl {
min-width: unset;
}
// .layui-cascader.layui-dropdown-up > dl {
// min-width: unset;
// }

View File

@ -1,5 +1,5 @@
<template>
<lay-dropdown class="layui-cascader" ref="dropdownRef">
<lay-dropdown class="layui-cascader" ref="dropdownRef" :autoFitMinWidth="false">
<lay-input
v-model="displayValue"
readonly

View File

@ -6,9 +6,7 @@
.layui-dropdown dl {
display: none;
position: absolute;
margin-top: 2px;
z-index: 899;
min-width: 100%;
background-color: #fff;
box-sizing: border-box;
border: 1px solid #e4e7ed;

View File

@ -6,37 +6,50 @@ export default {
<script setup lang="ts">
import "./index.less";
import { provide, ref } from "vue";
import { onClickOutside } from "@vueuse/core";
import { DropdownTrigger } from "./interface";
import { CSSProperties, nextTick, provide, ref, shallowRef, watch, watchEffect } from "vue";
import { onClickOutside, useWindowSize } from "@vueuse/core";
import { DropdownTrigger, dropdownPlacement } from "./interface";
export interface LayDropdownProps {
trigger?: DropdownTrigger;
placement?: dropdownPlacement;
disabled?: boolean;
autoFitPlacement?: boolean;
autoFitWidth?: boolean;
autoFitMinWidth?: boolean;
}
const props = withDefaults(defineProps<LayDropdownProps>(), {
trigger: "click",
disabled: false,
placement: 'bottom-left',
autoFitPlacement: true,
autoFitMinWidth: true,
autoFitWidth: false,
});
const dropdownRef = shallowRef<null | HTMLElement>();
const contentRef = shallowRef<null | HTMLElement>();
const contentStyle = ref<CSSProperties>({});
const { width: windowWidth, height: windowHeight } = useWindowSize();
const openState = ref(false);
const dropdownRef = ref<null | HTMLElement>();
const contentSpace = 2;
const emit = defineEmits(["open", "hide"]);
onClickOutside(dropdownRef, () => {
openState.value = false;
changeVisible(false)
});
const open = (): void => {
if (props.disabled == false) {
openState.value = true;
changeVisible(true);
emit("open");
}
};
const hide = (): void => {
openState.value = false;
changeVisible(false);
emit("hide");
};
@ -49,26 +62,172 @@ const toggle = (): void => {
}
};
const changeVisible = (visible: boolean) => {
if (visible === openState.value) {
return;
}
openState.value = visible;
nextTick(() => {
updateContentStyle();
});
}
const updateContentStyle = () => {
if (!dropdownRef.value || !contentRef.value){
return
}
const triggerRect = dropdownRef.value.getBoundingClientRect()
const contentRect = contentRef.value.getBoundingClientRect()
const { style } = getContentStyle(
props.placement,
triggerRect,
contentRect,
{autoFitPlacement: props.autoFitPlacement}
);
if (props.autoFitMinWidth) {
style.minWidth = `${triggerRect.width}px`;
}
if (props.autoFitWidth) {
style.width = `${triggerRect.width}px`;
}
contentStyle.value = style
}
const getContentStyle = (
placement: dropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect,
{
autoFitPlacement = false,
customStyle = {}
}: {
autoFitPlacement?: boolean;
customStyle?: CSSProperties;
} = {}
) => {
let { top, left } = getContentOffset(placement, triggerRect, contentRect)
if(autoFitPlacement){
const { top: fitTop, left: fitLeft } = getFitPlacement(top, left, placement, triggerRect, contentRect)
top = fitTop;
left = fitLeft;
}
const style = {
top: `${top}px`,
left: `${left}px`,
...customStyle,
}
return {
style
}
}
const getFitPlacement = (
top: number,
left: number,
placement: dropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect
) => {
//
if (triggerRect.bottom + contentRect.height > windowHeight.value) {
top = -contentRect.height - contentSpace
}
//
if (triggerRect.top - contentRect.height < 0) {
top = triggerRect.height + contentSpace
}
if(["bottom-right", "top-right"].includes(placement) ){
//
const contentRectLeft = triggerRect.left - (contentRect.width - triggerRect.width)
if (contentRectLeft < 0) {
left = left + (0 - contentRectLeft)
}
}
if(["bottom-left", "top-left"].includes(placement)){
//
const contentRectRight = triggerRect.right + (contentRect.width - triggerRect.width)
if (contentRectRight > windowWidth.value) {
left = left - (contentRectRight - windowWidth.value)
}
}
if(["bottom", "top"].includes(placement)){
const contentRectLeft = triggerRect.left - (contentRect.width - triggerRect.width) / 2
const contentRectRight = triggerRect.right + (contentRect.width - triggerRect.width) / 2
//
if (contentRectLeft < 0) {
left = left + (0 - contentRectLeft)
}
//
if (contentRectRight > windowWidth.value) {
left = left - (contentRectRight - windowWidth.value)
}
}
return {
top,
left
}
}
const getContentOffset = (
placement: dropdownPlacement,
triggerRect: DOMRect,
contentRect: DOMRect
) => {
switch (placement) {
case "top":
return {
top: -contentRect.height - contentSpace,
left: -(contentRect.width - triggerRect.width) / 2
}
case "top-left":
return {
top: -contentRect.height - contentSpace,
left: 0
}
case "top-right":
return {
top: -contentRect.height - contentSpace,
left: -(contentRect.width - triggerRect.width)
}
case "bottom":
return {
top: triggerRect.height + contentSpace,
left: -(contentRect.width - triggerRect.width) / 2
}
case "bottom-left":
return {
top: triggerRect.height + contentSpace,
left: 0
}
case "bottom-right":
return {
top: triggerRect.height + contentSpace,
left: -(contentRect.width - triggerRect.width)
}
default:
return {
left: 0,
top: 0,
};
}
}
provide("openState", openState);
defineExpose({ open, hide, toggle });
</script>
<template>
<div
ref="dropdownRef"
class="layui-dropdown"
@mouseenter="trigger == 'hover' && open()"
@mouseleave="trigger == 'hover' && hide()"
:class="{ 'layui-dropdown-up': openState }"
>
<div
@click="trigger == 'click' && toggle()"
@contextmenu.prevent="trigger == 'contextMenu' && toggle()"
>
<div ref="dropdownRef" class="layui-dropdown" @mouseenter="trigger == 'hover' && open()"
@mouseleave="trigger == 'hover' && hide()" :class="{ 'layui-dropdown-up': openState }">
<div @click="trigger == 'click' && toggle()" @contextmenu.prevent="trigger == 'contextMenu' && toggle()">
<slot></slot>
</div>
<dl class="layui-anim layui-anim-upbit">
<dl ref="contentRef" class="layui-anim layui-anim-upbit" :style="contentStyle">
<slot name="content"></slot>
</dl>
</div>

View File

@ -1 +1,2 @@
export type DropdownTrigger = "click" | "hover" | "contextMenu";
export type dropdownPlacement = 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right';

View File

@ -164,12 +164,69 @@ export default {
::: demo
<template>
<lay-dropdown>
<lay-button type="primary">下拉菜单</lay-button>
<lay-dropdown placement="top-left">
<lay-button type="primary">topLeft</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="top">
<lay-button type="primary">top</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="top-right">
<lay-button type="primary">topRight</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom-left">
<lay-button type="primary">bottomLeft</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom">
<lay-button type="primary">bottom</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom-right">
<lay-button type="primary">bottomRight</lay-button>
<template #content>
<div style="width:300px;height:200px;"></div>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom-left" :autoFitWidth="true">
<lay-button type="primary">开启 autoFitWidth</lay-button>
<template #content>
<lay-dropdown-menu>
<lay-dropdown-menu-item>选项一</lay-dropdown-menu-item>
<lay-dropdown-menu-item>选项二1111111111111111111111</lay-dropdown-menu-item>
<lay-dropdown-menu-item>选项三</lay-dropdown-menu-item>
</lay-dropdown-menu>
</template>
</lay-dropdown>
&nbsp;&nbsp;
<lay-dropdown placement="bottom-left" :autoFitMinWidth="false">
<lay-button type="primary">关闭 autoFitMinWidth</lay-button>
<template #content>
<lay-dropdown-menu>
<lay-dropdown-menu-item>选项一</lay-dropdown-menu-item>
<lay-dropdown-menu-item>选项二111111111</lay-dropdown-menu-item>
<lay-dropdown-menu-item>选项三</lay-dropdown-menu-item>
</lay-dropdown-menu>
</template>
</lay-dropdown>
</template>
<script>
@ -195,6 +252,11 @@ export default {
| ------- | -------- | --------------- |
| trigger | 触发方式 | `click` `hover` `contextMenu` |
| disabled | 是否禁用触发 | `true` `false` |
| placement | 下拉面板位置 |`top` `top-left` `top-right` `bottom` `bottom-left` `bottom-right`|
| autoFitPlacement| 是否自适应下拉面板位置,默认 `true` | `true` `false` |
| autoFitWidth | 是否将下拉面板宽度设置为触发器宽度, 默认 `false` | `true` `false` |
| autoFitMinWidth | 是否将下拉面板最小宽度设置为触发器宽度, 默认 `true` | `true` `false` |
:::