From: Simon Brunel Date: Sat, 14 Jan 2017 13:38:56 +0000 (+0100) Subject: Platform event API abstraction X-Git-Tag: v2.5.0~1^2~11 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=312773ba7b7e2acf6cd1273c92017d3c197a992b;p=thirdparty%2FChart.js.git Platform event API abstraction 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). --- diff --git a/src/chart.js b/src/chart.js index 186d07a42..77d2bb636 100644 --- a/src/chart.js +++ b/src/chart.js @@ -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); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e64cb3a9d..616df547b 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -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) { diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index bfbcf23d5..f95af269f 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -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); diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 0888fa959..03423c0d6 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -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 diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 45f51d059..b80bdbe25 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -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) { diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index c1ac78307..32589b652 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -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) { diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index fb41f8825..abfb3dee3 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -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 index 000000000..0f27e5868 --- /dev/null +++ b/src/platforms/platform.js @@ -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)); +}; diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js index f20b6e1ff..19c5bbe1a 100644 --- a/test/platform.dom.tests.js +++ b/test/platform.dom.tests.js @@ -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);