mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-13 01:21:58 +00:00
631 lines
15 KiB
TypeScript
631 lines
15 KiB
TypeScript
import { Component } from 'react';
|
|
import { EventEmitter } from 'events';
|
|
import { fromJS, Iterable, Map as IMMap } from 'immutable';
|
|
import logger from '@ali/vu-logger';
|
|
import { cloneDeep, isDataEqual, combineInitial, Transducer } from '@ali/ve-utils';
|
|
import I18nUtil from '@ali/ve-i18n-util';
|
|
import { editor, setters } from '@ali/lowcode-engine';
|
|
import { OldPropConfig, DISPLAY_TYPE } from './bundle/upgrade-metadata';
|
|
import { uniqueId } from '@ali/lowcode-utils';
|
|
|
|
const { getSetter } = setters;
|
|
|
|
type IPropConfig = OldPropConfig;
|
|
|
|
// 1: chain -1: start 0: discard
|
|
const CHAIN_START = -1;
|
|
const CHAIN_HAS_REACH = 0;
|
|
|
|
export enum PROP_VALUE_CHANGED_TYPE {
|
|
/**
|
|
* normal set value
|
|
*/
|
|
SET_VALUE = 'SET_VALUE',
|
|
/**
|
|
* value changed caused by sub-prop value change
|
|
*/
|
|
SUB_VALUE_CHANGE = 'SUB_VALUE_CHANGE',
|
|
}
|
|
|
|
/**
|
|
* Dynamic setter will use 've.plugin.setterProvider' to
|
|
* calculate setter type in runtime
|
|
*/
|
|
let dynamicSetterProvider: any;
|
|
|
|
export interface IHotDataMap extends IMMap<string, any> {
|
|
value: any;
|
|
hotValue: any;
|
|
}
|
|
|
|
export interface ISetValueOptions {
|
|
disableMutator?: boolean;
|
|
type?: PROP_VALUE_CHANGED_TYPE;
|
|
}
|
|
|
|
export interface IVariableSettable {
|
|
useVariable?: boolean;
|
|
variableValue: string;
|
|
isUseVariable: () => boolean;
|
|
isSupportVariable: () => boolean;
|
|
setVariableValue: (value: string) => void;
|
|
setUseVariable: (flag?: boolean) => void;
|
|
getVariableValue: () => string;
|
|
onUseVariableChange: (func: (data: { isUseVariable: boolean }) => any) => void;
|
|
}
|
|
|
|
export default class Prop implements IVariableSettable {
|
|
/**
|
|
* Setters predefined as default options
|
|
* can by selected by user for every prop
|
|
*
|
|
* @static
|
|
* @memberof Prop
|
|
*/
|
|
public static INSET_SETTER = {};
|
|
|
|
public id: string;
|
|
|
|
public emitter: EventEmitter;
|
|
|
|
public inited: boolean;
|
|
|
|
public i18nLink: any;
|
|
|
|
public loopLock: boolean;
|
|
|
|
public props: any;
|
|
|
|
public parent: any;
|
|
|
|
public config: IPropConfig;
|
|
|
|
public initial: any;
|
|
|
|
public initialData: any;
|
|
|
|
public expanded: boolean;
|
|
|
|
public useVariable?: boolean;
|
|
|
|
/**
|
|
* value to be saved in schema it is usually JSON serialized
|
|
* prototype.js can config Transducer.toNative to generate value
|
|
*/
|
|
public value: any;
|
|
|
|
/**
|
|
* value to be used in VisualDesigner more flexible
|
|
* prototype.js can config Transducer.toHot to generate hotValue
|
|
*/
|
|
public hotValue: any;
|
|
|
|
/**
|
|
* 启用变量之后,变量表达式字符串值
|
|
*/
|
|
public variableValue: string;
|
|
|
|
public hotData: IMMap<string, IHotDataMap>;
|
|
|
|
public defaultValue: any;
|
|
|
|
public transducer: any;
|
|
|
|
public inGroup: boolean;
|
|
|
|
constructor(parent: any, config: IPropConfig, data?: any) {
|
|
if (parent.isProps) {
|
|
this.props = parent;
|
|
this.parent = null;
|
|
} else {
|
|
this.props = parent.getProps();
|
|
this.parent = parent;
|
|
}
|
|
|
|
this.id = uniqueId('prop');
|
|
|
|
if (typeof config.setter === 'string') {
|
|
config.setter = getSetter(config.setter)?.component as any;
|
|
}
|
|
this.config = config;
|
|
this.emitter = new EventEmitter();
|
|
this.emitter.setMaxListeners(100);
|
|
this.initialData = data;
|
|
this.useVariable = false;
|
|
|
|
dynamicSetterProvider = editor.get('ve.plugin.setterProvider');
|
|
|
|
this.beforeInit();
|
|
}
|
|
|
|
public getId() {
|
|
return this.id;
|
|
}
|
|
|
|
public isTab() {
|
|
return this.getDisplay() === 'tab';
|
|
}
|
|
|
|
public isGroup() {
|
|
return false;
|
|
}
|
|
|
|
public beforeInit() {
|
|
if (IMMap.isMap(this.initialData)) {
|
|
this.value = this.initialData.get('value');
|
|
if (this.value && typeof this.value.toJS === 'function') {
|
|
this.value = this.value.toJS();
|
|
}
|
|
this.hotData = this.initialData;
|
|
} else {
|
|
this.value = this.initialData;
|
|
}
|
|
|
|
this.resolveValue();
|
|
|
|
let defaultValue = null;
|
|
if (this.config.defaultValue !== undefined) {
|
|
defaultValue = this.config.defaultValue;
|
|
} else if (typeof this.config.initialValue !== 'function') {
|
|
defaultValue = this.config.initialValue;
|
|
}
|
|
this.defaultValue = defaultValue;
|
|
this.transducer = new Transducer(this, this.config);
|
|
this.initial = combineInitial(this, this.config);
|
|
}
|
|
|
|
public resolveValue() {
|
|
if (this.value && this.value.type === 'variable') {
|
|
const { value, variable } = this.value;
|
|
this.value = value;
|
|
this.variableValue = variable;
|
|
this.useVariable = this.isSupportVariable();
|
|
} else {
|
|
this.useVariable = false;
|
|
}
|
|
}
|
|
|
|
public init(defaultValue?: any) {
|
|
if (this.inited) { return; }
|
|
|
|
this.value = this.initial(this.value,
|
|
this.defaultValue != null ? this.defaultValue : defaultValue);
|
|
|
|
if (this.hotData) {
|
|
const tempVal = this.hotData.get('value');
|
|
// if we create a prop from runtime data, we don't need initial() or set with defaultValue process
|
|
// but if we got an empty value, we fill with the initial() process and default value
|
|
if (Iterable.isIterable(tempVal)) {
|
|
this.value = tempVal.toJS() || this.value;
|
|
} else {
|
|
this.value = tempVal || this.value;
|
|
}
|
|
this.resolveValue();
|
|
}
|
|
|
|
this.i18nLink = I18nUtil.attach(this, this.value,
|
|
((val: any) => { this.setValue(val, false, true); }) as any);
|
|
|
|
// call config.accessor
|
|
const value = this.getValue();
|
|
|
|
if (this.hotData) {
|
|
this.hotValue = this.hotData.get('hotValue');
|
|
if (this.hotValue && Iterable.isIterable(this.hotValue)) {
|
|
this.hotValue = this.hotValue.toJS();
|
|
}
|
|
} else {
|
|
try {
|
|
this.hotValue = this.transducer.toHot(value);
|
|
} catch (e) {
|
|
logger.log('ERROR_PROP_VALUE');
|
|
logger.warn('属性初始化错误:', this);
|
|
}
|
|
|
|
this.hotData = fromJS({
|
|
hotValue: this.hotValue,
|
|
value: this.getMixValue(value),
|
|
});
|
|
}
|
|
this.inited = true;
|
|
}
|
|
|
|
public isInited() {
|
|
return this.inited;
|
|
}
|
|
|
|
public getHotData() {
|
|
return this.hotData;
|
|
}
|
|
|
|
public getProps() {
|
|
return this.props;
|
|
}
|
|
|
|
public getNode(): any {
|
|
return this.getProps().getNode();
|
|
}
|
|
|
|
/**
|
|
* 获得属性名称
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
public getName(): string {
|
|
const ns = this.parent ? `${this.parent.getName()}.` : '';
|
|
return ns + this.config.name;
|
|
}
|
|
|
|
public getKey() {
|
|
return this.config.name;
|
|
}
|
|
|
|
/**
|
|
* 获得属性标题
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
public getTitle() {
|
|
return this.config.title || this.getName();
|
|
}
|
|
|
|
public getTip() {
|
|
return this.config.tip || null;
|
|
}
|
|
|
|
public getValue(disableCache?: boolean, options?: {
|
|
disableAccessor?: boolean;
|
|
}) {
|
|
const { accessor } = this.config;
|
|
if (accessor && (!options || !options.disableAccessor)) {
|
|
const value = accessor.call(this as any, this.value);
|
|
if (!disableCache) {
|
|
this.value = value;
|
|
}
|
|
return value;
|
|
}
|
|
return this.value;
|
|
}
|
|
|
|
public getMixValue(value?: any) {
|
|
if (value == null) {
|
|
value = this.getValue();
|
|
}
|
|
if (this.isUseVariable()) {
|
|
value = {
|
|
type: 'variable',
|
|
value,
|
|
variable: this.getVariableValue(),
|
|
};
|
|
}
|
|
return value;
|
|
}
|
|
|
|
public toData() {
|
|
return cloneDeep(this.getMixValue());
|
|
}
|
|
|
|
public getDefaultValue() {
|
|
return this.defaultValue;
|
|
}
|
|
|
|
public getHotValue() {
|
|
return this.hotValue;
|
|
}
|
|
|
|
public getConfig<K extends keyof IPropConfig>(configName?: K): IPropConfig[K] | IPropConfig {
|
|
if (configName) {
|
|
return this.config[configName];
|
|
}
|
|
|
|
return this.config;
|
|
}
|
|
|
|
public sync() {
|
|
if (this.props.hasReach(this)) {
|
|
return;
|
|
}
|
|
|
|
const { sync } = this.config;
|
|
if (sync) {
|
|
const value = sync.call(this as any, this.getValue(true));
|
|
if (value !== undefined) {
|
|
this.setValue(value);
|
|
}
|
|
} else {
|
|
// sync 的时候不再需要调用经过 accessor 处理之后的值了
|
|
// 这里之所以需要 setValue 是为了过 getValue() 中的 accessor 修饰函数
|
|
this.setValue(this.getValue(true), false, false, {
|
|
disableMutator: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
public isUseVariable() {
|
|
return this.useVariable || false;
|
|
}
|
|
|
|
public isSupportVariable() {
|
|
return this.config.supportVariable || false;
|
|
}
|
|
|
|
public setVariableValue(value: string) {
|
|
if (!this.isUseVariable()) { return; }
|
|
|
|
const state = this.props.chainReach(this);
|
|
if (state === CHAIN_HAS_REACH) {
|
|
return;
|
|
}
|
|
|
|
this.variableValue = value;
|
|
|
|
if (this.modify()) {
|
|
this.valueChange();
|
|
this.props.syncPass(this);
|
|
}
|
|
|
|
if (state === CHAIN_START) {
|
|
this.props.endChain();
|
|
}
|
|
}
|
|
|
|
public setUseVariable(flag = false) {
|
|
if (this.useVariable === flag) { return; }
|
|
|
|
const state = this.props.chainReach(this);
|
|
if (state === CHAIN_HAS_REACH) {
|
|
return;
|
|
}
|
|
|
|
this.useVariable = flag;
|
|
this.expanded = true;
|
|
|
|
if (this.modify()) {
|
|
this.valueChange();
|
|
this.props.syncPass(this);
|
|
}
|
|
|
|
if (state === CHAIN_START) {
|
|
this.props.endChain();
|
|
}
|
|
|
|
this.emitter.emit('ve.prop.useVariableChange', { isUseVariable: flag });
|
|
if (this.config.useVariableChange) {
|
|
this.config.useVariableChange.call(this as any, { isUseVariable: flag });
|
|
}
|
|
}
|
|
|
|
public getVariableValue() {
|
|
return this.variableValue;
|
|
}
|
|
|
|
/**
|
|
* @param value
|
|
* @param isHotValue 是否为设计器热状态值
|
|
* @param force 是否强制触发更新
|
|
*/
|
|
public setValue(value: any, isHotValue?: boolean, force?: boolean, extraOptions?: ISetValueOptions) {
|
|
const state = this.props.chainReach(this);
|
|
if (state === CHAIN_HAS_REACH) {
|
|
return;
|
|
}
|
|
|
|
const preValue = this.value;
|
|
const preHotValue = this.hotValue;
|
|
|
|
if (isHotValue) {
|
|
this.hotValue = value;
|
|
this.value = this.transducer.toNative(this.hotValue);
|
|
} else {
|
|
if (!isDataEqual(value, this.value)) {
|
|
this.hotValue = this.transducer.toHot(value);
|
|
}
|
|
this.value = value;
|
|
}
|
|
|
|
this.i18nLink = I18nUtil.attach(this, this.value, ((val: any) => this.setValue(val, false, true)) as any);
|
|
|
|
const { mutator } = this.config;
|
|
|
|
if (!extraOptions) {
|
|
extraOptions = {};
|
|
}
|
|
|
|
if (mutator && !extraOptions.disableMutator) {
|
|
mutator.call(this as any, this.value);
|
|
}
|
|
|
|
if (this.modify(force)) {
|
|
this.valueChange(extraOptions);
|
|
this.props.syncPass(this);
|
|
}
|
|
|
|
if (state === CHAIN_START) {
|
|
this.props.endChain();
|
|
}
|
|
}
|
|
|
|
public setHotValue(hotValue: any, options?: ISetValueOptions) {
|
|
try {
|
|
this.setValue(hotValue, true, false, options);
|
|
} catch (e) {
|
|
logger.log('ERROR_PROP_VALUE');
|
|
logger.warn('属性值设置错误:', e, hotValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 验证是否存在变更
|
|
* @param force 是否强制返回已变更
|
|
*/
|
|
public modify(force?: boolean) {
|
|
const hotData = this.hotData.merge(fromJS({
|
|
hotValue: this.getHotValue(),
|
|
value: this.getMixValue(),
|
|
}));
|
|
|
|
if (!force && hotData.equals(this.hotData)) {
|
|
return false;
|
|
}
|
|
|
|
this.hotData = hotData;
|
|
|
|
(this.parent || this.props).modify(this.getName());
|
|
|
|
return true;
|
|
}
|
|
|
|
public setHotData(hotData: IMMap<string, IHotDataMap>, options?: ISetValueOptions) {
|
|
if (!IMMap.isMap(hotData)) {
|
|
return;
|
|
}
|
|
this.hotData = hotData;
|
|
let value = hotData.get('value');
|
|
if (value && typeof value.toJS === 'function') {
|
|
value = value.toJS();
|
|
}
|
|
let hotValue = hotData.get('hotValue');
|
|
if (hotValue && typeof hotValue.toJS === 'function') {
|
|
hotValue = hotValue.toJS();
|
|
}
|
|
|
|
const preValue = value;
|
|
const preHotValue = hotValue;
|
|
|
|
this.value = value;
|
|
this.hotValue = hotValue;
|
|
this.resolveValue();
|
|
|
|
if (!options || !options.disableMutator) {
|
|
const { mutator } = this.config;
|
|
if (mutator) {
|
|
mutator.call(this as any, value);
|
|
}
|
|
}
|
|
|
|
this.valueChange();
|
|
}
|
|
|
|
public valueChange(options?: ISetValueOptions) {
|
|
if (this.loopLock) { return; }
|
|
|
|
this.emitter.emit('valuechange', options);
|
|
if (this.parent) {
|
|
this.parent.valueChange(options);
|
|
}
|
|
}
|
|
|
|
public getDisplay() {
|
|
return this.config.display || this.config.fieldStyle || 'block';
|
|
}
|
|
|
|
public isHidden() {
|
|
if (!this.isInited() || this.getDisplay() === DISPLAY_TYPE.NONE || this.isDisabled()) {
|
|
return true;
|
|
}
|
|
|
|
let { hidden } = this.config;
|
|
if (typeof hidden === 'function') {
|
|
hidden = hidden.call(this as any, this.getValue());
|
|
}
|
|
return hidden === true;
|
|
}
|
|
|
|
public isDisabled() {
|
|
let { disabled } = this.config;
|
|
if (typeof disabled === 'function') {
|
|
disabled = disabled.call(this as any, this.getValue());
|
|
}
|
|
return disabled === true;
|
|
}
|
|
|
|
public isIgnore() {
|
|
if (this.isDisabled()) { return true; }
|
|
|
|
let { ignore } = this.config;
|
|
if (typeof ignore === 'function') {
|
|
ignore = ignore.call(this as any, this.getValue());
|
|
}
|
|
return ignore === true;
|
|
}
|
|
|
|
public isExpand() {
|
|
if (this.expanded == null) {
|
|
this.expanded = !(this.config.collapsed || this.config.fieldCollapsed);
|
|
}
|
|
return this.expanded;
|
|
}
|
|
|
|
public toggleExpand() {
|
|
if (this.expanded) {
|
|
this.expanded = false;
|
|
} else {
|
|
this.expanded = true;
|
|
}
|
|
this.emitter.emit('expandchange', this.expanded);
|
|
}
|
|
|
|
public getSetter() {
|
|
if (dynamicSetterProvider) {
|
|
const setter = dynamicSetterProvider.call(this, this, this.getNode().getPrototype());
|
|
if (setter) {
|
|
return setter;
|
|
}
|
|
}
|
|
const setterConfig = this.config.setter;
|
|
if (typeof setterConfig === 'function' && !(setterConfig.prototype instanceof Component)) {
|
|
return (setterConfig as any).call(this, this.getValue());
|
|
}
|
|
if (Array.isArray(setterConfig)) {
|
|
let item;
|
|
for (item of setterConfig) {
|
|
if (item.condition?.call(this, this.getValue())) {
|
|
return item.setter;
|
|
}
|
|
}
|
|
return setterConfig[0].setter;
|
|
}
|
|
return setterConfig;
|
|
}
|
|
|
|
public getSetterData(): any {
|
|
if (Array.isArray(this.config.setter)) {
|
|
let item;
|
|
for (item of this.config.setter) {
|
|
if (item.condition?.call(this, this.getValue())) {
|
|
return item;
|
|
}
|
|
}
|
|
return this.config.setter[0];
|
|
}
|
|
return { };
|
|
}
|
|
|
|
public destroy() {
|
|
if (this.i18nLink) {
|
|
this.i18nLink.detach();
|
|
}
|
|
}
|
|
|
|
public onValueChange(func: () => any) {
|
|
this.emitter.on('valuechange', func);
|
|
return () => {
|
|
this.emitter.removeListener('valuechange', func);
|
|
};
|
|
}
|
|
|
|
public onExpandChange(func: () => any) {
|
|
this.emitter.on('expandchange', func);
|
|
return () => {
|
|
this.emitter.removeListener('expandchange', func);
|
|
};
|
|
}
|
|
|
|
public onUseVariableChange(func: (data: { isUseVariable: boolean }) => any) {
|
|
this.emitter.on('ve.prop.useVariableChange', func);
|
|
return () => {
|
|
this.emitter.removeListener('ve.prop.useVariableChange', func);
|
|
};
|
|
}
|
|
}
|