Tobias Speicher aca2f08b77 refactor: replace deprecated String.prototype.substr()
.substr() is deprecated so we replace it with .slice() which works similarily but isn't deprecated

Signed-off-by: Tobias Speicher <rootcommander@gmail.com>
2022-03-30 14:07:03 +08:00

661 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BuiltinSimulatorRenderer, Component, DocumentModel, Node, NodeInstance } from '@alilc/lowcode-designer';
import { ComponentSchema, NodeSchema, NpmInfo, RootSchema, TransformStage } from '@alilc/lowcode-types';
import { Asset, compatibleLegaoSchema, cursor, isElement, isESModule, isPlainObject, isReactComponent, setNativeSelection } from '@alilc/lowcode-utils';
import LowCodeRenderer from '@alilc/lowcode-rax-renderer';
import { computed, observable as obx, untracked, makeObservable, configure } from 'mobx';
import DriverUniversal from 'driver-universal';
import { EventEmitter } from 'events';
import { createMemoryHistory, MemoryHistory } from 'history';
// @ts-ignore
import Rax, { ComponentType, createElement, render as raxRender, shared } from 'rax';
import Leaf from './builtin-components/leaf';
import Slot from './builtin-components/slot';
import { host } from './host';
import SimulatorRendererView from './renderer-view';
import { raxFindDOMNodes } from './utils/find-dom-nodes';
import { getClientRects } from './utils/get-client-rects';
import loader from './utils/loader';
import { parseQuery, withQueryParams } from './utils/url';
configure({ enforceActions: 'never' });
const { Instance } = shared;
export interface LibraryMap {
[key: string]: string;
}
const SYMBOL_VNID = Symbol('_LCNodeId');
const SYMBOL_VDID = Symbol('_LCDocId');
const INTERNAL = '_internal';
function accessLibrary(library: string | object) {
if (typeof library !== 'string') {
return library;
}
return (window as any)[library];
}
// Slot/Leaf and Fragment|FunctionComponent polyfill(ref)
const builtinComponents = {
Slot,
Leaf,
};
function buildComponents(
libraryMap: LibraryMap,
componentsMap: { [componentName: string]: NpmInfo | ComponentType<any> | 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 checkInstanceMounted(instance: any): boolean {
if (isElement(instance)) {
return instance.parentElement != null;
}
return true;
}
function isValidDesignModeRaxComponentInstance(
raxComponentInst: any,
): raxComponentInst is {
props: {
_leaf: Exclude<NodeInstance<any>['node'], null | undefined>;
};
} {
const leaf = raxComponentInst?.props?._leaf;
return leaf && typeof leaf === 'object' && leaf.isNode;
}
export class DocumentInstance {
private instancesMap = new Map<string, any[]>();
private emitter = new EventEmitter();
get schema(): any {
return this.document.export(TransformStage.Render);
}
constructor(readonly container: SimulatorRendererContainer, readonly document: DocumentModel) {
makeObservable(this);
}
@computed get suspended(): any {
return false;
}
@computed get scope(): any {
return null;
}
get path(): string {
return `/${ this.document.fileName}`;
}
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);
}
}
}
refresh() {
this.emitter.emit('rerender', { shouldRemount: true });
}
onReRender(fn: () => void) {
this.emitter.on('rerender', fn);
return () => {
this.emitter.removeListener('renderer', fn);
};
}
mountInstance(id: string, instance: any) {
const docId = this.document.id;
const { instancesMap } = this;
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;
}
(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);
}
getComponentInstances(id: string): any[] | null {
return this.instancesMap.get(id) || null;
}
getNode(id: string): Node<NodeSchema> | null {
return this.document.getNode(id);
}
}
export class SimulatorRendererContainer implements BuiltinSimulatorRenderer {
readonly isSimulatorRenderer = true;
private dispose?: () => void;
readonly history: MemoryHistory;
private emitter = new EventEmitter();
@obx.ref private _documentInstances: DocumentInstance[] = [];
get documentInstances() {
return this._documentInstances;
}
get currentDocumentInstance() {
return this._documentInstances.find((item) => item.id === host.project.currentDocument?.id);
}
constructor() {
this.dispose = host.connect(this, () => {
// sync layout config
// debugger;
this._layout = host.project.get('config').layout;
// todo: split with others, not all should recompute
if (this._libraryMap !== host.libraryMap || this._componentsMap !== host.designer.componentsMap) {
this._libraryMap = host.libraryMap || {};
this._componentsMap = host.designer.componentsMap;
this.buildComponents();
}
// sync designMode
this._designMode = host.designMode;
// sync requestHandlersMap
this._requestHandlersMap = host.requestHandlersMap;
// sync device
this._device = host.device;
this.emitter.emit('layoutChange');
});
const documentInstanceMap = new Map<string, DocumentInstance>();
let initialEntry = '/';
let firstRun = true;
host.autorun(() => {
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;
firstRun = false;
} else {
if (this.history.location.pathname !== path) {
this.history.replace(path);
}
this.emitter.emit('layoutChange');
}
});
const history = createMemoryHistory({
initialEntries: [initialEntry],
});
this.history = history;
history.listen(({ location }) => {
host.project.open(location.pathname.slice(1));
});
host.componentsConsumer.consume(async (componentsAsset) => {
if (componentsAsset) {
await this.load(componentsAsset);
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));
},
back() {
history.back();
},
},
legaoBuiltins: {
getUrlParams() {
const { search } = history.location;
return parseQuery(search);
},
},
},
constants: {},
requestHandlersMap: this._requestHandlersMap,
};
host.injectionConsumer.consume((data) => {
// sync utils, i18n, contants,... config
});
}
@obx private _layout: any = null;
@computed get layout(): any {
// TODO: parse layout Component
return this._layout;
}
set layout(value: any) {
this._layout = value;
}
private _libraryMap: { [key: string]: string } = {};
private buildComponents() {
// TODO: remove this.createComponent
this._components = buildComponents(this._libraryMap, this._componentsMap, this.createComponent.bind(this));
}
@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 _requestHandlersMap = null;
@computed get requestHandlersMap(): any {
return this._requestHandlersMap;
}
@obx.ref private _componentsMap = {};
@computed get componentsMap(): any {
return this._componentsMap;
}
/**
* 加载资源
*/
load(asset: Asset): Promise<any> {
return loader.load(asset);
}
getComponent(componentName: string) {
const paths = componentName.split('.');
const subs: string[] = [];
while (true) {
const component = this._components[componentName];
if (component) {
return getSubComponent(component, subs);
}
const sub = paths.pop();
if (!sub) {
return null;
}
subs.unshift(sub);
componentName = paths.join('.');
}
}
getNodeInstance(dom: HTMLElement): NodeInstance<any> | null {
const INTERNAL = '_internal';
let instance: any = dom;
if (!isElement(instance)) {
return {
docId: instance.props._leaf.document.id,
nodeId: instance.props._leaf.getId(),
instance,
node: instance.props._leaf,
};
}
instance = Instance.get(dom);
let loopNum = 0; // 防止由于某种意外而导致死循环
while (instance && instance[INTERNAL] && loopNum < 1000) {
if (isValidDesignModeRaxComponentInstance(instance)) {
// if (instance && SYMBOL_VNID in instance) {
// const docId = (instance.props as any).schema.docId;
return {
docId: instance.props._leaf.document.id,
nodeId: instance.props._leaf.getId(),
instance,
node: instance.props._leaf,
};
}
instance = getRaxVDomParentInstance(instance);
loopNum += 1;
}
return null;
}
getClosestNodeInstance(from: any, nodeId?: string): NodeInstance<any> | null {
const el: any = from;
if (el) {
// if (isElement(el)) {
// el = cacheReactKey(el);
// } else {
// return getNodeInstance(el, specId);
// }
return this.getNodeInstance(el);
}
return null;
}
findDOMNodes(instance: any, selector?: string): Array<Element | Text> | null {
let el = instance;
if (selector) {
el = document.querySelector(selector);
}
try {
return raxFindDOMNodes(el);
} catch (e) {
// ignore
}
if (el && el.type && el.props && el.props.componentId) {
el = document.querySelector(`${el.type}[componentid=${el.props.componentId}]`);
} else {
console.error(instance);
throw new Error('This instance may not a valid element');
}
return raxFindDOMNodes(el);
}
getClientRects(element: Element | Text) {
return getClientRects(element);
}
setNativeSelection(enableFlag: boolean) {
setNativeSelection(enableFlag);
}
setDraggingState(state: boolean) {
cursor.setDragging(state);
}
setCopyState(state: boolean) {
cursor.setCopy(state);
}
clearState() {
cursor.release();
}
onLayoutChange(cb: () => void) {
this.emitter.on('layoutChange', cb);
return () => {
this.emitter.removeListener('layoutChange', cb);
};
}
onReRender(fn: () => void) {
this.emitter.on('rerender', fn);
return () => {
this.emitter.removeListener('renderer', fn);
};
}
rerender() {
this.currentDocumentInstance?.refresh();
}
createComponent(schema: NodeSchema): Component | null {
const _schema: any = {
...compatibleLegaoSchema(schema),
};
_schema.methods = {};
_schema.lifeCycles = {};
if (schema.componentName === 'Component' && (schema as ComponentSchema).css) {
const doc = window.document;
const s = doc.createElement('style');
s.setAttribute('type', 'text/css');
s.setAttribute('id', `Component-${schema.id || ''}`);
s.appendChild(doc.createTextNode((schema as ComponentSchema).css || ''));
doc.getElementsByTagName('head')[0].appendChild(s);
}
const renderer = this;
const { componentsMap: components } = renderer;
class LowCodeComp extends Rax.Component {
render() {
const extraProps = getLowCodeComponentProps(this.props);
// @ts-ignore
return createElement(LowCodeRenderer, {
...extraProps,
schema: _schema,
components,
designMode: renderer.designMode,
device: renderer.device,
appHelper: renderer.context,
rendererName: 'LowCodeRenderer',
customCreateElement: (Comp: any, props: any, children: any) => {
const componentMeta = host.currentDocument?.getComponentMeta(Comp.displayName);
if (componentMeta?.isModal) {
return null;
}
const { __id, __designMode, ...viewProps } = props;
// mock _leaf减少性能开销
const _leaf = {
isEmpty: () => false,
};
viewProps._leaf = _leaf;
return createElement(Comp, viewProps, children);
},
});
}
}
return LowCodeComp;
}
private _running = false;
run() {
if (this._running) {
return;
}
this._running = true;
const containerId = 'app';
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
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
raxRender(createElement(SimulatorRendererView, {
rendererContainer: this,
}), container, {
driver: DriverUniversal,
});
host.project.setRendererReady(this);
}
}
function getSubComponent(library: any, paths: string[]) {
const l = paths.length;
if (l < 1 || !library) {
return library;
}
let i = 0;
let component: any;
while (i < l) {
const key = paths[i]!;
let ex: any;
try {
component = library[key];
} catch (e) {
ex = e;
component = null;
}
if (i === 0 && component == null && key === 'default') {
if (ex) {
return l === 1 ? library : null;
}
component = library;
} else if (component == null) {
return null;
}
library = component;
i++;
}
return component;
}
function findComponent(libraryMap: LibraryMap, componentName: string, npm?: NpmInfo) {
if (!npm) {
return accessLibrary(componentName);
}
// libraryName the key access to global
// export { exportName } from xxx exportName === global.libraryName.exportName
// export exportName from xxx exportName === global.libraryName.default || global.libraryName
// export { exportName as componentName } from package
// if exportName == null exportName === componentName;
// const componentName = exportName.subName, if exportName empty subName donot use
const exportName = npm.exportName || npm.componentName || componentName;
const libraryName = libraryMap[npm.package] || exportName;
const library = accessLibrary(libraryName);
const paths = npm.exportName && npm.subName ? npm.subName.split('.') : [];
if (npm.destructuring) {
paths.unshift(exportName);
} else if (isESModule(library)) {
paths.unshift('default');
}
return getSubComponent(library, paths);
}
function getLowCodeComponentProps(props: any) {
if (!props || !isPlainObject(props)) {
return props;
}
const newProps: any = {};
Object.keys(props).forEach(k => {
if (['children', 'componentId', '__designMode', '_componentName', '_leaf'].includes(k)) {
return;
}
newProps[k] = props[k];
});
return newProps;
}
/**
* 获取 Rax 里面 VDOM 的上一级的实例
* 注意Rax 的 development 的包是带有 __parentInstance
* 但是 production 的包 __parentInstance 会被压缩掉,
* 所以这里遍历下其中的所有值,尝试找到有 _internal 的那个(别的值不会带有这个属性的)
*/
function getRaxVDomParentInstance(instance: { _internal: any }) {
const internalInstance = instance._internal;
return internalInstance.__parentInstance ||
Object.values(internalInstance).find(v => (
v !== null &&
v !== instance &&
typeof v === 'object' &&
typeof (v as {_internal: unknown})._internal === 'object'
));
}
export default new SimulatorRendererContainer();