Skip to content

Commit

Permalink
Add d3.touch; fix #1786.
Browse files Browse the repository at this point in the history
The drag behavior no longer crashes when the element being dragged is removed
from the DOM. In addition, the new d3.touch method extracts a single identified
touch from the current touch event, making it more efficient during multitouch.
The drag behavior now assigns touchmove and touchend listeners on the target
element of the touchstart event, rather than the window.
  • Loading branch information
mbostock committed Mar 24, 2014
1 parent ee23f94 commit 3872491
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 63 deletions.
57 changes: 33 additions & 24 deletions d3.js
Expand Up @@ -1127,47 +1127,39 @@
}) : [];
};
d3.behavior.drag = function() {
var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, "mousemove", "mouseup"), touchstart = dragstart(touchid, touchposition, "touchmove", "touchend");
var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, d3_behavior_dragMouseSubject, "mousemove", "mouseup"), touchstart = dragstart(d3_behavior_dragTouchId, d3.touch, d3_behavior_dragTouchSubject, "touchmove", "touchend");
function drag() {
this.on("mousedown.drag", mousedown).on("touchstart.drag", touchstart);
}
function touchid() {
return d3.event.changedTouches[0].identifier;
}
function touchposition(parent, id) {
return d3.touches(parent).filter(function(p) {
return p.identifier === id;
})[0];
}
function dragstart(id, position, move, end) {
function dragstart(id, position, subject, move, end) {
return function() {
var target = this, parent = target.parentNode, event_ = event.of(target, arguments), eventTarget = d3.event.target, eventId = id(), drag = eventId == null ? "drag" : "drag-" + eventId, origin_ = position(parent, eventId), dragged = 0, offset, w = d3.select(d3_window).on(move + "." + drag, moved).on(end + "." + drag, ended), dragRestore = d3_event_dragSuppress();
var element = this, parent = element.parentNode, dispatch = event.of(element, arguments), dragged = 0, dragId = id(), dragName = ".drag" + (dragId == null ? "" : "-" + dragId), dragOffset, dragTarget = d3.event.target, dragSubject = d3.select(subject()).on(move + dragName, moved).on(end + dragName, ended), dragRestore = d3_event_dragSuppress(), position0 = position(parent, dragId);
if (origin) {
offset = origin.apply(target, arguments);
offset = [ offset.x - origin_[0], offset.y - origin_[1] ];
dragOffset = origin.apply(element, arguments);
dragOffset = [ dragOffset.x - position0[0], dragOffset.y - position0[1] ];
} else {
offset = [ 0, 0 ];
dragOffset = [ 0, 0 ];
}
event_({
dispatch({
type: "dragstart"
});
function moved() {
var p = position(parent, eventId), dx = p[0] - origin_[0], dy = p[1] - origin_[1];
var position1 = position(parent, dragId), dx = position1[0] - position0[0], dy = position1[1] - position0[1];
dragged |= dx | dy;
origin_ = p;
event_({
position0 = position1;
dispatch({
type: "drag",
x: p[0] + offset[0],
y: p[1] + offset[1],
x: position1[0] + dragOffset[0],
y: position1[1] + dragOffset[1],
dx: dx,
dy: dy
});
}
function ended() {
if (position(parent, eventId)) return;
w.on(move + "." + drag, null).on(end + "." + drag, null);
dragRestore(dragged && d3.event.target === eventTarget);
event_({
if (dragId != null && position(parent, dragId)) return;
dragSubject.on(move + dragName, null).on(end + dragName, null);
dragRestore(dragged && d3.event.target === dragTarget);
dispatch({
type: "dragend"
});
}
Expand All @@ -1180,6 +1172,15 @@
};
return d3.rebind(drag, event, "on");
};
function d3_behavior_dragTouchId() {
return d3.event.changedTouches[0].identifier;
}
function d3_behavior_dragTouchSubject() {
return d3.event.target;
}
function d3_behavior_dragMouseSubject() {
return d3_window;
}
var π = Math.PI, τ = 2 * π, halfπ = π / 2, ε = 1e-6, ε2 = ε * ε, d3_radians = π / 180, d3_degrees = 180 / π;
function d3_sgn(x) {
return x > 0 ? 1 : x < 0 ? -1 : 0;
Expand Down Expand Up @@ -2048,6 +2049,14 @@
};
d3.csv = d3.dsv(",", "text/csv");
d3.tsv = d3.dsv(" ", "text/tab-separated-values");
d3.touch = function(container, touches, identifier) {
if (arguments.length < 3) identifier = touches, touches = d3_eventSource().touches;
if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) {
if ((touch = touches[i]).identifier === identifier) {
return d3_mousePoint(container, touch);
}
}
};
var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_active, d3_timer_frame = d3_window[d3_vendorSymbol(d3_window, "requestAnimationFrame")] || function(callback) {
setTimeout(callback, 17);
};
Expand Down
10 changes: 5 additions & 5 deletions d3.min.js

Large diffs are not rendered by default.

84 changes: 50 additions & 34 deletions src/behavior/drag.js
Expand Up @@ -9,61 +9,59 @@ import "behavior";
d3.behavior.drag = function() {
var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"),
origin = null,
mousedown = dragstart(d3_noop, d3.mouse, "mousemove", "mouseup"),
touchstart = dragstart(touchid, touchposition, "touchmove", "touchend");
mousedown = dragstart(d3_noop, d3.mouse, d3_behavior_dragMouseSubject, "mousemove", "mouseup"),
touchstart = dragstart(d3_behavior_dragTouchId, d3.touch, d3_behavior_dragTouchSubject, "touchmove", "touchend");

function drag() {
this.on("mousedown.drag", mousedown)
.on("touchstart.drag", touchstart);
}

function touchid() {
return d3.event.changedTouches[0].identifier;
}

function touchposition(parent, id) {
return d3.touches(parent).filter(function(p) { return p.identifier === id; })[0];
}

function dragstart(id, position, move, end) {
function dragstart(id, position, subject, move, end) {
return function() {
var target = this,
parent = target.parentNode,
event_ = event.of(target, arguments),
eventTarget = d3.event.target,
eventId = id(),
drag = eventId == null ? "drag" : "drag-" + eventId,
origin_ = position(parent, eventId),
var element = this,
parent = element.parentNode,
dispatch = event.of(element, arguments),
dragged = 0,
offset,
w = d3.select(d3_window).on(move + "." + drag, moved).on(end + "." + drag, ended),
dragRestore = d3_event_dragSuppress();
dragId = id(),
dragName = ".drag" + (dragId == null ? "" : "-" + dragId),
dragOffset,
dragTarget = d3.event.target,
dragSubject = d3.select(subject()).on(move + dragName, moved).on(end + dragName, ended),
dragRestore = d3_event_dragSuppress(),
position0 = position(parent, dragId);

if (origin) {
offset = origin.apply(target, arguments);
offset = [offset.x - origin_[0], offset.y - origin_[1]];
dragOffset = origin.apply(element, arguments);
dragOffset = [dragOffset.x - position0[0], dragOffset.y - position0[1]];
} else {
offset = [0, 0];
dragOffset = [0, 0];
}

event_({type: "dragstart"});
dispatch({type: "dragstart"});

function moved() {
var p = position(parent, eventId),
dx = p[0] - origin_[0],
dy = p[1] - origin_[1];
var position1 = position(parent, dragId),
dx = position1[0] - position0[0],
dy = position1[1] - position0[1];

dragged |= dx | dy;
origin_ = p;
position0 = position1;

event_({type: "drag", x: p[0] + offset[0], y: p[1] + offset[1], dx: dx, dy: dy});
dispatch({
type: "drag",
x: position1[0] + dragOffset[0],
y: position1[1] + dragOffset[1],
dx: dx,
dy: dy
});
}

function ended() {
if (position(parent, eventId)) return; // this touch still active
w.on(move + "." + drag, null).on(end + "." + drag, null);
dragRestore(dragged && d3.event.target === eventTarget);
event_({type: "dragend"});
if (dragId != null && position(parent, dragId)) return; // this touch still active
dragSubject.on(move + dragName, null).on(end + dragName, null);
dragRestore(dragged && d3.event.target === dragTarget);
dispatch({type: "dragend"});
}
};
}
Expand All @@ -76,3 +74,21 @@ d3.behavior.drag = function() {

return d3.rebind(drag, event, "on");
};

// While it is possible to receive a touchstart event with more than one changed
// touch, the event is only shared by touches on the same target; for new
// touches targetting different elements, multiple touchstart events are
// received even when the touches start simultaneously. Since multiple touches
// cannot move the same target to different locations concurrently without
// tearing the fabric of spacetime, we allow the first touch to win.
function d3_behavior_dragTouchId() {
return d3.event.changedTouches[0].identifier;
}

function d3_behavior_dragTouchSubject() {
return d3.event.target;
}

function d3_behavior_dragMouseSubject() {
return d3_window;
}
1 change: 1 addition & 0 deletions src/event/index.js
@@ -1,5 +1,6 @@
import "dispatch";
import "event";
import "mouse";
import "touch";
import "touches";
import "timer";
11 changes: 11 additions & 0 deletions src/event/touch.js
@@ -0,0 +1,11 @@
import "event";
import "mouse";

d3.touch = function(container, touches, identifier) {
if (arguments.length < 3) identifier = touches, touches = d3_eventSource().touches;
if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) {
if ((touch = touches[i]).identifier === identifier) {
return d3_mousePoint(container, touch);
}
}
};

0 comments on commit 3872491

Please sign in to comment.