1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-05 19:42:38 +02:00
This commit is contained in:
Daniel Neto 2023-07-02 19:30:57 -03:00
parent ed9048ec37
commit 14f492d68f
60 changed files with 27146 additions and 0 deletions

30
node_modules/videojs-playlist-ui/CONTRIBUTING.md generated vendored Normal file
View file

@ -0,0 +1,30 @@
# CONTRIBUTING
We welcome contributions from everyone!
## Getting Started
Make sure you have Node.js 4.8 or higher and npm installed.
1. Fork this repository and clone your fork
1. Install dependencies: `npm install`
1. Run a development server: `npm start`
### Making Changes
Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship.
When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository.
### Running Tests
Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma].
- In all available and supported browsers: `npm test`
- In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc.
- While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local]
[karma]: http://karma-runner.github.io/
[local]: http://localhost:9999/test/
[conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md

13
node_modules/videojs-playlist-ui/LICENSE generated vendored Normal file
View file

@ -0,0 +1,13 @@
Copyright Brightcove, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

109
node_modules/videojs-playlist-ui/README.md generated vendored Normal file
View file

@ -0,0 +1,109 @@
# videojs-playlist-ui
[![NPM](https://nodei.co/npm/videojs-playlist-ui.png?downloads=true&downloadRank=true)](https://nodei.co/npm/videojs-playlist-ui/)
A playlist video picker for video.js and videojs-playlist
Maintenance Status: Stable
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Getting Started](#getting-started)
- [Root Element](#root-element)
- [Using Automatic Discovery (default, example)](#using-automatic-discovery-default-example)
- [Using a Custom Class (example)](#using-a-custom-class-example)
- [Using a Custom Element (example)](#using-a-custom-element-example)
- [Other Options](#other-options)
- [`className`](#classname)
- [playOnSelect](#playonselect)
- [Playlists and Advertisements](#playlists-and-advertisements)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Getting Started
Include the plugin script in your page, and a placeholder list element with the class `vjs-playlist` to house the playlist menu:
```html
<!-- Include the playlist menu styles somewhere in your page -->
<link href="videojs-playlist-ui.css" rel="stylesheet">
<!-- Your player will be created here: -->
<video-js data-setup='{}' controls></video-js>
<!-- The playlist menu will be built automatically in here -->
<div class="vjs-playlist"></div>
<!-- Include video.js, the videojs-playlist plugin and this plugin -->
<script src="video.js"></script>
<script src="videojs-playlist.js"></script>
<script src="videojs-playlist-ui.js"></script>
<script>
// Initialize the player
const player = videojs(document.querySelector('video-js'));
// Initialize the plugin and render the playlist
player.playlistUi();
</script>
```
There's also a [working example](example.html) of the plugin you can check out if you're having trouble.
## Root Element
Before this plugin will work at all, it needs an element in the DOM to which to attach itself. There are three ways to find or provide this element.
> **NOTE:** In v2.x of this plugin, the root element was expected to be a list element (i.e., `<ol>` or `<ul>`). As of v3.x, the plugin creates a list; so, this root element _must_ be a non-list container element (e.g., `<div>`).
### Using Automatic Discovery (default, [example](example.html))
By default, the plugin will search for the first element in the DOM with the `vjs-playlist` class.
To defend against problems caused by multiple playlist players on a page, the plugin will only use an element with the `vjs-playlist` class if that element has not been used by another player's playlist.
### Using a Custom Class ([example](example-custom-class.html))
A custom `className` option can be passed to override the class the plugin will search for to find the root element. The same defense against multiple playlist players is reused in this case.
```js
player.playlistUi({
className: 'hello-world'
});
```
### Using a Custom Element ([example](example-custom-element.html))
A custom element can be passed using the `el` option to explicitly define a specific root element.
```js
player.playlistUi({
el: document.getElementById('hello-world')
});
```
## Other Options
The options passed to the plugin are passed to the internal `PlaylistMenu` [video.js Component][components]; so, you may pass in [any option][components-options] that is accepted by a component.
In addition, the options object may contain the following specialized properties:
#### `className`
Type: `string`
Default: `"vjs-playlist"`
As mentioned [above](#using-a-custom-class), the name of the class to search for to populate the playlist menu.
#### playOnSelect
Type: `boolean`
Default: `false`
The default behavior is that the play state is expected to stay the same between videos. If the player is playing when switching playlist items, continue playing. If paused, stay paused.
When this boolean is set to `true`, clicking on the playlist menu items will always play the video.
## Playlists and Advertisements
The `PlaylistMenu` automatically adapts to ad integrations based on [videojs-contrib-ads][contrib-ads]. When a linear ad is being played, the menu will darken and stop responding to click or touch events. If you'd prefer to allow your viewers to change videos during ad playback, you can override this behavior through CSS. You will also need to make sure that your ad integration is properly cancelled and cleaned up before switching -- consult the documentation for your ad library for details on how to do that.
[components]: https://videojs.com/guides/components/
[components-options]: https://videojs.com/guides/components/#using-options
[contrib-ads]: https://github.com/videojs/videojs-contrib-ads

5
node_modules/videojs-playlist-ui/dist/lang/ar.js generated vendored Normal file
View file

@ -0,0 +1,5 @@
videojs.addLanguage('ar', {
"Now Playing": "قيد التشغيل الآن",
"Up Next": "التالي",
"Untitled Video": "مقطع فيديو بدون عنوان"
});

5
node_modules/videojs-playlist-ui/dist/lang/de.js generated vendored Normal file
View file

@ -0,0 +1,5 @@
videojs.addLanguage('de', {
"Now Playing": "Aktuelle Wiedergabe",
"Up Next": "Als Nächstes",
"Untitled Video": "Video ohne Titel"
});

5
node_modules/videojs-playlist-ui/dist/lang/en.js generated vendored Normal file
View file

@ -0,0 +1,5 @@
videojs.addLanguage('en', {
"Now Playing": "Now Playing",
"Up Next": "Up Next",
"Untitled Video": "Untitled Video"
});

5
node_modules/videojs-playlist-ui/dist/lang/es.js generated vendored Normal file
View file

@ -0,0 +1,5 @@
videojs.addLanguage('es', {
"Now Playing": "Reproduciendo",
"Up Next": "A continuación:",
"Untitled Video": "Vídeo sin título"
});

5
node_modules/videojs-playlist-ui/dist/lang/fr.js generated vendored Normal file
View file

@ -0,0 +1,5 @@
videojs.addLanguage('fr', {
"Now Playing": "En cours de lecture",
"Up Next": "À suivre",
"Untitled Video": "Vidéo sans titre"
});

5
node_modules/videojs-playlist-ui/dist/lang/ja.js generated vendored Normal file
View file

@ -0,0 +1,5 @@
videojs.addLanguage('ja', {
"Now Playing": "現在再生中",
"Up Next": "次の動画",
"Untitled Video": "無題の動画"
});

5
node_modules/videojs-playlist-ui/dist/lang/ko.js generated vendored Normal file
View file

@ -0,0 +1,5 @@
videojs.addLanguage('ko', {
"Now Playing": "지금 재생 중",
"Up Next": "다음",
"Untitled Video": "제목 없는 비디오"
});

View file

@ -0,0 +1,5 @@
videojs.addLanguage('zh-Hans', {
"Now Playing": "正在播放",
"Up Next": "播放下一个",
"Untitled Video": "无标题视频"
});

View file

@ -0,0 +1,5 @@
videojs.addLanguage('zh-Hant', {
"Now Playing": "正在播放",
"Up Next": "播放下一個",
"Untitled Video": "未命名視訊"
});

View file

@ -0,0 +1,467 @@
/*! @name videojs-playlist-ui @version 5.0.0 @license Apache-2.0 */
'use strict';
var document = require('global/document');
var videojs = require('video.js');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var document__default = /*#__PURE__*/_interopDefaultLegacy(document);
var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
var version = "5.0.0";
const Component$1 = videojs__default["default"].getComponent('Component');
const createThumbnail = function (thumbnail) {
if (!thumbnail) {
const placeholder = document__default["default"].createElement('div');
placeholder.className = 'vjs-playlist-thumbnail vjs-playlist-thumbnail-placeholder';
return placeholder;
}
const picture = document__default["default"].createElement('picture');
picture.className = 'vjs-playlist-thumbnail';
if (typeof thumbnail === 'string') {
// simple thumbnails
const img = document__default["default"].createElement('img');
img.loading = 'lazy';
img.src = thumbnail;
img.alt = '';
picture.appendChild(img);
} else {
// responsive thumbnails
// additional variations of a <picture> are specified as
// <source> elements
for (let i = 0; i < thumbnail.length - 1; i++) {
const variant = thumbnail[i];
const source = document__default["default"].createElement('source');
// transfer the properties of each variant onto a <source>
for (const prop in variant) {
source[prop] = variant[prop];
}
picture.appendChild(source);
}
// the default version of a <picture> is specified by an <img>
const variant = thumbnail[thumbnail.length - 1];
const img = document__default["default"].createElement('img');
img.loading = 'lazy';
img.alt = '';
for (const prop in variant) {
img[prop] = variant[prop];
}
picture.appendChild(img);
}
return picture;
};
class PlaylistMenuItem extends Component$1 {
constructor(player, playlistItem, settings) {
if (!playlistItem.item) {
throw new Error('Cannot construct a PlaylistMenuItem without an item option');
}
playlistItem.showDescription = settings.showDescription;
super(player, playlistItem);
this.item = playlistItem.item;
this.playOnSelect = settings.playOnSelect;
this.emitTapEvents();
this.on(['click', 'tap'], this.switchPlaylistItem_);
this.on('keydown', this.handleKeyDown_);
}
handleKeyDown_(event) {
// keycode 13 is <Enter>
// keycode 32 is <Space>
if (event.which === 13 || event.which === 32) {
this.switchPlaylistItem_();
}
}
switchPlaylistItem_(event) {
this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item));
if (this.playOnSelect) {
this.player_.play();
}
}
createEl() {
const li = document__default["default"].createElement('li');
const item = this.options_.item;
const showDescription = this.options_.showDescription;
if (typeof item.data === 'object') {
const dataKeys = Object.keys(item.data);
dataKeys.forEach(key => {
const value = item.data[key];
li.dataset[key] = value;
});
}
li.className = 'vjs-playlist-item';
li.setAttribute('tabIndex', 0);
// Thumbnail image
this.thumbnail = createThumbnail(item.thumbnail);
li.appendChild(this.thumbnail);
// Duration
if (item.duration) {
const duration = document__default["default"].createElement('time');
const time = videojs__default["default"].time.formatTime(item.duration);
duration.className = 'vjs-playlist-duration';
duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S');
duration.appendChild(document__default["default"].createTextNode(time));
li.appendChild(duration);
}
// Now playing
const nowPlayingEl = document__default["default"].createElement('span');
const nowPlayingText = this.localize('Now Playing');
nowPlayingEl.className = 'vjs-playlist-now-playing-text';
nowPlayingEl.appendChild(document__default["default"].createTextNode(nowPlayingText));
nowPlayingEl.setAttribute('title', nowPlayingText);
this.thumbnail.appendChild(nowPlayingEl);
// Title container contains title and "up next"
const titleContainerEl = document__default["default"].createElement('div');
titleContainerEl.className = 'vjs-playlist-title-container';
this.thumbnail.appendChild(titleContainerEl);
// Up next
const upNextEl = document__default["default"].createElement('span');
const upNextText = this.localize('Up Next');
upNextEl.className = 'vjs-up-next-text';
upNextEl.appendChild(document__default["default"].createTextNode(upNextText));
upNextEl.setAttribute('title', upNextText);
titleContainerEl.appendChild(upNextEl);
// Video title
const titleEl = document__default["default"].createElement('cite');
const titleText = item.name || this.localize('Untitled Video');
titleEl.className = 'vjs-playlist-name';
titleEl.appendChild(document__default["default"].createTextNode(titleText));
titleEl.setAttribute('title', titleText);
titleContainerEl.appendChild(titleEl);
// Populate thumbnails alt with the video title
this.thumbnail.getElementsByTagName('img').alt = titleText;
// We add thumbnail video description only if specified in playlist options
if (showDescription) {
const descriptionEl = document__default["default"].createElement('div');
const descriptionText = item.description || '';
descriptionEl.className = 'vjs-playlist-description';
descriptionEl.appendChild(document__default["default"].createTextNode(descriptionText));
descriptionEl.setAttribute('title', descriptionText);
titleContainerEl.appendChild(descriptionEl);
}
return li;
}
}
videojs__default["default"].registerComponent('PlaylistMenuItem', PlaylistMenuItem);
// we don't add `vjs-playlist-now-playing` in addSelectedClass
// so it won't conflict with `vjs-icon-play
// since it'll get added when we mouse out
const addSelectedClass = function (el) {
el.addClass('vjs-selected');
};
const removeSelectedClass = function (el) {
el.removeClass('vjs-selected');
if (el.thumbnail) {
videojs__default["default"].dom.removeClass(el.thumbnail, 'vjs-playlist-now-playing');
}
};
const upNext = function (el) {
el.addClass('vjs-up-next');
};
const notUpNext = function (el) {
el.removeClass('vjs-up-next');
};
const Component = videojs__default["default"].getComponent('Component');
class PlaylistMenu extends Component {
constructor(player, options) {
super(player, options);
this.items = [];
if (options.horizontal) {
this.addClass('vjs-playlist-horizontal');
} else {
this.addClass('vjs-playlist-vertical');
}
// If CSS pointer events aren't supported, we have to prevent
// clicking on playlist items during ads with slightly more
// invasive techniques. Details in the stylesheet.
if (options.supportsCssPointerEvents) {
this.addClass('vjs-csspointerevents');
}
this.createPlaylist_();
if (!videojs__default["default"].browser.TOUCH_ENABLED) {
this.addClass('vjs-mouse');
}
this.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], e => {
// The playlistadd and playlistremove events are handled separately. These
// also fire the playlistchange event with an `action` property, so can
// exclude them here.
if (e.type === 'playlistchange' && ['add', 'remove'].includes(e.action)) {
return;
}
this.update();
});
this.on(player, ['playlistadd'], e => this.addItems_(e.index, e.count));
this.on(player, ['playlistremove'], e => this.removeItems_(e.index, e.count));
// Keep track of whether an ad is playing so that the menu
// appearance can be adapted appropriately
this.on(player, 'adstart', () => {
this.addClass('vjs-ad-playing');
});
this.on(player, 'adend', () => {
this.removeClass('vjs-ad-playing');
});
this.on('dispose', () => {
this.empty_();
player.playlistMenu = null;
});
this.on(player, 'dispose', () => {
this.dispose();
});
}
createEl() {
return videojs__default["default"].dom.createEl('div', {
className: this.options_.className
});
}
empty_() {
if (this.items && this.items.length) {
this.items.forEach(i => i.dispose());
this.items.length = 0;
}
}
createPlaylist_() {
const playlist = this.player_.playlist() || [];
let list = this.el_.querySelector('.vjs-playlist-item-list');
let overlay = this.el_.querySelector('.vjs-playlist-ad-overlay');
if (!list) {
list = document__default["default"].createElement('ol');
list.className = 'vjs-playlist-item-list';
this.el_.appendChild(list);
}
this.empty_();
// create new items
for (let i = 0; i < playlist.length; i++) {
const item = new PlaylistMenuItem(this.player_, {
item: playlist[i]
}, this.options_);
this.items.push(item);
list.appendChild(item.el_);
}
// Inject the ad overlay. We use this element to block clicks during ad
// playback and darken the menu to indicate inactivity
if (!overlay) {
overlay = document__default["default"].createElement('li');
overlay.className = 'vjs-playlist-ad-overlay';
list.appendChild(overlay);
} else {
// Move overlay to end of list
list.appendChild(overlay);
}
// select the current playlist item
const selectedIndex = this.player_.playlist.currentItem();
if (this.items.length && selectedIndex >= 0) {
addSelectedClass(this.items[selectedIndex]);
const thumbnail = this.items[selectedIndex].$('.vjs-playlist-thumbnail');
if (thumbnail) {
videojs__default["default"].dom.addClass(thumbnail, 'vjs-playlist-now-playing');
}
}
}
/**
* Adds a number of playlist items to the UI.
*
* Each item that was added to the underlying playlist in a certain range
* has a new PlaylistMenuItem created for it.
*
* @param {number} index
* The index at which to start adding items.
*
* @param {number} count
* The number of items to add.
*/
addItems_(index, count) {
const playlist = this.player_.playlist();
const items = playlist.slice(index, count + index);
if (!items.length) {
return;
}
const listEl = this.el_.querySelector('.vjs-playlist-item-list');
const listNodes = this.el_.querySelectorAll('.vjs-playlist-item');
// When appending to the list, `insertBefore` will only reliably accept
// `null` as the second argument, so we need to explicitly fall back to it.
const refNode = listNodes[index] || null;
const menuItems = items.map(item => {
const menuItem = new PlaylistMenuItem(this.player_, {
item
}, this.options_);
listEl.insertBefore(menuItem.el_, refNode);
return menuItem;
});
this.items.splice(index, 0, ...menuItems);
}
/**
* Removes a number of playlist items from the UI.
*
* Each PlaylistMenuItem component is disposed properly.
*
* @param {number} index
* The index at which to start removing items.
*
* @param {number} count
* The number of items to remove.
*/
removeItems_(index, count) {
const components = this.items.slice(index, count + index);
if (!components.length) {
return;
}
components.forEach(c => c.dispose());
this.items.splice(index, count);
}
update() {
// replace the playlist items being displayed, if necessary
const playlist = this.player_.playlist();
if (this.items.length !== playlist.length) {
// if the menu is currently empty or the state is obviously out
// of date, rebuild everything.
this.createPlaylist_();
return;
}
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].item !== playlist[i]) {
// if any of the playlist items have changed, rebuild the
// entire playlist
this.createPlaylist_();
return;
}
}
// the playlist itself is unchanged so just update the selection
const currentItem = this.player_.playlist.currentItem();
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (i === currentItem) {
addSelectedClass(item);
if (document__default["default"].activeElement !== item.el()) {
videojs__default["default"].dom.addClass(item.thumbnail, 'vjs-playlist-now-playing');
}
notUpNext(item);
} else if (i === currentItem + 1) {
removeSelectedClass(item);
upNext(item);
} else {
removeSelectedClass(item);
notUpNext(item);
}
}
}
}
videojs__default["default"].registerComponent('PlaylistMenu', PlaylistMenu);
// see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/pointerevents.js
const supportsCssPointerEvents = (() => {
const element = document__default["default"].createElement('x');
element.style.cssText = 'pointer-events:auto';
return element.style.pointerEvents === 'auto';
})();
const defaults = {
className: 'vjs-playlist',
playOnSelect: false,
supportsCssPointerEvents
};
const Plugin = videojs__default["default"].getPlugin('plugin');
/**
* Initialize the plugin on a player.
*
* @param {Object} [options]
* An options object.
*
* @param {HTMLElement} [options.el]
* A DOM element to use as a root node for the playlist.
*
* @param {string} [options.className]
* An HTML class name to use to find a root node for the playlist.
*
* @param {boolean} [options.playOnSelect = false]
* If true, will attempt to begin playback upon selecting a new
* playlist item in the UI.
*/
class PlaylistUI extends Plugin {
constructor(player, options) {
super(player, options);
if (!player.usingPlugin('playlist')) {
player.log.error('videojs-playlist plugin is required by the videojs-playlist-ui plugin');
return;
}
options = this.options_ = videojs__default["default"].obj.merge(defaults, options);
if (!videojs__default["default"].dom.isEl(options.el)) {
options.el = this.findRoot_(options.className);
}
// Expose the playlist menu component on the player as well as the plugin
// This is a bit of an anti-pattern, but it's been that way forever and
// there are likely to be integrations relying on it.
this.playlistMenu = player.playlistMenu = new PlaylistMenu(player, options);
}
/**
* Dispose the plugin.
*/
dispose() {
super.dispose();
this.playlistMenu.dispose();
}
/**
* Returns a boolean indicating whether an element has child elements.
*
* Note that this is distinct from whether it has child _nodes_.
*
* @param {HTMLElement} el
* A DOM element.
*
* @return {boolean}
* Whether the element has child elements.
*/
hasChildEls_(el) {
for (let i = 0; i < el.childNodes.length; i++) {
if (videojs__default["default"].dom.isEl(el.childNodes[i])) {
return true;
}
}
return false;
}
/**
* Finds the first empty root element.
*
* @param {string} className
* An HTML class name to search for.
*
* @return {HTMLElement}
* A DOM element to use as the root for a playlist.
*/
findRoot_(className) {
const all = document__default["default"].querySelectorAll('.' + className);
for (let i = 0; i < all.length; i++) {
if (!this.hasChildEls_(all[i])) {
return all[i];
}
}
}
}
videojs__default["default"].registerPlugin('playlistUi', PlaylistUI);
PlaylistUI.VERSION = version;
module.exports = PlaylistUI;

View file

@ -0,0 +1 @@
.vjs-playlist{padding:0;background-color:#1a1a1a;color:#fff;list-style-type:none}.vjs-playlist img{display:block}.vjs-playlist .vjs-playlist-item-list{position:relative;margin:0;padding:0;list-style:none}.vjs-playlist .vjs-playlist-item{position:relative;cursor:pointer;overflow:hidden}.vjs-playlist .vjs-playlist-thumbnail-placeholder{background:#303030}.vjs-playlist .vjs-playlist-now-playing-text{display:none;position:absolute;top:0;left:0;padding-left:2px;margin:.8rem}.vjs-playlist .vjs-playlist-duration{position:absolute;top:.5rem;left:.5rem;padding:2px 5px 3px;margin-left:2px;background-color:rgba(26,26,26,.8)}.vjs-playlist .vjs-playlist-title-container{position:absolute;bottom:0;box-sizing:border-box;width:100%;padding:.5rem .8rem;text-shadow:1px 1px 2px #000,-1px 1px 2px #000,1px -1px 2px #000,-1px -1px 2px #000}.vjs-playlist .vjs-playlist-name{display:block;max-height:2.5em;padding:0 0 4px 2px;font-style:normal;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;line-height:20px}.vjs-playlist .vjs-playlist-description{text-overflow:ellipsis;overflow:hidden;white-space:nowrap;display:block;font-size:14px;padding:0 0 0 2px}.vjs-playlist .vjs-up-next-text{display:none;padding:.1rem 2px;font-size:.8em;text-transform:uppercase}.vjs-playlist .vjs-up-next .vjs-up-next-text{display:block}.vjs-playlist .vjs-selected{background-color:#141a21}.vjs-playlist .vjs-selected img{opacity:.2}.vjs-playlist .vjs-selected .vjs-playlist-duration{display:none}.vjs-playlist .vjs-selected .vjs-playlist-now-playing-text{display:block}.vjs-playlist .vjs-selected .vjs-playlist-title-container{text-shadow:none}.vjs-playlist-vertical{overflow-x:hidden;overflow-y:auto}.vjs-playlist-vertical img{width:100%;min-height:54px}.vjs-playlist-vertical .vjs-playlist-item{margin-bottom:5px}.vjs-playlist-vertical .vjs-playlist-thumbnail{display:block;width:100%}.vjs-playlist-vertical .vjs-playlist-thumbnail-placeholder{height:100px}.vjs-playlist-horizontal{overflow-x:auto;overflow-y:hidden}.vjs-playlist-horizontal img{min-width:100px;height:100%}.vjs-playlist-horizontal .vjs-playlist-item-list{height:100%;white-space:nowrap}.vjs-playlist-horizontal .vjs-playlist-item{display:inline-block;height:100%;margin-right:5px}.vjs-playlist-horizontal .vjs-playlist-thumbnail{display:block;height:100%}.vjs-playlist-horizontal .vjs-playlist-thumbnail-placeholder{height:100%;width:180px}.vjs-playlist.vjs-ad-playing.vjs-csspointerevents{pointer-events:none;overflow:auto}.vjs-playlist.vjs-ad-playing .vjs-playlist-ad-overlay{display:block;position:absolute;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.5)}.vjs-playlist{font-size:14px}.vjs-playlist .vjs-playlist-description{height:28px;line-height:21px}.vjs-mouse.vjs-playlist{font-size:15px}.vjs-mouse.vjs-playlist .vjs-playlist-description{height:30px;line-height:23px}@media(min-width: 600px){.vjs-mouse.vjs-playlist{font-size:17px}.vjs-mouse.vjs-playlist .vjs-playlist-description{height:34px;line-height:26px}.vjs-playlist .vjs-playlist-name{line-height:22px}}@media(max-width: 520px){.vjs-playlist .vjs-selected .vjs-playlist-now-playing-text,.vjs-playlist .vjs-up-next .vjs-up-next-text{display:none}.vjs-mouse.vjs-playlist .vjs-selected .vjs-playlist-now-playing-text,.vjs-mouse.vjs-playlist .vjs-up-next .vjs-up-next-text{display:none}}@media(min-width: 521px){.vjs-playlist img{min-height:85px}}@media(max-width: 750px){.vjs-playlist .vjs-playlist-duration{display:none}}/*# sourceMappingURL=videojs-playlist-ui.css.map */

View file

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../src/plugin.scss"],"names":[],"mappings":"AAaA,cACE,UACA,iBAdiB,QAejB,MATW,KAUX,qBAEA,kBACE,cAGF,sCACE,kBACA,SACA,UACA,gBAGF,iCACE,kBACA,eACA,gBAGF,kDACE,WA3B2B,QA8B7B,6CACE,aACA,kBACA,MACA,OAGA,iBACA,aAGF,qCACE,kBACA,UACA,WACA,oBACA,gBACA,mCAGF,4CACE,kBACA,SACA,sBACA,WACA,oBACA,oFAMF,iCACE,cACA,iBAGA,oBACA,kBACA,uBACA,gBACA,mBAIA,iBAGF,wCACE,uBACA,gBACA,mBACA,cACA,eACA,kBAGF,gCACE,aACA,kBACA,eACA,yBAKA,6CACE,cAKJ,4BACE,iBA7Gc,QA+Gd,gCACE,WAGF,mDACE,aAGF,2DACE,cAGF,0DACE,iBAMN,uBACE,kBACA,gBAEA,2BACE,WACA,gBAGF,0CACE,kBAGF,+CACE,cACA,WAGF,2DACE,aAKJ,yBACE,gBACA,kBAEA,6BACE,gBACA,YAGF,iDACE,YACA,mBAGF,4CACE,qBACA,YACA,iBAGF,iDACE,cACA,YAGF,6DACE,YACA,YAKJ,kDACE,oBACA,cAIF,sDACE,cACA,kBACA,MACA,OACA,WACA,YACA,gCAcF,cATE,UADoC,KAGpC,wCACE,YACA,iBAUJ,wBAdE,UAeuB,KAbvB,kDACE,YACA,iBAeJ,yBACE,wBApBA,UAqByB,KAnBzB,kDACE,YACA,iBAoBF,iCACE,kBAKJ,yBAGE,wGAEE,aAGF,4HAEE,cASJ,yBACE,kBACE,iBAKJ,yBACE,qCACE","file":"videojs-playlist-ui.css"}

View file

@ -0,0 +1,460 @@
/*! @name videojs-playlist-ui @version 5.0.0 @license Apache-2.0 */
import document from 'global/document';
import videojs from 'video.js';
var version = "5.0.0";
const Component$1 = videojs.getComponent('Component');
const createThumbnail = function (thumbnail) {
if (!thumbnail) {
const placeholder = document.createElement('div');
placeholder.className = 'vjs-playlist-thumbnail vjs-playlist-thumbnail-placeholder';
return placeholder;
}
const picture = document.createElement('picture');
picture.className = 'vjs-playlist-thumbnail';
if (typeof thumbnail === 'string') {
// simple thumbnails
const img = document.createElement('img');
img.loading = 'lazy';
img.src = thumbnail;
img.alt = '';
picture.appendChild(img);
} else {
// responsive thumbnails
// additional variations of a <picture> are specified as
// <source> elements
for (let i = 0; i < thumbnail.length - 1; i++) {
const variant = thumbnail[i];
const source = document.createElement('source');
// transfer the properties of each variant onto a <source>
for (const prop in variant) {
source[prop] = variant[prop];
}
picture.appendChild(source);
}
// the default version of a <picture> is specified by an <img>
const variant = thumbnail[thumbnail.length - 1];
const img = document.createElement('img');
img.loading = 'lazy';
img.alt = '';
for (const prop in variant) {
img[prop] = variant[prop];
}
picture.appendChild(img);
}
return picture;
};
class PlaylistMenuItem extends Component$1 {
constructor(player, playlistItem, settings) {
if (!playlistItem.item) {
throw new Error('Cannot construct a PlaylistMenuItem without an item option');
}
playlistItem.showDescription = settings.showDescription;
super(player, playlistItem);
this.item = playlistItem.item;
this.playOnSelect = settings.playOnSelect;
this.emitTapEvents();
this.on(['click', 'tap'], this.switchPlaylistItem_);
this.on('keydown', this.handleKeyDown_);
}
handleKeyDown_(event) {
// keycode 13 is <Enter>
// keycode 32 is <Space>
if (event.which === 13 || event.which === 32) {
this.switchPlaylistItem_();
}
}
switchPlaylistItem_(event) {
this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item));
if (this.playOnSelect) {
this.player_.play();
}
}
createEl() {
const li = document.createElement('li');
const item = this.options_.item;
const showDescription = this.options_.showDescription;
if (typeof item.data === 'object') {
const dataKeys = Object.keys(item.data);
dataKeys.forEach(key => {
const value = item.data[key];
li.dataset[key] = value;
});
}
li.className = 'vjs-playlist-item';
li.setAttribute('tabIndex', 0);
// Thumbnail image
this.thumbnail = createThumbnail(item.thumbnail);
li.appendChild(this.thumbnail);
// Duration
if (item.duration) {
const duration = document.createElement('time');
const time = videojs.time.formatTime(item.duration);
duration.className = 'vjs-playlist-duration';
duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S');
duration.appendChild(document.createTextNode(time));
li.appendChild(duration);
}
// Now playing
const nowPlayingEl = document.createElement('span');
const nowPlayingText = this.localize('Now Playing');
nowPlayingEl.className = 'vjs-playlist-now-playing-text';
nowPlayingEl.appendChild(document.createTextNode(nowPlayingText));
nowPlayingEl.setAttribute('title', nowPlayingText);
this.thumbnail.appendChild(nowPlayingEl);
// Title container contains title and "up next"
const titleContainerEl = document.createElement('div');
titleContainerEl.className = 'vjs-playlist-title-container';
this.thumbnail.appendChild(titleContainerEl);
// Up next
const upNextEl = document.createElement('span');
const upNextText = this.localize('Up Next');
upNextEl.className = 'vjs-up-next-text';
upNextEl.appendChild(document.createTextNode(upNextText));
upNextEl.setAttribute('title', upNextText);
titleContainerEl.appendChild(upNextEl);
// Video title
const titleEl = document.createElement('cite');
const titleText = item.name || this.localize('Untitled Video');
titleEl.className = 'vjs-playlist-name';
titleEl.appendChild(document.createTextNode(titleText));
titleEl.setAttribute('title', titleText);
titleContainerEl.appendChild(titleEl);
// Populate thumbnails alt with the video title
this.thumbnail.getElementsByTagName('img').alt = titleText;
// We add thumbnail video description only if specified in playlist options
if (showDescription) {
const descriptionEl = document.createElement('div');
const descriptionText = item.description || '';
descriptionEl.className = 'vjs-playlist-description';
descriptionEl.appendChild(document.createTextNode(descriptionText));
descriptionEl.setAttribute('title', descriptionText);
titleContainerEl.appendChild(descriptionEl);
}
return li;
}
}
videojs.registerComponent('PlaylistMenuItem', PlaylistMenuItem);
// we don't add `vjs-playlist-now-playing` in addSelectedClass
// so it won't conflict with `vjs-icon-play
// since it'll get added when we mouse out
const addSelectedClass = function (el) {
el.addClass('vjs-selected');
};
const removeSelectedClass = function (el) {
el.removeClass('vjs-selected');
if (el.thumbnail) {
videojs.dom.removeClass(el.thumbnail, 'vjs-playlist-now-playing');
}
};
const upNext = function (el) {
el.addClass('vjs-up-next');
};
const notUpNext = function (el) {
el.removeClass('vjs-up-next');
};
const Component = videojs.getComponent('Component');
class PlaylistMenu extends Component {
constructor(player, options) {
super(player, options);
this.items = [];
if (options.horizontal) {
this.addClass('vjs-playlist-horizontal');
} else {
this.addClass('vjs-playlist-vertical');
}
// If CSS pointer events aren't supported, we have to prevent
// clicking on playlist items during ads with slightly more
// invasive techniques. Details in the stylesheet.
if (options.supportsCssPointerEvents) {
this.addClass('vjs-csspointerevents');
}
this.createPlaylist_();
if (!videojs.browser.TOUCH_ENABLED) {
this.addClass('vjs-mouse');
}
this.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], e => {
// The playlistadd and playlistremove events are handled separately. These
// also fire the playlistchange event with an `action` property, so can
// exclude them here.
if (e.type === 'playlistchange' && ['add', 'remove'].includes(e.action)) {
return;
}
this.update();
});
this.on(player, ['playlistadd'], e => this.addItems_(e.index, e.count));
this.on(player, ['playlistremove'], e => this.removeItems_(e.index, e.count));
// Keep track of whether an ad is playing so that the menu
// appearance can be adapted appropriately
this.on(player, 'adstart', () => {
this.addClass('vjs-ad-playing');
});
this.on(player, 'adend', () => {
this.removeClass('vjs-ad-playing');
});
this.on('dispose', () => {
this.empty_();
player.playlistMenu = null;
});
this.on(player, 'dispose', () => {
this.dispose();
});
}
createEl() {
return videojs.dom.createEl('div', {
className: this.options_.className
});
}
empty_() {
if (this.items && this.items.length) {
this.items.forEach(i => i.dispose());
this.items.length = 0;
}
}
createPlaylist_() {
const playlist = this.player_.playlist() || [];
let list = this.el_.querySelector('.vjs-playlist-item-list');
let overlay = this.el_.querySelector('.vjs-playlist-ad-overlay');
if (!list) {
list = document.createElement('ol');
list.className = 'vjs-playlist-item-list';
this.el_.appendChild(list);
}
this.empty_();
// create new items
for (let i = 0; i < playlist.length; i++) {
const item = new PlaylistMenuItem(this.player_, {
item: playlist[i]
}, this.options_);
this.items.push(item);
list.appendChild(item.el_);
}
// Inject the ad overlay. We use this element to block clicks during ad
// playback and darken the menu to indicate inactivity
if (!overlay) {
overlay = document.createElement('li');
overlay.className = 'vjs-playlist-ad-overlay';
list.appendChild(overlay);
} else {
// Move overlay to end of list
list.appendChild(overlay);
}
// select the current playlist item
const selectedIndex = this.player_.playlist.currentItem();
if (this.items.length && selectedIndex >= 0) {
addSelectedClass(this.items[selectedIndex]);
const thumbnail = this.items[selectedIndex].$('.vjs-playlist-thumbnail');
if (thumbnail) {
videojs.dom.addClass(thumbnail, 'vjs-playlist-now-playing');
}
}
}
/**
* Adds a number of playlist items to the UI.
*
* Each item that was added to the underlying playlist in a certain range
* has a new PlaylistMenuItem created for it.
*
* @param {number} index
* The index at which to start adding items.
*
* @param {number} count
* The number of items to add.
*/
addItems_(index, count) {
const playlist = this.player_.playlist();
const items = playlist.slice(index, count + index);
if (!items.length) {
return;
}
const listEl = this.el_.querySelector('.vjs-playlist-item-list');
const listNodes = this.el_.querySelectorAll('.vjs-playlist-item');
// When appending to the list, `insertBefore` will only reliably accept
// `null` as the second argument, so we need to explicitly fall back to it.
const refNode = listNodes[index] || null;
const menuItems = items.map(item => {
const menuItem = new PlaylistMenuItem(this.player_, {
item
}, this.options_);
listEl.insertBefore(menuItem.el_, refNode);
return menuItem;
});
this.items.splice(index, 0, ...menuItems);
}
/**
* Removes a number of playlist items from the UI.
*
* Each PlaylistMenuItem component is disposed properly.
*
* @param {number} index
* The index at which to start removing items.
*
* @param {number} count
* The number of items to remove.
*/
removeItems_(index, count) {
const components = this.items.slice(index, count + index);
if (!components.length) {
return;
}
components.forEach(c => c.dispose());
this.items.splice(index, count);
}
update() {
// replace the playlist items being displayed, if necessary
const playlist = this.player_.playlist();
if (this.items.length !== playlist.length) {
// if the menu is currently empty or the state is obviously out
// of date, rebuild everything.
this.createPlaylist_();
return;
}
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].item !== playlist[i]) {
// if any of the playlist items have changed, rebuild the
// entire playlist
this.createPlaylist_();
return;
}
}
// the playlist itself is unchanged so just update the selection
const currentItem = this.player_.playlist.currentItem();
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (i === currentItem) {
addSelectedClass(item);
if (document.activeElement !== item.el()) {
videojs.dom.addClass(item.thumbnail, 'vjs-playlist-now-playing');
}
notUpNext(item);
} else if (i === currentItem + 1) {
removeSelectedClass(item);
upNext(item);
} else {
removeSelectedClass(item);
notUpNext(item);
}
}
}
}
videojs.registerComponent('PlaylistMenu', PlaylistMenu);
// see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/pointerevents.js
const supportsCssPointerEvents = (() => {
const element = document.createElement('x');
element.style.cssText = 'pointer-events:auto';
return element.style.pointerEvents === 'auto';
})();
const defaults = {
className: 'vjs-playlist',
playOnSelect: false,
supportsCssPointerEvents
};
const Plugin = videojs.getPlugin('plugin');
/**
* Initialize the plugin on a player.
*
* @param {Object} [options]
* An options object.
*
* @param {HTMLElement} [options.el]
* A DOM element to use as a root node for the playlist.
*
* @param {string} [options.className]
* An HTML class name to use to find a root node for the playlist.
*
* @param {boolean} [options.playOnSelect = false]
* If true, will attempt to begin playback upon selecting a new
* playlist item in the UI.
*/
class PlaylistUI extends Plugin {
constructor(player, options) {
super(player, options);
if (!player.usingPlugin('playlist')) {
player.log.error('videojs-playlist plugin is required by the videojs-playlist-ui plugin');
return;
}
options = this.options_ = videojs.obj.merge(defaults, options);
if (!videojs.dom.isEl(options.el)) {
options.el = this.findRoot_(options.className);
}
// Expose the playlist menu component on the player as well as the plugin
// This is a bit of an anti-pattern, but it's been that way forever and
// there are likely to be integrations relying on it.
this.playlistMenu = player.playlistMenu = new PlaylistMenu(player, options);
}
/**
* Dispose the plugin.
*/
dispose() {
super.dispose();
this.playlistMenu.dispose();
}
/**
* Returns a boolean indicating whether an element has child elements.
*
* Note that this is distinct from whether it has child _nodes_.
*
* @param {HTMLElement} el
* A DOM element.
*
* @return {boolean}
* Whether the element has child elements.
*/
hasChildEls_(el) {
for (let i = 0; i < el.childNodes.length; i++) {
if (videojs.dom.isEl(el.childNodes[i])) {
return true;
}
}
return false;
}
/**
* Finds the first empty root element.
*
* @param {string} className
* An HTML class name to search for.
*
* @return {HTMLElement}
* A DOM element to use as the root for a playlist.
*/
findRoot_(className) {
const all = document.querySelectorAll('.' + className);
for (let i = 0; i < all.length; i++) {
if (!this.hasChildEls_(all[i])) {
return all[i];
}
}
}
}
videojs.registerPlugin('playlistUi', PlaylistUI);
PlaylistUI.VERSION = version;
export { PlaylistUI as default };

View file

@ -0,0 +1,469 @@
/*! @name videojs-playlist-ui @version 5.0.0 @license Apache-2.0 */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js')) :
typeof define === 'function' && define.amd ? define(['video.js'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojsPlaylistUi = factory(global.videojs));
})(this, (function (videojs) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
var version = "5.0.0";
const Component$1 = videojs__default["default"].getComponent('Component');
const createThumbnail = function (thumbnail) {
if (!thumbnail) {
const placeholder = document.createElement('div');
placeholder.className = 'vjs-playlist-thumbnail vjs-playlist-thumbnail-placeholder';
return placeholder;
}
const picture = document.createElement('picture');
picture.className = 'vjs-playlist-thumbnail';
if (typeof thumbnail === 'string') {
// simple thumbnails
const img = document.createElement('img');
img.loading = 'lazy';
img.src = thumbnail;
img.alt = '';
picture.appendChild(img);
} else {
// responsive thumbnails
// additional variations of a <picture> are specified as
// <source> elements
for (let i = 0; i < thumbnail.length - 1; i++) {
const variant = thumbnail[i];
const source = document.createElement('source');
// transfer the properties of each variant onto a <source>
for (const prop in variant) {
source[prop] = variant[prop];
}
picture.appendChild(source);
}
// the default version of a <picture> is specified by an <img>
const variant = thumbnail[thumbnail.length - 1];
const img = document.createElement('img');
img.loading = 'lazy';
img.alt = '';
for (const prop in variant) {
img[prop] = variant[prop];
}
picture.appendChild(img);
}
return picture;
};
class PlaylistMenuItem extends Component$1 {
constructor(player, playlistItem, settings) {
if (!playlistItem.item) {
throw new Error('Cannot construct a PlaylistMenuItem without an item option');
}
playlistItem.showDescription = settings.showDescription;
super(player, playlistItem);
this.item = playlistItem.item;
this.playOnSelect = settings.playOnSelect;
this.emitTapEvents();
this.on(['click', 'tap'], this.switchPlaylistItem_);
this.on('keydown', this.handleKeyDown_);
}
handleKeyDown_(event) {
// keycode 13 is <Enter>
// keycode 32 is <Space>
if (event.which === 13 || event.which === 32) {
this.switchPlaylistItem_();
}
}
switchPlaylistItem_(event) {
this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item));
if (this.playOnSelect) {
this.player_.play();
}
}
createEl() {
const li = document.createElement('li');
const item = this.options_.item;
const showDescription = this.options_.showDescription;
if (typeof item.data === 'object') {
const dataKeys = Object.keys(item.data);
dataKeys.forEach(key => {
const value = item.data[key];
li.dataset[key] = value;
});
}
li.className = 'vjs-playlist-item';
li.setAttribute('tabIndex', 0);
// Thumbnail image
this.thumbnail = createThumbnail(item.thumbnail);
li.appendChild(this.thumbnail);
// Duration
if (item.duration) {
const duration = document.createElement('time');
const time = videojs__default["default"].time.formatTime(item.duration);
duration.className = 'vjs-playlist-duration';
duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S');
duration.appendChild(document.createTextNode(time));
li.appendChild(duration);
}
// Now playing
const nowPlayingEl = document.createElement('span');
const nowPlayingText = this.localize('Now Playing');
nowPlayingEl.className = 'vjs-playlist-now-playing-text';
nowPlayingEl.appendChild(document.createTextNode(nowPlayingText));
nowPlayingEl.setAttribute('title', nowPlayingText);
this.thumbnail.appendChild(nowPlayingEl);
// Title container contains title and "up next"
const titleContainerEl = document.createElement('div');
titleContainerEl.className = 'vjs-playlist-title-container';
this.thumbnail.appendChild(titleContainerEl);
// Up next
const upNextEl = document.createElement('span');
const upNextText = this.localize('Up Next');
upNextEl.className = 'vjs-up-next-text';
upNextEl.appendChild(document.createTextNode(upNextText));
upNextEl.setAttribute('title', upNextText);
titleContainerEl.appendChild(upNextEl);
// Video title
const titleEl = document.createElement('cite');
const titleText = item.name || this.localize('Untitled Video');
titleEl.className = 'vjs-playlist-name';
titleEl.appendChild(document.createTextNode(titleText));
titleEl.setAttribute('title', titleText);
titleContainerEl.appendChild(titleEl);
// Populate thumbnails alt with the video title
this.thumbnail.getElementsByTagName('img').alt = titleText;
// We add thumbnail video description only if specified in playlist options
if (showDescription) {
const descriptionEl = document.createElement('div');
const descriptionText = item.description || '';
descriptionEl.className = 'vjs-playlist-description';
descriptionEl.appendChild(document.createTextNode(descriptionText));
descriptionEl.setAttribute('title', descriptionText);
titleContainerEl.appendChild(descriptionEl);
}
return li;
}
}
videojs__default["default"].registerComponent('PlaylistMenuItem', PlaylistMenuItem);
// we don't add `vjs-playlist-now-playing` in addSelectedClass
// so it won't conflict with `vjs-icon-play
// since it'll get added when we mouse out
const addSelectedClass = function (el) {
el.addClass('vjs-selected');
};
const removeSelectedClass = function (el) {
el.removeClass('vjs-selected');
if (el.thumbnail) {
videojs__default["default"].dom.removeClass(el.thumbnail, 'vjs-playlist-now-playing');
}
};
const upNext = function (el) {
el.addClass('vjs-up-next');
};
const notUpNext = function (el) {
el.removeClass('vjs-up-next');
};
const Component = videojs__default["default"].getComponent('Component');
class PlaylistMenu extends Component {
constructor(player, options) {
super(player, options);
this.items = [];
if (options.horizontal) {
this.addClass('vjs-playlist-horizontal');
} else {
this.addClass('vjs-playlist-vertical');
}
// If CSS pointer events aren't supported, we have to prevent
// clicking on playlist items during ads with slightly more
// invasive techniques. Details in the stylesheet.
if (options.supportsCssPointerEvents) {
this.addClass('vjs-csspointerevents');
}
this.createPlaylist_();
if (!videojs__default["default"].browser.TOUCH_ENABLED) {
this.addClass('vjs-mouse');
}
this.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], e => {
// The playlistadd and playlistremove events are handled separately. These
// also fire the playlistchange event with an `action` property, so can
// exclude them here.
if (e.type === 'playlistchange' && ['add', 'remove'].includes(e.action)) {
return;
}
this.update();
});
this.on(player, ['playlistadd'], e => this.addItems_(e.index, e.count));
this.on(player, ['playlistremove'], e => this.removeItems_(e.index, e.count));
// Keep track of whether an ad is playing so that the menu
// appearance can be adapted appropriately
this.on(player, 'adstart', () => {
this.addClass('vjs-ad-playing');
});
this.on(player, 'adend', () => {
this.removeClass('vjs-ad-playing');
});
this.on('dispose', () => {
this.empty_();
player.playlistMenu = null;
});
this.on(player, 'dispose', () => {
this.dispose();
});
}
createEl() {
return videojs__default["default"].dom.createEl('div', {
className: this.options_.className
});
}
empty_() {
if (this.items && this.items.length) {
this.items.forEach(i => i.dispose());
this.items.length = 0;
}
}
createPlaylist_() {
const playlist = this.player_.playlist() || [];
let list = this.el_.querySelector('.vjs-playlist-item-list');
let overlay = this.el_.querySelector('.vjs-playlist-ad-overlay');
if (!list) {
list = document.createElement('ol');
list.className = 'vjs-playlist-item-list';
this.el_.appendChild(list);
}
this.empty_();
// create new items
for (let i = 0; i < playlist.length; i++) {
const item = new PlaylistMenuItem(this.player_, {
item: playlist[i]
}, this.options_);
this.items.push(item);
list.appendChild(item.el_);
}
// Inject the ad overlay. We use this element to block clicks during ad
// playback and darken the menu to indicate inactivity
if (!overlay) {
overlay = document.createElement('li');
overlay.className = 'vjs-playlist-ad-overlay';
list.appendChild(overlay);
} else {
// Move overlay to end of list
list.appendChild(overlay);
}
// select the current playlist item
const selectedIndex = this.player_.playlist.currentItem();
if (this.items.length && selectedIndex >= 0) {
addSelectedClass(this.items[selectedIndex]);
const thumbnail = this.items[selectedIndex].$('.vjs-playlist-thumbnail');
if (thumbnail) {
videojs__default["default"].dom.addClass(thumbnail, 'vjs-playlist-now-playing');
}
}
}
/**
* Adds a number of playlist items to the UI.
*
* Each item that was added to the underlying playlist in a certain range
* has a new PlaylistMenuItem created for it.
*
* @param {number} index
* The index at which to start adding items.
*
* @param {number} count
* The number of items to add.
*/
addItems_(index, count) {
const playlist = this.player_.playlist();
const items = playlist.slice(index, count + index);
if (!items.length) {
return;
}
const listEl = this.el_.querySelector('.vjs-playlist-item-list');
const listNodes = this.el_.querySelectorAll('.vjs-playlist-item');
// When appending to the list, `insertBefore` will only reliably accept
// `null` as the second argument, so we need to explicitly fall back to it.
const refNode = listNodes[index] || null;
const menuItems = items.map(item => {
const menuItem = new PlaylistMenuItem(this.player_, {
item
}, this.options_);
listEl.insertBefore(menuItem.el_, refNode);
return menuItem;
});
this.items.splice(index, 0, ...menuItems);
}
/**
* Removes a number of playlist items from the UI.
*
* Each PlaylistMenuItem component is disposed properly.
*
* @param {number} index
* The index at which to start removing items.
*
* @param {number} count
* The number of items to remove.
*/
removeItems_(index, count) {
const components = this.items.slice(index, count + index);
if (!components.length) {
return;
}
components.forEach(c => c.dispose());
this.items.splice(index, count);
}
update() {
// replace the playlist items being displayed, if necessary
const playlist = this.player_.playlist();
if (this.items.length !== playlist.length) {
// if the menu is currently empty or the state is obviously out
// of date, rebuild everything.
this.createPlaylist_();
return;
}
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].item !== playlist[i]) {
// if any of the playlist items have changed, rebuild the
// entire playlist
this.createPlaylist_();
return;
}
}
// the playlist itself is unchanged so just update the selection
const currentItem = this.player_.playlist.currentItem();
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (i === currentItem) {
addSelectedClass(item);
if (document.activeElement !== item.el()) {
videojs__default["default"].dom.addClass(item.thumbnail, 'vjs-playlist-now-playing');
}
notUpNext(item);
} else if (i === currentItem + 1) {
removeSelectedClass(item);
upNext(item);
} else {
removeSelectedClass(item);
notUpNext(item);
}
}
}
}
videojs__default["default"].registerComponent('PlaylistMenu', PlaylistMenu);
// see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/pointerevents.js
const supportsCssPointerEvents = (() => {
const element = document.createElement('x');
element.style.cssText = 'pointer-events:auto';
return element.style.pointerEvents === 'auto';
})();
const defaults = {
className: 'vjs-playlist',
playOnSelect: false,
supportsCssPointerEvents
};
const Plugin = videojs__default["default"].getPlugin('plugin');
/**
* Initialize the plugin on a player.
*
* @param {Object} [options]
* An options object.
*
* @param {HTMLElement} [options.el]
* A DOM element to use as a root node for the playlist.
*
* @param {string} [options.className]
* An HTML class name to use to find a root node for the playlist.
*
* @param {boolean} [options.playOnSelect = false]
* If true, will attempt to begin playback upon selecting a new
* playlist item in the UI.
*/
class PlaylistUI extends Plugin {
constructor(player, options) {
super(player, options);
if (!player.usingPlugin('playlist')) {
player.log.error('videojs-playlist plugin is required by the videojs-playlist-ui plugin');
return;
}
options = this.options_ = videojs__default["default"].obj.merge(defaults, options);
if (!videojs__default["default"].dom.isEl(options.el)) {
options.el = this.findRoot_(options.className);
}
// Expose the playlist menu component on the player as well as the plugin
// This is a bit of an anti-pattern, but it's been that way forever and
// there are likely to be integrations relying on it.
this.playlistMenu = player.playlistMenu = new PlaylistMenu(player, options);
}
/**
* Dispose the plugin.
*/
dispose() {
super.dispose();
this.playlistMenu.dispose();
}
/**
* Returns a boolean indicating whether an element has child elements.
*
* Note that this is distinct from whether it has child _nodes_.
*
* @param {HTMLElement} el
* A DOM element.
*
* @return {boolean}
* Whether the element has child elements.
*/
hasChildEls_(el) {
for (let i = 0; i < el.childNodes.length; i++) {
if (videojs__default["default"].dom.isEl(el.childNodes[i])) {
return true;
}
}
return false;
}
/**
* Finds the first empty root element.
*
* @param {string} className
* An HTML class name to search for.
*
* @return {HTMLElement}
* A DOM element to use as the root for a playlist.
*/
findRoot_(className) {
const all = document.querySelectorAll('.' + className);
for (let i = 0; i < all.length; i++) {
if (!this.hasChildEls_(all[i])) {
return all[i];
}
}
}
}
videojs__default["default"].registerPlugin('playlistUi', PlaylistUI);
PlaylistUI.VERSION = version;
return PlaylistUI;
}));

File diff suppressed because one or more lines are too long

18
node_modules/videojs-playlist-ui/index.html generated vendored Normal file
View file

@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>videojs-playlist-ui Demo</title>
</head>
<body>
<ul>
<li><a href="example.html">Example</a></li>
<li><a href="example-two-players.html">Example with Two Playlist Players</a></li>
<li><a href="example-custom-class.html">Example with Custom Class</a></li>
<li><a href="example-custom-element.html">Example with Custom Element</a></li>
<li><a href="example-horizontal.html">Example, Horizontal Implementation</a></li>
<li><a href="example-custom-data.html">Example with Custom Data Attributes</a></li>
<li><a href="test/debug.html">Run unit tests in browser.</a></li>
</ul>
</body>
</html>

105
node_modules/videojs-playlist-ui/package.json generated vendored Normal file
View file

@ -0,0 +1,105 @@
{
"name": "videojs-playlist-ui",
"version": "5.0.0",
"author": "Brightcove, Inc.",
"description": "A user interface for the videojs-playlist API",
"license": "Apache-2.0",
"keywords": [
"playlist",
"videojs",
"videojs-plugin"
],
"scripts": {
"prebuild": "npm run clean",
"build": "npm-run-all -p build:*",
"build:css": "sass src/plugin.scss dist/videojs-playlist-ui.css --style compressed",
"build:js": "rollup -c scripts/rollup.config.js",
"build:lang": "vjslang --dir dist/lang",
"clean": "shx rm -rf ./dist ./test/dist",
"postclean": "shx mkdir -p ./dist ./test/dist",
"docs": "doctoc README.md",
"lint": "vjsstandard",
"server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch",
"start": "npm-run-all -p server watch",
"pretest": "npm-run-all lint build",
"test": "karma start scripts/karma.conf.js",
"update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s",
"version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md",
"watch": "npm-run-all -p watch:*",
"watch:css": "npm run build:css -- -w",
"watch:js": "npm run build:js -- -w",
"posttest": "shx cat test/dist/coverage/text.txt",
"prepublishOnly": "npm run build && vjsverify --skip-es-check"
},
"repository": {
"type": "git",
"url": "https://github.com/videojs/videojs-playlist-ui"
},
"dependencies": {
"global": "^4.4.0"
},
"devDependencies": {
"conventional-changelog-cli": "^2.2.2",
"conventional-changelog-videojs": "^3.0.2",
"doctoc": "^2.2.1",
"husky": "^1.3.1",
"karma": "^6.4.2",
"lint-staged": "^13.2.2",
"not-prerelease": "^1.0.1",
"npm-merge-driver-install": "^3.0.0",
"npm-run-all": "^4.1.5",
"pkg-ok": "^2.2.0",
"rollup": "^2.61.1",
"sass": "^1.62.1",
"shx": "^0.3.2",
"sinon": "^6.1.5",
"video.js": "^8.0.0",
"videojs-generate-karma-config": "^8.0.1",
"videojs-generate-rollup-config": "^7.0.0",
"videojs-generator-verify": "^4.0.1",
"videojs-languages": "^1.0.0",
"videojs-playlist": "^5.1.0",
"videojs-standard": "^9.0.1"
},
"peerDependencies": {
"video.js": "^8.0.0",
"videojs-playlist": "^5.1.0"
},
"main": "dist/videojs-playlist-ui.cjs.js",
"module": "dist/videojs-playlist-ui.es.js",
"generator-videojs-plugin": {
"version": "7.3.2"
},
"vjsstandard": {
"jsdoc": false,
"ignore": [
"dist",
"docs",
"test/dist"
]
},
"files": [
"CONTRIBUTING.md",
"dist/",
"docs/",
"index.html",
"scripts/",
"src/",
"test/"
],
"lint-staged": {
"*.js": [
"vjsstandard --fix",
"git add"
],
"README.md": [
"npm run docs",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}

13
node_modules/videojs-playlist-ui/scripts/karma.conf.js generated vendored Normal file
View file

@ -0,0 +1,13 @@
const generate = require('videojs-generate-karma-config');
module.exports = function(config) {
// see https://github.com/videojs/videojs-generate-karma-config
// for options
const options = {};
config = generate(config, options);
// any other custom stuff not supported by options here!
};

View file

@ -0,0 +1,9 @@
const generate = require('videojs-generate-postcss-config');
module.exports = function(context) {
const result = generate({}, context);
// do custom stuff here
return result;
};

View file

@ -0,0 +1,11 @@
const generate = require('videojs-generate-rollup-config');
// see https://github.com/videojs/videojs-generate-rollup-config
// for options
const options = {};
const config = generate(options);
// Add additonal builds/customization here!
// export the builds to rollup
export default Object.values(config.builds);

View file

@ -0,0 +1,177 @@
import document from 'global/document';
import videojs from 'video.js';
const Component = videojs.getComponent('Component');
const createThumbnail = function(thumbnail) {
if (!thumbnail) {
const placeholder = document.createElement('div');
placeholder.className = 'vjs-playlist-thumbnail vjs-playlist-thumbnail-placeholder';
return placeholder;
}
const picture = document.createElement('picture');
picture.className = 'vjs-playlist-thumbnail';
if (typeof thumbnail === 'string') {
// simple thumbnails
const img = document.createElement('img');
img.loading = 'lazy';
img.src = thumbnail;
img.alt = '';
picture.appendChild(img);
} else {
// responsive thumbnails
// additional variations of a <picture> are specified as
// <source> elements
for (let i = 0; i < thumbnail.length - 1; i++) {
const variant = thumbnail[i];
const source = document.createElement('source');
// transfer the properties of each variant onto a <source>
for (const prop in variant) {
source[prop] = variant[prop];
}
picture.appendChild(source);
}
// the default version of a <picture> is specified by an <img>
const variant = thumbnail[thumbnail.length - 1];
const img = document.createElement('img');
img.loading = 'lazy';
img.alt = '';
for (const prop in variant) {
img[prop] = variant[prop];
}
picture.appendChild(img);
}
return picture;
};
class PlaylistMenuItem extends Component {
constructor(player, playlistItem, settings) {
if (!playlistItem.item) {
throw new Error('Cannot construct a PlaylistMenuItem without an item option');
}
playlistItem.showDescription = settings.showDescription;
super(player, playlistItem);
this.item = playlistItem.item;
this.playOnSelect = settings.playOnSelect;
this.emitTapEvents();
this.on(['click', 'tap'], this.switchPlaylistItem_);
this.on('keydown', this.handleKeyDown_);
}
handleKeyDown_(event) {
// keycode 13 is <Enter>
// keycode 32 is <Space>
if (event.which === 13 || event.which === 32) {
this.switchPlaylistItem_();
}
}
switchPlaylistItem_(event) {
this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item));
if (this.playOnSelect) {
this.player_.play();
}
}
createEl() {
const li = document.createElement('li');
const item = this.options_.item;
const showDescription = this.options_.showDescription;
if (typeof item.data === 'object') {
const dataKeys = Object.keys(item.data);
dataKeys.forEach(key => {
const value = item.data[key];
li.dataset[key] = value;
});
}
li.className = 'vjs-playlist-item';
li.setAttribute('tabIndex', 0);
// Thumbnail image
this.thumbnail = createThumbnail(item.thumbnail);
li.appendChild(this.thumbnail);
// Duration
if (item.duration) {
const duration = document.createElement('time');
const time = videojs.time.formatTime(item.duration);
duration.className = 'vjs-playlist-duration';
duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S');
duration.appendChild(document.createTextNode(time));
li.appendChild(duration);
}
// Now playing
const nowPlayingEl = document.createElement('span');
const nowPlayingText = this.localize('Now Playing');
nowPlayingEl.className = 'vjs-playlist-now-playing-text';
nowPlayingEl.appendChild(document.createTextNode(nowPlayingText));
nowPlayingEl.setAttribute('title', nowPlayingText);
this.thumbnail.appendChild(nowPlayingEl);
// Title container contains title and "up next"
const titleContainerEl = document.createElement('div');
titleContainerEl.className = 'vjs-playlist-title-container';
this.thumbnail.appendChild(titleContainerEl);
// Up next
const upNextEl = document.createElement('span');
const upNextText = this.localize('Up Next');
upNextEl.className = 'vjs-up-next-text';
upNextEl.appendChild(document.createTextNode(upNextText));
upNextEl.setAttribute('title', upNextText);
titleContainerEl.appendChild(upNextEl);
// Video title
const titleEl = document.createElement('cite');
const titleText = item.name || this.localize('Untitled Video');
titleEl.className = 'vjs-playlist-name';
titleEl.appendChild(document.createTextNode(titleText));
titleEl.setAttribute('title', titleText);
titleContainerEl.appendChild(titleEl);
// Populate thumbnails alt with the video title
this.thumbnail.getElementsByTagName('img').alt = titleText;
// We add thumbnail video description only if specified in playlist options
if (showDescription) {
const descriptionEl = document.createElement('div');
const descriptionText = item.description || '';
descriptionEl.className = 'vjs-playlist-description';
descriptionEl.appendChild(document.createTextNode(descriptionText));
descriptionEl.setAttribute('title', descriptionText);
titleContainerEl.appendChild(descriptionEl);
}
return li;
}
}
videojs.registerComponent('PlaylistMenuItem', PlaylistMenuItem);
export default PlaylistMenuItem;

252
node_modules/videojs-playlist-ui/src/playlist-menu.js generated vendored Normal file
View file

@ -0,0 +1,252 @@
import document from 'global/document';
import videojs from 'video.js';
import PlaylistMenuItem from './playlist-menu-item';
// we don't add `vjs-playlist-now-playing` in addSelectedClass
// so it won't conflict with `vjs-icon-play
// since it'll get added when we mouse out
const addSelectedClass = function(el) {
el.addClass('vjs-selected');
};
const removeSelectedClass = function(el) {
el.removeClass('vjs-selected');
if (el.thumbnail) {
videojs.dom.removeClass(el.thumbnail, 'vjs-playlist-now-playing');
}
};
const upNext = function(el) {
el.addClass('vjs-up-next');
};
const notUpNext = function(el) {
el.removeClass('vjs-up-next');
};
const Component = videojs.getComponent('Component');
class PlaylistMenu extends Component {
constructor(player, options) {
super(player, options);
this.items = [];
if (options.horizontal) {
this.addClass('vjs-playlist-horizontal');
} else {
this.addClass('vjs-playlist-vertical');
}
// If CSS pointer events aren't supported, we have to prevent
// clicking on playlist items during ads with slightly more
// invasive techniques. Details in the stylesheet.
if (options.supportsCssPointerEvents) {
this.addClass('vjs-csspointerevents');
}
this.createPlaylist_();
if (!videojs.browser.TOUCH_ENABLED) {
this.addClass('vjs-mouse');
}
this.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], (e) => {
// The playlistadd and playlistremove events are handled separately. These
// also fire the playlistchange event with an `action` property, so can
// exclude them here.
if (e.type === 'playlistchange' && ['add', 'remove'].includes(e.action)) {
return;
}
this.update();
});
this.on(player, ['playlistadd'], (e) => this.addItems_(e.index, e.count));
this.on(player, ['playlistremove'], (e) => this.removeItems_(e.index, e.count));
// Keep track of whether an ad is playing so that the menu
// appearance can be adapted appropriately
this.on(player, 'adstart', () => {
this.addClass('vjs-ad-playing');
});
this.on(player, 'adend', () => {
this.removeClass('vjs-ad-playing');
});
this.on('dispose', () => {
this.empty_();
player.playlistMenu = null;
});
this.on(player, 'dispose', () => {
this.dispose();
});
}
createEl() {
return videojs.dom.createEl('div', {className: this.options_.className});
}
empty_() {
if (this.items && this.items.length) {
this.items.forEach(i => i.dispose());
this.items.length = 0;
}
}
createPlaylist_() {
const playlist = this.player_.playlist() || [];
let list = this.el_.querySelector('.vjs-playlist-item-list');
let overlay = this.el_.querySelector('.vjs-playlist-ad-overlay');
if (!list) {
list = document.createElement('ol');
list.className = 'vjs-playlist-item-list';
this.el_.appendChild(list);
}
this.empty_();
// create new items
for (let i = 0; i < playlist.length; i++) {
const item = new PlaylistMenuItem(this.player_, {
item: playlist[i]
}, this.options_);
this.items.push(item);
list.appendChild(item.el_);
}
// Inject the ad overlay. We use this element to block clicks during ad
// playback and darken the menu to indicate inactivity
if (!overlay) {
overlay = document.createElement('li');
overlay.className = 'vjs-playlist-ad-overlay';
list.appendChild(overlay);
} else {
// Move overlay to end of list
list.appendChild(overlay);
}
// select the current playlist item
const selectedIndex = this.player_.playlist.currentItem();
if (this.items.length && selectedIndex >= 0) {
addSelectedClass(this.items[selectedIndex]);
const thumbnail = this.items[selectedIndex].$('.vjs-playlist-thumbnail');
if (thumbnail) {
videojs.dom.addClass(thumbnail, 'vjs-playlist-now-playing');
}
}
}
/**
* Adds a number of playlist items to the UI.
*
* Each item that was added to the underlying playlist in a certain range
* has a new PlaylistMenuItem created for it.
*
* @param {number} index
* The index at which to start adding items.
*
* @param {number} count
* The number of items to add.
*/
addItems_(index, count) {
const playlist = this.player_.playlist();
const items = playlist.slice(index, count + index);
if (!items.length) {
return;
}
const listEl = this.el_.querySelector('.vjs-playlist-item-list');
const listNodes = this.el_.querySelectorAll('.vjs-playlist-item');
// When appending to the list, `insertBefore` will only reliably accept
// `null` as the second argument, so we need to explicitly fall back to it.
const refNode = listNodes[index] || null;
const menuItems = items.map((item) => {
const menuItem = new PlaylistMenuItem(this.player_, {item}, this.options_);
listEl.insertBefore(menuItem.el_, refNode);
return menuItem;
});
this.items.splice(index, 0, ...menuItems);
}
/**
* Removes a number of playlist items from the UI.
*
* Each PlaylistMenuItem component is disposed properly.
*
* @param {number} index
* The index at which to start removing items.
*
* @param {number} count
* The number of items to remove.
*/
removeItems_(index, count) {
const components = this.items.slice(index, count + index);
if (!components.length) {
return;
}
components.forEach(c => c.dispose());
this.items.splice(index, count);
}
update() {
// replace the playlist items being displayed, if necessary
const playlist = this.player_.playlist();
if (this.items.length !== playlist.length) {
// if the menu is currently empty or the state is obviously out
// of date, rebuild everything.
this.createPlaylist_();
return;
}
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].item !== playlist[i]) {
// if any of the playlist items have changed, rebuild the
// entire playlist
this.createPlaylist_();
return;
}
}
// the playlist itself is unchanged so just update the selection
const currentItem = this.player_.playlist.currentItem();
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (i === currentItem) {
addSelectedClass(item);
if (document.activeElement !== item.el()) {
videojs.dom.addClass(item.thumbnail, 'vjs-playlist-now-playing');
}
notUpNext(item);
} else if (i === currentItem + 1) {
removeSelectedClass(item);
upNext(item);
} else {
removeSelectedClass(item);
notUpNext(item);
}
}
}
}
videojs.registerComponent('PlaylistMenu', PlaylistMenu);
export default PlaylistMenu;

112
node_modules/videojs-playlist-ui/src/plugin.js generated vendored Normal file
View file

@ -0,0 +1,112 @@
import document from 'global/document';
import videojs from 'video.js';
import {version as VERSION} from '../package.json';
import PlaylistMenu from './playlist-menu';
// see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/pointerevents.js
const supportsCssPointerEvents = (() => {
const element = document.createElement('x');
element.style.cssText = 'pointer-events:auto';
return element.style.pointerEvents === 'auto';
})();
const defaults = {
className: 'vjs-playlist',
playOnSelect: false,
supportsCssPointerEvents
};
const Plugin = videojs.getPlugin('plugin');
/**
* Initialize the plugin on a player.
*
* @param {Object} [options]
* An options object.
*
* @param {HTMLElement} [options.el]
* A DOM element to use as a root node for the playlist.
*
* @param {string} [options.className]
* An HTML class name to use to find a root node for the playlist.
*
* @param {boolean} [options.playOnSelect = false]
* If true, will attempt to begin playback upon selecting a new
* playlist item in the UI.
*/
class PlaylistUI extends Plugin {
constructor(player, options) {
super(player, options);
if (!player.usingPlugin('playlist')) {
player.log.error('videojs-playlist plugin is required by the videojs-playlist-ui plugin');
return;
}
options = this.options_ = videojs.obj.merge(defaults, options);
if (!videojs.dom.isEl(options.el)) {
options.el = this.findRoot_(options.className);
}
// Expose the playlist menu component on the player as well as the plugin
// This is a bit of an anti-pattern, but it's been that way forever and
// there are likely to be integrations relying on it.
this.playlistMenu = player.playlistMenu = new PlaylistMenu(player, options);
}
/**
* Dispose the plugin.
*/
dispose() {
super.dispose();
this.playlistMenu.dispose();
}
/**
* Returns a boolean indicating whether an element has child elements.
*
* Note that this is distinct from whether it has child _nodes_.
*
* @param {HTMLElement} el
* A DOM element.
*
* @return {boolean}
* Whether the element has child elements.
*/
hasChildEls_(el) {
for (let i = 0; i < el.childNodes.length; i++) {
if (videojs.dom.isEl(el.childNodes[i])) {
return true;
}
}
return false;
}
/**
* Finds the first empty root element.
*
* @param {string} className
* An HTML class name to search for.
*
* @return {HTMLElement}
* A DOM element to use as the root for a playlist.
*/
findRoot_(className) {
const all = document.querySelectorAll('.' + className);
for (let i = 0; i < all.length; i++) {
if (!this.hasChildEls_(all[i])) {
return all[i];
}
}
}
}
videojs.registerPlugin('playlistUi', PlaylistUI);
PlaylistUI.VERSION = VERSION;
export default PlaylistUI;

269
node_modules/videojs-playlist-ui/src/plugin.scss generated vendored Normal file
View file

@ -0,0 +1,269 @@
// The default color for the playlist menu background, almost black
$background-color: #1a1a1a;
// The color used to emphasize the currently playing video and for effects
$highlight-color: #141a21;
// The primary foreground color
$main-color: #fff;
// Background color for thumbnail placeholders
$placeholder-background-color: #303030;
// Rules common to mouse and touch devices
.vjs-playlist {
padding: 0;
background-color: $background-color;
color: $main-color;
list-style-type: none;
img {
display: block;
}
.vjs-playlist-item-list {
position: relative;
margin: 0;
padding: 0;
list-style: none;
}
.vjs-playlist-item {
position: relative;
cursor: pointer;
overflow: hidden;
}
.vjs-playlist-thumbnail-placeholder {
background: $placeholder-background-color;
}
.vjs-playlist-now-playing-text {
display: none;
position: absolute;
top: 0;
left: 0;
// This keeps this element aligned vertically with vjs-playlist-name.
padding-left: 2px;
margin: .8rem;
}
.vjs-playlist-duration {
position: absolute;
top: .5rem;
left: .5rem;
padding: 2px 5px 3px;
margin-left: 2px;
background-color: rgba(26, 26, 26, 0.8);
}
.vjs-playlist-title-container {
position: absolute;
bottom: 0;
box-sizing: border-box;
width: 100%;
padding: .5rem .8rem;
text-shadow: 1px 1px 2px black,
-1px 1px 2px black,
1px -1px 2px black,
-1px -1px 2px black;
}
.vjs-playlist-name {
display: block;
max-height: 2.5em;
// So drop shadow doesn't get cut off as overflow
padding: 0 0 4px 2px;
font-style: normal;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
// line-height: normal was causing overflow cutoff issues on Android, since normal
// lets different user agents pick a value. Here we set a consistent precise value.
line-height: 20px;
}
.vjs-playlist-description{
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: block;
font-size: 14px;
padding: 0 0 0 2px;
}
.vjs-up-next-text {
display: none;
padding: .1rem 2px;
font-size: .8em;
text-transform: uppercase;
}
.vjs-up-next {
.vjs-up-next-text {
display: block;
}
}
// Selected item rules
.vjs-selected {
background-color: $highlight-color;
img {
opacity: .2;
}
.vjs-playlist-duration {
display: none;
}
.vjs-playlist-now-playing-text {
display: block;
}
.vjs-playlist-title-container {
text-shadow: none;
}
}
}
// Vertical/default playlist orientation
.vjs-playlist-vertical {
overflow-x: hidden;
overflow-y: auto;
img {
width: 100%;
min-height: 54px;
}
.vjs-playlist-item {
margin-bottom: 5px;
}
.vjs-playlist-thumbnail {
display: block;
width: 100%;
}
.vjs-playlist-thumbnail-placeholder {
height: 100px;
}
}
// Horizontal playlist orientation
.vjs-playlist-horizontal {
overflow-x: auto;
overflow-y: hidden;
img {
min-width: 100px;
height: 100%;
}
.vjs-playlist-item-list {
height: 100%;
white-space: nowrap;
}
.vjs-playlist-item {
display: inline-block;
height: 100%;
margin-right: 5px;
}
.vjs-playlist-thumbnail {
display: block;
height: 100%;
}
.vjs-playlist-thumbnail-placeholder {
height: 100%;
width: 180px;
}
}
// prevent clicks and scrolling from affecting the playlist during ads
.vjs-playlist.vjs-ad-playing.vjs-csspointerevents {
pointer-events: none;
overflow: auto;
}
// darken the playlist menu display to indicate it's not interactive during ads
.vjs-playlist.vjs-ad-playing .vjs-playlist-ad-overlay {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
// Parametric rules. These are specialized for touch and mouse-based devices
@mixin playlist-base($base-font-size: 14px) {
font-size: $base-font-size;
.vjs-playlist-description {
height: $base-font-size * 2;
line-height: ceil($base-font-size * 1.5);
}
}
// Touch-device playlist dimensions
.vjs-playlist {
@include playlist-base();
}
// Mouse-only playlist dimensions
.vjs-mouse.vjs-playlist {
@include playlist-base(15px);
}
// Larger font size for larger player
@media (min-width: 600px) {
.vjs-mouse.vjs-playlist {
@include playlist-base(17px);
}
.vjs-playlist .vjs-playlist-name {
line-height: 22px;
}
}
// Don't show now playing / up next when there isn't room
@media (max-width: 520px) {
// These styles exist both with and without .vjs-mouse
.vjs-playlist .vjs-selected .vjs-playlist-now-playing-text,
.vjs-playlist .vjs-up-next .vjs-up-next-text {
display: none;
}
.vjs-mouse.vjs-playlist .vjs-selected .vjs-playlist-now-playing-text,
.vjs-mouse.vjs-playlist .vjs-up-next .vjs-up-next-text {
display: none;
}
}
// If now playing / up next are shown, make sure there is room.
// Only affects thumbnails with very wide aspect ratio, which get
// stretched vertically. We could avoid the stretching by making this a
// CSS background image and using background-size: cover; but then it'd
// get clipped, so not sure that's better.
@media (min-width: 521px) {
.vjs-playlist img {
min-height: 85px;
}
}
// Don't show duration when there isn't room
@media (max-width: 750px) {
.vjs-playlist .vjs-playlist-duration {
display: none;
}
}

6966
node_modules/videojs-playlist-ui/test/dist/bundle.js generated vendored Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

869
node_modules/videojs-playlist-ui/test/plugin.test.js generated vendored Normal file
View file

@ -0,0 +1,869 @@
/* eslint-disable no-console */
import document from 'global/document';
import window from 'global/window';
import QUnit from 'qunit';
import sinon from 'sinon';
import videojs from 'video.js';
import 'videojs-playlist';
import '../src/plugin';
const playlist = [{
name: 'Movie 1',
description: 'Movie 1 description',
duration: 100,
data: {
id: '1',
foo: 'bar'
},
sources: [{
src: '//example.com/movie1.mp4',
type: 'video/mp4'
}]
}, {
sources: [{
src: '//example.com/movie2.mp4',
type: 'video/mp4'
}],
thumbnail: '//example.com/movie2.jpg'
}];
const resolveUrl = url => {
const a = document.createElement('a');
a.href = url;
return a.href;
};
const Html5 = videojs.getTech('Html5');
QUnit.test('the environment is sane', function(assert) {
assert.ok(true, 'everything is swell');
});
function setup() {
this.oldVideojsBrowser = videojs.browser;
videojs.browser = videojs.obj.merge({}, videojs.browser);
this.fixture = document.querySelector('#qunit-fixture');
// force HTML support so the tests run in a reasonable
// environment under phantomjs
this.realIsHtmlSupported = Html5.isSupported;
Html5.isSupported = function() {
return true;
};
// create a video element
const video = document.createElement('video');
this.fixture.appendChild(video);
// create a video.js player
this.player = videojs(video);
// Create two playlist container elements.
this.fixture.appendChild(videojs.dom.createEl('div', {className: 'vjs-playlist'}));
this.fixture.appendChild(videojs.dom.createEl('div', {className: 'vjs-playlist'}));
}
function teardown() {
videojs.browser = this.oldVideojsBrowser;
Html5.isSupported = this.realIsHtmlSupported;
this.player.dispose();
this.player = null;
videojs.dom.emptyEl(this.fixture);
}
QUnit.module('videojs-playlist-ui', {beforeEach: setup, afterEach: teardown});
QUnit.test('registers itself', function(assert) {
assert.ok(this.player.playlistUi, 'registered the plugin');
});
QUnit.test('errors if used without the playlist plugin', function(assert) {
sinon.spy(this.player.log, 'error');
this.player.playlist = null;
this.player.playlistUi();
assert.ok(this.player.log.error.calledOnce, 'player.log.error was called');
});
QUnit.test('is empty if the playlist plugin isn\'t initialized', function(assert) {
this.player.playlistUi();
const items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.ok(this.fixture.querySelector('.vjs-playlist'), 'created the menu');
assert.strictEqual(items.length, 0, 'displayed no items');
});
QUnit.test('can be initialized with an element', function(assert) {
const elem = videojs.dom.createEl('div');
this.player.playlist(playlist);
this.player.playlistUi({el: elem});
assert.strictEqual(
elem.querySelectorAll('li.vjs-playlist-item').length,
playlist.length,
'created an element for each playlist item'
);
});
QUnit.test('can look for an element with the class "vjs-playlist" that is not already in use', function(assert) {
const firstEl = this.fixture.querySelectorAll('.vjs-playlist')[0];
const secondEl = this.fixture.querySelectorAll('.vjs-playlist')[1];
// Give the firstEl a child, so the plugin thinks it is in use and moves on
// to the next one.
firstEl.appendChild(videojs.dom.createEl('div'));
this.player.playlist(playlist);
this.player.playlistUi();
assert.strictEqual(this.player.playlistMenu.el(), secondEl, 'used the first matching/empty element');
assert.strictEqual(
secondEl.querySelectorAll('li.vjs-playlist-item').length,
playlist.length,
'found an element for each playlist item'
);
});
QUnit.test('can look for an element with a custom class that is not already in use', function(assert) {
const firstEl = videojs.dom.createEl('div', {className: 'super-playlist'});
const secondEl = videojs.dom.createEl('div', {className: 'super-playlist'});
// Give the firstEl a child, so the plugin thinks it is in use and moves on
// to the next one.
firstEl.appendChild(videojs.dom.createEl('div'));
this.fixture.appendChild(firstEl);
this.fixture.appendChild(secondEl);
this.player.playlist(playlist);
this.player.playlistUi({
className: 'super-playlist'
});
assert.strictEqual(this.player.playlistMenu.el(), secondEl, 'used the first matching/empty element');
assert.strictEqual(
this.fixture.querySelectorAll('li.vjs-playlist-item').length,
playlist.length,
'created an element for each playlist item'
);
});
QUnit.test('specializes the class name if touch input is absent', function(assert) {
videojs.browser.TOUCH_ENABLED = false;
this.player.playlist(playlist);
this.player.playlistUi();
assert.ok(this.player.playlistMenu.hasClass('vjs-mouse'), 'marked the playlist menu');
});
QUnit.test('can be re-initialized without doubling the contents of the list', function(assert) {
const el = this.fixture.querySelectorAll('.vjs-playlist')[0];
this.player.playlist(playlist);
this.player.playlistUi();
this.player.playlistUi();
this.player.playlistUi();
assert.strictEqual(this.player.playlistMenu.el(), el, 'used the first matching/empty element');
assert.strictEqual(
el.querySelectorAll('li.vjs-playlist-item').length,
playlist.length,
'found an element for each playlist item'
);
});
QUnit.module('videojs-playlist-ui: Components', {beforeEach: setup, afterEach: teardown});
// --------------------
// Creation and Updates
// --------------------
QUnit.test('includes the video name if provided', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
const items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(
items[0].querySelector('.vjs-playlist-name').textContent,
playlist[0].name,
'wrote the name'
);
assert.strictEqual(
items[1].querySelector('.vjs-playlist-name').textContent,
'Untitled Video',
'wrote a placeholder for the name'
);
});
QUnit.test('includes the video description if user specifies it', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi({showDescription: true});
const items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(
items[0].querySelector('.vjs-playlist-description').textContent,
playlist[0].description,
'description is displayed'
);
});
QUnit.test('hides video description by default', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
const items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(
items[0].querySelector('.vjs-playlist-description'),
null,
'description is not displayed'
);
});
QUnit.test('includes custom data attribute if provided', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
const items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(
items[0].dataset.id,
playlist[0].data.id,
'set a single data attribute'
);
assert.strictEqual(
items[0].dataset.id,
'1',
'set a single data attribute (actual value)'
);
assert.strictEqual(
items[0].dataset.foo,
playlist[0].data.foo,
'set an addtional data attribute'
);
assert.strictEqual(
items[0].dataset.foo,
'bar',
'set an addtional data attribute'
);
});
QUnit.test('outputs a <picture> for simple thumbnails', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
const pictures = this.fixture.querySelectorAll('.vjs-playlist-item picture');
assert.strictEqual(pictures.length, 1, 'output one picture');
const imgs = pictures[0].querySelectorAll('img');
assert.strictEqual(imgs.length, 1, 'output one img');
assert.strictEqual(imgs[0].src, window.location.protocol + playlist[1].thumbnail, 'set the src attribute');
});
QUnit.test('outputs a <picture> for responsive thumbnails', function(assert) {
const playlistOverride = [{
sources: [{
src: '//example.com/movie.mp4',
type: 'video/mp4'
}],
thumbnail: [{
srcset: '/test/example/oceans.jpg',
type: 'image/jpeg',
media: '(min-width: 400px;)'
}, {
src: '/test/example/oceans-low.jpg'
}]
}];
this.player.playlist(playlistOverride);
this.player.playlistUi();
const sources = this.fixture.querySelectorAll('.vjs-playlist-item picture source');
const imgs = this.fixture.querySelectorAll('.vjs-playlist-item picture img');
assert.strictEqual(sources.length, 1, 'output one source');
assert.strictEqual(
sources[0].srcset,
playlistOverride[0].thumbnail[0].srcset,
'wrote the srcset attribute'
);
assert.strictEqual(
sources[0].type,
playlistOverride[0].thumbnail[0].type,
'wrote the type attribute'
);
assert.strictEqual(
sources[0].media,
playlistOverride[0].thumbnail[0].media,
'wrote the type attribute'
);
assert.strictEqual(imgs.length, 1, 'output one img');
assert.strictEqual(
imgs[0].src,
resolveUrl(playlistOverride[0].thumbnail[1].src),
'output the img src attribute'
);
});
QUnit.test('outputs a placeholder for items without thumbnails', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
const thumbnails = this.fixture.querySelectorAll('.vjs-playlist-item .vjs-playlist-thumbnail');
assert.strictEqual(thumbnails.length, playlist.length, 'output two thumbnails');
assert.strictEqual(thumbnails[0].nodeName.toLowerCase(), 'div', 'the second is a placeholder');
});
QUnit.test('includes the duration if one is provided', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
const durations = this.fixture.querySelectorAll('.vjs-playlist-item .vjs-playlist-duration');
assert.strictEqual(durations.length, 1, 'skipped the item without a duration');
assert.strictEqual(
durations[0].textContent,
'1:40',
'wrote the duration'
);
assert.strictEqual(
durations[0].getAttribute('datetime'),
'PT0H0M' + playlist[0].duration + 'S',
'wrote a machine-readable datetime'
);
});
QUnit.test('marks the selected playlist item on startup', function(assert) {
this.player.playlist(playlist);
this.player.currentSrc = () => playlist[0].sources[0].src;
this.player.playlistUi();
const selectedItems = this.fixture.querySelectorAll('.vjs-playlist-item.vjs-selected');
assert.strictEqual(selectedItems.length, 1, 'marked one playlist item');
assert.strictEqual(
selectedItems[0].querySelector('.vjs-playlist-name').textContent,
playlist[0].name,
'marked the first playlist item'
);
});
QUnit.test('updates the selected playlist item on loadstart', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
this.player.playlist.currentItem(1);
this.player.currentSrc = () => playlist[1].sources[0].src;
this.player.trigger('loadstart');
const selectedItems = this.fixture.querySelectorAll('.vjs-playlist-item.vjs-selected');
assert.strictEqual(
this.fixture.querySelectorAll('.vjs-playlist-item').length,
playlist.length,
'displayed the correct number of items'
);
assert.strictEqual(selectedItems.length, 1, 'marked one playlist item');
assert.strictEqual(
selectedItems[0].querySelector('img').src,
resolveUrl(playlist[1].thumbnail),
'marked the second playlist item'
);
});
QUnit.test('selects no item if the playlist is not in use', function(assert) {
this.player.playlist(playlist);
this.player.playlist.currentItem = () => -1;
this.player.playlistUi();
this.player.trigger('loadstart');
assert.strictEqual(
this.fixture.querySelectorAll('.vjs-playlist-item.vjs-selected').length,
0,
'no items selected'
);
});
QUnit.test('updates on "playlistchange", different lengths', function(assert) {
this.player.playlist([]);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 0, 'no items initially');
this.player.playlist(playlist);
this.player.trigger('playlistchange');
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, playlist.length, 'updated with the new items');
});
QUnit.test('updates on "playlistchange", equal lengths', function(assert) {
this.player.playlist([{sources: []}, {sources: []}]);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist(playlist);
this.player.trigger('playlistchange');
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, playlist.length, 'updated with the new items');
assert.strictEqual(this.player.playlistMenu.items[0].item, playlist[0], 'we have updated items');
assert.strictEqual(this.player.playlistMenu.items[1].item, playlist[1], 'we have updated items');
});
QUnit.test('updates on "playlistchange", update selection', function(assert) {
this.player.playlist(playlist);
this.player.currentSrc = function() {
return playlist[0].sources[0].src;
};
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
assert.ok((/vjs-selected/).test(items[0].getAttribute('class')), 'first item is selected by default');
this.player.playlist.currentItem(1);
this.player.currentSrc = function() {
return playlist[1].sources[0].src;
};
this.player.trigger('playlistchange');
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, playlist.length, 'updated with the new items');
assert.ok((/vjs-selected/).test(items[1].getAttribute('class')), 'second item is selected after update');
assert.ok(!(/vjs-selected/).test(items[0].getAttribute('class')), 'first item is not selected after update');
});
QUnit.test('updates on "playlistsorted", different lengths', function(assert) {
this.player.playlist([]);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 0, 'no items initially');
this.player.playlist(playlist);
this.player.trigger('playlistsorted');
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, playlist.length, 'updated with the new items');
});
QUnit.test('updates on "playlistsorted", equal lengths', function(assert) {
this.player.playlist([{sources: []}, {sources: []}]);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist(playlist);
this.player.trigger('playlistsorted');
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, playlist.length, 'updated with the new items');
assert.strictEqual(this.player.playlistMenu.items[0].item, playlist[0], 'we have updated items');
assert.strictEqual(this.player.playlistMenu.items[1].item, playlist[1], 'we have updated items');
});
QUnit.test('updates on "playlistsorted", update selection', function(assert) {
this.player.playlist(playlist);
this.player.currentSrc = function() {
return playlist[0].sources[0].src;
};
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
assert.ok((/vjs-selected/).test(items[0].getAttribute('class')), 'first item is selected by default');
this.player.playlist.currentItem(1);
this.player.currentSrc = function() {
return playlist[1].sources[0].src;
};
this.player.trigger('playlistsorted');
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, playlist.length, 'updated with the new items');
assert.ok((/vjs-selected/).test(items[1].getAttribute('class')), 'second item is selected after update');
assert.ok(!(/vjs-selected/).test(items[0].getAttribute('class')), 'first item is not selected after update');
});
QUnit.test('tracks when an ad is playing', function(assert) {
this.player.playlist([]);
this.player.playlistUi();
this.player.duration = () => 5;
const playlistMenu = this.player.playlistMenu;
assert.ok(
!playlistMenu.hasClass('vjs-ad-playing'),
'does not have class vjs-ad-playing'
);
this.player.trigger('adstart');
assert.ok(
playlistMenu.hasClass('vjs-ad-playing'),
'has class vjs-ad-playing'
);
this.player.trigger('adend');
assert.ok(
!playlistMenu.hasClass('vjs-ad-playing'),
'does not have class vjs-ad-playing'
);
});
// -----------
// Interaction
// -----------
QUnit.test('changes the selection when tapped', function(assert) {
let playCalled = false;
this.player.playlist(playlist);
this.player.playlistUi({playOnSelect: true});
this.player.play = function() {
playCalled = true;
};
let sources;
this.player.src = (src) => {
if (src) {
sources = src;
}
return sources[0];
};
this.player.currentSrc = () => sources[0].src;
this.player.playlistMenu.items[1].trigger('tap');
// trigger a loadstart synchronously to simplify the test
this.player.trigger('loadstart');
assert.ok(
this.player.playlistMenu.items[1].hasClass('vjs-selected'),
'selected the new item'
);
assert.ok(
!this.player.playlistMenu.items[0].hasClass('vjs-selected'),
'deselected the old item'
);
assert.strictEqual(playCalled, true, 'play gets called if option is set');
});
QUnit.test('play should not get called by default upon selection of menu items ', function(assert) {
let playCalled = false;
this.player.playlist(playlist);
this.player.playlistUi();
this.player.play = function() {
playCalled = true;
};
let sources;
this.player.src = (src) => {
if (src) {
sources = src;
}
return sources[0];
};
this.player.currentSrc = () => sources[0].src;
this.player.playlistMenu.items[1].trigger('tap');
// trigger a loadstart synchronously to simplify the test
this.player.trigger('loadstart');
assert.strictEqual(playCalled, false, 'play should not get called by default');
});
QUnit.test('disposing the playlist menu nulls out the player\'s reference to it', function(assert) {
assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 2, 'there are two playlist containers at the start');
this.player.playlist(playlist);
this.player.playlistUi();
this.player.playlistMenu.dispose();
assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 1, 'only the unused playlist container is left');
assert.strictEqual(this.player.playlistMenu, null, 'the playlistMenu property is null');
});
QUnit.test('disposing the playlist menu removes playlist menu items', function(assert) {
assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 2, 'there are two playlist containers at the start');
this.player.playlist(playlist);
this.player.playlistUi();
// Cache some references so we can refer to them after disposal.
const items = [].concat(this.player.playlistMenu.items);
this.player.playlistMenu.dispose();
assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 1, 'only the unused playlist container is left');
assert.strictEqual(this.player.playlistMenu, null, 'the playlistMenu property is null');
items.forEach(i => {
assert.strictEqual(i.el_, null, `the item "${i.id_}" has been disposed`);
});
});
QUnit.test('disposing the player also disposes the playlist menu', function(assert) {
assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 2, 'there are two playlist containers at the start');
this.player.playlist(playlist);
this.player.playlistUi();
this.player.dispose();
assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 1, 'only the unused playlist container is left');
assert.strictEqual(this.player.playlistMenu, null, 'the playlistMenu property is null');
});
QUnit.module('videojs-playlist-ui: add/remove', {beforeEach: setup, afterEach: teardown});
QUnit.test('adding zero items at the start of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add([], 0);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, playlist.length, 'correct number of items');
});
QUnit.test('adding one item at the start of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add({name: 'Test 1'}, 0);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 3, 'correct number of items');
assert.strictEqual(items[0].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
});
QUnit.test('adding two items at the start of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 0);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 4, 'correct number of items');
assert.strictEqual(items[0].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
assert.strictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'has the correct name in the playlist DOM');
});
QUnit.test('adding one item in the middle of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add({name: 'Test 1'}, 1);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 3, 'correct number of items');
assert.strictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
});
QUnit.test('adding two items in the middle of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 1);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 4, 'correct number of items');
assert.strictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
assert.strictEqual(items[2].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'has the correct name in the playlist DOM');
});
QUnit.test('adding one item at the end of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add({name: 'Test 1'}, playlist.length);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 3, 'correct number of items');
assert.strictEqual(items[2].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
});
QUnit.test('adding two items at the end of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], playlist.length);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 4, 'correct number of items');
assert.strictEqual(items[2].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
assert.strictEqual(items[3].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'has the correct name in the playlist DOM');
});
QUnit.test('removing zero items at the start of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.remove(0, 0);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, playlist.length, 'correct number of items');
});
QUnit.test('removing one item at the start of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add({name: 'Test 1'}, 0);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 3, 'correct number of items');
this.player.playlist.remove(0, 1);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'correct number of items');
assert.notStrictEqual(items[0].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
});
QUnit.test('removing two items at the start of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 0);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 4, 'correct number of items');
this.player.playlist.remove(0, 2);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.notStrictEqual(items[0].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'the added item was properly removed from the DOM');
});
QUnit.test('removing one item in the middle of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add({name: 'Test 1'}, 1);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 3, 'correct number of items');
this.player.playlist.remove(1, 1);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'correct number of items');
assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
});
QUnit.test('removing two items in the middle of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 1);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 4, 'correct number of items');
this.player.playlist.remove(1, 2);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
assert.strictEqual(items[2], undefined, 'the added item was properly removed from the DOM');
});
QUnit.test('removing one item at the end of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add({name: 'Test 1'}, 2);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 3, 'correct number of items');
this.player.playlist.remove(2, 1);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'correct number of items');
assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
});
QUnit.test('removing two items at the end of the playlist', function(assert) {
this.player.playlist(playlist);
this.player.playlistUi();
let items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'two items initially');
this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 2);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 4, 'correct number of items');
this.player.playlist.remove(2, 2);
items = this.fixture.querySelectorAll('.vjs-playlist-item');
assert.strictEqual(items.length, 2, 'correct number of items');
assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'the added item was properly removed from the DOM');
});