使用TypeScript給Vue 3.0寫一個指令實現元件拖拽

青城同學發表於2020-10-27

最近在用vue3重構後臺的一個功能。一個彈窗元件,彈出一個表單。然後點選提交。

早上運維突然跑過來問我,為啥彈窗擋住了下邊的表格的資料,我新增的時候,都沒法對照表格來看了。你必須給我解決一下。

我參考了一下幾大Vue的ui元件庫。發現element iview antv。好像都沒這個功能。為啥運維需要這個功能??

但是沒辦法,只能整一個就是了。

做之前本來想直接做到dialog這個元件中。但是又擔心後面其他的元件會用到。於是決定把拖拽功能做到指令中。

整個功能點如圖。滑鼠在拖拽區域拖動,整個對話方塊在瀏覽器可視範圍內移動。

Drag指令主要實現思路

  1. 在指令掛載的時候,監聽當前html節點的滑鼠點選事件

  2. 然後在點選當前html節點的時候,判斷是否點選在drag-target這個class所在的子節點上。如果是,那麼觸發document滑鼠移動事件。然後計算出滑鼠移動距離,對應修改彈出框的left值和top值。並記錄下當前按下的位置x和y

    let x = e.clientX;
    let y = e.clientY;
    
  3. 如何限制拖動的節點只能在螢幕內移動,不能移動出螢幕呢?

    1. 限制left不能小於0,在定位為position: fixed 的時候,left如果小於0,那麼html節點的左側肯定已經在顯示區域外了。那麼我們不能讓left小於0

      let bodyW = document.body.clientWidth;
      let bodyH = document.body.clientHeight;
      
      let left = elLeft - (x - move.clientX);
      if (left < 0) {
          left = 0;
      }
      
    2. 限制left不能大於可視區域的寬度減去當前html節點的寬度,如果left大於這個寬度,那麼當前html節點肯定右側已經處於顯示區域的右側外邊了

      if (left > bodyW - el.offsetWidth) {
          left = bodyW - el.offsetWidth;
      }
      
    3. 上下拖拽位置限制和左右拖拽限制思路是一樣,只要保證top的值大於0且小於螢幕可視範圍的高度減去當前html節點的高度,那麼拖動就無法拖出螢幕了。

      let top = elTop - (y - move.clientY);
      if (top < 0) {
            top = 0;
      }
      if (top > bodyH - el.offsetHeight) {
            top = bodyH - el.offsetHeight
      }
      

    drag指令完整程式碼

    import { App } from 'vue';
    
    export default {
        install(Vue: App<Element>) {
            Vue.directive('drag', {
                mounted(el: HTMLElement, bind) {
                    el.onmousedown = (e) => {
                        let elLeft = el.offsetLeft;
                        let elTop = el.offsetTop;
                        let dom = <HTMLElement>e.target;
                        if (dom.classList.contains('drag-target')) {
                            let x = e.clientX;
                            let y = e.clientY;
                            document.onmousemove = (move: MouseEvent) => {
                                let bodyW = document.body.clientWidth;
                                let bodyH = document.body.clientHeight;
    
                                let left = elLeft - (x - move.clientX);
                                if (left < 0) {
                                    left = 0;
                                }
                                if (left > bodyW - el.offsetWidth) {
                                    left = bodyW - el.offsetWidth;
                                }
                                el.style.left = left + 'px'
                                let top = elTop - (y - move.clientY);
                                if (top < 0) {
                                    top = 0;
                                }
                                if (top > bodyH - el.offsetHeight) {
                                    top = bodyH - el.offsetHeight
                                }
                                el.style.top = top + 'px'
    
                                document.onmouseup = (up: MouseEvent) => {
                                    document.onmousemove = null;
                                    document.onmouseup = null
                                }
                                if (window.getSelection()) {
                                    window.getSelection()?.removeAllRanges()
                                }
                            }
                        }
                    }
                },
                unmounted(el, bind) {
                    el.onmousedown = null;
                }
    
            })
        }
    }
    

    使用

    import DragDirective from './DragDirective'
    
    createApp(App).use(DragDirective).mount('#app')
    
    

    註冊指令到Vue App上,然後在需要移動的html節點上加上 v-drag ,並在觸發拖拽的子節點的class上,加上drag-target

    <div
              class="f-dialog"
              v-if="show"
              v-drag
              ref="dialog"
              :style="{ left: data.left + 'px' }"
            >
              <div class="f-dialog-header drag-target">
                <slot name="header">
                  <span>{{ title }}</span>
                </slot>
                <f-icon
                  icon="icon-close"
                  class="f-modal-close"
                  @click="close(false)"
                ></f-icon>
              </div>
              <div class="f-dialog-content">
                <slot></slot>
              </div>
              <div class="f-dialog-footer">
                <slot name="footer">
                  <button @click="close(true)">確定</button>
                  <button @click="close(false)">取消</button>
                </slot>
              </div>
            </div>
    

    效果圖


更多幹貨,以及本文的示例程式碼, 歡迎關注我的公眾號: 青城同學 回覆 拖拽程式碼 獲取下載地址
當然也可以掃碼

相關文章