packages/designer/src/component-meta.ts (320 lines of code) (raw):
import { ReactElement } from 'react';
import {
IPublicTypeComponentMetadata,
IPublicTypeNpmInfo,
IPublicTypeNodeData,
IPublicTypeNodeSchema,
IPublicTypeTitleContent,
IPublicTypeTransformedComponentMetadata,
IPublicTypeNestingFilter,
IPublicTypeI18nData,
IPublicTypeFieldConfig,
IPublicModelComponentMeta,
IPublicTypeAdvanced,
IPublicTypeDisposable,
IPublicTypeLiveTextEditingConfig,
} from '@alilc/lowcode-types';
import { deprecate, isRegExp, isTitleConfig, isNode } from '@alilc/lowcode-utils';
import { computed, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
import { Node, INode } from './document';
import { Designer } from './designer';
import {
IconContainer,
IconPage,
IconComponent,
} from './icons';
export function ensureAList(list?: string | string[]): string[] | null {
if (!list) {
return null;
}
if (!Array.isArray(list)) {
if (typeof list !== 'string') {
return null;
}
list = list.split(/ *[ ,|] */).filter(Boolean);
}
if (list.length < 1) {
return null;
}
return list;
}
export function buildFilter(rule?: string | string[] | RegExp | IPublicTypeNestingFilter) {
if (!rule) {
return null;
}
if (typeof rule === 'function') {
return rule;
}
if (isRegExp(rule)) {
return (testNode: Node | IPublicTypeNodeSchema) => {
return rule.test(testNode.componentName);
};
}
const list = ensureAList(rule);
if (!list) {
return null;
}
return (testNode: Node | IPublicTypeNodeSchema) => {
return list.includes(testNode.componentName);
};
}
export interface IComponentMeta extends IPublicModelComponentMeta<INode> {
prototype?: any;
liveTextEditing?: IPublicTypeLiveTextEditingConfig[];
get rootSelector(): string | undefined;
setMetadata(metadata: IPublicTypeComponentMetadata): void;
onMetadataChange(fn: (args: any) => void): IPublicTypeDisposable;
}
export class ComponentMeta implements IComponentMeta {
readonly isComponentMeta = true;
private _npm?: IPublicTypeNpmInfo;
private emitter: IEventBus = createModuleEventBus('ComponentMeta');
get npm() {
return this._npm;
}
set npm(_npm: any) {
this.setNpm(_npm);
}
private _componentName?: string;
get componentName(): string {
return this._componentName!;
}
private _isContainer?: boolean;
get isContainer(): boolean {
return this._isContainer! || this.isRootComponent();
}
get isMinimalRenderUnit(): boolean {
return this._isMinimalRenderUnit || false;
}
private _isModal?: boolean;
get isModal(): boolean {
return this._isModal!;
}
private _descriptor?: string;
get descriptor(): string | undefined {
return this._descriptor;
}
private _rootSelector?: string;
get rootSelector(): string | undefined {
return this._rootSelector;
}
private _transformedMetadata?: IPublicTypeTransformedComponentMetadata;
get configure(): IPublicTypeFieldConfig[] {
const config = this._transformedMetadata?.configure;
return config?.combined || config?.props || [];
}
private _liveTextEditing?: IPublicTypeLiveTextEditingConfig[];
get liveTextEditing() {
return this._liveTextEditing;
}
private _isTopFixed?: boolean;
get isTopFixed(): boolean {
return !!(this._isTopFixed);
}
private parentWhitelist?: IPublicTypeNestingFilter | null;
private childWhitelist?: IPublicTypeNestingFilter | null;
private _title?: IPublicTypeTitleContent;
private _isMinimalRenderUnit?: boolean;
get title(): string | IPublicTypeI18nData | ReactElement {
// string | i18nData | ReactElement
// TitleConfig title.label
if (isTitleConfig(this._title)) {
return (this._title?.label as any) || this.componentName;
}
return this._title || this.componentName;
}
@computed get icon() {
// give Slot default icon
// if _title is TitleConfig get _title.icon
return (
this._transformedMetadata?.icon ||
// eslint-disable-next-line
(this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent)
);
}
private _acceptable?: boolean;
get acceptable(): boolean {
return this._acceptable!;
}
get advanced(): IPublicTypeAdvanced {
return this.getMetadata().configure.advanced || {};
}
/**
* @legacy compatiable for vision
* @deprecated
*/
prototype?: any;
constructor(readonly designer: Designer, metadata: IPublicTypeComponentMetadata) {
this.parseMetadata(metadata);
}
setNpm(info: IPublicTypeNpmInfo) {
if (!this._npm) {
this._npm = info;
}
}
private parseMetadata(metadata: IPublicTypeComponentMetadata) {
const { componentName, npm, ...others } = metadata;
let _metadata = metadata;
if ((metadata as any).prototype) {
this.prototype = (metadata as any).prototype;
}
if (!npm && !Object.keys(others).length) {
// 没有注册的组件,只能删除,不支持复制、移动等操作
_metadata = {
componentName,
configure: {
component: {
disableBehaviors: ['copy', 'move', 'lock', 'unlock'],
},
advanced: {
callbacks: {
onMoveHook: () => false,
},
},
},
};
}
this._npm = npm || this._npm;
this._componentName = componentName;
// 额外转换逻辑
this._transformedMetadata = this.transformMetadata(_metadata);
const { title } = this._transformedMetadata;
if (title) {
this._title =
typeof title === 'string'
? {
type: 'i18n',
'en-US': this.componentName,
'zh-CN': title,
}
: title;
}
const liveTextEditing = this.advanced.liveTextEditing || [];
function collectLiveTextEditing(items: IPublicTypeFieldConfig[]) {
items.forEach((config) => {
if (config?.items) {
collectLiveTextEditing(config.items);
} else {
const liveConfig = config.liveTextEditing || config.extraProps?.liveTextEditing;
if (liveConfig) {
liveTextEditing.push({
propTarget: String(config.name),
...liveConfig,
});
}
}
});
}
collectLiveTextEditing(this.configure);
this._liveTextEditing = liveTextEditing.length > 0 ? liveTextEditing : undefined;
const isTopFixed = this.advanced.isTopFixed;
if (isTopFixed) {
this._isTopFixed = isTopFixed;
}
const { configure = {} } = this._transformedMetadata;
this._acceptable = false;
const { component } = configure;
if (component) {
this._isContainer = !!component.isContainer;
this._isModal = !!component.isModal;
this._descriptor = component.descriptor;
this._rootSelector = component.rootSelector;
this._isMinimalRenderUnit = component.isMinimalRenderUnit;
if (component.nestingRule) {
const { parentWhitelist, childWhitelist } = component.nestingRule;
this.parentWhitelist = buildFilter(parentWhitelist);
this.childWhitelist = buildFilter(childWhitelist);
}
} else {
this._isContainer = false;
this._isModal = false;
}
this.emitter.emit('metadata_change');
}
refreshMetadata() {
this.parseMetadata(this.getMetadata());
}
private transformMetadata(
metadta: IPublicTypeComponentMetadata,
): IPublicTypeTransformedComponentMetadata {
const registeredTransducers = this.designer.componentActions.getRegisteredMetadataTransducers();
const result = registeredTransducers.reduce((prevMetadata, current) => {
return current(prevMetadata);
}, preprocessMetadata(metadta));
if (!result.configure) {
result.configure = {};
}
if (result.experimental && !result.configure.advanced) {
deprecate(result.experimental, '.experimental', '.configure.advanced');
result.configure.advanced = result.experimental;
}
return result as any;
}
isRootComponent(includeBlock = true): boolean {
return (
this.componentName === 'Page' ||
this.componentName === 'Component' ||
(includeBlock && this.componentName === 'Block')
);
}
@computed get availableActions() {
// eslint-disable-next-line prefer-const
let { disableBehaviors, actions } = this._transformedMetadata?.configure.component || {};
const disabled =
ensureAList(disableBehaviors) ||
(this.isRootComponent(false) ? ['copy', 'remove', 'lock', 'unlock'] : null);
actions = this.designer.componentActions.actions.concat(
this.designer.getGlobalComponentActions() || [],
actions || [],
);
if (disabled) {
if (disabled.includes('*')) {
return actions.filter((action) => action.condition === 'always');
}
return actions.filter((action) => disabled.indexOf(action.name) < 0);
}
return actions;
}
setMetadata(metadata: IPublicTypeComponentMetadata) {
this.parseMetadata(metadata);
}
getMetadata(): IPublicTypeTransformedComponentMetadata {
return this._transformedMetadata!;
}
checkNestingUp(my: INode | IPublicTypeNodeData, parent: INode) {
// 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器
if (this.parentWhitelist) {
return this.parentWhitelist(
parent.internalToShellNode(),
isNode<INode>(my) ? my.internalToShellNode() : my,
);
}
return true;
}
checkNestingDown(my: INode, target: INode | IPublicTypeNodeSchema | IPublicTypeNodeSchema[]): boolean {
// 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器
if (this.childWhitelist) {
const _target: any = !Array.isArray(target) ? [target] : target;
return _target.every((item: Node | IPublicTypeNodeSchema) => {
const _item = !isNode<INode>(item) ? new Node(my.document, item) : item;
return (
this.childWhitelist &&
this.childWhitelist(_item.internalToShellNode(), my.internalToShellNode())
);
});
}
return true;
}
onMetadataChange(fn: (args: any) => void): IPublicTypeDisposable {
this.emitter.on('metadata_change', fn);
return () => {
this.emitter.removeListener('metadata_change', fn);
};
}
}
export function isComponentMeta(obj: any): obj is ComponentMeta {
return obj && obj.isComponentMeta;
}
function preprocessMetadata(metadata: IPublicTypeComponentMetadata): IPublicTypeTransformedComponentMetadata {
if (metadata.configure) {
if (Array.isArray(metadata.configure)) {
return {
...metadata,
configure: {
props: metadata.configure,
},
};
}
return metadata as any;
}
return {
...metadata,
configure: {},
};
}