add space component
This commit is contained in:
+14
-2
@@ -9,6 +9,7 @@ import ArticleView from "./article-view";
|
||||
import NotifyEditor from "./notify-editor";
|
||||
import NotifyView from "./notify-view";
|
||||
import BottomPopup from "./bottom-popup";
|
||||
import Space from "./space";
|
||||
|
||||
export default [
|
||||
RootLayout,
|
||||
@@ -20,7 +21,8 @@ export default [
|
||||
ArticleView,
|
||||
NotifyEditor,
|
||||
NotifyView,
|
||||
BottomPopup
|
||||
BottomPopup,
|
||||
Space
|
||||
];
|
||||
|
||||
export {
|
||||
@@ -33,9 +35,19 @@ export {
|
||||
ArticleView,
|
||||
NotifyEditor,
|
||||
NotifyView,
|
||||
BottomPopup
|
||||
BottomPopup,
|
||||
Space
|
||||
};
|
||||
|
||||
export type {
|
||||
SpaceAlign,
|
||||
SpaceDirection,
|
||||
SpaceProps,
|
||||
SpaceSeparator,
|
||||
SpaceSize,
|
||||
SpaceSlots
|
||||
} from "./space";
|
||||
|
||||
export type {
|
||||
CodeEditorExpose
|
||||
} from "./code-editor";
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import view from "./index.vue";
|
||||
import { WebToolkit } from "timi-web";
|
||||
|
||||
export type {
|
||||
SpaceAlign,
|
||||
SpaceDirection,
|
||||
SpaceProps,
|
||||
SpaceSeparator,
|
||||
SpaceSize,
|
||||
SpaceSlots
|
||||
} from "./type";
|
||||
|
||||
export const Space = WebToolkit.withInstall(view);
|
||||
export default Space;
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div
|
||||
class="t-space"
|
||||
:class="spaceClass"
|
||||
:style="spaceStyle"
|
||||
>
|
||||
<space-item
|
||||
v-for="item in children"
|
||||
:key="getChildKey(item)"
|
||||
:child="item.node"
|
||||
:separator="separatorContent"
|
||||
:show-separator="hasSeparator && item.index < children.length - 1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Comment, Fragment, Teleport, defineComponent, h, isVNode, onBeforeUpdate, useSlots } from "vue";
|
||||
import type { CSSProperties, PropType, VNode, VNodeChild } from "vue";
|
||||
import type { SpaceProps, SpaceSlots, SpaceSize } from "./type";
|
||||
|
||||
defineOptions({
|
||||
name: "TSpace"
|
||||
});
|
||||
|
||||
const SIZE_MAP: Record<string, string> = {
|
||||
small: "8px",
|
||||
medium: "16px",
|
||||
large: "24px"
|
||||
};
|
||||
|
||||
interface FlatChild {
|
||||
node: VNodeChild;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const SpaceItem = defineComponent({
|
||||
name: "SpaceItem",
|
||||
props: {
|
||||
child: {
|
||||
type: null as unknown as PropType<VNodeChild>,
|
||||
required: true
|
||||
},
|
||||
separator: {
|
||||
type: null as unknown as PropType<VNodeChild>
|
||||
},
|
||||
showSeparator: Boolean
|
||||
},
|
||||
setup(itemProps) {
|
||||
return () => [
|
||||
h("div", { class: "t-space-item" }, toRenderableChildren(itemProps.child)),
|
||||
itemProps.showSeparator
|
||||
? h("div", { class: "t-space-item-separator" }, toRenderableChildren(itemProps.separator))
|
||||
: null
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
align: {
|
||||
type: String as PropType<SpaceProps["align"]>
|
||||
},
|
||||
breakLine: Boolean,
|
||||
direction: {
|
||||
type: String as PropType<SpaceProps["direction"]>,
|
||||
default: "horizontal"
|
||||
},
|
||||
separator: {
|
||||
type: null as unknown as PropType<SpaceProps["separator"]>
|
||||
},
|
||||
size: {
|
||||
type: [String, Number, Array] as PropType<SpaceProps["size"]>,
|
||||
default: "medium"
|
||||
}
|
||||
});
|
||||
|
||||
const slots = useSlots() as SpaceSlots;
|
||||
|
||||
let children = flattenChildren(slots.default?.() ?? []);
|
||||
let separatorContent = resolveSeparatorContent();
|
||||
let hasSeparator = hasSeparatorContent(separatorContent);
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
children = flattenChildren(slots.default?.() ?? []);
|
||||
separatorContent = resolveSeparatorContent();
|
||||
hasSeparator = hasSeparatorContent(separatorContent);
|
||||
});
|
||||
|
||||
function resolveSeparatorContent(): VNodeChild | undefined {
|
||||
if (props.separator !== undefined) {
|
||||
return typeof props.separator === "function" ? props.separator() : props.separator;
|
||||
}
|
||||
return slots.separator?.();
|
||||
}
|
||||
|
||||
function hasSeparatorContent(separator: VNodeChild | undefined): boolean {
|
||||
return Array.isArray(separator) ? 0 < separator.length : Boolean(separator);
|
||||
}
|
||||
|
||||
const spaceClass = computed(() => ({
|
||||
"t-space-align-start": props.align === "start",
|
||||
"t-space-align-end": props.align === "end",
|
||||
"t-space-align-baseline": props.align === "baseline",
|
||||
"t-space-align-center": props.align === "center",
|
||||
"t-space-vertical": props.direction === "vertical",
|
||||
"t-space-horizontal": props.direction === "horizontal",
|
||||
"t-space--break-line": props.breakLine
|
||||
}));
|
||||
|
||||
const spaceStyle = computed<CSSProperties>(() => ({
|
||||
gap: normalizeGap(props.size)
|
||||
}));
|
||||
|
||||
function normalizeGap(size: SpaceSize | SpaceSize[] | undefined): string {
|
||||
if (Array.isArray(size)) {
|
||||
return size.map(normalizeSize).join(" ");
|
||||
}
|
||||
return normalizeSize(size ?? "medium");
|
||||
}
|
||||
|
||||
function normalizeSize(size: SpaceSize): string {
|
||||
if (typeof size === "number") {
|
||||
return `${size}px`;
|
||||
}
|
||||
return SIZE_MAP[size] ?? size;
|
||||
}
|
||||
|
||||
type RenderableChild = string | number | VNode;
|
||||
|
||||
function toRenderableChildren(content: VNodeChild | undefined): RenderableChild[] {
|
||||
if (content === null || content === undefined || typeof content === "boolean") {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return content.flatMap(item => toRenderableChildren(item as VNodeChild));
|
||||
}
|
||||
return [content as RenderableChild];
|
||||
}
|
||||
|
||||
function flattenChildren(nodes: VNodeChild[]): FlatChild[] {
|
||||
const result: FlatChild[] = [];
|
||||
const position = { value: 0 };
|
||||
|
||||
function append(items: VNodeChild[]): void {
|
||||
for (const node of items) {
|
||||
if (isVNode(node) && node.type === Fragment && Array.isArray(node.children)) {
|
||||
append(node.children as VNodeChild[]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = position.value;
|
||||
position.value += 1;
|
||||
const skipped = isVNode(node)
|
||||
&& (node.type === Comment
|
||||
|| node.type === Teleport
|
||||
|| (typeof node.type === "symbol" && !node.children));
|
||||
if (skipped) {
|
||||
continue;
|
||||
}
|
||||
result.push({ node, index });
|
||||
}
|
||||
}
|
||||
|
||||
append(nodes);
|
||||
return result;
|
||||
}
|
||||
|
||||
function getChildKey(item: FlatChild): PropertyKey {
|
||||
if (isVNode(item.node) && item.node.key !== null && item.node.key !== undefined) {
|
||||
return item.node.key;
|
||||
}
|
||||
return item.index;
|
||||
}
|
||||
|
||||
defineSlots<SpaceSlots>();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.t-space {
|
||||
display: inline-flex;
|
||||
|
||||
:deep(.t-space-item) {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&-align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&-align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&-align-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
&-align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-vertical {
|
||||
flex-direction: column;
|
||||
|
||||
> :deep(.t-space-item-separator) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&--break-line {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { VNodeChild } from "vue";
|
||||
|
||||
/** Space 对齐方式。 */
|
||||
export type SpaceAlign = "start" | "end" | "center" | "baseline";
|
||||
|
||||
/** Space 排列方向。 */
|
||||
export type SpaceDirection = "vertical" | "horizontal";
|
||||
|
||||
/** Space 预设或自定义间距。 */
|
||||
export type SpaceSize = number | string;
|
||||
|
||||
/** Space 分隔符内容。 */
|
||||
export type SpaceSeparator = string | VNodeChild | (() => VNodeChild);
|
||||
|
||||
/** Space 组件属性。 */
|
||||
export interface SpaceProps {
|
||||
/** 对齐方式。 */
|
||||
align?: SpaceAlign;
|
||||
/** 是否自动换行,仅在 horizontal 时有效。 */
|
||||
breakLine?: boolean;
|
||||
/** 间距方向。 */
|
||||
direction?: SpaceDirection;
|
||||
/** 分隔符,也可以使用 separator 插槽。 */
|
||||
separator?: SpaceSeparator;
|
||||
/** 间距大小,数组依次表示横向和纵向间距。 */
|
||||
size?: SpaceSize | SpaceSize[];
|
||||
}
|
||||
|
||||
/** Space 插槽。 */
|
||||
export interface SpaceSlots {
|
||||
default?: () => VNodeChild[];
|
||||
separator?: () => VNodeChild[];
|
||||
}
|
||||
Reference in New Issue
Block a user