import defaults from './core.defaults';
import registry from './core.registry';
-import {mergeIf, valueOrDefault} from '../helpers/helpers.core';
+import {callback as callCallback, mergeIf, valueOrDefault} from '../helpers/helpers.core';
/**
* @typedef { import("./core.controller").default } Chart
*/
export default class PluginService {
+ constructor() {
+ this._init = [];
+ }
+
/**
* Calls enabled plugins for `chart` on the specified hook and with the given args.
* This method immediately returns as soon as a plugin explicitly returns false. The
* @returns {boolean} false if any of the plugins return false, else returns true.
*/
notify(chart, hook, args) {
- args = args || {};
- const descriptors = this._descriptors(chart);
+ const me = this;
- for (let i = 0; i < descriptors.length; ++i) {
- const descriptor = descriptors[i];
+ if (hook === 'beforeInit') {
+ me._init = me._createDescriptors(chart, true);
+ me._notify(me._init, chart, 'install');
+ }
+
+ const descriptors = me._descriptors(chart);
+ const result = me._notify(descriptors, chart, hook, args);
+
+ if (hook === 'destroy') {
+ me._notify(descriptors, chart, 'stop');
+ me._notify(me._init, chart, 'uninstall');
+ }
+ return result;
+ }
+
+ /**
+ * @private
+ */
+ _notify(descriptors, chart, hook, args) {
+ args = args || {};
+ for (const descriptor of descriptors) {
const plugin = descriptor.plugin;
const method = plugin[hook];
- if (typeof method === 'function') {
- const params = [chart, args, descriptor.options];
- if (method.apply(plugin, params) === false) {
- return false;
- }
+ const params = [chart, args, descriptor.options];
+ if (callCallback(method, params, plugin) === false) {
+ return false;
}
}
}
invalidate() {
+ this._oldCache = this._cache;
this._cache = undefined;
}
return this._cache;
}
+ const descriptors = this._cache = this._createDescriptors(chart);
+
+ this._notifyStateChanges(chart);
+
+ return descriptors;
+ }
+
+ _createDescriptors(chart, all) {
const config = chart && chart.config;
const options = valueOrDefault(config.options && config.options.plugins, {});
const plugins = allPlugins(config);
// options === false => all plugins are disabled
- const descriptors = options === false ? [] : createDescriptors(plugins, options);
-
- this._cache = descriptors;
+ return options === false && !all ? [] : createDescriptors(plugins, options, all);
+ }
- return descriptors;
+ /**
+ * @param {Chart} chart
+ * @private
+ */
+ _notifyStateChanges(chart) {
+ const previousDescriptors = this._oldCache || [];
+ const descriptors = this._cache;
+ const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id));
+ this._notify(diff(previousDescriptors, descriptors), chart, 'stop');
+ this._notify(diff(descriptors, previousDescriptors), chart, 'start');
}
}
return plugins;
}
-function createDescriptors(plugins, options) {
+function getOpts(options, all) {
+ if (!all && options === false) {
+ return null;
+ }
+ if (options === true) {
+ return {};
+ }
+ return options;
+}
+
+function createDescriptors(plugins, options, all) {
const result = [];
for (let i = 0; i < plugins.length; i++) {
const plugin = plugins[i];
const id = plugin.id;
-
- let opts = options[id];
- if (opts === false) {
+ const opts = getOpts(options[id], all);
+ if (opts === null) {
continue;
}
- if (opts === true) {
- opts = {};
- }
result.push({
plugin,
options: mergeIf({}, [opts, defaults.plugins[id]])
* @typedef {object} IPlugin
* @since 2.1.0
*/
+/**
+ * @method IPlugin#install
+ * @desc Called when plugin is installed for this chart instance. This hook is called on disabled plugins (options === false).
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
+/**
+ * @method IPlugin#start
+ * @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
+/**
+ * @method IPlugin#stop
+ * @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
/**
* @method IPlugin#beforeInit
* @desc Called before initializing `chart`.
*/
/**
* @method IPlugin#destroy
- * @desc Called after the chart as been destroyed.
+ * @desc Called after the chart has been destroyed.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ */
+/**
+ * @method IPlugin#uninstall
+ * @desc Called after chart is destroyed on all plugins that were installed for that chart. This hook is called on disabled plugins (options === false).
* @param {Chart} chart - The chart instance.
* @param {object} args - The call arguments.
* @param {object} options - The plugin options.
+ * @since 3.0.0
*/
});
describe('plugin.extensions', function() {
+ var hooks = {
+ install: ['install'],
+ uninstall: ['uninstall'],
+ init: [
+ 'beforeInit',
+ 'resize',
+ 'afterInit'
+ ],
+ start: ['start'],
+ stop: ['stop'],
+ update: [
+ 'beforeUpdate',
+ 'beforeLayout',
+ 'afterLayout',
+ 'beforeDatasetsUpdate',
+ 'beforeDatasetUpdate',
+ 'afterDatasetUpdate',
+ 'afterDatasetsUpdate',
+ 'afterUpdate',
+ ],
+ render: [
+ 'beforeRender',
+ 'beforeDraw',
+ 'beforeDatasetsDraw',
+ 'beforeDatasetDraw',
+ 'afterDatasetDraw',
+ 'afterDatasetsDraw',
+ 'beforeTooltipDraw',
+ 'afterTooltipDraw',
+ 'afterDraw',
+ 'afterRender',
+ ],
+ resize: [
+ 'resize'
+ ],
+ destroy: [
+ 'destroy'
+ ]
+ };
+
it ('should notify plugin in correct order', function(done) {
var plugin = this.plugin = {};
var sequence = [];
- var hooks = {
- init: [
- 'beforeInit',
- 'resize',
- 'afterInit'
- ],
- update: [
- 'beforeUpdate',
- 'beforeLayout',
- 'afterLayout',
- 'beforeDatasetsUpdate',
- 'beforeDatasetUpdate',
- 'afterDatasetUpdate',
- 'afterDatasetsUpdate',
- 'afterUpdate',
- ],
- render: [
- 'beforeRender',
- 'beforeDraw',
- 'beforeDatasetsDraw',
- 'beforeDatasetDraw',
- 'afterDatasetDraw',
- 'afterDatasetsDraw',
- 'beforeTooltipDraw',
- 'afterTooltipDraw',
- 'afterDraw',
- 'afterRender',
- ],
- resize: [
- 'resize'
- ],
- destroy: [
- 'destroy'
- ]
- };
Object.keys(hooks).forEach(function(group) {
hooks[group].forEach(function(name) {
chart.destroy();
expect(sequence).toEqual([].concat(
+ hooks.install,
+ hooks.start,
hooks.init,
hooks.update,
hooks.render,
hooks.resize,
hooks.update,
hooks.render,
- hooks.destroy
+ hooks.destroy,
+ hooks.stop,
+ hooks.uninstall
));
done();
chart.canvas.parentNode.style.width = '400px';
});
+ it ('should notify initially disabled plugin in correct order', function() {
+ var plugin = this.plugin = {id: 'plugin'};
+ var sequence = [];
+
+ Object.keys(hooks).forEach(function(group) {
+ hooks[group].forEach(function(name) {
+ plugin[name] = function() {
+ sequence.push(name);
+ };
+ });
+ });
+
+ var chart = window.acquireChart({
+ type: 'line',
+ data: {datasets: [{}]},
+ plugins: [plugin],
+ options: {
+ plugins: {
+ plugin: false
+ }
+ }
+ });
+
+ expect(sequence).toEqual([].concat(
+ hooks.install
+ ));
+
+ sequence = [];
+ chart.options.plugins.plugin = true;
+ chart.update();
+
+ expect(sequence).toEqual([].concat(
+ hooks.start,
+ hooks.update,
+ hooks.render
+ ));
+
+ sequence = [];
+ chart.options.plugins.plugin = false;
+ chart.update();
+
+ expect(sequence).toEqual(hooks.stop);
+
+ sequence = [];
+ chart.destroy();
+
+ expect(sequence).toEqual(hooks.uninstall);
+ });
+
it('should not notify before/afterDatasetDraw if dataset is hidden', function() {
var sequence = [];
var plugin = this.plugin = {
export interface Plugin<O = {}> {
id: string;
+ /**
+ * @desc Called when plugin is installed for this chart instance. This hook is also invoked for disabled plugins (options === false).
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
+ install?(chart: Chart, args: {}, options: O): void;
+ /**
+ * @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
+ start?(chart: Chart, args: {}, options: O): void;
+ /**
+ * @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
+ stop?(chart: Chart, args: {}, options: O): void;
/**
* @desc Called before initializing `chart`.
* @param {Chart} chart - The chart instance.
*/
resize?(chart: Chart, args: { size: { width: number, height: number } }, options: O): boolean | void;
/**
- * Called after the chart as been destroyed.
+ * Called after the chart has been destroyed.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ */
+ destroy?(chart: Chart, args: {}, options: O): boolean | void;
+ /**
+ * Called after chart is destroyed on all plugins that were installed for that chart. This hook is also invoked for disabled plugins (options === false).
* @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
* @param {object} options - The plugin options.
+ * @since 3.0.0
*/
- destroy?(chart: Chart, options: O): boolean | void;
+ uninstall?(chart: Chart, args: {}, options: O): void;
}
export declare type ChartComponentLike = ChartComponent | ChartComponent[] | { [key: string]: ChartComponent };