From 660f3d44bbdd17c3bfb1a859ac151b83fb7d89a2 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Thu, 14 Jul 2016 09:11:05 +0100 Subject: [PATCH] Add onPan and onZoom to the PanZoom interaction --- plottable-npm.d.ts | 32 +++ plottable.d.ts | 32 +++ plottable.js | 47 +++++ src/interactions/panZoomInteraction.ts | 57 +++++ test/interactions/panZoomInteractionTests.ts | 210 +++++++++++++++++++ 5 files changed, 378 insertions(+) diff --git a/plottable-npm.d.ts b/plottable-npm.d.ts index 2502c4977b..619924656e 100644 --- a/plottable-npm.d.ts +++ b/plottable-npm.d.ts @@ -4577,6 +4577,8 @@ declare namespace Plottable.Interactions { } } declare namespace Plottable.Interactions { + type PanCallback = (e: Event) => void; + type ZoomCallback = (e: Event) => void; class PanZoom extends Interaction { /** * The number of pixels occupied in a line. @@ -4595,6 +4597,8 @@ declare namespace Plottable.Interactions { private _touchCancelCallback; private _minDomainExtents; private _maxDomainExtents; + private _panCallbacks; + private _zoomCallbacks; /** * A PanZoom Interaction updates the domains of an x-scale and/or a y-scale * in response to the user panning or zooming. @@ -4707,6 +4711,34 @@ declare namespace Plottable.Interactions { * @returns {Interactions.PanZoom} The calling PanZoom Interaction. */ maxDomainExtent(quantitativeScale: QuantitativeScale, maxDomainExtent: D): this; + /** + * Adds a callback to be called when panning ends. + * + * @param {PanCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + onPan(callback: PanCallback): this; + /** + * Removes a callback that would be called when panning ends. + * + * @param {PanCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + offPan(callback: PanCallback): this; + /** + * Adds a callback to be called when zooming ends. + * + * @param {ZoomCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + onZoom(callback: ZoomCallback): this; + /** + * Removes a callback that would be called when zooming ends. + * + * @param {ZoomCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + offZoom(callback: ZoomCallback): this; } } declare namespace Plottable { diff --git a/plottable.d.ts b/plottable.d.ts index aa6c8fef92..5e474eab3a 100644 --- a/plottable.d.ts +++ b/plottable.d.ts @@ -4576,6 +4576,8 @@ declare namespace Plottable.Interactions { } } declare namespace Plottable.Interactions { + type PanCallback = (e: Event) => void; + type ZoomCallback = (e: Event) => void; class PanZoom extends Interaction { /** * The number of pixels occupied in a line. @@ -4594,6 +4596,8 @@ declare namespace Plottable.Interactions { private _touchCancelCallback; private _minDomainExtents; private _maxDomainExtents; + private _panCallbacks; + private _zoomCallbacks; /** * A PanZoom Interaction updates the domains of an x-scale and/or a y-scale * in response to the user panning or zooming. @@ -4706,6 +4710,34 @@ declare namespace Plottable.Interactions { * @returns {Interactions.PanZoom} The calling PanZoom Interaction. */ maxDomainExtent(quantitativeScale: QuantitativeScale, maxDomainExtent: D): this; + /** + * Adds a callback to be called when panning ends. + * + * @param {PanCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + onPan(callback: PanCallback): this; + /** + * Removes a callback that would be called when panning ends. + * + * @param {PanCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + offPan(callback: PanCallback): this; + /** + * Adds a callback to be called when zooming ends. + * + * @param {ZoomCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + onZoom(callback: ZoomCallback): this; + /** + * Removes a callback that would be called when zooming ends. + * + * @param {ZoomCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + offZoom(callback: ZoomCallback): this; } } declare namespace Plottable { diff --git a/plottable.js b/plottable.js index bff71c58d3..86030753b4 100644 --- a/plottable.js +++ b/plottable.js @@ -11222,6 +11222,8 @@ var Plottable; this._touchMoveCallback = function (ids, idToPoint, e) { return _this._handlePinch(ids, idToPoint, e); }; this._touchEndCallback = function (ids, idToPoint, e) { return _this._handleTouchEnd(ids, idToPoint, e); }; this._touchCancelCallback = function (ids, idToPoint, e) { return _this._handleTouchEnd(ids, idToPoint, e); }; + this._panCallbacks = new Plottable.Utils.CallbackSet(); + this._zoomCallbacks = new Plottable.Utils.CallbackSet(); this._xScales = new Plottable.Utils.Set(); this._yScales = new Plottable.Utils.Set(); this._dragInteraction = new Interactions.Drag(); @@ -11335,6 +11337,9 @@ var Plottable; ids.forEach(function (id) { _this._touchIds.remove(id.toString()); }); + if (this._touchIds.size() > 0) { + this._zoomCallbacks.callCallbacks(e); + } }; PanZoom.prototype._magnifyScale = function (scale, magnifyAmount, centerValue) { var magnifyTransform = function (rangeValue) { return scale.invert(centerValue - (centerValue - rangeValue) * magnifyAmount); }; @@ -11363,6 +11368,7 @@ var Plottable; this.yScales().forEach(function (yScale) { _this._magnifyScale(yScale, zoomAmount_1, translatedP.y); }); + this._zoomCallbacks.callCallbacks(e); } }; PanZoom.prototype._constrainedZoomAmount = function (scale, zoomAmount) { @@ -11395,6 +11401,7 @@ var Plottable; }); lastDragPoint = endPoint; }); + this._dragInteraction.onDragEnd(function (e) { return _this._panCallbacks.callCallbacks(e); }); }; PanZoom.prototype._nonLinearScaleWithExtents = function (scale) { return this.minDomainExtent(scale) != null && this.maxDomainExtent(scale) != null && @@ -11508,6 +11515,46 @@ var Plottable; this._maxDomainExtents.set(quantitativeScale, maxDomainExtent); return this; }; + /** + * Adds a callback to be called when panning ends. + * + * @param {PanCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + PanZoom.prototype.onPan = function (callback) { + this._panCallbacks.add(callback); + return this; + }; + /** + * Removes a callback that would be called when panning ends. + * + * @param {PanCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + PanZoom.prototype.offPan = function (callback) { + this._panCallbacks.delete(callback); + return this; + }; + /** + * Adds a callback to be called when zooming ends. + * + * @param {ZoomCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + PanZoom.prototype.onZoom = function (callback) { + this._zoomCallbacks.add(callback); + return this; + }; + /** + * Removes a callback that would be called when zooming ends. + * + * @param {ZoomCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + PanZoom.prototype.offZoom = function (callback) { + this._zoomCallbacks.delete(callback); + return this; + }; /** * The number of pixels occupied in a line. */ diff --git a/src/interactions/panZoomInteraction.ts b/src/interactions/panZoomInteraction.ts index 3443f708bb..fc124872e3 100644 --- a/src/interactions/panZoomInteraction.ts +++ b/src/interactions/panZoomInteraction.ts @@ -1,4 +1,8 @@ namespace Plottable.Interactions { + + export type PanCallback = (e: Event) => void; + export type ZoomCallback = (e: Event) => void; + export class PanZoom extends Interaction { /** * The number of pixels occupied in a line. @@ -22,6 +26,9 @@ namespace Plottable.Interactions { private _minDomainExtents: Utils.Map, any>; private _maxDomainExtents: Utils.Map, any>; + private _panCallbacks = new Utils.CallbackSet(); + private _zoomCallbacks = new Utils.CallbackSet(); + /** * A PanZoom Interaction updates the domains of an x-scale and/or a y-scale * in response to the user panning or zooming. @@ -171,6 +178,10 @@ namespace Plottable.Interactions { ids.forEach((id) => { this._touchIds.remove(id.toString()); }); + + if (this._touchIds.size() > 0) { + this._zoomCallbacks.callCallbacks(e); + } } private _magnifyScale(scale: QuantitativeScale, magnifyAmount: number, centerValue: number) { @@ -205,6 +216,7 @@ namespace Plottable.Interactions { this.yScales().forEach((yScale) => { this._magnifyScale(yScale, zoomAmount, translatedP.y); }); + this._zoomCallbacks.callCallbacks(e); } } @@ -242,6 +254,7 @@ namespace Plottable.Interactions { }); lastDragPoint = endPoint; }); + this._dragInteraction.onDragEnd((e) => this._panCallbacks.callCallbacks(e)); } private _nonLinearScaleWithExtents(scale: QuantitativeScale) { @@ -424,5 +437,49 @@ namespace Plottable.Interactions { this._maxDomainExtents.set(quantitativeScale, maxDomainExtent); return this; } + + /** + * Adds a callback to be called when panning ends. + * + * @param {PanCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + public onPan(callback: PanCallback) { + this._panCallbacks.add(callback); + return this; + } + + /** + * Removes a callback that would be called when panning ends. + * + * @param {PanCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + public offPan(callback: PanCallback) { + this._panCallbacks.delete(callback); + return this; + } + + /** + * Adds a callback to be called when zooming ends. + * + * @param {ZoomCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + public onZoom(callback: ZoomCallback) { + this._zoomCallbacks.add(callback); + return this; + } + + /** + * Removes a callback that would be called when zooming ends. + * + * @param {ZoomCallback} callback + * @returns {Event} The calling PanZoom Interaction. + */ + public offZoom(callback: ZoomCallback) { + this._zoomCallbacks.delete(callback); + return this; + } } } diff --git a/test/interactions/panZoomInteractionTests.ts b/test/interactions/panZoomInteractionTests.ts index 70a154f4a0..e70bd3eeda 100644 --- a/test/interactions/panZoomInteractionTests.ts +++ b/test/interactions/panZoomInteractionTests.ts @@ -581,5 +581,215 @@ describe("Interactions", () => { }); }); + describe("Registering and deregistering Pan and Zoom event callbacks", () => { + let svg: d3.Selection; + let SVG_WIDTH = 400; + let SVG_HEIGHT = 500; + + let eventTarget: d3.Selection; + + let xScale: Plottable.QuantitativeScale; + let panZoomInteraction: Plottable.Interactions.PanZoom; + + interface PanZoomTestCallback { + called: boolean; + reset: () => void; + (e: Event): void; + } + + function makeCallback () { + let callback = function(e: Event) { + callback.called = true; + }; + callback.called = false; + callback.reset = () => { + callback.called = false; + }; + return callback; + } + + beforeEach(() => { + xScale = new Plottable.Scales.Linear(); + xScale.domain([0, SVG_WIDTH / 2]); + + svg = TestMethods.generateSVG(SVG_WIDTH, SVG_HEIGHT); + + let component = new Plottable.Component(); + component.renderTo(svg); + + panZoomInteraction = new Plottable.Interactions.PanZoom(); + panZoomInteraction.addXScale(xScale); + panZoomInteraction.attachTo(component); + + eventTarget = component.background(); + }); + + it("registers callback using onPan", () => { + let callback = makeCallback(); + let startPoint = { x: SVG_WIDTH / 2, y: SVG_HEIGHT / 2 }; + let endPoint = { x: -SVG_WIDTH / 2, y: -SVG_HEIGHT / 2 }; + + assert.strictEqual(panZoomInteraction.onPan(callback), panZoomInteraction, "registration returns the calling Interaction"); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint]); + assert.isTrue(callback.called, "Interaction correctly triggers the callback (touch)"); + + callback.reset(); + + TestMethods.triggerFakeMouseEvent("mousedown", eventTarget, startPoint.x, startPoint.y); + TestMethods.triggerFakeMouseEvent("mousemove", eventTarget, endPoint.x, endPoint.y); + TestMethods.triggerFakeMouseEvent("mouseend", eventTarget, endPoint.x, endPoint.y); + TestMethods.triggerFakeMouseEvent("mouseup", eventTarget, endPoint.x, endPoint.y); + assert.isTrue(callback.called, "Interaction correctly triggers the callback (mouse)"); + + svg.remove(); + }); + + it("registers callback using onZoom", () => { + let callback = makeCallback(); + let startPoint = { x: SVG_WIDTH / 2, y: SVG_HEIGHT / 2 }; + let endPoint = { x: -SVG_WIDTH / 2, y: -SVG_HEIGHT / 2 }; + let scrollPoint = { x: SVG_WIDTH / 4, y: SVG_HEIGHT / 4 }; + let deltaY = 3000; + + assert.strictEqual(panZoomInteraction.onZoom(callback), panZoomInteraction, "registration returns the calling Interaction"); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint, scrollPoint], [0, 1]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint], [1]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint], [1]); + assert.isTrue(callback.called, "Interaction correctly triggers the callback (touch)"); + + callback.reset(); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint]); + assert.isFalse(callback.called, "Interaction does not trigger zoom callback on pan event (touch)"); + + callback.reset(); + + // HACKHACK PhantomJS doesn't implement fake creation of WheelEvents + // https://github.com/ariya/phantomjs/issues/11289 + if (window.PHANTOMJS) { + svg.remove(); + return; + } + + TestMethods.triggerFakeWheelEvent("wheel", svg, scrollPoint.x, scrollPoint.y, deltaY); + assert.isTrue(callback.called, "Interaction correctly triggers the callback (mouse)"); + svg.remove(); + }); + + it("deregisters callback using offPan", () => { + let callback = makeCallback(); + let startPoint = { x: SVG_WIDTH / 2, y: SVG_HEIGHT / 2 }; + let endPoint = { x: -SVG_WIDTH / 2, y: -SVG_HEIGHT / 2 }; + + panZoomInteraction.onPan(callback); + + assert.strictEqual(panZoomInteraction.offPan(callback), panZoomInteraction, "deregistration returns the calling Interaction"); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint]); + assert.isFalse(callback.called, "callback should be disconnected from the Interaction"); + + svg.remove(); + }); + + it("deregisters callback using offZoom", () => { + let callback = makeCallback(); + let startPoint = { x: SVG_WIDTH / 2, y: SVG_HEIGHT / 2 }; + let endPoint = { x: -SVG_WIDTH / 2, y: -SVG_HEIGHT / 2 }; + let scrollPoint = { x: SVG_WIDTH / 4, y: SVG_HEIGHT / 4 }; + panZoomInteraction.onZoom(callback); + + assert.strictEqual(panZoomInteraction.offZoom(callback), panZoomInteraction, "deregistration returns the calling Interaction"); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint, scrollPoint], [0, 1]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint], [1]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint], [1]); + + assert.isFalse(callback.called, "callback should be disconnected from the Interaction"); + + svg.remove(); + }); + + it("can register multiple onPan callbacks", () => { + let callback1 = makeCallback(); + let callback2 = makeCallback(); + let startPoint = { x: SVG_WIDTH / 2, y: SVG_HEIGHT / 2 }; + let endPoint = { x: -SVG_WIDTH / 2, y: -SVG_HEIGHT / 2 }; + + panZoomInteraction.onPan(callback1); + panZoomInteraction.onPan(callback2); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint]); + assert.isTrue(callback1.called, "Interaction should trigger the first callback"); + assert.isTrue(callback1.called, "Interaction should trigger the second callback"); + svg.remove(); + }); + + it("can register multiple onZoom callbacks", () => { + let callback1 = makeCallback(); + let callback2 = makeCallback(); + let startPoint = { x: SVG_WIDTH / 2, y: SVG_HEIGHT / 2 }; + let endPoint = { x: -SVG_WIDTH / 2, y: -SVG_HEIGHT / 2 }; + let scrollPoint = { x: SVG_WIDTH / 4, y: SVG_HEIGHT / 4 }; + + panZoomInteraction.onZoom(callback1); + panZoomInteraction.onZoom(callback2); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint, scrollPoint], [0, 1]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint], [1]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint], [1]); + + assert.isTrue(callback1.called, "Interaction should trigger the first callback"); + assert.isTrue(callback1.called, "Interaction should trigger the second callback"); + svg.remove(); + }); + + it("can deregister a onPan callback without affecting the other ones", () => { + let callback1 = makeCallback(); + let callback2 = makeCallback(); + let startPoint = { x: SVG_WIDTH / 2, y: SVG_HEIGHT / 2 }; + let endPoint = { x: -SVG_WIDTH / 2, y: -SVG_HEIGHT / 2 }; + + panZoomInteraction.onPan(callback1); + panZoomInteraction.onPan(callback2); + panZoomInteraction.offPan(callback1); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint]); + assert.isFalse(callback1.called, "Callback1 should be disconnected from the Interaction"); + assert.isTrue(callback2.called, "Callback2 should still exist on the Interaction"); + svg.remove(); + }); + + it("can deregister a onZoom callback without affecting the other ones", () => { + let callback1 = makeCallback(); + let callback2 = makeCallback(); + let startPoint = { x: SVG_WIDTH / 2, y: SVG_HEIGHT / 2 }; + let endPoint = { x: -SVG_WIDTH / 2, y: -SVG_HEIGHT / 2 }; + let scrollPoint = { x: SVG_WIDTH / 4, y: SVG_HEIGHT / 4 }; + + panZoomInteraction.onZoom(callback1); + panZoomInteraction.onZoom(callback2); + panZoomInteraction.offZoom(callback1); + + TestMethods.triggerFakeTouchEvent("touchstart", eventTarget, [startPoint, scrollPoint], [0, 1]); + TestMethods.triggerFakeTouchEvent("touchmove", eventTarget, [endPoint], [1]); + TestMethods.triggerFakeTouchEvent("touchend", eventTarget, [endPoint], [1]); + + assert.isFalse(callback1.called, "Callback1 should be disconnected from the Interaction"); + assert.isTrue(callback2.called, "Callback2 should still exist on the Interaction"); + svg.remove(); + }); + }); }); });