概要
本文主要利用html 5的draggable原生特性,实现一个可拖拽的效果。我们可以创建包含多个页面节点的容器,每个容器可以包含多个节点。通过拖拽,可以移动一个容器内的节点到其他容器,每个容器内的节点和以通过拖拽改变排列顺序。
需求和设计
设计效果图如下:
具体需求包括:
- 每个容器可以包含多个节点或0个节点
- 每个容器内的节点可以通过拖拽转移到其他容器
- 每个容器内的节点可以通过拖拽改变排列顺序
- 容器增加或减少不影响基本的拖拽功能
代码及实现
拖拽功能的实现,主要涉及一下几个JS事件:
- dragstart : 当用户开始拖动目标节点时触发
- drop: 当被拖拽节点放入指定区域后被触发
- dragend: 当用户的拖拽操作完成时被触发
本文中,被拖拽节点的判定方法是采用一个工具css 类,在dragstart 中为被拖拽元素增加一个dragging的css样式类,在dragend事件中移除该类。
具体代码如下:
function dragStartHandler(e){e.target.classList.add("dragging");
}
function dragEndHandler(e){e.target.classList.remove("dragging");
}
当指定节点被拖入指定区域后,drop事件在默认情况下无法触发,需要在dragenter,drapleave和dragover中,阻止默认行为,代码如下:
function dragHandler(e){e.preventDefault();
}
基于以上处理,事件初始化代码如下,html代码请见附录:
const init = ()=> {const oLists = document.querySelector("div.lists");[...oLists.querySelectorAll("div.list")].forEach(list => {list.addEventListener("dragover", dragHandler, false);list.addEventListener("dragleave", dragHandler, false);list.addEventListener("dragenter", dragHandler, false);list.addEventListener("drop", dragDropHandler, false);let items = [...list.querySelectorAll("div.list-item")];items.forEach(item => {item.setAttribute("draggable", true);item.addEventListener("dragstart", dragStartHandler, false);item.addEventListener("dragend", dragEndHandler, false);});});
};
- 为每个元素容器div.list添加dragover, dragleave和dragenter事件,阻止其默认行为,保证drop事件被正常触发;
- 为每个可拖拽元素激活draggable属性,并绑定dragstart和dragend事件,以标识正在被拖拽的节点;
- drop事件处理函数是实现所有需求的关键,代码如下:
const dragDropHandler= (e) => {const container = e.target;const classList = [...container.classList];if (!classList.includes("list")) {return;}const oDragging = document.querySelector("div.app div.dragging");const oPrev = getPrevNode(container, e.clientY);if (oPrev == null) {container.append(oDragging);} else {container.insertBefore(oDragging, oPrev);}};
- dragDropHandler 在释放鼠标的一刻被触发;
- 只有包含list css类的div才可以作为元素容器,如果不包含list类的div,将被忽略;
- 获取当前被拖动的节点,即div.dragging;
- 判断拖动节点的释放位置,如果拖动的目的位置下面没有其他可拖动元素了,则直接放到容器的最后,否则放到指定元素的上面。
- getPrevNode用于检测拖动元素的目的位置下面是否有其他可拖动的节点,如果有,则返回最近的节点,否则返回为空,具体代码如下:
const getPrevNode = (container, y) =>[...container.querySelectorAll("div.list-item:not(dragging)")].reduce(function (total, next) {const { height, top } = next.getBoundingClientRect();const offsetY = y - top - height / 2;if (offsetY < 0 && offsetY > total.offsetY) {return {offsetY,Element: next}}return total;}, {offsetY: Number.NEGATIVE_INFINITY}).Element;
- getPrevNode 接收两个参数,一个是当前鼠标拖动目的地的容器div,一个是当前鼠标y轴的位置,两个参数都可以通过drop事件的参数获取;
- 遍历目标容器中,确定目标位置,具体算法如下:
如上图所示,我们将dragging状态的节点拖入当前的容器,目标位置是item1和item2之间。现在我们通过每个元素的y轴位置,来确定谁是它的正下方节点。
- 设释放鼠标时候,它的y轴坐标为y;
- 遍历当前容器的所有节点,获取每个节点的位置(y轴坐标),即每个节点中线y轴坐标(top)和每个节点的高度(height);
- 比较鼠标坐标y和每个元素的y轴坐标 (top + height/2),对于item1,y - top - height/2,大于0,证明item1在dragging元素的上方,直接忽略;对于其他item,y - top - height/2和y都小于0,则说明它们都在dragging元素的下方,我们找到y - top - height/2绝对值最小的item2,该元素在dragging元素的正下方;
- 遍历过程中,item2和鼠标的位置最接近,证明dragging元素的下一个元素是item2,返回。
附录
html代码如下:
<div class="app"><div class="header">Draggle and Sortable</div><div class="lists"><div class="list"><div class="list-item">Item 1</div><div class="list-item">Item 2</div><div class="list-item">Item 3</div><div class="list-item">Item 4</div><div class="list-item">Item 5</div></div><div class="list"></div><div class="list"></div><div class="list"></div></div></div>
css代码如下:
* {margin : 0;padding: 0;
}div {display: flex;flex-direction: column;box-sizing: border-box;
}.app {width:100%;height: 800px;margin-top:20px;
}.header {justify-content: center;align-items: center; font-size: 16px;font-weight: 600;line-height: 16px;
}
.lists {flex-direction: row;flex:1;height: 100%;padding : 20px;
}
.list {background: rgba(0, 0, 0, 0.1);border : 1px solid #000;flex:1;margin-left: 20px;padding-top:20px;transition: all 2s linear;
}
.list-item {justify-content: center;align-items: center; background-color:yellow;box-shadow: 4px 4px 2px #888888;height:35px;margin:15px;}.list-item.dragging{cursor: move;opacity: 0.5;
}
.list-item:hover{cursor: move;
}