plyr/src/js/ui.js
2018-01-22 23:15:10 +11:00

349 lines
11 KiB
JavaScript

// ==========================================================================
// Plyr UI
// ==========================================================================
import utils from './utils';
import captions from './captions';
import controls from './controls';
import fullscreen from './fullscreen';
import listeners from './listeners';
const ui = {
addStyleHook() {
utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
},
// Toggle native HTML5 media controls
toggleNativeControls(toggle = false) {
if (toggle && this.isHTML5) {
this.media.setAttribute('controls', '');
} else {
this.media.removeAttribute('controls');
}
},
// Setup the UI
build() {
// Re-attach media element listeners
// TODO: Use event bubbling
listeners.media.call(this);
// Don't setup interface if no support
if (!this.supported.ui) {
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
// Remove controls
utils.removeElement.call(this, 'controls');
// Remove large play
utils.removeElement.call(this, 'buttons.play');
// Restore native controls
ui.toggleNativeControls.call(this, true);
// Bail
return;
}
// Inject custom controls if not present
if (!utils.is.element(this.elements.controls)) {
// Inject custom controls
controls.inject.call(this);
// Re-attach control listeners
listeners.controls.call(this);
}
// If there's no controls, bail
if (!utils.is.element(this.elements.controls)) {
return;
}
// Remove native controls
ui.toggleNativeControls.call(this);
// Setup fullscreen
fullscreen.setup.call(this);
// Captions
captions.setup.call(this);
// Reset volume
this.volume = null;
// Reset mute state
this.muted = null;
// Reset speed
this.speed = null;
// Reset loop state
this.loop = null;
// Reset quality options
this.options.quality = [];
// Reset time display
ui.timeUpdate.call(this);
// Update the UI
ui.checkPlaying.call(this);
// Ready for API calls
this.ready = true;
// Ready event at end of execution stack
utils.dispatchEvent.call(this, this.media, 'ready');
// Set the title
ui.setTitle.call(this);
},
// Setup aria attribute for play and iframe title
setTitle() {
// Find the current text
let label = this.config.i18n.play;
// If there's a media title set, use that for the label
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
label += `, ${this.config.title}`;
// Set container label
this.elements.container.setAttribute('aria-label', this.config.title);
}
// If there's a play button, set label
if (utils.is.nodeList(this.elements.buttons.play)) {
Array.from(this.elements.buttons.play).forEach(button => {
button.setAttribute('aria-label', label);
});
}
// Set iframe title
// https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) {
const iframe = utils.getElement.call(this, 'iframe');
if (!utils.is.element(iframe)) {
return;
}
// Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
iframe.setAttribute('title', this.config.i18n.frameTitle.replace('{title}', title));
}
},
// Check playing state
checkPlaying() {
// Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
// Set aria state
if (utils.is.nodeList(this.elements.buttons.play)) {
Array.from(this.elements.buttons.play).forEach(button => utils.toggleState(button, this.playing));
}
// Toggle controls
this.toggleControls(!this.playing);
},
// Check if media is loading
checkLoading(event) {
this.loading = [
'stalled',
'waiting',
].includes(event.type);
// Clear timer
clearTimeout(this.timers.loading);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Toggle container class hook
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if loading, hide if done
this.toggleControls(this.loading);
}, this.loading ? 250 : 0);
},
// Check if media failed to load
checkFailed() {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
this.failed = this.media.networkState === 3;
if (this.failed) {
utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
utils.toggleClass(this.elements.container, this.config.classNames.error, true);
}
// Clear timer
clearTimeout(this.timers.failed);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Toggle container class hook
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if loading, hide if done
this.toggleControls(this.loading);
}, this.loading ? 250 : 0);
},
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.element(this.elements.inputs.volume)) {
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update mute state
if (utils.is.element(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill
setRange(target, value = 0) {
if (!utils.is.element(target)) {
return;
}
// eslint-disable-next-line
target.value = value;
// Webkit range fill
controls.updateRangeFill.call(this, target);
},
// Set <progress> value
setProgress(target, input) {
const value = utils.is.number(input) ? input : 0;
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (utils.is.element(progress)) {
progress.value = value;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (utils.is.element(label)) {
label.childNodes[0].nodeValue = value;
}
}
},
// Update <progress> elements
updateProgress(event) {
if (!this.supported.ui || !utils.is.event(event)) {
return;
}
let value = 0;
if (event) {
switch (event.type) {
// Video playing
case 'timeupdate':
case 'seeking':
value = utils.getPercentage(this.currentTime, this.duration);
// Set seek range value only if it's a 'natural' time event
if (event.type === 'timeupdate') {
ui.setRange.call(this, this.elements.inputs.seek, value);
}
break;
// Check buffer status
case 'playing':
case 'progress':
value = (() => {
const { buffered } = this.media;
if (buffered && buffered.length) {
// HTML5
return utils.getPercentage(buffered.end(0), this.duration);
} else if (utils.is.number(buffered)) {
// YouTube returns between 0 and 1
return buffered * 100;
}
return 0;
})();
ui.setProgress.call(this, this.elements.display.buffer, value);
break;
default:
break;
}
}
},
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
if (!utils.is.element(target)) {
return;
}
// Always display hours if duration is over an hour
const displayHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.textContent = utils.formatTime(time, displayHours, inverted);
},
// Handle time change event
timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
// Duration
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
// Ignore updates while seeking
if (event && event.type === 'timeupdate' && this.media.seeking) {
return;
}
// Playing progress
ui.updateProgress.call(this, event);
},
// Show the duration on metadataloaded
durationUpdate() {
if (!this.supported.ui) {
return;
}
// If there's a spot to display duration
const hasDuration = utils.is.element(this.elements.display.duration);
// If there's only one time display, display duration there
if (!hasDuration && this.config.displayDuration && this.paused) {
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
}
// If there's a duration element, update content
if (hasDuration) {
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
};
export default ui;