diff --git a/docs/docs/api/model/document-model.md b/docs/docs/api/model/document-model.md index 68efe0575..0716588ce 100644 --- a/docs/docs/api/model/document-model.md +++ b/docs/docs/api/model/document-model.md @@ -42,6 +42,12 @@ sidebar_position: 0 参见 [模态节点管理](./modal-nodes-manager) +### dropLocation +文档的 dropLocation +相关类型:[IPublicModelDropLocation](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/model/drop-location.ts) + +**@since v1.1.0** + ## 方法签名 ### getNodeById @@ -90,6 +96,24 @@ removeNode(idOrNode: string | Node) function checkNesting(dropTarget: Node, dragObject: DragNodeObject | DragNodeDataObject): boolean {} ``` +### isDetectingNode +检查拖拽放置的目标节点是否可以放置该拖拽对象 + +```typescript +/** + * 判断是否当前节点处于被探测状态 + * check is node being detected + * @param node + * @since v1.1.0 + */ +isDetectingNode(node: IPublicModelNode): boolean; +``` +相关类型:[IPublicModelNode](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/model/node.ts) + + +**@since v1.1.0** + + ## 事件 ### onAddNode @@ -144,7 +168,53 @@ onChangeNodeProp(fn: (info: IPublicTypePropChangeOptions) => void) ### onImportSchema 当前 document 导入新的 schema 事件 -版本 >= 1.0.15 ```typescript -onImportSchema(fn: (schema: any) => void) -``` \ No newline at end of file +/** + * import schema event + * @param fn + * @since v1.0.15 + */ +onImportSchema(fn: (schema: IPublicTypeRootSchema) => void): IPublicTypeDisposable; +``` +相关类型: +- [IPublicTypeDisposable](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/disposable.ts) +- [IPublicTypeRootSchema](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/root-schema.ts) + +**@since v1.0.15** + +### onFocusNodeChanged +设置聚焦节点变化的回调 + +```typescript +/** + * 设置聚焦节点变化的回调 + * triggered focused node is set mannually from plugin + * @param fn + * @since v1.1.0 + */ +onFocusNodeChanged( + fn: (doc: IPublicModelDocumentModel, focusNode: IPublicModelNode) => void, +): IPublicTypeDisposable; +``` +相关类型: +- [IPublicTypeDisposable](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/disposable.ts) +- [IPublicModelNode](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/model/node.ts) + +**@since v1.1.0** + +### onDropLocationChanged +设置 DropLocation 变化的回调 + +```typescript +/** + * 设置 DropLocation 变化的回调 + * triggered when drop location changed + * @param fn + * @since v1.1.0 + */ +onDropLocationChanged(fn: (doc: IPublicModelDocumentModel) => void): IPublicTypeDisposable; +``` + +相关类型:[IPublicTypeDisposable](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/shell/type/disposable.ts) + +**@since v1.1.0** \ No newline at end of file diff --git a/packages/designer/src/designer/active-tracker.ts b/packages/designer/src/designer/active-tracker.ts index bf37efe80..d62ad9fe5 100644 --- a/packages/designer/src/designer/active-tracker.ts +++ b/packages/designer/src/designer/active-tracker.ts @@ -7,8 +7,9 @@ import { import { isNode } from '@alilc/lowcode-utils'; export interface IActiveTracker extends IPublicModelActiveTracker { - + track(originalTarget: IPublicTypeActiveTarget | INode): void; } + export class ActiveTracker implements IActiveTracker { private emitter: IEventBus = createModuleEventBus('ActiveTracker'); diff --git a/packages/designer/src/designer/location.ts b/packages/designer/src/designer/location.ts index c219a259d..a690b28d5 100644 --- a/packages/designer/src/designer/location.ts +++ b/packages/designer/src/designer/location.ts @@ -103,15 +103,11 @@ export interface IDropLocation extends IPublicModelDropLocation { readonly target: INode; - readonly detail: IPublicTypeLocationDetail; - readonly event: ILocateEvent; readonly source: string; get document(): IPublicModelDocumentModel; - - clone(event: ILocateEvent): IDropLocation; } export class DropLocation implements IDropLocation { diff --git a/packages/designer/src/document/document-model.ts b/packages/designer/src/document/document-model.ts index f38c2f8bc..5e0717e03 100644 --- a/packages/designer/src/document/document-model.ts +++ b/packages/designer/src/document/document-model.ts @@ -14,7 +14,6 @@ import { IPublicModelNode, IPublicApiProject, IPublicModelDropLocation, - IPublicEnumEventNames, IPublicEnumTransformStage, } from '@alilc/lowcode-types'; import { Project } from '../project'; @@ -326,7 +325,7 @@ export class DocumentModel implements IDocumentModel { this._dropLocation = loc; // pub event this.designer.editor.eventBus.emit( - IPublicEnumEventNames.DOCUMENT_DROPLOCATION_CHANGED, + 'document.dropLocation.changed', { document: this, location: loc }, ); } diff --git a/packages/editor-skeleton/src/skeleton.ts b/packages/editor-skeleton/src/skeleton.ts index a6a5f6ae6..649c45271 100644 --- a/packages/editor-skeleton/src/skeleton.ts +++ b/packages/editor-skeleton/src/skeleton.ts @@ -26,7 +26,6 @@ import { PluginClassSet, IPublicTypeWidgetBaseConfig, IPublicTypeWidgetConfigArea, - IPublicEnumEventNames, } from '@alilc/lowcode-types'; const logger = new Logger({ level: 'warn', bizName: 'skeleton' }); @@ -174,7 +173,7 @@ export class Skeleton { */ setupEvents() { // adjust pinned status when panel shown - this.editor.eventBus.on(IPublicEnumEventNames.SKELETON_PANEL_SHOW, (panelName, panel) => { + this.editor.eventBus.on(SkeletonEvents.PANEL_SHOW, (panelName, panel) => { const panelNameKey = `${panelName}-pinned-status-isFloat`; const isInFloatAreaPreferenceExists = engineConfig.getPreference()?.contains(panelNameKey, 'skeleton'); if (isInFloatAreaPreferenceExists) { diff --git a/packages/engine/src/engine-core.ts b/packages/engine/src/engine-core.ts index 1af53e9ba..d8d81aefc 100644 --- a/packages/engine/src/engine-core.ts +++ b/packages/engine/src/engine-core.ts @@ -171,7 +171,7 @@ export async function init( engineConfig.setEngineOptions(engineOptions as any); // 注册一批内置插件 - await plugins.register(OutlinePlugin); + await plugins.register(OutlinePlugin, {}, { autoInit: true }); await plugins.register(componentMetaParser(designer)); await plugins.register(setterRegistry, {}, { autoInit: true }); await plugins.register(defaultPanelRegistry(editor, designer)); diff --git a/packages/plugin-outline-pane/src/controllers/pane-controller.ts b/packages/plugin-outline-pane/src/controllers/pane-controller.ts index c472d4c9a..a368c8288 100644 --- a/packages/plugin-outline-pane/src/controllers/pane-controller.ts +++ b/packages/plugin-outline-pane/src/controllers/pane-controller.ts @@ -7,7 +7,7 @@ import { isLocationChildrenDetail, } from '@alilc/lowcode-utils'; import { - DragObject, + IPublicModelDragObject, IPublicModelScrollable, ISensor, IPublicTypeLocationChildrenDetail, @@ -50,7 +50,6 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab setup(); } - /** -------------------- ISensor begin -------------------- */ private indentTrack = new IndentTrack(); @@ -107,12 +106,12 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab const document = project.getCurrentDocument(); const pos = getPosFromEvent(e, this._shell); const irect = this.getInsertionRect(); - const originLoc = document.dropLocation; + const originLoc = document?.dropLocation; - const componentMeta = e.dragObject.nodes ? e.dragObject.nodes[0].componentMeta : null; - if (e.dragObject.type === 'node' && componentMeta && componentMeta.isModal) { + const componentMeta = e.dragObject?.nodes ? e.dragObject.nodes[0].componentMeta : null; + if (e.dragObject?.type === 'node' && componentMeta && componentMeta.isModal && document?.focusNode) { return canvas.createLocation({ - target: document.focusNode, + target: document?.focusNode, detail: { type: IPublicTypeLocationDetailType.Children, index: 0, @@ -123,7 +122,9 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab }); } - if (originLoc && ((pos && pos === 'unchanged') || (irect && globalY >= irect.top && globalY <= irect.bottom))) { + if (originLoc + && ((pos && pos === 'unchanged') || (irect && globalY >= irect.top && globalY <= irect.bottom)) + && dragObject) { const loc = originLoc.clone(e); const indented = this.indentTrack.getIndentParent(originLoc, loc); if (indented) { @@ -138,7 +139,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab detail: { type: IPublicTypeLocationDetailType.Children, index, - valid: document.checkNesting(parent, e.dragObject as any), + valid: document?.checkNesting(parent, e.dragObject as any), }, }); } @@ -177,7 +178,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab } } if (p !== node) { - node = p || document.focusNode; + node = p || document?.focusNode; treeNode = tree.getTreeNode(node); focusSlots = false; } @@ -258,7 +259,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab cancelIdleCallback(this.tryScrollAgain); this.tryScrollAgain = null; } - if (this.sensing || !this.bounds || !this.scroller || !this.scrollTarget) { + if (!this.bounds || !this.scroller || !this.scrollTarget) { // is a active sensor return; } @@ -305,7 +306,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab focus = { type: 'slots' }; } else { index = 0; - valid = document.checkNesting(target, event.dragObject as any); + valid = !!document?.checkNesting(target, event.dragObject as any); } canvas.createLocation({ target, @@ -320,23 +321,28 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab }); }); - private getNear(treeNode: TreeNode, e: IPublicModelLocateEvent, index?: number, rect?: DOMRect) { + private getNear(treeNode: TreeNode, e: IPublicModelLocateEvent, originalIndex?: number, originalRect?: DOMRect) { const { canvas, project } = this.pluginContext; const document = project.getCurrentDocument(); const { globalY, dragObject } = e; + if (!dragObject) { + return null; + } // TODO: check dragObject is anyData const { node, expanded } = treeNode; + let rect = originalRect; if (!rect) { rect = this.getTreeNodeRect(treeNode); if (!rect) { return null; } } + let index = originalIndex; if (index == null) { index = node.index; } - if (node.isSlot) { + if (node.isSlotNode) { // 是个插槽根节点 if (!treeNode.isContainer() && !treeNode.hasSlots()) { return canvas.createLocation({ @@ -385,7 +391,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab detail: { type: IPublicTypeLocationDetailType.Children, index, - valid: document.checkNesting(node.parent!, dragObject as any), + valid: document?.checkNesting(node.parent!, dragObject as any), near: { node, pos: 'before' }, focus: checkRecursion(focusNode, dragObject) ? { type: 'node', node: focusNode } : undefined, }, @@ -412,7 +418,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab detail: { type: IPublicTypeLocationDetailType.Children, index: index + 1, - valid: document.checkNesting(node.parent!, dragObject as any), + valid: document?.checkNesting(node.parent!, dragObject as any), near: { node, pos: 'after' }, focus: checkRecursion(focusNode, dragObject) ? { type: 'node', node: focusNode } : undefined, }, @@ -423,6 +429,9 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab const { canvas, project } = this.pluginContext; const document = project.getCurrentDocument(); const { dragObject, globalY } = e; + if (!dragObject) { + return null; + } if (!checkRecursion(treeNode.node, dragObject)) { return null; @@ -454,7 +463,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab detail.valid = false; } else { detail.index = 0; - detail.valid = document.checkNesting(container, dragObject); + detail.valid = document?.checkNesting(container, dragObject); } } @@ -526,7 +535,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab } else { detail.index = l; } - detail.valid = document.checkNesting(container, dragObject); + detail.valid = document?.checkNesting(container, dragObject); } return canvas.createLocation(locationData); @@ -572,9 +581,26 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab return; } this._shell = shell; + const { canvas, project } = this.pluginContext; if (shell) { - this._scrollTarget = this.pluginContext.canvas.createScrollTarget(shell); + this._scrollTarget = canvas.createScrollTarget(shell); this._sensorAvailable = true; + + // check if there is current selection and scroll to it + const selection = project.currentDocument?.selection; + const topNodes = selection?.getTopNodes(true); + const tree = this.treeMaster?.currentTree; + if (topNodes && topNodes[0] && tree) { + const treeNode = tree.getTreeNodeById(topNodes[0].id); + if (treeNode) { + // at this moment, it is possible that pane is not ready yet, so + // put ui related operations to the next loop + setTimeout(() => { + tree.setNodeSelected(treeNode.id); + this.scrollToNode(treeNode, null, 4); + }, 0); + } + } } else { this._scrollTarget = undefined; this._sensorAvailable = false; @@ -610,7 +636,7 @@ export class PaneController implements ISensor, ITreeBoard, IPublicModelScrollab } } -function checkRecursion(parent: IPublicModelNode | undefined | null, dragObject: DragObject): boolean { +function checkRecursion(parent: IPublicModelNode | undefined | null, dragObject: IPublicModelDragObject): boolean { if (!parent) { return false; } @@ -633,14 +659,14 @@ function getPosFromEvent( if (target.matches('.insertion')) { return 'unchanged'; } - target = target.closest('[data-id]'); - if (!target || !stop.contains(target)) { + const closest = target.closest('[data-id]'); + if (!closest || !stop.contains(closest)) { return null; } - const nodeId = (target as HTMLDivElement).dataset.id!; + const nodeId = (closest as HTMLDivElement).dataset.id!; return { - focusSlots: target.matches('.tree-node-slots'), + focusSlots: closest.matches('.tree-node-slots'), nodeId, }; } diff --git a/packages/plugin-outline-pane/src/controllers/ric-shim.d.ts b/packages/plugin-outline-pane/src/controllers/ric-shim.d.ts new file mode 100644 index 000000000..74f3fbd94 --- /dev/null +++ b/packages/plugin-outline-pane/src/controllers/ric-shim.d.ts @@ -0,0 +1 @@ +declare module 'ric-shim'; \ No newline at end of file diff --git a/packages/plugin-outline-pane/src/controllers/tree-master.ts b/packages/plugin-outline-pane/src/controllers/tree-master.ts index c660f0457..38b723a98 100644 --- a/packages/plugin-outline-pane/src/controllers/tree-master.ts +++ b/packages/plugin-outline-pane/src/controllers/tree-master.ts @@ -1,5 +1,5 @@ import { isLocationChildrenDetail } from '@alilc/lowcode-utils'; -import { IPublicModelPluginContext, IPublicTypeActiveTarget, IPublicModelNode, IPublicEnumEventNames } from '@alilc/lowcode-types'; +import { IPublicModelPluginContext, IPublicTypeActiveTarget, IPublicModelNode } from '@alilc/lowcode-types'; import TreeNode from './tree-node'; import { Tree } from './tree'; @@ -54,8 +54,8 @@ export class TreeMaster { time: (endTime - startTime).toFixed(2), }); }); - event.on(IPublicEnumEventNames.DESIGNER_DOCUMENT_REMOVE, (doc) => { - const { id } = doc as any; + project.onRemoveDocument((data: {id: string}) => { + const { id } = data; this.treeMap.delete(id); }); } diff --git a/packages/plugin-outline-pane/src/controllers/tree-node.ts b/packages/plugin-outline-pane/src/controllers/tree-node.ts index daac5bc2c..b6063e724 100644 --- a/packages/plugin-outline-pane/src/controllers/tree-node.ts +++ b/packages/plugin-outline-pane/src/controllers/tree-node.ts @@ -24,6 +24,10 @@ export interface FilterResult { export default class TreeNode { readonly pluginContext: IPublicModelPluginContext; onFilterResultChanged: () => void; + onExpandedChanged: (expanded: boolean) => void; + onHiddenChanged: (hidden: boolean) => void; + onLockedChanged: (locked: boolean) => void; + onTitleLabelChanged: (treeNode: TreeNode) => void; get id(): string { return this.node.id; @@ -87,7 +91,7 @@ export default class TreeNode { setExpanded(value: boolean) { this._expanded = value; - this.pluginContext.pluginEvent.emit('tree-node.expandedChanged', { expanded: value, nodeId: this.id }); + this.onExpandedChanged && this.onExpandedChanged(value); } get detecting() { @@ -108,7 +112,7 @@ export default class TreeNode { return; } this.node.setVisible(!flag); - this.pluginContext.pluginEvent.emit('tree-node.hiddenChanged', { hidden: flag, nodeId: this.id }); + this.onHiddenChanged && this.onHiddenChanged(flag); } get locked(): boolean { @@ -117,7 +121,7 @@ export default class TreeNode { setLocked(flag: boolean) { this.node.lock(flag); - this.pluginContext.pluginEvent.emit('tree-node.lockedChanged', { locked: flag, nodeId: this.id }); + this.onLockedChanged && this.onLockedChanged(flag); } get selected(): boolean { @@ -162,7 +166,7 @@ export default class TreeNode { } else { this.node.getExtraProp('title', true)?.setValue(label); } - this.pluginContext.pluginEvent.emit('tree-node.titleLabelChanged', { titleLabel: label, nodeId: this.id }); + this.onTitleLabelChanged && this.onTitleLabelChanged(this); } get icon() { diff --git a/packages/plugin-outline-pane/src/controllers/tree.ts b/packages/plugin-outline-pane/src/controllers/tree.ts index b99e04f64..a4106088b 100644 --- a/packages/plugin-outline-pane/src/controllers/tree.ts +++ b/packages/plugin-outline-pane/src/controllers/tree.ts @@ -20,6 +20,15 @@ export class Tree { this.id = this.pluginContext.project.currentDocument?.id; } + setNodeSelected(nodeId: string): void { + // 目标节点选中,其他节点展开 + const treeNode = this.treeNodesMap.get(nodeId); + if (!treeNode) { + return; + } + this.expandAllAncestors(treeNode); + } + getTreeNode(node: IPublicModelNode): TreeNode { if (this.treeNodesMap.has(node.id)) { const tnode = this.treeNodesMap.get(node.id)!; @@ -36,7 +45,29 @@ export class Tree { return this.treeNodesMap.get(id); } - expandAllDecendants(treeNode: TreeNode | undefined) { + expandAllAncestors(treeNode: TreeNode | undefined | null) { + if (!treeNode) { + return; + } + if (treeNode.isRoot()) { + return; + } + const ancestors = []; + let currentNode: TreeNode | null | undefined = treeNode; + while (!treeNode.isRoot()) { + currentNode = currentNode?.parent; + if (currentNode) { + ancestors.unshift(currentNode); + } else { + break; + } + } + ancestors.forEach((ancestor) => { + ancestor.setExpanded(true); + }); + } + + expandAllDecendants(treeNode: TreeNode | undefined | null) { if (!treeNode) { return; } @@ -49,7 +80,7 @@ export class Tree { } } - collapseAllDecendants(treeNode: TreeNode | undefined) { + collapseAllDecendants(treeNode: TreeNode | undefined | null): void { if (!treeNode) { return; } diff --git a/packages/plugin-outline-pane/src/helper/tree-title-extra.ts b/packages/plugin-outline-pane/src/helper/tree-title-extra.ts deleted file mode 100644 index b1bcbf6ed..000000000 --- a/packages/plugin-outline-pane/src/helper/tree-title-extra.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { globalContext } from '@alilc/lowcode-editor-core'; -import { ReactElement } from 'react'; - -const TREE_TITLE_EXTRA_KEY = 'TREE_TITLE_EXTRA_KEY'; - -export const registerTreeTitleExtra = (extra: ReactElement) => { - if (extra && !globalContext.has(TREE_TITLE_EXTRA_KEY)) { - globalContext.register(extra, TREE_TITLE_EXTRA_KEY); - } -}; - -export const getTreeTitleExtra = () => { - try { - return globalContext.get(TREE_TITLE_EXTRA_KEY); - } catch (e) { - // console.error('getTreeTitleExtra Error', e); - } - - return null; -}; diff --git a/packages/plugin-outline-pane/src/index.tsx b/packages/plugin-outline-pane/src/index.tsx index 0a6ef527c..bf3c95b1a 100644 --- a/packages/plugin-outline-pane/src/index.tsx +++ b/packages/plugin-outline-pane/src/index.tsx @@ -1,11 +1,13 @@ import { Pane } from './views/pane'; import { IconOutline } from './icons/outline'; -import { IPublicModelPluginContext, IPublicEnumEventNames } from '@alilc/lowcode-types'; +import { IPublicModelPluginContext, IPublicModelDocumentModel } from '@alilc/lowcode-types'; import { enUS, zhCN } from './locale'; import { MasterPaneName, BackupPaneName } from './helper/consts'; +import { getTreeMaster } from './controllers/tree-master'; +import { PaneController } from './controllers/pane-controller'; export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { - const { skeleton, config, common, event, canvas } = ctx; + const { skeleton, config, common, event, canvas, project } = ctx; const { intl, intlNode, getLocale } = common.utils.createIntl({ 'en-US': enUS, 'zh-CN': zhCN, @@ -24,7 +26,9 @@ export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { masterPane: false, backupPane: false, }; - + const treeMaster = getTreeMaster(ctx); + let masterPaneController: PaneController | null = null; + let backupPaneController: PaneController | null = null; return { async init() { skeleton.add({ @@ -39,10 +43,13 @@ export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { description: intlNode('Outline Tree'), }, content: (props: any) => { + masterPaneController = new PaneController(MasterPaneName, ctx, treeMaster); return ( ); @@ -65,12 +72,17 @@ export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { props: { hiddenWhenInit: true, }, - content: (props: any) => ( - - ), + content: (props: any) => { + backupPaneController = new PaneController(BackupPaneName, ctx, treeMaster); + return ( + + ); + }, }); // 处理 master pane 和 backup pane 切换 @@ -91,8 +103,7 @@ export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { canvas.dragon?.onDragend(() => { switchPanes(); }); - - event.on(IPublicEnumEventNames.SKELETON_PANEL_SHOW, (key: string) => { + skeleton.onShowPanel((key: string) => { if (key === MasterPaneName) { showingPanes.masterPane = true; } @@ -100,7 +111,7 @@ export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { showingPanes.backupPane = true; } }); - event.on(IPublicEnumEventNames.SKELETON_PANEL_HIDE, (key: string) => { + skeleton.onHidePanel((key: string) => { if (key === MasterPaneName) { showingPanes.masterPane = false; switchPanes(); @@ -109,6 +120,25 @@ export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { showingPanes.backupPane = false; } }); + project.onChangeDocument((document: IPublicModelDocumentModel) => { + if (!document) { + return; + } + + const { selection } = document; + + selection?.onSelectionChange(() => { + const selectedNodes = selection?.getNodes(); + if (!selectedNodes || selectedNodes.length === 0) { + return; + } + const tree = treeMaster.currentTree; + selectedNodes.forEach((node) => { + const treeNode = tree?.getTreeNodeById(node.id); + tree?.expandAllAncestors(treeNode); + }); + }); + }); }, }; }; diff --git a/packages/plugin-outline-pane/src/views/pane.tsx b/packages/plugin-outline-pane/src/views/pane.tsx index 6d037eb5b..1ed4c74ba 100644 --- a/packages/plugin-outline-pane/src/views/pane.tsx +++ b/packages/plugin-outline-pane/src/views/pane.tsx @@ -4,35 +4,29 @@ import TreeView from './tree'; import './style.less'; import { IPublicModelPluginContext } from '@alilc/lowcode-types'; import Filter from './filter'; -import { registerTreeTitleExtra } from '../helper/tree-title-extra'; -import { getTreeMaster, TreeMaster } from '../controllers/tree-master'; +import { TreeMaster } from '../controllers/tree-master'; export class Pane extends Component<{ config: any; pluginContext: IPublicModelPluginContext; + treeMaster: TreeMaster; + controller: PaneController; }> { private controller; private treeMaster: TreeMaster; constructor(props: any) { super(props); - this.treeMaster = getTreeMaster(this.props.pluginContext); - this.controller = new PaneController( - this.props.config.name || this.props.config.pluginKey, - this.props.pluginContext, - this.treeMaster, - ); + const { controller, treeMaster } = props; + this.treeMaster = treeMaster; + this.controller = controller; } componentWillUnmount() { this.controller.purge(); } - componentDidMount() { - registerTreeTitleExtra(this.props?.config?.contentProps?.treeTitleExtra); - } - render() { const tree = this.treeMaster.currentTree; diff --git a/packages/plugin-outline-pane/src/views/root-tree-node.tsx b/packages/plugin-outline-pane/src/views/root-tree-node.tsx deleted file mode 100644 index f82783835..000000000 --- a/packages/plugin-outline-pane/src/views/root-tree-node.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { Component } from 'react'; -import classNames from 'classnames'; -import TreeNode from '../controllers/tree-node'; -import TreeTitle from './tree-title'; -import TreeBranches from './tree-branches'; -import { IconEyeClose } from '../icons/eye-close'; -import { IPublicModelPluginContext, IPublicModelModalNodesManager, IPublicEnumEventNames } from '@alilc/lowcode-types'; - -class ModalTreeNodeView extends Component<{ - treeNode: TreeNode; - pluginContext: IPublicModelPluginContext; -}> { - private modalNodesManager: IPublicModelModalNodesManager | undefined | null; - readonly pluginContext: IPublicModelPluginContext; - - constructor(props: any) { - super(props); - - // 模态管理对象 - this.pluginContext = props.pluginContext; - const { project } = this.pluginContext; - this.modalNodesManager = project.currentDocument?.modalNodesManager; - } - - hideAllNodes() { - this.modalNodesManager?.hideModalNodes(); - } - - render() { - const { treeNode } = this.props; - // 当指定了新的根节点时,要从原始的根节点去获取模态节点 - const { project } = this.pluginContext; - const rootNode = project.currentDocument?.root; - const rootTreeNode = treeNode.tree.getTreeNode(rootNode!); - const modalNodes = rootTreeNode.children?.filter((item) => { - return item.node.componentMeta?.isModal; - }); - if (!modalNodes || modalNodes.length === 0) { - return null; - } - - const hasVisibleModalNode = !!this.modalNodesManager?.getVisibleModalNode(); - return ( -
-
- 模态视图层 -
- {hasVisibleModalNode ? : null} -
-
-
- -
-
- ); - } -} - -export default class RootTreeNodeView extends Component<{ - treeNode: TreeNode; - pluginContext: IPublicModelPluginContext; -}> { - state = { - expanded: false, - selected: false, - hidden: false, - locked: false, - detecting: false, - isRoot: false, - highlight: false, - dropping: false, - conditionFlow: false, - }; - - eventOffCallbacks: Array<() => void> = []; - - componentDidMount() { - const { treeNode, pluginContext } = this.props; - const { pluginEvent, event } = pluginContext; - const { id } = treeNode; - - this.state = { - expanded: false, - selected: false, - hidden: false, - locked: false, - detecting: false, - isRoot: treeNode.isRoot(), - // 是否投放响应 - dropping: treeNode.dropDetail?.index != null, - conditionFlow: treeNode.node.conditionGroup != null, - highlight: treeNode.isFocusingNode(), - }; - - const doc = pluginContext.project.currentDocument; - - this.eventOffCallbacks.push(pluginEvent.on('tree-node.hiddenChanged', (payload: any) => { - const { hidden, nodeId } = payload; - if (nodeId === id) { - this.setState({ hidden }); - } - })); - - this.eventOffCallbacks.push(pluginEvent.on('tree-node.expandedChanged', (payload: any) => { - const { expanded, nodeId } = payload; - if (nodeId === id) { - this.setState({ expanded }); - } - })); - - this.eventOffCallbacks.push(pluginEvent.on('tree-node.lockedChanged', (payload: any) => { - const { locked, nodeId } = payload; - if (nodeId === id) { - this.setState({ locked }); - } - })); - - this.eventOffCallbacks.push( - event.on( - IPublicEnumEventNames.DOCUMENT_DROPLOCATION_CHANGED, - (payload: any) => { - const { document } = payload; - if (document.id === doc?.id) { - this.setState({ - dropping: treeNode.dropDetail?.index != null, - }); - } - }, - ), - ); - - const offSelectionChange = doc?.selection?.onSelectionChange(() => { - this.setState({ selected: treeNode.selected }); - }); - this.eventOffCallbacks.push(offSelectionChange!); - const offDetectingChange = doc?.detecting?.onDetectingChange(() => { - this.setState({ detecting: treeNode.detecting }); - }); - this.eventOffCallbacks.push(offDetectingChange!); - } - - componentWillUnmount(): void { - this.eventOffCallbacks?.forEach((offFun: () => void) => { - offFun(); - }); - } - - render() { - const { treeNode } = this.props; - const className = classNames('tree-node', { - // 是否展开 - expanded: this.state.expanded, - // 是否选中的 - selected: this.state.selected, - // 是否隐藏的 - hidden: this.state.hidden, - // 是否锁定的 - locked: this.state.locked, - // 是否悬停中 - detecting: this.state.detecting, - // 是否投放响应 - dropping: this.state.dropping, - 'is-root': this.state.isRoot, - 'condition-flow': this.state.conditionFlow, - highlight: this.state.highlight, - }); - - return ( -
- - - -
- ); - } -} diff --git a/packages/plugin-outline-pane/src/views/tree-branches.tsx b/packages/plugin-outline-pane/src/views/tree-branches.tsx index 5dfdafb72..9af2dbd09 100644 --- a/packages/plugin-outline-pane/src/views/tree-branches.tsx +++ b/packages/plugin-outline-pane/src/views/tree-branches.tsx @@ -2,38 +2,33 @@ import { Component } from 'react'; import classNames from 'classnames'; import TreeNode from '../controllers/tree-node'; import TreeNodeView from './tree-node'; -import { IPublicModelPluginContext, IPublicModelExclusiveGroup, IPublicEnumEventNames, IPublicTypeLocationDetailType } from '@alilc/lowcode-types'; +import { IPublicModelPluginContext, IPublicModelExclusiveGroup } from '@alilc/lowcode-types'; export default class TreeBranches extends Component<{ treeNode: TreeNode; isModal?: boolean; pluginContext: IPublicModelPluginContext; + expanded: boolean; }> { state = { - expanded: false, filterWorking: false, matchChild: false, }; private offExpandedChanged: (() => void) | null; - componentDidMount() { - const { treeNode, pluginContext } = this.props; - const { expanded } = treeNode; - const { pluginEvent } = pluginContext; + constructor(props: any) { + super(props); + const { treeNode } = this.props; const { filterWorking, matchChild } = treeNode.filterReult; - this.setState({ expanded, filterWorking, matchChild }); + this.setState({ filterWorking, matchChild }); + } + + componentDidMount() { + const { treeNode } = this.props; treeNode.onFilterResultChanged = () => { const { filterWorking: newFilterWorking, matchChild: newMatchChild } = treeNode.filterReult; this.setState({ filterWorking: newFilterWorking, matchChild: newMatchChild }); }; - - this.offExpandedChanged = pluginEvent.on('tree-node.expandedChanged', (payload: any) => { - const { expanded: value, nodeId } = payload; - const { id } = this.props.treeNode; - if (nodeId === id) { - this.setState({ expanded: value }); - } - }); } componentWillUnmount(): void { @@ -43,8 +38,8 @@ export default class TreeBranches extends Component<{ } render() { - const { treeNode, isModal } = this.props; - const { filterWorking, matchChild, expanded } = this.state; + const { treeNode, isModal, expanded } = this.props; + const { filterWorking, matchChild } = this.state; // 条件过滤生效时,如果命中了子节点,需要将该节点展开 const expandInFilterResult = filterWorking && matchChild; @@ -57,7 +52,11 @@ export default class TreeBranches extends Component<{ { !isModal && } - + ); } @@ -77,7 +76,7 @@ class TreeNodeChildren extends Component<{ offLocationChanged: () => void; componentDidMount() { const { treeNode, pluginContext } = this.props; - const { event } = pluginContext; + const { project } = pluginContext; const { filterWorking, matchSelf, keywords } = treeNode.filterReult; const { dropDetail } = treeNode; this.setState({ @@ -98,11 +97,10 @@ class TreeNodeChildren extends Component<{ keywords: newKeywords, }); }; - this.offLocationChanged = event.on( - IPublicEnumEventNames.DOCUMENT_DROPLOCATION_CHANGED, - (payload: any) => { - this.setState({ dropDetail: treeNode.dropDetail }); - }, + this.offLocationChanged = project.currentDocument?.onDropLocationChanged( + () => { + this.setState({ dropDetail: treeNode.dropDetail }); + }, ); } componentWillUnmount(): void { diff --git a/packages/plugin-outline-pane/src/views/tree-node.tsx b/packages/plugin-outline-pane/src/views/tree-node.tsx index 72b68aef3..091530706 100644 --- a/packages/plugin-outline-pane/src/views/tree-node.tsx +++ b/packages/plugin-outline-pane/src/views/tree-node.tsx @@ -3,12 +3,67 @@ import classNames from 'classnames'; import TreeNode from '../controllers/tree-node'; import TreeTitle from './tree-title'; import TreeBranches from './tree-branches'; -import { IPublicModelPluginContext, IPublicEnumEventNames } from '@alilc/lowcode-types'; +import { IconEyeClose } from '../icons/eye-close'; +import { IPublicModelPluginContext, IPublicModelModalNodesManager, IPublicModelDocumentModel } from '@alilc/lowcode-types'; + +class ModalTreeNodeView extends Component<{ + treeNode: TreeNode; + pluginContext: IPublicModelPluginContext; +}> { + private modalNodesManager: IPublicModelModalNodesManager | undefined | null; + readonly pluginContext: IPublicModelPluginContext; + + constructor(props: any) { + super(props); + + // 模态管理对象 + this.pluginContext = props.pluginContext; + const { project } = this.pluginContext; + this.modalNodesManager = project.currentDocument?.modalNodesManager; + } + + hideAllNodes() { + this.modalNodesManager?.hideModalNodes(); + } + + render() { + const { treeNode } = this.props; + // 当指定了新的根节点时,要从原始的根节点去获取模态节点 + const { project } = this.pluginContext; + const rootNode = project.currentDocument?.root; + const rootTreeNode = treeNode.tree.getTreeNode(rootNode!); + const expanded = rootTreeNode.expanded; + + const hasVisibleModalNode = !!this.modalNodesManager?.getVisibleModalNode(); + return ( +
+
+ 模态视图层 +
+ {hasVisibleModalNode ? : null} +
+
+
+ +
+
+ ); + } +} export default class TreeNodeView extends Component<{ treeNode: TreeNode; isModal?: boolean; pluginContext: IPublicModelPluginContext; + isRootNode: boolean; }> { state = { expanded: false, @@ -20,64 +75,51 @@ export default class TreeNodeView extends Component<{ highlight: false, dropping: false, conditionFlow: false, + expandable: false, }; eventOffCallbacks: Array<() => void> = []; + constructor(props: any) { + super(props); - componentDidMount() { - const { treeNode, pluginContext } = this.props; - const { pluginEvent, event } = pluginContext; - const { id } = treeNode; - + const { treeNode, isRootNode } = this.props; this.state = { - expanded: false, - selected: false, - hidden: false, - locked: false, - detecting: false, + expanded: isRootNode ? true : treeNode.expanded, + selected: treeNode.selected, + hidden: treeNode.hidden, + locked: treeNode.locked, + detecting: treeNode.detecting, isRoot: treeNode.isRoot(), // 是否投放响应 dropping: treeNode.dropDetail?.index != null, conditionFlow: treeNode.node.conditionGroup != null, highlight: treeNode.isFocusingNode(), + expandable: treeNode.expandable, + }; + } + + componentDidMount() { + const { treeNode, pluginContext } = this.props; + const { event, project } = pluginContext; + + const doc = project.currentDocument; + + treeNode.onExpandedChanged = ((expanded: boolean) => { + this.setState({ expanded }); + }); + treeNode.onHiddenChanged = (hidden: boolean) => { + this.setState({ hidden }); + }; + treeNode.onLockedChanged = (locked: boolean) => { + this.setState({ locked }); }; - const doc = pluginContext.project.currentDocument; - - - this.eventOffCallbacks.push(pluginEvent.on('tree-node.hiddenChanged', (payload: any) => { - const { hidden, nodeId } = payload; - if (nodeId === id) { - this.setState({ hidden }); - } - })); - - this.eventOffCallbacks.push(pluginEvent.on('tree-node.expandedChanged', (payload: any) => { - const { expanded, nodeId } = payload; - if (nodeId === id) { - this.setState({ expanded }); - } - })); - - this.eventOffCallbacks.push(pluginEvent.on('tree-node.lockedChanged', (payload: any) => { - const { locked, nodeId } = payload; - if (nodeId === id) { - this.setState({ locked }); - } - })); - this.eventOffCallbacks.push( - event.on( - IPublicEnumEventNames.DOCUMENT_DROPLOCATION_CHANGED, - (payload: any) => { - const { document } = payload; - if (document.id === doc?.id) { - this.setState({ - dropping: treeNode.dropDetail?.index != null, - }); - } - }, - ), + doc?.onDropLocationChanged((document: IPublicModelDocumentModel) => { + this.setState({ + dropping: treeNode.dropDetail?.index != null, + }); + }), ); const offSelectionChange = doc?.selection?.onSelectionChange(() => { @@ -95,8 +137,25 @@ export default class TreeNodeView extends Component<{ }); } + shouldShowModalTreeNode(): boolean { + const { treeNode, isRootNode, pluginContext } = this.props; + if (!isRootNode) { + // 只在 当前树 的根节点展示模态节点 + return false; + } + + // 当指定了新的根节点时,要从原始的根节点去获取模态节点 + const { project } = pluginContext; + const rootNode = project.currentDocument?.root; + const rootTreeNode = treeNode.tree.getTreeNode(rootNode!); + const modalNodes = rootTreeNode.children?.filter((item) => { + return item.node.componentMeta?.isModal; + }); + return !!(modalNodes && modalNodes.length > 0); + } + render() { - const { treeNode, isModal } = this.props; + const { treeNode, isModal, isRootNode } = this.props; const className = classNames('tree-node', { // 是否展开 expanded: this.state.expanded, @@ -114,24 +173,39 @@ export default class TreeNodeView extends Component<{ 'condition-flow': this.state.conditionFlow, highlight: this.state.highlight, }); + let shouldShowModalTreeNode: boolean = this.shouldShowModalTreeNode(); + // filter 处理 const { filterWorking, matchChild, matchSelf } = treeNode.filterReult; - - // 条件过滤生效时,如果未命中本节点或子节点,则不展示该节点 - if (filterWorking && !matchChild && !matchSelf) { + if (!isRootNode && filterWorking && !matchChild && !matchSelf) { + // 条件过滤生效时,如果未命中本节点或子节点,则不展示该节点 + // 根节点始终展示 return null; } - return ( -
+
diff --git a/packages/plugin-outline-pane/src/views/tree-title.tsx b/packages/plugin-outline-pane/src/views/tree-title.tsx index 93f822988..92fac1bba 100644 --- a/packages/plugin-outline-pane/src/views/tree-title.tsx +++ b/packages/plugin-outline-pane/src/views/tree-title.tsx @@ -21,6 +21,10 @@ function emitOutlineEvent(event: IPublicApiEvent, type: string, treeNode: TreeNo export default class TreeTitle extends Component<{ treeNode: TreeNode; isModal?: boolean; + expanded: boolean; + hidden: boolean; + locked: boolean; + expandable: boolean; pluginContext: IPublicModelPluginContext; }> { state: { @@ -73,30 +77,18 @@ export default class TreeTitle extends Component<{ // 光标定位最后一个 // input.selectionStart = input.selectionEnd; }; - offTitleLabelChanged: (() => void) | undefined; componentDidMount() { - const { treeNode, pluginContext } = this.props; - const { id } = treeNode; - const { pluginEvent } = pluginContext; + const { treeNode } = this.props; this.setState({ editing: false, title: treeNode.titleLabel, }); - this.offTitleLabelChanged = pluginEvent.on('tree-node.titleLabelChanged', (payload: any) => { - const { nodeId } = payload; - if (nodeId === id) { - this.setState({ - title: treeNode.titleLabel, - }); - } - }); - } - - componentWillUnmount(): void { - if (this.offTitleLabelChanged) { - this.offTitleLabelChanged(); - } + treeNode.onTitleLabelChanged = () => { + this.setState({ + title: treeNode.titleLabel, + }); + }; } render() { @@ -158,7 +150,7 @@ export default class TreeTitle extends Component<{
)} - {isCNode && } + {isCNode && }
{createIcon(treeNode.icon)}
{editing ? ( @@ -200,8 +192,8 @@ export default class TreeTitle extends Component<{ )}
- {shouldShowHideBtn && } - {shouldShowLockBtn && } + {shouldShowHideBtn &&