import { ComponentMetadata, NpmInfo, NodeData, NodeSchema, ComponentAction, TitleContent, TransformedComponentMetadata, NestingFilter, isTitleConfig, I18nData, LiveTextEditingConfig, FieldConfig, } from '@ali/lowcode-types'; import { computed } from '@ali/lowcode-editor-core'; import { Node, ParentalNode, TransformStage } from './document'; import { Designer } from './designer'; import { intlNode } from './locale'; import { IconContainer } from './icons/container'; import { IconPage } from './icons/page'; import { IconComponent } from './icons/component'; import { IconRemove } from './icons/remove'; import { IconClone } from './icons/clone'; import { ReactElement } from 'react'; import { IconHidden } from './icons/hidden'; 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; } function isRegExp(obj: any): obj is RegExp { return obj && obj.test && obj.exec && obj.compile; } function buildFilter(rule?: string | string[] | RegExp | NestingFilter) { if (!rule) { return null; } if (typeof rule === 'function') { return rule; } if (isRegExp(rule)) { return (testNode: Node | NodeSchema) => rule.test(testNode.componentName); } const list = ensureAList(rule); if (!list) { return null; } return (testNode: Node | NodeSchema) => list.includes(testNode.componentName); } export class ComponentMeta { readonly isComponentMeta = true; private _npm?: NpmInfo; get npm() { return this._npm; } private _componentName?: string; get componentName(): string { return this._componentName!; } private _isContainer?: boolean; get isContainer(): boolean { return this._isContainer! || this.isRootComponent(); } 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?: TransformedComponentMetadata; get configure() { const config = this._transformedMetadata?.configure; return config?.combined || config?.props || []; } private _liveTextEditing?: LiveTextEditingConfig[]; get liveTextEditing() { return this._liveTextEditing; } private parentWhitelist?: NestingFilter | null; private childWhitelist?: NestingFilter | null; private _title?: TitleContent; get title(): string | I18nData | ReactElement { // TODO: 标记下。这块需要康师傅加一下API,页面正常渲染。 // 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() { // TODO: 标记下。这块需要康师傅加一下API,页面正常渲染。 // give Slot default icon // if _title is TitleConfig get _title.icon return ( this._transformedMetadata?.icon || (this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent) ); } private _acceptable?: boolean; get acceptable(): boolean { return this._acceptable!; } constructor(readonly designer: Designer, metadata: ComponentMetadata) { this.parseMetadata(metadata); } setNpm(info: NpmInfo) { if (!this._npm) { this._npm = info; } } private parseMetadata(metadta: ComponentMetadata) { const { componentName, npm } = metadta; this._npm = npm; this._componentName = componentName; // 额外转换逻辑 this._transformedMetadata = this.transformMetadata(metadta); const title = this._transformedMetadata.title; if (title) { this._title = typeof title === 'string' ? { type: 'i18n', 'en-US': this.componentName, 'zh-CN': title, } : title; } const liveTextEditing = this._transformedMetadata.experimental?.liveTextEditing || []; function collectLiveTextEditing(items: FieldConfig[]) { 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 { configure = {} } = this._transformedMetadata; this._acceptable = false; const { component } = configure; if (component) { this._isContainer = component.isContainer ? true : false; this._isModal = component.isModal ? true : false; this._descriptor = component.descriptor; this._rootSelector = component.rootSelector; if (component.nestingRule) { const { parentWhitelist, childWhitelist } = component.nestingRule; this.parentWhitelist = buildFilter(parentWhitelist); this.childWhitelist = buildFilter(childWhitelist); } } else { this._isContainer = false; this._isModal = false; } } private transformMetadata(metadta: ComponentMetadata): TransformedComponentMetadata { const result = getRegisteredMetadataTransducers().reduce((prevMetadata, current) => { return current(prevMetadata); }, preprocessMetadata(metadta)); if (!result.configure) { result.configure = {}; } return result as any; } isRootComponent() { return this.componentName === 'Page' || this.componentName === 'Block' || this.componentName === 'Component'; } @computed get availableActions() { let { disableBehaviors, actions } = this._transformedMetadata?.configure.component || {}; const disabled = ensureAList(disableBehaviors) || (this.isRootComponent() ? ['copy', 'remove'] : null); actions = builtinComponentActions.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: ComponentMetadata) { this.parseMetadata(metadata); } getMetadata(): TransformedComponentMetadata { return this._transformedMetadata!; } checkNestingUp(my: Node | NodeData, parent: ParentalNode) { // 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器 if (this.parentWhitelist) { return this.parentWhitelist(parent, my); } return true; } checkNestingDown(my: Node, target: Node | NodeSchema) { // 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器 if (this.childWhitelist) { return this.childWhitelist(target, my); } return true; } // compatiable vision prototype?: any; } export function isComponentMeta(obj: any): obj is ComponentMeta { return obj && obj.isComponentMeta; } function preprocessMetadata(metadata: ComponentMetadata): TransformedComponentMetadata { if (metadata.configure) { if (Array.isArray(metadata.configure)) { return { ...metadata, configure: { props: metadata.configure, }, }; } return metadata as any; } return { ...metadata, configure: {}, }; } export interface MetadataTransducer { (prev: TransformedComponentMetadata): TransformedComponentMetadata; /** * 0 - 9 system * 10 - 99 builtin-plugin * 100 - app & plugin */ level?: number; /** * use to replace TODO */ id?: string; } const metadataTransducers: MetadataTransducer[] = []; export function registerMetadataTransducer(transducer: MetadataTransducer, level: number = 100, id?: string) { transducer.level = level; transducer.id = id; const i = metadataTransducers.findIndex((item) => item.level != null && item.level > level); if (i < 0) { metadataTransducers.push(transducer); } else { metadataTransducers.splice(i, 0, transducer); } } export function getRegisteredMetadataTransducers(): MetadataTransducer[] { return metadataTransducers; } registerMetadataTransducer((metadata) => { const { configure, componentName } = metadata; const { component = {} } = configure; if (!component.nestingRule) { let m; // uri match xx.Group set subcontrolling: true, childWhiteList if ((m = /^(.+)\.Group$/.exec(componentName))) { // component.subControlling = true; if (!component.nestingRule) { component.nestingRule = { childWhitelist: [`${m[1]}`], }; } } // uri match xx.Node set selfControlled: false, parentWhiteList else if ((m = /^(.+)\.Node$/.exec(componentName))) { // component.selfControlled = false; component.nestingRule = { parentWhitelist: [`${m[1]}`, componentName], }; } // uri match .Item .Node .Option set parentWhiteList else if ((m = /^(.+)\.(Item|Node|Option)$/.exec(componentName))) { component.nestingRule = { parentWhitelist: [`${m[1]}`], }; } } if (component.isModal == null && /Dialog/.test(componentName)) { component.isModal = true; } return { ...metadata, configure: { ...configure, component, }, }; }); const builtinComponentActions: ComponentAction[] = [ { name: 'remove', content: { icon: IconRemove, title: intlNode('remove'), action(node: Node) { node.remove(); }, }, important: true, }, { name: 'hide', content: { icon: IconHidden, title: intlNode('hide'), action(node: Node) { node.getExtraProp('hidden', true)?.setValue(true); }, }, condition: (node: Node) => { return node.componentMeta.isModal; }, important: true, }, { name: 'copy', content: { icon: IconClone, title: intlNode('copy'), action(node: Node) { // node.remove(); const { document: doc, parent, index } = node; parent && doc.insertNode(parent, node, index, true); }, }, important: true, }, ]; export function removeBuiltinComponentAction(name: string) { const i = builtinComponentActions.findIndex((action) => action.name === name); if (i > -1) { builtinComponentActions.splice(i, 1); } } export function addBuiltinComponentAction(action: ComponentAction) { builtinComponentActions.push(action); }