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

简介: 在HT for Web提供了一下几种常用的Editor,分别是: slider:拉条 color picker:颜色选择器 enum:枚举类型 boolean:真假编辑器 string:普通的文本编辑器 除了这几种常用编辑器之外,用户还可以通过继承ht.widget.BaseItemEditor类来实现自定义编辑器。

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

首先来瞧瞧效果:

效果图中,左边表格的第二列,是定义了一个编辑器,用一个圆盘来表示当前文本的旋转角度,可以通过拖拉来实现角度变换;表格的第三列,是通过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 

目录
相关文章
|
1月前
|
存储 Web App开发 移动开发
HTML5 Web 存储详解
HTML5 Web存储提供了两种客户端数据存储机制:**Local Storage**和**Session Storage**。Local Storage用于长期存储数据,即使关闭浏览器数据也依然存在,适用于保存用户偏好设置等信息。Session Storage则在标签或窗口关闭时清除数据,适合存储临时信息。两者均提供了简单的API进行数据的存取操作,但需要注意的是,Web存储并非加密存储,不应存放敏感信息。现代浏览器普遍支持Web存储,合理利用这两种存储方式可提升Web应用的用户体验。
|
1月前
|
存储 移动开发 大数据
HTML5 Web IndexedDB 数据库详解
IndexedDB 是一种高效的浏览器存储方案,允许在本地存储大量结构化数据,支持索引和事务,适用于需要离线和大数据处理的应用。它由数据库、对象仓库等组成,通过键值对存储数据,确保数据一致性和完整性。本介绍展示了如何创建、读取、更新和删除数据,以及事务和错误处理的最佳实践。
|
2月前
|
JavaScript 前端开发 API
vue3 v-md-editor markdown编辑器(VMdEditor)和预览组件(VMdPreview )的使用
本文介绍了如何在Vue 3项目中使用v-md-editor组件库来创建markdown编辑器和预览组件。文章提供了安装步骤、如何在main.js中进行全局配置、以及如何在页面中使用VMdEditor和VMdPreview组件的示例代码。此外,还提供了一个完整示例的链接,包括编辑器和预览组件的使用效果和代码。
vue3 v-md-editor markdown编辑器(VMdEditor)和预览组件(VMdPreview )的使用
|
1月前
|
存储 移动开发 数据库
HTML5 Web IndexedDB 数据库常用数据存储类型
IndexedDB 支持多种数据存储类型,满足复杂数据结构的存储需求。它包括基本数据类型(如 Number、String、Boolean、Date)、对象(简单和嵌套对象)、数组、Blob(用于二进制数据如图像和视频)、ArrayBuffer 和 Typed Arrays(处理二进制数据)、结构化克隆(支持 Map 和 Set 等复杂对象),以及 JSON 数据。尽管不直接支持非序列化数据(如函数和 DOM 节点),但可以通过转换实现存储。开发者应根据具体需求选择合适的数据类型,以优化性能和使用体验。
|
1月前
|
移动开发 JavaScript 前端开发
HTML5 Web Workers详解
HTML5 Web Workers 允许在后台线程中运行 JavaScript,实现复杂计算而不影响用户界面,提升应用性能。其主要特性包括并行处理、异步通信、独立作用域及多数据类型支持。通过创建和使用 Worker 文件,如 `worker.js`,可执行后台任务,并与主线程通过消息传递机制通信。适用于数据处理、图像处理、复杂计算及网络请求并行等场景。需要注意的是,Web Workers 在浏览器兼容性、安全性限制、调试及资源消耗方面需特别关注。合理利用 Web Workers 可显著增强 Web 应用的流畅度和响应速度。
|
1月前
|
SQL 存储 移动开发
HTML5 Web SQL 数据库详解
Web SQL 数据库是 HTML5 中的一种本地存储技术,允许在浏览器中使用 SQL 语言操作本地数据,支持离线访问和事务处理,适用于缓存数据和小型应用。然而,其存储容量有限且仅部分现代浏览器支持,标准已不再积极维护,未来可能被 IndexedDB 和 localStorage 等技术取代。使用时需谨慎考虑兼容性和发展前景。
|
1月前
|
存储 移动开发 缓存
HTML5 Web 存储详解
HTML5 Web 存储包括 `localStorage` 和 `sessionStorage`,前者提供持久存储且无过期时间,后者仅在会话期间有效。两者均支持键值对形式存储数据,容量约为 5-10 MB。`localStorage` 适用于用户偏好设置、登录状态保持及离线应用缓存;`sessionStorage` 则用于临时数据如表单输入。数据以字符串形式存储,可通过 `JSON` 方法处理对象。由于数据存储于本地,不适合存放敏感信息。示例代码展示了如何使用按钮将输入框内容保存至 `localStorage` 并进行清除操作。
|
21天前
|
移动开发 前端开发 JavaScript
HTML与现代Web开发的不同技术
【10月更文挑战第11天】HTML与现代Web开发的不同技术
16 0
|
4月前
|
开发工具
vi编辑器,现在vi\vim是文本文件进行编辑的最佳选择,Vim是vi的加强的版本,兼容vi的所有指令,vim编辑器有三种工作模式,一开始进入的是命令模式,命令模式i是插入的意思,两下y+p复制内容
vi编辑器,现在vi\vim是文本文件进行编辑的最佳选择,Vim是vi的加强的版本,兼容vi的所有指令,vim编辑器有三种工作模式,一开始进入的是命令模式,命令模式i是插入的意思,两下y+p复制内容
|
5月前
|
开发工具
Vim 编辑器:高效文本编辑的瑞士军刀
**Vim 概览:** Vim 是一个功能丰富的文本编辑器,以其高度可定制性著称。文章介绍了 Vim 的高效使用技巧,包括快捷打开文件、命令行模式下的常用命令、查找与替换、删除和复制文本。还讨论了配置 `.vimrc` 文件以自定义设置,如改变 leader 键、设置缩进和高亮,并展示了安装插件如 vim-airline 和 vim-snazzy 的方法。通过这些技巧,用户能提升 Vim 使用效率。
60 5