videojs-playlist-ui
Advanced tools
Comparing version 4.2.1 to 5.0.0
@@ -1,2 +0,2 @@ | ||
/*! @name videojs-playlist-ui @version 4.2.1 @license Apache-2.0 */ | ||
/*! @name videojs-playlist-ui @version 5.0.0 @license Apache-2.0 */ | ||
'use strict'; | ||
@@ -12,42 +12,5 @@ | ||
var version = "4.2.1"; | ||
var version = "5.0.0"; | ||
const dom = videojs__default["default"].dom; | ||
const merge = videojs__default["default"].obj ? videojs__default["default"].obj.merge : videojs__default["default"].mergeOptions; | ||
const formatTime = videojs__default["default"].time ? videojs__default["default"].time.formatTime : videojs__default["default"].formatTime; // 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 | ||
}; // 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) { | ||
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$1 = videojs__default["default"].getComponent('Component'); | ||
const createThumbnail = function (thumbnail) { | ||
@@ -59,6 +22,4 @@ if (!thumbnail) { | ||
} | ||
const picture = document__default["default"].createElement('picture'); | ||
picture.className = 'vjs-playlist-thumbnail'; | ||
if (typeof thumbnail === 'string') { | ||
@@ -73,2 +34,3 @@ // simple thumbnails | ||
// responsive thumbnails | ||
// additional variations of a <picture> are specified as | ||
@@ -78,12 +40,12 @@ // <source> elements | ||
const variant = thumbnail[i]; | ||
const source = document__default["default"].createElement('source'); // transfer the properties of each variant onto a <source> | ||
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> | ||
} | ||
// the default version of a <picture> is specified by an <img> | ||
const variant = thumbnail[thumbnail.length - 1]; | ||
@@ -93,16 +55,10 @@ const img = document__default["default"].createElement('img'); | ||
img.alt = ''; | ||
for (const prop in variant) { | ||
img[prop] = variant[prop]; | ||
} | ||
picture.appendChild(img); | ||
} | ||
return picture; | ||
}; | ||
const Component = videojs__default["default"].getComponent('Component'); | ||
class PlaylistMenuItem extends Component { | ||
class PlaylistMenuItem extends Component$1 { | ||
constructor(player, playlistItem, settings) { | ||
@@ -112,3 +68,2 @@ if (!playlistItem.item) { | ||
} | ||
playlistItem.showDescription = settings.showDescription; | ||
@@ -122,3 +77,2 @@ super(player, playlistItem); | ||
} | ||
handleKeyDown_(event) { | ||
@@ -131,6 +85,4 @@ // keycode 13 is <Enter> | ||
} | ||
switchPlaylistItem_(event) { | ||
this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item)); | ||
if (this.playOnSelect) { | ||
@@ -140,3 +92,2 @@ this.player_.play(); | ||
} | ||
createEl() { | ||
@@ -146,3 +97,2 @@ const li = document__default["default"].createElement('li'); | ||
const showDescription = this.options_.showDescription; | ||
if (typeof item.data === 'object') { | ||
@@ -155,12 +105,13 @@ const dataKeys = Object.keys(item.data); | ||
} | ||
li.className = 'vjs-playlist-item'; | ||
li.setAttribute('tabIndex', 0); // Thumbnail image | ||
li.setAttribute('tabIndex', 0); | ||
// Thumbnail image | ||
this.thumbnail = createThumbnail(item.thumbnail); | ||
li.appendChild(this.thumbnail); // Duration | ||
li.appendChild(this.thumbnail); | ||
// Duration | ||
if (item.duration) { | ||
const duration = document__default["default"].createElement('time'); | ||
const time = formatTime(item.duration); | ||
const time = videojs__default["default"].time.formatTime(item.duration); | ||
duration.className = 'vjs-playlist-duration'; | ||
@@ -170,5 +121,5 @@ duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S'); | ||
li.appendChild(duration); | ||
} // Now playing | ||
} | ||
// Now playing | ||
const nowPlayingEl = document__default["default"].createElement('span'); | ||
@@ -179,8 +130,10 @@ const nowPlayingText = this.localize('Now Playing'); | ||
nowPlayingEl.setAttribute('title', nowPlayingText); | ||
this.thumbnail.appendChild(nowPlayingEl); // Title container contains title and "up next" | ||
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 | ||
this.thumbnail.appendChild(titleContainerEl); | ||
// Up next | ||
const upNextEl = document__default["default"].createElement('span'); | ||
@@ -191,4 +144,5 @@ const upNextText = this.localize('Up Next'); | ||
upNextEl.setAttribute('title', upNextText); | ||
titleContainerEl.appendChild(upNextEl); // Video title | ||
titleContainerEl.appendChild(upNextEl); | ||
// Video title | ||
const titleEl = document__default["default"].createElement('cite'); | ||
@@ -199,6 +153,8 @@ const titleText = item.name || this.localize('Untitled Video'); | ||
titleEl.setAttribute('title', titleText); | ||
titleContainerEl.appendChild(titleEl); // Populate thumbnails alt with the video title | ||
titleContainerEl.appendChild(titleEl); | ||
this.thumbnail.getElementsByTagName('img').alt = titleText; // We add thumbnail video description only if specified in playlist options | ||
// 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) { | ||
@@ -212,17 +168,30 @@ const descriptionEl = document__default["default"].createElement('div'); | ||
} | ||
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) { | ||
if (!player.playlist) { | ||
throw new Error('videojs-playlist is required for the playlist component'); | ||
} | ||
super(player, options); | ||
this.items = []; | ||
if (options.horizontal) { | ||
@@ -232,22 +201,28 @@ this.addClass('vjs-playlist-horizontal'); | ||
this.addClass('vjs-playlist-vertical'); | ||
} // If CSS pointer events aren't supported, we have to prevent | ||
} | ||
// 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)); | ||
this.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], event => { | ||
this.update(); | ||
}); // Keep track of whether an ad is playing so that the menu | ||
// Keep track of whether an ad is playing so that the menu | ||
// appearance can be adapted appropriately | ||
this.on(player, 'adstart', () => { | ||
@@ -267,9 +242,7 @@ this.addClass('vjs-ad-playing'); | ||
} | ||
createEl() { | ||
return dom.createEl('div', { | ||
return videojs__default["default"].dom.createEl('div', { | ||
className: this.options_.className | ||
}); | ||
} | ||
empty_() { | ||
@@ -281,3 +254,2 @@ if (this.items && this.items.length) { | ||
} | ||
createPlaylist_() { | ||
@@ -287,3 +259,2 @@ const playlist = this.player_.playlist() || []; | ||
let overlay = this.el_.querySelector('.vjs-playlist-ad-overlay'); | ||
if (!list) { | ||
@@ -294,5 +265,5 @@ list = document__default["default"].createElement('ol'); | ||
} | ||
this.empty_(); | ||
this.empty_(); // create new items | ||
// create new items | ||
for (let i = 0; i < playlist.length; i++) { | ||
@@ -304,6 +275,6 @@ const item = new PlaylistMenuItem(this.player_, { | ||
list.appendChild(item.el_); | ||
} // Inject the ad overlay. We use this element to block clicks during ad | ||
} | ||
// Inject the ad overlay. We use this element to block clicks during ad | ||
// playback and darken the menu to indicate inactivity | ||
if (!overlay) { | ||
@@ -316,13 +287,11 @@ overlay = document__default["default"].createElement('li'); | ||
list.appendChild(overlay); | ||
} // select the current playlist item | ||
} | ||
// 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) { | ||
dom.addClass(thumbnail, 'vjs-playlist-now-playing'); | ||
videojs__default["default"].dom.addClass(thumbnail, 'vjs-playlist-now-playing'); | ||
} | ||
@@ -332,6 +301,58 @@ } | ||
/** | ||
* 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) { | ||
@@ -343,3 +364,2 @@ // if the menu is currently empty or the state is obviously out | ||
} | ||
for (let i = 0; i < this.items.length; i++) { | ||
@@ -352,17 +372,13 @@ if (this.items[i].item !== playlist[i]) { | ||
} | ||
} // the playlist itself is unchanged so just update the selection | ||
} | ||
// 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()) { | ||
dom.addClass(item.thumbnail, 'vjs-playlist-now-playing'); | ||
videojs__default["default"].dom.addClass(item.thumbnail, 'vjs-playlist-now-playing'); | ||
} | ||
notUpNext(item); | ||
@@ -378,50 +394,18 @@ } else if (i === currentItem + 1) { | ||
} | ||
} | ||
/** | ||
* 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. | ||
*/ | ||
videojs__default["default"].registerComponent('PlaylistMenu', PlaylistMenu); | ||
const hasChildEls = el => { | ||
for (let i = 0; i < el.childNodes.length; i++) { | ||
if (dom.isEl(el.childNodes[i])) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
// 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 | ||
}; | ||
/** | ||
* 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. | ||
*/ | ||
const Plugin = videojs__default["default"].getPlugin('plugin'); | ||
const findRoot = className => { | ||
const all = document__default["default"].querySelectorAll('.' + className); | ||
let el; | ||
for (let i = 0; i < all.length; i++) { | ||
if (!hasChildEls(all[i])) { | ||
el = all[i]; | ||
break; | ||
} | ||
} | ||
return el; | ||
}; | ||
/** | ||
@@ -443,58 +427,69 @@ * Initialize the plugin on a player. | ||
*/ | ||
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); | ||
} | ||
const playlistUi = function (options) { | ||
const player = this; | ||
if (!player.playlist) { | ||
throw new Error('videojs-playlist plugin is required by the videojs-playlist-ui plugin'); | ||
// 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); | ||
} | ||
if (dom.isEl(options)) { | ||
videojs__default["default"].log.warn('videojs-playlist-ui: Passing an element directly to playlistUi() is deprecated, use the "el" option instead!'); | ||
options = { | ||
el: options | ||
}; | ||
/** | ||
* Dispose the plugin. | ||
*/ | ||
dispose() { | ||
super.dispose(); | ||
this.playlistMenu.dispose(); | ||
} | ||
options = merge(defaults, options); // If the player is already using this plugin, remove the pre-existing | ||
// PlaylistMenu, but retain the element and its location in the DOM because | ||
// it will be re-used. | ||
if (player.playlistMenu) { | ||
const el = player.playlistMenu.el(); // Catch cases where the menu may have been disposed elsewhere or the | ||
// element removed from the DOM. | ||
if (el) { | ||
const parentNode = el.parentNode; | ||
const nextSibling = el.nextSibling; // Disposing the menu will remove `el` from the DOM, but we need to | ||
// empty it ourselves to be sure. | ||
player.playlistMenu.dispose(); | ||
dom.emptyEl(el); // Put the element back in its place. | ||
if (nextSibling) { | ||
parentNode.insertBefore(el, nextSibling); | ||
} else { | ||
parentNode.appendChild(el); | ||
/** | ||
* 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; | ||
} | ||
options.el = el; | ||
} | ||
return false; | ||
} | ||
if (!dom.isEl(options.el)) { | ||
options.el = findRoot(options.className); | ||
/** | ||
* 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; | ||
player.playlistMenu = new PlaylistMenu(player, options); | ||
}; // register components | ||
videojs__default["default"].registerComponent('PlaylistMenu', PlaylistMenu); | ||
videojs__default["default"].registerComponent('PlaylistMenuItem', PlaylistMenuItem); // register the plugin | ||
videojs__default["default"].registerPlugin('playlistUi', playlistUi); | ||
playlistUi.VERSION = version; | ||
module.exports = playlistUi; | ||
module.exports = PlaylistUI; |
@@ -1,45 +0,8 @@ | ||
/*! @name videojs-playlist-ui @version 4.2.1 @license Apache-2.0 */ | ||
/*! @name videojs-playlist-ui @version 5.0.0 @license Apache-2.0 */ | ||
import document from 'global/document'; | ||
import videojs from 'video.js'; | ||
var version = "4.2.1"; | ||
var version = "5.0.0"; | ||
const dom = videojs.dom; | ||
const merge = videojs.obj ? videojs.obj.merge : videojs.mergeOptions; | ||
const formatTime = videojs.time ? videojs.time.formatTime : videojs.formatTime; // 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 | ||
}; // 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) { | ||
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$1 = videojs.getComponent('Component'); | ||
const createThumbnail = function (thumbnail) { | ||
@@ -51,6 +14,4 @@ if (!thumbnail) { | ||
} | ||
const picture = document.createElement('picture'); | ||
picture.className = 'vjs-playlist-thumbnail'; | ||
if (typeof thumbnail === 'string') { | ||
@@ -65,2 +26,3 @@ // simple thumbnails | ||
// responsive thumbnails | ||
// additional variations of a <picture> are specified as | ||
@@ -70,12 +32,12 @@ // <source> elements | ||
const variant = thumbnail[i]; | ||
const source = document.createElement('source'); // transfer the properties of each variant onto a <source> | ||
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> | ||
} | ||
// the default version of a <picture> is specified by an <img> | ||
const variant = thumbnail[thumbnail.length - 1]; | ||
@@ -85,16 +47,10 @@ const img = document.createElement('img'); | ||
img.alt = ''; | ||
for (const prop in variant) { | ||
img[prop] = variant[prop]; | ||
} | ||
picture.appendChild(img); | ||
} | ||
return picture; | ||
}; | ||
const Component = videojs.getComponent('Component'); | ||
class PlaylistMenuItem extends Component { | ||
class PlaylistMenuItem extends Component$1 { | ||
constructor(player, playlistItem, settings) { | ||
@@ -104,3 +60,2 @@ if (!playlistItem.item) { | ||
} | ||
playlistItem.showDescription = settings.showDescription; | ||
@@ -114,3 +69,2 @@ super(player, playlistItem); | ||
} | ||
handleKeyDown_(event) { | ||
@@ -123,6 +77,4 @@ // keycode 13 is <Enter> | ||
} | ||
switchPlaylistItem_(event) { | ||
this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item)); | ||
if (this.playOnSelect) { | ||
@@ -132,3 +84,2 @@ this.player_.play(); | ||
} | ||
createEl() { | ||
@@ -138,3 +89,2 @@ const li = document.createElement('li'); | ||
const showDescription = this.options_.showDescription; | ||
if (typeof item.data === 'object') { | ||
@@ -147,12 +97,13 @@ const dataKeys = Object.keys(item.data); | ||
} | ||
li.className = 'vjs-playlist-item'; | ||
li.setAttribute('tabIndex', 0); // Thumbnail image | ||
li.setAttribute('tabIndex', 0); | ||
// Thumbnail image | ||
this.thumbnail = createThumbnail(item.thumbnail); | ||
li.appendChild(this.thumbnail); // Duration | ||
li.appendChild(this.thumbnail); | ||
// Duration | ||
if (item.duration) { | ||
const duration = document.createElement('time'); | ||
const time = formatTime(item.duration); | ||
const time = videojs.time.formatTime(item.duration); | ||
duration.className = 'vjs-playlist-duration'; | ||
@@ -162,5 +113,5 @@ duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S'); | ||
li.appendChild(duration); | ||
} // Now playing | ||
} | ||
// Now playing | ||
const nowPlayingEl = document.createElement('span'); | ||
@@ -171,8 +122,10 @@ const nowPlayingText = this.localize('Now Playing'); | ||
nowPlayingEl.setAttribute('title', nowPlayingText); | ||
this.thumbnail.appendChild(nowPlayingEl); // Title container contains title and "up next" | ||
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 | ||
this.thumbnail.appendChild(titleContainerEl); | ||
// Up next | ||
const upNextEl = document.createElement('span'); | ||
@@ -183,4 +136,5 @@ const upNextText = this.localize('Up Next'); | ||
upNextEl.setAttribute('title', upNextText); | ||
titleContainerEl.appendChild(upNextEl); // Video title | ||
titleContainerEl.appendChild(upNextEl); | ||
// Video title | ||
const titleEl = document.createElement('cite'); | ||
@@ -191,6 +145,8 @@ const titleText = item.name || this.localize('Untitled Video'); | ||
titleEl.setAttribute('title', titleText); | ||
titleContainerEl.appendChild(titleEl); // Populate thumbnails alt with the video title | ||
titleContainerEl.appendChild(titleEl); | ||
this.thumbnail.getElementsByTagName('img').alt = titleText; // We add thumbnail video description only if specified in playlist options | ||
// 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) { | ||
@@ -204,17 +160,30 @@ const descriptionEl = document.createElement('div'); | ||
} | ||
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) { | ||
if (!player.playlist) { | ||
throw new Error('videojs-playlist is required for the playlist component'); | ||
} | ||
super(player, options); | ||
this.items = []; | ||
if (options.horizontal) { | ||
@@ -224,22 +193,28 @@ this.addClass('vjs-playlist-horizontal'); | ||
this.addClass('vjs-playlist-vertical'); | ||
} // If CSS pointer events aren't supported, we have to prevent | ||
} | ||
// 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)); | ||
this.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], event => { | ||
this.update(); | ||
}); // Keep track of whether an ad is playing so that the menu | ||
// Keep track of whether an ad is playing so that the menu | ||
// appearance can be adapted appropriately | ||
this.on(player, 'adstart', () => { | ||
@@ -259,9 +234,7 @@ this.addClass('vjs-ad-playing'); | ||
} | ||
createEl() { | ||
return dom.createEl('div', { | ||
return videojs.dom.createEl('div', { | ||
className: this.options_.className | ||
}); | ||
} | ||
empty_() { | ||
@@ -273,3 +246,2 @@ if (this.items && this.items.length) { | ||
} | ||
createPlaylist_() { | ||
@@ -279,3 +251,2 @@ const playlist = this.player_.playlist() || []; | ||
let overlay = this.el_.querySelector('.vjs-playlist-ad-overlay'); | ||
if (!list) { | ||
@@ -286,5 +257,5 @@ list = document.createElement('ol'); | ||
} | ||
this.empty_(); | ||
this.empty_(); // create new items | ||
// create new items | ||
for (let i = 0; i < playlist.length; i++) { | ||
@@ -296,6 +267,6 @@ const item = new PlaylistMenuItem(this.player_, { | ||
list.appendChild(item.el_); | ||
} // Inject the ad overlay. We use this element to block clicks during ad | ||
} | ||
// Inject the ad overlay. We use this element to block clicks during ad | ||
// playback and darken the menu to indicate inactivity | ||
if (!overlay) { | ||
@@ -308,13 +279,11 @@ overlay = document.createElement('li'); | ||
list.appendChild(overlay); | ||
} // select the current playlist item | ||
} | ||
// 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) { | ||
dom.addClass(thumbnail, 'vjs-playlist-now-playing'); | ||
videojs.dom.addClass(thumbnail, 'vjs-playlist-now-playing'); | ||
} | ||
@@ -324,6 +293,58 @@ } | ||
/** | ||
* 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) { | ||
@@ -335,3 +356,2 @@ // if the menu is currently empty or the state is obviously out | ||
} | ||
for (let i = 0; i < this.items.length; i++) { | ||
@@ -344,17 +364,13 @@ if (this.items[i].item !== playlist[i]) { | ||
} | ||
} // the playlist itself is unchanged so just update the selection | ||
} | ||
// 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()) { | ||
dom.addClass(item.thumbnail, 'vjs-playlist-now-playing'); | ||
videojs.dom.addClass(item.thumbnail, 'vjs-playlist-now-playing'); | ||
} | ||
notUpNext(item); | ||
@@ -370,50 +386,18 @@ } else if (i === currentItem + 1) { | ||
} | ||
} | ||
/** | ||
* 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. | ||
*/ | ||
videojs.registerComponent('PlaylistMenu', PlaylistMenu); | ||
const hasChildEls = el => { | ||
for (let i = 0; i < el.childNodes.length; i++) { | ||
if (dom.isEl(el.childNodes[i])) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
// 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 | ||
}; | ||
/** | ||
* 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. | ||
*/ | ||
const Plugin = videojs.getPlugin('plugin'); | ||
const findRoot = className => { | ||
const all = document.querySelectorAll('.' + className); | ||
let el; | ||
for (let i = 0; i < all.length; i++) { | ||
if (!hasChildEls(all[i])) { | ||
el = all[i]; | ||
break; | ||
} | ||
} | ||
return el; | ||
}; | ||
/** | ||
@@ -435,58 +419,69 @@ * Initialize the plugin on a player. | ||
*/ | ||
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); | ||
} | ||
const playlistUi = function (options) { | ||
const player = this; | ||
if (!player.playlist) { | ||
throw new Error('videojs-playlist plugin is required by the videojs-playlist-ui plugin'); | ||
// 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); | ||
} | ||
if (dom.isEl(options)) { | ||
videojs.log.warn('videojs-playlist-ui: Passing an element directly to playlistUi() is deprecated, use the "el" option instead!'); | ||
options = { | ||
el: options | ||
}; | ||
/** | ||
* Dispose the plugin. | ||
*/ | ||
dispose() { | ||
super.dispose(); | ||
this.playlistMenu.dispose(); | ||
} | ||
options = merge(defaults, options); // If the player is already using this plugin, remove the pre-existing | ||
// PlaylistMenu, but retain the element and its location in the DOM because | ||
// it will be re-used. | ||
if (player.playlistMenu) { | ||
const el = player.playlistMenu.el(); // Catch cases where the menu may have been disposed elsewhere or the | ||
// element removed from the DOM. | ||
if (el) { | ||
const parentNode = el.parentNode; | ||
const nextSibling = el.nextSibling; // Disposing the menu will remove `el` from the DOM, but we need to | ||
// empty it ourselves to be sure. | ||
player.playlistMenu.dispose(); | ||
dom.emptyEl(el); // Put the element back in its place. | ||
if (nextSibling) { | ||
parentNode.insertBefore(el, nextSibling); | ||
} else { | ||
parentNode.appendChild(el); | ||
/** | ||
* 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; | ||
} | ||
options.el = el; | ||
} | ||
return false; | ||
} | ||
if (!dom.isEl(options.el)) { | ||
options.el = findRoot(options.className); | ||
/** | ||
* 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; | ||
player.playlistMenu = new PlaylistMenu(player, options); | ||
}; // register components | ||
videojs.registerComponent('PlaylistMenu', PlaylistMenu); | ||
videojs.registerComponent('PlaylistMenuItem', PlaylistMenuItem); // register the plugin | ||
videojs.registerPlugin('playlistUi', playlistUi); | ||
playlistUi.VERSION = version; | ||
export { playlistUi as default }; | ||
export { PlaylistUI as default }; |
@@ -1,2 +0,2 @@ | ||
/*! @name videojs-playlist-ui @version 4.2.1 @license Apache-2.0 */ | ||
/*! @name videojs-playlist-ui @version 5.0.0 @license Apache-2.0 */ | ||
(function (global, factory) { | ||
@@ -12,42 +12,5 @@ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js')) : | ||
var version = "4.2.1"; | ||
var version = "5.0.0"; | ||
const dom = videojs__default["default"].dom; | ||
const merge = videojs__default["default"].obj ? videojs__default["default"].obj.merge : videojs__default["default"].mergeOptions; | ||
const formatTime = videojs__default["default"].time ? videojs__default["default"].time.formatTime : videojs__default["default"].formatTime; // 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 | ||
}; // 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) { | ||
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$1 = videojs__default["default"].getComponent('Component'); | ||
const createThumbnail = function (thumbnail) { | ||
@@ -59,6 +22,4 @@ if (!thumbnail) { | ||
} | ||
const picture = document.createElement('picture'); | ||
picture.className = 'vjs-playlist-thumbnail'; | ||
if (typeof thumbnail === 'string') { | ||
@@ -73,2 +34,3 @@ // simple thumbnails | ||
// responsive thumbnails | ||
// additional variations of a <picture> are specified as | ||
@@ -78,12 +40,12 @@ // <source> elements | ||
const variant = thumbnail[i]; | ||
const source = document.createElement('source'); // transfer the properties of each variant onto a <source> | ||
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> | ||
} | ||
// the default version of a <picture> is specified by an <img> | ||
const variant = thumbnail[thumbnail.length - 1]; | ||
@@ -93,16 +55,10 @@ const img = document.createElement('img'); | ||
img.alt = ''; | ||
for (const prop in variant) { | ||
img[prop] = variant[prop]; | ||
} | ||
picture.appendChild(img); | ||
} | ||
return picture; | ||
}; | ||
const Component = videojs__default["default"].getComponent('Component'); | ||
class PlaylistMenuItem extends Component { | ||
class PlaylistMenuItem extends Component$1 { | ||
constructor(player, playlistItem, settings) { | ||
@@ -112,3 +68,2 @@ if (!playlistItem.item) { | ||
} | ||
playlistItem.showDescription = settings.showDescription; | ||
@@ -122,3 +77,2 @@ super(player, playlistItem); | ||
} | ||
handleKeyDown_(event) { | ||
@@ -131,6 +85,4 @@ // keycode 13 is <Enter> | ||
} | ||
switchPlaylistItem_(event) { | ||
this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item)); | ||
if (this.playOnSelect) { | ||
@@ -140,3 +92,2 @@ this.player_.play(); | ||
} | ||
createEl() { | ||
@@ -146,3 +97,2 @@ const li = document.createElement('li'); | ||
const showDescription = this.options_.showDescription; | ||
if (typeof item.data === 'object') { | ||
@@ -155,12 +105,13 @@ const dataKeys = Object.keys(item.data); | ||
} | ||
li.className = 'vjs-playlist-item'; | ||
li.setAttribute('tabIndex', 0); // Thumbnail image | ||
li.setAttribute('tabIndex', 0); | ||
// Thumbnail image | ||
this.thumbnail = createThumbnail(item.thumbnail); | ||
li.appendChild(this.thumbnail); // Duration | ||
li.appendChild(this.thumbnail); | ||
// Duration | ||
if (item.duration) { | ||
const duration = document.createElement('time'); | ||
const time = formatTime(item.duration); | ||
const time = videojs__default["default"].time.formatTime(item.duration); | ||
duration.className = 'vjs-playlist-duration'; | ||
@@ -170,5 +121,5 @@ duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S'); | ||
li.appendChild(duration); | ||
} // Now playing | ||
} | ||
// Now playing | ||
const nowPlayingEl = document.createElement('span'); | ||
@@ -179,8 +130,10 @@ const nowPlayingText = this.localize('Now Playing'); | ||
nowPlayingEl.setAttribute('title', nowPlayingText); | ||
this.thumbnail.appendChild(nowPlayingEl); // Title container contains title and "up next" | ||
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 | ||
this.thumbnail.appendChild(titleContainerEl); | ||
// Up next | ||
const upNextEl = document.createElement('span'); | ||
@@ -191,4 +144,5 @@ const upNextText = this.localize('Up Next'); | ||
upNextEl.setAttribute('title', upNextText); | ||
titleContainerEl.appendChild(upNextEl); // Video title | ||
titleContainerEl.appendChild(upNextEl); | ||
// Video title | ||
const titleEl = document.createElement('cite'); | ||
@@ -199,6 +153,8 @@ const titleText = item.name || this.localize('Untitled Video'); | ||
titleEl.setAttribute('title', titleText); | ||
titleContainerEl.appendChild(titleEl); // Populate thumbnails alt with the video title | ||
titleContainerEl.appendChild(titleEl); | ||
this.thumbnail.getElementsByTagName('img').alt = titleText; // We add thumbnail video description only if specified in playlist options | ||
// 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) { | ||
@@ -212,17 +168,30 @@ const descriptionEl = document.createElement('div'); | ||
} | ||
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) { | ||
if (!player.playlist) { | ||
throw new Error('videojs-playlist is required for the playlist component'); | ||
} | ||
super(player, options); | ||
this.items = []; | ||
if (options.horizontal) { | ||
@@ -232,22 +201,28 @@ this.addClass('vjs-playlist-horizontal'); | ||
this.addClass('vjs-playlist-vertical'); | ||
} // If CSS pointer events aren't supported, we have to prevent | ||
} | ||
// 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)); | ||
this.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], event => { | ||
this.update(); | ||
}); // Keep track of whether an ad is playing so that the menu | ||
// Keep track of whether an ad is playing so that the menu | ||
// appearance can be adapted appropriately | ||
this.on(player, 'adstart', () => { | ||
@@ -267,9 +242,7 @@ this.addClass('vjs-ad-playing'); | ||
} | ||
createEl() { | ||
return dom.createEl('div', { | ||
return videojs__default["default"].dom.createEl('div', { | ||
className: this.options_.className | ||
}); | ||
} | ||
empty_() { | ||
@@ -281,3 +254,2 @@ if (this.items && this.items.length) { | ||
} | ||
createPlaylist_() { | ||
@@ -287,3 +259,2 @@ const playlist = this.player_.playlist() || []; | ||
let overlay = this.el_.querySelector('.vjs-playlist-ad-overlay'); | ||
if (!list) { | ||
@@ -294,5 +265,5 @@ list = document.createElement('ol'); | ||
} | ||
this.empty_(); | ||
this.empty_(); // create new items | ||
// create new items | ||
for (let i = 0; i < playlist.length; i++) { | ||
@@ -304,6 +275,6 @@ const item = new PlaylistMenuItem(this.player_, { | ||
list.appendChild(item.el_); | ||
} // Inject the ad overlay. We use this element to block clicks during ad | ||
} | ||
// Inject the ad overlay. We use this element to block clicks during ad | ||
// playback and darken the menu to indicate inactivity | ||
if (!overlay) { | ||
@@ -316,13 +287,11 @@ overlay = document.createElement('li'); | ||
list.appendChild(overlay); | ||
} // select the current playlist item | ||
} | ||
// 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) { | ||
dom.addClass(thumbnail, 'vjs-playlist-now-playing'); | ||
videojs__default["default"].dom.addClass(thumbnail, 'vjs-playlist-now-playing'); | ||
} | ||
@@ -332,6 +301,58 @@ } | ||
/** | ||
* 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) { | ||
@@ -343,3 +364,2 @@ // if the menu is currently empty or the state is obviously out | ||
} | ||
for (let i = 0; i < this.items.length; i++) { | ||
@@ -352,17 +372,13 @@ if (this.items[i].item !== playlist[i]) { | ||
} | ||
} // the playlist itself is unchanged so just update the selection | ||
} | ||
// 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()) { | ||
dom.addClass(item.thumbnail, 'vjs-playlist-now-playing'); | ||
videojs__default["default"].dom.addClass(item.thumbnail, 'vjs-playlist-now-playing'); | ||
} | ||
notUpNext(item); | ||
@@ -378,50 +394,18 @@ } else if (i === currentItem + 1) { | ||
} | ||
} | ||
/** | ||
* 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. | ||
*/ | ||
videojs__default["default"].registerComponent('PlaylistMenu', PlaylistMenu); | ||
const hasChildEls = el => { | ||
for (let i = 0; i < el.childNodes.length; i++) { | ||
if (dom.isEl(el.childNodes[i])) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
// 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 | ||
}; | ||
/** | ||
* 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. | ||
*/ | ||
const Plugin = videojs__default["default"].getPlugin('plugin'); | ||
const findRoot = className => { | ||
const all = document.querySelectorAll('.' + className); | ||
let el; | ||
for (let i = 0; i < all.length; i++) { | ||
if (!hasChildEls(all[i])) { | ||
el = all[i]; | ||
break; | ||
} | ||
} | ||
return el; | ||
}; | ||
/** | ||
@@ -443,60 +427,71 @@ * Initialize the plugin on a player. | ||
*/ | ||
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); | ||
} | ||
const playlistUi = function (options) { | ||
const player = this; | ||
if (!player.playlist) { | ||
throw new Error('videojs-playlist plugin is required by the videojs-playlist-ui plugin'); | ||
// 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); | ||
} | ||
if (dom.isEl(options)) { | ||
videojs__default["default"].log.warn('videojs-playlist-ui: Passing an element directly to playlistUi() is deprecated, use the "el" option instead!'); | ||
options = { | ||
el: options | ||
}; | ||
/** | ||
* Dispose the plugin. | ||
*/ | ||
dispose() { | ||
super.dispose(); | ||
this.playlistMenu.dispose(); | ||
} | ||
options = merge(defaults, options); // If the player is already using this plugin, remove the pre-existing | ||
// PlaylistMenu, but retain the element and its location in the DOM because | ||
// it will be re-used. | ||
if (player.playlistMenu) { | ||
const el = player.playlistMenu.el(); // Catch cases where the menu may have been disposed elsewhere or the | ||
// element removed from the DOM. | ||
if (el) { | ||
const parentNode = el.parentNode; | ||
const nextSibling = el.nextSibling; // Disposing the menu will remove `el` from the DOM, but we need to | ||
// empty it ourselves to be sure. | ||
player.playlistMenu.dispose(); | ||
dom.emptyEl(el); // Put the element back in its place. | ||
if (nextSibling) { | ||
parentNode.insertBefore(el, nextSibling); | ||
} else { | ||
parentNode.appendChild(el); | ||
/** | ||
* 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; | ||
} | ||
options.el = el; | ||
} | ||
return false; | ||
} | ||
if (!dom.isEl(options.el)) { | ||
options.el = findRoot(options.className); | ||
/** | ||
* 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; | ||
player.playlistMenu = new PlaylistMenu(player, options); | ||
}; // register components | ||
return PlaylistUI; | ||
videojs__default["default"].registerComponent('PlaylistMenu', PlaylistMenu); | ||
videojs__default["default"].registerComponent('PlaylistMenuItem', PlaylistMenuItem); // register the plugin | ||
videojs__default["default"].registerPlugin('playlistUi', playlistUi); | ||
playlistUi.VERSION = version; | ||
return playlistUi; | ||
})); |
@@ -1,2 +0,2 @@ | ||
/*! @name videojs-playlist-ui @version 4.2.1 @license Apache-2.0 */ | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("video.js")):"function"==typeof define&&define.amd?define(["video.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).videojsPlaylistUi=e(t.videojs)}(this,(function(t){"use strict";function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var s=e(t);const i=s.default.dom,l=s.default.obj?s.default.obj.merge:s.default.mergeOptions,a=s.default.time?s.default.time.formatTime:s.default.formatTime,n={className:"vjs-playlist",playOnSelect:!1,supportsCssPointerEvents:(()=>{const t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents})()},o=function(t){t.addClass("vjs-selected")},d=function(t){t.removeClass("vjs-selected"),t.thumbnail&&i.removeClass(t.thumbnail,"vjs-playlist-now-playing")},r=function(t){t.removeClass("vjs-up-next")},c=s.default.getComponent("Component");class p extends c{constructor(t,e,s){if(!e.item)throw new Error("Cannot construct a PlaylistMenuItem without an item option");e.showDescription=s.showDescription,super(t,e),this.item=e.item,this.playOnSelect=s.playOnSelect,this.emitTapEvents(),this.on(["click","tap"],this.switchPlaylistItem_),this.on("keydown",this.handleKeyDown_)}handleKeyDown_(t){13!==t.which&&32!==t.which||this.switchPlaylistItem_()}switchPlaylistItem_(t){this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item)),this.playOnSelect&&this.player_.play()}createEl(){const t=document.createElement("li"),e=this.options_.item,s=this.options_.showDescription;if("object"==typeof e.data){Object.keys(e.data).forEach((s=>{const i=e.data[s];t.dataset[s]=i}))}if(t.className="vjs-playlist-item",t.setAttribute("tabIndex",0),this.thumbnail=function(t){if(!t){const t=document.createElement("div");return t.className="vjs-playlist-thumbnail vjs-playlist-thumbnail-placeholder",t}const e=document.createElement("picture");if(e.className="vjs-playlist-thumbnail","string"==typeof t){const s=document.createElement("img");s.loading="lazy",s.src=t,s.alt="",e.appendChild(s)}else{for(let s=0;s<t.length-1;s++){const i=t[s],l=document.createElement("source");for(const t in i)l[t]=i[t];e.appendChild(l)}const s=t[t.length-1],i=document.createElement("img");i.loading="lazy",i.alt="";for(const t in s)i[t]=s[t];e.appendChild(i)}return e}(e.thumbnail),t.appendChild(this.thumbnail),e.duration){const s=document.createElement("time"),i=a(e.duration);s.className="vjs-playlist-duration",s.setAttribute("datetime","PT0H0M"+e.duration+"S"),s.appendChild(document.createTextNode(i)),t.appendChild(s)}const i=document.createElement("span"),l=this.localize("Now Playing");i.className="vjs-playlist-now-playing-text",i.appendChild(document.createTextNode(l)),i.setAttribute("title",l),this.thumbnail.appendChild(i);const n=document.createElement("div");n.className="vjs-playlist-title-container",this.thumbnail.appendChild(n);const o=document.createElement("span"),d=this.localize("Up Next");o.className="vjs-up-next-text",o.appendChild(document.createTextNode(d)),o.setAttribute("title",d),n.appendChild(o);const r=document.createElement("cite"),c=e.name||this.localize("Untitled Video");if(r.className="vjs-playlist-name",r.appendChild(document.createTextNode(c)),r.setAttribute("title",c),n.appendChild(r),this.thumbnail.getElementsByTagName("img").alt=c,s){const t=document.createElement("div"),s=e.description||"";t.className="vjs-playlist-description",t.appendChild(document.createTextNode(s)),t.setAttribute("title",s),n.appendChild(t)}return t}}class h extends c{constructor(t,e){if(!t.playlist)throw new Error("videojs-playlist is required for the playlist component");super(t,e),this.items=[],e.horizontal?this.addClass("vjs-playlist-horizontal"):this.addClass("vjs-playlist-vertical"),e.supportsCssPointerEvents&&this.addClass("vjs-csspointerevents"),this.createPlaylist_(),s.default.browser.TOUCH_ENABLED||this.addClass("vjs-mouse"),this.on(t,["loadstart","playlistchange","playlistsorted"],(t=>{this.update()})),this.on(t,"adstart",(()=>{this.addClass("vjs-ad-playing")})),this.on(t,"adend",(()=>{this.removeClass("vjs-ad-playing")})),this.on("dispose",(()=>{this.empty_(),t.playlistMenu=null})),this.on(t,"dispose",(()=>{this.dispose()}))}createEl(){return i.createEl("div",{className:this.options_.className})}empty_(){this.items&&this.items.length&&(this.items.forEach((t=>t.dispose())),this.items.length=0)}createPlaylist_(){const t=this.player_.playlist()||[];let e=this.el_.querySelector(".vjs-playlist-item-list"),s=this.el_.querySelector(".vjs-playlist-ad-overlay");e||(e=document.createElement("ol"),e.className="vjs-playlist-item-list",this.el_.appendChild(e)),this.empty_();for(let s=0;s<t.length;s++){const i=new p(this.player_,{item:t[s]},this.options_);this.items.push(i),e.appendChild(i.el_)}s||(s=document.createElement("li"),s.className="vjs-playlist-ad-overlay"),e.appendChild(s);const l=this.player_.playlist.currentItem();if(this.items.length&&l>=0){o(this.items[l]);const t=this.items[l].$(".vjs-playlist-thumbnail");t&&i.addClass(t,"vjs-playlist-now-playing")}}update(){const t=this.player_.playlist();if(this.items.length!==t.length)return void this.createPlaylist_();for(let e=0;e<this.items.length;e++)if(this.items[e].item!==t[e])return void this.createPlaylist_();const e=this.player_.playlist.currentItem();for(let t=0;t<this.items.length;t++){const s=this.items[t];t===e?(o(s),document.activeElement!==s.el()&&i.addClass(s.thumbnail,"vjs-playlist-now-playing"),r(s)):t===e+1?(d(s),s.addClass("vjs-up-next")):(d(s),r(s))}}}const m=t=>{for(let e=0;e<t.childNodes.length;e++)if(i.isEl(t.childNodes[e]))return!0;return!1},u=function(t){const e=this;if(!e.playlist)throw new Error("videojs-playlist plugin is required by the videojs-playlist-ui plugin");if(i.isEl(t)&&(s.default.log.warn('videojs-playlist-ui: Passing an element directly to playlistUi() is deprecated, use the "el" option instead!'),t={el:t}),t=l(n,t),e.playlistMenu){const s=e.playlistMenu.el();if(s){const l=s.parentNode,a=s.nextSibling;e.playlistMenu.dispose(),i.emptyEl(s),a?l.insertBefore(s,a):l.appendChild(s),t.el=s}}i.isEl(t.el)||(t.el=(t=>{const e=document.querySelectorAll("."+t);let s;for(let t=0;t<e.length;t++)if(!m(e[t])){s=e[t];break}return s})(t.className)),e.playlistMenu=new h(e,t)};return s.default.registerComponent("PlaylistMenu",h),s.default.registerComponent("PlaylistMenuItem",p),s.default.registerPlugin("playlistUi",u),u.VERSION="4.2.1",u})); | ||
/*! @name videojs-playlist-ui @version 5.0.0 @license Apache-2.0 */ | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("video.js")):"function"==typeof define&&define.amd?define(["video.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).videojsPlaylistUi=e(t.videojs)}(this,(function(t){"use strict";function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var s=e(t);const i=s.default.getComponent("Component");class l extends i{constructor(t,e,s){if(!e.item)throw new Error("Cannot construct a PlaylistMenuItem without an item option");e.showDescription=s.showDescription,super(t,e),this.item=e.item,this.playOnSelect=s.playOnSelect,this.emitTapEvents(),this.on(["click","tap"],this.switchPlaylistItem_),this.on("keydown",this.handleKeyDown_)}handleKeyDown_(t){13!==t.which&&32!==t.which||this.switchPlaylistItem_()}switchPlaylistItem_(t){this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item)),this.playOnSelect&&this.player_.play()}createEl(){const t=document.createElement("li"),e=this.options_.item,i=this.options_.showDescription;if("object"==typeof e.data){Object.keys(e.data).forEach((s=>{const i=e.data[s];t.dataset[s]=i}))}if(t.className="vjs-playlist-item",t.setAttribute("tabIndex",0),this.thumbnail=function(t){if(!t){const t=document.createElement("div");return t.className="vjs-playlist-thumbnail vjs-playlist-thumbnail-placeholder",t}const e=document.createElement("picture");if(e.className="vjs-playlist-thumbnail","string"==typeof t){const s=document.createElement("img");s.loading="lazy",s.src=t,s.alt="",e.appendChild(s)}else{for(let s=0;s<t.length-1;s++){const i=t[s],l=document.createElement("source");for(const t in i)l[t]=i[t];e.appendChild(l)}const s=t[t.length-1],i=document.createElement("img");i.loading="lazy",i.alt="";for(const t in s)i[t]=s[t];e.appendChild(i)}return e}(e.thumbnail),t.appendChild(this.thumbnail),e.duration){const i=document.createElement("time"),l=s.default.time.formatTime(e.duration);i.className="vjs-playlist-duration",i.setAttribute("datetime","PT0H0M"+e.duration+"S"),i.appendChild(document.createTextNode(l)),t.appendChild(i)}const l=document.createElement("span"),n=this.localize("Now Playing");l.className="vjs-playlist-now-playing-text",l.appendChild(document.createTextNode(n)),l.setAttribute("title",n),this.thumbnail.appendChild(l);const a=document.createElement("div");a.className="vjs-playlist-title-container",this.thumbnail.appendChild(a);const o=document.createElement("span"),d=this.localize("Up Next");o.className="vjs-up-next-text",o.appendChild(document.createTextNode(d)),o.setAttribute("title",d),a.appendChild(o);const r=document.createElement("cite"),c=e.name||this.localize("Untitled Video");if(r.className="vjs-playlist-name",r.appendChild(document.createTextNode(c)),r.setAttribute("title",c),a.appendChild(r),this.thumbnail.getElementsByTagName("img").alt=c,i){const t=document.createElement("div"),s=e.description||"";t.className="vjs-playlist-description",t.appendChild(document.createTextNode(s)),t.setAttribute("title",s),a.appendChild(t)}return t}}s.default.registerComponent("PlaylistMenuItem",l);const n=function(t){t.addClass("vjs-selected")},a=function(t){t.removeClass("vjs-selected"),t.thumbnail&&s.default.dom.removeClass(t.thumbnail,"vjs-playlist-now-playing")},o=function(t){t.removeClass("vjs-up-next")},d=s.default.getComponent("Component");class r extends d{constructor(t,e){super(t,e),this.items=[],e.horizontal?this.addClass("vjs-playlist-horizontal"):this.addClass("vjs-playlist-vertical"),e.supportsCssPointerEvents&&this.addClass("vjs-csspointerevents"),this.createPlaylist_(),s.default.browser.TOUCH_ENABLED||this.addClass("vjs-mouse"),this.on(t,["loadstart","playlistchange","playlistsorted"],(t=>{"playlistchange"===t.type&&["add","remove"].includes(t.action)||this.update()})),this.on(t,["playlistadd"],(t=>this.addItems_(t.index,t.count))),this.on(t,["playlistremove"],(t=>this.removeItems_(t.index,t.count))),this.on(t,"adstart",(()=>{this.addClass("vjs-ad-playing")})),this.on(t,"adend",(()=>{this.removeClass("vjs-ad-playing")})),this.on("dispose",(()=>{this.empty_(),t.playlistMenu=null})),this.on(t,"dispose",(()=>{this.dispose()}))}createEl(){return s.default.dom.createEl("div",{className:this.options_.className})}empty_(){this.items&&this.items.length&&(this.items.forEach((t=>t.dispose())),this.items.length=0)}createPlaylist_(){const t=this.player_.playlist()||[];let e=this.el_.querySelector(".vjs-playlist-item-list"),i=this.el_.querySelector(".vjs-playlist-ad-overlay");e||(e=document.createElement("ol"),e.className="vjs-playlist-item-list",this.el_.appendChild(e)),this.empty_();for(let s=0;s<t.length;s++){const i=new l(this.player_,{item:t[s]},this.options_);this.items.push(i),e.appendChild(i.el_)}i||(i=document.createElement("li"),i.className="vjs-playlist-ad-overlay"),e.appendChild(i);const a=this.player_.playlist.currentItem();if(this.items.length&&a>=0){n(this.items[a]);const t=this.items[a].$(".vjs-playlist-thumbnail");t&&s.default.dom.addClass(t,"vjs-playlist-now-playing")}}addItems_(t,e){const s=this.player_.playlist().slice(t,e+t);if(!s.length)return;const i=this.el_.querySelector(".vjs-playlist-item-list"),n=this.el_.querySelectorAll(".vjs-playlist-item")[t]||null,a=s.map((t=>{const e=new l(this.player_,{item:t},this.options_);return i.insertBefore(e.el_,n),e}));this.items.splice(t,0,...a)}removeItems_(t,e){const s=this.items.slice(t,e+t);s.length&&(s.forEach((t=>t.dispose())),this.items.splice(t,e))}update(){const t=this.player_.playlist();if(this.items.length!==t.length)return void this.createPlaylist_();for(let e=0;e<this.items.length;e++)if(this.items[e].item!==t[e])return void this.createPlaylist_();const e=this.player_.playlist.currentItem();for(let t=0;t<this.items.length;t++){const i=this.items[t];t===e?(n(i),document.activeElement!==i.el()&&s.default.dom.addClass(i.thumbnail,"vjs-playlist-now-playing"),o(i)):t===e+1?(a(i),i.addClass("vjs-up-next")):(a(i),o(i))}}}s.default.registerComponent("PlaylistMenu",r);const c={className:"vjs-playlist",playOnSelect:!1,supportsCssPointerEvents:(()=>{const t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents})()},p=s.default.getPlugin("plugin");class h extends p{constructor(t,e){super(t,e),t.usingPlugin("playlist")?(e=this.options_=s.default.obj.merge(c,e),s.default.dom.isEl(e.el)||(e.el=this.findRoot_(e.className)),this.playlistMenu=t.playlistMenu=new r(t,e)):t.log.error("videojs-playlist plugin is required by the videojs-playlist-ui plugin")}dispose(){super.dispose(),this.playlistMenu.dispose()}hasChildEls_(t){for(let e=0;e<t.childNodes.length;e++)if(s.default.dom.isEl(t.childNodes[e]))return!0;return!1}findRoot_(t){const e=document.querySelectorAll("."+t);for(let t=0;t<e.length;t++)if(!this.hasChildEls_(e[t]))return e[t]}}return s.default.registerPlugin("playlistUi",h),h.VERSION="5.0.0",h})); |
{ | ||
"name": "videojs-playlist-ui", | ||
"version": "4.2.1", | ||
"version": "5.0.0", | ||
"author": "Brightcove, Inc.", | ||
@@ -15,6 +15,3 @@ "description": "A user interface for the videojs-playlist API", | ||
"build": "npm-run-all -p build:*", | ||
"build:css": "npm-run-all build:css:sass build:css:copy-vertical build:css:copy-no-prefix", | ||
"build:css:copy-no-prefix": "shx cp dist/videojs-playlist-ui.css dist/videojs-playlist-ui.vertical.no-prefix.css", | ||
"build:css:copy-vertical": "shx cp dist/videojs-playlist-ui.css dist/videojs-playlist-ui.vertical.css", | ||
"build:css:sass": "node-sass src/plugin.scss dist/videojs-playlist-ui.css --output-style=compressed --linefeed=lf", | ||
"build:css": "sass src/plugin.scss dist/videojs-playlist-ui.css --style compressed", | ||
"build:js": "rollup -c scripts/rollup.config.js", | ||
@@ -31,6 +28,5 @@ "build:lang": "vjslang --dir dist/lang", | ||
"update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s", | ||
"preversion": "npm test", | ||
"version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md", | ||
"watch": "npm-run-all -p watch:*", | ||
"watch:css": "npm run build:css:sass -- -w", | ||
"watch:css": "npm run build:css -- -w", | ||
"watch:js": "npm run build:js -- -w", | ||
@@ -45,29 +41,31 @@ "posttest": "shx cat test/dist/coverage/text.txt", | ||
"dependencies": { | ||
"global": "^4.4.0", | ||
"video.js": "^6 || ^7 || ^8" | ||
"global": "^4.4.0" | ||
}, | ||
"devDependencies": { | ||
"conventional-changelog-cli": "^2.0.31", | ||
"conventional-changelog-cli": "^2.2.2", | ||
"conventional-changelog-videojs": "^3.0.2", | ||
"doctoc": "^1.3.1", | ||
"doctoc": "^2.2.1", | ||
"husky": "^1.3.1", | ||
"karma": "^4.4.1", | ||
"lint-staged": "^8.2.1", | ||
"node-sass": "^4.13.1", | ||
"karma": "^6.4.2", | ||
"lint-staged": "^13.2.2", | ||
"not-prerelease": "^1.0.1", | ||
"npm-merge-driver-install": "^1.0.0", | ||
"npm-merge-driver-install": "^3.0.0", | ||
"npm-run-all": "^4.1.5", | ||
"pkg-ok": "^2.2.0", | ||
"postcss-cli": "^6.1.3", | ||
"rollup": "^2.61.1", | ||
"sass": "^1.62.1", | ||
"shx": "^0.3.2", | ||
"sinon": "^6.1.5", | ||
"videojs-generate-karma-config": "^5.3.1", | ||
"videojs-generate-postcss-config": "~2.0.1", | ||
"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": "^2.0.0", | ||
"videojs-playlist": "^4.3.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", | ||
@@ -101,3 +99,3 @@ "module": "dist/videojs-playlist-ui.es.js", | ||
"README.md": [ | ||
"npm run docs:toc", | ||
"npm run docs", | ||
"git add" | ||
@@ -104,0 +102,0 @@ ] |
# videojs-playlist-ui | ||
[![Build Status](https://travis-ci.org/brightcove/videojs-playlist-ui.svg?branch=master)](https://travis-ci.org/brightcove/videojs-playlist-ui) | ||
[![Greenkeeper badge](https://badges.greenkeeper.io/brightcove/videojs-playlist-ui.svg)](https://greenkeeper.io/) | ||
[![Slack Status](http://slack.videojs.com/badge.svg)](http://slack.videojs.com) | ||
[![NPM](https://nodei.co/npm/videojs-playlist-ui.png?downloads=true&downloadRank=true)](https://nodei.co/npm/videojs-playlist-ui/) | ||
@@ -16,3 +12,2 @@ | ||
- [Getting Started](#getting-started) | ||
@@ -35,9 +30,12 @@ - [Root Element](#root-element) | ||
```html | ||
<!-- Include the playlist menu styles somewhere in your page: --> | ||
<link href="videojs-playlist-ui.vertical.css" rel="stylesheet"> | ||
<!-- Include the playlist menu styles somewhere in your page --> | ||
<link href="videojs-playlist-ui.css" rel="stylesheet"> | ||
<!-- The playlist menu will be built automatically in here: --> | ||
<!-- 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: --> | ||
<!-- Include video.js, the videojs-playlist plugin and this plugin --> | ||
<script src="video.js"></script> | ||
@@ -48,4 +46,7 @@ <script src="videojs-playlist.js"></script> | ||
<script> | ||
// Initialize the plugin and build the playlist! | ||
videojs(document.querySelector('video')).playlistUi(); | ||
// Initialize the player | ||
const player = videojs(document.querySelector('video-js')); | ||
// Initialize the plugin and render the playlist | ||
player.playlistUi(); | ||
</script> | ||
@@ -84,4 +85,2 @@ ``` | ||
> **NOTE:** Previously, the plugin supported passing the element in lieu of passing options. That feature is deprecated and will be removed in 4.0. Please use the `el` option going forward. | ||
## Other Options | ||
@@ -112,4 +111,4 @@ | ||
[components]: https://github.com/videojs/video.js/blob/master/docs/guides/components.md | ||
[components-options]: https://github.com/videojs/video.js/blob/master/docs/guides/options.md#component-options | ||
[components]: https://videojs.com/guides/components/ | ||
[components-options]: https://videojs.com/guides/components/#using-options | ||
[contrib-ads]: https://github.com/videojs/videojs-contrib-ads |
import document from 'global/document'; | ||
import videojs from 'video.js'; | ||
import {version as VERSION} from '../package.json'; | ||
import PlaylistMenu from './playlist-menu'; | ||
const dom = videojs.dom; | ||
const merge = videojs.obj ? videojs.obj.merge : videojs.mergeOptions; | ||
const formatTime = videojs.time ? videojs.time.formatTime : videojs.formatTime; | ||
// see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/pointerevents.js | ||
@@ -23,393 +20,5 @@ const supportsCssPointerEvents = (() => { | ||
// 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'); | ||
const Plugin = videojs.getPlugin('plugin'); | ||
if (el.thumbnail) { | ||
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 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; | ||
}; | ||
const Component = videojs.getComponent('Component'); | ||
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 = 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; | ||
} | ||
} | ||
class PlaylistMenu extends Component { | ||
constructor(player, options) { | ||
if (!player.playlist) { | ||
throw new Error('videojs-playlist is required for the playlist component'); | ||
} | ||
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'], (event) => { | ||
this.update(); | ||
}); | ||
// 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 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) { | ||
dom.addClass(thumbnail, 'vjs-playlist-now-playing'); | ||
} | ||
} | ||
} | ||
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()) { | ||
dom.addClass(item.thumbnail, 'vjs-playlist-now-playing'); | ||
} | ||
notUpNext(item); | ||
} else if (i === currentItem + 1) { | ||
removeSelectedClass(item); | ||
upNext(item); | ||
} else { | ||
removeSelectedClass(item); | ||
notUpNext(item); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* 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. | ||
*/ | ||
const hasChildEls = (el) => { | ||
for (let i = 0; i < el.childNodes.length; i++) { | ||
if (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. | ||
*/ | ||
const findRoot = (className) => { | ||
const all = document.querySelectorAll('.' + className); | ||
let el; | ||
for (let i = 0; i < all.length; i++) { | ||
if (!hasChildEls(all[i])) { | ||
el = all[i]; | ||
break; | ||
} | ||
} | ||
return el; | ||
}; | ||
/** | ||
* Initialize the plugin on a player. | ||
@@ -430,60 +39,76 @@ * | ||
*/ | ||
const playlistUi = function(options) { | ||
const player = this; | ||
class PlaylistUI extends Plugin { | ||
if (!player.playlist) { | ||
throw new Error('videojs-playlist plugin is required by the videojs-playlist-ui plugin'); | ||
} | ||
constructor(player, options) { | ||
super(player, options); | ||
if (dom.isEl(options)) { | ||
videojs.log.warn('videojs-playlist-ui: Passing an element directly to playlistUi() is deprecated, use the "el" option instead!'); | ||
options = {el: options}; | ||
} | ||
if (!player.usingPlugin('playlist')) { | ||
player.log.error('videojs-playlist plugin is required by the videojs-playlist-ui plugin'); | ||
return; | ||
} | ||
options = merge(defaults, options); | ||
options = this.options_ = videojs.obj.merge(defaults, options); | ||
// If the player is already using this plugin, remove the pre-existing | ||
// PlaylistMenu, but retain the element and its location in the DOM because | ||
// it will be re-used. | ||
if (player.playlistMenu) { | ||
const el = player.playlistMenu.el(); | ||
if (!videojs.dom.isEl(options.el)) { | ||
options.el = this.findRoot_(options.className); | ||
} | ||
// Catch cases where the menu may have been disposed elsewhere or the | ||
// element removed from the DOM. | ||
if (el) { | ||
const parentNode = el.parentNode; | ||
const nextSibling = el.nextSibling; | ||
// 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); | ||
} | ||
// Disposing the menu will remove `el` from the DOM, but we need to | ||
// empty it ourselves to be sure. | ||
player.playlistMenu.dispose(); | ||
dom.emptyEl(el); | ||
/** | ||
* Dispose the plugin. | ||
*/ | ||
dispose() { | ||
super.dispose(); | ||
this.playlistMenu.dispose(); | ||
} | ||
// Put the element back in its place. | ||
if (nextSibling) { | ||
parentNode.insertBefore(el, nextSibling); | ||
} else { | ||
parentNode.appendChild(el); | ||
/** | ||
* 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; | ||
} | ||
options.el = el; | ||
} | ||
return false; | ||
} | ||
if (!dom.isEl(options.el)) { | ||
options.el = findRoot(options.className); | ||
/** | ||
* 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]; | ||
} | ||
} | ||
} | ||
} | ||
player.playlistMenu = new PlaylistMenu(player, options); | ||
}; | ||
videojs.registerPlugin('playlistUi', PlaylistUI); | ||
// register components | ||
videojs.registerComponent('PlaylistMenu', PlaylistMenu); | ||
videojs.registerComponent('PlaylistMenuItem', PlaylistMenuItem); | ||
PlaylistUI.VERSION = VERSION; | ||
// register the plugin | ||
videojs.registerPlugin('playlistUi', playlistUi); | ||
playlistUi.VERSION = VERSION; | ||
export default playlistUi; | ||
export default PlaylistUI; |
@@ -5,2 +5,3 @@ /* eslint-disable no-console */ | ||
import QUnit from 'qunit'; | ||
import sinon from 'sinon'; | ||
import videojs from 'video.js'; | ||
@@ -38,3 +39,2 @@ | ||
const dom = videojs.dom || videojs; | ||
const Html5 = videojs.getTech('Html5'); | ||
@@ -47,6 +47,4 @@ | ||
function setup() { | ||
const merge = videojs.obj ? videojs.obj.merge : videojs.mergeOptions; | ||
this.oldVideojsBrowser = videojs.browser; | ||
videojs.browser = merge({}, videojs.browser); | ||
videojs.browser = videojs.obj.merge({}, videojs.browser); | ||
@@ -71,4 +69,4 @@ this.fixture = document.querySelector('#qunit-fixture'); | ||
// Create two playlist container elements. | ||
this.fixture.appendChild(dom.createEl('div', {className: 'vjs-playlist'})); | ||
this.fixture.appendChild(dom.createEl('div', {className: 'vjs-playlist'})); | ||
this.fixture.appendChild(videojs.dom.createEl('div', {className: 'vjs-playlist'})); | ||
this.fixture.appendChild(videojs.dom.createEl('div', {className: 'vjs-playlist'})); | ||
} | ||
@@ -81,3 +79,3 @@ | ||
this.player = null; | ||
dom.emptyEl(this.fixture); | ||
videojs.dom.emptyEl(this.fixture); | ||
} | ||
@@ -92,6 +90,8 @@ | ||
QUnit.test('errors if used without the playlist plugin', function(assert) { | ||
assert.throws(function() { | ||
this.player.playlist = null; | ||
this.player.playlistUi(); | ||
}, 'threw on init'); | ||
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'); | ||
}); | ||
@@ -108,17 +108,4 @@ | ||
QUnit.test('can be initialized with an element (deprecated form)', function(assert) { | ||
const elem = dom.createEl('div'); | ||
this.player.playlist(playlist); | ||
this.player.playlistUi(elem); | ||
assert.strictEqual( | ||
elem.querySelectorAll('li.vjs-playlist-item').length, | ||
playlist.length, | ||
'created an element for each playlist item' | ||
); | ||
}); | ||
QUnit.test('can be initialized with an element', function(assert) { | ||
const elem = dom.createEl('div'); | ||
const elem = videojs.dom.createEl('div'); | ||
@@ -141,3 +128,3 @@ this.player.playlist(playlist); | ||
// to the next one. | ||
firstEl.appendChild(dom.createEl('div')); | ||
firstEl.appendChild(videojs.dom.createEl('div')); | ||
@@ -156,8 +143,8 @@ this.player.playlist(playlist); | ||
QUnit.test('can look for an element with a custom class that is not already in use', function(assert) { | ||
const firstEl = dom.createEl('div', {className: 'super-playlist'}); | ||
const secondEl = dom.createEl('div', {className: 'super-playlist'}); | ||
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(dom.createEl('div')); | ||
firstEl.appendChild(videojs.dom.createEl('div')); | ||
@@ -656,1 +643,237 @@ this.fixture.appendChild(firstEl); | ||
}); | ||
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'); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
411323
21
9370
3
110
+ Addedvideojs-playlist@5.1.2(transitive)
- Removedvideo.js@^6 || ^7 || ^8