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

最近在用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>
    

    效果圖


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