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