React結合Drag API實現拖拽示例詳解

認識拖拽

鼠標拖拽是一個常見的交互場景,在這個熟悉的過程將會發生哪些事件?

拖拽事件指用戶通過鼠標(或其他指針設備)將元素移到一個新的位置上。拖拽過程涉及兩個對象:被拖拽元素(上圖中 A )和可釋放目標(上圖中 B )

被拖拽元素

默認情況下,圖片、鏈接和文本是可拖動的。HTML5 在所有 HTML 元素上規定瞭一個 draggable 屬性, 表示元素是否可以拖動。圖片和鏈接的 draggable 屬性自動被設置為 true,而其他所有元素此屬性的默認值為 false。

某個元素被拖動時,會依次觸發以下事件:

  • ondragstart:拖動開始,當鼠標按下並且開始移動鼠標時,觸發此事件;整個周期隻觸發一次;
  • ondrag:隻要元素仍被拖拽,就會持續觸發此事件;
  • ondragend:拖拽結束,當鼠標松開後,會觸發此事件;整個周期隻觸發一次。

可釋放目標

當把拖拽元素移動到一個有效的放置目標時,目標對象會觸發以下事件:

  • ondragenter:隻要一把拖拽元素移動到目標時,就會觸發此事件;
  • ondragover:拖拽元素在目標中拖動時,會持續觸發此事件;
  • ondragleaveondrop:拖拽元素離開目標時(沒有在目標上放下),會觸發ondragleave;當拖拽元素在目標放下(松開鼠標),則觸發ondrop事件。

🏝 目標元素默認是不能夠被拖放的,即不會觸發 ondrop 事件,可以通過在目標元素的 ondragover 事件中取消默認事件來解決此問題。

生命周期

拖拽操作中的數據傳輸

除非數據受影響,否則簡單的拖放並沒有實際意義。為實現拖動操作中的數據傳輸,event 對象上暴露瞭 dataTransfer 對象,用於從被拖動元素向放置目標傳遞字符串數據。我們使用它來通知畫佈,當前需要渲染的組件是什麼。

dataTransfer 對象主要有兩個方法:getData() 和 setData(),分別用來獲取和存儲值。setData()的第一個參數以及 getData()的唯一參數是一個字符串,表示要設置的數據類型:"text"或"URL"

🏝 雖然這兩種數據類型是 IE 最初引入的,但 HTML5 已經將其擴展為允許任何 MIME 類型。為向後 兼容,HTML5 還會繼續支持"text"和"URL",但它們會分別被映射到"text/plain"和"text/uri-list”

需要註意的是:存儲在 dataTransfer 對象中的數據隻能在放置事件中讀取。如果沒有在 ondrop 事件中取得這些數據,dataTransfer 對象就會被銷毀,數據也會丟失。

代碼實現

我在項目中使用 React 來實現,並且考慮到跨組件通信,我使用瞭 dva 來管理數據流。

如何標記當前拖拽的元素?

HTML5 支持的 data-x 屬性,我們可以將當前組件的類型 Rectangle 賦值給它,這樣處理和畫佈組件通信方便一些

const Block = (props) => {
  const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
    // 向拖拽數據中添加項目    
    e.dataTransfer.setData('text', e.target.dataset.index);
  };
  return (
    <div onDragStart={handleDragStart}>
      <Button draggable data-index="Rectangle">
        二維碼
      </Button>
    </div>
  );
};

在上文中講到,dataTransfer 的數據必須在 handleDrop 方法中獲取。實際的用來保存畫佈中的所有組件的數據:

function DragEditor(props) {
  const { dvaStore, dispatch } = props;
  // 阻止瀏覽器默認事件,否則 ondrop 不會觸發
  const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
  };
  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    // 獲取拖拽元素的組件類型
    const type = e.dataTransfer.getData('text');
    // COMPONENT_LIST 定義瞭組件的數據格式,根據 type 匹配
    const component = COMPONENT_LIST.filter(
      (i) => i.component === type,
    )[0];
    // 將組件數據添加到 store,畫佈將會根據數據渲染出組件
    if (component) {
      dispatch?.({
        type: 'store/addComponent',
        payload: component,
      });
    }
  };
  return (...);
}

在畫佈中拖動

拖動主要依賴組件的初始位置,鼠標開始位置、結束位置。根據後兩組得到鼠標移動的距離,和初始位置相加後,得到最終位置。

function DragEditor(props: IEditorProps) {
  const { dvaStore, dispatch } = props;
  const [startAxis, setStartAxis] = React.useState({ x: 0, y: 0 }); // 鼠標開始拖動時的位置
  const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
    setStartAxis({ x: e.clientX, y: e.clientY });
  };
  const handleDragEnd = (e: React.DragEvent<HTMLDivElement>, data: IComponentSchema) => {
    // 鼠標移動的距離
    const displacementX = e.clientX - startAxis.x;
    const displacementY = e.clientY - startAxis.y;
    // 計算組件的終點位置:初始位置 + 鼠標移動的距離
    const endX = Number(data.style.left) + displacementX;
    const endY = Number(data.style.top) + displacementY;
    // 限制坐標的最小值為 0
    const top = Math.max(endY, 0);
    const left = Math.max(endX, 0);
    // 更新當前組件樣式
    dispatch?.({
      type: 'store/setShapeStyle',
      payload: { top, left },
    });
  };
  return (
      {dvaStore.componentsData.map((i) => {
        return (
          <RenderComponent
            type={i.component}
            componentData={i}
            key={i.generateId}
            onDragStart={handleDragStart}
            onDragEnd={(e) => handleDragEnd(e, i)}
          />
        );
      })}
  );
}

數據結構

最後,就是組件和數據結構的設計,RenderComponent 是一個自定義的組件,會根據傳入的 type 屬性渲染對應的組件。組件的數據結構設計如下:

export const COMPONENT_LIST = [
  {
    component: 'Rectangle', // 組件名稱
    label: '矩形', // 左側 Blocks 組件列表中顯示的名字
    propValue: '', // 組件所使用的值
    icon: 'BorderOuterOutlined', // 左側組件列表中顯示的 icon 圖標
    animations: [], // 動畫列表
    events: {}, // 事件列表
    style: {    // 組件樣式
      width: 100,
      height: 100,
      top: 0,
      left: 0,
    },
  },
  {
    component: 'Text',
    label: '文字',
    propValue: '文字',
    icon: '',
    animations: [],
    events: {},
    style: {
      width: 200,
      height: 33,
      fontSize: 14,
      fontWeight: 500,
      lineHeight: '',
      letterSpacing: 0,
      textAlign: '',
      color: '',
    },
  },
];

總結

拖拽是非常有趣的一種交互,特別是在低代碼場景下非常重要。使用原生 API 能夠讓我們更加瞭解底層的一些細節,React 社區也有一些優秀的第三方框架,如:react-dragable, react-beautiful-dnd,大傢有興趣不妨再多瞭解下。

Links HTML 拖放 API

以上就是React結合Drag API實現拖拽示例詳解的詳細內容,更多關於React Drag API拖拽的資料請關註WalkonNet其它相關文章!

推薦閱讀: