HumanBreak/src/core/main/layout.ts
2024-03-01 19:52:30 +08:00

510 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Component,
RenderFunction,
SetupContext,
VNode,
VNodeChild,
defineComponent,
h as hVue,
isVNode,
onMounted
} from 'vue';
import BoxAnimate from '@/components/boxAnimate.vue';
import { ensureArray } from '@/plugin/utils';
interface VForRenderer {
type: '@v-for';
items: any[] | (() => any[]);
map: (value: any, index: number) => VNode;
}
interface MotaComponent extends MotaComponentConfig {
type: string;
children: (MComponent | MotaComponent | VNode)[];
}
interface MotaComponentConfig {
innerText?: string | (() => string);
props?: Record<string, () => any>;
component?: Component | MComponent;
dComponent?: () => Component;
/** 传递插槽 */
slots?: Record<string, (props: Record<string, any>) => VNode | VNode[]>;
vif?: () => boolean;
velse?: boolean;
}
type OnSetupFunction = (props: Record<string, any>, ctx: SetupContext) => void;
type SetupFunction = (
props: Record<string, any>,
ctx: SetupContext
) => RenderFunction | Promise<RenderFunction>;
type RetFunction = (
props: Record<string, any>,
ctx: SetupContext
) => VNodeChild | VNodeChild[];
type OnMountedFunction = (
props: Record<string, any>,
ctx: SetupContext,
canvas: HTMLCanvasElement[]
) => void;
type NonComponentConfig = Omit<
MotaComponentConfig,
'innerText' | 'component' | 'slots' | 'dComponent'
>;
type MComponentChildren =
| (MComponent | MotaComponent | VNode)[]
| MComponent
| MotaComponent
| VNode;
export class MComponent {
static mountNum: number = 0;
content: (MotaComponent | VForRenderer)[] = [];
private onSetupFn?: OnSetupFunction;
private setupFn?: SetupFunction;
private onMountedFn?: OnMountedFunction;
private retFn?: RetFunction;
private propsDef: Record<string, any> = {};
private emitsDef: string[] = [];
/**
* 定义一个props是一个对象键表示props名称值表示类型例如num: Number
* 对于直接通过`UiController.open`方法打开的ui应当包含以下两项
* - num: ui的唯一标识符类型为Number
* - ui: 对于的GameUi实例类型为GameUi
* @param props 被定义的props
*/
defineProps(props: Record<string, any>) {
this.propsDef = props;
}
/**
* 定义这个组件的emits是一个字符串数组表示emits的名称
* @param emits 被定义的emits
*/
defineEmits(emits: string[]) {
this.emitsDef = emits;
}
/**
* 添加一个div渲染内容
* @param children 渲染内容的子内容
* @param config 渲染内容的配置信息,参考 {@link MComponent.h}
*/
div(children?: MComponentChildren, config?: NonComponentConfig) {
return this.h('div', children, config);
}
/**
* 添加一个span渲染内容
* @param children 渲染内容的子内容
* @param config 渲染内容的配置信息,参考 {@link MComponent.h}
*/
span(children?: MComponentChildren, config?: NonComponentConfig) {
return this.h('span', children, config);
}
/**
* 添加一个canvas渲染内容
* @param config 渲染内容的配置信息,参考 {@link MComponent.h}
*/
canvas(config?: NonComponentConfig) {
return this.h('canvas', [], config);
}
/**
* 添加一个文字渲染内容
* @param text 要渲染的文字内容
*/
text(text: string | (() => string), config: NonComponentConfig = {}) {
return this.h('text', [], { ...config, innerText: text });
}
/**
* 添加一个组件渲染内容
* @param component 要添加的组件
* @param config 渲染内容的配置信息,参考 {@link MComponent.h}
*/
com(
component: Component | MComponent,
config?: Omit<MotaComponentConfig, 'innerText' | 'component'>
) {
return this.h(component, [], config);
}
/**
* 列表渲染一系列内容
* @param items 要遍历的列表,可以是一个数组,也可以是一个返回数组的函数
* @param map 遍历函数接收列表的每一项的值和索引并返回一个VNodeVNode可以通过Vue.h函数
* 或者MComponent.vNode函数生成。
*/
vfor<T>(items: T[] | (() => T[]), map: (value: T, index: number) => VNode) {
this.content.push(MCGenerator.vfor(items, map));
return this;
}
/**
* 添加渲染内容,注意区分组件和`MComponent`的区别,组件是经由`MComponent`的`export`函数输出的内容。
* 该函数是对`Vue`的`h`函数的高度包装,将`h`函数抽象成为了一个模板,然后经由`export`函数导出后直接输出成为一个组件。
* 而因此,几乎所有内容都要求传入一个函数,一般这个函数会在真正渲染的时候执行,并将返回值作为真正值传入。
* 不过对于部分内容,例如`slots`和`vfor.map`,并不是这样的。具体用法请参考参数注释。
* 注意如果使用了该包装,那么是无法实现响应式布局的,如果想要使用响应式布局,就必须调用`setup`方法,
* 手写全部的`setup`函数。
* @param type 要添加的渲染内容。
* - 可以是一个字符串表示dom元素例如`div` `span`等,
* - 可以是一个组件,也可以是一个`MComponent`,表示将其的导出作为组件。
* - 除此之外,还可以填`text`,表示这个渲染内容是一个单独的文字,同时`children`会无效,
* 必须填写`config`的`innerText`参数。
* - 该值还可以是字符串`component`,表示动态组件,同时必须填入`config`的`component`参数,
* 同时`children`会无效
* - 该值不能填`@v-for`
* @param children 该渲染内容的子内容。
* - 可以是一个`MComponent`数组,数组内容即是子内容
* - 也可以是一个`MComponent`,表示这个组件内容为子内容
* @param config 渲染内容的配置内容,包含下列内容,均为可选。
* - `innerText`: 当渲染内容为字符串时显示的内容,可以是字符串,或是返回字符串的函数
* - `props`: 传入渲染内容的`props`,是一个对象,每个值都是一个函数,其返回值是真正传入的`props`
* 对象的键是`prop`名称,如果是如`class` `id`这样的html属性那么会视为其`attribute`
* 会符合`Vue`的`attribute`透传。对于以on开头然后紧接着大写字母的属性会被视为事件监听
* 即v-on
* - `component`: 当为动态组件时,该项与`dComponent`必填其中之一,该项表示动态组件的内容
* - `dComponent`: 当为动态组件时,该项与`component`必填其中之一,该项是一个函数,返回值表示动态组件的内容
* 当`component`也填时,优先使用该项
* - `slots`: 传递插槽,将内容传入渲染内容的插槽,是一个对象,每个对象都是一个函数,
* 要求函数返回一个渲染VNode或数组可以通过`MComponent.vNode`函数将组件转换成VNode数组
* 返回值直接作为插槽内容
* - `vif`: 条件渲染,是一个函数,返回一个布尔值,表示条件是否满足,当`velse`为`true`时,
* 条件渲染将会变成 `else-if`
* - `velse`: 条件渲染,当前一个条件不满足时渲染该内容
*/
h(
type: string | Component | MComponent,
children?: MComponentChildren,
config: MotaComponentConfig = {}
): this {
this.content.push(MCGenerator.h(type, children, config));
return this;
}
/**
* 当setup被执行时要执行的函数接受props没有返回值可以不设置
*/
onSetup(fn: OnSetupFunction) {
this.onSetupFn = fn;
return this;
}
/**
* 当组件被挂载完毕后执行函数
* @param fn 当组件被挂载完毕后执行的函数接收props和当前级组件不包含子组件的所有画布作为参数
* 当前级组件表示直接在当前组件中渲染的内容,不包括子组件,子组件的画布需要在其对应的函数中获取
* 例如我在A组件中调用了B组件那么我只能获取A组件的画布而不能获取B组件的画布
*/
onMounted(fn: OnMountedFunction) {
this.onMountedFn = fn;
return this;
}
/**
* 完全设置setup执行函数接收props, slots并返回一个函数函数返回VNode可以不设置
*/
setup(fn: SetupFunction) {
this.setupFn = fn;
return this;
}
/**
* 完全设置setup返回的函数可以不设置
* @param fn setup返回的函数
*/
ret(fn: RetFunction) {
this.retFn = fn;
return this;
}
/**
* 将这个MComponent实例导出成为一个组件
*/
export() {
if (!this.setupFn) {
return defineComponent(
(props, ctx) => {
const mountNum = MComponent.mountNum++;
this.onSetupFn?.(props, ctx);
onMounted(() => {
this.onMountedFn?.(
props,
ctx,
Array.from(
document.getElementsByClassName(
`--mota-component-canvas-${mountNum}`
) as HTMLCollectionOf<HTMLCanvasElement>
)
);
});
if (this.retFn) return () => this.retFn!(props, ctx);
else {
return () => {
const vNodes = MComponent.vNode(
this.content,
mountNum
);
return vNodes;
};
}
},
{
emits: this.emitsDef,
props: this.propsDef
}
);
} else {
return defineComponent((props, ctx) => this.setupFn!(props, ctx));
}
}
/**
* 将单个渲染内容生成为一个单个的VNode
* @param child 要生成的单个渲染内容
* @param mount 组件生成时的挂载id一般不需要填用于画布获取
*/
static vNodeS(child: MotaComponent, mount?: number) {
return this.vNode([child], mount)[0];
}
/**
* 将一个MComponent实例生成为一个VNode列表
* @param mc 要生成VNode的组件
* @param mount 组件生成时的挂载id一般不需要填用于画布获取
*/
static vNodeM(mc: MComponent, mount?: number) {
return this.vNode(mc.content, mount);
}
/**
* 将一系列MComponent内容生成为一个VNode列表
* @param children 要生成VNode的内容列表
* @param mount 组件生成时的挂载id一般不需要填用于画布获取
*/
static vNode(
children: (MotaComponent | VForRenderer | VNode)[],
mount?: number
) {
const mountNum = mount ?? this.mountNum++;
const res: VNode[] = [];
const vifRes: Map<number, boolean> = new Map();
children.forEach((v, i) => {
if (isVNode(v)) {
res.push(v);
return;
}
if (v.type === '@v-for') {
const node = v as VForRenderer;
const items =
typeof node.items === 'function'
? node.items()
: node.items;
items.forEach((v, i) => {
res.push(node.map(v, i));
});
} else {
const node = v as MotaComponent;
if (node.velse && vifRes.get(i - 1)) {
vifRes.set(i, true);
return;
}
let vif = true;
if (node.vif) {
vifRes.set(i, (vif = node.vif()));
}
if (!vif) return;
const props = this.unwrapProps(node.props);
if (v.type === 'component') {
if (!v.component && !v.dComponent) {
throw new Error(
`Using dynamic component must provide component property.`
);
}
if (v.dComponent) {
res.push(hVue(v.dComponent(), props, v.slots));
} else {
if (v.component instanceof MComponent) {
res.push(
...MComponent.vNode(
v.component.content,
mountNum
)
);
} else {
res.push(hVue(v.component!, props, v.slots));
}
}
} else if (v.type === 'text') {
res.push(
hVue(
'span',
typeof v.innerText === 'function'
? v.innerText()
: v.innerText
)
);
} else if (v.type === 'canvas') {
const cls = `--mota-component-canvas-${mountNum}`;
const mix = !!props.class ? cls + ' ' + props.class : cls;
props.class = mix;
res.push(hVue('canvas', props, node.slots));
} else {
// 这个时候不可能会有插槽,只会有子内容,因此直接渲染子内容
const content = node.children;
const vn = this.vNode(
content
.map(v => (v instanceof MComponent ? v.content : v))
.flat(),
mountNum
);
res.push(hVue(v.type, props, vn));
}
}
});
return res;
}
/**
* 获取props的真实值。因为传入渲染内容的props是一个函数因此需要一层调用
* @param props 要获取的props
*/
static unwrapProps(props?: Record<string, () => any>): Record<string, any> {
if (!props) return {};
const res: Record<string, any> = {};
for (const [key, value] of Object.entries(props)) {
res[key] = value();
}
return res;
}
/**
* 在渲染时给一个组件传递props。实际效果为在调用后并不会传递当被传递的组件被渲染时将会传递props。
* @param component 要传递props的组件
* @param props 要传递的props
*/
static prop(component: Component, props: Record<string, any>) {
return hVue(component, props);
}
}
/**
* 创建一个MComponent实例由于该函数在创建ui时会频繁使用因此使用m这个简单的名字作为函数名
* @returns 一个新的MComponent实例
*/
export function m() {
return new MComponent();
}
export namespace MCGenerator {
export function h(
type: string | Component | MComponent,
children?: MComponentChildren,
config: MotaComponentConfig = {}
): MotaComponent {
if (typeof type === 'string') {
return {
type,
children: ensureArray(children ?? []),
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: config.component
};
} else {
return {
type: 'component',
children: ensureArray(children ?? []),
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: type
};
}
}
export function div(
children?: MComponentChildren,
config?: NonComponentConfig
): MotaComponent {
return h('div', children, config);
}
export function span(
children?: MComponentChildren,
config?: NonComponentConfig
): MotaComponent {
return h('span', children, config);
}
export function canvas(config?: NonComponentConfig): MotaComponent {
return h('canvas', [], config);
}
export function text(
text: string | (() => string),
config: NonComponentConfig = {}
): MotaComponent {
return h('text', [], { ...config, innerText: text });
}
export function com(
component: Component | MComponent,
config: Omit<MotaComponentConfig, 'innerText' | 'component'>
): MotaComponent {
return h(component, [], config);
}
/**
* 生成一个图标的VNode
* @param id 图标的id
* @param width 显示宽度,单位像素
* @param height 显示高度,单位像素
* @param noBoarder 显示的时候是否没有边框和背景
*/
export function icon(
id: AllIds,
width?: number,
height?: number,
noBoarder?: number
): VNode {
return hVue(BoxAnimate, { id, width, height, noBoarder });
}
export function vfor<T>(
items: T[] | (() => T[]),
map: (value: T, index: number) => VNode
): VForRenderer {
return {
type: '@v-for',
items,
map
};
}
/**
* 为一个常量创建为一个函数
* @param value 要创建成函数的值
*/
export function f<T>(value: T): () => T {
return () => value;
}
}