From: Jukka Kurkela Date: Sat, 28 Nov 2020 20:57:45 +0000 (+0200) Subject: Add new hooks for plugins (#8103) X-Git-Tag: v3.0.0-beta.7~10 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=78dbeea1f0f09e2681ec3de55bbebb2f0ba71565;p=thirdparty%2FChart.js.git 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 --- diff --git a/docs/docs/getting-started/v3-migration.md b/docs/docs/getting-started/v3-migration.md index ac35edfb6..80258d3f3 100644 --- a/docs/docs/getting-started/v3-migration.md +++ b/docs/docs/getting-started/v3-migration.md @@ -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` diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js index 5950d342f..f859734d4 100644 --- a/src/core/core.plugins.js +++ b/src/core/core.plugins.js @@ -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); - - 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'); } } @@ -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 */ diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 133df9da3..ef33ae0da 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -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 diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index 850ba89bf..04e59629b 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -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 { diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index 2dcb43385..23d47a2b2 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -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 = { diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index e924212dd..36a3439f4 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -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: { diff --git a/test/specs/plugin.title.tests.js b/test/specs/plugin.title.tests.js index ac339d095..9a794f65a 100644 --- a/test/specs/plugin.title.tests.js +++ b/test/specs/plugin.title.tests.js @@ -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: { diff --git a/types/core/index.d.ts b/types/core/index.d.ts index d31dbe853..1c9641a26 100644 --- a/types/core/index.d.ts +++ b/types/core/index.d.ts @@ -567,6 +567,30 @@ export const layouts: { export interface Plugin { 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 { */ 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 };