mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 03:08:21 +08:00
refactor(components): refactor space (#3757)
This commit is contained in:
parent
23a2ac5515
commit
3b9e63d587
@ -1,6 +1,6 @@
|
||||
import { nextTick, h } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Space from '../src/index'
|
||||
import Space from '../src/space'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
|
||||
|
@ -1,13 +1,9 @@
|
||||
import Space from './src/index'
|
||||
import { withInstall } from '@element-plus/utils/with-install'
|
||||
|
||||
import type { App } from 'vue'
|
||||
import type { SFCWithInstall } from '@element-plus/utils/types'
|
||||
import Space from './src/space'
|
||||
|
||||
const _Space = Space as SFCWithInstall<typeof Space>
|
||||
export const ElSpace = withInstall(Space)
|
||||
export default ElSpace
|
||||
|
||||
_Space.install = (app: App) => {
|
||||
app.component(_Space.name, _Space)
|
||||
}
|
||||
|
||||
export default _Space
|
||||
export const ElSpace = _Space
|
||||
export * from './src/space'
|
||||
export * from './src/use-space'
|
||||
|
@ -1,143 +0,0 @@
|
||||
import {
|
||||
defineComponent,
|
||||
renderSlot,
|
||||
createVNode,
|
||||
createTextVNode,
|
||||
isVNode,
|
||||
} from 'vue'
|
||||
import {
|
||||
PatchFlags,
|
||||
isFragment,
|
||||
isValidElementNode,
|
||||
} from '@element-plus/utils/vnode'
|
||||
import { isArray } from '@element-plus/utils/util'
|
||||
import Item from './item.vue'
|
||||
import { useSpace, defaultProps } from './useSpace'
|
||||
|
||||
import type { VNode, ExtractPropTypes, Slots } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElSpace',
|
||||
props: defaultProps,
|
||||
setup(props) {
|
||||
return useSpace(props)
|
||||
},
|
||||
|
||||
render(
|
||||
ctx: ReturnType<typeof useSpace> &
|
||||
ExtractPropTypes<typeof defaultProps> & { $slots: Slots }
|
||||
) {
|
||||
const {
|
||||
classes,
|
||||
$slots,
|
||||
containerStyle,
|
||||
itemStyle,
|
||||
spacer,
|
||||
prefixCls,
|
||||
direction,
|
||||
} = ctx
|
||||
|
||||
const children = renderSlot($slots, 'default', { key: 0 }, () => [])
|
||||
// retrieve the children out via a simple for loop
|
||||
// the edge case here is that when users uses directives like <v-for>, <v-if>
|
||||
// we need to go one layer deeper
|
||||
|
||||
if (children.children.length === 0) return null
|
||||
|
||||
// loop the children, if current children is rendered via `renderList` or `<v-for>`
|
||||
if (isArray(children.children)) {
|
||||
let extractedChildren = []
|
||||
children.children.forEach((child: VNode, loopKey) => {
|
||||
if (isFragment(child)) {
|
||||
if (isArray(child.children)) {
|
||||
child.children.forEach((nested, key) => {
|
||||
extractedChildren.push(
|
||||
createVNode(
|
||||
Item,
|
||||
{
|
||||
style: itemStyle,
|
||||
prefixCls,
|
||||
key: `nested-${key}`,
|
||||
},
|
||||
{
|
||||
default: () => [nested as VNode],
|
||||
},
|
||||
PatchFlags.PROPS | PatchFlags.STYLE,
|
||||
['style', 'prefixCls']
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
// if the current child is valid vnode, then append this current vnode
|
||||
// to item as child node.
|
||||
} else if (isValidElementNode(child)) {
|
||||
extractedChildren.push(
|
||||
createVNode(
|
||||
Item,
|
||||
{
|
||||
style: itemStyle,
|
||||
prefixCls,
|
||||
key: `LoopKey${loopKey}`,
|
||||
},
|
||||
{
|
||||
default: () => [child as VNode],
|
||||
},
|
||||
PatchFlags.PROPS | PatchFlags.STYLE,
|
||||
['style', 'prefixCls']
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (spacer) {
|
||||
// track the current rendering index, when encounters the last element
|
||||
// then no need to add a spacer after it.
|
||||
const len = extractedChildren.length - 1
|
||||
extractedChildren = extractedChildren.reduce((acc, child, idx) => {
|
||||
return idx === len
|
||||
? [...acc, child]
|
||||
: [
|
||||
...acc,
|
||||
child,
|
||||
createVNode(
|
||||
'span',
|
||||
// adding width 100% for vertical alignment,
|
||||
// when the spacer inherit the width from the
|
||||
// parent, this span's width was not set, so space
|
||||
// might disappear
|
||||
{
|
||||
style: [
|
||||
itemStyle,
|
||||
direction === 'vertical' ? 'width: 100%' : null,
|
||||
],
|
||||
key: idx,
|
||||
},
|
||||
[
|
||||
// if spacer is already a valid vnode, then append it to the current
|
||||
// span element.
|
||||
// otherwise, treat it as string.
|
||||
isVNode(spacer)
|
||||
? spacer
|
||||
: createTextVNode(spacer as string, PatchFlags.TEXT),
|
||||
],
|
||||
PatchFlags.STYLE
|
||||
),
|
||||
]
|
||||
}, [])
|
||||
}
|
||||
|
||||
// spacer container.
|
||||
return createVNode(
|
||||
'div',
|
||||
{
|
||||
class: classes,
|
||||
style: containerStyle,
|
||||
},
|
||||
extractedChildren,
|
||||
PatchFlags.STYLE | PatchFlags.CLASS
|
||||
)
|
||||
}
|
||||
|
||||
return children.children
|
||||
},
|
||||
})
|
@ -1,22 +1,28 @@
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { buildProps } from '@element-plus/utils/props'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
const spaceItem = buildProps({
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: 'el-space',
|
||||
},
|
||||
},
|
||||
} as const)
|
||||
|
||||
export default defineComponent({
|
||||
props: spaceItem,
|
||||
|
||||
setup(props) {
|
||||
const classes = computed(() => [`${props.prefixCls}__item`])
|
||||
|
||||
return {
|
||||
classes: computed(() => `${props.prefixCls}__item`),
|
||||
classes,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
208
packages/components/space/src/space.ts
Normal file
208
packages/components/space/src/space.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import {
|
||||
defineComponent,
|
||||
renderSlot,
|
||||
createVNode,
|
||||
createTextVNode,
|
||||
isVNode,
|
||||
} from 'vue'
|
||||
import { isString } from '@vue/shared'
|
||||
import {
|
||||
PatchFlags,
|
||||
isFragment,
|
||||
isValidElementNode,
|
||||
} from '@element-plus/utils/vnode'
|
||||
import { isArray, isNumber } from '@element-plus/utils/util'
|
||||
import {
|
||||
buildProps,
|
||||
componentSize,
|
||||
definePropType,
|
||||
} from '@element-plus/utils/props'
|
||||
import Item from './item.vue'
|
||||
import { useSpace } from './use-space'
|
||||
|
||||
import type { VNode, StyleValue, ExtractPropTypes, VNodeChild } from 'vue'
|
||||
import type { AlignItemsProperty } from 'csstype'
|
||||
|
||||
export const spaceProps = buildProps({
|
||||
direction: {
|
||||
type: String,
|
||||
values: ['horizontal', 'vertical'],
|
||||
default: 'horizontal',
|
||||
},
|
||||
|
||||
class: {
|
||||
type: definePropType<string | string[] | Record<string, boolean>>([
|
||||
String,
|
||||
Object,
|
||||
Array,
|
||||
]),
|
||||
default: '',
|
||||
},
|
||||
|
||||
style: {
|
||||
type: definePropType<StyleValue>([String, Array, Object]),
|
||||
default: '',
|
||||
},
|
||||
|
||||
alignment: {
|
||||
type: definePropType<AlignItemsProperty>(String),
|
||||
default: 'center',
|
||||
},
|
||||
|
||||
prefixCls: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
spacer: {
|
||||
type: definePropType<VNodeChild>([Object, String, Number, Array]),
|
||||
default: null,
|
||||
validator: (val: unknown) => isVNode(val) || isNumber(val) || isString(val),
|
||||
},
|
||||
|
||||
wrap: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
fill: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
fillRatio: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
|
||||
size: {
|
||||
type: [String, Array, Number],
|
||||
values: componentSize,
|
||||
validator: (val: unknown): val is [number, number] | number => {
|
||||
return (
|
||||
isNumber(val) ||
|
||||
(isArray(val) && val.length === 2 && val.every((i) => isNumber(i)))
|
||||
)
|
||||
},
|
||||
},
|
||||
} as const)
|
||||
export type SpaceProps = ExtractPropTypes<typeof spaceProps>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElSpace',
|
||||
|
||||
props: spaceProps,
|
||||
|
||||
setup(props, { slots }) {
|
||||
const { classes, containerStyle, itemStyle } = useSpace(props)
|
||||
|
||||
return () => {
|
||||
const { spacer, prefixCls, direction } = props
|
||||
|
||||
const children = renderSlot(slots, 'default', { key: 0 }, () => [])
|
||||
// retrieve the children out via a simple for loop
|
||||
// the edge case here is that when users uses directives like <v-for>, <v-if>
|
||||
// we need to go one layer deeper
|
||||
|
||||
if ((children.children ?? []).length === 0) return null
|
||||
|
||||
// loop the children, if current children is rendered via `renderList` or `<v-for>`
|
||||
if (isArray(children.children)) {
|
||||
let extractedChildren: VNode[] = []
|
||||
children.children.forEach((child, loopKey) => {
|
||||
if (isFragment(child)) {
|
||||
if (isArray(child.children)) {
|
||||
child.children.forEach((nested, key) => {
|
||||
extractedChildren.push(
|
||||
createVNode(
|
||||
Item,
|
||||
{
|
||||
style: itemStyle.value,
|
||||
prefixCls,
|
||||
key: `nested-${key}`,
|
||||
},
|
||||
{
|
||||
default: () => [nested],
|
||||
},
|
||||
PatchFlags.PROPS | PatchFlags.STYLE,
|
||||
['style', 'prefixCls']
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
// if the current child is valid vnode, then append this current vnode
|
||||
// to item as child node.
|
||||
} else if (isValidElementNode(child)) {
|
||||
extractedChildren.push(
|
||||
createVNode(
|
||||
Item,
|
||||
{
|
||||
style: itemStyle.value,
|
||||
prefixCls,
|
||||
key: `LoopKey${loopKey}`,
|
||||
},
|
||||
{
|
||||
default: () => [child],
|
||||
},
|
||||
PatchFlags.PROPS | PatchFlags.STYLE,
|
||||
['style', 'prefixCls']
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (spacer) {
|
||||
// track the current rendering index, when encounters the last element
|
||||
// then no need to add a spacer after it.
|
||||
const len = extractedChildren.length - 1
|
||||
extractedChildren = extractedChildren.reduce<VNode[]>(
|
||||
(acc, child, idx) => {
|
||||
const children = [...acc, child]
|
||||
if (idx !== len) {
|
||||
children.push(
|
||||
createVNode(
|
||||
'span',
|
||||
// adding width 100% for vertical alignment,
|
||||
// when the spacer inherit the width from the
|
||||
// parent, this span's width was not set, so space
|
||||
// might disappear
|
||||
{
|
||||
style: [
|
||||
itemStyle.value,
|
||||
direction === 'vertical' ? 'width: 100%' : null,
|
||||
],
|
||||
key: idx,
|
||||
},
|
||||
[
|
||||
// if spacer is already a valid vnode, then append it to the current
|
||||
// span element.
|
||||
// otherwise, treat it as string.
|
||||
isVNode(spacer)
|
||||
? spacer
|
||||
: createTextVNode(spacer as string, PatchFlags.TEXT),
|
||||
],
|
||||
PatchFlags.STYLE
|
||||
)
|
||||
)
|
||||
}
|
||||
return children
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
// spacer container.
|
||||
return createVNode(
|
||||
'div',
|
||||
{
|
||||
class: classes.value,
|
||||
style: containerStyle.value,
|
||||
},
|
||||
extractedChildren,
|
||||
PatchFlags.STYLE | PatchFlags.CLASS
|
||||
)
|
||||
}
|
||||
|
||||
return children.children
|
||||
}
|
||||
},
|
||||
})
|
84
packages/components/space/src/use-space.ts
Normal file
84
packages/components/space/src/use-space.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
import { isNumber } from '@element-plus/utils/util'
|
||||
import type { SpaceProps } from './space'
|
||||
|
||||
import type { CSSProperties, StyleValue } from 'vue'
|
||||
import type { ComponentSize } from '@element-plus/utils/types'
|
||||
|
||||
const SIZE_MAP: Record<ComponentSize, number> = {
|
||||
mini: 4,
|
||||
small: 8,
|
||||
medium: 12,
|
||||
large: 16,
|
||||
}
|
||||
|
||||
export function useSpace(props: SpaceProps) {
|
||||
const classes = computed(() => [
|
||||
'el-space',
|
||||
`el-space--${props.direction}`,
|
||||
props.class,
|
||||
])
|
||||
|
||||
const horizontalSize = ref(0)
|
||||
const verticalSize = ref(0)
|
||||
|
||||
const containerStyle = computed<StyleValue>(() => {
|
||||
const wrapKls: CSSProperties =
|
||||
props.wrap || props.fill
|
||||
? { flexWrap: 'wrap', marginBottom: `-${verticalSize.value}px` }
|
||||
: {}
|
||||
const alignment: CSSProperties = {
|
||||
alignItems: props.alignment,
|
||||
}
|
||||
return [wrapKls, alignment, props.style]
|
||||
})
|
||||
|
||||
const itemStyle = computed<StyleValue>(() => {
|
||||
const itemBaseStyle: CSSProperties = {
|
||||
paddingBottom: `${verticalSize.value}px`,
|
||||
marginRight: `${horizontalSize.value}px`,
|
||||
}
|
||||
|
||||
const fillStyle: CSSProperties = props.fill
|
||||
? { flexGrow: 1, minWidth: `${props.fillRatio}%` }
|
||||
: {}
|
||||
|
||||
return [itemBaseStyle, fillStyle]
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const { size = 'small', wrap, direction: dir, fill } = props
|
||||
|
||||
// when the specified size have been given
|
||||
if (Array.isArray(size)) {
|
||||
const [h = 0, v = 0] = size
|
||||
horizontalSize.value = h
|
||||
verticalSize.value = v
|
||||
} else {
|
||||
let val: number
|
||||
if (isNumber(size)) {
|
||||
val = size
|
||||
} else {
|
||||
val = SIZE_MAP[size] || SIZE_MAP.small
|
||||
}
|
||||
|
||||
if ((wrap || fill) && dir === 'horizontal') {
|
||||
horizontalSize.value = verticalSize.value = val
|
||||
} else {
|
||||
if (dir === 'horizontal') {
|
||||
horizontalSize.value = val
|
||||
verticalSize.value = 0
|
||||
} else {
|
||||
verticalSize.value = val
|
||||
horizontalSize.value = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
classes,
|
||||
containerStyle,
|
||||
itemStyle,
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
import { ref, computed, watch, isVNode } from 'vue'
|
||||
import { isValidComponentSize } from '@element-plus/utils/validators'
|
||||
import { isNumber, isArray, isString } from '@element-plus/utils/util'
|
||||
|
||||
import type { PropType, ExtractPropTypes, CSSProperties, VNodeChild } from 'vue'
|
||||
import type { ComponentSize } from '@element-plus/utils/types'
|
||||
|
||||
const SizeMap: Record<ComponentSize, number> = {
|
||||
mini: 4,
|
||||
small: 8,
|
||||
medium: 12,
|
||||
large: 16,
|
||||
}
|
||||
|
||||
export const defaultProps = {
|
||||
direction: {
|
||||
type: String as PropType<'horizontal' | 'vertical'>,
|
||||
default: 'horizontal',
|
||||
},
|
||||
|
||||
class: {
|
||||
type: [String, Object, Array],
|
||||
default: '',
|
||||
},
|
||||
|
||||
style: {
|
||||
type: [String, Array, Object] as PropType<
|
||||
string | Array<any> | CSSProperties
|
||||
>,
|
||||
},
|
||||
|
||||
alignment: {
|
||||
type: String as PropType<''>,
|
||||
default: 'center',
|
||||
},
|
||||
|
||||
prefixCls: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
spacer: {
|
||||
type: [Object, String, Number] as PropType<VNodeChild>,
|
||||
default: null,
|
||||
validator: (val: unknown) => {
|
||||
return isVNode(val) || isNumber(val) || isString(val)
|
||||
},
|
||||
},
|
||||
|
||||
wrap: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
fill: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
fillRatio: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
|
||||
size: {
|
||||
type: [String, Array, Number] as PropType<
|
||||
ComponentSize | [number, number] | number
|
||||
>,
|
||||
validator: (val: unknown) => {
|
||||
return (
|
||||
isValidComponentSize(val as string) || isNumber(val) || isArray(val)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function useSpace(props: ExtractPropTypes<typeof defaultProps>) {
|
||||
const classes = computed(() => [
|
||||
'el-space',
|
||||
`el-space--${props.direction}`,
|
||||
props.class,
|
||||
])
|
||||
|
||||
const horizontalSize = ref(0)
|
||||
const verticalSize = ref(0)
|
||||
|
||||
watch(
|
||||
() => [props.size, props.wrap, props.direction, props.fill],
|
||||
([size = 'small', wrap, dir, fill]) => {
|
||||
// when the specified size have been given
|
||||
if (isArray(size)) {
|
||||
const [h = 0, v = 0] = size
|
||||
horizontalSize.value = h
|
||||
verticalSize.value = v
|
||||
} else {
|
||||
let val: number
|
||||
if (isNumber(size)) {
|
||||
val = size as number
|
||||
} else {
|
||||
val = SizeMap[size as string] || SizeMap.small
|
||||
}
|
||||
|
||||
if ((wrap || fill) && dir === 'horizontal') {
|
||||
horizontalSize.value = verticalSize.value = val
|
||||
} else {
|
||||
if (dir === 'horizontal') {
|
||||
horizontalSize.value = val
|
||||
verticalSize.value = 0
|
||||
} else {
|
||||
verticalSize.value = val
|
||||
horizontalSize.value = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
const wrapKls: CSSProperties =
|
||||
props.wrap || props.fill
|
||||
? { flexWrap: 'wrap', marginBottom: `-${verticalSize.value}px` }
|
||||
: null
|
||||
const alignment: CSSProperties = {
|
||||
alignItems: props.alignment,
|
||||
}
|
||||
return [wrapKls, alignment, props.style] as Array<CSSProperties>
|
||||
})
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
const itemBaseStyle = {
|
||||
paddingBottom: `${verticalSize.value}px`,
|
||||
marginRight: `${horizontalSize.value}px`,
|
||||
}
|
||||
|
||||
const fillStyle = props.fill
|
||||
? { flexGrow: 1, minWidth: `${props.fillRatio}%` }
|
||||
: null
|
||||
|
||||
return [itemBaseStyle, fillStyle] as Array<CSSProperties>
|
||||
})
|
||||
|
||||
return {
|
||||
classes,
|
||||
containerStyle,
|
||||
itemStyle,
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ type BuildPropOption<T, D, R, V, C> = {
|
||||
|
||||
type IfUnknown<T, V> = [unknown] extends T ? V : T
|
||||
|
||||
type BuildPropType<T, V, C> =
|
||||
export type BuildPropType<T, V, C> =
|
||||
| (T extends PropWrapper<unknown>
|
||||
? T[typeof wrapperKey]
|
||||
: [V] extends [never]
|
||||
|
@ -144,8 +144,8 @@ export {
|
||||
extend,
|
||||
}
|
||||
|
||||
export const isBool = (val: unknown) => typeof val === 'boolean'
|
||||
export const isNumber = (val: unknown) => typeof val === 'number'
|
||||
export const isBool = (val: unknown): val is boolean => typeof val === 'boolean'
|
||||
export const isNumber = (val: unknown): val is number => typeof val === 'number'
|
||||
export const isHTMLElement = (val: unknown) => toRawType(val).startsWith('HTML')
|
||||
|
||||
export function rafThrottle<T extends AnyFunction<any>>(
|
||||
|
@ -35,8 +35,8 @@ export enum PatchFlags {
|
||||
BAIL = -2,
|
||||
}
|
||||
|
||||
export const isFragment = (node: VNodeChild) =>
|
||||
(node as VNode).type === Fragment
|
||||
export const isFragment = (node: unknown): node is VNode =>
|
||||
isVNode(node) && node.type === Fragment
|
||||
|
||||
export const isText = (node: VNodeChild) => (node as VNode).type === Text
|
||||
|
||||
@ -64,8 +64,8 @@ function getChildren(node: VNode, depth: number): undefined | VNode {
|
||||
* determine if the element is a valid element type rather than fragments and comment e.g. <template> v-if
|
||||
* @param node {VNode} node to be tested
|
||||
*/
|
||||
export const isValidElementNode = (node: VNodeChild) =>
|
||||
!(isFragment(node) || isComment(node))
|
||||
export const isValidElementNode = (node: unknown): node is VNode =>
|
||||
isVNode(node) && !isFragment(node) && !isComment(node)
|
||||
|
||||
export const getFirstValidNode = (
|
||||
nodes: VNodeChild,
|
||||
|
Loading…
Reference in New Issue
Block a user