diff options
author | Sam Potts <me@sampotts.me> | 2017-11-04 14:25:28 +1100 |
---|---|---|
committer | Sam Potts <me@sampotts.me> | 2017-11-04 14:25:28 +1100 |
commit | 1cc2930dc0b81183bc47442f5ad9b5d8df94cc5f (patch) | |
tree | 349313769a5e3d786a51b45b0a5c849dc7e3211d /src/js/ui.js | |
parent | 3d50936b47fdd691816843de962d5699c3c8f596 (diff) | |
download | plyr-1cc2930dc0b81183bc47442f5ad9b5d8df94cc5f.tar.lz plyr-1cc2930dc0b81183bc47442f5ad9b5d8df94cc5f.tar.xz plyr-1cc2930dc0b81183bc47442f5ad9b5d8df94cc5f.zip |
ES6-ified
Diffstat (limited to 'src/js/ui.js')
-rw-r--r-- | src/js/ui.js | 381 |
1 files changed, 381 insertions, 0 deletions
diff --git a/src/js/ui.js b/src/js/ui.js new file mode 100644 index 00000000..2d612cdb --- /dev/null +++ b/src/js/ui.js @@ -0,0 +1,381 @@ +// ========================================================================== +// Plyr UI +// ========================================================================== + +import utils from './utils'; +import captions from './captions'; +import controls from './controls'; +import fullscreen from './fullscreen'; +import listeners from './listeners'; +import storage from './storage'; + +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) { + 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.warn(`Basic support only for ${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.htmlElement(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.htmlElement(this.elements.controls)) { + return; + } + + // Remove native controls + ui.toggleNativeControls.call(this); + + // Setup fullscreen + fullscreen.setup.call(this); + + // Captions + captions.setup.call(this); + + // Set volume + this.volume = null; + ui.updateVolume.call(this); + + // Set playback speed + this.speed = null; + + // Set loop + // this.setLoop(); + + // Reset time display + ui.timeUpdate.call(this); + + // Update the UI + ui.checkPlaying.call(this); + + this.ready = true; + + // Ready event at end of execution stack + utils.dispatchEvent.call(this, this.media, 'ready'); + + // Autoplay + if (this.config.autoplay) { + this.play(); + } + }, + + // Show the duration on metadataloaded + displayDuration() { + if (!this.supported.ui) { + return; + } + + // If there's only one time display, display duration there + if (!this.elements.display.duration && this.config.displayDuration && this.media.paused) { + ui.updateTimeDisplay.call(this, this.duration, this.elements.display.currentTime); + } + + // If there's a duration element, update content + if (this.elements.display.duration) { + ui.updateTimeDisplay.call(this, this.duration, this.elements.display.duration); + } + + // Update the tooltip (if visible) + ui.updateSeekTooltip.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 (this.supported.ui) { + if (utils.is.htmlElement(this.elements.buttons.play)) { + this.elements.buttons.play.setAttribute('aria-label', label); + } + if (utils.is.htmlElement(this.elements.buttons.playLarge)) { + this.elements.buttons.playLarge.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.htmlElement(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() { + utils.toggleClass(this.elements.container, this.config.classNames.playing, !this.media.paused); + + utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.media.paused); + + this.toggleControls(this.media.paused); + }, + + // Update volume UI and storage + updateVolume() { + // Update the <input type="range"> if present + if (this.supported.ui) { + const value = this.media.muted ? 0 : this.media.volume; + + if (this.elements.inputs.volume) { + ui.setRange.call(this, this.elements.inputs.volume, value); + } + } + + // Update the volume in storage + storage.set.call(this, { + volume: this.media.volume, + }); + + // Toggle class if muted + utils.toggleClass(this.elements.container, this.config.classNames.muted, this.media.muted); + + // Update checkbox for mute state + if (this.supported.ui && this.elements.buttons.mute) { + utils.toggleState(this.elements.buttons.mute, this.media.muted); + } + }, + + // Check if media is loading + checkLoading(event) { + this.loading = event.type === 'waiting'; + + // 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); + }, + + // Update seek value and lower fill + setRange(target, value) { + if (!utils.is.htmlElement(target)) { + return; + } + + target.value = value; + + // Webkit range fill + controls.updateRangeFill.call(this, target); + }, + + // Set <progress> value + setProgress(target, input) { + // Default to 0 + const value = !utils.is.undefined(input) ? input : 0; + const progress = !utils.is.undefined(target) ? target : this.elements.display.buffer; + + // Update value and label + if (utils.is.htmlElement(progress)) { + progress.value = value; + + // Update text label inside + const label = progress.getElementsByTagName('span')[0]; + if (utils.is.htmlElement(label)) { + label.childNodes[0].nodeValue = value; + } + } + }, + + // Update <progress> elements + updateProgress(event) { + if (!this.supported.ui) { + 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(value, element) { + // Bail if there's no duration display + if (!utils.is.htmlElement(element)) { + return null; + } + + // Fallback to 0 + const time = !Number.isNaN(value) ? value : 0; + + let secs = parseInt(time % 60, 10); + let mins = parseInt((time / 60) % 60, 10); + const hours = parseInt((time / 60 / 60) % 60, 10); + + // Do we need to display hours? + const displayHours = parseInt((this.duration / 60 / 60) % 60, 10) > 0; + + // Ensure it's two digits. For example, 03 rather than 3. + secs = `0${secs}`.slice(-2); + mins = `0${mins}`.slice(-2); + + // Generate display + const display = `${(displayHours ? `${hours}:` : '') + mins}:${secs}`; + + // Render + element.textContent = display; + + // Return for looping + return display; + }, + + // Handle time change event + timeUpdate(event) { + // Duration + ui.updateTimeDisplay.call(this, this.currentTime, this.elements.display.currentTime); + + // Ignore updates while seeking + if (event && event.type === 'timeupdate' && this.media.seeking) { + return; + } + + // Playing progress + ui.updateProgress.call(this, event); + }, + + // 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 + const clientRect = this.elements.inputs.seek.getBoundingClientRect(); + let percent = 0; + 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'); + } + }, +}; + +export default ui; |