在我们的开发工程中经常会使用到各种图,所谓的图就是由节点和节点之间的连接所形成的系统,数学上专门有一个分支叫图论(Graph Theroy)。利用图我们可以做很多工具,比如思维导图,流程图,状态机,组织架构图,等等。今天我要做的是用开源的HTML5工具来快速构造一个做图的工具。

工具选择

预先善其事,必先利其器。第一件事是选择一件合适的工具,开源时代,程序员还是很幸福的,选择很多。

最终,我选择了jsPlumb,因为它完全开源,使用很简单,用D3的话可能会多花很多功夫。joint.js也不错。大家可以根据自己的需要选择。

构建静态应用

下面我们一步一步的来使用jsPlumb来创建我们的流程图工具。

第一步是等待DOM和jsPlumb初始化完毕,类似document.ready()和jquery.ready(), 要使用jsPlumb, 需要把代码放在这个函数里:

1
2
3
jsPlumb.ready(function() {
    // ... your code goes here ...
}

创建一个jsPlumb的实例,并初始化jsPlumb的配置参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
//Initialize JsPlumb
var color = "#E8C870";
var instance = jsPlumb.getInstance({
    // notice the 'curviness' argument to this Bezier curve.  the curves on this page are far smoother
    // than the curves on the first demo, which use the default curviness value.     
    Connector : [ "Bezier", { curviness:50 } ],
    DragOptions : { cursor: "pointer", zIndex:2000 },
    PaintStyle : { strokeStyle:color, lineWidth:2 },
    EndpointStyle : { radius:5, fillStyle:color },
    HoverPaintStyle : {strokeStyle:"#7073EB" },
    EndpointHoverStyle : {fillStyle:"#7073EB" },
    Container:"container-id"
 });

这里给给出了一些配置包括,连接线(这里配置了一个贝塞尔曲线),线的风格,连接点得风格。Container需要配置一个对应的DIV容器的id。(这里也可以使用setContainer的方法)

下面我们要创建一个节点(node),每一个节点可以用一个DIV来实现。我这里提供了一个函数来创建节点。

1
2
3
4
5
6
7
8
9
10
11
function addNode(parentId, nodeId, nodeLable, position) {
  var panel = d3.select("#" + parentId);
  panel.append('div').style('width','120px').style('height','50px')
    .style('position','absolute')
    .style('top',position.y).style('left',position.x)
    .style('border','2px #9DFFCA solid').attr('align','center')
    .attr('id',nodeId).classed('node',true)
    .text(nodeLable);
 
  return jsPlumb.getSelector('#' + nodeId)[0];
}

这里做的事情就是创建了一个DIV元素,并放在对应的容器的制定位置上,注意为了支持拖拽的功能,必须使用position:absolute 。

我使用D3来操作DOM,大家可能会更习惯JQuery,这纯属个人喜好的问题。

最后返回创建节点的实例引用,这是的selector使用了jsPlumb.getSelector()方法,它和JQuery的selector是一样的,这样用的好处是你可以使用不同的DOM操作库,例如Vanilla

下面我使用一个函数来创建端点/锚点(anchor),锚点就是节点上的连接点,用于连接不同的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function addPorts(instance, node, ports, type) {
  //Assume horizental layout
  var number_of_ports = ports.length;
  var i = 0;
  var height = $(node).height();  //Note, jquery does not include border for height
  var y_offset = 1 / ( number_of_ports + 1);
  var y = 0;
 
  for ( ; i < number_of_ports; i++ ) {
    var anchor = [0,0,0,0];
    var paintStyle = { radius:5, fillStyle:'#FF8891' };
    var isSource = false, isTarget = false;
    if ( type === 'output' ) {
      anchor[0] = 1;
      paintStyle.fillStyle = '#D4FFD6';
      isSource = true;
    } else {
      isTarget =true;
    }
 
    anchor[1] = y + y_offset;
    y = anchor[1];
 
    instance.addEndpoint(node, {
      uuid:node.getAttribute("id") + "-" + ports[i],
      paintStyle: paintStyle,
      anchor:anchor,
      maxConnections:-1,
      isSource:isSource,
      isTarget:isTarget
    });
  }
}

instance是jsPlumb的实例

node是我们用addNode方法创建的Node实例

ports,是一个string的数组,指定端点的个数和名字

type,可能是output或者input,指定端点的种类,一个节点的输出端口可以连接另一个节点的输入端口。

这里anchor是一个四维数组,0维和1维分别是锚点在节点x轴和y轴的偏移百分比。我这里希望把端口画在节点的左右两侧,并按照端口的数量均匀分布。

最后使用instance.addEndpoint来创建端点。注意这里只要指定isSource和isTarget就可以用drag&drop的方式来连接端点,非常方便。

下面一步我们提供一个函数来连接端点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function connectPorts(instance, node1, port1, node2 , port2) {
  // declare some common values:
  var color = "gray";
  var arrowCommon = { foldback:0.8, fillStyle:color, width:5 },
  // use three-arg spec to create two different arrows with the common values:
  overlays = [
    [ "Arrow", { location:0.8 }, arrowCommon ],
    [ "Arrow", { location:0.2, direction:-1 }, arrowCommon ]
  ];
 
  var uuid_source = node1.getAttribute("id") + "-" + port1;
  var uuid_target = node2.getAttribute("id") + "-" + port2;
 
  instance.connect({uuids:[uuid_source, uuid_target]});
}

node1和node2是源节点和目标节点的引用,port1和port2是源端口和目标端口的名字。

使用instance.connect方法来创建连接。 overlays用来添加连接线的箭头效果或者其他风格,我这里没有使用,因为觉得都不是很好看。大家如果要用,只要把overlays加入到instance.connect的方法参数就可以了。

调用以上方法来创建节点,端点和连接线。

1
2
3
4
5
6
7
var node1 = addNode('container-id','node1', 'node1', {x:'80px',y:'20px'});
var node2 = addNode('container-id','node2', 'node2', {x:'280px',y:'20px'});
 
addPorts(instance, node1, ['out1','out2'],'output');
addPorts(instance, node2, ['in','in1','in2'],'input');
 
connectPorts(instance, node1, 'out2', node2, 'in');

这里我们创建了两个节点,第一个节点有两个输出端口,第二个节点有三个输入端口,然后把第一个节点的out2端口连接到第二个端点的in端口。效果如下:

最后我们给节点增加drag&drop的功能,这样我们就可以拖动这些节点来改变图的布局了。

1
instance.draggable($('.node'));

这里似乎依赖于JQuery-UI,我还不是很清楚。

交互式创建节点

我们已经初步具有了创建图的功能,可是节点的创建必须通过程序,我们希望用交互的方式来创建节点。

通常我们希望有一个tree view的控件,让后通过拖拽来创建对应类型的节点。这里我使用了这个开源的tree view,基于bootstrap https://github.com/jonmiles/bootstrap-treeview

我们先创建一个tree view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function getTreeData() {
  var tree = [
    {
      text: "Nodes",
      nodes: [
        {
          text: "Node1",
        },
        {
          text: "Node2"
        }
      ]
    }
  ];
 
  return tree;
}
//Initialize Control Tree View
$('#control-panel').treeview({data: getTreeData()});

树上有两个节点:

然后我实现从树上拖拽对应的节点,到流程图上的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Handle drag and drop
$('.list-group-item').attr('draggable','true').on('dragstart', function(ev){
  //ev.dataTransfer.setData("text", ev.target.id);
  ev.originalEvent.dataTransfer.setData('text',ev.target.textContent);
  console.log('drag start');
});
 
$('#container-id').on('drop', function(ev){
  //avoid event conlict for jsPlumb
  if (ev.target.className.indexOf('_jsPlumb') >= 0 ) {
    return;
  }
 
  ev.preventDefault();
  var mx = '' + ev.originalEvent.offsetX + 'px';
  var my = '' + ev.originalEvent.offsetY + 'px';
 
  console.log('on drop : ' + ev.originalEvent.dataTransfer.getData('text'));
  var uid = new Date().getTime();
  var node = addNode('flow-panel','node' + uid, 'node', {x:mx,y:my});
  addPorts(instance, node, ['out'],'output');
  addPorts(instance, node, ['in1','in2'],'input');
  instance.draggable($(node));
}).on('dragover', function(ev){
  ev.preventDefault();
  console.log('on drag over');
});

这里要注意的是要避免和jsPlumb拖拽端点的逻辑冲突,当检测到target是jsPlumb对象是需要直接从drop方法中退出以执行对应的jsPlumb的drop逻辑。

好了,一个绘制流程图的软件工具初步完工。

我把代码放在oschina的代码托管服务上了, 大家有兴趣可以下来试试 http://git.oschina.net/gangtao/FlowChart-Builder

转至:http://gangtao.is-programmer.com/posts/71082.html

用HTML5构建一个流程图绘制工具的更多相关文章

  1. 最好的原型和流程图绘制工具:OmniGraffle_交互设计

    原文地址:http://www.shangxueba.com/jingyan/2230668.html 使用哪种原型设计工具"大概是设计师闲聊时出现频率最高的话题之一.据我了解一般以Visi ...

  2. 使用 Python 的 Socket 模块构建一个 UDP 扫描工具

    译文:oschina 英文:bt3gl 当涉及到对一些目标网络的侦察时,出发点无疑是首先发现宿主主机.这个任务还可能包含嗅探和解析网络中数据包的能力. 几周前,我曾经谈到了如何使用Wireshark来 ...

  3. 框架流程图绘制工具OmniGraffle 7 for Mac

    1.官网下载地址 https://www.omnigroup.com/omnigraffle 2.激活方法 Omnigraffle Pro 7注册码/许可证 名字:Appked 序列号:MFWG-GH ...

  4. 十分钟通过 NPM 创建一个命令行工具

    大过年的,要不要写点代码压压惊?来花十分钟学一下怎么通过 NPM 构建一个命令行工具. 写了一个小 demo,用于代替 touch 的创建文件命令 touchme ,可以创建自带“佛祖保佑”注释的文件 ...

  5. 让IE8支持HTML5及canvas功能!chart.js图表绘制工具库IE8上兼容方案

    第一步,我们加上对html5的支持. <!--[if IE]> <script src="/public/html5.js" type="text/ja ...

  6. Brunch:快捷的HTML5构建工具

    Brunch,一个超快的HTML5构建工具.它可以(官方介绍): 编译你的脚本,模板,样式,链接它们, 将脚本和模板封装进common.js/AMD模块里,链接脚本和样式, 为链接文件生成源地图,复制 ...

  7. 使用 XMPP 构建一个基于 web 的通知工具——转

    Inserting of file(使用 XMPP 构建一个基于 web 的通知工具.docx) failed. Please try again. http://www.ibm.com/develo ...

  8. 如何构建一个PKM系统

    原文链接 这篇文章是个人知识总结相关的第一篇文章,主要目的是为了阐述我们为什么需要一个 PKM 系统,并且简单对自己这几年亲身形成的PKM 系统进行总结. PKM 是什么? PKM 是 Persona ...

  9. atitit..代码生成流程图 流程图绘制解决方案 java  c#.net  php v2

    atitit..代码生成流程图 流程图绘制解决方案 java  c#.net  php v2 1.1. Markdown 推荐,就是代码和flow都不能直接使用.1 1.2. Java code2fl ...

随机推荐

  1. inno setup教程解释脚本

    inno setup教程解释脚本 2007-04-08 21:31:36|  分类: 科技-> Inno Setu |  标签:inno   |举报 |字号 订阅     下载LOFTER客户端 ...

  2. 深入分析Php处理浮点数的问题

    下文来为各位介绍Php处理浮点数的问题了,如果各位在使用过程中碰到这些问题我们可以一起来看看,希望文章对各位有帮助 公司要对产品价格做调整,因为做的外贸商城,所以价格要和国际接轨.比如国外的价格展示方 ...

  3. 转:【More Effective C#】Lambda表达式优化

    http://www.cnblogs.com/kongyiyun/archive/2010/10/19/1855274.html 使用Lambda表达式将会造成Lambda表达式主题部分的代码重复. ...

  4. Android 常用的常量

    常用的Android常量 1.Activity Action 字符串常量("android.intent.action."+)   描述ALL_APPS               ...

  5. const char* <----- > string

    (1) const char*      <-----     string const char* const_txt_path=txt_path.c_str(); (2)  string  ...

  6. VMware虚拟机固定IP后克隆出现无法访问网卡问题

    通常我们现在都喜欢使用虚拟机进行实验,进行集群搭建等,在这个过程中,会遇到克隆虚拟机问题,当没有修改任何IP的情况下,克隆后,在逐台修改IP地址是没有问题的,但是,如果我们先设置了固定IP地址后,克隆 ...

  7. CentOS安装时小坑记录

    在安装CentOS的时候,由于第一次安装小白,将VM虚拟机的内存设置为512M,导致进行安装的时候无法进入正常的画面安装模式,只能使用简版安装界面,可能对于很多小白不是很熟悉,特此记录,安装CentO ...

  8. 帝国cms缩略图:网站不同地方生成不同的缩略图

    本文转自:http://blog.sina.com.cn/s/blog_4d49ba58010115sd.html 方便网站多地方调用不同尺寸的缩略图. 根据图片源,在网站任意位置生成不同大小的缩略图 ...

  9. Python 将pdf转换成txt(不处理图片)

    上一篇文章中已经介绍了简单的python爬网页下载文档,但下载后的文档多为doc或pdf,对于数据处理仍然有很多限制,所以将doc/pdf转换成txt显得尤为重要.查找了很多资料,在linux下要将d ...

  10. python with as用法

    with as用途:取代原来的try...finally,主要是用于处理异常 基本语法:with EXPRESSION [ as VARIABLE] WITH-BLOCK 基本要求:with所求值的对 ...