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 NotifyEditor from "./notify-editor";
|
||||||
import NotifyView from "./notify-view";
|
import NotifyView from "./notify-view";
|
||||||
import BottomPopup from "./bottom-popup";
|
import BottomPopup from "./bottom-popup";
|
||||||
|
import Space from "./space";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
RootLayout,
|
RootLayout,
|
||||||
@@ -20,7 +21,8 @@ export default [
|
|||||||
ArticleView,
|
ArticleView,
|
||||||
NotifyEditor,
|
NotifyEditor,
|
||||||
NotifyView,
|
NotifyView,
|
||||||
BottomPopup
|
BottomPopup,
|
||||||
|
Space
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -33,9 +35,19 @@ export {
|
|||||||
ArticleView,
|
ArticleView,
|
||||||
NotifyEditor,
|
NotifyEditor,
|
||||||
NotifyView,
|
NotifyView,
|
||||||
BottomPopup
|
BottomPopup,
|
||||||
|
Space
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
SpaceAlign,
|
||||||
|
SpaceDirection,
|
||||||
|
SpaceProps,
|
||||||
|
SpaceSeparator,
|
||||||
|
SpaceSize,
|
||||||
|
SpaceSlots
|
||||||
|
} from "./space";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
CodeEditorExpose
|
CodeEditorExpose
|
||||||
} from "./code-editor";
|
} 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