|
import {dispatch} from "d3-dispatch"; |
|
import {dragDisable, dragEnable} from "d3-drag"; |
|
import {interpolate} from "d3-interpolate"; |
|
import {pointer, select} from "d3-selection"; |
|
import {interrupt} from "d3-transition"; |
|
import constant from "./constant.js"; |
|
import BrushEvent from "./event.js"; |
|
import noevent, {nopropagation} from "./noevent.js"; |
|
|
|
var MODE_DRAG = {name: "drag"}, |
|
MODE_SPACE = {name: "space"}, |
|
MODE_HANDLE = {name: "handle"}, |
|
MODE_CENTER = {name: "center"}; |
|
|
|
const {abs, max, min} = Math; |
|
|
|
function number1(e) { |
|
return [+e[0], +e[1]]; |
|
} |
|
|
|
function number2(e) { |
|
return [number1(e[0]), number1(e[1])]; |
|
} |
|
|
|
var X = { |
|
name: "x", |
|
handles: ["w", "e"].map(type), |
|
input: function(x, e) { return x == null ? null : [[+x[0], e[0][1]], [+x[1], e[1][1]]]; }, |
|
output: function(xy) { return xy && [xy[0][0], xy[1][0]]; } |
|
}; |
|
|
|
var Y = { |
|
name: "y", |
|
handles: ["n", "s"].map(type), |
|
input: function(y, e) { return y == null ? null : [[e[0][0], +y[0]], [e[1][0], +y[1]]]; }, |
|
output: function(xy) { return xy && [xy[0][1], xy[1][1]]; } |
|
}; |
|
|
|
var XY = { |
|
name: "xy", |
|
handles: ["n", "w", "e", "s", "nw", "ne", "sw", "se"].map(type), |
|
input: function(xy) { return xy == null ? null : number2(xy); }, |
|
output: function(xy) { return xy; } |
|
}; |
|
|
|
var cursors = { |
|
overlay: "crosshair", |
|
selection: "move", |
|
n: "ns-resize", |
|
e: "ew-resize", |
|
s: "ns-resize", |
|
w: "ew-resize", |
|
nw: "nwse-resize", |
|
ne: "nesw-resize", |
|
se: "nwse-resize", |
|
sw: "nesw-resize" |
|
}; |
|
|
|
var flipX = { |
|
e: "w", |
|
w: "e", |
|
nw: "ne", |
|
ne: "nw", |
|
se: "sw", |
|
sw: "se" |
|
}; |
|
|
|
var flipY = { |
|
n: "s", |
|
s: "n", |
|
nw: "sw", |
|
ne: "se", |
|
se: "ne", |
|
sw: "nw" |
|
}; |
|
|
|
var signsX = { |
|
overlay: +1, |
|
selection: +1, |
|
n: null, |
|
e: +1, |
|
s: null, |
|
w: -1, |
|
nw: -1, |
|
ne: +1, |
|
se: +1, |
|
sw: -1 |
|
}; |
|
|
|
var signsY = { |
|
overlay: +1, |
|
selection: +1, |
|
n: -1, |
|
e: null, |
|
s: +1, |
|
w: null, |
|
nw: -1, |
|
ne: -1, |
|
se: +1, |
|
sw: +1 |
|
}; |
|
|
|
function type(t) { |
|
return {type: t}; |
|
} |
|
|
|
|
|
function defaultFilter(event) { |
|
return !event.ctrlKey && !event.button; |
|
} |
|
|
|
function defaultExtent() { |
|
var svg = this.ownerSVGElement || this; |
|
if (svg.hasAttribute("viewBox")) { |
|
svg = svg.viewBox.baseVal; |
|
return [[svg.x, svg.y], [svg.x + svg.width, svg.y + svg.height]]; |
|
} |
|
return [[0, 0], [svg.width.baseVal.value, svg.height.baseVal.value]]; |
|
} |
|
|
|
function defaultTouchable() { |
|
return navigator.maxTouchPoints || ("ontouchstart" in this); |
|
} |
|
|
|
|
|
function local(node) { |
|
while (!node.__brush) if (!(node = node.parentNode)) return; |
|
return node.__brush; |
|
} |
|
|
|
function empty(extent) { |
|
return extent[0][0] === extent[1][0] |
|
|| extent[0][1] === extent[1][1]; |
|
} |
|
|
|
export function brushSelection(node) { |
|
var state = node.__brush; |
|
return state ? state.dim.output(state.selection) : null; |
|
} |
|
|
|
export function brushX() { |
|
return brush(X); |
|
} |
|
|
|
export function brushY() { |
|
return brush(Y); |
|
} |
|
|
|
export default function() { |
|
return brush(XY); |
|
} |
|
|
|
function brush(dim) { |
|
var extent = defaultExtent, |
|
filter = defaultFilter, |
|
touchable = defaultTouchable, |
|
keys = true, |
|
listeners = dispatch("start", "brush", "end"), |
|
handleSize = 6, |
|
touchending; |
|
|
|
function brush(group) { |
|
var overlay = group |
|
.property("__brush", initialize) |
|
.selectAll(".overlay") |
|
.data([type("overlay")]); |
|
|
|
overlay.enter().append("rect") |
|
.attr("class", "overlay") |
|
.attr("pointer-events", "all") |
|
.attr("cursor", cursors.overlay) |
|
.merge(overlay) |
|
.each(function() { |
|
var extent = local(this).extent; |
|
select(this) |
|
.attr("x", extent[0][0]) |
|
.attr("y", extent[0][1]) |
|
.attr("width", extent[1][0] - extent[0][0]) |
|
.attr("height", extent[1][1] - extent[0][1]); |
|
}); |
|
|
|
group.selectAll(".selection") |
|
.data([type("selection")]) |
|
.enter().append("rect") |
|
.attr("class", "selection") |
|
.attr("cursor", cursors.selection) |
|
.attr("fill", "#777") |
|
.attr("fill-opacity", 0.3) |
|
.attr("stroke", "#fff") |
|
.attr("shape-rendering", "crispEdges"); |
|
|
|
var handle = group.selectAll(".handle") |
|
.data(dim.handles, function(d) { return d.type; }); |
|
|
|
handle.exit().remove(); |
|
|
|
handle.enter().append("rect") |
|
.attr("class", function(d) { return "handle handle--" + d.type; }) |
|
.attr("cursor", function(d) { return cursors[d.type]; }); |
|
|
|
group |
|
.each(redraw) |
|
.attr("fill", "none") |
|
.attr("pointer-events", "all") |
|
.on("mousedown.brush", started) |
|
.filter(touchable) |
|
.on("touchstart.brush", started) |
|
.on("touchmove.brush", touchmoved) |
|
.on("touchend.brush touchcancel.brush", touchended) |
|
.style("touch-action", "none") |
|
.style("-webkit-tap-highlight-color", "rgba(0,0,0,0)"); |
|
} |
|
|
|
brush.move = function(group, selection, event) { |
|
if (group.tween) { |
|
group |
|
.on("start.brush", function(event) { emitter(this, arguments).beforestart().start(event); }) |
|
.on("interrupt.brush end.brush", function(event) { emitter(this, arguments).end(event); }) |
|
.tween("brush", function() { |
|
var that = this, |
|
state = that.__brush, |
|
emit = emitter(that, arguments), |
|
selection0 = state.selection, |
|
selection1 = dim.input(typeof selection === "function" ? selection.apply(this, arguments) : selection, state.extent), |
|
i = interpolate(selection0, selection1); |
|
|
|
function tween(t) { |
|
state.selection = t === 1 && selection1 === null ? null : i(t); |
|
redraw.call(that); |
|
emit.brush(); |
|
} |
|
|
|
return selection0 !== null && selection1 !== null ? tween : tween(1); |
|
}); |
|
} else { |
|
group |
|
.each(function() { |
|
var that = this, |
|
args = arguments, |
|
state = that.__brush, |
|
selection1 = dim.input(typeof selection === "function" ? selection.apply(that, args) : selection, state.extent), |
|
emit = emitter(that, args).beforestart(); |
|
|
|
interrupt(that); |
|
state.selection = selection1 === null ? null : selection1; |
|
redraw.call(that); |
|
emit.start(event).brush(event).end(event); |
|
}); |
|
} |
|
}; |
|
|
|
brush.clear = function(group, event) { |
|
brush.move(group, null, event); |
|
}; |
|
|
|
function redraw() { |
|
var group = select(this), |
|
selection = local(this).selection; |
|
|
|
if (selection) { |
|
group.selectAll(".selection") |
|
.style("display", null) |
|
.attr("x", selection[0][0]) |
|
.attr("y", selection[0][1]) |
|
.attr("width", selection[1][0] - selection[0][0]) |
|
.attr("height", selection[1][1] - selection[0][1]); |
|
|
|
group.selectAll(".handle") |
|
.style("display", null) |
|
.attr("x", function(d) { return d.type[d.type.length - 1] === "e" ? selection[1][0] - handleSize / 2 : selection[0][0] - handleSize / 2; }) |
|
.attr("y", function(d) { return d.type[0] === "s" ? selection[1][1] - handleSize / 2 : selection[0][1] - handleSize / 2; }) |
|
.attr("width", function(d) { return d.type === "n" || d.type === "s" ? selection[1][0] - selection[0][0] + handleSize : handleSize; }) |
|
.attr("height", function(d) { return d.type === "e" || d.type === "w" ? selection[1][1] - selection[0][1] + handleSize : handleSize; }); |
|
} |
|
|
|
else { |
|
group.selectAll(".selection,.handle") |
|
.style("display", "none") |
|
.attr("x", null) |
|
.attr("y", null) |
|
.attr("width", null) |
|
.attr("height", null); |
|
} |
|
} |
|
|
|
function emitter(that, args, clean) { |
|
var emit = that.__brush.emitter; |
|
return emit && (!clean || !emit.clean) ? emit : new Emitter(that, args, clean); |
|
} |
|
|
|
function Emitter(that, args, clean) { |
|
this.that = that; |
|
this.args = args; |
|
this.state = that.__brush; |
|
this.active = 0; |
|
this.clean = clean; |
|
} |
|
|
|
Emitter.prototype = { |
|
beforestart: function() { |
|
if (++this.active === 1) this.state.emitter = this, this.starting = true; |
|
return this; |
|
}, |
|
start: function(event, mode) { |
|
if (this.starting) this.starting = false, this.emit("start", event, mode); |
|
else this.emit("brush", event); |
|
return this; |
|
}, |
|
brush: function(event, mode) { |
|
this.emit("brush", event, mode); |
|
return this; |
|
}, |
|
end: function(event, mode) { |
|
if (--this.active === 0) delete this.state.emitter, this.emit("end", event, mode); |
|
return this; |
|
}, |
|
emit: function(type, event, mode) { |
|
var d = select(this.that).datum(); |
|
listeners.call( |
|
type, |
|
this.that, |
|
new BrushEvent(type, { |
|
sourceEvent: event, |
|
target: brush, |
|
selection: dim.output(this.state.selection), |
|
mode, |
|
dispatch: listeners |
|
}), |
|
d |
|
); |
|
} |
|
}; |
|
|
|
function started(event) { |
|
if (touchending && !event.touches) return; |
|
if (!filter.apply(this, arguments)) return; |
|
|
|
var that = this, |
|
type = event.target.__data__.type, |
|
mode = (keys && event.metaKey ? type = "overlay" : type) === "selection" ? MODE_DRAG : (keys && event.altKey ? MODE_CENTER : MODE_HANDLE), |
|
signX = dim === Y ? null : signsX[type], |
|
signY = dim === X ? null : signsY[type], |
|
state = local(that), |
|
extent = state.extent, |
|
selection = state.selection, |
|
W = extent[0][0], w0, w1, |
|
N = extent[0][1], n0, n1, |
|
E = extent[1][0], e0, e1, |
|
S = extent[1][1], s0, s1, |
|
dx = 0, |
|
dy = 0, |
|
moving, |
|
shifting = signX && signY && keys && event.shiftKey, |
|
lockX, |
|
lockY, |
|
points = Array.from(event.touches || [event], t => { |
|
const i = t.identifier; |
|
t = pointer(t, that); |
|
t.point0 = t.slice(); |
|
t.identifier = i; |
|
return t; |
|
}); |
|
|
|
interrupt(that); |
|
var emit = emitter(that, arguments, true).beforestart(); |
|
|
|
if (type === "overlay") { |
|
if (selection) moving = true; |
|
const pts = [points[0], points[1] || points[0]]; |
|
state.selection = selection = [[ |
|
w0 = dim === Y ? W : min(pts[0][0], pts[1][0]), |
|
n0 = dim === X ? N : min(pts[0][1], pts[1][1]) |
|
], [ |
|
e0 = dim === Y ? E : max(pts[0][0], pts[1][0]), |
|
s0 = dim === X ? S : max(pts[0][1], pts[1][1]) |
|
]]; |
|
if (points.length > 1) move(event); |
|
} else { |
|
w0 = selection[0][0]; |
|
n0 = selection[0][1]; |
|
e0 = selection[1][0]; |
|
s0 = selection[1][1]; |
|
} |
|
|
|
w1 = w0; |
|
n1 = n0; |
|
e1 = e0; |
|
s1 = s0; |
|
|
|
var group = select(that) |
|
.attr("pointer-events", "none"); |
|
|
|
var overlay = group.selectAll(".overlay") |
|
.attr("cursor", cursors[type]); |
|
|
|
if (event.touches) { |
|
emit.moved = moved; |
|
emit.ended = ended; |
|
} else { |
|
var view = select(event.view) |
|
.on("mousemove.brush", moved, true) |
|
.on("mouseup.brush", ended, true); |
|
if (keys) view |
|
.on("keydown.brush", keydowned, true) |
|
.on("keyup.brush", keyupped, true) |
|
|
|
dragDisable(event.view); |
|
} |
|
|
|
redraw.call(that); |
|
emit.start(event, mode.name); |
|
|
|
function moved(event) { |
|
for (const p of event.changedTouches || [event]) { |
|
for (const d of points) |
|
if (d.identifier === p.identifier) d.cur = pointer(p, that); |
|
} |
|
if (shifting && !lockX && !lockY && points.length === 1) { |
|
const point = points[0]; |
|
if (abs(point.cur[0] - point[0]) > abs(point.cur[1] - point[1])) |
|
lockY = true; |
|
else |
|
lockX = true; |
|
} |
|
for (const point of points) |
|
if (point.cur) point[0] = point.cur[0], point[1] = point.cur[1]; |
|
moving = true; |
|
noevent(event); |
|
move(event); |
|
} |
|
|
|
function move(event) { |
|
const point = points[0], point0 = point.point0; |
|
var t; |
|
|
|
dx = point[0] - point0[0]; |
|
dy = point[1] - point0[1]; |
|
|
|
switch (mode) { |
|
case MODE_SPACE: |
|
case MODE_DRAG: { |
|
if (signX) dx = max(W - w0, min(E - e0, dx)), w1 = w0 + dx, e1 = e0 + dx; |
|
if (signY) dy = max(N - n0, min(S - s0, dy)), n1 = n0 + dy, s1 = s0 + dy; |
|
break; |
|
} |
|
case MODE_HANDLE: { |
|
if (points[1]) { |
|
if (signX) w1 = max(W, min(E, points[0][0])), e1 = max(W, min(E, points[1][0])), signX = 1; |
|
if (signY) n1 = max(N, min(S, points[0][1])), s1 = max(N, min(S, points[1][1])), signY = 1; |
|
} else { |
|
if (signX < 0) dx = max(W - w0, min(E - w0, dx)), w1 = w0 + dx, e1 = e0; |
|
else if (signX > 0) dx = max(W - e0, min(E - e0, dx)), w1 = w0, e1 = e0 + dx; |
|
if (signY < 0) dy = max(N - n0, min(S - n0, dy)), n1 = n0 + dy, s1 = s0; |
|
else if (signY > 0) dy = max(N - s0, min(S - s0, dy)), n1 = n0, s1 = s0 + dy; |
|
} |
|
break; |
|
} |
|
case MODE_CENTER: { |
|
if (signX) w1 = max(W, min(E, w0 - dx * signX)), e1 = max(W, min(E, e0 + dx * signX)); |
|
if (signY) n1 = max(N, min(S, n0 - dy * signY)), s1 = max(N, min(S, s0 + dy * signY)); |
|
break; |
|
} |
|
} |
|
|
|
if (e1 < w1) { |
|
signX *= -1; |
|
t = w0, w0 = e0, e0 = t; |
|
t = w1, w1 = e1, e1 = t; |
|
if (type in flipX) overlay.attr("cursor", cursors[type = flipX[type]]); |
|
} |
|
|
|
if (s1 < n1) { |
|
signY *= -1; |
|
t = n0, n0 = s0, s0 = t; |
|
t = n1, n1 = s1, s1 = t; |
|
if (type in flipY) overlay.attr("cursor", cursors[type = flipY[type]]); |
|
} |
|
|
|
if (state.selection) selection = state.selection; |
|
if (lockX) w1 = selection[0][0], e1 = selection[1][0]; |
|
if (lockY) n1 = selection[0][1], s1 = selection[1][1]; |
|
|
|
if (selection[0][0] !== w1 |
|
|| selection[0][1] !== n1 |
|
|| selection[1][0] !== e1 |
|
|| selection[1][1] !== s1) { |
|
state.selection = [[w1, n1], [e1, s1]]; |
|
redraw.call(that); |
|
emit.brush(event, mode.name); |
|
} |
|
} |
|
|
|
function ended(event) { |
|
nopropagation(event); |
|
if (event.touches) { |
|
if (event.touches.length) return; |
|
if (touchending) clearTimeout(touchending); |
|
touchending = setTimeout(function() { touchending = null; }, 500); |
|
} else { |
|
dragEnable(event.view, moving); |
|
view.on("keydown.brush keyup.brush mousemove.brush mouseup.brush", null); |
|
} |
|
group.attr("pointer-events", "all"); |
|
overlay.attr("cursor", cursors.overlay); |
|
if (state.selection) selection = state.selection; |
|
if (empty(selection)) state.selection = null, redraw.call(that); |
|
emit.end(event, mode.name); |
|
} |
|
|
|
function keydowned(event) { |
|
switch (event.keyCode) { |
|
case 16: { |
|
shifting = signX && signY; |
|
break; |
|
} |
|
case 18: { |
|
if (mode === MODE_HANDLE) { |
|
if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX; |
|
if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY; |
|
mode = MODE_CENTER; |
|
move(event); |
|
} |
|
break; |
|
} |
|
case 32: { |
|
if (mode === MODE_HANDLE || mode === MODE_CENTER) { |
|
if (signX < 0) e0 = e1 - dx; else if (signX > 0) w0 = w1 - dx; |
|
if (signY < 0) s0 = s1 - dy; else if (signY > 0) n0 = n1 - dy; |
|
mode = MODE_SPACE; |
|
overlay.attr("cursor", cursors.selection); |
|
move(event); |
|
} |
|
break; |
|
} |
|
default: return; |
|
} |
|
noevent(event); |
|
} |
|
|
|
function keyupped(event) { |
|
switch (event.keyCode) { |
|
case 16: { |
|
if (shifting) { |
|
lockX = lockY = shifting = false; |
|
move(event); |
|
} |
|
break; |
|
} |
|
case 18: { |
|
if (mode === MODE_CENTER) { |
|
if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1; |
|
if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1; |
|
mode = MODE_HANDLE; |
|
move(event); |
|
} |
|
break; |
|
} |
|
case 32: { |
|
if (mode === MODE_SPACE) { |
|
if (event.altKey) { |
|
if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX; |
|
if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY; |
|
mode = MODE_CENTER; |
|
} else { |
|
if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1; |
|
if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1; |
|
mode = MODE_HANDLE; |
|
} |
|
overlay.attr("cursor", cursors[type]); |
|
move(event); |
|
} |
|
break; |
|
} |
|
default: return; |
|
} |
|
noevent(event); |
|
} |
|
} |
|
|
|
function touchmoved(event) { |
|
emitter(this, arguments).moved(event); |
|
} |
|
|
|
function touchended(event) { |
|
emitter(this, arguments).ended(event); |
|
} |
|
|
|
function initialize() { |
|
var state = this.__brush || {selection: null}; |
|
state.extent = number2(extent.apply(this, arguments)); |
|
state.dim = dim; |
|
return state; |
|
} |
|
|
|
brush.extent = function(_) { |
|
return arguments.length ? (extent = typeof _ === "function" ? _ : constant(number2(_)), brush) : extent; |
|
}; |
|
|
|
brush.filter = function(_) { |
|
return arguments.length ? (filter = typeof _ === "function" ? _ : constant(!!_), brush) : filter; |
|
}; |
|
|
|
brush.touchable = function(_) { |
|
return arguments.length ? (touchable = typeof _ === "function" ? _ : constant(!!_), brush) : touchable; |
|
}; |
|
|
|
brush.handleSize = function(_) { |
|
return arguments.length ? (handleSize = +_, brush) : handleSize; |
|
}; |
|
|
|
brush.keyModifiers = function(_) { |
|
return arguments.length ? (keys = !!_, brush) : keys; |
|
}; |
|
|
|
brush.on = function() { |
|
var value = listeners.on.apply(listeners, arguments); |
|
return value === listeners ? brush : value; |
|
}; |
|
|
|
return brush; |
|
} |
|
|