mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-02-27 12:20:39 +00:00
348 lines
7.9 KiB
TypeScript
348 lines
7.9 KiB
TypeScript
import { computed, obx } from '@ali/lowcode-editor-core';
|
|
import { PropsMap, PropsList, CompositeValue } from '@ali/lowcode-types';
|
|
import { uniqueId } from '@ali/lowcode-utils';
|
|
import { PropStash } from './prop-stash';
|
|
import { Prop, IPropParent, UNSET } from './prop';
|
|
import { Node } from '../node';
|
|
import { TransformStage } from '../transform-stage';
|
|
|
|
export const EXTRA_KEY_PREFIX = '___';
|
|
export function getConvertedExtraKey(key: string): string {
|
|
if (!key) {
|
|
return '';
|
|
}
|
|
let _key = key;
|
|
if (key.indexOf('.') > 0) {
|
|
_key = key.split('.')[0];
|
|
}
|
|
return EXTRA_KEY_PREFIX + _key + EXTRA_KEY_PREFIX + key.substr(_key.length);
|
|
}
|
|
export function getOriginalExtraKey(key: string): string {
|
|
return key.replace(new RegExp(`${EXTRA_KEY_PREFIX}`, 'g'), '');
|
|
}
|
|
|
|
export class Props implements IPropParent {
|
|
readonly id = uniqueId('props');
|
|
@obx.val private items: Prop[] = [];
|
|
@computed private get maps(): Map<string, Prop> {
|
|
const maps = new Map();
|
|
if (this.items.length > 0) {
|
|
this.items.forEach(prop => {
|
|
if (prop.key) {
|
|
maps.set(prop.key, prop);
|
|
}
|
|
});
|
|
}
|
|
return maps;
|
|
}
|
|
|
|
get props(): Props {
|
|
return this;
|
|
}
|
|
|
|
private stash = new PropStash(this, prop => {
|
|
this.items.push(prop);
|
|
prop.parent = this;
|
|
});
|
|
|
|
/**
|
|
* 元素个数
|
|
*/
|
|
@computed get size() {
|
|
return this.items.length;
|
|
}
|
|
|
|
@obx type: 'map' | 'list' = 'map';
|
|
|
|
constructor(readonly owner: Node, value?: PropsMap | PropsList | null, extras?: object) {
|
|
if (Array.isArray(value)) {
|
|
this.type = 'list';
|
|
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread));
|
|
} else if (value != null) {
|
|
this.items = Object.keys(value).map(key => new Prop(this, value[key], key));
|
|
}
|
|
if (extras) {
|
|
Object.keys(extras).forEach(key => {
|
|
this.items.push(new Prop(this, (extras as any)[key], getConvertedExtraKey(key)));
|
|
});
|
|
}
|
|
}
|
|
|
|
import(value?: PropsMap | PropsList | null, extras?: object) {
|
|
this.stash.clear();
|
|
const originItems = this.items;
|
|
if (Array.isArray(value)) {
|
|
this.type = 'list';
|
|
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread));
|
|
} else if (value != null) {
|
|
this.type = 'map';
|
|
this.items = Object.keys(value).map(key => new Prop(this, value[key], key));
|
|
} else {
|
|
this.type = 'map';
|
|
this.items = [];
|
|
}
|
|
if (extras) {
|
|
Object.keys(extras).forEach(key => {
|
|
this.items.push(new Prop(this, (extras as any)[key], getConvertedExtraKey(key)));
|
|
});
|
|
}
|
|
originItems.forEach(item => item.purge());
|
|
}
|
|
|
|
merge(value: PropsMap) {
|
|
Object.keys(value).forEach(key => {
|
|
this.query(key, true)!.setValue(value[key]);
|
|
});
|
|
}
|
|
|
|
export(stage: TransformStage = TransformStage.Save): { props?: PropsMap | PropsList; extras?: object } {
|
|
if (this.items.length < 1) {
|
|
return {};
|
|
}
|
|
let props: any = {};
|
|
const extras: any = {};
|
|
if (this.type === 'list') {
|
|
props = [];
|
|
this.items.forEach(item => {
|
|
let value = item.export(stage);
|
|
if (value === UNSET) {
|
|
value = undefined;
|
|
}
|
|
let name = item.key as string;
|
|
if (name && typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) {
|
|
name = getOriginalExtraKey(name);
|
|
extras[name] = value;
|
|
} else {
|
|
props.push({
|
|
spread: item.spread,
|
|
name,
|
|
value,
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
this.items.forEach(item => {
|
|
let name = item.key as string;
|
|
if (name == null) {
|
|
// todo ...spread
|
|
return;
|
|
}
|
|
let value = item.export(stage);
|
|
if (value === UNSET) {
|
|
value = undefined;
|
|
}
|
|
if (typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) {
|
|
name = getOriginalExtraKey(name);
|
|
extras[name] = value;
|
|
} else {
|
|
props[name] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
return { props, extras };
|
|
}
|
|
|
|
/**
|
|
* 根据 path 路径查询属性
|
|
*
|
|
* @param stash 如果没有则临时生成一个
|
|
*/
|
|
query(path: string, stash = true): Prop | null {
|
|
return this.get(path, stash);
|
|
// todo: future support list search
|
|
let matchedLength = 0;
|
|
let firstMatched = null;
|
|
if (this.items) {
|
|
// target: a.b.c
|
|
// trys: a.b.c, a.b, a
|
|
let i = this.items.length;
|
|
while (i-- > 0) {
|
|
const expr = this.items[i];
|
|
if (!expr.key) {
|
|
continue;
|
|
}
|
|
const name = String(expr.key);
|
|
if (name === path) {
|
|
// completely match
|
|
return expr;
|
|
}
|
|
|
|
// fisrt match
|
|
const l = name.length;
|
|
if (path.slice(0, l + 1) === `${name}.`) {
|
|
matchedLength = l;
|
|
firstMatched = expr;
|
|
}
|
|
}
|
|
}
|
|
|
|
let ret = null;
|
|
if (firstMatched) {
|
|
ret = firstMatched.get(path.slice(matchedLength + 1), true);
|
|
}
|
|
if (!ret && stash) {
|
|
return this.stash.get(path);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* 获取某个属性, 如果不存在,临时获取一个待写入
|
|
* @param stash 强制
|
|
*/
|
|
get(path: string, stash = false): Prop | null {
|
|
let entry = path;
|
|
let nest = '';
|
|
const i = path.indexOf('.');
|
|
if (i > 0) {
|
|
nest = path.slice(i + 1);
|
|
if (nest) {
|
|
entry = path.slice(0, i);
|
|
}
|
|
}
|
|
|
|
const prop = this.maps.get(entry) || (stash && this.stash.get(entry)) || null;
|
|
|
|
if (prop) {
|
|
return nest ? prop.get(nest, stash) : prop;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 删除项
|
|
*/
|
|
delete(prop: Prop): void {
|
|
const i = this.items.indexOf(prop);
|
|
if (i > -1) {
|
|
this.items.splice(i, 1);
|
|
prop.purge();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除 key
|
|
*/
|
|
deleteKey(key: string): void {
|
|
this.items = this.items.filter(item => {
|
|
if (item.key === key) {
|
|
item.purge();
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 添加值
|
|
*/
|
|
add(value: CompositeValue | null, key?: string | number, spread = false): Prop {
|
|
const prop = new Prop(this, value, key, spread);
|
|
this.items.push(prop);
|
|
return prop;
|
|
}
|
|
|
|
/**
|
|
* 是否存在 key
|
|
*/
|
|
has(key: string): boolean {
|
|
return this.maps.has(key);
|
|
}
|
|
|
|
/**
|
|
* 迭代器
|
|
*/
|
|
[Symbol.iterator](): { next(): { value: Prop } } {
|
|
let index = 0;
|
|
const items = this.items;
|
|
const length = items.length || 0;
|
|
return {
|
|
next() {
|
|
if (index < length) {
|
|
return {
|
|
value: items[index++],
|
|
done: false,
|
|
};
|
|
}
|
|
return {
|
|
value: undefined as any,
|
|
done: true,
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 遍历
|
|
*/
|
|
forEach(fn: (item: Prop, key: number | string | undefined) => void): void {
|
|
this.items.forEach(item => {
|
|
return fn(item, item.key);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 遍历
|
|
*/
|
|
map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null {
|
|
return this.items.map(item => {
|
|
return fn(item, item.key);
|
|
});
|
|
}
|
|
|
|
filter(fn: (item: Prop, key: number | string | undefined) => boolean) {
|
|
return this.items.filter(item => {
|
|
return fn(item, item.key);
|
|
});
|
|
}
|
|
|
|
private purged = false;
|
|
/**
|
|
* 回收销毁
|
|
*/
|
|
purge() {
|
|
if (this.purged) {
|
|
return;
|
|
}
|
|
this.purged = true;
|
|
this.stash.purge();
|
|
this.items.forEach(item => item.purge());
|
|
}
|
|
|
|
getProp(path: string, stash = true): Prop | null {
|
|
return this.query(path, stash as any) || null;
|
|
}
|
|
|
|
/**
|
|
* 获取单个属性值
|
|
*/
|
|
getPropValue(path: string): any {
|
|
return this.getProp(path, false)?.value;
|
|
}
|
|
|
|
/**
|
|
* 设置单个属性值
|
|
*/
|
|
setPropValue(path: string, value: any) {
|
|
this.getProp(path, true)!.setValue(value);
|
|
}
|
|
|
|
/**
|
|
* 获取 props 对应的 node
|
|
*/
|
|
getNode() {
|
|
return this.owner;
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
* 获取 props 对应的 node
|
|
*/
|
|
toData() {
|
|
return this.export()?.props;
|
|
}
|
|
}
|