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,
};
if (e.dragObject.nodes[0].getPrototype().isModal()) {
if (e.dragObject.type === 'node' && e.dragObject.nodes[0].getPrototype().isModal()) {
return this.designer.createLocation({
target: this.document.rootNode,
detail,
detail: {
type: LocationDetailType.Children,
index: 0,
valid: true,
},
source: 'simulator' + this.document.id,
event: e,
});

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group';
import { TransformStage } from './transform-stage';
import { ReactElement } from 'react';
import { SettingTopEntry } from 'designer/src/designer';
import { EventEmitter } from 'events';
/**
*
@ -72,6 +73,7 @@ import { SettingTopEntry } from 'designer/src/designer';
* hidden
*/
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.emitter = new EventEmitter();
}
private initProps(props: any): any {
@ -415,6 +418,22 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
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 {
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 irect = this.getInsertionRect();
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))) {
const loc = originLoc.clone(e);
const indented = this.indentTrack.getIndentParent(originLoc, loc);

View File

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

View File

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

View File

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

View File

@ -10,11 +10,14 @@ import TreeNode from '../tree-node';
import { IconEye } from '../icons/eye';
import { IconCond } from '../icons/cond';
import { IconLoop } from '../icons/loop';
import { IconRadioActive } from '../icons/radio-active';
import { IconRadio } from '../icons/radio';
import { createIcon } from '@ali/lowcode-utils';
@observer
export default class TreeTitle extends Component<{
treeNode: TreeNode;
isModal?: boolean;
}> {
state: {
editing: boolean;
@ -62,7 +65,7 @@ export default class TreeTitle extends Component<{
};
render() {
const { treeNode } = this.props;
const { treeNode, isModal } = this.props;
const { editing } = this.state;
const isCNode = !treeNode.isRoot();
const { node } = treeNode;
@ -72,7 +75,7 @@ export default class TreeTitle extends Component<{
const depth = treeNode.depth;
const indent = depth * 12;
style = {
paddingLeft: indent,
paddingLeft: indent + (isModal ? 12 : 0),
marginLeft: -indent,
};
}
@ -84,8 +87,31 @@ export default class TreeTitle extends Component<{
})}
style={style}
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} />}
<div className="tree-node-icon">{createIcon(treeNode.icon)}</div>
<div className="tree-node-title-label" onDoubleClick={isNodeParent ? this.enableEdit : undefined}>
@ -123,7 +149,7 @@ export default class TreeTitle extends Component<{
</Fragment>
)}
</div>
{isCNode && isNodeParent && <HideBtn treeNode={treeNode} />}
{isCNode && isNodeParent && !isModal && <HideBtn treeNode={treeNode} />}
{/*isCNode && isNodeParent && <LockBtn treeNode={treeNode} />*/}
</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 { isFormEvent } from '@ali/lowcode-utils';
import { Tree } from '../tree';
import TreeNodeView from './tree-node';
import RootTreeNodeView from './root-tree-node';
function getTreeNodeIdByEvent(e: ReactMouseEvent, stop: Element): null | string {
let target: Element | null = e.target as Element;
@ -155,7 +155,7 @@ export default class TreeView extends Component<{ tree: Tree }> {
onClick={this.onClick}
onMouseLeave={this.onMouseLeave}
>
<TreeNodeView key={root.id} treeNode={root} />
<RootTreeNodeView key={root.id} treeNode={root} />
</div>
);
}