Add support for local plugins and plugin options

Plugins can now be declared in the chart `config.plugins` array and will only be applied to the associated chart(s), after the globally registered plugins. Plugin specific options are now scoped under the `config.options.plugins` options. Hooks now receive the chart instance as first argument and the plugin options as last argument.
This commit is contained in:
Simon Brunel 2016-11-03 22:40:47 +01:00 committed by Evert Timberg
parent e249de7162
commit 3187a788e1
5 changed files with 365 additions and 78 deletions

View File

@ -5,13 +5,13 @@ var Chart = require('./core/core.js')();
require('./core/core.helpers')(Chart);
require('./core/core.canvasHelpers')(Chart);
require('./core/core.plugin.js')(Chart);
require('./core/core.element')(Chart);
require('./core/core.animation')(Chart);
require('./core/core.controller')(Chart);
require('./core/core.datasetController')(Chart);
require('./core/core.layoutService')(Chart);
require('./core/core.scaleService')(Chart);
require('./core/core.plugin.js')(Chart);
require('./core/core.ticks.js')(Chart);
require('./core/core.scale')(Chart);
require('./core/core.title')(Chart);

View File

@ -258,7 +258,7 @@ module.exports = function(Chart) {
var me = this;
// Before init plugin notification
Chart.plugins.notify('beforeInit', [me]);
Chart.plugins.notify(me, 'beforeInit');
me.bindEvents();
@ -273,7 +273,7 @@ module.exports = function(Chart) {
me.update();
// After init plugin notification
Chart.plugins.notify('afterInit', [me]);
Chart.plugins.notify(me, 'afterInit');
return me;
},
@ -315,7 +315,7 @@ module.exports = function(Chart) {
if (!silent) {
// Notify any plugins about the resize
var newSize = {width: newWidth, height: newHeight};
Chart.plugins.notify('resize', [me, newSize]);
Chart.plugins.notify(me, 'resize', [newSize]);
// Notify of resize
if (me.options.onResize) {
@ -460,7 +460,7 @@ module.exports = function(Chart) {
var me = this;
updateConfig(me);
Chart.plugins.notify('beforeUpdate', [me]);
Chart.plugins.notify(me, 'beforeUpdate');
// In case the entire data object changed
me.tooltip._data = me.data;
@ -476,7 +476,7 @@ module.exports = function(Chart) {
Chart.layoutService.update(me, me.chart.width, me.chart.height);
// Apply changes to the datasets that require the scales to have been calculated i.e BorderColor changes
Chart.plugins.notify('afterScaleUpdate', [me]);
Chart.plugins.notify(me, 'afterScaleUpdate');
// Can only reset the new controllers after the scales have been updated
helpers.each(newControllers, function(controller) {
@ -486,7 +486,7 @@ module.exports = function(Chart) {
me.updateDatasets();
// Do this before render so that any plugins that need final scale updates can use it
Chart.plugins.notify('afterUpdate', [me]);
Chart.plugins.notify(me, 'afterUpdate');
if (me._bufferedRender) {
me._bufferedRequest = {
@ -530,18 +530,18 @@ module.exports = function(Chart) {
var me = this;
var i, ilen;
if (Chart.plugins.notify('beforeDatasetsUpdate', [me])) {
if (Chart.plugins.notify(me, 'beforeDatasetsUpdate')) {
for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) {
me.getDatasetMeta(i).controller.update();
}
Chart.plugins.notify('afterDatasetsUpdate', [me]);
Chart.plugins.notify(me, 'afterDatasetsUpdate');
}
},
render: function(duration, lazy) {
var me = this;
Chart.plugins.notify('beforeRender', [me]);
Chart.plugins.notify(me, 'beforeRender');
var animationOptions = me.options.animation;
if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) {
@ -577,7 +577,7 @@ module.exports = function(Chart) {
var easingDecimal = ease || 1;
me.clear();
Chart.plugins.notify('beforeDraw', [me, easingDecimal]);
Chart.plugins.notify(me, 'beforeDraw', [easingDecimal]);
// Draw all the scales
helpers.each(me.boxes, function(box) {
@ -587,7 +587,7 @@ module.exports = function(Chart) {
me.scale.draw();
}
Chart.plugins.notify('beforeDatasetsDraw', [me, easingDecimal]);
Chart.plugins.notify(me, 'beforeDatasetsDraw', [easingDecimal]);
// Draw each dataset via its respective controller (reversed to support proper line stacking)
helpers.each(me.data.datasets, function(dataset, datasetIndex) {
@ -596,12 +596,12 @@ module.exports = function(Chart) {
}
}, me, true);
Chart.plugins.notify('afterDatasetsDraw', [me, easingDecimal]);
Chart.plugins.notify(me, 'afterDatasetsDraw', [easingDecimal]);
// Finally draw the tooltip
me.tooltip.transition(easingDecimal).draw();
Chart.plugins.notify('afterDraw', [me, easingDecimal]);
Chart.plugins.notify(me, 'afterDraw', [easingDecimal]);
},
// Get the single element that was clicked on
@ -701,7 +701,7 @@ module.exports = function(Chart) {
me.chart.ctx = null;
}
Chart.plugins.notify('destroy', [me]);
Chart.plugins.notify(me, 'destroy');
delete Chart.instances[me.id];
},

View File

@ -2,7 +2,10 @@
module.exports = function(Chart) {
var noop = Chart.helpers.noop;
var helpers = Chart.helpers;
var noop = helpers.noop;
Chart.defaults.global.plugins = {};
/**
* The plugin service singleton
@ -10,8 +13,20 @@ module.exports = function(Chart) {
* @since 2.1.0
*/
Chart.plugins = {
/**
* Globally registered plugins.
* @private
*/
_plugins: [],
/**
* This identifier is used to invalidate the descriptors cache attached to each chart
* when a global plugin is registered or unregistered. In this case, the cache ID is
* incremented and descriptors are regenerated during following API calls.
* @private
*/
_cacheId: 0,
/**
* Registers the given plugin(s) if not already registered.
* @param {Array|Object} plugins plugin instance(s).
@ -23,6 +38,8 @@ module.exports = function(Chart) {
p.push(plugin);
}
});
this._cacheId++;
},
/**
@ -37,6 +54,8 @@ module.exports = function(Chart) {
p.splice(idx, 1);
}
});
this._cacheId++;
},
/**
@ -45,6 +64,7 @@ module.exports = function(Chart) {
*/
clear: function() {
this._plugins = [];
this._cacheId++;
},
/**
@ -66,28 +86,78 @@ module.exports = function(Chart) {
},
/**
* Calls registered plugins on the specified extension, with the given args. This
* method immediately returns as soon as a plugin explicitly returns false. The
* Calls enabled plugins for chart, on the specified extension and with the given args.
* This method immediately returns as soon as a plugin explicitly returns false. The
* returned value can be used, for instance, to interrupt the current action.
* @param {Object} chart chart instance for which plugins should be called.
* @param {String} extension the name of the plugin method to call (e.g. 'beforeUpdate').
* @param {Array} [args] extra arguments to apply to the extension call.
* @returns {Boolean} false if any of the plugins return false, else returns true.
*/
notify: function(extension, args) {
var plugins = this._plugins;
var ilen = plugins.length;
var i, plugin;
notify: function(chart, extension, args) {
var descriptors = this.descriptors(chart);
var ilen = descriptors.length;
var i, descriptor, plugin, params, method;
for (i=0; i<ilen; ++i) {
plugin = plugins[i];
if (typeof plugin[extension] === 'function') {
if (plugin[extension].apply(plugin, args || []) === false) {
descriptor = descriptors[i];
plugin = descriptor.plugin;
method = plugin[extension];
if (typeof method === 'function') {
params = [chart].concat(args || []);
params.push(descriptor.options);
if (method.apply(plugin, params) === false) {
return false;
}
}
}
return true;
},
/**
* Returns descriptors of enabled plugins for the given chart.
* @returns {Array} [{ plugin, options }]
* @private
*/
descriptors: function(chart) {
var cache = chart._plugins || (chart._plugins = {});
if (cache.id === this._cacheId) {
return cache.descriptors;
}
var plugins = [];
var descriptors = [];
var config = (chart && chart.config) || {};
var defaults = Chart.defaults.global.plugins;
var options = (config.options && config.options.plugins) || {};
this._plugins.concat(config.plugins || []).forEach(function(plugin) {
var idx = plugins.indexOf(plugin);
if (idx !== -1) {
return;
}
var id = plugin.id;
var opts = options[id];
if (opts === false) {
return;
}
if (opts === true) {
opts = helpers.clone(defaults[id]);
}
plugins.push(plugin);
descriptors.push({
plugin: plugin,
options: opts || {}
});
});
cache.descriptors = descriptors;
cache.id = this._cacheId;
return descriptors;
}
};
@ -96,7 +166,7 @@ module.exports = function(Chart) {
* @interface Chart.PluginBase
* @since 2.1.0
*/
Chart.PluginBase = Chart.Element.extend({
Chart.PluginBase = helpers.inherits({
// Called at start of chart init
beforeInit: noop,
@ -123,7 +193,7 @@ module.exports = function(Chart) {
* Provided for backward compatibility, use Chart.plugins instead
* @namespace Chart.pluginService
* @deprecated since version 2.1.5
* @todo remove me at version 3
* TODO remove me at version 3
*/
Chart.pluginService = Chart.plugins;
};

View File

@ -1,18 +1,15 @@
describe('Chart.plugins', function() {
var oldPlugins;
beforeAll(function() {
oldPlugins = Chart.plugins.getAll();
});
afterAll(function() {
Chart.plugins.register(oldPlugins);
});
beforeEach(function() {
this._plugins = Chart.plugins.getAll();
Chart.plugins.clear();
});
afterEach(function() {
Chart.plugins.clear();
Chart.plugins.register(this._plugins);
delete this._plugins;
});
describe('Chart.plugins.register', function() {
it('should register a plugin', function() {
Chart.plugins.register({});
@ -66,63 +63,282 @@ describe('Chart.plugins', function() {
});
describe('Chart.plugins.notify', function() {
it('should call plugins with arguments', function() {
var myplugin = {
count: 0,
trigger: function(chart) {
myplugin.count = chart.count;
}
};
it('should call inline plugins with arguments', function() {
var plugin = {hook: function() {}};
var chart = window.acquireChart({
plugins: [plugin]
});
Chart.plugins.register(myplugin);
Chart.plugins.notify('trigger', [{count: 10}]);
expect(myplugin.count).toBe(10);
spyOn(plugin, 'hook');
Chart.plugins.notify(chart, 'hook', 42);
expect(plugin.hook.calls.count()).toBe(1);
expect(plugin.hook.calls.first().args[0]).toBe(chart);
expect(plugin.hook.calls.first().args[1]).toBe(42);
expect(plugin.hook.calls.first().args[2]).toEqual({});
});
it('should call global plugins with arguments', function() {
var plugin = {hook: function() {}};
var chart = window.acquireChart({});
spyOn(plugin, 'hook');
Chart.plugins.register(plugin);
Chart.plugins.notify(chart, 'hook', 42);
expect(plugin.hook.calls.count()).toBe(1);
expect(plugin.hook.calls.first().args[0]).toBe(chart);
expect(plugin.hook.calls.first().args[1]).toBe(42);
expect(plugin.hook.calls.first().args[2]).toEqual({});
});
it('should call plugin only once even if registered multiple times', function() {
var plugin = {hook: function() {}};
var chart = window.acquireChart({
plugins: [plugin, plugin]
});
spyOn(plugin, 'hook');
Chart.plugins.register([plugin, plugin]);
Chart.plugins.notify(chart, 'hook');
expect(plugin.hook.calls.count()).toBe(1);
});
it('should call plugins in the correct order (global first)', function() {
var results = [];
var chart = window.acquireChart({
plugins: [{
hook: function() {
results.push(1);
}
}, {
hook: function() {
results.push(2);
}
}, {
hook: function() {
results.push(3);
}
}]
});
Chart.plugins.register([{
hook: function() {
results.push(4);
}
}, {
hook: function() {
results.push(5);
}
}, {
hook: function() {
results.push(6);
}
}]);
var ret = Chart.plugins.notify(chart, 'hook');
expect(ret).toBeTruthy();
expect(results).toEqual([4, 5, 6, 1, 2, 3]);
});
it('should return TRUE if no plugin explicitly returns FALSE', function() {
Chart.plugins.register({
check: function() {}
var chart = window.acquireChart({
plugins: [{
hook: function() {}
}, {
hook: function() {
return null;
}
}, {
hook: function() {
return 0;
}
}, {
hook: function() {
return true;
}
}, {
hook: function() {
return 1;
}
}]
});
Chart.plugins.register({
check: function() {
return;
}
var plugins = chart.config.plugins;
plugins.forEach(function(plugin) {
spyOn(plugin, 'hook').and.callThrough();
});
Chart.plugins.register({
check: function() {
return null;
}
var ret = Chart.plugins.notify(chart, 'hook');
expect(ret).toBeTruthy();
plugins.forEach(function(plugin) {
expect(plugin.hook).toHaveBeenCalled();
});
Chart.plugins.register({
check: function() {
return 42;
}
});
var res = Chart.plugins.notify('check');
expect(res).toBeTruthy();
});
it('should return FALSE if no plugin explicitly returns FALSE', function() {
Chart.plugins.register({
check: function() {}
it('should return FALSE if any plugin explicitly returns FALSE', function() {
var chart = window.acquireChart({
plugins: [{
hook: function() {}
}, {
hook: function() {
return null;
}
}, {
hook: function() {
return false;
}
}, {
hook: function() {
return 42;
}
}, {
hook: function() {
return 'bar';
}
}]
});
Chart.plugins.register({
check: function() {
return;
var plugins = chart.config.plugins;
plugins.forEach(function(plugin) {
spyOn(plugin, 'hook').and.callThrough();
});
var ret = Chart.plugins.notify(chart, 'hook');
expect(ret).toBeFalsy();
expect(plugins[0].hook).toHaveBeenCalled();
expect(plugins[1].hook).toHaveBeenCalled();
expect(plugins[2].hook).toHaveBeenCalled();
expect(plugins[3].hook).not.toHaveBeenCalled();
expect(plugins[4].hook).not.toHaveBeenCalled();
});
});
describe('config.options.plugins', function() {
it('should call plugins with options at last argument', function() {
var plugin = {id: 'foo', hook: function() {}};
var chart = window.acquireChart({
options: {
plugins: {
foo: {a: '123'},
}
}
});
Chart.plugins.register({
check: function() {
return false;
spyOn(plugin, 'hook');
Chart.plugins.register(plugin);
Chart.plugins.notify(chart, 'hook');
Chart.plugins.notify(chart, 'hook', ['bla']);
Chart.plugins.notify(chart, 'hook', ['bla', 42]);
expect(plugin.hook.calls.count()).toBe(3);
expect(plugin.hook.calls.argsFor(0)[1]).toEqual({a: '123'});
expect(plugin.hook.calls.argsFor(1)[2]).toEqual({a: '123'});
expect(plugin.hook.calls.argsFor(2)[3]).toEqual({a: '123'});
});
it('should call plugins with options associated to their identifier', function() {
var plugins = {
a: {id: 'a', hook: function() {}},
b: {id: 'b', hook: function() {}},
c: {id: 'c', hook: function() {}}
};
Chart.plugins.register(plugins.a);
var chart = window.acquireChart({
plugins: [plugins.b, plugins.c],
options: {
plugins: {
a: {a: '123'},
b: {b: '456'},
c: {c: '789'}
}
}
});
Chart.plugins.register({
check: function() {
return 42;
spyOn(plugins.a, 'hook');
spyOn(plugins.b, 'hook');
spyOn(plugins.c, 'hook');
Chart.plugins.notify(chart, 'hook');
expect(plugins.a.hook).toHaveBeenCalled();
expect(plugins.b.hook).toHaveBeenCalled();
expect(plugins.c.hook).toHaveBeenCalled();
expect(plugins.a.hook.calls.first().args[1]).toEqual({a: '123'});
expect(plugins.b.hook.calls.first().args[1]).toEqual({b: '456'});
expect(plugins.c.hook.calls.first().args[1]).toEqual({c: '789'});
});
it('should not called plugins when config.options.plugins.{id} is FALSE', function() {
var plugins = {
a: {id: 'a', hook: function() {}},
b: {id: 'b', hook: function() {}},
c: {id: 'c', hook: function() {}}
};
Chart.plugins.register(plugins.a);
var chart = window.acquireChart({
plugins: [plugins.b, plugins.c],
options: {
plugins: {
a: false,
b: false
}
}
});
var res = Chart.plugins.notify('check');
expect(res).toBeFalsy();
spyOn(plugins.a, 'hook');
spyOn(plugins.b, 'hook');
spyOn(plugins.c, 'hook');
Chart.plugins.notify(chart, 'hook');
expect(plugins.a.hook).not.toHaveBeenCalled();
expect(plugins.b.hook).not.toHaveBeenCalled();
expect(plugins.c.hook).toHaveBeenCalled();
});
it('should call plugins with default options when plugin options is TRUE', function() {
var plugin = {id: 'a', hook: function() {}};
Chart.defaults.global.plugins.a = {a: 42};
Chart.plugins.register(plugin);
var chart = window.acquireChart({
options: {
plugins: {
a: true
}
}
});
spyOn(plugin, 'hook');
Chart.plugins.notify(chart, 'hook');
expect(plugin.hook).toHaveBeenCalled();
expect(plugin.hook.calls.first().args[1]).toEqual({a: 42});
});
it('should call plugins with default options if plugin config options is undefined', function() {
var plugin = {id: 'a', hook: function() {}};
Chart.defaults.global.plugins.a = {a: 'foobar'};
Chart.plugins.register(plugin);
spyOn(plugin, 'hook');
var chart = window.acquireChart();
Chart.plugins.notify(chart, 'hook');
expect(plugin.hook).toHaveBeenCalled();
expect(plugin.hook.calls.first().args[1]).toEqual({a: 'foobar'});
});
});
});

View File

@ -274,6 +274,7 @@
var canvas = document.createElement('canvas');
var chart, key;
config = config || {};
options = options || {};
options.canvas = options.canvas || {height: 512, width: 512};
options.wrapper = options.wrapper || {class: 'chartjs-wrapper'};