✨(component): [dropdown]下拉面板位置自适应
This commit is contained in:
parent
aed6091707
commit
753f0f545b
@ -42,6 +42,6 @@
|
|||||||
background-color: var(--global-checked-color);
|
background-color: var(--global-checked-color);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.layui-cascader.layui-dropdown-up > dl {
|
// .layui-cascader.layui-dropdown-up > dl {
|
||||||
min-width: unset;
|
// min-width: unset;
|
||||||
}
|
// }
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<lay-dropdown class="layui-cascader" ref="dropdownRef">
|
<lay-dropdown class="layui-cascader" ref="dropdownRef" :autoFitMinWidth="false">
|
||||||
<lay-input
|
<lay-input
|
||||||
v-model="displayValue"
|
v-model="displayValue"
|
||||||
readonly
|
readonly
|
||||||
|
@ -6,9 +6,7 @@
|
|||||||
.layui-dropdown dl {
|
.layui-dropdown dl {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: 2px;
|
|
||||||
z-index: 899;
|
z-index: 899;
|
||||||
min-width: 100%;
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid #e4e7ed;
|
border: 1px solid #e4e7ed;
|
||||||
|
@ -6,37 +6,50 @@ export default {
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
import { provide, ref } from "vue";
|
import { CSSProperties, nextTick, provide, ref, shallowRef, watch, watchEffect } from "vue";
|
||||||
import { onClickOutside } from "@vueuse/core";
|
import { onClickOutside, useWindowSize } from "@vueuse/core";
|
||||||
import { DropdownTrigger } from "./interface";
|
import { DropdownTrigger, dropdownPlacement } from "./interface";
|
||||||
|
|
||||||
export interface LayDropdownProps {
|
export interface LayDropdownProps {
|
||||||
trigger?: DropdownTrigger;
|
trigger?: DropdownTrigger;
|
||||||
|
placement?: dropdownPlacement;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
autoFitPlacement?: boolean;
|
||||||
|
autoFitWidth?: boolean;
|
||||||
|
autoFitMinWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<LayDropdownProps>(), {
|
const props = withDefaults(defineProps<LayDropdownProps>(), {
|
||||||
trigger: "click",
|
trigger: "click",
|
||||||
disabled: false,
|
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 openState = ref(false);
|
||||||
const dropdownRef = ref<null | HTMLElement>();
|
const contentSpace = 2;
|
||||||
|
|
||||||
const emit = defineEmits(["open", "hide"]);
|
const emit = defineEmits(["open", "hide"]);
|
||||||
|
|
||||||
onClickOutside(dropdownRef, () => {
|
onClickOutside(dropdownRef, () => {
|
||||||
openState.value = false;
|
changeVisible(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
const open = (): void => {
|
const open = (): void => {
|
||||||
if (props.disabled == false) {
|
if (props.disabled == false) {
|
||||||
openState.value = true;
|
changeVisible(true);
|
||||||
emit("open");
|
emit("open");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hide = (): void => {
|
const hide = (): void => {
|
||||||
openState.value = false;
|
changeVisible(false);
|
||||||
emit("hide");
|
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);
|
provide("openState", openState);
|
||||||
|
|
||||||
defineExpose({ open, hide, toggle });
|
defineExpose({ open, hide, toggle });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="dropdownRef" class="layui-dropdown" @mouseenter="trigger == 'hover' && open()"
|
||||||
ref="dropdownRef"
|
@mouseleave="trigger == 'hover' && hide()" :class="{ 'layui-dropdown-up': openState }">
|
||||||
class="layui-dropdown"
|
<div @click="trigger == 'click' && toggle()" @contextmenu.prevent="trigger == 'contextMenu' && toggle()">
|
||||||
@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>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<dl class="layui-anim layui-anim-upbit">
|
<dl ref="contentRef" class="layui-anim layui-anim-upbit" :style="contentStyle">
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export type DropdownTrigger = "click" | "hover" | "contextMenu";
|
export type DropdownTrigger = "click" | "hover" | "contextMenu";
|
||||||
|
export type dropdownPlacement = 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right';
|
||||||
|
@ -164,12 +164,69 @@ export default {
|
|||||||
::: demo
|
::: demo
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<lay-dropdown>
|
<lay-dropdown placement="top-left">
|
||||||
<lay-button type="primary">下拉菜单</lay-button>
|
<lay-button type="primary">topLeft</lay-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div style="width:300px;height:200px;"></div>
|
<div style="width:300px;height:200px;"></div>
|
||||||
</template>
|
</template>
|
||||||
</lay-dropdown>
|
</lay-dropdown>
|
||||||
|
|
||||||
|
<lay-dropdown placement="top">
|
||||||
|
<lay-button type="primary">top</lay-button>
|
||||||
|
<template #content>
|
||||||
|
<div style="width:300px;height:200px;"></div>
|
||||||
|
</template>
|
||||||
|
</lay-dropdown>
|
||||||
|
|
||||||
|
<lay-dropdown placement="top-right">
|
||||||
|
<lay-button type="primary">topRight</lay-button>
|
||||||
|
<template #content>
|
||||||
|
<div style="width:300px;height:200px;"></div>
|
||||||
|
</template>
|
||||||
|
</lay-dropdown>
|
||||||
|
|
||||||
|
<lay-dropdown placement="bottom-left">
|
||||||
|
<lay-button type="primary">bottomLeft</lay-button>
|
||||||
|
<template #content>
|
||||||
|
<div style="width:300px;height:200px;"></div>
|
||||||
|
</template>
|
||||||
|
</lay-dropdown>
|
||||||
|
|
||||||
|
<lay-dropdown placement="bottom">
|
||||||
|
<lay-button type="primary">bottom</lay-button>
|
||||||
|
<template #content>
|
||||||
|
<div style="width:300px;height:200px;"></div>
|
||||||
|
</template>
|
||||||
|
</lay-dropdown>
|
||||||
|
|
||||||
|
<lay-dropdown placement="bottom-right">
|
||||||
|
<lay-button type="primary">bottomRight</lay-button>
|
||||||
|
<template #content>
|
||||||
|
<div style="width:300px;height:200px;"></div>
|
||||||
|
</template>
|
||||||
|
</lay-dropdown>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -195,6 +252,11 @@ export default {
|
|||||||
| ------- | -------- | --------------- |
|
| ------- | -------- | --------------- |
|
||||||
| trigger | 触发方式 | `click` `hover` `contextMenu` |
|
| trigger | 触发方式 | `click` `hover` `contextMenu` |
|
||||||
| disabled | 是否禁用触发 | `true` `false` |
|
| 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` |
|
||||||
|
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user