问题背景
在工作中有个需求需要画一幅“树图”,本想使用echarts进行绘制的。奈何echarts的label中不支持formatter html,即html文本的渲染。故为了使图的节点更具有交互性,这里选择使用jointjs。
问题解决
jointjs是一个js图表库,其可在浏览器的网页中创建可交互的图表工具,它依赖于js与SVG。(这个在网页渲染后可以在浏览器按F12查看页面元素看到)
基本使用
关于jointjs的使用以及在jointjs元素中使用html这些在官方的文档中已经很详细了。我这里也只当加一下自己的理解翻译了。首先还是来回顾下它的基本使用吧。
- 配置
这个主要就是配置下jointjs的相关js库,官方网页有下载的资源和示例。如果这里还想用bootstrap,可以将其也包含进来。最后将这些资源放入到html的head标签里面。我这里盗用下官方的图吧。 - 画图
下来就是在网页中画图了。jointjs画图的步骤还是很人性化的。其跟我们平时画流程图的步骤一样,准备纸张,在纸张里画好对应的节点图,然后将图形之间连线。jointjs的步骤也是如此。下来就看看官方的hello world吧。其代码如下:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" />
</head>
<body>
<!-- content -->
// 在网页中定义一个盒子
<div id="myholder"></div>
<!-- dependencies -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"></script>
<!-- code -->
<script type="text/javascript">
// 定义一个图表模板。我这里理解应该是jointjs想将纸上面的图表作为一个整体图表模板进行管理。官方文档也说了在纸张上画的图形元素都需要进行加入到模板里才能在网页上渲染出来
var graph = new joint.dia.Graph;
// 定义一张纸。包括纸的背景颜色、长宽之类的。只有有了纸,我们才能在上面画画
var paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
//这里相当于将graph图表模板放入到纸张里面了。纸张到时候就会渲染graph里面的图形元素,我们只需要将想画的图形元素放入graph模板里就可以了
model: graph,
width: 600,
height: 100,
gridSize: 1
});
// 在纸上面画一个矩形。jointjs已为我们定义好了一些矩形、圆角矩形、三角形等形状供选择,我们可以通过“new一个矩形的对象”来在纸上画一个矩形
var rect = new joint.shapes.standard.Rectangle();
// 矩形的位置。以纸张左上角为原点,相当于以纸张左上角为原点的y轴箭头向下、x轴箭头向右的二维坐标系中画图形
rect.position(100, 30);
// 矩形的长宽
rect.resize(100, 40);
// 矩形的属性。包括填充色、里面的文字、字体的颜色等
rect.attr({
body: {
fill: 'blue'
},
label: {
text: 'Hello',
fill: 'white'
}
});
// 最后别忘了加入到图表模板中
rect.addTo(graph);
// 定义另一个矩形。这里用了colone函数,clone了上一个矩形的部分属性,如长、宽等。
var rect2 = rect.clone();
rect2.translate(300, 0);
rect2.attr('label/text', 'World!');
// 加入到图表模板中
rect2.addTo(graph);
//定义一条连接线,将两个矩形连接起来。
var link = new joint.shapes.standard.Link();
//连接线的起点
link.source(rect);
// 连接线的终点
link.target(rect2);
// 也添加到图表模板中
link.addTo(graph);
</script>
</body>
</html>
代码生成的效果如下,截一张官方图放这。
jointjs画图的整个过程走下来,跟我们在纸上画流程图的过程是一样的。
在jonitjs元素中使用html
这个过程其实相当于要给纸张里面的图形元素加上html元素,比如按钮之类的。比如我们要给上面hello_world中的矩形里面加入一个按钮。不幸的是jointjs中并没有那种现成的可利用的个性化定制图形。不过幸运的是jointjs给了我们足够的接口,让我们可以随心所欲的定义自己需求的图形元素(这里感觉跟Android中的自定义view很像)。好了,废话不多说,先看官方教程吧,我这里也权当翻译吧。
(function() {
// 创建图表模板
var graph = new joint.dia.Graph;
// 定义纸张
var paper = new joint.dia.Paper({ el: $('#paper-html-elements'), width: 650, height: 400, gridSize: 1, model: graph });
// Create a custom element.
// 主要是这里,要自定义一个从无到有的html类型图形元素的母板,就像hello world示例的基本的矩形图形
// ------------------------
// jointjs给了最初始的html母板,并且母板的初始形状为矩形
joint.shapes.html = {};
joint.shapes.html.Element = joint.shapes.basic.Rect.extend({
defaults: joint.util.deepSupplement({
type: 'html.Element',
attrs: {
rect: { stroke: 'none', 'fill-opacity': 0 }
}
}, joint.shapes.basic.Rect.prototype.defaults)
});
// Create a custom view for that element that displays an HTML div above it.
// -------------------------------------------------------------------------
// 这一步就是在上面创建的母板里自定义可交互的html。从下面看里面包含了按钮button、label、span、单选框select、输入框input的元素
joint.shapes.html.ElementView = joint.dia.ElementView.extend({
template: [
'<div class="html-element">',
'<button class="delete">x</button>',
'<label></label>',
'<span></span>', '<br/>',
'<select><option>--</option><option>one</option><option>two</option></select>',
'<input type="text" value="I\'m HTML input" />',
'</div>'
].join(''),
// 下面这个函数则是初始化自定义图形里面的html元素的交互情况以及实时更新html里元素的位置。例如按钮点击时移除一个图形元素等。
initialize: function() {
_.bindAll(this, 'updateBox');
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
this.$box = $(_.template(this.template)());
// 阻止纸张争夺了点击事件
// Prevent paper from handling pointerdown.
this.$box.find('input,select').on('mousedown click', function(evt) {
evt.stopPropagation();
});
// This is an example of reacting on the input change and storing the input data in the cell model.
// 将input输入的内容呈现到图形元素上
this.$box.find('input').on('change', _.bind(function(evt) {
this.model.set('input', $(evt.target).val());
}, this));
// 监听单选框值的改变事件,并将单选框元素的显示值设为点击的值
this.$box.find('select').on('change', _.bind(function(evt) {
this.model.set('select', $(evt.target).val());
}, this));
this.$box.find('select').val(this.model.get('select'));
this.$box.find('.delete').on('click', _.bind(this.model.remove, this.model));
// Update the box position whenever the underlying model changes.
// 当图形位置改变时,其里面的html位置也会改变,但它们之间的相对位置不变
this.model.on('change', this.updateBox, this);
// Remove the box when the model gets removed from the graph.
// 移除图形元素
this.model.on('remove', this.removeBox, this);
this.updateBox();
},
render: function() {
joint.dia.ElementView.prototype.render.apply(this, arguments);
this.paper.$el.prepend(this.$box);
this.updateBox();
return this;
},
updateBox: function() {
// Set the position and dimension of the box so that it covers the JointJS element.
var bbox = this.model.getBBox();
// Example of updating the HTML with a data stored in the cell model.
// 将设置label的值给html里面的label元素
this.$box.find('label').text(this.model.get('label'));
// 将html中的span元素的值设为html中select元素的值
this.$box.find('span').text(this.model.get('select'));
this.$box.css({
width: bbox.width,
height: bbox.height,
left: bbox.x,
top: bbox.y,
transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)'
});
},
// 移除图形
removeBox: function(evt) {
this.$box.remove();
}
});
// 自定义的图形模板已经创建好了。下面的操作跟hello world里的一样,即在纸张里画两自定义图形,并创建一条连接线将其连接起来
// Create JointJS elements and add them to the graph as usual.
// -----------------------------------------------------------
var el1 = new joint.shapes.html.Element({
position: { x: 80, y: 80 },
size: { width: 170, height: 100 },
label: 'I am HTML',
select: 'one'
});
var el2 = new joint.shapes.html.Element({
position: { x: 370, y: 160 },
size: { width: 170, height: 100 },
// 给自定义图形中的html的label设值
label: 'Me too',
select: 'two'
});
// 创建连接线
var l = new joint.dia.Link({
source: { id: el1.id },
target: { id: el2.id },
attrs: { '.connection': { 'stroke-width': 5, stroke: '#34495E' } }
});
// 将几个图形元素加入到图表模板中
graph.addCells([el1, el2, l]);
}())
官方文档中还有一个css静态资源文件,用来对自定义的html中各标签进行位置、背景、颜色等的设置,这里就不写了.感觉官方举的例子有些冗余了。下面是我创建了一个自定义图形中只包含一个按钮的示例。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="joint.css" />
<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />
<script src="jquery.js"></script>
<script src="lodash.js"></script>
<script src="backbone.js"></script>
<script src="joint.js"></script>
<!-- 包含了bootstrap -->
<script src="bootstrap.min.js"></script>
<style>
#paper-html-elements {
position: relative;
border: 1px solid gray;
display: inline-block;
background: transparent;
overflow: hidden;
}
#paper-html-elements svg {
background: transparent;
}
#paper-html-elements svg .link {
z-index: 2;
}
.html-element {
position: absolute;
background: #3498DB;
/* Make sure events are propagated to the JointJS element so, e.g. dragging works.*/
pointer-events: none;
-webkit-user-select: none;
border-radius: 4px;
border: 2px solid #2980B9;
box-shadow: inset 0 0 5px black, 2px 2px 1px gray;
padding: 5px;
box-sizing: border-box;
z-index: 2;
}
.html-element button {
/* Enable interacting with inputs only. */
pointer-events: auto;
}
.html-element button.viewlog {
color: white;
line-height: 15px;
text-align: middle;
position: absolute;
padding: 0;
margin: 0;
font-weight: bold;
cursor: pointer;
}
.html-element label {
color: #333;
text-shadow: 1px 0 0 lightgray;
font-weight: bold;
}
</style>
</head>
<body>
<div id="myholder"></div>
<script type="text/javascript">
(function() {
var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({ el: $('#myholder'), width: 650, height: 400, gridSize: 1, model: graph });
joint.shapes.html = {};
joint.shapes.html.Element = joint.shapes.basic.Rect.extend({
defaults: joint.util.deepSupplement({
type: 'html.Element',
attrs: {
rect: { stroke: 'none', 'fill-opacity': 0 }
}
}, joint.shapes.basic.Rect.prototype.defaults)
});
joint.shapes.html.ElementView = joint.dia.ElementView.extend({
template: [
'<div class="html-element">',
'<label></label>',
'<div style="margin-right: 10px;text-align: center;"><button type="button" class="viewlog btn btn-success" title="一个小按钮" data-container="body" data-toggle="popover" data-placement="right" data-content="右侧的 Popover 中的一些内容">查看详情</button></div>',
'</div>'
].join(''),
initialize: function() {
_.bindAll(this, 'updateBox');
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
this.$box = $(_.template(this.template)());
this.$box.find('.viewlog').on('click', function (e) {
// 这里面添加按钮viewlog的点击事件等
alert("啊哈哈");
});
this.model.on('change', this.updateBox, this);
this.updateBox();
},
render: function() {
joint.dia.ElementView.prototype.render.apply(this, arguments);
this.paper.$el.prepend(this.$box);
this.updateBox();
return this;
},
updateBox: function() {
var bbox = this.model.getBBox();
this.$box.find('label').text(this.model.get('label'));
this.$box.css({
width: bbox.width,
height: bbox.height,
left: bbox.x,
top: bbox.y,
transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)'
});
},
});
var el1 = new joint.shapes.html.Element({
position: { x: 80, y: 80 },
size: { width: 170, height: 60 },
label: 'I am HTML',
select: 'one'
});
var el2 = new joint.shapes.html.Element({
position: { x: 370, y: 160 },
size: { width: 170, height: 60 },
label: 'Me too',
select: 'two'
});
var l = new joint.dia.Link({
source: { id: el1.id },
target: { id: el2.id },
attrs: { '.connection': { 'stroke-width': 5, stroke: '#34495E' } }
});
graph.addCells([el1, el2, l]);
}());
//定义连线
function createlink(source, target){
var cell = new joint.shapes.standard.Link();
cell.source(source);
cell.target(target);
cell.attr({
line: {
stroke: 'black',
strokeWidth: 1
}
});
graph.addCell(cell);
return cell;
}
$(function (){
//使用了bootstrap的popover
$("[data-toggle='popover']").popover();
});
</script>
</body>
</html>
结果如下:
总结
可以看到,在jointjs的图形中使用html本质上其实就是根据jointjs提供的接口自定义自己的图形,然后在纸张上进行画图、连线等。另外渲染好的图在浏览器中查看元素时,可以看到就是一些SVG和html、js。也正好印证了jointjs的自我介绍那句话。