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).
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);
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);
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 = {};
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;
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;
// 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();
}
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;
}
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);
});
},
}
},
+ /**
+ * @private
+ */
eventHandler: function(e) {
var me = this;
var tooltip = me.tooltip;
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) {
/**
* 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) {
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) {
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);
/**
* 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) {
*/
/**
- * @namespace Chart.Interaction
* Contains interaction related functions
+ * @namespace Chart.Interaction
*/
Chart.Interaction = {
// Helper function for different modes
/**
* 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) {
/**
* 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) {
'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',
pointermove: 'mousemove',
pointerup: 'mouseup',
pointerleave: 'mouseout',
- pointerout: 'mouseout',
-
- // Key events
- keydown: 'keydown',
- keypress: 'keypress',
- keyup: 'keyup',
+ pointerout: 'mouseout'
};
/**
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);
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) {
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);
}
};
};
--- /dev/null
+'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));
+};
// 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);