plyr/src/js/controls.js

1244 lines
39 KiB
JavaScript

// ==========================================================================
// Plyr controls
// ==========================================================================
import support from './support';
import utils from './utils';
import ui from './ui';
// Sniff out the browser
const browser = utils.getBrowser();
const controls = {
// Webkit polyfill for lower fill range
updateRangeFill(target) {
// WebKit only
if (!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 || (browser.isIE && !window.svg4everybody),
};
},
// 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');
const path = `${iconPath}-${type}`;
// If the new `href` attribute is supported, use that
// https://github.com/sampotts/plyr/issues/460
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
if ('href' in use) {
use.setAttribute('href', path);
} else {
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
}
// 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--pressed',
})
);
button.appendChild(
controls.createIcon.call(this, iconDefault, {
class: 'icon--not-pressed',
})
);
} else {
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;
// Set the fill for webkit now
controls.updateRangeFill.call(this, 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;
},
// Update hover tooltip for seeking
updateSeekTooltip(event) {
// Bail if setting not true
if (
!this.config.tooltips.seek ||
!utils.is.htmlElement(this.elements.inputs.seek) ||
!utils.is.htmlElement(this.elements.display.seekTooltip) ||
this.duration === 0
) {
return;
}
// Calculate percentage
let percent = 0;
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`;
// Determine percentage, if already visible
if (utils.is.event(event)) {
percent = 100 / clientRect.width * (event.pageX - clientRect.left);
} else if (utils.hasClass(this.elements.display.seekTooltip, visible)) {
percent = this.elements.display.seekTooltip.style.left.replace('%', '');
} else {
return;
}
// Set bounds
if (percent < 0) {
percent = 0;
} else if (percent > 100) {
percent = 100;
}
// Display the time a click would seek to
ui.updateTimeDisplay.call(this, this.duration / 100 * percent, this.elements.display.seekTooltip);
// Set position
this.elements.display.seekTooltip.style.left = `${percent}%`;
// Show/hide the tooltip
// If the event is a moues in/out and percentage is inside bounds
if (utils.is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {
utils.toggleClass(this.elements.display.seekTooltip, visible, event.type === 'mouseenter');
}
},
// 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}&times;`;
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.console.warn(`Unsupported value of '${value}' for ${setting}`);
return;
}
// Disabled value
if (!this.config[setting].options.includes(value)) {
this.console.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
// TODO: rework this to user the getter in the API?
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.container.appendChild(controls.createButton.call(this, 'play-large'));
}
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 icon = controls.getIconUrl.call(this);
// Only load external sprite using AJAX
if (icon.absolute) {
utils.loadSprite(icon.url, 'sprite-plyr');
}
}
// 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,
title: this.config.title,
});
} else {
// Create controls
container = controls.create.call(this, {
id: this.id,
seektime: this.config.seekTime,
speed: this.speed,
quality: this.quality,
captions: controls.getLanguage.call(this),
// TODO: Looping
// 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;