tree编写指南

2019-11-26 16:35 来源:未知

Omi框架能够以少量的代码声明式地编写可拖拽移动节点的树形组件。
通常树组件能够考验UI框架的健壮性,因为需要使用到UI框架的如下特性:

      拖拽(Drag/Drop)是个非常普遍的功能。你可以抓住一个对象,并且拖动到你想放置的区域。 很多javascript都类似实现了相关的功能,例如,jQueryUI的drag and drop组件。在HTML5中,拖拽(drag and drop)成为了标准操作,任何元素都支持。正因为这个功能太普遍了,所有的主流浏览器都支持这个操作。

  • 组件嵌套
  • 组件传值
  • 组件批量传值
  • 组件依赖自身递归嵌套(nest-self)
  • 子、孙或炎黄子孙访问根组件实例

启用拖拽 - draggable属性       非常简单,只需要将一个元素的拖动属性修改为draggable,这个元素就支持拖动了,如下所示:

下面来介绍下使用 omi-tree 的开发全过程。你也可以无视文章,先体验一把和直接编辑源码试一把:

<img draggable="true" /> 

omi-tree playground

拖动中数据的传递

类划分

  • tree.js 树组件的根容器类,包含节点移动,根据id获取节点等通用方法,这里把其排除在tree-node之外
  • tree-node.js 树节点,自递归嵌套组件,因为tree-node中可以包含tree-node

树的数据规则:

{
    name: 'Root',
    children: [
        {
            name: 'A',
            id: 1,
            children: [
                { id: 4, name: 'A1', children: [] },
                { id: 7, name: 'A2', children: [] }
            ]
        },
        {
            name: 'B',
            id: 2,
            children: [
                { id: 5, name: 'B1', children: [] },
                { id: 8, name: 'B2', children: [] }
            ]
        },
        {
            name: 'C',
            id: 3, children: [
            { id: 6, name: 'C1', children: [] },
            { id: 9, name: 'C2', children: [] }
        ]
        }
    ]
}

可以看到,每个节点都有唯一的id来标识,每个节点也有children属性来存放自己的子节点的信息。

      拖动的过程中,我们往往需要传递相应的逻辑数据来完成转换的过程,这里主要是使用dataTransfer对象进行数据传递,下面先看看它的成员:
方法成员:

组件HTML结构

tree结构:

<ul>
  <tree-node o-repeat="child in children" group-data="data.children"></tree-node>
</ul>
  • 通过 o-repeat 生成所有 tree-node
  • group-data 把 data.children 的数据批量传递给各个 tree-node

这里需要特别注意的是:

  • o-repeat 等所有指令对应的 scope 数据是 this.data
  • group-data,data等等 的 scope 是 this

tree-node结构:

<li data-node-id="{{id}}"  draggable="true"  ondragstart="dragStartHandler" ondragleave="dragLeaveHandler"  ondrop="dropHandler" ondragover="dragOverHandler" >
    <div data-node-id="{{id}}">{{name}}</div>
    <ul data-node-id="{{id}}" o-if="children.length > 0">
        <tree-node o-repeat="child in children" group-data="data.children"></tree-node>
    </ul>
</li>

可以看到每个tree-node都标记了draggable代表可以拖拽,drag和drop的支持情况大家可以caniuse一把。

  • 每个tree-node 既是拖拽对应,也是drop容器对象
  • li、div和ul都标记了 data-node-id 来存放id在dom元素上方便js里读取和传递

setData(format, data):把被拖动的数据赋值给dataTransfer对象。

完整代码解析

先看tree:

class Tree extends Omi.Component {
    //移动节点
    moveNode(id, parentId) {
        if (id === parentId) {
            return
        }

        if(this.check(parentId, id)) {
            let parent = this.getChildById(parentId, this.data.children)
            let child = this.removeChildById(id, this.data.children)
            parent.children.push(child)
            this.update()
        }
    }
    //验证子节点的孩子节点是否包含父亲节点,这里主要是为了防止把父节点拖拽到自己的孩子节点当中,这是个错误的逻辑操作
    check(parentId, childId){
        let current = this.getChildById(childId, this.data.children),
            children = current.children
        for (let i = 0, len = children.length; i < len; i++) {
            let child = children[i]
            if (child.id === parentId) {
                return false
            }

            let errorIds = this.check(parentId, child.id )
            if (!errorIds) {
                return false
            }
        }

        return true
    }
    //根据id移除child节点数据
    removeChildById(id, children) {

        for (let i = 0, len = children.length; i < len; i++) {
            let child = children[i]
            if (child.id === id) {
                children.splice(i, 1)
                return child
            }

            let target = this.removeChildById(id, child.children)
            if (target) {
                return target
            }

        }
    }
    //根据id获取child节点数据
    getChildById(id, children) {
        for (let i = 0, len = children.length; i < len; i++) {
            let child = children[i]
            if (child.id === id) {
                return child
            }

            let target = this.getChildById(id, child.children)
            if (target) {
                return target
            }
        }
    }

    render() {
        return `<ul>
                  <tree-node o-repeat="child in children" group-data="data.children"></tree-node>
              </ul>`
    }
}

//生成标签用于声明式嵌入其他组件
Omi.tag('tree', Tree)

下面来看 tree-node:

class TreeNode extends Omi.Component {

    dropHandler(evt) {
        //通过evt.dataTransfer.getData接收传递过来的数据
        this.getRootInstance(this.parent).moveNode(parseInt(evt.dataTransfer.getData("node-id")), parseInt(evt.target.dataset['nodeId']))
        this.node && this.node.classList.remove('drag-over')
        evt.stopPropagation()
        evt.preventDefault()

    }

    getRootInstance(parent){
        if(parent.moveNode){
            return parent
        }else{
            return this.getRootInstance(parent.parent)
        }

    }

    dragOverHandler(evt){
        this.node.classList.add('drag-over')
        evt.stopPropagation()
        evt.preventDefault()
    }

    dragLeaveHandler(){
        this.node.classList.remove('drag-over')
    }

    dragStartHandler(evt){
        //设置要传递的数据
        evt.dataTransfer.setData("node-id",this.data.id)
        evt.stopPropagation()
    }

    //局部样式,drag-over是拖拽在node之上的一个激活样式
    style(){
        return `
            .drag-over{
                border:1px dashed black;
            }
        `
    }

    render(){
        return `
                <li data-node-id="{{id}}"  draggable="true"  ondragstart="dragStartHandler" ondragleave="dragLeaveHandler"  ondrop="dropHandler" ondragover="dragOverHandler" >
                    <div data-node-id="{{id}}">{{name}}</div>
                    <ul data-node-id="{{id}}" o-if="children.length > 0">
                        <tree-node o-repeat="child in children" group-data="data.children"></tree-node>
                    </ul>
                </li>
            `
    }
}

//生成标签用于声明式嵌入其他组件
Omi.tag('tree-node',TreeNode)
  • dragStart的时候通过evt.dataTransfer.setData设置需要传递的数据,这里存放了拖拽的节点id
  • drop的时候通过evt.dataTransfer.getData读取传递过来的数据,这里取drag的node的节点id
  • 通过 o-if="children.length > 0" 决定是否生成 ul 标签
  • getRootInstance组件是递归去调取tree的对象的实例(因为tree-node可能包含tree-node,所以需要递归读parent)
  • 拿到tree的实例之后,调用tree的对象的实例的moveNode方法去移动节点,moveNode的本质就是修改节点数据,然后update组件

到此位置,复杂的拖拽移动都完成了。增删改查就更加简单了,大家可以接着试试~~~

      format:一个String型参数,指定被拖动数据的类型。该参数取值可以是“Text”(文本类型)和“URL”(URL类型)。该参数是大小写无关的,所以传入"text"与"Text"是一样的。
      data:一个变体类型参数,指定被拖动的数据。该数据可以是文本,图片路径,URL等等。
      该函数有Boolean类型的返回值,true表示数据成功加到dataTransfer中,false代表不成功。如果需要,可以通过这个参数来决定是否应该继续执行某些逻辑。

Omi相关

  • Omi官网omijs.org
  • Omi的Github地址
  • 如果想体验一下Omi框架,可以访问 Omi Playground
  • 如果想使用Omi框架或者开发完善Omi框架,可以访问 Omi使用文档
  • 如果你想获得更佳的阅读体验,可以访问 Docs Website
  • 如果你懒得搭建项目脚手架,可以试试 omi-cli
  • 如果你有Omi相关的问题可以 New issue
  • 如果想更加方便的交流关于Omi的一切可以加入QQ的Omi交流群(256426170)

getData(format):获取dataTransfer中存放的拖动数据。

      format意义与setData中的一样,取值可以是"Text"(文本类型)和"URL"(URL类型)。

clearData(format):移除指定类型的数据。

      这里的format除了上面可以指定的"Text"(文本类型)和"URL"(URL类型)外,还可以取下列值:file

  • 文件,html - html元素,image - 图片。
          这个方法可以用于去选择性的处理拖动的数据类型。  

属性成员:

effectAllowed:设置或获取数据源元素中的数据可以执行的操作。

      属性类型为字符串,取值范围如下:
          "copy"-复制数据.
          "link"-链接数据.
          "move"-移动数据
          "copyLink"-复制或链接数据,由目标对象来确定。
          "copyMove"-复制或移动数据,由目标对象来确定。
          "linkMove"-链接或移动数据,由目标对象来确定。
          "all"-所有的操作都是支持的。
          "none"-禁止拖动。
          "uninitialized"-默认值,采用默认的行为。
      注意设置effectAllowed为none以后,拖动是禁止的,但是鼠标形状还是显示没有可拖动的对象的形状,如果想不显示这个鼠标形状,则需要将window的event事件的属性returnValue设置为false。

dropEffect:设置或获取拖动的目标上允许的操作以及相关的鼠标形状。

      属性类型为字符串,取值范围如下:
         "copy"-鼠标显示为复制时的形状;
         "link"-鼠标显示为连接的形状;
         "move"-鼠标显示为移动的形状。
         "none"(默认值)-鼠标显示为没有拖动的形状。
      effectAllowed指定了数据源支持的操作,所以通常在ondragstart事件中指定。dropEffect指定了拖动放置的目标支持的操作,所以与effectAllowed配合,通常在拖动的目标上的 ondragenter, ondragover和ondrop等事件中使用。

files:返回拖动的文件的列表FileList。
types:ondragstart中发送的数据(被拖动的数据)类型的列表。

      dataTransfer对象的存在,使得在拖动的数据源和目标元素之间传递逻辑数据变成了可能。通常我们使用setData方法在数据源元素的ondragstart事件中提供数据,然后再目标元素中,使用getData方法获取数据。

 

拖动中触发的事件       下面是一次拖拽会发生的事件,基本上事件的触发顺序也就是下面的顺序:

dragstart:要被拖拽的元素开始拖拽时触发,这个事件对象是被拖拽元素。
drag:拖拽元素时触发,这个事件对象是被拖拽元素。
dragenter:拖拽元素进入目标元素时触发,这个事件对象是目标元素。
dragover:拖拽某元素在目标元素上移动时触发,这个事件对象是目标元素。
dragleave:拖拽某元素离开目标元素时触发,这个事件对象是目标元素。
drop:将被拖拽元素放在目标元素内时触发,这个事件对象是目标元素。
dragend:在drop之后触发,就是拖拽完毕时触发,这个事件对象是被拖拽元素。

      基本上事件的参数event都会传入相关的元素,可以很方便的进行一些修改。这里,我们并不需要处理每个事件,通常只需要挂接主要的几个事件即可。

拖动开始 - ondragstart事件       从这个事件传入的参数含有的信息非常丰富,从中可以很方便的获取到被拖动的元素(event.Target);从中可以设置被拖动数据(event.dataTransfer.setData);所以你可以很方便实现拖动的背后逻辑(当然你绑定的时候也可以传递其他的参数)。
拖动过程中 - ondrag,ondragover,ondragenter和ondragleave事件       ondrag事件的对象是被拖拽元素,通常这个事件处理的比较少。ondragenter事件是当拖动进入当前元素时发生,ondragleave事件是在当拖动离开当前元素时发生,ondragover事件是在拖动在当前元素中移动时发生。
      这里只需要注意一点,因为默认情况下,浏览器是禁止元素drop的,所以为了让元素可以drop,需要在这个函数中返回false或者调用event.preventDefault()方法。如下面的例子所示。
拖动结束 - ondrop,ondragend事件       当可拖动的数据被drop的时候,drop事件触发。drop结束后,dragend事件被触发,这个事件使用的也相对少一点。

TAG标签:
版权声明:本文由金沙澳门官网4166发布于文物考古,转载请注明出处:tree编写指南