@@ -35,11 +38,8 @@ export default class OutlinePane extends Component<{ editor: any }> {
return (
-
this.main.mount(shell)}
- className="lc-outline-tree-container"
- >
-
+
this.main.mount(shell)} className="lc-outline-tree-container">
+
);
diff --git a/packages/plugin-outline-tree/src/views/style.less b/packages/plugin-outline-tree/src/views/style.less
new file mode 100644
index 000000000..a13fa5167
--- /dev/null
+++ b/packages/plugin-outline-tree/src/views/style.less
@@ -0,0 +1,295 @@
+.lc-outline-pane {
+ height: 100%;
+ width: 100%;
+ position: relative;
+
+ > .lc-outline-tree-container {
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ position: absolute;
+ overflow: auto;
+ }
+}
+
+.lc-outline-tree {
+ overflow: hidden;
+ margin-bottom: 20px;
+ user-select: none;
+
+ .tree-node-branches::before {
+ position: absolute;
+ display: block;
+ width: 0;
+ border-left: 1px solid transparent;
+ height: 100%;
+ top: 0;
+ left: 6px;
+ content: ' ';
+ z-index: 2;
+ }
+
+ &:hover {
+ .tree-node-branches::before {
+ border-left-color: #ddd;
+ }
+ }
+
+ .insertion {
+ pointer-events: none !important;
+ border: 1px dashed var(--color-brand-light);
+ height: 18px;
+ transform: translateZ(0);
+ }
+
+ .condition-group-container {
+ border-bottom: 1px solid #7b605b;
+ position: relative;
+
+ &:before {
+ position: absolute;
+ display: block;
+ width: 0;
+ border-left: 0.5px solid #7b605b;
+ height: 100%;
+ top: 0;
+ left: 0;
+ content: ' ';
+ z-index: 2;
+ }
+ >.condition-group-title {
+ text-align: center;
+ background-color: #7b605b;
+ height: 14px;
+ > .lc-title {
+ font-size: 12px;
+ transform: scale(0.8);
+ transform-origin: top;
+ color: white;
+ text-shadow: 0px 0px 2px black;
+ display: block;
+ }
+ }
+ }
+ .tree-node-slots {
+ border-bottom: 1px solid rgb(144, 94, 190);
+ position: relative;
+ &:before {
+ position: absolute;
+ display: block;
+ width: 0;
+ border-left: 0.5px solid rgb(144, 94, 190);
+ height: 100%;
+ top: 0;
+ left: 0;
+ content: ' ';
+ z-index: 2;
+ }
+ >.tree-node-slots-title {
+ text-align: center;
+ background-color: rgb(144, 94, 190);
+ height: 14px;
+ > .lc-title {
+ font-size: 12px;
+ transform: scale(0.8);
+ transform-origin: top;
+ color: white;
+ text-shadow: 0px 0px 2px black;
+ display: block;
+ }
+ }
+ }
+
+ .tree-node {
+ .tree-node-expand-btn {
+ width: 12px;
+ line-height: 0;
+ align-self: stretch;
+ display: flex;
+ align-items: center;
+ transition: color 200ms ease;
+ color: var(--color-icon-normal);
+ &:hover {
+ color: var(--color-icon-hover);
+ }
+ > svg {
+ transform-origin: center;
+ transform: rotate(-90deg);
+ transition: transform 100ms ease;
+ }
+ margin-right: 4px;
+ }
+ .tree-node-expand-placeholder {
+ width: 12px;
+ height: 12px;
+ margin-right: 4px;
+ }
+
+ .tree-node-icon {
+ transform: translateZ(0);
+ display: flex;
+ align-items: center;
+ margin-right: 4px;
+ color: var(--color-text);
+
+ & > svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
+ .tree-node-title {
+ font-size: var(--font-size-text);
+ cursor: pointer;
+ background: var(--color-pane-background);
+ border-bottom: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
+ display: flex;
+ align-items: center;
+ height: 30px;
+ position: relative;
+ transform: translateZ(0);
+ padding-right: 5px;
+ & > :first-child {
+ margin-left: 2px;
+ }
+
+ .tree-node-title-label {
+ flex: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ display: flex;
+ align-items: center;
+ align-self: stretch;
+ overflow: visible;
+ margin-right: 5px;
+
+ .tree-node-title-input {
+ flex: 1;
+ border: 1px solid var(--color-brand-light);
+ background-color: var(--color-pane-background);
+ color: var(--color-text);
+ line-height: 18px;
+ padding: 2px;
+ outline: none;
+ margin-left: -3px;
+ border-radius: 2px;
+ }
+ }
+
+ .tree-node-hide-btn, .tree-node-lock-btn {
+ opacity: 0;
+ color: var(--color-text);
+ line-height: 0;
+ align-self: stretch;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ &:hover {
+ opacity: 1 !important;
+ }
+ }
+ &:hover {
+ .tree-node-hide-btn, .tree-node-lock-btn {
+ opacity: 0.5;
+ }
+ }
+ &.editing {
+ & > .tree-node-hide-btn, & >.tree-node-lock-btn {
+ display: none;
+ }
+ }
+
+ .tree-node-tag {
+ margin-left: 5px;
+ display: flex;
+ align-items: center;
+ line-height: 0;
+ &.cond {
+ color: rgb(179, 52, 6);
+ }
+ &.loop {
+ color: rgb(103, 187, 187);
+ }
+ &.slot {
+ color: rgb(211, 90, 211);
+ }
+ }
+ }
+
+ &.is-root {
+ > .tree-node-title {
+ padding-left: 5px;
+ }
+ }
+
+ &.expanded {
+ & > .tree-node-title > .tree-node-expand-btn > svg {
+ transform: rotate(0);
+ }
+ }
+
+ &.hovering > .tree-node-title {
+ background: var(--color-block-background-light);
+ }
+
+ // 选中节点处理
+ &.selected {
+ & > .tree-node-title {
+ background: var(--color-block-background-shallow);
+ }
+
+ & > .tree-node-branches::before {
+ border-left-color: var(--color-brand-light);
+ }
+ }
+
+ &.hidden {
+ .tree-node-title-label {
+ color: #9b9b9b;
+ }
+ & > .tree-node-title > .tree-node-hide-btn {
+ opacity: 0.8;
+ }
+ .tree-node-branches {
+ .tree-node-hide-btn {
+ opacity: 0;
+ }
+ }
+ }
+
+ &.condition-flow {
+ & > .tree-node-title > .tree-node-hide-btn {
+ opacity: 1;
+ }
+ &.hidden > .tree-node-title > .tree-node-hide-btn {
+ opacity: 0;
+ }
+ }
+
+ &.locked {
+ & > .tree-node-title > .tree-node-lock-btn {
+ opacity: 0.8;
+ }
+ .tree-node-branches {
+ .tree-node-lock-btn, .tree-node-hide-btn {
+ opacity: 0;
+ }
+ }
+ }
+
+ // 处理拖入节点
+ &.dropping {
+ & > .tree-node-branches::before {
+ border-left: 1px solid var(--color-brand);
+ }
+ }
+
+ .tree-node-branches {
+ padding-left: 12px;
+ position: relative;
+ }
+ }
+}
diff --git a/packages/plugin-outline-tree/src/views/tree-branches.tsx b/packages/plugin-outline-tree/src/views/tree-branches.tsx
new file mode 100644
index 000000000..25d904367
--- /dev/null
+++ b/packages/plugin-outline-tree/src/views/tree-branches.tsx
@@ -0,0 +1,114 @@
+import { observer, Title } from '../../../globals';
+import { Component } from 'react';
+import TreeNode from '../tree-node';
+import TreeNodeView from './tree-node';
+import ExclusiveGroup from '../../../designer/src/designer/document/node/exclusive-group';
+import { intl } from '../locale';
+
+@observer
+export default class TreeBranches extends Component<{
+ treeNode: TreeNode;
+}> {
+ shouldComponentUpdate() {
+ return false;
+ }
+
+ render() {
+ const treeNode = this.props.treeNode;
+ const { expanded } = treeNode;
+
+ if (!expanded) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ );
+ }
+}
+
+@observer
+class TreeNodeChildren extends Component<{
+ treeNode: TreeNode;
+}> {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ const { treeNode } = this.props;
+ let children: any = [];
+ let groupContents: any[] = [];
+ let currentGrp: ExclusiveGroup;
+ const endGroup = () => {
+ if (groupContents.length > 0) {
+ children.push(
+
+
+
+
+ {groupContents}
+
,
+ );
+ groupContents = [];
+ }
+ };
+ const { dropIndex } = treeNode;
+ treeNode.children?.forEach((child, index) => {
+ const { conditionGroup } = child.node;
+ if (conditionGroup !== currentGrp) {
+ endGroup();
+ }
+
+ if (conditionGroup) {
+ currentGrp = conditionGroup;
+ if (index === dropIndex) {
+ if (groupContents.length > 0) {
+ groupContents.push(
);
+ } else {
+ children.push(
);
+ }
+ }
+ groupContents.push(
);
+ } else {
+ if (index === dropIndex) {
+ children.push(
);
+ }
+ children.push(
);
+ }
+ });
+ endGroup();
+ if (dropIndex != null && dropIndex === treeNode.children?.length) {
+ children.push(
);
+ }
+
+ return
{children}
;
+ }
+}
+
+@observer
+class TreeNodeSlots extends Component<{
+ treeNode: TreeNode;
+}> {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ const { treeNode } = this.props;
+ if (!treeNode.isSlotContainer()) {
+ return null;
+ }
+ return (
+
+
+
+
+ {treeNode.slots.map(tnode => (
+
+ ))}
+
+ );
+ }
+}
diff --git a/packages/plugin-outline-tree/src/views/tree-node.tsx b/packages/plugin-outline-tree/src/views/tree-node.tsx
new file mode 100644
index 000000000..b18a9688a
--- /dev/null
+++ b/packages/plugin-outline-tree/src/views/tree-node.tsx
@@ -0,0 +1,43 @@
+import { Component } from 'react';
+import classNames from 'classnames';
+import { observer } from '../../../globals';
+import TreeNode from '../tree-node';
+import TreeTitle from './tree-title';
+import TreeBranches from './tree-branches';
+
+@observer
+export default class TreeNodeView extends Component<{ treeNode: TreeNode }> {
+ shouldComponentUpdate() {
+ return false;
+ }
+
+ render() {
+ const { treeNode } = this.props;
+ const className = classNames('tree-node', {
+ // 是否展开
+ expanded: treeNode.expanded,
+ // 是否悬停中
+ hovering: treeNode.hovering,
+ // 是否选中的
+ selected: treeNode.selected,
+ // 是否隐藏的
+ hidden: treeNode.hidden,
+ // 是否忽略的
+ // ignored: treeNode.ignored,
+ // 是否锁定的
+ locked: treeNode.locked,
+ // 是否投放响应
+ dropping: treeNode.dropIndex != null,
+ 'is-root': treeNode.isRoot(),
+ 'condition-flow': treeNode.node.conditionGroup != null,
+ // highlight: treeNode.isResponseDropping() && treeNode.dropIndex == null,
+ });
+
+ return (
+
+
+
+
+ );
+ }
+}
diff --git a/packages/plugin-outline-tree/src/views/tree-title.tsx b/packages/plugin-outline-tree/src/views/tree-title.tsx
new file mode 100644
index 000000000..2e89551fc
--- /dev/null
+++ b/packages/plugin-outline-tree/src/views/tree-title.tsx
@@ -0,0 +1,199 @@
+import { Component, KeyboardEvent, FocusEvent, Fragment } from 'react';
+import classNames from 'classnames';
+import { observer, createIcon, Title, EmbedTip } from '../../../globals';
+import { IconArrowRight } from '../icons/arrow-right';
+import { IconEyeClose } from '../icons/eye-close';
+import { IconLock } from '../icons/lock';
+import { IconUnlock } from '../icons/unlock';
+import { intl } from '../locale';
+import TreeNode from '../tree-node';
+import { IconEye } from '../icons/eye';
+import { IconCond } from '../icons/cond';
+import { IconLoop } from '../icons/loop';
+import { IconSlot } from '../icons/slot';
+
+@observer
+export default class TreeTitle extends Component<{
+ treeNode: TreeNode;
+}> {
+ state = {
+ editing: false,
+ };
+
+ private enableEdit = () => {
+ this.setState({
+ editing: true,
+ });
+ };
+
+ private cancelEdit() {
+ this.setState({
+ editing: false,
+ });
+ }
+
+ private saveEdit = (e: FocusEvent
| KeyboardEvent) => {
+ const { treeNode } = this.props;
+ treeNode.setTitleLabel((e.target as HTMLInputElement).value || '');
+ this.cancelEdit();
+ };
+
+ private handleKeyUp = (e: KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ this.saveEdit(e);
+ }
+ if (e.keyCode === 27) {
+ this.cancelEdit();
+ }
+ };
+
+ componentDidUpdate() {
+ // TODO:
+ /*
+ const { current } = this.inputRef;
+ if (current) {
+ current.select();
+ }
+ */
+ }
+
+ render() {
+ const { treeNode } = this.props;
+ const { editing } = this.state;
+ const isCNode = !treeNode.isRoot();
+ const isNodeParent = treeNode.node.isNodeParent;
+ let style: any;
+ if (isCNode) {
+ const depth = treeNode.depth;
+ const indent = depth * 12;
+ style = {
+ paddingLeft: indent,
+ marginLeft: -indent,
+ };
+ }
+
+ return (
+ treeNode.mount(ref)}
+ style={style}
+ onClick={treeNode.node.conditionGroup ? () => treeNode.node.setConditionalVisible() : undefined}
+ >
+ {isCNode &&
}
+
{createIcon(treeNode.icon)}
+
+ {isCNode && isNodeParent &&
}
+ {isCNode && isNodeParent &&
}
+
+ );
+ }
+}
+
+@observer
+class LockBtn extends Component<{
+ treeNode: TreeNode;
+}> {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ const { treeNode } = this.props;
+ return (
+ {
+ e.stopPropagation();
+ treeNode.setLocked(!treeNode.locked);
+ }}
+ >
+ {treeNode.locked ? : }
+ {treeNode.locked ? intl('Unlock') : intl('Lock')}
+
+ );
+ }
+}
+
+@observer
+class HideBtn extends Component<{
+ treeNode: TreeNode;
+}> {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ const { treeNode } = this.props;
+ return (
+ {
+ e.stopPropagation();
+ treeNode.setHidden(!treeNode.hidden);
+ }}
+ >
+ {treeNode.hidden ? : }
+ {treeNode.hidden ? intl('Show') : intl('Hide')}
+
+ );
+ }
+}
+
+@observer
+class ExpandBtn extends Component<{
+ treeNode: TreeNode;
+}> {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ const { treeNode } = this.props;
+ if (!treeNode.expandable) {
+ return ;
+ }
+ return (
+ {
+ e.stopPropagation();
+ treeNode.setExpanded(!treeNode.expanded);
+ }}
+ >
+
+ {treeNode.expanded ? intl('Collapse') : intl('Expand')}
+
+ );
+ }
+}
diff --git a/packages/plugin-outline-pane/src/views/tree.tsx b/packages/plugin-outline-tree/src/views/tree.tsx
similarity index 82%
rename from packages/plugin-outline-pane/src/views/tree.tsx
rename to packages/plugin-outline-tree/src/views/tree.tsx
index 551147ea9..bc219cf41 100644
--- a/packages/plugin-outline-pane/src/views/tree.tsx
+++ b/packages/plugin-outline-tree/src/views/tree.tsx
@@ -1,8 +1,11 @@
-@observer
-export default class TreeView extends React.Component {
- private ref = React.createRef();
- private dispose?: () => void;
+import { Component } from 'react';
+import { observer } from '../../../globals';
+import { Tree } from '../tree';
+import TreeNodeView from './tree-node';
+@observer
+export default class TreeView extends Component<{ tree: Tree }> {
+ /*
hover(e: any) {
const treeNode = tree.getTreeNodeByEvent(e);
@@ -68,12 +71,13 @@ export default class TreeView extends React.Component {
});
}
}
+ */
render() {
const { tree } = this.props;
const root = tree.root;
return (
-
+
);
diff --git a/packages/plugin-outline-pane/tsconfig.json b/packages/plugin-outline-tree/tsconfig.json
similarity index 100%
rename from packages/plugin-outline-pane/tsconfig.json
rename to packages/plugin-outline-tree/tsconfig.json