]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add support for local plugins and plugin options
authorSimon Brunel <simonbrunel@users.noreply.github.com>
Thu, 3 Nov 2016 21:40:47 +0000 (22:40 +0100)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sun, 18 Dec 2016 13:46:15 +0000 (08:46 -0500)
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.

src/chart.js
src/core/core.controller.js
src/core/core.plugin.js
test/core.plugin.tests.js
test/mockContext.js

index 2c5e628264c5d7f0c76ab390c16bb70e3c8f9220..7c490e7eabcc8820250cc82917933a38d7569be0 100644 (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);
index 61f99b0eb569734bd8dc9185b711c82bc78e7e78..4e28773880edee9593afde8bc3d1737b16f1af66 100644 (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];
                },
index fe6ac31f170e9d2113266db711bca78208398b19..657c85a35455fd0622c9e65bd993ef795e7cc6e3 100644 (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;
 };
index 59a71b1c55e2514127b59e49adf6a716d22de88d..387d78808c72069cc942aaae38f3b111389124c2 100644 (file)
@@ -1,16 +1,13 @@
 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();
        });
 
-       beforeEach(function() {
+       afterEach(function() {
                Chart.plugins.clear();
+               Chart.plugins.register(this._plugins);
+               delete this._plugins;
        });
 
        describe('Chart.plugins.register', function() {
@@ -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]
+                       });
+
+                       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);
+                               }
+                       }]);
 
-                       Chart.plugins.register(myplugin);
-                       Chart.plugins.notify('trigger', [{count: 10}]);
-                       expect(myplugin.count).toBe(10);
+                       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;
-                               }
+               });
+
+               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';
+                                       }
+                               }]
                        });
-                       var res = Chart.plugins.notify('check');
-                       expect(res).toBeTruthy();
+
+                       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();
                });
+       });
 
-               it('should return FALSE if no plugin explicitly returns FALSE', function() {
-                       Chart.plugins.register({
-                               check: function() {}
+       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;
+
+                       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 false;
+
+                       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
+                                       }
                                }
                        });
-                       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).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
+                                       }
                                }
                        });
-                       var res = Chart.plugins.notify('check');
-                       expect(res).toBeFalsy();
+
+                       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'});
                });
        });
 });
index d065f2a7ad95e37302394d36ee0f7801d867afb5..0d12384778d2f5e6978a54316e9dbab284b8d359 100644 (file)
                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'};