前端通过连接后端核心库,为客户提供分组的功能
当通过ctrl模式一次性选中多个组件时,可以通过右侧的功能栏的分组按钮进行分组。完成分组中,组内元素可以有统一的移动和其他特性
1.功能介绍
关于分组有两个功能:link和group
group的实现基于link。
link:在组件之间建立父子关系。之后父组件的行动会影响子组件,但是子组件的行动不会影响父组件。
group:在组件之间建立分组关系。组内元素可以一起移动。
2.功能实现分析
可以看出 link是实现group的基础,但是link和group之间又有许多不同之处。
通过后端的三个信号,实现这两个功能。
link:
link <id1:str> <id2:str>
将id1作为id2的儿子。
unlink:
unlink <id1:str> <id2:str>
移除id1与id2间的父子关系,顺序任意。
即使id1和id2间没有父子关系,也能返回成功。
cover_children
cover_children
使矩形自适应包裹所有子组件。
link的实现可以依靠信号,但是group的实现需要前端进行许多添加
group实现分析
前端向后端请求建立一个rect,并在rect和所有组内元素之间建立link关系,同时隐藏rect,这样可以做到点击空白处并不会唤醒分组。
ungroup的时候,将rect进行删除。
整体逻辑较为清晰,但是在实现的时候会遇到各种各样的问题
3.功能详细介绍
link界面:暂时通过右侧的功能栏进行实现。
实际效果如下
该右侧栏的主要功能是为用户提供unlink和link的端口。
并提供组件的树形结构视图。
用户通过input框要求的指令进行link和unlink,并可以通过选中组件查看组件的树形结构视图。
group的功能
通过group按钮进行分组,并通过ungroup按钮进行解除分组后,分组后选中元素会首先定位到整个分组,再次点击后层级游标向下移动。
eg:
样例为两层group,第一次点击右下角组件时,如图所示
再次点击时,游标会在树形结构中进行下滑。
移动后外层包裹的层层rect会进行自动放缩
4.具体实现介绍
A.组件树的建立
需要用树形结构来保存组件的树形逻辑。
class Module_tree {
this.head=[]
this.aliasList={}
this.nameList={}
this.nodeList={}//id not alias
}
参数说明:
head为整张图的结构,为森林结构
aliasList和nameList为id<–>alias的双向映射,通过为用户提供别名而避开繁杂的id。
nodelist为id到组件在head中index的索引
B.向组件树中添加组件
在组件创建之后,需要在组件树种进行注册。同时为组件创建别名并注册。
addModule(id,type){
let counter=++aliasCounter[type];
this.aliasList[type+"_"+counter]=id;
this.nameList[id]=type+"_"+counter;
// console.log(this.aliasList);
// console.log(this.nameList);
let item={};
item['id']=this.nameList[id];
item['children']=[];
item['tid']=this.head.length;
item['father']=this.head.length;
this.nodeList[id]=this.head.length;
this.head.push(item);
// console.log(this.head);
}
C.在组件之间建立link
可以通过别名和id进行设置,不过只是进行简单的替换之后调用即可。
link后,将子组件的索引保存在父组件的children属性中,并保存父组件的索引,方便进行双向查找
linkInTree(id1,id2){
id1=this.nameList[id1];
id2=this.nameList[id2];
let module1="";
let module2="";
//找到两个节点
for(var i=0;i<this.head.length;i++){
if(this.head[i]['id']===id1){
module1=i;
break;
}
}
for(var i=0;i<this.head.length;i++){
if(this.head[i]['id']===id2){
module2=this.head[i];
this.head[module1]['father']=module2['tid'];
module2['children'].push(this.head[module1]['tid']);
break;
}
}
console.log(this.head)
}
D.提供某一个组件的完整树形结构
分为两个部分,第一个部分为寻找祖先,第二个部分为从祖先开始进行记录,记录时通过bfs对node和link进行记录,并调用echart进行图形绘制。
getRelationById(id){
let now=this.nodeList[id];
// console.log(now);
// console.log(this.head);
while(true){
if(this.head[now]['tid']!==this.head[now]['father']){
now=this.head[now]['father'];
}else{
break;
}
}
let msg=this.getTree(now,0);
return msg
// console.log(id);
// console.log(list)
}
getTree(now,width){
let linkList=[];
let nodeList=[];
let queue=[];
let node={id:this.head[now]['id'],width:width,tid:this.head[now]['tid']}
queue.push(node);
while(queue.length!==0){
let node=queue[0];
queue.splice(0,1);
nodeList.push(node);
let tid=node['tid'];
let children=this.head[tid]['children'];
for(let i=0;i<children.length;i++){
let child=this.head[children[i]];
let nod={id:child['id'],width:node['width']+1,tid:child['tid']};
linkList.push({source:node['id'],target:nod['id']});
queue.push(nod)
}
}
let msg={node:nodeList,link:linkList};
return msg;
}
E.删除组件后的消除动作
删除一个组件后,需要对它的儿子和父亲进行处理后再进行删除。将父亲的子节点指向自己的儿子,同时将儿子的子节点指向自己的父亲,需要注意的是,当自己没有父亲的时候,儿子的父亲改为指向自己(这是一个特判)
deleteById(id){
let now=this.nodeList[id];
let father=this.head[now]['father']
if(father!==now){
father=this.head[father];
for(let i=0;i<this.head[now]['children'].length;i++){
father['children'].push(this.head[now]['children'][i])
}
for(let i=0;i<this.head[now]['children'].length;i++){
if(father['children'][i]===this.head[now]['tid']){
father['children'].splice(i,1);
}
}
}
let children=this.head[now]['children']
for(let i=0;i<children.length;i++){
let child=this.head[children[i]]['tid'];
child=this.head[child];
child['father']=father===now?child['tid']:father['tid'];
}
this.head[now]["tid"]=undefined
this.head[now]["father"]=undefined
this.head[now]["children"]=undefined
this.head[now]["id"]=undefined
console.log(this.head)
}
至此:我们提供了link给group所需要的大部分功能,但是还有细节需要进行处理。
5.细节处理
A.建立group的时候,需要创建一个rect并进行隐藏。
由于group没有专用的指令进行执行,我们需要修改一部分代码。通过在创建rect的时候添加一个多余的信号show标识该组件是否为group所用rect进行区分,并对addAction的after进行修改。
this.msg['show']=this.cmd['show'];
if(!this.cmd['show']){
let node=document.getElementById(this.id);
let parser=new cssParser();
parser.parseStyle(node.getAttribute("style"))
parser.updateStyle({display:"none"})
parser.updateStyle({'stroke-width':0})
node.setAttribute("style",parser.get());
createModule(this.msg,false);
addModuleToTree(this.id,this.type);
let coreList=getCoreList();
for (var i=0;i<coreList.length;i++){
linkByGroup(coreList[i],this.id);
}
P("cursors",{ids:[this.msg['id']]})
P("cover_children",{})
return;
}
createModule(this.msg);//需要修改
P("cursors",{ids:[this.msg['id']]})
addModuleToTree(this.id,this.type);
这样我们就完成了隐藏功能,同时对cover进行了修改。
B.点击组件后所选中的组件
这里有点麻烦,因为我们需要考虑当前祖先情况,同时需要避开link而只考虑group的rect。
思路:我们先构造一个方法,获取所点击组件的所有祖先,然后遍历祖先两轮,第一轮先遍历是否有祖先被选中,如果有的话,选中下一个group节点(当选中节点为自己是保持不变)
如果没有的话,第二轮遍历从祖先开始,寻找第一个show=false(group_rect)的节点
if(that.move){
that.move=false;
}else if(that.isCore===false) {
let ancestor=undefined;
let ancestorList=getAncestorAll(that.g_id);
for(let i=0;i<ancestorList.length;i++) {
let el = getModuleByGid(ancestorList[i])
if(el.isCore){
i++;
while(i<ancestorList.length){
el=getModuleByGid(ancestorList[i])
if(!el.show)ancestor=el.g_id;
break;
}
if (ancestor===undefined){
ancestor=that.g_id;
}
pushElementInQueue([ancestor]);
return;
}
}
for(let i=0;i<ancestorList.length;i++){
let el = getModuleByGid(ancestorList[i])
if(!el.show)ancestor=el.g_id;
break;
}
if (ancestor===undefined){
ancestor=that.g_id;
}
// console.log(ancestor);
pushElementInQueue([ancestor]);
!!!这里要多进行一个判断,移动的前提条件为当前图形被选中(而此时图形未被选中,选中的是它的祖先)
6.总结
分组是一个很重要的功能,它提供了对多个组件同时进行操作的可能性。