HT UI 表格和树表格手册

索引


表格概述

UI 库提供了表格组件类 ht.ui.TableView,用于显示 DataModel 数据容器中 Data 类型对象的属性信息,支持排序和过滤等功能

通过 tableView = new ht.ui.TableView(dataModel); 初始化构建一个表格组件对象,dataModel 参数为表格组件绑定的数据模型,该模型为空时表格组件构造函数内部将创建一个新的数据模型进行绑定

表格组件的 getColumnModel() 函数返回列模型对象,其本质也是个 DataModel 类型对象,只不过该对象是仅用于添加 ht.ui.Column 类型对象(注意不要和 ht.Column 混淆,ht.Column 用于 ht.widget.TableView), ht.ui.Column 类型的父类为 ht.Data,增加了属性定义相关的函数接口。

因此用户所需要做的工作是,根据要显示的属性信息构建出 ht.ui.Column 对象,然后添加到 tableView.getColumnModel() 函数返回的列模型, 这样 tableView.getDataModel() 的数据模型中的 Data 的相关属性信息,就会根据 tableView.getColumnModel() 上存储的 ht.ui.Column 对象的配置进行显示。

表格列

ht.ui.Column 列类继承于 ht.Data,也可是设置父子关系实现表头嵌套功能,具备以下属性函数:

枚举是较为常见的属性编辑应用,在编辑时候呈现下拉列表,因此 UI 库提供了 ht.ui.EnumColumn( 从 ht.ui.Column 继承) 处理枚举展示和编辑需求(内置下拉框作为编辑器)

下面各种 Column 的例子(需要访问外网请求 jQuery 类库):

上面例子中,我们创建一个 TableView 组件,并且创建了八个列:

第一个列只是简单的显示节点的 id 属性:

var column = new ht.ui.Column();
column.setName('id');
column.setDisplayName('ID');
columnModel.add(column);

第二列显示 name 属性,并且将其设置为可编辑(双击启动编辑):

column = new ht.ui.Column();
column.setName('name');
column.setDisplayName('Title');
// 设置可编辑
column.setEditable(true);
// 使用字符串编辑器
column.setEditorClass('ht.ui.editor.StringEditor');
columnModel.add(column);

第三列用于显示 duration 属性,这个属性为 int 类型,但是我们重写了 formatValue 函数,通过拼接字符串返回了新的格式:

column = new ht.ui.Column();
column.setName('duration');
column.setAccessType('attr'); // 指示通过 data.getAttr('duration') 获取值
column.setDisplayName('Duration');
// 使用整数编辑器,编辑时只能输入整数
column.setEditorClass('ht.ui.editor.IntEditor');
column.formatValue = function (value, data) {
    return value + ' Days';
};
column.setEditable(true);
columnModel.add(column);

第四列显示 progress 进度,通过重写 drawCell 绘制进度条

column = new ht.ui.Column();
column.setName('progress');
column.setAccessType('attr'); // 指示通过 data.getAttr('progress') 获取值
column.setDisplayName('% Complete');
column.drawCell = function (g, data, selected, column, x, y, w, h, view) {
    var value = data.a('progress'),
        rectY = y + (h - 20) / 2;

    g.beginPath();
    g.fillStyle = 'white';
    g.fillRect(x, rectY, w, 20);
    g.beginPath();
    g.fillStyle = '#139dd7';
    g.fillRect(x, rectY, w * (value / 100), 20);

    ht.Default.drawText(g, value + '%', '12px Arial', 'black', x, y, w, h, 'center', 'middle');
}
column.setEditable(true);
column.setEditorClass('ht.ui.editor.NumberEditor');
columnModel.add(column);

第五列创建了一个 boolean 类型的列,自动用复选框渲染:

column = new ht.ui.Column();
column.setName('important');
column.setDisplayName('Important');
column.setAccessType('attr');
column.setValueType('boolean');// valueType 设置为 boolean 类型后,无需再设置 editorClass,只要 editable 设为 true 即可编辑
column.setEditable(true);
columnModel.add(column);

第六列显示颜色值:

column = new ht.ui.Column();
column.setName('color');
column.setDisplayName('Color');
column.setAccessType('attr');
// 颜色类型
column.setValueType('color');
column.setEditable(true);
column.setEditorClass('ht.ui.editor.ColorEditor');
columnModel.add(column);

第七列是一个枚举列(ht.ui.EnumColumn)

// 创建枚举列
column = new ht.ui.EnumColumn();
// 设置枚举数据
column.setDatas(belongTos);
column.setName('belongTo');
column.setDisplayName('Belong To');
column.setAccessType('attr');
// 只需要设置可编辑即可,EnumColumn 内置了下拉框类型的编辑器
column.setEditable(true);
columnModel.add(column);

第八列重写了 drawCell 自定义使用 DOM 对象渲染单元格:

column = new ht.ui.EnumColumn();
column.setName('popularity');
column.setDisplayName('Popularity');
column.setAccessType('attr');
column.setEditable(true);
columnModel.add(column);
// 返回 DOM 对象
column.drawCell = function (g, data, selected, column, x, y, width, height) {
    var chartDiv = data.chartDiv;
    // 为了节省性能,chartDiv 需要缓存在 data 上
    if (!chartDiv) {
        // 设置 div 属性
        chartDiv = data.chartDiv = document.createElement('div');
        chartDiv.style.position = 'absolute';
        chartDiv.style.top = (height - 20) / 2 + 'px';
        chartDiv.style.width = width + 'px';
        chartDiv.style.height = '20px';
    }
    if (data._timeId != null) {
        clearTimeout(data._timeId);
        delete data._timeId;
    }
    // 使用 sparkline 填充 div,注意要 setTimeout,因为 sparkline 填充 div 时要求 div 已经显示在页面中
    data._timeId = setTimeout(function () {
        $(chartDiv).sparkline(data.a('popularity'),
            { width: width, height: 20, type: 'bar', barColor: '#00a65a', negBarColor: '#ff4444' });
        delete data._timeId;
    }, 50);
    return chartDiv;
};

单元格编辑组件的属性可以通过 ColumneditorViewProperties 配置:

// 修改某一列数据的编辑组件属性
column.s('editorViewProperties', {
    background: 'red', // 这里可以配置编辑组件的任意属性
    color: 'white'
});

上面表格中列都是固定宽度,通过简单的代码即可实现百分比宽度(独立运行示例然后调整浏览器窗口大小查看效果):

Column 的宽度为负数表示当前列的宽度占表格剩余宽度(总宽度减去固定宽度)的百分比

// column 列的宽度为表格剩余宽度的 10%
column.setWidth(-0.1);

表格组件

表格支持使用 visibleFunc 过滤只显示部分数据:

tableView.setVisibleFunc(function(data){                    
    if (data.a('belongTo') === 'Yancy')
        return true;
    else
        return false;
});

重写 drawRowBackground 可实现斑马线效果:

table.drawRowBackground = function (drawable, x, y, width, height, data) {
    var g = table.getRootContext();
    if (table.isSelected(data)) {
        g.fillStyle = '#87A6CB';
    }
    else if (table.getRowIndex(data) % 2 === 0) {
        g.fillStyle = '#F1F4F7';
    }
    else {
        g.fillStyle = '#FAFAFA';
    }
    g.beginPath();
    g.rect(x, y, width, height);
    g.fill();
};

表格支持每行使用不同的高度,参考下面的例子:

例子中重写了第二列的 drawCell 函数创建一个 div 作为文本渲染器

column.drawCell = function (g, data, selected, column, x, y, width, height) {
    var self = this,
        div = data.div;

    if (!div) {
        div = data.div = document.createElement('div');
        div.style.position = 'absolute';
        div.style.wordWrap = 'break-word';
        div.style.wordBreak = 'break-all';
        div.style.paddingLeft = '4px';
        div.style.touchAction = 'none';
        div.style.cursor = 'default';
        div.style.font = table.getLabelFont();

        div.innerHTML = data.a('content');
    }

    div.style.color = ht.ui.uiTheme.textColor;

    return div;
};

每一行的高度由文字内容确定,所以行高需要存储在每个 Data 上:

// 通过临时 DOM 的方式计算文本高度
function initRowsHeight(table) {
    var dataModel = table.dm(),
        div = table._tempDiv;
    if (!div) {
        var div = table._tempDiv = document.createElement('div');
        div.style.position = 'absolute';
        div.style.visibility = 'hidden';
        div.style.wordWrap = 'break-word';
        div.style.wordBreak = 'break-all';
        div.style.paddingLeft = '4px';
    }

    div.style.width = table.getColumnModel().getDatas().get(1).getWidth() + 'px';
    div.style.font = table.getLabelFont();
    document.body.appendChild(div);
    dataModel.getDatas().each(function (data) {
        div.innerHTML = data.a('content');
        // 高度值存储在 _rowHeight 私有变量上
        data._rowHeight = div.scrollHeight;
    });
    document.body.removeChild(div);
}

然后通过 setRowHeightFunc 指定获取每行高度的方式:

table.setRowHeightFunc(function (data) {
    return data._rowHeight;
});

上面例子中同时演示了 TableHeader 表头组件的用法:

// 使用 splitLayout 布局表头和表格组件
splitLayout.addView(tableHeader, {
    region: 'first'
});
splitLayout.addView(table, {
    region: 'second'
});

下面例子演示了如何使用利用自定义行高和动画动态插入数据:

表格拖拽

表格的拖拽和 ListView 组件类似,如有需求请参考 ListView 手册;

表格默认会自动生成拖拽图片,但是如果表格中重写了 drawCell 函数使用 DOM 渲染单元格,自动生成的拖拽图片中不会包含 DOM 对象,此时一般需要重写 createDragImage 函数, 返回自定义的拖拽图片:

这个例子中,重写了 createDragImage 生成自定义的拖拽图片(通过这种方式,可实现任意效果的拖拽图片):

tableView.createDragImage = function (data, selection) {
    // 返回矢量对象
    return {
        width: 80,
        height: 24,
        comps: [
            {
                type: 'rect',
                relative: true,
                rect: [17, 1, 1],
                background: '#006dc8'
            },
            {
                type: 'text',
                relative: true,
                rect: [17, 1, 1],
                text: data.getName(),
                color: 'white'
            }
        ]
    }
}

表格面板

自定义行高的例子中演示了使用 SplitLayout 布局表头和表格;为了简化布局操作,UI 库提供了表格面板容器(ht.ui.TablePane),这个容器内部自动创建表头组件、表格组件、表尾组件:

这个例子中创建了 TablePane 实例,并且获取 TableViewDataModel 再处理:

tablePane = new ht.ui.TablePane();
tableView = tablePane.getTableView();
dataModel = tableView.getDataModel();
columnModel = tableView.getColumnModel();

//...

// 加到页面中
tablePane.addToDOM();

TableFooter 表尾组件默认是隐藏的,通过下面的方式将其显示出来:

tableFooter = tablePane.getTableFooter();
tableFooter.setVisible(true);

// ...

// 设置列的统计方式:
column.setSummary('max');

另外可以看到例子中第二、三、四列有同一个分组列:

// GroupColumn 用来对列进行分组
groupColumn = new ht.ui.GroupColumn();
groupColumn.setAlign('center');
groupColumn.setName('RGB');

再一个需要关注的地方是第二、三、四列使用了自定义的 SliderEditor 作为编辑器,单击即可使用 Slider 编辑颜色值的三原色:

column = new ht.ui.Column();
column.setName('r');
column.setDisplayName('R');
column.setAccessType('attr');
column.setEditable(true);
column.setEditorClass(SliderEditor);
columnModel.add(column);

为了拖拽拉条时能实时改变第一列的颜色值,我们监听了 rgb 三个属性变化:

dataModel.addDataPropertyChangeListener(function (e) {
    var property = e.property,
        data = e.data;
    if (property === 'a:r' || property === 'a:g' || property === 'a:b') {
        var r = data.a('r'),
            g = data.a('g'),
            b = data.a('b');

        data.a('color', 'rgb(' + r + ',' + g + ',' + b + ')');
    }
});

表格面板支持使用 addColumns 增加多列(等同于 columnModel.add):

tablePane.addColumns([
    {
        name: 'id',
        displayName: '序号'
    },
    {
        name: 'background',
        accessType: 'style'
    },
    {
        name: 'belongTo',
        displayName: 'Belong To',
        className: 'ht.ui.EnumColumn',
        editable: true,
        accessType: 'attr',
        datas: ['Eric', 'Yancy', 'David']
    },
    {
        name: 'Group Column', // 创建一个分组列
        className: 'ht.ui.GroupColumn',
        id: 'groupId1'
    },
    {
        name: 'childColumn',
        displayName: 'Child Column',
        accessType: 'attr',
        parentId: 'groupId1' // 指定从属的分组列
    }
]);

树表格组件

UI 提供了树表组件类 ht.ui.TreeTableView,该组件融合了树和表格的展示方式,显示 DataModel 数据容器中 Data 对象的父子关系和属性信息,支持排序和过滤等功能

TreeTableViewTableView 非常相似:列模型、数据模型、单元格渲染、单元格编辑的使用方式基本一致,但是增加了父子关系呈现(需要访问外网请求 jQuery 类库):

TreeTableView 默认会自动构建一个 treeColumn 列,通过 treeTableView.getTreeColumn() 可得到该列进行属性修改。

treeTableView = treeTablePane.getTableView();  
treeColumn = treeTableView.getTreeColumn();
treeColumn.setDisplayName('目录结构');
treeColumn.setAlign('center');
treeColumn.setWidth(260);

调用 treeTableView.expandAll() 可张开所有树节点,需注意要在模型数据添加完后进行:

treeTableView.expandAll();

分页显示

表格分页也是很常见的需求,请看下面的例子:

这个例子中我们创建了 ht.ui.Pagination 组件表示分页组件,这个组件可以使用布局器自由布局到任意位置,使用时需要设置对应的表格、数据行数等:

pagination = new ht.ui.Pagination();
pagination.setRowCount(paginationTableDatas.length);
var pageButtonGroup = pagination.getPageButtonGroup();
pageButtonGroup.setButtonCount(4);
pagination.setRowsPerPage(10);
pagination.fit(tableView);
// 请求分页数据, 需要在这里根据当前页填充表格数据
pagination.on('execRequest', function(e) {
    var tableView = e.tableView;
    var pageIndex = e.currentPage;
    var rowsPerPage = e.rowsPerPage;
    var sortColumn = tableView.getSortColumn();

    var datas = paginationTableDatas.slice(0);
    // sort datas
    if (sortColumn) {
        var sortName = sortColumn.getName();
        if (sortName === 'renderingEngine') {
            if (sortColumn.getSortOrder() === 'asc') {
                datas.sort(function(d1, d2) {
                    if (d1[0] > d2[0]) return 1;
                    else if (d1[0] < d2[0]) return -1;
                    else return 0;
                });
            }
            else if (sortColumn.getSortOrder() === 'desc') {
                datas.sort(function(d1, d2) {
                    if (d1[0] > d2[0]) return -1;
                    else if (d1[0] < d2[0]) return 1;
                    else return 0;
                });
            }
        }
    }

    var start = pageIndex * rowsPerPage;

    tableView.dm().clear();
    for (var i = start; i < (pageIndex + 1) * rowsPerPage; i++) {
        var row = datas[i];
        if (row) {
            var htData = new ht.Data();
            htData.a('id', i);
            htData.a('renderingEngine', row[0]);
            htData.a('browser', row[1]);
            htData.a('platform', row[2]);
            htData.a('engineVersion', row[3]);
            htData.a('cssGrade', row[4]);
            tableView.dm().add(htData);
        }
    }
});

注意 execRequest 事件,每次点击分页组件中页数按钮(切换当前页)和表头列(排序)都会派发事件,上面例子中我们直接操作了内存数据,另外一种常见的需求是服务器端分页,示范代码:

paginationView.on('execRequest', function(e) {
    var tableView = e.tableView;
    var pageIndex = e.currentPage;
    var rowsPerPage = e.rowsPerPage;
    var sortColumn = tableView.getSortColumn();

    var sortName,
        sortOrder;
    if (sortColumn) {
        sortName = sortColumn.getName();
        sortOrder = sortColumn.getSortOrder() || '';
    }

    ht.Default.xhrLoad('getDatas?pageIndex=' + pageIndex + '&sortName=' + sortName + '&sortOrder=' + sortOrder, function(datas) {
         // 重新填充表格数据
        tableView.dm().clear();

        for (var i = 0; i < datas.length; i++) {
            var row = datas[i];
            if (row) {
                var htData = new ht.Data();
                htData.a('id', i);
                htData.a('renderingEngine', row[0]);
                htData.a('browser', row[1]);
                htData.a('platform', row[2]);
                htData.a('engineVersion', row[3]);
                htData.a('cssGrade', row[4]);
                tableView.dm().add(htData);
            }
        }
    });
});

欢迎交流 service@hightopo.com