videojs-playlist-ui.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. /*! @name videojs-playlist-ui @version 3.5.2 @license Apache-2.0 */
  2. (function (global, factory) {
  3. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('global/document'), require('video.js')) :
  4. typeof define === 'function' && define.amd ? define(['global/document', 'video.js'], factory) :
  5. (global.videojsPlaylistUi = factory(global.document,global.videojs));
  6. }(this, (function (document,videojs) { 'use strict';
  7. document = document && document.hasOwnProperty('default') ? document['default'] : document;
  8. videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs;
  9. function _inheritsLoose(subClass, superClass) {
  10. subClass.prototype = Object.create(superClass.prototype);
  11. subClass.prototype.constructor = subClass;
  12. subClass.__proto__ = superClass;
  13. }
  14. var version = "3.5.2";
  15. var dom = videojs.dom || videojs;
  16. var registerPlugin = videojs.registerPlugin || videojs.plugin; // Array#indexOf analog for IE8
  17. var indexOf = function indexOf(array, target) {
  18. for (var i = 0, length = array.length; i < length; i++) {
  19. if (array[i] === target) {
  20. return i;
  21. }
  22. }
  23. return -1;
  24. }; // see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/pointerevents.js
  25. var supportsCssPointerEvents = function () {
  26. var element = document.createElement('x');
  27. element.style.cssText = 'pointer-events:auto';
  28. return element.style.pointerEvents === 'auto';
  29. }();
  30. var defaults = {
  31. className: 'vjs-playlist',
  32. playOnSelect: false,
  33. supportsCssPointerEvents: supportsCssPointerEvents
  34. }; // we don't add `vjs-playlist-now-playing` in addSelectedClass
  35. // so it won't conflict with `vjs-icon-play
  36. // since it'll get added when we mouse out
  37. var addSelectedClass = function addSelectedClass(el) {
  38. el.addClass('vjs-selected');
  39. };
  40. var removeSelectedClass = function removeSelectedClass(el) {
  41. el.removeClass('vjs-selected');
  42. if (el.thumbnail) {
  43. dom.removeClass(el.thumbnail, 'vjs-playlist-now-playing');
  44. }
  45. };
  46. var upNext = function upNext(el) {
  47. el.addClass('vjs-up-next');
  48. };
  49. var notUpNext = function notUpNext(el) {
  50. el.removeClass('vjs-up-next');
  51. };
  52. var createThumbnail = function createThumbnail(thumbnail) {
  53. if (!thumbnail) {
  54. var placeholder = document.createElement('div');
  55. placeholder.className = 'vjs-playlist-thumbnail vjs-playlist-thumbnail-placeholder';
  56. return placeholder;
  57. }
  58. var picture = document.createElement('picture');
  59. picture.className = 'vjs-playlist-thumbnail';
  60. if (typeof thumbnail === 'string') {
  61. // simple thumbnails
  62. var img = document.createElement('img');
  63. img.src = thumbnail;
  64. img.alt = '';
  65. picture.appendChild(img);
  66. } else {
  67. // responsive thumbnails
  68. // additional variations of a <picture> are specified as
  69. // <source> elements
  70. for (var i = 0; i < thumbnail.length - 1; i++) {
  71. var _variant = thumbnail[i];
  72. var source = document.createElement('source'); // transfer the properties of each variant onto a <source>
  73. for (var prop in _variant) {
  74. source[prop] = _variant[prop];
  75. }
  76. picture.appendChild(source);
  77. } // the default version of a <picture> is specified by an <img>
  78. var variant = thumbnail[thumbnail.length - 1];
  79. var _img = document.createElement('img');
  80. _img.alt = '';
  81. for (var _prop in variant) {
  82. _img[_prop] = variant[_prop];
  83. }
  84. picture.appendChild(_img);
  85. }
  86. return picture;
  87. };
  88. var Component = videojs.getComponent('Component');
  89. var PlaylistMenuItem =
  90. /*#__PURE__*/
  91. function (_Component) {
  92. _inheritsLoose(PlaylistMenuItem, _Component);
  93. function PlaylistMenuItem(player, playlistItem, settings) {
  94. var _this;
  95. if (!playlistItem.item) {
  96. throw new Error('Cannot construct a PlaylistMenuItem without an item option');
  97. }
  98. _this = _Component.call(this, player, playlistItem) || this;
  99. _this.item = playlistItem.item;
  100. _this.playOnSelect = settings.playOnSelect;
  101. _this.emitTapEvents();
  102. _this.on(['click', 'tap'], _this.switchPlaylistItem_);
  103. _this.on('keydown', _this.handleKeyDown_);
  104. return _this;
  105. }
  106. var _proto = PlaylistMenuItem.prototype;
  107. _proto.handleKeyDown_ = function handleKeyDown_(event) {
  108. // keycode 13 is <Enter>
  109. // keycode 32 is <Space>
  110. if (event.which === 13 || event.which === 32) {
  111. this.switchPlaylistItem_();
  112. }
  113. };
  114. _proto.switchPlaylistItem_ = function switchPlaylistItem_(event) {
  115. this.player_.playlist.currentItem(indexOf(this.player_.playlist(), this.item));
  116. if (this.playOnSelect) {
  117. this.player_.play();
  118. }
  119. };
  120. _proto.createEl = function createEl() {
  121. var li = document.createElement('li');
  122. var item = this.options_.item;
  123. if (typeof item.data === 'object') {
  124. var dataKeys = Object.keys(item.data);
  125. dataKeys.forEach(function (key) {
  126. var value = item.data[key];
  127. li.dataset[key] = value;
  128. });
  129. }
  130. li.className = 'vjs-playlist-item';
  131. li.setAttribute('tabIndex', 0); // Thumbnail image
  132. this.thumbnail = createThumbnail(item.thumbnail);
  133. li.appendChild(this.thumbnail); // Duration
  134. if (item.duration) {
  135. var duration = document.createElement('time');
  136. var time = videojs.formatTime(item.duration);
  137. duration.className = 'vjs-playlist-duration';
  138. duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S');
  139. duration.appendChild(document.createTextNode(time));
  140. li.appendChild(duration);
  141. } // Now playing
  142. var nowPlayingEl = document.createElement('span');
  143. var nowPlayingText = this.localize('');
  144. nowPlayingEl.className = 'vjs-playlist-now-playing-text';
  145. nowPlayingEl.appendChild(document.createTextNode(nowPlayingText));
  146. nowPlayingEl.setAttribute('title', nowPlayingText);
  147. this.thumbnail.appendChild(nowPlayingEl); // Title container contains title and "up next"
  148. var titleContainerEl = document.createElement('div');
  149. titleContainerEl.className = 'vjs-playlist-title-container';
  150. this.thumbnail.appendChild(titleContainerEl); // Up next
  151. var upNextEl = document.createElement('span');
  152. var upNextText = this.localize('Up Next');
  153. upNextEl.className = 'vjs-up-next-text';
  154. upNextEl.appendChild(document.createTextNode(upNextText));
  155. upNextEl.setAttribute('title', upNextText);
  156. titleContainerEl.appendChild(upNextEl); // Video title
  157. var titleEl = document.createElement('cite');
  158. var titleText = item.name || this.localize('Untitled Video');
  159. titleEl.className = 'vjs-playlist-name';
  160. titleEl.appendChild(document.createTextNode(titleText));
  161. titleEl.setAttribute('title', titleText);
  162. titleContainerEl.appendChild(titleEl);
  163. return li;
  164. };
  165. return PlaylistMenuItem;
  166. }(Component);
  167. var PlaylistMenu =
  168. /*#__PURE__*/
  169. function (_Component2) {
  170. _inheritsLoose(PlaylistMenu, _Component2);
  171. function PlaylistMenu(player, options) {
  172. var _this2;
  173. if (!player.playlist) {
  174. throw new Error('videojs-playlist is required for the playlist component');
  175. }
  176. _this2 = _Component2.call(this, player, options) || this;
  177. _this2.items = [];
  178. if (options.horizontal) {
  179. _this2.addClass('vjs-playlist-horizontal');
  180. } else {
  181. _this2.addClass('vjs-playlist-vertical');
  182. } // If CSS pointer events aren't supported, we have to prevent
  183. // clicking on playlist items during ads with slightly more
  184. // invasive techniques. Details in the stylesheet.
  185. if (options.supportsCssPointerEvents) {
  186. _this2.addClass('vjs-csspointerevents');
  187. }
  188. _this2.createPlaylist_();
  189. if (!videojs.browser.TOUCH_ENABLED) {
  190. _this2.addClass('vjs-mouse');
  191. }
  192. _this2.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], function (event) {
  193. _this2.update();
  194. }); // Keep track of whether an ad is playing so that the menu
  195. // appearance can be adapted appropriately
  196. _this2.on(player, 'adstart', function () {
  197. _this2.addClass('vjs-ad-playing');
  198. });
  199. _this2.on(player, 'adend', function () {
  200. _this2.removeClass('vjs-ad-playing');
  201. });
  202. _this2.on('dispose', function () {
  203. _this2.empty_();
  204. player.playlistMenu = null;
  205. });
  206. _this2.on(player, 'dispose', function () {
  207. _this2.dispose();
  208. });
  209. return _this2;
  210. }
  211. var _proto2 = PlaylistMenu.prototype;
  212. _proto2.createEl = function createEl() {
  213. return dom.createEl('div', {
  214. className: this.options_.className
  215. });
  216. };
  217. _proto2.empty_ = function empty_() {
  218. if (this.items && this.items.length) {
  219. this.items.forEach(function (i) {
  220. return i.dispose();
  221. });
  222. this.items.length = 0;
  223. }
  224. };
  225. _proto2.createPlaylist_ = function createPlaylist_() {
  226. var playlist = this.player_.playlist() || [];
  227. var list = this.el_.querySelector('.vjs-playlist-item-list');
  228. var overlay = this.el_.querySelector('.vjs-playlist-ad-overlay');
  229. if (!list) {
  230. list = document.createElement('ol');
  231. list.className = 'vjs-playlist-item-list';
  232. this.el_.appendChild(list);
  233. }
  234. this.empty_(); // create new items
  235. for (var i = 0; i < playlist.length; i++) {
  236. var item = new PlaylistMenuItem(this.player_, {
  237. item: playlist[i]
  238. }, this.options_);
  239. this.items.push(item);
  240. list.appendChild(item.el_);
  241. } // Inject the ad overlay. IE<11 doesn't support "pointer-events:
  242. // none" so we use this element to block clicks during ad
  243. // playback.
  244. if (!overlay) {
  245. overlay = document.createElement('li');
  246. overlay.className = 'vjs-playlist-ad-overlay';
  247. list.appendChild(overlay);
  248. } else {
  249. // Move overlay to end of list
  250. list.appendChild(overlay);
  251. } // select the current playlist item
  252. var selectedIndex = this.player_.playlist.currentItem();
  253. if (this.items.length && selectedIndex >= 0) {
  254. addSelectedClass(this.items[selectedIndex]);
  255. var thumbnail = this.items[selectedIndex].$('.vjs-playlist-thumbnail');
  256. if (thumbnail) {
  257. dom.addClass(thumbnail, 'vjs-playlist-now-playing');
  258. }
  259. }
  260. };
  261. _proto2.update = function update() {
  262. // replace the playlist items being displayed, if necessary
  263. var playlist = this.player_.playlist();
  264. if (this.items.length !== playlist.length) {
  265. // if the menu is currently empty or the state is obviously out
  266. // of date, rebuild everything.
  267. this.createPlaylist_();
  268. return;
  269. }
  270. for (var i = 0; i < this.items.length; i++) {
  271. if (this.items[i].item !== playlist[i]) {
  272. // if any of the playlist items have changed, rebuild the
  273. // entire playlist
  274. this.createPlaylist_();
  275. return;
  276. }
  277. } // the playlist itself is unchanged so just update the selection
  278. var currentItem = this.player_.playlist.currentItem();
  279. for (var _i = 0; _i < this.items.length; _i++) {
  280. var item = this.items[_i];
  281. if (_i === currentItem) {
  282. addSelectedClass(item);
  283. if (document.activeElement !== item.el()) {
  284. dom.addClass(item.thumbnail, 'vjs-playlist-now-playing');
  285. }
  286. notUpNext(item);
  287. } else if (_i === currentItem + 1) {
  288. removeSelectedClass(item);
  289. upNext(item);
  290. } else {
  291. removeSelectedClass(item);
  292. notUpNext(item);
  293. }
  294. }
  295. };
  296. return PlaylistMenu;
  297. }(Component);
  298. /**
  299. * Returns a boolean indicating whether an element has child elements.
  300. *
  301. * Note that this is distinct from whether it has child _nodes_.
  302. *
  303. * @param {HTMLElement} el
  304. * A DOM element.
  305. *
  306. * @return {boolean}
  307. * Whether the element has child elements.
  308. */
  309. var hasChildEls = function hasChildEls(el) {
  310. for (var i = 0; i < el.childNodes.length; i++) {
  311. if (dom.isEl(el.childNodes[i])) {
  312. return true;
  313. }
  314. }
  315. return false;
  316. };
  317. /**
  318. * Finds the first empty root element.
  319. *
  320. * @param {string} className
  321. * An HTML class name to search for.
  322. *
  323. * @return {HTMLElement}
  324. * A DOM element to use as the root for a playlist.
  325. */
  326. var findRoot = function findRoot(className) {
  327. var all = document.querySelectorAll('.' + className);
  328. var el;
  329. for (var i = 0; i < all.length; i++) {
  330. if (!hasChildEls(all[i])) {
  331. el = all[i];
  332. break;
  333. }
  334. }
  335. return el;
  336. };
  337. /**
  338. * Initialize the plugin on a player.
  339. *
  340. * @param {Object} [options]
  341. * An options object.
  342. *
  343. * @param {HTMLElement} [options.el]
  344. * A DOM element to use as a root node for the playlist.
  345. *
  346. * @param {string} [options.className]
  347. * An HTML class name to use to find a root node for the playlist.
  348. *
  349. * @param {boolean} [options.playOnSelect = false]
  350. * If true, will attempt to begin playback upon selecting a new
  351. * playlist item in the UI.
  352. */
  353. var playlistUi = function playlistUi(options) {
  354. var player = this;
  355. if (!player.playlist) {
  356. throw new Error('videojs-playlist plugin is required by the videojs-playlist-ui plugin');
  357. }
  358. if (dom.isEl(options)) {
  359. videojs.log.warn('videojs-playlist-ui: Passing an element directly to playlistUi() is deprecated, use the "el" option instead!');
  360. options = {
  361. el: options
  362. };
  363. }
  364. options = videojs.mergeOptions(defaults, options); // If the player is already using this plugin, remove the pre-existing
  365. // PlaylistMenu, but retain the element and its location in the DOM because
  366. // it will be re-used.
  367. if (player.playlistMenu) {
  368. var el = player.playlistMenu.el(); // Catch cases where the menu may have been disposed elsewhere or the
  369. // element removed from the DOM.
  370. if (el) {
  371. var parentNode = el.parentNode;
  372. var nextSibling = el.nextSibling; // Disposing the menu will remove `el` from the DOM, but we need to
  373. // empty it ourselves to be sure.
  374. player.playlistMenu.dispose();
  375. dom.emptyEl(el); // Put the element back in its place.
  376. if (nextSibling) {
  377. parentNode.insertBefore(el, nextSibling);
  378. } else {
  379. parentNode.appendChild(el);
  380. }
  381. options.el = el;
  382. }
  383. }
  384. if (!dom.isEl(options.el)) {
  385. options.el = findRoot(options.className);
  386. }
  387. player.playlistMenu = new PlaylistMenu(player, options);
  388. }; // register components
  389. videojs.registerComponent('PlaylistMenu', PlaylistMenu);
  390. videojs.registerComponent('PlaylistMenuItem', PlaylistMenuItem); // register the plugin
  391. registerPlugin('playlistUi', playlistUi);
  392. playlistUi.VERSION = version;
  393. return playlistUi;
  394. })));