頗有幸,將本身有使用過的,也是標準組件庫裏可能沒有的組件封裝成了一個小小的組件庫,沒想到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
吧~