因业务场景须要一个可拖拽修改节点位置的树形组件,所以动手撸了一个,乘此机会摸了一把html5原生拖拽。近期有时间将核心部分代码抽出,简单说下实现方式。javascript
树形结构很是简单,tree组件做为父组件,结构以下html
tree.vue前端
<template>
<div>
<Tree-Node v-for="item in data" :key="item.title" :node-data="item"></Tree-Node>
</div>
</template>
复制代码
vue组件容许在它们本身的模板中调用自身,所以能够造成树形结构,在组件中必须填写惟一的name。vue
tree-node.vuehtml5
<template>
<transition name="slide-up">
<ul :class="classes">
<li>
<div :class="[prefixCls + '-item']">
<i class="sp-icon sp-icon-arrow-right" :class="arrowClasses" @click.stop="toggleCollapseStatus()"></i>
<span :class="[prefixCls + '-title-wrap']" ref="dropTarget">
<span :class="[dragClasses,dragOverClass]" ref="draggAbleDom" v-html="nodeData.title"></span>
</span>
</div>
<Tree-Node v-for="item in nodeData.children" :key="item.title" :node-data="item" v-show="nodeData.children.length && nodeData.isExpand"></Tree-Node>
</li>
</ul>
</transition>
</template>
复制代码
1.draggable属性规定元素是否可拖动,目前Internet Explorer 9+, Firefox, Opera, Chrome, and Safari 支持 draggable 属性 2.HTML 5 拖放apijava
处理拖拽节点须要几个关键变量node
所以定义了一个用于保存拖拽信息的对象git
dragOverStatus: {
overNodeKey: "",
dropPosition: "",
dragNode: {}
}
复制代码
这里将ondragstart事件绑定在子元素上,将其余事件绑定在父元素上,由于在测试真机IE10的时候,发现ondragstart和其余事件绑定在同一个元素上,没法触发ondragenter等事件。github
<span :class="[prefixCls + '-title-wrap']" ref="dropTarget">
<span :class="[dragClasses,dragOverClass]" ref="draggAbleDom" v-html="nodeData.title"></span>
</span>
复制代码
mounted() {
//绑定拖拽事件
if (this.root.draggable) {
this.$refs.draggAbleDom.draggable = !this.nodeData.noDrag;
this.$refs.draggAbleDom.ondragstart = this.onDragStart;
this.$refs.dropTarget.ondragenter = this.onDragEnter;
this.$refs.dropTarget.ondragover = this.onDragOver;
this.$refs.dropTarget.ondragleave = this.onDragLeave;
this.$refs.dropTarget.ondrop = this.onDrop;
this.$refs.dropTarget.ondragend = this.onDragEnd;
}
}
复制代码
触发某节点的拖拽事件时,就能够从拖拽事件里拿到当前节点实例。 使用HTML5提供的专门的拖拽与拖放API,原生的实现了复杂的操做,不须要本身用鼠标事件模拟,所以实现拖拽效果很是简单。api
(1).开始拖拽:在拖拽元素上触发,事件内只须要保存当前拖拽节点的信息便可
onDragStart(e, treeNode) {
this.dragOverStatus.dragNode = {
nodeData: treeNode.nodeData,
parentNode: treeNode.parentNodeData
};
this.$emit("on-dragStart", {
treeNode: treeNode.nodeData,
parentNode: treeNode.parentNodeData,
event: e
});
}
复制代码
(2).进入目标节点:在目标元素上触发,主要保存当前通过的节点的key,而后向外层发出事件,供组件调用者作其余操做。为了不拖拽一个元素快速通过许多个节点时频繁发出事件,设置定时器当停留必定时间后触发。
onDragEnter(e, treeNode) {
//当没有设置拖拽节点时,禁止做为目标节点
if (!this.hasDragNode()) {
return;
}
this.dragOverStatus.overNodeKey = "";
//拖拽节点与目标节点是同一个,return掉
if (
treeNode.nodeData._hash === this.dragOverStatus.dragNode.nodeData._hash
) {
return;
}
this.dragOverStatus.overNodeKey = treeNode.nodeData._hash; //当前通过的可放置的节点的key
//当前节点禁止作为放置节点时
if (treeNode.nodeData.noDrop) {
return;
}
//设置dragEnter定时器,停留250毫秒后触发事件
if (!this.delayedDragEnterLogic) {
this.delayedDragEnterLogic = {};
}
Object.keys(this.delayedDragEnterLogic).forEach(key => {
clearTimeout(this.delayedDragEnterLogic[key]);
});
this.delayedDragEnterLogic[
treeNode.nodeData._hash
] = setTimeout(() => {
if (!treeNode.nodeData.isExpand) {
treeNode.toggleCollapseStatus();
}
this.$emit("on-dragEnter", {
treeNode: treeNode.nodeData,
parentNode: treeNode.parentNodeData,
event: e
});
}, 250);
}
复制代码
(3).在目标节点上通过:在目标元素上触发,即时计算鼠标在目标节点上的位置,用于判断最终的放置位置,0(做为目标节点的子节点),-1(放置在目标节点的前面),1(放置在目标节点的后面),显示相应的样式。
onDragOver(e, treeNode) {
//当没有设置拖拽节点时,禁止做为目标节点
if (!this.hasDragNode()) {
return;
}
if (
this.dragOverStatus.overNodeKey === treeNode.nodeData._hash
) {
this.dragOverStatus.dropPosition = this.calDropPosition(e); //放置标识0,-1,1
}
this.$emit("on-dragOver", {
treeNode: treeNode.nodeData,
parentNode: treeNode.parentNodeData,
event: e
});
this.dragOverClass = this.setDragOverClass();//设置鼠标通过样式
},
复制代码
当鼠标处于目标节点内目标节点偏上方(1/5处),则意为放在目标节点前面-同级,当鼠标处于目标节点内目标节点偏下方(1/5处),意为放在目标节点后面-同级,不然做为目标节点的子节点
calDropPosition(e) {
var offsetTop = this.getOffset(e.target).top;
var offsetHeight = e.target.offsetHeight;
var pageY = e.pageY;
var gapHeight = 0.2 * offsetHeight;
if (pageY > offsetTop + offsetHeight - gapHeight) {
//放在目标节点后面-同级
return 1;
}
if (pageY < offsetTop + gapHeight) {
//放在目标节点前面-同级
return -1;
}
//放在目标节点里面-做为子节点
return 0;
}
复制代码
(4).放置节点:在目标元素上触发,此时将拖拽的信息变量做为参数将事件发射到外层,其他操做由外层来决定便可。
onDrop(e, treeNode) {
//当没有设置拖拽节点时,禁止做为目标节点
if (!this.hasDragNode()) {
return;
}
//当前节点禁止拖拽时
if (treeNode.nodeData.noDrop) {
return;
}
//拖拽节点与目标节点是同一个,不作任何操做
if (
this.dragOverStatus.dragNode.nodeData._hash === treeNode.nodeData._hash
) {
return;
}
var res = {
event: e,
dragNode: this.dragOverStatus.dragNode,
dropNode: {
nodeData: treeNode.nodeData,
parentNode: treeNode.parentNodeData
},
dropPosition: this.dragOverStatus.dropPosition
};
this.$emit("on-drop", res);
}
复制代码
(5).拖拽结束:做用在拖拽元素上,拖拽结束后将清除变量,恢复样式。
onDragEnd(e, treeNode) {
//当没有设置拖拽节点时,禁止做为目标节点
if (!this.hasDragNode()) {
return;
}
//当前节点禁止拖拽时
if (treeNode.nodeData.noDrop) {
return true;
}
this.dragOverStatus.dragNode = null;
this.dragOverStatus.overNodeKey = "";
this.$emit("on-dragEnd", {
treeNode: treeNode.nodeData,
parentNode: treeNode.parentNodeData,
event: e
});
}
复制代码
调用树形拖拽组件,获取拖拽过程当中的拖拽节点,目标节点,以及放置位置,具体处理拖拽结果由调用方决定,能够是经过调接口更新树结构,也能够由前端处理输入数据,更新视图。
<template>
<Tree :data="data1" draggable @on-drop="getDropData">
</Tree>
</template>
复制代码
getDropData(info) {
var dragData = info.dragNode.nodeData;
var dragParent = info.dragNode.parentNode;
var dropData = info.dropNode.nodeData;
var dropParent = info.dropNode.parentNode;
var dropPosition = info.dropPosition; //0做为子级,-1放在目标节点前面,1放在目标节点后面
//把拖拽元素从父节点中删除
dragParent.children.splice(dragParent.children.indexOf(dragData), 1);
if (dropPosition === 0) {
dropData.children.push(dragData);
} else {
var index = dropParent.children.indexOf(dropData);
if (dropPosition === -1) {
dropParent.children.splice(index, 0, dragData);
} else {
dropParent.children.splice(index + 1, 0, dragData);
}
}
}
复制代码
做为子节点,改变层级
修改排序,将拖拽节点放在目标节点后面