索引
UI
库提供了列表组件类 ht.ui.ListView
,用于显示 DataModel
数据容器中 Data
类型对象的属性信息,支持排序和过滤等功能。
通过 list = new ht.ui.ListView(dataModel);
初始化构建一个列表组件对象,dataModel
参数为列表组件绑定的数据模型,该模型参数为空时列表组件构造函数内部将创建一个新的数据模型进行绑定。
可以看到,ListView
可以像普通组件那样设置背景、边框等属性;ListView
提供两个 Drawable
样式用来设置 hover
和选中两种状态的数据行的背景:
hoverBackground
和 hoverBackgroundDrawable
鼠标划过(hover
)的数据行的背景selectBackground
和 selectBackgroundDrawable
选中的数据行的背景,默认选中背景是 ht.ui.drawable.SelectListItemDrawable
实例,API
文档中有这个 Drawable
的属性说明示例代码:
// 设置 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(drawable, x, y, width, height, data)
绘制行背景drawIcon(g, data, x, y, width, height)
绘制行图标drawLabel(g, data, x, y, 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)
可实现自定义绘制整行,绘制行有两种方式:
drawRowBackground
使用 Canvas
的画笔绘制DOM
对象用 DOM
渲染(如果文本是一段 HTML
文本,带有颜色、字体、甚至图片等信息,就要用这种方式)首先看一个使用画笔绘制的例子:
这个例子实现了一个用户列表,完全重写了 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);
}
}