♻️(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 { .layui-cascader {
display: inline-block;
&[size="lg"] { &[size="lg"] {
.set-size(@lg,@lg-width); .set-size(@lg,@lg-width);
} }

View File

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

View File

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

View File

@ -1,12 +1,25 @@
<script lang="ts"> <script lang="ts">
export default { export default {
name: "LayDropdown", name: "LayDropdown",
inheritAttrs: false,
}; };
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import "./index.less"; 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 { import {
computed, computed,
nextTick, nextTick,
@ -30,6 +43,8 @@ import {
DropdownContext, DropdownContext,
} from "./interface"; } from "./interface";
import TeleportWrapper from "../_components/teleportWrapper.vue"; import TeleportWrapper from "../_components/teleportWrapper.vue";
import { useFirstElement } from "./useFirstElement";
import RenderFunction from "../_components/renderFunction";
export type DropdownTrigger = "click" | "hover" | "focus" | "contextMenu"; export type DropdownTrigger = "click" | "hover" | "focus" | "contextMenu";
@ -75,13 +90,15 @@ const props = withDefaults(defineProps<LayDropdownProps>(), {
alignPoint: false, alignPoint: false,
popupContainer: "body", popupContainer: "body",
}); });
const slots = useSlots();
const attrs = useAttrs();
const childrenRefs = new Set<Ref<HTMLElement>>(); const childrenRefs = new Set<Ref<HTMLElement>>();
const dropdownCtx = inject<DropdownContext | undefined>( const dropdownCtx = inject<DropdownContext | undefined>(
dropdownInjectionKey, dropdownInjectionKey,
undefined undefined
); );
const dropdownRef = shallowRef<HTMLElement | undefined>(); const { children, firstElement: dropdownRef } = useFirstElement();
//const dropdownRef = shallowRef<HTMLElement | undefined>();
const contentRef = shallowRef<HTMLElement | undefined>(); const contentRef = shallowRef<HTMLElement | undefined>();
const contentStyle = ref<CSSProperties>({}); const contentStyle = ref<CSSProperties>({});
const { width: windowWidth, height: windowHeight } = useWindowSize(); const { width: windowWidth, height: windowHeight } = useWindowSize();
@ -91,6 +108,7 @@ const mousePosition = reactive({
}); });
const { x: mouseLeft, y: mouseTop } = toRefs(mousePosition); const { x: mouseLeft, y: mouseTop } = toRefs(mousePosition);
const openState = ref(false); const openState = ref(false);
let scrollElements: HTMLElement[] | undefined;
const containerRef = computed(() => const containerRef = computed(() =>
props.popupContainer 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(() => { onMounted(() => {
if (props.updateAtScroll) { if (props.updateAtScroll) {
scrollElements = getScrollElements(dropdownRef.value); scrollElements = getScrollElements(dropdownRef.value);
@ -611,29 +650,20 @@ defineExpose({ show, hide, toggle });
</script> </script>
<template> <template>
<div <RenderFunction
ref="dropdownRef" :renderFunc="onlyChildRenderFunc"
class="layui-dropdown" v-bind="$attrs"
@mouseenter="handleMouseEnter" ></RenderFunction>
@mouseleave="handleMouseLeave" <TeleportWrapper :to="popupContainer">
@focusin="handleFocusin()" <div
@focusout="handleFocusout()" v-if="openState"
:class="{ 'layui-dropdown-up': openState }" ref="contentRef"
> class="layui-dropdown-content layui-anim layui-anim-upbit"
<div @click="handleClick" @contextmenu="handleContextMenuClick"> :style="contentStyle"
<slot></slot> @mouseenter="handleMouseEnterWithContext"
@mouseleave="handleMouseLeaveWithContext"
>
<slot name="content"></slot>
</div> </div>
<TeleportWrapper :to="popupContainer"> </TeleportWrapper>
<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>
</template> </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> <template>
<lay-space> <lay-space>
<lay-dropdown placement="bottom-left" autoFitWidth updateAtScroll> <lay-dropdown placement="bottom-start" autoFitWidth updateAtScroll>
<lay-button type="primary">autoFitWidth</lay-button> <lay-button type="primary">autoFitWidth</lay-button>
<template #content> <template #content>
<lay-dropdown-menu> <lay-dropdown-menu>
@ -488,7 +488,7 @@ export default {
</lay-dropdown-menu> </lay-dropdown-menu>
</template> </template>
</lay-dropdown> </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> <lay-button type="primary">关闭 autoFitMinWidth</lay-button>
<template #content> <template #content>
<lay-dropdown-menu> <lay-dropdown-menu>
@ -498,7 +498,7 @@ export default {
</lay-dropdown-menu> </lay-dropdown-menu>
</template> </template>
</lay-dropdown> </lay-dropdown>
<lay-dropdown placement="bottom-left" updateAtScroll> <lay-dropdown placement="bottom-start" updateAtScroll>
<lay-button type="primary">updateAtScroll</lay-button> <lay-button type="primary">updateAtScroll</lay-button>
<template #content> <template #content>
<lay-dropdown-menu> <lay-dropdown-menu>
@ -508,7 +508,7 @@ export default {
</lay-dropdown-menu> </lay-dropdown-menu>
</template> </template>
</lay-dropdown> </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> <lay-button type="primary">contentOffset: 8px</lay-button>
<template #content> <template #content>
<lay-dropdown-menu> <lay-dropdown-menu>