1
0
mirror of https://github.com/videojs/video.js.git synced 2025-02-02 11:34:50 +02:00

feat: Add breakpoints option to support toggling classes based on player width. (#5471)

This adds a breakpoints option. By default, this option is false meaning this is an opt-in feature.

When passing true, it will use a default set of breakpoints. Or custom breakpoints can be passed if users do not like our breakpoints (or previously-existing style decisions).

- Add breakpoints option.
- Adds some new (currently unused) classes: vjs-layout-medium, vjs-layout-large, vjs-layout-x-large, and vjs-layout-huge.
- Add updateCurrentBreakpoint and currentBreakpoint methods to the player.
- Update css/components/_adaptive.scss
- Add sandbox/responsive.html.example

Closes videojs/video.js#4371
This commit is contained in:
Pat O'Neill 2018-10-10 15:30:20 -04:00 committed by Gary Katsevman
parent 7292253fe0
commit 51bd49f4bc
16 changed files with 473 additions and 45 deletions

View File

@ -16,7 +16,7 @@
don't currently support 'description' text tracks in any
useful way! Currently this means that iOS will not display
ANY text tracks -->
<video id="example_video_1" class="video-js vjs-default-skin" controls preload="none" width="640" height="360"
<video id="example_video_1" class="video-js" controls preload="none" width="640" height="360"
data-setup='{ "html5" : { "nativeTextTracks" : false } }'
poster="http://d2zihajmogu5jn.cloudfront.net/elephantsdream/poster.png">

View File

@ -9,7 +9,7 @@
</head>
<body>
<video id="example_video_1" class="video-js vjs-default-skin" controls preload="none" width="640" height="264" poster="http://vjs.zencdn.net/v/oceans.png" data-setup="{}">
<video id="example_video_1" class="video-js" controls preload="none" width="640" height="264" poster="http://vjs.zencdn.net/v/oceans.png" data-setup="{}">
<source src="http://vjs.zencdn.net/v/oceans.mp4" type="video/mp4">
<source src="http://vjs.zencdn.net/v/oceans.webm" type="video/webm">
<source src="http://vjs.zencdn.net/v/oceans.ogv" type="video/ogg">

View File

@ -163,6 +163,48 @@ Prevents the player from running the autoSetup for media elements with `data-set
> **Note**: this must be set globally with `videojs.options.autoSetup = false` in the same tick as videojs source is loaded to take effect.
### `breakpoints`
> Type: `boolean|Array`, Default: `false`
Set layout breakpoints that will configure how class names are toggled on the player to adjust the UI based on the player's dimensions.
By default, no breakpoints are supported, but passing `true` will set some sensible default breakpoints:
Class Name | Width Range
---------------------|------------
`vjs-layout-tiny` | 0-210
`vjs-layout-x-small` | 211-320
`vjs-layout-small` | 321-425
`vjs-layout-medium` | 426-768
`vjs-layout-large` | 769-1440
`vjs-layout-x-large` | 1441-2560
`vjs-layout-huge` | 2561+
While the class names cannot be changed, the width ranges can be configured via an object like this:
```js
breakpoints: {
tiny: 300,
xsmall: 400,
small: 500,
medium: 600,
large: 700,
xlarge: 800,
huge: 900
}
```
* The _keys_ of the `breakpoints` object are derived from the associated class names by removing the `vjs-layout-` prefix and any `-` characters.
* The _values_ of the `breakpoints` object define the max width for a range.
* Not all keys need to be defined. You can easily override a single breakpoint by passing an object with one key/value pair! Customized breakpoints will be merged with default breakpoints when the player is created.
When the player's size changes, the merged breakpoints will be inspected in the size order until a matching breakpoint is found.
That breakpoint's associated class name will be added as a class to the player. The previous breakpoint's class will be removed.
See the file `sandbox/responsive.html.example` for an example of a fluid/responsive player using the default breakpoints.
### `children`
> Type: `Array|Object`

View File

@ -22,7 +22,7 @@
<pre>open http://localhost:9999/sandbox/index.html</pre>
</div>
<video id="vid1" class="video-js vjs-default-skin" lang="en" controls preload="auto" width="640" height="360" poster="//d2zihajmogu5jn.cloudfront.net/elephantsdream/poster.png">
<video id="vid1" class="video-js" lang="en" controls preload="auto" width="640" height="360" poster="//d2zihajmogu5jn.cloudfront.net/elephantsdream/poster.png">
<source src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/ed_hd.mp4" type="video/mp4">
<source src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/ed_hd.ogg" type="video/ogg">
<track kind="captions" src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/captions.en.vtt" srclang="en" label="English">

View File

@ -21,7 +21,7 @@
don't currently support 'description' text tracks in any
useful way! Currently this means that iOS will not display
ANY text tracks -->
<video id="example_video_1" class="video-js vjs-default-skin" controls preload="none" width="640" height="360"
<video id="example_video_1" class="video-js" controls preload="none" width="640" height="360"
data-setup='{ "html5" : { "nativeTextTracks" : false } }'
poster="http://d2zihajmogu5jn.cloudfront.net/elephantsdream/poster.png">

View File

@ -23,7 +23,7 @@
<pre>open http://localhost:9999/sandbox/flash.html</pre>
</div>
<video id="vid1" class="video-js vjs-default-skin" controls preload="auto" width="640" height="264"
<video id="vid1" class="video-js" controls preload="auto" width="640" height="264"
poster="http://vjs.zencdn.net/v/oceans.png"
data-setup=''>
<source src="http://vjs.zencdn.net/v/oceans.mp4" type="video/mp4">

View File

@ -3,17 +3,8 @@
<head>
<meta charset="utf-8" />
<title>Video.js Sandbox</title>
<!-- Load the source files -->
<link href="../dist/video-js.css" rel="stylesheet" type="text/css">
<script src="../dist/video.js"></script>
<script src="../node_modules/videojs-flash/dist/videojs-flash.js"></script>
<!-- Set the location of the flash SWF -->
<script>
videojs.options.flash.swf = '../node_modules/videojs-flash/node_modules/videojs-swf/dist/video-js.swf';
</script>
</head>
<body>
<div style="background-color:#eee; border: 1px solid #777; padding: 10px; margin-bottom: 20px; font-size: .8em; line-height: 1.5em; font-family: Verdana, sans-serif;">
@ -23,20 +14,23 @@
<pre>open http://localhost:9999/sandbox/index.html</pre>
</div>
<video id="vid1" class="video-js vjs-default-skin" controls preload="auto" width="640" height="264"
poster="http://vjs.zencdn.net/v/oceans.png"
data-setup='{}'>
<video-js
id="vid1"
controls
preload="auto"
width="640"
height="264"
poster="http://vjs.zencdn.net/v/oceans.png">
<source src="http://vjs.zencdn.net/v/oceans.mp4" type="video/mp4">
<source src="http://vjs.zencdn.net/v/oceans.webm" type="video/webm">
<source src="http://vjs.zencdn.net/v/oceans.ogv" type="video/ogg">
<track kind="captions" src="../docs/examples/shared/example-captions.vtt" srclang="en" label="English">
<p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
</video>
</video-js>
<script>
var vid = document.getElementById("vid1");
var vid = document.getElementById('vid1');
var player = videojs(vid);
</script>
</body>

View File

@ -47,7 +47,7 @@
</head>
<body>
<video id="vid1" class="video-js vjs-default-skin" controls preload="auto" width="640" height="264" data-setup=''>
<video id="vid1" class="video-js" controls preload="auto" width="640" height="264" data-setup=''>
<!--
<source src="http://video-js.zencoder.com/oceans-clip.mp4" type='video/mp4'>
<source src="http://video-js.zencoder.com/oceans-clip.webm" type='video/webm'>

View File

@ -17,7 +17,7 @@
<body>
<p style="background-color:#eee; border: 1px solid #777; padding: 10px; font-size: .8em; line-height: 1.5em; font-family: Verdana, sans-serif;">This page shows you how to create, register and initialize a Video.js plugin.</p>
<video id="vid1" class="video-js vjs-default-skin" controls preload="auto" width="640" height="264" poster="http://vjs.zencdn.net/v/oceans.png">
<video id="vid1" class="video-js" controls preload="auto" width="640" height="264" poster="http://vjs.zencdn.net/v/oceans.png">
<source src="http://vjs.zencdn.net/v/oceans.mp4" type="video/mp4">
<source src="http://vjs.zencdn.net/v/oceans.webm" type="video/webm">
<source src="http://vjs.zencdn.net/v/oceans.ogv" type="video/ogg">

View File

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Video.js Sandbox - Responsive</title>
<link href="../dist/video-js.css" rel="stylesheet" type="text/css">
<script src="../dist/video.js"></script>
<style type="text/css">
body {
margin: 0;
padding: 0;
}
.breakpoints, .video-js, table {
margin: 1em 0;
}
table {
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 0.5em 1em;
}
tbody tr {
color: #999;
}
tbody tr:first-child {
color: #000;
}
.breakpoints div {
background-color: red;
color: white;
padding: 0.5em 1em;
}
@media (max-width: 210px) {
.breakpoints .tiny {
background-color: green;
}
}
@media (min-width: 211px) and (max-width: 320px) {
.breakpoints .x-small {
background-color: green;
}
}
@media (min-width: 321px) and (max-width: 425px) {
.breakpoints .small {
background-color: green;
}
}
@media (min-width: 426px) and (max-width: 768px) {
.breakpoints .medium {
background-color: green;
}
}
@media (min-width: 769px) and (max-width: 1440px) {
.breakpoints .large {
background-color: green;
}
}
@media (min-width: 1441px) and (max-width: 2560px) {
.breakpoints .x-large {
background-color: green;
}
}
@media (min-width: 2561px) {
.breakpoints .huge {
background-color: green;
}
}
</style>
</head>
<body>
<p>
The following boxes indicate which breakpoint should be applied to the
player when it fills the width of its containing viewport.
</p>
<p>
Use these to validate that the default breakpoints match up with how
CSS media queries work.
</p>
<p>
<b>Because these bars are updated by CSS, they will change before the <code>playerresize</code> event occurs!</b>
</p>
<div class="breakpoints">
<div class="tiny">vjs-layout-tiny (0px-210px)</div>
<div class="x-small">vjs-layout-x-small (211px-320px)</div>
<div class="small">vjs-layout-small (321px-425px)</div>
<div class="medium">vjs-layout-medium (426px-768px)</div>
<div class="large">vjs-layout-large (769px-1440px)</div>
<div class="x-large">vjs-layout-x-large (1441px-2560px)</div>
<div class="huge">vjs-layout-huge (2561px+)</div>
</div>
<video-js
class="vjs-fluid"
controls
preload="auto"
poster="http://vjs.zencdn.net/v/oceans.png">
<source src="http://vjs.zencdn.net/v/oceans.mp4" type="video/mp4">
<source src="http://vjs.zencdn.net/v/oceans.webm" type="video/webm">
<source src="http://vjs.zencdn.net/v/oceans.ogv" type="video/ogg">
<track kind="captions" src="../docs/examples/shared/example-captions.vtt" srclang="en" label="English">
<p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
</video-js>
<p>
Each time the player size changes, a row is prepended to this table.
</p>
<table>
<thead>
<tr>
<th>Class</th>
<th>Player Width</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
var vid = document.querySelector('video-js');
var player = videojs(vid, {breakpoints: true});
var tbody = document.querySelector('table tbody');
player.on('playerresize', function() {
var values = {
breakpoint: player.currentBreakpoint(),
className: player.el().className.match(/vjs-layout-([a-z\-]+)/)[0],
playerWidth: player.currentWidth()
};
videojs.log('playerresize', values);
var tr = document.createElement('tr');
tr.innerHTML = '<td>' +
values.className +
'</td><td>' +
values.playerWidth +
'</td>';
tbody.insertBefore(tr, tbody.firstChild);
});
</script>
</body>
</html>

View File

@ -2,14 +2,18 @@
// - Play button
// - Fullscreen Button
.video-js.vjs-layout-tiny:not(.vjs-fullscreen) {
.vjs-custom-control-spacer { @include flex(auto); }
.vjs-custom-control-spacer {
@include flex(auto);
display: block;
}
&.vjs-no-flex .vjs-custom-control-spacer { width: auto; }
.vjs-current-time, .vjs-time-divider, .vjs-duration, .vjs-remaining-time,
.vjs-playback-rate, .vjs-progress-control,
.vjs-mute-control, .vjs-volume-control,
.vjs-mute-control, .vjs-volume-control, .vjs-volume-panel,
.vjs-chapters-button, .vjs-descriptions-button, .vjs-captions-button,
.vjs-subtitles-button, .vjs-audio-button { display: none; }
.vjs-subtitles-button, .vjs-subs-caps-button, .vjs-audio-button { display: none; }
}
// When the player is x-small, display nothing but:
@ -19,9 +23,9 @@
.video-js.vjs-layout-x-small:not(.vjs-fullscreen) {
.vjs-current-time, .vjs-time-divider, .vjs-duration, .vjs-remaining-time,
.vjs-playback-rate,
.vjs-mute-control, .vjs-volume-control,
.vjs-mute-control, .vjs-volume-control, .vjs-volume-panel,
.vjs-chapters-button, .vjs-descriptions-button, .vjs-captions-button,
.vjs-subtitles-button, .vjs-audio-button { display: none; }
.vjs-subtitles-button, .vjs-subs-caps-button, .vjs-audio-button { display: none; }
}
@ -29,12 +33,12 @@
// - Play button
// - Progress bar
// - Volume menu button
// - Captions Button
// - Subs-Caps Button
// - Fullscreen button
.video-js.vjs-layout-small:not(.vjs-fullscreen) {
.vjs-current-time, .vjs-time-divider, .vjs-duration, .vjs-remaining-time,
.vjs-playback-rate,
.vjs-mute-control, .vjs-volume-control,
.vjs-mute-control, .vjs-volume-control, .vjs-volume-panel,
.vjs-chapters-button, .vjs-descriptions-button, .vjs-captions-button,
.vjs-subtitles-button .vjs-audio-button { display: none; }
.vjs-subtitles-button, .vjs-audio-button { display: none; }
}

View File

@ -960,8 +960,9 @@ class Component {
}
/**
* Get the width or the height of the `Component` elements computed style. Uses
* `window.getComputedStyle`.
* Get the computed width or the height of the component's element.
*
* Uses `window.getComputedStyle`.
*
* @param {string} widthOrHeight
* A string containing 'width' or 'height'. Whichever one you want to get.
@ -1012,11 +1013,13 @@ class Component {
*/
/**
* Get an object that contains width and height values of the `Component`s
* computed style.
* Get an object that contains computed width and height values of the
* component's element.
*
* Uses `window.getComputedStyle`.
*
* @return {Component~DimensionObject}
* The dimensions of the components element
* The computed dimensions of the component's element.
*/
currentDimensions() {
return {
@ -1026,20 +1029,24 @@ class Component {
}
/**
* Get the width of the `Component`s computed style. Uses `window.getComputedStyle`.
* Get the computed width of the component's element.
*
* @return {number} width
* The width of the `Component`s computed style.
* Uses `window.getComputedStyle`.
*
* @return {number}
* The computed width of the component's element.
*/
currentWidth() {
return this.currentDimension('width');
}
/**
* Get the height of the `Component`s computed style. Uses `window.getComputedStyle`.
* Get the computed height of the component's element.
*
* @return {number} height
* The height of the `Component`s computed style.
* Uses `window.getComputedStyle`.
*
* @return {number}
* The computed height of the component's element.
*/
currentHeight() {
return this.currentDimension('height');

View File

@ -22,7 +22,7 @@ import * as stylesheet from './utils/stylesheet.js';
import FullscreenApi from './fullscreen-api.js';
import MediaError from './media-error.js';
import safeParseTuple from 'safe-json-parse/tuple';
import {assign} from './utils/obj';
import {assign, isObject} from './utils/obj';
import mergeOptions from './utils/merge-options.js';
import {silencePromise} from './utils/promise';
import textTrackConverter from './tracks/text-track-list-converter.js';
@ -240,6 +240,41 @@ const TECH_EVENTS_QUEUE = {
seeked: 'Seeked'
};
const BREAKPOINT_ORDER = [
'tiny',
'xsmall',
'small',
'medium',
'large',
'xlarge',
'huge'
];
const BREAKPOINT_CLASSES = {};
// grep: vjs-layout-tiny
// grep: vjs-layout-x-small
// grep: vjs-layout-small
// grep: vjs-layout-medium
// grep: vjs-layout-large
// grep: vjs-layout-x-large
// grep: vjs-layout-huge
BREAKPOINT_ORDER.forEach(k => {
const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
});
const DEFAULT_BREAKPOINTS = {
tiny: 210,
xsmall: 320,
small: 425,
medium: 768,
large: 1440,
xlarge: 2560,
huge: Infinity
};
/**
* An instance of the `Player` class is created when any of the Video.js setup methods
* are used to initialize a video.
@ -487,6 +522,8 @@ class Player extends Component {
this.on('fullscreenchange', this.handleFullscreenChange_);
this.on('stageclick', this.handleStageClick_);
this.setBreakpoints(this.options_.breakpoints);
this.changingSrc_ = false;
this.playWaitingForReady_ = false;
this.playOnLoadstart_ = null;
@ -3675,6 +3712,96 @@ class Player extends Component {
return modal;
}
/**
* Change layout breakpoint classes when the player resizes.
*
* @private
*/
updateCurrentBreakpoint_() {
if (!this.breakpoints_) {
return;
}
const currentBreakpoint = this.currentBreakpoint();
const currentWidth = this.currentWidth();
for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
const candidateBreakpoint = BREAKPOINT_ORDER[i];
const maxWidth = this.breakpoints_[candidateBreakpoint];
if (currentWidth <= maxWidth) {
// The current breakpoint did not change, nothing to do.
if (currentBreakpoint === candidateBreakpoint) {
return;
}
// Only remove a class if there is a current breakpoint.
if (currentBreakpoint) {
this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
}
this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
this.breakpoint_ = candidateBreakpoint;
break;
}
}
}
/**
* Set layout breakpoints on the player.
*
* @param {boolean|Object} breakpoints
* A boolean indicating whether breakpoints should or should not be
* used - or an object describing custom max widths for the supported
* breakpoints.
*/
setBreakpoints(breakpoints) {
const hadBps = Boolean(this.breakpoints_);
if (!breakpoints) {
this.breakpoint_ = null;
this.breakpoints_ = null;
this.off('playerresize', this.updateCurrentBreakpoint_);
return;
}
this.breakpoints_ = assign({}, DEFAULT_BREAKPOINTS);
if (isObject(breakpoints)) {
assign(this.breakpoints_, breakpoints);
}
this.updateCurrentBreakpoint_();
// If we were not previously supporting breakpoints, add a listener.
if (!hadBps) {
this.on('playerresize', this.updateCurrentBreakpoint_);
}
}
/**
* Get current layout breakpoints for the player.
*
* @return {Object}
* An object describing the current layout breakpoints and their
* max widths.
*/
getBreakpoints() {
return this.breakpoints_ && assign(this.breakpoints_);
}
/**
* Get current layout breakpoint name, if any.
*
* @return {string|null}
* If there is currently a layout breakpoint set, returns a the key
* from the breakpoints object matching it. Otherwise, returns `null`.
*/
currentBreakpoint() {
return this.breakpoint_;
}
/**
* Gets tag settings
*
@ -3874,7 +4001,9 @@ Player.prototype.options_ = {
languages: {},
// Default message to show when a video cannot be played.
notSupportedMessage: 'No compatible source was found for this media.'
notSupportedMessage: 'No compatible source was found for this media.',
breakpoints: false
};
[

View File

@ -265,7 +265,7 @@ function testHelperMakeTag() {
const videoTag = document.createElement('video');
videoTag.id = 'example_1';
videoTag.className = 'video-js vjs-default-skin';
videoTag.className = 'video-js';
return videoTag;
}

View File

@ -0,0 +1,92 @@
/* eslint-env qunit */
import sinon from 'sinon';
import TestHelpers from './test-helpers';
import {assign} from '../../src/js/utils/obj';
const getExpectedBreakpoints = (o) => assign({}, {
tiny: 210,
xsmall: 320,
small: 425,
medium: 768,
large: 1440,
xlarge: 2560,
huge: Infinity
}, o);
QUnit.module('Player: Breakpoints', {
beforeEach() {
this.clock = sinon.useFakeTimers();
this.player = TestHelpers.makePlayer({});
},
afterEach() {
this.player.dispose();
this.clock.restore();
}
});
QUnit.test('breakpoints are disabled by default', function(assert) {
assert.strictEqual(this.player.getBreakpoints(), null, 'no breakpoints defined');
assert.strictEqual(this.player.currentBreakpoint(), null, 'no current breakpoint set');
});
QUnit.test('setting default breakpoints', function(assert) {
this.player.setBreakpoints(true);
assert.deepEqual(this.player.getBreakpoints(), getExpectedBreakpoints(), 'breakpoints defined');
// Player should be 300x150 by default.
assert.strictEqual(this.player.currentBreakpoint(), 'xsmall', 'current breakpoint set');
});
QUnit.test('setting custom breakpoints', function(assert) {
this.player.setBreakpoints({tiny: 300});
assert.deepEqual(this.player.getBreakpoints(), getExpectedBreakpoints({tiny: 300}), 'breakpoints defined');
// Player should be 300x150 by default.
assert.strictEqual(this.player.currentBreakpoint(), 'tiny', 'current breakpoint set');
});
QUnit.test('setting breakpoints via option', function(assert) {
const player = TestHelpers.makePlayer({breakpoints: {tiny: 300}});
assert.deepEqual(player.getBreakpoints(), getExpectedBreakpoints({tiny: 300}), 'breakpoints defined');
// Player should be 300x150 by default.
assert.strictEqual(player.currentBreakpoint(), 'tiny', 'current breakpoint set');
});
QUnit.test('changing the player size triggers breakpoints', function(assert) {
let currentWidth;
this.player.setBreakpoints(true);
this.player.currentWidth = () => currentWidth;
currentWidth = 200;
this.player.trigger('playerresize');
assert.strictEqual(this.player.currentBreakpoint(), 'tiny', 'current breakpoint is correct');
currentWidth = 300;
this.player.trigger('playerresize');
assert.strictEqual(this.player.currentBreakpoint(), 'xsmall', 'current breakpoint is correct');
currentWidth = 400;
this.player.trigger('playerresize');
assert.strictEqual(this.player.currentBreakpoint(), 'small', 'current breakpoint is correct');
currentWidth = 600;
this.player.trigger('playerresize');
assert.strictEqual(this.player.currentBreakpoint(), 'medium', 'current breakpoint is correct');
currentWidth = 900;
this.player.trigger('playerresize');
assert.strictEqual(this.player.currentBreakpoint(), 'large', 'current breakpoint is correct');
currentWidth = 1600;
this.player.trigger('playerresize');
assert.strictEqual(this.player.currentBreakpoint(), 'xlarge', 'current breakpoint is correct');
currentWidth = 3000;
this.player.trigger('playerresize');
assert.strictEqual(this.player.currentBreakpoint(), 'huge', 'current breakpoint is correct');
});

View File

@ -7,7 +7,7 @@ const TestHelpers = {
const videoTag = document.createElement('video');
videoTag.id = 'example_1';
videoTag.className = 'video-js vjs-default-skin';
videoTag.className = 'video-js';
return videoTag;
},