HT UI 列表组件手册

索引


概述

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

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

可以看到,ListView 可以像普通组件那样设置背景、边框等属性;ListView 提供两个 Drawable 样式用来设置 hover 和选中两种状态的数据行的背景:

示例代码:

// 设置 hover 背景,等同于: list.setHoverBackgroundDrawable(new ht.ui.drawable.ColorDrawable('red'));
list.setHoverBackground('red');

// 设置选中背景
list.setSelectBackgroundDrawable(new ht.ui.drawable.SelectListItemDrawable(2, 'red', 'black', 'gray'));

通过 checkMode 属性可以控制是否启用复选框选中模式:

list.setCheckMode(true);

使用此组件时,一般需要监听选中节点变化做相应的处理,如下面的代码:

// 获取选中模型
var selectionModel = list.sm();
// 监听选中事件变化,在控制台打印选中的节点
selectionModel.addSelectionChangeListener(function(e) {
    console.log(selectionModel.getSelection());
});

可以看到,列表组件是由选中模型(SelectionModel)和数据模型(DataModel)驱动的,这两个模型的用法参考这里:数据模型

另外一种常见的需求是:点击时获取被点击的节点,可以用下面的方式实现:

// list.getView() 获取到组件 div,然后添加 mousedown 事件
list.getView().addEventListener('mousedown', function(e) {
    // 调用 getDataAt 函数,传入事件对象,获取并打印节点
    console.log(list.getDataAt(e));
});

渲染器

列表组件支持自定义每行的渲染效果,行的完整绘制可以通过重写 drawRow(g, data, selected, x, y, width, height) 实现,这个函数内部默认调用:

下面先看一个斑马线背景的例子:

这个例子中重写了 drawRowBackground 自行绘制行背景:

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

重写 drawRow(g, data, selected, x, y, width, height) 可实现自定义绘制整行,绘制行有两种方式:

首先看一个使用画笔绘制的例子:

这个例子实现了一个用户列表,完全重写了 drawRow 函数绘制用户头像、名称等信息:

// 重写 drawRow 绘制行
list.drawRow = function (g, data, selected, x, y, width, height) {
    var self = this,
        startX = 15,
        iconWidth = 48,
        iconHeight = 48,
        icon = data.s('user').avatar,
        name = data.s('user').name,
        time = data.s('user').time,
        message = data.s('user').message,
        drawable = self.getRowBackgroundDrawable(data, selected);

    // 绘制背景
    self.drawRowBackground(drawable, x, y, width, height, data);

    g.beginPath();

    // 绘制头像
    g.save();
    g.arc(startX + 24, y + height / 2, 24, 0, Math.PI * 2);
    g.clip();
    ht.Default.drawImage(g, ht.Default.getImage(icon), startX, y + 11, iconWidth, iconHeight, data, self);
    g.restore();

    startX += 48 + 10;

    var textColors = ['#95a2a8', '#687b83', '#586368'];

    // 绘制时间文字
    g.beginPath();
    ht.Default.drawText(g, time, '10px Lato, sans-serif', textColors[0],
        startX, y + 14, width - startX, 14, 'left', 'middle');

    // 绘制名字文字
    ht.Default.drawText(g, name, 'bold 14px Lato, sans-serif', textColors[1],
        startX, y + 28, width - startX, 20, 'left', 'middle');

    // 绘制内容文字
    ht.Default.drawText(g, message, '12px Lato, sans-serif', textColors[2],
        startX, y + 48, width - startX, 20, 'left', 'middle');
};

接下来演示使用 DOM 渲染:

这个例子中重写了 drawRow 返回一个 div 渲染大段文字:

drawRow: function (g, data, selected, x, y, width, height) {
    var self = this,
        div = data.div,
        drawable = self.getRowBackgroundDrawable(data, selected);
    // 绘制背景
    self.drawRowBackground(drawable, x, y, width, height, data);

    if (!div) {
        // 注意 div 缓存在 data 上,否则每次渲染都重新创建,会影响性能
        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 = self.getLabelFont();

        div.innerHTML = data.a('text');
    }
    return div;
}

每行文字内容不一样,高度也是不一样的,所以我们通过设置 setRowHeightFunc 分别设置每行的高度:

// 每行的高度缓存在 data._rowHeight 中
this.setRowHeightFunc(function (data) {
    return data._rowHeight;
});

// 计算每行的文本高度
initRowsHeight: function () {
    var dataModel = this.dm(),
        div = this._tempDiv;

    div.style.width = this.getContentWidth() + 'px';
    div.style.font = this.getLabelFont();
    document.body.appendChild(div);
    dataModel.getDatas().each(function (data) {
        div.innerHTML = data.a('text');
        data._rowHeight = div.scrollHeight;
    });
    document.body.removeChild(div);
}

编辑器

列表组件支持使用 ht.ui.editor.Editor 接口的实现类作为编辑器,如果希望用户双击能修改节点的名称,可以使用下面的方式:

list.setEditable(true);
// 也可以指定别的编辑器,如 ht.ui.editor.ColorEditor,UI 库提供的编辑器列表请参考 API 文档
list.setEditorClass('ht.ui.editor.StringEditor');

结束编辑时,如果不希望修改节点的 name 属性,而是修改自定义的字段,可以用下面的方式:

// 重写 setDataValue,修改 attr 属性
list.setDataValue = function(value, data) {
    data.setAttr('value', value);
};

有时候希望能修改编辑器组件的样式,如改变编辑文本框的背景,重定义一个 Editor 工作量未免太大了,UI 库提供了一种简单的方式:

list.setEditable(true);
// 通过 & 指定编辑器组件的样式名为 myeditor;指定多个样式名也是允许的,
// 如 'ht.ui.editor.StringEditor&myeditor&myeditor2&myeditor3'
list.setEditorClass('ht.ui.editor.StringEditor&myeditor');

// 外部配置 myeditor 样式
<script rel="ht-style">
    ({
        '&myeditor': {
            background: 'red',
            color: 'white'
        }
    })
</script>

也可以使用数据元素的 editorViewProperties 样式修改编辑组件:

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

除了每个数据元素单独配置,也可以重写 ListView#getEditorViewProperties 配置编辑组件属性:

listView.getEditorViewProperties = function(data) {
    // 这个函数默认返回 data.s('editorViewProperties'),
    // 我们这里重写以后为所有数据行的编辑组件使用相同的配置
    return {
        background: 'red', // 这里可以配置编辑组件的任意属性
        color: 'white'
    }
}

例子如下(双击启动编辑):

接下来我们创建一个自定义的下拉框 Editor 作为列表组件的编辑器:

这个例子中自定义了一个 ListEnumEditor,这个编辑器实现了 ht.ui.editor.Editor 接口,并在内部创建一个下拉框作为编辑组件;Editor 接口的函数列表请参考 API 文档;需要注意,因为下拉框的值是一个对象,所以例子中重写了列表的 setDataValue 函数分解这个对象设置不同的属性:

list.setDataValue = function (value, data) {
    if (value) {
        data.setName(value.label);
        data.setIcon(value.icon);
        data.a('id', value.id);
    }
};

拖拽

列表组件对启动拖拽和接受拖拽数据功能做了封装(无需再使用 DragHelper 手动处理),可通过下面的开关打开:

// 允许拖拽节点到其它组件或自身
list.setDragEnabled(true);

// 接受来自其它组件或自身拖拽过来的数据
list.setDropEnabled(true);

这个例子中左侧的两个列表组件,我们都打开了拖拽开关,所以数据可以互相拖拽;注意例子中重写了第二个列表组件的 handleDrop 函数,复制创建了一份新的拖拽数据,所以第一个列表中的数据拖拽到第二个列表组件中时,类似于【复制】(原数据不受影响),第二个列表中的数据拖拽到第一个列表组件中时,类似于【剪切】(原数据被移除)

var oldHandleDrop = list2.handleDrop;
list2.handleDrop = function(dragEvent, datas, refType, refData) {
    // 自身节点拖拽用默认逻辑处理(调整顺序)
    if (dragEvent.source === list2) {
        oldHandleDrop.call(list2, dragEvent, datas, refType, refData);    
    }
    else {
        // 复制从左侧列表拖拽过来的数据
        var newDatas = [];
        // 创建一份新节点
        for (var i = 0; i < datas.length; i++) {
            var data = datas[i];
            var newData = new ht.Data();
            newData.setName(data.getName() + ' Copy');
            newData.setIcon(data.getIcon());
            newDatas.push(newData);
        }
        oldHandleDrop.call(list2, dragEvent, newDatas, refType, refData);    
    }
};

最右侧的 Label 组件也可以接受拖拽数据,因为 UI 并没有直接在 Label 组件上封装拖拽操作,所以需要使用 ht.ui.DragHelper 处理接受拖拽数据:

// 监听拖拽事件
label.addViewListener(handleDragEvents);

// 处理拖拽事件
function handleDragEvents(e) {
    if (e.kind === 'dragEnter') {
        var target = e.target;
        // 接受数据
        ht.ui.DragHelper.acceptDragDrop(target);
        // 修改边框提醒用户
        target.setBorder(new ht.ui.border.LineBorder(2, 'blue'));
    }
    else if (e.kind === 'dragMove') {
    }
    else if (e.kind === 'dragCompleted') {
        var target = e.target,
            data = e.data.data;
        // 拖拽结束时修改 Label 的文本和图标
        if (data) {
            target.setText(data.getName());
            target.setIcon(data.getIcon());
        }

        // 恢复原来的边框
        target.setBorder(new ht.ui.border.LineBorder(2, 'rgb(159, 212, 148)'));
    }
    else if (e.kind === 'dragExit' || e.kind === 'dragCanceled') {
        var target = e.target;
        // 拖拽取消时恢复原来的边框
        target.setBorder(new ht.ui.border.LineBorder(2, 'rgb(159, 212, 148)'));
    }
}

接下来我们看一个从其它类型的组件 (Label) 拖拽数据到列表组件中的例子:

这个例子左侧上方是一个普通的 Label 组件,左侧下方是一个启动拖拽的列表组件;右侧是一个接受拖拽数据的列表组件, 首先对 Label 组件做些处理让它可以拖拽数据:

// 监听鼠标移动,实时设置文本内容为鼠标坐标(但是做了判断,如果已经开始拖拽了,就不要再改变文本)
label.on('d:mousemove', function (e) {
    if (!ht.ui.DragHelper.isDragging(label)) {
        label.setText('X: ' + e.clientX + ' Y: ' + e.clientY);
    }
});
// 监听鼠标按下,启动拖拽
label.on('d:mousedown', function (e) {
    ht.ui.DragHelper.doDrag(label, label.getText(), label.getRootCanvas(), -label.getWidth() / 2, -label.getHeight() / 2);
});

ht.ui.DragHelper 的用法在拖拽手册中有详细介绍,这里不再重复

因为 Label 的拖拽数据只是一段字符串(不是 ht.Data 实例),我们还要重写列表组件的 handleDrop 将其转换为 ht.Data 对象:

var oldHandleDrop = list1.handleDrop;
list1.handleDrop = function (dragEvent, datas, refType, refData) {
    // 创建一个新 ht.Data 实例
    var data = new ht.Data();
    data.setName(datas);
    oldHandleDrop.call(list1, dragEvent, [data], refType, refData);
};

还有一个细节需要注意,右侧的列表组件只接受 Label 组件拖拽过来的数据,不接受左侧下方的列表组件拖拽过来的数据,是因为例子中重写了 acceptDrop 函数:

list1.acceptDrop = function(e) {
    // 只接受来自 Label 组件的拖拽数据
    if (e.source instanceof ht.ui.Label) {
        ht.ui.DragHelper.acceptDragDrop(this);
    }
}

欢迎交流 service@hightopo.com