mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-03 06:54:02 +01:00
Add new hooks for plugins (#8103)
* Notify beforeUpdate on disabled plugins cc? cc2 cc3 typo * init, unInit, enabled, disabled self review :) update the new hook signatures to unified merge error * Review update * start/stop, cc * types, jsdoc * stop between destroy and uninstall
This commit is contained in:
@@ -499,3 +499,4 @@ All helpers are now exposed in a flat hierarchy, e.g., `Chart.helpers.canvas.cli
|
||||
* `afterDatasetsUpdate`, `afterUpdate`, `beforeDatasetsUpdate`, and `beforeUpdate` now receive `args` object as second argument. `options` argument is always the last and thus was moved from 2nd to 3rd place.
|
||||
* `afterEvent` and `beforeEvent` now receive a wrapped `event` as the `event` property of the second argument. The native event is available via `args.event.native`.
|
||||
* Initial `resize` is no longer silent. Meaning that `resize` event can fire between `beforeInit` and `afterInit`
|
||||
* New hooks: `install`, `start`, `stop`, and `uninstall`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
@@ -9,6 +9,10 @@ import {mergeIf, valueOrDefault} from '../helpers/helpers.core';
|
||||
*/
|
||||
|
||||
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
|
||||
@@ -19,18 +23,34 @@ export default class PluginService {
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +58,7 @@ export default class PluginService {
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
this._oldCache = this._cache;
|
||||
this._cache = undefined;
|
||||
}
|
||||
|
||||
@@ -50,15 +71,31 @@ export default class PluginService {
|
||||
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);
|
||||
return options === false && !all ? [] : createDescriptors(plugins, options, all);
|
||||
}
|
||||
|
||||
this._cache = descriptors;
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,20 +121,26 @@ function allPlugins(config) {
|
||||
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]])
|
||||
@@ -113,6 +156,30 @@ function createDescriptors(plugins, options) {
|
||||
* @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`.
|
||||
@@ -338,8 +405,16 @@ function createDescriptors(plugins, options) {
|
||||
*/
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
@@ -653,12 +653,14 @@ export default {
|
||||
*/
|
||||
_element: Legend,
|
||||
|
||||
beforeInit(chart) {
|
||||
start(chart) {
|
||||
const legendOpts = resolveOptions(chart.options.plugins.legend);
|
||||
createNewLegendAndAttach(chart, legendOpts);
|
||||
},
|
||||
|
||||
if (legendOpts) {
|
||||
createNewLegendAndAttach(chart, legendOpts);
|
||||
}
|
||||
stop(chart) {
|
||||
layouts.removeBox(chart, chart.legend);
|
||||
delete chart.legend;
|
||||
},
|
||||
|
||||
// During the beforeUpdate step, the layout configuration needs to run
|
||||
|
||||
@@ -184,11 +184,17 @@ export default {
|
||||
*/
|
||||
_element: Title,
|
||||
|
||||
beforeInit(chart, options) {
|
||||
start(chart, _args, options) {
|
||||
createTitle(chart, options);
|
||||
},
|
||||
|
||||
beforeUpdate(chart, args, options) {
|
||||
stop(chart) {
|
||||
const titleBlock = chart.titleBlock;
|
||||
layouts.removeBox(chart, titleBlock);
|
||||
delete chart.titleBlock;
|
||||
},
|
||||
|
||||
beforeUpdate(chart, _args, options) {
|
||||
if (options === false) {
|
||||
removeTitle(chart);
|
||||
} else {
|
||||
|
||||
@@ -1383,44 +1383,49 @@ describe('Chart', function() {
|
||||
});
|
||||
|
||||
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) {
|
||||
@@ -1447,13 +1452,17 @@ describe('Chart', function() {
|
||||
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();
|
||||
@@ -1461,6 +1470,55 @@ describe('Chart', function() {
|
||||
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 = {
|
||||
|
||||
@@ -760,7 +760,7 @@ describe('Legend block tests', function() {
|
||||
expect(chart.legend.weight).toBe(42);
|
||||
});
|
||||
|
||||
xit ('should remove the legend if the new options are false', function() {
|
||||
it ('should remove the legend if the new options are false', function() {
|
||||
var chart = acquireChart({
|
||||
type: 'line',
|
||||
data: {
|
||||
|
||||
@@ -295,7 +295,7 @@ describe('Title block tests', function() {
|
||||
expect(chart.titleBlock.weight).toBe(42);
|
||||
});
|
||||
|
||||
xit ('should remove the title if the new options are false', function() {
|
||||
it ('should remove the title if the new options are false', function() {
|
||||
var chart = acquireChart({
|
||||
type: 'line',
|
||||
data: {
|
||||
|
||||
37
types/core/index.d.ts
vendored
37
types/core/index.d.ts
vendored
@@ -567,6 +567,30 @@ export const layouts: {
|
||||
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.
|
||||
@@ -772,11 +796,20 @@ export interface Plugin<O = {}> {
|
||||
*/
|
||||
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, options: O): boolean | void;
|
||||
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
|
||||
*/
|
||||
uninstall?(chart: Chart, args: {}, options: O): void;
|
||||
}
|
||||
|
||||
export declare type ChartComponentLike = ChartComponent | ChartComponent[] | { [key: string]: ChartComponent };
|
||||
|
||||
Reference in New Issue
Block a user