低開開發筆記(四):實現編輯器內拖拽

养肥胖虎發表於2024-04-22

好傢伙,

本篇我們來說說,編輯器內如何實現拖拽

完整程式碼已開源

https://github.com/Fattiger4399/ph-questionnaire.git

0.效果預覽

1.思路

1.1.檢視操作分析

這一塊是這一章節最核心的部分

使用者進行了什麼操作?

(1)點選編輯器中第一個元件

(2)鬆開

(3)在setter中修改第一個元件的資料

(4)按下第一個元件(不鬆開滑鼠左鍵)

(5)拖拽(不鬆開滑鼠左鍵)

(6)到達目標地點(鬆開滑鼠左鍵)

上述操作中,只有第六個是需要我們進行分析的

但其實也非常簡單,

圖片中畫紅框的區域如何確定呢?

目標區域的位置 小於 第四個元件的開始座標 加上 一半的第四個元件的高度

並且

目標區域的位置 大於 第四個元件的開始座標 減去 一半的第三個元件的高度   

換成公式大概長這個樣子

mouseupY > phoffsetTopbox[i] + 0.5 * phoffsetHeightbox[i] && mouseupY < phoffsetTopbox[i + 1] + 0.5 * phoffsetHeightbox[i + 1]

放到四個後面。。。放到第五個後面。。。

後面的情況以此類推

好了

1.2.資料操作分析

編輯器本身就是按資料的順序渲染的,所以,只要排好序,然後更新渲染器就可以了

2.開始操作

我們需要用到的一些座標

事件物件e的一些屬性

  • isTrusted: 表示事件是否是由使用者操作觸發的,如果是由指令碼建立的事件,則為 false,如果是由使用者操作觸發的則為 true
  • altKey, ctrlKey, metaKey, shiftKey: 分別表示是否按下了 Alt、Ctrl、Meta、Shift 鍵。
  • bubbles: 表示該事件是否會冒泡。
  • button: 表示按下的是哪個滑鼠按鈕(左鍵為 0,中鍵為 1,右鍵為 2)。
  • clientX, clientY: 表示滑鼠指標在視口中的座標。
  • pageX, pageY: 表示滑鼠指標相對於頁面的座標。
  • screenX, screenY: 表示滑鼠指標相對於螢幕的座標。
  • target: 表示事件的目標元素。
  • type: 表示事件的型別,這裡是 "dragend"。
  • timeStamp: 表示事件發生的時間戳。
  • x, y: 與 clientX, clientY 相同,表示滑鼠指標在視口中的座標。
  • offsetX: 表示滑鼠指標位置相對於觸發事件的物件的 X 座標。換句話說,它是滑鼠指標距離事件目標元素的左側邊緣的畫素距離。
  • offsetY: 表示滑鼠指標位置相對於觸發事件的物件的 Y 座標。它是滑鼠指標距離事件目標元素的頂部邊緣的畫素距離。

3.程式碼分析

(以下僅分析關鍵程式碼,完整程式碼請參考)

select(config) {
            // 去除所有選中樣式
            this.dsl = removeChildrenBorder(this.dsl);
            // 新增選中樣式
            config.dsl = addChildrenBorder(config.dsl);
            this.model.selected = config.dsl;

            // 元件拖拽處理
            const editorElement = this.$refs.editor.$el;
            const node = `div.${this.model.selected.component}`;
            const allElements = editorElement.querySelectorAll('div');
            const childElements = editorElement.querySelectorAll(node);

            let sameid = this.model.selected.wid - 1;
            // 篩選出以 ph- 開頭的 div 元素
            const phElements = Array.from(allElements).filter(div => {
                // 獲取元素的類名陣列
                const classList = div.classList;
                // 檢查是否有任何類名以 'ph-' 開頭
                return Array.from(classList).some(className => className.startsWith('ph-'));
            });
            let phoffsetTopbox = []
            let phoffsetHeightbox = []
            // 列印出所有匹配的元素
            phElements.forEach((item) => {
                phoffsetTopbox.push(item.offsetTop)
                phoffsetHeightbox.push(item.offsetHeight)
            })

            // 定義事件處理函式,並儲存引用
            this.dragStartHandler = (e) => {
            };

            this.dragEndHandler = (e) => {
                // 注意:這裡不需要移除事件監聽器,因為它們會在select方法開始時被移除
                // console.log(e);
                // console.log("mouseupY軸   " + e.offsetY);
                //進行位置交換操作
                let newdsl;
                if (e.offsetY) {
                    const arraylength = this.dsl.children.length;
                    console.log(arraylength, phoffsetTopbox)
                    //將一號位元件拖拽到三號位上方
                    //拖拽大約為範圍為150-250px
                    let i, j;
                    let overdragid;
                    let using_id = this.model.selected.wid;
                    //phoffsetTopbox
                    //phoffsetHeightbox
                    const mouseupY = phoffsetTopbox[using_id - 1] + e.offsetY;
                    const childlength = this.dsl.children.length
                    console.log(mouseupY)
                    //向下
                    if (e.offsetY > 0) {
                        for (i = using_id; i <= this.dsl.children.length;) {
                            // console.log(i)
                            // console.log(phoffsetTopbox[i], phoffsetHeightbox[i])
                            //向下
                            if (mouseupY > phoffsetTopbox[i] + 0.5 * phoffsetHeightbox[i] && mouseupY < phoffsetTopbox[i + 1] + 0.5 * phoffsetHeightbox[i + 1]) {
                                i++;
                                break;
                            } else if (mouseupY > 0 && mouseupY < phoffsetTopbox[i] + 0.5 * phoffsetHeightbox[i]) {
                                break;

                            } else if (mouseupY > phoffsetTopbox[childlength - 1] + 0.5 * phoffsetHeightbox[childlength - 1]) {
                                i = childlength;
                                break;
                            }
                            else {
                                i++;
                            }
                        }
                    } else {
                        //向上
                        for (i = using_id; i > 0;) {
                            // console.log(i)
                            console.log(mouseupY)
                            if (mouseupY > phoffsetTopbox[i - 2] - 0.5 * phoffsetHeightbox[i - 2] && mouseupY < phoffsetTopbox[i - 1] - 0.5 * phoffsetHeightbox[i - 1]) {
                                i = i - 2;
                                break;
                            }
                            //上半部分
                            else if (mouseupY < phoffsetTopbox[i] && mouseupY > phoffsetTopbox[i - 2] + 0.5 * phoffsetHeightbox[i - 2]) {
                                break;
                            } else if (mouseupY < phoffsetTopbox[0] + 0.5 * phoffsetHeightbox[0]) {
                                i = 0;
                                break;
                            } else {
                                i--;
                            }
                        }
                    }
                    console.log(this.dsl.children)
                    if (this.model.selected.wid != i) {
                        //複製
                        let copied_element = this.dsl.children[this.model.selected.wid - 1]
                        // //插入
                        this.dsl.children.splice(i, 0, copied_element)
                        //刪除
                        this.dsl.children.splice(using_id - 1, 1)
                    }
                    console.log(i)
                }
            };
            // 在新增新的監聽器之前,先移除舊的監聽器
            if (childElements[sameid]) {
                childElements[sameid].removeEventListener('dragstart', this.dragStartHandler);
                childElements[sameid].removeEventListener('dragend', this.dragEndHandler);
            }
            // 新增新的監聽器
            childElements[sameid].setAttribute("draggable", "true");
            childElements[sameid].addEventListener('dragstart', this.dragStartHandler);
            childElements[sameid].addEventListener('dragend', this.dragEndHandler);

            //更新檢視
            this.$refs.editor.$forceUpdate();
        },

  1. 首先,根據傳入的 config 物件,更新選中的樣式,將選中的元件標記為 config.dsl

  2. 然後,透過獲取編輯器元素和選中元件的類名,找到所有符合條件的子元素並儲存在 childElements 中。

  3. 根據選中元件的 wid 屬性計算出 sameid,用於定位對應的子元素。

  4. 透過篩選出以 ph- 開頭的 div 元素,計算出這些元素的 offsetTopoffsetHeight,並儲存在 phoffsetTopboxphoffsetHeightbox 陣列中。

  5. 定義了兩個事件處理函式 dragStartHandlerdragEndHandler,分別處理拖拽開始和拖拽結束的邏輯。

  6. 在拖拽結束時,根據滑鼠在 Y 軸上的位置,判斷拖拽的方向(向上或向下),並根據計算得到的位置資訊,將選中的元件插入到新的位置上。

  7. 移除舊的事件監聽器,新增新的事件監聽器,並設定元素為可拖拽狀態。

相關文章