跳至內容

d3-drag

範例 · 拖曳是操作空間元素的熱門互動方式:將指標移到物件上,按住並拖曳物件到新位置,然後放開以「放下」物件。D3 的拖曳行為提供了一個靈活的拖曳抽象化。例如,你可以拖曳力導向圖中的節點

Force-Directed Graph

或碰撞圓圈的模擬

Force Dragging II

拖曳行為不只是用來移動元素;有許多方法可以回應拖曳手勢。例如,你可以用它在散佈圖中套索元素,或在畫布上繪製線條

Line Drawing

拖曳行為可以與其他行為結合,例如 d3-zoom 用於縮放

Drag & Zoom II

拖曳行為與 DOM 無關,因此你可以將它用於 SVG、HTML 甚至畫布!而且你可以使用進階選取技術來擴充它,例如 Voronoi 覆蓋或最近目標搜尋

Circle Dragging IVCircle Dragging II

拖曳行為統一了滑鼠和觸控輸入,並避免瀏覽器怪異行為。未來,拖曳行為也會支援 指標事件

drag()

原始碼 · 建立一個新的拖曳行為。傳回的行為,drag,同時是一個物件和一個函式,通常透過 selection.call 套用至選取的元素。

js
const drag = d3.drag();

drag(selection)

原始碼 · 將此拖曳行為套用至指定的 選取。此函式通常不會直接呼叫,而是透過 選取.call 呼叫。例如,要實例化拖曳行為並將其套用至選取

js
d3.selectAll(".node").call(d3.drag().on("start", started));

在內部,拖曳行為會使用 選取.on 來繫結拖曳所需的事件監聽器。監聽器使用名稱 .drag,因此您可以隨後解除拖曳行為的繫結,如下所示

js
selection.on(".drag", null);

套用拖曳行為也會將 -webkit-tap-highlight-color 樣式設定為透明,停用 iOS 上的輕觸亮顯。如果您想要不同的輕觸亮顯顏色,請在套用拖曳行為後移除或重新套用此樣式。

drag.container(container)

原始碼 · 如果指定 container,則將容器存取器設定為指定的物件或函式,並傳回拖曳行為。如果未指定 container,則傳回目前的容器存取器,其預設值為

js
function container() {
  return this.parentNode;
}

拖曳手勢的 container 會決定後續 拖曳事件 的座標系統,影響 event.x 和 event.y。容器存取器傳回的元素隨後會傳遞至 pointer 以決定指標的區域座標。

預設容器存取器會傳回原始選取 (請參閱 drag) 中接收啟動輸入事件的元素的父節點。在拖曳 SVG 或 HTML 元素時,這通常很合適,因為這些元素通常相對於父項定位。然而,對於使用 Canvas 拖曳圖形元素,您可能希望將容器重新定義為啟動元素本身

js
function container() {
  return this;
}

或者,容器可以直接指定為元素,例如 drag.container(canvas)

drag.filter(filter)

原始碼 · 如果指定 filter,則將事件篩選器設定為指定的函式,並傳回拖曳行為。如果未指定 filter,則傳回目前的篩選器,其預設值為

js
function filter(event) {
  return !event.ctrlKey && !event.button;
}

如果篩選器傳回假值,則會忽略啟動事件,且不會啟動任何拖曳手勢。因此,篩選器會決定忽略哪些輸入事件;預設篩選器會忽略次要按鈕上的 mousedown 事件,因為這些按鈕通常用於其他目的,例如內容選單。

drag.touchable(touchable)

原始碼 · 如果指定 touchable,則將觸控支援偵測器設定為指定的函式,並傳回拖曳行為。如果未指定 touchable,則傳回目前的觸控支援偵測器,其預設值為

js
function touchable() {
  return navigator.maxTouchPoints || ("ontouchstart" in this);
}

只有當偵測器在 套用 拖曳行為時對應元素傳回真值,才會註冊觸控事件監聽器。預設偵測器適用於大多數具備觸控輸入功能的瀏覽器,但並非全部;例如,Chrome 的行動裝置模擬器就會偵測失敗。

drag.subject(subject)

來源 · 如果指定了 subject,則將 subject 存取器設定為指定的物件或函式,並傳回拖曳行為。如果未指定 subject,則傳回目前的 subject 存取器,預設為

js
function subject(event, d) {
  return d == null ? {x: event.x, y: event.y} : d;
}

拖曳手勢的 subject 代表正在拖曳的事物。當收到啟動輸入事件(例如 mousedown 或 touchstart)時,會在拖曳手勢開始前立即計算。然後,此 subject 會在後續的 拖曳事件 中作為 event.subject 顯示。

預設的 subject 是接收啟動輸入事件的原始選取中的元素的 資料(請參閱 drag);如果此資料未定義,則會建立一個代表指標座標的物件。因此,在 SVG 中拖曳圓形元素時,預設的 subject 是正在拖曳的圓形的資料。對於 畫布,預設的 subject 是畫布元素的資料(不論您在畫布上的哪個位置按一下)。在這種情況下,自訂的 subject 存取器會更合適,例如在給定的搜尋半徑內選取最靠近滑鼠的圓形

js
function subject(event) {
  let n = circles.length,
      i,
      dx,
      dy,
      d2,
      s2 = radius * radius,
      circle,
      subject;

  for (i = 0; i < n; ++i) {
    circle = circles[i];
    dx = event.x - circle.x;
    dy = event.y - circle.y;
    d2 = dx * dx + dy * dy;
    if (d2 < s2) subject = circle, s2 = d2;
  }

  return subject;
}

提示

如有必要,可以使用 quadtree.findsimulation.finddelaunay.find 加速上述動作。

傳回的 subject 應為公開 xy 屬性的物件,以便在拖曳手勢期間保留 subject 和指標的相對位置。如果 subject 為 null 或未定義,則不會為此指標啟動拖曳手勢;但是,其他啟動觸控仍可能啟動拖曳手勢。另請參閱 drag.filter

拖曳手勢的主體在手勢開始後不可變更。主體存取器會使用與 selection.on 監聽器相同的內容和參數呼叫:目前的事件 (event) 和資料 d,其中 this 內容為目前的 DOM 元素。在評估主體存取器期間,event 是開始前 拖曳事件。使用 event.sourceEvent 存取啟動輸入事件,並使用 event.identifier 存取觸控識別碼。event.x 和 event.y 相對於 容器,並使用 指標 計算。

drag.clickDistance(distance)

原始碼 · 如果指定 distance,則設定滑鼠在 mousedown 和 mouseup 之間可以移動的最大距離,這將觸發後續的 click 事件。如果在 mousedown 和 mouseup 之間的任何時間點,滑鼠與 mousedown 時的位置距離大於或等於 distance,則會抑制 mouseup 之後的 click 事件。如果未指定 distance,則傳回目前的距離閾值,預設為零。距離閾值以客戶端座標測量 (event.clientXevent.clientY).

drag.on(typenames, listener)

原始碼 · 如果指定 listener,則設定指定 typenames 的事件 listener,並傳回拖曳行為。如果已為相同的類型和名稱註冊事件監聽器,則會在新增新的監聽器之前移除現有的監聽器。如果 listener 為 null,則移除指定 typenames 的目前事件監聽器(如果有)。如果未指定 listener,則傳回目前指定的與指定 typenames 相符的第一個監聽器(如果有)。當指定的事件被觸發時,每個 listener 都會使用與 selection.on 監聽器相同的內容和參數呼叫:目前的事件 (event) 和資料 d,其中 this 內容為目前的 DOM 元素。

typenames 是包含一個或多個 typename 的字串,各 typename 以空白分隔。每個 typename 都是一個 type,後面可以選擇加上一個句點 (.) 和一個 name,例如 drag.foodrag.bar;此名稱允許為相同的 type 註冊多個監聽器。type 必須是下列其中之一

  • start - 在新的指標變為 active 之後(在 mousedown 或 touchstart)。
  • drag - 在 active 指標移動之後(在 mousemove 或 touchmove)。
  • end - 在活動指標變為非活動狀態後(在 mouseup、touchend 或 touchcancel 時)。

請參閱 dispatch.on 以進一步了解。

在拖曳手勢期間透過 drag.on 變更已註冊的監聽器不會影響目前的拖曳手勢。相反地,您必須使用 event.on,它也允許您為目前的拖曳手勢註冊暫時事件監聽器。在拖曳手勢期間會為每個活動指標傳送個別事件。例如,如果同時用多個手指拖曳多個主體,則會為每個手指傳送一個開始事件,即使兩個手指同時開始觸摸。請參閱 拖曳事件 以進一步了解。

dragDisable(window)

原始碼 · 阻止在指定的 window 上進行原生拖放和文字選取。作為防止 mousedown 事件的預設動作的替代方案(請參閱 #9),此方法會防止 mousedown 之後出現不必要的預設動作。在受支援的瀏覽器中,這表示擷取 dragstart 和 selectstart 事件,防止相關的預設動作,並立即停止它們的傳播。在不支援選取事件的瀏覽器中,會將 user-select CSS 屬性設定為文件元素上的 none。此方法旨在在 mousedown 時呼叫,然後在 mouseup 時呼叫 dragEnable

dragEnable(window, noclick)

原始碼 · 允許在指定的 window 上進行原生拖放和文字選取;取消 dragDisable 的效果。此方法旨在在 mouseup 時呼叫,並在 mousedown 時先呼叫 dragDisable。如果 noclick 為 true,此方法也會暫時抑制 click 事件。click 事件的抑制會在零毫秒的逾時後到期,因此它只會抑制緊接在目前的 mouseup 事件之後的 click 事件(如果有的話)。

拖曳事件

當呼叫 拖曳事件監聽器 時,它會接收目前的拖曳事件作為其第一個引數。event 物件會公開多個欄位

  • target - 相關的 拖曳行為
  • type - 字串「start」、「drag」或「end」;請參閱 drag.on
  • subject - 由 drag.subject 定義的拖曳主體。
  • x - 主體新的 x 座標;請參閱 drag.container
  • y - 主體的新 y 座標;請參閱 drag.container
  • dx - 自上一次拖曳事件以來 x 座標的變化。
  • dy - 自上一次拖曳事件以來 y 座標的變化。
  • identifier - 字串「mouse」或數字 觸控識別碼
  • active - 目前活動中的拖曳手勢數量(開始和結束時,不包括此手勢)。
  • sourceEvent - 底層輸入事件,例如 mousemove 或 touchmove。

event.active 欄位可用於偵測一系列同時進行的拖曳手勢中的第一個開始事件和最後一個結束事件:第一個拖曳手勢開始時為零,最後一個拖曳手勢結束時也為零。

event 物件也公開 event.on 方法。

此表格說明拖曳行為如何詮釋原生事件

事件監聽元素拖曳事件預設已阻止?
mousedown⁵selection開始否¹
mousemove²window¹拖曳
mouseup²window¹結束
dragstart²window-
selectstart²window-
click³window-
touchstartselection開始否⁴
touchmoveselection拖曳
touchendselection結束否⁴
touchcancelselection結束否⁴

所有已消耗事件的傳播都會 立即停止。如果您要防止某些事件啟動拖曳手勢,請使用 drag.filter

¹ 必須擷取 iframe 外部的事件;請參閱 #9
² 僅適用於活動中的、基於滑鼠的手勢;請參閱 #9
³ 僅適用於某些基於滑鼠的手勢之後;請參閱 drag.clickDistance
⁴ 必須允許 在觸控輸入上模擬點擊;請參閱 #9
⁵ 如果在觸控手勢結束後 500 毫秒內,將會被忽略;假設 模擬點擊

event.on(typenames, listener)

來源 · 等同於 drag.on,但僅適用於目前的拖曳手勢。在拖曳手勢開始之前,會建立目前的拖曳 事件監聽器副本。此副本會繫結到目前的拖曳手勢,並由 event.on 修改。這對於僅接收目前拖曳手勢事件的暫時監聽器很有用。例如,此開始事件監聽器會將暫時的拖曳和結束事件監聽器註冊為封閉函數

js
function started(event) {
  const circle = d3.select(this).classed("dragging", true);
  const dragged = (event, d) => circle.raise().attr("cx", d.x = event.x).attr("cy", d.y = event.y);
  const ended = () => circle.classed("dragging", false);
  event.on("drag", dragged).on("end", ended);
}