]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Platform event API abstraction
authorSimon Brunel <simonbrunel@users.noreply.github.com>
Sat, 14 Jan 2017 13:38:56 +0000 (14:38 +0100)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sun, 15 Jan 2017 19:25:38 +0000 (14:25 -0500)
Move base platform definition and logic in src/platform/platform.js and simplify the browser -> Chart.js event mapping by listing only different naming then fallback to the native type.

Replace `createEvent` by `add/removeEventListener` methods which dispatch Chart.js IEvent objects instead of native events. Move `add/removeResizeListener` implementation into the DOM platform which is now accessible via `platform.add/removeEventListener(chart, 'resize', listener)`.

Finally, remove `bindEvent` and `unbindEvent` from the helpers since the implementation is specific to the chart controller (and should be private).

src/chart.js
src/core/core.controller.js
src/core/core.helpers.js
src/core/core.interaction.js
src/core/core.legend.js
src/core/core.tooltip.js
src/platforms/platform.dom.js
src/platforms/platform.js [new file with mode: 0644]
test/platform.dom.tests.js

index 186d07a42f5824c504d40b22baadd300f0150d31..77d2bb636c8655cdeef01dda264decf8f57baf20 100644 (file)
@@ -4,6 +4,7 @@
 var Chart = require('./core/core.js')();
 
 require('./core/core.helpers')(Chart);
+require('./platforms/platform.js')(Chart);
 require('./core/core.canvasHelpers')(Chart);
 require('./core/core.plugin.js')(Chart);
 require('./core/core.element')(Chart);
@@ -19,9 +20,6 @@ require('./core/core.legend')(Chart);
 require('./core/core.interaction')(Chart);
 require('./core/core.tooltip')(Chart);
 
-// By default, we only load the browser platform.
-Chart.platform = require('./platforms/platform.dom')(Chart);
-
 require('./elements/element.arc')(Chart);
 require('./elements/element.line')(Chart);
 require('./elements/element.point')(Chart);
index e64cb3a9dde92f5e5b9874cd6e0179ea67f4d00d..616df547be77dab5e7b40cd3ca1af73a5f513f50 100644 (file)
@@ -3,6 +3,7 @@
 module.exports = function(Chart) {
 
        var helpers = Chart.helpers;
+       var platform = Chart.platform;
 
        // Create a dictionary of chart types, to allow for extension of existing types
        Chart.types = {};
@@ -63,7 +64,7 @@ module.exports = function(Chart) {
 
                config = initConfig(config);
 
-               var context = Chart.platform.acquireContext(item, config);
+               var context = platform.acquireContext(item, config);
                var canvas = context && context.canvas;
                var height = canvas && canvas.height;
                var width = canvas && canvas.width;
@@ -99,21 +100,6 @@ module.exports = function(Chart) {
                        return me;
                }
 
-               helpers.retinaScale(instance);
-
-               // Responsiveness is currently based on the use of an iframe, however this method causes
-               // performance issues and could be troublesome when used with ad blockers. So make sure
-               // that the user is still able to create a chart without iframe when responsive is false.
-               // See https://github.com/chartjs/Chart.js/issues/2210
-               if (me.options.responsive) {
-                       helpers.addResizeListener(canvas.parentNode, function() {
-                               me.resize();
-                       });
-
-                       // Initial resize before chart draws (must be silent to preserve initial animations).
-                       me.resize(true);
-               }
-
                me.initialize();
 
                return me;
@@ -126,8 +112,15 @@ module.exports = function(Chart) {
                        // Before init plugin notification
                        Chart.plugins.notify(me, 'beforeInit');
 
+                       helpers.retinaScale(me.chart);
+
                        me.bindEvents();
 
+                       if (me.options.responsive) {
+                               // Initial resize before chart draws (must be silent to preserve initial animations).
+                               me.resize(true);
+                       }
+
                        // Make sure controllers are built first so that each dataset is bound to an axis before the scales
                        // are built
                        me.ensureScalesHaveIDs();
@@ -559,10 +552,9 @@ module.exports = function(Chart) {
                        }
 
                        if (canvas) {
-                               helpers.unbindEvents(me, me.events);
-                               helpers.removeResizeListener(canvas.parentNode);
+                               me.unbindEvents();
                                helpers.clear(me.chart);
-                               Chart.platform.releaseContext(me.chart.ctx);
+                               platform.releaseContext(me.chart.ctx);
                                me.chart.canvas = null;
                                me.chart.ctx = null;
                        }
@@ -587,10 +579,48 @@ module.exports = function(Chart) {
                        me.tooltip.initialize();
                },
 
+               /**
+                * @private
+                */
                bindEvents: function() {
                        var me = this;
-                       helpers.bindEvents(me, me.options.events, function(evt) {
-                               me.eventHandler(evt);
+                       var listeners = me._listeners = {};
+                       var listener = function() {
+                               me.eventHandler.apply(me, arguments);
+                       };
+
+                       helpers.each(me.options.events, function(type) {
+                               platform.addEventListener(me, type, listener);
+                               listeners[type] = listener;
+                       });
+
+                       // Responsiveness is currently based on the use of an iframe, however this method causes
+                       // performance issues and could be troublesome when used with ad blockers. So make sure
+                       // that the user is still able to create a chart without iframe when responsive is false.
+                       // See https://github.com/chartjs/Chart.js/issues/2210
+                       if (me.options.responsive) {
+                               listener = function() {
+                                       me.resize();
+                               };
+
+                               platform.addEventListener(me, 'resize', listener);
+                               listeners.resize = listener;
+                       }
+               },
+
+               /**
+                * @private
+                */
+               unbindEvents: function() {
+                       var me = this;
+                       var listeners = me._listeners;
+                       if (!listeners) {
+                               return;
+                       }
+
+                       delete me._listeners;
+                       helpers.each(listeners, function(listener, type) {
+                               platform.removeEventListener(me, type, listener);
                        });
                },
 
@@ -606,6 +636,9 @@ module.exports = function(Chart) {
                        }
                },
 
+               /**
+                * @private
+                */
                eventHandler: function(e) {
                        var me = this;
                        var tooltip = me.tooltip;
@@ -615,12 +648,9 @@ module.exports = function(Chart) {
                        me._bufferedRender = true;
                        me._bufferedRequest = null;
 
-                       // Create platform agnostic chart event using platform specific code
-                       var chartEvent = Chart.platform.createEvent(e, me.chart);
-
-                       var changed = me.handleEvent(chartEvent);
-                       changed |= tooltip && tooltip.handleEvent(chartEvent);
-                       changed |= Chart.plugins.notify(me, 'onEvent', [chartEvent]);
+                       var changed = me.handleEvent(e);
+                       changed |= tooltip && tooltip.handleEvent(e);
+                       changed |= Chart.plugins.notify(me, 'onEvent', [e]);
 
                        var bufferedRequest = me._bufferedRequest;
                        if (bufferedRequest) {
@@ -644,7 +674,7 @@ module.exports = function(Chart) {
                /**
                 * Handle an event
                 * @private
-                * param e {Core.Event} the event to handle
+                * @param {IEvent} event the event to handle
                 * @return {Boolean} true if the chart needs to re-render
                 */
                handleEvent: function(e) {
index bfbcf23d5f5b2806ca77cc4c943e25894b19773f..f95af269f6bfb829fd59938c62259958fa025c75 100644 (file)
@@ -734,23 +734,6 @@ module.exports = function(Chart) {
                        node['on' + eventType] = helpers.noop;
                }
        };
-       helpers.bindEvents = function(chartInstance, arrayOfEvents, handler) {
-               // Create the events object if it's not already present
-               var events = chartInstance.events = chartInstance.events || {};
-
-               helpers.each(arrayOfEvents, function(eventName) {
-                       events[eventName] = function() {
-                               handler.apply(chartInstance, arguments);
-                       };
-                       helpers.addEvent(chartInstance.chart.canvas, eventName, events[eventName]);
-               });
-       };
-       helpers.unbindEvents = function(chartInstance, arrayOfEvents) {
-               var canvas = chartInstance.chart.canvas;
-               helpers.each(arrayOfEvents, function(handler, eventName) {
-                       helpers.removeEvent(canvas, eventName, handler);
-               });
-       };
 
        // Private helper function to convert max-width/max-height values that may be percentages into a number
        function parseMaxStyle(styleValue, node, parentProperty) {
@@ -941,73 +924,6 @@ module.exports = function(Chart) {
 
                return color(c);
        };
-       helpers.addResizeListener = function(node, callback) {
-               var iframe = document.createElement('iframe');
-               iframe.className = 'chartjs-hidden-iframe';
-               iframe.style.cssText =
-                       'display:block;'+
-                       'overflow:hidden;'+
-                       'border:0;'+
-                       'margin:0;'+
-                       'top:0;'+
-                       'left:0;'+
-                       'bottom:0;'+
-                       'right:0;'+
-                       'height:100%;'+
-                       'width:100%;'+
-                       'position:absolute;'+
-                       'pointer-events:none;'+
-                       'z-index:-1;';
-
-               // Prevent the iframe to gain focus on tab.
-               // https://github.com/chartjs/Chart.js/issues/3090
-               iframe.tabIndex = -1;
-
-               // Let's keep track of this added iframe and thus avoid DOM query when removing it.
-               var stub = node._chartjs = {
-                       resizer: iframe,
-                       ticking: false
-               };
-
-               // Throttle the callback notification until the next animation frame.
-               var notify = function() {
-                       if (!stub.ticking) {
-                               stub.ticking = true;
-                               helpers.requestAnimFrame.call(window, function() {
-                                       if (stub.resizer) {
-                                               stub.ticking = false;
-                                               return callback();
-                                       }
-                               });
-                       }
-               };
-
-               // If the iframe is re-attached to the DOM, the resize listener is removed because the
-               // content is reloaded, so make sure to install the handler after the iframe is loaded.
-               // https://github.com/chartjs/Chart.js/issues/3521
-               helpers.addEvent(iframe, 'load', function() {
-                       helpers.addEvent(iframe.contentWindow || iframe, 'resize', notify);
-
-                       // The iframe size might have changed while loading, which can also
-                       // happen if the size has been changed while detached from the DOM.
-                       notify();
-               });
-
-               node.insertBefore(iframe, node.firstChild);
-       };
-       helpers.removeResizeListener = function(node) {
-               if (!node || !node._chartjs) {
-                       return;
-               }
-
-               var iframe = node._chartjs.resizer;
-               if (iframe) {
-                       iframe.parentNode.removeChild(iframe);
-                       node._chartjs.resizer = null;
-               }
-
-               delete node._chartjs;
-       };
        helpers.isArray = Array.isArray?
                function(obj) {
                        return Array.isArray(obj);
index 0888fa9591ba30ac2e5b6c162c73f36053f4f0ac..03423c0d6042fb160a57f053d9a659935dcedc14 100644 (file)
@@ -5,8 +5,8 @@ module.exports = function(Chart) {
 
        /**
         * Helper function to get relative position for an event
-        * @param e {Event|Core.Event} the event to get the position for
-        * @param chart {chart} the chart
+        * @param {Event|IEvent} event - The event to get the position for
+        * @param {Chart} chart - The chart
         * @returns {Point} the event position
         */
        function getRelativePosition(e, chart) {
@@ -135,8 +135,8 @@ module.exports = function(Chart) {
         */
 
        /**
-        * @namespace Chart.Interaction
         * Contains interaction related functions
+        * @namespace Chart.Interaction
         */
        Chart.Interaction = {
                // Helper function for different modes
index 45f51d05923b7134e67b94431727b73d04f82bef..b80bdbe25ad74300a26f77534ec3b026778c470a 100644 (file)
@@ -439,7 +439,7 @@ module.exports = function(Chart) {
                /**
                 * Handle an event
                 * @private
-                * @param e {Core.Event} the event to handle
+                * @param {IEvent} event - The event to handle
                 * @return {Boolean} true if a change occured
                 */
                handleEvent: function(e) {
index c1ac7830739145e792340536c3a1066664a8c0a1..32589b652e6a0e818b13653bb1d9002ae23df9be 100755 (executable)
@@ -763,7 +763,7 @@ module.exports = function(Chart) {
                /**
                 * Handle an event
                 * @private
-                * @param e {Core.Event} the event to handle
+                * @param {IEvent} event - The event to handle
                 * @returns {Boolean} true if the tooltip changed
                 */
                handleEvent: function(e) {
index fb41f88259fe0f9c7ab16ce7b78a9dc7a1377a55..abfb3dee3e1b736b06a7f2f28d51eb8222662b9a 100644 (file)
@@ -1,57 +1,13 @@
 'use strict';
 
-/**
- * @interface IPlatform
- * Allows abstracting platform dependencies away from the chart
- */
-/**
- * Creates a chart.js event from a platform specific event
- * @method IPlatform#createEvent
- * @param e {Event} : the platform event to translate
- * @returns {Core.Event} chart.js event
- */
-/**
- * @method IPlatform#acquireContext
- * @param item {Object} the context or canvas to use
- * @param config {ChartOptions} the chart options
- * @returns {CanvasRenderingContext2D} a context2d instance implementing the w3c Canvas 2D context API standard.
- */
-/**
- * @method IPlatform#releaseContext
- * @param context {CanvasRenderingContext2D} the context to release. This is the item returned by @see {@link IPlatform#acquireContext}
- */
-
 // Chart.Platform implementation for targeting a web browser
 module.exports = function(Chart) {
        var helpers = Chart.helpers;
 
-       /*
-        * Key is the browser event type
-        * Chart.js internal events are:
-        *              mouseenter
-        *              mousedown
-        *              mousemove
-        *              mouseup
-        *              mouseout
-        *              click
-        *              dblclick
-        *              contextmenu
-        *              keydown
-        *              keypress
-        *              keyup
-        */
-       var typeMap = {
-               // Mouse events
-               mouseenter: 'mouseenter',
-               mousedown: 'mousedown',
-               mousemove: 'mousemove',
-               mouseup: 'mouseup',
-               mouseout: 'mouseout',
-               mouseleave: 'mouseout',
-               click: 'click',
-               dblclick: 'dblclick',
-               contextmenu: 'contextmenu',
-
+       // DOM event types -> Chart.js event types.
+       // Note: only events with different types are mapped.
+       // https://developer.mozilla.org/en-US/docs/Web/Events
+       var eventTypeMap = {
                // Touch events
                touchstart: 'mousedown',
                touchmove: 'mousemove',
@@ -63,12 +19,7 @@ module.exports = function(Chart) {
                pointermove: 'mousemove',
                pointerup: 'mouseup',
                pointerleave: 'mouseout',
-               pointerout: 'mouseout',
-
-               // Key events
-               keydown: 'keydown',
-               keypress: 'keypress',
-               keyup: 'keyup',
+               pointerout: 'mouseout'
        };
 
        /**
@@ -141,38 +92,97 @@ module.exports = function(Chart) {
                return canvas;
        }
 
-       return {
-               /**
-                * Creates a Chart.js event from a raw event
-                * @method BrowserPlatform#createEvent
-                * @implements IPlatform.createEvent
-                * @param e {Event} the raw event (such as a mouse event)
-                * @param chart {Chart} the chart to use
-                * @returns {Core.Event} the chart.js event for this event
-                */
-               createEvent: function(e, chart) {
-                       var relativePosition = helpers.getRelativePosition(e, chart);
-                       return {
-                               // allow access to the native event
-                               native: e,
-
-                               // our interal event type
-                               type: typeMap[e.type],
-
-                               // width and height of chart
-                               width: chart.width,
-                               height: chart.height,
-
-                               // Position relative to the canvas
-                               x: relativePosition.x,
-                               y: relativePosition.y
-                       };
-               },
+       function createEvent(type, chart, x, y, native) {
+               return {
+                       type: type,
+                       chart: chart,
+                       native: native || null,
+                       x: x !== undefined? x : null,
+                       y: y !== undefined? y : null,
+               };
+       }
+
+       function fromNativeEvent(event, chart) {
+               var type = eventTypeMap[event.type] || event.type;
+               var pos = helpers.getRelativePosition(event, chart);
+               return createEvent(type, chart, pos.x, pos.y, event);
+       }
+
+       function createResizer(handler) {
+               var iframe = document.createElement('iframe');
+               iframe.className = 'chartjs-hidden-iframe';
+               iframe.style.cssText =
+                       'display:block;'+
+                       'overflow:hidden;'+
+                       'border:0;'+
+                       'margin:0;'+
+                       'top:0;'+
+                       'left:0;'+
+                       'bottom:0;'+
+                       'right:0;'+
+                       'height:100%;'+
+                       'width:100%;'+
+                       'position:absolute;'+
+                       'pointer-events:none;'+
+                       'z-index:-1;';
 
-               /**
-                * @method BrowserPlatform#acquireContext
-                * @implements IPlatform#acquireContext
-                */
+               // Prevent the iframe to gain focus on tab.
+               // https://github.com/chartjs/Chart.js/issues/3090
+               iframe.tabIndex = -1;
+
+               // If the iframe is re-attached to the DOM, the resize listener is removed because the
+               // content is reloaded, so make sure to install the handler after the iframe is loaded.
+               // https://github.com/chartjs/Chart.js/issues/3521
+               helpers.addEvent(iframe, 'load', function() {
+                       helpers.addEvent(iframe.contentWindow || iframe, 'resize', handler);
+
+                       // The iframe size might have changed while loading, which can also
+                       // happen if the size has been changed while detached from the DOM.
+                       handler();
+               });
+
+               return iframe;
+       }
+
+       function addResizeListener(node, listener, chart) {
+               var stub = node._chartjs = {
+                       ticking: false
+               };
+
+               // Throttle the callback notification until the next animation frame.
+               var notify = function() {
+                       if (!stub.ticking) {
+                               stub.ticking = true;
+                               helpers.requestAnimFrame.call(window, function() {
+                                       if (stub.resizer) {
+                                               stub.ticking = false;
+                                               return listener(createEvent('resize', chart));
+                                       }
+                               });
+                       }
+               };
+
+               // Let's keep track of this added iframe and thus avoid DOM query when removing it.
+               stub.resizer = createResizer(notify);
+
+               node.insertBefore(stub.resizer, node.firstChild);
+       }
+
+       function removeResizeListener(node) {
+               if (!node || !node._chartjs) {
+                       return;
+               }
+
+               var resizer = node._chartjs.resizer;
+               if (resizer) {
+                       resizer.parentNode.removeChild(resizer);
+                       node._chartjs.resizer = null;
+               }
+
+               delete node._chartjs;
+       }
+
+       return {
                acquireContext: function(item, config) {
                        if (typeof item === 'string') {
                                item = document.getElementById(item);
@@ -200,11 +210,6 @@ module.exports = function(Chart) {
                        return null;
                },
 
-               /**
-                * Restores the canvas initial state, such as render/display sizes and style.
-                * @method BrowserPlatform#releaseContext
-                * @implements IPlatform#releaseContext
-                */
                releaseContext: function(context) {
                        var canvas = context.canvas;
                        if (!canvas._chartjs) {
@@ -232,6 +237,41 @@ module.exports = function(Chart) {
                        canvas.width = canvas.width;
 
                        delete canvas._chartjs;
+               },
+
+               addEventListener: function(chart, type, listener) {
+                       var canvas = chart.chart.canvas;
+                       if (type === 'resize') {
+                               // Note: the resize event is not supported on all browsers.
+                               addResizeListener(canvas.parentNode, listener, chart.chart);
+                               return;
+                       }
+
+                       var stub = listener._chartjs || (listener._chartjs = {});
+                       var proxies = stub.proxies || (stub.proxies = {});
+                       var proxy = proxies[chart.id + '_' + type] = function(event) {
+                               listener(fromNativeEvent(event, chart.chart));
+                       };
+
+                       helpers.addEvent(canvas, type, proxy);
+               },
+
+               removeEventListener: function(chart, type, listener) {
+                       var canvas = chart.chart.canvas;
+                       if (type === 'resize') {
+                               // Note: the resize event is not supported on all browsers.
+                               removeResizeListener(canvas.parentNode, listener);
+                               return;
+                       }
+
+                       var stub = listener._chartjs || {};
+                       var proxies = stub.proxies || {};
+                       var proxy = proxies[chart.id + '_' + type];
+                       if (!proxy) {
+                               return;
+                       }
+
+                       helpers.removeEvent(canvas, type, proxy);
                }
        };
 };
diff --git a/src/platforms/platform.js b/src/platforms/platform.js
new file mode 100644 (file)
index 0000000..0f27e58
--- /dev/null
@@ -0,0 +1,69 @@
+'use strict';
+
+// By default, select the browser (DOM) platform.
+// @TODO Make possible to select another platform at build time.
+var implementation = require('./platform.dom.js');
+
+module.exports = function(Chart) {
+       /**
+        * @namespace Chart.platform
+        * @see https://chartjs.gitbooks.io/proposals/content/Platform.html
+        * @since 2.4.0
+        */
+       Chart.platform = {
+               /**
+                * Called at chart construction time, returns a context2d instance implementing
+                * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}.
+                * @param {*} item - The native item from which to acquire context (platform specific)
+                * @param {Object} options - The chart options
+                * @returns {CanvasRenderingContext2D} context2d instance
+                */
+               acquireContext: function() {},
+
+               /**
+                * Called at chart destruction time, releases any resources associated to the context
+                * previously returned by the acquireContext() method.
+                * @param {CanvasRenderingContext2D} context - The context2d instance
+                * @returns {Boolean} true if the method succeeded, else false
+                */
+               releaseContext: function() {},
+
+               /**
+                * Registers the specified listener on the given chart.
+                * @param {Chart} chart - Chart from which to listen for event
+                * @param {String} type - The ({@link IEvent}) type to listen for
+                * @param {Function} listener - Receives a notification (an object that implements
+                * the {@link IEvent} interface) when an event of the specified type occurs.
+                */
+               addEventListener: function() {},
+
+               /**
+                * Removes the specified listener previously registered with addEventListener.
+                * @param {Chart} chart -Chart from which to remove the listener
+                * @param {String} type - The ({@link IEvent}) type to remove
+                * @param {Function} listener - The listener function to remove from the event target.
+                */
+               removeEventListener: function() {}
+       };
+
+       /**
+        * @interface IPlatform
+        * Allows abstracting platform dependencies away from the chart
+        * @borrows Chart.platform.acquireContext as acquireContext
+        * @borrows Chart.platform.releaseContext as releaseContext
+        * @borrows Chart.platform.addEventListener as addEventListener
+        * @borrows Chart.platform.removeEventListener as removeEventListener
+        */
+
+       /**
+        * @interface IEvent
+        * @prop {String} type - The event type name, possible values are:
+        * 'contextmenu', 'mouseenter', 'mousedown', 'mousemove', 'mouseup', 'mouseout',
+        * 'click', 'dblclick', 'keydown', 'keypress', 'keyup' and 'resize'
+        * @prop {*} native - The original native event (null for emulated events, e.g. 'resize')
+        * @prop {Number} x - The mouse x position, relative to the canvas (null for incompatible events)
+        * @prop {Number} y - The mouse y position, relative to the canvas (null for incompatible events)
+        */
+
+       Chart.helpers.extend(Chart.platform, implementation(Chart));
+};
index f20b6e1ff68c6032287b59a67f55dd83db13f756..19c5bbe1aabd64df0116e5baa3d035880865fb2f 100644 (file)
@@ -361,10 +361,6 @@ describe('Platform.dom', function() {
                        // Is type correctly translated
                        expect(notifiedEvent.type).toBe(evt.type);
 
-                       // Canvas width and height
-                       expect(notifiedEvent.width).toBe(chart.chart.width);
-                       expect(notifiedEvent.height).toBe(chart.chart.height);
-
                        // Relative Position
                        expect(notifiedEvent.x).toBe(chart.chart.width / 2);
                        expect(notifiedEvent.y).toBe(chart.chart.height / 2);