1
0
mirror of https://github.com/videojs/video.js.git synced 2024-12-25 02:42:10 +02:00

@heff enhanced the event listener API to allow for auto-cleanup of listeners on other componenets and elements. closes #1588

This commit is contained in:
Steve Heffernan 2014-10-28 11:16:56 -07:00
parent b8cc047a2e
commit 4b818d9ebf
18 changed files with 324 additions and 79 deletions

View File

@ -9,6 +9,7 @@ CHANGELOG
* @thijstriemstra added a Dutch translation ([view](https://github.com/videojs/video.js/pull/1566))
* @heff updated the poster to use CSS styles to display; fixed the poster not showing if not originally set ([view](https://github.com/videojs/video.js/pull/1568))
* @mmcc fixed an issue where errors on source tags could get missed ([view](https://github.com/videojs/video.js/pull/1575))
* @heff enhanced the event listener API to allow for auto-cleanup of listeners on other componenets and elements ([view](https://github.com/videojs/video.js/pull/1588))
--------------------

View File

@ -531,46 +531,169 @@ vjs.Component.prototype.buildCSSClass = function(){
* Add an event listener to this component's element
*
* var myFunc = function(){
* var myPlayer = this;
* var myComponent = this;
* // Do something when the event is fired
* };
*
* myPlayer.on("eventName", myFunc);
* myComponent.on('eventType', myFunc);
*
* The context will be the component.
* The context of myFunc will be myComponent unless previously bound.
*
* @param {String} type The event type e.g. 'click'
* @param {Function} fn The event listener
* @return {vjs.Component} self
* Alternatively, you can add a listener to another element or component.
*
* myComponent.on(otherElement, 'eventName', myFunc);
* myComponent.on(otherComponent, 'eventName', myFunc);
*
* The benefit of using this over `vjs.on(otherElement, 'eventName', myFunc)`
* and `otherComponent.on('eventName', myFunc)` is that this way the listeners
* will be automatically cleaned up when either component is diposed.
* It will also bind myComponent as the context of myFunc.
*
* **NOTE**: When using this on elements in the page other than window
* and document (both permanent), if you remove the element from the DOM
* you need to call `vjs.trigger(el, 'dispose')` on it to clean up
* references to it and allow the browser to garbage collect it.
*
* @param {String|vjs.Component} first The event type or other component
* @param {Function|String} second The event handler or event type
* @param {Function} third The event handler
* @return {vjs.Component} self
*/
vjs.Component.prototype.on = function(type, fn){
vjs.on(this.el_, type, vjs.bind(this, fn));
vjs.Component.prototype.on = function(first, second, third){
var target, type, fn, removeOnDispose, cleanRemover, thisComponent;
if (typeof first === 'string' || vjs.obj.isArray(first)) {
vjs.on(this.el_, first, vjs.bind(this, second));
// Targeting another component or element
} else {
target = first;
type = second;
fn = vjs.bind(this, third);
thisComponent = this;
// When this component is disposed, remove the listener from the other component
removeOnDispose = function(){
thisComponent.off(target, type, fn);
};
// Use the same function ID so we can remove it later it using the ID
// of the original listener
removeOnDispose.guid = fn.guid;
this.on('dispose', removeOnDispose);
// If the other component is disposed first we need to clean the reference
// to the other component in this component's removeOnDispose listener
// Otherwise we create a memory leak.
cleanRemover = function(){
thisComponent.off('dispose', removeOnDispose);
};
// Add the same function ID so we can easily remove it later
cleanRemover.guid = fn.guid;
// Check if this is a DOM node
if (first.nodeName) {
// Add the listener to the other element
vjs.on(target, type, fn);
vjs.on(target, 'dispose', cleanRemover);
// Should be a component
// Not using `instanceof vjs.Component` because it makes mock players difficult
} else if (typeof first.on === 'function') {
// Add the listener to the other component
target.on(type, fn);
target.on('dispose', cleanRemover);
}
}
return this;
};
/**
* Remove an event listener from the component's element
* Remove an event listener from this component's element
*
* myComponent.off("eventName", myFunc);
* myComponent.off('eventType', myFunc);
*
* @param {String=} type Event type. Without type it will remove all listeners.
* @param {Function=} fn Event listener. Without fn it will remove all listeners for a type.
* If myFunc is excluded, ALL listeners for the event type will be removed.
* If eventType is excluded, ALL listeners will be removed from the component.
*
* Alternatively you can use `off` to remove listeners that were added to other
* elements or components using `myComponent.on(otherComponent...`.
* In this case both the event type and listener function are REQUIRED.
*
* myComponent.off(otherElement, 'eventType', myFunc);
* myComponent.off(otherComponent, 'eventType', myFunc);
*
* @param {String=|vjs.Component} first The event type or other component
* @param {Function=|String} second The listener function or event type
* @param {Function=} third The listener for other component
* @return {vjs.Component}
*/
vjs.Component.prototype.off = function(type, fn){
vjs.off(this.el_, type, fn);
vjs.Component.prototype.off = function(first, second, third){
var target, otherComponent, type, fn, otherEl;
if (!first || typeof first === 'string' || vjs.obj.isArray(first)) {
vjs.off(this.el_, first, second);
} else {
target = first;
type = second;
// Ensure there's at least a guid, even if the function hasn't been used
fn = vjs.bind(this, third);
// Remove the dispose listener on this component,
// which was given the same guid as the event listener
this.off('dispose', fn);
if (first.nodeName) {
// Remove the listener
vjs.off(target, type, fn);
// Remove the listener for cleaning the dispose listener
vjs.off(target, 'dispose', fn);
} else {
target.off(type, fn);
target.off('dispose', fn);
}
}
return this;
};
/**
* Add an event listener to be triggered only once and then removed
*
* @param {String} type Event type
* @param {Function} fn Event listener
* myComponent.one('eventName', myFunc);
*
* Alternatively you can add a listener to another element or component
* that will be triggered only once.
*
* myComponent.one(otherElement, 'eventName', myFunc);
* myComponent.one(otherComponent, 'eventName', myFunc);
*
* @param {String|vjs.Component} first The event type or other component
* @param {Function|String} second The listener function or event type
* @param {Function=} third The listener function for other component
* @return {vjs.Component}
*/
vjs.Component.prototype.one = function(type, fn) {
vjs.one(this.el_, type, vjs.bind(this, fn));
vjs.Component.prototype.one = function(first, second, third) {
var target, type, fn, thisComponent, newFunc;
if (typeof first === 'string' || vjs.obj.isArray(first)) {
vjs.one(this.el_, first, vjs.bind(this, second));
} else {
target = first;
type = second;
fn = vjs.bind(this, third);
thisComponent = this;
newFunc = function(){
thisComponent.off(target, type, newFunc);
fn.apply(this, arguments);
};
// Keep the same function ID so we can remove it later
newFunc.guid = fn.guid;
this.on(target, type, newFunc);
}
return this;
};

View File

@ -10,19 +10,20 @@ vjs.MuteToggle = vjs.Button.extend({
init: function(player, options){
vjs.Button.call(this, player, options);
player.on('volumechange', vjs.bind(this, this.update));
this.on(player, 'volumechange', this.update);
// hide mute toggle if the current tech doesn't support volume control
if (player.tech && player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
}
player.on('loadstart', vjs.bind(this, function(){
this.on(player, 'loadstart', function(){
if (player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
} else {
this.removeClass('vjs-hidden');
}
}));
});
}
});

View File

@ -10,8 +10,8 @@ vjs.PlayToggle = vjs.Button.extend({
init: function(player, options){
vjs.Button.call(this, player, options);
player.on('play', vjs.bind(this, this.onPlay));
player.on('pause', vjs.bind(this, this.onPause));
this.on(player, 'play', this.onPlay);
this.on(player, 'pause', this.onPause);
}
});
@ -32,14 +32,14 @@ vjs.PlayToggle.prototype.onClick = function(){
// OnPlay - Add the vjs-playing class to the element so it can change appearance
vjs.PlayToggle.prototype.onPlay = function(){
vjs.removeClass(this.el_, 'vjs-paused');
vjs.addClass(this.el_, 'vjs-playing');
this.removeClass('vjs-paused');
this.addClass('vjs-playing');
this.el_.children[0].children[0].innerHTML = this.localize('Pause'); // change the button text to "Pause"
};
// OnPause - Add the vjs-paused class to the element so it can change appearance
vjs.PlayToggle.prototype.onPause = function(){
vjs.removeClass(this.el_, 'vjs-playing');
vjs.addClass(this.el_, 'vjs-paused');
this.removeClass('vjs-playing');
this.addClass('vjs-paused');
this.el_.children[0].children[0].innerHTML = this.localize('Play'); // change the button text to "Play"
};

View File

@ -13,8 +13,8 @@ vjs.PlaybackRateMenuButton = vjs.MenuButton.extend({
this.updateVisibility();
this.updateLabel();
player.on('loadstart', vjs.bind(this, this.updateVisibility));
player.on('ratechange', vjs.bind(this, this.updateLabel));
this.on(player, 'loadstart', this.updateVisibility);
this.on(player, 'ratechange', this.updateLabel);
}
});
@ -115,7 +115,7 @@ vjs.PlaybackRateMenuItem = vjs.MenuItem.extend({
options['selected'] = rate === 1;
vjs.MenuItem.call(this, player, options);
this.player().on('ratechange', vjs.bind(this, this.update));
this.on(player, 'ratechange', this.update);
}
});

View File

@ -36,7 +36,7 @@ vjs.SeekBar = vjs.Slider.extend({
/** @constructor */
init: function(player, options){
vjs.Slider.call(this, player, options);
player.on('timeupdate', vjs.bind(this, this.updateARIAAttributes));
this.on(player, 'timeupdate', this.updateARIAAttributes);
player.ready(vjs.bind(this, this.updateARIAAttributes));
}
});
@ -118,7 +118,7 @@ vjs.LoadProgressBar = vjs.Component.extend({
/** @constructor */
init: function(player, options){
vjs.Component.call(this, player, options);
player.on('progress', vjs.bind(this, this.update));
this.on(player, 'progress', this.update);
}
});
@ -197,7 +197,7 @@ vjs.PlayProgressBar.prototype.createEl = function(){
vjs.SeekHandle = vjs.SliderHandle.extend({
init: function(player, options) {
vjs.SliderHandle.call(this, player, options);
player.on('timeupdate', vjs.bind(this, this.updateContent));
this.on(player, 'timeupdate', this.updateContent);
}
});

View File

@ -9,7 +9,7 @@ vjs.CurrentTimeDisplay = vjs.Component.extend({
init: function(player, options){
vjs.Component.call(this, player, options);
player.on('timeupdate', vjs.bind(this, this.updateContent));
this.on(player, 'timeupdate', this.updateContent);
}
});
@ -50,7 +50,7 @@ vjs.DurationDisplay = vjs.Component.extend({
// so the value cannot be written out using this method.
// Once the order of durationchange and this.player_.duration() being set is figured out,
// this can be updated.
player.on('timeupdate', vjs.bind(this, this.updateContent));
this.on(player, 'timeupdate', this.updateContent);
}
});
@ -110,7 +110,7 @@ vjs.RemainingTimeDisplay = vjs.Component.extend({
init: function(player, options){
vjs.Component.call(this, player, options);
player.on('timeupdate', vjs.bind(this, this.updateContent));
this.on(player, 'timeupdate', this.updateContent);
}
});

View File

@ -14,13 +14,13 @@ vjs.VolumeControl = vjs.Component.extend({
if (player.tech && player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
}
player.on('loadstart', vjs.bind(this, function(){
this.on(player, 'loadstart', function(){
if (player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
} else {
this.removeClass('vjs-hidden');
}
}));
});
}
});
@ -47,7 +47,7 @@ vjs.VolumeBar = vjs.Slider.extend({
/** @constructor */
init: function(player, options){
vjs.Slider.call(this, player, options);
player.on('volumechange', vjs.bind(this, this.updateARIAAttributes));
this.on(player, 'volumechange', this.updateARIAAttributes);
player.ready(vjs.bind(this, this.updateARIAAttributes));
}
});

View File

@ -8,19 +8,19 @@ vjs.VolumeMenuButton = vjs.MenuButton.extend({
vjs.MenuButton.call(this, player, options);
// Same listeners as MuteToggle
player.on('volumechange', vjs.bind(this, this.update));
this.on(player, 'volumechange', this.update);
// hide mute toggle if the current tech doesn't support volume control
if (player.tech && player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
}
player.on('loadstart', vjs.bind(this, function(){
this.on(player, 'loadstart', function(){
if (player.tech['featuresVolumeControl'] === false) {
this.addClass('vjs-hidden');
} else {
this.removeClass('vjs-hidden');
}
}));
});
this.addClass('vjs-menu-button');
}
});

View File

@ -9,7 +9,7 @@ vjs.ErrorDisplay = vjs.Component.extend({
vjs.Component.call(this, player, options);
this.update();
player.on('error', vjs.bind(this, this.update));
this.on(player, 'error', this.update);
}
});

View File

@ -92,10 +92,10 @@ vjs.Flash = vjs.MediaTechController.extend({
// bugzilla bug: https://bugzilla.mozilla.org/show_bug.cgi?id=836786
if (vjs.IS_FIREFOX) {
this.ready(function(){
vjs.on(this.el(), 'mousemove', vjs.bind(this, function(){
this.on('mousemove', function(){
// since it's a custom event, don't bubble higher than the player
this.player().trigger({ 'type':'mousemove', 'bubbles': false });
}));
});
});
}

View File

@ -121,7 +121,7 @@ vjs.Html5.prototype.createEl = function(){
// Triggers removed using this.off when disposed
vjs.Html5.prototype.setupTriggers = function(){
for (var i = vjs.Html5.Events.length - 1; i >= 0; i--) {
vjs.on(this.el_, vjs.Html5.Events[i], vjs.bind(this, this.eventHandler));
this.on(vjs.Html5.Events[i], this.eventHandler);
}
};
@ -214,16 +214,16 @@ vjs.Html5.prototype.enterFullScreen = function(){
var video = this.el_;
if ('webkitDisplayingFullscreen' in video) {
this.one('webkitbeginfullscreen', vjs.bind(this, function(e) {
this.one('webkitbeginfullscreen', function() {
this.player_.isFullscreen(true);
this.one('webkitendfullscreen', vjs.bind(this, function(e) {
this.one('webkitendfullscreen', function() {
this.player_.isFullscreen(false);
this.player_.trigger('fullscreenchange');
}));
});
this.player_.trigger('fullscreenchange');
}));
});
}
if (video.paused && video.networkState <= video.HAVE_METADATA) {

View File

@ -53,24 +53,21 @@ vjs.MediaTechController = vjs.Component.extend({
* any controls will still keep the user active
*/
vjs.MediaTechController.prototype.initControlsListeners = function(){
var player, tech, activateControls, deactivateControls;
var player, activateControls;
tech = this;
player = this.player();
var activateControls = function(){
activateControls = function(){
if (player.controls() && !player.usingNativeControls()) {
tech.addControlsListeners();
this.addControlsListeners();
}
};
deactivateControls = vjs.bind(tech, tech.removeControlsListeners);
// Set up event listeners once the tech is ready and has an element to apply
// listeners to
this.ready(activateControls);
player.on('controlsenabled', activateControls);
player.on('controlsdisabled', deactivateControls);
this.on(player, 'controlsenabled', activateControls);
this.on(player, 'controlsdisabled', this.removeControlsListeners);
// if we're loading the playback object after it has started loading or playing the
// video (often with autoplay on) then the loadstart event has already fired and we
@ -201,10 +198,12 @@ vjs.MediaTechController.prototype.stopTrackingProgress = function(){ clearInterv
/*! Time Tracking -------------------------------------------------------------- */
vjs.MediaTechController.prototype.manualTimeUpdatesOn = function(){
var player = this.player_;
this.manualTimeUpdates = true;
this.player().on('play', vjs.bind(this, this.trackCurrentTime));
this.player().on('pause', vjs.bind(this, this.stopTrackingCurrentTime));
this.on(player, 'play', this.trackCurrentTime);
this.on(player, 'pause', this.stopTrackingCurrentTime);
// timeupdate is also called by .currentTime whenever current time is set
// Watch for native timeupdate event

View File

@ -22,13 +22,10 @@ vjs.Slider = vjs.Component.extend({
this.on('blur', this.onBlur);
this.on('click', this.onClick);
this.player_.on('controlsvisible', vjs.bind(this, this.update));
player.on(this.playerEvent, vjs.bind(this, this.update));
this.on(player, 'controlsvisible', this.update);
this.on(player, this.playerEvent, this.update);
this.boundEvents = {};
this.boundEvents.move = vjs.bind(this, this.onMouseMove);
this.boundEvents.end = vjs.bind(this, this.onMouseUp);
}

View File

@ -733,7 +733,7 @@ vjs.TextTrackMenuItem = vjs.MenuItem.extend({
options['selected'] = track.dflt();
vjs.MenuItem.call(this, player, options);
this.player_.on(track.kind() + 'trackchange', vjs.bind(this, this.update));
this.on(player, track.kind() + 'trackchange', this.update);
}
});

View File

@ -161,6 +161,132 @@ test('should trigger a listener once using one()', function(){
comp.trigger('test-event');
});
test('should add listeners to other components and remove them', function(){
var player = getFakePlayer(),
comp1 = new vjs.Component(player),
comp2 = new vjs.Component(player),
listenerFired = 0,
testListener;
testListener = function(){
equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.on(comp2, 'test-event', testListener);
comp2.trigger('test-event');
equal(listenerFired, 1, 'listener was fired once');
listenerFired = 0;
comp1.off(comp2, 'test-event', testListener);
comp2.trigger('test-event');
equal(listenerFired, 0, 'listener was not fired after being removed');
// this component is disposed first
listenerFired = 0;
comp1.on(comp2, 'test-event', testListener);
comp1.dispose();
comp2.trigger('test-event');
equal(listenerFired, 0, 'listener was removed when this component was disposed first');
comp1.off = function(){ throw 'Comp1 off called'; };
comp2.dispose();
ok(true, 'this component removed dispose listeners from other component');
});
test('should add listeners to other components and remove when them other component is disposed', function(){
var player = getFakePlayer(),
comp1 = new vjs.Component(player),
comp2 = new vjs.Component(player),
listenerFired = 0,
testListener;
testListener = function(){
equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.on(comp2, 'test-event', testListener);
comp2.dispose();
comp2.off = function(){ throw 'Comp2 off called'; };
comp1.dispose();
ok(true, 'this component removed dispose listener from this component that referenced other component');
});
test('should add listeners to other components that are fired once', function(){
var player = getFakePlayer(),
comp1 = new vjs.Component(player),
comp2 = new vjs.Component(player),
listenerFired = 0,
testListener;
testListener = function(){
equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.one(comp2, 'test-event', testListener);
comp2.trigger('test-event');
equal(listenerFired, 1, 'listener was executed once');
comp2.trigger('test-event');
equal(listenerFired, 1, 'listener was executed only once');
});
test('should add listeners to other element and remove them', function(){
var player = getFakePlayer(),
comp1 = new vjs.Component(player),
el = document.createElement('div'),
listenerFired = 0,
testListener;
testListener = function(){
equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.on(el, 'test-event', testListener);
vjs.trigger(el, 'test-event');
equal(listenerFired, 1, 'listener was fired once');
listenerFired = 0;
comp1.off(el, 'test-event', testListener);
vjs.trigger(el, 'test-event');
equal(listenerFired, 0, 'listener was not fired after being removed from other element');
// this component is disposed first
listenerFired = 0;
comp1.on(el, 'test-event', testListener);
comp1.dispose();
vjs.trigger(el, 'test-event');
equal(listenerFired, 0, 'listener was removed when this component was disposed first');
comp1.off = function(){ throw 'Comp1 off called'; };
try {
vjs.trigger(el, 'dispose');
} catch(e) {
ok(false, 'listener was not removed from other element');
}
vjs.trigger(el, 'dispose');
ok(true, 'this component removed dispose listeners from other element');
});
test('should add listeners to other components that are fired once', function(){
var player = getFakePlayer(),
comp1 = new vjs.Component(player),
el = document.createElement('div'),
listenerFired = 0,
testListener;
testListener = function(){
equal(this, comp1, 'listener has the first component as context');
listenerFired++;
};
comp1.one(el, 'test-event', testListener);
vjs.trigger(el, 'test-event');
equal(listenerFired, 1, 'listener was executed once');
vjs.trigger(el, 'test-event');
equal(listenerFired, 1, 'listener was executed only once');
});
test('should trigger a listener when ready', function(){
expect(2);

23
test/unit/controls.js vendored
View File

@ -35,7 +35,10 @@ test('should test and toggle volume control on `loadstart`', function(){
language: noop,
languages: noop,
on: function(event, callback){
listeners.push(callback);
// don't fire dispose listeners
if (event !== 'dispose') {
listeners.push(callback);
}
},
ready: noop,
volume: function(){
@ -53,30 +56,24 @@ test('should test and toggle volume control on `loadstart`', function(){
volumeControl = new vjs.VolumeControl(player);
muteToggle = new vjs.MuteToggle(player);
ok(volumeControl.el().className.indexOf('vjs-hidden') < 0,
'volumeControl is hidden initially');
ok(muteToggle.el().className.indexOf('vjs-hidden') < 0,
'muteToggle is hidden initially');
equal(volumeControl.hasClass('vjs-hidden'), false, 'volumeControl is hidden initially');
equal(muteToggle.hasClass('vjs-hidden'), false, 'muteToggle is hidden initially');
player.tech['featuresVolumeControl'] = false;
for (i = 0; i < listeners.length; i++) {
listeners[i]();
}
ok(volumeControl.el().className.indexOf('vjs-hidden') >= 0,
'volumeControl does not hide itself');
ok(muteToggle.el().className.indexOf('vjs-hidden') >= 0,
'muteToggle does not hide itself');
equal(volumeControl.hasClass('vjs-hidden'), true, 'volumeControl does not hide itself');
equal(muteToggle.hasClass('vjs-hidden'), true, 'muteToggle does not hide itself');
player.tech['featuresVolumeControl'] = true;
for (i = 0; i < listeners.length; i++) {
listeners[i]();
}
ok(volumeControl.el().className.indexOf('vjs-hidden') < 0,
'volumeControl does not show itself');
ok(muteToggle.el().className.indexOf('vjs-hidden') < 0,
'muteToggle does not show itself');
equal(volumeControl.hasClass('vjs-hidden'), false, 'volumeControl does not show itself');
equal(muteToggle.hasClass('vjs-hidden'), false, 'muteToggle does not show itself');
});
test('calculateDistance should use changedTouches, if available', function() {

View File

@ -120,6 +120,7 @@ test('dispose() should stop time tracking', function() {
var tech = new videojs.MediaTechController({
id: noop,
on: noop,
off: noop,
trigger: noop
});
tech.dispose();