前端知识 | HTML5 Drag 以及 Sortable 和 Dragula 使用

Posted SEATELL海说软件

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端知识 | HTML5 Drag 以及 Sortable 和 Dragula 使用相关的知识,希望对你有一定的参考价值。


-海说软件接受各种技术咨询及开发业务-


前端知识 | HTML5 Drag 以及 Sortable 和 Dragula 使用

前端知识 | HTML5 Drag 以及 Sortable 和 Dragula 使用


       拖放(Drag和 drop)是 HTML5 标准的组成部分。本文主要讲解开发中根据实际情况选择使用原生拖拽还是借助拖拽库,以及遇到的一些问题如何解决。

       首先说一下 Html5 Drag 的基本用法:

       1.      HTML5 原生:

       设置元素为可拖放:

    首先,为了使元素可拖动,把 draggable 属性设置为 true :

<imgdraggable="true" />

 

       ondragstart 和 setData():


     ondragstart 属性调用了一个函数,drag(event),它规定了被拖动的数据。

     dataTransfer.setData()方法设置被拖数据的数据类型和值:

 

function drag(ev) {ev.dataTransfer.setData("Text",ev.target.id); }

 

       注意事项:

       火狐浏览器必须设置 setData,否则拖动无效,当设置成 Text 时,火狐浏览器拖拽会出现新建标签,提供一种本人的解决方法:


onDragStart={(e) => {e.dataTransfer.setData('Number', v._id)}}


       只要 setData 不是 Text,火狐就不会出现拖拽一个产生一个新建标签,但是这个有一个弊端,那就是 Edge 浏览器拖拽就会报错,可以通过判断浏览器来处理,只有火狐浏览器设置 setDtate('"Text"),

给出判断火狐浏览器代码:


//判断是火狐浏览器

window.userAgent=window.navigator.userAgent.toLowerCase().indexOf('firefox')>=1;

onDragStart={(e) => {e.dataTransfer.setData(userAgent? 'Number' :'Text', v._id),}}


       ondrop

      当放置被拖数据时,会发生 drop 事件。


functiondrop(ev) {     ev.preventDefault();     var data=ev.dataTransfer.getData("Text");     ev.target.appendChild(document.getElementById(data)); }


       总结一下:

整个拖拽事件触发的顺序如下:

dragstart-> drag -> dragenter -> dragover -> dragleave -> drop ->dragend

事件详情

dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发。

darg:事件主体是被拖放元素,在正在拖放被拖放元素时触发。

dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。

dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。

dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。

drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。

dragend:事件主体是被拖放元素,在整个拖放操作结束时触发。

 

       原生拖拽支持拖拽的同时操作鼠标滚轮滚动页面,以及按 ESC 键取消拖拽,下面介绍两种拖拽的库,各有优缺点,可根据实际情况选择使用

 

2.       Sortable和React-sortable

      因为项目开发使用react,所以也讲解一下 react-sortable 的用法,react-sorable 是构建在 Sortable 之上的 React 组件(https://github.com/RubaXa/Sortable)。

      基本使用:


import Sortable from 'react-sortablejs'

<Sortable

    key={uniqueId()}

    options={{

       group: {name:'set', put: false},

        filter: '.root',

        sort: false,

        onStart:(e) => {

            dragId = e.item.dataset.id

        },

        onAdd:(e) => {

            this.changePermission(5, e)

        },

       setData: function (dataTransfer, dragEl){

            dataTransfer.setData(userAgent? 'Number' : 'Text', dragEl.textContent)

        },

    }}

    style={{minHeight: '100px'}}

    //onChange={() => {}}

    className="member-list-box"

    tag='div'

> 

//遍历拖动列表内容

    {empty.check(roots)? roots.map((v, index)=> {

        return (

            <div data-id={v._id}

                key={uniqueId()}>

              项目内容

            </div>

        )

    }) : ''}

</Sortable>

 

      示例图如下:左右两个列表可互相拖拽


前端知识 | HTML5 Drag 以及 Sortable 和 Dragula 使用



      部分参数讲解:

group:"name"// or { name: "...", pull:[true, false, clone], put: [true, false, array] }   

    //分组名称,只有名称相同的container才可以互相拖拽放置,put:false,表示此container不允许其他container拖动项目放置


filter:".ignore-elements"// Selectors that do not lead to dragging(String or Function),设置了filter,对应的item会不允许拖动


sort:true// sorting inside list,默认container内部的元素可以随意拖动排序,如果不允许则设置sort为false


onStart: (e) => {

            dragId= e.item.dataset.id

        },//拖动开始,一般与真实数据交互时,可能需要记录拖动元素的唯一标识值,这里是_id,设置data-id就可以通过e.item.dataset.id来获取到


setData: function (dataTransfer,dragEl) {

     dataTransfer.setData(userAgent?'Number':'Text', dragEl.textContent)

}, //这里没有用到data数据,只是为了解决火狐浏览器拖拽必须设置setData


 onAdd: (e) => {

            this.changeList(e)

        },//还有onUpdate,表示在同一个列表内顺序发生改变,因为本项目只关注两个list之间的拖拽,同一列表内不允许变化顺序,所以这里使用onAdd,当列表添加项目时执行

 

       自定义滚动:有时可能因为页面布局导致拖拽时滚轮操作滚动失效,或者拖拽时默认靠近窗口顶部的区域才触发浏览器滚动,想要这个触发区域扩大我们就需要自己来实现滚动了。实现思路:监听页面鼠标,在指定区域,操作滚动条滚动


       当你真正实现的时候你会发现,拖拽时鼠标监听事件是无效的,找了很久的方法,发现有一个


onMove:function (/**Event*/evt, /**Event*/originalEvent) {

// Example:http://jsbin.com/tuyafe/1/edit?js,output         

evt.dragged; // dragged htmlElement         

evt.draggedRect; // TextRectangle {left, top, right иbottom}           

evt.related; // HTMLElement on which have guided            

evt.relatedRect; // TextRectangle           

originalEvent.clientY;// mouse position// returnfalse; — for cancel}


       很开心,看到了 originalEvent.clientY ,这个可以监听到鼠标的位置,然而,当移动到拖动列表之外的时候这个监听就又无效了,最后使用的还是查看了原生拖拽,找到了解决方法。


       在页面的最大容器上面设置

<div onDragOver={(e) =>{this.allowDrop(e)}}>


allowDrop (e){

    e.preventDefault()

    if(e.clientY <=40 && this.state.type ==2) {

        currPoi = document.documentElement.scrollTop ||document.body.scrollTop

        if(currPoi > 0) {

            currPoi-= 10

        }

        window.scrollTo(0,currPoi)

        console.log(e.clientX, e.clientY, currPoi)

    }

}


      这样就实现了自定义滚动了。

 

      优点:简单易使用,支持拖拽时鼠标滚轮滚动以及 ESC 取消拖拽

      弊端:Sortable 有一个最大的弊端就是没有离开容器时拖动元素回到最原始位置,这个在一个网格布局的拖拽时会非常不方便,有时候拖拽发现又不想拖拽了,但是也不记得原来的位置,他必须放在某一个容器中,这就很尴尬了。所以,这时候考虑换一个库,使用dragula.

3.      dragula

      支持以下框架:

  1. Official Angular bridge for dragula (demo)

  2. Official Angular 2 bridge for dragula (demo)

  3. Official React bridge for dragula (demo)

      特点:

  1. 超级容易设置

  2. 没有臃肿的依赖关系

  3. 数据自行排序

  4. 项目将被丢弃的阴影提供了视觉反馈

  5. 触摸事件

  6. 无需任何配置即可无缝处理点击

      示例图片:




      这是一个网格布局的拖拽,横向纵向都可以拖拽,示例代码如下(在 react 中使用):


{v.children.map((v2, index2) => {

    return(

        <div style={{

            borderTop: '1pxsolid rgba(0,0,0,.25)',

            borderRight: '1pxsolid rgba(0,0,0,.25)',

        }}

             key={uniqueId()}

             data-state={index2+ 1}

             data-id={v.belong._id}

             className="col-md-6 list-card dragula-container"

         >

            {v2.map((v3, index3) => {

                return(

                    <div

                       data-id={v3._id}

                        key={uniqueId()}

                       className="col-md-22 card-track f6"

                    >

                item

                   </div>

                )

            })}

        </div>

    )

})}


       类名 dragula-container 就是一个 list 容器,v2 遍历出来的每一项都是 list 里面可拖拽的元素,具体使用如下:


componentDidMount () {

    drake =Dragula({

        isContainer: function (el) {

            returnel.classList.contains('dragula-container') //获取到所有dragula-container容器

        },

        revertOnSpill: true,

        moves: function (el,target, source,sibling) {

            // console.log(el.classList)

            if(el.classList.contains('dragu-trace')) {

                returnfalse

            }

            returntrue

        },

        accepts: function (el,target, source,sibling) {

            if(target.dataset.state== source.dataset.state&& target.dataset.id== source.dataset.id) {

                returnfalse

            }

            returntrue

        }

    }).on('drag', (el)=> {

        dragId = el.dataset.id

    }).on('drop', (el, target,source, sibling) => {

        if(target.className === 'col-md-6list-card dragula-container'){

            this.dragGrid(target, sibling)

        } else{

            this.onDrop(target, target.dataset.id)

        }

    }).on('over', function(el, container){

        if(container.className === 'col-md-6list-card dragula-container'){

        } else{

           container.className += ' z'

        }

    }).on('out', function(el, container){

        container.className = container.className.replace('z', '')

    })

}

 

      部分参数讲解:

  isContainer:function (el) {     return el.classList.contains('dragula-container');  } 

//子动态地将所有的 DOM 元素用一个 CSS 类 dragula-container 作为 dragula 容器来处理

 

 revertOnSpill:true,              // spilling will put the element back where it wasdragged from, if this is true,这个就是解决 sortable 没有移出容器 item 不能放回到最原始位置

 

 moves:function (el, source, handle, sibling) {     returntrue; //elements are always draggable by default},例子中的  if (el.classList.contains('dragu-trace')) {

               return false

}返回 false 表示类名为 dragu-trace 设置为不可拖动

 

 accepts:function (el, target, source, sibling) {     returntrue; } //elements can be dropped in any of the `containers` by default,例子中 accepts: function (el, target, source, sibling) {

    if(target.dataset.state == source.dataset.state && target.dataset.id ==source.dataset.id) {

        returnfalse

    }

    returntrue

}表示同一个列表内部的元素不能放置,也就是同一个 container 不允许变换位置,在实际项目中,同一个列表里面的 item 交换位置没有实际意义,所以很多时候回有这个需求

 

      接下来就是拖拽过程中的一些事件处理,如下:

drake.on (Events)


      drake 是一个事件发射器. 使用 drake.on(type,listener)可以跟踪以下事件:


事件名

参数

描述

drag

el, source

el 拖放源

dragend

el

拖拽事件以cancel, remove, or drop结束

drop

el, target, source, sibling

sibling:放置到 target 的兄弟元素

cancel

el, container, source

el 被拖动,但是没有被放置在其他容器中,而是回到了最原始的位置

remove

el, container, source

el 从 dom 结构中被移除

shadow

el, container, source

el, 辅助阴影, 被移入容器中. el 位置被改变时也许会触发多次, 甚至在同一个容器中时

over

el, container, source

el 经过容器时

out

el, container, source

el 被移出容器或者放置时

cloned

clone, original, type

DOM 被克隆时,type为 ('mirror' or 'copy'),copy: true

 


海说软件会持续推出前端教学课程、技术干货。

往期课程:






















-END- 


以上是关于前端知识 | HTML5 Drag 以及 Sortable 和 Dragula 使用的主要内容,如果未能解决你的问题,请参考以下文章

HTML5 drag和drop的亲手实践

干货|前端开发全面知识库

原生 drag drop HTML5

如何在 Drag-Sort ListView 中刷新 ListView?

在 SQLite DB 中使用 Drag-Sort ListView

HTML5Drag&DropAPI