♻️(component): [dropdown]移除包装元素

This commit is contained in:
sight 2022-08-28 23:09:18 +08:00
parent 8d2acb70e1
commit f901c8b07f
6 changed files with 226 additions and 90 deletions

View File

@ -21,6 +21,7 @@
}
.layui-cascader {
display: inline-block;
&[size="lg"] {
.set-size(@lg,@lg-width);
}

View File

@ -1,62 +1,64 @@
<template>
<lay-dropdown
class="layui-cascader"
:class="{ 'layui-cascader-opend': openState }"
ref="dropdownRef"
:autoFitMinWidth="false"
:updateAtScroll="true"
:disabled="dropDownDisabled"
<div
:size="size"
@open="openState = true"
@hide="openState = false"
:class="['layui-cascader', { 'layui-cascader-opend': openState }]"
>
<lay-input
v-model="displayValue"
readonly
suffix-icon="layui-icon-triangle-d"
:placeholder="placeholder"
v-if="!slots.default"
:allow-clear="allowClear"
@clear="onClear"
:size="size"
></lay-input>
<slot v-else></slot>
<lay-dropdown
ref="dropdownRef"
:autoFitMinWidth="false"
:updateAtScroll="true"
:disabled="dropDownDisabled"
@show="openState = true"
@hide="openState = false"
>
<lay-input
v-model="displayValue"
readonly
suffix-icon="layui-icon-triangle-d"
:placeholder="placeholder"
v-if="!slots.default"
:allow-clear="allowClear"
@clear="onClear"
:size="size"
></lay-input>
<slot v-else></slot>
<template #content>
<div class="layui-cascader-panel">
<template v-for="(itemCol, index) in treeData">
<lay-scroll
height="180px"
class="layui-cascader-menu"
:key="'cascader-menu' + index"
v-if="itemCol.data.length"
>
<div
class="layui-cascader-menu-item"
v-for="(item, i) in itemCol.data"
:key="index + i"
@click="selectBar(item, i, index)"
:class="[
{
'layui-cascader-selected': itemCol.selectIndex === i,
},
]"
<template #content>
<div class="layui-cascader-panel">
<template v-for="(itemCol, index) in treeData">
<lay-scroll
height="180px"
class="layui-cascader-menu"
:key="'cascader-menu' + index"
v-if="itemCol.data.length"
>
<slot
:name="item.slot"
v-if="item.slot && slots[item.slot]"
></slot>
<template v-else>{{ item.label }}</template>
<i
class="layui-icon layui-icon-right"
v-if="item.children && item.children.length"
></i>
</div>
</lay-scroll>
</template>
</div>
</template>
</lay-dropdown>
<div
class="layui-cascader-menu-item"
v-for="(item, i) in itemCol.data"
:key="index + i"
@click="selectBar(item, i, index)"
:class="[
{
'layui-cascader-selected': itemCol.selectIndex === i,
},
]"
>
<slot
:name="item.slot"
v-if="item.slot && slots[item.slot]"
></slot>
<template v-else>{{ item.label }}</template>
<i
class="layui-icon layui-icon-right"
v-if="item.children && item.children.length"
></i>
</div>
</lay-scroll>
</template>
</div>
</template>
</lay-dropdown>
</div>
</template>
<script lang="ts">

View File

@ -1,13 +1,13 @@
<template>
<div>
<div
:class="['layui-date-picker', { 'layui-date-range-picker': range }]"
:size="size"
>
<lay-dropdown
ref="dropdownRef"
:disabled="disabled"
:autoFitMinWidth="false"
updateAtScroll
class="layui-date-picker"
:class="{ 'layui-date-range-picker': range }"
:size="size"
>
<lay-input
:name="name"

View File

@ -1,12 +1,25 @@
<script lang="ts">
export default {
name: "LayDropdown",
inheritAttrs: false,
};
</script>
<script setup lang="ts">
import "./index.less";
import { ComputedRef, CSSProperties, inject, reactive, Ref, toRefs } from "vue";
import {
ComputedRef,
CSSProperties,
h,
inject,
reactive,
Ref,
toRefs,
useSlots,
Fragment,
cloneVNode,
useAttrs,
} from "vue";
import {
computed,
nextTick,
@ -30,6 +43,8 @@ import {
DropdownContext,
} from "./interface";
import TeleportWrapper from "../_components/teleportWrapper.vue";
import { useFirstElement } from "./useFirstElement";
import RenderFunction from "../_components/renderFunction";
export type DropdownTrigger = "click" | "hover" | "focus" | "contextMenu";
@ -75,13 +90,15 @@ const props = withDefaults(defineProps<LayDropdownProps>(), {
alignPoint: false,
popupContainer: "body",
});
const slots = useSlots();
const attrs = useAttrs();
const childrenRefs = new Set<Ref<HTMLElement>>();
const dropdownCtx = inject<DropdownContext | undefined>(
dropdownInjectionKey,
undefined
);
const dropdownRef = shallowRef<HTMLElement | undefined>();
const { children, firstElement: dropdownRef } = useFirstElement();
//const dropdownRef = shallowRef<HTMLElement | undefined>();
const contentRef = shallowRef<HTMLElement | undefined>();
const contentStyle = ref<CSSProperties>({});
const { width: windowWidth, height: windowHeight } = useWindowSize();
@ -91,6 +108,7 @@ const mousePosition = reactive({
});
const { x: mouseLeft, y: mouseTop } = toRefs(mousePosition);
const openState = ref(false);
let scrollElements: HTMLElement[] | undefined;
const containerRef = computed(() =>
props.popupContainer
@ -564,7 +582,28 @@ onClickOutside(
}
);
let scrollElements: HTMLElement[] | undefined;
const onlyChildRenderFunc = () => {
const slotContent = slots.default ? slots.default() : [];
const transformedSlotContent = slotContent.map((vnode) =>
cloneVNode(
vnode,
{
onClick: handleClick,
onContextmenu: handleContextMenuClick,
onMouseenter: handleMouseEnter,
onMouseleave: handleMouseLeave,
onFocusin: handleFocusin,
onFocusout: handleFocusout,
...attrs,
},
true
)
);
children.value = transformedSlotContent;
return h(Fragment, children.value);
};
onMounted(() => {
if (props.updateAtScroll) {
scrollElements = getScrollElements(dropdownRef.value);
@ -611,29 +650,20 @@ defineExpose({ show, hide, toggle });
</script>
<template>
<div
ref="dropdownRef"
class="layui-dropdown"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@focusin="handleFocusin()"
@focusout="handleFocusout()"
:class="{ 'layui-dropdown-up': openState }"
>
<div @click="handleClick" @contextmenu="handleContextMenuClick">
<slot></slot>
<RenderFunction
:renderFunc="onlyChildRenderFunc"
v-bind="$attrs"
></RenderFunction>
<TeleportWrapper :to="popupContainer">
<div
v-if="openState"
ref="contentRef"
class="layui-dropdown-content layui-anim layui-anim-upbit"
:style="contentStyle"
@mouseenter="handleMouseEnterWithContext"
@mouseleave="handleMouseLeaveWithContext"
>
<slot name="content"></slot>
</div>
<TeleportWrapper :to="popupContainer">
<dl
v-if="openState"
ref="contentRef"
class="layui-dropdown-content layui-anim layui-anim-upbit"
:style="contentStyle"
@mouseenter="handleMouseEnterWithContext"
@mouseleave="handleMouseLeaveWithContext"
>
<slot name="content"></slot>
</dl>
</TeleportWrapper>
</div>
</TeleportWrapper>
</template>

View File

@ -0,0 +1,103 @@
import { Component, onMounted, onUpdated, ref, VNode, VNodeTypes } from "vue";
export interface SlotChildren {
value?: VNode[];
}
// Quoted from arco-vue
// https://github.com/arco-design/arco-design-vue/blob/main/packages/web-vue/components/_utils/vue-utils.ts
export enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
TEXT_CHILDREN = 1 << 3,
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
}
export const isElement = (vn: VNode) => {
return Boolean(vn && vn.shapeFlag & ShapeFlags.ELEMENT);
};
export const isComponent = (
vn: VNode,
type?: VNodeTypes
): type is Component => {
return Boolean(vn && vn.shapeFlag & ShapeFlags.COMPONENT);
};
export const isArrayChildren = (
vn: VNode,
children: VNode["children"]
): children is VNode[] => {
return Boolean(vn && vn.shapeFlag & ShapeFlags.ARRAY_CHILDREN);
};
export const getChildrenArray = (vn: VNode): VNode[] | undefined => {
if (isArrayChildren(vn, vn.children)) {
return vn.children;
}
if (Array.isArray(vn)) {
return vn;
}
return undefined;
};
export const getFirstElementFromVNode = (
vn: VNode
): HTMLElement | undefined => {
if (isElement(vn)) {
return vn.el as HTMLElement;
}
if (isComponent(vn)) {
if ((vn.el as Node)?.nodeType === 1) {
return vn.el as HTMLElement;
}
if (vn.component?.subTree) {
const ele = getFirstElementFromVNode(vn.component.subTree);
if (ele) return ele;
}
} else {
const children = getChildrenArray(vn);
return getFirstElementFromChildren(children);
}
return undefined;
};
export const getFirstElementFromChildren = (
children: VNode[] | undefined
): HTMLElement | undefined => {
if (children && children.length > 0) {
for (const child of children) {
const element = getFirstElementFromVNode(child);
if (element) return element;
}
}
return undefined;
};
export const useFirstElement = () => {
const children: SlotChildren = {};
const firstElement = ref<HTMLElement>();
const getFirstElement = () => {
const element = getFirstElementFromChildren(children.value);
if (element !== firstElement.value) {
firstElement.value = element;
}
};
onMounted(() => getFirstElement());
onUpdated(() => getFirstElement());
return {
children,
firstElement,
};
};

View File

@ -478,7 +478,7 @@ export default {
<template>
<lay-space>
<lay-dropdown placement="bottom-left" autoFitWidth updateAtScroll>
<lay-dropdown placement="bottom-start" autoFitWidth updateAtScroll>
<lay-button type="primary">autoFitWidth</lay-button>
<template #content>
<lay-dropdown-menu>
@ -488,7 +488,7 @@ export default {
</lay-dropdown-menu>
</template>
</lay-dropdown>
<lay-dropdown placement="bottom-left" :autoFitMinWidth="false" updateAtScroll>
<lay-dropdown placement="bottom-start" :autoFitMinWidth="false" updateAtScroll>
<lay-button type="primary">关闭 autoFitMinWidth</lay-button>
<template #content>
<lay-dropdown-menu>
@ -498,7 +498,7 @@ export default {
</lay-dropdown-menu>
</template>
</lay-dropdown>
<lay-dropdown placement="bottom-left" updateAtScroll>
<lay-dropdown placement="bottom-start" updateAtScroll>
<lay-button type="primary">updateAtScroll</lay-button>
<template #content>
<lay-dropdown-menu>
@ -508,7 +508,7 @@ export default {
</lay-dropdown-menu>
</template>
</lay-dropdown>
<lay-dropdown placement="bottom-left" updateAtScroll :contentOffset="8">
<lay-dropdown placement="bottom-start" updateAtScroll :contentOffset="8">
<lay-button type="primary">contentOffset: 8px</lay-button>
<template #content>
<lay-dropdown-menu>