Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: playerresize event in all cases #4864

Merged
merged 32 commits into from
Jan 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
332e896
resizer
gkatsev Mar 29, 2017
92adf24
wip
gkatsev Jan 8, 2018
2ea72f9
use resize observer or iframe
gkatsev Jan 8, 2018
e7fa09b
Revert "feat: playerresize event on Player dimension API calls (#4800)"
gkatsev Jan 9, 2018
5902483
make sure that contentWindow is available before remove listener
gkatsev Jan 9, 2018
439ac9e
dispose iframe load handler properly
gkatsev Jan 9, 2018
c98a945
dispose some players in tests when done
gkatsev Jan 9, 2018
5219730
dispose all the things
gkatsev Jan 9, 2018
a063076
add .off to fake player in poster image tests
gkatsev Jan 9, 2018
818a574
add removeEventListener to tracks tests
gkatsev Jan 9, 2018
8b1cee1
nullcheck contentWindow
gkatsev Jan 9, 2018
e0400e5
use vjs event helpers
gkatsev Jan 9, 2018
e66acb1
can't delegate, use actual event helpers
gkatsev Jan 9, 2018
a2494d5
disable resize manager to IE8
gkatsev Jan 9, 2018
4fed939
push to children
gkatsev Jan 9, 2018
69ad100
allow a ResizeObserver polyfill to be passed in.
gkatsev Jan 10, 2018
157adcf
test RM, can't test iframe because async
gkatsev Jan 10, 2018
b9c5967
lint
gkatsev Jan 10, 2018
18854a3
point at right player
gkatsev Jan 10, 2018
6b7c278
update tests
gkatsev Jan 10, 2018
902bfde
add debounce inspired by lodash and underscore
gkatsev Jan 10, 2018
4fa8e31
debounce handlers and don't create el if not necessary
gkatsev Jan 10, 2018
1ffd0aa
fixup tests
gkatsev Jan 10, 2018
a8f6ebb
dispose debounce with player, use fake timers in test
gkatsev Jan 10, 2018
fec334b
sinon fake timers
gkatsev Jan 10, 2018
a742ad4
linter
gkatsev Jan 10, 2018
5cb16a3
add jsdoc to resize manager
gkatsev Jan 17, 2018
e891260
update glob for remark
gkatsev Jan 17, 2018
062d31f
add ResizeManager to components
gkatsev Jan 17, 2018
93449c0
remark fix components
gkatsev Jan 17, 2018
18d354b
code review comments
gkatsev Jan 17, 2018
3041792
small typo corrections
ldayananda Jan 18, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion docs/guides/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The architecture of the Video.js player is centered around components. The `Play
* [Specific Component Details](#specific-component-details)
* [Volume Panel](#volume-panel)
* [Text Track Settings](#text-track-settings)
* [Resize Manager](#resize-manager)

## What is a Component?

Expand Down Expand Up @@ -312,7 +313,8 @@ Player
│ ├── AudioTrackButton (hidden, unless there are relevant tracks)
│ └── FullscreenToggle
├── ErrorDisplay (hidden, until there is an error)
└── TextTrackSettings
├── TextTrackSettings
└── ResizeManager (hidden)
```

## Specific Component Details
Expand All @@ -338,3 +340,37 @@ let player = videojs('myplayer', {
The text track settings component is only available when using emulated text tracks.

[api]: http://docs.videojs.com/Component.html

### Resize Manager

This new component is in charge of triggering a `playerresize` event when the player size changed.
It uses the ResizeObserver if available or a polyfill was provided. It has no element when using the ResizeObserver.
If a ResizeObserver is not available, it will fallback to an iframe element and listen to its resize event via a debounced handler.

A ResizeObserver polyfill can be passed in like so:

```js
var player = videojs('myplayer', {
resizeManager: {
ResizeObserver: ResizeObserverPoylfill
}
});
```

To force using the iframe fallback, pass in `null` as the `ResizeObserver`:

```js
var player = videojs('myplayer', {
resizeManager: {
ResizeObserver: null
}
});
```

The ResizeManager can also just be disabled like so:

```js
var player = videojs('myplayer', {
resizeManager: false
});
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"docs:api": "jsdoc -c .jsdoc.json",
"postdocs:api": "node ./build/fix-api-docs.js",
"netlify": "babel-node ./build/netlify-docs.js",
"docs:lint": "remark -- './**/*.md'",
"docs:fix": "remark --output -- './**/*.md'",
"docs:lint": "remark -- './{,!(node_modules)/**/}!(CHANGELOG)*.md'",
"docs:fix": "remark --output -- './{,!(node_modules)/**/}!(CHANGELOG)*.md'",
"babel": "babel src/js -d es5",
"prepublish": "not-in-install && run-p build || in-install",
"publish": "node build/gh-release.js",
Expand Down
10 changes: 10 additions & 0 deletions src/css/video-js.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,13 @@
@import "components/captions-settings";

@import "print";

.vjs-resize-manager {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
visibility: hidden;
}
37 changes: 10 additions & 27 deletions src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import './close-button.js';
import './control-bar/control-bar.js';
import './error-display.js';
import './tracks/text-track-settings.js';
import './resize-manager.js';

// Import Html5 tech, at least for disposing the original video tag.
import './tech/html5.js';
Expand Down Expand Up @@ -661,14 +662,11 @@ class Player extends Component {
* @param {number} [value]
* The value to set the `Player`'s width to.
*
* @param {boolean} [skipListeners]
* Skip the playerresize event trigger
*
* @return {number}
* The current width of the `Player` when getting.
*/
width(value, skipListeners) {
return this.dimension('width', value, skipListeners);
width(value) {
return this.dimension('width', value);
}

/**
Expand All @@ -678,21 +676,16 @@ class Player extends Component {
* @param {number} [value]
* The value to set the `Player`'s heigth to.
*
* @param {boolean} [skipListeners]
* Skip the playerresize event trigger
*
* @return {number}
* The current height of the `Player` when getting.
*/
height(value, skipListeners) {
return this.dimension('height', value, skipListeners);
height(value) {
return this.dimension('height', value);
}

/**
* A getter/setter for the `Player`'s width & height.
*
* @fires Player#playerresize
*
* @param {string} dimension
* This string can be:
* - 'width'
Expand All @@ -701,13 +694,10 @@ class Player extends Component {
* @param {number} [value]
* Value for dimension specified in the first argument.
*
* @param {boolean} [skipListeners]
* Skip the playerresize event trigger
*
* @return {number}
* The dimension arguments value when getting (width/height).
*/
dimension(dimension, value, skipListeners) {
dimension(dimension, value) {
const privDimension = dimension + '_';

if (value === undefined) {
Expand All @@ -730,17 +720,6 @@ class Player extends Component {

this[privDimension] = parsedVal;
this.updateStyleEl_();

// skipListeners allows us to avoid triggering the resize event when setting both width and height
if (this.isReady_ && !skipListeners) {
/**
* Triggered when the player is resized.
*
* @event Player#playerresize
* @type {EventTarget~Event}
*/
this.trigger('playerresize');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the event trigger would have worked in IE8 as well - since we're reverting this for the ResizeObserver and that's only available after IE8, is this a breaking change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are falling back to using an iframe for playerresize events when ResizeObserver isn't available, currently, that's basically everywhere, though, chrome will be shipping ResizeObserver soon.

}
}

/**
Expand Down Expand Up @@ -3483,6 +3462,10 @@ Player.prototype.options_ = {
notSupportedMessage: 'No compatible source was found for this media.'
};

if (!browser.IS_IE8) {
Player.prototype.options_.children.push('resizeManager');
}

[
/**
* Returns whether or not the player is in the "ended" state.
Expand Down
121 changes: 121 additions & 0 deletions src/js/resize-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @file resize-manager.js
*/
import window from 'global/window';
import { debounce } from './utils/fn.js';
import * as Events from './utils/events.js';
import mergeOptions from './utils/merge-options.js';
import Component from './component.js';

/**
* A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
*
* It'll either create an iframe and use a debounced resize handler on it or use the new {@link https://wicg.github.io/ResizeObserver/|ResizeObserver}.
*
* If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
* If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
* @example <caption>How to disable the resize manager</caption>
* const player = videojs('#vid', {
* resizeManager: false
* });
*
* @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
*
* @extends Component
*/
class ResizeManager extends Component {

/**
* Create the ResizeManager.
*
* @param {Object} player
* The `Player` that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of ResizeManager options.
*
* @param {Object} [options.ResizeObserver]
* A polyfill for ResizeObserver can be passed in here.
* If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
*/
constructor(player, options) {
let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window.ResizeObserver;

// if `null` was passed, we want to disable the ResizeObserver
if (options.ResizeObserver === null) {
RESIZE_OBSERVER_AVAILABLE = false;
}

// Only create an element when ResizeObserver isn't available
const options_ = mergeOptions({createEl: !RESIZE_OBSERVER_AVAILABLE}, options);

super(player, options_);

this.ResizeObserver = options.ResizeObserver || window.ResizeObserver;
this.loadListener_ = null;
this.resizeObserver_ = null;
this.debouncedHandler_ = debounce(() => {
this.resizeHandler();
}, 100, false, player);

if (RESIZE_OBSERVER_AVAILABLE) {
this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
this.resizeObserver_.observe(player.el());

} else {
this.loadListener_ = () => {
if (this.el_.contentWindow) {
Events.on(this.el_.contentWindow, 'resize', this.debouncedHandler_);
}
this.off('load', this.loadListener_);
};

this.on('load', this.loadListener_);
}
}

createEl() {
return super.createEl('iframe', {
className: 'vjs-resize-manager'
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the element will be created even when it's not needed. Should we suppress the creation of it when ResizeObserver is available?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line in the constructor will make it so no element is created if ResizeObserver is used: https://github.com/videojs/video.js/pull/4864/files#diff-8947c14c969c2d82a892c5fac0d0efd0R19

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't notice that. Carry on. 👍


/**
* Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
*
* @fires Player#playerresize
*/
resizeHandler() {
/**
* Called when the player size has changed
*
* @event Player#playerresize
* @type {EventTarget~Event}
*/
this.player_.trigger('playerresize');
}

dispose() {
if (this.resizeObserver_) {
this.resizeObserver_.unobserve(this.player_.el());
this.resizeObserver_.disconnect();
}

if (this.el_ && this.el_.contentWindow) {
Events.off(this.el_.contentWindow, 'resize', this.debouncedHandler_);
}

if (this.loadListener_) {
this.off('load', this.loadListener_);
}

this.ResizeObserver = null;
this.resizeObserver = null;
this.debouncedHandler_ = null;
this.loadListener_ = null;
}

}

Component.registerComponent('ResizeManager', ResizeManager);
export default ResizeManager;
52 changes: 52 additions & 0 deletions src/js/utils/fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module fn
*/
import { newGUID } from './guid.js';
import window from 'global/window';

/**
* Bind (a.k.a proxy or Context). A simple method for changing the context of a function
Expand Down Expand Up @@ -68,3 +69,54 @@ export const throttle = function(fn, wait) {

return throttled;
};

/**
* Creates a debounced function that delays invoking `func` until after `wait`
* milliseconds have elapsed since the last time the debounced function was
* invoked.
*
* Inspired by lodash and underscore implementations.
*
* @param {Function} func
* The function to wrap with debounce behavior.
*
* @param {number} wait
* The number of milliseconds to wait after the last invocation.
*
* @param {boolean} [immediate]
* Whether or not to invoke the function immediately upon creation.
*
* @param {Object} [context=window]
* The "context" in which the debounced function should debounce. For
* example, if this function should be tied to a Video.js player,
* the player can be passed here. Alternatively, defaults to the
* global `window` object.
*
* @return {Function}
* A debounced function.
*/
export const debounce = function(func, wait, immediate, context = window) {
let timeout;

/* eslint-disable consistent-this */
return function() {
const self = this;
const args = arguments;

let later = function() {
timeout = null;
later = null;
if (!immediate) {
func.apply(self, args);
}
};

if (!timeout && immediate) {
func.apply(self, args);
}

context.clearTimeout(timeout);
timeout = context.setTimeout(later, wait);
};
/* eslint-enable consistent-this */
};
4 changes: 4 additions & 0 deletions test/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ QUnit.test('should be able to access expected component API methods', function(a
assert.ok(comp.clearInterval, 'clearInterval exists');
assert.ok(comp.setTimeout, 'setTimeout exists');
assert.ok(comp.clearTimeout, 'clearTimeout exists');

comp.dispose();
});

QUnit.test('should be able to access expected MediaTech API methods', function(assert) {
Expand Down Expand Up @@ -289,4 +291,6 @@ QUnit.test('should extend Component', function(assert) {
const noMethods = new NoMethods({});

assert.ok(noMethods.on, 'should extend component with no methods or constructor');

myComponent.dispose();
});
3 changes: 2 additions & 1 deletion test/unit/button.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ QUnit.test('should localize its text', function(assert) {
assert.ok(el.nodeName.toLowerCase().match('button'));
assert.ok(el.innerHTML.match(/vjs-control-text"?>Juego/));
assert.equal(el.getAttribute('title'), 'Juego');
player.dispose();

testButton.dispose();
player.dispose();
});
Loading