{
+ public static displayName = 'VEField';
+
+ public readonly props: IVEFieldProps;
+
+ public classNames: string[] = [];
+
+ public state: IVEFieldState = {
+ hasError: false,
+ };
+
+ public componentDidCatch(error: Error, info: React.ErrorInfo) {
+ console.error(error);
+ console.warn(info.componentStack);
+ }
+
+ public renderHead(): JSX.Element | JSX.Element[] | null {
+ const { title, tip, compact, propName } = this.props;
+ return getFieldTitle(title!, tip, compact, propName);
+ }
+
+ public renderBody(): JSX.Element | string {
+ return this.props.children;
+ }
+
+ public renderFoot(): any {
+ return null;
+ }
+
+ public render(): JSX.Element {
+ const { stageName, headDIY } = this.props;
+ const classNameList = classnames(...this.classNames, this.props.className);
+ const fieldProps: any = {};
+
+ if (stageName) {
+ // 为 stage 切换奠定基础
+ fieldProps['data-stage-target'] = this.props.stageName;
+ }
+
+ if (this.state.hasError) {
+ return (
+ Field render error, please open console to find out.
+ );
+ }
+
+ const headContent = headDIY ? this.renderHead()
+ : {this.renderHead()}
;
+
+ return (
+
+ {headContent}
+
+ {this.renderBody()}
+
+
+ {this.renderFoot()}
+
+
+ );
+ }
+}
diff --git a/packages/vision-polyfill/src/fields/fields.less b/packages/vision-polyfill/src/fields/fields.less
new file mode 100644
index 000000000..951654da2
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/fields.less
@@ -0,0 +1,272 @@
+@import '~@ali/ve-less-variables/index.less';
+
+.engine-setting-field {
+ white-space: nowrap;
+ position: relative;
+
+ &:after, &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ clear: both;
+ }
+
+ .engine-field-title {
+ font-size: 12px;
+ font-family: @font-family;
+ line-height: 1em;
+ user-select: none;
+ color: var(--color-text, @dark-alpha-3);
+ width: fit-content;
+ white-space: initial;
+ word-break: break-word;
+ &::first-letter {
+ text-transform: capitalize;
+ }
+
+ .engine-word {
+ flex: 1;
+ text-align: center;
+ font-weight: normal;
+ &:first-child {
+ text-align: left;
+ }
+ &:last-of-type {
+ text-align: right;
+ }
+ &:only-of-type {
+ text-align: center;
+ }
+ overflow: hidden;
+ }
+ }
+
+ a.engine-field-title {
+ border-bottom: 1px dashed var(--color-line-normal, @normal-alpha-7);
+ text-decoration: none;
+ padding-bottom: 2px;
+ &:hover {
+ cursor: help;
+ }
+ }
+
+ .engine-field-variable-wrapper {
+ margin-left: 5px;
+ }
+
+ .engine-field-variable {
+ cursor: pointer;
+ opacity: 0.6;
+ &.engine-active {
+ opacity: 1;
+ color: var(--color-brand, @brand-color-1);
+ }
+ }
+
+ .engine-field-head {
+ padding-left: 10px;
+ height: 32px;
+ background: var(--color-block-background-shallow, @normal-alpha-8);
+ display: flex;
+ align-items: center;
+ font-weight: 500;
+ border-top: 1px solid var(--color-line-normal, @normal-alpha-7);
+ border-bottom: 1px solid var(--color-line-normal, @normal-alpha-7);
+ color: var(--color-title, @dark-alpha-2);
+ >.engine-icontip {
+ margin-left: 2px;
+ }
+ }
+
+ .engine-field-body {
+ min-height: 20px;
+ margin: 6px 0;
+
+ &:after, &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ clear: both;
+ }
+
+ .engine-field-head {
+ height: 28px;
+ border: none;
+ font-weight: 400;
+ }
+ }
+
+ &.engine-plain-field {
+ >.engine-field-variable {
+ position: absolute;
+ right: 5px;
+ top: 8px;
+ }
+ &:hover {
+ >.engine-field-variable {
+ opacity: 1;
+ }
+ }
+ }
+
+ &.engine-entry-field {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ height: 32px;
+ padding-left: 10px;
+ font-weight: 500;
+ border-top: 1px solid var(--color-line-normal, @normal-alpha-7);
+ border-bottom: 1px solid var(--color-line-normal, @normal-alpha-7);
+ background: var(--color-block-background-shallow, @normal-alpha-8);
+ margin-bottom: 6px;
+
+ >.engine-field-title {
+ letter-spacing: 1px;
+ }
+
+ >.engine-icontip {
+ margin-left: 2px;
+ }
+
+ >.engine-field-arrow {
+ position: absolute;
+ right: 5px;
+ top: 50%;
+ transform: translateY(-50%) rotate(-90deg);
+ opacity: 0.4;
+ }
+ &:hover {
+ >.engine-field-arrow {
+ opacity: 1;
+ }
+ }
+ }
+
+ &.engine-popup-field {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ height: 32px;
+ padding-left: 10px;
+ background: var(--color-block-background-shallow, @normal-alpha-8);
+ margin-bottom: 1px;
+
+ >.engine-field-title {
+ letter-spacing: 1px;
+ }
+
+ >.engine-icontip {
+ margin-left: 2px;
+ }
+
+ >.engine-field-icon {
+ position: absolute;
+ right: 5px;
+ top: 50%;
+ transform: translateY(-50%);
+ opacity: 0.6;
+ }
+ &:hover {
+ >.engine-field-icon {
+ opacity: 1;
+ }
+ }
+ }
+
+ &.engine-block-field {
+ >.engine-field-head {
+ > .engine-field-title {
+ letter-spacing: 1px;
+ }
+ >.engine-field-variable {
+ margin-left: 2px;
+ }
+ }
+ >.engine-field-body {
+ margin: 6px;
+ }
+ }
+
+ &.engine-inline-field {
+ display: flex;
+ align-items: center;
+ margin: 10px;
+ >.engine-field-head {
+ display: inline-flex;
+ background: none;
+ padding: 0;
+ border: none;
+
+ >.engine-field-title {
+ display: inline-flex;
+ width: 50px;
+ margin-right: 5px;
+ }
+ }
+ >.engine-field-body {
+ width: 100%;
+ display: inline-flex;
+ align-items: flex-start;
+ padding: 0;
+ margin: 0;
+ flex: 1;
+ position: relative;
+ }
+ >.engine-field-variable {
+ margin-left: 2px;
+ }
+ &:hover {
+ >.engine-field-variable {
+ opacity: 1;
+ }
+ }
+ }
+
+ &.engine-accordion-field {
+ >.engine-field-head {
+ position: relative;
+ cursor: pointer;
+ >.engine-field-title {
+ letter-spacing: 1px;
+ }
+ >.engine-field-arrow {
+ transform: rotate(180deg);
+ position: absolute;
+ right: 7px;
+ top: 7px;
+ transition: transform 0.1s ease;
+ opacity: 0.6;
+ }
+ >.engine-field-variable {
+ margin-left: 2px;
+ }
+ }
+ &.engine-collapsed {
+ >.engine-field-head {
+ margin-bottom: 6px;
+ }
+ >.engine-field-head > .engine-field-arrow {
+ transform: rotate(0);
+ }
+ >.engine-field-body {
+ display: none;
+ }
+ }
+ >.engine-field-body {
+ margin: 6px;
+ }
+ }
+}
+
+.engine-block-field,.engine-accordion-field,.engine-entry-field {
+ .engine-input-control {
+ margin: 10px;
+ }
+}
+
+.engine-field-tip-icon {
+ margin-left: 2px;
+}
diff --git a/packages/vision-polyfill/src/fields/fields.tsx b/packages/vision-polyfill/src/fields/fields.tsx
new file mode 100644
index 000000000..d864d4a29
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/fields.tsx
@@ -0,0 +1,379 @@
+import Icons from '@ali/ve-icons';
+import classNames from 'classnames';
+import { Component } from 'react';
+import { testType } from '@ali/ve-utils';
+import VEField, { IVEFieldProps } from './field';
+import { SettingField } from './setting-field';
+import VariableSwitcher from './variable-switcher';
+import popups from '@ali/ve-popups';
+
+import './fields.less';
+
+interface IHelpTip {
+ url?: string;
+ content?: string | JSX.Element;
+}
+
+function renderTip(tip: IHelpTip, prop?: { propName?: string }) {
+ const propName = prop && prop.propName;
+ if (!tip) {
+ return (
+
+
+
+ );
+ }
+ if (testType(tip) === 'object') {
+ return (
+
+
+
属性:{propName}
+
说明:{tip.content}
+
+
+ );
+ }
+ return (
+
+
+
属性:{propName}
+
说明:{tip}
+
+
+ );
+}
+
+export class PlainField extends VEField {
+ public static defaultProps = {
+ headDIY: true,
+ };
+
+ public static displayName = 'PlainField';
+
+ public renderHead(): null {
+ return null;
+ }
+}
+
+export class InlineField extends VEField {
+ public static displayName = 'InlineField';
+
+ constructor(props: any) {
+ super(props);
+ this.classNames = ['engine-setting-field', 'engine-inline-field'];
+ }
+
+ public renderFoot() {
+ return (
+
+
+
+ );
+ }
+}
+
+export class BlockField extends VEField {
+ public static displayName = 'BlockField';
+
+ constructor(props: IVEFieldProps) {
+ super(props);
+ this.classNames = ['engine-setting-field', 'engine-block-field', props.isGroup ? 'engine-group-field' : ''];
+ }
+
+ public renderHead() {
+ const { title, tip, propName } = this.props;
+ return [
+
+ {title}
+ ,
+ renderTip(tip, { propName }),
+ ,
+ ];
+ }
+}
+
+export class AccordionField extends VEField {
+ public readonly props: IVEFieldProps;
+
+ private willDetach?: () => any;
+
+ constructor(props: IVEFieldProps) {
+ super(props);
+ this._generateClassNames(props);
+ if (props.onExpandChange) {
+ this.willDetach = props.onExpandChange(() => this.forceUpdate());
+ }
+ }
+
+ public componentWillReceiveProps(nextProps: IVEFieldProps) {
+ this.classNames = this._generateClassNames(nextProps);
+ }
+
+ public componentWillUnmount() {
+ if (this.willDetach) {
+ this.willDetach();
+ }
+ }
+
+ public renderHead() {
+ const { title, tip, toggleExpand, propName } = this.props;
+ return (
+ toggleExpand && toggleExpand()}>
+
+ {title}
+ {renderTip(tip, { propName })}
+ {}
+
+ );
+ }
+
+ private _generateClassNames(props: IVEFieldProps) {
+ this.classNames = [
+ 'engine-setting-field',
+ 'engine-accordion-field',
+ props.isGroup ? 'engine-group-field' : '',
+ !props.isExpand ? 'engine-collapsed' : '',
+ ];
+ return this.classNames;
+ }
+}
+
+export class EntryField extends VEField {
+ constructor(props: any) {
+ super(props);
+ this.classNames = ['engine-setting-field', 'engine-entry-field'];
+ }
+
+ public render() {
+ const { propName, stageName, tip, title } = this.props;
+ const classNameList = classNames(...this.classNames, this.props.className);
+ const fieldProps: any = {};
+
+ if (stageName) {
+ // 为 stage 切换奠定基础
+ fieldProps['data-stage-target'] = this.props.stageName;
+ }
+
+ const innerElements = [
+
+ {title}
+ ,
+ renderTip(tip, { propName }),
+ ,
+ ];
+
+ return (
+
+ {innerElements}
+
+ );
+ }
+}
+
+export class PopupField extends VEField {
+ constructor(props: any) {
+ super(props);
+ this.classNames = ['engine-setting-field', 'engine-popup-field'];
+ }
+
+ public renderBody() {
+ return '';
+ }
+
+ public render() {
+ const { propName, stageName, tip, title } = this.props;
+ const classNameList = classNames(...this.classNames, this.props.className);
+ const fieldProps: any = {};
+
+ if (stageName) {
+ // 为 stage 切换奠定基础
+ fieldProps['data-stage-target'] = this.props.stageName;
+ }
+
+ return (
+ popups.popup({
+ cancelOnBlur: true,
+ content: this.props.children,
+ position: 'left bottom',
+ showClose: true,
+ sizeFixed: true,
+ target: e.currentTarget,
+ })
+ }
+ >
+ {title}
+ {renderTip(tip, { propName })}
+
+
+
+ );
+ }
+}
+
+export class CaptionField extends VEField {
+ constructor(props: IVEFieldProps) {
+ super(props);
+ this.classNames = ['engine-setting-field', 'engine-caption-field'];
+ }
+
+ public renderHead() {
+ const { title, tip, propName } = this.props;
+ return (
+
+ {title}
+ {renderTip(tip, { propName })}
+
+ );
+ }
+}
+
+export class Stage extends Component {
+ public readonly props: {
+ key: any;
+ stage: any;
+ current?: boolean;
+ direction?: any;
+ };
+
+ public stage: any;
+
+ public additionClassName: string;
+
+ public shell: Element | null = null;
+
+ private willDetach: () => any;
+
+ public componentWillMount() {
+ this.stage = this.props.stage;
+ if (this.stage.onCurrentTabChange) {
+ this.willDetach = this.stage.onCurrentTabChange(() => this.forceUpdate());
+ }
+ }
+
+ public componentDidMount() {
+ this.doSkate();
+ }
+
+ public componentWillReceiveProps(props: any) {
+ if (props.stage !== this.stage) {
+ this.stage = props.stage;
+ if (this.willDetach) {
+ this.willDetach();
+ }
+ if (this.stage.onCurrentTabChange) {
+ this.willDetach = this.stage.onCurrentTabChange(() => this.forceUpdate());
+ }
+ }
+ }
+
+ public componentDidUpdate() {
+ this.doSkate();
+ }
+
+ public componentWillUnmount() {
+ if (this.willDetach) {
+ this.willDetach();
+ }
+ }
+
+ public doSkate() {
+ if (this.additionClassName) {
+ setTimeout(() => {
+ const elem = this.shell;
+ if (elem && elem.classList) {
+ if (this.props.current) {
+ elem.classList.remove(this.additionClassName);
+ } else {
+ elem.classList.add(this.additionClassName);
+ }
+ this.additionClassName = '';
+ }
+ }, 10);
+ }
+ }
+
+ public render() {
+ const { stage } = this;
+ let content = null;
+ let tabs = null;
+
+ let className = 'engine-settings-stage';
+
+ if (stage.getTabs) {
+ const selected = stage.getNode();
+ // stat for cache
+ stage.stat();
+ const currentTab = stage.getCurrentTab();
+
+ if (stage.hasTabs()) {
+ className += ' engine-has-tabs';
+ tabs = (
+
+ {stage.getTabs().map((tab: any) => (
+
stage.setCurrentTab(tab)}
+ >
+ {tab.getTitle()}
+ {renderTip(tab.getTip())}
+
+ ))}
+
+ );
+ }
+
+ if (currentTab) {
+ if (currentTab.getVisibleItems) {
+ content = currentTab
+ .getVisibleItems()
+ .map((item: any) => );
+ } else if (currentTab.getSetter) {
+ content = (
+
+ );
+ }
+ }
+ } else {
+ content = stage.getContent();
+ }
+
+ if (this.props.current) {
+ if (this.props.direction) {
+ this.additionClassName = `engine-stagein-${this.props.direction}`;
+ className += ` ${this.additionClassName}`;
+ }
+ } else if (this.props.direction) {
+ this.additionClassName = `engine-stageout-${this.props.direction}`;
+ }
+
+ let stageBacker = null;
+ if (stage.hasBack()) {
+ className += ' engine-has-backer';
+ stageBacker = (
+
+
+ {stage.getTitle()}
+ {renderTip(stage.getTip())}
+
+ );
+ }
+
+ return (
+ {
+ this.shell = ref;
+ }}
+ className={className}
+ >
+ {stageBacker}
+ {tabs}
+
{content}
+
+ );
+ }
+}
diff --git a/packages/vision-polyfill/src/fields/index.ts b/packages/vision-polyfill/src/fields/index.ts
new file mode 100644
index 000000000..25edd9a28
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/index.ts
@@ -0,0 +1,2 @@
+export * from './setting-field';
+export * from './fields';
diff --git a/packages/vision-polyfill/src/fields/inlinetip.tsx b/packages/vision-polyfill/src/fields/inlinetip.tsx
new file mode 100644
index 000000000..6ba0ec658
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/inlinetip.tsx
@@ -0,0 +1,30 @@
+import { Component } from 'react';
+
+export interface InlineTipProps {
+ position: string;
+ theme?: 'green' | 'black';
+ children: React.ReactNode;
+}
+
+export default class InlineTip extends Component {
+ public static displayName = 'InlineTip';
+
+ public static defaultProps = {
+ position: 'auto',
+ theme: 'black',
+ };
+
+ public render(): React.ReactNode {
+ const { position, theme, children } = this.props;
+ return (
+
+ {children}
+
+ );
+ }
+}
diff --git a/packages/vision-polyfill/src/fields/setting-field.tsx b/packages/vision-polyfill/src/fields/setting-field.tsx
new file mode 100644
index 000000000..49c8763d1
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/setting-field.tsx
@@ -0,0 +1,189 @@
+import VariableSetter from './variable-setter';
+import context from '../context';
+import { VE_HOOKS } from '../base/const';
+import {
+ AccordionField,
+ BlockField,
+ EntryField,
+ InlineField,
+ PlainField,
+ PopupField,
+} from './fields';
+
+import { ComponentClass, Component, isValidElement, createElement } from 'react';
+import { editorCabin, setters } from '@ali/lowcode-engine';
+
+const { getSetter } = setters;
+const { createSetterContent } = editorCabin;
+
+function isReactClass(obj: any): obj is ComponentClass {
+ return (
+ obj &&
+ obj.prototype &&
+ (obj.prototype.isReactComponent || obj.prototype instanceof Component)
+ );
+}
+
+interface IExtraProps {
+ stageName?: string;
+ isGroup?: boolean;
+ isExpand?: boolean;
+ propName?: string;
+ toggleExpand?: () => any;
+ onExpandChange?: () => any;
+}
+
+const FIELD_TYPE_MAP: any = {
+ accordion: AccordionField,
+ block: BlockField,
+ entry: EntryField,
+ inline: InlineField,
+ plain: PlainField,
+ popup: PopupField,
+ tab: AccordionField,
+};
+
+export class SettingField extends Component {
+ public readonly props: {
+ prop: any;
+ selected?: boolean;
+ forceDisplay?: string;
+ className?: string;
+ children?: JSX.Element | string;
+ compact?: boolean;
+ key?: string;
+ addonProps?: object;
+ };
+
+ /**
+ * VariableSetter placeholder
+ */
+ public variableSetter: any;
+
+ constructor(props: any) {
+ super(props);
+
+ this.variableSetter = getSetter('VariableSetter')?.component || VariableSetter;
+ }
+
+ public render() {
+ const { prop, selected, addonProps } = this.props;
+ const display = this.props.forceDisplay || prop.getDisplay();
+
+ if (display === 'none') {
+ return null;
+ }
+
+ // 标准的属性,即每一个 Field 在 VE 下都拥有的属性
+ const standardProps = {
+ className: this.props.className,
+ compact: this.props.compact,
+
+ isSupportMultiSetter: this.supportMultiSetter(),
+ isSupportVariable: prop.isSupportVariable(),
+ isUseVariable: prop.isUseVariable(),
+ prop,
+ setUseVariable: () => prop.setUseVariable(!prop.isUseVariable()),
+ tip: prop.getTip(),
+ title: prop.getTitle(),
+ };
+
+ // 部分 Field 所需要的额外 fieldProps
+ const extraProps = {};
+ const ctx = context;
+ const plugin = ctx.getPlugin(VE_HOOKS.VE_SETTING_FIELD_PROVIDER);
+ let Field;
+ if (typeof plugin === 'function') {
+ Field = plugin(display, FIELD_TYPE_MAP, prop);
+ }
+ if (!Field) {
+ Field = FIELD_TYPE_MAP[display] || PlainField;
+ }
+ this._prepareProps(display, extraProps);
+
+ if (display === 'entry') {
+ return ;
+ }
+
+ let setter;
+ const props: any = {
+ prop,
+ selected,
+ };
+ const fieldProps = { ...standardProps, ...extraProps };
+
+ if (prop.isUseVariable() && !this.variableSetter.isPopup) {
+ props.placeholder = '请输入表达式: ${var}';
+ props.key = `${prop.getId()}-variable`;
+ setter = createElement(this.variableSetter, props);
+ return {setter};
+ }
+
+ // for composited prop
+ if (prop.getVisibleItems) {
+ setter = prop
+ .getVisibleItems()
+ .map((item: any) => (
+
+ ));
+ return {setter};
+ }
+
+ setter = prop.getSetter();
+ if (
+ typeof setter === 'object' &&
+ 'componentName' in setter &&
+ !(isValidElement(setter) || isReactClass(setter))
+ ) {
+ const { componentName: setterType, props: setterProps } = setter as any;
+ setter = createSetterContent(setterType, {
+ ...addonProps,
+ ...setterProps,
+ ...props,
+ });
+ } else {
+ setter = createSetterContent(setter, {
+ ...addonProps,
+ ...props,
+ });
+ }
+
+ return {setter};
+ }
+
+ private supportMultiSetter() {
+ const { prop } = this.props;
+ const setter = prop && prop.getConfig && prop.getConfig('setter');
+ return prop.isSupportVariable() || Array.isArray(setter);
+ }
+
+ private _prepareProps(displayType: string, extraProps: IExtraProps): void {
+ const { prop } = this.props;
+ extraProps.propName = prop.isGroup()
+ ? '组合属性,无属性名称'
+ : prop.getName();
+ switch (displayType) {
+ case 'title':
+ break;
+ case 'block':
+ Object.assign(extraProps, { isGroup: prop.isGroup() });
+ break;
+ case 'accordion':
+ Object.assign(extraProps, {
+ headDIY: true,
+ isExpand: prop.isExpand(),
+ isGroup: prop.isGroup(),
+ onExpandChange: () => prop.onExpandChange(() => this.forceUpdate()),
+ toggleExpand: () => {
+ prop.toggleExpand();
+ },
+ });
+ break;
+ case 'entry':
+ Object.assign(extraProps, { stageName: prop.getName() });
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/packages/vision-polyfill/src/fields/variable-setter.less b/packages/vision-polyfill/src/fields/variable-setter.less
new file mode 100644
index 000000000..6ea5653d3
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/variable-setter.less
@@ -0,0 +1,43 @@
+@import '~@ali/ve-less-variables/index.less';
+
+.engine-input-control {
+ box-sizing: border-box;
+ font-size: 12px;
+ font-family: Consolas, "Courier New", Courier, FreeMono, monospace;
+ color: var(--color-text, @dark-alpha-3);
+ background: var(--color-field-background, @white-alpha-1);
+ border: 1px solid var(--color-field-border, @normal-alpha-5);
+ flex: 1;
+ border-radius: @global-border-radius;
+ max-height: 200px;
+
+ &:hover {
+ border-color: var(--color-field-border-hover, @normal-alpha-4);
+ }
+
+ &.engine-focused {
+ border-color: var(--color-field-border-active, @normal-alpha-3);
+ }
+
+ textarea {
+ resize: none;
+ }
+
+ >.engine-input {
+ box-sizing: border-box;
+ padding: 6px;
+ display: block;
+ font-size: 12px;
+ line-height: 16px;
+ color: var(--color-text, @dark-alpha-3);
+ width: 100%;
+ border: 0;
+ margin: 0;
+ background: transparent;
+ outline: none;
+
+ &::-webkit-input-placeholder {
+ color: var(--color-field-placeholder, @normal-alpha-5);
+ }
+ }
+}
diff --git a/packages/vision-polyfill/src/fields/variable-setter.tsx b/packages/vision-polyfill/src/fields/variable-setter.tsx
new file mode 100644
index 000000000..f520c128f
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/variable-setter.tsx
@@ -0,0 +1,86 @@
+import './variable-setter.less';
+import { Component } from 'react';
+
+class Input extends Component {
+ public props: {
+ value: string;
+ placeholder: string;
+ onChange: (val: any) => any;
+ };
+
+ public state: { focused: boolean };
+
+ constructor(props: object) {
+ super(props);
+ this.state = {
+ focused: false,
+ };
+ }
+
+ public componentDidMount() {
+ this.adjustTextAreaHeight();
+ }
+
+ private domRef: HTMLTextAreaElement | null = null;
+
+ public adjustTextAreaHeight() {
+ if (!this.domRef) {
+ return;
+ }
+ this.domRef.style.height = '1px';
+ const calculatedHeight = this.domRef.scrollHeight;
+ this.domRef.style.height = calculatedHeight >= 200 ? '200px' : `${calculatedHeight }px`;
+ }
+
+ public render() {
+ const { value, placeholder, onChange } = this.props;
+ return (
+
+
+ );
+ }
+}
+
+export default class VariableSetter extends Component<{
+ prop: any;
+ placeholder: string;
+}> {
+ public willDetach: () => any;
+
+ public componentWillMount() {
+ this.willDetach = this.props.prop.onValueChange(() => this.forceUpdate());
+ }
+
+ public componentWillUnmount() {
+ if (this.willDetach) {
+ this.willDetach();
+ }
+ }
+
+ public render() {
+ const { prop } = this.props;
+ return (
+ prop.setVariableValue(val)}
+ />
+ );
+ }
+}
diff --git a/packages/vision-polyfill/src/fields/variable-switcher.less b/packages/vision-polyfill/src/fields/variable-switcher.less
new file mode 100644
index 000000000..0991b0ed6
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/variable-switcher.less
@@ -0,0 +1,20 @@
+@import '~@ali/ve-less-variables/index.less';
+
+.engine-field-variable-switcher {
+ cursor: pointer;
+ opacity: 0.6;
+ margin-left: 2px;
+
+ &.engine-active {
+ opacity: 1;
+ background: var(--color-brand, @brand-color-1);
+ color: #fff !important;
+ border-radius: 3px;
+ margin-left: 4px;
+
+ svg {
+ height: 22px !important;
+ width: 22px !important;
+ }
+ }
+}
diff --git a/packages/vision-polyfill/src/fields/variable-switcher.tsx b/packages/vision-polyfill/src/fields/variable-switcher.tsx
new file mode 100644
index 000000000..b825efc89
--- /dev/null
+++ b/packages/vision-polyfill/src/fields/variable-switcher.tsx
@@ -0,0 +1,61 @@
+import VariableSetter from './variable-setter';
+import Icons from '@ali/ve-icons';
+import { IVEFieldProps } from './field';
+import './variable-switcher.less';
+import { Component } from 'react';
+import { setters } from '@ali/lowcode-engine';
+
+const { getSetter } = setters;
+
+interface IState {
+ visible: boolean;
+}
+
+export default class VariableSwitcher extends Component {
+ private ref: HTMLElement | null = null;
+
+ private VariableSetter: any;
+
+ constructor(props: IVEFieldProps) {
+ super(props);
+
+ this.VariableSetter = getSetter('VariableSetter')?.component || VariableSetter;
+
+ this.state = {
+ visible: false,
+ };
+ }
+
+ public render() {
+ const { isUseVariable, prop } = this.props;
+ const { visible } = this.state;
+ const isSupportVariable = prop.isSupportVariable();
+ const tip = !isUseVariable ? '绑定变量' : prop.getVariableValue();
+ if (!isSupportVariable) {
+ return null;
+ }
+ return (
+
+ {
+ e.stopPropagation();
+ if (this.VariableSetter.isPopup) {
+ this.VariableSetter.show({
+ prop,
+ });
+ } else {
+ prop.setUseVariable(!isUseVariable);
+ }
+ }}
+ >
+ 绑定变量
+
+
+ );
+ }
+}
diff --git a/packages/vision-polyfill/src/flags.ts b/packages/vision-polyfill/src/flags.ts
new file mode 100644
index 000000000..2a68db857
--- /dev/null
+++ b/packages/vision-polyfill/src/flags.ts
@@ -0,0 +1,152 @@
+import domReady from 'domready';
+
+import { EventEmitter } from 'events';
+
+const Shells = ['iphone6'];
+
+export class Flags {
+ public emitter: EventEmitter;
+
+ public flags: string[];
+
+ public ready: boolean;
+
+ public lastFlags: string[];
+
+ public lastShell: string;
+
+ private lastSimulatorDevice: string;
+
+ constructor() {
+ this.emitter = new EventEmitter();
+ this.flags = [];
+
+ domReady(() => {
+ this.ready = true;
+ this.applyFlags();
+ });
+ }
+
+ public setDragMode(flag: boolean) {
+ if (flag) {
+ this.add('drag-mode');
+ } else {
+ this.remove('drag-mode');
+ }
+ }
+
+ public setPreviewMode(flag: boolean) {
+ if (flag) {
+ this.add('preview-mode');
+ this.remove('design-mode');
+ } else {
+ this.add('design-mode');
+ this.remove('preview-mode');
+ }
+ }
+
+ public setWithShell(shell: string) {
+ if (shell === this.lastShell) {
+ return;
+ }
+ if (this.lastShell) {
+ this.remove(`with-${this.lastShell}shell`);
+ }
+ if (shell) {
+ if (Shells.indexOf(shell) < 0) {
+ shell = Shells[0];
+ }
+ this.add(`with-${shell}shell`);
+ this.lastShell = shell;
+ }
+ }
+
+ public setSimulator(device: string) {
+ if (this.lastSimulatorDevice) {
+ this.remove(`simulator-${this.lastSimulatorDevice}`);
+ }
+ if (device !== '' && device !== 'pc') {
+ this.add(`simulator-${device}`);
+ }
+ this.lastSimulatorDevice = device;
+ }
+
+ public setHideSlate(flag: boolean) {
+ if (this.has('slate-fixed')) {
+ return;
+ }
+ if (flag) {
+ this.add('hide-slate');
+ } else {
+ this.remove('hide-slate');
+ }
+ }
+
+ public setSlateFixedMode(flag: boolean) {
+ if (flag) {
+ this.remove('hide-slate');
+ this.add('slate-fixed');
+ } else {
+ this.remove('slate-fixed');
+ }
+ }
+
+ public setSlateFullMode(flag: boolean) {
+ if (flag) {
+ this.add('slate-full-screen');
+ } else {
+ this.remove('slate-full-screen');
+ }
+ }
+
+ public getFlags() {
+ return this.flags;
+ }
+
+ public applyFlags(modifiedFlag?: string) {
+ if (!this.ready) {
+ return;
+ }
+
+ const doc = document.documentElement;
+ if (this.lastFlags) {
+ this.lastFlags.filter((flag: string) => this.flags.indexOf(flag) < 0).forEach((flag) => {
+ doc.classList.remove(`engine-${flag}`);
+ });
+ }
+ this.flags.forEach((flag) => {
+ doc.classList.add(`engine-${flag}`);
+ });
+
+ this.lastFlags = this.flags.slice(0);
+ this.emitter.emit('flagschange', this.flags, modifiedFlag);
+ }
+
+ public has(flag: string) {
+ return this.flags.indexOf(flag) > -1;
+ }
+
+ public add(flag: string) {
+ if (!this.has(flag)) {
+ this.flags.push(flag);
+ this.applyFlags(flag);
+ }
+ }
+
+ public remove(flag: string) {
+ const i = this.flags.indexOf(flag);
+ if (i > -1) {
+ this.flags.splice(i, 1);
+ this.applyFlags(flag);
+ }
+ }
+
+ public onFlagsChange(func: () => any) {
+ this.emitter.on('flagschange', func);
+ return () => {
+ this.emitter.removeListener('flagschange', func);
+ };
+ }
+}
+
+export default new Flags();
diff --git a/packages/vision-polyfill/src/i18n-util/index.d.ts b/packages/vision-polyfill/src/i18n-util/index.d.ts
new file mode 100644
index 000000000..254e4ed65
--- /dev/null
+++ b/packages/vision-polyfill/src/i18n-util/index.d.ts
@@ -0,0 +1,84 @@
+import { Node } from '@ali/lowcode-designer';
+
+declare enum LANGUAGES {
+ zh_CN = 'zh_CN',
+ en_US = 'en_US'
+}
+
+export interface I18nRecord {
+ type?: 'i18n';
+ [key: string]: string;
+ /**
+ * i18n unique key
+ */
+ key?: string;
+}
+
+export interface I18nRecordData {
+ gmtCreate: Date;
+ gmtModified: Date;
+ i18nKey: string;
+ i18nText: I18nRecord;
+ id: number;
+}
+
+export interface II18nUtilConfigs {
+ items?: {};
+ /**
+ * 是否禁用初始化加载
+ */
+ disableInstantLoad?: boolean;
+ /**
+ * 初始化的时候是否全量加载
+ */
+ disableFullLoad?: boolean;
+ loader?: (configs: ILoaderConfigs) => Promise;
+ remover?: (key: string, dic: I18nRecord) => Promise;
+ saver?: (key: string, dic: I18nRecord) => Promise;
+}
+
+export interface ILoaderConfigs {
+ /**
+ * search keywords
+ */
+ keyword?: string;
+ /**
+ * should load all i18n items
+ */
+ isFull?: boolean;
+ /**
+ * search i18n item based on uniqueKey
+ */
+ key?: string;
+}
+
+export interface II18nUtil {
+ init(config: II18nUtilConfigs): void;
+ isInitialized(): boolean;
+ isReady(): boolean;
+ attach(prop: object, value: I18nRecord, updator: () => any);
+ search(keyword: string, silent?: boolean);
+ load(configs: ILoaderConfigs): Promise;
+ /**
+ * Get local i18n Record
+ * @param key
+ * @param lang
+ */
+ get(key: string, lang: string, info?: {
+ node?: Node,
+ path?: string,
+ }): string | I18nRecord;
+ getFromRemote(key: string): Promise;
+ getItem(key: string, forceData?: boolean): any;
+ getItems(): I18nRecord[];
+ update(key: string, doc: I18nRecord, lang: LANGUAGES);
+ create(doc: I18nRecord, lang: LANGUAGES): string;
+ remove(key: string): Promise;
+
+ onReady(func: () => any);
+ onRowsChange(func: () => any);
+ onChange(func: (dic: I18nRecord) => any);
+}
+
+declare const i18nUtil: II18nUtil;
+export default i18nUtil;
diff --git a/packages/vision-polyfill/src/i18n-util/index.js b/packages/vision-polyfill/src/i18n-util/index.js
new file mode 100644
index 000000000..97efdf4ad
--- /dev/null
+++ b/packages/vision-polyfill/src/i18n-util/index.js
@@ -0,0 +1,357 @@
+import { EventEmitter } from 'events';
+import { editorCabin, editor } from '@ali/lowcode-engine';
+import lodashGet from 'lodash.get';
+import { GlobalEvent } from '@ali/lowcode-types';
+
+const { obx } = editorCabin;
+
+let keybase = Date.now();
+function keygen(maps) {
+ let key;
+ do {
+ key = `i18n-${(keybase).toString(36)}`;
+ keybase += 1;
+ } while (key in maps);
+ return key;
+}
+
+class DocItem {
+ constructor(parent, doc, unInitial) {
+ this.parent = parent;
+ const { use, ...strings } = doc;
+ this.doc = obx({
+ type: 'i18n',
+ ...strings,
+ });
+ this.emitter = new EventEmitter();
+ this.nodeList = new Map();
+ this.onChange((doc, oldDoc) => {
+ if (this.nodeList.size <= 0) {
+ return;
+ }
+ this.nodeList.forEach(({
+ node,
+ path,
+ }) => {
+ const prop = node.settingEntry.getProp(path);
+ const changeInfo = {
+ key: prop?.key,
+ prop,
+ newValue: doc,
+ oldValue: oldDoc,
+ };
+ node.emitPropChange(changeInfo);
+ editor.emit(GlobalEvent.Node.Prop.Change, {
+ node,
+ ...changeInfo,
+ });
+ editor.emit(GlobalEvent.Node.Prop.InnerChange, {
+ node,
+ ...changeInfo,
+ });
+ });
+ });
+ this.inited = unInitial !== true;
+ }
+
+ getKey() {
+ return this.doc.key;
+ }
+
+ getDoc(lang) {
+ if (lang) {
+ return this.doc[lang];
+ }
+ return Object.assign({}, this.doc);
+ }
+
+ setDoc(doc, lang, initial) {
+ const oldValue = Object.assign({}, this.doc);
+ if (lang) {
+ this.doc[lang] = doc;
+ } else {
+ const { use, strings } = doc || {};
+ Object.assign(this.doc, doc, strings);
+ }
+ this.emitter.emit('change', this.doc, oldValue);
+
+ if (initial) {
+ this.inited = true;
+ } else if (this.inited) {
+ this.parent._saveChange(this.doc.key, this.doc);
+ }
+ }
+
+ collectNode = (nodeInfo) => {
+ const key = lodashGet(nodeInfo, 'node.id');
+ if (!key) {
+ return;
+ }
+ this.nodeList.set(key, nodeInfo);
+ };
+
+ remove() {
+ if (!this.inited) return Promise.reject('not initialized');
+
+ const { key, ...doc } = this.doc; // eslint-disable-line
+ this.emitter.emit('change', doc);
+ return this.parent.remove(this.getKey());
+ }
+
+ onChange(func) {
+ this.emitter.on('change', func);
+ return () => {
+ this.emitter.removeListener('change', func);
+ };
+ }
+}
+
+class I18nUtil {
+ constructor() {
+ this.emitter = new EventEmitter();
+ // original data source from remote
+ this.i18nData = {};
+ // current i18n records on the left pane
+ this.items = [];
+ this.maps = {};
+ // full list of i18n records for synchronized call
+ this.fullList = [];
+ this.fullMap = {};
+
+ this.config = {};
+ this.ready = false;
+ this.isInited = false;
+ }
+
+ _prepareItems(items, isFull = false, isSilent = false) {
+ this[isFull ? 'fullList' : 'items'] = items.map((dict) => {
+ let item = this[isFull ? 'fullMap' : 'maps'][dict.key];
+ if (item) {
+ item.setDoc(dict, null, true);
+ } else {
+ item = new DocItem(this, dict);
+ this[isFull ? 'fullMap' : 'maps'][dict.key] = item;
+ }
+ return item;
+ });
+
+ if (this.ready && !isSilent) {
+ this.emitter.emit('rowschange');
+ this.emitter.emit('change');
+ } else {
+ this.ready = true;
+ this.emitter.emit('ready');
+ }
+ }
+
+ _load(configs = {}, silent) {
+ if (!this.config.loader) {
+ console.error(new Error('Please load loader while init I18nUtil.'));
+ return Promise.reject();
+ }
+
+ return this.config.loader(configs).then((data) => {
+ if (configs.i18nKey) {
+ return Promise.resolve(data.i18nText);
+ }
+ this._prepareItems(data.data, configs.isFull, silent);
+ // set pagination data to i18nData
+ this.i18nData = data;
+ if (!silent) {
+ this.emitter.emit('rowschange');
+ this.emitter.emit('change');
+ }
+ return Promise.resolve(this.items.map(i => i.getDoc()));
+ });
+ }
+
+ _saveToItems(key, dict) {
+ let item = null;
+ item = this.items.find(doc => doc.getKey() === key);
+ if (!item) {
+ item = this.fullList.find(doc => doc.getKey() === key);
+ }
+
+ if (!item && (this.maps[key] || this.fullMap[key])) {
+ // 如果从 items 和 fullList 里面找到 DocItem,就从 maps/fullMap 里面取
+ item = this.maps[key] || this.fullMap[key];
+ this.items.unshift(item);
+ this.fullList.unshift(item);
+ }
+
+ if (item) {
+ item.setDoc(dict);
+ } else {
+ item = new DocItem(this, {
+ key,
+ ...dict,
+ });
+ this.items.unshift(item);
+ this.fullList.unshift(item);
+ this.maps[key] = item;
+ this.fullMap[key] = item;
+ this._saveChange(key, dict, true);
+ }
+ }
+
+ _saveChange(key, dict, rowschange) {
+ if (rowschange) {
+ this.emitter.emit('rowschange');
+ }
+ this.emitter.emit('change');
+ if (dict === null) {
+ delete this.maps[key];
+ delete this.fullMap[key];
+ }
+ return this._save(key, dict);
+ }
+
+ _save(key, dict) {
+ const saver = dict === null ? this.config.remover : this.config.saver;
+ if (!saver) return Promise.reject('Saver function is not set');
+ return saver(key, dict);
+ }
+
+ init(config) {
+ if (this.isInited) return;
+ this.config = config || {};
+ if (this.config.items) {
+ // inject to current page
+ this._prepareItems(this.config.items);
+ }
+ if (!this.config.disableInstantLoad) {
+ this._load({ isFull: !this.config.disableFullLoad });
+ }
+ this.isInited = true;
+ }
+
+ isInitialized() {
+ return this.isInited;
+ }
+
+ isReady() {
+ return this.ready;
+ }
+
+ // add events updater when i18n record change
+ // we should notify engine's view to change
+ attach(prop, value, updator) {
+ const isI18nValue = value && value.type === 'i18n' && value.key;
+ const key = isI18nValue ? value.key : null;
+ if (prop.i18nLink) {
+ if (isI18nValue && (key === prop.i18nLink.key)) {
+ return prop.i18nLink;
+ }
+ prop.i18nLink.detach();
+ }
+
+ if (isI18nValue) {
+ return {
+ key,
+ detach: this.getItem(key, value).onChange(updator),
+ };
+ }
+
+ return null;
+ }
+
+ /**
+ * 搜索 i18n 词条
+ *
+ * @param {any} keyword 搜索关键字
+ * @param {boolean} [silent=false] 是否刷新左侧的 i18n 数据
+ * @returns
+ *
+ * @memberof I18nUtil
+ */
+ search(keyword, silent = false) {
+ return this._load({ keyword }, silent);
+ }
+
+ load(configs = {}) {
+ return this._load(configs);
+ }
+
+ get(key, lang, nodeInfo) {
+ const item = this.getItem(key);
+ if (item) {
+ item.collectNode(nodeInfo);
+ return item.getDoc(lang);
+ }
+ return null;
+ }
+
+ getFromRemote(key) {
+ return this._load({ i18nKey: key });
+ }
+
+ getItem(key, forceData) {
+ if (forceData && !this.maps[key] && !this.fullList[key]) {
+ const item = new DocItem(this, {
+ key,
+ ...forceData,
+ }, true);
+ this.maps[key] = item;
+ this.fullMap[key] = item;
+ this.fullList.push(item);
+ this.items.push(item);
+ }
+ return this.maps[key] || this.fullMap[key];
+ }
+
+ getItems() {
+ return this.items;
+ }
+
+ update(key, doc, lang) {
+ let dict = this.get(key) || {};
+ if (!lang) {
+ dict = doc;
+ } else {
+ dict[lang] = doc;
+ }
+ this._saveToItems(key, dict);
+ }
+
+ create(doc, lang) {
+ const dict = lang ? { [lang]: doc } : doc;
+ const key = keygen(this.fullMap);
+ this._saveToItems(key, dict);
+ return key;
+ }
+
+ remove(key) {
+ const index = this.items.findIndex(item => item.getKey() === key);
+ const indexG = this.fullList.findIndex(item => item.getKey() === key);
+ if (index > -1) {
+ this.items.splice(index, 1);
+ }
+ if (indexG > -1) {
+ this.fullList.splice(index, 1);
+ }
+ return this._saveChange(key, null, true);
+ }
+
+ onReady(func) {
+ this.emitter.on('ready', func);
+ return () => {
+ this.emitter.removeListener('ready', func);
+ };
+ }
+
+ onRowsChange(func) {
+ this.emitter.on('rowschange', func);
+ return () => {
+ this.emitter.removeListener('rowschange', func);
+ };
+ }
+
+ onChange(func) {
+ this.emitter.on('change', func);
+ return () => {
+ this.emitter.removeListener('change', func);
+ };
+ }
+}
+
+export default new I18nUtil();
diff --git a/packages/vision-polyfill/src/index.ts b/packages/vision-polyfill/src/index.ts
new file mode 100644
index 000000000..e8a902c4a
--- /dev/null
+++ b/packages/vision-polyfill/src/index.ts
@@ -0,0 +1,155 @@
+import { createElement } from 'react';
+import { render } from 'react-dom';
+import * as utils from '@ali/ve-utils';
+import Popup from '@ali/ve-popups';
+import Icons from '@ali/ve-icons';
+import logger from '@ali/vu-logger';
+import I18nUtil from './i18n-util';
+import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS, VERSION as Version } from './base/const';
+import Bus from './bus';
+import { skeleton, designer, editor, plugins, init, hotkey as Hotkey, monitor, designerCabin } from '@ali/lowcode-engine';
+import Panes from './panes';
+import Exchange from './exchange';
+import context from './context';
+import VisualManager from './base/visualManager';
+import VisualDesigner from './base/visualDesigner';
+import Trunk from './bundle/trunk';
+import Prototype from './bundle/prototype';
+import Bundle from './bundle/bundle';
+import Pages from './pages';
+import * as Field from './fields';
+import Prop from './prop';
+import Env from './env';
+import DragEngine from './drag-engine';
+// import Flags from './base/flags';
+import Viewport from './viewport';
+import Project from './project';
+import Symbols from './symbols';
+import { invariant } from './utils';
+import './reducers';
+
+import './vision.less';
+
+invariant((window as any).AliLowCodeEngine, 'AliLowCodeEngine is required, since vision polyfill is totally based on AliLowCodeEngine');
+
+/**
+ * VE.ui.xxx
+ *
+ * Core UI Components
+ */
+const ui = {
+ Field,
+ Icon: Icons,
+ Icons,
+ Popup,
+};
+
+const modules = {
+ VisualManager,
+ VisualDesigner,
+ I18nUtil,
+ Prop,
+};
+
+const { registerMetadataTransducer } = designerCabin;
+
+const VisualEngine = {
+ designer,
+ designerCabin,
+ editor,
+ skeleton,
+ /**
+ * VE.Popup
+ */
+ Popup,
+ /**
+ * VE Utils
+ */
+ utils,
+ I18nUtil,
+ Hotkey,
+ Env,
+ monitor,
+ /* pub/sub 集线器 */
+ Bus,
+ /* 事件 */
+ EVENTS,
+ /* 修饰方法 */
+ HOOKS,
+ Exchange,
+ context,
+ /**
+ * VE.init
+ *
+ * Initialized the whole VisualEngine UI
+ */
+ init,
+ ui,
+ Panes,
+ modules,
+ Trunk,
+ Prototype,
+ Bundle,
+ Pages,
+ DragEngine,
+ Viewport,
+ Version,
+ Project,
+ logger,
+ Symbols,
+ registerMetadataTransducer,
+ plugins,
+ // Flags,
+};
+
+(window as any).VisualEngine = VisualEngine;
+
+export default VisualEngine;
+
+export {
+ designer,
+ designerCabin,
+ editor,
+ skeleton,
+ /**
+ * VE.Popup
+ */
+ Popup,
+ /**
+ * VE Utils
+ */
+ utils,
+ I18nUtil,
+ Hotkey,
+ Env,
+ monitor,
+ /* pub/sub 集线器 */
+ Bus,
+ /* 事件 */
+ EVENTS,
+ /* 修饰方法 */
+ HOOKS,
+ Exchange,
+ context,
+ /**
+ * VE.init
+ *
+ * Initialized the whole VisualEngine UI
+ */
+ init,
+ ui,
+ Panes,
+ modules,
+ Trunk,
+ Prototype,
+ Bundle,
+ Pages,
+ DragEngine,
+ Viewport,
+ Version,
+ Project,
+ logger,
+ Symbols,
+ registerMetadataTransducer,
+ plugins,
+};
diff --git a/packages/vision-polyfill/src/module.d.ts b/packages/vision-polyfill/src/module.d.ts
new file mode 100644
index 000000000..5392ba0b6
--- /dev/null
+++ b/packages/vision-polyfill/src/module.d.ts
@@ -0,0 +1 @@
+declare module '@ali/vu-css-style';
diff --git a/packages/vision-polyfill/src/pages.ts b/packages/vision-polyfill/src/pages.ts
new file mode 100644
index 000000000..0a5d4fbd7
--- /dev/null
+++ b/packages/vision-polyfill/src/pages.ts
@@ -0,0 +1,147 @@
+import { RootSchema } from '@ali/lowcode-types';
+import { DocumentModel } from '@ali/lowcode-designer';
+import { designer } from '@ali/lowcode-engine';
+import NodeCacheVisitor from './root-node-visitor';
+
+const { project } = designer;
+
+export interface PageDataV1 {
+ id: string;
+ componentsTree: RootSchema[];
+ layout: RootSchema;
+ [dataAddon: string]: any;
+}
+
+export interface PageDataV2 {
+ id: string;
+ componentsTree: RootSchema[];
+ [dataAddon: string]: any;
+}
+
+function isPageDataV1(obj: any): obj is PageDataV1 {
+ return obj && obj.layout;
+}
+function isPageDataV2(obj: any): obj is PageDataV2 {
+ return obj && obj.componentsTree && Array.isArray(obj.componentsTree);
+}
+
+type OldPageData = PageDataV1 | PageDataV2;
+
+const pages = Object.assign(project, {
+ setPages(pages: OldPageData[]) {
+ if (!pages || !Array.isArray(pages) || pages.length === 0) {
+ throw new Error('pages schema 不合法');
+ }
+ // todo: miniapp
+ let componentsTree: any = [];
+ if (window.pageConfig?.isNoCodeMiniApp) {
+ // 小程序多页面
+ pages.forEach((item: any) => {
+ if (isPageDataV1(item)) {
+ componentsTree.push(item.layout);
+ } else {
+ componentsTree.push(item.componentsTree[0]);
+ }
+ });
+ } else if (isPageDataV1(pages[0])) {
+ componentsTree = [pages[0].layout];
+ } else {
+ // if (!pages[0].componentsTree) return;
+ componentsTree = pages[0].componentsTree;
+ if (componentsTree[0]) {
+ componentsTree[0].componentName = componentsTree[0].componentName || 'Page';
+ // FIXME
+ if (componentsTree[0].componentName === 'Page' || componentsTree[0].componentName === 'Component') {
+ componentsTree[0].methods = {};
+ }
+ }
+ }
+
+ componentsTree.forEach((item: any) => {
+ item.componentName = item.componentName || 'Page';
+ if (item.componentName === 'Page' || item.componentName === 'Component') {
+ item.methods = {};
+ }
+ });
+
+ project.load(
+ {
+ version: '1.0.0',
+ componentsMap: [],
+ componentsTree,
+ id: pages[0].id,
+ config: {
+ ...project.config,
+ ...pages[0].config,
+ },
+ },
+ true,
+ );
+ // FIXME: 在页面节点初始化结束后,还有响应式变量变化导致了多次变化
+ // 这样可以避免页面加载之后就被标记为 isModified
+ setTimeout(() => {
+ project.currentDocument?.history.savePoint();
+ }, 1000);
+ },
+ addPage(data: OldPageData | RootSchema) {
+ if (isPageDataV1(data)) {
+ data = data.layout;
+ } else if (isPageDataV2(data)) {
+ data = data.componentsTree[0];
+ }
+ return project.open(data);
+ },
+ getPage(fnOrIndex: ((page: DocumentModel) => boolean) | number) {
+ if (typeof fnOrIndex === 'number') {
+ return project.documents[fnOrIndex];
+ } else if (typeof fnOrIndex === 'function') {
+ return project.documents.find(fnOrIndex);
+ }
+ return null;
+ },
+ removePage(page: DocumentModel) {
+ page.remove();
+ },
+ getPages() {
+ return project.documents;
+ },
+ setCurrentPage(page: DocumentModel) {
+ page.activate();
+ },
+ getCurrentPage() {
+ return project.currentDocument;
+ },
+ onPagesChange() {
+ // noop
+ },
+ onCurrentPageChange(fn: (page: DocumentModel) => void) {
+ return project.onCurrentDocumentChange(fn);
+ },
+ toData() {
+ return project.documents.map((doc) => doc.toData());
+ },
+});
+
+Object.defineProperty(pages, 'currentPage', {
+ get() {
+ return project.currentDocument;
+ },
+ set(_currentPage) {
+ // do nothing
+ },
+});
+
+pages.onCurrentPageChange((page: DocumentModel) => {
+ if (!page) {
+ return;
+ }
+ page.acceptRootNodeVisitor('NodeCache', (rootNode) => {
+ const visitor: NodeCacheVisitor = page.getRootNodeVisitor('NodeCache');
+ if (visitor) {
+ visitor.destroy();
+ }
+ return new NodeCacheVisitor(page, rootNode);
+ });
+});
+
+export default pages;
diff --git a/packages/vision-polyfill/src/panes.ts b/packages/vision-polyfill/src/panes.ts
new file mode 100644
index 000000000..0ace2bda1
--- /dev/null
+++ b/packages/vision-polyfill/src/panes.ts
@@ -0,0 +1,286 @@
+import { skeleton, editor } from '@ali/lowcode-engine';
+import { ReactElement } from 'react';
+import { IWidgetBaseConfig } from '@ali/lowcode-editor-skeleton';
+import { IconType } from '@ali/lowcode-types';
+import { uniqueId } from '@ali/lowcode-utils';
+import bus from './bus';
+
+export interface IContentItemConfig {
+ title: string;
+ content: JSX.Element;
+ tip?: {
+ content: string;
+ url?: string;
+ };
+}
+
+export interface OldPaneConfig {
+ // 'dock' | 'action' | 'tab' | 'widget' | 'stage'
+ type?: string; // where
+
+ id?: string;
+ name: string;
+ title?: string;
+ content?: any;
+
+ place?: string; // align: left|right|top|center|bottom
+ description?: string; // tip?
+ tip?:
+ | string
+ | {
+ // as help tip
+ url?: string;
+ content?: string | JSX.Element;
+ }; // help
+
+ init?: () => any;
+ destroy?: () => any;
+ props?: any;
+
+ contents?: IContentItemConfig[];
+ hideTitleBar?: boolean;
+ width?: number;
+ maxWidth?: number;
+ height?: number;
+ maxHeight?: number;
+ position?: string | string[]; // todo
+ menu?: JSX.Element; // as title
+ index?: number; // todo
+ isAction?: boolean; // as normal dock
+ fullScreen?: boolean; // todo
+ canSetFixed?: boolean; // 是否可以设置固定模式
+ defaultFixed?: boolean; // 是否默认固定
+ enableDrag?: boolean;
+ icon?: IconType; // 支持旧vision pane传icon (menu是title, 并非icon)
+}
+
+function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: string } {
+ const { type, id, name, title, content, place, description, init, destroy, props, index } = config;
+
+ const newConfig: any = {
+ id,
+ name,
+ content,
+ props: {
+ title,
+ description,
+ align: place,
+ },
+ contentProps: props,
+ index: index || props?.index,
+ };
+
+ if (type === 'dock') {
+ newConfig.type = 'PanelDock';
+ newConfig.area = 'left';
+ newConfig.props.description = description || title;
+ const {
+ contents,
+ hideTitleBar,
+ tip,
+ width,
+ maxWidth,
+ height,
+ maxHeight,
+ menu,
+ icon,
+ isAction,
+ canSetFixed,
+ defaultFixed,
+ enableDrag,
+ } = config;
+ if (menu) {
+ newConfig.props.title = menu;
+ }
+ if (icon) {
+ newConfig.props.icon = icon;
+ }
+ if (isAction) {
+ newConfig.type = 'Dock';
+ } else {
+ newConfig.panelProps = {
+ title,
+ hideTitleBar,
+ help: tip,
+ width,
+ maxWidth,
+ height,
+ maxHeight,
+ canSetFixed,
+ enableDrag,
+ };
+
+ if (defaultFixed) {
+ newConfig.panelProps.area = 'leftFixedArea';
+ }
+
+ if (contents && Array.isArray(contents)) {
+ newConfig.content = contents.map(({ title, content, tip }, index) => {
+ return {
+ type: 'Panel',
+ name: typeof title === 'string' ? title : `${name}:${index}`,
+ content,
+ contentProps: props,
+ props: {
+ title,
+ help: tip,
+ },
+ };
+ });
+ }
+ }
+ } else if (type === 'action') {
+ newConfig.area = 'top';
+ newConfig.type = 'Dock';
+ } else if (type === 'tab') {
+ newConfig.area = 'right';
+ newConfig.type = 'Panel';
+ } else if (type === 'stage') {
+ newConfig.area = 'stages';
+ newConfig.type = 'Widget';
+ } else {
+ newConfig.area = 'main';
+ newConfig.type = 'Widget';
+ }
+ newConfig.props.onInit = init;
+ newConfig.props.onDestroy = destroy;
+ return newConfig;
+}
+
+function add(config: (() => OldPaneConfig) | OldPaneConfig, extraConfig?: any) {
+ if (typeof config === 'function') {
+ config = config.call(null);
+ }
+ if (!config || !config.type) {
+ return null;
+ }
+ if (extraConfig) {
+ config = { ...config, ...extraConfig };
+ }
+
+ const upgraded = upgradeConfig(config);
+ if (upgraded.area === 'stages') {
+ if (upgraded.id) {
+ upgraded.name = upgraded.id;
+ } else if (!upgraded.name) {
+ upgraded.name = uniqueId('stage');
+ }
+ const stage = skeleton.add(upgraded);
+ return stage?.getName();
+ } else {
+ return skeleton.add(upgraded);
+ }
+}
+
+const actionPane = Object.assign(skeleton.topArea, {
+ /**
+ * compatible *VE.actionPane.getActions*
+ */
+ getActions(): any {
+ return skeleton.topArea.container.items;
+ },
+ /**
+ * compatible *VE.actionPane.activeDock*
+ */
+ setActions() {
+ // empty
+ },
+ get actions() {
+ return skeleton.topArea.container.items;
+ },
+});
+const dockPane = Object.assign(skeleton.leftArea, {
+ /**
+ * compatible *VE.dockPane.activeDock*
+ */
+ activeDock(item: any) {
+ if (!item) {
+ skeleton.leftFloatArea?.current?.hide();
+ return;
+ }
+ const name = item.name || item;
+ const pane = skeleton.getPanel(name);
+ if (!pane) {
+ console.warn(`Could not find pane with name ${name}`);
+ }
+ pane?.active();
+ bus.emit('ve.dock_pane.active_doc', pane);
+ },
+
+ /**
+ * compatible *VE.dockPane.onDockShow*
+ */
+ onDockShow(fn: (dock: any) => void): () => void {
+ const f = (_: any, dock: any) => {
+ fn(dock);
+ };
+ editor.on('skeleton.panel-dock.active', f);
+ return () => {
+ editor.removeListener('skeleton.panel-dock.active', f);
+ };
+ },
+ /**
+ * compatible *VE.dockPane.onDockHide*
+ */
+ onDockHide(fn: (dock: any) => void): () => void {
+ const f = (_: any, dock: any) => {
+ fn(dock);
+ };
+ editor.on('skeleton.panel-dock.unactive', f);
+ return () => {
+ editor.removeListener('skeleton.panel-dock.unactive', f);
+ };
+ },
+ /**
+ * compatible *VE.dockPane.setFixed*
+ */
+ setFixed(flag: boolean) {
+ // todo:
+ },
+ getDocks() {
+ return skeleton.leftFloatArea?.container.items;
+ },
+});
+const tabPane = Object.assign(skeleton.rightArea, {
+ setFloat(flag: boolean) {
+ // todo:
+ },
+});
+const toolbar = Object.assign(skeleton.toolbar, {
+ setContents(contents: ReactElement) {
+ // todo:
+ },
+});
+const widgets = skeleton.mainArea;
+
+const stages = Object.assign(skeleton.stages, {
+ getStage(name: string) {
+ return skeleton.stages.container.get(name);
+ },
+
+ createStage(config: any) {
+ config = upgradeConfig(config);
+ if (config.id) {
+ config.name = config.id;
+ } else if (!config.name) {
+ config.name = uniqueId('stage');
+ }
+
+ const stage = skeleton.stages.add(config);
+ return stage.getName();
+ },
+});
+
+export default {
+ ActionPane: actionPane, // topArea
+ actionPane, //
+ DockPane: dockPane, // leftArea
+ dockPane,
+ TabPane: tabPane, // rightArea
+ tabPane,
+ add,
+ toolbar, // toolbar
+ Stages: stages,
+ Widgets: widgets, // centerArea
+ widgets,
+};
diff --git a/packages/vision-polyfill/src/project.ts b/packages/vision-polyfill/src/project.ts
new file mode 100644
index 000000000..145d4f80c
--- /dev/null
+++ b/packages/vision-polyfill/src/project.ts
@@ -0,0 +1,20 @@
+import { designer } from '@ali/lowcode-engine';
+
+const { project } = designer;
+
+const visionProject = {};
+Object.assign(visionProject, project, {
+ getSchema(): any {
+ return this.schema || {};
+ },
+
+ setSchema(schema: any) {
+ this.schema = schema;
+ },
+
+ setConfig(config: any) {
+ this.set('config', config);
+ },
+});
+
+export default visionProject;
diff --git a/packages/vision-polyfill/src/prop.ts b/packages/vision-polyfill/src/prop.ts
new file mode 100644
index 000000000..3e7f77d84
--- /dev/null
+++ b/packages/vision-polyfill/src/prop.ts
@@ -0,0 +1,630 @@
+import { Component } from 'react';
+import { EventEmitter } from 'events';
+import { fromJS, Iterable, Map as IMMap } from 'immutable';
+import logger from '@ali/vu-logger';
+import { cloneDeep, isDataEqual, combineInitial, Transducer } from '@ali/ve-utils';
+import I18nUtil from '@ali/ve-i18n-util';
+import { editor, setters } from '@ali/lowcode-engine';
+import { OldPropConfig, DISPLAY_TYPE } from './bundle/upgrade-metadata';
+import { uniqueId } from '@ali/lowcode-utils';
+
+const { getSetter } = setters;
+
+type IPropConfig = OldPropConfig;
+
+// 1: chain -1: start 0: discard
+const CHAIN_START = -1;
+const CHAIN_HAS_REACH = 0;
+
+export enum PROP_VALUE_CHANGED_TYPE {
+ /**
+ * normal set value
+ */
+ SET_VALUE = 'SET_VALUE',
+ /**
+ * value changed caused by sub-prop value change
+ */
+ SUB_VALUE_CHANGE = 'SUB_VALUE_CHANGE',
+}
+
+/**
+ * Dynamic setter will use 've.plugin.setterProvider' to
+ * calculate setter type in runtime
+ */
+let dynamicSetterProvider: any;
+
+export interface IHotDataMap extends IMMap {
+ value: any;
+ hotValue: any;
+}
+
+export interface ISetValueOptions {
+ disableMutator?: boolean;
+ type?: PROP_VALUE_CHANGED_TYPE;
+}
+
+export interface IVariableSettable {
+ useVariable?: boolean;
+ variableValue: string;
+ isUseVariable: () => boolean;
+ isSupportVariable: () => boolean;
+ setVariableValue: (value: string) => void;
+ setUseVariable: (flag?: boolean) => void;
+ getVariableValue: () => string;
+ onUseVariableChange: (func: (data: { isUseVariable: boolean }) => any) => void;
+}
+
+export default class Prop implements IVariableSettable {
+ /**
+ * Setters predefined as default options
+ * can by selected by user for every prop
+ *
+ * @static
+ * @memberof Prop
+ */
+ public static INSET_SETTER = {};
+
+ public id: string;
+
+ public emitter: EventEmitter;
+
+ public inited: boolean;
+
+ public i18nLink: any;
+
+ public loopLock: boolean;
+
+ public props: any;
+
+ public parent: any;
+
+ public config: IPropConfig;
+
+ public initial: any;
+
+ public initialData: any;
+
+ public expanded: boolean;
+
+ public useVariable?: boolean;
+
+ /**
+ * value to be saved in schema it is usually JSON serialized
+ * prototype.js can config Transducer.toNative to generate value
+ */
+ public value: any;
+
+ /**
+ * value to be used in VisualDesigner more flexible
+ * prototype.js can config Transducer.toHot to generate hotValue
+ */
+ public hotValue: any;
+
+ /**
+ * 启用变量之后,变量表达式字符串值
+ */
+ public variableValue: string;
+
+ public hotData: IMMap;
+
+ public defaultValue: any;
+
+ public transducer: any;
+
+ public inGroup: boolean;
+
+ constructor(parent: any, config: IPropConfig, data?: any) {
+ if (parent.isProps) {
+ this.props = parent;
+ this.parent = null;
+ } else {
+ this.props = parent.getProps();
+ this.parent = parent;
+ }
+
+ this.id = uniqueId('prop');
+
+ if (typeof config.setter === 'string') {
+ config.setter = getSetter(config.setter)?.component as any;
+ }
+ this.config = config;
+ this.emitter = new EventEmitter();
+ this.emitter.setMaxListeners(100);
+ this.initialData = data;
+ this.useVariable = false;
+
+ dynamicSetterProvider = editor.get('ve.plugin.setterProvider');
+
+ this.beforeInit();
+ }
+
+ public getId() {
+ return this.id;
+ }
+
+ public isTab() {
+ return this.getDisplay() === 'tab';
+ }
+
+ public isGroup() {
+ return false;
+ }
+
+ public beforeInit() {
+ if (IMMap.isMap(this.initialData)) {
+ this.value = this.initialData.get('value');
+ if (this.value && typeof this.value.toJS === 'function') {
+ this.value = this.value.toJS();
+ }
+ this.hotData = this.initialData;
+ } else {
+ this.value = this.initialData;
+ }
+
+ this.resolveValue();
+
+ let defaultValue = null;
+ if (this.config.defaultValue !== undefined) {
+ defaultValue = this.config.defaultValue;
+ } else if (typeof this.config.initialValue !== 'function') {
+ defaultValue = this.config.initialValue;
+ }
+ this.defaultValue = defaultValue;
+ this.transducer = new Transducer(this, this.config);
+ this.initial = combineInitial(this, this.config);
+ }
+
+ public resolveValue() {
+ if (this.value && this.value.type === 'variable') {
+ const { value, variable } = this.value;
+ this.value = value;
+ this.variableValue = variable;
+ this.useVariable = this.isSupportVariable();
+ } else {
+ this.useVariable = false;
+ }
+ }
+
+ public init(defaultValue?: any) {
+ if (this.inited) { return; }
+
+ this.value = this.initial(this.value,
+ this.defaultValue != null ? this.defaultValue : defaultValue);
+
+ if (this.hotData) {
+ const tempVal = this.hotData.get('value');
+ // if we create a prop from runtime data, we don't need initial() or set with defaultValue process
+ // but if we got an empty value, we fill with the initial() process and default value
+ if (Iterable.isIterable(tempVal)) {
+ this.value = tempVal.toJS() || this.value;
+ } else {
+ this.value = tempVal || this.value;
+ }
+ this.resolveValue();
+ }
+
+ this.i18nLink = I18nUtil.attach(this, this.value,
+ ((val: any) => { this.setValue(val, false, true); }) as any);
+
+ // call config.accessor
+ const value = this.getValue();
+
+ if (this.hotData) {
+ this.hotValue = this.hotData.get('hotValue');
+ if (this.hotValue && Iterable.isIterable(this.hotValue)) {
+ this.hotValue = this.hotValue.toJS();
+ }
+ } else {
+ try {
+ this.hotValue = this.transducer.toHot(value);
+ } catch (e) {
+ logger.log('ERROR_PROP_VALUE');
+ logger.warn('属性初始化错误:', this);
+ }
+
+ this.hotData = fromJS({
+ hotValue: this.hotValue,
+ value: this.getMixValue(value),
+ });
+ }
+ this.inited = true;
+ }
+
+ public isInited() {
+ return this.inited;
+ }
+
+ public getHotData() {
+ return this.hotData;
+ }
+
+ public getProps() {
+ return this.props;
+ }
+
+ public getNode(): any {
+ return this.getProps().getNode();
+ }
+
+ /**
+ * 获得属性名称
+ *
+ * @returns {string}
+ */
+ public getName(): string {
+ const ns = this.parent ? `${this.parent.getName()}.` : '';
+ return ns + this.config.name;
+ }
+
+ public getKey() {
+ return this.config.name;
+ }
+
+ /**
+ * 获得属性标题
+ *
+ * @returns {string}
+ */
+ public getTitle() {
+ return this.config.title || this.getName();
+ }
+
+ public getTip() {
+ return this.config.tip || null;
+ }
+
+ public getValue(disableCache?: boolean, options?: {
+ disableAccessor?: boolean;
+ }) {
+ const { accessor } = this.config;
+ if (accessor && (!options || !options.disableAccessor)) {
+ const value = accessor.call(this as any, this.value);
+ if (!disableCache) {
+ this.value = value;
+ }
+ return value;
+ }
+ return this.value;
+ }
+
+ public getMixValue(value?: any) {
+ if (value == null) {
+ value = this.getValue();
+ }
+ if (this.isUseVariable()) {
+ value = {
+ type: 'variable',
+ value,
+ variable: this.getVariableValue(),
+ };
+ }
+ return value;
+ }
+
+ public toData() {
+ return cloneDeep(this.getMixValue());
+ }
+
+ public getDefaultValue() {
+ return this.defaultValue;
+ }
+
+ public getHotValue() {
+ return this.hotValue;
+ }
+
+ public getConfig(configName?: K): IPropConfig[K] | IPropConfig {
+ if (configName) {
+ return this.config[configName];
+ }
+
+ return this.config;
+ }
+
+ public sync() {
+ if (this.props.hasReach(this)) {
+ return;
+ }
+
+ const { sync } = this.config;
+ if (sync) {
+ const value = sync.call(this as any, this.getValue(true));
+ if (value !== undefined) {
+ this.setValue(value);
+ }
+ } else {
+ // sync 的时候不再需要调用经过 accessor 处理之后的值了
+ // 这里之所以需要 setValue 是为了过 getValue() 中的 accessor 修饰函数
+ this.setValue(this.getValue(true), false, false, {
+ disableMutator: true,
+ });
+ }
+ }
+
+ public isUseVariable() {
+ return this.useVariable || false;
+ }
+
+ public isSupportVariable() {
+ return this.config.supportVariable || false;
+ }
+
+ public setVariableValue(value: string) {
+ if (!this.isUseVariable()) { return; }
+
+ const state = this.props.chainReach(this);
+ if (state === CHAIN_HAS_REACH) {
+ return;
+ }
+
+ this.variableValue = value;
+
+ if (this.modify()) {
+ this.valueChange();
+ this.props.syncPass(this);
+ }
+
+ if (state === CHAIN_START) {
+ this.props.endChain();
+ }
+ }
+
+ public setUseVariable(flag = false) {
+ if (this.useVariable === flag) { return; }
+
+ const state = this.props.chainReach(this);
+ if (state === CHAIN_HAS_REACH) {
+ return;
+ }
+
+ this.useVariable = flag;
+ this.expanded = true;
+
+ if (this.modify()) {
+ this.valueChange();
+ this.props.syncPass(this);
+ }
+
+ if (state === CHAIN_START) {
+ this.props.endChain();
+ }
+
+ this.emitter.emit('ve.prop.useVariableChange', { isUseVariable: flag });
+ if (this.config.useVariableChange) {
+ this.config.useVariableChange.call(this as any, { isUseVariable: flag });
+ }
+ }
+
+ public getVariableValue() {
+ return this.variableValue;
+ }
+
+ /**
+ * @param value
+ * @param isHotValue 是否为设计器热状态值
+ * @param force 是否强制触发更新
+ */
+ public setValue(value: any, isHotValue?: boolean, force?: boolean, extraOptions?: ISetValueOptions) {
+ const state = this.props.chainReach(this);
+ if (state === CHAIN_HAS_REACH) {
+ return;
+ }
+
+ const preValue = this.value;
+ const preHotValue = this.hotValue;
+
+ if (isHotValue) {
+ this.hotValue = value;
+ this.value = this.transducer.toNative(this.hotValue);
+ } else {
+ if (!isDataEqual(value, this.value)) {
+ this.hotValue = this.transducer.toHot(value);
+ }
+ this.value = value;
+ }
+
+ this.i18nLink = I18nUtil.attach(this, this.value, ((val: any) => this.setValue(val, false, true)) as any);
+
+ const { mutator } = this.config;
+
+ if (!extraOptions) {
+ extraOptions = {};
+ }
+
+ if (mutator && !extraOptions.disableMutator) {
+ mutator.call(this as any, this.value);
+ }
+
+ if (this.modify(force)) {
+ this.valueChange(extraOptions);
+ this.props.syncPass(this);
+ }
+
+ if (state === CHAIN_START) {
+ this.props.endChain();
+ }
+ }
+
+ public setHotValue(hotValue: any, options?: ISetValueOptions) {
+ try {
+ this.setValue(hotValue, true, false, options);
+ } catch (e) {
+ logger.log('ERROR_PROP_VALUE');
+ logger.warn('属性值设置错误:', e, hotValue);
+ }
+ }
+
+ /**
+ * 验证是否存在变更
+ * @param force 是否强制返回已变更
+ */
+ public modify(force?: boolean) {
+ const hotData = this.hotData.merge(fromJS({
+ hotValue: this.getHotValue(),
+ value: this.getMixValue(),
+ }));
+
+ if (!force && hotData.equals(this.hotData)) {
+ return false;
+ }
+
+ this.hotData = hotData;
+
+ (this.parent || this.props).modify(this.getName());
+
+ return true;
+ }
+
+ public setHotData(hotData: IMMap, options?: ISetValueOptions) {
+ if (!IMMap.isMap(hotData)) {
+ return;
+ }
+ this.hotData = hotData;
+ let value = hotData.get('value');
+ if (value && typeof value.toJS === 'function') {
+ value = value.toJS();
+ }
+ let hotValue = hotData.get('hotValue');
+ if (hotValue && typeof hotValue.toJS === 'function') {
+ hotValue = hotValue.toJS();
+ }
+
+ const preValue = value;
+ const preHotValue = hotValue;
+
+ this.value = value;
+ this.hotValue = hotValue;
+ this.resolveValue();
+
+ if (!options || !options.disableMutator) {
+ const { mutator } = this.config;
+ if (mutator) {
+ mutator.call(this as any, value);
+ }
+ }
+
+ this.valueChange();
+ }
+
+ public valueChange(options?: ISetValueOptions) {
+ if (this.loopLock) { return; }
+
+ this.emitter.emit('valuechange', options);
+ if (this.parent) {
+ this.parent.valueChange(options);
+ }
+ }
+
+ public getDisplay() {
+ return this.config.display || this.config.fieldStyle || 'block';
+ }
+
+ public isHidden() {
+ if (!this.isInited() || this.getDisplay() === DISPLAY_TYPE.NONE || this.isDisabled()) {
+ return true;
+ }
+
+ let { hidden } = this.config;
+ if (typeof hidden === 'function') {
+ hidden = hidden.call(this as any, this.getValue());
+ }
+ return hidden === true;
+ }
+
+ public isDisabled() {
+ let { disabled } = this.config;
+ if (typeof disabled === 'function') {
+ disabled = disabled.call(this as any, this.getValue());
+ }
+ return disabled === true;
+ }
+
+ public isIgnore() {
+ if (this.isDisabled()) { return true; }
+
+ let { ignore } = this.config;
+ if (typeof ignore === 'function') {
+ ignore = ignore.call(this as any, this.getValue());
+ }
+ return ignore === true;
+ }
+
+ public isExpand() {
+ if (this.expanded == null) {
+ this.expanded = !(this.config.collapsed || this.config.fieldCollapsed);
+ }
+ return this.expanded;
+ }
+
+ public toggleExpand() {
+ if (this.expanded) {
+ this.expanded = false;
+ } else {
+ this.expanded = true;
+ }
+ this.emitter.emit('expandchange', this.expanded);
+ }
+
+ public getSetter() {
+ if (dynamicSetterProvider) {
+ const setter = dynamicSetterProvider.call(this, this, this.getNode().getPrototype());
+ if (setter) {
+ return setter;
+ }
+ }
+ const setterConfig = this.config.setter;
+ if (typeof setterConfig === 'function' && !(setterConfig.prototype instanceof Component)) {
+ return (setterConfig as any).call(this, this.getValue());
+ }
+ if (Array.isArray(setterConfig)) {
+ let item;
+ for (item of setterConfig) {
+ if (item.condition?.call(this, this.getValue())) {
+ return item.setter;
+ }
+ }
+ return setterConfig[0].setter;
+ }
+ return setterConfig;
+ }
+
+ public getSetterData(): any {
+ if (Array.isArray(this.config.setter)) {
+ let item;
+ for (item of this.config.setter) {
+ if (item.condition?.call(this, this.getValue())) {
+ return item;
+ }
+ }
+ return this.config.setter[0];
+ }
+ return { };
+ }
+
+ public destroy() {
+ if (this.i18nLink) {
+ this.i18nLink.detach();
+ }
+ }
+
+ public onValueChange(func: () => any) {
+ this.emitter.on('valuechange', func);
+ return () => {
+ this.emitter.removeListener('valuechange', func);
+ };
+ }
+
+ public onExpandChange(func: () => any) {
+ this.emitter.on('expandchange', func);
+ return () => {
+ this.emitter.removeListener('expandchange', func);
+ };
+ }
+
+ public onUseVariableChange(func: (data: { isUseVariable: boolean }) => any) {
+ this.emitter.on('ve.prop.useVariableChange', func);
+ return () => {
+ this.emitter.removeListener('ve.prop.useVariableChange', func);
+ };
+ }
+}
diff --git a/packages/vision-polyfill/src/props-reducers/downgrade-schema-reducer.ts b/packages/vision-polyfill/src/props-reducers/downgrade-schema-reducer.ts
new file mode 100644
index 000000000..e572cd21f
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/downgrade-schema-reducer.ts
@@ -0,0 +1,52 @@
+import {
+ isPlainObject,
+} from '@ali/lowcode-utils';
+import { isJSExpression, isJSSlot } from '@ali/lowcode-types';
+import { Node } from '@ali/lowcode-designer';
+import { engineConfig } from '@ali/lowcode-engine';
+
+export function compatibleReducer(props: any, node: Node): any {
+ // 如果禁用了降级reducer,则不做处理
+ if (engineConfig.get('visionSettings.disableCompatibleReducer')) {
+ return props;
+ }
+ // 如果不是 vc 体系,不做这个兼容处理
+ if (!node.componentMeta.prototype) {
+ return props;
+ }
+ if (!props || !isPlainObject(props)) {
+ return props;
+ }
+ // 为了能降级到老版本,建议在后期版本去掉以下代码
+ if (isJSSlot(props)) {
+ return {
+ type: 'JSBlock',
+ value: {
+ componentName: 'Slot',
+ children: props.value,
+ props: {
+ slotTitle: props.title,
+ slotName: props.name,
+ slotParams: props.params,
+ },
+ },
+ };
+ }
+ if (!props.events && isJSExpression(props)) {
+ return {
+ type: 'variable',
+ value: props.mock,
+ variable: props.value,
+ };
+ }
+ const newProps: any = {};
+ Object.entries(props).forEach(([key, val]) => {
+ // // TODO: 目前 dataSource 面板里既用到了 JSExpression,又用到了 variable,这里先都不处理,后面再重构
+ // if (key === 'dataSource') {
+ // newProps[key] = props[key];
+ // return;
+ // }
+ newProps[key] = compatibleReducer(val, node);
+ });
+ return newProps;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/filter-empty-event.ts b/packages/vision-polyfill/src/props-reducers/filter-empty-event.ts
new file mode 100644
index 000000000..0fc6aa801
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/filter-empty-event.ts
@@ -0,0 +1,20 @@
+import { Node } from '@ali/lowcode-engine';
+import { hasOwnProperty, isPlainObject } from '@ali/lowcode-utils';
+
+function isEmptyEvent(event: any) {
+ return event?.ignored === true;
+}
+
+/**
+ * 在使用老版的 vu-events-property,会有空的事件描述,比如 onClick: { ignored: true } 的情况
+ */
+export function filterEmptyEventReducer(props: any, node: Node): any {
+ if (!props || !isPlainObject(props)) return props;
+ // 基于性能考虑,只过滤第一层
+ Object.keys(props).forEach(name => {
+ if (name.startsWith('on') && isEmptyEvent(props[name])) {
+ delete props[name];
+ }
+ });
+ return props;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/filter-reducer.ts b/packages/vision-polyfill/src/props-reducers/filter-reducer.ts
new file mode 100644
index 000000000..0239ab102
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/filter-reducer.ts
@@ -0,0 +1,31 @@
+import logger from '@ali/vu-logger';
+import { Node, PropsReducerContext, designerCabin, engineConfig } from '@ali/lowcode-engine';
+import { hasOwnProperty } from '@ali/lowcode-utils';
+const { TransformStage } = designerCabin;
+
+export function filterReducer(props: any, node: Node, ctx?: PropsReducerContext): any {
+ // 老的 vision 逻辑是 render 阶段不走 filter 逻辑
+ if (ctx?.stage === TransformStage.Render && !engineConfig.get('visionSettings.enableFilterReducerInRenderStage', false)) {
+ return props;
+ }
+ const filters = node.componentMeta.getMetadata().experimental?.filters;
+ if (filters && filters.length) {
+ const newProps = { ...props };
+ filters.forEach((item) => {
+ // FIXME! item.name could be 'xxx.xxx'
+ if (!hasOwnProperty(newProps, item.name)) {
+ return;
+ }
+ try {
+ if (item.filter(node.settingEntry.getProp(item.name), props[item.name]) === false) {
+ delete newProps[item.name];
+ }
+ } catch (e) {
+ console.warn(e);
+ logger.trace(e);
+ }
+ });
+ return newProps;
+ }
+ return props;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/index.ts b/packages/vision-polyfill/src/props-reducers/index.ts
new file mode 100644
index 000000000..dd5a5e888
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/index.ts
@@ -0,0 +1,10 @@
+export * from './downgrade-schema-reducer';
+export * from './filter-reducer';
+export * from './init-node-reducer';
+export * from './live-lifecycle-reducer';
+export * from './remove-empty-prop-reducer';
+export * from './style-reducer';
+export * from './upgrade-reducer';
+export * from './node-top-fixed-reducer';
+export * from './reset-loop-default-value-reducer';
+export * from './filter-empty-event';
diff --git a/packages/vision-polyfill/src/props-reducers/init-node-reducer.ts b/packages/vision-polyfill/src/props-reducers/init-node-reducer.ts
new file mode 100644
index 000000000..e65cbf7c3
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/init-node-reducer.ts
@@ -0,0 +1,52 @@
+import {
+ hasOwnProperty,
+ isI18NObject,
+ isUseI18NSetter,
+ convertToI18NObject,
+ isString,
+} from '@ali/lowcode-utils';
+import { isJSExpression, isJSBlock, isJSSlot } from '@ali/lowcode-types';
+import { isVariable, getCurrentFieldIds } from '../utils';
+
+export function initNodeReducer(props, node) {
+ // run initials
+ const newProps: any = {
+ ...props,
+ };
+ if (newProps.fieldId) {
+ const { doc, fieldIds } = getCurrentFieldIds();
+
+ // 全局的关闭 uniqueIdChecker 信号,在 ve-utils 中实现
+ if (doc === node.document && fieldIds.indexOf(props.fieldId) >= 0 && !(window as any).__disable_unique_id_checker__) {
+ newProps.fieldId = undefined;
+ }
+ }
+ const initials = node.componentMeta.getMetadata().experimental?.initials;
+
+ if (initials) {
+ initials.forEach(item => {
+ try {
+ // FIXME! item.name could be 'xxx.xxx'
+ const value = newProps[item.name];
+ // 几种不再进行 initial 计算的情况
+ // 1. name === 'fieldId' 并且已经有值
+ // 2. 结构为 JSExpression 并且带有 events 字段
+ if ((item.name === 'fieldId' && value) || (isJSExpression(value) && value.events)) {
+ if (newProps[item.name] && !node.props.has(item.name)) {
+ node.props.add(value, item.name, false);
+ }
+ return;
+ }
+ newProps[item.name] = item.initial(node as any, newProps[item.name]);
+ if (newProps[item.name] && !node.props.has(item.name)) {
+ node.props.add(value, item.name, false);
+ }
+ } catch (e) {
+ if (hasOwnProperty(props, item.name)) {
+ newProps[item.name] = props[item.name];
+ }
+ }
+ });
+ }
+ return newProps;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/live-lifecycle-reducer.ts b/packages/vision-polyfill/src/props-reducers/live-lifecycle-reducer.ts
new file mode 100644
index 000000000..7bb606934
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/live-lifecycle-reducer.ts
@@ -0,0 +1,30 @@
+import { editor } from '@ali/lowcode-engine';
+import { Node } from '@ali/lowcode-designer';
+
+export function liveLifecycleReducer(props: any, node: Node) {
+ // 如果不是 vc 体系,不做这个兼容处理
+ if (!node.componentMeta.prototype) {
+ return props;
+ }
+ // live 模式下解析 lifeCycles
+ if (node.isRoot() && props && props.lifeCycles) {
+ if (editor.get('designMode') === 'live') {
+ const lifeCycleMap = {
+ didMount: 'componentDidMount',
+ willUnmount: 'componentWillUnMount',
+ };
+ const { lifeCycles } = props;
+ Object.keys(lifeCycleMap).forEach(key => {
+ if (lifeCycles[key]) {
+ lifeCycles[lifeCycleMap[key]] = lifeCycles[key];
+ }
+ });
+ return props;
+ }
+ return {
+ ...props,
+ lifeCycles: {},
+ };
+ }
+ return props;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/node-top-fixed-reducer.ts b/packages/vision-polyfill/src/props-reducers/node-top-fixed-reducer.ts
new file mode 100644
index 000000000..08563cea0
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/node-top-fixed-reducer.ts
@@ -0,0 +1,12 @@
+import { Node } from '@ali/lowcode-designer';
+
+export function nodeTopFixedReducer(props: any, node: Node) {
+ if (node.componentMeta.isTopFixed) {
+ return {
+ ...props,
+ // experimental prop value
+ __isTopFixed__: true,
+ };
+ }
+ return props;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/remove-empty-prop-reducer.ts b/packages/vision-polyfill/src/props-reducers/remove-empty-prop-reducer.ts
new file mode 100644
index 000000000..d725a78d2
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/remove-empty-prop-reducer.ts
@@ -0,0 +1,24 @@
+import {
+ cloneDeep,
+} from '@ali/lowcode-utils';
+import { Node } from '@ali/lowcode-designer';
+
+// 清除空的 props value
+export function removeEmptyPropsReducer(props: any, node: Node) {
+ if (node.isRoot() && props.dataSource && Array.isArray(props.dataSource.online)) {
+ const online = cloneDeep(props.dataSource.online);
+ online.forEach((item: any) => {
+ const newParam: any = {};
+ if (Array.isArray(item?.options?.params)) {
+ item.options.params.forEach((element: any) => {
+ if (element.name) {
+ newParam[element.name] = element.value;
+ }
+ });
+ item.options.params = newParam;
+ }
+ });
+ props.dataSource.list = online;
+ }
+ return props;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/reset-loop-default-value-reducer.ts b/packages/vision-polyfill/src/props-reducers/reset-loop-default-value-reducer.ts
new file mode 100644
index 000000000..fc5036e99
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/reset-loop-default-value-reducer.ts
@@ -0,0 +1,10 @@
+// 讲loop=[]的情况处理成loop=false
+export function resetLoopDefaultValueReducer(props: any) {
+ if (props.loop && Array.isArray(props.loop) && props.loop.length === 0) {
+ return {
+ ...props,
+ loop: undefined,
+ };
+ }
+ return props;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/style-reducer.ts b/packages/vision-polyfill/src/props-reducers/style-reducer.ts
new file mode 100644
index 000000000..f424eefe8
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/style-reducer.ts
@@ -0,0 +1,85 @@
+import { project } from '@ali/lowcode-engine';
+import { isPlainObject, css } from '@ali/lowcode-utils';
+
+const { toCss } = css;
+
+export function stylePropsReducer(props: any, node: any) {
+ let cssId;
+ let cssClass;
+ let styleProp;
+ if (!props || typeof props !== 'object') return props;
+ if (props.__style__) {
+ cssId = `_style_pseudo_${node.id.replace(/\$/g, '_')}`;
+ cssClass = `_css_pseudo_${node.id.replace(/\$/g, '_')}`;
+ styleProp = props.__style__;
+ appendStyleNode(props, styleProp, cssClass, cssId);
+ props.className = cssClass;
+ }
+ if (props.pageStyle) {
+ cssId = '_style_pseudo_engine-document';
+ cssClass = 'engine-document';
+ styleProp = props.pageStyle;
+ appendStyleNode(props, styleProp, cssClass, cssId);
+ props.className = cssClass;
+ }
+ if (props.containerStyle) {
+ cssId = `_style_pseudo_${node.id}`;
+ cssClass = `_css_pseudo_${node.id.replace(/\$/g, '_')}`;
+ styleProp = props.containerStyle;
+ appendStyleNode(props, styleProp, cssClass, cssId);
+ props.className = cssClass;
+ }
+
+ return props;
+}
+
+function appendStyleNode(props: any, styleProp: any, cssClass: string, cssId: string) {
+ const doc = project.simulator?.contentDocument;
+
+ if (!doc) {
+ return;
+ }
+ if (isPlainObject(styleProp)) {
+ styleProp = isEmptyObject(styleProp) ? '' : toCss(styleProp);
+ }
+ if (typeof styleProp === 'string' && styleProp) {
+ const dom = doc.getElementById(cssId) as HTMLStyleElement;
+ const newStyleStr = transformStyleStr(styleProp, cssClass);
+ if (!dom) {
+ const s = doc.createElement('style');
+ s.setAttribute('type', 'text/css');
+ s.setAttribute('id', cssId);
+ doc.getElementsByTagName('head')[0].appendChild(s);
+ s.appendChild(doc.createTextNode(newStyleStr));
+ return;
+ }
+ if (!stringEquals(dom.innerHTML!, newStyleStr)) {
+ dom.innerHTML = newStyleStr;
+ }
+ }
+}
+
+function isEmptyObject(obj: any) {
+ if (!isPlainObject(obj)) return false;
+ let empty = true;
+ for (let k in obj) {
+ empty = false;
+ }
+ return empty;
+}
+
+function stringEquals(str: string, targetStr: string): boolean {
+ return removeWhitespace(str) === removeWhitespace(targetStr);
+}
+
+function removeWhitespace(str: string = ''): string {
+ return str.replace(/\s/g, '');
+}
+
+function transformStyleStr(styleStr: string = '', cssClass: string): string {
+ return styleStr
+ .replace(/(\d+)rpx/g, (all, num) => {
+ return `${num / 2}px`;
+ })
+ .replace(/:root/g, `.${cssClass}`);
+}
diff --git a/packages/vision-polyfill/src/props-reducers/upgrade-reducer.ts b/packages/vision-polyfill/src/props-reducers/upgrade-reducer.ts
new file mode 100644
index 000000000..7fb8939c8
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/upgrade-reducer.ts
@@ -0,0 +1,54 @@
+import { Node } from '@ali/lowcode-designer';
+import { isPlainObject, isVariable } from '@ali/lowcode-utils';
+import { isJSBlock } from '@ali/lowcode-types';
+import { designerCabin } from '@ali/lowcode-engine';
+
+const { getConvertedExtraKey } = designerCabin;
+
+export function upgradePropsReducer(props: any): any {
+ if (!props || !isPlainObject(props)) {
+ return props;
+ }
+
+ if (isJSBlock(props)) {
+ if (props.value.componentName === 'Slot') {
+ return {
+ type: 'JSSlot',
+ title: (props.value.props as any)?.slotTitle,
+ name: (props.value.props as any)?.slotName,
+ value: props.value.children,
+ };
+ } else {
+ return props.value;
+ }
+ }
+ if (isVariable(props)) {
+ return {
+ type: 'JSExpression',
+ value: props.variable,
+ mock: props.value,
+ };
+ }
+ const newProps: any = {};
+ Object.keys(props).forEach((key) => {
+ if (/^__slot__/.test(key) && props[key] === true) {
+ return;
+ }
+ newProps[key] = upgradePropsReducer(props[key]);
+ });
+ return newProps;
+}
+
+export function upgradePageLifeCyclesReducer(props: any, node: Node) {
+ const lifeCycleNames = ['didMount', 'willUnmount'];
+ if (node.isRoot()) {
+ lifeCycleNames.forEach(key => {
+ if (props[key]) {
+ const lifeCycles = node.props.getPropValue(getConvertedExtraKey('lifeCycles')) || {};
+ lifeCycles[key] = props[key];
+ node.props.setPropValue(getConvertedExtraKey('lifeCycles'), lifeCycles);
+ }
+ });
+ }
+ return props;
+}
diff --git a/packages/vision-polyfill/src/props-reducers/value-parser.ts b/packages/vision-polyfill/src/props-reducers/value-parser.ts
new file mode 100644
index 000000000..d748f30bb
--- /dev/null
+++ b/packages/vision-polyfill/src/props-reducers/value-parser.ts
@@ -0,0 +1,82 @@
+import Env from '../env';
+import { Node } from '@ali/lowcode-designer';
+import { isJSSlot, isI18nData, isJSExpression } from '@ali/lowcode-types';
+import { isPlainObject } from '@ali/lowcode-utils';
+import i18nUtil from '../i18n-util';
+import { editor } from '@ali/lowcode-engine';
+import { isVariable } from '../utils';
+
+// FIXME: 表达式使用 mock 值,未来live 模式直接使用原始值
+// TODO: designType
+export function valueParser(obj: any, node: Node): any {
+ return deepValueParser(obj, {
+ node,
+ path: '',
+ });
+}
+
+function deepValueParser(obj: any, info: {
+ node: Node;
+ path?: string;
+}): any {
+ const {
+ path = '',
+ node,
+ } = info;
+ // 如果不是 vc 体系,不做这个兼容处理
+ if (!node.componentMeta.prototype) {
+ return obj;
+ }
+ if (isJSExpression(obj)) {
+ if (editor.get('designMode') === 'live') {
+ return obj;
+ }
+ obj = obj.mock;
+ }
+ // 兼容 ListSetter 中的变量结构
+ if (isVariable(obj)) {
+ if (editor.get('designMode') === 'live') {
+ return {
+ type: 'JSExpression',
+ value: obj.variable,
+ mock: obj.value,
+ };
+ }
+ obj = obj.value;
+ }
+ if (!obj) {
+ return obj;
+ }
+ if (Array.isArray(obj)) {
+ return obj.map((item) => deepValueParser(item, { node }));
+ }
+ if (isPlainObject(obj)) {
+ if (isI18nData(obj)) {
+ // FIXME! use editor.get
+ let locale = Env.getLocale();
+ if (obj.key && i18nUtil.get(obj.key, locale)) {
+ return i18nUtil.get(obj.key, locale, {
+ node,
+ path,
+ });
+ }
+ if (locale !== 'zh_CN' && locale !== 'zh_TW' && !obj[locale]) {
+ locale = 'en_US';
+ }
+ return obj[obj.use || locale] || obj.zh_CN;
+ }
+
+ if (isJSSlot(obj)) {
+ return obj;
+ }
+ const out: any = {};
+ Object.keys(obj).forEach((key) => {
+ out[key] = deepValueParser(obj[key], {
+ node,
+ path: path ? `${path}.${key}` : key,
+ });
+ });
+ return out;
+ }
+ return obj;
+}
diff --git a/packages/vision-polyfill/src/reducers.ts b/packages/vision-polyfill/src/reducers.ts
new file mode 100644
index 000000000..0a2c67682
--- /dev/null
+++ b/packages/vision-polyfill/src/reducers.ts
@@ -0,0 +1,67 @@
+import { editor, designer, designerCabin } from '@ali/lowcode-engine';
+import bus from './bus';
+import { VE_EVENTS } from './base/const';
+
+import { valueParser } from './props-reducers/value-parser';
+import { liveEditingRule, liveEditingSaveHander } from './vc-live-editing';
+import {
+ compatibleReducer,
+ upgradePageLifeCyclesReducer,
+ stylePropsReducer,
+ upgradePropsReducer,
+ filterReducer,
+ removeEmptyPropsReducer,
+ initNodeReducer,
+ liveLifecycleReducer,
+ nodeTopFixedReducer,
+ resetLoopDefaultValueReducer,
+ filterEmptyEventReducer,
+} from './props-reducers';
+
+const { LiveEditing, TransformStage } = designerCabin;
+
+// 清理引擎自带的规则和保存函数,会影响 vc i18n 的保存
+LiveEditing.clearLiveEditingSpecificRule();
+LiveEditing.clearLiveEditingSaveHandler();
+LiveEditing.addLiveEditingSpecificRule(liveEditingRule);
+LiveEditing.addLiveEditingSaveHandler(liveEditingSaveHander);
+
+designer.project.onCurrentDocumentChange((doc) => {
+ bus.emit(VE_EVENTS.VE_PAGE_PAGE_READY);
+ editor.set('currentDocument', doc);
+});
+
+// 升级 Props
+designer.addPropsReducer(upgradePropsReducer, TransformStage.Upgrade);
+
+// 节点 props 初始化
+designer.addPropsReducer(initNodeReducer, TransformStage.Init);
+
+designer.addPropsReducer(liveLifecycleReducer, TransformStage.Render);
+
+designer.addPropsReducer(filterReducer, TransformStage.Save);
+designer.addPropsReducer(filterReducer, TransformStage.Render);
+
+designer.addPropsReducer(filterEmptyEventReducer, TransformStage.Save);
+designer.addPropsReducer(filterEmptyEventReducer, TransformStage.Render);
+
+// FIXME: Dirty fix, will remove this reducer
+designer.addPropsReducer(compatibleReducer, TransformStage.Save);
+// 兼容历史版本的 Page 组件
+designer.addPropsReducer(upgradePageLifeCyclesReducer, TransformStage.Save);
+
+// 设计器组件样式处理
+designer.addPropsReducer(stylePropsReducer, TransformStage.Render);
+// 国际化 & Expression 渲染时处理
+designer.addPropsReducer(valueParser, TransformStage.Render);
+
+// Init 的时候没有拿到 dataSource, 只能在 Render 和 Save 的时候都调用一次,理论上执行时机在 Init
+// Render 和 Save 都要各调用一次,感觉也是有问题的,是不是应该在 Render 执行一次就行了?见上 filterReducer 也是一样的处理方式。
+designer.addPropsReducer(removeEmptyPropsReducer, TransformStage.Render);
+designer.addPropsReducer(removeEmptyPropsReducer, TransformStage.Save);
+
+designer.addPropsReducer(nodeTopFixedReducer, TransformStage.Render);
+designer.addPropsReducer(nodeTopFixedReducer, TransformStage.Save);
+
+// loop的默认值处理
+designer.addPropsReducer(resetLoopDefaultValueReducer, TransformStage.Save);
diff --git a/packages/vision-polyfill/src/root-node-visitor.ts b/packages/vision-polyfill/src/root-node-visitor.ts
new file mode 100644
index 000000000..3efd4d017
--- /dev/null
+++ b/packages/vision-polyfill/src/root-node-visitor.ts
@@ -0,0 +1,95 @@
+import { findIndex } from 'lodash';
+import { DocumentModel, Node, RootNode } from '@ali/lowcode-designer';
+
+/**
+ * RootNodeVisitor for VisualEngine Page
+ *
+ * - store / cache node
+ * - quickly find / search or do operations on Node
+ */
+export default class RootNodeVisitor {
+ public nodeIdMap: {[id: string]: Node} = {};
+
+ public nodeFieldIdMap: {[fieldId: string]: Node} = {};
+
+ public nodeList: Node[] = [];
+
+ private page: DocumentModel;
+
+ private root: RootNode;
+
+ private cancelers: Function[] = [];
+
+ constructor(page: DocumentModel, rootNode: RootNode) {
+ this.page = page;
+ this.root = rootNode;
+
+ this._findNode(this.root);
+ this._init();
+ }
+
+ public getNodeList() {
+ return this.nodeList;
+ }
+
+ public getNodeIdMap() {
+ return this.nodeIdMap;
+ }
+
+ public getNodeFieldIdMap() {
+ return this.nodeFieldIdMap;
+ }
+
+ public getNodeById(id?: string) {
+ if (!id) { return this.nodeIdMap; }
+ return this.nodeIdMap[id];
+ }
+
+ public getNodeByFieldId(fieldId?: string) {
+ if (!fieldId) { return this.nodeFieldIdMap; }
+ return this.nodeFieldIdMap[fieldId];
+ }
+
+ public destroy() {
+ this.cancelers.forEach((canceler) => canceler());
+ }
+
+ private _init() {
+ this.cancelers.push(
+ this.page.onNodeCreate((node) => {
+ this.nodeList.push(node);
+ this.nodeIdMap[node.id] = node;
+ if (node.getPropValue('fieldId')) {
+ this.nodeFieldIdMap[node.getPropValue('fieldId')] = node;
+ }
+ }),
+ );
+
+ this.cancelers.push(
+ this.page.onNodeDestroy((node) => {
+ const idx = findIndex(this.nodeList, (n) => node.id === n.id);
+ this.nodeList.splice(idx, 1);
+ delete this.nodeIdMap[node.id];
+ if (node.getPropValue('fieldId')) {
+ delete this.nodeFieldIdMap[node.getPropValue('fieldId')];
+ }
+ }),
+ );
+ }
+
+ private _findNode(node: Node) {
+ const props = node.getProps();
+ const fieldId = props && props.getPropValue('fieldId');
+
+ this.nodeIdMap[node.getId()] = node;
+ this.nodeList.push(node);
+ if (fieldId) {
+ this.nodeFieldIdMap[fieldId] = node;
+ }
+
+ const children = node.getChildren();
+ if (children) {
+ children.forEach((child) => this._findNode(child));
+ }
+ }
+}
diff --git a/packages/vision-polyfill/src/symbols.ts b/packages/vision-polyfill/src/symbols.ts
new file mode 100644
index 000000000..81091e371
--- /dev/null
+++ b/packages/vision-polyfill/src/symbols.ts
@@ -0,0 +1,17 @@
+export class SymbolManager {
+ private symbolMap: { [symbolName: string]: symbol } = {};
+
+ public create(name: string): symbol {
+ if (this.symbolMap[name]) {
+ return this.symbolMap[name];
+ }
+ this.symbolMap[name] = Symbol(name);
+ return this.symbolMap[name];
+ }
+
+ public get(name: string) {
+ return this.symbolMap[name];
+ }
+}
+
+export default new SymbolManager();
diff --git a/packages/vision-polyfill/src/utils/index.ts b/packages/vision-polyfill/src/utils/index.ts
new file mode 100644
index 000000000..c815eb5ac
--- /dev/null
+++ b/packages/vision-polyfill/src/utils/index.ts
@@ -0,0 +1,21 @@
+import { designer } from '@ali/lowcode-engine';
+export { isVariable } from '@ali/lowcode-utils';
+
+export function getCurrentFieldIds() {
+ const fieldIds: any = [];
+ const nodesMap = designer?.currentDocument?.nodesMap || new Map();
+ nodesMap.forEach((curNode: any) => {
+ const fieldId = nodesMap?.get(curNode.id)?.getPropValue('fieldId');
+ if (fieldId) {
+ fieldIds.push(fieldId);
+ }
+ });
+ return { doc: designer?.currentDocument, fieldIds };
+}
+
+export function invariant(check: any, message: string, thing?: any) {
+ if (!check) {
+ throw new Error(`Invariant failed: ${ message }${thing ? ` in '${thing}'` : ''}`);
+ }
+}
+
diff --git a/packages/vision-polyfill/src/vc-live-editing.ts b/packages/vision-polyfill/src/vc-live-editing.ts
new file mode 100644
index 000000000..9d4e4af80
--- /dev/null
+++ b/packages/vision-polyfill/src/vc-live-editing.ts
@@ -0,0 +1,116 @@
+import { EditingTarget, Node as DocNode, SaveHandler } from '@ali/lowcode-designer';
+import Env from './env';
+import { isJSExpression, isI18nData } from '@ali/lowcode-types';
+import i18nUtil from './i18n-util';
+
+interface I18nObject {
+ type?: string;
+ use?: string;
+ key?: string;
+ [lang: string]: string | undefined;
+}
+
+function getI18nText(obj: I18nObject) {
+ let locale = Env.getLocale();
+ if (obj.key) {
+ return i18nUtil.get(obj.key, locale);
+ }
+ if (locale !== 'zh_CN' && locale !== 'zh_TW' && !obj[locale]) {
+ locale = 'en_US';
+ }
+ return obj[obj.use || locale] || obj.zh_CN;
+}
+
+function getText(node: DocNode, prop: string) {
+ const p = node.getProp(prop, false);
+ if (!p || p.isUnset()) {
+ return null;
+ }
+ let v = p.getValue();
+ if (isJSExpression(v)) {
+ v = v.mock;
+ }
+ if (v == null) {
+ return null;
+ }
+ if (p.type === 'literal') {
+ return v;
+ }
+ if ((v as any).type === 'i18n') {
+ return getI18nText(v as any);
+ }
+ return Symbol.for('not-literal');
+}
+
+export function liveEditingRule(target: EditingTarget) {
+ // for vision components specific
+ const { node, rootElement, event } = target;
+
+ const targetElement = event.target as HTMLElement;
+
+ if (!Array.from(targetElement.childNodes).every(item => item.nodeType === Node.TEXT_NODE)) {
+ return null;
+ }
+
+ const { innerText } = targetElement;
+ const propTarget = ['title', 'label', 'text', 'content', 'children'].find(prop => {
+ return equalText(getText(node, prop), innerText);
+ });
+
+ if (propTarget) {
+ return {
+ propElement: targetElement,
+ propTarget,
+ };
+ }
+ return null;
+}
+
+function equalText(v: any, innerText: string) {
+ // TODO: enhance compare text logic
+ if (typeof v !== 'string') {
+ return false;
+ }
+ return v.trim() === innerText;
+}
+
+export const liveEditingSaveHander: SaveHandler = {
+ condition: (prop) => {
+ const v = prop.getValue();
+ return prop.type === 'expression' || isI18nData(v);
+ },
+ onSaveContent: (content, prop) => {
+ const v = prop.getValue();
+ const locale = Env.getLocale();
+ let data = v;
+ if (isJSExpression(v)) {
+ data = v.mock;
+ }
+ if (isI18nData(data)) {
+ const i18n = data.key ? i18nUtil.getItem(data.key) : null;
+ if (i18n) {
+ i18n.setDoc(content, locale);
+ return;
+ }
+ data = {
+ ...(data as any),
+ [locale]: content,
+ };
+ } else {
+ data = content;
+ }
+ if (isJSExpression(v)) {
+ prop.setValue({
+ type: 'JSExpression',
+ value: v.value,
+ mock: data,
+ });
+ } else {
+ prop.setValue(data);
+ }
+ },
+};
+// TODO:
+// 非文本编辑
+// 国际化数据,改变当前
+// JSExpression, 改变 mock 或 弹出绑定变量
diff --git a/packages/vision-polyfill/src/viewport.ts b/packages/vision-polyfill/src/viewport.ts
new file mode 100644
index 000000000..65feed571
--- /dev/null
+++ b/packages/vision-polyfill/src/viewport.ts
@@ -0,0 +1,289 @@
+import { EventEmitter } from 'events';
+import Flags from './flags';
+import { editor } from '@ali/lowcode-engine';
+
+const domReady = require('domready');
+
+function enterFullscreen() {
+ const elem = document.documentElement;
+ if (elem.requestFullscreen) {
+ elem.requestFullscreen();
+ }
+}
+
+function exitFullscreen() {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ }
+}
+
+function isFullscreen() {
+ return document.fullscreen || false;
+}
+
+interface IStyleResourceConfig {
+ media?: string;
+ type?: string;
+ content?: string;
+}
+
+class StyleResource {
+ config: IStyleResourceConfig;
+
+ styleElement: HTMLStyleElement;
+
+ mounted: boolean;
+
+ inited: boolean;
+
+ constructor(config: IStyleResourceConfig) {
+ this.config = config || {};
+ }
+
+ matchDevice(device: string) {
+ const { media } = this.config;
+
+ if (!media || media === 'ALL' || media === '*') {
+ return true;
+ }
+
+ return media.toUpperCase() === device.toUpperCase();
+ }
+
+ init() {
+ if (this.inited) {
+ return;
+ }
+
+ this.inited = true;
+
+ const { type, content } = this.config;
+ let styleElement: any;
+ if (type === 'URL') {
+ styleElement = document.createElement('link');
+ styleElement.href = content || '';
+ styleElement.rel = 'stylesheet';
+ } else {
+ styleElement = document.createElement('style');
+ styleElement.setAttribute('type', 'text/css');
+ if (styleElement.styleSheet) {
+ styleElement.styleSheet.cssText = content;
+ } else {
+ styleElement.appendChild(document.createTextNode(content || ''));
+ }
+ }
+ this.styleElement = styleElement;
+ }
+
+ apply() {
+ if (this.mounted) {
+ return;
+ }
+
+ this.init();
+ document.head.appendChild(this.styleElement);
+ this.mounted = true;
+ }
+
+ unmount() {
+ if (!this.mounted) {
+ return;
+ }
+ document.head.removeChild(this.styleElement);
+ this.mounted = false;
+ }
+}
+
+export class Viewport {
+ preview: boolean;
+
+ focused: boolean;
+
+ slateFixed: boolean;
+
+ emitter: EventEmitter;
+
+ device: string;
+
+ focusTarget: any;
+
+ cssResourceSet: StyleResource[];
+
+ constructor() {
+ this.preview = false;
+ this.emitter = new EventEmitter();
+ document.addEventListener('webkitfullscreenchange', () => {
+ this.emitter.emit('fullscreenchange', this.isFullscreen());
+ });
+ domReady(() => this.applyMediaCSS());
+ }
+
+ setFullscreen(flag: boolean) {
+ const fullscreen = this.isFullscreen();
+ if (fullscreen && !flag) {
+ exitFullscreen();
+ } else if (!fullscreen && flag) {
+ enterFullscreen();
+ }
+ }
+
+ toggleFullscreen() {
+ if (this.isFullscreen()) {
+ exitFullscreen();
+ } else {
+ enterFullscreen();
+ }
+ }
+
+ isFullscreen() {
+ return isFullscreen();
+ }
+
+ setFocus(flag: boolean) {
+ if (this.focused && !flag) {
+ this.focused = false;
+ Flags.remove('view-focused');
+ this.emitter.emit('focuschange', false);
+ } else if (!this.focused && flag) {
+ this.focused = true;
+ Flags.add('view-focused');
+ this.emitter.emit('focuschange', true);
+ }
+ }
+
+ setFocusTarget(focusTarget: any) {
+ this.focusTarget = focusTarget;
+ }
+
+ returnFocus() {
+ if (this.focusTarget) {
+ this.focusTarget.focus();
+ }
+ }
+
+ isFocus() {
+ return this.focused;
+ }
+
+ setPreview(flag: boolean) {
+ if (this.preview && !flag) {
+ this.preview = false;
+ Flags.setPreviewMode(false);
+ this.emitter.emit('preview', false);
+ this.changeViewport();
+ } else if (!this.preview && flag) {
+ this.preview = true;
+ Flags.setPreviewMode(true);
+ this.emitter.emit('preview', true);
+ this.changeViewport();
+ }
+ }
+
+ togglePreview() {
+ if (this.isPreview()) {
+ this.setPreview(false);
+ } else {
+ this.setPreview(true);
+ }
+ }
+
+ isPreview() {
+ return this.preview;
+ }
+
+ async setDevice(device = 'pc') {
+ if (this.getDevice() !== device) {
+ this.device = device;
+ const simulator = await editor.onceGot('simulator');
+ simulator?.set('device', device === 'mobile' ? 'mobile' : 'default');
+ // Flags.setSimulator(device);
+ // this.applyMediaCSS();
+ this.emitter.emit('devicechange', device);
+ this.changeViewport();
+ }
+ }
+
+ getDevice() {
+ return this.device || 'pc';
+ }
+
+ changeViewport() {
+ this.emitter.emit('viewportchange', this.getViewport());
+ }
+
+ getViewport() {
+ return `${this.isPreview() ? 'preview' : 'design'}-${this.getDevice()}`;
+ }
+
+ applyMediaCSS() {
+ if (!document.head || !this.cssResourceSet) {
+ return;
+ }
+ const device = this.getDevice();
+ this.cssResourceSet.forEach((item) => {
+ if (item.matchDevice(device)) {
+ item.apply();
+ } else {
+ item.unmount();
+ }
+ });
+ }
+
+ setGlobalCSS(resourceSet: IStyleResourceConfig[]) {
+ if (this.cssResourceSet) {
+ this.cssResourceSet.forEach((item) => {
+ item.unmount();
+ });
+ }
+ this.cssResourceSet = resourceSet.map((item: IStyleResourceConfig) => new StyleResource(item)).reverse();
+ this.applyMediaCSS();
+ }
+
+ setWithShell(shell: string) {
+ // Flags.setWithShell(shell);
+ }
+
+ onFullscreenChange(func: () => any) {
+ this.emitter.on('fullscreenchange', func);
+ return () => {
+ this.emitter.removeListener('fullscreenchange', func);
+ };
+ }
+
+ onPreview(func: () => any) {
+ this.emitter.on('preview', func);
+ return () => {
+ this.emitter.removeListener('preview', func);
+ };
+ }
+
+ onDeviceChange(func: () => any) {
+ this.emitter.on('devicechange', func);
+ return () => {
+ this.emitter.removeListener('devicechange', func);
+ };
+ }
+
+ onSlateFixedChange(func: (flag: boolean) => any) {
+ this.emitter.on('slatefixed', func);
+ return () => {
+ this.emitter.removeListener('slatefixed', func);
+ };
+ }
+
+ onViewportChange(func: () => any) {
+ this.emitter.on('viewportchange', func);
+ return () => {
+ this.emitter.removeListener('viewportchange', func);
+ };
+ }
+
+ onFocusChange(func: (flag: boolean) => any) {
+ this.emitter.on('focuschange', func);
+ return () => {
+ this.emitter.removeListener('focuschange', func);
+ };
+ }
+}
+
+export default new Viewport();
diff --git a/packages/vision-polyfill/src/vision.less b/packages/vision-polyfill/src/vision.less
new file mode 100644
index 000000000..82ae7f3e9
--- /dev/null
+++ b/packages/vision-polyfill/src/vision.less
@@ -0,0 +1,128 @@
+html.engine-cursor-move, html.engine-cursor-move * {
+ cursor: grabbing !important;
+}
+
+html.engine-cursor-copy, html.engine-cursor-copy * {
+ cursor: copy !important;
+}
+
+html.engine-cursor-ew-resize, html.engine-cursor-ew-resize * {
+ cursor: ew-resize !important;
+}
+
+body, #engine {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+ text-rendering: optimizeLegibility;
+ -webkit-user-select: none;
+ -webkit-user-drag: none;
+ -webkit-text-size-adjust: none;
+ -webkit-touch-callout: none;
+ -webkit-font-smoothing: antialiased;
+}
+
+html {
+ min-width: 1024px;
+}
+
+::-webkit-scrollbar {
+ width: 5px;
+ height: 5px;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.3);
+ border-radius: 5px;
+}
+
+html.engine-blur #engine {
+ filter: blur(4px);
+}
+
+.engine-main {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+
+ .ve-icon-button {
+ > .ve-icon-contents {
+ color: var(--color-text, rgba(51,51,51,.6));
+ }
+ &:hover, &.active {
+ > .ve-icon-contents {
+ color: var(--color-text-light, rgba(51,51,51,.8));
+ }
+ }
+ }
+}
+
+.engine-empty {
+ background: #f2f3f5;
+ color: #a7b1bd;
+ outline: 1px dashed rgba(31, 56, 88, 0.2);
+ outline-offset: -1px !important;
+ height: 66px;
+ max-height: 100%;
+ min-width: 140px;
+ text-align: center;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+}
+
+.engine-empty:before {
+ content: '\62D6\62FD\7EC4\4EF6\6216\6A21\677F\5230\8FD9\91CC';
+ font-size: 14px;
+ z-index: 1;
+ width: 100%;
+ white-space: nowrap;
+}
+
+
+// dirty fix override vision reset
+.engine-design-mode {
+ .next-input-group,
+ .next-checkbox-group,.next-date-picker,.next-input,.next-month-picker,
+ .next-number-picker,.next-radio-group,.next-range,.next-range-picker,
+ .next-rating,.next-select,.next-switch,.next-time-picker,.next-upload,
+ .next-year-picker,
+ .next-breadcrumb-item,.next-calendar-header,.next-calendar-table {
+ pointer-events: auto !important;
+ }
+}
+
+.vs-icon .vs-icon-del, .vs-icon .vs-icon-entry {
+ width: 16px!important;
+ height: 16px!important;
+}
+
+// .lc-left-float-pane {
+// font-size: 14px;
+// }
+
+html.engine-preview-mode {
+ .lc-left-area, .lc-right-area {
+ display: none !important;
+ }
+}
+
+.ve-popups .ve-message {
+ right: calc(var(--right-area-width, 300px) + 10px);
+
+ .ve-message-content {
+ display: flex;
+ align-items: center;
+ line-height: 22px;
+ }
+}
+
+.lc-setter-mixed {
+ flex: 1;
+}
diff --git a/packages/vision-polyfill/tests/bundle/bundle.test.ts b/packages/vision-polyfill/tests/bundle/bundle.test.ts
new file mode 100644
index 000000000..31174d481
--- /dev/null
+++ b/packages/vision-polyfill/tests/bundle/bundle.test.ts
@@ -0,0 +1,116 @@
+import { Component } from 'react';
+import set from 'lodash/set';
+import cloneDeep from 'lodash/clonedeep';
+import '../fixtures/window';
+import divPrototypeConfig from '../fixtures/prototype/div-vision';
+import trunk from '../../src/bundle/trunk';
+import Prototype from '../../src/bundle/prototype';
+import Bundle from '../../src/bundle/bundle';
+import { Editor } from '@ali/lowcode-editor-core';
+
+jest.mock('../../src/bundle/trunk', () => {
+ // mockComponentPrototype = jest.fn();
+ // return {
+ // mockComponentPrototype: jest.fn().mockImplementation(() => {
+ // return proto;
+ // }),
+ // }
+ // return jest.fn().mockImplementation(() => {
+ // return {playSoundFile: fakePlaySoundFile};
+ // });
+ // return jest.fn().mockImplementation(() => {
+ // return { mockComponentPrototype };
+ // });
+ return {
+ __esModule: true,
+ default: {
+ mockComponentPrototype: jest.fn(),
+ },
+ };
+});
+
+function wrap(name, thing) {
+ return {
+ name,
+ componentName: name,
+ category: '布局',
+ module: thing,
+ };
+}
+
+const proto1 = new Prototype(divPrototypeConfig);
+const protoConfig2 = cloneDeep(divPrototypeConfig);
+set(protoConfig2, 'componentName', 'Div2');
+const proto2 = new Prototype(protoConfig2);
+
+const protoConfig3 = cloneDeep(divPrototypeConfig);
+set(protoConfig3, 'componentName', 'Div3');
+const proto3 = new Prototype(protoConfig3);
+
+const protoConfig4 = cloneDeep(divPrototypeConfig);
+set(protoConfig4, 'componentName', 'Div4');
+const proto4 = new Prototype(protoConfig4);
+
+const protoConfig5 = cloneDeep(divPrototypeConfig);
+set(protoConfig5, 'componentName', 'Div5');
+const proto5 = new Prototype(protoConfig5);
+
+function getComponentProtos() {
+ return [
+ wrap('Div', proto1),
+ // wrap('Div2', proto2),
+ // wrap('Div3', proto3),
+ wrap('DivPortal', [proto2, proto3]),
+ ];
+}
+
+class Div extends Component {}
+Div.displayName = 'Div';
+class Div2 extends Component {}
+Div2.displayName = 'Div2';
+class Div3 extends Component {}
+Div3.displayName = 'Div3';
+class Div4 extends Component {}
+Div4.displayName = 'Div4';
+class Div5 extends Component {}
+Div5.displayName = 'Div5';
+
+function getComponentViews() {
+ return [
+ wrap('Div', Div),
+ // wrap('Div2', Div2),
+ // wrap('Div3', Div3),
+ wrap('DivPortal', [Div2, Div3]),
+ ];
+}
+
+describe('Bundle', () => {
+ it('构造函数', () => {
+ const protos = getComponentProtos();
+ const views = getComponentViews();
+ const bundle = new Bundle(protos, views);
+ expect(bundle.getList()).toHaveLength(3);
+ expect(bundle.get('Div')).toBe(proto1);
+ expect(bundle.get('Div2')).toBe(proto2);
+ expect(bundle.get('Div3')).toBe(proto3);
+ bundle.addComponentBundle([proto4, Div4]);
+ expect(bundle.getList()).toHaveLength(4);
+ expect(bundle.get('Div4')).toBe(proto4);
+ bundle.replacePrototype('Div4', proto3);
+ expect(proto3.getView()).toBe(Div4);
+
+ bundle.removeComponentBundle('Div2');
+ expect(bundle.getList()).toHaveLength(3);
+ expect(bundle.get('Div2')).toBeUndefined;
+
+ expect(bundle.getFromMeta('Div')).toBe(proto1);
+ bundle.getFromMeta('Div5');
+ expect(bundle.getList()).toHaveLength(4);
+ });
+ it('静态方法 create', () => {
+ const protos = getComponentProtos();
+ const views = getComponentViews();
+ const bundle = Bundle.create(protos, views);
+ expect(bundle).toBeTruthy();
+ });
+});
diff --git a/packages/vision-polyfill/tests/bundle/prototype.test.ts b/packages/vision-polyfill/tests/bundle/prototype.test.ts
new file mode 100644
index 000000000..525a8a01e
--- /dev/null
+++ b/packages/vision-polyfill/tests/bundle/prototype.test.ts
@@ -0,0 +1,219 @@
+import { Component } from 'react';
+import set from 'lodash/set';
+import cloneDeep from 'lodash/clonedeep';
+import '../fixtures/window';
+// import { Project } from '../../src/project/project';
+// import { Node } from '../../src/document/node/node';
+// import { Designer } from '../../src/designer/designer';
+import divPrototypeConfig from '../fixtures/prototype/div-vision';
+import divFullPrototypeConfig from '../fixtures/prototype/div-vision-full';
+import divPrototypeMeta from '../fixtures/prototype/div-meta';
+// import VisualEngine from '../../src';
+import { designer } from '../../src/reducers';
+import Prototype, { isPrototype } from '../../src/bundle/prototype';
+import { Editor } from '@ali/lowcode-editor-core';
+// import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
+
+describe('Prototype', () => {
+ it('构造函数 - OldPrototypeConfig', () => {
+ const proto = new Prototype(divPrototypeConfig);
+ expect(isPrototype(proto)).toBeTruthy;
+ expect(proto.getComponentName()).toBe('Div');
+ expect(proto.getId()).toBe('Div');
+ expect(proto.getCategory()).toBe('布局');
+ expect(proto.getDocUrl()).toBe(
+ 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ );
+ expect(proto.getIcon()).toBeUndefined;
+ expect(proto.getTitle()).toBe('Div');
+ expect(proto.isPrototype).toBeTruthy;
+ expect(proto.isContainer()).toBeTruthy;
+ expect(proto.isModal()).toBeFalsy;
+ });
+ it('构造函数 - 全量 OldPrototypeConfig', () => {
+ const proto = new Prototype(divFullPrototypeConfig);
+ expect(isPrototype(proto)).toBeTruthy;
+ expect(proto.getComponentName()).toBe('Div');
+ expect(proto.getId()).toBe('Div');
+ expect(proto.getCategory()).toBe('布局');
+ expect(proto.getDocUrl()).toBe(
+ 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ );
+ expect(proto.getIcon()).toBeUndefined;
+ expect(proto.getTitle()).toBe('Div');
+ expect(proto.isPrototype).toBeTruthy;
+ expect(proto.isContainer()).toBeTruthy;
+ expect(proto.isModal()).toBeFalsy;
+ });
+ it('构造函数 - ComponentMetadata', () => {
+ const proto = new Prototype(divPrototypeMeta);
+ expect(proto.getComponentName()).toBe('Div');
+ expect(proto.getId()).toBe('Div');
+ expect(proto.getCategory()).toBe('布局');
+ expect(proto.getDocUrl()).toBe(
+ 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ );
+ expect(proto.getIcon()).toBeUndefined;
+ expect(proto.getTitle()).toBe('Div');
+ expect(proto.isPrototype).toBeTruthy;
+ expect(proto.isContainer()).toBeTruthy;
+ expect(proto.isModal()).toBeFalsy;
+ });
+ it('构造函数 - ComponentMeta', () => {
+ const meta = designer.createComponentMeta(divPrototypeMeta);
+ const proto = new Prototype(meta);
+ expect(proto.getComponentName()).toBe('Div');
+ expect(proto.getId()).toBe('Div');
+ expect(proto.getCategory()).toBe('布局');
+ expect(proto.getDocUrl()).toBe(
+ 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ );
+ expect(proto.getIcon()).toBeUndefined;
+ expect(proto.getTitle()).toBe('Div');
+ expect(proto.isPrototype).toBeTruthy;
+ expect(proto.isContainer()).toBeTruthy;
+ expect(proto.isModal()).toBeFalsy;
+ });
+ it('构造函数 - 静态函数 create', () => {
+ const proto = Prototype.create(divPrototypeConfig);
+ expect(proto.getComponentName()).toBe('Div');
+ expect(proto.getId()).toBe('Div');
+ expect(proto.getCategory()).toBe('布局');
+ expect(proto.getDocUrl()).toBe(
+ 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ );
+ expect(proto.getIcon()).toBeUndefined;
+ expect(proto.getTitle()).toBe('Div');
+ expect(proto.isPrototype).toBeTruthy;
+ expect(proto.isContainer()).toBeTruthy;
+ expect(proto.isModal()).toBeFalsy;
+ });
+ it('构造函数 - lookup: true', () => {
+ const proto = Prototype.create(divPrototypeConfig);
+ const proto2 = Prototype.create(divPrototypeConfig, {}, true);
+ expect(proto).toBe(proto2);
+ });
+ describe('类成员函数', () => {
+ let proto: Prototype = null;
+ beforeEach(() => {
+ proto = new Prototype(divPrototypeConfig);
+ });
+ afterEach(() => {
+ proto = null;
+ });
+ it('各种函数', () => {
+ expect(proto.componentName).toBe('Div');
+ expect(proto.getComponentName()).toBe('Div');
+ expect(proto.getId()).toBe('Div');
+ expect(proto.getContextInfo('anything')).toBeUndefined;
+ expect(proto.getPropConfigs()).toBe(divPrototypeConfig);
+ expect(proto.getConfig()).toBe(divPrototypeConfig);
+ expect(proto.getConfig('componentName')).toBe('Div');
+ expect(proto.getConfig('configure')).toBe(divPrototypeConfig.configure);
+ expect(proto.getConfig('docUrl')).toBe(
+ 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ );
+ expect(proto.getConfig('title')).toBe('容器');
+ expect(proto.getConfig('isContainer')).toBeTruthy;
+
+ class MockView extends Component {}
+
+ expect(proto.getView()).toBeUndefined;
+ proto.setView(MockView);
+ expect(proto.getView()).toBe(MockView);
+ expect(proto.meta.getMetadata().experimental?.view).toBe(MockView);
+
+ expect(proto.getPackageName()).toBeUndefined;
+ proto.setPackageName('@ali/vc-div');
+ expect(proto.getPackageName()).toBe('@ali/vc-div');
+
+ expect(proto.getConfig('category')).toBe('布局');
+ proto.setCategory('布局 new');
+ expect(proto.getConfig('category')).toBe('布局 new');
+
+ expect(proto.getConfigure()).toHaveLength(3);
+ expect(proto.getConfigure()[0].name).toBe('#props');
+ expect(proto.getConfigure()[1].name).toBe('#styles');
+ expect(proto.getConfigure()[2].name).toBe('#advanced');
+
+ expect(proto.getRectSelector()).toBeUndefined;
+ expect(proto.isAutoGenerated()).toBeFalsy;
+ });
+ });
+
+ describe('类成员函数', () => {
+ it('addGlobalPropsConfigure', () => {
+ Prototype.addGlobalPropsConfigure({
+ name: 'globalInsertProp1',
+ });
+ const proto1 = new Prototype(divPrototypeConfig);
+ expect(proto1.getConfigure()[2].items).toHaveLength(4);
+ expect(proto1.getConfigure()[2].items[3].name).toBe('globalInsertProp1');
+ Prototype.addGlobalPropsConfigure({
+ name: 'globalInsertProp2',
+ });
+ const proto2 = new Prototype(divPrototypeConfig);
+ expect(proto2.getConfigure()[2].items).toHaveLength(5);
+ expect(proto1.getConfigure()[2].items[4].name).toBe('globalInsertProp2');
+
+ Prototype.addGlobalPropsConfigure({
+ name: 'globalInsertProp3',
+ position: 'top',
+ });
+
+ const proto3 = new Prototype(divPrototypeConfig);
+ expect(proto3.getConfigure()[0].items).toHaveLength(3);
+ expect(proto1.getConfigure()[0].items[0].name).toBe('globalInsertProp3');
+ });
+
+ it('removeGlobalPropsConfigure', () => {
+ Prototype.removeGlobalPropsConfigure('globalInsertProp1');
+ Prototype.removeGlobalPropsConfigure('globalInsertProp2');
+ Prototype.removeGlobalPropsConfigure('globalInsertProp3');
+ const proto2 = new Prototype(divPrototypeConfig);
+ expect(proto2.getConfigure()[0].items).toHaveLength(2);
+ expect(proto2.getConfigure()[2].items).toHaveLength(3);
+ });
+
+ it('overridePropsConfigure', () => {
+ Prototype.addGlobalPropsConfigure({
+ name: 'globalInsertProp1',
+ title: 'globalInsertPropTitle',
+ position: 'top',
+ });
+ const proto1 = new Prototype(divPrototypeConfig);
+ expect(proto1.getConfigure()[0].items).toHaveLength(3);
+ expect(proto1.getConfigure()[0].items[0].name).toBe('globalInsertProp1');
+ expect(proto1.getConfigure()[0].items[0].title).toBe('globalInsertPropTitle');
+
+ Prototype.overridePropsConfigure('Div', [
+ {
+ name: 'globalInsertProp1',
+ title: 'globalInsertPropTitleChanged',
+ },
+ ]);
+ const proto2 = new Prototype(divPrototypeConfig);
+ expect(proto2.getConfigure()[0].name).toBe('globalInsertProp1');
+ expect(proto2.getConfigure()[0].title).toBe('globalInsertPropTitleChanged');
+
+ Prototype.overridePropsConfigure('Div', {
+ globalInsertProp1: {
+ name: 'globalInsertProp1',
+ title: 'globalInsertPropTitleChanged new',
+ position: 'top',
+ },
+ });
+ const proto3 = new Prototype(divPrototypeConfig);
+ expect(proto3.getConfigure()[0].items[0].name).toBe('globalInsertProp1');
+ expect(proto3.getConfigure()[0].items[0].title).toBe('globalInsertPropTitleChanged new');
+ });
+
+ it('addGlobalExtraActions', () => {
+ function haha() { return 'heihei'; }
+ Prototype.addGlobalExtraActions(haha);
+ const proto1 = new Prototype(divPrototypeConfig);
+ expect(proto1.meta.availableActions).toHaveLength(4);
+ expect(proto1.meta.availableActions[3].name).toBe('haha');
+ });
+ });
+});
diff --git a/packages/vision-polyfill/tests/bundle/trunk.test.ts b/packages/vision-polyfill/tests/bundle/trunk.test.ts
new file mode 100644
index 000000000..d4bca021f
--- /dev/null
+++ b/packages/vision-polyfill/tests/bundle/trunk.test.ts
@@ -0,0 +1,111 @@
+import { Component } from 'react';
+import set from 'lodash/set';
+import cloneDeep from 'lodash/clonedeep';
+import '../fixtures/window';
+import divPrototypeConfig from '../fixtures/prototype/div-vision';
+import Prototype from '../../src/bundle/prototype';
+import Bundle from '../../src/bundle/bundle';
+import trunk from '../../src/bundle/trunk';
+import lg from '@ali/vu-logger';
+
+const proto1 = new Prototype(divPrototypeConfig);
+const protoConfig2 = cloneDeep(divPrototypeConfig);
+set(protoConfig2, 'componentName', 'Div2');
+const proto2 = new Prototype(protoConfig2);
+
+const protoConfig3 = cloneDeep(divPrototypeConfig);
+set(protoConfig3, 'componentName', 'Div3');
+const proto3 = new Prototype(protoConfig3);
+
+const mockComponentPrototype = jest.fn();
+jest.mock('../../src/bundle/bundle', () => {
+ // return {
+ // mockComponentPrototype: jest.fn().mockImplementation(() => {
+ // return proto;
+ // }),
+ // }
+ // return jest.fn().mockImplementation(() => {
+ // return {playSoundFile: fakePlaySoundFile};
+ // });
+ return jest.fn().mockImplementation(() => {
+ return {
+ get: () => {},
+ getList: () => { return []; },
+ getFromMeta: () => {},
+ };
+ });
+});
+
+const mockError = jest.fn();
+jest.mock('@ali/vu-logger');
+lg.error = mockError;
+
+function wrap(name, thing) {
+ return {
+ name,
+ componentName: name,
+ category: '布局',
+ module: thing,
+ };
+}
+
+function getComponentProtos() {
+ return [
+ wrap('Div', proto1),
+ // wrap('Div2', proto2),
+ // wrap('Div3', proto3),
+ wrap('DivPortal', [proto2, proto3]),
+ ];
+}
+
+class Div extends Component {}
+Div.displayName = 'Div';
+class Div2 extends Component {}
+Div2.displayName = 'Div2';
+class Div3 extends Component {}
+Div3.displayName = 'Div3';
+
+function getComponentViews() {
+ return [
+ wrap('Div', Div),
+ // wrap('Div2', Div2),
+ // wrap('Div3', Div3),
+ wrap('DivPortal', [Div2, Div3]),
+ ];
+}
+
+describe('Trunk', () => {
+ it('构造函数', () => {
+ const warn = console.warn = jest.fn();
+ const trunkChangeHandler = jest.fn();
+ const off = trunk.onTrunkChange(trunkChangeHandler);
+ trunk.addBundle(new Bundle([proto1], [Div]));
+ trunk.addBundle(new Bundle([proto2], [Div2]));
+ expect(trunkChangeHandler).toHaveBeenCalledTimes(2);
+ off();
+ trunk.addBundle(new Bundle([proto3], [Div3]));
+ expect(trunkChangeHandler).toHaveBeenCalledTimes(2);
+ trunk.getList();
+ trunk.getPrototype('Div');
+ trunk.getPrototypeById('Div');
+ trunk.getPrototypeView('Div');
+ trunk.listByCategory();
+ expect(trunk.mockComponentPrototype(new Bundle([proto3], [Div3]))).toBeUndefined;
+ expect(mockError).toHaveBeenCalled();
+ trunk.registerComponentPrototypeMocker({ mockPrototype: jest.fn().mockImplementation(() => { return proto3; }) });
+ expect(trunk.mockComponentPrototype(new Bundle([proto3], [Div3]))).toBe(proto3);
+ const hahaSetter = () => 'haha';
+ trunk.registerSetter('haha', hahaSetter);
+ expect(trunk.getSetter('haha')).toBe(hahaSetter);
+ trunk.getRecents(5);
+ trunk.setPackages();
+ expect(warn).toHaveBeenCalledTimes(1);
+ trunk.beforeLoadBundle();
+ expect(warn).toHaveBeenCalledTimes(2);
+ trunk.afterLoadBundle();
+ expect(warn).toHaveBeenCalledTimes(3);
+ trunk.getBundle();
+ expect(warn).toHaveBeenCalledTimes(4);
+ expect(trunk.isReady()).toBeTruthy;
+ });
+});
diff --git a/packages/vision-polyfill/tests/fixtures/prototype/div-meta.ts b/packages/vision-polyfill/tests/fixtures/prototype/div-meta.ts
new file mode 100644
index 000000000..a2b410494
--- /dev/null
+++ b/packages/vision-polyfill/tests/fixtures/prototype/div-meta.ts
@@ -0,0 +1,259 @@
+export default {
+ componentName: 'Div',
+ title: '容器',
+ docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ devMode: 'procode',
+ tags: ['布局'],
+ configure: {
+ props: [
+ {
+ type: 'field',
+ name: 'behavior',
+ title: '默认状态',
+ extraProps: {
+ display: 'inline',
+ defaultValue: 'NORMAL',
+ },
+ setter: {
+ componentName: 'MixedSetter',
+ props: {
+ setters: [
+ {
+ key: null,
+ ref: null,
+ props: {
+ options: [
+ {
+ title: '普通',
+ value: 'NORMAL',
+ },
+ {
+ title: '隐藏',
+ value: 'HIDDEN',
+ },
+ ],
+ loose: false,
+ cancelable: false,
+ },
+ _owner: null,
+ },
+ 'VariableSetter',
+ ],
+ },
+ },
+ },
+ {
+ type: 'field',
+ name: '__style__',
+ title: {
+ label: '样式设置',
+ tip: '点击 ? 查看样式设置器用法指南',
+ docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
+ },
+ extraProps: {
+ display: 'accordion',
+ defaultValue: {},
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ advanced: true,
+ },
+ _owner: null,
+ },
+ },
+ {
+ type: 'group',
+ name: 'groupkh97h5kc',
+ title: '高级',
+ extraProps: {
+ display: 'accordion',
+ },
+ items: [
+ {
+ type: 'field',
+ name: 'fieldId',
+ title: {
+ label: '唯一标识',
+ },
+ extraProps: {
+ display: 'block',
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ placeholder: '请输入唯一标识',
+ multiline: false,
+ rows: 10,
+ required: false,
+ pattern: null,
+ maxLength: null,
+ },
+ _owner: null,
+ },
+ },
+ {
+ type: 'field',
+ name: 'useFieldIdAsDomId',
+ title: {
+ label: '将唯一标识用作 DOM ID',
+ },
+ extraProps: {
+ display: 'block',
+ defaultValue: false,
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {},
+ _owner: null,
+ },
+ },
+ {
+ type: 'field',
+ name: 'customClassName',
+ title: '自定义样式类',
+ extraProps: {
+ display: 'block',
+ defaultValue: '',
+ },
+ setter: {
+ componentName: 'MixedSetter',
+ props: {
+ setters: [
+ {
+ key: null,
+ ref: null,
+ props: {
+ placeholder: null,
+ multiline: false,
+ rows: 10,
+ required: false,
+ pattern: null,
+ maxLength: null,
+ },
+ _owner: null,
+ },
+ 'VariableSetter',
+ ],
+ },
+ },
+ },
+ {
+ type: 'field',
+ name: 'events',
+ title: {
+ label: '动作设置',
+ tip: '点击 ? 查看如何设置组件的事件响应动作',
+ docUrl: 'https://lark.alipay.com/legao/legao/events-call',
+ },
+ extraProps: {
+ display: 'accordion',
+ defaultValue: {
+ ignored: true,
+ },
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ events: [
+ {
+ name: 'onClick',
+ title: '当点击时',
+ initialValue:
+ "/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
+ },
+ {
+ name: 'onMouseEnter',
+ title: '当鼠标进入时',
+ initialValue:
+ "/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
+ },
+ {
+ name: 'onMouseLeave',
+ title: '当鼠标离开时',
+ initialValue:
+ "/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
+ },
+ ],
+ },
+ _owner: null,
+ },
+ },
+ {
+ type: 'field',
+ name: 'onClick',
+ extraProps: {
+ defaultValue: {
+ ignored: true,
+ },
+ },
+ setter: 'I18nSetter',
+ },
+ {
+ type: 'field',
+ name: 'onMouseEnter',
+ extraProps: {
+ defaultValue: {
+ ignored: true,
+ },
+ },
+ setter: 'I18nSetter',
+ },
+ {
+ type: 'field',
+ name: 'onMouseLeave',
+ extraProps: {
+ defaultValue: {
+ ignored: true,
+ },
+ },
+ setter: 'I18nSetter',
+ },
+ ],
+ },
+ ],
+ component: {
+ isContainer: true,
+ nestingRule: {},
+ },
+ supports: {},
+ },
+ experimental: {
+ callbacks: {},
+ initials: [
+ {
+ name: 'behavior',
+ },
+ {
+ name: '__style__',
+ },
+ {
+ name: 'fieldId',
+ },
+ {
+ name: 'useFieldIdAsDomId',
+ },
+ {
+ name: 'customClassName',
+ },
+ {
+ name: 'events',
+ },
+ {
+ name: 'onClick',
+ },
+ {
+ name: 'onMouseEnter',
+ },
+ {
+ name: 'onMouseLeave',
+ },
+ ],
+ filters: [],
+ autoruns: [],
+ },
+};
diff --git a/packages/vision-polyfill/tests/fixtures/prototype/div-vision-full.ts b/packages/vision-polyfill/tests/fixtures/prototype/div-vision-full.ts
new file mode 100644
index 000000000..756c37649
--- /dev/null
+++ b/packages/vision-polyfill/tests/fixtures/prototype/div-vision-full.ts
@@ -0,0 +1,293 @@
+export default {
+ title: '容器',
+ componentName: 'Div',
+ docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ category: '布局',
+ isContainer: true,
+ canOperating: false,
+ extraActions: [],
+ canContain: 'Form',
+ canDropTo: 'Div',
+ canDropIn: 'Div',
+ canResizing: true,
+ canDraging: false,
+ context: {},
+ initialChildren() {},
+ didDropIn() {},
+ didDropOut() {},
+ subtreeModified() {},
+ onResize() {},
+ onResizeStart() {},
+ onResizeEnd() {},
+ canUseCondition: true,
+ canLoop: true,
+ snippets: [
+ {
+ screenshot: 'https://img.alicdn.com/tfs/TB1CHN3u4z1gK0jSZSgXXavwpXa-112-64.png',
+ label: '普通型',
+ schema: {
+ componentName: 'Div',
+ props: {},
+ },
+ },
+ ],
+ configure: [
+ {
+ name: 'myName',
+ title: '我的名字',
+ display: 'tab',
+ initialValue: 'NORMAL',
+ defaultValue: 'NORMAL',
+ collapsed: true,
+ supportVariable: true,
+ accessor(field, val) {},
+ mutator(field, val) {},
+ disabled() {
+ return true;
+ },
+ useVariableChange() {},
+ allowTextInput: true,
+ liveTextEditing: true,
+ setter: [
+ {
+ key: null,
+ ref: null,
+ props: {
+ options: [
+ {
+ title: '普通',
+ value: 'NORMAL',
+ },
+ {
+ title: '隐藏',
+ value: 'HIDDEN',
+ },
+ ],
+ loose: false,
+ cancelable: false,
+ },
+ _owner: null,
+ },
+ {
+ key: null,
+ ref: null,
+ props: {
+ options: [
+ {
+ title: '普通',
+ value: 'NORMAL',
+ },
+ {
+ title: '隐藏',
+ value: 'HIDDEN',
+ },
+ ],
+ loose: false,
+ cancelable: false,
+ },
+ _owner: null,
+ },
+ ],
+ },
+ {
+ name: 'mySlotName',
+ slotName: 'mySlotName',
+ slotTitle: '我的 Slot 名字',
+ display: 'tab',
+ initialValue: 'NORMAL',
+ defaultValue: 'NORMAL',
+ collapsed: true,
+ supportVariable: true,
+ accessor(field, val) {},
+ mutator(field, val) {},
+ disabled() {
+ return true;
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ options: [
+ {
+ title: '普通',
+ value: 'NORMAL',
+ },
+ {
+ title: '隐藏',
+ value: 'HIDDEN',
+ },
+ ],
+ loose: false,
+ cancelable: false,
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: 'behavior',
+ title: '默认状态',
+ display: 'inline',
+ initialValue: 'NORMAL',
+ supportVariable: true,
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ options: [
+ {
+ title: '普通',
+ value: 'NORMAL',
+ },
+ {
+ title: '隐藏',
+ value: 'HIDDEN',
+ },
+ ],
+ loose: false,
+ cancelable: false,
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: '__style__',
+ title: '样式设置',
+ display: 'accordion',
+ collapsed: false,
+ initialValue: {},
+ tip: {
+ url: 'https://lark.alipay.com/legao/help/design-tool-style',
+ content: '点击 ? 查看样式设置器用法指南',
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ advanced: true,
+ },
+ _owner: null,
+ },
+ },
+ {
+ type: 'group',
+ title: '高级',
+ display: 'accordion',
+ items: [
+ {
+ name: 'fieldId',
+ title: '唯一标识',
+ display: 'block',
+ tip:
+ '组件的唯一标识符,不能够与其它组件重名,不能够为空,且只能够使用以字母开头的,下划线以及数字的组合。',
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ placeholder: '请输入唯一标识',
+ multiline: false,
+ rows: 10,
+ required: false,
+ pattern: null,
+ maxLength: null,
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: 'useFieldIdAsDomId',
+ title: '将唯一标识用作 DOM ID',
+ display: 'block',
+ tip:
+ '开启这个配置项后,会在当前组件的 HTML 元素上加入 id="当前组件的 fieldId",一般用于做 utils 的绑定,不常用',
+ initialValue: false,
+ setter: {
+ key: null,
+ ref: null,
+ props: {},
+ _owner: null,
+ },
+ },
+ {
+ name: 'customClassName',
+ title: '自定义样式类',
+ display: 'block',
+ supportVariable: true,
+ initialValue: '',
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ placeholder: null,
+ multiline: false,
+ rows: 10,
+ required: false,
+ pattern: null,
+ maxLength: null,
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: 'events',
+ title: '动作设置',
+ tip: {
+ url: 'https://lark.alipay.com/legao/legao/events-call',
+ content: '点击 ? 查看如何设置组件的事件响应动作',
+ },
+ display: 'accordion',
+ initialValue: {
+ ignored: true,
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ events: [
+ {
+ name: 'onClick',
+ title: '当点击时',
+ initialValue:
+ "/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
+ },
+ {
+ name: 'onMouseEnter',
+ title: '当鼠标进入时',
+ initialValue:
+ "/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
+ },
+ {
+ name: 'onMouseLeave',
+ title: '当鼠标离开时',
+ initialValue:
+ "/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
+ },
+ ],
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: 'onClick',
+ display: 'none',
+ initialValue: {
+ ignored: true,
+ },
+ },
+ {
+ name: 'onMouseEnter',
+ display: 'none',
+ initialValue: {
+ ignored: true,
+ },
+ },
+ {
+ name: 'onMouseLeave',
+ display: 'none',
+ initialValue: {
+ ignored: true,
+ },
+ },
+ ],
+ },
+ ],
+};
diff --git a/packages/vision-polyfill/tests/fixtures/prototype/div-vision.ts b/packages/vision-polyfill/tests/fixtures/prototype/div-vision.ts
new file mode 100644
index 000000000..c4ae4374f
--- /dev/null
+++ b/packages/vision-polyfill/tests/fixtures/prototype/div-vision.ts
@@ -0,0 +1,175 @@
+export default {
+ title: '容器',
+ componentName: 'Div',
+ docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
+ category: '布局',
+ isContainer: true,
+ configure: [
+ {
+ name: 'behavior',
+ title: '默认状态',
+ display: 'inline',
+ initialValue: 'NORMAL',
+ supportVariable: true,
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ options: [
+ {
+ title: '普通',
+ value: 'NORMAL',
+ },
+ {
+ title: '隐藏',
+ value: 'HIDDEN',
+ },
+ ],
+ loose: false,
+ cancelable: false,
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: '__style__',
+ title: '样式设置',
+ display: 'accordion',
+ collapsed: false,
+ initialValue: {},
+ tip: {
+ url: 'https://lark.alipay.com/legao/help/design-tool-style',
+ content: '点击 ? 查看样式设置器用法指南',
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ advanced: true,
+ },
+ _owner: null,
+ },
+ },
+ {
+ type: 'group',
+ title: '高级',
+ display: 'accordion',
+ items: [
+ {
+ name: 'fieldId',
+ title: '唯一标识',
+ display: 'block',
+ tip:
+ '组件的唯一标识符,不能够与其它组件重名,不能够为空,且只能够使用以字母开头的,下划线以及数字的组合。',
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ placeholder: '请输入唯一标识',
+ multiline: false,
+ rows: 10,
+ required: false,
+ pattern: null,
+ maxLength: null,
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: 'useFieldIdAsDomId',
+ title: '将唯一标识用作 DOM ID',
+ display: 'block',
+ tip:
+ '开启这个配置项后,会在当前组件的 HTML 元素上加入 id="当前组件的 fieldId",一般用于做 utils 的绑定,不常用',
+ initialValue: false,
+ setter: {
+ key: null,
+ ref: null,
+ props: {},
+ _owner: null,
+ },
+ },
+ {
+ name: 'customClassName',
+ title: '自定义样式类',
+ display: 'block',
+ supportVariable: true,
+ initialValue: '',
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ placeholder: null,
+ multiline: false,
+ rows: 10,
+ required: false,
+ pattern: null,
+ maxLength: null,
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: 'events',
+ title: '动作设置',
+ tip: {
+ url: 'https://lark.alipay.com/legao/legao/events-call',
+ content: '点击 ? 查看如何设置组件的事件响应动作',
+ },
+ display: 'accordion',
+ initialValue: {
+ ignored: true,
+ },
+ setter: {
+ key: null,
+ ref: null,
+ props: {
+ events: [
+ {
+ name: 'onClick',
+ title: '当点击时',
+ initialValue:
+ "/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
+ },
+ {
+ name: 'onMouseEnter',
+ title: '当鼠标进入时',
+ initialValue:
+ "/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
+ },
+ {
+ name: 'onMouseLeave',
+ title: '当鼠标离开时',
+ initialValue:
+ "/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
+ },
+ ],
+ },
+ _owner: null,
+ },
+ },
+ {
+ name: 'onClick',
+ display: 'none',
+ initialValue: {
+ ignored: true,
+ },
+ },
+ {
+ name: 'onMouseEnter',
+ display: 'none',
+ initialValue: {
+ ignored: true,
+ },
+ },
+ {
+ name: 'onMouseLeave',
+ display: 'none',
+ initialValue: {
+ ignored: true,
+ },
+ },
+ ],
+ },
+ ],
+};
diff --git a/packages/vision-polyfill/tests/fixtures/schema/form.ts b/packages/vision-polyfill/tests/fixtures/schema/form.ts
new file mode 100644
index 000000000..5492a9ffb
--- /dev/null
+++ b/packages/vision-polyfill/tests/fixtures/schema/form.ts
@@ -0,0 +1,955 @@
+export default {
+ componentName: 'Page',
+ id: 'node_k1ow3cb9',
+ props: {
+ extensions: {
+ 启用页头: true,
+ },
+ pageStyle: {
+ backgroundColor: '#f2f3f5',
+ },
+ containerStyle: {},
+ className: 'page_kh05zf9c',
+ templateVersion: '1.0.0',
+ },
+ lifeCycles: {
+ constructor: {
+ type: 'js',
+ compiled:
+ "function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
+ source:
+ "function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
+ },
+ },
+ condition: true,
+ css:
+ 'body{background-color:#f2f3f5}.card_kh05zf9d {\n margin-bottom: 12px;\n}.card_kh05zf9e {\n margin-bottom: 12px;\n}.button_kh05zf9f {\n margin-right: 16px;\n width: 80px\n}.button_kh05zf9g {\n width: 80px;\n}.div_kh05zf9h {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
+ methods: {
+ __initMethods__: {
+ type: 'js',
+ source: 'function (exports, module) { /*set actions code here*/ }',
+ compiled: 'function (exports, module) { /*set actions code here*/ }',
+ },
+ },
+ dataSource: {
+ offline: [],
+ globalConfig: {
+ fit: {
+ compiled: '',
+ source: '',
+ type: 'js',
+ error: {},
+ },
+ },
+ online: [],
+ sync: true,
+ list: [],
+ },
+ children: [
+ {
+ componentName: 'RootHeader',
+ id: 'node_k1ow3cba',
+ props: {},
+ condition: true,
+ children: [
+ {
+ componentName: 'PageHeader',
+ id: 'node_k1ow3cbd',
+ props: {
+ extraContent: '',
+ __slot__extraContent: false,
+ __slot__action: false,
+ title: '',
+ content: '',
+ __slot__logo: false,
+ __slot__crumb: false,
+ crumb: '',
+ tab: '',
+ logo: '',
+ action: '',
+ __slot__tab: false,
+ __style__: {},
+ __slot__content: false,
+ fieldId: 'pageHeader_k1ow3h1i',
+ subTitle: '',
+ },
+ condition: true,
+ },
+ ],
+ },
+ {
+ componentName: 'RootContent',
+ id: 'node_k1ow3cbb',
+ props: {
+ contentBgColor: 'transparent',
+ contentPadding: '0',
+ contentMargin: '20',
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'Form',
+ id: 'form',
+ props: {
+ size: 'medium',
+ labelAlign: 'top',
+ autoValidate: true,
+ scrollToFirstError: true,
+ autoUnmount: true,
+ behavior: 'NORMAL',
+ dataSource: {
+ type: 'variable',
+ variable: 'state.formData',
+ },
+ __style__: {},
+ fieldId: 'form',
+ fieldOptions: {},
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'Card',
+ id: 'node_k1ow3cbj',
+ props: {
+ __slot__title: false,
+ subTitle: {
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ __slot__subTitle: false,
+ extra: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ className: 'card_kh05zf9d',
+ title: {
+ use: 'zh_CN',
+ en_US: 'Title',
+ zh_CN: '基本信息',
+ type: 'i18n',
+ },
+ __slot__extra: false,
+ showHeadDivider: true,
+ __style__: ':root {\n margin-bottom: 12px;\n}',
+ showTitleBullet: true,
+ contentHeight: '',
+ fieldId: 'card_k1ow3h1l',
+ dividerNoInset: false,
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'CardContent',
+ id: 'node_k1ow3cbk',
+ props: {},
+ condition: true,
+ children: [
+ {
+ componentName: 'ColumnsLayout',
+ id: 'node_k1ow3cbw',
+ props: {
+ layout: '4:8',
+ columnGap: '20',
+ rowGap: 0,
+ __style__: {},
+ fieldId: 'columns_k1ow3h1v',
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'Column',
+ id: 'node_k1ow3cbx',
+ props: {
+ colSpan: '',
+ __style__: {},
+ fieldId: 'column_k1p1bnjm',
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'TextField',
+ id: 'node_k1ow3cbz',
+ props: {
+ fieldName: 'name',
+ hasClear: false,
+ autoFocus: false,
+ tips: {
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ trim: false,
+ labelTextAlign: 'right',
+ placeholder: {
+ use: 'zh_CN',
+ en_US: 'please input',
+ zh_CN: '请输入',
+ type: 'i18n',
+ },
+ state: '',
+ behavior: 'NORMAL',
+ value: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ addonBefore: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ validation: [
+ {
+ type: 'required',
+ },
+ ],
+ hasLimitHint: false,
+ cutString: false,
+ __style__: {},
+ fieldId: 'textField_k1ow3h1w',
+ htmlType: 'input',
+ autoHeight: false,
+ labelColOffset: 0,
+ label: {
+ use: 'zh_CN',
+ en_US: 'TextField',
+ zh_CN: '姓名',
+ type: 'i18n',
+ },
+ __category__: 'form',
+ labelColSpan: 4,
+ wrapperColSpan: 0,
+ rows: 4,
+ addonAfter: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ wrapperColOffset: 0,
+ size: 'medium',
+ labelAlign: 'top',
+ __useMediator: 'value',
+ labelTipsTypes: 'none',
+ labelTipsIcon: '',
+ labelTipsText: {
+ type: 'i18n',
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ },
+ maxLength: 200,
+ },
+ condition: true,
+ },
+ {
+ componentName: 'TextField',
+ id: 'node_k1ow3cc1',
+ props: {
+ fieldName: 'englishName',
+ hasClear: false,
+ autoFocus: false,
+ tips: {
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ trim: false,
+ labelTextAlign: 'right',
+ placeholder: {
+ use: 'zh_CN',
+ en_US: 'please input',
+ zh_CN: '请输入',
+ type: 'i18n',
+ },
+ state: '',
+ behavior: 'NORMAL',
+ value: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ addonBefore: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ validation: [],
+ hasLimitHint: false,
+ cutString: false,
+ __style__: {},
+ fieldId: 'textField_k1ow3h1y',
+ htmlType: 'input',
+ autoHeight: false,
+ labelColOffset: 0,
+ label: {
+ use: 'zh_CN',
+ en_US: 'TextField',
+ zh_CN: '英文名',
+ type: 'i18n',
+ },
+ __category__: 'form',
+ labelColSpan: 4,
+ wrapperColSpan: 0,
+ rows: 4,
+ addonAfter: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ wrapperColOffset: 0,
+ size: 'medium',
+ labelAlign: 'top',
+ __useMediator: 'value',
+ labelTipsTypes: 'none',
+ labelTipsIcon: '',
+ labelTipsText: {
+ type: 'i18n',
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ },
+ maxLength: 200,
+ },
+ condition: true,
+ },
+ {
+ componentName: 'TextField',
+ id: 'node_k1ow3cc3',
+ props: {
+ fieldName: 'jobTitle',
+ hasClear: false,
+ autoFocus: false,
+ tips: {
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ trim: false,
+ labelTextAlign: 'right',
+ placeholder: {
+ use: 'zh_CN',
+ en_US: 'please input',
+ zh_CN: '请输入',
+ type: 'i18n',
+ },
+ state: '',
+ behavior: 'NORMAL',
+ value: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ addonBefore: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ validation: [],
+ hasLimitHint: false,
+ cutString: false,
+ __style__: {},
+ fieldId: 'textField_k1ow3h20',
+ htmlType: 'input',
+ autoHeight: false,
+ labelColOffset: 0,
+ label: {
+ use: 'zh_CN',
+ en_US: 'TextField',
+ zh_CN: '职位',
+ type: 'i18n',
+ },
+ __category__: 'form',
+ labelColSpan: 4,
+ wrapperColSpan: 0,
+ rows: 4,
+ addonAfter: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ wrapperColOffset: 0,
+ size: 'medium',
+ labelAlign: 'top',
+ __useMediator: 'value',
+ labelTipsTypes: 'none',
+ labelTipsIcon: '',
+ labelTipsText: {
+ type: 'i18n',
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ },
+ maxLength: 200,
+ },
+ condition: true,
+ },
+ ],
+ },
+ {
+ componentName: 'Column',
+ id: 'node_k1ow3cby',
+ props: {
+ colSpan: '',
+ __style__: {},
+ fieldId: 'column_k1p1bnjn',
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'TextField',
+ id: 'node_k1ow3cc2',
+ props: {
+ fieldName: 'nickName',
+ hasClear: false,
+ autoFocus: false,
+ tips: {
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ trim: false,
+ labelTextAlign: 'right',
+ placeholder: {
+ use: 'zh_CN',
+ en_US: 'please input',
+ zh_CN: '请输入',
+ type: 'i18n',
+ },
+ state: '',
+ behavior: 'NORMAL',
+ value: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ addonBefore: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ validation: [],
+ hasLimitHint: false,
+ cutString: false,
+ __style__: {},
+ fieldId: 'textField_k1ow3h1z',
+ htmlType: 'input',
+ autoHeight: false,
+ labelColOffset: 0,
+ label: {
+ use: 'zh_CN',
+ en_US: 'TextField',
+ zh_CN: '花名',
+ type: 'i18n',
+ },
+ __category__: 'form',
+ labelColSpan: 4,
+ wrapperColSpan: 0,
+ rows: 4,
+ addonAfter: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ wrapperColOffset: 0,
+ size: 'medium',
+ labelAlign: 'top',
+ __useMediator: 'value',
+ labelTipsTypes: 'none',
+ labelTipsIcon: '',
+ labelTipsText: {
+ type: 'i18n',
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ },
+ maxLength: 200,
+ },
+ condition: true,
+ },
+ {
+ componentName: 'SelectField',
+ id: 'node_k1ow3cc0',
+ props: {
+ fieldName: 'gender',
+ hasClear: false,
+ tips: {
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ mode: 'single',
+ showSearch: false,
+ autoWidth: true,
+ labelTextAlign: 'right',
+ placeholder: {
+ use: 'zh_CN',
+ en_US: 'please select',
+ zh_CN: '请选择',
+ type: 'i18n',
+ },
+ hasBorder: true,
+ behavior: 'NORMAL',
+ value: '',
+ validation: [
+ {
+ type: 'required',
+ },
+ ],
+ __style__: {},
+ fieldId: 'select_k1ow3h1x',
+ notFoundContent: {
+ use: 'zh_CN',
+ type: 'i18n',
+ },
+ labelColOffset: 0,
+ label: {
+ use: 'zh_CN',
+ en_US: 'SelectField',
+ zh_CN: '性别',
+ type: 'i18n',
+ },
+ __category__: 'form',
+ labelColSpan: 4,
+ wrapperColSpan: 0,
+ wrapperColOffset: 0,
+ hasSelectAll: false,
+ hasArrow: true,
+ size: 'medium',
+ labelAlign: 'top',
+ filterLocal: true,
+ dataSource: [
+ {
+ defaultChecked: false,
+ text: {
+ en_US: 'Option 1',
+ zh_CN: '男',
+ type: 'i18n',
+ __sid__: 'param_k1owc4tb',
+ },
+ __sid__: 'serial_k1owc4t1',
+ value: 'M',
+ sid: 'opt_k1owc4t2',
+ },
+ {
+ defaultChecked: false,
+ text: {
+ en_US: 'Option 2',
+ zh_CN: '女',
+ type: 'i18n',
+ __sid__: 'param_k1owc4tf',
+ },
+ __sid__: 'serial_k1owc4t2',
+ value: 'F',
+ sid: 'opt_k1owc4t3',
+ },
+ ],
+ __useMediator: 'value',
+ labelTipsTypes: 'none',
+ labelTipsIcon: '',
+ labelTipsText: {
+ type: 'i18n',
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ },
+ useDetailValue: false,
+ searchDelay: 300,
+ },
+ condition: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ componentName: 'Card',
+ id: 'node_k1ow3cbl',
+ props: {
+ __slot__title: false,
+ subTitle: {
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ __slot__subTitle: false,
+ extra: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ className: 'card_kh05zf9e',
+ title: {
+ use: 'zh_CN',
+ en_US: 'Title',
+ zh_CN: '部门信息',
+ type: 'i18n',
+ },
+ __slot__extra: false,
+ showHeadDivider: true,
+ __style__: ':root {\n margin-bottom: 12px;\n}',
+ showTitleBullet: true,
+ contentHeight: '',
+ fieldId: 'card_k1ow3h1m',
+ dividerNoInset: false,
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'CardContent',
+ id: 'node_k1ow3cbm',
+ props: {},
+ condition: true,
+ children: [
+ {
+ componentName: 'TextField',
+ id: 'node_k1ow3cc4',
+ props: {
+ fieldName: 'department',
+ hasClear: false,
+ autoFocus: false,
+ tips: {
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ trim: false,
+ labelTextAlign: 'right',
+ placeholder: {
+ use: 'zh_CN',
+ en_US: 'please input',
+ zh_CN: '请输入',
+ type: 'i18n',
+ },
+ state: '',
+ behavior: 'NORMAL',
+ value: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ addonBefore: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ validation: [],
+ hasLimitHint: false,
+ cutString: false,
+ __style__: {},
+ fieldId: 'textField_k1ow3h21',
+ htmlType: 'input',
+ autoHeight: false,
+ labelColOffset: 0,
+ label: {
+ use: 'zh_CN',
+ en_US: 'TextField',
+ zh_CN: '所属部门',
+ type: 'i18n',
+ },
+ __category__: 'form',
+ labelColSpan: 4,
+ wrapperColSpan: 0,
+ rows: 4,
+ addonAfter: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ wrapperColOffset: 0,
+ size: 'medium',
+ labelAlign: 'top',
+ __useMediator: 'value',
+ labelTipsTypes: 'none',
+ labelTipsIcon: '',
+ labelTipsText: {
+ type: 'i18n',
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ },
+ maxLength: 200,
+ },
+ condition: true,
+ },
+ {
+ componentName: 'ColumnsLayout',
+ id: 'node_k1ow3cc5',
+ props: {
+ layout: '6:6',
+ columnGap: '20',
+ rowGap: 0,
+ __style__: {},
+ fieldId: 'columns_k1ow3h22',
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'Column',
+ id: 'node_k1ow3cc6',
+ props: {
+ colSpan: '',
+ __style__: {},
+ fieldId: 'column_k1p1bnjo',
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'TextField',
+ id: 'node_k1ow3cc8',
+ props: {
+ fieldName: 'leader',
+ hasClear: false,
+ autoFocus: false,
+ tips: {
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ trim: false,
+ labelTextAlign: 'right',
+ placeholder: {
+ use: 'zh_CN',
+ en_US: 'please input',
+ zh_CN: '请输入',
+ type: 'i18n',
+ },
+ state: '',
+ behavior: 'NORMAL',
+ value: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ addonBefore: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ validation: [],
+ hasLimitHint: false,
+ cutString: false,
+ __style__: {},
+ fieldId: 'textField_k1ow3h23',
+ htmlType: 'input',
+ autoHeight: false,
+ labelColOffset: 0,
+ label: {
+ use: 'zh_CN',
+ en_US: 'TextField',
+ zh_CN: '主管',
+ type: 'i18n',
+ },
+ __category__: 'form',
+ labelColSpan: 4,
+ wrapperColSpan: 0,
+ rows: 4,
+ addonAfter: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ wrapperColOffset: 0,
+ size: 'medium',
+ labelAlign: 'top',
+ __useMediator: 'value',
+ labelTipsTypes: 'none',
+ labelTipsIcon: '',
+ labelTipsText: {
+ type: 'i18n',
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ },
+ maxLength: 200,
+ },
+ condition: true,
+ },
+ ],
+ },
+ {
+ componentName: 'Column',
+ id: 'node_k1ow3cc7',
+ props: {
+ colSpan: '',
+ __style__: {},
+ fieldId: 'column_k1p1bnjp',
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'TextField',
+ id: 'node_k1ow3cc9',
+ props: {
+ fieldName: 'hrg',
+ hasClear: false,
+ autoFocus: false,
+ tips: {
+ en_US: '',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ trim: false,
+ labelTextAlign: 'right',
+ placeholder: {
+ use: 'zh_CN',
+ en_US: 'please input',
+ zh_CN: '请输入',
+ type: 'i18n',
+ },
+ state: '',
+ behavior: 'NORMAL',
+ value: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ addonBefore: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ validation: [],
+ hasLimitHint: false,
+ cutString: false,
+ __style__: {},
+ fieldId: 'textField_k1ow3h24',
+ htmlType: 'input',
+ autoHeight: false,
+ labelColOffset: 0,
+ label: {
+ use: 'zh_CN',
+ en_US: 'TextField',
+ zh_CN: 'HRG',
+ type: 'i18n',
+ },
+ __category__: 'form',
+ labelColSpan: 4,
+ wrapperColSpan: 0,
+ rows: 4,
+ addonAfter: {
+ use: 'zh_CN',
+ zh_CN: '',
+ type: 'i18n',
+ },
+ wrapperColOffset: 0,
+ size: 'medium',
+ labelAlign: 'top',
+ __useMediator: 'value',
+ labelTipsTypes: 'none',
+ labelTipsIcon: '',
+ labelTipsText: {
+ type: 'i18n',
+ use: 'zh_CN',
+ en_US: '',
+ zh_CN: '',
+ },
+ maxLength: 200,
+ },
+ condition: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ componentName: 'Div',
+ id: 'node_k1ow3cbo',
+ props: {
+ className: 'div_kh05zf9h',
+ behavior: 'NORMAL',
+ __style__:
+ ':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
+ events: {},
+ fieldId: 'div_k1ow3h1o',
+ useFieldIdAsDomId: false,
+ customClassName: '',
+ },
+ condition: true,
+ children: [
+ {
+ componentName: 'Button',
+ id: 'node_k1ow3cbn',
+ props: {
+ triggerEventsWhenLoading: false,
+ onClick: {
+ rawType: 'events',
+ type: 'JSExpression',
+ value:
+ 'this.utils.legaoBuiltin.execEventFlow.bind(this, [this.submit])',
+ events: [
+ {
+ name: 'submit',
+ id: 'submit',
+ params: {},
+ type: 'actionRef',
+ uuid: '1570966253282_0',
+ },
+ ],
+ },
+ size: 'medium',
+ baseIcon: '',
+ otherIcon: '',
+ className: 'button_kh05zf9f',
+ type: 'primary',
+ behavior: 'NORMAL',
+ loading: false,
+ content: {
+ use: 'zh_CN',
+ en_US: 'Button',
+ zh_CN: '提交',
+ type: 'i18n',
+ },
+ __style__: ':root {\n margin-right: 16px;\n width: 80px\n}',
+ fieldId: 'button_k1ow3h1n',
+ },
+ condition: true,
+ },
+ {
+ componentName: 'Button',
+ id: 'node_k1ow3cbp',
+ props: {
+ triggerEventsWhenLoading: false,
+ size: 'medium',
+ baseIcon: '',
+ otherIcon: '',
+ className: 'button_kh05zf9g',
+ type: 'normal',
+ behavior: 'NORMAL',
+ loading: false,
+ content: {
+ use: 'zh_CN',
+ en_US: 'Button',
+ zh_CN: '取消',
+ type: 'i18n',
+ },
+ __style__: ':root {\n width: 80px;\n}',
+ fieldId: 'button_k1ow3h1p',
+ },
+ condition: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ componentName: 'RootFooter',
+ id: 'node_k1ow3cbc',
+ props: {},
+ condition: true,
+ },
+ ],
+};
diff --git a/packages/vision-polyfill/tests/fixtures/window.ts b/packages/vision-polyfill/tests/fixtures/window.ts
new file mode 100644
index 000000000..adba16c13
--- /dev/null
+++ b/packages/vision-polyfill/tests/fixtures/window.ts
@@ -0,0 +1,8 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+React.PropTypes = PropTypes;
+window.React = React;
+
+document.documentElement.requestFullscreen = () => {};
+document.exitFullscreen = () => {};
diff --git a/packages/vision-polyfill/tests/master/__snapshots__/deep-value-parser.test.ts.snap b/packages/vision-polyfill/tests/master/__snapshots__/deep-value-parser.test.ts.snap
new file mode 100644
index 000000000..0dd46763f
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/__snapshots__/deep-value-parser.test.ts.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`deepValueParser 测试 designMode: design 1`] = `
+Object {
+ "a": "111",
+ "arr": Array [
+ "111",
+ "111",
+ ],
+ "b": "222",
+ "c": "中文",
+ "slot": Object {
+ "type": "JSSlot",
+ "value": Array [
+ Object {
+ "componentName": "Div",
+ "props": Object {},
+ },
+ ],
+ },
+}
+`;
+
+exports[`deepValueParser 测试 designMode: live 1`] = `
+Object {
+ "a": Object {
+ "mock": "111",
+ "type": "JSExpression",
+ "value": "state.a",
+ },
+ "arr": Array [
+ Object {
+ "mock": "111",
+ "type": "JSExpression",
+ "value": "state.a",
+ },
+ Object {
+ "mock": "111",
+ "type": "JSExpression",
+ "value": "state.b",
+ },
+ ],
+ "b": Object {
+ "mock": "222",
+ "type": "JSExpression",
+ "value": "state.b",
+ },
+ "c": "中文",
+}
+`;
diff --git a/packages/vision-polyfill/tests/master/bus.test.ts b/packages/vision-polyfill/tests/master/bus.test.ts
new file mode 100644
index 000000000..c705a5b92
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/bus.test.ts
@@ -0,0 +1,240 @@
+import '../fixtures/window';
+import bus from '../../src/bus';
+import { editor } from '../../src/reducers';
+
+describe('bus 测试', () => {
+ afterEach(() => {
+ bus.unsub('evt1');
+ });
+ it('sub / pub 测试', () => {
+ const mockFn1 = jest.fn();
+ const off1 = bus.sub('evt1', mockFn1);
+
+ const evtData = { a: 1 };
+ bus.pub('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+
+ off1();
+
+ bus.pub('evt1', evtData);
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ });
+
+ it('on / emit 测试', () => {
+ const mockFn1 = jest.fn();
+ const off1 = bus.on('evt1', mockFn1);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+
+ off1();
+
+ bus.emit('evt1', evtData);
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ });
+
+ it('once / emit 测试', () => {
+ const mockFn1 = jest.fn();
+ bus.once('evt1', mockFn1);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+
+ bus.emit('evt1', evtData);
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ });
+
+ it('once / emit 测试,调用解绑函数', () => {
+ const mockFn1 = jest.fn();
+ const off1 = bus.once('evt1', mockFn1);
+
+ off1();
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).not.toHaveBeenCalled();
+ });
+
+ it('removeListener 测试', () => {
+ const mockFn1 = jest.fn();
+ bus.on('evt1', mockFn1);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+
+ bus.removeListener('evt1', mockFn1);
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ });
+
+ it('unsub 测试 - 只有一个 handler', () => {
+ const mockFn1 = jest.fn();
+ bus.on('evt1', mockFn1);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+
+ bus.unsub('evt1', mockFn1);
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ });
+
+ it('unsub 测试 - 只 unsub 一个 handler', () => {
+ const mockFn1 = jest.fn();
+ const mockFn2 = jest.fn();
+ bus.on('evt1', mockFn1);
+ bus.on('evt1', mockFn2);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ expect(mockFn2).toHaveBeenCalledTimes(1);
+ expect(mockFn2).toHaveBeenCalledWith(evtData);
+
+ bus.unsub('evt1', mockFn1);
+ const evtData2 = { a: 2 };
+ bus.emit('evt1', evtData2);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ expect(mockFn2).toHaveBeenCalledTimes(2);
+ expect(mockFn2).toHaveBeenLastCalledWith(evtData2);
+ });
+
+ it('unsub 测试 - 多个 handler', () => {
+ const mockFn1 = jest.fn();
+ const mockFn2 = jest.fn();
+ bus.on('evt1', mockFn1);
+ bus.on('evt1', mockFn2);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ expect(mockFn2).toHaveBeenCalledTimes(1);
+ expect(mockFn2).toHaveBeenCalledWith(evtData);
+
+ bus.unsub('evt1');
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ expect(mockFn2).toHaveBeenCalledTimes(1);
+ expect(mockFn2).toHaveBeenCalledWith(evtData);
+ });
+
+ it('off 测试 - 只有一个 handler', () => {
+ const mockFn1 = jest.fn();
+ bus.on('evt1', mockFn1);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+
+ bus.off('evt1', mockFn1);
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ });
+
+ it('off 测试 - 只 off 一个 handler', () => {
+ const mockFn1 = jest.fn();
+ const mockFn2 = jest.fn();
+ bus.on('evt1', mockFn1);
+ bus.on('evt1', mockFn2);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ expect(mockFn2).toHaveBeenCalledTimes(1);
+ expect(mockFn2).toHaveBeenCalledWith(evtData);
+
+ bus.off('evt1', mockFn1);
+ const evtData2 = { a: 2 };
+ bus.emit('evt1', evtData2);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ expect(mockFn2).toHaveBeenCalledTimes(2);
+ expect(mockFn2).toHaveBeenLastCalledWith(evtData2);
+ });
+
+ it('off 测试 - 多个 handler', () => {
+ const mockFn1 = jest.fn();
+ const mockFn2 = jest.fn();
+ bus.on('evt1', mockFn1);
+ bus.on('evt1', mockFn2);
+
+ const evtData = { a: 1 };
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ expect(mockFn2).toHaveBeenCalledTimes(1);
+ expect(mockFn2).toHaveBeenCalledWith(evtData);
+
+ bus.off('evt1');
+ bus.emit('evt1', evtData);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData);
+ expect(mockFn2).toHaveBeenCalledTimes(1);
+ expect(mockFn2).toHaveBeenCalledWith(evtData);
+ });
+
+ it('简单测试(dummy)', () => {
+ bus.getEmitter();
+ });
+
+ describe('editor 事件转发', () => {
+ const fwdEvtMap = {
+ 've.hotkey.callback.call': 'hotkey.callback.call',
+ 've.history.back': 'history.back',
+ 've.history.forward': 'history.forward',
+ 'node.prop.change': 'node.prop.change',
+ };
+
+ Object.keys(fwdEvtMap).forEach(veEventName => {
+ it(`${veEventName} 测试`, () => {
+ const mockFn1 = jest.fn();
+ const evtData1 = { a: 1 };
+ bus.on(veEventName, mockFn1);
+
+ editor.emit(fwdEvtMap[veEventName], evtData1);
+
+ expect(mockFn1).toHaveBeenCalledTimes(1);
+ expect(mockFn1).toHaveBeenCalledWith(evtData1);
+ });
+ });
+ });
+});
diff --git a/packages/vision-polyfill/tests/master/context.test.ts b/packages/vision-polyfill/tests/master/context.test.ts
new file mode 100644
index 000000000..362e4103d
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/context.test.ts
@@ -0,0 +1,62 @@
+import '../fixtures/window';
+// import { Project } from '../../src/project/project';
+// import { Node } from '../../src/document/node/node';
+// import { Designer } from '../../src/designer/designer';
+import { VisualEngineContext } from '../../src/context';
+import { autorun } from '@ali/lowcode-editor-core';
+
+describe('VisualEngineContext 测试', () => {
+ it('registerManager | getManager', () => {
+ const ctx = new VisualEngineContext();
+
+ ctx.registerManager({
+ mgr1: {},
+ });
+ ctx.registerManager('mgr2', {});
+ expect(ctx.getManager('mgr1')).toEqual({});
+ });
+
+ it('registerModule | getModule', () => {
+ const ctx = new VisualEngineContext();
+
+ ctx.registerModule({
+ mod1: {},
+ });
+ ctx.registerModule('mod2', {});
+ expect(ctx.getModule('mod1')).toEqual({});
+ });
+
+ it('use | getPlugin', () => {
+ const ctx = new VisualEngineContext();
+
+ ctx.use('plugin1', { plugin: 1 });
+ ctx.registerManager({
+ mgr1: { manager: 1 },
+ });
+ ctx.registerModule({
+ mod1: { mod: 1 },
+ });
+ expect(ctx.getPlugin('plugin1')).toEqual({ plugin: 1 });
+ expect(ctx.getPlugin('mgr1')).toEqual({ manager: 1 });
+ expect(ctx.getPlugin('mod1')).toEqual({ mod: 1 });
+ expect(ctx.getPlugin()).toBeUndefined;
+
+ ctx.use('ve.settingField.variableSetter', {});
+ });
+
+ it('registerTreePane | getModule', () => {
+ const ctx = new VisualEngineContext();
+
+ ctx.registerTreePane({ pane: 1 }, { core: 2 });
+ expect(ctx.getModule('TreePane')).toEqual({ pane: 1 });
+ expect(ctx.getModule('TreeCore')).toEqual({ core: 2 });
+ });
+
+ it('registerDynamicSetterProvider', () => {
+ const ctx = new VisualEngineContext();
+
+ ctx.registerDynamicSetterProvider({});
+ expect(ctx.getPlugin('ve.plugin.setterProvider')).toEqual({});
+ ctx.registerDynamicSetterProvider();
+ });
+});
diff --git a/packages/vision-polyfill/tests/master/deep-value-parser.test.ts b/packages/vision-polyfill/tests/master/deep-value-parser.test.ts
new file mode 100644
index 000000000..d2305d559
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/deep-value-parser.test.ts
@@ -0,0 +1,84 @@
+import '../fixtures/window';
+import { deepValueParser } from '../../src/props-reducers/deep-value-reducer';
+import { editor } from '../../src/reducers';
+
+describe('deepValueParser 测试', () => {
+ it('null & undefined', () => {
+ expect(deepValueParser()).toBeNull;
+ expect(deepValueParser()).toBeUndefined;
+ });
+
+ it('designMode: design', () => {
+ expect(deepValueParser({
+ a: {
+ type: 'variable',
+ variable: 'state.a',
+ value: '111',
+ },
+ b: {
+ type: 'JSExpression',
+ value: 'state.b',
+ mock: '222',
+ },
+ c: {
+ type: 'i18n',
+ use: 'zh_CN',
+ zh_CN: '中文',
+ en_US: 'eng',
+ },
+ slot: {
+ type: 'JSSlot',
+ value: [{
+ componentName: 'Div',
+ props: {},
+ }],
+ },
+ arr: [
+ {
+ type: 'variable',
+ variable: 'state.a',
+ value: '111',
+ },
+ {
+ type: 'variable',
+ variable: 'state.b',
+ value: '111',
+ },
+ ],
+ })).toMatchSnapshot();
+ });
+
+ it('designMode: live', () => {
+ editor.set('designMode', 'live');
+ expect(deepValueParser({
+ a: {
+ type: 'variable',
+ variable: 'state.a',
+ value: '111',
+ },
+ b: {
+ type: 'JSExpression',
+ value: 'state.b',
+ mock: '222',
+ },
+ c: {
+ type: 'i18n',
+ use: 'zh_CN',
+ zh_CN: '中文',
+ en_US: 'eng',
+ },
+ arr: [
+ {
+ type: 'variable',
+ variable: 'state.a',
+ value: '111',
+ },
+ {
+ type: 'variable',
+ variable: 'state.b',
+ value: '111',
+ },
+ ],
+ })).toMatchSnapshot();
+ });
+});
diff --git a/packages/vision-polyfill/tests/master/drag-engine.test.ts b/packages/vision-polyfill/tests/master/drag-engine.test.ts
new file mode 100644
index 000000000..335dad6da
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/drag-engine.test.ts
@@ -0,0 +1,182 @@
+import '../fixtures/window';
+// import { Project } from '../../src/project/project';
+// import { Node } from '../../src/document/node/node';
+// import { Editor } from '@ali/lowcode-editor-core';
+// import { Designer } from '@ali/lowcode-designer';
+import { designer } from '../../src/reducers';
+import DragEngine from '../../src/drag-engine';
+import formSchema from '../fixtures/schema/form';
+
+// const editor = new Editor();
+// const designer = new Designer({ editor });
+designer.project.open(formSchema);
+
+const mockBoostPrototype = jest.fn((e: MouseEvent) => {
+ return {
+ isPrototype: true,
+ getComponentName() {
+ return 'Div';
+ },
+ };
+});
+
+const mockBoostNode = jest.fn((e: MouseEvent) => {
+ return designer.currentDocument?.getNode('node_k1ow3cbo');
+});
+
+const mockBoostNodeData = jest.fn((e: MouseEvent) => {
+ return {
+ type: 'NodeData',
+ componentName: 'Div',
+ };
+});
+
+const mockBoostNull = jest.fn((e: MouseEvent) => {
+ return null;
+});
+
+const mockDragstart = jest.fn();
+const mockDrag = jest.fn();
+const mockDragend = jest.fn();
+
+describe('drag-engine 测试', () => {
+ it('prototype', async () => {
+ DragEngine.from(document, mockBoostPrototype);
+
+ DragEngine.onDragstart(mockDragstart);
+ DragEngine.onDrag(mockDrag);
+ DragEngine.onDragend(mockDragend);
+
+ const mousedownEvt = new MouseEvent('mousedown');
+ document.dispatchEvent(mousedownEvt);
+ designer.dragon.emitter.emit('dragstart', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ // await new Promise(resolve => resolve(setTimeout, 500));
+
+ expect(mockDragstart).toHaveBeenCalled();
+
+ designer.dragon.emitter.emit('drag', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ expect(mockDrag).toHaveBeenCalled();
+ expect(DragEngine.inDragging()).toBeTruthy;
+
+ designer.dragon.emitter.emit('dragend', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ expect(mockDragend).toHaveBeenCalled();
+ });
+
+ it('Node', async () => {
+ DragEngine.from(document, mockBoostNode);
+
+ DragEngine.onDragstart(mockDragstart);
+ DragEngine.onDrag(mockDrag);
+ DragEngine.onDragend(mockDragend);
+
+ const mousedownEvt = new MouseEvent('mousedown');
+ document.dispatchEvent(mousedownEvt);
+ designer.dragon.emitter.emit('dragstart', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ // await new Promise(resolve => resolve(setTimeout, 500));
+
+ expect(mockDragstart).toHaveBeenCalled();
+
+ designer.dragon.emitter.emit('drag', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ expect(mockDrag).toHaveBeenCalled();
+
+ designer.dragon.emitter.emit('dragend', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ expect(mockDragend).toHaveBeenCalled();
+ });
+
+ it('NodeData', async () => {
+ DragEngine.from(document, mockBoostNodeData);
+
+ DragEngine.onDragstart(mockDragstart);
+ DragEngine.onDrag(mockDrag);
+ DragEngine.onDragend(mockDragend);
+
+ const mousedownEvt = new MouseEvent('mousedown');
+ document.dispatchEvent(mousedownEvt);
+ designer.dragon.emitter.emit('dragstart', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ // await new Promise(resolve => resolve(setTimeout, 500));
+
+ expect(mockDragstart).toHaveBeenCalled();
+
+ designer.dragon.emitter.emit('drag', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ expect(mockDrag).toHaveBeenCalled();
+
+ designer.dragon.emitter.emit('dragend', {
+ dragObject: {
+ type: 'nodedata',
+ data: {
+ componentName: 'Div',
+ },
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ expect(mockDragend).toHaveBeenCalled();
+ });
+
+ it('null', async () => {
+ DragEngine.from(document, mockBoostNull);
+
+ DragEngine.onDragstart(mockDragstart);
+ DragEngine.onDrag(mockDrag);
+ DragEngine.onDragend(mockDragend);
+
+ const mousedownEvt = new MouseEvent('mousedown');
+ document.dispatchEvent(mousedownEvt);
+ designer.dragon.emitter.emit('dragstart', {
+ dragObject: {
+ nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
+ },
+ originalEvent: mousedownEvt,
+ });
+
+ expect(mockDragstart).toHaveBeenCalled();
+ });
+});
diff --git a/packages/vision-polyfill/tests/master/env.test.ts b/packages/vision-polyfill/tests/master/env.test.ts
new file mode 100644
index 000000000..6f280295c
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/env.test.ts
@@ -0,0 +1,111 @@
+import '../fixtures/window';
+// import { Project } from '../../src/project/project';
+// import { Node } from '../../src/document/node/node';
+// import { Designer } from '../../src/designer/designer';
+import env from '../../src/env';
+import { autorun } from '@ali/lowcode-editor-core';
+
+describe('env 测试', () => {
+ describe('常规 API 测试', () => {
+ it('setEnv / getEnv / setEnvMap / set / get', () => {
+ expect(env.getEnv('xxx')).toBeUndefined;
+
+ const mockFn1 = jest.fn();
+ const off1 = env.onEnvChange(mockFn1);
+
+ const envData = { a: 1 };
+ env.setEnv('xxx', envData);
+ expect(env.getEnv('xxx')).toEqual(envData);
+ expect(env.get('xxx')).toEqual(envData);
+ expect(mockFn1).toHaveBeenCalled();
+ expect(mockFn1).toHaveBeenCalledWith(env.envs, 'xxx', envData);
+ mockFn1.mockClear();
+
+ // 设置相同的值
+ env.setEnv('xxx', envData);
+ expect(env.getEnv('xxx')).toEqual(envData);
+ expect(env.get('xxx')).toEqual(envData);
+ expect(mockFn1).not.toHaveBeenCalled();
+ mockFn1.mockClear();
+
+ // 设置另一个 envName
+ const envData2 = { b: 1 };
+ env.set('yyy', envData2);
+ expect(env.getEnv('yyy')).toEqual(envData2);
+ expect(env.get('yyy')).toEqual(envData2);
+ expect(mockFn1).toHaveBeenCalled();
+ expect(mockFn1).toHaveBeenCalledWith(env.envs, 'yyy', envData2);
+ mockFn1.mockClear();
+
+ env.setEnvMap({
+ zzz: { a: 1, b: 1 },
+ });
+ expect(env.getEnv('xxx')).toBeUndefined;
+ expect(env.getEnv('yyy')).toBeUndefined;
+ expect(env.getEnv('zzz')).toEqual({ a: 1, b: 1 });
+ expect(mockFn1).toHaveBeenCalled();
+ expect(mockFn1).toHaveBeenCalledWith(env.envs);
+ mockFn1.mockClear();
+
+ // 解绑事件
+ off1();
+ env.setEnvMap({
+ zzz: { a: 1, b: 1 },
+ });
+ expect(mockFn1).not.toHaveBeenCalled();
+ mockFn1.mockClear();
+ });
+
+ it('setLocale / getLocale', () => {
+ expect(env.getLocale()).toBe('zh_CN');
+ env.setLocale('en_US');
+ expect(env.getLocale()).toBe('en_US');
+ });
+
+ it('setExpertMode / isExpertMode', () => {
+ expect(env.isExpertMode()).toBeFalsy;
+ env.setExpertMode('truthy value');
+ expect(env.isExpertMode()).toBeTruthy;
+ });
+
+ it('getSupportFeatures / setSupportFeatures / supports', () => {
+ expect(env.getSupportFeatures()).toEqual({});
+ env.setSupportFeatures({
+ mobile: true,
+ pc: true,
+ });
+ expect(env.getSupportFeatures()).toEqual({
+ mobile: true,
+ pc: true,
+ });
+ expect(env.supports('mobile')).toBeTruthy;
+ expect(env.supports('pc')).toBeTruthy;
+ expect(env.supports('iot')).toBeFalsy;
+ });
+
+ it('getAliSchemaVersion', () => {
+ expect(env.getAliSchemaVersion()).toBe('1.0.0');
+ });
+
+ it('envs obx 测试', async () => {
+ const mockFn = jest.fn();
+ env.clear();
+
+ autorun(() => {
+ mockFn(env.envs);
+ env.envs;
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 16));
+
+ expect(mockFn).toHaveBeenCalledTimes(1);
+ expect(mockFn).toHaveBeenLastCalledWith({});
+
+ env.setEnv('abc', { a: 1 });
+
+ await new Promise(resolve => setTimeout(resolve, 16));
+ expect(mockFn).toHaveBeenCalledTimes(2);
+ expect(mockFn).toHaveBeenLastCalledWith({ abc: { a: 1 } });
+ });
+ });
+});
diff --git a/packages/vision-polyfill/tests/master/flags.test.ts b/packages/vision-polyfill/tests/master/flags.test.ts
new file mode 100644
index 000000000..c5c3301e6
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/flags.test.ts
@@ -0,0 +1,71 @@
+import '../fixtures/window';
+import flagsCtrl from '../../src/flags';
+import domready from 'domready';
+
+jest.mock('domready', () => {
+ return (fn) => fn();
+});
+// domready.mockImplementation((fn) => fn());
+
+describe('flags 测试', () => {
+ it('flags', () => {
+ const mockFlagsChange = jest.fn();
+ flagsCtrl.flags = [];
+ const off = flagsCtrl.onFlagsChange(mockFlagsChange);
+ flagsCtrl.add('a');
+ expect(mockFlagsChange).toHaveBeenCalledTimes(1);
+ off();
+ flagsCtrl.add('b');
+ expect(mockFlagsChange).toHaveBeenCalledTimes(1);
+
+
+ expect(flagsCtrl.getFlags()).toEqual(['a', 'b']);
+
+ flagsCtrl.flags = [];
+ flagsCtrl.setDragMode(true);
+ expect(flagsCtrl.getFlags()).toEqual(['drag-mode']);
+ flagsCtrl.setDragMode(false);
+ expect(flagsCtrl.getFlags()).toEqual([]);
+
+ flagsCtrl.setPreviewMode(true);
+ expect(flagsCtrl.getFlags()).toEqual(['preview-mode']);
+ flagsCtrl.setPreviewMode(false);
+ expect(flagsCtrl.getFlags()).toEqual(['design-mode']);
+
+ flagsCtrl.flags = [];
+ flagsCtrl.setHideSlate(true);
+ expect(flagsCtrl.getFlags()).toEqual(['hide-slate']);
+ flagsCtrl.setHideSlate(false);
+ expect(flagsCtrl.getFlags()).toEqual([]);
+
+ flagsCtrl.flags = [];
+ flagsCtrl.setSlateFixedMode(true);
+ expect(flagsCtrl.getFlags()).toEqual(['slate-fixed']);
+ flagsCtrl.setHideSlate(true);
+ expect(flagsCtrl.getFlags()).toEqual(['slate-fixed']);
+ flagsCtrl.setSlateFixedMode(false);
+ expect(flagsCtrl.getFlags()).toEqual([]);
+
+ flagsCtrl.flags = [];
+ flagsCtrl.setSlateFullMode(true);
+ expect(flagsCtrl.getFlags()).toEqual(['slate-full-screen']);
+ flagsCtrl.setSlateFullMode(false);
+ expect(flagsCtrl.getFlags()).toEqual([]);
+
+ expect([].slice.apply(document.documentElement.classList)).toEqual(flagsCtrl.getFlags());
+
+ flagsCtrl.flags = [];
+ // setWithShell
+ flagsCtrl.setWithShell('shellA');
+ expect(flagsCtrl.getFlags()).toEqual(['with-iphone6shell']);
+ flagsCtrl.setWithShell('iPhone6');
+ expect(flagsCtrl.getFlags()).toEqual(['with-iphone6shell']);
+
+ flagsCtrl.flags = [];
+ // setSimulator
+ flagsCtrl.setSimulator('simA');
+ expect(flagsCtrl.getFlags()).toEqual(['simulator-simA']);
+ flagsCtrl.setSimulator('simB');
+ expect(flagsCtrl.getFlags()).toEqual(['simulator-simB']);
+ });
+});
diff --git a/packages/vision-polyfill/tests/master/panes.test.ts b/packages/vision-polyfill/tests/master/panes.test.ts
new file mode 100644
index 000000000..1a36eb271
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/panes.test.ts
@@ -0,0 +1,176 @@
+import '../fixtures/window';
+// import { Project } from '../../src/project/project';
+// import { Node } from '../../src/document/node/node';
+// import { Designer } from '../../src/designer/designer';
+import panes from '../../src/panes';
+import { autorun } from '@ali/lowcode-editor-core';
+
+describe('panes 测试', () => {
+ it('add: type dock | PanelDock', () => {
+ const mockDockShow = jest.fn();
+ const mockDockHide = jest.fn();
+ const { DockPane } = panes;
+ const offDockShow = DockPane.onDockShow(mockDockShow);
+ const offDockHide = DockPane.onDockHide(mockDockHide);
+
+ const pane1 = panes.add({
+ name: 'trunk',
+ type: 'dock',
+ width: 300,
+ description: '组件库',
+ contents: [
+ {
+ title: '普通组件',
+ tip: '普通组件',
+ content: () => 'haha',
+ },
+ ],
+ menu: '组件库',
+ defaultFixed: true,
+ });
+
+ const pane2 = panes.add({
+ name: 'trunk2',
+ type: 'dock',
+ width: 300,
+ description: '组件库',
+ contents: [
+ {
+ title: '普通组件',
+ tip: '普通组件',
+ content: () => 'haha',
+ },
+ ],
+ menu: '组件库',
+ defaultFixed: true,
+ });
+
+ const pane3 = panes.add({
+ name: 'trunk3',
+ type: 'dock',
+ isAction: true,
+ });
+
+ // DockPane.container.items.map(item => console.log(item.name))
+ // 2 trunks + 1 outline-pane
+ expect(DockPane.container.items.length).toBe(4);
+
+ DockPane.activeDock(pane1);
+ // expect(mockDockShow).toHaveBeenCalledTimes(1);
+ // expect(mockDockShow).toHaveBeenLastCalledWith(pane1);
+ expect(DockPane.container.items[1].visible).toBeTruthy;
+
+ DockPane.activeDock(pane2);
+ expect(DockPane.container.items[2].visible).toBeTruthy;
+ // expect(mockDockShow).toHaveBeenCalledTimes(2);
+ // expect(mockDockShow).toHaveBeenLastCalledWith(pane2);
+ // expect(mockDockHide).toHaveBeenCalledTimes(1);
+ // expect(mockDockHide).toHaveBeenLastCalledWith(pane1);
+
+ DockPane.activeDock();
+ DockPane.activeDock({ name: 'unexisting' });
+
+ offDockShow();
+ offDockHide();
+
+ // DockPane.activeDock(pane1);
+ // expect(mockDockShow).toHaveBeenCalledTimes(2);
+ // expect(mockDockHide).toHaveBeenCalledTimes(1);
+
+ expect(typeof DockPane.getDocks).toBe('function');
+ DockPane.getDocks();
+ expect(typeof DockPane.setFixed).toBe('function');
+ DockPane.setFixed();
+ });
+
+ it('add: type action', () => {
+ panes.add({
+ name: 'trunk',
+ type: 'action',
+ init() {},
+ destroy() {},
+ });
+
+ const { ActionPane } = panes;
+ expect(typeof ActionPane.getActions).toBe('function');
+ ActionPane.getActions();
+ expect(typeof ActionPane.setActions).toBe('function');
+ ActionPane.setActions();
+ expect(ActionPane.getActions()).toBe(ActionPane.actions);
+ });
+
+ it('add: type action - extraConfig', () => {
+ panes.add({
+ name: 'trunk',
+ type: 'action',
+ init() {},
+ destroy() {},
+ }, {});
+ });
+
+ it('add: type action - function', () => {
+ panes.add(() => ({
+ name: 'trunk',
+ type: 'action',
+ init() {},
+ destroy() {},
+ }));
+ });
+
+ it('add: type tab', () => {
+ panes.add({
+ name: 'trunk',
+ type: 'tab',
+ });
+ const { TabPane } = panes;
+ expect(typeof TabPane.setFloat).toBe('function');
+ TabPane.setFloat();
+ });
+
+ it('add: type stage', () => {
+ panes.add({
+ id: 'stage1',
+ type: 'stage',
+ });
+ panes.add({
+ type: 'stage',
+ });
+
+ const { Stages } = panes;
+ expect(typeof Stages.getStage).toBe('function');
+ Stages.getStage();
+ expect(typeof Stages.createStage).toBe('function');
+ Stages.createStage({
+ id: 'stage1',
+ type: 'stage',
+ });
+ Stages.createStage({
+ type: 'stage',
+ });
+ });
+
+ it('add: type stage - id', () => {
+ panes.add({
+ id: 'trunk',
+ name: 'trunk',
+ type: 'stage',
+ });
+ });
+
+ it('add: type widget', () => {
+ panes.add({
+ name: 'trunk',
+ type: 'widget',
+ });
+ });
+
+ it('add: type null', () => {
+ panes.add({
+ name: 'trunk',
+ });
+
+ const { toolbar } = panes;
+ expect(typeof toolbar.setContents).toBe('function');
+ toolbar.setContents();
+ });
+});
diff --git a/packages/vision-polyfill/tests/master/symbols.test.ts b/packages/vision-polyfill/tests/master/symbols.test.ts
new file mode 100644
index 000000000..35463ad8a
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/symbols.test.ts
@@ -0,0 +1,15 @@
+import set from 'lodash/set';
+import cloneDeep from 'lodash/clonedeep';
+import '../fixtures/window';
+// import { Project } from '../../src/project/project';
+// import { Node } from '../../src/document/node/node';
+// import { Designer } from '../../src/designer/designer';
+import symbols from '../../src/symbols';
+
+describe('symbols 测试', () => {
+ it('API', () => {
+ symbols.create('abc');
+ symbols.create('abc');
+ symbols.get('abc');
+ });
+});
diff --git a/packages/vision-polyfill/tests/master/viewport.test.ts b/packages/vision-polyfill/tests/master/viewport.test.ts
new file mode 100644
index 000000000..1fd82bc03
--- /dev/null
+++ b/packages/vision-polyfill/tests/master/viewport.test.ts
@@ -0,0 +1,189 @@
+import '../fixtures/window';
+import { Editor, globalContext } from '@ali/lowcode-editor-core';
+import { editor } from '../../src/reducers';
+import { Viewport } from '../../src/viewport';
+import domready from 'domready';
+
+// const editor = globalContext.get(Editor);
+
+jest.mock('domready', () => {
+ return (fn) => fn();
+});
+
+// 貌似 jsdom 没有响应 fullscreen 变更事件,先这么 mock 吧
+const mockSetFullscreen = flag => { document.fullscreen = flag; };
+
+describe('viewport 测试', () => {
+ mockSetFullscreen(true);
+
+ it('getDevice / setDevice / getViewport / onDeviceChange / onViewportChange', async () => {
+ const viewport = new Viewport();
+ const mockDeviceChange = jest.fn();
+ const mockViewportChange = jest.fn();
+ const offDevice = viewport.onDeviceChange(mockDeviceChange);
+ const offViewport = viewport.onViewportChange(mockViewportChange);
+ expect(viewport.getDevice()).toBe('pc');
+ expect(viewport.getViewport()).toBe('design-pc');
+ editor.set('currentDocument', { simulator: { set() {} } });
+
+ await viewport.setDevice('mobile');
+ expect(viewport.getDevice()).toBe('mobile');
+ expect(viewport.getViewport()).toBe('design-mobile');
+ expect(mockDeviceChange).toHaveBeenCalledTimes(1);
+ expect(mockViewportChange).toHaveBeenCalledTimes(1);
+
+ offDevice();
+ offViewport();
+ await viewport.setDevice('pc');
+ expect(mockDeviceChange).toHaveBeenCalledTimes(1);
+ expect(mockViewportChange).toHaveBeenCalledTimes(1);
+ });
+
+ it('setPreview / isPreview / togglePreivew / getViewport / onViewportChange', () => {
+ const viewport = new Viewport();
+ const mockViewportChange = jest.fn();
+ const mockPreivewChange = jest.fn();
+ const off = viewport.onViewportChange(mockViewportChange);
+ const offPreview = viewport.onPreview(mockPreivewChange);
+ viewport.setPreview(true);
+ expect(viewport.isPreview).toBeTruthy;
+ expect(viewport.getViewport()).toBe('preview-pc');
+ expect(mockViewportChange).toHaveBeenCalledTimes(1);
+ expect(mockPreivewChange).toHaveBeenCalledTimes(1);
+ viewport.setPreview(false);
+ expect(viewport.isPreview).toBeFalsy;
+ expect(viewport.getViewport()).toBe('design-pc');
+ expect(mockViewportChange).toHaveBeenCalledTimes(2);
+ expect(mockPreivewChange).toHaveBeenCalledTimes(2);
+ viewport.togglePreview();
+ expect(viewport.getViewport()).toBe('preview-pc');
+ expect(mockViewportChange).toHaveBeenCalledTimes(3);
+ expect(mockPreivewChange).toHaveBeenCalledTimes(3);
+ viewport.togglePreview();
+ expect(viewport.getViewport()).toBe('design-pc');
+ expect(mockViewportChange).toHaveBeenCalledTimes(4);
+ expect(mockPreivewChange).toHaveBeenCalledTimes(4);
+
+ off();
+ offPreview();
+ viewport.togglePreview();
+ expect(mockViewportChange).toHaveBeenCalledTimes(4);
+ expect(mockPreivewChange).toHaveBeenCalledTimes(4);
+ });
+
+ it('setFocusTarget / returnFocus / setFocus / isFocus / onFocusChange', () => {
+ const viewport = new Viewport();
+ const mockFocusChange = jest.fn();
+ const off = viewport.onFocusChange(mockFocusChange);
+ viewport.setFocusTarget(document.createElement('div'));
+ viewport.returnFocus();
+
+ viewport.setFocus(true);
+ expect(viewport.isFocus()).toBeTruthy();
+ expect(mockFocusChange).toHaveBeenCalledTimes(1);
+ expect(mockFocusChange).toHaveBeenLastCalledWith(true);
+ viewport.setFocus(false);
+ expect(viewport.isFocus()).toBeFalsy();
+ expect(mockFocusChange).toHaveBeenCalledTimes(2);
+ expect(mockFocusChange).toHaveBeenLastCalledWith(false);
+
+ off();
+ viewport.setFocus(false);
+ expect(mockFocusChange).toHaveBeenCalledTimes(2);
+ });
+
+ it('isFullscreen / toggleFullscreen / setFullscreen / onFullscreenChange', () => {
+ const viewport = new Viewport();
+ const mockFullscreenChange = jest.fn();
+ const off = viewport.onFullscreenChange(mockFullscreenChange);
+
+ mockSetFullscreen(false);
+ viewport.setFullscreen(true);
+ mockSetFullscreen(true);
+ expect(viewport.isFullscreen()).toBeTruthy;
+ // expect(mockFullscreenChange).toHaveBeenCalledTimes(1);
+ viewport.setFullscreen(true);
+ // expect(mockFullscreenChange).toHaveBeenCalledTimes(1);
+
+ mockSetFullscreen(true);
+ viewport.setFullscreen(false);
+ mockSetFullscreen(false);
+ expect(viewport.isFullscreen()).toBeFalsy;
+ // expect(mockFullscreenChange).toHaveBeenCalledTimes(2);
+ viewport.setFullscreen(false);
+ // expect(mockFullscreenChange).toHaveBeenCalledTimes(2);
+
+ mockSetFullscreen(true);
+ viewport.toggleFullscreen();
+ mockSetFullscreen(false);
+ // expect(mockFullscreenChange).toHaveBeenCalledTimes(3);
+ viewport.toggleFullscreen();
+ // expect(mockFullscreenChange).toHaveBeenCalledTimes(4);
+
+ off();
+ viewport.toggleFullscreen();
+ // expect(mockFullscreenChange).toHaveBeenCalledTimes(4);
+ });
+
+ it('setWithShell', () => {
+ const viewport = new Viewport();
+ viewport.setWithShell();
+ });
+
+ it('onSlateFixedChange', () => {
+ const viewport = new Viewport();
+ const mockSlateFixedChange = jest.fn();
+ const off = viewport.onSlateFixedChange(mockSlateFixedChange);
+
+ viewport.emitter.emit('slatefixed');
+ expect(mockSlateFixedChange).toHaveBeenCalledTimes(1);
+ off();
+ viewport.emitter.emit('slatefixed');
+ expect(mockSlateFixedChange).toHaveBeenCalledTimes(1);
+ });
+
+ it('setGlobalCSS', () => {
+ const viewport = new Viewport();
+ viewport.setGlobalCSS([{
+ media: '*',
+ type: 'URL',
+ content: '//path/to.css',
+ }, {
+ media: 'ALL',
+ type: 'text',
+ content: 'body {font-size: 50px;}',
+ }, {
+ media: '',
+ type: 'text',
+ content: 'body {font-size: 50px;}',
+ }, {
+ media: 'mobile',
+ type: 'text',
+ content: 'body {font-size: 50px;}',
+ }]);
+
+ viewport.cssResourceSet[0].apply();
+ viewport.cssResourceSet[0].init();
+ viewport.cssResourceSet[1].apply();
+ viewport.cssResourceSet[1].apply();
+ viewport.cssResourceSet[1].unmount();
+
+ viewport.setGlobalCSS([{
+ media: '*',
+ type: 'URL',
+ content: '//path/to.css',
+ }, {
+ media: 'ALL',
+ type: 'text',
+ content: 'body {font-size: 50px;}',
+ }, {
+ media: '',
+ type: 'text',
+ content: 'body {font-size: 50px;}',
+ }, {
+ media: 'mobile',
+ type: 'text',
+ content: 'body {font-size: 50px;}',
+ }]);
+ });
+});
diff --git a/packages/vision-polyfill/tests/props-reducers/downgrade-schema.test.ts b/packages/vision-polyfill/tests/props-reducers/downgrade-schema.test.ts
new file mode 100644
index 000000000..ac7f3f45e
--- /dev/null
+++ b/packages/vision-polyfill/tests/props-reducers/downgrade-schema.test.ts
@@ -0,0 +1,96 @@
+import '../fixtures/window';
+import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
+import { Editor } from '@ali/lowcode-editor-core';
+import {
+ compatibleReducer,
+} from '../../src/props-reducers/downgrade-schema-reducer';
+import formSchema from '../fixtures/schema/form';
+
+describe('compatibleReducer 测试', () => {
+ it('compatibleReducer 测试', () => {
+ const downgradedProps = {
+ a: {
+ type: 'JSBlock',
+ value: {
+ componentName: 'Slot',
+ props: {
+ slotTitle: '标题',
+ slotName: 'title',
+ },
+ children: [],
+ },
+ },
+ c: {
+ c1: {
+ type: 'JSBlock',
+ value: {
+ componentName: 'Slot',
+ props: {
+ slotTitle: '标题',
+ slotName: 'title',
+ },
+ },
+ },
+ },
+ d: {
+ type: 'variable',
+ variable: 'state.a',
+ value: '111',
+ },
+ e: {
+ e1: {
+ type: 'variable',
+ variable: 'state.b',
+ value: '222',
+ },
+ e2: {
+ type: 'JSExpression',
+ value: 'state.b',
+ mock: '222',
+ events: {},
+ },
+ },
+ };
+
+ expect(compatibleReducer({
+ a: {
+ type: 'JSSlot',
+ title: '标题',
+ name: 'title',
+ value: [],
+ },
+ c: {
+ c1: {
+ type: 'JSSlot',
+ title: '标题',
+ name: 'title',
+ value: undefined,
+ },
+ },
+ d: {
+ type: 'JSExpression',
+ value: 'state.a',
+ mock: '111',
+ },
+ e: {
+ e1: {
+ type: 'JSExpression',
+ value: 'state.b',
+ mock: '222',
+ },
+ e2: {
+ type: 'JSExpression',
+ value: 'state.b',
+ mock: '222',
+ events: {},
+ },
+ },
+ })).toEqual(downgradedProps);
+ });
+
+ it('空值', () => {
+ expect(compatibleReducer(null)).toBeNull;
+ expect(compatibleReducer(undefined)).toBeUndefined;
+ expect(compatibleReducer(111)).toBe(111);
+ });
+});
diff --git a/packages/vision-polyfill/tests/props-reducers/filter.test.ts b/packages/vision-polyfill/tests/props-reducers/filter.test.ts
new file mode 100644
index 000000000..691cfc086
--- /dev/null
+++ b/packages/vision-polyfill/tests/props-reducers/filter.test.ts
@@ -0,0 +1,81 @@
+import '../fixtures/window';
+import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
+import { Editor } from '@ali/lowcode-editor-core';
+import { filterReducer } from '../../src/props-reducers/filter-reducer';
+import formSchema from '../fixtures/schema/form';
+
+describe('filterReducer 测试', () => {
+ it('filterReducer 测试 - 有 filters', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {
+ filters: [
+ {
+ name: 'shouldBeFitlered',
+ filter: () => false,
+ },
+ {
+ name: 'keeped',
+ filter: () => true,
+ },
+ {
+ name: 'throwErr',
+ filter: () => { throw new Error('xxx'); },
+ },
+ {
+ name: 'zzz',
+ filter: () => true,
+ },
+ ],
+ },
+ };
+ },
+ },
+ settingEntry: {
+ getProp(propName) {
+ return { name: propName };
+ },
+ },
+ };
+ expect(filterReducer({
+ shouldBeFitlered: 111,
+ keeped: 222,
+ noCorresponingFilter: 222,
+ throwErr: 111,
+ }, mockNode)).toEqual({
+ keeped: 222,
+ noCorresponingFilter: 222,
+ throwErr: 111,
+ });
+ });
+
+ it('filterReducer 测试 - 无 filters', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {
+ filters: [],
+ },
+ };
+ },
+ },
+ settingEntry: {
+ getProp(propName) {
+ return { name: propName };
+ },
+ },
+ };
+ expect(filterReducer({
+ shouldBeFitlered: 111,
+ keeped: 222,
+ noCorresponingFilter: 222,
+ }, mockNode)).toEqual({
+ shouldBeFitlered: 111,
+ keeped: 222,
+ noCorresponingFilter: 222,
+ });
+ });
+});
diff --git a/packages/vision-polyfill/tests/props-reducers/init-node.test.ts b/packages/vision-polyfill/tests/props-reducers/init-node.test.ts
new file mode 100644
index 000000000..874576a70
--- /dev/null
+++ b/packages/vision-polyfill/tests/props-reducers/init-node.test.ts
@@ -0,0 +1,488 @@
+import '../fixtures/window';
+import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
+import { Editor, globalContext } from '@ali/lowcode-editor-core';
+import { initNodeReducer } from '../../src/props-reducers/init-node-reducer';
+import formSchema from '../fixtures/schema/form';
+
+describe('initNodeReducer 测试', () => {
+ it('initNodeReducer 测试 - 有 initials', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {
+ initials: [
+ {
+ name: 'propA',
+ initial: () => '111',
+ },
+ {
+ name: 'propB',
+ initial: () => '111',
+ },
+ {
+ name: 'propC',
+ initial: () => {
+ throw new Error('111');
+ },
+ },
+ {
+ name: 'propD',
+ initial: () => '111',
+ },
+ {
+ name: 'propE',
+ initial: () => '111',
+ },
+ {
+ name: 'propF',
+ initial: () => '111',
+ },
+ ],
+ },
+ };
+ },
+ prototype: {
+ options: {
+ configure: [
+ {
+ name: 'propF',
+ setter: {
+ type: {
+ displayName: 'I18nSetter',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ settingEntry: {
+ getProp(propName) {
+ return { name: propName };
+ },
+ },
+ props: {
+ has() {
+ return false;
+ },
+ add() {},
+ },
+ };
+ expect(
+ initNodeReducer(
+ {
+ propA: '111',
+ propC: '222',
+ propD: {
+ type: 'JSExpression',
+ mock: '111',
+ },
+ propE: {
+ type: 'variable',
+ value: '111',
+ },
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propA: '111',
+ propB: '111',
+ propC: '222',
+ propD: {
+ type: 'JSExpression',
+ mock: '111',
+ },
+ propE: {
+ type: 'variable',
+ value: '111',
+ },
+ propF: {
+ type: 'i18n',
+ use: 'zh_CN',
+ zh_CN: '111',
+ },
+ });
+ });
+
+ it('filterReducer 测试 - 无 initials', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {},
+ };
+ },
+ },
+ settingEntry: {
+ getProp(propName) {
+ return { name: propName };
+ },
+ },
+ };
+ expect(
+ initNodeReducer(
+ {
+ propA: 111,
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propA: 111,
+ });
+ });
+
+ describe('i18n', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {
+ initials: [
+ {
+ name: 'propF',
+ initial: () => 111,
+ },
+ ],
+ },
+ };
+ },
+ },
+ prototype: {
+ options: {
+ configure: [
+ {
+ name: 'propF',
+ setter: {
+ type: {
+ displayName: 'I18nSetter',
+ },
+ },
+ },
+ ],
+ },
+ },
+ props: {
+ has() {
+ return false;
+ },
+ add() {},
+ },
+ };
+
+ it('isI18NObject(ov): true', () => {
+ expect(
+ initNodeReducer(
+ {
+ propF: {
+ type: 'i18n',
+ zh_CN: '222',
+ },
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propF: {
+ type: 'i18n',
+ zh_CN: '222',
+ },
+ });
+ });
+
+ it('isJSExpression(ov): true', () => {
+ expect(
+ initNodeReducer(
+ {
+ propF: {
+ type: 'JSExpression',
+ value: 'state.a',
+ },
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propF: {
+ type: 'JSExpression',
+ value: 'state.a',
+ },
+ });
+ });
+
+ it('isJSBlock(ov): true', () => {
+ expect(
+ initNodeReducer(
+ {
+ propF: {
+ type: 'JSBlock',
+ value: 'state.a',
+ },
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propF: {
+ type: 'JSBlock',
+ value: 'state.a',
+ },
+ });
+ });
+
+ it('isJSSlot(ov): true', () => {
+ expect(
+ initNodeReducer(
+ {
+ propF: {
+ type: 'JSSlot',
+ value: 'state.a',
+ },
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propF: {
+ type: 'JSSlot',
+ value: 'state.a',
+ },
+ });
+ });
+
+ it('isVariable(ov): true', () => {
+ expect(
+ initNodeReducer(
+ {
+ propF: {
+ type: 'variable',
+ value: 'state.a',
+ },
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propF: {
+ type: 'variable',
+ value: 'state.a',
+ },
+ });
+ });
+
+ it('isI18NObject(v): false', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {
+ initials: [
+ {
+ name: 'propF',
+ initial: () => 111,
+ },
+ ],
+ },
+ };
+ },
+ prototype: {
+ options: {
+ configure: [
+ {
+ name: 'propF',
+ setter: {
+ type: {
+ displayName: 'I18nSetter',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ props: {
+ has() {
+ return false;
+ },
+ add() {},
+ },
+ };
+ expect(
+ initNodeReducer(
+ {
+ propF: {
+ type: 'variable',
+ value: 'state.a',
+ },
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propF: {
+ type: 'variable',
+ value: 'state.a',
+ },
+ });
+ });
+
+ it('isI18NObject(v): false', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {
+ initials: [{
+ name: 'propF',
+ initial: () => 111,
+ }],
+ },
+ };
+ },
+ },
+ prototype: {
+ options: {
+ configure: [
+ {
+ name: 'propF',
+ setter: {
+ type: {
+ displayName: 'I18nSetter',
+ },
+ },
+ },
+ ],
+ },
+ },
+ props: {
+ has() {
+ return false;
+ },
+ add() {},
+ },
+ };
+ expect(
+ initNodeReducer(
+ {
+ propF: {
+ type: 'variable',
+ value: 'state.a',
+ },
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propF: {
+ type: 'variable',
+ value: 'state.a',
+ },
+ });
+ });
+ });
+
+ it('成功使用兼容后的 i18n 对象', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {
+ initials: [{
+ name: 'propF',
+ initial: () => {
+ return {
+ type: 'i18n',
+ use: 'zh_CN',
+ zh_CN: '111',
+ };
+ },
+ }],
+ },
+ };
+ },
+ prototype: {
+ options: {
+ configure: [
+ {
+ name: 'propF',
+ setter: {
+ type: {
+ displayName: 'I18nSetter',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ props: {
+ has() {
+ return false;
+ },
+ add() {},
+ },
+ };
+ expect(
+ initNodeReducer(
+ {
+ propF: '111',
+ },
+ mockNode,
+ ),
+ ).toEqual({
+ propF: {
+ type: 'i18n',
+ use: 'zh_CN',
+ zh_CN: '111',
+ },
+ });
+ });
+
+ describe('fieldId', () => {
+ const mockNode = {
+ componentMeta: {
+ getMetadata() {
+ return {
+ experimental: {
+ initials: [
+ {
+ name: 'propA',
+ initial: () => '111',
+ },
+ ],
+ },
+ };
+ },
+ },
+ settingEntry: {
+ getProp(propName) {
+ return { name: propName };
+ },
+ },
+ props: {
+ has() {
+ return false;
+ },
+ add() {},
+ },
+ };
+ const editor = new Editor();
+ globalContext.register(editor, Editor);
+ const designer = new Designer({ editor });
+ editor.set('designer', designer);
+ designer.project.open(formSchema);
+ it('fieldId - 已存在', () => {
+ expect(initNodeReducer({
+ propA: '111',
+ fieldId: 'form',
+ }, mockNode)).toEqual({
+ propA: '111',
+ fieldId: undefined,
+ });
+ });
+
+ it('fieldId - 已存在,但有全局关闭标识', () => {
+ window.__disable_unique_id_checker__ = true;
+ expect(initNodeReducer({
+ propA: '111',
+ fieldId: 'form',
+ }, mockNode)).toEqual({
+ propA: '111',
+ fieldId: 'form',
+ });
+ });
+ });
+});
diff --git a/packages/vision-polyfill/tests/props-reducers/live-lifecycle.test.ts b/packages/vision-polyfill/tests/props-reducers/live-lifecycle.test.ts
new file mode 100644
index 000000000..cd396da5e
--- /dev/null
+++ b/packages/vision-polyfill/tests/props-reducers/live-lifecycle.test.ts
@@ -0,0 +1,78 @@
+import '../fixtures/window';
+import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
+import { Editor, globalContext } from '@ali/lowcode-editor-core';
+import { liveLifecycleReducer } from '../../src/props-reducers/live-lifecycle-reducer';
+import formSchema from '../fixtures/schema/form';
+
+const editor = new Editor();
+globalContext.register(editor, Editor);
+
+it('liveLifecycleReducer 测试 - live', () => {
+ const mockDidMount = jest.fn();
+ const mockWillUnmount = jest.fn();
+ editor.set('designMode', 'live');
+ const newProps = liveLifecycleReducer(
+ {
+ lifeCycles: {
+ didMount: mockDidMount,
+ willUnmount: mockWillUnmount,
+ },
+ },
+ {
+ isRoot() {
+ return true;
+ },
+ },
+ );
+
+ const { lifeCycles } = newProps;
+ expect(typeof lifeCycles.componentDidMount).toBe('function');
+ expect(typeof lifeCycles.componentWillUnMount).toBe('function');
+
+ lifeCycles.didMount();
+ lifeCycles.willUnmount();
+
+ expect(mockDidMount).toHaveBeenCalled();
+ expect(mockWillUnmount).toHaveBeenCalled();
+});
+
+it('liveLifecycleReducer 测试 - design', () => {
+ const mockDidMount = jest.fn();
+ const mockWillUnmount = jest.fn();
+ editor.set('designMode', 'design');
+ const newProps = liveLifecycleReducer(
+ {
+ lifeCycles: {
+ didMount: mockDidMount,
+ willUnmount: mockWillUnmount,
+ },
+ },
+ {
+ isRoot() {
+ return true;
+ },
+ },
+ );
+
+ const { lifeCycles } = newProps;
+ expect(lifeCycles).toEqual({});
+});
+
+it('liveLifecycleReducer 测试', () => {
+ const mockDidMount = jest.fn();
+ const mockWillUnmount = jest.fn();
+ editor.set('designMode', 'design');
+ const newProps = liveLifecycleReducer(
+ {
+ propA: '111',
+ },
+ {
+ isRoot() {
+ return true;
+ },
+ },
+ );
+
+ const { lifeCycles } = newProps;
+ expect(lifeCycles).toBeUndefined;
+});
diff --git a/packages/vision-polyfill/tests/props-reducers/node-top-fixed.test.ts b/packages/vision-polyfill/tests/props-reducers/node-top-fixed.test.ts
new file mode 100644
index 000000000..b1becca60
--- /dev/null
+++ b/packages/vision-polyfill/tests/props-reducers/node-top-fixed.test.ts
@@ -0,0 +1,28 @@
+import '../fixtures/window';
+import { nodeTopFixedReducer } from '../../src/props-reducers/node-top-fixed-reducer';
+import formSchema from '../fixtures/schema/form';
+
+it('nodeTopFixedReducer 测试', () => {
+ expect(
+ nodeTopFixedReducer(
+ {
+ propA: '111',
+ },
+ { componentMeta: { isTopFixed: true } },
+ ),
+ ).toEqual({
+ propA: '111',
+ __isTopFixed__: true,
+ });
+
+ expect(
+ nodeTopFixedReducer(
+ {
+ propA: '111',
+ },
+ { componentMeta: { } },
+ ),
+ ).toEqual({
+ propA: '111',
+ });
+});
diff --git a/packages/vision-polyfill/tests/props-reducers/remove-empty-prop.test.ts b/packages/vision-polyfill/tests/props-reducers/remove-empty-prop.test.ts
new file mode 100644
index 000000000..2e5fb8627
--- /dev/null
+++ b/packages/vision-polyfill/tests/props-reducers/remove-empty-prop.test.ts
@@ -0,0 +1,62 @@
+import '../fixtures/window';
+import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
+import { Editor, globalContext } from '@ali/lowcode-editor-core';
+import { removeEmptyPropsReducer } from '../../src/props-reducers/remove-empty-prop-reducer';
+import formSchema from '../fixtures/schema/form';
+
+it('removeEmptyPropsReducer 测试', () => {
+ const newProps = removeEmptyPropsReducer(
+ {
+ propA: '111',
+ dataSource: {
+ online: [
+ {
+ options: {
+ params: [
+ {
+ name: 'propA',
+ value: '111',
+ },
+ {
+ value: '111',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ isRoot() {
+ return true;
+ },
+ },
+ );
+
+ expect(newProps).toEqual({
+ propA: '111',
+ dataSource: {
+ online: [
+ {
+ options: {
+ params: [{
+ name: 'propA',
+ value: '111',
+ }, {
+ value: '111',
+ }],
+ },
+ },
+ ],
+ list: [
+ {
+ options: {
+ params: {
+ propA: '111',
+ },
+ },
+ },
+ ],
+ },
+ });
+});
diff --git a/packages/vision-polyfill/tests/props-reducers/style-props.test.ts b/packages/vision-polyfill/tests/props-reducers/style-props.test.ts
new file mode 100644
index 000000000..d79fbac75
--- /dev/null
+++ b/packages/vision-polyfill/tests/props-reducers/style-props.test.ts
@@ -0,0 +1,121 @@
+import '../fixtures/window';
+import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
+import { Editor, globalContext } from '@ali/lowcode-editor-core';
+import { stylePropsReducer } from '../../src/props-reducers/style-reducer';
+import formSchema from '../fixtures/schema/form';
+
+const editor: Editor = new Editor();
+globalContext.register(editor, Editor);
+
+beforeEach(() => {
+ // const designer = new Designer({ editor });
+ editor.set('designer', {
+ currentDocument: {
+ simulator: {
+ contentDocument: document,
+ },
+ },
+ });
+});
+// designer.project.open(formSchema);
+
+describe('stylePropsReducer 测试', () => {
+ it('无 style 相关属性', () => {
+ expect(stylePropsReducer({ propA: 1 })).toEqual({ propA: 1 });
+ });
+
+ it('__style__', () => {
+ const props = {
+ __style__: {
+ 'font-size': '50px',
+ },
+ };
+ const mockNode = { id: 'id1' };
+ expect(stylePropsReducer(props, mockNode)).toEqual({
+ className: '_css_pesudo_id1',
+ __style__: {
+ 'font-size': '50px',
+ },
+ });
+ expect(document.querySelector('#_style_pesudo_id1')).textContent =
+ '._css_pesudo_id1 { font-size: 50px; }';
+ });
+
+ it('__style__ - 无 contentDocument', () => {
+ editor.set('designer', {
+ currentDocument: {
+ simulator: {
+ contentDocument: undefined,
+ },
+ },
+ });
+ const props = {
+ __style__: {
+ 'font-size': '50px',
+ },
+ };
+ const mockNode = { id: 'id11' };
+ expect(stylePropsReducer(props, mockNode)).toEqual({
+ __style__: {
+ 'font-size': '50px',
+ },
+ });
+ expect(document.querySelector('#_style_pesudo_id11')).toBeNull;
+ });
+
+ it('__style__ - css id 已存在', () => {
+ const s = document.createElement('style');
+ s.setAttribute('type', 'text/css');
+ s.setAttribute('id', '_style_pesudo_id2');
+ document.getElementsByTagName('head')[0].appendChild(s);
+ s.appendChild(document.createTextNode('body {}'));
+ const props = {
+ __style__: {
+ 'font-size': '50px',
+ },
+ };
+ const mockNode = { id: 'id2' };
+ expect(stylePropsReducer(props, mockNode)).toEqual({
+ className: '_css_pesudo_id2',
+ __style__: {
+ 'font-size': '50px',
+ },
+ });
+ expect(document.querySelector('#_style_pesudo_id2')).textContent =
+ '._css_pesudo_id2 { font-size: 50px; }';
+ });
+
+ it('containerStyle', () => {
+ const props = {
+ containerStyle: {
+ 'font-size': '50px',
+ },
+ };
+ const mockNode = { id: 'id3' };
+ expect(stylePropsReducer(props, mockNode)).toEqual({
+ className: '_css_pesudo_id3',
+ containerStyle: {
+ 'font-size': '50px',
+ },
+ });
+ expect(document.querySelector('#_style_pesudo_id3')).textContent =
+ '._css_pesudo_id3 { font-size: 50px; }';
+ });
+
+ it('pageStyle', () => {
+ const props = {
+ pageStyle: {
+ 'font-size': '50rpx',
+ },
+ };
+ const mockNode = { id: 'id4' };
+ expect(stylePropsReducer(props, mockNode)).toEqual({
+ className: 'engine-document',
+ pageStyle: {
+ 'font-size': '50rpx',
+ },
+ });
+ expect(document.querySelector('#_style_pesudo_id4')).textContent =
+ '._css_pesudo_id4 { font-size: 50px; }';
+ });
+});
diff --git a/packages/vision-polyfill/tests/props-reducers/upgrade-schema.test copy.ts b/packages/vision-polyfill/tests/props-reducers/upgrade-schema.test copy.ts
new file mode 100644
index 000000000..b16c9b127
--- /dev/null
+++ b/packages/vision-polyfill/tests/props-reducers/upgrade-schema.test copy.ts
@@ -0,0 +1,107 @@
+import '../fixtures/window';
+import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
+import { Editor } from '@ali/lowcode-editor-core';
+import {
+ upgradePropsReducer,
+ upgradePageLifeCyclesReducer,
+} from '../../src/props-reducers/upgrade-reducer';
+import formSchema from '../fixtures/schema/form';
+
+describe('upgradePropsReducer 测试', () => {
+ it('upgradePropsReducer 测试', () => {
+ const props = {
+ a: {
+ type: 'JSBlock',
+ value: {
+ componentName: 'Slot',
+ props: {
+ slotTitle: '标题',
+ slotName: 'title',
+ },
+ children: [],
+ },
+ },
+ b: {
+ type: 'JSBlock',
+ value: {
+ componentName: 'Div',
+ props: {},
+ },
+ },
+ c: {
+ c1: {
+ type: 'JSBlock',
+ value: {
+ componentName: 'Slot',
+ props: {
+ slotTitle: '标题',
+ slotName: 'title',
+ },
+ },
+ },
+ },
+ d: {
+ type: 'variable',
+ variable: 'state.a',
+ value: '111',
+ },
+ __slot__haha: true,
+ };
+
+ expect(upgradePropsReducer(props)).toEqual({
+ a: {
+ type: 'JSSlot',
+ title: '标题',
+ name: 'title',
+ value: [],
+ },
+ b: {
+ componentName: 'Div',
+ props: {},
+ },
+ c: {
+ c1: {
+ type: 'JSSlot',
+ title: '标题',
+ name: 'title',
+ value: undefined,
+ },
+ },
+ d: {
+ type: 'JSExpression',
+ value: 'state.a',
+ mock: '111',
+ },
+ });
+ });
+
+ it('空值', () => {
+ expect(upgradePropsReducer(null)).toBeNull;
+ expect(upgradePropsReducer(undefined)).toBeUndefined;
+ });
+});
+
+const editor = new Editor();
+const designer = new Designer({ editor });
+designer.project.open(formSchema);
+
+it('upgradePageLifeCyclesReducer 测试', () => {
+ const rootNode = designer.currentDocument?.rootNode;
+ const mockDidMount = jest.fn();
+ const mockWillUnmount = jest.fn();
+ upgradePageLifeCyclesReducer({
+ didMount: mockDidMount,
+ willUnmount: mockWillUnmount,
+ }, rootNode);
+
+ const lifeCycles = rootNode?.getPropValue(getConvertedExtraKey('lifeCycles'));
+
+ expect(typeof lifeCycles.didMount).toBe('function');
+ expect(typeof lifeCycles.willUnmount).toBe('function');
+
+ lifeCycles.didMount();
+ lifeCycles.willUnmount();
+
+ expect(mockDidMount).toHaveBeenCalled();
+ expect(mockWillUnmount).toHaveBeenCalled();
+});
diff --git a/packages/vision-polyfill/tests/utils/index.ts b/packages/vision-polyfill/tests/utils/index.ts
new file mode 100644
index 000000000..70fce0af2
--- /dev/null
+++ b/packages/vision-polyfill/tests/utils/index.ts
@@ -0,0 +1 @@
+export { getIdsFromSchema, getNodeFromSchemaById } from '@ali/lowcode-test-mate/es/utils';
diff --git a/packages/vision-polyfill/tests/vision-api/api-export.test.ts b/packages/vision-polyfill/tests/vision-api/api-export.test.ts
new file mode 100644
index 000000000..699c2f0a5
--- /dev/null
+++ b/packages/vision-polyfill/tests/vision-api/api-export.test.ts
@@ -0,0 +1,87 @@
+import set from 'lodash/set';
+import cloneDeep from 'lodash/clonedeep';
+import '../fixtures/window';
+// import { Project } from '../../src/project/project';
+import formSchema from '../fixtures/schema/form';
+import VisualEngine, {
+ designer,
+ editor,
+ skeleton,
+ /**
+ * VE.Popup
+ */
+ Popup,
+ /**
+ * VE Utils
+ */
+ utils,
+ I18nUtil,
+ Hotkey,
+ Env,
+ monitor,
+ /* pub/sub 集线器 */
+ Bus,
+ /* 事件 */
+ EVENTS,
+ /* 修饰方法 */
+ HOOKS,
+ Exchange,
+ context,
+ /**
+ * VE.init
+ *
+ * Initialized the whole VisualEngine UI
+ */
+ init,
+ ui,
+ Panes,
+ modules,
+ Trunk,
+ Prototype,
+ Bundle,
+ Pages,
+ DragEngine,
+ Viewport,
+ Version,
+ Project,
+ logger,
+ Symbols,
+} from '../../src';
+import { Editor } from '@ali/lowcode-editor-core';
+
+describe('API 多种导出场景测试', () => {
+ it('window.VisualEngine 和 npm 导出 API 测试', () => {
+ expect(VisualEngine).toBe(window.VisualEngine);
+ });
+
+ it('npm 导出 API 对比测试', () => {
+ expect(VisualEngine.designer).toBe(designer);
+ expect(VisualEngine.editor).toBe(editor);
+ expect(VisualEngine.skeleton).toBe(skeleton);
+ expect(VisualEngine.Popup).toBe(Popup);
+ expect(VisualEngine.utils).toBe(utils);
+ expect(VisualEngine.I18nUtil).toBe(I18nUtil);
+ expect(VisualEngine.Hotkey).toBe(Hotkey);
+ expect(VisualEngine.Env).toBe(Env);
+ expect(VisualEngine.monitor).toBe(monitor);
+ expect(VisualEngine.Bus).toBe(Bus);
+ expect(VisualEngine.EVENTS).toBe(EVENTS);
+ expect(VisualEngine.HOOKS).toBe(HOOKS);
+ expect(VisualEngine.Exchange).toBe(Exchange);
+ expect(VisualEngine.context).toBe(context);
+ expect(VisualEngine.init).toBe(init);
+ expect(VisualEngine.ui).toBe(ui);
+ expect(VisualEngine.Panes).toBe(Panes);
+ expect(VisualEngine.modules).toBe(modules);
+ expect(VisualEngine.Trunk).toBe(Trunk);
+ expect(VisualEngine.Prototype).toBe(Prototype);
+ expect(VisualEngine.Bundle).toBe(Bundle);
+ expect(VisualEngine.DragEngine).toBe(DragEngine);
+ expect(VisualEngine.Pages).toBe(Pages);
+ expect(VisualEngine.Viewport).toBe(Viewport);
+ expect(VisualEngine.Version).toBe(Version);
+ expect(VisualEngine.Project).toBe(Project);
+ expect(VisualEngine.logger).toBe(logger);
+ expect(VisualEngine.Symbols).toBe(Symbols);
+ });
+});
diff --git a/packages/vision-polyfill/tests/vision-api/exchange.test.ts b/packages/vision-polyfill/tests/vision-api/exchange.test.ts
new file mode 100644
index 000000000..16c93b313
--- /dev/null
+++ b/packages/vision-polyfill/tests/vision-api/exchange.test.ts
@@ -0,0 +1,23 @@
+import set from 'lodash/set';
+import cloneDeep from 'lodash/clonedeep';
+import '../fixtures/window';
+import formSchema from '../fixtures/schema/form';
+import VisualEngine from '../../src';
+
+describe('VisualEngine.Exchange 相关 API 测试', () => {
+ it('select / getSelected', () => {
+ const doc = VisualEngine.Pages.addPage(formSchema);
+ VisualEngine.Exchange.select(doc?.getNode('form'));
+ expect(VisualEngine.Exchange.getSelected()?.componentName).toBe('Form');
+ expect(VisualEngine.Exchange.getSelected()?.id).toBe('form');
+
+ // clear selection
+ VisualEngine.Exchange.select();
+ expect(VisualEngine.Exchange.getSelected()).toBeUndefined;
+ });
+
+ it('onIntoView', () => {
+ expect(typeof VisualEngine.Exchange.onIntoView).toBe('function');
+ VisualEngine.Exchange.onIntoView();
+ });
+});
diff --git a/packages/vision-polyfill/tests/vision-api/pages.test.ts b/packages/vision-polyfill/tests/vision-api/pages.test.ts
new file mode 100644
index 000000000..87b04db0a
--- /dev/null
+++ b/packages/vision-polyfill/tests/vision-api/pages.test.ts
@@ -0,0 +1,170 @@
+import set from 'lodash/set';
+import cloneDeep from 'lodash/clonedeep';
+import '../fixtures/window';
+import formSchema from '../fixtures/schema/form';
+import VisualEngine, { Prototype } from '../../src';
+import { Editor } from '@ali/lowcode-editor-core';
+import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
+import divPrototypeConfig from '../fixtures/prototype/div-vision';
+
+const pageSchema = { componentsTree: [formSchema] };
+
+describe('VisualEngine.Pages 相关 API 测试', () => {
+ afterEach(() => {
+ VisualEngine.Pages.unload();
+ });
+ describe('addPage 系列', () => {
+ it('基本的节点模型初始化,初始化传入 schema', () => {
+ const doc = VisualEngine.Pages.addPage(pageSchema)!;
+ expect(doc).toBeTruthy();
+ const ids = getIdsFromSchema(formSchema);
+ const expectedNodeCnt = ids.length;
+ expect(doc.nodesMap.size).toBe(expectedNodeCnt);
+ });
+ it('基本的节点模型初始化,初始化传入 schema,带有 slot', () => {
+ const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title', {
+ type: 'JSBlock',
+ value: {
+ componentName: 'Slot',
+ children: [
+ {
+ componentName: 'Text',
+ id: 'node_k1ow3cbf',
+ props: {
+ showTitle: false,
+ behavior: 'NORMAL',
+ content: {
+ type: 'variable',
+ value: {
+ use: 'zh_CN',
+ en_US: 'Title',
+ zh_CN: '个人信息',
+ type: 'i18n',
+ },
+ variable: 'state.title',
+ },
+ __style__: {},
+ fieldId: 'text_k1ow3h1j',
+ maxLine: 0,
+ },
+ condition: true,
+ },
+ ],
+ props: {
+ slotTitle: '标题区域',
+ slotName: 'title',
+ },
+ },
+ });
+ const doc = VisualEngine.Pages.addPage({ componentsTree: [formSchemaWithSlot] })!;
+ expect(doc).toBeTruthy();
+ const ids = getIdsFromSchema(formSchema);
+ const expectedNodeCnt = ids.length;
+ // slot 会多出(1 + N)个节点
+ expect(doc.nodesMap.size).toBe(expectedNodeCnt + 2);
+ });
+ it('基本的节点模型初始化,初始化传入 schema,构造 prototype', () => {
+ const proto = new Prototype(divPrototypeConfig);
+ const doc = VisualEngine.Pages.addPage(pageSchema)!;
+ expect(doc).toBeTruthy();
+ const ids = getIdsFromSchema(formSchema);
+ const expectedNodeCnt = ids.length;
+ expect(doc.nodesMap.size).toBe(expectedNodeCnt);
+ });
+ it('导出 schema', () => {
+ const doc = VisualEngine.Pages.addPage(pageSchema)!;
+ expect(doc).toBeTruthy();
+ const ids = getIdsFromSchema(formSchema);
+ const expectedNodeCnt = ids.length;
+ const exportedData = doc.toData();
+ expect(exportedData).toHaveProperty('componentsMap');
+ expect(exportedData).toHaveProperty('componentsTree');
+ expect(exportedData.componentsTree).toHaveLength(1);
+ const exportedSchema = exportedData.componentsTree[0];
+ expect(getIdsFromSchema(exportedSchema).length).toBe(expectedNodeCnt);
+ });
+ });
+ describe('removePage 系列', () => {
+ it('removePage', () => {
+ const doc = VisualEngine.Pages.addPage(pageSchema)!;
+ expect(doc).toBeTruthy();
+ expect(VisualEngine.Pages.documents).toHaveLength(1);
+ VisualEngine.Pages.removePage(doc);
+ expect(VisualEngine.Pages.documents).toHaveLength(0);
+ });
+ });
+ describe('getPage 系列', () => {
+ it('getPage', () => {
+ const doc = VisualEngine.Pages.addPage(pageSchema);
+ const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page');
+ const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] });
+ expect(VisualEngine.Pages.getPage(0)).toBe(doc);
+ expect(VisualEngine.Pages.getPage((_doc) => _doc.rootNode.id === 'page')).toBe(doc2);
+ });
+ });
+ describe('setPages 系列', () => {
+ it('setPages componentsTree 只有一个元素', () => {
+ VisualEngine.Pages.setPages([pageSchema]);
+ const { currentDocument } = VisualEngine.Pages;
+ const ids = getIdsFromSchema(formSchema);
+ const expectedNodeCnt = ids.length;
+ const exportedData = currentDocument.toData();
+ expect(exportedData).toHaveProperty('componentsMap');
+ expect(exportedData).toHaveProperty('componentsTree');
+ expect(exportedData.componentsTree).toHaveLength(1);
+ const exportedSchema = exportedData.componentsTree[0];
+ expect(getIdsFromSchema(exportedSchema).length).toBe(expectedNodeCnt);
+ });
+ });
+ describe('setCurrentPage / getCurrentPage / currentPage / currentDocument 系列', () => {
+ it('getCurrentPage', () => {
+ const doc = VisualEngine.Pages.addPage(pageSchema)!;
+ expect(doc).toBeTruthy();
+ expect(doc).toBe(VisualEngine.Pages.getCurrentPage());
+ expect(doc).toBe(VisualEngine.Pages.currentDocument);
+ expect(doc).toBe(VisualEngine.Pages.currentPage);
+ });
+ it('setCurrentPage', () => {
+ const doc = VisualEngine.Pages.addPage(pageSchema);
+ expect(doc).toBe(VisualEngine.Pages.currentDocument);
+ const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page');
+ const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] });
+ expect(doc2).toBe(VisualEngine.Pages.currentDocument);
+ VisualEngine.Pages.setCurrentPage(doc);
+ expect(doc).toBe(VisualEngine.Pages.currentDocument);
+ });
+ });
+ describe('onCurrentPageChange 系列', () => {
+ it('多次切换', () => {
+ const doc = VisualEngine.Pages.addPage(pageSchema);
+ const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page');
+ const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] });
+ const docChangeHandler = jest.fn();
+ VisualEngine.Pages.onCurrentDocumentChange(docChangeHandler);
+ VisualEngine.Pages.setCurrentPage(doc);
+ expect(docChangeHandler).toHaveBeenCalledTimes(1);
+ expect(docChangeHandler).toHaveBeenLastCalledWith(doc);
+
+ VisualEngine.Pages.setCurrentPage(doc2);
+ expect(docChangeHandler).toHaveBeenCalledTimes(2);
+ expect(docChangeHandler).toHaveBeenLastCalledWith(doc2);
+ });
+ });
+ describe('toData 系列', () => {
+ it('基本的节点模型初始化,模型导出,初始化传入 schema', () => {
+ const doc = VisualEngine.Pages.addPage(pageSchema);
+ const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page');
+ const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] });
+ const dataList = VisualEngine.Pages.toData();
+ expect(dataList.length).toBe(2);
+ expect(dataList[0]).toHaveProperty('componentsMap');
+ expect(dataList[0]).toHaveProperty('componentsTree');
+ expect(dataList[0].componentsTree).toHaveLength(1);
+ expect(dataList[0].componentsTree[0].id).toBe('node_k1ow3cb9');
+ expect(dataList[1]).toHaveProperty('componentsMap');
+ expect(dataList[1]).toHaveProperty('componentsTree');
+ expect(dataList[1].componentsTree).toHaveLength(1);
+ expect(dataList[1].componentsTree[0].id).toBe('page');
+ });
+ });
+});
diff --git a/packages/vision-polyfill/tests/vision-api/project.test.ts b/packages/vision-polyfill/tests/vision-api/project.test.ts
new file mode 100644
index 000000000..fa89ec9e6
--- /dev/null
+++ b/packages/vision-polyfill/tests/vision-api/project.test.ts
@@ -0,0 +1,25 @@
+import set from 'lodash/set';
+import cloneDeep from 'lodash/clonedeep';
+import '../fixtures/window';
+import formSchema from '../fixtures/schema/form';
+import { Project } from '../../src';
+
+describe('VisualEngine.Project 相关 API 测试', () => {
+ it('getSchema / setSchema 系列', () => {
+ Project.setSchema({
+ componentsMap: {},
+ componentsTree: [formSchema],
+ });
+ expect(Project.getSchema()).toEqual({
+ componentsMap: {},
+ componentsTree: [formSchema],
+ });
+ });
+
+ it('setConfig', () => {
+ Project.setConfig({ haha: 1 });
+ expect(Project.get('config')).toEqual({
+ haha: 1,
+ });
+ });
+});
diff --git a/packages/vision-polyfill/tsconfig.json b/packages/vision-polyfill/tsconfig.json
new file mode 100644
index 000000000..c37b76ecc
--- /dev/null
+++ b/packages/vision-polyfill/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "lib"
+ },
+ "include": [
+ "./src/"
+ ]
+}
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index 9b9ad4b3b..b162dc2d1 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -35,5 +35,7 @@ mv ./packages/rax-simulator-renderer/dist/js/* $BUILD_DEST
mv ./packages/rax-simulator-renderer/dist/css/* $BUILD_DEST
mv ./packages/engine/dist/js/* $BUILD_DEST
mv ./packages/engine/dist/css/* $BUILD_DEST
+mv ./packages/vision-polyfill/dist/js/* $BUILD_DEST
+mv ./packages/vision-polyfill/dist/css/* $BUILD_DEST
echo "Complete"