diff options
Diffstat (limited to 'src/js/ui.js')
-rw-r--r-- | src/js/ui.js | 513 |
1 files changed, 254 insertions, 259 deletions
diff --git a/src/js/ui.js b/src/js/ui.js index 32db6ae7..99faa9b8 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -13,267 +13,262 @@ import is from './utils/is'; import loadImage from './utils/load-image'; const ui = { - addStyleHook() { - toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); - toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); - }, + addStyleHook() { + toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); + 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? - this.listeners.media(); - - // Don't setup interface if no support - if (!this.supported.ui) { - this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); - - // Restore native controls - ui.toggleNativeControls.call(this, true); - - // Bail - return; - } - - // Inject custom controls if not present - if (!is.element(this.elements.controls)) { - // Inject custom controls - controls.inject.call(this); - - // Re-attach control listeners - this.listeners.controls(); - } - - // Remove native controls - ui.toggleNativeControls.call(this); - - // Setup captions for HTML5 - if (this.isHTML5) { - captions.setup.call(this); - } - - // Reset volume - this.volume = null; - - // Reset mute state - this.muted = null; - - // Reset loop state - this.loop = null; - - // Reset quality setting - this.quality = null; - - // Reset speed - this.speed = null; - - // Reset volume display - controls.updateVolume.call(this); - - // Reset time display - controls.timeUpdate.call(this); - - // Update the UI - ui.checkPlaying.call(this); - - // Check for picture-in-picture support - toggleClass( - this.elements.container, - this.config.classNames.pip.supported, - support.pip && this.isHTML5 && this.isVideo, - ); - - // Check for airplay support - toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); - - // Add iOS class - toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); - - // Add touch class - toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); - - // Ready for API calls - this.ready = true; - - // Ready event at end of execution stack - setTimeout(() => { - triggerEvent.call(this, this.media, 'ready'); - }, 0); - - // Set the title - ui.setTitle.call(this); - - // Assure the poster image is set, if the property was added before the element was created - if (this.poster) { - ui.setPoster.call(this, this.poster, false).catch(() => {}); - } - - // Manually set the duration if user has overridden it. - // The event listeners for it doesn't get called if preload is disabled (#701) - if (this.config.duration) { - controls.durationUpdate.call(this); - } - }, - - // Setup aria attribute for play and iframe title - setTitle() { - // Find the current text - let label = i18n.get('play', this.config); - - // If there's a media title set, use that for the label - if (is.string(this.config.title) && !is.empty(this.config.title)) { - label += `, ${this.config.title}`; - } - - // If there's a play button, set label - 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 = getElement.call(this, 'iframe'); - - if (!is.element(iframe)) { - return; - } - - // Default to media type - const title = !is.empty(this.config.title) ? this.config.title : 'video'; - const format = i18n.get('frameTitle', this.config); - - iframe.setAttribute('title', format.replace('{title}', title)); - } - }, - - // Toggle poster - togglePoster(enable) { - toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); - }, - - // Set the poster image (async) - // Used internally for the poster setter, with the passive option forced to false - setPoster(poster, passive = true) { - // Don't override if call is passive - if (passive && this.poster) { - return Promise.reject(new Error('Poster already set')); - } - - // Set property synchronously to respect the call order - this.media.setAttribute('poster', poster); - - // HTML5 uses native poster attribute - if (this.isHTML5) { - return Promise.resolve(poster); - } - - // Wait until ui is ready - return ( - ready - .call(this) - // Load image - .then(() => loadImage(poster)) - .catch(err => { - // Hide poster on error unless it's been set by another call - if (poster === this.poster) { - ui.togglePoster.call(this, false); - } - // Rethrow - throw err; - }) - .then(() => { - // Prevent race conditions - if (poster !== this.poster) { - throw new Error('setPoster cancelled by later call to setPoster'); - } - }) - .then(() => { - Object.assign(this.elements.poster.style, { - backgroundImage: `url('${poster}')`, - // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) - backgroundSize: '', - }); - - ui.togglePoster.call(this, true); - - return poster; - }) - ); - }, - - // Check playing state - checkPlaying(event) { - // Class hooks - toggleClass(this.elements.container, this.config.classNames.playing, this.playing); - toggleClass(this.elements.container, this.config.classNames.paused, this.paused); - toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); - - // Set state - Array.from(this.elements.buttons.play || []).forEach(target => { - Object.assign(target, { pressed: this.playing }); - target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config)); - }); - - // Only update controls on non timeupdate events - if (is.event(event) && event.type === 'timeupdate') { - return; - } - - // Toggle controls + // 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? + this.listeners.media(); + + // Don't setup interface if no support + if (!this.supported.ui) { + this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); + + // Restore native controls + ui.toggleNativeControls.call(this, true); + + // Bail + return; + } + + // Inject custom controls if not present + if (!is.element(this.elements.controls)) { + // Inject custom controls + controls.inject.call(this); + + // Re-attach control listeners + this.listeners.controls(); + } + + // Remove native controls + ui.toggleNativeControls.call(this); + + // Setup captions for HTML5 + if (this.isHTML5) { + captions.setup.call(this); + } + + // Reset volume + this.volume = null; + + // Reset mute state + this.muted = null; + + // Reset loop state + this.loop = null; + + // Reset quality setting + this.quality = null; + + // Reset speed + this.speed = null; + + // Reset volume display + controls.updateVolume.call(this); + + // Reset time display + controls.timeUpdate.call(this); + + // Update the UI + ui.checkPlaying.call(this); + + // Check for picture-in-picture support + toggleClass( + this.elements.container, + this.config.classNames.pip.supported, + support.pip && this.isHTML5 && this.isVideo, + ); + + // Check for airplay support + toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); + + // Add iOS class + toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); + + // Add touch class + toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); + + // Ready for API calls + this.ready = true; + + // Ready event at end of execution stack + setTimeout(() => { + triggerEvent.call(this, this.media, 'ready'); + }, 0); + + // Set the title + ui.setTitle.call(this); + + // Assure the poster image is set, if the property was added before the element was created + if (this.poster) { + ui.setPoster.call(this, this.poster, false).catch(() => {}); + } + + // Manually set the duration if user has overridden it. + // The event listeners for it doesn't get called if preload is disabled (#701) + if (this.config.duration) { + controls.durationUpdate.call(this); + } + }, + + // Setup aria attribute for play and iframe title + setTitle() { + // Find the current text + let label = i18n.get('play', this.config); + + // If there's a media title set, use that for the label + if (is.string(this.config.title) && !is.empty(this.config.title)) { + label += `, ${this.config.title}`; + } + + // If there's a play button, set label + 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 = getElement.call(this, 'iframe'); + + if (!is.element(iframe)) { + return; + } + + // Default to media type + const title = !is.empty(this.config.title) ? this.config.title : 'video'; + const format = i18n.get('frameTitle', this.config); + + iframe.setAttribute('title', format.replace('{title}', title)); + } + }, + + // Toggle poster + togglePoster(enable) { + toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); + }, + + // Set the poster image (async) + // Used internally for the poster setter, with the passive option forced to false + setPoster(poster, passive = true) { + // Don't override if call is passive + if (passive && this.poster) { + return Promise.reject(new Error('Poster already set')); + } + + // Set property synchronously to respect the call order + this.media.setAttribute('poster', poster); + + // HTML5 uses native poster attribute + if (this.isHTML5) { + return Promise.resolve(poster); + } + + // Wait until ui is ready + return ( + ready + .call(this) + // Load image + .then(() => loadImage(poster)) + .catch(err => { + // Hide poster on error unless it's been set by another call + if (poster === this.poster) { + ui.togglePoster.call(this, false); + } + // Rethrow + throw err; + }) + .then(() => { + // Prevent race conditions + if (poster !== this.poster) { + throw new Error('setPoster cancelled by later call to setPoster'); + } + }) + .then(() => { + Object.assign(this.elements.poster.style, { + backgroundImage: `url('${poster}')`, + // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) + backgroundSize: '', + }); + + ui.togglePoster.call(this, true); + + return poster; + }) + ); + }, + + // Check playing state + checkPlaying(event) { + // Class hooks + toggleClass(this.elements.container, this.config.classNames.playing, this.playing); + toggleClass(this.elements.container, this.config.classNames.paused, this.paused); + toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); + + // Set state + Array.from(this.elements.buttons.play || []).forEach(target => { + Object.assign(target, { pressed: this.playing }); + target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config)); + }); + + // Only update controls on non timeupdate events + if (is.event(event) && event.type === 'timeupdate') { + return; + } + + // Toggle controls + ui.toggleControls.call(this); + }, + + // 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( + () => { + // Update progress bar loading class state + toggleClass(this.elements.container, this.config.classNames.loading, this.loading); + + // Update controls visibility ui.toggleControls.call(this); - }, - - // 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( - () => { - // Update progress bar loading class state - toggleClass(this.elements.container, this.config.classNames.loading, this.loading); - - // Update controls visibility - ui.toggleControls.call(this); - }, - this.loading ? 250 : 0, - ); - }, - - // Toggle controls based on state and `force` argument - toggleControls(force) { - const { controls: controlsElement } = this.elements; - - if (controlsElement && this.config.hideControls) { - // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.) - const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); - - // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide - this.toggleControls( - Boolean( - force || - this.loading || - this.paused || - controlsElement.pressed || - controlsElement.hover || - recentTouchSeek, - ), - ); - } - }, + }, + this.loading ? 250 : 0, + ); + }, + + // Toggle controls based on state and `force` argument + toggleControls(force) { + const { controls: controlsElement } = this.elements; + + if (controlsElement && this.config.hideControls) { + // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.) + const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); + + // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide + this.toggleControls( + Boolean( + force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek, + ), + ); + } + }, }; export default ui; |