如何使用React來開發拖拽組件 理解HTML5 拖放API 實現Drag組件 實現Drop組件 添加拖放效果 進一步完善 參考資料

拖拽組件是在前端開發中十分常見的一個功能,現在無論你是使用React還是Vue,都有很多現成的拖拽組件可以使用。不過,有些時候你可能還是需要自己去實現,那麼就必須需要理解其實現原理。接下來這篇文章,我將詳細介紹如何使用React框架來實現一個拖拽組件。

理解HTML5 拖放API

現如今,大部分的前端拖拽組件都依託於HTML5原生提供的拖放接口。那麼在開始用具體框架來封裝組件的之前,就需要搞清楚這些原生的接口功能。

HTML 5的DOM鼠標事件中添加了drag這個事件。對於一個設置了draggable屬性的頁面元素來說,只要將其拖動到一個同樣帶有droppable屬性的元素上,就算完成了一次完整的拖放功能。在這一過程中,會分別觸發一些如下事件類型:

事件類型 事件處理函數 含義
drag ondrag 拖放進行中
dragend/dragstart ondragend/ondragstart 開始拖放和結束拖放
dragover ondragover 當元素或選中的文本被拖到一個目標目標上(每100毫秒觸發一次)。
dragenter/dragleave ondragenter/ondragleave 源對象開始進入/離開目標對象範圍內
drop ondrop 源對象被拖放到目標對象上

熟悉這些基本事件類型後,實現上就是在源對象和目標對象上分別綁定對應的事件處理函數,並監聽處理即可。

除了這些拖放的事件接口外,我們通常還需要處理數據的傳遞。HTML5中同樣提供了簡便的接口,在對應的監聽函數內,我們可以拿到event對象,在這個對象內部有個DataTransfer接口,可專門用來保存事件的數據內容。對應的接口有:

  • event.dataTransfer.setData: 添加拖拽數據,這個方法接收兩個參數,第一個參數是數據類型(可自定義),第二個參數是對應的數據
  • event.dataTransfer.getData:反向操作,獲取數據,只接收一個參數,即數據類型
  • event.dataTransfer.clearData: 清除數據
  • event.dataTransfer.setDragImage: 可自定義拖放過程中鼠標旁邊的圖像
  • event.dataTransfer.effectAllowed: 指定拖放操作所允許的一個效果,有多個屬性值,如link, move等,具體可參考https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/effectAllowed

瞭解完這些基本接口後,我們就可以着手使用React來編寫自己的拖放組件了:

實現Drag組件

我們第一個要實現的是Drag組件,它會作爲我們的源對象,其子組件都可以進行拖動。就像這樣:

<Drag dataItem="item">
    <div>這個組件可以拖動</div>
</Drag>

我們先來實現最基礎的功能,通過setData接口來傳遞數據:

const Drag = (props) => {
    
    const startDrag = ev => {
        // 傳輸數據
        ev.dataTransfer.setData("drag-item", props.dataItem);
    };
    
    return (
      <div draggable onDragStart={startDrag}>
        {props.children}
      </div>);
}

實現Drop組件

接着我們就要來實現目標組件了,需要定義一個對外暴露的接口用來接收拖拽完成後的事件:

<DropTarget onItemDropped={itemDropped}>
    <div>
        請將組件拖放到這裏
    </div>
</DropTarget>

從實現上來說,監聽onDragOveronDrop這兩個事件就可以了:

const DropTarget = (props) => {
    const dragOver = ev => {
        ev.preventDefault();
    }

    const drop = ev => {
        // 獲取數據
        const droppedItem = ev.dataTransfer.getData("drag-item");
        if (droppedItem) {
            // 觸發回調函數
            props.onItemDropped(droppedItem);
        }
    }
    
    return (
        <div onDragOver={dragOver} onDrop={drop}>
            {props.children}
        </div>
    )
}

添加拖放效果

要實現拖放的視覺效果,需要effectAllowed和dropEffect兩個屬性結合起來使用。

先在Drag組件上設置effectAllowed屬性:

const Drag = (props) => {
    
    const startDrag = ev => {
        ev.dataTransfer.setData("drag-item", props.dataItem);
        // 添加效果
        ev.dataTransfer.effectAllowed = props.dropEffect;
    };
    
    return (
      <div draggable onDragStart={startDrag}>
        {props.children}
      </div>);
}

接着我們設置一些效果常量:

export const All = "all";
export const Move = "move";
export const Copy = "copy";
export const Link = "link";
export const CopyOrMove = "copyMove";
export const CopyOrLink = "copyLink";
export const LinkOrMove = "linkMove";
export const None = "none";

然後在目標組件上,我們通過給dropEffect屬性賦值來引用這些效果常量,修改代碼如下:

const DropTarget = (props) => {
    const dragOver = ev => {
        ev.preventDefault();
        // 添加效果
        ev.dataTransfer.dropEffect = props.dropEffect;
    }
    
    const dragEnter = ev => {
        ev.dataTransfer.dropEffect = props.dropEffect;
    }

    const drop = ev => {
        const droppedItem = ev.dataTransfer.getData("drag-item");
        if (droppedItem) {
            props.onItemDropped(droppedItem);
        }
    }
    
    return (
        <div onDragOver={dragOver} onDrop={drop} onDragEnter={dragEnter}>
            {props.children}
        </div>
    )
}

Drag.defaultProps = {
    dropEffect: dropEffects.All, // 設置默認的效果
};

進一步完善

到這一步,大體的功能我們都完成的七七八八了,最後還剩下一些收尾的工作。首先我們可以添加接口用來讓用戶可以自定義拖拽圖像:

const Drag = (props) => {
    
    const image  = React.useRef(null);
    
    React.useEffect(() => {
        image.current = null;
        if (props.dragImage) {
            image.current = new Image();
            image.current.src = props.dragImage;
        }
    }, [props.dragImage]);
    
    const startDrag = ev => {
        ev.dataTransfer.setData("drag-item", props.dataItem);
        ev.dataTransfer.effectAllowed = props.dropEffect;
        // 設置圖片
        if (image.current) {
            ev.dataTransfer.setDragImage(image.current, 0, 0);
        }
    };
    
    return (
      <div draggable onDragStart={startDrag}>
        {props.children}
      </div>);
}

接着,我們再來添加樣式:

// 樣式
const draggingStyle = {
    opacity: 0.25,
};

const Drag = props => {
    const [isDragging, setIsDragging] = React.useState(false);
    const image = React.useRef(null);

    React.useEffect(() => {
        image.current = null;
        if (props.dragImage) {
            image.current = new Image();
            image.current.src = props.dragImage;
        }
    }, [props.dragImage]);

    const startDrag = ev => {
        setIsDragging(true);
        ev.dataTransfer.setData("drag-item", props.dataItem);
        ev.dataTransfer.effectAllowed = props.dropEffect;
        if (image.current) {
            ev.dataTransfer.setDragImage(image.current, 0, 0);
        }
    };

    // 拖拽結束時,添加樣式
    const dragEnd = () => setIsDragging(false);

    return (
        <div style={isDragging ? draggingStyle : {}} draggable onDragStart={startDrag} onDragEnd={dragEnd}>
            {props.children}
        </div>
    );
};

最後,需要注意的是,如果需要處理移動端的兼容性,那麼可以使用如下庫:

https://github.com/timruffles/mobile-drag-drop

——本文首發於個人公衆號,轉載請註明出處———


最後,歡迎大家關注我的公衆號,一起學習交流。

參考資料

https://app.pluralsight.com/guides/drag-and-drop-react-components(本文代碼例子主要來源於此)

https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API

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