HT UI 树组件手册

索引


概述

UI 库提供了树组件类 ht.ui.TreeView,用于显示 DataModel 数据容器中 Data 类型对象的父子关系树形结构,支持排序和过滤等功能。

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

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

示例代码:

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

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

例子中通过 checkMode 属性演示了复选框选中模式:

tree.setCheckMode('all');

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

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

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

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

// 添加 mousedown 事件
tree.on('d:mousedown', function(e) {
    // 调用 getDataAt 函数,传入事件对象,获取并打印节点
    console.log(tree.getDataAt(e));
});

通过 on(addViewListener) 可监听到树节点展开合并事件:

tree.on('expand', function(e) {
    // 监听节点展开事件,其它可用事件请参考 API 文档
    console.log(e);
});

loader 函数可以延迟加载数据:

tree.setLoader({
    // 如果 isLoaded(data) 为 false,展开此 data 时,会调用此 load 函数加载数据
    load: function(data) {
        // 定时器模拟网络加载
        setTimeout(function() {
            for (var i = 0; i < 10; i++) {
                var child = new ht.Data();
                child.setName('Data' + i);
                child.setParent(data);
                tree.dm().add(child);
            }
            // 加载标志设为 true
            data._loaded = true;
        });
    },
    // 判断节点是否需要延迟加载
    isLoaded: function(data) {
        // 只有 Button 按钮才需要延迟加载
        if (data.getName() === 'Button') {
            // _loaded 私有属性表示是否加载过
            return data._loaded;
        }
        else return true;
    }
});

渲染器

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

下面先看一个重写 drawRowBackground 绘制斑马线背景的例子:

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

tree.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 实现 Accordion 效果:

注意有些节点右侧有气泡信息,这些气泡信息有时候是需要响应事件的,比如单击,请参考例子的控制台输出

编辑器

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

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

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

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

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

tree.setEditable(true);
// 通过 & 指定编辑器组件的样式名为 myeditor;指定多个样式名也是允许的,
// 如 'ht.ui.editor.StringEditor&myeditor&myeditor2&myeditor3'
tree.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'
});

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

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

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

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

这个例子中复用了列表组件的枚举编辑器:

<script src="../../listview/examples/ListEnumEditor.js"></script>

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

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

拖拽

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

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

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

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

var oldHandleDrop = tree2.handleDrop;
tree2.handleDrop = function(dragEvent, datas, refType, refData) {
    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);

        // 复制子节点
        if (data.hasChildren()) {
            var children = data.getChildren();
            for (var j = 0; j < children.size(); j++) {
                var child = children.get(j),
                    newChild = new ht.Data();
                newChild.setName(child.getName() + 'Copy');
                newChild.setIcon(child.getIcon());
                newChild.setParent(newData);
            }
        }
    }
    oldHandleDrop.call(tree2, 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 = tree1.handleDrop;
tree1.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 函数:

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

欢迎交流 service@hightopo.com