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

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

  • 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拓扑图组件ide

首先来瞧瞧效果:函数

图片描述

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

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

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

{
    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对象:

  • 绘制内外圆盘,经过arc()方法绘制两个间隔10px的同心圆;
  • 绘制值区域,经过结合arc()方法及lineTo()方法绘制一个扇形区域,在经过fill方法填充颜色;
  • 绘制指针,经过lineTo()方法绘制两个指针;
  • 绘制文本,在绘制文本的时候,不能直接将文本绘制在圆心处,由于圆心处是指针的交汇处,若是直接绘制文本的话,将与指针重叠,这时,经过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()方法。

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

最后附上程序的全部代码,供你们参考,有什么问题欢迎留言咨询。

TableRendererEditor.zip

相关文章
相关标签/搜索