diff --git a/packages/rax-simulator-renderer/src/renderer-view.tsx b/packages/rax-simulator-renderer/src/renderer-view.tsx index 1612f265b..ae4a80e8d 100644 --- a/packages/rax-simulator-renderer/src/renderer-view.tsx +++ b/packages/rax-simulator-renderer/src/renderer-view.tsx @@ -2,7 +2,7 @@ import { Fragment, Component, createElement } from 'rax'; // import { observer } from './obx-rax/observer'; import RaxEngine from '@ali/lowcode-rax-renderer/lib/index'; // import RaxEngine from '../../rax-render/lib/index'; -import { SimulatorRenderer } from './renderer'; +import { SimulatorRendererContainer, DocumentInstance } from './renderer'; import { host } from './host'; import './renderer.less'; @@ -37,17 +37,31 @@ const originCloneElement = (window as any).Rax.cloneElement; return originCloneElement(child, props, ...rest); }; -export default class SimulatorRendererView extends Component<{ renderer: SimulatorRenderer }> { +export default class SimulatorRendererView extends Component<{ rendererContainer: SimulatorRendererContainer }> { render() { - const { renderer } = this.props; + const { rendererContainer } = this.props; return ( - - + + ); } } +export class Routes extends Component<{ rendererContainer: SimulatorRendererContainer }> { + render() { + const { rendererContainer } = this.props; + return ( + + {rendererContainer.documentInstances.map((instance) => { + return ( + + ); + })} + + ); + } +} function ucfirst(s: string) { return s.charAt(0).toUpperCase() + s.substring(1); } @@ -68,14 +82,13 @@ function getDeviceView(view: any, device: string, mode: string) { return view; } -// @observer -class Layout extends Component<{ renderer: SimulatorRenderer }> { +class Layout extends Component<{ rendererContainer: SimulatorRendererContainer }> { shouldComponentUpdate() { return false; } render() { - const { renderer, children } = this.props; - const layout = renderer.layout; + const { rendererContainer, children } = this.props; + const layout = rendererContainer.layout; if (layout) { const { Component, props } = layout; @@ -86,11 +99,10 @@ class Layout extends Component<{ renderer: SimulatorRenderer }> { } } -// @observer -class Renderer extends Component<{ renderer: SimulatorRenderer }> { +class Renderer extends Component<{ rendererContainer: SimulatorRendererContainer, documentInstance: DocumentInstance }> { constructor(props: any) { super(props); - this.props.renderer.onReRender(() => { + this.props.rendererContainer.onReRender(() => { this.forceUpdate(); }); } @@ -98,24 +110,28 @@ class Renderer extends Component<{ renderer: SimulatorRenderer }> { return false; } render() { - const { renderer } = this.props; - const { device, designMode } = renderer; + const { documentInstance } = this.props; + const { container } = documentInstance; + const { designMode, device } = container; + const { rendererContainer: renderer } = this.props; + // const { device, designMode } = renderer; return ( { - renderer.mountInstance(schema.id, ref); + documentInstance.mountInstance(schema.id, ref); }} customCreateElement={(Component: any, props: any, children: any) => { const { __id, __desingMode, ...viewProps } = props; viewProps.componentId = __id; - const leaf = host.document.getNode(__id); + const leaf: any = null; + // const leaf = host.document.getNode(__id); viewProps._leaf = leaf; viewProps._componentName = leaf?.componentName; diff --git a/packages/rax-simulator-renderer/src/renderer.ts b/packages/rax-simulator-renderer/src/renderer.ts index be68ca9ca..f35d4de48 100644 --- a/packages/rax-simulator-renderer/src/renderer.ts +++ b/packages/rax-simulator-renderer/src/renderer.ts @@ -1,5 +1,6 @@ -import { BuiltinSimulatorRenderer, NodeInstance, Component } from '@ali/lowcode-designer'; -import { shared, render as raxRender, createElement } from 'rax'; +import { BuiltinSimulatorRenderer, NodeInstance, Component, DocumentModel } from '@ali/lowcode-designer'; +// @ts-ignore +import { shared, render as raxRender, createElement, ComponentType } from 'rax'; import DriverUniversal from 'driver-universal'; import { computed, obx } from '@recore/obx'; import { RootSchema, NpmInfo, ComponentSchema } from '@ali/lowcode-types'; @@ -23,6 +24,7 @@ export interface LibraryMap { } const SYMBOL_VNID = Symbol('_LCNodeId'); +const SYMBOL_VDID = Symbol('_LCDocId'); function accessLibrary(library: string | object) { if (typeof library !== 'string') { @@ -32,45 +34,116 @@ function accessLibrary(library: string | object) { return (window as any)[library]; } -export class SimulatorRenderer implements BuiltinSimulatorRenderer { - readonly isSimulatorRenderer = true; - private dispose?: () => void; +// Slot/Leaf and Fragment|FunctionComponent polyfill(ref) +const builtinComponents = { + Slot, + Leaf, +}; + +function buildComponents(libraryMap: LibraryMap, + componentsMap: { [componentName: string]: NpmInfo | ComponentType | ComponentSchema }, + createComponent: (schema: ComponentSchema) => Component | null) { + const components: any = { + ...builtinComponents, + }; + Object.keys(componentsMap).forEach((componentName) => { + let component = componentsMap[componentName]; + if (component && (component as ComponentSchema).componentName === 'Component') { + components[componentName] = createComponent(component as ComponentSchema); + } else if (isReactComponent(component)) { + components[componentName] = component; + } else { + component = findComponent(libraryMap, componentName, component as NpmInfo); + if (component) { + components[componentName] = component; + } + } + }); + return components; +} + +let REACT_KEY = ''; +function cacheReactKey(el: Element): Element { + if (REACT_KEY !== '') { + return el; + } + REACT_KEY = Object.keys(el).find((key) => key.startsWith('__reactInternalInstance$')) || ''; + if (!REACT_KEY && (el as HTMLElement).parentElement) { + return cacheReactKey((el as HTMLElement).parentElement!); + } + return el; +} + +function getClosestNodeInstance(from: any, specId?: string): NodeInstance | null { + const el: any = from; + if (el) { + // if (isElement(el)) { + // el = cacheReactKey(el); + // } else { + // return getNodeInstance(el, specId); + // } + return getNodeInstance(el); + } + return null; +} + +function getNodeInstance(dom: HTMLElement): NodeInstance | null { + const INTERNAL = '_internal'; + + let instance = Instance.get(dom); + while (instance && instance[INTERNAL]) { + if (isValidDesignModeRaxComponentInstance(instance)) { + const docId = (instance as any)[SYMBOL_VDID]; + return { + docId, + nodeId: instance.props._leaf.getId(), + instance: instance, + node: instance.props._leaf, + }; + } + + instance = instance[INTERNAL].__parentInstance; + } + + return null; +} + + +function checkInstanceMounted(instance: any): boolean { + if (isElement(instance)) { + return instance.parentElement != null; + } + return true; +} + + +function isValidDesignModeRaxComponentInstance( + raxComponentInst: any, +): raxComponentInst is { + props: { + _leaf: Exclude['node'], null | undefined>; + }; +} { + const leaf = raxComponentInst?.props?._leaf; + return leaf && typeof leaf === 'object' && leaf.isNode; +} + +export class DocumentInstance { private instancesMap = new Map(); + @obx.ref private _schema?: RootSchema; @computed get schema(): any { return this._schema; } - private _libraryMap: { [key: string]: string } = {}; - private buildComponents() { - this._components = buildComponents(this._libraryMap, this._componentsMap); - } - @obx.ref private _components: any = {}; - @computed get components(): object { - // 根据 device 选择不同组件,进行响应式 - // 更好的做法是,根据 device 选择加载不同的组件资源,甚至是 simulatorUrl - return this._components; - } - @obx.ref private _designMode = 'design'; - @computed get designMode(): any { - return this._designMode; - } + private dispose?: () => void; - @obx.ref private _componentsMap = {}; - @computed get componentsMap(): any { - return this._componentsMap; - } - - @obx.ref private _device = 'default'; - @computed get device() { - return this._device; - } - - // context from: utils、constants、history、location、match - @obx.ref private _appContext = {}; - @computed get context(): any { - return this._appContext; + constructor(readonly container: SimulatorRendererContainer, readonly document: DocumentModel) { + this.dispose = host.autorun(() => { + // sync schema + this._schema = document.export(1); + }); } @computed get suspended(): any { @@ -80,23 +153,110 @@ export class SimulatorRenderer implements BuiltinSimulatorRenderer { return null; } - @computed get layout(): any { - // TODO: parse layout Component - return null; + get path(): string { + return '/' + this.document.fileName; } - private emitter = new EventEmitter(); - constructor() { - if (!host) { + get id() { + return this.document.id; + } + + private unmountIntance(id: string, instance: any) { + const instances = this.instancesMap.get(id); + if (instances) { + const i = instances.indexOf(instance); + if (i > -1) { + instances.splice(i, 1); + host.setInstance(this.document.id, id, instances); + } + } + } + + mountInstance(id: string, instance: any) { + const docId = this.document.id; + const instancesMap = this.instancesMap; + if (instance == null) { + let instances = this.instancesMap.get(id); + if (instances) { + instances = instances.filter(checkInstanceMounted); + if (instances.length > 0) { + instancesMap.set(id, instances); + host.setInstance(this.document.id, id, instances); + } else { + instancesMap.delete(id); + host.setInstance(this.document.id, id, null); + } + } return; } + const unmountIntance = this.unmountIntance.bind(this); + const origId = (instance as any)[SYMBOL_VNID]; + if (origId && origId !== id) { + // 另外一个节点的 instance 在此被复用了,需要从原来地方卸载 + unmountIntance(origId, instance); + } + if (isElement(instance)) { + cacheReactKey(instance); + } else if (origId !== id) { + // 涵盖 origId == null || origId !== id 的情况 + let origUnmount: any = instance.componentWillUnmount; + if (origUnmount && origUnmount.origUnmount) { + origUnmount = origUnmount.origUnmount; + } + // hack! delete instance from map + const newUnmount = function(this: any) { + unmountIntance(id, instance); + origUnmount && origUnmount.call(this); + }; + (newUnmount as any).origUnmount = origUnmount; + instance.componentWillUnmount = newUnmount; + } - this.dispose = host.connect(this as any, () => { + (instance as any)[SYMBOL_VNID] = id; + (instance as any)[SYMBOL_VDID] = docId; + let instances = this.instancesMap.get(id); + if (instances) { + const l = instances.length; + instances = instances.filter(checkInstanceMounted); + let updated = instances.length !== l; + if (!instances.includes(instance)) { + instances.push(instance); + updated = true; + } + if (!updated) { + return; + } + } else { + instances = [instance]; + } + instancesMap.set(id, instances); + host.setInstance(this.document.id, id, instances); + } + + mountContext(docId: string, id: string, ctx: object) { + // this.ctxMap.set(id, ctx); + } + + getNode(id: string): Node | null { + return null; + // return this.document.getNode(id); + } +} + +export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { + readonly isSimulatorRenderer = true; + private dispose?: () => void; + // TODO: history + readonly history: any; + + @obx.ref private _documentInstances: DocumentInstance[] = []; + get documentInstances() { + return this._documentInstances; + } + + constructor() { + this.dispose = host.connect(this, () => { // sync layout config - - // sync schema - this._schema = host.document.export(1); - // todo: split with others, not all should recompute if (this._libraryMap !== host.libraryMap || this._componentsMap !== host.designer.componentsMap) { this._libraryMap = host.libraryMap || {}; @@ -107,14 +267,38 @@ export class SimulatorRenderer implements BuiltinSimulatorRenderer { // sync designMode this._designMode = host.designMode; - // sync suspended - - // sync scope - // sync device this._device = host.device; - - this.emitter.emit('rerender'); + }); + const documentInstanceMap = new Map(); + let initialEntry = '/'; + host.autorun(({ firstRun }) => { + this._documentInstances = host.project.documents.map((doc) => { + let inst = documentInstanceMap.get(doc.id); + if (!inst) { + inst = new DocumentInstance(this, doc); + documentInstanceMap.set(doc.id, inst); + } + return inst; + }); + const path = host.project.currentDocument + ? documentInstanceMap.get(host.project.currentDocument.id)!.path + : '/'; + if (firstRun) { + initialEntry = path; + } else { + if (this.history.location.pathname !== path) { + this.history.replace(path); + } + } + }); + /* + const history = createMemoryHistory({ + initialEntries: [initialEntry], + }); + this.history = history; + history.listen((location, action) => { + host.project.open(location.pathname.substr(1)); }); host.componentsConsumer.consume(async (componentsAsset) => { if (componentsAsset) { @@ -122,31 +306,69 @@ export class SimulatorRenderer implements BuiltinSimulatorRenderer { this.buildComponents(); } }); + this._appContext = { + utils: { + router: { + push(path: string, params?: object) { + history.push(withQueryParams(path, params)); + }, + replace(path: string, params?: object) { + history.replace(withQueryParams(path, params)); + }, + }, + legaoBuiltins: { + getUrlParams() { + const search = history.location.search; + return parseQuery(search); + } + } + }, + constants: {}, + }; host.injectionConsumer.consume((data) => { // sync utils, i18n, contants,... config - this._appContext = { - utils: {}, - constants: { - name: 'demo', - }, - }; }); + */ } - getClosestNodeInstance(from: any, nodeId?: string): NodeInstance | null { - const node = getClosestNodeInstance(from, nodeId); - return node; + @computed get layout(): any { + // TODO: parse layout Component + return null; } - getComponentInstances(id: string): any[] | null { - return this.instancesMap.get(id) || null; + private _libraryMap: { [key: string]: string } = {}; + private buildComponents() { + // TODO: remove this.createComponent + this._components = buildComponents(this._libraryMap, this._componentsMap, this.createComponent.bind(this)); } - - onReRender(fn: () => void) { - this.emitter.on('rerender', fn); - return () => { - this.emitter.removeListener('renderer', fn); - }; + @obx.ref private _components: any = {}; + @computed get components(): object { + // 根据 device 选择不同组件,进行响应式 + // 更好的做法是,根据 device 选择加载不同的组件资源,甚至是 simulatorUrl + return this._components; + } + // context from: utils、constants、history、location、match + @obx.ref private _appContext = {}; + @computed get context(): any { + return this._appContext; + } + @obx.ref private _designMode: string = 'design'; + @computed get designMode(): any { + return this._designMode; + } + @obx.ref private _device: string = 'default'; + @computed get device() { + return this._device; + } + @obx.ref private _componentsMap = {}; + @computed get componentsMap(): any { + return this._componentsMap; + } + /** + * 加载资源 + */ + load(asset: Asset): Promise { + return loader.load(asset); } getComponent(componentName: string) { @@ -167,20 +389,47 @@ export class SimulatorRenderer implements BuiltinSimulatorRenderer { componentName = paths.join('.'); } - // return null; + return null; } - createComponent(schema: ComponentSchema): Component | null { + getClosestNodeInstance(from: any, nodeId?: string): NodeInstance | null { + const el: any = from; + if (el) { + // if (isElement(el)) { + // el = cacheReactKey(el); + // } else { + // return getNodeInstance(el, specId); + // } + return getNodeInstance(el); + } return null; } + getComponentInstances(id: string): any[] | null { + return this.instancesMap.get(id) || null; + } + + onReRender(fn: () => void) { + this.emitter.on('rerender', fn); + return () => { + this.emitter.removeListener('renderer', fn); + }; + } + + findDOMNodes(instance: any): Array | null { + return raxFindDOMNodes(instance); + } + + getClientRects(element: Element | Text) { + return getClientRects(element); + } + setNativeSelection(enableFlag: boolean) { setNativeSelection(enableFlag); } setDraggingState(state: boolean) { cursor.setDragging(state); } - setCopyState(state: boolean) { cursor.setCopy(state); } @@ -188,93 +437,100 @@ export class SimulatorRenderer implements BuiltinSimulatorRenderer { cursor.release(); } - findDOMNodes(instance: any): Array | null { - return [raxFindDOMNodes(instance)]; - } + createComponent(schema: ComponentSchema): Component | null { + return null; + // TODO: use ComponentEngine refactor + /* + const _schema = { + ...schema, + }; + _schema.methods = {}; + _schema.lifeCycles = {}; - /** - * 加载资源 - */ - load(asset: Asset): Promise { - return loader.load(asset); - } + const node = host.document.createNode(_schema); + _schema = node.export(TransformStage.Render); - // private instancesMap = new Map(); - private unmountIntance(id: string, instance: any) { - const instances = this.instancesMap.get(id); - if (instances) { - const i = instances.indexOf(instance); - if (i > -1) { - instances.splice(i, 1); - host.setInstance(id, instances); + const processPropsSchema = (propsSchema: any, propsMap: any): any => { + if (!propsSchema) { + return {}; } - } - } - mountInstance(id: string, instance: any | null) { - const instancesMap = this.instancesMap; - if (instance == null) { - let instances = this.instancesMap.get(id); - if (instances) { - instances = instances.filter(checkInstanceMounted); - if (instances.length > 0) { - instancesMap.set(id, instances); - host.setInstance(id, instances); - } else { - instancesMap.delete(id); - host.setInstance(id, null); + + const result = { ...propsSchema }; + const reg = /^(?:this\.props|props)\.(\S+)$/; + Object.keys(result).map((key: string) => { + if (result[key]?.type === 'JSExpression') { + const { value } = result[key]; + const matched = reg.exec(value); + if (matched) { + const propName = matched[1]; + result[key] = propsMap[propName]; + } + } else if (result[key]?.type === 'JSSlot') { + const schema = result[key].value; + result[key] = createElement(Ele, {schema, propsMap: {}}); + } + }); + + return result; + }; + + const renderer = this; + const componentsMap = renderer.componentsMap; + + class Ele extends React.Component<{ schema: any, propsMap: any }> { + private isModal: boolean; + + constructor(props: any){ + super(props); + const componentMeta = host.document.getComponentMeta(props.schema.componentName); + if (componentMeta?.prototype?.isModal()) { + this.isModal = true; + return; } } - return; - } - const unmountIntance = this.unmountIntance.bind(this); - const origId = (instance as any)[SYMBOL_VNID]; - if (origId && origId !== id) { - // 另外一个节点的 instance 在此被复用了,需要从原来地方卸载 - unmountIntance(origId, instance); - } - if (isElement(instance)) { - // cacheReactKey(instance); - } else if (origId !== id) { - // 涵盖 origId == null || origId !== id 的情况 - let origUnmount: any = instance.componentWillUnmount; - if (origUnmount && origUnmount.origUnmount) { - origUnmount = origUnmount.origUnmount; + + render() { + if (this.isModal) { + return null; + } + const { schema, propsMap } = this.props; + const Com = componentsMap[schema.componentName]; + if (!Com) { + return null; + } + let children = null; + if (schema.children && schema.children.length > 0) { + children = schema.children.map((item: any) => createElement(Ele, {schema: item, propsMap})); + } + const props = processPropsSchema(schema.props, propsMap); + const _leaf = host.document.createNode(schema); + + return createElement(Com, {...props, _leaf}, children); + } + const _leaf = this.document.designer.currentDocument?.createNode(schema); + const node = this.document.createNode(schema); + let props = processPropsSchema(schema.props, propsMap); + props = this.document.designer.transformProps(props, node, TransformStage.Init); + props = this.document.designer.transformProps(props, node, TransformStage.Render); + return createElement(Com, { ...props, _leaf }, children); + }; + + const container = this; + class Com extends React.Component { + render() { + const componentsMap = container.componentsMap; + let children = null; + if (_schema.children && Array.isArray(_schema.children)) { + children = _schema.children?.map((item: any) => getElement(componentsMap, item, this.props)); + } } - // hack! delete instance from map - const newUnmount = function(this: any) { - unmountIntance(id, instance); - origUnmount && origUnmount.call(this); - }; - (newUnmount as any).origUnmount = origUnmount; - instance.componentWillUnmount = newUnmount; } - (instance as any)[SYMBOL_VNID] = id; - let instances = this.instancesMap.get(id); - if (instances) { - const l = instances.length; - instances = instances.filter(checkInstanceMounted); - let updated = instances.length !== l; - if (!instances.includes(instance)) { - instances.push(instance); - updated = true; - } - if (!updated) { - return; - } - } else { - instances = [instance]; - } - instancesMap.set(id, instances); - host.setInstance(id, instances); - } - - getClientRects(element: Element | Text) { - return getClientRects(element); + return Com; + */ } private _running = false; - run() { if (this._running) { return; @@ -287,6 +543,7 @@ export class SimulatorRenderer implements BuiltinSimulatorRenderer { document.body.appendChild(container); container.id = containerId; } + // ==== compatiable vision document.documentElement.classList.add('engine-page'); document.body.classList.add('engine-document'); // important! Stylesheet.invoke depends @@ -294,7 +551,7 @@ export class SimulatorRenderer implements BuiltinSimulatorRenderer { raxRender(createElement(SimulatorRendererView, { renderer: this }), container, { driver: DriverUniversal, }); - host.document.setRendererReady(this); + host.project.setRendererReady(this); } } @@ -350,77 +607,4 @@ function findComponent(libraryMap: LibraryMap, componentName: string, npm?: NpmI return getSubComponent(library, paths); } -const builtinComponents = { - Slot, - Leaf, -}; - -function buildComponents(libraryMap: LibraryMap, componentsMap: { [componentName: string]: NpmInfo }) { - const components: any = { - ...builtinComponents, - }; - Object.keys(componentsMap).forEach((componentName) => { - let component = componentsMap[componentName]; - if (isReactComponent(component)) { - components[componentName] = component; - } else { - component = findComponent(libraryMap, componentName, component); - if (component) { - components[componentName] = component; - } - } - }); - return components; -} - -function getClosestNodeInstance(from: any, specId?: string): NodeInstance | null { - const el: any = from; - if (el) { - // if (isElement(el)) { - // el = cacheReactKey(el); - // } else { - // return getNodeInstance(el, specId); - // } - return getNodeInstance(el); - } - return null; -} - -function isValidDesignModeRaxComponentInstance( - raxComponentInst: any, -): raxComponentInst is { - props: { - _leaf: Exclude['node'], null | undefined>; - }; -} { - const leaf = raxComponentInst?.props?._leaf; - return leaf && typeof leaf === 'object' && leaf.isNode; -} - -function getNodeInstance(dom: HTMLElement): NodeInstance | null { - const INTERNAL = '_internal'; - - let instance = Instance.get(dom); - while (instance && instance[INTERNAL]) { - if (isValidDesignModeRaxComponentInstance(instance)) { - return { - nodeId: instance.props._leaf.getId(), - instance: instance, - node: instance.props._leaf, - }; - } - - instance = instance[INTERNAL].__parentInstance; - } - - return null; -} - -function checkInstanceMounted(instance: any): boolean { - if (isElement(instance)) { - return instance.parentElement != null; - } - return true; -} - -export default new SimulatorRenderer(); +export default new SimulatorRendererContainer();