2022-08-09 18:25:47 +08:00

720 lines
16 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 { untracked, computed, obx, engineConfig, action, makeObservable, mobx, runInAction } from '@alilc/lowcode-editor-core';
import { CompositeValue, GlobalEvent, isJSExpression, isJSSlot, JSSlot, SlotSchema } from '@alilc/lowcode-types';
import { uniqueId, isPlainObject, hasOwnProperty, compatStage } from '@alilc/lowcode-utils';
import { valueToSource } from './value-to-source';
import { Props } from './props';
import { SlotNode, Node } from '../node';
import { TransformStage } from '../transform-stage';
const { set: mobxSet, isObservableArray } = mobx;
export const UNSET = Symbol.for('unset');
// eslint-disable-next-line no-redeclare
export type UNSET = typeof UNSET;
export interface IPropParent {
delete(prop: Prop): void;
readonly props: Props;
readonly owner: Node;
readonly path: string[];
}
export type ValueTypes = 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot';
export class Prop implements IPropParent {
readonly isProp = true;
readonly owner: Node;
/**
* 键值
*/
@obx key: string | number | undefined;
/**
* 扩展值
*/
@obx spread: boolean;
readonly props: Props;
readonly options: any;
constructor(
public parent: IPropParent,
value: CompositeValue | UNSET = UNSET,
key?: string | number,
spread = false,
options = {},
) {
makeObservable(this);
this.owner = parent.owner;
this.props = parent.props;
this.key = key;
this.spread = spread;
this.options = options;
if (value !== UNSET) {
this.setValue(value);
}
this.setupItems();
}
// TODO: 先用调用方式触发子 prop 的初始化,后续须重构
@action
setupItems() {
return this.items;
}
/**
* @see SettingTarget
*/
@action
getPropValue(propName: string | number): any {
return this.get(propName)!.getValue();
}
/**
* @see SettingTarget
*/
@action
setPropValue(propName: string | number, value: any): void {
this.set(propName, value);
}
/**
* @see SettingTarget
*/
@action
clearPropValue(propName: string | number): void {
this.get(propName, false)?.unset();
}
readonly id = uniqueId('prop$');
@obx.ref private _type: ValueTypes = 'unset';
/**
* 属性类型
*/
get type(): ValueTypes {
return this._type;
}
@obx private _value: any = UNSET;
/**
* 属性值
*/
@computed get value(): CompositeValue | UNSET {
return this.export(TransformStage.Serilize);
}
export(stage: TransformStage = TransformStage.Save): CompositeValue {
stage = compatStage(stage);
const type = this._type;
if (stage === TransformStage.Render && this.key === '___condition___') {
// 在设计器里,所有组件默认需要展示,除非开启了 enableCondition 配置
if (engineConfig?.get('enableCondition') !== true) {
return true;
}
return this._value;
}
if (type === 'unset') {
return undefined;
}
if (type === 'literal' || type === 'expression') {
// TODO 后端改造之后删除此逻辑
if (this._value === null && stage === TransformStage.Save) {
return '';
}
return this._value;
}
if (type === 'slot') {
const schema = this._slotNode?.export(stage) || {} as any;
if (stage === TransformStage.Render) {
return {
type: 'JSSlot',
params: schema.params,
value: schema,
};
}
return {
type: 'JSSlot',
params: schema.params,
value: schema.children,
title: schema.title,
name: schema.name,
};
}
if (type === 'map') {
if (!this._items) {
return this._value;
}
let maps: any;
this.items!.forEach((prop, key) => {
if (!prop.isUnset()) {
const v = prop.export(stage);
if (v != null) {
maps = maps || {};
maps[prop.key || key] = v;
}
}
});
return maps;
}
if (type === 'list') {
if (!this._items) {
return this._value;
}
const values = this.items!.map((prop) => {
return prop.export(stage);
});
if (values.every(val => val === undefined)) {
return undefined;
}
return values;
}
}
private _code: string | null = null;
/**
* 获得表达式值
*/
@computed get code() {
if (isJSExpression(this.value)) {
return this.value.value;
}
// todo: JSFunction ...
if (this.type === 'slot') {
return JSON.stringify(this._slotNode!.export(TransformStage.Save));
}
return this._code != null ? this._code : JSON.stringify(this.value);
}
/**
* 设置表达式值
*/
set code(code: string) {
if (isJSExpression(this._value)) {
this.setValue({
...this._value,
value: code,
});
this._code = code;
return;
}
try {
const v = JSON.parse(code);
this.setValue(v);
this._code = code;
return;
} catch (e) {
// ignore
}
this.setValue({
type: 'JSExpression',
value: code,
mock: this._value,
});
this._code = code;
}
getAsString(): string {
if (this.type === 'literal') {
return this._value ? String(this._value) : '';
}
return '';
}
/**
* set value, val should be JSON Object
*/
@action
setValue(val: CompositeValue) {
if (val === this._value) return;
const editor = this.owner.document?.designer.editor;
const oldValue = this._value;
this._value = val;
this._code = null;
const t = typeof val;
if (val == null) {
// this._value = undefined;
this._type = 'literal';
} else if (t === 'string' || t === 'number' || t === 'boolean') {
this._type = 'literal';
} else if (Array.isArray(val)) {
this._type = 'list';
} else if (isPlainObject(val)) {
if (isJSSlot(val)) {
this.setAsSlot(val);
} else if (isJSExpression(val)) {
this._type = 'expression';
} else {
this._type = 'map';
}
} else /* istanbul ignore next */ {
this._type = 'expression';
this._value = {
type: 'JSExpression',
value: valueToSource(val),
};
}
this.dispose();
if (oldValue !== this._value) {
const propsInfo = {
key: this.key,
prop: this,
oldValue,
newValue: this._value,
};
editor?.emit(GlobalEvent.Node.Prop.InnerChange, {
node: this.owner as any,
...propsInfo,
});
this.owner?.emitPropChange?.(propsInfo);
}
}
getValue(): CompositeValue {
return this.export(TransformStage.Serilize);
}
@action
private dispose() {
const items = untracked(() => this._items);
if (items) {
items.forEach((prop) => prop.purge());
}
this._items = null;
this._prevMaps = this._maps;
this._maps = null;
if (this._type !== 'slot' && this._slotNode) {
this._slotNode.remove();
this._slotNode = undefined;
}
}
private _slotNode?: SlotNode;
get slotNode() {
return this._slotNode;
}
@action
setAsSlot(data: JSSlot) {
this._type = 'slot';
const slotSchema: SlotSchema = {
componentName: 'Slot',
title: data.title,
id: data.id,
name: data.name,
params: data.params,
children: data.value,
};
if (this._slotNode) {
this._slotNode.import(slotSchema);
} else {
const { owner } = this.props;
this._slotNode = owner.document.createNode<SlotNode>(slotSchema);
owner.addSlot(this._slotNode);
this._slotNode.internalSetSlotFor(this);
}
}
/**
* 取消设置值
*/
@action
unset() {
this._type = 'unset';
}
/**
* 是否未设置值
*/
@action
isUnset() {
return this._type === 'unset';
}
isVirtual() {
return typeof this.key === 'string' && this.key.charAt(0) === '!';
}
/**
* @returns 0: the same 1: maybe & like 2: not the same
*/
compare(other: Prop | null): number {
if (!other || other.isUnset()) {
return this.isUnset() ? 0 : 2;
}
if (other.type !== this.type) {
return 2;
}
// list
if (this.type === 'list') {
return this.size === other.size ? 1 : 2;
}
if (this.type === 'map') {
return 1;
}
// 'literal' | 'map' | 'expression' | 'slot'
return this.code === other.code ? 0 : 2;
}
@obx.shallow private _items: Prop[] | null = null;
@obx.shallow private _maps: Map<string | number, Prop> | null = null;
/**
* 作为 _maps 的一层缓存机制,主要是复用部分已存在的 Prop保持响应式关系比如
* 当前 Prop#_value 值为 { a: 1 },当调用 setValue({ a: 2 }) 时,所有原来的子 Prop 均被销毁,
* 导致假如外部有 mobx reaction常见于 observer此时响应式链路会被打断
* 因为 reaction 监听的是原 Prop(a) 的 _value而不是新 Prop(a) 的 _value。
*/
private _prevMaps: Map<string | number, Prop> | null = null;
get path(): string[] {
return (this.parent.path || []).concat(this.key as string);
}
/**
* 构造 items 属性,同时构造 maps 属性
*/
private get items(): Prop[] | null {
if (this._items) return this._items;
return runInAction(() => {
let items: Prop[] | null = null;
if (this._type === 'list') {
const data = this._value;
data.forEach((item: any, idx: number) => {
items = items || [];
items.push(new Prop(this, item, idx));
});
this._maps = null;
} else if (this._type === 'map') {
const data = this._value;
const maps = new Map<string, Prop>();
const keys = Object.keys(data);
for (const key of keys) {
let prop: Prop;
if (this._prevMaps?.has(key)) {
prop = this._prevMaps.get(key)!;
prop.setValue(data[key]);
} else {
prop = new Prop(this, data[key], key);
}
items = items || [];
items.push(prop);
maps.set(key, prop);
}
this._maps = maps;
} else {
items = null;
this._maps = null;
}
this._items = items;
return this._items;
});
}
@computed private get maps(): Map<string | number, Prop> | null {
if (!this.items) {
return null;
}
return this._maps;
}
/**
* 获取某个属性
* @param createIfNone 当没有的时候,是否创建一个
*/
@action
get(path: string | number, createIfNone = true): Prop | null {
const type = this._type;
if (type !== 'map' && type !== 'list' && type !== 'unset' && !createIfNone) {
return null;
}
const maps = type === 'map' ? this.maps : null;
const items = type === 'list' ? this.items : null;
let entry = path;
let nest = '';
if (typeof path !== 'number') {
const i = path.indexOf('.');
if (i > 0) {
nest = path.slice(i + 1);
if (nest) {
entry = path.slice(0, i);
}
}
}
let prop: any;
if (type === 'list') {
if (isValidArrayIndex(entry, this.size)) {
prop = items![entry];
}
} else if (type === 'map') {
prop = maps?.get(entry);
}
if (prop) {
return nest ? prop.get(nest, createIfNone) : prop;
}
if (createIfNone) {
prop = new Prop(this, UNSET, entry);
this.set(entry, prop, true);
if (nest) {
return prop.get(nest, true);
}
return prop;
}
return null;
}
/**
* 从父级移除本身
*/
@action
remove() {
this.parent.delete(this);
}
/**
* 删除项
*/
@action
delete(prop: Prop): void {
/* istanbul ignore else */
if (this._items) {
const i = this._items.indexOf(prop);
if (i > -1) {
this._items.splice(i, 1);
prop.purge();
}
if (this._maps && prop.key) {
this._maps.delete(String(prop.key));
}
}
}
/**
* 删除 key
*/
@action
deleteKey(key: string): void {
/* istanbul ignore else */
if (this.maps) {
const prop = this.maps.get(key);
if (prop) {
this.delete(prop);
}
}
}
/**
* 元素个数
*/
get size(): number {
return this.items?.length || 0;
}
/**
* 添加值到列表
*
* @param force 强制
*/
@action
add(value: CompositeValue, force = false): Prop | null {
const type = this._type;
if (type !== 'list' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'list')) {
this.setValue([]);
}
const prop = new Prop(this, value);
this._items = this._items || [];
this._items.push(prop);
return prop;
}
/**
* 设置值到字典
*
* @param force 强制
*/
@action
set(key: string | number, value: CompositeValue | Prop, force = false) {
const type = this._type;
if (type !== 'map' && type !== 'list' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'map')) {
if (isValidArrayIndex(key)) {
if (type !== 'list') {
this.setValue([]);
}
} else {
this.setValue({});
}
}
const prop = isProp(value) ? value : new Prop(this, value, key);
let items = this._items! || [];
if (this.type === 'list') {
if (!isValidArrayIndex(key)) {
return null;
}
if (isObservableArray(items)) {
mobxSet(items, key, prop);
} else {
items[key] = prop;
}
this._items = items;
} else if (this.type === 'map') {
const maps = this._maps || new Map<string, Prop>();
const orig = maps?.get(key);
if (orig) {
// replace
const i = items.indexOf(orig);
if (i > -1) {
items.splice(i, 1, prop)[0].purge();
}
maps?.set(key, prop);
} else {
// push
items.push(prop);
this._items = items;
maps?.set(key, prop);
}
this._maps = maps;
} /* istanbul ignore next */ else {
return null;
}
return prop;
}
/**
* 是否存在 key
*/
has(key: string): boolean {
if (this._type !== 'map') {
return false;
}
if (this._maps) {
return this._maps.has(key);
}
return hasOwnProperty(this._value, key);
}
private purged = false;
/**
* 回收销毁
*/
@action
purge() {
if (this.purged) {
return;
}
this.purged = true;
if (this._items) {
this._items.forEach((item) => item.purge());
}
this._items = null;
this._maps = null;
if (this._slotNode && this._slotNode.slotFor === this) {
this._slotNode.remove();
this._slotNode = undefined;
}
}
/**
* 迭代器
*/
[Symbol.iterator](): { next(): { value: Prop } } {
let index = 0;
const { items } = this;
const length = items?.length || 0;
return {
next() {
if (index < length) {
return {
value: items![index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
* 遍历
*/
@action
forEach(fn: (item: Prop, key: number | string | undefined) => void): void {
const { items } = this;
if (!items) {
return;
}
const isMap = this._type === 'map';
items.forEach((item, index) => {
return isMap ? fn(item, item.key) : fn(item, index);
});
}
/**
* 遍历
*/
@action
map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null {
const { items } = this;
if (!items) {
return null;
}
const isMap = this._type === 'map';
return items.map((item, index) => {
return isMap ? fn(item, item.key) : fn(item, index);
});
}
getProps() {
return this.props;
}
getNode() {
return this.owner;
}
}
export function isProp(obj: any): obj is Prop {
return obj && obj.isProp;
}
export function isValidArrayIndex(key: any, limit = -1): key is number {
const n = parseFloat(String(key));
return n >= 0 && Math.floor(n) === n && isFinite(n) && (limit < 0 || n < limit);
}