From 44524fa8da4aebc012e2e4a2f9e11f5c293f84f4 Mon Sep 17 00:00:00 2001 From: Pat O'Neill Date: Wed, 5 Apr 2017 12:05:37 -0400 Subject: [PATCH 1/3] feat: Add 'beforepluginsetup' event and named plugin setup events (e.g. 'pluginsetup:foo') --- src/js/plugin.js | 69 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/js/plugin.js b/src/js/plugin.js index bd306a586f..39d95d7869 100644 --- a/src/js/plugin.js +++ b/src/js/plugin.js @@ -75,6 +75,26 @@ const markPluginAsActive = (player, name) => { player[PLUGIN_CACHE_KEY][name] = true; }; +/** + * Triggers a pair of plugin setup events. + * + * @private + * @param {Player} player + * A Video.js player instance. + * + * @param {Plugin~PluginEventHash} hash + * A plugin event hash. + * + * @param {Boolean} [before] + * If true, modifies the events to be "before" events. + */ +const triggerSetupEvent = (player, hash, before) => { + const eventName = (before ? 'before' : '') + 'pluginsetup'; + + player.trigger(eventName, hash); + player.trigger(eventName + ':' + hash.name, hash); +}; + /** * Takes a basic plugin function and returns a wrapper function which marks * on the player that the plugin has been activated. @@ -91,15 +111,20 @@ const markPluginAsActive = (player, name) => { */ const createBasicPlugin = function(name, plugin) { const basicPluginWrapper = function() { + + // We trigger the "beforepluginsetup" and "pluginsetup" events on the player + // regardless, but we want the hash to be consistent with the hash provided + // for advanced plugins. + // + // The only potentially counter-intuitive thing here is the `instance` in + // the "pluginsetup" event is the value returned by the `plugin` function. + triggerSetupEvent(this, {name, plugin, instance: null}, true); + const instance = plugin.apply(this, arguments); markPluginAsActive(this, name); + triggerSetupEvent(this, {name, plugin, instance}); - // We trigger the "pluginsetup" event on the player regardless, but we want - // the hash to be consistent with the hash provided for advanced plugins. - // The only potentially counter-intuitive thing here is the `instance` is the - // value returned by the `plugin` function. - this.trigger('pluginsetup', {name, plugin, instance}); return instance; }; @@ -133,6 +158,8 @@ const createPluginFactory = (name, PluginSubClass) => { PluginSubClass.prototype.name = name; return function(...args) { + triggerSetupEvent(this, {name, plugin: PluginSubClass, instance: null}, true); + const instance = new PluginSubClass(...[this, ...args]); // The plugin is replaced by a function that returns the current instance. @@ -147,7 +174,10 @@ const createPluginFactory = (name, PluginSubClass) => { * * @mixes module:evented~EventedMixin * @mixes module:stateful~StatefulMixin + * @fires Player#beforepluginsetup + * @fires Player#beforepluginsetup:$name * @fires Player#pluginsetup + * @fires Player#pluginsetup:$name * @listens Player#dispose * @throws {Error} * If attempting to instantiate the base {@link Plugin} class @@ -164,12 +194,12 @@ class Plugin { * A Video.js player instance. */ constructor(player) { - this.player = player; - if (this.constructor === Plugin) { throw new Error('Plugin must be sub-classed; not directly instantiated.'); } + this.player = player; + // Make this object evented, but remove the added `trigger` method so we // use the prototype version instead. evented(this); @@ -184,7 +214,7 @@ class Plugin { // If the player is disposed, dispose the plugin. player.on('dispose', this.dispose); - player.trigger('pluginsetup', this.getEventHash()); + triggerSetupEvent(player, this.getEventHash()); } /** @@ -432,6 +462,21 @@ Player.prototype.hasPlugin = function(name) { export default Plugin; +/** + * Signals that a plugin is about to be set up on a player. + * + * @event Player#beforepluginsetup + * @type {Plugin~PluginEventHash} + */ + +/** + * Signals that a plugin is about to be set up on a player - by name. The name + * is the name of the plugin. + * + * @event Player#beforepluginsetup:$name + * @type {Plugin~PluginEventHash} + */ + /** * Signals that a plugin has just been set up on a player. * @@ -439,6 +484,14 @@ export default Plugin; * @type {Plugin~PluginEventHash} */ +/** + * Signals that a plugin has just been set up on a player - by name. The name + * is the name of the plugin. + * + * @event Player#pluginsetup:$name + * @type {Plugin~PluginEventHash} + */ + /** * @typedef {Object} Plugin~PluginEventHash * From 6ed50ba69190e9ae52c2b2b72f743e05b977f74e Mon Sep 17 00:00:00 2001 From: Pat O'Neill Date: Wed, 12 Apr 2017 16:06:11 -0400 Subject: [PATCH 2/3] Add tests for all pluginsetup events --- src/js/plugin.js | 3 ++- test/unit/plugin-advanced.test.js | 35 ++++++++++++++++++++----------- test/unit/plugin-basic.test.js | 34 ++++++++++++++++++++---------- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/js/plugin.js b/src/js/plugin.js index 39d95d7869..ce6cc1bc58 100644 --- a/src/js/plugin.js +++ b/src/js/plugin.js @@ -86,7 +86,8 @@ const markPluginAsActive = (player, name) => { * A plugin event hash. * * @param {Boolean} [before] - * If true, modifies the events to be "before" events. + * If true, prefixes the event name with "before". In other words, + * use this to trigger "beforepluginsetup" instead of "pluginsetup". */ const triggerSetupEvent = (player, hash, before) => { const eventName = (before ? 'before' : '') + 'pluginsetup'; diff --git a/test/unit/plugin-advanced.test.js b/test/unit/plugin-advanced.test.js index 23c4d3dccf..2ea499dbb4 100644 --- a/test/unit/plugin-advanced.test.js +++ b/test/unit/plugin-advanced.test.js @@ -67,24 +67,35 @@ QUnit.test('setup', function(assert) { ); }); -QUnit.test('"pluginsetup" event', function(assert) { +QUnit.test('all "pluginsetup" events', function(assert) { const setupSpy = sinon.spy(); + const events = [ + 'beforepluginsetup', + 'beforepluginsetup:mock', + 'pluginsetup', + 'pluginsetup:mock' + ]; - this.player.on('pluginsetup', setupSpy); + this.player.on(events, setupSpy); const instance = this.player.mock(); - const event = setupSpy.firstCall.args[0]; - const hash = setupSpy.firstCall.args[1]; - assert.strictEqual(setupSpy.callCount, 1, 'the "pluginsetup" event was triggered'); - assert.strictEqual(event.type, 'pluginsetup', 'the event has the correct type'); - assert.strictEqual(event.target, this.player.el_, 'the event has the correct target'); + events.forEach((type, i) => { + const event = setupSpy.getCall(i).args[0]; + const hash = setupSpy.getCall(i).args[1]; - assert.deepEqual(hash, { - name: 'mock', - instance, - plugin: this.MockPlugin - }, 'the event hash object is correct'); + assert.strictEqual(event.type, type, `the "${type}" event was triggered`); + assert.strictEqual(event.target, this.player.el_, 'the event has the correct target'); + + assert.deepEqual(hash, { + name: 'mock', + + // The "before" events have a `null` instance and the others have the + // return value of the plugin factory. + instance: i < 2 ? null : instance, + plugin: this.MockPlugin + }, 'the event hash object is correct'); + }); }); QUnit.test('defaultState static property is used to populate state', function(assert) { diff --git a/test/unit/plugin-basic.test.js b/test/unit/plugin-basic.test.js index ff7ad73fd2..7ae81ebcd1 100644 --- a/test/unit/plugin-basic.test.js +++ b/test/unit/plugin-basic.test.js @@ -47,23 +47,35 @@ QUnit.test('setup', function(assert) { assert.ok(this.player.hasPlugin('basic'), 'player has the plugin available'); }); -QUnit.test('"pluginsetup" event', function(assert) { +QUnit.test('all "pluginsetup" events', function(assert) { const setupSpy = sinon.spy(); + const events = [ + 'beforepluginsetup', + 'beforepluginsetup:basic', + 'pluginsetup', + 'pluginsetup:basic' + ]; - this.player.on('pluginsetup', setupSpy); + this.player.on(events, setupSpy); const instance = this.player.basic(); - const event = setupSpy.firstCall.args[0]; - const hash = setupSpy.firstCall.args[1]; - assert.strictEqual(setupSpy.callCount, 1, 'the "pluginsetup" event was triggered'); - assert.strictEqual(event.type, 'pluginsetup', 'the event has the correct type'); + events.forEach((type, i) => { + const event = setupSpy.getCall(i).args[0]; + const hash = setupSpy.getCall(i).args[1]; - assert.deepEqual(hash, { - name: 'basic', - instance, - plugin: this.basic - }, 'the event hash object is correct'); + assert.strictEqual(event.type, type, `the "${type}" event was triggered`); + assert.strictEqual(event.target, this.player.el_, 'the event has the correct target'); + + assert.deepEqual(hash, { + name: 'basic', + + // The "before" events have a `null` instance and the others have the + // return value of the plugin factory. + instance: i < 2 ? null : instance, + plugin: this.basic + }, 'the event hash object is correct'); + }); }); QUnit.test('properties are copied', function(assert) { From bc4adc14546c77afbf71fc1d9f24fec952947372 Mon Sep 17 00:00:00 2001 From: Pat O'Neill Date: Wed, 12 Apr 2017 16:24:28 -0400 Subject: [PATCH 3/3] Documentation --- docs/guides/plugins.md | 21 +++++++++++++++++++++ src/js/plugin.js | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/guides/plugins.md b/docs/guides/plugins.md index ce76d09b32..2ff0d4eba2 100644 --- a/docs/guides/plugins.md +++ b/docs/guides/plugins.md @@ -176,6 +176,14 @@ Like components, advanced plugins offer an implementation of events. This includ By offering a built-in events system, advanced plugins offer a wider range of options for code structure with a pattern familiar to most web developers. +##### Extra Event Data + +All events triggered by plugins include an additional data object as a second argument. This object has three properties: + +- `name`: The name of the plugin (e.g. `"examplePlugin"`) as a string. +- `plugin`: The plugin constructor (e.g. `ExamplePlugin`). +- `instance`: The plugin constructor instance. + #### Statefulness A new concept introduced for advanced plugins is _statefulness_. This is similar to React components' `state` property and `setState` method. @@ -307,6 +315,19 @@ player.examplePlugin({customClass: 'example-class'}); These two methods are functionally identical - use whichever you prefer! +### Plugin Setup Events + +Occasionally, a use-case arises where some code needs to wait for a plugin to be initialized. As of Video.js 6, this can be achieved by listening for `pluginsetup` events on the player. + +For any given plugin initialization, there are four events to be aware of: + +- `beforepluginsetup`: Triggered immediately before any plugin is initialized. +- `beforepluginsetup:examplePlugin` Triggered immediately before the `examplePlugin` is initialized. +- `pluginsetup`: Triggered after any plugin is initialized. +- `pluginsetup:examplePlugin`: Triggered after he `examplePlugin` is initialized. + +These events work for both basic and advanced plugins. They are triggered on the player and each includes an object of [extra event data](#extra-event-data) as a second argument to its listeners. + ## References * [Player API][api-player] diff --git a/src/js/plugin.js b/src/js/plugin.js index ce6cc1bc58..90285237dc 100644 --- a/src/js/plugin.js +++ b/src/js/plugin.js @@ -166,6 +166,8 @@ const createPluginFactory = (name, PluginSubClass) => { // The plugin is replaced by a function that returns the current instance. this[name] = () => instance; + triggerSetupEvent(this, instance.getEventHash()); + return instance; }; }; @@ -215,7 +217,6 @@ class Plugin { // If the player is disposed, dispose the plugin. player.on('dispose', this.dispose); - triggerSetupEvent(player, this.getEventHash()); } /**