ES6-ified
This commit is contained in:
		
							
								
								
									
										212
									
								
								src/js/captions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								src/js/captions.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,212 @@ | ||||
| // ========================================================================== | ||||
| // Plyr Captions | ||||
| // ========================================================================== | ||||
|  | ||||
| import support from './support'; | ||||
| import utils from './utils'; | ||||
| import controls from './controls'; | ||||
|  | ||||
| const captions = { | ||||
|     // Setup captions | ||||
|     setup() { | ||||
|         // Requires UI support | ||||
|         if (!this.supported.ui) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Set default language if not set | ||||
|         if (!utils.is.empty(this.storage.language)) { | ||||
|             this.captions.language = this.storage.language; | ||||
|         } else if (utils.is.empty(this.captions.language)) { | ||||
|             this.captions.language = this.config.captions.language.toLowerCase(); | ||||
|         } | ||||
|  | ||||
|         // Set captions enabled state if not set | ||||
|         if (!utils.is.boolean(this.captions.enabled)) { | ||||
|             if (!utils.is.empty(this.storage.language)) { | ||||
|                 this.captions.enabled = this.storage.captions; | ||||
|             } else { | ||||
|                 this.captions.enabled = this.config.captions.active; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Only Vimeo and HTML5 video supported at this point | ||||
|         if (!['video', 'vimeo'].includes(this.type) || (this.type === 'video' && !support.textTracks)) { | ||||
|             this.captions.tracks = null; | ||||
|  | ||||
|             // Clear menu and hide | ||||
|             if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) { | ||||
|                 controls.setCaptionsMenu.call(this); | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Inject the container | ||||
|         if (!utils.is.htmlElement(this.elements.captions)) { | ||||
|             this.elements.captions = utils.createElement( | ||||
|                 'div', | ||||
|                 utils.getAttributesFromSelector(this.config.selectors.captions) | ||||
|             ); | ||||
|             utils.insertAfter(this.elements.captions, this.elements.wrapper); | ||||
|         } | ||||
|  | ||||
|         // Get tracks from HTML5 | ||||
|         if (this.type === 'video') { | ||||
|             this.captions.tracks = this.media.textTracks; | ||||
|         } | ||||
|  | ||||
|         // Set the class hook | ||||
|         utils.toggleClass( | ||||
|             this.elements.container, | ||||
|             this.config.classNames.captions.enabled, | ||||
|             !utils.is.empty(this.captions.tracks) | ||||
|         ); | ||||
|  | ||||
|         // If no caption file exists, hide container for caption text | ||||
|         if (utils.is.empty(this.captions.tracks)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Enable UI | ||||
|         captions.show.call(this); | ||||
|  | ||||
|         // Get a track | ||||
|         const setCurrentTrack = () => { | ||||
|             // Reset by default | ||||
|             this.captions.currentTrack = null; | ||||
|  | ||||
|             // Filter doesn't seem to work for a TextTrackList :-( | ||||
|             Array.from(this.captions.tracks).forEach(track => { | ||||
|                 if (track.language === this.captions.language.toLowerCase()) { | ||||
|                     this.captions.currentTrack = track; | ||||
|                 } | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         // Get current track | ||||
|         setCurrentTrack(); | ||||
|  | ||||
|         // If we couldn't get the requested language, revert to default | ||||
|         if (!utils.is.track(this.captions.currentTrack)) { | ||||
|             const { language } = this.config.captions; | ||||
|  | ||||
|             // Reset to default | ||||
|             // We don't update user storage as the selected language could become available | ||||
|             this.captions.language = language; | ||||
|  | ||||
|             // Get fallback track | ||||
|             setCurrentTrack(); | ||||
|  | ||||
|             // If no match, disable captions | ||||
|             if (!utils.is.track(this.captions.currentTrack)) { | ||||
|                 this.toggleCaptions(false); | ||||
|             } | ||||
|  | ||||
|             controls.updateSetting.call(this, 'captions'); | ||||
|         } | ||||
|  | ||||
|         // Setup HTML5 track rendering | ||||
|         if (this.type === 'video') { | ||||
|             // Turn off native caption rendering to avoid double captions | ||||
|             Array.from(this.captions.tracks).forEach(track => { | ||||
|                 // Remove previous bindings (if we've changed source or language) | ||||
|                 utils.off(track, 'cuechange', event => captions.setCue.call(this, event)); | ||||
|  | ||||
|                 // Hide captions | ||||
|                 track.mode = 'hidden'; | ||||
|             }); | ||||
|  | ||||
|             // Check if suported kind | ||||
|             const supported = | ||||
|                 this.captions.currentTrack && ['captions', 'subtitles'].includes(this.captions.currentTrack.kind); | ||||
|  | ||||
|             if (utils.is.track(this.captions.currentTrack) && supported) { | ||||
|                 utils.on(this.captions.currentTrack, 'cuechange', event => captions.setCue.call(this, event)); | ||||
|  | ||||
|                 // If we change the active track while a cue is already displayed we need to update it | ||||
|                 if (this.captions.currentTrack.activeCues && this.captions.currentTrack.activeCues.length > 0) { | ||||
|                     controls.setCue.call(this, this.captions.currentTrack); | ||||
|                 } | ||||
|             } | ||||
|         } else if (this.type === 'vimeo' && this.captions.active) { | ||||
|             this.embed.enableTextTrack(this.captions.language); | ||||
|         } | ||||
|  | ||||
|         // Set available languages in list | ||||
|         if (this.config.controls.includes('settings') && this.config.settings.includes('captions')) { | ||||
|             controls.setCaptionsMenu.call(this); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Display active caption if it contains text | ||||
|     setCue(input) { | ||||
|         // Get the track from the event if needed | ||||
|         const track = utils.is.event(input) ? input.target : input; | ||||
|         const active = track.activeCues[0]; | ||||
|  | ||||
|         // Display a cue, if there is one | ||||
|         if (utils.is.cue(active)) { | ||||
|             captions.set.call(this, active.getCueAsHTML()); | ||||
|         } else { | ||||
|             captions.set.call(this); | ||||
|         } | ||||
|  | ||||
|         utils.dispatchEvent.call(this, this.media, 'cuechange'); | ||||
|     }, | ||||
|  | ||||
|     // Set the current caption | ||||
|     set(input) { | ||||
|         // Requires UI | ||||
|         if (!this.supported.ui) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (utils.is.htmlElement(this.elements.captions)) { | ||||
|             const content = utils.createElement('span'); | ||||
|  | ||||
|             // Empty the container | ||||
|             utils.emptyElement(this.elements.captions); | ||||
|  | ||||
|             // Default to empty | ||||
|             const caption = !utils.is.undefined(input) ? input : ''; | ||||
|  | ||||
|             // Set the span content | ||||
|             if (utils.is.string(caption)) { | ||||
|                 content.textContent = caption.trim(); | ||||
|             } else { | ||||
|                 content.appendChild(caption); | ||||
|             } | ||||
|  | ||||
|             // Set new caption text | ||||
|             this.elements.captions.appendChild(content); | ||||
|         } else { | ||||
|             this.warn('No captions element to render to'); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Display captions container and button (for initialization) | ||||
|     show() { | ||||
|         // If there's no caption toggle, bail | ||||
|         if (!this.elements.buttons.captions) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Try to load the value from storage | ||||
|         let active = this.storage.captions; | ||||
|  | ||||
|         // Otherwise fall back to the default config | ||||
|         if (!utils.is.boolean(active)) { | ||||
|             ({ active } = this.captions); | ||||
|         } else { | ||||
|             this.captions.active = active; | ||||
|         } | ||||
|  | ||||
|         if (active) { | ||||
|             utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true); | ||||
|             utils.toggleState(this.elements.buttons.captions, true); | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default captions; | ||||
							
								
								
									
										1177
									
								
								src/js/controls.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1177
									
								
								src/js/controls.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										301
									
								
								src/js/defaults.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								src/js/defaults.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,301 @@ | ||||
| // Default config | ||||
| const defaults = { | ||||
|     // Disable | ||||
|     enabled: true, | ||||
|  | ||||
|     // Custom media title | ||||
|     title: '', | ||||
|  | ||||
|     // Logging to console | ||||
|     debug: false, | ||||
|  | ||||
|     // Auto play (if supported) | ||||
|     autoplay: false, | ||||
|  | ||||
|     // Default time to skip when rewind/fast forward | ||||
|     seekTime: 10, | ||||
|  | ||||
|     // Default volume | ||||
|     volume: 1, | ||||
|     muted: false, | ||||
|  | ||||
|     // Display the media duration | ||||
|     displayDuration: true, | ||||
|  | ||||
|     // Click video to play | ||||
|     clickToPlay: true, | ||||
|  | ||||
|     // Auto hide the controls | ||||
|     hideControls: true, | ||||
|  | ||||
|     // Revert to poster on finish (HTML5 - will cause reload) | ||||
|     showPosterOnEnd: false, | ||||
|  | ||||
|     // Disable the standard context menu | ||||
|     disableContextMenu: true, | ||||
|  | ||||
|     // Sprite (for icons) | ||||
|     loadSprite: true, | ||||
|     iconPrefix: 'plyr', | ||||
|     iconUrl: 'https://cdn.plyr.io/2.0.10/plyr.svg', | ||||
|  | ||||
|     // Blank video (used to prevent errors on source change) | ||||
|     blankVideo: 'https://cdn.plyr.io/static/blank.mp4', | ||||
|  | ||||
|     // Pass a custom duration | ||||
|     duration: null, | ||||
|  | ||||
|     // Quality default | ||||
|     quality: { | ||||
|         default: 'default', | ||||
|         options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'], | ||||
|     }, | ||||
|  | ||||
|     // Set loops | ||||
|     loop: { | ||||
|         active: false, | ||||
|         start: null, | ||||
|         end: null, | ||||
|     }, | ||||
|  | ||||
|     // Speed default and options to display | ||||
|     speed: { | ||||
|         default: 1, | ||||
|         options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], | ||||
|     }, | ||||
|  | ||||
|     // Keyboard shortcut settings | ||||
|     keyboard: { | ||||
|         focused: true, | ||||
|         global: false, | ||||
|     }, | ||||
|  | ||||
|     // Display tooltips | ||||
|     tooltips: { | ||||
|         controls: false, | ||||
|         seek: true, | ||||
|     }, | ||||
|  | ||||
|     // Captions settings | ||||
|     captions: { | ||||
|         active: false, | ||||
|         language: window.navigator.language.split('-')[0], | ||||
|     }, | ||||
|  | ||||
|     // Fullscreen settings | ||||
|     fullscreen: { | ||||
|         enabled: true, // Allow fullscreen? | ||||
|         fallback: true, // Fallback for vintage browsers | ||||
|     }, | ||||
|  | ||||
|     // Local storage | ||||
|     storage: { | ||||
|         enabled: true, | ||||
|         key: 'plyr', | ||||
|     }, | ||||
|  | ||||
|     // Default controls | ||||
|     controls: [ | ||||
|         'play-large', | ||||
|         'play', | ||||
|         'progress', | ||||
|         'current-time', | ||||
|         'mute', | ||||
|         'volume', | ||||
|         'captions', | ||||
|         'settings', | ||||
|         'pip', | ||||
|         'airplay', | ||||
|         'fullscreen', | ||||
|     ], | ||||
|     settings: ['captions', 'quality', 'speed', 'loop'], | ||||
|  | ||||
|     // Localisation | ||||
|     i18n: { | ||||
|         restart: 'Restart', | ||||
|         rewind: 'Rewind {seektime} secs', | ||||
|         play: 'Play', | ||||
|         pause: 'Pause', | ||||
|         forward: 'Forward {seektime} secs', | ||||
|         seek: 'Seek', | ||||
|         played: 'Played', | ||||
|         buffered: 'Buffered', | ||||
|         currentTime: 'Current time', | ||||
|         duration: 'Duration', | ||||
|         volume: 'Volume', | ||||
|         toggleMute: 'Toggle Mute', | ||||
|         toggleCaptions: 'Toggle Captions', | ||||
|         toggleFullscreen: 'Toggle Fullscreen', | ||||
|         frameTitle: 'Player for {title}', | ||||
|         captions: 'Captions', | ||||
|         settings: 'Settings', | ||||
|         speed: 'Speed', | ||||
|         quality: 'Quality', | ||||
|         loop: 'Loop', | ||||
|         start: 'Start', | ||||
|         end: 'End', | ||||
|         all: 'All', | ||||
|         reset: 'Reset', | ||||
|         none: 'None', | ||||
|         disabled: 'Disabled', | ||||
|     }, | ||||
|  | ||||
|     // URLs | ||||
|     urls: { | ||||
|         vimeo: { | ||||
|             api: 'https://player.vimeo.com/api/player.js', | ||||
|         }, | ||||
|         youtube: { | ||||
|             api: 'https://www.youtube.com/iframe_api', | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     // Custom control listeners | ||||
|     listeners: { | ||||
|         seek: null, | ||||
|         play: null, | ||||
|         pause: null, | ||||
|         restart: null, | ||||
|         rewind: null, | ||||
|         forward: null, | ||||
|         mute: null, | ||||
|         volume: null, | ||||
|         captions: null, | ||||
|         fullscreen: null, | ||||
|         pip: null, | ||||
|         airplay: null, | ||||
|         speed: null, | ||||
|         quality: null, | ||||
|         loop: null, | ||||
|         language: null, | ||||
|     }, | ||||
|  | ||||
|     // Events to watch and bubble | ||||
|     events: [ | ||||
|         // Events to watch on HTML5 media elements and bubble | ||||
|         // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events | ||||
|         'ended', | ||||
|         'progress', | ||||
|         'stalled', | ||||
|         'playing', | ||||
|         'waiting', | ||||
|         'canplay', | ||||
|         'canplaythrough', | ||||
|         'loadstart', | ||||
|         'loadeddata', | ||||
|         'loadedmetadata', | ||||
|         'timeupdate', | ||||
|         'volumechange', | ||||
|         'play', | ||||
|         'pause', | ||||
|         'error', | ||||
|         'seeking', | ||||
|         'seeked', | ||||
|         'emptied', | ||||
|         'ratechange', | ||||
|         'cuechange', | ||||
|  | ||||
|         // Custom events | ||||
|         'enterfullscreen', | ||||
|         'exitfullscreen', | ||||
|         'captionsenabled', | ||||
|         'captionsdisabled', | ||||
|         'captionchange', | ||||
|         'controlshidden', | ||||
|         'controlsshown', | ||||
|         'ready', | ||||
|  | ||||
|         // YouTube | ||||
|         'statechange', | ||||
|         'qualitychange', | ||||
|         'qualityrequested', | ||||
|     ], | ||||
|  | ||||
|     // Selectors | ||||
|     // Change these to match your template if using custom HTML | ||||
|     selectors: { | ||||
|         editable: 'input, textarea, select, [contenteditable]', | ||||
|         container: '.plyr', | ||||
|         controls: { | ||||
|             container: null, | ||||
|             wrapper: '.plyr__controls', | ||||
|         }, | ||||
|         labels: '[data-plyr]', | ||||
|         buttons: { | ||||
|             play: '[data-plyr="play"]', | ||||
|             pause: '[data-plyr="pause"]', | ||||
|             restart: '[data-plyr="restart"]', | ||||
|             rewind: '[data-plyr="rewind"]', | ||||
|             forward: '[data-plyr="fast-forward"]', | ||||
|             mute: '[data-plyr="mute"]', | ||||
|             captions: '[data-plyr="captions"]', | ||||
|             fullscreen: '[data-plyr="fullscreen"]', | ||||
|             pip: '[data-plyr="pip"]', | ||||
|             airplay: '[data-plyr="airplay"]', | ||||
|             settings: '[data-plyr="settings"]', | ||||
|             loop: '[data-plyr="loop"]', | ||||
|         }, | ||||
|         inputs: { | ||||
|             seek: '[data-plyr="seek"]', | ||||
|             volume: '[data-plyr="volume"]', | ||||
|             speed: '[data-plyr="speed"]', | ||||
|             language: '[data-plyr="language"]', | ||||
|             quality: '[data-plyr="quality"]', | ||||
|         }, | ||||
|         display: { | ||||
|             currentTime: '.plyr__time--current', | ||||
|             duration: '.plyr__time--duration', | ||||
|             buffer: '.plyr__progress--buffer', | ||||
|             played: '.plyr__progress--played', | ||||
|             loop: '.plyr__progress--loop', | ||||
|             volume: '.plyr__volume--display', | ||||
|         }, | ||||
|         progress: '.plyr__progress', | ||||
|         captions: '.plyr__captions', | ||||
|         menu: { | ||||
|             quality: '.js-plyr__menu__list--quality', | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     // Class hooks added to the player in different states | ||||
|     classNames: { | ||||
|         video: 'plyr__video-wrapper', | ||||
|         embed: 'plyr__video-embed', | ||||
|         control: 'plyr__control', | ||||
|         type: 'plyr--{0}', | ||||
|         stopped: 'plyr--stopped', | ||||
|         playing: 'plyr--playing', | ||||
|         muted: 'plyr--muted', | ||||
|         loading: 'plyr--loading', | ||||
|         hover: 'plyr--hover', | ||||
|         tooltip: 'plyr__tooltip', | ||||
|         hidden: 'plyr__sr-only', | ||||
|         hideControls: 'plyr--hide-controls', | ||||
|         isIos: 'plyr--is-ios', | ||||
|         isTouch: 'plyr--is-touch', | ||||
|         uiSupported: 'plyr--full-ui', | ||||
|         menu: { | ||||
|             value: 'plyr__menu__value', | ||||
|             badge: 'plyr__badge', | ||||
|         }, | ||||
|         captions: { | ||||
|             enabled: 'plyr--captions-enabled', | ||||
|             active: 'plyr--captions-active', | ||||
|         }, | ||||
|         fullscreen: { | ||||
|             enabled: 'plyr--fullscreen-enabled', | ||||
|             fallback: 'plyr--fullscreen-fallback', | ||||
|         }, | ||||
|         pip: { | ||||
|             supported: 'plyr--pip-supported', | ||||
|             active: 'plyr--pip-active', | ||||
|         }, | ||||
|         airplay: { | ||||
|             supported: 'plyr--airplay-supported', | ||||
|             active: 'plyr--airplay-active', | ||||
|         }, | ||||
|         tabFocus: 'tab-focus', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default defaults; | ||||
							
								
								
									
										129
									
								
								src/js/fullscreen.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/js/fullscreen.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | ||||
| // ========================================================================== | ||||
| // Plyr fullscreen API | ||||
| // ========================================================================== | ||||
|  | ||||
| import utils from './utils'; | ||||
|  | ||||
| // Determine the prefix | ||||
| const prefix = (() => { | ||||
|     let value = false; | ||||
|  | ||||
|     if (utils.is.function(document.cancelFullScreen)) { | ||||
|         value = ''; | ||||
|     } else { | ||||
|         // Check for fullscreen support by vendor prefix | ||||
|         ['webkit', 'o', 'moz', 'ms', 'khtml'].some(pre => { | ||||
|             if (utils.is.function(document[`${pre}CancelFullScreen`])) { | ||||
|                 value = pre; | ||||
|                 return true; | ||||
|             } else if (utils.is.function(document.msExitFullscreen) && document.msFullscreenEnabled) { | ||||
|                 // Special case for MS (when isn't it?) | ||||
|                 value = 'ms'; | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return value; | ||||
| })(); | ||||
|  | ||||
| // Fullscreen API | ||||
| const fullscreen = { | ||||
|     // Get the prefix | ||||
|     prefix, | ||||
|  | ||||
|     // Check if we can use it | ||||
|     enabled: | ||||
|         document.fullscreenEnabled || | ||||
|         document.webkitFullscreenEnabled || | ||||
|         document.mozFullScreenEnabled || | ||||
|         document.msFullscreenEnabled, | ||||
|  | ||||
|     // Yet again Microsoft awesomeness, | ||||
|     // Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes | ||||
|     eventType: prefix === 'ms' ? 'MSFullscreenChange' : `${prefix}fullscreenchange`, | ||||
|  | ||||
|     // Is an element fullscreen | ||||
|     isFullScreen(element) { | ||||
|         if (!fullscreen.enabled) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         const target = utils.is.undefined(element) ? document.body : element; | ||||
|  | ||||
|         switch (prefix) { | ||||
|             case '': | ||||
|                 return document.fullscreenElement === target; | ||||
|  | ||||
|             case 'moz': | ||||
|                 return document.mozFullScreenElement === target; | ||||
|  | ||||
|             default: | ||||
|                 return document[`${prefix}FullscreenElement`] === target; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Make an element fullscreen | ||||
|     requestFullScreen(element) { | ||||
|         if (!fullscreen.enabled) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         const target = utils.is.undefined(element) ? document.body : element; | ||||
|  | ||||
|         return !prefix.length | ||||
|             ? target.requestFullScreen() | ||||
|             : target[prefix + (prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')](); | ||||
|     }, | ||||
|  | ||||
|     // Bail from fullscreen | ||||
|     cancelFullScreen() { | ||||
|         if (!fullscreen.enabled) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return !prefix.length | ||||
|             ? document.cancelFullScreen() | ||||
|             : document[prefix + (prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')](); | ||||
|     }, | ||||
|  | ||||
|     // Get the current element | ||||
|     element() { | ||||
|         if (!fullscreen.enabled) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return !prefix.length ? document.fullscreenElement : document[`${prefix}FullscreenElement`]; | ||||
|     }, | ||||
|  | ||||
|     // Setup fullscreen | ||||
|     setup() { | ||||
|         if (!this.supported.ui || this.type === 'audio' || !this.config.fullscreen.enabled) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Check for native support | ||||
|         const nativeSupport = fullscreen.enabled; | ||||
|  | ||||
|         if (nativeSupport || (this.config.fullscreen.fallback && !utils.inFrame())) { | ||||
|             this.log(`${nativeSupport ? 'Native' : 'Fallback'} fullscreen enabled`); | ||||
|  | ||||
|             // Add styling hook to show button | ||||
|             utils.toggleClass(this.elements.container, this.config.classNames.fullscreen.enabled, true); | ||||
|         } else { | ||||
|             this.log('Fullscreen not supported and fallback disabled'); | ||||
|         } | ||||
|  | ||||
|         // Toggle state | ||||
|         if (this.elements.buttons && this.elements.buttons.fullscreen) { | ||||
|             utils.toggleState(this.elements.buttons.fullscreen, false); | ||||
|         } | ||||
|  | ||||
|         // Trap focus in container | ||||
|         utils.trapFocus.call(this); | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default fullscreen; | ||||
							
								
								
									
										569
									
								
								src/js/listeners.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										569
									
								
								src/js/listeners.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,569 @@ | ||||
| // ========================================================================== | ||||
| // Plyr Event Listeners | ||||
| // ========================================================================== | ||||
|  | ||||
| import support from './support'; | ||||
| import utils from './utils'; | ||||
| import controls from './controls'; | ||||
| import fullscreen from './fullscreen'; | ||||
| import storage from './storage'; | ||||
| import ui from './ui'; | ||||
|  | ||||
| const listeners = { | ||||
|     // Listen for media events | ||||
|     media() { | ||||
|         // Time change on media | ||||
|         utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event)); | ||||
|  | ||||
|         // Display duration | ||||
|         utils.on(this.media, 'durationchange loadedmetadata', event => ui.displayDuration.call(this, event)); | ||||
|  | ||||
|         // Handle the media finishing | ||||
|         utils.on(this.media, 'ended', () => { | ||||
|             // Show poster on end | ||||
|             if (this.type === 'video' && this.config.showPosterOnEnd) { | ||||
|                 // Restart | ||||
|                 this.restart(); | ||||
|  | ||||
|                 // Re-load media | ||||
|                 this.media.load(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Check for buffer progress | ||||
|         utils.on(this.media, 'progress playing', event => ui.updateProgress.call(this, event)); | ||||
|  | ||||
|         // Handle native mute | ||||
|         utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event)); | ||||
|  | ||||
|         // Handle native play/pause | ||||
|         utils.on(this.media, 'play pause ended', event => ui.checkPlaying.call(this, event)); | ||||
|  | ||||
|         // Loading | ||||
|         utils.on(this.media, 'waiting canplay seeked', event => ui.checkLoading.call(this, event)); | ||||
|  | ||||
|         // Click video | ||||
|         if (this.supported.ui && this.config.clickToPlay && this.type !== 'audio') { | ||||
|             // Re-fetch the wrapper | ||||
|             const wrapper = utils.getElement.call(this, `.${this.config.classNames.video}`); | ||||
|  | ||||
|             // Bail if there's no wrapper (this should never happen) | ||||
|             if (!wrapper) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Set cursor | ||||
|             wrapper.style.cursor = 'pointer'; | ||||
|  | ||||
|             // On click play, pause ore restart | ||||
|             utils.on(wrapper, 'click', () => { | ||||
|                 // Touch devices will just show controls (if we're hiding controls) | ||||
|                 if (this.config.hideControls && support.touch && !this.media.paused) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (this.media.paused) { | ||||
|                     this.play(); | ||||
|                 } else if (this.media.ended) { | ||||
|                     this.restart(); | ||||
|                     this.play(); | ||||
|                 } else { | ||||
|                     this.pause(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // Disable right click | ||||
|         if (this.config.disableContextMenu) { | ||||
|             utils.on( | ||||
|                 this.media, | ||||
|                 'contextmenu', | ||||
|                 event => { | ||||
|                     event.preventDefault(); | ||||
|                 }, | ||||
|                 false | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // Speed change | ||||
|         utils.on(this.media, 'ratechange', () => { | ||||
|             // Update UI | ||||
|             controls.updateSetting.call(this, 'speed'); | ||||
|  | ||||
|             // Save speed to localStorage | ||||
|             storage.set.call(this, { | ||||
|                 speed: this.speed, | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // Quality change | ||||
|         utils.on(this.media, 'qualitychange', () => { | ||||
|             // Update UI | ||||
|             controls.updateSetting.call(this, 'quality'); | ||||
|  | ||||
|             // Save speed to localStorage | ||||
|             storage.set.call(this, { | ||||
|                 quality: this.quality, | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // Caption language change | ||||
|         utils.on(this.media, 'captionchange', () => { | ||||
|             // Save speed to localStorage | ||||
|             storage.set.call(this, { | ||||
|                 language: this.captions.language, | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // Captions toggle | ||||
|         utils.on(this.media, 'captionsenabled captionsdisabled', () => { | ||||
|             // Update UI | ||||
|             controls.updateSetting.call(this, 'captions'); | ||||
|  | ||||
|             // Save speed to localStorage | ||||
|             storage.set.call(this, { | ||||
|                 captions: this.captions.enabled, | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // Proxy events to container | ||||
|         // Bubble up key events for Edge | ||||
|         utils.on(this.media, this.config.events.concat(['keyup', 'keydown']).join(' '), event => { | ||||
|             utils.dispatchEvent.call(this, this.elements.container, event.type, true); | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     // Listen for control events | ||||
|     controls() { | ||||
|         // IE doesn't support input event, so we fallback to change | ||||
|         const inputEvent = this.browser.isIE ? 'change' : 'input'; | ||||
|         let last = null; | ||||
|  | ||||
|         // Click play/pause helper | ||||
|         const togglePlay = () => { | ||||
|             const play = this.togglePlay(); | ||||
|  | ||||
|             // Determine which buttons | ||||
|             const target = this.elements.buttons[play ? 'pause' : 'play']; | ||||
|  | ||||
|             // Transfer focus | ||||
|             if (utils.is.htmlElement(target)) { | ||||
|                 target.focus(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         // Get the key code for an event | ||||
|         function getKeyCode(event) { | ||||
|             return event.keyCode ? event.keyCode : event.which; | ||||
|         } | ||||
|  | ||||
|         function handleKey(event) { | ||||
|             const code = getKeyCode(event); | ||||
|             const pressed = event.type === 'keydown'; | ||||
|             const held = pressed && code === last; | ||||
|  | ||||
|             // If the event is bubbled from the media element | ||||
|             // Firefox doesn't get the keycode for whatever reason | ||||
|             if (!utils.is.number(code)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Seek by the number keys | ||||
|             function seekByKey() { | ||||
|                 // Divide the max duration into 10th's and times by the number value | ||||
|                 this.currentTime = this.duration / 10 * (code - 48); | ||||
|             } | ||||
|  | ||||
|             // Handle the key on keydown | ||||
|             // Reset on keyup | ||||
|             if (pressed) { | ||||
|                 // Which keycodes should we prevent default | ||||
|                 const preventDefault = [ | ||||
|                     48, | ||||
|                     49, | ||||
|                     50, | ||||
|                     51, | ||||
|                     52, | ||||
|                     53, | ||||
|                     54, | ||||
|                     56, | ||||
|                     57, | ||||
|                     32, | ||||
|                     75, | ||||
|                     38, | ||||
|                     40, | ||||
|                     77, | ||||
|                     39, | ||||
|                     37, | ||||
|                     70, | ||||
|                     67, | ||||
|                     73, | ||||
|                     76, | ||||
|                     79, | ||||
|                 ]; | ||||
|                 const checkFocus = [38, 40]; | ||||
|  | ||||
|                 if (checkFocus.includes(code)) { | ||||
|                     const focused = utils.getFocusElement(); | ||||
|  | ||||
|                     if (utils.is.htmlElement(focused) && utils.getFocusElement().type === 'radio') { | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // If the code is found prevent default (e.g. prevent scrolling for arrows) | ||||
|                 if (preventDefault.includes(code)) { | ||||
|                     event.preventDefault(); | ||||
|                     event.stopPropagation(); | ||||
|                 } | ||||
|  | ||||
|                 switch (code) { | ||||
|                     case 48: | ||||
|                     case 49: | ||||
|                     case 50: | ||||
|                     case 51: | ||||
|                     case 52: | ||||
|                     case 53: | ||||
|                     case 54: | ||||
|                     case 55: | ||||
|                     case 56: | ||||
|                     case 57: | ||||
|                         // 0-9 | ||||
|                         if (!held) { | ||||
|                             seekByKey(); | ||||
|                         } | ||||
|                         break; | ||||
|  | ||||
|                     case 32: | ||||
|                     case 75: | ||||
|                         // Space and K key | ||||
|                         if (!held) { | ||||
|                             togglePlay(); | ||||
|                         } | ||||
|                         break; | ||||
|  | ||||
|                     case 38: | ||||
|                         // Arrow up | ||||
|                         this.increaseVolume(0.1); | ||||
|                         break; | ||||
|  | ||||
|                     case 40: | ||||
|                         // Arrow down | ||||
|                         this.decreaseVolume(0.1); | ||||
|                         break; | ||||
|  | ||||
|                     case 77: | ||||
|                         // M key | ||||
|                         if (!held) { | ||||
|                             this.toggleMute(); | ||||
|                         } | ||||
|                         break; | ||||
|  | ||||
|                     case 39: | ||||
|                         // Arrow forward | ||||
|                         this.forward(); | ||||
|                         break; | ||||
|  | ||||
|                     case 37: | ||||
|                         // Arrow back | ||||
|                         this.rewind(); | ||||
|                         break; | ||||
|  | ||||
|                     case 70: | ||||
|                         // F key | ||||
|                         this.toggleFullscreen(); | ||||
|                         break; | ||||
|  | ||||
|                     case 67: | ||||
|                         // C key | ||||
|                         if (!held) { | ||||
|                             this.toggleCaptions(); | ||||
|                         } | ||||
|                         break; | ||||
|  | ||||
|                     case 73: | ||||
|                         this.setLoop('start'); | ||||
|                         break; | ||||
|  | ||||
|                     case 76: | ||||
|                         this.setLoop(); | ||||
|                         break; | ||||
|  | ||||
|                     case 79: | ||||
|                         this.setLoop('end'); | ||||
|                         break; | ||||
|  | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
|                 // Escape is handle natively when in full screen | ||||
|                 // So we only need to worry about non native | ||||
|                 if (!fullscreen.enabled && this.fullscreen.active && code === 27) { | ||||
|                     this.toggleFullscreen(); | ||||
|                 } | ||||
|  | ||||
|                 // Store last code for next cycle | ||||
|                 last = code; | ||||
|             } else { | ||||
|                 last = null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Keyboard shortcuts | ||||
|         if (this.config.keyboard.focused) { | ||||
|             // Handle global presses | ||||
|             if (this.config.keyboard.global) { | ||||
|                 utils.on( | ||||
|                     window, | ||||
|                     'keydown keyup', | ||||
|                     event => { | ||||
|                         const code = getKeyCode(event); | ||||
|                         const focused = utils.getFocusElement(); | ||||
|                         const allowed = [48, 49, 50, 51, 52, 53, 54, 56, 57, 75, 77, 70, 67, 73, 76, 79]; | ||||
|  | ||||
|                         // Only handle global key press if key is in the allowed keys | ||||
|                         // and if the focused element is not editable (e.g. text input) | ||||
|                         // and any that accept key input http://webaim.org/techniques/keyboard/ | ||||
|                         if ( | ||||
|                             allowed.includes(code) && | ||||
|                             (!utils.is.htmlElement(focused) || !utils.matches(focused, this.config.selectors.editable)) | ||||
|                         ) { | ||||
|                             handleKey(event); | ||||
|                         } | ||||
|                     }, | ||||
|                     false | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             // Handle presses on focused | ||||
|             utils.on(this.elements.container, 'keydown keyup', handleKey, false); | ||||
|         } | ||||
|  | ||||
|         // Detect tab focus | ||||
|         // Remove class on blur/focusout | ||||
|         utils.on(this.elements.container, 'focusout', event => { | ||||
|             utils.toggleClass(event.target, this.config.classNames.tabFocus, false); | ||||
|         }); | ||||
|  | ||||
|         // Add classname to tabbed elements | ||||
|         utils.on(this.elements.container, 'keydown', event => { | ||||
|             if (event.keyCode !== 9) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Delay the adding of classname until the focus has changed | ||||
|             // This event fires before the focusin event | ||||
|             window.setTimeout(() => { | ||||
|                 utils.toggleClass(utils.getFocusElement(), this.config.classNames.tabFocus, true); | ||||
|             }, 0); | ||||
|         }); | ||||
|  | ||||
|         // Trigger custom and default handlers | ||||
|         const handlerProxy = (event, customHandler, defaultHandler) => { | ||||
|             if (utils.is.function(customHandler)) { | ||||
|                 customHandler.call(this, event); | ||||
|             } | ||||
|             if (utils.is.function(defaultHandler)) { | ||||
|                 defaultHandler.call(this, event); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         // Play | ||||
|         utils.proxy(this.elements.buttons.play, 'click', this.config.listeners.play, togglePlay); | ||||
|         utils.proxy(this.elements.buttons.playLarge, 'click', this.config.listeners.play, togglePlay); | ||||
|  | ||||
|         // Pause | ||||
|         utils.proxy(this.elements.buttons.pause, 'click', this.config.listeners.pause, togglePlay); | ||||
|  | ||||
|         // Pause | ||||
|         utils.proxy(this.elements.buttons.restart, 'click', this.config.listeners.restart, () => { | ||||
|             this.restart(); | ||||
|         }); | ||||
|  | ||||
|         // Rewind | ||||
|         utils.proxy(this.elements.buttons.rewind, 'click', this.config.listeners.rewind, () => { | ||||
|             this.rewind(); | ||||
|         }); | ||||
|  | ||||
|         // Rewind | ||||
|         utils.proxy(this.elements.buttons.forward, 'click', this.config.listeners.forward, () => { | ||||
|             this.forward(); | ||||
|         }); | ||||
|  | ||||
|         // Mute | ||||
|         utils.proxy(this.elements.buttons.mute, 'click', this.config.listeners.mute, () => { | ||||
|             this.toggleMute(); | ||||
|         }); | ||||
|  | ||||
|         // Captions | ||||
|         utils.proxy(this.elements.buttons.captions, 'click', this.config.listeners.captions, () => { | ||||
|             this.toggleCaptions(); | ||||
|         }); | ||||
|  | ||||
|         // Fullscreen | ||||
|         utils.proxy(this.elements.buttons.fullscreen, 'click', this.config.listeners.fullscreen, () => { | ||||
|             this.toggleFullscreen(); | ||||
|         }); | ||||
|  | ||||
|         // Picture-in-Picture | ||||
|         utils.proxy(this.elements.buttons.pip, 'click', this.config.listeners.pip, () => { | ||||
|             this.togglePictureInPicture(); | ||||
|         }); | ||||
|  | ||||
|         // Airplay | ||||
|         utils.proxy(this.elements.buttons.airplay, 'click', this.config.listeners.airplay, () => { | ||||
|             this.airPlay(); | ||||
|         }); | ||||
|  | ||||
|         // Settings menu | ||||
|         utils.on(this.elements.buttons.settings, 'click', event => { | ||||
|             controls.toggleMenu.call(this, event); | ||||
|         }); | ||||
|  | ||||
|         // Click anywhere closes menu | ||||
|         utils.on(document.documentElement, 'click', event => { | ||||
|             controls.toggleMenu.call(this, event); | ||||
|         }); | ||||
|  | ||||
|         // Settings menu | ||||
|         utils.on(this.elements.settings.form, 'click', event => { | ||||
|             // Show tab in menu | ||||
|             controls.showTab.call(this, event); | ||||
|  | ||||
|             // Settings menu items - use event delegation as items are added/removed | ||||
|             // Settings - Language | ||||
|             if (utils.matches(event.target, this.config.selectors.inputs.language)) { | ||||
|                 handlerProxy.call(this, event, this.config.listeners.language, () => { | ||||
|                     this.toggleCaptions(true); | ||||
|                     this.language = event.target.value.toLowerCase(); | ||||
|                 }); | ||||
|             } else if (utils.matches(event.target, this.config.selectors.inputs.quality)) { | ||||
|                 // Settings - Quality | ||||
|                 handlerProxy.call(this, event, this.config.listeners.quality, () => { | ||||
|                     this.quality = event.target.value; | ||||
|                 }); | ||||
|             } else if (utils.matches(event.target, this.config.selectors.inputs.speed)) { | ||||
|                 // Settings - Speed | ||||
|                 handlerProxy.call(this, event, this.config.listeners.speed, () => { | ||||
|                     this.speed = parseFloat(event.target.value); | ||||
|                 }); | ||||
|             } else if (utils.matches(event.target, this.config.selectors.buttons.loop)) { | ||||
|                 // Settings - Looping | ||||
|                 // TODO: use toggle buttons | ||||
|                 handlerProxy.call(this, event, this.config.listeners.loop, () => { | ||||
|                     // TODO: This should be done in the method itself I think | ||||
|                     // var value = event.target.getAttribute('data-loop__value') || event.target.getAttribute('data-loop__type'); | ||||
|  | ||||
|                     this.warn('Set loop'); | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Seek | ||||
|         utils.proxy(this.elements.inputs.seek, inputEvent, this.config.listeners.seek, event => { | ||||
|             this.currentTime = event.target.value / event.target.max * this.duration; | ||||
|         }); | ||||
|  | ||||
|         // Volume | ||||
|         utils.proxy(this.elements.inputs.volume, inputEvent, this.config.listeners.volume, event => { | ||||
|             this.setVolume(event.target.value); | ||||
|         }); | ||||
|  | ||||
|         // Polyfill for lower fill in <input type="range"> for webkit | ||||
|         if (this.browser.isWebkit) { | ||||
|             utils.on(utils.getElements.call(this, 'input[type="range"]'), 'input', event => { | ||||
|                 controls.updateRangeFill.call(this, event.target); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // Seek tooltip | ||||
|         utils.on(this.elements.progress, 'mouseenter mouseleave mousemove', event => | ||||
|             ui.updateSeekTooltip.call(this, event) | ||||
|         ); | ||||
|  | ||||
|         // Toggle controls visibility based on mouse movement | ||||
|         if (this.config.hideControls) { | ||||
|             // Toggle controls on mouse events and entering fullscreen | ||||
|             utils.on( | ||||
|                 this.elements.container, | ||||
|                 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', | ||||
|                 event => { | ||||
|                     this.toggleControls(event); | ||||
|                 } | ||||
|             ); | ||||
|  | ||||
|             // Watch for cursor over controls so they don't hide when trying to interact | ||||
|             utils.on(this.elements.controls, 'mouseenter mouseleave', event => { | ||||
|                 this.elements.controls.hover = event.type === 'mouseenter'; | ||||
|             }); | ||||
|  | ||||
|             // Watch for cursor over controls so they don't hide when trying to interact | ||||
|             utils.on(this.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { | ||||
|                 this.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); | ||||
|             }); | ||||
|  | ||||
|             // Focus in/out on controls | ||||
|             // TODO: Check we need capture here | ||||
|             utils.on( | ||||
|                 this.elements.controls, | ||||
|                 'focus blur', | ||||
|                 event => { | ||||
|                     this.toggleControls(event); | ||||
|                 }, | ||||
|                 true | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // Mouse wheel for volume | ||||
|         utils.proxy( | ||||
|             this.elements.inputs.volume, | ||||
|             'wheel', | ||||
|             this.config.listeners.volume, | ||||
|             event => { | ||||
|                 // Detect "natural" scroll - suppored on OS X Safari only | ||||
|                 // Other browsers on OS X will be inverted until support improves | ||||
|                 const inverted = event.webkitDirectionInvertedFromDevice; | ||||
|                 const step = 1 / 50; | ||||
|                 let direction = 0; | ||||
|  | ||||
|                 // Scroll down (or up on natural) to decrease | ||||
|                 if (event.deltaY < 0 || event.deltaX > 0) { | ||||
|                     if (inverted) { | ||||
|                         this.decreaseVolume(step); | ||||
|                         direction = -1; | ||||
|                     } else { | ||||
|                         this.increaseVolume(step); | ||||
|                         direction = 1; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Scroll up (or down on natural) to increase | ||||
|                 if (event.deltaY > 0 || event.deltaX < 0) { | ||||
|                     if (inverted) { | ||||
|                         this.increaseVolume(step); | ||||
|                         direction = 1; | ||||
|                     } else { | ||||
|                         this.decreaseVolume(step); | ||||
|                         direction = -1; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Don't break page scrolling at max and min | ||||
|                 if ((direction === 1 && this.media.volume < 1) || (direction === -1 && this.media.volume > 0)) { | ||||
|                     event.preventDefault(); | ||||
|                 } | ||||
|             }, | ||||
|             false | ||||
|         ); | ||||
|  | ||||
|         // Handle user exiting fullscreen by escaping etc | ||||
|         if (fullscreen.enabled) { | ||||
|             utils.on(document, fullscreen.eventType, event => { | ||||
|                 this.toggleFullscreen(event); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default listeners; | ||||
							
								
								
									
										109
									
								
								src/js/media.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/js/media.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,109 @@ | ||||
| // ========================================================================== | ||||
| // Plyr Media | ||||
| // ========================================================================== | ||||
|  | ||||
| import support from './support'; | ||||
| import utils from './utils'; | ||||
| import youtube from './plugins/youtube'; | ||||
| import vimeo from './plugins/vimeo'; | ||||
| import ui from './ui'; | ||||
|  | ||||
| const media = { | ||||
|     // Setup media | ||||
|     setup() { | ||||
|         // If there's no media, bail | ||||
|         if (!this.media) { | ||||
|             this.warn('No media element found!'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Add type class | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); | ||||
|  | ||||
|         // Add video class for embeds | ||||
|         // This will require changes if audio embeds are added | ||||
|         if (this.isEmbed) { | ||||
|             utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); | ||||
|         } | ||||
|  | ||||
|         if (this.supported.ui) { | ||||
|             // Check for picture-in-picture support | ||||
|             utils.toggleClass( | ||||
|                 this.elements.container, | ||||
|                 this.config.classNames.pip.supported, | ||||
|                 support.pip && this.type === 'video' | ||||
|             ); | ||||
|  | ||||
|             // Check for airplay support | ||||
|             utils.toggleClass( | ||||
|                 this.elements.container, | ||||
|                 this.config.classNames.airplay.supported, | ||||
|                 support.airplay && this.isHTML5 | ||||
|             ); | ||||
|  | ||||
|             // If there's no autoplay attribute, assume the video is stopped and add state class | ||||
|             utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay); | ||||
|  | ||||
|             // Add iOS class | ||||
|             utils.toggleClass(this.elements.container, this.config.classNames.isIos, this.browser.isIos); | ||||
|  | ||||
|             // Add touch class | ||||
|             utils.toggleClass(this.elements.container, this.config.classNames.isTouch, support.touch); | ||||
|         } | ||||
|  | ||||
|         // Inject the player wrapper | ||||
|         if (['video', 'youtube', 'vimeo'].includes(this.type)) { | ||||
|             // Create the wrapper div | ||||
|             this.elements.wrapper = utils.createElement('div', { | ||||
|                 class: this.config.classNames.video, | ||||
|             }); | ||||
|  | ||||
|             // Wrap the video in a container | ||||
|             utils.wrap(this.media, this.elements.wrapper); | ||||
|         } | ||||
|  | ||||
|         // Embeds | ||||
|         if (this.isEmbed) { | ||||
|             switch (this.type) { | ||||
|                 case 'youtube': | ||||
|                     youtube.setup.call(this); | ||||
|                     break; | ||||
|  | ||||
|                 case 'vimeo': | ||||
|                     vimeo.setup.call(this); | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         ui.setTitle.call(this); | ||||
|     }, | ||||
|  | ||||
|     // Cancel current network requests | ||||
|     // See https://github.com/sampotts/plyr/issues/174 | ||||
|     cancelRequests() { | ||||
|         if (!this.isHTML5) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Remove child sources | ||||
|         Array.from(this.media.querySelectorAll('source')).forEach(utils.removeElement); | ||||
|  | ||||
|         // Set blank video src attribute | ||||
|         // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error | ||||
|         // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection | ||||
|         this.media.setAttribute('src', this.config.blankVideo); | ||||
|  | ||||
|         // Load the new empty source | ||||
|         // This will cancel existing requests | ||||
|         // See https://github.com/sampotts/plyr/issues/174 | ||||
|         this.media.load(); | ||||
|  | ||||
|         // Debugging | ||||
|         this.log('Cancelled network requests'); | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default media; | ||||
							
								
								
									
										165
									
								
								src/js/plugins/vimeo.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								src/js/plugins/vimeo.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | ||||
| // ========================================================================== | ||||
| // Vimeo plugin | ||||
| // ========================================================================== | ||||
|  | ||||
| import utils from './../utils'; | ||||
| import captions from './../captions'; | ||||
| import ui from './../ui'; | ||||
|  | ||||
| const vimeo = { | ||||
|     // Setup YouTube | ||||
|     setup() { | ||||
|         // Remove old containers | ||||
|         const containers = utils.getElements.call(this, `[id^="${this.type}-"]`); | ||||
|         Array.from(containers).forEach(utils.removeElement); | ||||
|  | ||||
|         // Add embed class for responsive | ||||
|         utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); | ||||
|  | ||||
|         // Set ID | ||||
|         this.media.setAttribute('id', utils.generateId(this.type)); | ||||
|  | ||||
|         // Load the API if not already | ||||
|         if (!utils.is.object(window.Vimeo)) { | ||||
|             utils.loadScript(this.config.urls.vimeo.api); | ||||
|             // Wait for load | ||||
|             const vimeoTimer = window.setInterval(() => { | ||||
|                 if (utils.is.object(window.Vimeo)) { | ||||
|                     window.clearInterval(vimeoTimer); | ||||
|                     vimeo.ready.call(this); | ||||
|                 } | ||||
|             }, 50); | ||||
|         } else { | ||||
|             vimeo.ready.call(this); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Ready | ||||
|     ready() { | ||||
|         const player = this; | ||||
|  | ||||
|         // Get Vimeo params for the iframe | ||||
|         const options = { | ||||
|             loop: this.config.loop.active, | ||||
|             autoplay: this.config.autoplay, | ||||
|             byline: false, | ||||
|             portrait: false, | ||||
|             title: false, | ||||
|             transparent: 0, | ||||
|         }; | ||||
|         const params = utils.buildUrlParameters(options); | ||||
|         const id = utils.parseVimeoId(this.embedId); | ||||
|  | ||||
|         // Build an iframe | ||||
|         const iframe = utils.createElement('iframe'); | ||||
|         const src = `https://player.vimeo.com/video/${id}?${params}`; | ||||
|         iframe.setAttribute('src', src); | ||||
|         iframe.setAttribute('allowfullscreen', ''); | ||||
|         player.media.appendChild(iframe); | ||||
|  | ||||
|         // Setup instance | ||||
|         // https://github.com/vimeo/this.js | ||||
|         player.embed = new window.Vimeo.Player(iframe); | ||||
|  | ||||
|         // Create a faux HTML5 API using the Vimeo API | ||||
|         player.media.play = () => { | ||||
|             player.embed.play(); | ||||
|             player.media.paused = false; | ||||
|         }; | ||||
|         player.media.pause = () => { | ||||
|             player.embed.pause(); | ||||
|             player.media.paused = true; | ||||
|         }; | ||||
|         player.media.stop = () => { | ||||
|             player.embed.stop(); | ||||
|             player.media.paused = true; | ||||
|         }; | ||||
|  | ||||
|         player.media.paused = true; | ||||
|         player.media.currentTime = 0; | ||||
|  | ||||
|         // Rebuild UI | ||||
|         ui.build.call(player); | ||||
|  | ||||
|         player.embed.getCurrentTime().then(value => { | ||||
|             player.media.currentTime = value; | ||||
|             utils.dispatchEvent.call(this, this.media, 'timeupdate'); | ||||
|         }); | ||||
|  | ||||
|         player.embed.getDuration().then(value => { | ||||
|             player.media.duration = value; | ||||
|             utils.dispatchEvent.call(player, player.media, 'durationchange'); | ||||
|         }); | ||||
|  | ||||
|         // Get captions | ||||
|         player.embed.getTextTracks().then(tracks => { | ||||
|             player.captions.tracks = tracks; | ||||
|  | ||||
|             captions.setup.call(player); | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('cuechange', data => { | ||||
|             let cue = null; | ||||
|  | ||||
|             if (data.cues.length) { | ||||
|                 cue = utils.stripHTML(data.cues[0].text); | ||||
|             } | ||||
|  | ||||
|             captions.set.call(player, cue); | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('loaded', () => { | ||||
|             if (utils.is.htmlElement(player.embed.element) && player.supported.ui) { | ||||
|                 const frame = player.embed.element; | ||||
|  | ||||
|                 // Fix Vimeo controls issue | ||||
|                 // https://github.com/sampotts/plyr/issues/697 | ||||
|                 // frame.src = `${frame.src}&transparent=0`; | ||||
|  | ||||
|                 // Fix keyboard focus issues | ||||
|                 // https://github.com/sampotts/plyr/issues/317 | ||||
|                 frame.setAttribute('tabindex', -1); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('play', () => { | ||||
|             player.media.paused = false; | ||||
|             utils.dispatchEvent.call(player, player.media, 'play'); | ||||
|             utils.dispatchEvent.call(player, player.media, 'playing'); | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('pause', () => { | ||||
|             player.media.paused = true; | ||||
|             utils.dispatchEvent.call(player, player.media, 'pause'); | ||||
|         }); | ||||
|  | ||||
|         this.embed.on('timeupdate', data => { | ||||
|             this.media.seeking = false; | ||||
|             this.media.currentTime = data.seconds; | ||||
|             utils.dispatchEvent.call(this, this.media, 'timeupdate'); | ||||
|         }); | ||||
|  | ||||
|         this.embed.on('progress', data => { | ||||
|             this.media.buffered = data.percent; | ||||
|             utils.dispatchEvent.call(this, this.media, 'progress'); | ||||
|  | ||||
|             if (parseInt(data.percent, 10) === 1) { | ||||
|                 // Trigger event | ||||
|                 utils.dispatchEvent.call(this, this.media, 'canplaythrough'); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this.embed.on('seeked', () => { | ||||
|             this.media.seeking = false; | ||||
|             utils.dispatchEvent.call(this, this.media, 'seeked'); | ||||
|             utils.dispatchEvent.call(this, this.media, 'play'); | ||||
|         }); | ||||
|  | ||||
|         this.embed.on('ended', () => { | ||||
|             this.media.paused = true; | ||||
|             utils.dispatchEvent.call(this, this.media, 'ended'); | ||||
|         }); | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default vimeo; | ||||
							
								
								
									
										256
									
								
								src/js/plugins/youtube.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								src/js/plugins/youtube.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,256 @@ | ||||
| // ========================================================================== | ||||
| // YouTube plugin | ||||
| // ========================================================================== | ||||
|  | ||||
| import utils from './../utils'; | ||||
| import controls from './../controls'; | ||||
| import ui from './../ui'; | ||||
|  | ||||
| /* Object.defineProperty(input, "value", { | ||||
|     get: function() {return this._value;}, | ||||
|     set: function(v) { | ||||
|         // Do your stuff | ||||
|         this._value = v; | ||||
|     } | ||||
| }); */ | ||||
|  | ||||
| const youtube = { | ||||
|     // Setup YouTube | ||||
|     setup() { | ||||
|         const videoId = utils.parseYouTubeId(this.embedId); | ||||
|  | ||||
|         // Remove old containers | ||||
|         const containers = utils.getElements.call(this, `[id^="${this.type}-"]`); | ||||
|         Array.from(containers).forEach(utils.removeElement); | ||||
|  | ||||
|         // Add embed class for responsive | ||||
|         utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); | ||||
|  | ||||
|         // Set ID | ||||
|         this.media.setAttribute('id', utils.generateId(this.type)); | ||||
|  | ||||
|         // Setup API | ||||
|         if (utils.is.object(window.YT)) { | ||||
|             youtube.ready.call(this, videoId); | ||||
|         } else { | ||||
|             // Load the API | ||||
|             utils.loadScript(this.config.urls.youtube.api); | ||||
|  | ||||
|             // Setup callback for the API | ||||
|             window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; | ||||
|  | ||||
|             // Add to queue | ||||
|             window.onYouTubeReadyCallbacks.push(() => { | ||||
|                 youtube.ready.call(this, videoId); | ||||
|             }); | ||||
|  | ||||
|             // Set callback to process queue | ||||
|             window.onYouTubeIframeAPIReady = () => { | ||||
|                 window.onYouTubeReadyCallbacks.forEach(callback => { | ||||
|                     callback(); | ||||
|                 }); | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Handle YouTube API ready | ||||
|     ready(videoId) { | ||||
|         const player = this; | ||||
|  | ||||
|         // Setup instance | ||||
|         // https://developers.google.com/youtube/iframe_api_reference | ||||
|         player.embed = new window.YT.Player(player.media.id, { | ||||
|             videoId, | ||||
|             playerVars: { | ||||
|                 autoplay: player.config.autoplay ? 1 : 0, // Autoplay | ||||
|                 controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported | ||||
|                 rel: 0, // No related vids | ||||
|                 showinfo: 0, // Hide info | ||||
|                 iv_load_policy: 3, // Hide annotations | ||||
|                 modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused) | ||||
|                 disablekb: 1, // Disable keyboard as we handle it | ||||
|                 playsinline: 1, // Allow iOS inline playback | ||||
|  | ||||
|                 // Tracking for stats | ||||
|                 origin: window && window.location.hostname, | ||||
|                 widget_referrer: window && window.location.href, | ||||
|  | ||||
|                 // Captions is flaky on YouTube | ||||
|                 // cc_load_policy: (this.captions.active ? 1 : 0), | ||||
|                 // cc_lang_pref: 'en', | ||||
|             }, | ||||
|             events: { | ||||
|                 onError(event) { | ||||
|                     utils.dispatchEvent.call(player, player.media, 'error', true, { | ||||
|                         code: event.data, | ||||
|                         embed: event.target, | ||||
|                     }); | ||||
|                 }, | ||||
|                 onPlaybackQualityChange(event) { | ||||
|                     // Get the instance | ||||
|                     const instance = event.target; | ||||
|  | ||||
|                     // Get current quality | ||||
|                     player.media.quality = instance.getPlaybackQuality(); | ||||
|  | ||||
|                     utils.dispatchEvent.call(player, player.media, 'qualitychange'); | ||||
|                 }, | ||||
|                 onPlaybackRateChange(event) { | ||||
|                     // Get the instance | ||||
|                     const instance = event.target; | ||||
|  | ||||
|                     // Get current speed | ||||
|                     player.media.playbackRate = instance.getPlaybackRate(); | ||||
|  | ||||
|                     utils.dispatchEvent.call(player, player.media, 'ratechange'); | ||||
|                 }, | ||||
|                 onReady(event) { | ||||
|                     // Get the instance | ||||
|                     const instance = event.target; | ||||
|  | ||||
|                     // Create a faux HTML5 API using the YouTube API | ||||
|                     player.media.play = () => { | ||||
|                         instance.playVideo(); | ||||
|                         player.media.paused = false; | ||||
|                     }; | ||||
|                     player.media.pause = () => { | ||||
|                         instance.pauseVideo(); | ||||
|                         player.media.paused = true; | ||||
|                     }; | ||||
|                     player.media.stop = () => { | ||||
|                         instance.stopVideo(); | ||||
|                         player.media.paused = true; | ||||
|                     }; | ||||
|                     player.media.duration = instance.getDuration(); | ||||
|                     player.media.paused = true; | ||||
|                     player.media.muted = instance.isMuted(); | ||||
|                     player.media.currentTime = 0; | ||||
|  | ||||
|                     // Get available speeds | ||||
|                     if (player.config.controls.includes('settings') && player.config.settings.includes('speed')) { | ||||
|                         controls.setSpeedMenu.call(player, instance.getAvailablePlaybackRates()); | ||||
|                     } | ||||
|  | ||||
|                     // Set title | ||||
|                     player.config.title = instance.getVideoData().title; | ||||
|  | ||||
|                     // Set the tabindex to avoid focus entering iframe | ||||
|                     if (player.supported.ui) { | ||||
|                         player.media.setAttribute('tabindex', -1); | ||||
|                     } | ||||
|  | ||||
|                     // Rebuild UI | ||||
|                     ui.build.call(player); | ||||
|  | ||||
|                     utils.dispatchEvent.call(player, player.media, 'timeupdate'); | ||||
|                     utils.dispatchEvent.call(player, player.media, 'durationchange'); | ||||
|  | ||||
|                     // Reset timer | ||||
|                     window.clearInterval(player.timers.buffering); | ||||
|  | ||||
|                     // Setup buffering | ||||
|                     player.timers.buffering = window.setInterval(() => { | ||||
|                         // Get loaded % from YouTube | ||||
|                         player.media.buffered = instance.getVideoLoadedFraction(); | ||||
|  | ||||
|                         // Trigger progress only when we actually buffer something | ||||
|                         if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { | ||||
|                             utils.dispatchEvent.call(player, player.media, 'progress'); | ||||
|                         } | ||||
|  | ||||
|                         // Set last buffer point | ||||
|                         player.media.lastBuffered = player.media.buffered; | ||||
|  | ||||
|                         // Bail if we're at 100% | ||||
|                         if (player.media.buffered === 1) { | ||||
|                             window.clearInterval(player.timers.buffering); | ||||
|  | ||||
|                             // Trigger event | ||||
|                             utils.dispatchEvent.call(player, player.media, 'canplaythrough'); | ||||
|                         } | ||||
|                     }, 200); | ||||
|                 }, | ||||
|                 onStateChange(event) { | ||||
|                     // Get the instance | ||||
|                     const instance = event.target; | ||||
|  | ||||
|                     // Reset timer | ||||
|                     window.clearInterval(player.timers.playing); | ||||
|  | ||||
|                     // Handle events | ||||
|                     // -1   Unstarted | ||||
|                     // 0    Ended | ||||
|                     // 1    Playing | ||||
|                     // 2    Paused | ||||
|                     // 3    Buffering | ||||
|                     // 5    Video cued | ||||
|                     switch (event.data) { | ||||
|                         case 0: | ||||
|                             // YouTube doesn't support loop for a single video, so mimick it. | ||||
|                             if (player.config.loop.active) { | ||||
|                                 // YouTube needs a call to `stopVideo` before playing again | ||||
|                                 instance.stopVideo(); | ||||
|                                 instance.playVideo(); | ||||
|  | ||||
|                                 break; | ||||
|                             } | ||||
|  | ||||
|                             player.media.paused = true; | ||||
|  | ||||
|                             utils.dispatchEvent.call(player, player.media, 'ended'); | ||||
|  | ||||
|                             break; | ||||
|  | ||||
|                         case 1: | ||||
|                             player.media.paused = false; | ||||
|  | ||||
|                             // If we were seeking, fire seeked event | ||||
|                             if (player.media.seeking) { | ||||
|                                 utils.dispatchEvent.call(player, player.media, 'seeked'); | ||||
|                             } | ||||
|  | ||||
|                             player.media.seeking = false; | ||||
|  | ||||
|                             utils.dispatchEvent.call(player, player.media, 'play'); | ||||
|                             utils.dispatchEvent.call(player, player.media, 'playing'); | ||||
|  | ||||
|                             // Poll to get playback progress | ||||
|                             player.timers.playing = window.setInterval(() => { | ||||
|                                 player.media.currentTime = instance.getCurrentTime(); | ||||
|                                 utils.dispatchEvent.call(player, player.media, 'timeupdate'); | ||||
|                             }, 100); | ||||
|  | ||||
|                             // Check duration again due to YouTube bug | ||||
|                             // https://github.com/sampotts/plyr/issues/374 | ||||
|                             // https://code.google.com/p/gdata-issues/issues/detail?id=8690 | ||||
|                             if (player.media.duration !== instance.getDuration()) { | ||||
|                                 player.media.duration = instance.getDuration(); | ||||
|                                 utils.dispatchEvent.call(player, player.media, 'durationchange'); | ||||
|                             } | ||||
|  | ||||
|                             // Get quality | ||||
|                             controls.setQualityMenu.call(player, instance.getAvailableQualityLevels()); | ||||
|  | ||||
|                             break; | ||||
|  | ||||
|                         case 2: | ||||
|                             player.media.paused = true; | ||||
|  | ||||
|                             utils.dispatchEvent.call(player, player.media, 'pause'); | ||||
|  | ||||
|                             break; | ||||
|  | ||||
|                         default: | ||||
|                             break; | ||||
|                     } | ||||
|  | ||||
|                     utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, { | ||||
|                         code: event.data, | ||||
|                     }); | ||||
|                 }, | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default youtube; | ||||
							
								
								
									
										5778
									
								
								src/js/plyr.js
									
									
									
									
									
								
							
							
						
						
									
										5778
									
								
								src/js/plyr.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										162
									
								
								src/js/source.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								src/js/source.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,162 @@ | ||||
| // ========================================================================== | ||||
| // Plyr source update | ||||
| // ========================================================================== | ||||
|  | ||||
| import types from './types'; | ||||
| import utils from './utils'; | ||||
| import media from './media'; | ||||
| import ui from './ui'; | ||||
| import support from './support'; | ||||
|  | ||||
| const source = { | ||||
|     // Add elements to HTML5 media (source, tracks, etc) | ||||
|     insertElements(type, attributes) { | ||||
|         if (utils.is.string(attributes)) { | ||||
|             utils.insertElement(type, this.media, { | ||||
|                 src: attributes, | ||||
|             }); | ||||
|         } else if (utils.is.array(attributes)) { | ||||
|             this.warn(attributes); | ||||
|  | ||||
|             attributes.forEach(attribute => { | ||||
|                 utils.insertElement(type, this.media, attribute); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Update source | ||||
|     // Sources are not checked for support so be careful | ||||
|     change(input) { | ||||
|         if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) { | ||||
|             this.warn('Invalid source format'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Cancel current network requests | ||||
|         media.cancelRequests.call(this); | ||||
|  | ||||
|         // Destroy instance and re-setup | ||||
|         this.destroy.call( | ||||
|             this, | ||||
|             () => { | ||||
|                 // TODO: Reset menus here | ||||
|  | ||||
|                 // Remove elements | ||||
|                 utils.removeElement(this.media); | ||||
|                 this.media = null; | ||||
|  | ||||
|                 // Reset class name | ||||
|                 if (utils.is.htmlElement(this.elements.container)) { | ||||
|                     this.elements.container.removeAttribute('class'); | ||||
|                 } | ||||
|  | ||||
|                 // Set the type | ||||
|                 if ('type' in input) { | ||||
|                     this.type = input.type; | ||||
|  | ||||
|                     // Get child type for video (it might be an embed) | ||||
|                     if (this.type === 'video') { | ||||
|                         const firstSource = input.sources[0]; | ||||
|  | ||||
|                         if ('type' in firstSource && types.embed.includes(firstSource.type)) { | ||||
|                             this.type = firstSource.type; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Check for support | ||||
|                 this.supported = support.check(this.type, this.config.inline); | ||||
|  | ||||
|                 // Create new markup | ||||
|                 switch (this.type) { | ||||
|                     case 'video': | ||||
|                         this.media = utils.createElement('video'); | ||||
|                         break; | ||||
|  | ||||
|                     case 'audio': | ||||
|                         this.media = utils.createElement('audio'); | ||||
|                         break; | ||||
|  | ||||
|                     case 'youtube': | ||||
|                     case 'vimeo': | ||||
|                         this.media = utils.createElement('div'); | ||||
|                         this.embedId = input.sources[0].src; | ||||
|                         break; | ||||
|  | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
|                 // Inject the new element | ||||
|                 this.elements.container.appendChild(this.media); | ||||
|  | ||||
|                 // Autoplay the new source? | ||||
|                 if (utils.is.boolean(input.autoplay)) { | ||||
|                     this.config.autoplay = input.autoplay; | ||||
|                 } | ||||
|  | ||||
|                 // Set attributes for audio and video | ||||
|                 if (this.isHTML5) { | ||||
|                     if (this.config.crossorigin) { | ||||
|                         this.media.setAttribute('crossorigin', ''); | ||||
|                     } | ||||
|                     if (this.config.autoplay) { | ||||
|                         this.media.setAttribute('autoplay', ''); | ||||
|                     } | ||||
|                     if ('poster' in input) { | ||||
|                         this.media.setAttribute('poster', input.poster); | ||||
|                     } | ||||
|                     if (this.config.loop.active) { | ||||
|                         this.media.setAttribute('loop', ''); | ||||
|                     } | ||||
|                     if (this.config.muted) { | ||||
|                         this.media.setAttribute('muted', ''); | ||||
|                     } | ||||
|                     if (this.config.inline) { | ||||
|                         this.media.setAttribute('playsinline', ''); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Restore class hooks | ||||
|                 utils.toggleClass( | ||||
|                     this.elements.container, | ||||
|                     this.config.classNames.captions.active, | ||||
|                     this.supported.ui && this.captions.enabled | ||||
|                 ); | ||||
|  | ||||
|                 ui.addStyleHook.call(this); | ||||
|  | ||||
|                 // Set new sources for html5 | ||||
|                 if (this.isHTML5) { | ||||
|                     source.insertElements.call(this, 'source', input.sources); | ||||
|                 } | ||||
|  | ||||
|                 // Set video title | ||||
|                 this.config.title = input.title; | ||||
|  | ||||
|                 // Set up from scratch | ||||
|                 media.setup.call(this); | ||||
|  | ||||
|                 // HTML5 stuff | ||||
|                 if (this.isHTML5) { | ||||
|                     // Setup captions | ||||
|                     if ('tracks' in input) { | ||||
|                         source.insertElements.call(this, 'track', input.tracks); | ||||
|                     } | ||||
|  | ||||
|                     // Load HTML5 sources | ||||
|                     this.media.load(); | ||||
|                 } | ||||
|  | ||||
|                 // If HTML5 or embed but not fully supported, setupInterface and call ready now | ||||
|                 if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) { | ||||
|                     // Setup interface | ||||
|                     ui.build.call(this); | ||||
|                 } | ||||
|             }, | ||||
|             true | ||||
|         ); | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default source; | ||||
							
								
								
									
										56
									
								
								src/js/storage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/js/storage.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| // ========================================================================== | ||||
| // Plyr storage | ||||
| // ========================================================================== | ||||
|  | ||||
| import support from './support'; | ||||
| import utils from './utils'; | ||||
|  | ||||
| // Save a value back to local storage | ||||
| function set(value) { | ||||
|     // Bail if we don't have localStorage support or it's disabled | ||||
|     if (!support.storage || !this.config.storage.enabled) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Update the working copy of the values | ||||
|     utils.extend(this.storage, value); | ||||
|  | ||||
|     // Update storage | ||||
|     window.localStorage.setItem(this.config.storage.key, JSON.stringify(this.storage)); | ||||
| } | ||||
|  | ||||
| // Setup localStorage | ||||
| function setup() { | ||||
|     let value = null; | ||||
|     let storage = {}; | ||||
|  | ||||
|     // Bail if we don't have localStorage support or it's disabled | ||||
|     if (!support.storage || !this.config.storage.enabled) { | ||||
|         return storage; | ||||
|     } | ||||
|  | ||||
|     // Clean up old volume | ||||
|     // https://github.com/sampotts/plyr/issues/171 | ||||
|     window.localStorage.removeItem('plyr-volume'); | ||||
|  | ||||
|     // load value from the current key | ||||
|     value = window.localStorage.getItem(this.config.storage.key); | ||||
|  | ||||
|     if (!value) { | ||||
|         // Key wasn't set (or had been cleared), move along | ||||
|     } else if (/^\d+(\.\d+)?$/.test(value)) { | ||||
|         // If value is a number, it's probably volume from an older | ||||
|         // version of this. See: https://github.com/sampotts/plyr/pull/313 | ||||
|         // Update the key to be JSON | ||||
|         set({ | ||||
|             volume: parseFloat(value), | ||||
|         }); | ||||
|     } else { | ||||
|         // Assume it's JSON from this or a later version of plyr | ||||
|         storage = JSON.parse(value); | ||||
|     } | ||||
|  | ||||
|     return storage; | ||||
| } | ||||
|  | ||||
| export default { setup, set }; | ||||
							
								
								
									
										174
									
								
								src/js/support.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/js/support.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | ||||
| // ========================================================================== | ||||
| // Plyr support checks | ||||
| // ========================================================================== | ||||
|  | ||||
| import utils from './utils'; | ||||
|  | ||||
| // Check for feature support | ||||
| const support = { | ||||
|     // Basic support | ||||
|     audio: 'canPlayType' in document.createElement('audio'), | ||||
|     video: 'canPlayType' in document.createElement('video'), | ||||
|  | ||||
|     // Check for support | ||||
|     // Basic functionality vs full UI | ||||
|     check(type, inline) { | ||||
|         let api = false; | ||||
|         let ui = false; | ||||
|         const browser = utils.getBrowser(); | ||||
|         const playsInline = browser.isIPhone && inline && support.inline; | ||||
|  | ||||
|         switch (type) { | ||||
|             case 'video': | ||||
|                 api = support.video; | ||||
|                 ui = api && support.rangeInput && (!browser.isIPhone || playsInline); | ||||
|                 break; | ||||
|  | ||||
|             case 'audio': | ||||
|                 api = support.audio; | ||||
|                 ui = api && support.rangeInput; | ||||
|                 break; | ||||
|  | ||||
|             case 'youtube': | ||||
|                 api = true; | ||||
|                 ui = support.rangeInput && (!browser.isIPhone || playsInline); | ||||
|                 break; | ||||
|  | ||||
|             case 'vimeo': | ||||
|                 api = true; | ||||
|                 ui = support.rangeInput && !browser.isIPhone; | ||||
|                 break; | ||||
|  | ||||
|             default: | ||||
|                 api = support.audio && support.video; | ||||
|                 ui = api && support.rangeInput; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             api, | ||||
|             ui, | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     // Local storage | ||||
|     // We can't assume if local storage is present that we can use it | ||||
|     storage: (() => { | ||||
|         if (!('localStorage' in window)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Try to use it (it might be disabled, e.g. user is in private/porn mode) | ||||
|         // see: https://github.com/sampotts/plyr/issues/131 | ||||
|         const test = '___test'; | ||||
|         try { | ||||
|             window.localStorage.setItem(test, test); | ||||
|             window.localStorage.removeItem(test); | ||||
|             return true; | ||||
|         } catch (e) { | ||||
|             return false; | ||||
|         } | ||||
|     })(), | ||||
|  | ||||
|     // Picture-in-picture support | ||||
|     // Safari only currently | ||||
|     pip: (() => { | ||||
|         const browser = utils.getBrowser(); | ||||
|         return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode); | ||||
|     })(), | ||||
|  | ||||
|     // Airplay support | ||||
|     // Safari only currently | ||||
|     airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent), | ||||
|  | ||||
|     // Inline playback support | ||||
|     // https://webkit.org/blog/6784/new-video-policies-for-ios/ | ||||
|     inline: 'playsInline' in document.createElement('video'), | ||||
|  | ||||
|     // Check for mime type support against a player instance | ||||
|     // Credits: http://diveintohtml5.info/everything.html | ||||
|     // Related: http://www.leanbackplayer.com/test/h5mt.html | ||||
|     mime(player, type) { | ||||
|         const media = { player }; | ||||
|  | ||||
|         try { | ||||
|             // Bail if no checking function | ||||
|             if (!utils.is.function(media.canPlayType)) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             // Type specific checks | ||||
|             if (player.type === 'video') { | ||||
|                 switch (type) { | ||||
|                     case 'video/webm': | ||||
|                         return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''); | ||||
|                     case 'video/mp4': | ||||
|                         return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''); | ||||
|                     case 'video/ogg': | ||||
|                         return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''); | ||||
|                     default: | ||||
|                         return false; | ||||
|                 } | ||||
|             } else if (player.type === 'audio') { | ||||
|                 switch (type) { | ||||
|                     case 'audio/mpeg': | ||||
|                         return media.canPlayType('audio/mpeg;').replace(/no/, ''); | ||||
|                     case 'audio/ogg': | ||||
|                         return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, ''); | ||||
|                     case 'audio/wav': | ||||
|                         return media.canPlayType('audio/wav; codecs="1"').replace(/no/, ''); | ||||
|                     default: | ||||
|                         return false; | ||||
|                 } | ||||
|             } | ||||
|         } catch (e) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // If we got this far, we're stuffed | ||||
|         return false; | ||||
|     }, | ||||
|  | ||||
|     // Check for textTracks support | ||||
|     textTracks: 'textTracks' in document.createElement('video'), | ||||
|  | ||||
|     // Check for passive event listener support | ||||
|     // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md | ||||
|     // https://www.youtube.com/watch?v=NPM6172J22g | ||||
|     passiveListeners: (() => { | ||||
|         // Test via a getter in the options object to see if the passive property is accessed | ||||
|         let supported = false; | ||||
|         try { | ||||
|             const options = Object.defineProperty({}, 'passive', { | ||||
|                 get() { | ||||
|                     supported = true; | ||||
|                     return null; | ||||
|                 }, | ||||
|             }); | ||||
|             window.addEventListener('test', null, options); | ||||
|         } catch (e) { | ||||
|             // Do nothing | ||||
|         } | ||||
|  | ||||
|         return supported; | ||||
|     })(), | ||||
|  | ||||
|     // <input type="range"> Sliders | ||||
|     rangeInput: (() => { | ||||
|         const range = document.createElement('input'); | ||||
|         range.type = 'range'; | ||||
|         return range.type === 'range'; | ||||
|     })(), | ||||
|  | ||||
|     // Touch | ||||
|     // Remember a device can be moust + touch enabled | ||||
|     touch: 'ontouchstart' in document.documentElement, | ||||
|  | ||||
|     // Detect transitions support | ||||
|     transitions: utils.transitionEnd !== false, | ||||
|  | ||||
|     // Reduced motion iOS & MacOS setting | ||||
|     // https://webkit.org/blog/7551/responsive-design-for-motion/ | ||||
|     reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches, | ||||
| }; | ||||
|  | ||||
| export default support; | ||||
							
								
								
									
										10
									
								
								src/js/types.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/js/types.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| // ========================================================================== | ||||
| // Plyr supported types | ||||
| // ========================================================================== | ||||
|  | ||||
| const types = { | ||||
|     embed: ['youtube', 'vimeo'], | ||||
|     html5: ['video', 'audio'], | ||||
| }; | ||||
|  | ||||
| export default types; | ||||
							
								
								
									
										381
									
								
								src/js/ui.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										381
									
								
								src/js/ui.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
							
								
								
									
										667
									
								
								src/js/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										667
									
								
								src/js/utils.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,667 @@ | ||||
| // ========================================================================== | ||||
| // Plyr utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import support from './support'; | ||||
|  | ||||
| const utils = { | ||||
|     // Check variable types | ||||
|     is: { | ||||
|         object(input) { | ||||
|             return this.getConstructor(input) === Object; | ||||
|         }, | ||||
|         number(input) { | ||||
|             return this.getConstructor(input) === Number && !Number.isNaN(input); | ||||
|         }, | ||||
|         string(input) { | ||||
|             return this.getConstructor(input) === String; | ||||
|         }, | ||||
|         boolean(input) { | ||||
|             return this.getConstructor(input) === Boolean; | ||||
|         }, | ||||
|         function(input) { | ||||
|             return this.getConstructor(input) === Function; | ||||
|         }, | ||||
|         array(input) { | ||||
|             return !this.undefined(input) && Array.isArray(input); | ||||
|         }, | ||||
|         nodeList(input) { | ||||
|             return !this.undefined(input) && input instanceof NodeList; | ||||
|         }, | ||||
|         htmlElement(input) { | ||||
|             return !this.undefined(input) && input instanceof HTMLElement; | ||||
|         }, | ||||
|         event(input) { | ||||
|             return !this.undefined(input) && input instanceof Event; | ||||
|         }, | ||||
|         cue(input) { | ||||
|             return this.instanceOf(input, window.TextTrackCue) || this.instanceOf(input, window.VTTCue); | ||||
|         }, | ||||
|         track(input) { | ||||
|             return ( | ||||
|                 !this.undefined(input) && (this.instanceOf(input, window.TextTrack) || typeof input.kind === 'string') | ||||
|             ); | ||||
|         }, | ||||
|         undefined(input) { | ||||
|             return input !== null && typeof input === 'undefined'; | ||||
|         }, | ||||
|         empty(input) { | ||||
|             return ( | ||||
|                 input === null || | ||||
|                 typeof input === 'undefined' || | ||||
|                 ((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) || | ||||
|                 (this.object(input) && Object.keys(input).length === 0) | ||||
|             ); | ||||
|         }, | ||||
|         getConstructor(input) { | ||||
|             if (input === null || typeof input === 'undefined') { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             return input.constructor; | ||||
|         }, | ||||
|         instanceOf(input, constructor) { | ||||
|             return Boolean(input && constructor && input instanceof constructor); | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     // Unfortunately, due to mixed support, UA sniffing is required | ||||
|     getBrowser() { | ||||
|         return { | ||||
|             isIE: /* @cc_on!@ */ false || !!document.documentMode, | ||||
|             isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent), | ||||
|             isIPhone: /(iPhone|iPod)/gi.test(navigator.platform), | ||||
|             isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform), | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     // Load an external script | ||||
|     loadScript(url) { | ||||
|         // Check script is not already referenced | ||||
|         if (document.querySelectorAll(`script[src="${url}"]`).length) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const tag = document.createElement('script'); | ||||
|         tag.src = url; | ||||
|  | ||||
|         const firstScriptTag = document.getElementsByTagName('script')[0]; | ||||
|         firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); | ||||
|     }, | ||||
|  | ||||
|     // Generate a random ID | ||||
|     generateId(prefix) { | ||||
|         return `${prefix}-${Math.floor(Math.random() * 10000)}`; | ||||
|     }, | ||||
|  | ||||
|     // Determine if we're in an iframe | ||||
|     inFrame() { | ||||
|         try { | ||||
|             return window.self !== window.top; | ||||
|         } catch (e) { | ||||
|             return true; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Wrap an element | ||||
|     wrap(elements, wrapper) { | ||||
|         // Convert `elements` to an array, if necessary. | ||||
|         const targets = elements.length ? elements : [elements]; | ||||
|  | ||||
|         // Loops backwards to prevent having to clone the wrapper on the | ||||
|         // first element (see `child` below). | ||||
|         Array.from(targets) | ||||
|             .reverse() | ||||
|             .forEach((element, index) => { | ||||
|                 const child = index > 0 ? wrapper.cloneNode(true) : wrapper; | ||||
|  | ||||
|                 // Cache the current parent and sibling. | ||||
|                 const parent = element.parentNode; | ||||
|                 const sibling = element.nextSibling; | ||||
|  | ||||
|                 // Wrap the element (is automatically removed from its current | ||||
|                 // parent). | ||||
|                 child.appendChild(element); | ||||
|  | ||||
|                 // If the element had a sibling, insert the wrapper before | ||||
|                 // the sibling to maintain the HTML structure; otherwise, just | ||||
|                 // append it to the parent. | ||||
|                 if (sibling) { | ||||
|                     parent.insertBefore(child, sibling); | ||||
|                 } else { | ||||
|                     parent.appendChild(child); | ||||
|                 } | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     // Remove an element | ||||
|     removeElement(element) { | ||||
|         if (!utils.is.htmlElement(element) || !utils.is.htmlElement(element.parentNode)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         element.parentNode.removeChild(element); | ||||
|  | ||||
|         return element; | ||||
|     }, | ||||
|  | ||||
|     // Inaert an element after another | ||||
|     insertAfter(element, target) { | ||||
|         target.parentNode.insertBefore(element, target.nextSibling); | ||||
|     }, | ||||
|  | ||||
|     // Create a DocumentFragment | ||||
|     createElement(type, attributes, text) { | ||||
|         // Create a new <element> | ||||
|         const element = document.createElement(type); | ||||
|  | ||||
|         // Set all passed attributes | ||||
|         if (utils.is.object(attributes)) { | ||||
|             utils.setAttributes(element, attributes); | ||||
|         } | ||||
|  | ||||
|         // Add text node | ||||
|         if (utils.is.string(text)) { | ||||
|             element.textContent = text; | ||||
|         } | ||||
|  | ||||
|         // Return built element | ||||
|         return element; | ||||
|     }, | ||||
|  | ||||
|     // Insert a DocumentFragment | ||||
|     insertElement(type, parent, attributes, text) { | ||||
|         // Inject the new <element> | ||||
|         parent.appendChild(utils.createElement(type, attributes, text)); | ||||
|     }, | ||||
|  | ||||
|     // Remove all child elements | ||||
|     emptyElement(element) { | ||||
|         let { length } = element.childNodes; | ||||
|  | ||||
|         while (length > 0) { | ||||
|             element.removeChild(element.lastChild); | ||||
|             length -= 1; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Set attributes | ||||
|     setAttributes(element, attributes) { | ||||
|         Object.keys(attributes).forEach(key => { | ||||
|             element.setAttribute(key, attributes[key]); | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     // Get an attribute object from a string selector | ||||
|     getAttributesFromSelector(sel, existingAttributes) { | ||||
|         // For example: | ||||
|         // '.test' to { class: 'test' } | ||||
|         // '#test' to { id: 'test' } | ||||
|         // '[data-test="test"]' to { 'data-test': 'test' } | ||||
|  | ||||
|         if (!utils.is.string(sel) || utils.is.empty(sel)) { | ||||
|             return {}; | ||||
|         } | ||||
|  | ||||
|         const attributes = {}; | ||||
|         const existing = existingAttributes; | ||||
|  | ||||
|         sel.split(',').forEach(s => { | ||||
|             // Remove whitespace | ||||
|             const selector = s.trim(); | ||||
|             const className = selector.replace('.', ''); | ||||
|             const stripped = selector.replace(/[[\]]/g, ''); | ||||
|  | ||||
|             // Get the parts and value | ||||
|             const parts = stripped.split('='); | ||||
|             const key = parts[0]; | ||||
|             const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; | ||||
|  | ||||
|             // Get the first character | ||||
|             const start = selector.charAt(0); | ||||
|  | ||||
|             switch (start) { | ||||
|                 case '.': | ||||
|                     // Add to existing classname | ||||
|                     if (utils.is.object(existing) && utils.is.string(existing.class)) { | ||||
|                         existing.class += ` ${className}`; | ||||
|                     } | ||||
|  | ||||
|                     attributes.class = className; | ||||
|                     break; | ||||
|  | ||||
|                 case '#': | ||||
|                     // ID selector | ||||
|                     attributes.id = selector.replace('#', ''); | ||||
|                     break; | ||||
|  | ||||
|                 case '[': | ||||
|                     // Attribute selector | ||||
|                     attributes[key] = value; | ||||
|  | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return attributes; | ||||
|     }, | ||||
|  | ||||
|     // Toggle class on an element | ||||
|     toggleClass(element, className, toggle) { | ||||
|         if (utils.is.htmlElement(element)) { | ||||
|             const contains = element.classList.contains(className); | ||||
|  | ||||
|             element.classList[toggle ? 'add' : 'remove'](className); | ||||
|  | ||||
|             return (toggle && !contains) || (!toggle && contains); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     }, | ||||
|  | ||||
|     // Has class name | ||||
|     hasClass(element, className) { | ||||
|         return utils.is.htmlElement(element) && element.classList.contains(className); | ||||
|     }, | ||||
|  | ||||
|     // Element matches selector | ||||
|     matches(element, selector) { | ||||
|         const prototype = { Element }; | ||||
|  | ||||
|         function match() { | ||||
|             return Array.from(document.querySelectorAll(selector)).includes(this); | ||||
|         } | ||||
|  | ||||
|         const matches = | ||||
|             prototype.matches || | ||||
|             prototype.webkitMatchesSelector || | ||||
|             prototype.mozMatchesSelector || | ||||
|             prototype.msMatchesSelector || | ||||
|             match; | ||||
|  | ||||
|         return matches.call(element, selector); | ||||
|     }, | ||||
|  | ||||
|     // Find all elements | ||||
|     getElements(selector) { | ||||
|         return this.elements.container.querySelectorAll(selector); | ||||
|     }, | ||||
|  | ||||
|     // Find a single element | ||||
|     getElement(selector) { | ||||
|         return this.elements.container.querySelector(selector); | ||||
|     }, | ||||
|  | ||||
|     // Find the UI controls and store references in custom controls | ||||
|     // TODO: Allow settings menus with custom controls | ||||
|     findElements() { | ||||
|         try { | ||||
|             this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper); | ||||
|  | ||||
|             // Buttons | ||||
|             this.elements.buttons = { | ||||
|                 play: utils.getElements.call(this, this.config.selectors.buttons.play), | ||||
|                 pause: utils.getElement.call(this, this.config.selectors.buttons.pause), | ||||
|                 restart: utils.getElement.call(this, this.config.selectors.buttons.restart), | ||||
|                 rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind), | ||||
|                 forward: utils.getElement.call(this, this.config.selectors.buttons.forward), | ||||
|                 mute: utils.getElement.call(this, this.config.selectors.buttons.mute), | ||||
|                 pip: utils.getElement.call(this, this.config.selectors.buttons.pip), | ||||
|                 airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay), | ||||
|                 settings: utils.getElement.call(this, this.config.selectors.buttons.settings), | ||||
|                 captions: utils.getElement.call(this, this.config.selectors.buttons.captions), | ||||
|                 fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen), | ||||
|             }; | ||||
|  | ||||
|             // Progress | ||||
|             this.elements.progress = utils.getElement.call(this, this.config.selectors.progress); | ||||
|  | ||||
|             // Inputs | ||||
|             this.elements.inputs = { | ||||
|                 seek: utils.getElement.call(this, this.config.selectors.inputs.seek), | ||||
|                 volume: utils.getElement.call(this, this.config.selectors.inputs.volume), | ||||
|             }; | ||||
|  | ||||
|             // Display | ||||
|             this.elements.display = { | ||||
|                 buffer: utils.getElement.call(this, this.config.selectors.display.buffer), | ||||
|                 duration: utils.getElement.call(this, this.config.selectors.display.duration), | ||||
|                 currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime), | ||||
|             }; | ||||
|  | ||||
|             // Seek tooltip | ||||
|             if (utils.is.htmlElement(this.elements.progress)) { | ||||
|                 this.elements.display.seekTooltip = this.elements.progress.querySelector( | ||||
|                     `.${this.config.classNames.tooltip}` | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } catch (error) { | ||||
|             // Log it | ||||
|             this.warn('It looks like there is a problem with your custom controls HTML', error); | ||||
|  | ||||
|             // Restore native video controls | ||||
|             this.toggleNativeControls(true); | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Get the focused element | ||||
|     getFocusElement() { | ||||
|         let focused = document.activeElement; | ||||
|  | ||||
|         if (!focused || focused === document.body) { | ||||
|             focused = null; | ||||
|         } else { | ||||
|             focused = document.querySelector(':focus'); | ||||
|         } | ||||
|  | ||||
|         return focused; | ||||
|     }, | ||||
|  | ||||
|     // Trap focus inside container | ||||
|     trapFocus() { | ||||
|         const tabbables = utils.getElements.call(this, 'input:not([disabled]), button:not([disabled])'); | ||||
|         const first = tabbables[0]; | ||||
|         const last = tabbables[tabbables.length - 1]; | ||||
|  | ||||
|         utils.on( | ||||
|             this.elements.container, | ||||
|             'keydown', | ||||
|             event => { | ||||
|                 // If it is tab | ||||
|                 if (event.which === 9 && this.fullscreen.active) { | ||||
|                     if (event.target === last && !event.shiftKey) { | ||||
|                         // Move focus to first element that can be tabbed if Shift isn't used | ||||
|                         event.preventDefault(); | ||||
|                         first.focus(); | ||||
|                     } else if (event.target === first && event.shiftKey) { | ||||
|                         // Move focus to last element that can be tabbed if Shift is used | ||||
|                         event.preventDefault(); | ||||
|                         last.focus(); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             false | ||||
|         ); | ||||
|     }, | ||||
|  | ||||
|     // Bind along with custom handler | ||||
|     proxy(element, eventName, customListener, defaultListener, passive, capture) { | ||||
|         utils.on( | ||||
|             element, | ||||
|             eventName, | ||||
|             event => { | ||||
|                 if (customListener) { | ||||
|                     customListener.apply(element, [event]); | ||||
|                 } | ||||
|                 defaultListener.apply(element, [event]); | ||||
|             }, | ||||
|             passive, | ||||
|             capture | ||||
|         ); | ||||
|     }, | ||||
|  | ||||
|     // Toggle event listener | ||||
|     toggleListener(elements, event, callback, toggle, passive, capture) { | ||||
|         // Bail if no elements | ||||
|         if (elements === null || utils.is.undefined(elements)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // If a nodelist is passed, call itself on each node | ||||
|         if (elements instanceof NodeList) { | ||||
|             // Create listener for each node | ||||
|             Array.from(elements).forEach(element => { | ||||
|                 if (element instanceof Node) { | ||||
|                     utils.toggleListener.call(null, element, event, callback, toggle, passive, capture); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Allow multiple events | ||||
|         const events = event.split(' '); | ||||
|  | ||||
|         // Build options | ||||
|         // Default to just capture boolean | ||||
|         let options = utils.is.boolean(capture) ? capture : false; | ||||
|  | ||||
|         // If passive events listeners are supported | ||||
|         if (support.passiveListeners) { | ||||
|             options = { | ||||
|                 // Whether the listener can be passive (i.e. default never prevented) | ||||
|                 passive: utils.is.boolean(passive) ? passive : true, | ||||
|                 // Whether the listener is a capturing listener or not | ||||
|                 capture: utils.is.boolean(capture) ? capture : false, | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         // If a single node is passed, bind the event listener | ||||
|         events.forEach(type => { | ||||
|             elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     // Bind event handler | ||||
|     on(element, events, callback, passive, capture) { | ||||
|         utils.toggleListener(element, events, callback, true, passive, capture); | ||||
|     }, | ||||
|  | ||||
|     // Unbind event handler | ||||
|     off(element, events, callback, passive, capture) { | ||||
|         utils.toggleListener(element, events, callback, false, passive, capture); | ||||
|     }, | ||||
|  | ||||
|     // Trigger event | ||||
|     dispatchEvent(element, type, bubbles, properties) { | ||||
|         // Bail if no element | ||||
|         if (!element || !type) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Create and dispatch the event | ||||
|         const event = new CustomEvent(type, { | ||||
|             bubbles: utils.is.boolean(bubbles) ? bubbles : false, | ||||
|             detail: Object.assign({}, properties, { | ||||
|                 plyr: this instanceof Plyr ? this : null, | ||||
|             }), | ||||
|         }); | ||||
|  | ||||
|         // Dispatch the event | ||||
|         element.dispatchEvent(event); | ||||
|     }, | ||||
|  | ||||
|     // Toggle aria-pressed state on a toggle button | ||||
|     // http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles | ||||
|     toggleState(target, state) { | ||||
|         // Bail if no target | ||||
|         if (!target) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Get state | ||||
|         const newState = utils.is.boolean(state) ? state : !target.getAttribute('aria-pressed'); | ||||
|  | ||||
|         // Set the attribute on target | ||||
|         target.setAttribute('aria-pressed', newState); | ||||
|  | ||||
|         return newState; | ||||
|     }, | ||||
|  | ||||
|     // Get percentage | ||||
|     getPercentage(current, max) { | ||||
|         if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { | ||||
|             return 0; | ||||
|         } | ||||
|         return (current / max * 100).toFixed(2); | ||||
|     }, | ||||
|  | ||||
|     // Deep extend/merge destination object with N more objects | ||||
|     // http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/ | ||||
|     // Removed call to arguments.callee (used explicit function name instead) | ||||
|     extend(...objects) { | ||||
|         const { length } = objects; | ||||
|  | ||||
|         // Bail if nothing to merge | ||||
|         if (!length) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Return first if specified but nothing to merge | ||||
|         if (length === 1) { | ||||
|             return objects[0]; | ||||
|         } | ||||
|  | ||||
|         // First object is the destination | ||||
|         let destination = Array.prototype.shift.call(objects); | ||||
|         if (!utils.is.object(destination)) { | ||||
|             destination = {}; | ||||
|         } | ||||
|  | ||||
|         // Loop through all objects to merge | ||||
|         objects.forEach(source => { | ||||
|             if (!utils.is.object(source)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             Object.keys(source).forEach(property => { | ||||
|                 if (source[property] && source[property].constructor && source[property].constructor === Object) { | ||||
|                     destination[property] = destination[property] || {}; | ||||
|                     utils.extend(destination[property], source[property]); | ||||
|                 } else { | ||||
|                     destination[property] = source[property]; | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         return destination; | ||||
|     }, | ||||
|  | ||||
|     // Parse YouTube ID from URL | ||||
|     parseYouTubeId(url) { | ||||
|         const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; | ||||
|         return url.match(regex) ? RegExp.$2 : url; | ||||
|     }, | ||||
|  | ||||
|     // Parse Vimeo ID from URL | ||||
|     parseVimeoId(url) { | ||||
|         if (utils.is.number(Number(url))) { | ||||
|             return url; | ||||
|         } | ||||
|  | ||||
|         const regex = /^.*(vimeo.com\/|video\/)(\d+).*/; | ||||
|         return url.match(regex) ? RegExp.$2 : url; | ||||
|     }, | ||||
|  | ||||
|     // Convert object to URL parameters | ||||
|     buildUrlParameters(input) { | ||||
|         if (!utils.is.object(input)) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
|         return Object.keys(input) | ||||
|             .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`) | ||||
|             .join('&'); | ||||
|     }, | ||||
|  | ||||
|     // Remove HTML from a string | ||||
|     stripHTML(source) { | ||||
|         const fragment = document.createDocumentFragment(); | ||||
|         const element = document.createElement('div'); | ||||
|         fragment.appendChild(element); | ||||
|         element.innerHTML = source; | ||||
|         return fragment.firstChild.innerText; | ||||
|     }, | ||||
|  | ||||
|     // Load an SVG sprite | ||||
|     loadSprite(url, id) { | ||||
|         if (typeof url !== 'string') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const prefix = 'cache-'; | ||||
|         const hasId = typeof id === 'string'; | ||||
|         let isCached = false; | ||||
|  | ||||
|         function updateSprite(data) { | ||||
|             // Inject content | ||||
|             this.innerHTML = data; | ||||
|  | ||||
|             // Inject the SVG to the body | ||||
|             document.body.insertBefore(this, document.body.childNodes[0]); | ||||
|         } | ||||
|  | ||||
|         // Only load once | ||||
|         if (!hasId || !document.querySelectorAll(`#${id}`).length) { | ||||
|             // Create container | ||||
|             const container = document.createElement('div'); | ||||
|             container.setAttribute('hidden', ''); | ||||
|  | ||||
|             if (hasId) { | ||||
|                 container.setAttribute('id', id); | ||||
|             } | ||||
|  | ||||
|             // Check in cache | ||||
|             if (support.storage) { | ||||
|                 const cached = window.localStorage.getItem(prefix + id); | ||||
|                 isCached = cached !== null; | ||||
|  | ||||
|                 if (isCached) { | ||||
|                     const data = JSON.parse(cached); | ||||
|                     updateSprite.call(container, data.content); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // ReSharper disable once InconsistentNaming | ||||
|             const xhr = new XMLHttpRequest(); | ||||
|  | ||||
|             // XHR for Chrome/Firefox/Opera/Safari | ||||
|             if ('withCredentials' in xhr) { | ||||
|                 xhr.open('GET', url, true); | ||||
|             } else { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Once loaded, inject to container and body | ||||
|             xhr.onload = () => { | ||||
|                 if (support.storage) { | ||||
|                     window.localStorage.setItem( | ||||
|                         prefix + id, | ||||
|                         JSON.stringify({ | ||||
|                             content: xhr.responseText, | ||||
|                         }) | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 updateSprite.call(container, xhr.responseText); | ||||
|             }; | ||||
|  | ||||
|             xhr.send(); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Get the transition end event | ||||
|     transitionEnd: (() => { | ||||
|         const element = document.createElement('span'); | ||||
|  | ||||
|         const events = { | ||||
|             WebkitTransition: 'webkitTransitionEnd', | ||||
|             MozTransition: 'transitionend', | ||||
|             OTransition: 'oTransitionEnd otransitionend', | ||||
|             transition: 'transitionend', | ||||
|         }; | ||||
|  | ||||
|         const type = Object.keys(events).find(event => element.style[event] !== undefined); | ||||
|  | ||||
|         return typeof type === 'string' ? type : false; | ||||
|     })(), | ||||
| }; | ||||
|  | ||||
| export default utils; | ||||
		Reference in New Issue
	
	Block a user