扩展HT for Web之HTML5表格组件的Renderer和Editor

时间:2022-01-01 22:48:33
扩展HT for Web之HTML5表格组件的Renderer和Editor

HT for Web提供了一下几种常用的Editor,分别是:

  • slider:拉条
  • color picker:颜色选择器
  • enum:枚举类型
  • boolean:真假编辑器
  • string:普通的文本编辑器

除了这几种常用编辑器之外,用户还可以通过继承ht.widget.BaseItemEditor类来实现自定义编辑器。

而渲染器,在HT for Web提供常用的Renderer有:

  • enum:枚举类型
  • color:颜色类型
  • boolean:真假渲染器
  • text:文本渲染器

和编辑器一样也可以自定义渲染器,但是方式不太一样,渲染器是通过定义column中drawCell()方法来自定义单元格展现效果。

今天我们就来实现一把自定义HTML5表格组件的Renderer和Editor,为了更直观地演示编辑效果,我们正好利用HT for Web强大的HTML5拓扑图组件

首先来瞧瞧效果:

扩展HT for Web之HTML5表格组件的Renderer和Editor

效果图中,左边表格的第二列,是定义了一个编辑器,用一个圆盘来表示当前文本的旋转角度,可以通过拖拉来实现角度变换;表格的第三列,是通过drawCell()方法来绘制单元格内容,中间线标识旋转角度为零,向左表示文本逆时针旋转指定角度,向右表示文本顺时针旋转指定角度。

HT for Web的拓扑图网络节点的文字,简单修改label.rotation属性即可实现文字旋转功能,为了更直观我特意加上label.background使得网络拓扑图节点文字具有背景效果。

接下来我们就来看看具体的实现,先来了解下渲染器的实现:

{
name : 'label.rotation',
accessType : 'style',
drawCell : function(g, data, selected, column, x, y, w, h, tableView) {
var degree = Math.round(data.s('label.rotation') / Math.PI * 180),
width = Math.abs(w / 360 * degree),
begin = w / 2,
rectColor = '#29BB9C',
fontColor = '#000',
background = '#F8F0E5'; if (selected) {
rectColor = '#F7F283';
background = '#29BB9C';
}
g.beginPath();
g.fillStyle = background;
g.fillRect(x, y, w, h);
g.beginPath();
if (degree < 0) begin -= width;
g.fillStyle = rectColor;
g.fillRect(x + begin, y, width, h);
g.beginPath();
g.font = '12px arial, sans-serif';
g.fillStyle = fontColor;
g.textAlign = 'center';
g.textBaseline = 'middle';
g.fillText(degree, x + w / 2, y + h / 2);
}
}

上面的代码就是定义表格第三列的代码,可以看到除了定义column自身属性外,还添加了drawCell()方法,通过drawCell()方法传递进来的参数,来绘制自己想要的效果。

渲染就是这么简单,那么编辑器就没那么容易了,在设计自定义编辑器之前,得先来了解下编辑器的基类ht.widget.BaseItemEditor,其代码如下:

ht.widget.BaseItemEditor = function (data, column, master, editInfo) {
this._data = data;
this._column = column;
this._master = master;
this._editInfo = editInfo;
};
ht.Default.def(‘ht.widget.BaseItemEditor’, Object, {
ms_ac:["data", "column", "master", "editInfo"],
editBeginning: function() {},
getView: function() {},
getValue: function() {},
setValue: function() {}
});

它处理构造函数中初始化类变量外,就定义了几个接口,让用户重载实现相关业务操作逻辑处理。那么接下来说说这些接口的具体用意:

  • editBeginning:在单元格开始编辑前调用
  • getView:获取编辑器view,值类型为DOM元素
  • getValue:获取编辑器值
  • setValue:设置编辑器值,并做编辑器的页面初始化操作

在创建一个自定义编辑器的时候,必须实现这些接口,并在不同的接口中,做不同的操作。

现在我们来看看旋转角度的自定义编辑是如何设计的:

1. 按照HT for Web组件的设计惯例,我们需要创建一个Div作为view,在view中包含一个canvas元素,组件内容在canvas上绘制;

2. editor需要与用户有交互,因此,需要在view上添加事件监听,监听用户有可能的操作,在这次的Demo中,我们希望用户通过拖拉角度控制盘来控制角度,所以,我们在view上添加了mousedown、mousemove及mouseup三个事件监听;

3. 用户通过拖拉组件可以改变角度,这个改变是连续的,而且在拖拉的时候有可能鼠标会离开组件区域,要实现离开组件区域也能够正确的改变值,那么这时候就需要调用HT for Web的startDragging()方法;

以上讲述的操作都在构造函数中处理,接下来看看构造函数长什么样:

// 类ht.widget.RotationEditor构造函数
ht.widget.RotationEditor = function(data, column, master, editInfo) {
// 调用父类构造函数初始化参数
this.getSuperClass().call(this, data, column, master, editInfo); var self = this,
view = self._view = createDiv(1),
canvas = self._canvas = createCanvas(self._view);
view.style.boxShadow = '2px 2px 10px #000'; // 在view上添加mousemove监听
view.addEventListener('mousemove', function(e) {
if (self._state) {
ht.Default.startDragging(self, e);
}
}); // 在view上添加mousedown监听
view.addEventListener('mousedown', function(e) {
self._state = 1;
self.handleWindowMouseMove(e);
}); // 在view上添加mouseup监听,做些清理操作
view.addEventListener('mouseup', function(e) {
self.clear();
});
};

4. 接下来就是通过def()方法来定义ht.widget.RotationEditor类继承于ht.widget.BaseItemEditor,并实现父类的方法,代码如下,在代码中,我没有贴出setValue()方法的实现,因为这块有些复杂,我们单独抽出来讲解;

ht.Default.def('ht.widget.RotationEditor', ht.widget.BaseItemEditor, {
editBeginning : function() {
var self = this,
editInfo = self.getEditInfo(),
rect = editInfo.rect; // 编辑前再对组件做一次布局,避免组件宽高计算不到位
layout(self, rect.x, rect.y, rect.width, rect.width);
},
getView : function() {
return this._view;
},
getValue : function() {
return this._value;
},
setValue : function(val) {
// 设置编辑器值,并做编辑器的页面初始化操作
}
});

5. 我们要在setValue()方法中绘制出文章开头的效果图上面展现的效果,大致分解了些,可以分成以下四步来绘制,当然在绘制之前需要线获得canvas的context对象:

5.1. 绘制内外圆盘,通过arc()方法绘制两个间隔10px的同心圆;

5.2. 绘制值区域,通过结合arc()方法及lineTo()方法绘制一个扇形区域,在通过fill方法填充颜色;

5.3. 绘制指针,通过lineTo()方法绘制两个指针;

5.4. 绘制文本,在绘制文本的时候,不能直接将文本绘制在圆心处,因为圆心处是指针的交汇处,如果直接绘制文本的话,将与指针重叠,这时,通过clearRect()方法来清除文本区域,在通过fillRect()方法将背景填充上去,不然文本区域块将是透明的,接下来就调用fillText()方法绘制文本。

这些就是组件绘制的所有逻辑,但是有一点必须注意,在绘制完组件后,必须调用下restore()方法,因为在initContext()方法中做了一次save()操作,接下来看看具体实现(代码有些长);

setValue : function(val) {
var self = this;
if (self._value === val) return; // 设置组件值
self._value = val; var editInfo = self.getEditInfo(),
rect = editInfo.rect,
canvas = self._canvas,
radius = self._radius = rect.width / 2,
det = 10,
border = 2,
x = radius,
y = radius; // 弧度到角度的转换
val = Math.round(val / Math.PI * 180);
// 设置canvas大小
setCanvas(canvas, rect.width, rect.width);
// 获取画笔
var g = initContext(canvas);
translateAndScale(g, 0, 0, 1); // 绘制背景
g.fillStyle = '#FFF';
g.fillRect(0, 0, radius * 2, radius * 2); // 设置线条颜色及线条宽度
g.strokeStyle = '#969698';
g.lineWidth = border; // 绘制外圈
g.beginPath();
g.arc(x, y, radius - border, 0, Math.PI * 2, true);
g.stroke(); // 绘制内圈
g.beginPath();
g.arc(x, y, radius - det - border, 0, Math.PI * 2, true);
g.stroke(); // 绘制值区域
var start = -Math.PI / 2,
end = Math.PI * val / 180 - Math.PI / 2;
g.beginPath();
g.fillStyle = 'rgba(255, 0, 0, 0.7)';
g.arc(x, y, radius - border, end, start, !(val < 0));
g.lineTo(x, border + det);
g.arc(x, y, radius - det - border, start, end, val < 0);
g.closePath();
// 填充值区域
g.fill();
// 绘制值区域末端到圆心的线条
g.lineTo(x, y);
g.lineTo(x, det + border);
g.stroke(); // 绘制文本
var font = '12px arial, sans-serif';
// 计算文本大小
var textSize = ht.Default.getTextSize(font, '-180');
// 文本区域
var textRect = {
x : x - textSize.width / 2,
y : y - textSize.height / 2,
width : textSize.width,
height : textSize.height
};
g.beginPath();
// 清空文本区域
g.clearRect(textRect.x, textRect.y, textRect.width, textRect.height);
g.fillStyle = '#FFF';
// 补上背景
g.fillRect(textRect.x, textRect.y, textRect.width, textRect.height);
// 设置文本样式
g.textAlign = 'center';
g.textBaseline = 'middle';
g.font = font;
g.fillStyle = 'black';
// 绘制文本
g.fillText(val, x, y); // restore()和save()是配对的,在initContext()方法中已经做了save()操作
g.restore();
}

6. 这时候编辑器的设计就大体完成,那么编辑器该如何用到表格上呢?很简单,在表格定义列的时候,加上下面两行代码就可以开始使用编辑器了;

editable : true, // 启动编辑
itemEditor : ‘ht.widget.RotationEditor' // 指点编辑器类

7. 在构造函数中,view的mousemove事件调用了startDragging()方法,其实这个方法是有依赖的,它需要组件重载handleWindowMouseMove()及handleWindowMouseUp()两个方法。原因很简单,就如第3点种提到的,用户在拖拉组件的时候,有可能拖离了组件区域,这时候只能通过window上的mousemove及mouseup两个事件监听令用户继续操作;

// 监听window的mousemove事件,在view的mousemove事件中,调用了startDragging()方法,
// 而startDragging()方法中的实质就是触发window的mousemove事件
// 该方法计算值的变化,并通过setValue()方法来改变值
handleWindowMouseMove : function(e) {
var rect = this._view.getBoundingClientRect(),
x = e.x - rect.left,
y = e.y - rect.top,
radius = this._radius,
// 通过反三角函数计算弧度,再将弧度转换为角度
value = Math.round(Math.atan2(y - radius, x - radius) / Math.PI * 180); if (value > 90) {
value = -(180 - value + 90);
}
else {
value = value + 90;
}
this.setValue(value / 180 * Math.PI);
},
handleWindowMouseUp : function(e) {
this.clear();
},
clear : function() {
// 清楚状态组件状态
delete this._state;
}

加上上面的三个方法,运行代码可以发现编辑器可以正常编辑了。但是只有在结束编辑后,才可以在拓扑图上看到文本旋转角度变化,如果可以实时更新拓扑图上的文本旋转角度,将会更加直观些,那么现在该怎么办呢?

8. 自定义编辑器这块并像其他已经实现了的编辑器那样可以指定编辑器的属性,自定义编辑器能够指定的就只有一个类名,所以在编辑器上设置参数是没用的,用户无法设置到编辑器中。一个偷巧的方法是在column上做手脚,借鉴其他编辑器的设计思想,在column上添加一个名字为_instant的属性,在代码中通过该属性值来判断是否要立即更新对应的属性值,因此只需要在setValue()方法中添加如下代码,就能够实现实时更新属性值的效果;

// 判断列对象是否设置了_instant属性
if (column._instant) {
var table = self.getMaster();
table.setValue(self.getData(), column, val);
}

9. 至此,编辑器的设计已经完成,现在来看看具体的用法,下面的代码是Table中具体的列定义,在列定义中,指定itemEditor属性值,并设置_instant属性为true,就可以实现编辑器实时更新的效果

{
accessType : 'style',
name : 'label.rotation',
editable : true,
itemEditor : 'ht.widget.RotationEditor',
_instant : true,
formatValue : function(value) {
return Math.round(value / Math.PI * 180);
}
}

代码中你会发现定义了一个formatValue()方法,该方法是为了与编辑器中编辑的值类型一致,都将弧度转换为角度。

在表格的第三列中,通过渲染器自定义了单元格样式,同时我也为其定义了另外一个编辑器,通过左右拖拉单元格来实现角度的变化,这个编辑器的实现与上面谈及的编辑器略有不同,具体的不同之处在于,第三列的编辑器通过HT for Web中定义的ms_listener模块来添加监听,让构造函数与交互分离开,看起来更加清晰明了。

介绍下ms_listener模块,如果类添加了ms_listener模块,那么在类中将会多以下两个方法:

  • addListeners:将类中定义的handle_XXX()方法(XXX代表某个DOM事件名称,如:mousemove等)作为相应的事件监听函数添加到组件的view上;
  • removeListeners:将类中定义的handle_XXX()方法对应的事件从view上移除。

那么类中如何添加ms_listener模块呢,只需要在def()方法中类的方法定义上,添加ms_listener:true这行代码,并在方法定义上添加DOM事件对应的handle函数,再在构造函数中调用类的addListeners()方法。

具体的代码我就不在阐述了,思路与前面讲述的编辑器的思路差不多。

最后附上程序的所有代码,供大家参考,有什么问题欢迎留言咨询。

TabelRendererEditor.zip