颇有幸,将本身有使用过的,也是标准组件库里可能没有的组件封装成了一个小小的组件库,没想到start
数破百了,vue-gn-components,接下来就是一步步丰富这个项目了~。期待你们的start~
,这也是我持续丰富这个组件库源源不断的动力!css
首先第一个添加的是一个拖拽组件,功能很简单,就是让渲染出来的dom
是能够拖拽的。至于具体的dom
是啥,这个组件并不关心,使用slot
承接,本身往里面塞就行。vue
vue
的组件按照用途来讲,能够分为三类 (开发难度依次递增):router
切换。vue
组件的接口组件接口就是三样:props
、自定义事件、插槽。也就是告知别人怎么使用你的组件,因此一个组件在设计之初就要规划好这三样,使用者习惯你加功能,可不会习惯你改接口。这个拖拽组件设计以下:node
DragWrap
:<组件>
设计成了两个组件。最外层容器的组件,完成Dom
的移动及其余逻辑。DragItem
:<组件>
某一个须要拖拽的项,在这里面将拖拽的信息派发给容器组件。data
:<props>
接收一个数组,拖拽组件对应的渲染数据,拖拽以后Dom
变了,原渲染的数组也须要变动。例如能够告知后台,下次进来就按照变动后的数据渲染。watchData
:<事件>
派发出变动以后的和Dom
一一对应的原数据。drag
: <具名插槽>
若是不写具名插槽,点击整个拖拽的项均可以拖拽,不然只有具名插槽里的Dom
才能控制整个项拖拽。1. 拖拽改变当前
Dom
的顺序。2. 拖拽结束后,派发出改变的数据。
3. 完成插槽接口以及交互。
Dom
的顺序
h5
拖拽事件
标记
:这个很重要!!! 不知道为何不少人讲拖拽都不讲这个,也就是上面gif
展现里黄色的原点,它的位移决定了拖拽事件的行为。当点击开始拖拽以后,鼠标点击所在的位置就是标记。git
dragstart
:↓当单击下鼠标,并移动以后执行。↓github
drag
:↓在dragstart
执行以后,鼠标在移动时连续触发。↓vuex
dragend
:↓当拖拽行为结束,也就是松开鼠标的时候触发。↓npm
dragenter
:↓当正在拖拽的元素的标记进入某个Dom
元素时触发,自身首先会触发。被进入的Dom
元素会触发这个事件。↓数组
dragover
:当拖拽的元素的标记在进入的Dom
元素上移动时触发,在自身移动时也会触发。浏览器
dragleave
:↓当拖拽的元素在离开进入的Dom
时触发。↓app
h5
拖拽属性
draggable
:当须要某个元素能够拖拽时,需设置为true
,默认为false
。选中的文本、图片、连接默承认以拖拽。
DataTransfer对象
:该属性用于保存拖放的数据和交互信息,该组件没有使用到,暂忽略。
经过上面对事件的理解,咱们想了想,只须要监听三个事件dragstart
、dragenter
、dragend
。须要知道开始拖拽时的元素是谁,拖拽后去往的元素是哪一个,以及最后拖拽的结束。由于每个拖拽的项都是一个组件,因此这三个事件每次拖拽都会触发。因此咱们写出如下代码:
drag-item.vue <template> <div @dragstart.stop="onDragstart" // 拖拽开始时 @dragenter.stop="onDragenter" // 拖拽进入当前组件时 @dragend.stop="onDragend" // 拖拽结束时 draggable // 能够拖拽 class="__drag_item" > <slot /> </div> </template> <script> import Emitter from "../../mixins/emitter"; export default { name: "DragItem", mixins: [Emitter], mounted() { this.dispatch("DragWrap", "putChild", this.$el); // this.$el为当前组件实例对应的真实Dom。 // 触发DragWrap这个组件上的putChild方法,参数是当前组件的真实Dom。 }, methods: { onDragstart() { this.$el.style.opacity = "0.3"; this.dispatch("DragWrap", "dragstart", this.$el); // 触发dragstart }, onDragenter() { this.dispatch("DragWrap", "dragenter", this.$el); // 触发dragenter }, onDragend() { this.$el.style.opacity = "1"; this.dispatch("DragWrap", "dragend"); // 触发dragend } } }; </script>
可能看的有点蒙,这里解释一下Emitter
这么个mixin
,也是从iView
里copy
的,是组件库里会常用到的两个方法的注入,由于独立组件是不会去使用vuex
或bus
来通讯的,因此跨组件通讯要有本身的骚操做。
我这里先解释下vue
自定义事件的原理,父组件经过this.$on
往子组件的事件中心去注册事件,子组件经过this.$emit
触发本身事件中心的事件,但因为触发的这个事件是在父组件做用域下的,因此就完成了父子之间的自定义事件通讯,其实压根就是子组件本身玩本身的。
如下的两个方法broadcast
和dispatch
它们的原理就是在当前组件找到目标组件的实例,只不过一个是向下,一个是向上。而后经过this.$emit
去触发目标组件已经经过this.$on
注册的事件,因而就能够完成跨组件之间的通讯,它们找组件的方式是经过组件定义的name
属性。
function broadcast(componentName, eventName, params) { this.$children.forEach(child => { const name = child.$options.name; if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)); } else { broadcast.apply(child, [componentName, eventName].concat([params])); } }); } export default { methods: { dispatch(componentName, eventName, params) { let parent = this.$parent || this.$root; let name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); } } };
第一篇会啰嗦点,写独立组件确实有不少须要先交代下。接下来咱们写出如下DragWrap
组件的代码:
drag-wrap.vue <template> <div ref="wrap" @dragenter.prevent @dragover.prevent> // 阻止浏览器默认行为,否则会显示一个叉叉,很差看 <slot /> </div> </template> <script> export default { name: "DragWrap", // 组件名,很重要! created() { this.toDom = ""; // 拖拽时进入的元素 this.fromDom = ""; // 拖拽起始的元素 this.children = []; // 存放全部子组件元素的集合,以后说明用途 this.$on("dragstart", this.onDragstart); // 子组件会$emit触发dragstart,因此要先注册 this.$on("dragenter", this.onDragenter); // 子组件会$emit触发dragenter,因此要先注册 this.$on("dragend", this.onDragend); // 子组件会$emit触发dragend,因此要先注册 this.$on("putChild", child => { // 这里的child对应的是子组件的this.$el this.children.push(child); // 将全部的子组件的Dom元素收集起来 }); }, methods: { onDragstart(el) { this.fromDom = el; // 记录拖拽时开始的元素 }, onDragenter(el) { this.toDom = el; // 由于拖拽会不停的触发enter事件,因此进入的哪一个元素也要记录下来 if (this.fromDom === this.toDom) { return; } }, onDragend() {} } }; </script>
这里有几个要点须要先注意,this.$on
必定要比this.$emit
先执行,由于要先注册才能被触发吧,否则哪来事件触发了。还有就是父子组件的钩子执行顺序,mounted
是子组件先执行,created
是父组件先执行。
好了,接下来咱们有了拖拽开始的元素以及进入的元素,接下来开始拖拽使用insertBefore
交换它们的位置便可。不过这里有个注意点就是要知道当前拖拽元素是往前拖动仍是日后拖动,因此咱们在DragWrap
组件内添加如下代码:
drag-wrap.vue ... methods: { onDragenter(el) { this.toDom = el; if (this.fromDom === this.toDom) { return; } if(this.isPrevNode(this.fromDom, this.toDom)) { // 判断进入节点是否在起始节点的前面 this.$refs["wrap"].insertBefore(this.fromDom, this.toDom); // 将起始节点插入到进入节点的前面 } else { // 不然就是在以后 this.$refs["wrap"].insertBefore(this.fromDom, this.toDom.nextSibling); // 将起始节点插入到进入节点下一个兄弟节点的前面 } }, isPrevNode(from, to) { // to是否在from的前面 while(from.previousSibling !== null) { if(from.previousSibling === to) { return true; } from = from.previousSibling; } } } ...
通过上面代码的编写,如今元素已经能够拖拽并按照咱们预想的切换Dom
的位置,但这样还仅仅不够,Dom
顺序改了,对应的数据应该是什么样子,也须要知道,否则一刷新页面就是老样子也毫无心义。
还记得咱们以前在created
里定义的this.children = []
么,它里面包含了全部的拖拽组件的真实Dom
元素,但这个时候它已经被拖拽给打乱了。↓
这个时候咱们须要知道真实顺序的Dom
树怎么样的,而后和这颗被打乱的Dom
进行对比,以计算出对应的数组顺序被打乱成了什么样子,因此咱们在DragWrap
组件内添加如下代码:
drag-wrap.vue ... methods: { onDragend() { if (!this.data.length) return; const realDomOrder = [...this.$el.children].filter(child => //获取真实的Dom树 child.classList.contains("__drag_item") ); this.getDataOrder(realDomOrder, this.children); // 对比两颗树 }, getDataOrder(realList, dragAfterList) { const order = realList.map(realItem => { // 拿到打乱Dom树对应的序号 return dragAfterList.findIndex(dragItem => realItem === dragItem); }); const newData = []; order.forEach((item, i) => { // 将原数组的数据按照打乱的序号赋值给新数组 newData[i] = this.data[item]; }); this.$emit("watchData", newData); // 新数组的顺序就对应打乱Dom的序号,派发出去 } } ...
这个时候拖拽整个drag-item
组件的任意位置均可以进行拖拽,但有时候拖拽能够触发的位置用户想本身定义,因此咱们须要给用户这个接口,再DragItem
内进行如下更改:
<template> <div @dragstart.stop="onDragstart" @dragenter.stop="onDragenter" @dragend.stop="onDragend" :draggable="!$slots.drag || isDrag" // 若是有设置具名插槽,当前整个不能被拖拽 :style="{cursor: !$slots.drag ? 'move': ''}" // 具名插槽决定这个组件的交互手势 class="__drag_item" > <slot name="drag" /> // 提供一个具名插槽drag <slot /> </div> </template> export default { data() { return { isDrag: false }; }, mounted() { if(this.$slots.drag) { // 若是有定义具名插槽drag this.setSlotAttr(); } this.dispatch("DragWrap", "putChild", this.$el); }, methods: { setSlotAttr() { const slotVNode = this.$slots.default.find( // 找到vnode的第一个有效节点 vnode => !vnode.data && vnode.text !== " " ); const dragDom = slotVNode.elm.previousSibling; // 具名插槽对应的真实Dom if (dragDom.previousSibling !== null) { // 规定具名插槽内只能有一个根元素,不然报错~ throw "具名插槽内只能有一个根节点~"; } dragDom.addEventListener("mouseenter", () => { // 进入具名插槽的Dom,设置可拖动 this.isDrag = true; }); dragDom.addEventListener("mouseleave", () => { // 离开具名插槽的Dom,设置不可拖动 this.isDrag = false; }); dragDom.style.cursor = "move"; // 手势变为可移动 } } }
不知道为何,vue
对应的默认插槽是能够直接拿到真实Dom
的,而具名插槽是没法拿到的,有点坑~ 这里使用这么一个不太优雅的方式拿到,slotVNode.elm.previousSibling
,亲测也不影响使用。
而后咱们规定具名插槽内只能有一个根元素,否则下面设置的属性就只能只对一个元素起做用。
交换Dom
位置时,左右有个10%
的晃动吧~
<style scoped> .__drag_item { animation: shake .3s; } @keyframes shake { 0% { transform: translate3d(-10%, 0, 0); } 50% { transform: translate3d(10%, 0, 0); } 100% { transform: translate3d(0, 0, 0); } } </style>
npm i vue-gn-components import { DragWrap, DragItem } from 'vue-gn-components'; import "vue-gn-components/lib/style/index.css"; Vue.use(DragWrap).use(DragItem)
<template> <drag-wrap class="wrap" :data="list" @watchData="watchData"> <drag-item class="item" v-for="(item, index) in list" :key="index"> <template #drag> <div>拖拽Dom</div> </template> <div>{{item}}</div> </drag-item> </drag-wrap> </template> export default { data() { return { list: [111, 222, 333, 444, 555, 666, 777, 888, 999] }; }, methods: { watchData(newList) { console.log("newList", newList); } } }
drag-item
里面不能再写drag-wrap
。嵌套的版本也写出来了,逻辑比这个复杂了很多,不过最后发现好像没什么用。想了半天,感受只有一个场景会用到,开发一个拖拽进行布局的工具,拖拽结束后,导出布局代码。算了,算了,这个需求搞不了。start
吧~