diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js
index 97d95aa3f49..328709e92ba 100644
--- a/src/components/annotations/draw.js
+++ b/src/components/annotations/draw.js
@@ -285,8 +285,13 @@ function drawOne(gd, index) {
posPx.text = basePx + textShift;
}
+ // padplus/minus are used by autorange
options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift;
options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift;
+
+ // size/shift are used during dragging
+ options['_' + axLetter + 'size'] = annSize;
+ options['_' + axLetter + 'shift'] = textShift;
});
if(annotationIsOffscreen) {
diff --git a/test/jasmine/assets/drag.js b/test/jasmine/assets/drag.js
new file mode 100644
index 00000000000..16020b07493
--- /dev/null
+++ b/test/jasmine/assets/drag.js
@@ -0,0 +1,78 @@
+var mouseEvent = require('../assets/mouse_event');
+
+/*
+ * drag: grab a node and drag it (dx, dy) pixels
+ * optionally specify an edge ('n', 'se', 'w' etc)
+ * to grab it by an edge or corner (otherwise the middle is used)
+ */
+module.exports = function(node, dx, dy, edge) {
+
+ edge = edge || '';
+ var bbox = node.getBoundingClientRect(),
+ fromX, fromY;
+
+ if(edge.indexOf('n') !== -1) fromY = bbox.top;
+ else if(edge.indexOf('s') !== -1) fromY = bbox.bottom;
+ else fromY = (bbox.bottom + bbox.top) / 2;
+
+ if(edge.indexOf('w') !== -1) fromX = bbox.left;
+ else if(edge.indexOf('e') !== -1) fromX = bbox.right;
+ else fromX = (bbox.left + bbox.right) / 2;
+
+
+ var toX = fromX + dx,
+ toY = fromY + dy;
+
+ mouseEvent('mousemove', fromX, fromY, {element: node});
+ mouseEvent('mousedown', fromX, fromY, {element: node});
+
+ var promise = waitForDragCover().then(function(dragCoverNode) {
+ mouseEvent('mousemove', toX, toY, {element: dragCoverNode});
+ mouseEvent('mouseup', toX, toY, {element: dragCoverNode});
+ return waitForDragCoverRemoval();
+ });
+
+ return promise;
+};
+
+function waitForDragCover() {
+ return new Promise(function(resolve) {
+ var interval = 5,
+ timeout = 5000;
+
+ var id = setInterval(function() {
+ var dragCoverNode = document.querySelector('.dragcover');
+ if(dragCoverNode) {
+ clearInterval(id);
+ resolve(dragCoverNode);
+ }
+
+ timeout -= interval;
+ if(timeout < 0) {
+ clearInterval(id);
+ throw new Error('waitForDragCover: timeout');
+ }
+ }, interval);
+ });
+}
+
+function waitForDragCoverRemoval() {
+ return new Promise(function(resolve) {
+ var interval = 5,
+ timeout = 5000;
+
+ var id = setInterval(function() {
+ var dragCoverNode = document.querySelector('.dragcover');
+ if(!dragCoverNode) {
+ clearInterval(id);
+ resolve(dragCoverNode);
+ }
+
+ timeout -= interval;
+ if(timeout < 0) {
+ clearInterval(id);
+ throw new Error('waitForDragCoverRemoval: timeout');
+ }
+ }, interval);
+ });
+}
diff --git a/test/jasmine/assets/mouse_event.js b/test/jasmine/assets/mouse_event.js
index 39101d76d35..153314c5abd 100644
--- a/test/jasmine/assets/mouse_event.js
+++ b/test/jasmine/assets/mouse_event.js
@@ -10,7 +10,7 @@ module.exports = function(type, x, y, opts) {
fullOpts.buttons = opts.buttons;
}
- var el = document.elementFromPoint(x, y),
+ var el = (opts && opts.element) || document.elementFromPoint(x, y),
ev;
if(type === 'scroll') {
@@ -20,4 +20,6 @@ module.exports = function(type, x, y, opts) {
}
el.dispatchEvent(ev);
+
+ return el;
};
diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js
index d898a65341a..d6701bd1f71 100644
--- a/test/jasmine/tests/annotations_test.js
+++ b/test/jasmine/tests/annotations_test.js
@@ -11,6 +11,7 @@ var customMatchers = require('../assets/custom_matchers');
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var failTest = require('../assets/fail_test');
+var drag = require('../assets/drag');
describe('Test annotations', function() {
@@ -740,3 +741,233 @@ describe('annotation clicktoshow', function() {
.then(done);
});
});
+
+describe('annotation dragging', function() {
+ var gd;
+
+ function textDrag() { return gd.querySelector('.annotation-text-g>g'); }
+ function arrowDrag() { return gd.querySelector('.annotation-arrow-g>.anndrag'); }
+ function textBox() { return gd.querySelector('.annotation-text-g'); }
+
+ beforeAll(function() {
+ jasmine.addMatchers(customMatchers);
+ });
+
+ beforeEach(function(done) {
+ gd = createGraphDiv();
+
+ // we've already tested autorange with relayout, so fix the geometry
+ // completely so we know exactly what we're dealing with
+ // plot area is 300x300, and covers data range 100x100
+ Plotly.plot(gd,
+ [{x: [0, 100], y: [0, 100], mode: 'markers'}],
+ {
+ xaxis: {range: [0, 100]},
+ yaxis: {range: [0, 100]},
+ width: 500,
+ height: 500,
+ margin: {l: 100, r: 100, t: 100, b: 100, pad: 0}
+ },
+ {editable: true}
+ )
+ .then(done);
+ });
+
+ afterEach(destroyGraphDiv);
+
+ function initAnnotation(annotation) {
+ return Plotly.relayout(gd, {annotations: [annotation]})
+ .then(function() {
+ return Plots.previousPromises(gd);
+ });
+ }
+
+ function dragAndReplot(node, dx, dy, edge) {
+ return drag(node, dx, dy, edge).then(function() {
+ return Plots.previousPromises(gd);
+ });
+ }
+
+ /*
+ * run through a series of drags of the same annotation
+ * findDragger: fn that returns the element to drag on
+ * (either textDrag or ArrowDrag)
+ * autoshiftX, autoshiftY: how much does the annotation anchor shift
+ * moving between one region and the next. Zero except if autoanchor
+ * is active, ie paper-referenced with no arrow
+ * coordScale: how big is the full plot? paper-referenced has scale 1
+ * and for the plot defined above, data-referenced has scale 100
+ */
+ function checkDragging(findDragger, autoshiftX, autoshiftY, coordScale) {
+ var bboxInitial = textBox().getBoundingClientRect();
+ // first move it within the same auto-anchor zone
+ return dragAndReplot(findDragger(), 30, -30)
+ .then(function() {
+ var bbox = textBox().getBoundingClientRect();
+
+ // I'm not sure why these calculations aren't exact - they end up
+ // being off by a fraction of a pixel, or a full pixel sometimes
+ // even though as far as I can see in practice the positioning is
+ // exact. In any event, this precision is enough to ensure that
+ // anchor: auto is being used.
+ expect(bbox.left).toBeWithin(bboxInitial.left + 30, 1);
+ expect(bbox.top).toBeWithin(bboxInitial.top - 30, 1);
+
+ var ann = gd.layout.annotations[0];
+ expect(ann.x).toBeWithin(0.1 * coordScale, 0.01 * coordScale);
+ expect(ann.y).toBeWithin(0.1 * coordScale, 0.01 * coordScale);
+
+ // now move it to the center
+ // note that we explicitly offset by half the box size because the
+ // auto-anchor will move to the center
+ return dragAndReplot(findDragger(), 120 - autoshiftX, -120 + autoshiftY);
+ })
+ .then(function() {
+ var bbox = textBox().getBoundingClientRect();
+ expect(bbox.left).toBeWithin(bboxInitial.left + 150 - autoshiftX, 2);
+ expect(bbox.top).toBeWithin(bboxInitial.top - 150 + autoshiftY, 2);
+
+ var ann = gd.layout.annotations[0];
+ expect(ann.x).toBeWithin(0.5 * coordScale, 0.01 * coordScale);
+ expect(ann.y).toBeWithin(0.5 * coordScale, 0.01 * coordScale);
+
+ // next move it near the upper right corner, where the auto-anchor
+ // moves to the top right corner
+ // we don't move it all the way to the corner, so the annotation will
+ // still be entirely on the plot even with an arrow.
+ return dragAndReplot(findDragger(), 90 - autoshiftX, -90 + autoshiftY);
+ })
+ .then(function() {
+ var bbox = textBox().getBoundingClientRect();
+ expect(bbox.left).toBeWithin(bboxInitial.left + 240 - 2 * autoshiftX, 2);
+ expect(bbox.top).toBeWithin(bboxInitial.top - 240 + 2 * autoshiftY, 2);
+
+ var ann = gd.layout.annotations[0];
+ expect(ann.x).toBeWithin(0.8 * coordScale, 0.01 * coordScale);
+ expect(ann.y).toBeWithin(0.8 * coordScale, 0.01 * coordScale);
+
+ // finally move it back to 0, 0
+ return dragAndReplot(findDragger(), -240 + 2 * autoshiftX, 240 - 2 * autoshiftY);
+ })
+ .then(function() {
+ var bbox = textBox().getBoundingClientRect();
+ expect(bbox.left).toBeWithin(bboxInitial.left, 2);
+ expect(bbox.top).toBeWithin(bboxInitial.top, 2);
+
+ var ann = gd.layout.annotations[0];
+ expect(ann.x).toBeWithin(0 * coordScale, 0.01 * coordScale);
+ expect(ann.y).toBeWithin(0 * coordScale, 0.01 * coordScale);
+ });
+ }
+
+ // for annotations with arrows: check that dragging the text moves only
+ // ax and ay (and the textbox itself)
+ function checkTextDrag() {
+ var ann = gd.layout.annotations[0],
+ x0 = ann.x,
+ y0 = ann.y,
+ ax0 = ann.ax,
+ ay0 = ann.ay;
+
+ var bboxInitial = textBox().getBoundingClientRect();
+
+ return dragAndReplot(textDrag(), 50, -50)
+ .then(function() {
+ var bbox = textBox().getBoundingClientRect();
+ expect(bbox.left).toBeWithin(bboxInitial.left + 50, 1);
+ expect(bbox.top).toBeWithin(bboxInitial.top - 50, 1);
+
+ ann = gd.layout.annotations[0];
+
+ expect(ann.x).toBe(x0);
+ expect(ann.y).toBe(y0);
+ expect(ann.ax).toBeWithin(ax0 + 50, 1);
+ expect(ann.ay).toBeWithin(ay0 - 50, 1);
+ });
+ }
+
+ it('respects anchor: auto when paper-referenced without arrow', function(done) {
+ initAnnotation({
+ x: 0,
+ y: 0,
+ showarrow: false,
+ text: 'blah
blah blah',
+ xref: 'paper',
+ yref: 'paper'
+ })
+ .then(function() {
+ var bbox = textBox().getBoundingClientRect();
+
+ return checkDragging(textDrag, bbox.width / 2, bbox.height / 2, 1);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('also works paper-referenced with explicit anchors and no arrow', function(done) {
+ initAnnotation({
+ x: 0,
+ y: 0,
+ showarrow: false,
+ text: 'blah
blah blah',
+ xref: 'paper',
+ yref: 'paper',
+ xanchor: 'left',
+ yanchor: 'top'
+ })
+ .then(function() {
+ // with offsets 0, 0 because the anchor doesn't change now
+ return checkDragging(textDrag, 0, 0, 1);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('works paper-referenced with arrows', function(done) {
+ initAnnotation({
+ x: 0,
+ y: 0,
+ text: 'blah
blah blah',
+ xref: 'paper',
+ yref: 'paper',
+ ax: 30,
+ ay: 30
+ })
+ .then(function() {
+ return checkDragging(arrowDrag, 0, 0, 1);
+ })
+ .then(checkTextDrag)
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('works data-referenced with no arrow', function(done) {
+ initAnnotation({
+ x: 0,
+ y: 0,
+ showarrow: false,
+ text: 'blah
blah blah'
+ })
+ .then(function() {
+ return checkDragging(textDrag, 0, 0, 100);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('works data-referenced with arrow', function(done) {
+ initAnnotation({
+ x: 0,
+ y: 0,
+ text: 'blah
blah blah',
+ ax: 30,
+ ay: -30
+ })
+ .then(function() {
+ return checkDragging(arrowDrag, 0, 0, 100);
+ })
+ .then(checkTextDrag)
+ .catch(failTest)
+ .then(done);
+ });
+});
diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js
index bfd3ed490b2..7b35e5c0a61 100644
--- a/test/jasmine/tests/shapes_test.js
+++ b/test/jasmine/tests/shapes_test.js
@@ -14,6 +14,7 @@ var customMatchers = require('../assets/custom_matchers');
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var failTest = require('../assets/fail_test');
+var drag = require('../assets/drag');
describe('Test shapes defaults:', function() {
@@ -748,7 +749,7 @@ describe('Test shapes', function() {
var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p);
- return resize(direction, node, dx, dy).then(function() {
+ return drag(node, dx, dy, direction).then(function() {
var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p);
var keyN, keyS, keyW, keyE;
@@ -815,118 +816,3 @@ describe('Test shapes', function() {
return coordinates;
}
});
-
-var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY;
-
-function mouseDown(node, x, y) {
- node.dispatchEvent(new MouseEvent('mousedown', {
- bubbles: true,
- clientX: x,
- clientY: y
- }));
-}
-
-function mouseMove(node, x, y) {
- node.dispatchEvent(new MouseEvent('mousemove', {
- bubbles: true,
- clientX: x,
- clientY: y
- }));
-}
-
-function mouseUp(node, x, y) {
- node.dispatchEvent(new MouseEvent('mouseup', {
- bubbles: true,
- clientX: x,
- clientY: y
- }));
-}
-
-function drag(node, dx, dy) {
- var bbox = node.getBoundingClientRect(),
- fromX = (bbox.left + bbox.right) / 2,
- fromY = (bbox.bottom + bbox.top) / 2,
- toX = fromX + dx,
- toY = fromY + dy;
-
- mouseMove(node, fromX, fromY);
- mouseDown(node, fromX, fromY);
-
- var promise = waitForDragCover().then(function(dragCoverNode) {
- mouseMove(dragCoverNode, toX, toY);
- mouseUp(dragCoverNode, toX, toY);
- return waitForDragCoverRemoval();
- });
-
- return promise;
-}
-
-function resize(direction, node, dx, dy) {
- var bbox = node.getBoundingClientRect();
-
- var fromX, fromY, toX, toY;
-
- if(~direction.indexOf('n')) fromY = bbox.top;
- else if(~direction.indexOf('s')) fromY = bbox.bottom;
- else fromY = (bbox.bottom + bbox.top) / 2;
-
- if(~direction.indexOf('w')) fromX = bbox.left;
- else if(~direction.indexOf('e')) fromX = bbox.right;
- else fromX = (bbox.left + bbox.right) / 2;
-
- toX = fromX + dx;
- toY = fromY + dy;
-
- mouseMove(node, fromX, fromY);
- mouseDown(node, fromX, fromY);
-
- var promise = waitForDragCover().then(function(dragCoverNode) {
- mouseMove(dragCoverNode, toX, toY);
- mouseUp(dragCoverNode, toX, toY);
- return waitForDragCoverRemoval();
- });
-
- return promise;
-}
-
-function waitForDragCover() {
- return new Promise(function(resolve) {
- var interval = DBLCLICKDELAY / 4,
- timeout = 5000;
-
- var id = setInterval(function() {
- var dragCoverNode = d3.selectAll('.dragcover').node();
- if(dragCoverNode) {
- clearInterval(id);
- resolve(dragCoverNode);
- }
-
- timeout -= interval;
- if(timeout < 0) {
- clearInterval(id);
- throw new Error('waitForDragCover: timeout');
- }
- }, interval);
- });
-}
-
-function waitForDragCoverRemoval() {
- return new Promise(function(resolve) {
- var interval = DBLCLICKDELAY / 4,
- timeout = 5000;
-
- var id = setInterval(function() {
- var dragCoverNode = d3.selectAll('.dragcover').node();
- if(!dragCoverNode) {
- clearInterval(id);
- resolve(dragCoverNode);
- }
-
- timeout -= interval;
- if(timeout < 0) {
- clearInterval(id);
- throw new Error('waitForDragCoverRemoval: timeout');
- }
- }, interval);
- });
-}