mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-14 13:03:07 +00:00
complete metadata transducer
This commit is contained in:
parent
f37743327b
commit
5f569cc1ca
@ -29,7 +29,7 @@ import {
|
||||
CanvasPoint,
|
||||
} from '../../../designer/helper/location';
|
||||
import { isNodeSchema, NodeSchema } from '../../../designer/schema';
|
||||
import { ComponentDescription } from '../../../designer/component-type';
|
||||
import { ComponentMetadata } from '../../../designer/component-meta';
|
||||
import { ReactInstance } from 'react';
|
||||
import { setNativeSelection } from '../../../designer/helper/navtive-selection';
|
||||
import cursor from '../../../designer/helper/cursor';
|
||||
@ -332,8 +332,14 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
/**
|
||||
* @see ISimulator
|
||||
*/
|
||||
describeComponent(component: Component): ComponentDescription {
|
||||
throw new Error('Method not implemented.');
|
||||
generateComponentMetadata(componentName: string): ComponentMetadata {
|
||||
const component = this.getComponent(componentName);
|
||||
// TODO:
|
||||
// 1. generate builtin div/p/h1/h2
|
||||
// 2. read propTypes
|
||||
return {
|
||||
componentName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -826,7 +832,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
return this.checkDropTarget(container, dragObject as any);
|
||||
}
|
||||
|
||||
const config = container.componentType;
|
||||
const config = container.componentMeta;
|
||||
|
||||
if (!config.isContainer) {
|
||||
return false;
|
||||
@ -911,7 +917,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
|
||||
checkNestingUp(parent: NodeParent, target: NodeSchema | Node): boolean {
|
||||
if (isNode(target) || isNodeSchema(target)) {
|
||||
const config = isNode(target) ? target.componentType : this.designer.getComponentType(target.componentName);
|
||||
const config = isNode(target) ? target.componentMeta : this.document.getComponentMeta(target.componentName);
|
||||
if (config) {
|
||||
return config.checkNestingUp(target, parent);
|
||||
}
|
||||
@ -921,7 +927,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
}
|
||||
|
||||
checkNestingDown(parent: NodeParent, target: NodeSchema | Node): boolean {
|
||||
const config = parent.componentType;
|
||||
const config = parent.componentMeta;
|
||||
return config.checkNestingDown(parent, target) && this.checkNestingUp(parent, target);
|
||||
}
|
||||
// #endregion
|
||||
|
||||
@ -7,7 +7,7 @@ import { RootSchema, NpmInfo } from '../../../designer/schema';
|
||||
import { getClientRects } from '../../../utils/get-client-rects';
|
||||
import { Asset } from '../utils/asset';
|
||||
import loader from '../utils/loader';
|
||||
import { ComponentDescription } from '../../../designer/component-type';
|
||||
import { ComponentMetadata } from '../../../designer/component-meta';
|
||||
import { reactFindDOMNodes, FIBER_KEY } from '../utils/react-find-dom-nodes';
|
||||
import { isESModule } from '../../../../../utils/is-es-module';
|
||||
import { NodeInstance } from '../../../designer/simulator';
|
||||
@ -39,13 +39,13 @@ export class SimulatorRenderer {
|
||||
|
||||
// sync device
|
||||
});
|
||||
host.componentsConsumer.consume(async (componentsAsset) => {
|
||||
host.componentsConsumer.consume(async componentsAsset => {
|
||||
if (componentsAsset) {
|
||||
await this.load(componentsAsset);
|
||||
this.buildComponents();
|
||||
}
|
||||
});
|
||||
host.injectionConsumer.consume((data) => {
|
||||
host.injectionConsumer.consume(data => {
|
||||
// sync utils, i18n, contants,... config
|
||||
this._appContext = {
|
||||
utils: {},
|
||||
@ -142,7 +142,7 @@ export class SimulatorRenderer {
|
||||
origUnmount = origUnmount.origUnmount;
|
||||
}
|
||||
// hack! delete instance from map
|
||||
const newUnmount = function (this: any) {
|
||||
const newUnmount = function(this: any) {
|
||||
unmountIntance(id, instance);
|
||||
origUnmount && origUnmount.call(this);
|
||||
};
|
||||
@ -204,7 +204,7 @@ export class SimulatorRenderer {
|
||||
cursor.release();
|
||||
}
|
||||
|
||||
private _running: boolean = false;
|
||||
private _running = false;
|
||||
run() {
|
||||
if (this._running) {
|
||||
return;
|
||||
@ -281,15 +281,14 @@ function findComponent(componentName: string, npm?: NpmInfo) {
|
||||
return getSubComponent(library, paths);
|
||||
}
|
||||
|
||||
function buildComponents(componentsMap: { [componentName: string]: ComponentDescription }) {
|
||||
function buildComponents(componentsMap: { [componentName: string]: NpmInfo }) {
|
||||
const components: any = {};
|
||||
Object.keys(componentsMap).forEach(componentName => {
|
||||
components[componentName] = findComponent(componentName, componentsMap[componentName].npm);
|
||||
components[componentName] = findComponent(componentName, componentsMap[componentName]);
|
||||
});
|
||||
return components;
|
||||
}
|
||||
|
||||
|
||||
let REACT_KEY = '';
|
||||
function cacheReactKey(el: Element): Element {
|
||||
if (REACT_KEY !== '') {
|
||||
|
||||
224
packages/designer/src/designer/component-meta.ts
Normal file
224
packages/designer/src/designer/component-meta.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import { ReactNode } from 'react';
|
||||
import Node, { NodeParent } from './document/node/node';
|
||||
import { NodeData, NodeSchema } from './schema';
|
||||
import { PropConfig } from './prop-config';
|
||||
|
||||
export interface NestingRule {
|
||||
childWhitelist?: string[];
|
||||
parentWhitelist?: string[];
|
||||
}
|
||||
|
||||
export interface Configure {
|
||||
props?: any[];
|
||||
styles?: object;
|
||||
events?: object;
|
||||
component?: {
|
||||
isContainer?: boolean;
|
||||
isModal?: boolean;
|
||||
descriptor?: string;
|
||||
nestingRule?: NestingRule;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ComponentMetadata {
|
||||
componentName: string;
|
||||
/**
|
||||
* unique id
|
||||
*/
|
||||
uri?: string;
|
||||
/**
|
||||
* title or description
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* svg icon for component
|
||||
*/
|
||||
icon?: string | ReactNode;
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
docUrl?: string;
|
||||
screenshot?: string;
|
||||
devMode?: 'procode' | 'lowcode';
|
||||
npm?: {
|
||||
package: string;
|
||||
exportName: string;
|
||||
subName: string;
|
||||
main: string;
|
||||
destructuring: boolean;
|
||||
version: string;
|
||||
};
|
||||
props?: PropConfig[];
|
||||
configure?: any[] | Configure;
|
||||
}
|
||||
|
||||
interface TransformedComponentMetadata extends ComponentMetadata {
|
||||
configure?: Configure & {
|
||||
combined?: any[];
|
||||
};
|
||||
}
|
||||
|
||||
function ensureAList(list?: string | string[]): string[] | null {
|
||||
if (!list) {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(list)) {
|
||||
list = list.split(/ *[ ,|] */).filter(Boolean);
|
||||
}
|
||||
if (list.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function npmToURI(npm: {
|
||||
package: string;
|
||||
exportName?: string;
|
||||
subName?: string;
|
||||
destructuring?: boolean;
|
||||
main?: string;
|
||||
version: string;
|
||||
}): string {
|
||||
const pkg = [];
|
||||
if (npm.package) {
|
||||
pkg.push(npm.package);
|
||||
}
|
||||
if (npm.main) {
|
||||
if (npm.main[0] === '/') {
|
||||
pkg.push(npm.main.slice(1));
|
||||
} else if (npm.main.slice(0, 2) === './') {
|
||||
pkg.push(npm.main.slice(2));
|
||||
} else {
|
||||
pkg.push(npm.main);
|
||||
}
|
||||
}
|
||||
|
||||
let uri = pkg.join('/');
|
||||
uri += `:${npm.destructuring && npm.exportName ? npm.exportName : 'default'}`;
|
||||
|
||||
if (npm.subName) {
|
||||
uri += `.${npm.subName}`;
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
export type MetadataTransducer = (prev: ComponentMetadata) => TransformedComponentMetadata;
|
||||
const metadataTransducers: MetadataTransducer[] = [];
|
||||
|
||||
export function registerMetadataTransducer(transducer: MetadataTransducer) {
|
||||
metadataTransducers.push(transducer);
|
||||
}
|
||||
|
||||
export class ComponentMeta {
|
||||
readonly isComponentMeta = true;
|
||||
private _uri?: string;
|
||||
get uri(): string {
|
||||
return this._uri!;
|
||||
}
|
||||
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 {
|
||||
return this._descriptor!;
|
||||
}
|
||||
private _acceptable?: boolean;
|
||||
get acceptable(): boolean {
|
||||
return this._acceptable!;
|
||||
}
|
||||
private _transformedMetadata?: TransformedComponentMetadata;
|
||||
get configure() {
|
||||
const config = this._transformedMetadata?.configure;
|
||||
return config?.combined || config?.props || [];
|
||||
}
|
||||
|
||||
private parentWhitelist?: string[] | null;
|
||||
private childWhitelist?: string[] | null;
|
||||
|
||||
get title() {
|
||||
return this._metadata.title || this.componentName;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this._metadata.icon;
|
||||
}
|
||||
|
||||
constructor(private _metadata: ComponentMetadata) {
|
||||
this.parseMetadata(_metadata);
|
||||
}
|
||||
|
||||
private parseMetadata(metadta: ComponentMetadata) {
|
||||
const { componentName, uri, npm, props } = metadta;
|
||||
this._uri = uri || (npm ? npmToURI(npm) : componentName);
|
||||
this._componentName = componentName;
|
||||
|
||||
metadta.uri = this._uri;
|
||||
// 额外转换逻辑
|
||||
this._transformedMetadata = this.transformMetadata(metadta);
|
||||
|
||||
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;
|
||||
if (component.nestingRule) {
|
||||
const { parentWhitelist, childWhitelist } = component.nestingRule;
|
||||
this.parentWhitelist = ensureAList(parentWhitelist);
|
||||
this.childWhitelist = ensureAList(childWhitelist);
|
||||
}
|
||||
} else {
|
||||
this._isContainer = false;
|
||||
this._isModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
private transformMetadata(metadta: ComponentMetadata): TransformedComponentMetadata {
|
||||
const result = metadataTransducers.reduce((prevMetadata, current) => {
|
||||
return current(prevMetadata);
|
||||
}, metadta);
|
||||
|
||||
if (!result.configure) {
|
||||
result.configure = {};
|
||||
}
|
||||
return result as any;
|
||||
}
|
||||
|
||||
isRootComponent() {
|
||||
return this.componentName === 'Page' || this.componentName === 'Block' || this.componentName === 'Component';
|
||||
}
|
||||
|
||||
set metadata(metadata: ComponentMetadata) {
|
||||
this._metadata = metadata;
|
||||
this.parseMetadata(metadata);
|
||||
}
|
||||
|
||||
get metadata(): ComponentMetadata {
|
||||
return this._metadata;
|
||||
}
|
||||
|
||||
checkNestingUp(my: Node | NodeData, parent: NodeParent) {
|
||||
if (this.parentWhitelist) {
|
||||
return this.parentWhitelist.includes(parent.componentName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
checkNestingDown(my: Node, target: Node | NodeSchema) {
|
||||
if (this.childWhitelist) {
|
||||
return this.childWhitelist.includes(target.componentName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,343 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import Node, { NodeParent } from './document/node/node';
|
||||
import { NodeData, NodeSchema } from './schema';
|
||||
|
||||
export type BasicTypes = 'array' | 'bool' | 'func' | 'number' | 'object' | 'string' | 'node' | 'element' | 'any';
|
||||
export interface CompositeType {
|
||||
type: BasicTypes;
|
||||
isRequired: boolean;
|
||||
}
|
||||
|
||||
// TODO: add complex types
|
||||
|
||||
export interface PropConfig {
|
||||
name: string;
|
||||
propType: BasicTypes | CompositeType;
|
||||
description?: string;
|
||||
defaultValue?: any;
|
||||
}
|
||||
|
||||
export interface NestingRule {
|
||||
childWhitelist?: string[];
|
||||
parentWhitelist?: string[];
|
||||
}
|
||||
|
||||
export interface Configure {
|
||||
props?: any[];
|
||||
styles?: object;
|
||||
events?: object;
|
||||
component?: {
|
||||
isContainer?: boolean;
|
||||
isModal?: boolean;
|
||||
descriptor?: string;
|
||||
nestingRule?: NestingRule;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ComponentDescription {
|
||||
componentName: string;
|
||||
/**
|
||||
* unique id
|
||||
*/
|
||||
uri?: string;
|
||||
/**
|
||||
* title or description
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* svg icon for component
|
||||
*/
|
||||
icon?: string | ReactNode;
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
docUrl?: string;
|
||||
screenshot?: string;
|
||||
devMode?: 'procode' | 'lowcode';
|
||||
npm?: {
|
||||
package: string;
|
||||
exportName: string;
|
||||
subName: string;
|
||||
main: string;
|
||||
destructuring: boolean;
|
||||
version: string;
|
||||
};
|
||||
props?: PropConfig[];
|
||||
configure?: any[] | Configure;
|
||||
}
|
||||
|
||||
function ensureAList(list?: string | string[]): string[] | null {
|
||||
if (!list) {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(list)) {
|
||||
list = list.split(/ *[ ,|] */).filter(Boolean);
|
||||
}
|
||||
if (list.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function npmToURI(npm: {
|
||||
package: string;
|
||||
exportName?: string;
|
||||
subName?: string;
|
||||
destructuring?: boolean;
|
||||
main?: string;
|
||||
version: string;
|
||||
}): string {
|
||||
const pkg = [];
|
||||
if (npm.package) {
|
||||
pkg.push(npm.package);
|
||||
}
|
||||
if (npm.main) {
|
||||
if (npm.main[0] === '/') {
|
||||
pkg.push(npm.main.slice(1));
|
||||
} else if (npm.main.slice(0, 2) === './') {
|
||||
pkg.push(npm.main.slice(2));
|
||||
} else {
|
||||
pkg.push(npm.main);
|
||||
}
|
||||
}
|
||||
|
||||
let uri = pkg.join('/');
|
||||
uri += `:${npm.destructuring && npm.exportName ? npm.exportName : 'default'}`;
|
||||
|
||||
if (npm.subName) {
|
||||
uri += `.${npm.subName}`;
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
function generatePropsConfigure(props: PropConfig[]) {
|
||||
// todo:
|
||||
return [];
|
||||
}
|
||||
|
||||
export class ComponentType {
|
||||
readonly isComponentType = true;
|
||||
private _uri?: string;
|
||||
get uri(): string {
|
||||
return this._uri!;
|
||||
}
|
||||
private _componentName?: string;
|
||||
get componentName(): string {
|
||||
return this._componentName!;
|
||||
}
|
||||
private _isContainer?: boolean;
|
||||
get isContainer(): boolean {
|
||||
return true; // this._isContainer! || this.isRootComponent();
|
||||
}
|
||||
private _isModal?: boolean;
|
||||
get isModal(): boolean {
|
||||
return this._isModal!;
|
||||
}
|
||||
private _descriptor?: string;
|
||||
get descriptor(): string {
|
||||
return this._descriptor!;
|
||||
}
|
||||
private _acceptable?: boolean;
|
||||
get acceptable(): boolean {
|
||||
return this._acceptable!;
|
||||
}
|
||||
private _configure?: Configure;
|
||||
get configure() {
|
||||
return [
|
||||
{
|
||||
name: '#props',
|
||||
title: '属性',
|
||||
items: [
|
||||
{
|
||||
name: 'label',
|
||||
title: '标签',
|
||||
setter: 'StringSetter',
|
||||
},
|
||||
{
|
||||
name: 'data',
|
||||
title: '数据',
|
||||
setter: {
|
||||
componentName: 'ArraySetter',
|
||||
props: {
|
||||
itemConfig: {
|
||||
setter: {
|
||||
componentName: 'ObjectSetter',
|
||||
props: {
|
||||
config: {
|
||||
items: [
|
||||
{
|
||||
name: 'title',
|
||||
title: '名称',
|
||||
setter: 'StringSetter',
|
||||
important: true,
|
||||
},
|
||||
{
|
||||
name: 'records',
|
||||
title: '记录集',
|
||||
setter: {
|
||||
componentName: 'ArraySetter',
|
||||
props: {
|
||||
itemConfig: {
|
||||
setter: {
|
||||
componentName: 'ArraySetter',
|
||||
props: {
|
||||
itemConfig: {
|
||||
setter: 'StringSetter',
|
||||
defaultValue: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultValue: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
important: true,
|
||||
},
|
||||
],
|
||||
extraConfig: {},
|
||||
},
|
||||
// mode: 'popup'
|
||||
},
|
||||
},
|
||||
defaultValue: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
title: '年龄',
|
||||
setter: 'NumberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '#styles',
|
||||
title: '样式',
|
||||
items: [
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名绑定',
|
||||
setter: 'ClassNameSetter',
|
||||
},
|
||||
{
|
||||
name: 'className2',
|
||||
title: '类名绑定',
|
||||
setter: 'StringSetter',
|
||||
},
|
||||
{
|
||||
name: '#inlineStyles',
|
||||
title: '行内样式',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '#events',
|
||||
title: '事件',
|
||||
items: [
|
||||
{
|
||||
name: '!events',
|
||||
title: '事件绑定',
|
||||
setter: {
|
||||
componentName: 'EventsSetter',
|
||||
},
|
||||
extraProps: {
|
||||
getValue(field: any) {
|
||||
console.info('lifeCycles', field.getExtraPropValue('lifeCycles'));
|
||||
return field.getPropValue('xxx');
|
||||
},
|
||||
setValue(field: any, val: any) {
|
||||
field.setExtraPropValue('lifeCycles', val);
|
||||
field.setPropValue('xxx', val);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '#data',
|
||||
title: '数据',
|
||||
items: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private parentWhitelist?: string[] | null;
|
||||
private childWhitelist?: string[] | null;
|
||||
|
||||
get title() {
|
||||
return this._spec.title || this.componentName;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this._spec.icon;
|
||||
}
|
||||
|
||||
constructor(private _spec: ComponentDescription) {
|
||||
this.parseSpec(_spec);
|
||||
}
|
||||
|
||||
private parseSpec(spec: ComponentDescription) {
|
||||
const { componentName, uri, configure, npm, props } = spec;
|
||||
this._uri = uri || (npm ? npmToURI(npm) : componentName);
|
||||
this._componentName = componentName;
|
||||
this._acceptable = false;
|
||||
|
||||
if (!configure || Array.isArray(configure)) {
|
||||
this._configure = {
|
||||
props: !configure ? [] : configure,
|
||||
styles: {
|
||||
supportClassName: true,
|
||||
supportInlineStyle: true,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
this._configure = configure;
|
||||
}
|
||||
if (!this._configure.props) {
|
||||
this._configure.props = props ? generatePropsConfigure(props) : [];
|
||||
}
|
||||
const { component } = this._configure;
|
||||
if (component) {
|
||||
this._isContainer = component.isContainer ? true : false;
|
||||
this._isModal = component.isModal ? true : false;
|
||||
this._descriptor = component.descriptor;
|
||||
if (component.nestingRule) {
|
||||
const { parentWhitelist, childWhitelist } = component.nestingRule;
|
||||
this.parentWhitelist = ensureAList(parentWhitelist);
|
||||
this.childWhitelist = ensureAList(childWhitelist);
|
||||
}
|
||||
} else {
|
||||
this._isContainer = false;
|
||||
this._isModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
isRootComponent() {
|
||||
return this.componentName === 'Page' || this.componentName === 'Block' || this.componentName === 'Component';
|
||||
}
|
||||
|
||||
set spec(spec: ComponentDescription) {
|
||||
this._spec = spec;
|
||||
this.parseSpec(spec);
|
||||
}
|
||||
|
||||
get spec(): ComponentDescription {
|
||||
return this._spec;
|
||||
}
|
||||
|
||||
checkNestingUp(my: Node | NodeData, parent: NodeParent) {
|
||||
if (this.parentWhitelist) {
|
||||
return this.parentWhitelist.includes(parent.componentName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
checkNestingDown(my: Node, target: Node | NodeSchema) {
|
||||
if (this.childWhitelist) {
|
||||
return this.childWhitelist.includes(target.componentName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { ComponentType as ReactComponentType } from 'react';
|
||||
import { obx, computed, autorun } from '@recore/obx';
|
||||
import BuiltinSimulatorView from '../builtins/simulator';
|
||||
import Project from './project';
|
||||
import { ProjectSchema } from './schema';
|
||||
import { ProjectSchema, NpmInfo } from './schema';
|
||||
import Dragon, { isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './helper/dragon';
|
||||
import ActiveTracker from './helper/active-tracker';
|
||||
import Hovering from './helper/hovering';
|
||||
@ -10,7 +10,7 @@ import Location, { LocationData, isLocationChildrenDetail } from './helper/locat
|
||||
import DocumentModel from './document/document-model';
|
||||
import Node, { insertChildren } from './document/node/node';
|
||||
import { isRootNode } from './document/node/root-node';
|
||||
import { ComponentDescription, ComponentType } from './component-type';
|
||||
import { ComponentMetadata, ComponentMeta } from './component-meta';
|
||||
import Scroller, { IScrollable } from './helper/scroller';
|
||||
import { INodeSelector } from './simulator';
|
||||
import OffsetObserver, { createOffsetObserver } from './helper/offset-observer';
|
||||
@ -25,7 +25,7 @@ export interface DesignerProps {
|
||||
simulatorComponent?: ReactComponentType<any>;
|
||||
dragGhostComponent?: ReactComponentType<any>;
|
||||
suspensed?: boolean;
|
||||
componentsDescription?: ComponentDescription[];
|
||||
componentsDescription?: ComponentMetadata[];
|
||||
eventPipe?: EventEmitter;
|
||||
onMount?: (designer: Designer) => void;
|
||||
onDragstart?: (e: LocateEvent) => void;
|
||||
@ -222,7 +222,7 @@ export default class Designer {
|
||||
this.suspensed = props.suspensed;
|
||||
}
|
||||
if (props.componentsDescription !== this.props.componentsDescription && props.componentsDescription != null) {
|
||||
this.buildComponentTypesMap(props.componentsDescription);
|
||||
this.buildComponentMetasMap(props.componentsDescription);
|
||||
}
|
||||
} else {
|
||||
// init hotkeys
|
||||
@ -239,7 +239,7 @@ export default class Designer {
|
||||
this.suspensed = props.suspensed;
|
||||
}
|
||||
if (props.componentsDescription != null) {
|
||||
this.buildComponentTypesMap(props.componentsDescription);
|
||||
this.buildComponentMetasMap(props.componentsDescription);
|
||||
}
|
||||
}
|
||||
this.props = props;
|
||||
@ -283,52 +283,53 @@ export default class Designer {
|
||||
// todo:
|
||||
}
|
||||
|
||||
@obx.val private _componentTypesMap = new Map<string, ComponentType>();
|
||||
private _lostComponentTypesMap = new Map<string, ComponentType>();
|
||||
@obx.val private _componentMetasMap = new Map<string, ComponentMeta>();
|
||||
private _lostComponentMetasMap = new Map<string, ComponentMeta>();
|
||||
|
||||
private buildComponentTypesMap(specs: ComponentDescription[]) {
|
||||
specs.forEach(spec => {
|
||||
const key = spec.componentName;
|
||||
let cType = this._componentTypesMap.get(key);
|
||||
if (cType) {
|
||||
cType.spec = spec;
|
||||
private buildComponentMetasMap(metas: ComponentMetadata[]) {
|
||||
metas.forEach(data => {
|
||||
const key = data.componentName;
|
||||
let meta = this._componentMetasMap.get(key);
|
||||
if (meta) {
|
||||
meta.metadata = data;
|
||||
} else {
|
||||
cType = this._lostComponentTypesMap.get(key);
|
||||
meta = this._lostComponentMetasMap.get(key);
|
||||
|
||||
if (cType) {
|
||||
cType.spec = spec;
|
||||
this._lostComponentTypesMap.delete(key);
|
||||
if (meta) {
|
||||
meta.metadata = data;
|
||||
this._lostComponentMetasMap.delete(key);
|
||||
} else {
|
||||
cType = new ComponentType(spec);
|
||||
meta = new ComponentMeta(data);
|
||||
}
|
||||
|
||||
this._componentTypesMap.set(key, cType);
|
||||
this._componentMetasMap.set(key, meta);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getComponentType(componentName: string): ComponentType {
|
||||
if (this._componentTypesMap.has(componentName)) {
|
||||
return this._componentTypesMap.get(componentName)!;
|
||||
getComponentMeta(componentName: string, generateMetadata?: () => ComponentMetadata | null): ComponentMeta {
|
||||
if (this._componentMetasMap.has(componentName)) {
|
||||
return this._componentMetasMap.get(componentName)!;
|
||||
}
|
||||
|
||||
if (this._lostComponentTypesMap.has(componentName)) {
|
||||
return this._lostComponentTypesMap.get(componentName)!;
|
||||
if (this._lostComponentMetasMap.has(componentName)) {
|
||||
return this._lostComponentMetasMap.get(componentName)!;
|
||||
}
|
||||
|
||||
const cType = new ComponentType({
|
||||
const meta = new ComponentMeta({
|
||||
componentName,
|
||||
...(generateMetadata ? generateMetadata() : null),
|
||||
});
|
||||
|
||||
this._lostComponentTypesMap.set(componentName, cType);
|
||||
this._lostComponentMetasMap.set(componentName, meta);
|
||||
|
||||
return cType;
|
||||
return meta;
|
||||
}
|
||||
|
||||
get componentsMap(): { [key: string]: ComponentDescription } {
|
||||
get componentsMap(): { [key: string]: NpmInfo } {
|
||||
const maps: any = {};
|
||||
this._componentTypesMap.forEach((config, key) => {
|
||||
maps[key] = config.spec;
|
||||
this._componentMetasMap.forEach((config, key) => {
|
||||
maps[key] = config.metadata.npm;
|
||||
});
|
||||
return maps;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import RootNode from './node/root-node';
|
||||
import { ISimulator, Component } from '../simulator';
|
||||
import { computed, obx, autorun } from '@recore/obx';
|
||||
import Location from '../helper/location';
|
||||
import { ComponentType } from '../component-type';
|
||||
import { ComponentMeta } from '../component-meta';
|
||||
import History from '../helper/history';
|
||||
import Prop from './node/props/prop';
|
||||
|
||||
@ -41,7 +41,7 @@ export default class DocumentModel {
|
||||
}
|
||||
|
||||
get fileName(): string {
|
||||
return (this.rootNode.getExtraProp('fileName')?.getAsString()) || this.id;
|
||||
return this.rootNode.getExtraProp('fileName')?.getAsString() || this.id;
|
||||
}
|
||||
|
||||
set fileName(fileName: string) {
|
||||
@ -60,7 +60,7 @@ export default class DocumentModel {
|
||||
this.id = this.rootNode.id;
|
||||
this.history = new History(
|
||||
() => this.schema,
|
||||
(schema) => this.import(schema as RootSchema, true),
|
||||
schema => this.import(schema as RootSchema, true),
|
||||
);
|
||||
this.setupListenActiveNodes();
|
||||
}
|
||||
@ -237,7 +237,7 @@ export default class DocumentModel {
|
||||
return this.rootNode.schema as any;
|
||||
}
|
||||
|
||||
import(schema: RootSchema, checkId: boolean = false) {
|
||||
import(schema: RootSchema, checkId = false) {
|
||||
this.rootNode.import(schema, checkId);
|
||||
// todo: purge something
|
||||
// todo: select added and active track added
|
||||
@ -285,13 +285,15 @@ export default class DocumentModel {
|
||||
return this.simulator!.getComponent(componentName);
|
||||
}
|
||||
|
||||
getComponentType(componentName: string, component?: Component | null): ComponentType {
|
||||
// TODO: guess componentConfig from component by simulator
|
||||
return this.designer.getComponentType(componentName);
|
||||
getComponentMeta(componentName: string): ComponentMeta {
|
||||
return this.designer.getComponentMeta(
|
||||
componentName,
|
||||
() => this.simulator?.generateComponentMetadata(componentName) || null,
|
||||
);
|
||||
}
|
||||
|
||||
@obx.ref private _opened: boolean = false;
|
||||
@obx.ref private _suspensed: boolean = false;
|
||||
@obx.ref private _opened = false;
|
||||
@obx.ref private _suspensed = false;
|
||||
|
||||
/**
|
||||
* 是否不是激活的
|
||||
|
||||
@ -6,7 +6,7 @@ import NodeChildren from './node-children';
|
||||
import Prop from './props/prop';
|
||||
import NodeContent from './node-content';
|
||||
import { Component } from '../../simulator';
|
||||
import { ComponentType } from '../../component-type';
|
||||
import { ComponentMeta } from '../../component-meta';
|
||||
|
||||
/**
|
||||
* 基础节点
|
||||
@ -78,8 +78,8 @@ export default class Node {
|
||||
|
||||
@computed get title(): string {
|
||||
let t = this.getExtraProp('title');
|
||||
if (!t && this.componentType.descriptor) {
|
||||
t = this.getProp(this.componentType.descriptor, false);
|
||||
if (!t && this.componentMeta.descriptor) {
|
||||
t = this.getProp(this.componentMeta.descriptor, false);
|
||||
}
|
||||
if (t) {
|
||||
const v = t.getAsString();
|
||||
@ -87,7 +87,7 @@ export default class Node {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return this.componentType.title;
|
||||
return this.componentMeta.title;
|
||||
}
|
||||
|
||||
get isSlotRoot(): boolean {
|
||||
@ -157,7 +157,7 @@ export default class Node {
|
||||
/**
|
||||
* 悬停高亮
|
||||
*/
|
||||
hover(flag: boolean = true) {
|
||||
hover(flag = true) {
|
||||
if (flag) {
|
||||
this.document.designer.hovering.hover(this);
|
||||
} else {
|
||||
@ -168,18 +168,18 @@ export default class Node {
|
||||
/**
|
||||
* 节点组件类
|
||||
*/
|
||||
@obx.ref get component(): Component | null {
|
||||
@obx.ref get component(): Component {
|
||||
if (this.isNodeParent) {
|
||||
return this.document.getComponent(this.componentName);
|
||||
return this.document.getComponent(this.componentName) || this.componentName;
|
||||
}
|
||||
return null;
|
||||
return this.componentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点组件描述
|
||||
*/
|
||||
@computed get componentType(): ComponentType {
|
||||
return this.document.getComponentType(this.componentName, this.component);
|
||||
@computed get componentMeta(): ComponentMeta {
|
||||
return this.document.getComponentMeta(this.componentName);
|
||||
}
|
||||
|
||||
@computed get propsData(): PropsMap | PropsList | null {
|
||||
@ -223,19 +223,19 @@ export default class Node {
|
||||
}
|
||||
|
||||
wrapWith(schema: NodeSchema) {
|
||||
|
||||
// todo
|
||||
}
|
||||
|
||||
replaceWith(schema: NodeSchema, migrate: boolean = true) {
|
||||
replaceWith(schema: NodeSchema, migrate = true) {
|
||||
// reuse the same id? or replaceSelection
|
||||
//
|
||||
}
|
||||
|
||||
getProp(path: string, useStash: boolean = true): Prop | null {
|
||||
getProp(path: string, useStash = true): Prop | null {
|
||||
return this.props?.query(path, useStash as any) || null;
|
||||
}
|
||||
|
||||
getExtraProp(key: string, useStash: boolean = true): Prop | null {
|
||||
getExtraProp(key: string, useStash = true): Prop | null {
|
||||
return this.props?.get(EXTRA_KEY_PREFIX + key, useStash) || null;
|
||||
}
|
||||
|
||||
@ -316,7 +316,7 @@ export default class Node {
|
||||
this.import(data);
|
||||
}
|
||||
|
||||
import(data: NodeSchema, checkId: boolean = false) {
|
||||
import(data: NodeSchema, checkId = false) {
|
||||
const { componentName, id, children, props, ...extras } = data;
|
||||
|
||||
if (isNodeParent(this)) {
|
||||
@ -514,4 +514,3 @@ export function insertChildren(
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
46
packages/designer/src/designer/prop-config.ts
Normal file
46
packages/designer/src/designer/prop-config.ts
Normal file
@ -0,0 +1,46 @@
|
||||
export type PropType = BasicType | RequiredType | ComplexType;
|
||||
export type BasicType = 'array' | 'bool' | 'func' | 'number' | 'object' | 'string' | 'node' | 'element' | 'any';
|
||||
export type ComplexType = OneOf | OneOfType | ArrayOf | ObjectOf | Shape | Exact;
|
||||
|
||||
export interface RequiredType {
|
||||
type: BasicType;
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
export interface OneOf {
|
||||
type: 'oneOf';
|
||||
value: string[];
|
||||
isRequired?: boolean;
|
||||
}
|
||||
export interface OneOfType {
|
||||
type: 'oneOfType';
|
||||
value: PropType[];
|
||||
isRequired?: boolean;
|
||||
}
|
||||
export interface ArrayOf {
|
||||
type: 'arrayOf';
|
||||
value: PropType;
|
||||
isRequired?: boolean;
|
||||
}
|
||||
export interface ObjectOf {
|
||||
type: 'objectOf';
|
||||
value: PropType;
|
||||
isRequired?: boolean;
|
||||
}
|
||||
export interface Shape {
|
||||
type: 'shape';
|
||||
value: PropConfig[];
|
||||
isRequired?: boolean;
|
||||
}
|
||||
export interface Exact {
|
||||
type: 'exact';
|
||||
value: PropConfig[];
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
export interface PropConfig {
|
||||
name: string;
|
||||
propType: PropType;
|
||||
description?: string;
|
||||
defaultValue?: any;
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { LocateEvent, ISensor } from './helper/dragon';
|
||||
import { Point } from './helper/location';
|
||||
import Node from './document/node/node';
|
||||
import { ScrollTarget, IScrollable } from './helper/scroller';
|
||||
import { ComponentDescription } from './component-type';
|
||||
import { ComponentMetadata } from './component-meta';
|
||||
|
||||
export type AutoFit = '100%';
|
||||
export const AutoFit = '100%';
|
||||
@ -85,7 +85,6 @@ export interface ISimulator<P = object> extends ISensor {
|
||||
// 获取区块代码, 通过 components 传递,可异步获取
|
||||
setProps(props: P): void;
|
||||
|
||||
|
||||
setSuspense(suspensed: boolean): void;
|
||||
|
||||
// #region ========= drag and drop helpers =============
|
||||
@ -117,7 +116,7 @@ export interface ISimulator<P = object> extends ISensor {
|
||||
/**
|
||||
* 描述组件
|
||||
*/
|
||||
describeComponent(component: Component): ComponentDescription;
|
||||
generateComponentMetadata(componentName: string): ComponentMetadata;
|
||||
/**
|
||||
* 根据组件信息获取组件类
|
||||
*/
|
||||
@ -158,7 +157,7 @@ export interface NodeInstance<T = ComponentInstance> {
|
||||
/**
|
||||
* 组件类定义
|
||||
*/
|
||||
export type Component = ComponentType<any> | object;
|
||||
export type Component = ComponentType<any> | object | string;
|
||||
|
||||
/**
|
||||
* 组件实例定义
|
||||
|
||||
@ -16,11 +16,8 @@ interface ArraySetterState {
|
||||
interface ArraySetterProps {
|
||||
value: any[];
|
||||
field: SettingField;
|
||||
itemConfig?: {
|
||||
setter?: SetterType;
|
||||
defaultValue?: any | ((field: SettingField) => any);
|
||||
required?: boolean;
|
||||
};
|
||||
itemSetter?: SetterType;
|
||||
columns?: FieldConfig[];
|
||||
multiValue?: boolean;
|
||||
}
|
||||
|
||||
@ -45,8 +42,8 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
|
||||
if (newLength > originLength) {
|
||||
for (let i = originLength; i < newLength; i++) {
|
||||
const item = field.createField({
|
||||
...props.itemConfig,
|
||||
name: i,
|
||||
setter: props.itemSetter,
|
||||
forceInline: 2,
|
||||
});
|
||||
items[i] = item;
|
||||
@ -86,16 +83,16 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
|
||||
private scrollToLast: boolean = false;
|
||||
onAdd() {
|
||||
const { items, itemsMap } = this.state;
|
||||
const { itemConfig } = this.props;
|
||||
const defaultValue = itemConfig ? itemConfig.defaultValue : null;
|
||||
const { itemSetter } = this.props;
|
||||
const initialValue = typeof itemSetter === 'object' ? (itemSetter as any).initialValue : null;
|
||||
const item = this.props.field.createField({
|
||||
...itemConfig,
|
||||
name: items.length,
|
||||
forceInline: 1,
|
||||
setter: itemSetter,
|
||||
forceInline: 2,
|
||||
});
|
||||
items.push(item);
|
||||
itemsMap.set(item.id, item);
|
||||
item.setValue(typeof defaultValue === 'function' ? defaultValue(item) : defaultValue);
|
||||
item.setValue(typeof initialValue === 'function' ? initialValue(item) : initialValue);
|
||||
this.scrollToLast = true;
|
||||
this.setState({
|
||||
items: items.slice(),
|
||||
@ -132,13 +129,11 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
|
||||
}
|
||||
|
||||
render() {
|
||||
// mini Button: depends popup
|
||||
if (this.props.itemConfig) {
|
||||
// check is ObjectSetter then check if show columns
|
||||
let columns: any = null;
|
||||
if (this.props.columns) {
|
||||
columns = this.props.columns.map(column => <Title title={column.title || (column.name as string)} />);
|
||||
}
|
||||
|
||||
console.info(this.state.items);
|
||||
|
||||
const { items } = this.state;
|
||||
const scrollToLast = this.scrollToLast;
|
||||
this.scrollToLast = false;
|
||||
@ -172,6 +167,7 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
|
||||
<span>添加</span>
|
||||
</Button>
|
||||
</div>*/}
|
||||
{columns && <div className="lc-setter-list-columns">{columns}</div>}
|
||||
{content}
|
||||
<Button className="lc-setter-list-add" type="primary" onClick={this.onAdd.bind(this)}>
|
||||
<Icon type="add" />
|
||||
@ -226,7 +222,6 @@ export default class ArraySetter extends Component<{
|
||||
itemConfig?: {
|
||||
setter?: SetterType;
|
||||
defaultValue?: any | ((field: SettingField) => any);
|
||||
required?: boolean;
|
||||
};
|
||||
mode?: 'popup' | 'list';
|
||||
forceInline?: boolean;
|
||||
@ -237,6 +232,20 @@ export default class ArraySetter extends Component<{
|
||||
render() {
|
||||
const { mode, forceInline, ...props } = this.props;
|
||||
const { field, itemConfig } = props;
|
||||
let columns: FieldConfig[] | undefined;
|
||||
const setter: any = itemConfig?.setter;
|
||||
if (setter?.componentName === 'ObjectSetter') {
|
||||
const items: FieldConfig[] = setter.props?.config?.items;
|
||||
if (items && Array.isArray(items)) {
|
||||
columns = items.filter(item => item.isRequired || item.important);
|
||||
if (columns.length === 3) {
|
||||
columns = columns.slice(0, 3);
|
||||
} else if (columns.length > 3) {
|
||||
columns = columns.slice(0, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'popup' || forceInline) {
|
||||
const title = (
|
||||
<Fragment>
|
||||
@ -246,26 +255,22 @@ export default class ArraySetter extends Component<{
|
||||
);
|
||||
if (!this.pipe) {
|
||||
let width = 360;
|
||||
const setter: any = itemConfig?.setter;
|
||||
if (setter?.componentName === 'ObjectSetter') {
|
||||
const items: FieldConfig[] = setter.props?.config?.items;
|
||||
if (items && Array.isArray(items)) {
|
||||
const length = items.filter(item => item.required || item.important).length;
|
||||
if (length === 3) {
|
||||
width = 480;
|
||||
} else if (length > 3) {
|
||||
width = 600;
|
||||
}
|
||||
if (columns) {
|
||||
if (columns.length === 3) {
|
||||
width = 480;
|
||||
} else if (columns.length > 3) {
|
||||
width = 600;
|
||||
}
|
||||
}
|
||||
this.pipe = (this.context as PopupPipe).create({ width });
|
||||
}
|
||||
this.pipe.send(
|
||||
<TableSetter key={field.id} {...props} />,
|
||||
<TableSetter key={field.id} {...props} columns={columns} />,
|
||||
title,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
type={forceInline ? 'normal' : 'primary'}
|
||||
onClick={e => {
|
||||
this.pipe.show((e as any).target, field.id);
|
||||
}}
|
||||
@ -275,7 +280,7 @@ export default class ArraySetter extends Component<{
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return <ListSetter {...props} />;
|
||||
return <ListSetter {...props} columns={columns?.slice(0, 2)} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,18 @@
|
||||
margin-top: 8px;;
|
||||
}
|
||||
|
||||
|
||||
.lc-setter-list-columns {
|
||||
display: flex;
|
||||
> .lc-title {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
margin-left: 47px;
|
||||
margin-right: 28px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.lc-setter-list-scroll-body {
|
||||
margin: -8px -5px;
|
||||
padding: 8px 10px;
|
||||
|
||||
@ -34,7 +34,6 @@ interface ObjectSetterConfig {
|
||||
items?: FieldConfig[];
|
||||
extraConfig?: {
|
||||
setter?: SetterType;
|
||||
defaultValue?: any | ((field: SettingField, editor: any) => any);
|
||||
};
|
||||
}
|
||||
|
||||
@ -62,7 +61,7 @@ class RowSetter extends Component<RowSetterProps> {
|
||||
const l = Math.min(config.items.length, columns);
|
||||
for (let i = 0; i < l; i++) {
|
||||
const conf = config.items[i];
|
||||
if (conf.required || conf.important) {
|
||||
if (conf.isRequired || conf.important) {
|
||||
const item = field.createField({
|
||||
...conf,
|
||||
// in column-cell
|
||||
|
||||
@ -7,6 +7,7 @@ import SettingsPane, { registerSetter, createSetterContent, getSetter, createSet
|
||||
import Node from '../../designer/src/designer/document/node/node';
|
||||
import ArraySetter from './builtin-setters/array-setter';
|
||||
import ObjectSetter from './builtin-setters/object-setter';
|
||||
import './register-transducer';
|
||||
|
||||
export default class SettingsMainView extends Component {
|
||||
private main: SettingsMain;
|
||||
@ -31,9 +32,9 @@ export default class SettingsMainView extends Component {
|
||||
if (this.main.isMulti) {
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{this.main.componentType!.icon || <Icon type="ellipsis" size="small" />}
|
||||
{this.main.componentMeta!.icon || <Icon type="ellipsis" size="small" />}
|
||||
<span>
|
||||
{this.main.componentType!.title} x {this.main.nodes.length}
|
||||
{this.main.componentMeta!.title} x {this.main.nodes.length}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@ -57,7 +58,7 @@ export default class SettingsMainView extends Component {
|
||||
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{this.main.componentType!.icon || <Icon type="ellipsis" size="small" />}
|
||||
{this.main.componentMeta!.icon || <Icon type="ellipsis" size="small" />}
|
||||
<Breadcrumb className="lc-settings-node-breadcrumb">{items}</Breadcrumb>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { uniqueId } from '../../utils/unique-id';
|
||||
import { ComponentType } from '../../designer/src/designer/component-type';
|
||||
import { ComponentMeta } from '../../designer/src/designer/component-meta';
|
||||
import Node from '../../designer/src/designer/document/node/node';
|
||||
import { TitleContent } from './title';
|
||||
import { ReactElement, ComponentType as ReactComponentType, isValidElement } from 'react';
|
||||
@ -12,7 +12,7 @@ export interface SettingTarget {
|
||||
// 所设置的节点集,至少一个
|
||||
readonly nodes: Node[];
|
||||
|
||||
readonly componentType: ComponentType | null;
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
|
||||
readonly items: Array<SettingField | CustomView>;
|
||||
|
||||
@ -92,6 +92,8 @@ export interface SetterConfig {
|
||||
*/
|
||||
props?: object | DynamicProps;
|
||||
children?: any;
|
||||
isRequired?: boolean;
|
||||
initialValue?: any | ((field: SettingField) => any);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,7 +105,7 @@ export interface FieldExtraProps {
|
||||
/**
|
||||
* 是否必填参数
|
||||
*/
|
||||
required?: boolean;
|
||||
isRequired?: boolean;
|
||||
/**
|
||||
* default value of target prop for setter use
|
||||
*/
|
||||
@ -172,7 +174,7 @@ export class SettingField implements SettingTarget {
|
||||
readonly isOne: boolean;
|
||||
readonly isNone: boolean;
|
||||
readonly nodes: Node[];
|
||||
readonly componentType: ComponentType | null;
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
readonly designer: Designer;
|
||||
readonly top: SettingTarget;
|
||||
get path() {
|
||||
@ -212,7 +214,7 @@ export class SettingField implements SettingTarget {
|
||||
// copy parent properties
|
||||
this.editor = parent.editor;
|
||||
this.nodes = parent.nodes;
|
||||
this.componentType = parent.componentType;
|
||||
this.componentMeta = parent.componentMeta;
|
||||
this.isSame = parent.isSame;
|
||||
this.isMulti = parent.isMulti;
|
||||
this.isOne = parent.isOne;
|
||||
@ -369,7 +371,7 @@ export class SettingsMain implements SettingTarget {
|
||||
private _nodes: Node[] = [];
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
private _sessionId = '';
|
||||
private _componentType: ComponentType | null = null;
|
||||
private _componentMeta: ComponentMeta | null = null;
|
||||
private _isSame: boolean = true;
|
||||
readonly path = [];
|
||||
readonly top: SettingTarget = this;
|
||||
@ -378,8 +380,8 @@ export class SettingsMain implements SettingTarget {
|
||||
return this._nodes;
|
||||
}
|
||||
|
||||
get componentType() {
|
||||
return this._componentType;
|
||||
get componentMeta() {
|
||||
return this._componentMeta;
|
||||
}
|
||||
|
||||
get items() {
|
||||
@ -506,7 +508,7 @@ export class SettingsMain implements SettingTarget {
|
||||
this._sessionId = sessionId;
|
||||
|
||||
// setups
|
||||
this.setupComponentType();
|
||||
this.setupComponentMeta();
|
||||
|
||||
// todo: enhance when componentType not changed do merge
|
||||
// clear fields
|
||||
@ -521,36 +523,36 @@ export class SettingsMain implements SettingTarget {
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
private setupComponentType() {
|
||||
private setupComponentMeta() {
|
||||
if (this.nodes.length < 1) {
|
||||
this._isSame = false;
|
||||
this._componentType = null;
|
||||
this._componentMeta = null;
|
||||
return;
|
||||
}
|
||||
const first = this.nodes[0];
|
||||
const type = first.componentType;
|
||||
const meta = first.componentMeta;
|
||||
const l = this.nodes.length;
|
||||
let theSame = true;
|
||||
for (let i = 1; i < l; i++) {
|
||||
const other = this.nodes[i];
|
||||
if ((other as any).componentType !== type) {
|
||||
if ((other as any).componentType !== meta) {
|
||||
theSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (theSame) {
|
||||
this._isSame = true;
|
||||
this._componentType = type;
|
||||
this._componentMeta = meta;
|
||||
} else {
|
||||
this._isSame = false;
|
||||
this._componentType = null;
|
||||
this._componentMeta = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setupItems() {
|
||||
this.disposeItems();
|
||||
if (this.componentType) {
|
||||
this._items = this.componentType.configure.map(item => {
|
||||
if (this.componentMeta) {
|
||||
this._items = this.componentMeta.configure.map(item => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
459
packages/plugin-settings/src/register-transducer.ts
Normal file
459
packages/plugin-settings/src/register-transducer.ts
Normal file
@ -0,0 +1,459 @@
|
||||
import {
|
||||
PropConfig,
|
||||
PropType,
|
||||
Shape,
|
||||
OneOf,
|
||||
ObjectOf,
|
||||
ArrayOf,
|
||||
OneOfType,
|
||||
} from '../../designer/src/designer/prop-config';
|
||||
import { SetterType, FieldConfig, SettingField } from './main';
|
||||
import { registerMetadataTransducer } from '../../designer/src/designer/component-meta';
|
||||
|
||||
export function propConfigToFieldConfig(propConfig: PropConfig): FieldConfig {
|
||||
return {
|
||||
...propConfig,
|
||||
setter: propTypeToSetter(propConfig.propType),
|
||||
};
|
||||
}
|
||||
|
||||
export function propTypeToSetter(propType: PropType): SetterType {
|
||||
let typeName: string;
|
||||
let isRequired: boolean | undefined = false;
|
||||
if (typeof propType === 'string') {
|
||||
typeName = propType;
|
||||
} else {
|
||||
typeName = propType.type;
|
||||
isRequired = propType.isRequired;
|
||||
}
|
||||
// TODO: use mixinSetter wrapper
|
||||
switch (typeName) {
|
||||
case 'string':
|
||||
return {
|
||||
componentName: 'StringSetter',
|
||||
isRequired,
|
||||
initialValue: '',
|
||||
};
|
||||
|
||||
case 'number':
|
||||
return {
|
||||
componentName: 'NumberSetter',
|
||||
isRequired,
|
||||
initialValue: 0,
|
||||
};
|
||||
case 'bool':
|
||||
return {
|
||||
componentName: 'NumberSetter',
|
||||
isRequired,
|
||||
initialValue: false,
|
||||
};
|
||||
case 'oneOf':
|
||||
const dataSource = ((propType as OneOf).value || []).map((value, index) => {
|
||||
const t = typeof value;
|
||||
return {
|
||||
label: t === 'string' || t === 'number' || t === 'boolean' ? String(value) : `value ${index}`,
|
||||
value,
|
||||
};
|
||||
});
|
||||
const componentName = dataSource.length > 4 ? 'SelectSetter' : 'RadioSetter';
|
||||
return {
|
||||
componentName,
|
||||
props: { dataSource },
|
||||
isRequired,
|
||||
initialValue: dataSource[0] ? dataSource[0].value : null,
|
||||
};
|
||||
|
||||
case 'element':
|
||||
case 'node':
|
||||
return {
|
||||
// slotSetter
|
||||
componentName: 'NodeSetter',
|
||||
props: {
|
||||
mode: typeName,
|
||||
},
|
||||
isRequired,
|
||||
initialValue: {
|
||||
type: 'JSSlot',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
case 'shape':
|
||||
case 'exact':
|
||||
const items = (propType as Shape).value.map(item => propConfigToFieldConfig(item));
|
||||
return {
|
||||
componentName: 'ObjectSetter',
|
||||
props: {
|
||||
config: {
|
||||
items,
|
||||
extraSetter: typeName === 'shape' ? propTypeToSetter('any') : null,
|
||||
},
|
||||
},
|
||||
isRequired,
|
||||
initialValue: (field: any) => {
|
||||
const data: any = {};
|
||||
items.forEach(item => {
|
||||
let initial = item.defaultValue;
|
||||
if (initial == null && item.setter && typeof item.setter === 'object') {
|
||||
initial = (item.setter as any).initialValue;
|
||||
}
|
||||
data[item.name] = initial ? (typeof initial === 'function' ? initial(field) : initial) : null;
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
case 'object':
|
||||
case 'objectOf':
|
||||
return {
|
||||
componentName: 'ObjectSetter',
|
||||
props: {
|
||||
config: {
|
||||
extraSetter: propTypeToSetter(typeName === 'objectOf' ? (propType as ObjectOf).value : 'any'),
|
||||
},
|
||||
},
|
||||
isRequired,
|
||||
};
|
||||
case 'array':
|
||||
case 'arrayOf':
|
||||
return {
|
||||
componentName: 'ArraySetter',
|
||||
props: {
|
||||
itemSetter: propTypeToSetter(typeName === 'arrayOf' ? (propType as ArrayOf).value : 'any'),
|
||||
},
|
||||
isRequired,
|
||||
initialValue: [],
|
||||
};
|
||||
case 'func':
|
||||
return {
|
||||
componentName: 'FunctionSetter',
|
||||
isRequired,
|
||||
initialValue: {
|
||||
type: 'JSFunction',
|
||||
value: 'function(){}',
|
||||
},
|
||||
};
|
||||
case 'oneOfType':
|
||||
return {
|
||||
componentName: 'MixinSetter',
|
||||
props: {
|
||||
setters: (propType as OneOfType).value.map(item => propTypeToSetter(item)),
|
||||
},
|
||||
isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
componentName: 'MixinSetter',
|
||||
isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
const EVENT_RE = /^on[A-Z][\w]*$/;
|
||||
|
||||
registerMetadataTransducer(metadata => {
|
||||
if (metadata.configure) {
|
||||
if (Array.isArray(metadata.configure)) {
|
||||
return {
|
||||
...metadata,
|
||||
configure: {
|
||||
props: metadata.configure,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (metadata.configure.props) {
|
||||
return metadata as any;
|
||||
}
|
||||
}
|
||||
if (!metadata.props) {
|
||||
return {
|
||||
...metadata,
|
||||
configure: {
|
||||
props: metadata.configure && Array.isArray(metadata.configure) ? metadata.configure : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { configure = {} } = metadata;
|
||||
const { props = [], component = {}, events = {}, styles = {} } = configure;
|
||||
const supportEvents: string[] | null = (events as any).supportEvents ? null : [];
|
||||
|
||||
metadata.props.forEach(prop => {
|
||||
const { name, propType } = prop;
|
||||
if (name === 'children' && (component.isContainer || (propType === 'node' || propType === 'element' || propType === 'any'))) {
|
||||
if (component.isContainer !== false) {
|
||||
component.isContainer = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (EVENT_RE.test(name) && (propType === 'func' || propType === 'any')) {
|
||||
if (supportEvents) {
|
||||
supportEvents.push(name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'className' && (propType === 'string' || propType === 'any')) {
|
||||
if ((styles as any).supportClassName == null) {
|
||||
(styles as any).supportClassName = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'style' && (propType === 'object' || propType === 'any')) {
|
||||
if ((styles as any).supportInlineStyle == null) {
|
||||
(styles as any).supportInlineStyle = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
props.push(propConfigToFieldConfig(prop));
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
configure: {
|
||||
...configure,
|
||||
props,
|
||||
events,
|
||||
styles,
|
||||
component,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
registerMetadataTransducer((metadata) => {
|
||||
const { configure = {}, componentName } = metadata;
|
||||
const { component = {} } = configure as any;
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
registerMetadataTransducer((metadata) => {
|
||||
const { componentName, configure = {} } = metadata;
|
||||
if (componentName === '#frag') {
|
||||
return {
|
||||
...metadata,
|
||||
configure: {
|
||||
...configure,
|
||||
combined: [{
|
||||
name: 'children',
|
||||
title: '内容设置',
|
||||
setter: {
|
||||
componentName: 'MixinSetter',
|
||||
props: {
|
||||
setters: [{
|
||||
componentName: 'StringSetter',
|
||||
initialValue: '',
|
||||
}, {
|
||||
componentName: 'ExpressionSetter',
|
||||
initialValue: {
|
||||
type: 'JSExpression',
|
||||
value: ''
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { props, events, styles } = configure as any;
|
||||
let supportEvents: any;
|
||||
let isRoot: boolean = false;
|
||||
if (componentName === 'Page' || componentName === 'Component') {
|
||||
isRoot = true;
|
||||
// todo
|
||||
/*
|
||||
supportEvents = [{
|
||||
description: '初始化时',
|
||||
name: 'constructor'
|
||||
}, {
|
||||
description: '装载后',
|
||||
name: 'componentDidMount'
|
||||
}, {
|
||||
description: '更新时',
|
||||
name: 'componentDidMount'
|
||||
}, {
|
||||
description: '卸载时',
|
||||
name: 'componentWillUnmount'
|
||||
}]
|
||||
*/
|
||||
} else {
|
||||
supportEvents = (events?.supportEvents || []).map((event: any) => {
|
||||
return typeof event === 'string' ? {
|
||||
name: event,
|
||||
} : event;
|
||||
});
|
||||
}
|
||||
// 通用设置
|
||||
const propsGroup = props || [];
|
||||
propsGroup.push({
|
||||
name: '#generals',
|
||||
title: '通用',
|
||||
items: [{
|
||||
name: 'id',
|
||||
title: 'ID',
|
||||
setter: 'StringSetter',
|
||||
}, {
|
||||
name: 'key',
|
||||
title: 'Key',
|
||||
setter: 'StringSetter',
|
||||
}, {
|
||||
name: 'ref',
|
||||
setter: 'StringSetter',
|
||||
}, {
|
||||
name: '!more',
|
||||
title: '更多',
|
||||
setter: 'PropertiesSetter'
|
||||
}]
|
||||
});
|
||||
const combined = [{
|
||||
title: '属性',
|
||||
name: '#props',
|
||||
items: propsGroup,
|
||||
}];
|
||||
const stylesGroup = [];
|
||||
if (styles?.supportClassName) {
|
||||
stylesGroup.push({
|
||||
name: 'className',
|
||||
title: '类名绑定',
|
||||
setter: 'ClassNameSetter'
|
||||
});
|
||||
}
|
||||
if (styles?.supportInlineStyle) {
|
||||
stylesGroup.push({
|
||||
name: 'style',
|
||||
title: '行内样式',
|
||||
setter: 'StyleSetter'
|
||||
});
|
||||
}
|
||||
if (stylesGroup.length > 0) {
|
||||
combined.push({
|
||||
name: '#styles',
|
||||
title: '样式',
|
||||
items: stylesGroup,
|
||||
});
|
||||
}
|
||||
|
||||
if (supportEvents) {
|
||||
combined.push({
|
||||
name: '#events',
|
||||
title: '事件',
|
||||
items: [{
|
||||
name: '!events',
|
||||
title: '事件设置',
|
||||
setter: {
|
||||
componentName: 'EventsSetter',
|
||||
props: {
|
||||
definition: []
|
||||
}
|
||||
},
|
||||
getValue(field: SettingField) {
|
||||
return [];
|
||||
},
|
||||
setValue(field: SettingField) {
|
||||
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
if (isRoot) {
|
||||
// todo...
|
||||
} else {
|
||||
combined.push({
|
||||
name: '#advanced',
|
||||
title: '高级',
|
||||
items: [{
|
||||
name: '__condition',
|
||||
title: '条件显示',
|
||||
setter: 'ExpressionSetter'
|
||||
}, {
|
||||
name: '#loop',
|
||||
title: '循环',
|
||||
items: [{
|
||||
name: '__loop',
|
||||
title: '循环数据',
|
||||
setter: {
|
||||
componentName: 'MixinSetter',
|
||||
props: {
|
||||
setters: [{
|
||||
componentName: 'JSONSetter',
|
||||
props: {
|
||||
mode: 'popup',
|
||||
placeholder: '编辑数据'
|
||||
}
|
||||
}, {
|
||||
componentName: 'ExpressionSetter',
|
||||
props: {
|
||||
placeholder: '绑定数据'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}, {
|
||||
name: '__loopArgs.0',
|
||||
title: '迭代变量名',
|
||||
setter: {
|
||||
componentName: 'StringSetter',
|
||||
placeholder: '默认为 item'
|
||||
}
|
||||
}, {
|
||||
name: '__loopArgs.1',
|
||||
title: '索引变量名',
|
||||
setter: {
|
||||
componentName: 'StringSetter',
|
||||
placeholder: '默认为 index'
|
||||
}
|
||||
}, {
|
||||
name: 'key',
|
||||
title: 'Key',
|
||||
setter: 'ExpressionSetter',
|
||||
}]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
configure: {
|
||||
...configure,
|
||||
combined,
|
||||
},
|
||||
};
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user