Merge branch 'preset-vision/0.9.0' into fiebai

This commit is contained in:
飞百 2020-05-13 15:21:19 +08:00
commit 813f362758
1863 changed files with 543887 additions and 2605 deletions

3
.gitignore vendored
View File

@ -100,3 +100,6 @@ typings/
# mac config files
.DS_Store
# codealike
codealike.json

View File

@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
<a name="0.8.18"></a>
## [0.8.18](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-demo@0.8.17...@ali/lowcode-demo@0.8.18) (2020-05-13)
**Note:** Version bump only for package @ali/lowcode-demo
<a name="0.8.17"></a>
## [0.8.17](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-demo@0.8.16...@ali/lowcode-demo@0.8.17) (2020-05-13)
**Note:** Version bump only for package @ali/lowcode-demo
<a name="0.8.16"></a>
## [0.8.16](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-demo@0.8.15...@ali/lowcode-demo@0.8.16) (2020-05-08)
**Note:** Version bump only for package @ali/lowcode-demo
<a name="0.8.15"></a>
## [0.8.15](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-demo@0.8.14...@ali/lowcode-demo@0.8.15) (2020-05-08)
**Note:** Version bump only for package @ali/lowcode-demo
<a name="0.8.14"></a>
## [0.8.14](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-demo@0.8.13...@ali/lowcode-demo@0.8.14) (2020-05-07)
### Bug Fixes
* 🐛 add history pane for vision demo ([3ce7079](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3ce7079))
* 🐛 清理无用代码 ([015b58a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/015b58a))
<a name="0.8.13"></a>
## [0.8.13](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-demo@0.8.12...@ali/lowcode-demo@0.8.13) (2020-04-27)

View File

@ -1,6 +1,5 @@
{
"entry": {
"index": "src/vision/index.ts",
"vision-preset": "../vision-preset/src/index.ts",
"react-simulator-renderer": "../react-simulator-renderer/src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@ali/lowcode-demo",
"version": "0.8.13",
"version": "0.8.18",
"private": true,
"description": "低代码引擎 DEMO",
"scripts": {
@ -11,38 +11,38 @@
},
"config": {},
"dependencies": {
"@ali/lowcode-editor-core": "^0.8.9",
"@ali/lowcode-editor-skeleton": "^0.8.10",
"@ali/lowcode-plugin-components-pane": "^0.8.8",
"@ali/lowcode-plugin-designer": "^0.9.6",
"@ali/lowcode-plugin-event-bind-dialog": "^0.8.9",
"@ali/lowcode-plugin-outline-pane": "^0.8.12",
"@ali/lowcode-plugin-sample-logo": "^0.8.8",
"@ali/lowcode-plugin-sample-preview": "^0.8.11",
"@ali/lowcode-editor-core": "^0.8.12",
"@ali/lowcode-editor-skeleton": "^0.8.15",
"@ali/lowcode-plugin-components-pane": "^0.8.11",
"@ali/lowcode-plugin-designer": "^0.9.9",
"@ali/lowcode-plugin-event-bind-dialog": "^0.8.12",
"@ali/lowcode-plugin-outline-pane": "^0.8.15",
"@ali/lowcode-plugin-sample-logo": "^0.8.11",
"@ali/lowcode-plugin-sample-preview": "^0.8.14",
"@ali/lowcode-plugin-settings-pane": "^0.8.8",
"@ali/lowcode-plugin-undo-redo": "^0.8.9",
"@ali/lowcode-plugin-variable-bind-dialog": "^0.8.7",
"@ali/lowcode-plugin-zh-en": "^0.8.11",
"@ali/lowcode-react-renderer": "^0.8.5",
"@ali/lowcode-plugin-undo-redo": "^0.8.14",
"@ali/lowcode-plugin-variable-bind-dialog": "^0.8.10",
"@ali/lowcode-plugin-zh-en": "^0.8.14",
"@ali/lowcode-react-renderer": "^0.8.7",
"@ali/lowcode-runtime": "^0.8.13",
"@ali/lowcode-utils": "^0.8.2",
"@ali/lowcode-utils": "^0.8.4",
"@ali/ve-action-pane": "^4.7.0-beta.0",
"@ali/ve-datapool-pane": "^6.4.3",
"@ali/ve-history-pane": "4.0.0",
"@ali/ve-i18n-manage-pane": "^4.3.0",
"@ali/ve-i18n-pane": "^4.0.0-beta.0",
"@ali/ve-page-history": "1.2.0",
"@ali/ve-page-history-pane": "^5.0.0-beta.0",
"@ali/ve-trunk-pane": "^5.1.0-beta.14",
"@ali/vs-variable-setter": "^3.1.0",
"@ali/vu-function-parser": "^2.5.0-beta.0",
"@ali/vu-legao-design-fetch-context": "^1.0.3",
"@ali/ve-page-history": "1.2.0",
"@ali/ve-history-pane": "4.0.0",
"@ali/ve-page-history-pane": "^5.0.0-beta.0",
"@alifd/next": "^1.19.12",
"@alife/theme-lowcode-dark": "^0.1.0",
"@alife/theme-lowcode-light": "^0.1.0",
"compare-versions": "^3.0.1",
"react": "^16.8.1",
"react-dom": "^16.8.1",
"@ali/vu-function-parser": "^2.5.0-beta.0",
"compare-versions": "^3.0.1"
"react-dom": "^16.8.1"
},
"devDependencies": {
"@ali/iceluna-cli": "^0.0.16",

View File

@ -3,6 +3,64 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
<a name="0.9.9"></a>
## [0.9.9](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-designer@0.9.8...@ali/lowcode-designer@0.9.9) (2020-05-13)
### Bug Fixes
* 🐛 add hotkey up/down/left/right ([9c8afe8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9c8afe8))
* 🐛 error when quick search ([801d954](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/801d954))
* 🐛 快捷键支持 ([73374dd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/73374dd))
* 🐛 移动快捷键 ([7c8a27c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7c8a27c))
* cancel dragging on invalid position ([f961096](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f961096))
* panel visible time ([18ac1fa](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/18ac1fa))
* quickSearch error ([a8009ef](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a8009ef))
* supports ([371b84c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/371b84c))
### Features
* show value state ([bd49e50](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bd49e50))
* support global inline editing ([4f7179b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/4f7179b))
* support plaintext liveediting ([ea62f12](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ea62f12))
<a name="0.9.8"></a>
## [0.9.8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-designer@0.9.7...@ali/lowcode-designer@0.9.8) (2020-05-08)
**Note:** Version bump only for package @ali/lowcode-designer
<a name="0.9.7"></a>
## [0.9.7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-designer@0.9.6...@ali/lowcode-designer@0.9.7) (2020-05-07)
### Bug Fixes
* 🐛 add pollyfill for vision page.getHistory ([0b905d0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0b905d0))
* 🐛 title缺少icon字段临时转接一下 ([2f9bb25](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2f9bb25))
* 🐛 增加 getAddonData api ([68b7e29](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/68b7e29))
* 🐛 增加剪切快捷键 ([a73a82e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a73a82e))
* border action style ([6b91535](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6b91535))
* documentModel toData 方法 ([1ea0d73](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1ea0d73))
* settingfield添加props修复地区组件切换类型报错 ([88348f7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/88348f7))
* 在Transducer中添加对MixedSetter的支持 ([7317f2f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7317f2f))
### Features
* 🎸 增加icon获取api ([f1a0823](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f1a0823))
* duplicate ([ec932aa](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ec932aa))
* 修复状态切换失效 ([2e3f60d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2e3f60d))
<a name="0.9.6"></a>
## [0.9.6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-designer@0.9.5...@ali/lowcode-designer@0.9.6) (2020-04-27)

View File

@ -1,6 +1,6 @@
{
"name": "@ali/lowcode-designer",
"version": "0.9.6",
"version": "0.9.9",
"description": "Designer for Ali LowCode Engine",
"main": "lib/index.js",
"module": "es/index.js",
@ -15,9 +15,9 @@
},
"license": "MIT",
"dependencies": {
"@ali/lowcode-editor-core": "^0.8.9",
"@ali/lowcode-types": "^0.8.1",
"@ali/lowcode-utils": "^0.8.2",
"@ali/lowcode-editor-core": "^0.8.12",
"@ali/lowcode-types": "^0.8.3",
"@ali/lowcode-utils": "^0.8.4",
"classnames": "^2.2.6",
"event": "^1.0.0",
"react": "^16",

View File

@ -1,11 +1,10 @@
import { Component, Fragment, PureComponent } from 'react';
import classNames from 'classnames';
import { computed, observer, Title } from '@ali/lowcode-editor-core';
import { SimulatorContext } from '../context';
import { BuiltinSimulatorHost } from '../host';
import { TitleContent } from '@ali/lowcode-types';
export class BorderHoveringInstance extends PureComponent<{
export class BorderDetectingInstance extends PureComponent<{
title: TitleContent;
rect: DOMRect | null;
scale: number;
@ -24,7 +23,7 @@ export class BorderHoveringInstance extends PureComponent<{
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
};
const className = classNames('lc-borders lc-borders-hovering');
const className = classNames('lc-borders lc-borders-detecting');
// TODO:
// 1. thinkof icon
@ -39,7 +38,7 @@ export class BorderHoveringInstance extends PureComponent<{
}
@observer
export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> {
shouldComponentUpdate() {
return false;
}
@ -60,7 +59,7 @@ export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
const host = this.props.host;
const doc = host.document;
const selection = doc.selection;
const current = host.designer.hovering.current;
const current = host.designer.detecting.current;
if (!current || current.document !== doc || selection.has(current.id)) {
return null;
}
@ -70,36 +69,36 @@ export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> {
render() {
const host = this.props.host;
const current = this.current;
if (!current || host.viewport.scrolling) {
return <Fragment />;
if (!current || host.viewport.scrolling || host.liveEditing.editing) {
return null;
}
const instances = host.getComponentInstances(current);
if (!instances || instances.length < 1) {
return <Fragment />;
return null;
}
if (instances.length === 1) {
return (
<BorderHoveringInstance
<BorderDetectingInstance
key="line-h"
title={current.title}
scale={this.scale}
scrollX={this.scrollX}
scrollY={this.scrollY}
rect={host.computeComponentInstanceRect(instances[0], current.componentMeta.rectSelector)}
rect={host.computeComponentInstanceRect(instances[0], current.componentMeta.rootSelector)}
/>
);
}
return (
<Fragment>
{instances.map((inst, i) => (
<BorderHoveringInstance
<BorderDetectingInstance
key={`line-h-${i}`}
title={current.title}
scale={this.scale}
scrollX={this.scrollX}
scrollY={this.scrollY}
rect={host.computeComponentInstanceRect(inst, current.componentMeta.rectSelector)}
rect={host.computeComponentInstanceRect(inst, current.componentMeta.rootSelector)}
/>
))}
</Fragment>

View File

@ -12,7 +12,6 @@ import classNames from 'classnames';
import { observer, computed, Tip } from '@ali/lowcode-editor-core';
import { createIcon, isReactComponent } from '@ali/lowcode-utils';
import { ActionContentObject, isActionContentObject } from '@ali/lowcode-types';
import { SimulatorContext } from '../context';
import { BuiltinSimulatorHost } from '../host';
import { OffsetObserver } from '../../designer';
import { Node } from '../../document';
@ -188,7 +187,7 @@ export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
@computed get selecting() {
const doc = this.host.document;
if (doc.suspensed) {
if (doc.suspensed || this.host.liveEditing.editing) {
return null;
}
const selection = doc.selection;
@ -202,8 +201,7 @@ export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
render() {
const selecting = this.selecting;
if (!selecting || selecting.length < 1) {
// DIRTY FIX, recore has a bug!
return <Fragment />;
return null;
}
return (

View File

@ -49,28 +49,29 @@
}
}
&.lc-resize-box {
border-width: 0;
z-index: 1;
cursor: ew-resize;
pointer-events: auto;
align-items: center;
justify-content: center;
display: flex;
&.lc-resize-box {
border-width: 0;
z-index: 1;
cursor: ew-resize;
pointer-events: auto;
align-items: center;
justify-content: center;
display: flex;
&:after {
content: "";
display: block;
height: calc(100% - 20px);
min-height: 50%;
width: 4px;
background: #738397;
border-radius: 2px;
// animation: flashing 1.5s infinite linear;
}
&:after {
content: "";
display: block;
height: calc(100% - 20px);
min-height: 50%;
width: 4px;
background: #738397;
border-radius: 2px;
// animation: flashing 1.5s infinite linear;
}
}
&&-hovering {
// &&-hovering {
&&-detecting {
z-index: 1;
border-style: dashed;
background: rgba(0,121,242,.04);

View File

@ -1,6 +1,6 @@
import { Component } from 'react';
import { observer } from '@ali/lowcode-editor-core';
import { BorderHovering } from './border-hovering';
import { BorderDetecting } from './border-detecting';
import { BuiltinSimulatorHost } from '../host';
import { BorderSelecting } from './border-selecting';
import BorderResizing from './border-resizing';
@ -19,10 +19,9 @@ export class BemTools extends Component<{ host: BuiltinSimulatorHost }> {
const { scrollX, scrollY, scale } = host.viewport;
return (
<div className="lc-bem-tools" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
<BorderHovering key="hovering" />
<BorderSelecting key="selecting" />
<InsertionView key="insertion" />
<BorderResizing key="resizing" />
<BorderDetecting key="hovering" host={host} />
<BorderSelecting key="selecting" host={host} />
<InsertionView key="insertion" host={host} />
</div>
);
}

View File

@ -106,7 +106,7 @@ function processDetail({ target, detail, document }: DropLocation): InsertionDat
if (!instances) {
return {};
}
const edge = sim.computeComponentInstanceRect(instances[0], target.componentMeta.rectSelector);
const edge = sim.computeComponentInstanceRect(instances[0], target.componentMeta.rootSelector);
return edge ? { edge, insertType: 'cover', coverRect: edge } : {};
}
}

View File

@ -1,4 +1,4 @@
import { obx, autorun, computed, getPublicPath, hotkey } from '@ali/lowcode-editor-core';
import { obx, autorun, computed, getPublicPath, hotkey, focusTracker } from '@ali/lowcode-editor-core';
import { ISimulatorHost, Component, NodeInstance, ComponentInstance } from '../simulator';
import Viewport from './viewport';
import { createSimulator } from './create-simulator';
@ -25,6 +25,7 @@ import { parseMetadata } from './utils/parse-metadata';
import { ComponentMetadata } from '@ali/lowcode-types';
import { BuiltinSimulatorRenderer } from './renderer';
import clipboard from '../designer/clipboard';
import { LiveEditing } from './live-editing/live-editing';
export interface LibraryItem {
package: string;
@ -214,6 +215,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
// bind hotkey & clipboard
hotkey.mount(this._contentWindow);
focusTracker.mount(this._contentWindow);
clipboard.injectCopyPaster(this._contentDocument);
// TODO: dispose the bindings
}
@ -223,7 +225,8 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
// just listen special callback
// because iframe maybe reload
this.setupDragAndClick();
this.setupHovering();
this.setupDetecting();
this.setupLiveEditing();
}
setupDragAndClick() {
@ -237,6 +240,9 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
doc.addEventListener(
'mousedown',
(downEvent: MouseEvent) => {
if (this.liveEditing.editing) {
return;
}
// stop response document focus event
downEvent.stopPropagation();
downEvent.preventDefault();
@ -249,7 +255,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
doc.removeEventListener('mouseup', checkSelect, true);
if (!isShaken(downEvent, e)) {
const id = node.id;
designer.activeTracker.track(node);
designer.activeTracker.track({ node, instance: nodeInst?.instance });
if (isMulti && !isRootNode(node) && selection.has(id)) {
selection.remove(id);
} else {
@ -264,7 +270,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
if (isMulti) {
// multi select mode, directily add
if (!selection.has(node.id)) {
designer.activeTracker.track(node);
designer.activeTracker.track({ node, instance: nodeInst?.instance });
selection.add(node.id);
ignoreUpSelected = true;
}
@ -304,36 +310,24 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
},
true,
);
// cause edit
doc.addEventListener(
'dblclick',
(e: MouseEvent) => {
// stop response document dblclick event
e.stopPropagation();
e.preventDefault();
// todo: quick editing
},
true,
);
}
private disableHovering?: () => void;
/**
*
*/
setupHovering() {
setupDetecting() {
const doc = this.contentDocument!;
const hovering = this.document.designer.hovering;
const detecting = this.document.designer.detecting;
const hover = (e: MouseEvent) => {
if (!hovering.enable) {
if (!detecting.enable) {
return;
}
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
hovering.hover(nodeInst?.node || null);
detecting.capture(nodeInst?.node || null);
e.stopPropagation();
};
const leave = () => hovering.leave(this.document);
const leave = () => detecting.leave(this.document);
doc.addEventListener('mouseover', hover, true);
doc.addEventListener('mouseleave', leave, false);
@ -348,13 +342,47 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
);
this.disableHovering = () => {
hovering.leave(this.document);
detecting.leave(this.document);
doc.removeEventListener('mouseover', hover, true);
doc.removeEventListener('mouseleave', leave, false);
this.disableHovering = undefined;
};
}
readonly liveEditing = new LiveEditing();
setupLiveEditing() {
const doc = this.contentDocument!;
// cause edit
doc.addEventListener(
'dblclick',
(e: MouseEvent) => {
// stop response document dblclick event
e.stopPropagation();
e.preventDefault();
const targetElement = e.target as HTMLElement;
const nodeInst = this.getNodeInstanceFromElement(targetElement);
if (!nodeInst) {
return;
}
const node = nodeInst.node || this.document.rootNode;
const rootElement = this.findDOMNodes(nodeInst.instance, node.componentMeta.rootSelector)?.find(item => item.contains(targetElement)) as HTMLElement;
if (!rootElement) {
return;
}
this.liveEditing.apply({
node,
rootElement,
event: e,
});
},
true,
);
}
/**
* @see ISimulator
*/
@ -367,7 +395,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
} else {
// weekup some autorun reaction
if (!this.disableHovering) {
this.setupHovering();
this.setupDetecting();
}
}
}
@ -454,7 +482,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
if (!instances) {
return null;
}
return this.computeComponentInstanceRect(instances[0], node.componentMeta.rectSelector);
return this.computeComponentInstanceRect(instances[0], node.componentMeta.rootSelector);
}
/**
@ -462,19 +490,12 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
*/
computeComponentInstanceRect(instance: ComponentInstance, selector?: string): Rect | null {
const renderer = this.renderer!;
const elements = renderer.findDOMNodes(instance);
const elements = this.findDOMNodes(instance, selector);
if (!elements) {
return null;
}
let elems = elements.slice();
if (selector) {
const matched = getMatched(elems, selector);
if (!matched) {
return null;
}
elems = [matched];
}
const elems = elements.slice();
let rects: DOMRect[] | undefined;
let last: { x: number; y: number; r: number; b: number } | undefined;
let computed = false;
@ -533,8 +554,20 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
/**
* @see ISimulator
*/
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null {
return this._renderer?.findDOMNodes(instance) || null;
findDOMNodes(instance: ComponentInstance, selector?: string): Array<Element | Text> | null {
const elements = this._renderer?.findDOMNodes(instance);
if (!elements) {
return null;
}
if (selector) {
const matched = getMatched(elements, selector);
if (!matched) {
return null;
}
return [matched];
}
return elements;
}
/**
@ -716,7 +749,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
const { container, instance: containerInstance } = dropContainer;
const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rectSelector);
const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rootSelector);
if (!edge) {
return null;
@ -757,7 +790,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
? instances.find((inst) => this.getClosestNodeInstance(inst, container.id)?.instance === containerInstance)
: instances[0]
: null;
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rectSelector) : null;
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rootSelector) : null;
if (!rect) {
continue;

View File

@ -1,3 +1,4 @@
export * from './host';
export * from './host-view';
export * from './renderer';
export * from './live-editing/live-editing';

View File

@ -0,0 +1,197 @@
import { obx } from '@ali/lowcode-editor-core';
import { LiveTextEditingConfig } from '@ali/lowcode-types';
import { Node, Prop } from '../../document';
const EDITOR_KEY = 'data-setter-prop';
function getSetterPropElement(ele: HTMLElement, root: HTMLElement): HTMLElement | null {
const box = ele.closest(`[${EDITOR_KEY}]`);
if (!box || !root.contains(box)) {
return null;
}
return box as HTMLElement;
}
function defaultSaveContent(content: string, prop: Prop) {
prop.setValue(content);
}
export interface EditingTarget {
node: Node;
rootElement: HTMLElement;
event: MouseEvent;
}
const saveHandlers: SaveHandler[] = [];
function addLiveEditingSaveHandler(handler: SaveHandler) {
saveHandlers.push(handler);
}
const specificRules: SpecificRule[] = [];
function addLiveEditingSpecificRule(rule: SpecificRule) {
specificRules.push(rule);
}
export class LiveEditing {
static addLiveEditingSpecificRule = addLiveEditingSpecificRule;
static addLiveEditingSaveHandler = addLiveEditingSaveHandler;
@obx.ref private _editing: Prop | null = null;
apply(target: EditingTarget) {
const { node, event, rootElement } = target;
const targetElement = event.target as HTMLElement;
const liveTextEditing = node.componentMeta.liveTextEditing;
let setterPropElement = getSetterPropElement(targetElement, rootElement);
let propTarget = setterPropElement?.dataset.setterProp;
let matched: (LiveTextEditingConfig & { propElement?: HTMLElement; }) | undefined | null;
if (liveTextEditing) {
if (propTarget) {
// 已埋点命中 data-setter-prop="proptarget", 从 liveTextEditing 读取配置mode|onSaveContent
matched = liveTextEditing.find(config => config.propTarget == propTarget);
} else {
// 执行 embedTextEditing selector 规则,获得第一个节点 是否 contains e.target若匹配读取配置
matched = liveTextEditing.find(config => {
if (!config.selector) {
return false;
}
setterPropElement = queryPropElement(rootElement, targetElement, config.selector);
return setterPropElement ? true : false;
});
propTarget = matched?.propTarget;
}
} else {
specificRules.some((rule) => {
matched = rule(target);
return matched ? true : false;
});
if (matched) {
propTarget = matched.propTarget;
setterPropElement = matched.propElement || queryPropElement(rootElement, targetElement, matched.selector);
}
}
if (!propTarget) {
// 自动纯文本编辑满足一下情况:
// 1. children 内容都是 Leaf 且都是文本(一期)
// 2. DOM 节点是单层容器,子集都是文本节点 (已满足)
const isAllText = node.children?.every(item => {
return item.isLeaf() && item.getProp('children')?.type === 'literal';
});
// TODO:
}
if (propTarget && setterPropElement) {
const prop = node.getProp(propTarget, true)!;
if (this._editing === prop) {
return;
}
// 进入编辑
// 1. 设置contentEditable="plaintext|..."
// 2. 添加类名
// 3. focus & cursor locate
// 4. 监听 blur 事件
// 5. 设置编辑锁定disable hover | disable select | disable canvas drag
const onSaveContent = matched?.onSaveContent || saveHandlers.find(item => item.condition(prop))?.onSaveContent || defaultSaveContent;
setterPropElement.setAttribute('contenteditable', matched?.mode && matched.mode !== 'plaintext' ? 'true' : 'plaintext-only');
setterPropElement.classList.add('engine-live-editing');
// be sure
setterPropElement.focus();
setCaret(event);
this._save = () => {
onSaveContent(setterPropElement!.innerText, prop);
};
this._dispose = () => {
setterPropElement!.removeAttribute('contenteditable');
setterPropElement!.classList.remove('engine-live-editing');
};
setterPropElement.addEventListener('focusout', (e) => {
this.saveAndDispose();
});
this._editing = prop;
}
// TODO: process enter | esc events & joint the FocusTracker
// TODO: upward testing for b/i/a html elements
// 非文本编辑
// 国际化数据,改变当前
// JSExpression, 改变 mock 或 弹出绑定变量
}
get editing() {
return this._editing;
}
private _dispose?: () => void;
private _save?: () => void;
saveAndDispose() {
if (this._save) {
this._save();
this._save = undefined;
}
this.dispose();
}
dispose() {
if (this._dispose) {
this._dispose();
this._dispose = undefined;
}
this._editing = null;
}
}
export type SpecificRule = (target: EditingTarget) => (LiveTextEditingConfig & {
propElement?: HTMLElement;
}) | null;
export interface SaveHandler {
condition: (prop: Prop) => boolean;
onSaveContent: (content: string, prop: Prop) => void;
}
function setCaret(event: MouseEvent) {
const doc = event.view?.document!;
const range = doc.caretRangeFromPoint(event.clientX, event.clientY);
if (range) {
selectRange(doc, range);
setTimeout(() => selectRange(doc, range), 1);
}
}
function selectRange(doc: Document, range: Range) {
const selection = doc.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
}
function queryPropElement(rootElement: HTMLElement, targetElement: HTMLElement, selector?: string) {
if (!selector) {
return null;
}
let propElement = selector === ':root' ? rootElement : rootElement.querySelector(selector);
if (!propElement) {
return null;
}
if (!propElement.contains(targetElement)) {
// try selectorAll
propElement = Array.from(rootElement.querySelectorAll(selector)).find(item => item.contains(targetElement)) as HTMLElement;
if (!propElement) {
return null;
}
}
return propElement as HTMLElement;
}

View File

@ -9,6 +9,8 @@ import {
NestingFilter,
isTitleConfig,
I18nData,
LiveTextEditingConfig,
FieldConfig,
} from '@ali/lowcode-types';
import { computed } from '@ali/lowcode-editor-core';
import { Node, ParentalNode } from './document';
@ -81,9 +83,9 @@ export class ComponentMeta {
get descriptor(): string | undefined {
return this._descriptor;
}
private _rectSelector?: string;
get rectSelector(): string | undefined {
return this._rectSelector;
private _rootSelector?: string;
get rootSelector(): string | undefined {
return this._rootSelector;
}
private _transformedMetadata?: TransformedComponentMetadata;
get configure() {
@ -91,6 +93,11 @@ export class ComponentMeta {
return config?.combined || config?.props || [];
}
private _liveTextEditing?: LiveTextEditingConfig[];
get liveTextEditing() {
return this._liveTextEditing;
}
private parentWhitelist?: NestingFilter | null;
private childWhitelist?: NestingFilter | null;
@ -150,6 +157,26 @@ export class ComponentMeta {
: title;
}
const liveTextEditing = this._transformedMetadata.experimental?.liveTextEditing || [];
function collectLiveTextEditing(items: FieldConfig[]) {
items.forEach(config => {
if (config.items) {
collectLiveTextEditing(config.items);
} else {
const liveConfig = config.liveTextEditing || config.extraProps?.liveTextEditing;
if (liveConfig) {
liveTextEditing.push({
propTarget: String(config.name),
...liveConfig,
});
}
}
});
}
collectLiveTextEditing(this.configure);
this._liveTextEditing = liveTextEditing.length > 0 ? liveTextEditing : undefined;
const { configure = {} } = this._transformedMetadata;
this._acceptable = false;
@ -158,7 +185,7 @@ export class ComponentMeta {
this._isContainer = component.isContainer ? true : false;
this._isModal = component.isModal ? true : false;
this._descriptor = component.descriptor;
this._rectSelector = component.rectSelector;
this._rootSelector = component.rootSelector;
if (component.nestingRule) {
const { parentWhitelist, childWhitelist } = component.nestingRule;
this.parentWhitelist = buildFilter(parentWhitelist);

View File

@ -1,17 +1,38 @@
import { EventEmitter } from 'events';
import { LocationDetail } from './location';
import { Node, isNode } from '../document/node/node';
import { ComponentInstance } from '../simulator';
import { obx } from '@ali/lowcode-editor-core';
export interface ActiveTarget {
node: Node;
detail?: LocationDetail;
instance?: ComponentInstance;
}
export class ActiveTracker {
private emitter = new EventEmitter();
@obx.ref private _target?: ActiveTarget;
track(target: ActiveTarget | Node) {
this.emitter.emit('change', isNode(target) ? { node: target } : target);
if (isNode(target)) {
target = { node: target };
}
this._target = target;
this.emitter.emit('change', target);
}
get currentNode() {
return this._target?.node;
}
get detail() {
return this._target?.detail;
}
get intance() {
return this._target?.instance;
}
onChange(fn: (target: ActiveTarget) => void): () => void {

View File

@ -36,7 +36,6 @@ function getNextForSelect(next: any, head?: any, parent?: any): any {
function getPrevForSelect(prev: any, head?: any, parent?: any): any {
if (prev) {
debugger;
let ret;
if (!head && prev.isContainer()) {
const children = prev.getChildren() || [];
@ -67,6 +66,7 @@ function getPrevForSelect(prev: any, head?: any, parent?: any): any {
// hotkey binding
hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => {
// TODO: use focus-tracker
const doc = focusing.focusDesigner?.currentDocument;
if (isFormEvent(e) || !doc) {
return;
@ -102,14 +102,6 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
}
e.preventDefault();
/*
const doc = getCurrentDocument();
if (isFormEvent(e) || !doc || !(focusing.id === 'outline' || focusing.id === 'canvas')) {
return;
}
e.preventDefault();
*/
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) return;
@ -120,7 +112,7 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
clipboard.setData(data);
const cutMode = action.indexOf('x') > 0;
const cutMode = action && action.indexOf('x') > 0;
if (cutMode) {
selected.forEach((node) => {
const parentNode = node.getParent();
@ -213,7 +205,7 @@ hotkey.bind(['up', 'down'], (e, action) => {
}
});
hotkey.bind(['option+up', 'option+down', 'option+left', 'option+right'], (e, action) => {
hotkey.bind(['option+left', 'option+right'], (e, action) => {
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !doc) {
@ -225,24 +217,17 @@ hotkey.bind(['option+up', 'option+down', 'option+left', 'option+right'], (e, act
return;
}
// TODO: 此处需要增加判断当前节点是否可被操作移动原ve里是用 node.canOperating()来判断
// TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断
const firstNode = selected[0];
const parent = firstNode.getParent();
if (!parent) return;
const isPrev = /(up|left)$/.test(action);
const isTravel = /(up|down)$/.test(action);
const isPrev = action && /(left)$/.test(action);
const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling;
if (silbing) {
if (isTravel && silbing.isContainer()) {
const place = silbing.getSuitablePlace(firstNode, null);
if (isPrev) {
place.container.insertAfter(firstNode, place.ref);
} else {
place.container.insertBefore(firstNode, place.ref);
}
} else if (isPrev) {
if (isPrev) {
parent.insertBefore(firstNode, silbing);
} else {
parent.insertAfter(firstNode, silbing);
@ -250,14 +235,82 @@ hotkey.bind(['option+up', 'option+down', 'option+left', 'option+right'], (e, act
firstNode?.select();
return;
}
if (isTravel) {
});
hotkey.bind(['option+up'], (e, action) => {
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) {
return;
}
// TODO: 此处需要增加判断当前节点是否可被操作移动原ve里是用 node.canOperating()来判断
// TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断
const firstNode = selected[0];
const parent = firstNode.getParent();
if (!parent) {
return;
}
const silbing = firstNode.prevSibling;
if (silbing) {
if (silbing.isContainer()) {
const place = silbing.getSuitablePlace(firstNode, null);
place.container.insertAfter(firstNode, place.ref);
} else {
parent.insertBefore(firstNode, silbing);
}
firstNode?.select();
return;
} else {
const place = parent.getSuitablePlace(firstNode, null); // upwards
if (place) {
if (isPrev) {
place.container.insertBefore(firstNode, place.ref);
} else {
place.container.insertAfter(firstNode, place.ref);
}
place.container.insertBefore(firstNode, place.ref);
firstNode?.select();
}
}
});
hotkey.bind(['option+down'], (e, action) => {
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) {
return;
}
// TODO: 此处需要增加判断当前节点是否可被操作移动原ve里是用 node.canOperating()来判断
// TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断
const firstNode = selected[0];
const parent = firstNode.getParent();
if (!parent) {
return;
}
const silbing = firstNode.nextSibling;
if (silbing) {
if (silbing.isContainer()) {
// const place = silbing.getSuitablePlace(firstNode, null);
silbing.insertBefore(firstNode, undefined);
// place.container.insertBefore(firstNode, place.ref);
} else {
parent.insertAfter(firstNode, silbing);
}
firstNode?.select();
return;
} else {
const place = parent.getSuitablePlace(firstNode, null); // upwards
if (place) {
place.container.insertAfter(firstNode, place.ref);
firstNode?.select();
}
}

View File

@ -16,7 +16,7 @@ import { INodeSelector, Component } from '../simulator';
import { Scroller, IScrollable } from './scroller';
import { Dragon, isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './dragon';
import { ActiveTracker } from './active-tracker';
import { Hovering } from './hovering';
import { Detecting } from './detecting';
import { DropLocation, LocationData, isLocationChildrenDetail } from './location';
import { OffsetObserver, createOffsetObserver } from './offset-observer';
import { focusing } from './focusing';
@ -44,7 +44,7 @@ export interface DesignerProps {
export class Designer {
readonly dragon = new Dragon(this);
readonly activeTracker = new ActiveTracker();
readonly hovering = new Hovering();
readonly detecting = new Detecting();
readonly project: Project;
readonly editor: IEditor;
@ -68,7 +68,7 @@ export class Designer {
this.project = new Project(this, props.defaultSchema);
this.dragon.onDragstart((e) => {
this.hovering.enable = false;
this.detecting.enable = false;
const { dragObject } = e;
if (isDragNodeObject(dragObject)) {
if (dragObject.nodes.length === 1) {
@ -99,7 +99,7 @@ export class Designer {
const { dragObject, copy } = e;
const loc = this._dropLocation;
if (loc) {
if (isLocationChildrenDetail(loc.detail)) {
if (isLocationChildrenDetail(loc.detail) && loc.detail.valid !== false) {
let nodes: Node[] | undefined;
if (isDragNodeObject(dragObject)) {
nodes = insertChildren(loc.target, dragObject.nodes, loc.detail.index, copy);
@ -114,12 +114,11 @@ export class Designer {
}
}
}
this.clearLocation();
if (this.props?.onDragend) {
this.props.onDragend(e, loc);
}
this.postEvent('dragend', e, loc);
this.hovering.enable = true;
this.detecting.enable = true;
});
this.activeTracker.onChange(({ node, detail }) => {
@ -174,6 +173,10 @@ export class Designer {
}
private _dropLocation?: DropLocation;
get dropLocation() {
return this._dropLocation;
}
/**
* dragon
*/
@ -202,8 +205,30 @@ export class Designer {
return new Scroller(scrollable);
}
private oobxList: OffsetObserver[] = [];
createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null {
return createOffsetObserver(nodeInstance);
const oobx = createOffsetObserver(nodeInstance);
this.clearOobxList();
if (oobx) {
this.oobxList.push(oobx);
}
return oobx;
}
private clearOobxList(force?: boolean) {
let l = this.oobxList.length;
if (l > 20 || force) {
while (l-- > 0) {
if (this.oobxList[l].isPurged()) {
this.oobxList.splice(l, 1);
}
}
}
}
touchOffsetObserver() {
this.clearOobxList(true);
this.oobxList.forEach(item => item.compute());
}
createSettingEntry(editor: IEditor, nodes: Node[]) {

View File

@ -1,7 +1,7 @@
import { obx } from '@ali/lowcode-editor-core';
import { Node, DocumentModel } from '../document';
export class Hovering {
export class Detecting {
@obx.ref private _enable = true;
get enable() {
return this._enable;
@ -19,11 +19,11 @@ export class Hovering {
return this._current;
}
hover(node: Node | null) {
capture(node: Node | null) {
this._current = node;
}
unhover(node: Node) {
release(node: Node) {
if (this._current === node) {
this._current = null;
}

View File

@ -8,7 +8,7 @@
align-items: center;
pointer-events: none;
background-color: rgba(0, 0, 0, 0.4);
opacity: 0.5;
//opacity: 0.9;
box-shadow: 0 0 6px grey;
transform: translate(-10%, -50%);
.lc-ghost {

View File

@ -365,6 +365,7 @@ export class Dragon {
exception = ex;
}
}
designer.clearLocation();
handleEvents((doc) => {
if (isBoostFromDragAPI) {

View File

@ -1,7 +1,6 @@
import { Designer } from './designer';
// TODO:
// 当前激活区域管理
// TODO: use focus-tracker replace
class Focusing {
focusDesigner?: Designer;
}

View File

@ -2,7 +2,7 @@ import './builtin-hotkey';
export * from './designer';
export * from './designer-view';
export * from './dragon';
export * from './hovering';
export * from './detecting';
export * from './location';
export * from './offset-observer';
export * from './scroller';

View File

@ -82,6 +82,8 @@ export class OffsetObserver {
private isRoot: boolean;
readonly node: Node;
readonly compute: () => void;
constructor(readonly nodeInstance: INodeSelector) {
const { node, instance } = nodeInstance;
this.node = node;
@ -103,7 +105,7 @@ export class OffsetObserver {
return;
}
const rect = host.computeComponentInstanceRect(instance!, node.componentMeta.rectSelector);
const rect = host.computeComponentInstanceRect(instance!, node.componentMeta.rootSelector);
if (!rect) {
this.hasOffset = false;
@ -121,6 +123,8 @@ export class OffsetObserver {
this.pid = pid = (window as any).requestIdleCallback(compute);
};
this.compute = compute;
// try first
compute();
// try second, ensure the dom mounted
@ -133,6 +137,10 @@ export class OffsetObserver {
}
this.pid = undefined;
}
isPurged() {
return this.pid == null;
}
}
export function createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null {

View File

@ -3,6 +3,7 @@ import { Transducer } from './utils';
import { SettingPropEntry } from './setting-prop-entry';
import { SettingEntry } from './setting-entry';
import { computed, obx } from '@ali/lowcode-editor-core';
import { cloneDeep } from '@ali/lowcode-utils';
export class SettingField extends SettingPropEntry implements SettingEntry {
readonly isSettingField = true;
@ -83,46 +84,33 @@ export class SettingField extends SettingPropEntry implements SettingEntry {
return new SettingField(this, config);
}
// ====== 当前属性读写 =====
/**
*
* 0 /
* 1
* 2
*/
get valueState(): number {
if (this.type !== 'field') {
return 0;
}
const propName = this.path.join('.');
const first = this.nodes[0].getProp(propName)!;
let l = this.nodes.length;
let state = 2;
while (l-- > 1) {
const next = this.nodes[l].getProp(propName, false);
const s = first.compare(next);
if (s > 1) {
return 0;
}
if (s === 1) {
state = 1;
}
}
return state;
}
purge() {
this.disposeItems();
}
// ======= compatibles for vision ======
getHotValue(): any {
return this.transducer.toHot(this.getValue());
// avoid View modify
let v = cloneDeep(this.getMockOrValue());
if (v == null) {
v = this.extraProps.defaultValue;
}
return this.transducer.toHot(v);
}
setHotValue(data: any) {
this.setValue(this.transducer.toNative(data));
const v = this.transducer.toNative(data);
if (this.isUseVariable()) {
const ov = this.getValue();
this.setValue({
type: 'JSExpression',
value: ov.value,
mock: v,
});
} else {
this.setValue(v);
}
this.valueChange();
}
onEffect(action: () => void): () => void {

View File

@ -62,14 +62,6 @@ export class SettingPropEntry implements SettingEntry {
this.isSingle = parent.isSingle;
this.designer = parent.designer;
this.top = parent.top;
autorun(({ firstRun }) => {
const value = this.getValue();
if (firstRun) {
return;
}
this.emitter.emit('valuechange', value);
});
}
getId() {
@ -105,11 +97,43 @@ export class SettingPropEntry implements SettingEntry {
// ====== 当前属性读写 =====
/**
*
* -1
* 0
* 1
* 2
*/
@computed get valueState(): number {
if (this.type !== 'field') {
const { getValue } = this.extraProps;
return getValue ? (getValue(this, undefined) === undefined ? 0 : 1) : 0;
}
const propName = this.path.join('.');
const first = this.nodes[0].getProp(propName)!;
let l = this.nodes.length;
let state = 2;
while (l-- > 1) {
const next = this.nodes[l].getProp(propName, false);
const s = first.compare(next);
if (s > 1) {
return -1;
}
if (s === 1) {
state = 1;
}
}
if (state === 2 && first.isUnset()) {
return 0;
}
return state;
}
/**
*
*/
@computed getValue(): any {
let val: any = null;
let val: any = undefined;
if (this.type === 'field') {
val = this.parent.getPropValue(this.name);
}
@ -130,6 +154,19 @@ export class SettingPropEntry implements SettingEntry {
}
}
/**
*
*/
clearValue() {
if (this.type === 'field') {
this.parent.clearPropValue(this.name);
}
const { setValue } = this.extraProps;
if (setValue) {
setValue(this, undefined);
}
}
/**
*
*/
@ -146,6 +183,14 @@ export class SettingPropEntry implements SettingEntry {
this.top.setPropValue(path, value);
}
/**
*
*/
clearPropValue(propName: string | number) {
const path = this.path.concat(propName).join('.');
this.top.clearPropValue(path);
}
/**
*
*/
@ -180,11 +225,11 @@ export class SettingPropEntry implements SettingEntry {
return this.top;
}
// add settingfield props
// add settingfield props
get props() {
return this.top;
}
onValueChange(func: () => any) {
this.emitter.on('valuechange', func);
@ -197,8 +242,6 @@ export class SettingPropEntry implements SettingEntry {
* @deprecated
*/
valueChange() {
console.warn('valueChange deprecated');
this.emitter.emit('valuechange');
}

View File

@ -136,6 +136,15 @@ export class SettingTopEntry implements SettingEntry {
});
}
/**
*
*/
clearPropValue(propName: string) {
this.nodes.forEach((node) => {
node.clearPropValue(propName);
});
}
/**
*
*/

View File

@ -46,7 +46,7 @@ export class Transducer {
setter = setter.componentName;
}
if (typeof setter === 'string') {
setter = getSetter(setter);
setter = getSetter(setter).component;
}
this.setterTransducer = combineTransducer(

View File

@ -41,7 +41,6 @@ export class DocumentModel {
private seqId = 0;
private _simulator?: ISimulatorHost;
/**
*
*/
@ -119,7 +118,6 @@ export class DocumentModel {
// return this.addonsData[name];
// }
/**
* id
*/
@ -240,12 +238,12 @@ export class DocumentModel {
node.remove();
}
getAddonData(name: string) {
const addon = this.getNode(name)
const addon = this.getNode(name);
if (addon) {
// 无法确定是否有这个api
// return addon.exportData();
}
return addon
return addon;
}
@obx.ref private _dropLocation: DropLocation | null = null;
@ -485,7 +483,8 @@ export class DocumentModel {
// add toData
toData() {
return { componentsTree: [this.project?.currentDocument?.export(TransformStage.Save)] };
const node = this.project?.currentDocument?.export(TransformStage.Serilize);
return { componentsTree: [node] };
}
getHistory(): History {

View File

@ -213,10 +213,18 @@ export class NodeChildren {
});
}
every(fn: (item: Node, index: number) => any): boolean {
return this.children.every((child, index) => fn(child, index));
}
some(fn: (item: Node, index: number) => any): boolean {
return this.children.some((child, index) => fn(child, index));
}
filter(fn: (item: Node, index: number) => item is Node) {
return this.children.filter(fn);
}
mergeChildren(remover: () => any, adder: (children: Node[]) => NodeData[] | null, sorter: () => any) {
let changed = false;
if (remover) {

View File

@ -284,9 +284,9 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
*/
hover(flag = true) {
if (flag) {
this.document.designer.hovering.hover(this);
this.document.designer.detecting.capture(this);
} else {
this.document.designer.hovering.unhover(this);
this.document.designer.detecting.release(this);
}
}
@ -395,6 +395,13 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
this.getProp(path, true)!.setValue(value);
}
/**
*
*/
clearPropValue(path: string): void {
this.getProp(path, false)?.unset();
}
/**
*
*/
@ -511,7 +518,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
const schema: any = {
...baseSchema,
props: this.document.designer.transformProps(props, this, stage),
..._extras_,
...this.document.designer.transformProps(_extras_, this, stage),
};
if (this.isParental() && this.children.size > 0) {

View File

@ -245,7 +245,9 @@ export class Prop implements IPropParent {
return typeof this.key === 'string' && this.key.charAt(0) === '!';
}
// TODO: improve this logic
/**
* @returns 0: the same 1: maybe & like 2: not the same
*/
compare(other: Prop | null): number {
if (!other || other.isUnset()) {
return this.isUnset() ? 0 : 2;

View File

@ -134,7 +134,7 @@ export interface ISimulatorHost<P = object> extends ISensor {
computeComponentInstanceRect(instance: ComponentInstance, selector?: string): DOMRect | null;
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null;
findDOMNodes(instance: ComponentInstance, selector?: string): Array<Element | Text> | null;
/**
*

View File

@ -3,6 +3,37 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
<a name="0.8.12"></a>
## [0.8.12](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.11...@ali/lowcode-editor-core@0.8.12) (2020-05-13)
### Bug Fixes
* supports ([371b84c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/371b84c))
* tip direction ([f51d496](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f51d496))
<a name="0.8.11"></a>
## [0.8.11](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.10...@ali/lowcode-editor-core@0.8.11) (2020-05-08)
**Note:** Version bump only for package @ali/lowcode-editor-core
<a name="0.8.10"></a>
## [0.8.10](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.9...@ali/lowcode-editor-core@0.8.10) (2020-05-07)
### Bug Fixes
* intl ([8a061ab](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/8a061ab))
<a name="0.8.9"></a>
## [0.8.9](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.8...@ali/lowcode-editor-core@0.8.9) (2020-04-27)

View File

@ -1,6 +1,6 @@
{
"name": "@ali/lowcode-editor-core",
"version": "0.8.9",
"version": "0.8.12",
"description": "Core Api for Ali lowCode engine",
"license": "MIT",
"main": "lib/index.js",
@ -15,11 +15,11 @@
"cloud-build": "build-scripts build --skip-demo"
},
"dependencies": {
"@ali/lowcode-types": "^0.8.1",
"@ali/lowcode-utils": "^0.8.2",
"@ali/lowcode-types": "^0.8.3",
"@ali/lowcode-utils": "^0.8.4",
"@alifd/next": "^1.19.16",
"@recore/obx": "^1.0.8",
"@recore/obx-react": "^1.0.7",
"@recore/obx": "^1.0.9",
"@recore/obx-react": "^1.0.8",
"classnames": "^2.2.6",
"debug": "^4.1.1",
"intl-messageformat": "^8.3.1",

View File

@ -35,7 +35,16 @@ class AliGlobalLocale {
if (this._locale != null) {
return this._locale;
}
const { g_config, navigator } = window as any;
// TODO: store 1 & store 2 abstract out as custom implements
// store 1: config from window
let locale: string = getConfig('locale');
if (locale) {
return languageMap[locale] || locale.replace('_', '-');
}
// store 2: config from storage
if (hasLocalStorage(window)) {
const store = window.localStorage;
let config: any;
@ -47,20 +56,14 @@ class AliGlobalLocale {
if (config?.locale) {
return (config.locale || '').replace('_', '-');
}
} else if (g_config) {
if (g_config.locale) {
return languageMap[g_config.locale] || g_config.locale.replace('_', '-');
}
}
let locale: string = '';
// store 2: config from system
const { navigator } = window as any;
if (navigator.language) {
const lang = (navigator.language as string);
return languageMap[lang] || lang.replace('_', '-');
}
// IE10 及更低版本使用 browserLanguage
if (navigator.browserLanguage) {
} else if (navigator.browserLanguage) {
const it = navigator.browserLanguage.split('-');
locale = it[0];
if (it[1]) {
@ -116,6 +119,15 @@ class AliGlobalLocale {
}
}
function getConfig(name: string) {
const win: any = window;
return (
win[name]
|| (win.g_config || {})[name]
|| (win.pageConfig || {})[name]
);
}
function hasLocalStorage(obj: any): obj is WindowLocalStorage {
return obj.localStorage;
}

View File

@ -0,0 +1,140 @@
export class FocusTracker {
mount(win: Window) {
const checkDown = (e: MouseEvent) => {
if (this.checkModalDown(e)) {
return;
}
const first = this.first;
if (first && !first.internalCheckInRange(e)) {
this.internalSuspenseItem(first);
first.internalTriggerBlur();
}
};
win.document.addEventListener('click', checkDown, true);
return () => {
win.document.removeEventListener('click', checkDown, true);
};
}
private actives: Focusable[] = [];
get first() {
return this.actives[0];
}
private modals: Array<{ checkDown: (e: MouseEvent) => boolean; checkOpen: () => boolean }> = [];
addModal(checkDown: (e: MouseEvent) => boolean, checkOpen: () => boolean) {
this.modals.push({
checkDown,
checkOpen,
});
}
private checkModalOpen(): boolean {
return this.modals.some(item => item.checkOpen());
}
private checkModalDown(e: MouseEvent): boolean {
return this.modals.some(item => item.checkDown(e));
}
execSave() {
// has Modal return;
if (this.checkModalOpen()) {
return;
}
// catch
if (this.first) {
this.first.internalTriggerSave();
}
}
execEsc() {
const first = this.first;
if (first) {
this.internalSuspenseItem(first);
first.internalTriggerEsc();
}
}
create(config: FocusableConfig) {
return new Focusable(this, config);
}
internalActiveItem(item: Focusable) {
const first = this.actives[0];
if (first === item) {
return;
}
const i = this.actives.indexOf(item);
if (i > -1) {
this.actives.splice(i, 1);
}
this.actives.unshift(item);
if (!item.isModal && first) {
// trigger Blur
first.internalTriggerBlur();
}
// trigger onActive
item.internalTriggerActive();
}
internalSuspenseItem(item: Focusable) {
const i = this.actives.indexOf(item);
if (i > -1) {
this.actives.splice(i, 1);
this.first?.internalTriggerActive();
}
}
}
export interface FocusableConfig {
range: HTMLElement | ((e: MouseEvent) => boolean);
modal?: boolean; // 模态窗口级别
onEsc?: () => void;
onBlur?: () => void;
onSave?: () => void;
onActive?: () => void;
}
export class Focusable {
readonly isModal: boolean;
constructor(private tracker: FocusTracker, private config: FocusableConfig) {
this.isModal = config.modal == null ? false : config.modal;
}
active() {
this.tracker.internalActiveItem(this);
}
suspense() {
this.tracker.internalSuspenseItem(this);
}
purge() {
this.tracker.internalSuspenseItem(this);
}
internalCheckInRange(e: MouseEvent) {
const { range } = this.config;
if (!range) {
return false;
}
if (typeof range === 'function') {
return range(e);
}
return range.contains(e.target as HTMLElement);
}
internalTriggerBlur() {
if (this.config.onBlur) {
this.config.onBlur();
}
}
internalTriggerSave() {
if (this.config.onSave) {
this.config.onSave();
return true;
}
return false;
}
internalTriggerEsc() {
if (this.config.onEsc) {
this.config.onEsc();
}
}
internalTriggerActive() {
if (this.config.onActive) {
this.config.onActive();
}
}
}
export const focusTracker = new FocusTracker();
focusTracker.mount(window);

View File

@ -1,48 +0,0 @@
class FocusingManager {
deploy() {
}
send(e: MouseEvent | KeyboardEvent) {
}
addModalCheck() {
}
create(config: FocusableConfig) {
}
activeItem() {
}
suspenceItem() {
}
}
export interface FocusableConfig {
range: HTMLElement | ((e: MouseEvent) => boolean);
modal?: boolean;
onEsc?: () => void;
onBlur?: () => void;
}
class Focusable {
readonly isModal: boolean;
constructor(private manager: FocusingManager, { range, modal }: FocusableConfig) {
this.isModal = modal == null ? false : modal;
}
checkRange(e: MouseEvent) {
}
active() {
this.manager.activeItem(this);
}
suspence() {
this.manager.suspenceItem(this);
}
purge() {
}
}

View File

@ -2,3 +2,4 @@ export * from './get-public-path';
export * from './goldlog';
export * from './obx';
export * from './request';
export * from './focus-tracker';

View File

@ -64,8 +64,17 @@ function resolveDirection(popup: any, target: any, edge: any, bounds: any, prefe
return prefer;
}
function resolvePrefer(prefer: any) {
function resolvePrefer(prefer: any, targetRect: any, bounds: any) {
if (!prefer) {
if (targetRect.left - bounds.left < 10) {
return { dir: 'right' };
} else if (targetRect.top - bounds.top < 10) {
return { dir: 'bottom' };
} else if (bounds.bottom - targetRect.bottom < 10) {
return { dir: 'top' };
} else if (bounds.right - targetRect.right < 10) {
return { dir: 'left' };
}
return {};
}
const force = prefer[0] === '!';
@ -105,7 +114,7 @@ export function resolvePosition(popup: any, target: any, arrow: any, bounds: any
width: popup.width,
};
const prefers = resolvePrefer(prefer);
const prefers = resolvePrefer(prefer, target, bounds);
const edge = resolveEdge(popup, target, arrow, bounds);

View File

@ -27,7 +27,7 @@ export class Title extends Component<{ title: TitleContent; className?: string;
typeof title.tip === 'object' && !(isValidElement(title.tip) || isI18nData(title.tip))
? title.tip
: { children: title.tip };
tip = <Tip direction="top" theme="black" {...tipProps} />;
tip = <Tip {...tipProps} />;
}
}

View File

@ -3,6 +3,69 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
<a name="0.8.15"></a>
## [0.8.15](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-skeleton@0.8.14...@ali/lowcode-editor-skeleton@0.8.15) (2020-05-13)
**Note:** Version bump only for package @ali/lowcode-editor-skeleton
<a name="0.8.14"></a>
## [0.8.14](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-skeleton@0.8.13...@ali/lowcode-editor-skeleton@0.8.14) (2020-05-13)
### Bug Fixes
* 🐛 add tip on setter title ([c93c1d0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c93c1d0))
* panel visible time ([18ac1fa](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/18ac1fa))
* set i18n setter value when change mixed setter ([72d81c2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/72d81c2))
* supports ([371b84c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/371b84c))
### Features
* show value state ([bd49e50](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bd49e50))
<a name="0.8.13"></a>
## [0.8.13](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-skeleton@0.8.12...@ali/lowcode-editor-skeleton@0.8.13) (2020-05-08)
**Note:** Version bump only for package @ali/lowcode-editor-skeleton
<a name="0.8.12"></a>
## [0.8.12](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-skeleton@0.8.11...@ali/lowcode-editor-skeleton@0.8.12) (2020-05-08)
**Note:** Version bump only for package @ali/lowcode-editor-skeleton
<a name="0.8.11"></a>
## [0.8.11](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-skeleton@0.8.10...@ali/lowcode-editor-skeleton@0.8.11) (2020-05-07)
### Bug Fixes
* 🐛 history pane zindex ([48f3be1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/48f3be1))
* 🐛 修复编辑面板 ([a0bad77](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a0bad77))
* 🐛 绑定动作无法打开代码面板 ([160d6f7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/160d6f7))
* 在Transducer中添加对MixedSetter的支持 ([7317f2f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7317f2f))
### Features
* left pane style ([c149f64](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c149f64))
* left pane title style; setting pane style ([66e8c25](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/66e8c25))
<a name="0.8.10"></a>
## [0.8.10](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-skeleton@0.8.9...@ali/lowcode-editor-skeleton@0.8.10) (2020-04-27)

View File

@ -1,6 +1,6 @@
{
"name": "@ali/lowcode-editor-skeleton",
"version": "0.8.10",
"version": "0.8.15",
"description": "alibaba lowcode editor skeleton",
"main": "lib/index.js",
"module": "es/index.js",
@ -19,10 +19,10 @@
"editor"
],
"dependencies": {
"@ali/lowcode-designer": "^0.9.6",
"@ali/lowcode-editor-core": "^0.8.9",
"@ali/lowcode-types": "^0.8.1",
"@ali/lowcode-utils": "^0.8.2",
"@ali/lowcode-designer": "^0.9.9",
"@ali/lowcode-editor-core": "^0.8.12",
"@ali/lowcode-types": "^0.8.3",
"@ali/lowcode-utils": "^0.8.4",
"@alifd/next": "^1.x",
"classnames": "^2.2.6",
"react": "^16.8.1",

View File

@ -1,17 +1,25 @@
import { Component } from 'react';
import { isObject } from 'lodash';
import classNames from 'classnames';
import { Icon } from '@alifd/next';
import { Title } from '@ali/lowcode-editor-core';
import { Title, Tip } from '@ali/lowcode-editor-core';
import { TitleContent } from '@ali/lowcode-types';
import { PopupPipe, PopupContext } from '../popup';
import { intlNode } from '../../locale';
import './index.less';
import { IconClear } from '../../icons/clear';
import InlineTip from './inlinetip';
export interface FieldProps {
className?: string;
title?: TitleContent | null;
defaultDisplay?: 'accordion' | 'inline' | 'block';
collapsed?: boolean;
valueState?: number;
name?: string;
tip?: any;
onExpandChange?: (expandState: boolean) => void;
onClear?: () => void;
}
export class Field extends Component<FieldProps> {
@ -72,10 +80,36 @@ export class Field extends Component<FieldProps> {
}
}
getTipContent(propName: string, tip?: any): any {
let tipContent = (
<div>
<div>{propName}</div>
</div>
);
if (isObject(tip)) {
tipContent = (
<div>
<div>{propName}</div>
<div>{(tip as any).content}</div>
</div>
);
} else if (tip) {
tipContent = (
<div>
<div>{propName}</div>
<div>{tip}</div>
</div>
);
}
return tipContent;
}
render() {
const { className, children, title } = this.props;
const { className, children, title, valueState, onClear, name: propName, tip } = this.props;
const { display, collapsed } = this.state;
const isAccordion = display === 'accordion';
const tipContent = this.getTipContent(propName!, tip);
return (
<div
className={classNames(`lc-field lc-${display}-field`, className, {
@ -84,7 +118,9 @@ export class Field extends Component<FieldProps> {
>
<div className="lc-field-head" onClick={isAccordion ? this.toggleExpand : undefined}>
<div className="lc-field-title">
{createValueState(valueState, onClear)}
<Title title={title || ''} />
<InlineTip position="top">{tipContent}</InlineTip>
</div>
{isAccordion && <Icon className="lc-field-icon" type="arrow-up" size="xs" />}
</div>
@ -96,6 +132,51 @@ export class Field extends Component<FieldProps> {
}
}
/**
* ****
*
* -1
* 0 | null
* 1
* 2
* 10
*
* TODO: turn number to enum
*/
function createValueState(valueState?: number, onClear?: () => void) {
let tip: any = null;
let className = 'lc-valuestate';
let icon: any = null;
if (valueState) {
if (valueState < 0) {
// multiple value 橘黄色点: tip多种值点击清除
tip = intlNode('Multiple Value, Click to Clear');
className += ' valuestate-multiple';
icon = <IconClear size={6} />;
} else if (valueState === 10) {
// isset orangered tip: 必填项
tip = intlNode('Required');
className += ' valuestate-required';
onClear = undefined;
} else if (valueState > 0) {
// isset 蓝点 tip: 已设置值,点击清除
tip = intlNode('Setted Value, Click to Clear');
className += ' valuestate-isset';
icon = <IconClear size={6} />;
}
} else {
onClear = undefined;
// unset 占位空间
}
return (
<i className={className} onClick={onClear}>
{icon}
{tip && <Tip>{tip}</Tip>}
</i>
);
}
export interface PopupFieldProps extends FieldProps {
width?: number;
}

View File

@ -11,6 +11,54 @@
.lc-field-title {
display: flex;
align-items: center;
.lc-valuestate {
height: 6px;
width: 6px;
min-width: 6px;
border-radius: 100%;
margin-right: 2px;
pointer-events: none;
display: inline-flex;
align-items: center;
justify-content: center;
color: white;
> svg {
display: none;
}
&.valuestate-multiple {
background-color: rgb(232, 145, 83);
pointer-events: auto;
&:hover {
background-color: rgb(223, 139, 30);
cursor: pointer;
transform: scale(2);
transform-origin: center;
> svg {
display: block;
}
}
}
&.valuestate-isset {
background-color: rgba(124, 177, 238, 0.6);
pointer-events: auto;
&:hover {
background-color: rgb(45, 126, 219);
cursor: pointer;
transform: scale(2);
transform-origin: center;
> svg {
display: block;
}
}
}
&.valuestate-required {
background-color: rgb(250, 82, 76);
pointer-events: auto;
&:hover {
background-color: rgb(224, 46, 40);
}
}
}
}
.lc-field-icon {
// margin-right: @x-gap;
@ -75,7 +123,7 @@
// background: var(--color-block-background-shallow, rgba(31,56,88,.06));
// border-bottom: 1px solid var(--color-line-normal);
// color: var(--color-title);
padding: 0 16px;
padding: 0 16px 0 10px;
background-color: #F7F9FC;
color: #8F9BB3;
user-select: none;

View File

@ -8,7 +8,9 @@ export interface FieldProps {
title?: TitleContent | null;
display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry';
collapsed?: boolean;
valueState?: number;
onExpandChange?: (collapsed: boolean) => void;
onClear?: () => void;
[extra: string]: any;
}

View File

@ -0,0 +1,25 @@
import * as React from 'react';
export interface InlineTipProps {
position: string;
theme?: 'green' | 'black';
children: React.ReactNode;
}
export default class InlineTip extends React.Component<InlineTipProps> {
static displayName = 'InlineTip';
static defaultProps = {
position: 'auto',
theme: 'black',
};
render(): React.ReactNode {
const { position, theme, children } = this.props;
return (
<div style={{ display: 'none' }} data-role="tip" data-position={position} data-theme={theme}>
{children}
</div>
);
}
}

View File

@ -1,10 +1,11 @@
import { Component } from 'react';
import { intl, shallowIntl, createSetterContent, observer } from '@ali/lowcode-editor-core';
import { shallowIntl, createSetterContent, observer } from '@ali/lowcode-editor-core';
import { createContent } from '@ali/lowcode-utils';
import { Field, createField } from '../field';
import PopupService from '../popup';
import { SettingField, isSettingField, SettingTopEntry, SettingEntry } from '@ali/lowcode-designer';
import { isSetterConfig, CustomView } from '@ali/lowcode-types';
import { intl } from '../../locale';
@observer
class SettingFieldView extends Component<{ field: SettingField }> {
@ -41,26 +42,19 @@ class SettingFieldView extends Component<{ field: SettingField }> {
setterType = setter;
}
let value = null;
if (field.type === 'field') {
if (defaultValue != null && !('defaultValue' in setterProps)) {
setterProps.defaultValue = defaultValue;
if (initialValue == null) {
initialValue = defaultValue;
}
if (defaultValue != null && !('defaultValue' in setterProps)) {
setterProps.defaultValue = defaultValue;
if (initialValue == null) {
initialValue = defaultValue;
}
if (field.valueState > 0) {
value = field.getValue();
} else {
setterProps.multiValue = true;
if (!('placeholder' in setterProps)) {
// FIXME! move to locale file
setterProps.placeholder = intl({
type: 'i18n',
'zh-CN': '多种值',
'en-US': 'Multiple Value',
});
}
}
if (field.valueState === -1) {
setterProps.multiValue = true;
if (!('placeholder' in setterProps)) {
setterProps.placeholder = intl('Multiple Value');
}
} else {
value = field.getValue();
}
// todo: error handling
@ -69,7 +63,10 @@ class SettingFieldView extends Component<{ field: SettingField }> {
{
title: field.title,
collapsed: !field.expanded,
valueState: field.isRequired ? 10 : field.valueState,
onExpandChange: (expandState) => field.setExpanded(expandState),
onClear: () => field.clearValue(),
...extraProps,
},
createSetterContent(setterType, {
...shallowIntl(setterProps),
@ -95,7 +92,7 @@ class SettingFieldView extends Component<{ field: SettingField }> {
value,
});
field.setValue(value);
}
},
}),
extraProps.forceInline ? 'plain' : extraProps.display,
);

View File

@ -0,0 +1,11 @@
import { SVGIcon, IconProps } from "@ali/lowcode-utils";
export function IconClear(props: IconProps) {
return (
<SVGIcon viewBox="0 0 1024 1024" {...props}>
<path d="M761.6 701.44a21.333333 21.333333 0 0 1 0 30.293333l-29.866667 29.866667a21.333333 21.333333 0 0 1-30.293333 0L512 572.16l-189.44 189.44a21.333333 21.333333 0 0 1-30.293333 0l-29.866667-29.866667a21.333333 21.333333 0 0 1 0-30.293333L451.84 512 262.4 322.56a21.333333 21.333333 0 0 1 0-30.293333l29.866667-29.866667a21.333333 21.333333 0 0 1 30.293333 0L512 451.84l189.44-189.44a21.333333 21.333333 0 0 1 30.293333 0l29.866667 29.866667a21.333333 21.333333 0 0 1 0 30.293333L572.16 512z" />
</SVGIcon>
);
}
IconClear.displayName = 'Clear';

View File

@ -3,12 +3,7 @@ import { SVGIcon, IconProps } from "@ali/lowcode-utils";
export function IconConvert(props: IconProps) {
return (
<SVGIcon viewBox="0 0 1024 1024" {...props}>
<path d="M508.16 889.6C291.84 889.6 115.2 714.24 115.2 497.92 115.2 281.6 291.84 106.24 509.44 106.24c43.52 0 85.76 6.4 124.16 20.48l-10.24 30.72c-35.84-11.52-72.96-17.92-113.92-17.92-199.68 0-362.24 161.28-362.24 359.68s162.56 358.4 360.96 358.4 359.68-161.28 359.68-359.68c0-66.56-17.92-131.84-51.2-185.6L844.8 294.4c37.12 60.16 56.32 130.56 56.32 203.52-1.28 216.32-176.64 391.68-392.96 391.68z" />
<path d="M627.2 140.8m-15.36 0a15.36 15.36 0 1 0 30.72 0 15.36 15.36 0 1 0-30.72 0Z" />
<path d="M832 304.64m-15.36 0a15.36 15.36 0 1 0 30.72 0 15.36 15.36 0 1 0-30.72 0Z" />
<path d="M348.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" />
<path d="M508.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" />
<path d="M668.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" />
<path d="M620.8 256c-12.8-12.8-32-12.8-44.8 0s-12.8 32 0 44.8l83.2 83.2H288c-19.2 0-32 12.8-32 32s12.8 32 32 32h448c6.4 0 32 0 32-32 0-19.2-6.4-25.6-6.4-25.6L620.8 256zM736 576H288c-6.4 0-32 0-32 32 0 19.2 6.4 25.6 6.4 25.6L403.2 768c12.8 12.8 32 12.8 44.8 0s12.8-32 0-44.8L364.8 640H736c19.2 0 32-12.8 32-32s-12.8-32-32-32zM512 64C262.4 64 64 262.4 64 512s198.4 448 448 448 448-198.4 448-448S761.6 64 512 64z m0 832c-211.2 0-384-172.8-384-384s172.8-384 384-384 384 172.8 384 384-172.8 384-384 384z" />
</SVGIcon>
);
}

View File

@ -5,12 +5,17 @@ import { Button, Icon } from '@alifd/next';
import Area from '../area';
import { PanelConfig } from '../types';
import Panel from '../widget/panel';
import { Designer } from '@ali/lowcode-designer';
@observer
export default class LeftFixedPane extends Component<{ area: Area<PanelConfig, Panel> }> {
shouldComponentUpdate() {
return false;
}
componentDidUpdate() {
// FIXME: dirty fix, need deep think
this.props.area.skeleton.editor.get(Designer)?.touchOffsetObserver();
}
render() {
const { area } = this.props;
const hideTitleBar = area.current?.config.props?.hideTitleBar;

View File

@ -1,6 +1,6 @@
import { Component, Fragment } from 'react';
import classNames from 'classnames';
import { observer } from '@ali/lowcode-editor-core';
import { observer, Focusable, focusTracker } from '@ali/lowcode-editor-core';
import { Button, Icon } from '@alifd/next';
import Area from '../area';
import Panel from '../widget/panel';
@ -12,7 +12,7 @@ export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }>
}
private dispose?: () => void;
// private focusing?: FocusingItem;
private focusing?: Focusable;
private shell: HTMLElement | null = null;
componentDidMount() {
const { area } = this.props;
@ -22,30 +22,39 @@ export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }>
area.skeleton.editor.removeListener('designer.dragstart', triggerClose);
}
/*
this.focusing = focusingTrack.create(this.shell!, {
this.focusing = focusTracker.create({
range: (e) => {
const target = e.target as HTMLElement;
if (!target) {
return false;
}
if (this.shell?.contains(target)) {
return true;
}
const docks = area.current?.getAssocDocks();
if (docks && docks?.length) {
return docks.some(dock => dock.getDOMNode()?.contains(target));
}
return false;
},
onEsc: () => {
this.props.area.setVisible(false);
},
onBlur: () => {
this.props.area.setVisible(false);
},
// modal: boolean
});
*/
this.onEffect();
}
onEffect() {
/*
const { area } = this.props;
if (area.visible) {
this.focusing?.active();
} else {
this.focusing?.suspense();
}
*/
}
componentDidUpdate() {
@ -53,7 +62,7 @@ export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }>
}
componentWillUnmount() {
// this.focusing?.purge();
this.focusing?.purge();
this.dispose?.();
}

View File

@ -127,122 +127,6 @@ body {
&.hidden {
display: none;
}
/*
.my-tabs {
width: 100%;
height: 100%;
position: relative;
.tabs-title {
display: flex;
height: var(--pane-title-height);
margin-right: 30px;
> .tab-title {
cursor: pointer;
padding: 0;
flex: 1;
min-width: 0;
justify-content: center;
border-bottom: 2px solid transparent;
&.actived {
cursor: default;
color: var(--color-text-avtived);
border-bottom-color: #3896ee;
}
}
}
.tabs-content {
position: absolute;
top: var(--pane-title-height);
bottom: 0;
left: 0;
right: 0;
height: calc(100% - var(--pane-title-height));
overflow: hidden;
}
}
*/
/*覆盖旧面板*/
/*组件面板*/
// .ve-component-list {
// .ve-component-list-body{
// .ve-component-list-sidebar{
// .ve-component-list-navigator{
// .navigator-group{
// &:last-child{
// &::after{
// display: none;
// }
// }
// &::after{
// content: '';
// display: block;
// height: 1px;
// background-color: #EDEFF3;
// line-height: 0;
// margin: 4px 12px 0;
// }
// .navigator-group-head{
// .navigator-group-title{
// border-bottom: none;
// }
// }
// .navigator-group-item{
// border-left: 2px solid transparent;
// &.active{
// border-left-color: #0079f2;
// border-right: none;
// }
// }
// }
// }
// }
// }
// }
/*数据源*/
// .engine-datapool{
// .engine-datapool-view-group{
// padding-top: 48px;
// .engine-datapool-view-group-title{
// height: 48px;
// line-height: 48px;
// font-size: 16px;
// background-color: transparent;
// padding: 0 16px;
// border-bottom: 1px solid #EDEFF3;
// }
// }
// }
/*动作面板*/
// .ve-action-pane{
// border-top: none;
// .rc-tabs{
// .rc-tabs-bar{
// background-color: transparent;
// .rc-tabs-tab{
// line-height: 1;
// &.rc-tabs-tab-active{
// }
// }
// }
// }
// }
/*设置面板*/
// .ve-field .ve-field-head,
// .ve-field.ve-accordion2-field > .ve-field-head .ve-field-title-content{
// padding: 0;
// }
// .ve-field.ve-accordion2-field > .ve-field-split-line{
// display: none;
// }
// .vs-style .vs-style-source{
// margin: 0 0 16px;
// }
// .vs-code-button,
// .vs-json-button{
// margin: 0;
// }
}
@ -286,10 +170,11 @@ body {
position: absolute;
top: 0;
bottom: 0;
// width: var(--dock-pane-width);
width: var(--dock-pane-width);
min-width: var(--dock-fixed-pane-width);
left: calc(var(--left-area-width) + 1px);
background-color: var(--color-pane-background);
box-shadow: 4px 0 16px 0 rgba(31,50,88,0.08);
box-shadow: 4px 6px 6px 0 rgba(31,50,88,0.08);
z-index: 820;
display: none;
// padding-top: 36px;
@ -449,6 +334,12 @@ body {
display: none;
flex-shrink: 0;
margin-left: 2px;
position: relative;
>.lc-panel {
position: absolute;
left: 0;
top: 0;
}
&.lc-area-visible {
display: block;
}

View File

@ -1,5 +1,9 @@
{
"Binded: {expr}": "Binded: {expr}",
"Variable Binding": "Variable Binding",
"Switch Setter": "Switch Setter"
"Switch Setter": "Switch Setter",
"Multiple Value, Click to Clear": "Multiple Value, Click to Clear",
"Required": "Required",
"Setted Value, Click to Clear": "Setted Value, Click to Clear",
"Multiple Value": "Multiple Value"
}

View File

@ -1,5 +1,9 @@
{
"Binded: {expr}": "已绑定: {expr}",
"Variable Binding": "变量绑定",
"Switch Setter": "切换设置器"
"Switch Setter": "切换设置器",
"Multiple Value, Click to Clear": "多种值, 点击清除",
"Required": "必填项",
"Setted Value, Click to Clear": "已设置值,点击清除",
"Multiple Value": "多种值"
}

View File

@ -110,7 +110,7 @@ export class Skeleton {
}
return this.createPanel(config);
},
true,
false,
true,
);
this.mainArea = new Area(
@ -192,6 +192,7 @@ export class Skeleton {
this.editor.emit(event, ...args);
}
readonly widgets: IWidget[] = [];
createWidget(config: IWidgetBaseConfig | IWidget) {
if (isWidget(config)) {
return config;
@ -220,6 +221,7 @@ export class Skeleton {
} else {
widget = new Widget(this, config as WidgetConfig);
}
this.widgets.push(widget);
return widget;
}

View File

@ -13,7 +13,7 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
name: 'children',
title: { type: 'i18n', 'zh-CN': '内容设置', 'en-US': 'Content' },
setter: {
componentName: 'MixinSetter',
componentName: 'MixedSetter',
props: {
// TODO:
setters: [
@ -41,11 +41,11 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
};
}
const { props, events = {}, styles } = configure as any;
const { props, supports = {} } = configure as any;
const isRoot: boolean = componentName === 'Page' || componentName === 'Component';
const eventsDefinition: any[] = [];
const supportedLifecycles =
events.supportedLifecycles ||
supports.lifecycles ||
(isRoot
? /*[
{
@ -73,11 +73,11 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
list: supportedLifecycles.map((event: any) => (typeof event === 'string' ? { name: event } : event)),
});
}
if (events.supportedEvents) {
if (supports.events) {
eventsDefinition.push({
type: 'events',
title: '事件',
list: (events.supportedEvents || []).map((event: any) => (typeof event === 'string' ? { name: event } : event)),
list: (supports.events || []).map((event: any) => (typeof event === 'string' ? { name: event } : event)),
});
}
// 通用设置
@ -125,6 +125,24 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
],
});
*/
const stylesGroup: FieldConfig[] = [];
let advanceGroup: FieldConfig[] = [];
if (propsGroup) {
let l = propsGroup.length;
while (l-- > 0) {
const item = propsGroup[l];
if (item.type === 'group' && (item.title === '高级' || item.title?.label === '高级')) {
advanceGroup = item.items || [];
propsGroup.splice(l, 1);
} else if (item.name === '__style__' || item.name === 'containerStyle' || item.name === 'pageStyle') {
propsGroup.splice(l, 1);
stylesGroup.push(item);
if (item.extraProps?.defaultCollapsed && item.name !== 'containerStyle') {
item.extraProps.defaultCollapsed = false;
}
}
}
}
const combined: FieldConfig[] = [
{
title: { type: 'i18n', 'zh-CN': '属性', 'en-US': 'Props' },
@ -132,15 +150,14 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
items: propsGroup,
},
];
const stylesGroup: FieldConfig[] = [];
if (styles?.supportClassName) {
if (supports.className) {
stylesGroup.push({
name: 'className',
title: { type: 'i18n', 'zh-CN': '类名绑定', 'en-US': 'ClassName' },
setter: 'ClassNameSetter',
});
}
if (styles?.supportInlineStyle) {
if (supports.style) {
stylesGroup.push({
name: 'style',
title: { type: 'i18n', 'zh-CN': '行内样式', 'en-US': 'Style' },
@ -183,79 +200,75 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
});
}
if (isRoot) {
/*
if (!isRoot) {
if (supports.condition !== false) {
advanceGroup.push({
name: '___condition',
title: { type: 'i18n', 'zh-CN': '是否渲染', 'en-US': 'Condition' },
defaultValue: true,
setter: [{
componentName: 'BoolSetter',
}, {
componentName: 'VariableSetter'
}],
});
}
if (supports.loop !== false) {
advanceGroup.push({
name: '#loop',
title: { type: 'i18n', 'zh-CN': '循环', 'en-US': 'Loop' },
items: [
{
name: '___loop',
title: { type: 'i18n', 'zh-CN': '循环数据', 'en-US': 'Loop Data' },
defaultValue: [],
setter: [{
componentName: 'JsonSetter',
props: {
label: { type: 'i18n', 'zh-CN': '编辑数据', 'en-US': 'Edit Data'},
},
}, {
componentName: 'VariableSetter'
}],
},
{
name: '___loopArgs.0',
title: { type: 'i18n', 'zh-CN': '迭代变量名', 'en-US': 'Loop Item' },
setter: {
componentName: 'StringSetter',
props: {
placeholder: { type: 'i18n', 'zh-CN': '默认为: item', 'en-US': 'Defaults: item' },
}
},
},
{
name: '___loopArgs.1',
title: { type: 'i18n', 'zh-CN': '索引变量名', 'en-US': 'Loop Index' },
setter: {
componentName: 'StringSetter',
props: {
placeholder: { type: 'i18n', 'zh-CN': '默认为: index', 'en-US': 'Defaults: index' },
}
},
},
{
name: 'key',
title: '循环 Key',
setter: [{
componentName: 'StringSetter',
}, {
componentName: 'VariableSetter'
}],
},
],
})
}
}
if (advanceGroup.length > 0) {
combined.push({
name: '#advanced',
title: { type: 'i18n', 'zh-CN': '高级', 'en-US': 'Advance' },
items: [],
});
*/
} else {
combined.push({
name: '#advanced',
title: { type: 'i18n', 'zh-CN': '高级', 'en-US': 'Advance' },
items: [
{
name: '___condition',
title: { type: 'i18n', 'zh-CN': '是否渲染', 'en-US': 'Condition' },
setter: [{
componentName: 'BoolSetter',
props: {
defaultValue: true,
}
}, {
componentName: 'VariableSetter'
}],
},
{
name: '#loop',
title: { type: 'i18n', 'zh-CN': '循环', 'en-US': 'Loop' },
items: [
{
name: '___loop',
title: { type: 'i18n', 'zh-CN': '循环数据', 'en-US': 'Loop Data' },
setter: [{
componentName: 'JsonSetter',
props: {
label: { type: 'i18n', 'zh-CN': '编辑数据', 'en-US': 'Edit Data'},
},
}, {
componentName: 'VariableSetter'
}],
},
{
name: '___loopArgs.0',
title: { type: 'i18n', 'zh-CN': '迭代变量名', 'en-US': 'Loop Item' },
setter: {
componentName: 'StringSetter',
props: {
placeholder: { type: 'i18n', 'zh-CN': '默认为: item', 'en-US': 'Defaults: item' },
}
},
},
{
name: '___loopArgs.1',
title: { type: 'i18n', 'zh-CN': '索引变量名', 'en-US': 'Loop Index' },
setter: {
componentName: 'StringSetter',
props: {
placeholder: { type: 'i18n', 'zh-CN': '默认为: index', 'en-US': 'Defaults: index' },
}
},
},
{
name: 'key',
title: '循环 Key',
setter: [{
componentName: 'StringSetter',
}, {
componentName: 'VariableSetter'
}],
},
],
},
],
items: advanceGroup,
});
}

View File

@ -143,7 +143,7 @@ function propTypeToSetter(propType: PropType): SetterType {
};
case 'oneOfType':
return {
componentName: 'MixinSetter',
componentName: 'MixedSetter',
props: {
// TODO:
// setters: (propType as OneOfType).value.map(item => propTypeToSetter(item)),
@ -153,7 +153,7 @@ function propTypeToSetter(propType: PropType): SetterType {
}
return {
componentName: 'MixinSetter',
componentName: 'MixedSetter',
isRequired,
};
}
@ -175,8 +175,8 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
},
};
}
const { component = {}, events = {}, styles = {} } = configure;
const supportedEvents: any[] | null = (events as any).supportedEvents ? null : [];
const { component = {}, supports = {} } = configure;
const supportedEvents: any[] | null = (supports as any).events ? null : [];
const props: FieldConfig[] = [];
metadata.props.forEach((prop) => {
@ -197,21 +197,21 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
name,
description,
});
(events as any).supportedEvents = supportedEvents;
(supports as any).events = supportedEvents;
}
return;
}
if (name === 'className' && (propType === 'string' || propType === 'any')) {
if ((styles as any).supportClassName == null) {
(styles as any).supportClassName = true;
if ((supports as any).className == null) {
(supports as any).className = true;
}
return;
}
if (name === 'style' && (propType === 'object' || propType === 'any')) {
if ((styles as any).supportInlineStyle == null) {
(styles as any).supportInlineStyle = true;
if ((supports as any).style == null) {
(supports as any).style = true;
}
return;
}
@ -224,8 +224,7 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp
configure: {
...configure,
props,
events,
styles,
supports,
component,
},
};

View File

@ -1,5 +1,6 @@
import { ReactElement, ComponentType } from 'react';
import { TitleContent, IconType, I18nData, TipContent } from '@ali/lowcode-types';
import { IWidget } from './widget/widget';
export interface IWidgetBaseConfig {
type: string;
@ -16,6 +17,7 @@ export interface WidgetConfig extends IWidgetBaseConfig {
type: "Widget";
props?: {
align?: "left" | "right" | "bottom" | "center" | "top";
onInit?: (widget: IWidget) => void;
};
content?: string | ReactElement | ComponentType<any>; // children
}
@ -47,6 +49,7 @@ export function isDividerConfig(obj: any): obj is DividerConfig {
export interface IDockBaseConfig extends IWidgetBaseConfig {
props?: DockProps & {
align?: "left" | "right" | "bottom" | "center" | "top";
onInit?: (widget: IWidget) => void;
};
}
@ -95,7 +98,8 @@ export interface PanelProps {
height?: number; // panel.props
maxWidth?: number; // panel.props
maxHeight?: number; // panel.props
onInit?: () => any;
condition?: (widget: IWidget) => any;
onInit?: (widget: IWidget) => any;
onDestroy?: () => any;
shortcut?: string; // 只有在特定位置,可触发 toggle show
}

View File

@ -46,7 +46,7 @@ export default class Dock implements IWidget {
this._body = createElement(DockView, props);
}
this.inited = true;
return this._body;
}
@ -54,6 +54,9 @@ export default class Dock implements IWidget {
const { props = {}, name } = config;
this.name = name;
this.align = props.align;
if (props.onInit) {
props.onInit.call(this, this);
}
}
setVisible(flag: boolean) {

View File

@ -1,15 +1,17 @@
import { obx, computed } from '@ali/lowcode-editor-core';
import { uniqueId } from '@ali/lowcode-utils';
import { createElement, ReactNode } from 'react';
import { createElement, ReactNode, ReactInstance } from 'react';
import { Skeleton } from '../skeleton';
import { PanelDockConfig } from '../types';
import Panel from './panel';
import { PanelDockView, WidgetView } from '../components/widget-views';
import { IWidget } from './widget';
import { composeTitle } from './utils';
import { findDOMNode } from 'react-dom';
export default class PanelDock implements IWidget {
readonly isWidget = true;
readonly isPanelDock = true;
readonly id: string;
readonly name: string;
readonly align?: string;
@ -31,13 +33,21 @@ export default class PanelDock implements IWidget {
return this._body;
}
private _shell: ReactInstance | null = null;
get content(): ReactNode {
return createElement(WidgetView, {
widget: this,
ref: (ref) => {
this._shell = ref;
},
key: this.id,
});
}
getDOMNode() {
return this._shell ? findDOMNode(this._shell) : null;
}
@obx.ref private _visible: boolean = true;
get visible() {
return this._visible;
@ -58,20 +68,24 @@ export default class PanelDock implements IWidget {
this.name = name;
this.id = uniqueId(`dock:${name}$`);
this.panelName = config.panelName || name;
this.align = props?.align;
if (content) {
const _panelProps: any = { ...panelProps };
if (_panelProps.title == null && props) {
_panelProps.title = composeTitle(props.title, undefined, props.description, true, true);
}
this._panel = this.skeleton.add({
type: "Panel",
type: 'Panel',
name: this.panelName,
props: _panelProps,
contentProps,
content,
area: panelProps?.area
area: panelProps?.area,
}) as Panel;
}
if (props?.onInit) {
props.onInit.call(this, this);
}
}
setVisible(flag: boolean) {
@ -117,3 +131,8 @@ export default class PanelDock implements IWidget {
this.panel?.setActive(true);
}
}
export function isPanelDock(obj: any): obj is PanelDock {
return obj && obj.isPanelDock;
}

View File

@ -1,6 +1,6 @@
import { EventEmitter } from 'events';
import { createElement, ReactNode } from 'react';
import { obx } from '@ali/lowcode-editor-core';
import { obx, computed } from '@ali/lowcode-editor-core';
import { uniqueId, createContent } from '@ali/lowcode-utils';
import { TitleContent } from '@ali/lowcode-types';
import WidgetContainer from './widget-container';
@ -9,20 +9,25 @@ import { TitledPanelView, TabsPanelView, PanelView } from '../components/widget-
import { Skeleton } from '../skeleton';
import { composeTitle } from './utils';
import { IWidget } from './widget';
import PanelDock, { isPanelDock } from './panel-dock';
export default class Panel implements IWidget {
readonly isWidget = true;
readonly name: string;
readonly id: string;
@obx.ref inited = false;
@obx.ref private _actived = false;
@obx.ref private _actived: boolean = false;
private emitter = new EventEmitter();
get actived(): boolean {
return this._actived;
}
get visible(): boolean {
if (this.parent?.visible) {
@computed get visible(): boolean {
if (!this.parent || this.parent.visible) {
const { props } = this.config;
if (props?.condition) {
return props.condition(this);
}
return this._actived;
}
return false;
@ -30,10 +35,21 @@ export default class Panel implements IWidget {
readonly isPanel = true;
private _body?: ReactNode;
get body() {
this.initBody();
return this._body;
if (this.container) {
return createElement(TabsPanelView, {
container: this.container,
});
}
const { content, contentProps } = this.config;
return createContent(content, {
...contentProps,
editor: this.skeleton.editor,
config: this.config,
panel: this,
pane: this,
});
}
get content(): ReactNode {
@ -79,30 +95,12 @@ export default class Panel implements IWidget {
);
content.forEach((item) => this.add(item));
}
if (props.onInit) {
props.onInit.call(this, this);
}
// todo: process shortcut
}
private initBody() {
if (this.inited) {
return;
}
this.inited = true;
if (this.container) {
this._body = createElement(TabsPanelView, {
container: this.container,
});
} else {
const { content, contentProps } = this.config;
this._body = createContent(content, {
...contentProps,
editor: this.skeleton.editor,
config: this.config,
panel: this,
pane: this,
});
}
}
setParent(parent: WidgetContainer) {
if (parent === this.parent) {
return;
@ -147,11 +145,11 @@ export default class Panel implements IWidget {
return;
}
if (flag) {
if (!this.inited) {
this.initBody();
}
this._actived = true;
this.parent?.active(this);
if (!this.inited) {
this.inited = true;
}
this.emitter.emit('activechange', true);
} else if (this.inited) {
this._actived = false;
@ -172,6 +170,12 @@ export default class Panel implements IWidget {
this.setActive(true);
}
getAssocDocks(): PanelDock[] {
return this.skeleton.widgets.filter(item => {
return isPanelDock(item) && item.panelName === this.name;
}) as any;
}
/**
* @deprecated
*/

View File

@ -60,6 +60,9 @@ export default class Widget implements IWidget {
const { props = {}, name } = config;
this.name = name;
this.align = props.align;
if (props.onInit) {
props.onInit.call(this, this);
}
}
getId() {

View File

@ -3,17 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.9.1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-material-parser@0.9.0...@ali/lowcode-material-parser@0.9.1) (2020-04-01)
## [0.9.3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-material-parser@0.9.2...@ali/lowcode-material-parser@0.9.3) (2020-05-08)
### Bug Fixes
### Features
* fix bug of missing ajv ([a37d655](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a37d655))
* 🎸 support parsing sub components ([70f3e32](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/70f3e325c64bafe6a098e7eb872a81308566e811))
<a name="0.9.2"></a>
## [0.9.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-material-parser@0.9.1...@ali/lowcode-material-parser@0.9.2) (2020-05-07)
**Note:** Version bump only for package @ali/lowcode-material-parser
<a name="0.9.0"></a>
# [0.9.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-material-parser@0.8.4...@ali/lowcode-material-parser@0.9.0) (2020-03-31)

View File

@ -1,6 +1,6 @@
{
"name": "@ali/lowcode-material-parser",
"version": "0.9.1",
"version": "0.9.3",
"description": "material parser for Ali lowCode engine",
"main": "lib/index.js",
"files": [
@ -54,7 +54,8 @@
"lodash": "^4.17.15",
"react-docgen": "^5.3.0",
"semver": "^7.1.3",
"short-uuid": "^3.1.1"
"short-uuid": "^3.1.1",
"typescript": "^3.8.3"
},
"publishConfig": {
"registry": "https://registry.npm.alibaba-inc.com"

View File

@ -45,10 +45,10 @@ export async function genManifest(
npm: {
package: matScanModel.pkgName,
version: matScanModel.pkgVersion,
exportName: matParsedModel.componentName,
exportName: matParsedModel.meta?.exportName || matParsedModel.componentName,
main: matScanModel.mainFilePath,
destructuring: false,
subName: '',
destructuring: matParsedModel.meta?.exportName !== 'default',
subName: matParsedModel.meta?.subName || '',
},
};

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import { namedTypes as t } from 'ast-types';
const {
getMemberValuePath,
isReactComponentClass,
isReactComponentMethod,
resolveToValue,
match,
} = require('react-docgen').utils;
import getMethodDocumentation from '../utils/getMethodDocumentation';
const { traverseShallow } = require('react-docgen/dist/utils/traverse');
/**
* The following values/constructs are considered methods:
*
* - Method declarations in classes (except "constructor" and React lifecycle
* methods
* - Public class fields in classes whose value are a functions
* - Object properties whose values are functions
*/
function isMethod(path: any) {
const isProbablyMethod =
(t.MethodDefinition.check(path.node) && path.node.kind !== 'constructor') ||
((t.ClassProperty.check(path.node) || t.Property.check(path.node)) && t.Function.check(path.get('value').node));
return isProbablyMethod && !isReactComponentMethod(path);
}
function findAssignedMethods(scope: any, idPath: any) {
const results: any[] = [];
if (!t.Identifier.check(idPath.node)) {
return results;
}
const name = idPath.node.name;
const idScope = idPath.scope.lookup(idPath.node.name);
traverseShallow(scope.path, {
visitAssignmentExpression: function(path: any) {
const node = path.node;
if (
match(node.left, {
type: 'MemberExpression',
object: { type: 'Identifier', name },
}) &&
path.scope.lookup(name) === idScope &&
t.Function.check(resolveToValue(path.get('right')).node)
) {
results.push(path);
return false;
}
return this.traverse(path);
},
});
return results;
}
/**
* Extract all flow types for the methods of a react component. Doesn't
* return any react specific lifecycle methods.
*/
export default function componentMethodsHandler(documentation: any, path: any) {
// Extract all methods from the class or object.
let methodPaths = [];
if (isReactComponentClass(path)) {
methodPaths = path.get('body', 'body').filter(isMethod);
} else if (t.ObjectExpression.check(path.node)) {
methodPaths = path.get('properties').filter(isMethod);
// Add the statics object properties.
const statics = getMemberValuePath(path, 'statics');
if (statics) {
statics.get('properties').each((p: any) => {
if (isMethod(p)) {
p.node.static = true;
methodPaths.push(p);
}
});
}
} else if (
t.VariableDeclarator.check(path.parent.node) &&
path.parent.node.init === path.node &&
t.Identifier.check(path.parent.node.id)
) {
methodPaths = findAssignedMethods(path.parent.scope, path.parent.get('id'));
} else if (
t.AssignmentExpression.check(path.parent.node) &&
path.parent.node.right === path.node &&
t.Identifier.check(path.parent.node.left)
) {
methodPaths = findAssignedMethods(path.parent.scope, path.parent.get('left'));
} else if (t.FunctionDeclaration.check(path.node)) {
methodPaths = findAssignedMethods(path.parent.scope, path.get('id'));
}
documentation.set('methods', methodPaths.map(getMethodDocumentation).filter(Boolean));
}

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import { namedTypes as t } from 'ast-types';
import getTSType from '../utils/getTSType';
const { unwrapUtilityType } = require('react-docgen/dist/utils/flowUtilityTypes');
const { getFlowType, getPropertyName, resolveToValue } = require('react-docgen').utils;
const setPropDescription = require('react-docgen/dist/utils/setPropDescription').default;
import getFlowTypeFromReactComponent from '../utils/getFlowTypeFromReactComponent';
import { applyToFlowTypeProperties } from '../utils/getFlowTypeFromReactComponent';
function setPropDescriptor(documentation: any, path: any, typeParams: any) {
if (t.ObjectTypeSpreadProperty.check(path.node)) {
const argument = unwrapUtilityType(path.get('argument'));
if (t.ObjectTypeAnnotation.check(argument.node)) {
applyToFlowTypeProperties(
documentation,
argument,
(propertyPath: any, innerTypeParams: any) => {
setPropDescriptor(documentation, propertyPath, innerTypeParams);
},
typeParams,
);
return;
}
const name = argument.get('id').get('name');
const resolvedPath = resolveToValue(name);
if (resolvedPath && t.TypeAlias.check(resolvedPath.node)) {
const right = resolvedPath.get('right');
applyToFlowTypeProperties(
documentation,
right,
(propertyPath: any, innerTypeParams: any) => {
setPropDescriptor(documentation, propertyPath, innerTypeParams);
},
typeParams,
);
} else {
documentation.addComposes(name.node.name);
}
} else if (t.ObjectTypeProperty.check(path.node)) {
const type = getFlowType(path.get('value'), typeParams);
const propName = getPropertyName(path);
if (!propName) return;
const propDescriptor = documentation.getPropDescriptor(propName);
propDescriptor.required = !path.node.optional;
propDescriptor.flowType = type;
// We are doing this here instead of in a different handler
// to not need to duplicate the logic for checking for
// imported types that are spread in to props.
setPropDescription(documentation, path);
} else if (t.TSPropertySignature.check(path.node)) {
const type = getTSType(path.get('typeAnnotation'), typeParams);
const propName = getPropertyName(path);
if (!propName) return;
const propDescriptor = documentation.getPropDescriptor(propName);
propDescriptor.required = !path.node.optional;
propDescriptor.tsType = type;
// We are doing this here instead of in a different handler
// to not need to duplicate the logic for checking for
// imported types that are spread in to props.
setPropDescription(documentation, path);
}
}
/**
* This handler tries to find flow Type annotated react components and extract
* its types to the documentation. It also extracts docblock comments which are
* inlined in the type definition.
*/
export default function flowTypeHandler(documentation: any, path: any) {
const flowTypesPath = getFlowTypeFromReactComponent(path);
if (!flowTypesPath) {
return;
}
applyToFlowTypeProperties(documentation, flowTypesPath, (propertyPath: any, typeParams: any) => {
setPropDescriptor(documentation, propertyPath, typeParams);
});
}

View File

@ -1,23 +1,23 @@
import {
propTypeHandler,
contextTypeHandler,
childContextTypeHandler,
} from './propTypeHandler';
import { propTypeHandler, contextTypeHandler, childContextTypeHandler } from './propTypeHandler';
import defaultPropsHandler from './defaultPropsHandler';
import flowTypeHandler from './flowTypeHandler';
import componentMethodsHandler from './componentMethodsHandler';
import preProcessHandler from './preProcessHandler';
const { handlers } = require('react-docgen');
const defaultHandlers = [
preProcessHandler,
propTypeHandler,
contextTypeHandler,
childContextTypeHandler,
handlers.propTypeCompositionHandler,
handlers.propDocBlockHandler,
handlers.flowTypeHandler,
flowTypeHandler,
defaultPropsHandler,
handlers.componentDocblockHandler,
handlers.displayNameHandler,
handlers.componentMethodsHandler,
componentMethodsHandler,
handlers.componentMethodsJsDocHandler,
];

View File

@ -0,0 +1,3 @@
export default function preProcessHandler(documentation: any, path: any) {
documentation.set('meta', path.__meta);
}

View File

@ -14,6 +14,9 @@ export default function parse(params: { fileContent: string; filePath: string })
return resolver(ast);
},
handlers,
{
filename: filePath,
},
);
const coms = result.reduce((res: any[], info: any) => {
if (!info || !info.props) return res;
@ -29,6 +32,7 @@ export default function parse(params: { fileContent: string; filePath: string })
res.push({
componentName: info.displayName,
props,
meta: info.meta || {},
});
return res;
}, []);

View File

@ -1,5 +1,6 @@
export default function checkIsIIFE(path: any) {
return (
path.value &&
path.value.callee &&
path.value.callee.type === 'FunctionExpression' &&
path.node.type === 'CallExpression'

View File

@ -0,0 +1,54 @@
import { namedTypes as t } from 'ast-types';
const { match, resolveToValue } = require('react-docgen').utils;
const { traverseShallow } = require('react-docgen/dist/utils/traverse');
import isReactComponentStaticMember from './isReactComponentStaticMember';
import getRoot from '../utils/getRoot';
function findAssignedMethods(scope: any, idPath: any) {
const results: any[] = [];
if (!t.Identifier.check(idPath.node)) {
return results;
}
const name = idPath.node.name;
const idScope = idPath.scope.lookup(idPath.node.name);
traverseShallow(scope.path, {
visitAssignmentExpression: function(path: any) {
const node = path.node;
if (
match(node.left, {
type: 'MemberExpression',
object: { type: 'Identifier', name },
})
// && path.scope.lookup(name) === idScope
) {
results.push(path);
return false;
}
return this.traverse(path);
},
});
return results.filter((x) => !isReactComponentStaticMember(x.get('left')));
}
export default findAssignedMethods;
// const findAssignedMethodsFromScopes = (scope: any, idPath: any) => {
// const rootNode = getRoot(idPath);
// let { __scope: scopes = [] } = rootNode;
// if (!scopes.find((x: any) => x.scope === scope && x.idPath === idPath)) {
// scopes = [
// ...scopes,
// {
// scope,
// idPath,
// },
// ];
// }
// return scopes.map(({ scope: s, idPath: id }: any) => findAssignedMethods(s, id)).flatMap((x: any) => x);
// };
// export { findAssignedMethodsFromScopes };

View File

@ -7,11 +7,19 @@
*/
import { namedTypes as t, visit } from 'ast-types';
import { uniqBy } from 'lodash';
import checkIsIIFE from './checkIsIIFE';
import resolveHOC from './resolveHOC';
import resolveIIFE from './resolveIIFE';
import resolveImport from './resolveImport';
import resolveImport, { isImportLike } from './resolveImport';
import resolveTranspiledClass from './resolveTranspiledClass';
import isStaticMethod from './isStaticMethod';
import findAssignedMethods from './findAssignedMethods';
import resolveExportDeclaration from './resolveExportDeclaration';
import makeProxy from '../utils/makeProxy';
const expressionTo = require('react-docgen/dist/utils/expressionTo');
import { get, set, has, ICache } from '../utils/cache';
import getName from '../utils/getName';
const {
isExportsOrModuleAssignment,
@ -20,8 +28,8 @@ const {
isReactForwardRefCall,
isStatelessComponent,
normalizeClassDefinition,
resolveExportDeclaration,
resolveToValue,
getMemberValuePath,
} = require('react-docgen').utils;
function ignore() {
@ -47,16 +55,14 @@ function resolveDefinition(definition: any) {
} else if (isReactComponentClass(definition)) {
normalizeClassDefinition(definition);
return definition;
} else if (
isStatelessComponent(definition) ||
isReactForwardRefCall(definition)
) {
} else if (isStatelessComponent(definition) || isReactForwardRefCall(definition)) {
return definition;
}
return null;
}
function getDefinition(definition: any): any {
function getDefinition(definition: any, cache: ICache = {}): any {
const { __meta: exportMeta = {} } = definition;
if (checkIsIIFE(definition)) {
definition = resolveToValue(resolveIIFE(definition));
if (!isComponentDefinition(definition)) {
@ -64,24 +70,195 @@ function getDefinition(definition: any): any {
}
} else {
definition = resolveToValue(resolveHOC(definition));
if (isComponentDefinition(definition)) {
definition = makeProxy(definition, {
__meta: exportMeta,
});
return definition;
}
if (checkIsIIFE(definition)) {
definition = resolveToValue(resolveIIFE(definition));
if (!isComponentDefinition(definition)) {
definition = resolveTranspiledClass(definition);
}
} else if (t.SequenceExpression.check(definition.node)) {
return getDefinition(
resolveToValue(definition.get('expressions').get(0)),
);
return getDefinition(resolveToValue(definition.get('expressions').get(0)), cache);
} else {
definition = resolveImport(
definition,
findAllExportedComponentDefinition,
);
return resolveImport(definition, (ast: any, sourcePath: string) => {
const importMeta: any[] = [];
if (t.ImportDeclaration.check(definition.node)) {
// @ts-ignore
const specifiers = definition.get('specifiers');
specifiers.each((spec: any) => {
const { node } = spec;
importMeta.push({
localName: node.local.name,
importedName: node.imported ? node.imported.name : 'default',
});
});
}
let result;
if (has('ast-export', ast.__path)) {
result = get('ast-export', ast.__path);
} else {
result = findAllExportedComponentDefinition(ast);
set('ast-export', ast.__path, result);
}
const exportList: any[] = [];
const importList: any[] = [];
result = result.forEach((def: any) => {
let { __meta: meta = {} } = def;
let exportName = meta.exportName;
for (let item of importMeta) {
if (exportName === item.importedName) {
exportName = item.localName;
break;
}
}
if (exportName) {
importList.push(makeProxy(def, { __meta: { exportName } }));
}
const nextMeta: any = {
exportName,
};
if (exportName === exportMeta.localName) {
nextMeta.exportName = exportMeta.exportName;
} else {
return;
}
if (exportMeta.subName) {
nextMeta.subName = exportMeta.subName;
} else if (meta.subName) {
nextMeta.subName = meta.subName;
}
exportList.push(makeProxy(def, { __meta: nextMeta }));
});
cache[sourcePath] = importList;
// result = result.filter((x) => !x.__shouldDelete);
return exportList;
});
}
}
if (definition && !definition.__meta) {
definition.__meta = exportMeta;
}
return definition;
}
export interface IMethodsPath {
subName: string;
localName: string;
value: any;
}
/**
* Extract all flow types for the methods of a react component. Doesn't
* return any react specific lifecycle methods.
*/
function getSubComponents(path: any, scope: any, cache: ICache) {
// Extract all methods from the class or object.
let methodPaths = [];
if (isReactComponentClass(path)) {
methodPaths = path.get('body', 'body').filter(isStaticMethod);
methodPaths = [...methodPaths, ...findAssignedMethods(scope || path.scope, path.get('id'))];
} else if (t.ObjectExpression.check(path.node)) {
methodPaths = path.get('properties').filter(isStaticMethod);
methodPaths = [...methodPaths, ...findAssignedMethods(scope || path.scope, path.get('id'))];
// Add the statics object properties.
const statics = getMemberValuePath(path, 'statics');
if (statics) {
statics.get('properties').each((p: any) => {
if (isStaticMethod(p)) {
p.node.static = true;
methodPaths.push(p);
}
});
}
} else if (
t.VariableDeclarator.check(path.parent.node) &&
path.parent.node.init === path.node &&
t.Identifier.check(path.parent.node.id)
) {
methodPaths = findAssignedMethods(scope || path.parent.scope, path.parent.get('id'));
} else if (
t.AssignmentExpression.check(path.parent.node) &&
path.parent.node.right === path.node &&
t.Identifier.check(path.parent.node.left)
) {
methodPaths = findAssignedMethods(scope || path.parent.scope, path.parent.get('left'));
} else if (t.FunctionDeclaration.check(path.node)) {
methodPaths = findAssignedMethods(scope || path.parent.scope, path.get('id'));
} else if (t.ArrowFunctionExpression.check(path.node)) {
methodPaths = findAssignedMethods(scope || path.parent.scope, path.parent.get('id'));
}
return (
methodPaths
.map((x: any) => {
if (t.ClassProperty.check(x.node)) {
return {
value: x.get('value'),
subName: x.node.key.name,
localName: getName(x.get('value')),
};
}
return {
value: x,
subName: x.node.left.property.name,
localName: getName(x.get('right')),
};
})
.map(({ subName, localName, value }: IMethodsPath) => ({
subName,
localName,
value: resolveToValue(value),
}))
.map(({ subName, localName, value }: IMethodsPath) => {
let def = getDefinition(
makeProxy(value, {
__meta: {
localName,
subName,
exportName: path.__meta && path.__meta.exportName,
},
}),
cache,
);
if (!Array.isArray(def)) {
def = [def];
}
return {
subName,
localName,
value: def.flatMap((x: any) => x).filter((x: any) => isComponentDefinition(x)),
};
})
.map(({ subName, localName, value }: IMethodsPath) =>
value.map((x: any) => ({
subName,
localName,
value: x,
})),
)
// @ts-ignore
.flatMap((x: any) => x)
.map(({ subName, localName, value }: IMethodsPath) => {
const __meta = {
subName: subName,
exportName: path.__meta && path.__meta.exportName,
};
return makeProxy(value, { __meta });
})
);
}
/**
* Given an AST, this function tries to find the exported component definition.
*
@ -99,6 +276,8 @@ function getDefinition(definition: any): any {
*/
export default function findAllExportedComponentDefinition(ast: any) {
const components: any[] = [];
const cache: ICache = {};
let programScope: any;
function exportDeclaration(path: any) {
const definitions = resolveExportDeclaration(path)
@ -106,7 +285,7 @@ export default function findAllExportedComponentDefinition(ast: any) {
if (isComponentDefinition(definition)) {
acc.push(definition);
} else {
definition = getDefinition(definition);
definition = getDefinition(definition, cache);
if (!Array.isArray(definition)) {
definition = [definition];
}
@ -118,7 +297,11 @@ export default function findAllExportedComponentDefinition(ast: any) {
}
return acc;
}, [])
.map((definition: any) => resolveDefinition(definition));
.map((definition: any) => {
const { __meta: meta } = definition;
const def = resolveDefinition(definition);
return makeProxy(def, { __meta: meta });
});
if (definitions.length === 0) {
return false;
@ -132,6 +315,10 @@ export default function findAllExportedComponentDefinition(ast: any) {
}
visit(ast, {
visitProgram: function(path) {
programScope = path.scope;
return this.traverse(path);
},
visitFunctionDeclaration: ignore,
visitFunctionExpression: ignore,
visitClassDeclaration: ignore,
@ -149,9 +336,7 @@ export default function findAllExportedComponentDefinition(ast: any) {
visitExportNamedDeclaration: exportDeclaration,
visitExportDefaultDeclaration: exportDeclaration,
visitExportAllDeclaration: function(path) {
components.push(
...resolveImport(path, findAllExportedComponentDefinition),
);
components.push(...resolveImport(path, findAllExportedComponentDefinition));
return false;
},
@ -161,20 +346,44 @@ export default function findAllExportedComponentDefinition(ast: any) {
if (!isExportsOrModuleAssignment(path)) {
return false;
}
const arr = expressionTo.Array(path.get('left'));
const meta: any = {
exportName: arr[1] === 'exports' ? 'default' : arr[1],
};
// Resolve the value of the right hand side. It should resolve to a call
// expression, something like React.createClass
path = resolveToValue(path.get('right'));
if (!isComponentDefinition(path)) {
path = getDefinition(path);
path = getDefinition(path, cache);
}
const definition = resolveDefinition(path);
if (definition && components.indexOf(definition) === -1) {
components.push(definition);
let definitions = resolveDefinition(path);
if (!Array.isArray(definitions)) {
definitions = [definitions];
}
definitions.forEach((definition: any) => {
if (definition && components.indexOf(definition) === -1) {
// if (definition.__meta) {
definition = makeProxy(definition, {
__meta: meta,
});
// }
components.push(definition);
}
});
return false;
},
});
return components;
const result = components.reduce((acc, item) => {
let subModuleDefinitions = [];
subModuleDefinitions = getSubComponents(item, programScope, cache);
return [...acc, item, ...subModuleDefinitions];
}, []);
const res = uniqBy(result, (x: any) => {
return `${x.__meta.exportName}/${x.__meta.subName}`;
});
return res;
}

View File

@ -0,0 +1,13 @@
import { namedTypes as t } from 'ast-types';
const { getPropertyName } = require('react-docgen').utils;
const reactStaticMembers = ['propTypes', 'defaultProps', 'contextTypes'];
export default function isReactComponentStaticMember(methodPath: any) {
let name;
if (t.MemberExpression.check(methodPath.node)) {
name = methodPath.node.property.name;
} else {
name = getPropertyName(methodPath);
}
return !!name && reactStaticMembers.indexOf(name) !== -1;
}

View File

@ -0,0 +1,14 @@
import { namedTypes as t } from 'ast-types';
import isReactComponentStaticMember from './isReactComponentStaticMember';
const { isReactComponentMethod } = require('react-docgen').utils;
/**
* judge if static method
*/
function isStaticMethod(path: any) {
const isProbablyStaticMethod = t.ClassProperty.check(path.node) && path.node.static === true;
return isProbablyStaticMethod && !isReactComponentStaticMember(path) && !isReactComponentMethod(path);
}
export default isStaticMethod;

View File

@ -0,0 +1,53 @@
import { namedTypes as t } from 'ast-types';
import makeProxy from '../utils/makeProxy';
import getName from '../utils/getName';
export default function resolveExportDeclaration(path: any) {
const definitions = [];
if (path.node.default || t.ExportDefaultDeclaration.check(path.node)) {
const def = path.get('declaration');
const meta: { [name: string]: string } = {
exportName: 'default',
localName: getName(def),
};
definitions.push(makeProxy(def, { __meta: meta }));
} else if (path.node.declaration) {
if (t.VariableDeclaration.check(path.node.declaration)) {
path.get('declaration', 'declarations').each((declarator: any) => {
definitions.push(
makeProxy(declarator, {
__meta: {
exportName: declarator.get('id').node.name,
},
}),
);
});
} else {
const def = path.get('declaration');
definitions.push(
makeProxy(def, {
__meta: {
exportName: 'default',
},
}),
);
}
} else if (path.node.specifiers) {
path.get('specifiers').each((specifier: any) => {
const def = specifier.node.id ? specifier.get('id') : specifier.get('local');
const exportName = specifier.get('exported').node.name;
const localName = def.get('local').node.name;
definitions.push(
makeProxy(def, {
__meta: {
exportName: exportName,
localName: localName,
},
}),
);
});
}
return definitions;
}

View File

@ -1,21 +1,10 @@
import { namedTypes as t } from 'ast-types';
import fs from 'fs';
import p from 'path';
import getRoot from '../utils/getRoot';
function getRoot(node: any) {
let root = node.parent;
while (root.parent) {
root = root.parent;
}
return root.node;
}
function isImportLike(node: any) {
return (
t.ImportDeclaration.check(node) ||
t.ExportAllDeclaration.check(node) ||
t.ExportNamedDeclaration.check(node)
);
export function isImportLike(node: any) {
return t.ImportDeclaration.check(node) || t.ExportAllDeclaration.check(node) || t.ExportNamedDeclaration.check(node);
}
function getPath(path: any, name: any) {
@ -27,7 +16,7 @@ function getPath(path: any, name: any) {
if (fs.existsSync(p.resolve(__path, name))) {
name = name + '/index';
}
const suffix = suffixes.find(suf => {
const suffix = suffixes.find((suf) => {
return fs.existsSync(p.resolve(__path, name + suf));
});
if (!suffix) return;
@ -35,9 +24,12 @@ function getPath(path: any, name: any) {
}
const buildParser = require('react-docgen/dist/babelParser').default;
const parser = buildParser();
const suffixes = ['.js', '.jsx', '.ts', '.tsx'];
const cache: {
[name: string]: any;
} = {};
export default function resolveImport(path: any, callback: any) {
let name;
if (path.name === 'local') {
@ -50,11 +42,19 @@ export default function resolveImport(path: any, callback: any) {
if (name) {
const __path = getPath(path, name);
if (!__path) return path;
const fileContent = fs.readFileSync(__path, 'utf8');
const ast = parser.parse(fileContent);
ast.__src = fileContent;
ast.__path = __path;
return callback(ast);
let ast;
if (!cache[__path]) {
const fileContent = fs.readFileSync(__path, 'utf8');
const parser = buildParser({ filename: __path });
ast = parser.parse(fileContent);
ast.__src = fileContent;
ast.__path = __path;
cache[__path] = ast;
} else {
ast = cache[__path];
}
return callback(ast, __path);
}
return path;
}

View File

@ -1,9 +1,9 @@
export function transformType(type: any) {
if (typeof type === 'string') return type;
const { name, elements, value = elements, computed, required } = type;
if (!value && !required) {
return name;
}
export function transformType(itemType: any) {
if (typeof itemType === 'string') return itemType;
const { name, elements, value = elements, computed, required, type } = itemType;
// if (!value && !required && !type) {
// return name;
// }
if (computed !== undefined && value) {
return eval(value);
}
@ -21,6 +21,7 @@ export function transformType(type: any) {
case 'func':
case 'symbol':
case 'object':
case 'null':
break;
case 'literal':
return eval(value);
@ -36,13 +37,24 @@ export function transformType(type: any) {
case 'boolean':
result.type = 'bool';
break;
case 'Array': {
case 'Function':
result.type = 'func';
break;
case 'unknown':
result.type = 'any';
break;
case 'Array':
case 'arrayOf': {
result.type = 'arrayOf';
const v = transformType(value[0]);
if (typeof v.type === 'string') result.value = v.type;
break;
}
case 'signature': {
if (typeof type === 'string') {
result.type = type;
break;
}
result.type = 'shape';
const {
signature: { properties },
@ -103,22 +115,28 @@ export function transformType(type: any) {
result.value = name;
break;
}
if (Object.keys(result).length === 1) {
return result.type;
}
return result;
}
export function transformItem(name: string, item: any) {
const { description, flowType, type = flowType, required, defaultValue } = item;
const { description, flowType, tsType, type = tsType || flowType, required, defaultValue } = item;
const result: any = {
name,
propType: transformType({
};
if (type) {
result.propType = transformType({
...type,
required: !!required,
}),
};
});
}
if (description) {
result.description = description;
}
if (defaultValue) {
if (defaultValue !== undefined) {
try {
const value = eval(defaultValue.value);
result.defaultValue = value;
@ -127,6 +145,5 @@ export function transformItem(name: string, item: any) {
if (result.propType === undefined) {
delete result.propType;
}
return result;
}

View File

@ -0,0 +1,18 @@
export interface ICache {
[name: string]: any;
}
const cache: ICache = {};
export function set(scope: string, name: string, value: any) {
cache[scope] = cache[scope] || {};
cache[scope][name] = value;
}
export function get(scope: string, name: string) {
return (cache[scope] || {})[name];
}
export function has(scope: string, name: string) {
return cache[scope] && cache[scope].hasOwnProperty(name);
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { namedTypes as t } from 'ast-types';
const supportedUtilityTypes = new Set(['$Exact', '$ReadOnly']);
/**
* See `supportedUtilityTypes` for which types are supported and
* https://flow.org/en/docs/types/utilities/ for which types are available.
*/
function isSupportedUtilityType(path: any) {
if (t.GenericTypeAnnotation.check(path.node)) {
const idPath = path.get('id');
return !!idPath && supportedUtilityTypes.has(idPath.node.name);
}
return false;
}
export { isSupportedUtilityType };
function isReactUtilityType(path: any) {
if (t.TSTypeReference.check(path.node)) {
const objName = path.get('typeName', 'left').node.name;
if (objName === 'React') {
return true;
}
}
return false;
}
/**
* Unwraps well known utility types. For example:
*
* $ReadOnly<T> => T
*/
function unwrapUtilityType(path: any) {
while (isSupportedUtilityType(path) || isReactUtilityType(path)) {
path = path.get('typeParameters', 'params', 0);
}
return path;
}
export { unwrapUtilityType };

View File

@ -0,0 +1,119 @@
import { namedTypes as t } from 'ast-types';
const {
isReactComponentClass,
isReactForwardRefCall,
getTypeAnnotation,
resolveToValue,
getMemberValuePath,
} = require('react-docgen').utils;
import resolveGenericTypeAnnotation from './resolveGenericTypeAnnotation';
const getTypeParameters = require('react-docgen/dist/utils/getTypeParameters').default;
function getStatelessPropsPath(componentDefinition: any) {
const value = resolveToValue(componentDefinition);
if (isReactForwardRefCall(value)) {
const inner = resolveToValue(value.get('arguments', 0));
return inner.get('params', 0);
}
if (t.VariableDeclarator.check(componentDefinition.parent.node)) {
const id = componentDefinition.parent.get('id');
if (id.node.typeAnnotation) {
return id;
}
}
return value.get('params', 0);
}
/**
* Given an React component (stateless or class) tries to find the
* flow type for the props. If not found or not one of the supported
* component types returns null.
*/
export default (path: any) => {
let typePath = null;
if (isReactComponentClass(path)) {
const superTypes = path.get('superTypeParameters');
if (superTypes.value) {
const params = superTypes.get('params');
if (params.value.length === 3) {
typePath = params.get(1);
} else {
typePath = params.get(0);
}
} else {
const propsMemberPath = getMemberValuePath(path, 'props');
if (!propsMemberPath) {
return null;
}
typePath = getTypeAnnotation(propsMemberPath.parentPath);
}
return typePath;
}
const propsParam = getStatelessPropsPath(path);
if (propsParam) {
typePath = getTypeAnnotation(propsParam);
}
return typePath;
};
function applyToFlowTypeProperties(documentation: any, path: any, callback: any, typeParams?: any) {
if (path.node.properties) {
path.get('properties').each((propertyPath: any) => callback(propertyPath, typeParams));
} else if (path.node.members) {
path.get('members').each((propertyPath: any) => callback(propertyPath, typeParams));
} else if (path.node.type === 'InterfaceDeclaration') {
if (path.node.extends) {
applyExtends(documentation, path, callback, typeParams);
}
path.get('body', 'properties').each((propertyPath: any) => callback(propertyPath, typeParams));
} else if (path.node.type === 'TSInterfaceDeclaration') {
if (path.node.extends) {
applyExtends(documentation, path, callback, typeParams);
}
path.get('body', 'body').each((propertyPath: any) => callback(propertyPath, typeParams));
} else if (path.node.type === 'IntersectionTypeAnnotation' || path.node.type === 'TSIntersectionType') {
path
.get('types')
.each((typesPath: any) => applyToFlowTypeProperties(documentation, typesPath, callback, typeParams));
} else if (path.node.type !== 'UnionTypeAnnotation') {
// The react-docgen output format does not currently allow
// for the expression of union types
const typePath = resolveGenericTypeAnnotation(path);
if (typePath) {
applyToFlowTypeProperties(documentation, typePath, callback, typeParams);
}
}
}
function applyExtends(documentation: any, path: any, callback: any, typeParams: any) {
path.get('extends').each((extendsPath: any) => {
const resolvedPath = resolveGenericTypeAnnotation(extendsPath);
if (resolvedPath) {
if (resolvedPath.node.typeParameters && extendsPath.node.typeParameters) {
typeParams = getTypeParameters(
resolvedPath.get('typeParameters'),
extendsPath.get('typeParameters'),
typeParams,
);
}
applyToFlowTypeProperties(documentation, resolvedPath, callback, typeParams);
} else {
const id = extendsPath.node.id || extendsPath.node.typeName || extendsPath.node.expression;
if (id && id.type === 'Identifier') {
documentation.addComposes(id.name);
}
}
});
}
export { applyToFlowTypeProperties };

View File

@ -0,0 +1,164 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import getTSType from './getTSType';
const { namedTypes: t } = require('ast-types');
const {
resolveToValue,
getFlowType,
getParameterName,
getPropertyName,
getTypeAnnotation,
} = require('react-docgen').utils;
const { getDocblock } = require('react-docgen/dist/utils/docblock');
function getMethodFunctionExpression(methodPath) {
if (t.AssignmentExpression.check(methodPath.node)) {
return resolveToValue(methodPath.get('right'));
}
// Otherwise this is a method/property node
return methodPath.get('value');
}
function getMethodParamsDoc(methodPath) {
const params = [];
const functionExpression = getMethodFunctionExpression(methodPath);
// Extract param flow types.
functionExpression.get('params').each((paramPath) => {
let type = null;
const typePath = getTypeAnnotation(paramPath);
if (typePath && t.Flow.check(typePath.node)) {
type = getFlowType(typePath);
if (t.GenericTypeAnnotation.check(typePath.node)) {
type.alias = typePath.node.id.name;
}
} else if (typePath) {
type = getTSType(typePath);
if (t.TSTypeReference.check(typePath.node)) {
type.alias = typePath.node.typeName.name;
}
}
const param = {
name: getParameterName(paramPath),
optional: paramPath.node.optional,
type,
};
params.push(param);
});
return params;
}
// Extract flow return type.
function getMethodReturnDoc(methodPath) {
const functionExpression = getMethodFunctionExpression(methodPath);
if (functionExpression.node.returnType) {
const returnType = getTypeAnnotation(functionExpression.get('returnType'));
if (returnType && t.Flow.check(returnType.node)) {
return { type: getFlowType(returnType) };
}
if (returnType) {
return { type: getTSType(returnType) };
}
}
return null;
}
function getMethodModifiers(methodPath) {
if (t.AssignmentExpression.check(methodPath.node)) {
return ['static'];
}
// Otherwise this is a method/property node
const modifiers = [];
if (methodPath.node.static) {
modifiers.push('static');
}
if (methodPath.node.kind === 'get' || methodPath.node.kind === 'set') {
modifiers.push(methodPath.node.kind);
}
const functionExpression = methodPath.get('value').node;
if (functionExpression.generator) {
modifiers.push('generator');
}
if (functionExpression.async) {
modifiers.push('async');
}
return modifiers;
}
function getMethodName(methodPath) {
if (t.AssignmentExpression.check(methodPath.node) && t.MemberExpression.check(methodPath.node.left)) {
const { left } = methodPath.node;
const { property } = left;
if (!left.computed) {
return property.name;
}
if (t.Literal.check(property)) {
return String(property.value);
}
return null;
}
return getPropertyName(methodPath);
}
function getMethodAccessibility(methodPath) {
if (t.AssignmentExpression.check(methodPath.node)) {
return null;
}
// Otherwise this is a method/property node
return methodPath.node.accessibility;
}
function getMethodDocblock(methodPath) {
if (t.AssignmentExpression.check(methodPath.node)) {
let path = methodPath;
do {
path = path.parent;
} while (path && !t.ExpressionStatement.check(path.node));
if (path) {
return getDocblock(path);
}
return null;
}
// Otherwise this is a method/property node
return getDocblock(methodPath);
}
// Gets the documentation object for a component method.
// Component methods may be represented as class/object method/property nodes
// or as assignment expresions of the form `Component.foo = function() {}`
export default function getMethodDocumentation(methodPath) {
if (getMethodAccessibility(methodPath) === 'private') {
return null;
}
const name = getMethodName(methodPath);
if (!name) return null;
return {
name,
docblock: getMethodDocblock(methodPath),
modifiers: getMethodModifiers(methodPath),
params: getMethodParamsDoc(methodPath),
returns: getMethodReturnDoc(methodPath),
};
}

View File

@ -0,0 +1,14 @@
import { namedTypes as t } from 'ast-types';
export default function(def: any) {
let name = '';
if (def.node.name) {
name = def.node.name;
// hoc
} else if (t.CallExpression.check(def.node)) {
if (def.node.arguments && def.node.arguments.length && t.Identifier.check(def.get('arguments', 0).node))
name = def.get('arguments', 0).node.name;
}
return name;
}

View File

@ -0,0 +1,7 @@
export default function getRoot(path: any) {
let root = path.parent;
while (root.parent) {
root = root.parent;
}
return root.node;
}

View File

@ -0,0 +1,355 @@
/* eslint-disable */
const { namedTypes: t } = require('ast-types');
const {
getPropertyName,
printValue,
resolveToValue,
getTypeAnnotation,
resolveObjectToNameArray,
getTypeParameters,
} = require('react-docgen').utils;
const tsTypes = {
TSAnyKeyword: 'any',
TSBooleanKeyword: 'boolean',
TSUnknownKeyword: 'unknown',
TSNeverKeyword: 'never',
TSNullKeyword: 'null',
TSUndefinedKeyword: 'undefined',
TSNumberKeyword: 'number',
TSStringKeyword: 'string',
TSSymbolKeyword: 'symbol',
TSThisType: 'this',
TSObjectKeyword: 'object',
TSVoidKeyword: 'void',
};
const namedTypes = {
TSArrayType: handleTSArrayType,
TSTypeReference: handleTSTypeReference,
TSTypeLiteral: handleTSTypeLiteral,
TSInterfaceDeclaration: handleTSInterfaceDeclaration,
TSUnionType: handleTSUnionType,
TSFunctionType: handleTSFunctionType,
TSIntersectionType: handleTSIntersectionType,
TSMappedType: handleTSMappedType,
TSTupleType: handleTSTupleType,
TSTypeQuery: handleTSTypeQuery,
TSTypeOperator: handleTSTypeOperator,
TSIndexedAccessType: handleTSIndexedAccessType,
};
function handleTSArrayType(path, typeParams) {
return {
name: 'Array',
elements: [getTSTypeWithResolvedTypes(path.get('elementType'), typeParams)],
raw: printValue(path),
};
}
function handleTSTypeReference(path, typeParams) {
let type;
if (t.TSQualifiedName.check(path.node.typeName)) {
const typeName = path.get('typeName');
if (typeName.node.left.name === 'React') {
type = {
name: `${typeName.node.left.name}${typeName.node.right.name}`,
raw: printValue(typeName),
};
} else {
type = { name: printValue(typeName).replace(/<.*>$/, '') };
}
} else {
type = { name: path.node.typeName.name };
}
const resolvedPath = (typeParams && typeParams[type.name]) || resolveToValue(path.get('typeName'));
if (path.node.typeParameters && resolvedPath.node.typeParameters) {
typeParams = getTypeParameters(resolvedPath.get('typeParameters'), path.get('typeParameters'), typeParams);
}
if (typeParams && typeParams[type.name]) {
type = getTSTypeWithResolvedTypes(resolvedPath);
}
if (resolvedPath && resolvedPath.node.typeAnnotation) {
type = getTSTypeWithResolvedTypes(resolvedPath.get('typeAnnotation'), typeParams);
} else if (path.node.typeParameters) {
const params = path.get('typeParameters').get('params');
type = {
...type,
elements: params.map((param) => getTSTypeWithResolvedTypes(param, typeParams)),
raw: printValue(path),
};
}
return type;
}
function getTSTypeWithRequirements(path, typeParams) {
const type = getTSTypeWithResolvedTypes(path, typeParams);
type.required = !path.parentPath.node.optional;
return type;
}
function handleTSTypeLiteral(path, typeParams) {
const type = {
name: 'signature',
type: 'object',
raw: printValue(path),
signature: { properties: [] },
};
path.get('members').each((param) => {
if (t.TSPropertySignature.check(param.node) || t.TSMethodSignature.check(param.node)) {
const propName = getPropertyName(param);
if (!propName) {
return;
}
type.signature.properties.push({
key: propName,
value: getTSTypeWithRequirements(param.get('typeAnnotation'), typeParams),
});
} else if (t.TSCallSignatureDeclaration.check(param.node)) {
type.signature.constructor = handleTSFunctionType(param, typeParams);
} else if (t.TSIndexSignature.check(param.node)) {
type.signature.properties.push({
key: getTSTypeWithResolvedTypes(
param
.get('parameters')
.get(0)
.get('typeAnnotation'),
typeParams,
),
value: getTSTypeWithRequirements(param.get('typeAnnotation'), typeParams),
});
}
});
return type;
}
function handleTSInterfaceDeclaration(path) {
// Interfaces are handled like references which would be documented separately,
// rather than inlined like type aliases.
return {
name: path.node.id.name,
};
}
function handleTSUnionType(path, typeParams) {
return {
name: 'union',
raw: printValue(path),
elements: path.get('types').map((subType) => getTSTypeWithResolvedTypes(subType, typeParams)),
};
}
function handleTSIntersectionType(path, typeParams) {
return {
name: 'intersection',
raw: printValue(path),
elements: path.get('types').map((subType) => getTSTypeWithResolvedTypes(subType, typeParams)),
};
}
function handleTSMappedType(path, typeParams) {
const key = getTSTypeWithResolvedTypes(path.get('typeParameter').get('constraint'), typeParams);
key.required = !path.node.optional;
return {
name: 'signature',
type: 'object',
raw: printValue(path),
signature: {
properties: [
{
key,
value: getTSTypeWithResolvedTypes(path.get('typeAnnotation'), typeParams),
},
],
},
};
}
function handleTSFunctionType(path, typeParams) {
const type = {
name: 'signature',
type: 'function',
raw: printValue(path),
signature: {
arguments: [],
return: getTSTypeWithResolvedTypes(path.get('typeAnnotation'), typeParams),
},
};
path.get('parameters').each((param) => {
const typeAnnotation = getTypeAnnotation(param);
const arg = {
name: param.node.name || '',
type: typeAnnotation ? getTSTypeWithResolvedTypes(typeAnnotation, typeParams) : undefined,
};
if (param.node.name === 'this') {
type.signature.this = arg.type;
return;
}
if (param.node.type === 'RestElement') {
arg.name = param.node.argument.name;
arg.rest = true;
}
type.signature.arguments.push(arg);
});
return type;
}
function handleTSTupleType(path, typeParams) {
const type = {
name: 'tuple',
raw: printValue(path),
elements: [],
};
path.get('elementTypes').each((param) => {
type.elements.push(getTSTypeWithResolvedTypes(param, typeParams));
});
return type;
}
function handleTSTypeQuery(path, typeParams) {
const resolvedPath = resolveToValue(path.get('exprName'));
if (resolvedPath && resolvedPath.node.typeAnnotation) {
return getTSTypeWithResolvedTypes(resolvedPath.get('typeAnnotation'), typeParams);
}
return { name: path.node.exprName.name };
}
function handleTSTypeOperator(path) {
if (path.node.operator !== 'keyof') {
return null;
}
let value = path.get('typeAnnotation');
if (t.TSTypeQuery.check(value.node)) {
value = value.get('exprName');
} else if (value.node.id) {
value = value.get('id');
}
const resolvedPath = resolveToValue(value);
if (resolvedPath && (t.ObjectExpression.check(resolvedPath.node) || t.TSTypeLiteral.check(resolvedPath.node))) {
const keys = resolveObjectToNameArray(resolvedPath, true);
if (keys) {
return {
name: 'union',
raw: printValue(path),
elements: keys.map((key) => ({ name: 'literal', value: key })),
};
}
}
}
function handleTSIndexedAccessType(path, typeParams) {
// eslint-disable-next-line no-undef
const objectType = getTSTypeWithResolvedTypes(path.get('objectType'), typeParams);
// eslint-disable-next-line no-undef
const indexType = getTSTypeWithResolvedTypes(path.get('indexType'), typeParams);
// We only get the signature if the objectType is a type (vs interface)
if (!objectType.signature) {
return {
name: `${objectType.name}[${(indexType.value || indexType.name).toString()}]`,
raw: printValue(path),
};
}
const resolvedType = objectType.signature.properties.find(
(p) =>
// indexType.value = "'foo'"
p.key === indexType.value.replace(/['"]+/g, ''),
);
if (!resolvedType) {
return { name: 'unknown' };
}
return {
name: resolvedType.value.name,
raw: printValue(path),
};
}
let visitedTypes = {};
function getTSTypeWithResolvedTypes(path, typeParams) {
if (t.TSTypeAnnotation.check(path.node)) {
path = path.get('typeAnnotation');
}
const { node } = path;
let type;
const isTypeAlias = t.TSTypeAliasDeclaration.check(path.parentPath.node);
// When we see a typealias mark it as visited so that the next
// call of this function does not run into an endless loop
if (isTypeAlias) {
if (visitedTypes[path.parentPath.node.id.name] === true) {
// if we are currently visiting this node then just return the name
// as we are starting to endless loop
return { name: path.parentPath.node.id.name };
}
if (typeof visitedTypes[path.parentPath.node.id.name] === 'object') {
// if we already resolved the type simple return it
return visitedTypes[path.parentPath.node.id.name];
}
// mark the type as visited
visitedTypes[path.parentPath.node.id.name] = true;
}
if (node.type in tsTypes) {
type = { name: tsTypes[node.type] };
} else if (t.TSLiteralType.check(node)) {
type = {
name: 'literal',
value: node.literal.raw || `${node.literal.value}`,
};
} else if (node.type in namedTypes) {
type = namedTypes[node.type](path, typeParams);
}
if (!type) {
type = { name: 'unknown' };
}
if (isTypeAlias) {
// mark the type as unvisited so that further calls can resolve the type again
visitedTypes[path.parentPath.node.id.name] = type;
}
return type;
}
/**
* Tries to identify the typescript type by inspecting the path for known
* typescript type names. This method doesn't check whether the found type is actually
* existing. It simply assumes that a match is always valid.
*
* If there is no match, "unknown" is returned.
*/
export default function getTSType(path, typeParamMap) {
// Empty visited types before an after run
// Before: in case the detection threw and we rerun again
// After: cleanup memory after we are done here
visitedTypes = {};
const type = getTSTypeWithResolvedTypes(path, typeParamMap);
visitedTypes = {};
return type;
}

View File

@ -0,0 +1,19 @@
function makeProxy(target: { [name: string]: any }, meta: any = {}): any {
if (target.__isProxy) {
const value = target.__getRaw();
const rawMeta = target.__getMeta();
return makeProxy(value, Object.assign({}, rawMeta, meta));
}
return new Proxy(target, {
get: (obj, prop: string | number) => {
if (prop === '__isProxy') return true;
if (prop === '__getRaw') return () => target;
if (prop === '__getMeta') return () => meta;
return meta.hasOwnProperty(prop) ? meta[prop] : obj[prop];
// return obj[prop];
},
has: (obj, prop) => obj.hasOwnProperty(prop) || meta.hasOwnProperty(prop),
});
}
export default makeProxy;

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { namedTypes as t } from 'ast-types';
const { resolveToValue } = require('react-docgen').utils;
const { unwrapUtilityType } = require('./flowUtilityTypes');
const isUnreachableFlowType = require('react-docgen/dist/utils/isUnreachableFlowType').default;
function tryResolveGenericTypeAnnotation(path: any): any {
let typePath = unwrapUtilityType(path);
let idPath;
if (typePath.node.id) {
idPath = typePath.get('id');
} else if (t.TSTypeReference.check(typePath.node)) {
idPath = typePath.get('typeName');
} else if (t.TSExpressionWithTypeArguments.check(typePath.node)) {
idPath = typePath.get('expression');
}
if (idPath) {
typePath = resolveToValue(idPath);
if (isUnreachableFlowType(typePath)) {
return;
}
if (t.TypeAlias.check(typePath.node)) {
return tryResolveGenericTypeAnnotation(typePath.get('right'));
} else if (t.TSTypeAliasDeclaration.check(typePath.node)) {
return tryResolveGenericTypeAnnotation(typePath.get('typeAnnotation'));
}
return typePath;
}
return typePath;
}
/**
* Given an React component (stateless or class) tries to find the
* flow type for the props. If not found or not one of the supported
* component types returns undefined.
*/
export default function resolveGenericTypeAnnotation(path: any) {
if (!path) return;
const typePath = tryResolveGenericTypeAnnotation(path);
if (!typePath || typePath === path) return;
return typePath;
}

View File

@ -15,35 +15,8 @@ export interface IMaterialParsedModel {
// filePath: string;
componentName: string;
props?: PropsSection['props'];
// componentNames: {
// exportedName: string;
// localName: string;
// source?: string;
// }[];
// importModules: {
// importDefaultName?: string;
// importName?: string;
// localName?: string;
// source: string;
// }[];
// exportModules: {
// exportedName: string;
// localName: string;
// source?: string;
// }[];
// /**
// * 子模块形如Demo.SubModule = value; 或者 Demo.SubModule.Sub = subValue;
// */
// subModules: {
// objectName: string[];
// propertyName: string;
// value?: string;
// // value 是否对应匿名函数
// isValueAnonymousFunc: boolean;
// }[];
// propsTypes: IPropTypes;
// propsDefaults: {
// name: string;
// defaultValue: any;
// }[];
meta?: {
exportName?: string;
subName?: string;
};
}

View File

@ -0,0 +1,14 @@
import { NodePath, Path } from 'ast-types';
export interface IFileMeta {
src: string;
path: string;
exports: IDefinitionMeta[];
}
export interface IDefinitionMeta {
subDefinitions: IDefinitionMeta[];
nodePath: typeof Path;
exportName: string;
id: string;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`antd exports modules correctly 1`] = `
Array [
"Affix",
"Anchor",
"AutoComplete",
"Alert",
"Avatar",
"BackTop",
"Badge",
"Breadcrumb",
"Button",
"Calendar",
"Card",
"Collapse",
"Carousel",
"Cascader",
"Checkbox",
"Col",
"Comment",
"ConfigProvider",
"DatePicker",
"Descriptions",
"Divider",
"Dropdown",
"Drawer",
"Empty",
"Form",
"Grid",
"Input",
"InputNumber",
"Layout",
"List",
"message",
"Menu",
"Mentions",
"Modal",
"Statistic",
"notification",
"PageHeader",
"Pagination",
"Popconfirm",
"Popover",
"Progress",
"Radio",
"Rate",
"Result",
"Row",
"Select",
"Skeleton",
"Slider",
"Space",
"Spin",
"Steps",
"Switch",
"Table",
"Transfer",
"Tree",
"TreeSelect",
"Tabs",
"Tag",
"TimePicker",
"Timeline",
"Tooltip",
"Typography",
"Upload",
"version",
]
`;

View File

@ -0,0 +1,21 @@
const OLD_NODE_ENV = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const antd = require('..');
describe('antd', () => {
afterAll(() => {
process.env.NODE_ENV = OLD_NODE_ENV;
});
it('exports modules correctly', () => {
expect(Object.keys(antd)).toMatchSnapshot();
});
it('should hint when import all components in dev mode', () => {
expect(warnSpy).toHaveBeenCalledWith(
'You are using a whole package of antd, please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size.',
);
warnSpy.mockRestore();
});
});

View File

@ -0,0 +1,71 @@
const __NULL__ = { notExist: true };
type ElementType<P> = {
prototype: P;
};
export function spyElementPrototypes<P extends {}>(Element: ElementType<P>, properties: P) {
const propNames = Object.keys(properties);
const originDescriptors = {};
propNames.forEach(propName => {
const originDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, propName);
originDescriptors[propName] = originDescriptor || __NULL__;
const spyProp = properties[propName];
if (typeof spyProp === 'function') {
// If is a function
Element.prototype[propName] = function spyFunc(...args) {
return spyProp.call(this, originDescriptor, ...args);
};
} else {
// Otherwise tread as a property
Object.defineProperty(Element.prototype, propName, {
...spyProp,
set(value) {
if (spyProp.set) {
return spyProp.set.call(this, originDescriptor, value);
}
return originDescriptor.set(value);
},
get() {
if (spyProp.get) {
return spyProp.get.call(this, originDescriptor);
}
return originDescriptor.get();
},
});
}
});
return {
mockRestore() {
propNames.forEach(propName => {
const originDescriptor = originDescriptors[propName];
if (originDescriptor === __NULL__) {
delete Element.prototype[propName];
} else if (typeof originDescriptor === 'function') {
Element.prototype[propName] = originDescriptor;
} else {
Object.defineProperty(Element.prototype, propName, originDescriptor);
}
});
},
};
}
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T] &
string;
export function spyElementPrototype<P extends {}, K extends FunctionPropertyNames<P>>(
Element: ElementType<P>,
propName: K,
property: P[K],
) {
return spyElementPrototypes(Element, {
[propName]: property,
});
}

View File

@ -0,0 +1,13 @@
import { easeInOutCubic } from '../easings';
describe('Test easings', () => {
it('easeInOutCubic return value', () => {
const nums = [];
// eslint-disable-next-line no-plusplus
for (let index = 0; index < 5; index++) {
nums.push(easeInOutCubic(index, 1, 5, 4));
}
expect(nums).toEqual([1, 1.25, 3, 4.75, 5]);
});
});

View File

@ -0,0 +1,11 @@
/**
* @jest-environment node
*/
import getScroll from '../getScroll';
describe('getScroll', () => {
it('getScroll return 0 in node envioronment', async () => {
expect(getScroll(null, true)).toBe(0);
expect(getScroll(null, false)).toBe(0);
});
});

View File

@ -0,0 +1,49 @@
import scrollTo from '../scrollTo';
import { sleep } from '../../../tests/utils';
describe('Test ScrollTo function', () => {
let dateNowMock;
beforeEach(() => {
dateNowMock = jest
.spyOn(Date, 'now')
.mockImplementationOnce(() => 0)
.mockImplementationOnce(() => 1000);
});
afterEach(() => {
dateNowMock.mockRestore();
});
it('test scrollTo', async () => {
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((x, y) => {
window.scrollY = y;
window.pageYOffset = y;
});
scrollTo(1000);
await sleep(20);
expect(window.pageYOffset).toBe(1000);
scrollToSpy.mockRestore();
});
it('test callback - option', async () => {
const cbMock = jest.fn();
scrollTo(1000, {
callback: cbMock,
});
await sleep(20);
expect(cbMock).toHaveBeenCalledTimes(1);
});
it('test getContainer - option', async () => {
const div = document.createElement('div');
scrollTo(1000, {
getContainer: () => div,
});
await sleep(20);
expect(div.scrollTop).toBe(1000);
});
});

View File

@ -0,0 +1,8 @@
import UnreachableException from '../unreachableException';
describe('UnreachableException', () => {
it('error thrown matches snapshot', () => {
const exception = new UnreachableException('some value');
expect(exception.message).toMatchInlineSnapshot(`"unreachable case: \\"some value\\""`);
});
});

View File

@ -0,0 +1,206 @@
import raf from 'raf';
import React from 'react';
import { mount } from 'enzyme';
import KeyCode from 'rc-util/lib/KeyCode';
import delayRaf from '../raf';
import throttleByAnimationFrame from '../throttleByAnimationFrame';
import getDataOrAriaProps from '../getDataOrAriaProps';
import Wave from '../wave';
import TransButton from '../transButton';
import openAnimation from '../openAnimation';
import { sleep } from '../../../tests/utils';
import focusTest from '../../../tests/shared/focusTest';
describe('Test utils function', () => {
focusTest(TransButton);
it('throttle function should work', async () => {
const callback = jest.fn();
const throttled = throttleByAnimationFrame(callback);
expect(callback).not.toHaveBeenCalled();
throttled();
throttled();
await sleep(20);
expect(callback).toHaveBeenCalled();
expect(callback.mock.calls.length).toBe(1);
});
it('throttle function should be canceled', async () => {
const callback = jest.fn();
const throttled = throttleByAnimationFrame(callback);
throttled();
throttled.cancel();
await sleep(20);
expect(callback).not.toHaveBeenCalled();
});
describe('getDataOrAriaProps', () => {
it('returns all data-* properties from an object', () => {
const props = {
onClick: () => {},
isOpen: true,
'data-test': 'test-id',
'data-id': 1234,
};
const results = getDataOrAriaProps(props);
expect(results).toEqual({
'data-test': 'test-id',
'data-id': 1234,
});
});
it('does not return data-__ properties from an object', () => {
const props = {
onClick: () => {},
isOpen: true,
'data-__test': 'test-id',
'data-__id': 1234,
};
const results = getDataOrAriaProps(props);
expect(results).toEqual({});
});
it('returns all aria-* properties from an object', () => {
const props = {
onClick: () => {},
isOpen: true,
'aria-labelledby': 'label-id',
'aria-label': 'some-label',
};
const results = getDataOrAriaProps(props);
expect(results).toEqual({
'aria-labelledby': 'label-id',
'aria-label': 'some-label',
});
});
it('returns role property from an object', () => {
const props = {
onClick: () => {},
isOpen: true,
role: 'search',
};
const results = getDataOrAriaProps(props);
expect(results).toEqual({ role: 'search' });
});
});
it('delayRaf', done => {
jest.useRealTimers();
let bamboo = false;
delayRaf(() => {
bamboo = true;
}, 3);
// Do nothing, but insert in the frame
// https://github.com/ant-design/ant-design/issues/16290
delayRaf(() => {}, 3);
// Variable bamboo should be false in frame 2 but true in frame 4
raf(() => {
expect(bamboo).toBe(false);
// Frame 2
raf(() => {
expect(bamboo).toBe(false);
// Frame 3
raf(() => {
// Frame 4
raf(() => {
expect(bamboo).toBe(true);
done();
});
});
});
});
});
describe('wave', () => {
it('bindAnimationEvent should return when node is null', () => {
const wrapper = mount(
<Wave>
<button type="button" disabled>
button
</button>
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
it('bindAnimationEvent.onClick should return when children is hidden', () => {
const wrapper = mount(
<Wave>
<button type="button" style={{ display: 'none' }}>
button
</button>
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
it('bindAnimationEvent.onClick should return when children is input', () => {
const wrapper = mount(
<Wave>
<input />
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
it('should not throw when click it', () => {
expect(() => {
const wrapper = mount(
<Wave>
<div />
</Wave>,
);
wrapper.simulate('click');
}).not.toThrow();
});
it('should not throw when no children', () => {
expect(() => mount(<Wave />)).not.toThrow();
});
});
describe('TransButton', () => {
it('can be focus/blur', () => {
const wrapper = mount(<TransButton>TransButton</TransButton>);
expect(typeof wrapper.instance().focus).toBe('function');
expect(typeof wrapper.instance().blur).toBe('function');
});
it('should trigger onClick when press enter', () => {
const onClick = jest.fn();
const preventDefault = jest.fn();
const wrapper = mount(<TransButton onClick={onClick}>TransButton</TransButton>);
wrapper.simulate('keyUp', { keyCode: KeyCode.ENTER });
expect(onClick).toHaveBeenCalled();
wrapper.simulate('keyDown', { keyCode: KeyCode.ENTER, preventDefault });
expect(preventDefault).toHaveBeenCalled();
});
});
describe('openAnimation', () => {
it('should support openAnimation', () => {
const done = jest.fn();
const domNode = document.createElement('div');
expect(typeof openAnimation.enter).toBe('function');
expect(typeof openAnimation.leave).toBe('function');
expect(typeof openAnimation.appear).toBe('function');
const appear = openAnimation.appear(domNode, done);
const enter = openAnimation.enter(domNode, done);
const leave = openAnimation.leave(domNode, done);
expect(typeof appear.stop).toBe('function');
expect(typeof enter.stop).toBe('function');
expect(typeof leave.stop).toBe('function');
expect(done).toHaveBeenCalled();
});
});
});

Some files were not shown because too many files have changed in this diff Show More