complete metadata transducer

This commit is contained in:
kangwei 2020-03-15 01:50:28 +08:00
parent f37743327b
commit 5f569cc1ca
15 changed files with 878 additions and 467 deletions

View File

@ -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

View File

@ -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 !== '') {

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
/**
* 

View File

@ -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;
}

View 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;
}

View File

@ -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;
/**
*

View File

@ -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)} />;
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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>
);

View File

@ -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;
}

View 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,
},
};
});