【詳】JS實現拖拽元素互換位置

寫在前面的廢話

大家好,我是練習js時長接近兩年半的個人練習生--李大雷

算了,直接 雞,你太美~

應用場景

很多時候,我們需要讓用戶來自定義自己想要的菜單順序,或者一些按鈕的排序,那麼這個時候,怎麼給用戶自定義順序呢?
拖拽無疑是最簡單易懂的,因爲玩過手機的都知道怎麼拖動桌面的app來改變位置。

那麼要怎麼做呢?最簡單的方式肯定是用H5的拖放啦~

一些你需要了解的基礎知識

首先我們先來看看,這兩個單詞,drag--拖,drop--放,從這裏就很容易看出來,這裏的操作邏輯了。
我們來看看有哪些事件可以給我們使用。

    • 被我們拖的元素(按住鼠標)
    1. ondragstart - 用戶開始拖動元素時觸發
    2. ondrag - 元素正在拖動時觸發
    3. ondragend - 用戶完成元素拖動後觸發
    • 釋放拖拽元素時觸發的事件(鬆開鼠標)
    1. ondragenter - 當被鼠標拖動的對象進入其容器範圍內時觸發此事件
    2. ondragover - 當某被拖動的對象在另一對象容器範圍內拖動時觸發此事件
    3. ondragleave - 當被鼠標拖動的對象離開其容器範圍內時觸發此事件
    4. ondrop - 在一個拖動過程中,釋放鼠標鍵時觸發此事件
    我們來舉例子說明一下
    <div class="A" draggable="true" ondragstart="fn()" ondrag="fn()" ondragend="fn()"></div>
    <div class="B" ondragenter="fn()" ondragover="fn()" ondragleave="fn()" ondrop="fn()"></div>

    假設有div A和div B,當我按住A,開始拖動(A dragstart觸發一次)(drag在你移動的時候不斷觸發),然後你經過了B(B觸發了dragenter事件),然後你在B裏瘋狂摩擦(那就瘋狂觸發B的dragover,這句話怎麼越讀越不對勁?),然後你從B中出來(那就觸發了B的dragleave),然後又進入B中(並且放開鼠標,那麼就會觸發B的drop和A的dragend);

    對於A來說,它的事件就前面3個,對於B來說,它的事件就是後面4個;
    A是攻,那麼B就是受了。當然你也可以自攻自受,就像孟德爾的自交豌豆一樣
    我們下面做的拖拽也是自攻自受的情況,因爲你可能拖動A和B交換,也可能拖動B來和A交換位置。

    一些需要注意的點:
    1. 如果只需要拖動外層div,請務必把子元素的draggable屬性設置爲false(如果子元素裏面有默認可拖動元素,則需要把裏面的可拖動元素的屬性設置爲false);不然會引起很多奇怪的現象(比如你想拖一個包含圖片的div,結果只把圖片拖出來了);
    2. 鏈接和圖片是默認可以拖動的;
    3. ondragenter和ondragleave可能會觸發多次,如果你把A拖動到B裏,B一個大div設置了enter和leave事件,但是它裏面還有很多子div,那麼每進出一個子div,都會觸發一次enter和leave事件。

    開始操刀

    這個標題的cao是第一聲。
    經過我們上面的一頓基礎知識學習以後呢,我們就很容易想清楚這個實現邏輯。

    把A設置爲可以拖動,當A拖動到B的時候,我們就互換A和B兩個dom節點。

    至於怎麼互換呢?我們可以直接調換兩個節點的內容,或者我們調換兩個dom節點的位置兩種方法,這裏我用的是第一種方法,第二種留給大家去嘗試啦~

    1. 我們先寫一個大概的樣式

    clipboard.png

    2. html結構如下

    <div class="card" draggable="true" 
    ondrag="handleDrag(event,this)" 
    ondragstart="handleDragStart(event,this)" 
    ondragover="handleDragOver(event,this)" 
    ondragend="handleDragEnd(event,this)" 
    ondrop="handleDrop(event,this)" 
    ondragenter="handleDragEnter(event,this)">
                
        <span class="card-name">
            ${title}
        </span>
        <div class="card-img">
            <img src="${src}" draggable="false" alt="">
        </div>
    </div>

    3. 開始寫邏輯,請仔細查看註釋

    //先定義兩個變量來保存源元素,以及目標元素,還有記錄一下上次交換的dom
    //爲什麼要這一步呢?往後面看
    let fromDom = null,
        toDom = null,
        lastDom = null;
    
    //開始拖拽
    function handleDragStart(e, dom) {
        //開始拖拽的時候,把來源保存下來
        fromDom = dom;
    }
    //拖拽中
    function handleDrag(){
        console.log('如果你有業務邏輯的話,你可以寫,但是我沒有,抱歉')
    }
    //拖到了另一個div中,這個時候的dom就是另一個元素了哦
    function handleDragEnter(e, dom) {
        //保存目標元素
        toDom = dom;
        if(fromDom == lastDom){
            //第一次調換
            //爲什麼要分爲幾次調換位置呢?
            //想一下,如果我剛A和B調換了位置,那麼就是B和A了但是此時我的鼠標還沒有鬆開!
            //那麼我又移動到C,那麼互換的位置就是B和C了,但是其實我一開始拖拽的是A,我只想換AC只是不小心路過了B!
            //因此我們這裏就要使用一個lastDom來記錄上次路過交換的DOM,同時也要區分第幾次調換。
            swapDom(lastDom, toDom);
            //記錄新的‘上一個dom’
            lastDom = toDom;
        }else{
            //這個防止enter多次觸發
            if(lastDom == toDom){return;}
            //第N+1次調換,要先把上一個div的東西還原回去,再跟第三個div互換
            swapDom(fromDom,lastDom);
            swapDom(fromDom,toDom);
            //記錄新的‘上一個dom’
            lastDom = toDom;
        }
    }
    
    //在B中移動
    function handleDragOver(e, dom) {
        //默認無法把元素放置到其他元素當中,如果這個不寫,無法交換div的innerHTML值,所以需要阻止默認事件,這一步很重要!!
        e.preventDefault();
    }
    
    //放手
    function handleDragEnd(e,dom){
        //拖拽時鬆開鼠標就會會觸發dragend事件,這個dom是拖拽的節點。
        //重置toDom,下次拖拽就是新拖拽了,fromDom和lastDom會在dragStart的時候重置
        toDom = null;
    }
    //有上面那個,其實這個可以省略了。
    function handleDrop(e, dom) {
        //只有在可放置的元素上面鬆開鼠標纔會觸發drop事件,所以這個dom是被放置的dom節點。
        //重置toDom,下次拖拽就是新拖拽了,fromDom和lastDom會在dragStart的時候重置
        toDom = null;
    }
    
    //交換dom內容
    function swapDom(from, to) {
        let temp = a.innerHTML;
        a.innerHTML = b.innerHTML;
        b.innerHTML = temp;
    }

    總結

    其實我們用不上那麼多事件回調,主要的是 開始拖拽保存來源,進入目標時,保存目標,並且經過判斷後交換,交換完以後,我們就把目標重置,完事~
    邏輯比較簡單,不過寫動態css比較麻煩(因爲我們需要一些css的效果來分辨哪個是被你拖動的,那個又互換了位置之類的,有比較好的用戶體驗),剛開始寫經常傻傻分不清是來源dom還是目標dom~

    辣雞源碼

    此部分適合新手玩家,因爲自己只是隨意寫寫,並沒有寫得很規範,希望大家不要學習!

    <!DOCTYPE html>
    <html>
    
    <head>
        <title></title>
        <style>
        body {
            margin: 0;
        }
    
        .box {
            display: flex;
            justify-content: flex-start;
            flex-wrap: wrap;
        }
    
        .card {
            flex: 1;
            min-width: 26%;
            max-width: calc(33.3% - 40px);
            height: 200px;
            margin: 30px 10px;
            position: relative;
            padding: 10px;
            box-shadow: 0 2px 5px 0 #999;
            border-radius: 5px;
            border: 2px dashed transparent;
        }
    
        .card-name {
            position: absolute;
            top: 10px;
            left: 10px;
            line-height: 20px;
            height: 20px;
        }
    
        .card-img {
            position: relative;
            padding-top: 20px;
            box-sizing: border-box;
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
        .card-img img {
            width: 100%;
            height: 100%;
        }
    
        .dragging-over * {
            pointer-events: none;
        }
        </style>
    </head>
    
    <body>
        <div class="box">
        </div>
    </body>
    <script>
    const htmlArr = [
        { title: '示例1-風景', src: 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2756575517,833879878&fm=200&gp=0.jpg' },
        { title: '示例2-風景', src: 'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=328517395,2303970886&fm=26&gp=0.jpg' },
        { title: '示例3-風景', src: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1554369684535&di=1c1dbfbd4545ad0a05e12cbbbfe3eeef&imgtype=0&src=http%3A%2F%2Fpic41.nipic.com%2F20140601%2F18681759_143805185000_2.jpg' },
        { title: '示例4-風景', src: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1554369684534&di=d6e34af6fce6564f9df6c4eecc27d2ce&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fimgad%2Fpic%2Fitem%2F4d086e061d950a7b9138ff1000d162d9f3d3c9d1.jpg' },
    ]
    
    let fromDom = null,
        toDom = null,
        lastDom = null;
    
    function handleDragStart(e, dom) {
        lastDom = fromDom = dom;
        dom.style.border = "2px dashed #999";
        dom.style.opacity = 0.4;
    }
    
    function handleDrop(e, dom) {
        //只有在可放置的元素上面鬆開鼠標纔會觸發drop事件
        console.log('drop');
        dom.style.opacity = "";
        fromDom = null;
        toDom = null;
    }
    function handleDragEnd(e,dom){
        //拖拽時鬆開鼠標就會會觸發dragend事件
        console.log('end');
        dom.style.border = "2px dashed transparent";
        dom.style.opacity = "";
        toDom = null;
    }
    function handleDragEnter(e, dom) {
        toDom = dom;
        if(fromDom == lastDom){
            //第一次調換
            swapDom(lastDom, toDom);
            lastDom = toDom;
        }else{
            //第N+1次調換,要先把上一個div的東西還原回去,再跟第三個div互換
            //這個防止enter多次觸發
            if(lastDom == toDom){return;}
            swapDom(fromDom,lastDom);
            swapDom(fromDom,toDom);
            lastDom = toDom;
        }
    }
    function handleDragOver(e, dom) {
        //默認無法把元素放置到其他元素當中,所以需要prevent
        e.preventDefault();
        e.dataTransfer.effectAllowed = "move";
    }
    
    function swapDom(a, b) {
            //a和b的innerHTML互換
        let temp = a.innerHTML;
        a.innerHTML = b.innerHTML;
        b.innerHTML = temp;
    }
    
    //生成dom結構
    function createDom(arr) {
        let body = document.getElementsByClassName('box')[0];
        let html = [];
        for (let i = 0, len = arr.length; i < len; i++) {
            html.push(template(arr[i].title, arr[i].src));
        }
        body.innerHTML = html.join('');
    }
    
    //html模板,根據該模板動態生成dom節點
    function template(title, src) {
        let tpl = `<div class="card" draggable="true" ondragstart="handleDragStart(event,this)" ondragover="handleDragOver(event,this)" ondragend="handleDragEnd(event,this)" ondrop="handleDrop(event,this)" ondragenter="handleDragEnter(event,this)">
                <span class="card-name">
                    ${title}
                </span>
                <div class="card-img">
                    <img src="${src}" draggable="false" alt="">
                </div>
            </div>`
        return tpl;
    }
    window.onload = function() {
        createDom(htmlArr);
    }
    </script>
    
    </html>
    

    一些你可能不感興趣的後語

    其實在沒有這個drag之前,是用鼠標事件來實現的,這裏就簡單講講思路好了,懶得寫了~

    1. 註冊mousedown事件,
    2. 在mousedown觸發的時候註冊mousemove事件,根據鼠標移動的位置來定位點擊的dom,也就是讓這個元素跟着你的鼠標移動(你的dom得絕對定位哦),這裏比較麻煩的就是一些邊界的判定,因爲你的鼠標能到邊界,但是你的div不一定可以(div面積比較大),而且根據業務不同,你也可能有不同的操作,這裏因人而異啦~
    3. 在mousedown裏也註冊mouseup事件,mouseup的作用就是把mousemove事件清空,因爲要每一次鼠標按下去的時候纔能有mousemove事件。
    4. 至於交換的話,上面也有說了。

    謝謝大家,希望大家寫代碼不要像cxk。

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章