diff options
Diffstat (limited to 'src/js/controls.js')
-rw-r--r-- | src/js/controls.js | 1177 |
1 files changed, 1177 insertions, 0 deletions
diff --git a/src/js/controls.js b/src/js/controls.js new file mode 100644 index 00000000..1e10f2f2 --- /dev/null +++ b/src/js/controls.js @@ -0,0 +1,1177 @@ +// ========================================================================== +// Plyr controls +// ========================================================================== + +import support from './support'; +import utils from './utils'; + +const controls = { + // Webkit polyfill for lower fill range + updateRangeFill(target) { + // WebKit only + if (!this.browser.isWebkit) { + return; + } + + // Get range from event if event passed + const range = utils.is.event(target) ? target.target : target; + + // Needs to be a valid <input type='range'> + if (!utils.is.htmlElement(range) || range.getAttribute('type') !== 'range') { + return; + } + + // Inject the stylesheet if needed + if (!utils.is.htmlElement(this.elements.styleSheet)) { + this.elements.styleSheet = utils.createElement('style'); + this.elements.container.appendChild(this.elements.styleSheet); + } + + const styleSheet = this.elements.styleSheet.sheet; + const percentage = range.value / range.max * 100; + const selector = `#${range.id}::-webkit-slider-runnable-track`; + const styles = `{ background-image: linear-gradient(to right, currentColor ${percentage}%, transparent ${percentage}%) }`; + + // Find old rule if it exists + const index = Array.from(styleSheet.rules).findIndex(rule => rule.selectorText === selector); + + // Remove old rule + if (index !== -1) { + styleSheet.deleteRule(index); + } + + // Insert new one + styleSheet.insertRule([selector, styles].join(' ')); + }, + + // Get icon URL + getIconUrl() { + return { + url: this.config.iconUrl, + absolute: this.config.iconUrl.indexOf('http') === 0 || this.browser.isIE, + }; + }, + + // Create <svg> icon + createIcon(type, attributes) { + const namespace = 'http://www.w3.org/2000/svg'; + const iconUrl = controls.getIconUrl.call(this); + const iconPath = `${!iconUrl.absolute ? iconUrl.url : ''}#${this.config.iconPrefix}`; + + // Create <svg> + const icon = document.createElementNS(namespace, 'svg'); + utils.setAttributes( + icon, + utils.extend(attributes, { + role: 'presentation', + }) + ); + + // Create the <use> to reference sprite + const use = document.createElementNS(namespace, 'use'); + use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `${iconPath}-${type}`); + + // Add <use> to <svg> + icon.appendChild(use); + + return icon; + }, + + // Create hidden text label + createLabel(type) { + let text = this.config.i18n[type]; + + switch (type) { + case 'pip': + text = 'PIP'; + break; + + case 'airplay': + text = 'AirPlay'; + break; + + default: + break; + } + + return utils.createElement( + 'span', + { + class: this.config.classNames.hidden, + }, + text + ); + }, + + // Create a badge + createBadge(text) { + const badge = utils.createElement('span', { + class: this.config.classNames.menu.value, + }); + + badge.appendChild( + utils.createElement( + 'span', + { + class: this.config.classNames.menu.badge, + }, + text + ) + ); + + return badge; + }, + + // Create a <button> + createButton(buttonType, attr) { + const button = utils.createElement('button'); + const attributes = Object.assign({}, attr); + let type = buttonType; + let iconDefault; + let iconToggled; + let labelKey; + + if (!('type' in attributes)) { + attributes.type = 'button'; + } + + if ('class' in attributes) { + if (attributes.class.indexOf(this.config.classNames.control) === -1) { + attributes.class += ` ${this.config.classNames.control}`; + } + } else { + attributes.class = this.config.classNames.control; + } + + // Large play button + switch (type) { + case 'mute': + labelKey = 'toggleMute'; + iconDefault = 'volume'; + iconToggled = 'muted'; + break; + + case 'captions': + labelKey = 'toggleCaptions'; + iconDefault = 'captions-off'; + iconToggled = 'captions-on'; + break; + + case 'fullscreen': + labelKey = 'toggleFullscreen'; + iconDefault = 'enter-fullscreen'; + iconToggled = 'exit-fullscreen'; + break; + + case 'play-large': + attributes.class = 'plyr__play-large'; + type = 'play'; + labelKey = 'play'; + iconDefault = 'play'; + break; + + default: + labelKey = type; + iconDefault = type; + } + + // Merge attributes + utils.extend(attributes, utils.getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); + + // Add toggle icon if needed + if (utils.is.string(iconToggled)) { + button.appendChild( + controls.createIcon.call(this, iconToggled, { + class: `icon--${iconToggled}`, + }) + ); + } + + button.appendChild(controls.createIcon.call(this, iconDefault)); + button.appendChild(controls.createLabel.call(this, labelKey)); + + utils.setAttributes(button, attributes); + + this.elements.buttons[type] = button; + + return button; + }, + + // Create an <input type='range'> + createRange(type, attributes) { + // Seek label + const label = utils.createElement( + 'label', + { + for: attributes.id, + class: this.config.classNames.hidden, + }, + this.config.i18n[type] + ); + + // Seek input + const input = utils.createElement( + 'input', + utils.extend( + utils.getAttributesFromSelector(this.config.selectors.inputs[type]), + { + type: 'range', + min: 0, + max: 100, + step: 0.01, + value: 0, + autocomplete: 'off', + }, + attributes + ) + ); + + this.elements.inputs[type] = input; + + return { + label, + input, + }; + }, + + // Create a <progress> + createProgress(type, attributes) { + const progress = utils.createElement( + 'progress', + utils.extend( + utils.getAttributesFromSelector(this.config.selectors.display[type]), + { + min: 0, + max: 100, + value: 0, + }, + attributes + ) + ); + + // Create the label inside + if (type !== 'volume') { + progress.appendChild(utils.createElement('span', null, '0')); + + let suffix = ''; + switch (type) { + case 'played': + suffix = this.config.i18n.played; + break; + + case 'buffer': + suffix = this.config.i18n.buffered; + break; + + default: + break; + } + + progress.textContent = `% ${suffix.toLowerCase()}`; + } + + this.elements.display[type] = progress; + + return progress; + }, + + // Create time display + createTime(type) { + const container = utils.createElement('span', { + class: 'plyr__time', + }); + + container.appendChild( + utils.createElement( + 'span', + { + class: this.config.classNames.hidden, + }, + this.config.i18n[type] + ) + ); + + container.appendChild( + utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00') + ); + + this.elements.display[type] = container; + + return container; + }, + + // Hide/show a tab + toggleTab(setting, toggle) { + const tab = this.elements.settings.tabs[setting]; + const pane = this.elements.settings.panes[setting]; + + if (utils.is.htmlElement(tab)) { + if (toggle) { + tab.removeAttribute('hidden'); + } else { + tab.setAttribute('hidden', ''); + } + } + + if (utils.is.htmlElement(pane)) { + if (toggle) { + pane.removeAttribute('hidden'); + } else { + pane.setAttribute('hidden', ''); + } + } + }, + + // Set the YouTube quality menu + // TODO: Support for HTML5 + setQualityMenu(options) { + const list = this.elements.settings.panes.quality.querySelector('ul'); + + // Set options if passed and filter based on config + if (utils.is.array(options)) { + this.options.quality = options.filter(quality => this.config.quality.options.includes(quality)); + } else { + this.options.quality = this.config.quality.options; + } + + // Toggle the pane and tab + const toggle = !utils.is.empty(this.options.quality) && this.type === 'youtube'; + controls.toggleTab.call(this, 'quality', toggle); + + // If we're hiding, nothing more to do + if (!toggle) { + return; + } + + // Empty the menu + utils.emptyElement(list); + + // Get the badge HTML for HD, 4K etc + const getBadge = quality => { + let label = ''; + + switch (quality) { + case 'hd2160': + label = '4K'; + break; + + case 'hd1440': + label = 'WQHD'; + break; + + case 'hd1080': + label = 'HD'; + break; + + case 'hd720': + label = 'HD'; + break; + + default: + break; + } + + if (!label.length) { + return null; + } + + return controls.createBadge.call(this, label); + }; + + this.options.quality.forEach(quality => { + const item = utils.createElement('li'); + + const label = utils.createElement('label', { + class: this.config.classNames.control, + }); + + const radio = utils.createElement( + 'input', + utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.quality), { + type: 'radio', + name: 'plyr-quality', + value: quality, + }) + ); + + label.appendChild(radio); + label.appendChild(document.createTextNode(controls.getLabel.call(this, 'quality', quality))); + + const badge = getBadge(quality); + if (utils.is.htmlElement(badge)) { + label.appendChild(badge); + } + + item.appendChild(label); + list.appendChild(item); + }); + + controls.updateSetting.call(this, 'quality', list); + }, + + // Translate a value into a nice label + // TODO: Localisation + getLabel(setting, value) { + switch (setting) { + case 'speed': + return value === 1 ? 'Normal' : `${value}×`; + + case 'quality': + switch (value) { + case 'hd2160': + return '2160P'; + case 'hd1440': + return '1440P'; + case 'hd1080': + return '1080P'; + case 'hd720': + return '720P'; + case 'large': + return '480P'; + case 'medium': + return '360P'; + case 'small': + return '240P'; + case 'tiny': + return 'Tiny'; + case 'default': + return 'Auto'; + default: + return value; + } + + case 'captions': + return controls.getLanguage.call(this); + + default: + return null; + } + }, + + // Update the selected setting + updateSetting(setting, container) { + const pane = this.elements.settings.panes[setting]; + let value = null; + let list = container; + + switch (setting) { + case 'captions': + value = this.captions.language; + + if (!this.captions.enabled) { + value = ''; + } + + break; + + default: + value = this[setting]; + + // Get default + if (utils.is.empty(value)) { + value = this.config[setting].default; + } + + // Unsupported value + if (!this.options[setting].includes(value)) { + this.warn(`Unsupported value of '${value}' for ${setting}`); + return; + } + + // Disabled value + if (!this.config[setting].options.includes(value)) { + this.warn(`Disabled value of '${value}' for ${setting}`); + return; + } + + break; + } + + // Get the list if we need to + if (!utils.is.htmlElement(list)) { + list = pane && pane.querySelector('ul'); + } + + // Find the radio option + const target = list && list.querySelector(`input[value="${value}"]`); + + if (!utils.is.htmlElement(target)) { + return; + } + + // Check it + target.checked = true; + + // Find the label + const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`); + label.innerHTML = controls.getLabel.call(this, setting, value); + }, + + // Set the looping options + setLoopMenu() { + const options = ['start', 'end', 'all', 'reset']; + const list = this.elements.settings.panes.loop.querySelector('ul'); + + // Show the pane and tab + this.elements.settings.tabs.loop.removeAttribute('hidden'); + this.elements.settings.panes.loop.removeAttribute('hidden'); + + // Toggle the pane and tab + const toggle = !utils.is.empty(this.loop.options); + controls.toggleTab.call(this, 'loop', toggle); + + // Empty the menu + utils.emptyElement(list); + + options.forEach(option => { + const item = utils.createElement('li'); + + const button = utils.createElement( + 'button', + utils.extend(utils.getAttributesFromSelector(this.config.selectors.buttons.loop), { + type: 'button', + class: this.config.classNames.control, + 'data-plyr-loop-action': option, + }), + this.config.i18n[option] + ); + + if (['start', 'end'].includes(option)) { + const badge = controls.createBadge.call(this, '00:00'); + button.appendChild(badge); + } + + item.appendChild(button); + list.appendChild(item); + }); + }, + + // Get current selected caption language + getLanguage() { + if (!this.supported.ui) { + return null; + } + + if (!support.textTracks || utils.is.empty(this.captions.tracks)) { + return this.config.i18n.none; + } + + if (this.captions.enabled) { + return this.captions.currentTrack.label; + } + return this.config.i18n.disabled; + }, + + // Set a list of available captions languages + setCaptionsMenu() { + const list = this.elements.settings.panes.captions.querySelector('ul'); + + // Toggle the pane and tab + const toggle = !utils.is.empty(this.captions.tracks); + controls.toggleTab.call(this, 'captions', toggle); + + // Empty the menu + utils.emptyElement(list); + + // If there's no captions, bail + if (utils.is.empty(this.captions.tracks)) { + return; + } + + // Re-map the tracks into just the data we need + const tracks = Array.from(this.captions.tracks).map(track => ({ + language: track.language, + badge: true, + label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(), + })); + + // Add the "None" option to turn off captions + tracks.unshift({ + language: '', + label: this.config.i18n.none, + }); + + // Generate options + tracks.forEach(track => { + const item = utils.createElement('li'); + + const label = utils.createElement('label', { + class: this.config.classNames.control, + }); + + const radio = utils.createElement( + 'input', + utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.language), { + type: 'radio', + name: 'plyr-language', + value: track.language, + }) + ); + + if (track.language.toLowerCase() === this.captions.language.toLowerCase()) { + radio.checked = true; + } + + label.appendChild(radio); + label.appendChild(document.createTextNode(track.label || track.language)); + + if (track.badge) { + label.appendChild(controls.createBadge.call(this, track.language.toUpperCase())); + } + + item.appendChild(label); + list.appendChild(item); + }); + + controls.updateSetting.call(this, 'captions', list); + }, + + // Set a list of available captions languages + setSpeedMenu(options) { + // Set options if passed and filter based on config + if (utils.is.array(options)) { + this.options.speed = options.filter(speed => this.config.speed.options.includes(speed)); + } else { + this.options.speed = this.config.speed.options; + } + + // Toggle the pane and tab + const toggle = !utils.is.empty(this.options.speed); + controls.toggleTab.call(this, 'speed', toggle); + + // If we're hiding, nothing more to do + if (!toggle) { + return; + } + + // Get the list to populate + const list = this.elements.settings.panes.speed.querySelector('ul'); + + // Show the pane and tab + this.elements.settings.tabs.speed.removeAttribute('hidden'); + this.elements.settings.panes.speed.removeAttribute('hidden'); + + // Empty the menu + utils.emptyElement(list); + + // Create items + this.options.speed.forEach(speed => { + const item = utils.createElement('li'); + + const label = utils.createElement('label', { + class: this.config.classNames.control, + }); + + const radio = utils.createElement( + 'input', + utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.speed), { + type: 'radio', + name: 'plyr-speed', + value: speed, + }) + ); + + label.appendChild(radio); + label.insertAdjacentHTML('beforeend', controls.getLabel.call(this, 'speed', speed)); + item.appendChild(label); + list.appendChild(item); + }); + + controls.updateSetting.call(this, 'speed', list); + }, + + // Show/hide menu + toggleMenu(event) { + const { form } = this.elements.settings; + const button = this.elements.buttons.settings; + const show = utils.is.boolean(event) ? event : form && form.getAttribute('aria-hidden') === 'true'; + + if (utils.is.event(event)) { + const isMenuItem = form && form.contains(event.target); + const isButton = event.target === this.elements.buttons.settings; + + // If the click was inside the form or if the click + // wasn't the button or menu item and we're trying to + // show the menu (a doc click shouldn't show the menu) + if (isMenuItem || (!isMenuItem && !isButton && show)) { + return; + } + + // Prevent the toggle being caught by the doc listener + if (isButton) { + event.stopPropagation(); + } + } + + // Set form and button attributes + if (button) { + button.setAttribute('aria-expanded', show); + } + if (form) { + form.setAttribute('aria-hidden', !show); + + if (show) { + form.removeAttribute('tabindex'); + } else { + form.setAttribute('tabindex', -1); + } + } + }, + + // Get the natural size of a tab + getTabSize(tab) { + const clone = tab.cloneNode(true); + clone.style.position = 'absolute'; + clone.style.opacity = 0; + clone.setAttribute('aria-hidden', false); + + // Prevent input's being unchecked due to the name being identical + Array.from(clone.querySelectorAll('input[name]')).forEach(input => { + const name = input.getAttribute('name'); + input.setAttribute('name', `${name}-clone`); + }); + + // Append to parent so we get the "real" size + tab.parentNode.appendChild(clone); + + // Get the sizes before we remove + const width = clone.scrollWidth; + const height = clone.scrollHeight; + + // Remove from the DOM + utils.removeElement(clone); + + return { + width, + height, + }; + }, + + // Toggle Menu + showTab(event) { + const { menu } = this.elements.settings; + const tab = event.target; + const show = tab.getAttribute('aria-expanded') === 'false'; + const pane = document.getElementById(tab.getAttribute('aria-controls')); + + // Nothing to show, bail + if (!utils.is.htmlElement(pane)) { + return; + } + + // Are we targetting a tab? If not, bail + const isTab = pane.getAttribute('role') === 'tabpanel'; + if (!isTab) { + return; + } + + // Hide all other tabs + // Get other tabs + const current = menu.querySelector('[role="tabpanel"][aria-hidden="false"]'); + const container = current.parentNode; + + // Set other toggles to be expanded false + Array.from(menu.querySelectorAll(`[aria-controls="${current.getAttribute('id')}"]`)).forEach(toggle => { + toggle.setAttribute('aria-expanded', false); + }); + + // If we can do fancy animations, we'll animate the height/width + if (support.transitions && !support.reducedMotion) { + // Set the current width as a base + container.style.width = `${current.scrollWidth}px`; + container.style.height = `${current.scrollHeight}px`; + + // Get potential sizes + const size = controls.getTabSize.call(this, pane); + + // Restore auto height/width + const restore = e => { + // We're only bothered about height and width on the container + if (e.target !== container || !['width', 'height'].includes(e.propertyName)) { + return; + } + + // Revert back to auto + container.style.width = ''; + container.style.height = ''; + + // Only listen once + utils.off(container, utils.transitionEnd, restore); + }; + + // Listen for the transition finishing and restore auto height/width + utils.on(container, utils.transitionEnd, restore); + + // Set dimensions to target + container.style.width = `${size.width}px`; + container.style.height = `${size.height}px`; + } + + // Set attributes on current tab + current.setAttribute('aria-hidden', true); + current.setAttribute('tabindex', -1); + + // Set attributes on target + pane.setAttribute('aria-hidden', !show); + tab.setAttribute('aria-expanded', show); + pane.removeAttribute('tabindex'); + }, + + // Build the default HTML + // TODO: Set order based on order in the config.controls array? + create(data) { + // Do nothing if we want no controls + if (utils.is.empty(this.config.controls)) { + return null; + } + + // Create the container + const container = utils.createElement( + 'div', + utils.getAttributesFromSelector(this.config.selectors.controls.wrapper) + ); + + // Restart button + if (this.config.controls.includes('restart')) { + container.appendChild(controls.createButton.call(this, 'restart')); + } + + // Rewind button + if (this.config.controls.includes('rewind')) { + container.appendChild(controls.createButton.call(this, 'rewind')); + } + + // Play Pause button + if (this.config.controls.includes('play')) { + container.appendChild(controls.createButton.call(this, 'play')); + container.appendChild(controls.createButton.call(this, 'pause')); + } + + // Fast forward button + if (this.config.controls.includes('fast-forward')) { + container.appendChild(controls.createButton.call(this, 'fast-forward')); + } + + // Progress + if (this.config.controls.includes('progress')) { + const progress = utils.createElement( + 'span', + utils.getAttributesFromSelector(this.config.selectors.progress) + ); + + // Seek range slider + const seek = controls.createRange.call(this, 'seek', { + id: `plyr-seek-${data.id}`, + }); + progress.appendChild(seek.label); + progress.appendChild(seek.input); + + // Buffer progress + progress.appendChild(controls.createProgress.call(this, 'buffer')); + + // TODO: Add loop display indicator + + // Seek tooltip + if (this.config.tooltips.seek) { + const tooltip = utils.createElement( + 'span', + { + role: 'tooltip', + class: this.config.classNames.tooltip, + }, + '00:00' + ); + + progress.appendChild(tooltip); + this.elements.display.seekTooltip = tooltip; + } + + this.elements.progress = progress; + container.appendChild(this.elements.progress); + } + + // Media current time display + if (this.config.controls.includes('current-time')) { + container.appendChild(controls.createTime.call(this, 'currentTime')); + } + + // Media duration display + if (this.config.controls.includes('duration')) { + container.appendChild(controls.createTime.call(this, 'duration')); + } + + // Toggle mute button + if (this.config.controls.includes('mute')) { + container.appendChild(controls.createButton.call(this, 'mute')); + } + + // Volume range control + if (this.config.controls.includes('volume')) { + const volume = utils.createElement('span', { + class: 'plyr__volume', + }); + + // Set the attributes + const attributes = { + max: 1, + step: 0.05, + value: this.config.volume, + }; + + // Create the volume range slider + const range = controls.createRange.call( + this, + 'volume', + utils.extend(attributes, { + id: `plyr-volume-${data.id}`, + }) + ); + volume.appendChild(range.label); + volume.appendChild(range.input); + + container.appendChild(volume); + } + + // Toggle captions button + if (this.config.controls.includes('captions')) { + container.appendChild(controls.createButton.call(this, 'captions')); + } + + // Settings button / menu + if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { + const menu = utils.createElement('div', { + class: 'plyr__menu', + }); + + menu.appendChild( + controls.createButton.call(this, 'settings', { + id: `plyr-settings-toggle-${data.id}`, + 'aria-haspopup': true, + 'aria-controls': `plyr-settings-${data.id}`, + 'aria-expanded': false, + }) + ); + + const form = utils.createElement('form', { + class: 'plyr__menu__container', + id: `plyr-settings-${data.id}`, + 'aria-hidden': true, + 'aria-labelled-by': `plyr-settings-toggle-${data.id}`, + role: 'tablist', + tabindex: -1, + }); + + const inner = utils.createElement('div'); + + const home = utils.createElement('div', { + id: `plyr-settings-${data.id}-home`, + 'aria-hidden': false, + 'aria-labelled-by': `plyr-settings-toggle-${data.id}`, + role: 'tabpanel', + }); + + // Create the tab list + const tabs = utils.createElement('ul', { + role: 'tablist', + }); + + // Build the tabs + this.config.settings.forEach(type => { + const tab = utils.createElement('li', { + role: 'tab', + hidden: '', + }); + + const button = utils.createElement( + 'button', + utils.extend(utils.getAttributesFromSelector(this.config.selectors.buttons.settings), { + type: 'button', + class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, + id: `plyr-settings-${data.id}-${type}-tab`, + 'aria-haspopup': true, + 'aria-controls': `plyr-settings-${data.id}-${type}`, + 'aria-expanded': false, + }), + this.config.i18n[type] + ); + + const value = utils.createElement('span', { + class: this.config.classNames.menu.value, + }); + + // Speed contains HTML entities + value.innerHTML = data[type]; + + button.appendChild(value); + tab.appendChild(button); + tabs.appendChild(tab); + + this.elements.settings.tabs[type] = tab; + }); + + home.appendChild(tabs); + inner.appendChild(home); + + // Build the panes + this.config.settings.forEach(type => { + const pane = utils.createElement('div', { + id: `plyr-settings-${data.id}-${type}`, + 'aria-hidden': true, + 'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`, + role: 'tabpanel', + tabindex: -1, + hidden: '', + }); + + const back = utils.createElement( + 'button', + { + type: 'button', + class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, + 'aria-haspopup': true, + 'aria-controls': `plyr-settings-${data.id}-home`, + 'aria-expanded': false, + }, + this.config.i18n[type] + ); + + pane.appendChild(back); + + const options = utils.createElement('ul'); + + pane.appendChild(options); + inner.appendChild(pane); + + this.elements.settings.panes[type] = pane; + }); + + form.appendChild(inner); + menu.appendChild(form); + container.appendChild(menu); + + this.elements.settings.form = form; + this.elements.settings.menu = menu; + } + + // Picture in picture button + if (this.config.controls.includes('pip') && support.pip) { + container.appendChild(controls.createButton.call(this, 'pip')); + } + + // Airplay button + if (this.config.controls.includes('airplay') && support.airplay) { + container.appendChild(controls.createButton.call(this, 'airplay')); + } + + // Toggle fullscreen button + if (this.config.controls.includes('fullscreen')) { + container.appendChild(controls.createButton.call(this, 'fullscreen')); + } + + // Larger overlaid play button + if (this.config.controls.includes('play-large')) { + this.elements.buttons.playLarge = controls.createButton.call(this, 'play-large'); + this.elements.container.appendChild(this.elements.buttons.playLarge); + } + + this.elements.controls = container; + + if (this.config.controls.includes('settings') && this.config.settings.includes('speed')) { + controls.setSpeedMenu.call(this); + } + + return container; + }, + + // Insert controls + inject() { + // Sprite + if (this.config.loadSprite) { + const iconUrl = controls.getIconUrl.call(this); + + // Only load external sprite using AJAX + if (iconUrl.absolute) { + this.log(`AJAX loading absolute SVG sprite ${this.browser.isIE ? '(due to IE)' : ''}`); + utils.loadSprite(iconUrl.url, 'sprite-plyr'); + } else { + this.log('Sprite will be used as external resource directly'); + } + } + + // Create a unique ID + this.id = Math.floor(Math.random() * 10000); + + // Null by default + let container = null; + + // HTML passed as the option + if (utils.is.string(this.config.controls)) { + container = this.config.controls; + } else if (utils.is.function(this.config.controls)) { + // A custom function to build controls + // The function can return a HTMLElement or String + container = this.config.controls({ + id: this.id, + seektime: this.config.seekTime, + }); + } else { + // Create controls + container = controls.create.call(this, { + id: this.id, + seektime: this.config.seekTime, + speed: '-', + // TODO: Get current quality + quality: '-', + captions: controls.getLanguage.call(this), + // TODO: Get loop + loop: 'None', + }); + } + + // Controls container + let target; + + // Inject to custom location + if (utils.is.string(this.config.selectors.controls.container)) { + target = document.querySelector(this.config.selectors.controls.container); + } + + // Inject into the container by default + if (!utils.is.htmlElement(target)) { + target = this.elements.container; + } + + // Inject controls HTML + if (utils.is.htmlElement(container)) { + target.appendChild(container); + } else { + target.insertAdjacentHTML('beforeend', container); + } + + // Find the elements if need be + if (utils.is.htmlElement(this.elements.controls)) { + utils.findElements.call(this); + } + + // Setup tooltips + if (this.config.tooltips.controls) { + const labels = utils.getElements.call( + this, + [ + this.config.selectors.controls.wrapper, + ' ', + this.config.selectors.labels, + ' .', + this.config.classNames.hidden, + ].join('') + ); + + Array.from(labels).forEach(label => { + utils.toggleClass(label, this.config.classNames.hidden, false); + utils.toggleClass(label, this.config.classNames.tooltip, true); + }); + } + }, +}; + +export default controls; |