UNPKG

youtube-chapters

Version:

A chapters markers generator plugin for a YouTube video, built using JavaScript.

581 lines (515 loc) 15.9 kB
import { defaultOption, checkIds, getTime, convertTime, sort, loader, } from '../helper-functions'; import Q from '../query'; const _elements = []; // Elements array to store the elements const _players = []; // Players array to store the current players /** * Main YouTubeChapters class * * @class YouTubeChapters * @example * YouTubeChapters('elementID', options); * * or * * let player = new YouTubeChapters('elementID', options); */ export class YouTubeChapters { /** * Main constructor for YouTubeChapters class * @constructor * @param {string} element - A DOM element to wrap in a YouTubeChapters object. * @param {object} options - The options for the YouTube video * @param {string} options.youtubeId - Sets the YouTube video based on the ID. * @param {string} [options.id=A random number] - The random ID of the element. * @param {string} [options.loader=Default loader] - Loader for the YouTube chapter frame. * @param {bool} [options.fluid=false] - Sets if the YouTube video should be fluid/responsive. * @param {array} [options.width=500px] - Sets the video and wrapper width. * @param {array} [options.height=350px] - Sets the video height. * @param {object} [options.playerVars=[]] - Sets the YouTube playerVars. * @param {bool} [options.showTime=false] - Sets if the times should appear in the chapter frame. * @param {array} options.chapters -Sets the chapters of the YouTube video. */ constructor(element, options) { this.options = defaultOption(options, {}); for (let i = 0; i < _elements.length; i += 1) { if (_elements[i] === element) { throw new Error(`An instance of YouTubeChapters with element ${element} already exists.`); } } if (!element || typeof element !== 'string') { throw new Error('Element has not been set, please set an element.'); } if (Q(element).length < 1) { throw new Error(`Element ${this.element} does not exsit.`); } this._interval = null; this._playerReadyFunction = []; this._readyFunction = []; this.element = element; this.ytc = {}; this.ytc.id = defaultOption(this.options.id, Math.floor(Math.random() * 99999)); this.ytc.element = `youtube-iframe-${this.ytc.id}`; this.ytc.player = null; this.ytc.options = { youtubeId: defaultOption(this.options.youtubeId, 'bHQqvYy5KYo'), loader: defaultOption(this.options.loader, loader()), fluid: defaultOption(this.options.fluid, false), playerVars: defaultOption(this.options.playerVars, {}), width: defaultOption(this.options.width, '500px'), height: defaultOption(this.options.height, '350px'), showTime: defaultOption(this.options.showTime, false), chapters: sort(defaultOption(this.options.chapters, [])) }; if (checkIds(this.options.chapters)) { throw new Error('All ID\'s must be unqiue'); } this._init(); } /** * Begin the initialisation of the library. * @private * @returns {YouTubeChapters} YouTubeChapters */ _init() { _elements.push(this.element); this._buildStruture() ._addChapterPoints() ._addTextPoints() ._addYouTubeVideo(); } /** * Builds the main structure of YouTube frame. * @private * @returns {YouTubeChapters} YouTubeChapters */ _buildStruture() { const mainElement = Q(this.element); mainElement .addElement('div', { class: 'loading' }, this.ytc.options.loader) .addElement('div', { class: 'youtube-chapter-wrapper', }); mainElement .find('.loading') .width(mainElement.parent().width()); mainElement .find('.youtube-chapter-wrapper') .addElement('div', { class: 'youtube-wrapper', style: `max-width: ${this.ytc.options.width}` }) .addElement('div', { class: 'chapters-wrapper' }) .addElement('div', { class: 'text-wrapper' }) .find('.youtube-wrapper') .addElement('div', { class: `youtube-video ${this.ytc.options.fluid ? 'fluid-wrapper' : ''}`, id: `youtube-video-${this.ytc.id}` }) .find('.youtube-video') .addElement('div', { class: 'youtube-iframe', id: this.ytc.element }); mainElement .find('.chapters-wrapper') .addElement('ul', { id: `chapters-wrapper-${this.ytc.id}` }); mainElement .find('.text-wrapper') .addElement('ul', { id: `text-wrapper-${this.ytc.id}` }); return this; } /** * Add the chapter points to the YouTube player frame. * @private * @returns {YouTubeChapters} YouTubeChapters */ _addChapterPoints() { /** * Sets the click event to each chapter point. * @private * @param {event} event */ const goToChatper = event => { event.preventDefault(); this.seekTo(event.currentTarget.getAttribute('data-time')); }; const chaptersWrapper = Q(`#chapters-wrapper-${this.ytc.id}`); Q(this.ytc.options.chapters).each((index, current) => { chaptersWrapper .addElement('li', { class: 'chapter-point-wrapper', id: `chapter-${current.id}-${this.ytc.id}`, dataTime: convertTime(current.time) }) .find('li.chapter-point-wrapper') .addElement('div', { class: 'chapter-point', id: `chapter-point-${current.id}-${this.ytc.id}` }, current.title) .find('.chapter-point') .addElement('span', { class: 'time' }, (this.ytc.options.showTime ? current.time : '')); }); chaptersWrapper .find('li.chapter-point-wrapper') .addEvent('click', goToChatper); return this; } /** * Adds the chapter’s optional text. * @private * @returns {YouTubeChapters} YouTubeChapters */ _addTextPoints() { const textWrapper = Q(`#text-wrapper-${this.ytc.id}`); Q(this.ytc.options.chapters).each((index, current) => { textWrapper .addElement('li', { class: 'text-point', id: `text-point-${current.id}-${this.ytc.id}` }, current.text); }); return this; } /** * Adds the YouTube video script and sets up the YouTube video. * @private * @returns {YouTubeChapters} YouTubeChapters */ _addYouTubeVideo() { if (window.YT && window.YT.Player) { this._playerSetup(this.ytc); } else { _players.push(() => { this._playerSetup(this.ytc); }); } if (!window.onYouTubeIframeAPIReady) { const tag = document.createElement('script'); tag.src = 'https://www.youtube.com/iframe_api'; const firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); window.onYouTubePlayerAPIReady = () => { Q(_players).each((index, player) => { player(); }); }; } return this; } /** * Sets up the player height and displays the YouTube player has * finished loading. * @private * @returns {YouTubeChapters} YouTubeChapters */ _complete() { const element = Q(this.element); const iframeHeight = this.ytc.options.fluid ? element .find(`#youtube-video-${this.ytc.id}`) .height() : this.ytc.options.height.replace('px', ''); element .find('.chapters-wrapper') .height(iframeHeight); element .find('.youtube-chapter-wrapper') .addClass('complete'); element .find('.loading') .remove(); return this; } /** * Calls the chaining methods such as play, pause, goToChatperId if there * are any within the this._playerReadyFunction array. * @private * @returns {YouTubeChapters} YouTubeChapters */ _callPlayerReady() { Q(this._playerReadyFunction).each((index, playerReady) => { playerReady(); }); this._playerReadyFunction.length = 0; return this; } /** * Calls the ready method once the YouTube video has loaded. * @private * @returns {YouTubeChapters} YouTubeChapters */ _callReady() { if (this._readyFunction.length) { this._readyFunction[0](); } return this; } /** * Sets up the player. * @param {object} params * @param {string} params.id - The random ID of the element. * @param {string} params.element - The element. * @param {object} params.player - The YouTube video player. * @param {object} params.options - The original options. * @private */ _playerSetup(params) { /** * On ready method that is called once the video has finished playing. * @private */ const onReady = () => { this._complete() ._callReady() ._callPlayerReady(); }; /** * The method that runs while the YouTube video is being played. * @param {event} event * @private */ const onStateChange = event => { const chapters = Q(`${this.element} .chapter-point-wrapper`); const player = this.ytc.player; /** * The playing method that add the active class and shows the chapter text * to the current chapter. * @private */ const playing = () => { chapters.each((index, element) => { const current = Q(element); const textPoint = Q(`#${element.getAttribute('id').replace('chapter', 'text-point')}`); const time = parseFloat(current.attr('data-time')); let next; if (current.next().length > 0) { next = parseFloat(current.next().attr('data-time')); } else { next = player.getDuration(); } current.removeClass('current'); textPoint.removeClass('current'); if (player.getCurrentTime() > time && player.getCurrentTime() < next) { current.addClass('current'); if (!textPoint.isEmpty()) { textPoint.addClass('current'); } } }); }; switch (event.data) { case YT.PlayerState.PLAYING: this._interval = setInterval(playing, 250); break; case YT.PlayerState.UNSTARTED: case YT.PlayerState.BUFFERING: case YT.PlayerState.CUED: break; case YT.PlayerState.PAUSED: case YT.PlayerState.ENDED: clearInterval(this._interval); break; default: clearInterval(this._interval); throw new Error('Something went wrong.'); } }; /** * The on error method if an error occurs while the video is being played. * @param {event} event * @private */ const onError = event => { const errormsg = event.data; if (errormsg === 150 || errormsg === 101) { throw new Error('Error: An error has occurred while the video was loading.'); } event.target.stopVideo(); }; params.player = new YT.Player(params.element, { height: params.options.height, width: params.options.width, videoId: params.options.youtubeId, playerVars: params.options.playerVars, events: { onReady, onStateChange, onError } }); } /** * Function seek to and play video. * @param {number} time - The time the video should seek to. * @private * @todo Add play and pause option */ _seek(time) { this.ytc.player.seekTo(time); this.ytc.player.playVideo(); } /** * Ready function to call once the video has loaded. * @param {function} callback - Function callback after player is finished loading. * @param {YT} [callback.player] - The video player. * @example * YouTubeChapters(element, options) * .ready(function(player) { * .... * }); * * or * * let player = new YouTubeChapters(); * player.ready(function(player) { * .... * }); * * @returns {YouTubeChapters} YouTubeChapters */ ready(callback) { if (typeof callback !== 'function') { throw new Error('ready method must include a function.'); } if (window.YT && window.YT.Player) { callback.call(this); } else { this._readyFunction.push(() => { callback.call(this, this); }); } return this; } /** * Goes to the position of the YouTube video. * @param {number|string} seekToTime - The time to seek to. * @example * YouTubeChapters(element, options) * .seekTo('03:43'); or * .seekTo('2m 0s'); or * .seekTo(67); * * or * * let player = new YouTubeChapters(element, options); * player.seekTo('03:43'); or * player.seekTo('2m 0s'); or * player.seekTo(67); * * @returns {YouTubeChapters} YouTubeChapters */ seekTo(seekToTime) { const sTT = parseFloat(convertTime(seekToTime)); if (window.YT && window.YT.Player && this._playerReadyFunction.length === 0) { this._seek(sTT); } else { this._playerReadyFunction.push(() => { this._seek(sTT); }); } return this; } /** * Goes to the video position of the chapter ID. * @param {string} id - ID of the chapter. * @example * YouTubeChapters(element, options) * .goToChatperId('id1'); * * or * * let player = new YouTubeChapters(element, options); * player.goToChatperId('id1'); * * @returns {YouTubeChapters} YouTubeChapters */ goToChatperId(id) { const time = getTime(this.ytc.options.chapters, id); if (!time) { throw new Error(`No id with ${id} found.`); } const seekToTime = parseFloat(time); if (window.YT && window.YT.Player && this._playerReadyFunction.length === 0) { this._seek(seekToTime); } else { this._playerReadyFunction.push(() => { this._seek(seekToTime); }); } return this; } /** * Pauses the YouTube video. * @example * YouTubeChapters(element, options) * .pause(); * * or * * let player = new YouTubeChapters(element, options); * player.pause(); * * @returns {YouTubeChapters} YouTubeChapters */ pause() { if (window.YT && window.YT.Player && this._playerReadyFunction.length === 0) { this.ytc.player.pauseVideo(); } else { this._playerReadyFunction.push(() => { this.ytc.player.pauseVideo(); }); } return this; } /** * Plays the YouTube video. * @example * YouTubeChapters(element, options) * .play(); * * or * * let player = new YouTubeChapters(element, options); * player.play(); * * @returns {YouTubeChapters} YouTubeChapters */ play() { if (window.YT && window.YT.Player && this._playerReadyFunction.length === 0) { this.ytc.player.playVideo(); } else { this._playerReadyFunction.push(() => { this.ytc.player.playVideo(); }); } return this; } /** * Static method so class doesn't have to be called with new. * @param {string} element - A DOM element to wrap in a YouTubeChapters object. * @param {object} options - The options for the YouTube video. * @private * @returns {YouTubeChapters} YouTubeChapters */ static setup(element, options) { return new YouTubeChapters(element, options); } }