Merge branch 'feat/support-model-node-in-outline-pane' into 'release/0.9.0'

Feat/support model node in outline pane

大纲树支持模态视图
模态视图只能在根节点

See merge request !893801
This commit is contained in:
康为 2020-07-15 14:24:31 +08:00
commit d282a37542
15 changed files with 268 additions and 43 deletions

View File

@ -826,10 +826,14 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
event: e, event: e,
}; };
if (e.dragObject.nodes[0].getPrototype().isModal()) { if (e.dragObject.type === 'node' && e.dragObject.nodes[0].getPrototype().isModal()) {
return this.designer.createLocation({ return this.designer.createLocation({
target: this.document.rootNode, target: this.document.rootNode,
detail, detail: {
type: LocationDetailType.Children,
index: 0,
valid: true,
},
source: 'simulator' + this.document.id, source: 'simulator' + this.document.id,
event: e, event: e,
}); });

View File

@ -10,7 +10,7 @@ import { Selection } from './selection';
import { History } from './history'; import { History } from './history';
import { TransformStage } from './node'; import { TransformStage } from './node';
import { uniqueId } from '@ali/lowcode-utils'; import { uniqueId } from '@ali/lowcode-utils';
import ModalNodesManager from './node/modalNodesManager'; import { ModalNodesManager } from './node';
export type GetDataType<T, NodeType> = T extends undefined export type GetDataType<T, NodeType> = T extends undefined
? NodeType extends { ? NodeType extends {
@ -37,6 +37,10 @@ export class DocumentModel {
* *
*/ */
readonly history: History; readonly history: History;
/**
*
*/
readonly modalNodesManager: ModalNodesManager;
private nodesMap = new Map<string, Node>(); private nodesMap = new Map<string, Node>();
@obx.val private nodes = new Set<Node>(); @obx.val private nodes = new Set<Node>();
@ -44,7 +48,6 @@ export class DocumentModel {
private _simulator?: ISimulatorHost; private _simulator?: ISimulatorHost;
private emitter: EventEmitter; private emitter: EventEmitter;
private rootNodeVisitorMap: { [visitorName: string]: any } = {}; private rootNodeVisitorMap: { [visitorName: string]: any } = {};
private modalNodesManager: ModalNodesManager;
/** /**
* *

View File

@ -5,3 +5,4 @@ export * from './props/prop';
export * from './props/prop-stash'; export * from './props/prop-stash';
export * from './props/props'; export * from './props/props';
export * from './transform-stage'; export * from './transform-stage';
export * from './modal-nodes-manager';

View File

@ -17,11 +17,11 @@ function getModalNodes(node: Node) {
return nodes; return nodes;
} }
export default class ModalNodesManager { export class ModalNodesManager {
public willDestroy: any; public willDestroy: any;
private page: DocumentModel; private page: DocumentModel;
private modalNodes: [Node]; private modalNodes: Node[];
private nodeRemoveEvents: any; private nodeRemoveEvents: any;
private emitter: EventEmitter; private emitter: EventEmitter;
@ -44,7 +44,7 @@ export default class ModalNodesManager {
public getVisibleModalNode() { public getVisibleModalNode() {
const visibleNode = this.modalNodes const visibleNode = this.modalNodes
? this.modalNodes.find((node: Node) => { ? this.modalNodes.find((node: Node) => {
return !node.getExtraProp('hidden'); return node.getVisible();
}) })
: null; : null;
return visibleNode; return visibleNode;
@ -53,18 +53,18 @@ export default class ModalNodesManager {
public hideModalNodes() { public hideModalNodes() {
if (this.modalNodes) { if (this.modalNodes) {
this.modalNodes.forEach((node: Node) => { this.modalNodes.forEach((node: Node) => {
node.getExtraProp('hidden')?.setValue(true); node.setVisible(false);
}); });
} }
} }
public setVisible(node: Node) { public setVisible(node: Node) {
this.hideModalNodes(); this.hideModalNodes();
node.getExtraProp('hidden')?.setValue(false); node.setVisible(true);
} }
public setInvisible(node: Node) { public setInvisible(node: Node) {
node.getExtraProp('hidden')?.setValue(true); node.setVisible(false);
} }
public onVisibleChange(func: () => any) { public onVisibleChange(func: () => any) {
@ -101,26 +101,24 @@ export default class ModalNodesManager {
} }
this.removeNodeEvent(node); this.removeNodeEvent(node);
this.emitter.emit('modalNodesChange'); this.emitter.emit('modalNodesChange');
if (!node.getExtraProp('hidden')) { if (node.getVisible()) {
this.emitter.emit('visibleChange'); this.emitter.emit('visibleChange');
} }
} }
} }
private addNodeEvent(node: Node) { private addNodeEvent(node: Node) {
// this.nodeRemoveEvents[node.getId()] = this.nodeRemoveEvents[node.getId()] =
// node.onStatusChange((status: any, field: any) => { node.onVisibleChange((flag) => {
// if (field === 'visibility') { this.emitter.emit('visibleChange');
// this.emitter.emit('visibleChange'); });
// }
// });
} }
private removeNodeEvent(node: Node) { private removeNodeEvent(node: Node) {
// if (this.nodeRemoveEvents[node.getId()]) { if (this.nodeRemoveEvents[node.getId()]) {
// this.nodeRemoveEvents[node.getId()](); this.nodeRemoveEvents[node.getId()]();
// delete this.nodeRemoveEvents[node.getId()]; delete this.nodeRemoveEvents[node.getId()];
// } }
} }
private setNodes() { private setNodes() {

View File

@ -22,6 +22,7 @@ import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group';
import { TransformStage } from './transform-stage'; import { TransformStage } from './transform-stage';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import { SettingTopEntry } from 'designer/src/designer'; import { SettingTopEntry } from 'designer/src/designer';
import { EventEmitter } from 'events';
/** /**
* *
@ -72,6 +73,7 @@ import { SettingTopEntry } from 'designer/src/designer';
* hidden * hidden
*/ */
export class Node<Schema extends NodeSchema = NodeSchema> { export class Node<Schema extends NodeSchema = NodeSchema> {
private emitter: EventEmitter;
/** /**
* *
*/ */
@ -162,6 +164,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
} }
this.settingEntry = this.document.designer.createSettingEntry([ this ]); this.settingEntry = this.document.designer.createSettingEntry([ this ]);
this.emitter = new EventEmitter();
} }
private initProps(props: any): any { private initProps(props: any): any {
@ -415,6 +418,22 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
return node; return node;
} }
setVisible(flag: boolean): void {
this.getExtraProp('hidden')?.setValue(!flag);
this.emitter.emit('visibleChange', flag);
}
getVisible(): boolean {
return !this.getExtraProp('hidden', false)?.getValue();
}
onVisibleChange(func: (flag: boolean) => any) {
this.emitter.on('visibleChange', func);
return () => {
this.emitter.removeListener('visibleChange', func);
};
}
getProp(path: string, stash = true): Prop | null { getProp(path: string, stash = true): Prop | null {
return this.props.query(path, stash as any) || null; return this.props.query(path, stash as any) || null;
} }

View File

@ -0,0 +1,14 @@
import { SVGIcon, IconProps } from '@ali/lowcode-utils';
export function IconRadioActive(props: IconProps) {
return (
<SVGIcon viewBox="0 0 1024 1024" {...props}>
<path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024z m0-256a256 256 0 1 0 0-512 256 256 0 0 0 0 512z"></path>
</SVGIcon>
);
}
IconRadioActive.displayName = 'IconRadioActive';

View File

@ -0,0 +1,14 @@
import { SVGIcon, IconProps } from '@ali/lowcode-utils';
export function IconRadio(props: IconProps) {
return (
<SVGIcon viewBox="0 0 1024 1024" {...props}>
<path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024z m0-64A448 448 0 1 0 512 64a448 448 0 0 0 0 896z"></path>
</SVGIcon>
);
}
IconRadio.displayName = 'IconRadio';

View File

@ -134,6 +134,20 @@ export class OutlineMain implements ISensor, ITreeBoard, IScrollable {
const pos = getPosFromEvent(e, this._shell); const pos = getPosFromEvent(e, this._shell);
const irect = this.getInsertionRect(); const irect = this.getInsertionRect();
const originLoc = document.dropLocation; const originLoc = document.dropLocation;
if (e.dragObject.type === 'node' && e.dragObject.nodes[0].getPrototype().isModal()) {
return designer.createLocation({
target: document.rootNode,
detail: {
type: LocationDetailType.Children,
index: 0,
valid: true,
},
source: this.id,
event: e,
});
}
if (originLoc && ((pos && pos === 'unchanged') || (irect && globalY >= irect.top && globalY <= irect.bottom))) { if (originLoc && ((pos && pos === 'unchanged') || (irect && globalY >= irect.top && globalY <= irect.bottom))) {
const loc = originLoc.clone(e); const loc = originLoc.clone(e);
const indented = this.indentTrack.getIndentParent(originLoc, loc); const indented = this.indentTrack.getIndentParent(originLoc, loc);

View File

@ -72,7 +72,7 @@ export default class TreeNode {
@computed get hidden(): boolean { @computed get hidden(): boolean {
const cv = this.node.isConditionalVisible(); const cv = this.node.isConditionalVisible();
if (cv == null) { if (cv == null) {
return this.node.getExtraProp('hidden', false)?.getValue() === true; return !this.node.getVisible();
} }
return !cv; return !cv;
} }
@ -81,11 +81,7 @@ export default class TreeNode {
if (this.node.conditionGroup) { if (this.node.conditionGroup) {
return; return;
} }
if (flag) { this.node.setVisible(!flag);
this.node.getExtraProp('hidden', true)?.setValue(true);
} else {
this.node.getExtraProp('hidden', false)?.remove();
}
} }
@computed get locked(): boolean { @computed get locked(): boolean {

View File

@ -0,0 +1,85 @@
import { Component } from 'react';
import classNames from 'classnames';
import { observer } from '@ali/lowcode-editor-core';
import TreeNode from '../tree-node';
import TreeTitle from './tree-title';
import TreeBranches from './tree-branches';
import { ModalNodesManager } from '@ali/lowcode-designer';
import { IconEyeClose } from '../icons/eye-close';
@observer
class ModalTreeNodeView extends Component<{ treeNode: TreeNode }> {
private modalNodesManager: ModalNodesManager;
constructor(props: any) {
super(props);
// 模态管理对象
this.modalNodesManager = props.treeNode.document.modalNodesManager;
}
shouldComponentUpdate() {
return false;
}
hideAllNodes() {
this.modalNodesManager.hideModalNodes();
}
render() {
const { treeNode } = this.props;
const hasVisibleModalNode = !!this.modalNodesManager.getVisibleModalNode();
return (
<div className="tree-node-modal">
<div className="tree-node-modal-title">
<span></span>
<div className="tree-node-modal-title-visible-icon"
onClick={this.hideAllNodes.bind(this)}>
{hasVisibleModalNode ? <IconEyeClose /> : null}
</div>
</div>
<div className="tree-pane-modal-content">
<TreeBranches treeNode={treeNode} isModal={true}/>
</div>
</div>
);
}
}
@observer
export default class RootTreeNodeView extends Component<{ treeNode: TreeNode }> {
shouldComponentUpdate() {
return false;
}
render() {
const { treeNode } = this.props;
const className = classNames('tree-node', {
// 是否展开
expanded: treeNode.expanded,
// 是否悬停中
detecting: treeNode.detecting,
// 是否选中的
selected: treeNode.selected,
// 是否隐藏的
hidden: treeNode.hidden,
// 是否忽略的
// ignored: treeNode.ignored,
// 是否锁定的
locked: treeNode.locked,
// 是否投放响应
dropping: treeNode.dropDetail?.index != null,
'is-root': treeNode.isRoot(),
'condition-flow': treeNode.node.conditionGroup != null,
highlight: treeNode.isFocusingNode(),
});
return (
<div className={className} data-id={treeNode.id}>
<TreeTitle treeNode={treeNode} />
<ModalTreeNodeView treeNode={treeNode} />
<TreeBranches treeNode={treeNode}/>
</div>
);
}
}

View File

@ -21,6 +21,46 @@
margin-bottom: @treeNodeHeight; margin-bottom: @treeNodeHeight;
user-select: none; user-select: none;
.tree-node-modal {
margin: 5px;
border: 1px solid rgba(31, 56, 88, 0.2);
border-radius: 3px;
box-shadow: 0 1px 4px 0 rgba(31, 56, 88, 0.15);
.tree-node-modal-title {
position: relative;
background: rgba(31, 56, 88, 0.04);
padding: 0 10px;
height: 32px;
line-height: 32px;
border-bottom: 1px solid rgba(31, 56, 88, 0.2);
.tree-node-modal-title-visible-icon {
position: absolute;
top: 4px;
right: 12px;
cursor: pointer;
}
}
.tree-pane-modal-content {
& > .tree-node-branches::before {
display: none;
}
}
.tree-node-modal-radio, .tree-node-modal-radio-active {
margin-right: 4px;
opacity: 0.8;
position: absolute;
top: 7px;
left: 6px;
}
.tree-node-modal-radio-active {
color: #006cff;
}
}
.tree-node-branches::before { .tree-node-branches::before {
position: absolute; position: absolute;
display: block; display: block;

View File

@ -9,13 +9,14 @@ import { intlNode } from '../locale';
@observer @observer
export default class TreeBranches extends Component<{ export default class TreeBranches extends Component<{
treeNode: TreeNode; treeNode: TreeNode;
isModal?: boolean;
}> { }> {
shouldComponentUpdate() { shouldComponentUpdate() {
return false; return false;
} }
render() { render() {
const treeNode = this.props.treeNode; const { treeNode, isModal } = this.props;
const { expanded } = treeNode; const { expanded } = treeNode;
if (!expanded) { if (!expanded) {
@ -24,8 +25,10 @@ export default class TreeBranches extends Component<{
return ( return (
<div className="tree-node-branches"> <div className="tree-node-branches">
<TreeNodeSlots treeNode={treeNode} /> {
<TreeNodeChildren treeNode={treeNode} /> !isModal && <TreeNodeSlots treeNode={treeNode}/>
}
<TreeNodeChildren treeNode={treeNode} isModal={isModal || false}/>
</div> </div>
); );
} }
@ -34,12 +37,13 @@ export default class TreeBranches extends Component<{
@observer @observer
class TreeNodeChildren extends Component<{ class TreeNodeChildren extends Component<{
treeNode: TreeNode; treeNode: TreeNode;
isModal?: boolean;
}> { }> {
shouldComponentUpdate() { shouldComponentUpdate() {
return false; return false;
} }
render() { render() {
const { treeNode } = this.props; const { treeNode, isModal } = this.props;
let children: any = []; let children: any = [];
let groupContents: any[] = []; let groupContents: any[] = [];
let currentGrp: ExclusiveGroup; let currentGrp: ExclusiveGroup;
@ -67,6 +71,10 @@ class TreeNodeChildren extends Component<{
/> />
); );
treeNode.children?.forEach((child, index) => { treeNode.children?.forEach((child, index) => {
const childIsModal = child.node.getPrototype().isModal();
if (isModal != childIsModal) {
return;
}
const { conditionGroup } = child.node; const { conditionGroup } = child.node;
if (conditionGroup !== currentGrp) { if (conditionGroup !== currentGrp) {
endGroup(); endGroup();
@ -81,12 +89,12 @@ class TreeNodeChildren extends Component<{
children.push(insertion); children.push(insertion);
} }
} }
groupContents.push(<TreeNodeView key={child.id} treeNode={child} />); groupContents.push(<TreeNodeView key={child.id} treeNode={child} isModal={isModal}/>);
} else { } else {
if (index === dropIndex) { if (index === dropIndex) {
children.push(insertion); children.push(insertion);
} }
children.push(<TreeNodeView key={child.id} treeNode={child} />); children.push(<TreeNodeView key={child.id} treeNode={child} isModal={isModal}/>);
} }
}); });
endGroup(); endGroup();

View File

@ -6,13 +6,16 @@ import TreeTitle from './tree-title';
import TreeBranches from './tree-branches'; import TreeBranches from './tree-branches';
@observer @observer
export default class TreeNodeView extends Component<{ treeNode: TreeNode }> { export default class TreeNodeView extends Component<{
treeNode: TreeNode;
isModal?: boolean;
}> {
shouldComponentUpdate() { shouldComponentUpdate() {
return false; return false;
} }
render() { render() {
const { treeNode } = this.props; const { treeNode, isModal } = this.props;
const className = classNames('tree-node', { const className = classNames('tree-node', {
// 是否展开 // 是否展开
expanded: treeNode.expanded, expanded: treeNode.expanded,
@ -35,8 +38,8 @@ export default class TreeNodeView extends Component<{ treeNode: TreeNode }> {
return ( return (
<div className={className} data-id={treeNode.id}> <div className={className} data-id={treeNode.id}>
<TreeTitle treeNode={treeNode} /> <TreeTitle treeNode={treeNode} isModal={isModal}/>
<TreeBranches treeNode={treeNode} /> <TreeBranches treeNode={treeNode} isModal={false}/>
</div> </div>
); );
} }

View File

@ -10,11 +10,14 @@ import TreeNode from '../tree-node';
import { IconEye } from '../icons/eye'; import { IconEye } from '../icons/eye';
import { IconCond } from '../icons/cond'; import { IconCond } from '../icons/cond';
import { IconLoop } from '../icons/loop'; import { IconLoop } from '../icons/loop';
import { IconRadioActive } from '../icons/radio-active';
import { IconRadio } from '../icons/radio';
import { createIcon } from '@ali/lowcode-utils'; import { createIcon } from '@ali/lowcode-utils';
@observer @observer
export default class TreeTitle extends Component<{ export default class TreeTitle extends Component<{
treeNode: TreeNode; treeNode: TreeNode;
isModal?: boolean;
}> { }> {
state: { state: {
editing: boolean; editing: boolean;
@ -62,7 +65,7 @@ export default class TreeTitle extends Component<{
}; };
render() { render() {
const { treeNode } = this.props; const { treeNode, isModal } = this.props;
const { editing } = this.state; const { editing } = this.state;
const isCNode = !treeNode.isRoot(); const isCNode = !treeNode.isRoot();
const { node } = treeNode; const { node } = treeNode;
@ -72,7 +75,7 @@ export default class TreeTitle extends Component<{
const depth = treeNode.depth; const depth = treeNode.depth;
const indent = depth * 12; const indent = depth * 12;
style = { style = {
paddingLeft: indent, paddingLeft: indent + (isModal ? 12 : 0),
marginLeft: -indent, marginLeft: -indent,
}; };
} }
@ -84,8 +87,31 @@ export default class TreeTitle extends Component<{
})} })}
style={style} style={style}
data-id={treeNode.id} data-id={treeNode.id}
onClick={node.conditionGroup ? () => node.setConditionalVisible() : undefined} onClick={() => {
if (isModal) {
node.document.modalNodesManager.setVisible(node);
return;
}
if (node.conditionGroup) {
node.setConditionalVisible();
return;
}
}}
> >
{isModal && node.getVisible() && (
<div onClick={() => {
node.document.modalNodesManager.setInvisible(node);
}}>
<IconRadioActive className="tree-node-modal-radio-active"/>
</div>
)}
{isModal && !node.getVisible() && (
<div onClick={() => {
node.document.modalNodesManager.setVisible(node);
}}>
<IconRadio className="tree-node-modal-radio"/>
</div>
)}
{isCNode && <ExpandBtn treeNode={treeNode} />} {isCNode && <ExpandBtn treeNode={treeNode} />}
<div className="tree-node-icon">{createIcon(treeNode.icon)}</div> <div className="tree-node-icon">{createIcon(treeNode.icon)}</div>
<div className="tree-node-title-label" onDoubleClick={isNodeParent ? this.enableEdit : undefined}> <div className="tree-node-title-label" onDoubleClick={isNodeParent ? this.enableEdit : undefined}>
@ -123,7 +149,7 @@ export default class TreeTitle extends Component<{
</Fragment> </Fragment>
)} )}
</div> </div>
{isCNode && isNodeParent && <HideBtn treeNode={treeNode} />} {isCNode && isNodeParent && !isModal && <HideBtn treeNode={treeNode} />}
{/*isCNode && isNodeParent && <LockBtn treeNode={treeNode} />*/} {/*isCNode && isNodeParent && <LockBtn treeNode={treeNode} />*/}
</div> </div>
); );

View File

@ -3,7 +3,7 @@ import { observer, Editor, globalContext } from '@ali/lowcode-editor-core';
import { isRootNode, Node, DragObjectType, isShaken } from '@ali/lowcode-designer'; import { isRootNode, Node, DragObjectType, isShaken } from '@ali/lowcode-designer';
import { isFormEvent } from '@ali/lowcode-utils'; import { isFormEvent } from '@ali/lowcode-utils';
import { Tree } from '../tree'; import { Tree } from '../tree';
import TreeNodeView from './tree-node'; import RootTreeNodeView from './root-tree-node';
function getTreeNodeIdByEvent(e: ReactMouseEvent, stop: Element): null | string { function getTreeNodeIdByEvent(e: ReactMouseEvent, stop: Element): null | string {
let target: Element | null = e.target as Element; let target: Element | null = e.target as Element;
@ -155,7 +155,7 @@ export default class TreeView extends Component<{ tree: Tree }> {
onClick={this.onClick} onClick={this.onClick}
onMouseLeave={this.onMouseLeave} onMouseLeave={this.onMouseLeave}
> >
<TreeNodeView key={root.id} treeNode={root} /> <RootTreeNodeView key={root.id} treeNode={root} />
</div> </div>
); );
} }