Merge branch 'develop' into a11y-improvements
# Conflicts: # dist/plyr.js # dist/plyr.js.map # dist/plyr.min.js # dist/plyr.min.js.map # dist/plyr.polyfilled.js # dist/plyr.polyfilled.js.map # dist/plyr.polyfilled.min.js # dist/plyr.polyfilled.min.js.map # src/js/controls.js # src/js/fullscreen.js # src/js/plyr.js # src/js/ui.js # src/js/utils.js
This commit is contained in:
		| @ -6,7 +6,21 @@ | ||||
| import controls from './controls'; | ||||
| import i18n from './i18n'; | ||||
| import support from './support'; | ||||
| import utils from './utils'; | ||||
| import { dedupe } from './utils/arrays'; | ||||
| import browser from './utils/browser'; | ||||
| import { | ||||
|     createElement, | ||||
|     emptyElement, | ||||
|     getAttributesFromSelector, | ||||
|     insertAfter, | ||||
|     removeElement, | ||||
|     toggleClass, | ||||
| } from './utils/elements'; | ||||
| import { on, triggerEvent } from './utils/events'; | ||||
| import fetch from './utils/fetch'; | ||||
| import is from './utils/is'; | ||||
| import { getHTML } from './utils/strings'; | ||||
| import { parseUrl } from './utils/urls'; | ||||
|  | ||||
| const captions = { | ||||
|     // Setup captions | ||||
| @ -19,7 +33,11 @@ const captions = { | ||||
|         // Only Vimeo and HTML5 video supported at this point | ||||
|         if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { | ||||
|             // Clear menu and hide | ||||
|             if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { | ||||
|             if ( | ||||
|                 is.array(this.config.controls) && | ||||
|                 this.config.controls.includes('settings') && | ||||
|                 this.config.settings.includes('captions') | ||||
|             ) { | ||||
|                 controls.setCaptionsMenu.call(this); | ||||
|             } | ||||
|  | ||||
| @ -27,15 +45,12 @@ const captions = { | ||||
|         } | ||||
|  | ||||
|         // Inject the container | ||||
|         if (!utils.is.element(this.elements.captions)) { | ||||
|             this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); | ||||
|         if (!is.element(this.elements.captions)) { | ||||
|             this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); | ||||
|  | ||||
|             utils.insertAfter(this.elements.captions, this.elements.wrapper); | ||||
|             insertAfter(this.elements.captions, this.elements.wrapper); | ||||
|         } | ||||
|  | ||||
|         // Get browser info | ||||
|         const browser = utils.getBrowser(); | ||||
|  | ||||
|         // Fix IE captions if CORS is used | ||||
|         // Fetch captions and inject as blobs instead (data URIs not supported!) | ||||
|         if (browser.isIE && window.URL) { | ||||
| @ -43,84 +58,96 @@ const captions = { | ||||
|  | ||||
|             Array.from(elements).forEach(track => { | ||||
|                 const src = track.getAttribute('src'); | ||||
|                 const href = utils.parseUrl(src); | ||||
|                 const url = parseUrl(src); | ||||
|  | ||||
|                 if (href.hostname !== window.location.href.hostname && [ | ||||
|                     'http:', | ||||
|                     'https:', | ||||
|                 ].includes(href.protocol)) { | ||||
|                     utils | ||||
|                         .fetch(src, 'blob') | ||||
|                 if ( | ||||
|                     url !== null && | ||||
|                     url.hostname !== window.location.href.hostname && | ||||
|                     ['http:', 'https:'].includes(url.protocol) | ||||
|                 ) { | ||||
|                     fetch(src, 'blob') | ||||
|                         .then(blob => { | ||||
|                             track.setAttribute('src', window.URL.createObjectURL(blob)); | ||||
|                         }) | ||||
|                         .catch(() => { | ||||
|                             utils.removeElement(track); | ||||
|                             removeElement(track); | ||||
|                         }); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // Try to load the value from storage | ||||
|         let active = this.storage.get('captions'); | ||||
|         // Get and set initial data | ||||
|         // The "preferred" options are not realized unless / until the wanted language has a match | ||||
|         // * languages: Array of user's browser languages. | ||||
|         // * language:  The language preferred by user settings or config | ||||
|         // * active:    The state preferred by user settings or config | ||||
|         // * toggled:   The real captions state | ||||
|  | ||||
|         // Otherwise fall back to the default config | ||||
|         if (!utils.is.boolean(active)) { | ||||
|         const languages = dedupe( | ||||
|             Array.from(navigator.languages || navigator.userLanguage).map(language => language.split('-')[0]), | ||||
|         ); | ||||
|  | ||||
|         let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); | ||||
|  | ||||
|         // Use first browser language when language is 'auto' | ||||
|         if (language === 'auto') { | ||||
|             [language] = languages; | ||||
|         } | ||||
|  | ||||
|         let active = this.storage.get('captions'); | ||||
|         if (!is.boolean(active)) { | ||||
|             ({ active } = this.config.captions); | ||||
|         } | ||||
|  | ||||
|         // Get language from storage, fallback to config | ||||
|         let language = this.storage.get('language') || this.config.captions.language; | ||||
|         if (language === 'auto') { | ||||
|             [ language ] = (navigator.language || navigator.userLanguage).split('-'); | ||||
|         } | ||||
|         // Set language and show if active | ||||
|         captions.setLanguage.call(this, language, active); | ||||
|         Object.assign(this.captions, { | ||||
|             toggled: false, | ||||
|             active, | ||||
|             language, | ||||
|             languages, | ||||
|         }); | ||||
|  | ||||
|         // Watch changes to textTracks and update captions menu | ||||
|         if (this.isHTML5) { | ||||
|             const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; | ||||
|             utils.on(this.media.textTracks, trackEvents, captions.update.bind(this)); | ||||
|             on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this)); | ||||
|         } | ||||
|  | ||||
|         // Update available languages in list next tick (the event must not be triggered before the listeners) | ||||
|         setTimeout(captions.update.bind(this), 0); | ||||
|     }, | ||||
|  | ||||
|     // Update available language options in settings based on tracks | ||||
|     update() { | ||||
|         const tracks = captions.getTracks.call(this, true); | ||||
|         // Get the wanted language | ||||
|         const { language, meta } = this.captions; | ||||
|         const { active, language, meta, currentTrackNode } = this.captions; | ||||
|         const languageExists = Boolean(tracks.find(track => track.language === language)); | ||||
|  | ||||
|         // Handle tracks (add event listener and "pseudo"-default) | ||||
|         if (this.isHTML5 && this.isVideo) { | ||||
|             tracks | ||||
|                 .filter(track => !meta.get(track)) | ||||
|                 .forEach(track => { | ||||
|                     this.debug.log('Track added', track); | ||||
|                     // Attempt to store if the original dom element was "default" | ||||
|                     meta.set(track, { | ||||
|                         default: track.mode === 'showing', | ||||
|                     }); | ||||
|  | ||||
|                     // Turn off native caption rendering to avoid double captions | ||||
|                     track.mode = 'hidden'; | ||||
|  | ||||
|                     // Add event listener for cue changes | ||||
|                     utils.on(track, 'cuechange', () => captions.updateCues.call(this)); | ||||
|             tracks.filter(track => !meta.get(track)).forEach(track => { | ||||
|                 this.debug.log('Track added', track); | ||||
|                 // Attempt to store if the original dom element was "default" | ||||
|                 meta.set(track, { | ||||
|                     default: track.mode === 'showing', | ||||
|                 }); | ||||
|  | ||||
|                 // Turn off native caption rendering to avoid double captions | ||||
|                 track.mode = 'hidden'; | ||||
|  | ||||
|                 // Add event listener for cue changes | ||||
|                 on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode); | ||||
|         const firstMatch = this.language !== language && tracks.find(track => track.language === language); | ||||
|  | ||||
|         // Update language if removed or first matching track added | ||||
|         if (trackRemoved || firstMatch) { | ||||
|             captions.setLanguage.call(this, language, this.config.captions.active); | ||||
|         // Update language first time it matches, or if the previous matching track was removed | ||||
|         if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) { | ||||
|             captions.setLanguage.call(this, language); | ||||
|             captions.toggle.call(this, active && languageExists); | ||||
|         } | ||||
|  | ||||
|         // Enable or disable captions based on track length | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks)); | ||||
|         toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); | ||||
|  | ||||
|         // Update available languages in list | ||||
|         if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { | ||||
| @ -128,16 +155,70 @@ const captions = { | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     set(index, setLanguage = true, show = true) { | ||||
|     // Toggle captions display | ||||
|     // Used internally for the toggleCaptions method, with the passive option forced to false | ||||
|     toggle(input, passive = true) { | ||||
|         // If there's no full support | ||||
|         if (!this.supported.ui) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const { toggled } = this.captions; // Current state | ||||
|         const activeClass = this.config.classNames.captions.active; | ||||
|  | ||||
|         // Get the next state | ||||
|         // If the method is called without parameter, toggle based on current value | ||||
|         const active = is.nullOrUndefined(input) ? !toggled : input; | ||||
|  | ||||
|         // Update state and trigger event | ||||
|         if (active !== toggled) { | ||||
|             // When passive, don't override user preferences | ||||
|             if (!passive) { | ||||
|                 this.captions.active = active; | ||||
|                 this.storage.set({ captions: active }); | ||||
|             } | ||||
|  | ||||
|             // Force language if the call isn't passive and there is no matching language to toggle to | ||||
|             if (!this.language && active && !passive) { | ||||
|                 const tracks = captions.getTracks.call(this); | ||||
|                 const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true); | ||||
|  | ||||
|                 // Override user preferences to avoid switching languages if a matching track is added | ||||
|                 this.captions.language = track.language; | ||||
|  | ||||
|                 // Set caption, but don't store in localStorage as user preference | ||||
|                 captions.set.call(this, tracks.indexOf(track)); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Toggle state | ||||
|             this.elements.buttons.captions.pressed = active; | ||||
|  | ||||
|             // Add class hook | ||||
|             toggleClass(this.elements.container, activeClass, active); | ||||
|  | ||||
|             this.captions.toggled = active; | ||||
|  | ||||
|             // Update settings menu | ||||
|             controls.updateSetting.call(this, 'captions'); | ||||
|  | ||||
|             // Trigger event (not used internally) | ||||
|             triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Set captions by track index | ||||
|     // Used internally for the currentTrack setter with the passive option forced to false | ||||
|     set(index, passive = true) { | ||||
|         const tracks = captions.getTracks.call(this); | ||||
|  | ||||
|         // Disable captions if setting to -1 | ||||
|         if (index === -1) { | ||||
|             this.toggleCaptions(false); | ||||
|             captions.toggle.call(this, false, passive); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!utils.is.number(index)) { | ||||
|         if (!is.number(index)) { | ||||
|             this.debug.warn('Invalid caption argument', index); | ||||
|             return; | ||||
|         } | ||||
| @ -149,15 +230,19 @@ const captions = { | ||||
|  | ||||
|         if (this.captions.currentTrack !== index) { | ||||
|             this.captions.currentTrack = index; | ||||
|             const track = captions.getCurrentTrack.call(this); | ||||
|             const track = tracks[index]; | ||||
|             const { language } = track || {}; | ||||
|  | ||||
|             // Store reference to node for invalidation on remove | ||||
|             this.captions.currentTrackNode = track; | ||||
|  | ||||
|             // Prevent setting language in some cases, since it can violate user's intentions | ||||
|             if (setLanguage) { | ||||
|             // Update settings menu | ||||
|             controls.updateSetting.call(this, 'captions'); | ||||
|  | ||||
|             // When passive, don't override user preferences | ||||
|             if (!passive) { | ||||
|                 this.captions.language = language; | ||||
|                 this.storage.set({ language }); | ||||
|             } | ||||
|  | ||||
|             // Handle Vimeo captions | ||||
| @ -166,32 +251,33 @@ const captions = { | ||||
|             } | ||||
|  | ||||
|             // Trigger event | ||||
|             utils.dispatchEvent.call(this, this.media, 'languagechange'); | ||||
|             triggerEvent.call(this, this.media, 'languagechange'); | ||||
|         } | ||||
|  | ||||
|         // Show captions | ||||
|         captions.toggle.call(this, true, passive); | ||||
|  | ||||
|         if (this.isHTML5 && this.isVideo) { | ||||
|             // If we change the active track while a cue is already displayed we need to update it | ||||
|             captions.updateCues.call(this); | ||||
|         } | ||||
|  | ||||
|         // Show captions | ||||
|         if (show) { | ||||
|             this.toggleCaptions(true); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     setLanguage(language, show = true) { | ||||
|         if (!utils.is.string(language)) { | ||||
|             this.debug.warn('Invalid language argument', language); | ||||
|     // Set captions by language | ||||
|     // Used internally for the language setter with the passive option forced to false | ||||
|     setLanguage(input, passive = true) { | ||||
|         if (!is.string(input)) { | ||||
|             this.debug.warn('Invalid language argument', input); | ||||
|             return; | ||||
|         } | ||||
|         // Normalize | ||||
|         this.captions.language = language.toLowerCase(); | ||||
|         const language = input.toLowerCase(); | ||||
|         this.captions.language = language; | ||||
|  | ||||
|         // Set currentTrack | ||||
|         const tracks = captions.getTracks.call(this); | ||||
|         const track = captions.getCurrentTrack.call(this, true); | ||||
|         captions.set.call(this, tracks.indexOf(track), false, show); | ||||
|         const track = captions.findTrack.call(this, [language]); | ||||
|         captions.set.call(this, tracks.indexOf(track), passive); | ||||
|     }, | ||||
|  | ||||
|     // Get current valid caption tracks | ||||
| @ -204,34 +290,42 @@ const captions = { | ||||
|         // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) | ||||
|         return tracks | ||||
|             .filter(track => !this.isHTML5 || update || this.captions.meta.has(track)) | ||||
|             .filter(track => [ | ||||
|                 'captions', | ||||
|                 'subtitles', | ||||
|             ].includes(track.kind)); | ||||
|             .filter(track => ['captions', 'subtitles'].includes(track.kind)); | ||||
|     }, | ||||
|  | ||||
|     // Get the current track for the current language | ||||
|     getCurrentTrack(fromLanguage = false) { | ||||
|     // Match tracks based on languages and get the first | ||||
|     findTrack(languages, force = false) { | ||||
|         const tracks = captions.getTracks.call(this); | ||||
|         const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); | ||||
|         const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); | ||||
|         return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0]; | ||||
|         let track; | ||||
|         languages.every(language => { | ||||
|             track = sorted.find(track => track.language === language); | ||||
|             return !track; // Break iteration if there is a match | ||||
|         }); | ||||
|         // If no match is found but is required, get first | ||||
|         return track || (force ? sorted[0] : undefined); | ||||
|     }, | ||||
|  | ||||
|     // Get the current track | ||||
|     getCurrentTrack() { | ||||
|         return captions.getTracks.call(this)[this.currentTrack]; | ||||
|     }, | ||||
|  | ||||
|     // Get UI label for track | ||||
|     getLabel(track) { | ||||
|         let currentTrack = track; | ||||
|  | ||||
|         if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) { | ||||
|         if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { | ||||
|             currentTrack = captions.getCurrentTrack.call(this); | ||||
|         } | ||||
|  | ||||
|         if (utils.is.track(currentTrack)) { | ||||
|             if (!utils.is.empty(currentTrack.label)) { | ||||
|         if (is.track(currentTrack)) { | ||||
|             if (!is.empty(currentTrack.label)) { | ||||
|                 return currentTrack.label; | ||||
|             } | ||||
|  | ||||
|             if (!utils.is.empty(currentTrack.language)) { | ||||
|             if (!is.empty(currentTrack.language)) { | ||||
|                 return track.language.toUpperCase(); | ||||
|             } | ||||
|  | ||||
| @ -249,13 +343,13 @@ const captions = { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!utils.is.element(this.elements.captions)) { | ||||
|         if (!is.element(this.elements.captions)) { | ||||
|             this.debug.warn('No captions element to render to'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Only accept array or empty input | ||||
|         if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) { | ||||
|         if (!is.nullOrUndefined(input) && !Array.isArray(input)) { | ||||
|             this.debug.warn('updateCues: Invalid input', input); | ||||
|             return; | ||||
|         } | ||||
| @ -267,7 +361,7 @@ const captions = { | ||||
|             const track = captions.getCurrentTrack.call(this); | ||||
|             cues = Array.from((track || {}).activeCues || []) | ||||
|                 .map(cue => cue.getCueAsHTML()) | ||||
|                 .map(utils.getHTML); | ||||
|                 .map(getHTML); | ||||
|         } | ||||
|  | ||||
|         // Set new caption text | ||||
| @ -276,13 +370,13 @@ const captions = { | ||||
|  | ||||
|         if (changed) { | ||||
|             // Empty the container and create a new child element | ||||
|             utils.emptyElement(this.elements.captions); | ||||
|             const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption)); | ||||
|             emptyElement(this.elements.captions); | ||||
|             const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); | ||||
|             caption.innerHTML = content; | ||||
|             this.elements.captions.appendChild(caption); | ||||
|  | ||||
|             // Trigger event | ||||
|             utils.dispatchEvent.call(this, this.media, 'cuechange'); | ||||
|             triggerEvent.call(this, this.media, 'cuechange'); | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| @ -93,15 +93,7 @@ const defaults = { | ||||
|     // Speed default and options to display
 | ||||
|     speed: { | ||||
|         selected: 1, | ||||
|         options: [ | ||||
|             0.5, | ||||
|             0.75, | ||||
|             1, | ||||
|             1.25, | ||||
|             1.5, | ||||
|             1.75, | ||||
|             2, | ||||
|         ], | ||||
|         options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], | ||||
|     }, | ||||
| 
 | ||||
|     // Keyboard shortcut settings
 | ||||
| @ -155,11 +147,7 @@ const defaults = { | ||||
|         'airplay', | ||||
|         'fullscreen', | ||||
|     ], | ||||
|     settings: [ | ||||
|         'captions', | ||||
|         'quality', | ||||
|         'speed', | ||||
|     ], | ||||
|     settings: ['captions', 'quality', 'speed'], | ||||
| 
 | ||||
|     // Localisation
 | ||||
|     i18n: { | ||||
| @ -215,7 +203,8 @@ const defaults = { | ||||
|         }, | ||||
|         youtube: { | ||||
|             sdk: 'https://www.youtube.com/iframe_api', | ||||
|             api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet', | ||||
|             api: | ||||
|                 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet', | ||||
|         }, | ||||
|         googleIMA: { | ||||
|             sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', | ||||
| @ -13,4 +13,22 @@ export const types = { | ||||
|     video: 'video', | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Get provider by URL | ||||
|  * @param {string} url | ||||
|  */ | ||||
| export function getProviderByUrl(url) { | ||||
|     // YouTube
 | ||||
|     if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) { | ||||
|         return providers.youtube; | ||||
|     } | ||||
| 
 | ||||
|     // Vimeo
 | ||||
|     if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { | ||||
|         return providers.vimeo; | ||||
|     } | ||||
| 
 | ||||
|     return null; | ||||
| } | ||||
| 
 | ||||
| export default { providers, types }; | ||||
							
								
								
									
										478
									
								
								src/js/controls.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										478
									
								
								src/js/controls.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -3,9 +3,10 @@ | ||||
| // https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing | ||||
| // ========================================================================== | ||||
|  | ||||
| import utils from './utils'; | ||||
|  | ||||
| const browser = utils.getBrowser(); | ||||
| import browser from './utils/browser'; | ||||
| import { hasClass, toggleClass, trapFocus } from './utils/elements'; | ||||
| import { on, triggerEvent } from './utils/events'; | ||||
| import is from './utils/is'; | ||||
|  | ||||
| function onChange() { | ||||
|     if (!this.enabled) { | ||||
| @ -14,16 +15,16 @@ function onChange() { | ||||
|  | ||||
|     // Update toggle button | ||||
|     const button = this.player.elements.buttons.fullscreen; | ||||
|     if (utils.is.element(button)) { | ||||
|     if (is.element(button)) { | ||||
|         button.pressed = this.active; | ||||
|     } | ||||
|  | ||||
|     // Trigger an event | ||||
|     utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); | ||||
|     triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); | ||||
|  | ||||
|     // Trap focus in container | ||||
|     if (!browser.isIos) { | ||||
|         utils.trapFocus.call(this.player, this.target, this.active); | ||||
|         trapFocus.call(this.player, this.target, this.active); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -42,7 +43,7 @@ function toggleFallback(toggle = false) { | ||||
|     document.body.style.overflow = toggle ? 'hidden' : ''; | ||||
|  | ||||
|     // Toggle class hook | ||||
|     utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); | ||||
|     toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); | ||||
|  | ||||
|     // Toggle button and fire events | ||||
|     onChange.call(this); | ||||
| @ -62,15 +63,20 @@ class Fullscreen { | ||||
|  | ||||
|         // Register event listeners | ||||
|         // Handle event (incase user presses escape etc) | ||||
|         utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { | ||||
|         on.call( | ||||
|             this.player, | ||||
|             document, | ||||
|             this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, | ||||
|             () => { | ||||
|             // TODO: Filter for target?? | ||||
|             onChange.call(this); | ||||
|         }); | ||||
|             }, | ||||
|         ); | ||||
|  | ||||
|         // Fullscreen toggle on double click | ||||
|         utils.on(this.player.elements.container, 'dblclick', event => { | ||||
|         on.call(this.player, this.player.elements.container, 'dblclick', event => { | ||||
|             // Ignore double click in controls | ||||
|             if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { | ||||
|             if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @ -83,26 +89,27 @@ class Fullscreen { | ||||
|  | ||||
|     // Determine if native supported | ||||
|     static get native() { | ||||
|         return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); | ||||
|         return !!( | ||||
|             document.fullscreenEnabled || | ||||
|             document.webkitFullscreenEnabled || | ||||
|             document.mozFullScreenEnabled || | ||||
|             document.msFullscreenEnabled | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // Get the prefix for handlers | ||||
|     static get prefix() { | ||||
|         // No prefix | ||||
|         if (utils.is.function(document.exitFullscreen)) { | ||||
|         if (is.function(document.exitFullscreen)) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
|         // Check for fullscreen support by vendor prefix | ||||
|         let value = ''; | ||||
|         const prefixes = [ | ||||
|             'webkit', | ||||
|             'moz', | ||||
|             'ms', | ||||
|         ]; | ||||
|         const prefixes = ['webkit', 'moz', 'ms']; | ||||
|  | ||||
|         prefixes.some(pre => { | ||||
|             if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) { | ||||
|             if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) { | ||||
|                 value = pre; | ||||
|                 return true; | ||||
|             } | ||||
| @ -135,7 +142,7 @@ class Fullscreen { | ||||
|  | ||||
|         // Fallback using classname | ||||
|         if (!Fullscreen.native) { | ||||
|             return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback); | ||||
|             return hasClass(this.target, this.player.config.classNames.fullscreen.fallback); | ||||
|         } | ||||
|  | ||||
|         const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`]; | ||||
| @ -145,7 +152,9 @@ class Fullscreen { | ||||
|  | ||||
|     // Get target element | ||||
|     get target() { | ||||
|         return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; | ||||
|         return browser.isIos && this.player.config.fullscreen.iosNative | ||||
|             ? this.player.media | ||||
|             : this.player.elements.container; | ||||
|     } | ||||
|  | ||||
|     // Update UI | ||||
| @ -157,7 +166,7 @@ class Fullscreen { | ||||
|         } | ||||
|  | ||||
|         // Add styling hook to show button | ||||
|         utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled); | ||||
|         toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled); | ||||
|     } | ||||
|  | ||||
|     // Make an element fullscreen | ||||
| @ -175,7 +184,7 @@ class Fullscreen { | ||||
|             toggleFallback.call(this, true); | ||||
|         } else if (!this.prefix) { | ||||
|             this.target.requestFullscreen(); | ||||
|         } else if (!utils.is.empty(this.prefix)) { | ||||
|         } else if (!is.empty(this.prefix)) { | ||||
|             this.target[`${this.prefix}Request${this.property}`](); | ||||
|         } | ||||
|     } | ||||
| @ -194,7 +203,7 @@ class Fullscreen { | ||||
|             toggleFallback.call(this, false); | ||||
|         } else if (!this.prefix) { | ||||
|             (document.cancelFullScreen || document.exitFullscreen).call(document); | ||||
|         } else if (!utils.is.empty(this.prefix)) { | ||||
|         } else if (!is.empty(this.prefix)) { | ||||
|             const action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; | ||||
|             document[`${this.prefix}${action}${this.property}`](); | ||||
|         } | ||||
|  | ||||
| @ -3,40 +3,28 @@ | ||||
| // ========================================================================== | ||||
|  | ||||
| import support from './support'; | ||||
| import utils from './utils'; | ||||
| import { removeElement } from './utils/elements'; | ||||
| import { triggerEvent } from './utils/events'; | ||||
|  | ||||
| const html5 = { | ||||
|     getSources() { | ||||
|         if (!this.isHTML5) { | ||||
|             return null; | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         return this.media.querySelectorAll('source'); | ||||
|         const sources = Array.from(this.media.querySelectorAll('source')); | ||||
|  | ||||
|         // Filter out unsupported sources | ||||
|         return sources.filter(source => support.mime.call(this, source.getAttribute('type'))); | ||||
|     }, | ||||
|  | ||||
|     // Get quality levels | ||||
|     getQualityOptions() { | ||||
|         if (!this.isHTML5) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Get sources | ||||
|         const sources = html5.getSources.call(this); | ||||
|  | ||||
|         if (utils.is.empty(sources)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Get <source> with size attribute | ||||
|         const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size'))); | ||||
|  | ||||
|         // If none, bail | ||||
|         if (utils.is.empty(sizes)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Reduce to unique list | ||||
|         return utils.dedupe(sizes.map(source => Number(source.getAttribute('size')))); | ||||
|         // Get sizes from <source> elements | ||||
|         return html5.getSources | ||||
|             .call(this) | ||||
|             .map(source => Number(source.getAttribute('size'))) | ||||
|             .filter(Boolean); | ||||
|     }, | ||||
|  | ||||
|     extend() { | ||||
| @ -51,60 +39,34 @@ const html5 = { | ||||
|             get() { | ||||
|                 // Get sources | ||||
|                 const sources = html5.getSources.call(player); | ||||
|                 const [source] = sources.filter(source => source.getAttribute('src') === player.source); | ||||
|  | ||||
|                 if (utils.is.empty(sources)) { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source); | ||||
|  | ||||
|                 if (utils.is.empty(matches)) { | ||||
|                     return null; | ||||
|                 } | ||||
|  | ||||
|                 return Number(matches[0].getAttribute('size')); | ||||
|                 // Return size, if match is found | ||||
|                 return source && Number(source.getAttribute('size')); | ||||
|             }, | ||||
|             set(input) { | ||||
|                 // Get sources | ||||
|                 const sources = html5.getSources.call(player); | ||||
|  | ||||
|                 if (utils.is.empty(sources)) { | ||||
|                 // Get first match for requested size | ||||
|                 const source = sources.find(source => Number(source.getAttribute('size')) === input); | ||||
|  | ||||
|                 // No matching source found | ||||
|                 if (!source) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 // Get matches for requested size | ||||
|                 const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input); | ||||
|  | ||||
|                 // No matches for requested size | ||||
|                 if (utils.is.empty(matches)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 // Get supported sources | ||||
|                 const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type'))); | ||||
|  | ||||
|                 // No supported sources | ||||
|                 if (utils.is.empty(supported)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 // Trigger change event | ||||
|                 utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { | ||||
|                     quality: input, | ||||
|                 }); | ||||
|  | ||||
|                 // Get current state | ||||
|                 const { currentTime, playing } = player; | ||||
|  | ||||
|                 // Set new source | ||||
|                 player.media.src = supported[0].getAttribute('src'); | ||||
|                 player.media.src = source.getAttribute('src'); | ||||
|  | ||||
|                 // Restore time | ||||
|                 const onLoadedMetaData = () => { | ||||
|                     player.currentTime = currentTime; | ||||
|                     player.off('loadedmetadata', onLoadedMetaData); | ||||
|                 }; | ||||
|                 player.on('loadedmetadata', onLoadedMetaData); | ||||
|                 player.once('loadedmetadata', onLoadedMetaData); | ||||
|  | ||||
|                 // Load new source | ||||
|                 player.media.load(); | ||||
| @ -115,7 +77,7 @@ const html5 = { | ||||
|                 } | ||||
|  | ||||
|                 // Trigger change event | ||||
|                 utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { | ||||
|                 triggerEvent.call(player, player.media, 'qualitychange', false, { | ||||
|                     quality: input, | ||||
|                 }); | ||||
|             }, | ||||
| @ -130,7 +92,7 @@ const html5 = { | ||||
|         } | ||||
|  | ||||
|         // Remove child sources | ||||
|         utils.removeElement(html5.getSources()); | ||||
|         removeElement(html5.getSources.call(this)); | ||||
|  | ||||
|         // Set blank video src attribute | ||||
|         // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error | ||||
|  | ||||
| @ -2,17 +2,19 @@ | ||||
| // Plyr internationalization | ||||
| // ========================================================================== | ||||
|  | ||||
| import utils from './utils'; | ||||
| import is from './utils/is'; | ||||
| import { getDeep } from './utils/objects'; | ||||
| import { replaceAll } from './utils/strings'; | ||||
|  | ||||
| const i18n = { | ||||
|     get(key = '', config = {}) { | ||||
|         if (utils.is.empty(key) || utils.is.empty(config)) { | ||||
|         if (is.empty(key) || is.empty(config)) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
|         let string = utils.getDeep(config.i18n, key); | ||||
|         let string = getDeep(config.i18n, key); | ||||
|  | ||||
|         if (utils.is.empty(string)) { | ||||
|         if (is.empty(string)) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
| @ -21,11 +23,8 @@ const i18n = { | ||||
|             '{title}': config.title, | ||||
|         }; | ||||
|  | ||||
|         Object.entries(replace).forEach(([ | ||||
|             key, | ||||
|             value, | ||||
|         ]) => { | ||||
|             string = utils.replaceAll(string, key, value); | ||||
|         Object.entries(replace).forEach(([key, value]) => { | ||||
|             string = replaceAll(string, key, value); | ||||
|         }); | ||||
|  | ||||
|         return string; | ||||
|  | ||||
| @ -4,10 +4,10 @@ | ||||
|  | ||||
| import controls from './controls'; | ||||
| import ui from './ui'; | ||||
| import utils from './utils'; | ||||
|  | ||||
| // Sniff out the browser | ||||
| const browser = utils.getBrowser(); | ||||
| import browser from './utils/browser'; | ||||
| import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements'; | ||||
| import { on, once, toggleListener, triggerEvent } from './utils/events'; | ||||
| import is from './utils/is'; | ||||
|  | ||||
| class Listeners { | ||||
|     constructor(player) { | ||||
| @ -32,7 +32,7 @@ class Listeners { | ||||
|  | ||||
|         // If the event is bubbled from the media element | ||||
|         // Firefox doesn't get the keycode for whatever reason | ||||
|         if (!utils.is.number(code)) { | ||||
|         if (!is.number(code)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -46,37 +46,16 @@ class Listeners { | ||||
|         // 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 preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79]; | ||||
|  | ||||
|             // Check focused element | ||||
|             // and if the focused element is not editable (e.g. text input) | ||||
|             // and any that accept key input http://webaim.org/techniques/keyboard/ | ||||
|             const focused = utils.getFocusElement(); | ||||
|             if (utils.is.element(focused) && ( | ||||
|                 focused !== this.player.elements.inputs.seek && | ||||
|                 utils.matches(focused, this.player.config.selectors.editable)) | ||||
|             const focused = getFocusElement(); | ||||
|             if ( | ||||
|                 is.element(focused) && | ||||
|                 (focused !== this.player.elements.inputs.seek && | ||||
|                     matches(focused, this.player.config.selectors.editable)) | ||||
|             ) { | ||||
|                 return; | ||||
|             } | ||||
| @ -195,41 +174,37 @@ class Listeners { | ||||
|         this.player.touch = true; | ||||
|  | ||||
|         // Add touch class | ||||
|         utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); | ||||
|  | ||||
|         // Clean up | ||||
|         utils.off(document.body, 'touchstart', this.firstTouch); | ||||
|         toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); | ||||
|     } | ||||
|  | ||||
|     // Global window & document listeners | ||||
|     global(toggle = true) { | ||||
|         // Keyboard shortcuts | ||||
|         if (this.player.config.keyboard.global) { | ||||
|             utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false); | ||||
|             toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false); | ||||
|         } | ||||
|  | ||||
|         // Click anywhere closes menu | ||||
|         utils.toggleListener(document.body, 'click', this.toggleMenu, toggle); | ||||
|         toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle); | ||||
|  | ||||
|         // Detect touch by events | ||||
|         utils.on(document.body, 'touchstart', this.firstTouch); | ||||
|         once.call(this.player, document.body, 'touchstart', this.firstTouch); | ||||
|     } | ||||
|  | ||||
|     // Container listeners | ||||
|     container() { | ||||
|         // Keyboard shortcuts | ||||
|         if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { | ||||
|             utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false); | ||||
|             on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false); | ||||
|         } | ||||
|  | ||||
|         // Detect tab focus | ||||
|         // Remove class on blur/focusout | ||||
|         utils.on(this.player.elements.container, 'focusout', event => { | ||||
|             utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false); | ||||
|         on.call(this.player, this.player.elements.container, 'focusout', event => { | ||||
|             toggleClass(event.target, this.player.config.classNames.tabFocus, false); | ||||
|         }); | ||||
|  | ||||
|         // Add classname to tabbed elements | ||||
|         utils.on(this.player.elements.container, 'keydown', event => { | ||||
|         on.call(this.player, this.player.elements.container, 'keydown', event => { | ||||
|             if (event.keyCode !== 9) { | ||||
|                 return; | ||||
|             } | ||||
| @ -237,59 +212,64 @@ class Listeners { | ||||
|             // Delay the adding of classname until the focus has changed | ||||
|             // This event fires before the focusin event | ||||
|             setTimeout(() => { | ||||
|                 utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true); | ||||
|                 toggleClass(getFocusElement(), this.player.config.classNames.tabFocus, true); | ||||
|             }, 0); | ||||
|         }); | ||||
|  | ||||
|         // Toggle controls on mouse events and entering fullscreen | ||||
|         utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { | ||||
|             const { controls } = this.player.elements; | ||||
|         on.call( | ||||
|             this.player, | ||||
|             this.player.elements.container, | ||||
|             'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', | ||||
|             event => { | ||||
|                 const { controls } = this.player.elements; | ||||
|  | ||||
|             // Remove button states for fullscreen | ||||
|             if (event.type === 'enterfullscreen') { | ||||
|                 controls.pressed = false; | ||||
|                 controls.hover = false; | ||||
|             } | ||||
|                 // Remove button states for fullscreen | ||||
|                 if (event.type === 'enterfullscreen') { | ||||
|                     controls.pressed = false; | ||||
|                     controls.hover = false; | ||||
|                 } | ||||
|  | ||||
|             // Show, then hide after a timeout unless another control event occurs | ||||
|             const show = [ | ||||
|                 'touchstart', | ||||
|                 'touchmove', | ||||
|                 'mousemove', | ||||
|             ].includes(event.type); | ||||
|                 // Show, then hide after a timeout unless another control event occurs | ||||
|                 const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); | ||||
|  | ||||
|             let delay = 0; | ||||
|                 let delay = 0; | ||||
|  | ||||
|             if (show) { | ||||
|                 ui.toggleControls.call(this.player, true); | ||||
|                 // Use longer timeout for touch devices | ||||
|                 delay = this.player.touch ? 3000 : 2000; | ||||
|             } | ||||
|                 if (show) { | ||||
|                     ui.toggleControls.call(this.player, true); | ||||
|                     // Use longer timeout for touch devices | ||||
|                     delay = this.player.touch ? 3000 : 2000; | ||||
|                 } | ||||
|  | ||||
|             // Clear timer | ||||
|             clearTimeout(this.player.timers.controls); | ||||
|             // Timer to prevent flicker when seeking | ||||
|             this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); | ||||
|         }); | ||||
|                 // Clear timer | ||||
|                 clearTimeout(this.player.timers.controls); | ||||
|                 // Timer to prevent flicker when seeking | ||||
|                 this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // Listen for media events | ||||
|     media() { | ||||
|         // Time change on media | ||||
|         utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event)); | ||||
|         on.call(this.player, this.player.media, 'timeupdate seeking seeked', event => | ||||
|             controls.timeUpdate.call(this.player, event), | ||||
|         ); | ||||
|  | ||||
|         // Display duration | ||||
|         utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); | ||||
|         on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event => | ||||
|             controls.durationUpdate.call(this.player, event), | ||||
|         ); | ||||
|  | ||||
|         // Check for audio tracks on load | ||||
|         // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point | ||||
|         utils.on(this.player.media, 'loadeddata', () => { | ||||
|             utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio); | ||||
|             utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); | ||||
|         on.call(this.player, this.player.media, 'canplay', () => { | ||||
|             toggleHidden(this.player.elements.volume, !this.player.hasAudio); | ||||
|             toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); | ||||
|         }); | ||||
|  | ||||
|         // Handle the media finishing | ||||
|         utils.on(this.player.media, 'ended', () => { | ||||
|         on.call(this.player, this.player.media, 'ended', () => { | ||||
|             // Show poster on end | ||||
|             if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) { | ||||
|                 // Restart | ||||
| @ -298,20 +278,28 @@ class Listeners { | ||||
|         }); | ||||
|  | ||||
|         // Check for buffer progress | ||||
|         utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); | ||||
|         on.call(this.player, this.player.media, 'progress playing seeking seeked', event => | ||||
|             controls.updateProgress.call(this.player, event), | ||||
|         ); | ||||
|  | ||||
|         // Handle volume changes | ||||
|         utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event)); | ||||
|         on.call(this.player, this.player.media, 'volumechange', event => | ||||
|             controls.updateVolume.call(this.player, event), | ||||
|         ); | ||||
|  | ||||
|         // Handle play/pause | ||||
|         utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event)); | ||||
|         on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event => | ||||
|             ui.checkPlaying.call(this.player, event), | ||||
|         ); | ||||
|  | ||||
|         // Loading state | ||||
|         utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); | ||||
|         on.call(this.player, this.player.media, 'waiting canplay seeked playing', event => | ||||
|             ui.checkLoading.call(this.player, event), | ||||
|         ); | ||||
|  | ||||
|         // If autoplay, then load advertisement if required | ||||
|         // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows | ||||
|         utils.on(this.player.media, 'playing', () => { | ||||
|         on.call(this.player, this.player.media, 'playing', () => { | ||||
|             if (!this.player.ads) { | ||||
|                 return; | ||||
|             } | ||||
| @ -326,15 +314,15 @@ class Listeners { | ||||
|         // Click video | ||||
|         if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) { | ||||
|             // Re-fetch the wrapper | ||||
|             const wrapper = utils.getElement.call(this.player, `.${this.player.config.classNames.video}`); | ||||
|             const wrapper = getElement.call(this.player, `.${this.player.config.classNames.video}`); | ||||
|  | ||||
|             // Bail if there's no wrapper (this should never happen) | ||||
|             if (!utils.is.element(wrapper)) { | ||||
|             if (!is.element(wrapper)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // On click play, pause ore restart | ||||
|             utils.on(wrapper, 'click', () => { | ||||
|             on.call(this.player, wrapper, 'click', () => { | ||||
|                 // Touch devices will just show controls (if we're hiding controls) | ||||
|                 if (this.player.config.hideControls && this.player.touch && !this.player.paused) { | ||||
|                     return; | ||||
| @ -353,7 +341,8 @@ class Listeners { | ||||
|  | ||||
|         // Disable right click | ||||
|         if (this.player.supported.ui && this.player.config.disableContextMenu) { | ||||
|             utils.on( | ||||
|             on.call( | ||||
|                 this.player, | ||||
|                 this.player.elements.wrapper, | ||||
|                 'contextmenu', | ||||
|                 event => { | ||||
| @ -364,13 +353,13 @@ class Listeners { | ||||
|         } | ||||
|  | ||||
|         // Volume change | ||||
|         utils.on(this.player.media, 'volumechange', () => { | ||||
|         on.call(this.player, this.player.media, 'volumechange', () => { | ||||
|             // Save to storage | ||||
|             this.player.storage.set({ volume: this.player.volume, muted: this.player.muted }); | ||||
|         }); | ||||
|  | ||||
|         // Speed change | ||||
|         utils.on(this.player.media, 'ratechange', () => { | ||||
|         on.call(this.player, this.player.media, 'ratechange', () => { | ||||
|             // Update UI | ||||
|             controls.updateSetting.call(this.player, 'speed'); | ||||
|  | ||||
| @ -379,49 +368,29 @@ class Listeners { | ||||
|         }); | ||||
|  | ||||
|         // Quality request | ||||
|         utils.on(this.player.media, 'qualityrequested', event => { | ||||
|         on.call(this.player, this.player.media, 'qualityrequested', event => { | ||||
|             // Save to storage | ||||
|             this.player.storage.set({ quality: event.detail.quality }); | ||||
|         }); | ||||
|  | ||||
|         // Quality change | ||||
|         utils.on(this.player.media, 'qualitychange', event => { | ||||
|         on.call(this.player, this.player.media, 'qualitychange', event => { | ||||
|             // Update UI | ||||
|             controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); | ||||
|         }); | ||||
|  | ||||
|         // Caption language change | ||||
|         utils.on(this.player.media, 'languagechange', () => { | ||||
|             // Update UI | ||||
|             controls.updateSetting.call(this.player, 'captions'); | ||||
|  | ||||
|             // Save to storage | ||||
|             this.player.storage.set({ language: this.player.language }); | ||||
|         }); | ||||
|  | ||||
|         // Captions toggle | ||||
|         utils.on(this.player.media, 'captionsenabled captionsdisabled', () => { | ||||
|             // Update UI | ||||
|             controls.updateSetting.call(this.player, 'captions'); | ||||
|  | ||||
|             // Save to storage | ||||
|             this.player.storage.set({ captions: this.player.captions.active }); | ||||
|         }); | ||||
|  | ||||
|         // Proxy events to container | ||||
|         // Bubble up key events for Edge | ||||
|         utils.on(this.player.media, this.player.config.events.concat([ | ||||
|             'keyup', | ||||
|             'keydown', | ||||
|         ]).join(' '), event => { | ||||
|             let {detail = {}} = event; | ||||
|         const proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' '); | ||||
|         on.call(this.player, this.player.media, proxyEvents, event => { | ||||
|             let { detail = {} } = event; | ||||
|  | ||||
|             // Get error details from media | ||||
|             if (event.type === 'error') { | ||||
|                 detail = this.player.media.error; | ||||
|             } | ||||
|  | ||||
|             utils.dispatchEvent.call(this.player, this.player.elements.container, event.type, true, detail); | ||||
|             triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @ -433,7 +402,7 @@ class Listeners { | ||||
|         // Run default and custom handlers | ||||
|         const proxy = (event, defaultHandler, customHandlerKey) => { | ||||
|             const customHandler = this.player.config.listeners[customHandlerKey]; | ||||
|             const hasCustomHandler = utils.is.function(customHandler); | ||||
|             const hasCustomHandler = is.function(customHandler); | ||||
|             let returned = true; | ||||
|  | ||||
|             // Execute custom handler | ||||
| @ -442,33 +411,41 @@ class Listeners { | ||||
|             } | ||||
|  | ||||
|             // Only call default handler if not prevented in custom handler | ||||
|             if (returned && utils.is.function(defaultHandler)) { | ||||
|             if (returned && is.function(defaultHandler)) { | ||||
|                 defaultHandler.call(this.player, event); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         // Trigger custom and default handlers | ||||
|         const on = (element, type, defaultHandler, customHandlerKey, passive = true) => { | ||||
|         const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => { | ||||
|             const customHandler = this.player.config.listeners[customHandlerKey]; | ||||
|             const hasCustomHandler = utils.is.function(customHandler); | ||||
|             const hasCustomHandler = is.function(customHandler); | ||||
|  | ||||
|             utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); | ||||
|             on.call( | ||||
|                 this.player, | ||||
|                 element, | ||||
|                 type, | ||||
|                 event => proxy(event, defaultHandler, customHandlerKey), | ||||
|                 passive && !hasCustomHandler, | ||||
|             ); | ||||
|         }; | ||||
|  | ||||
|         // Play/pause toggle | ||||
|         on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play'); | ||||
|         Array.from(this.player.elements.buttons.play).forEach(button => { | ||||
|             bind(button, 'click', this.player.togglePlay, 'play'); | ||||
|         }); | ||||
|  | ||||
|         // Pause | ||||
|         on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); | ||||
|         bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); | ||||
|  | ||||
|         // Rewind | ||||
|         on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); | ||||
|         bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); | ||||
|  | ||||
|         // Rewind | ||||
|         on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); | ||||
|         bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); | ||||
|  | ||||
|         // Mute toggle | ||||
|         on( | ||||
|         bind( | ||||
|             this.player.elements.buttons.mute, | ||||
|             'click', | ||||
|             () => { | ||||
| @ -478,10 +455,10 @@ class Listeners { | ||||
|         ); | ||||
|  | ||||
|         // Captions toggle | ||||
|         on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions); | ||||
|         bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions()); | ||||
|  | ||||
|         // Fullscreen toggle | ||||
|         on( | ||||
|         bind( | ||||
|             this.player.elements.buttons.fullscreen, | ||||
|             'click', | ||||
|             () => { | ||||
| @ -491,7 +468,7 @@ class Listeners { | ||||
|         ); | ||||
|  | ||||
|         // Picture-in-Picture | ||||
|         on( | ||||
|         bind( | ||||
|             this.player.elements.buttons.pip, | ||||
|             'click', | ||||
|             () => { | ||||
| @ -501,15 +478,15 @@ class Listeners { | ||||
|         ); | ||||
|  | ||||
|         // Airplay | ||||
|         on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); | ||||
|         bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); | ||||
|  | ||||
|         // Settings menu | ||||
|         on(this.player.elements.buttons.settings, 'click', event => { | ||||
|         bind(this.player.elements.buttons.settings, 'click', event => { | ||||
|             controls.toggleMenu.call(this.player, event); | ||||
|         }); | ||||
|  | ||||
|         // Settings menu | ||||
|         on(this.player.elements.settings.form, 'click', event => { | ||||
|         bind(this.player.elements.settings.form, 'click', event => { | ||||
|             event.stopPropagation(); | ||||
|  | ||||
|             // Go back to home tab on click | ||||
| @ -519,7 +496,7 @@ class Listeners { | ||||
|             }; | ||||
|  | ||||
|             // Settings menu items - use event delegation as items are added/removed | ||||
|             if (utils.matches(event.target, this.player.config.selectors.inputs.language)) { | ||||
|             if (matches(event.target, this.player.config.selectors.inputs.language)) { | ||||
|                 proxy( | ||||
|                     event, | ||||
|                     () => { | ||||
| @ -528,7 +505,7 @@ class Listeners { | ||||
|                     }, | ||||
|                     'language', | ||||
|                 ); | ||||
|             } else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) { | ||||
|             } else if (matches(event.target, this.player.config.selectors.inputs.quality)) { | ||||
|                 proxy( | ||||
|                     event, | ||||
|                     () => { | ||||
| @ -537,7 +514,7 @@ class Listeners { | ||||
|                     }, | ||||
|                     'quality', | ||||
|                 ); | ||||
|             } else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) { | ||||
|             } else if (matches(event.target, this.player.config.selectors.inputs.speed)) { | ||||
|                 proxy( | ||||
|                     event, | ||||
|                     () => { | ||||
| @ -553,14 +530,14 @@ class Listeners { | ||||
|         }); | ||||
|  | ||||
|         // Set range input alternative "value", which matches the tooltip time (#954) | ||||
|         on(this.player.elements.inputs.seek, 'mousedown mousemove', event => { | ||||
|         bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => { | ||||
|             const clientRect = this.player.elements.progress.getBoundingClientRect(); | ||||
|             const percent = 100 / clientRect.width * (event.pageX - clientRect.left); | ||||
|             event.currentTarget.setAttribute('seek-value', percent); | ||||
|         }); | ||||
|  | ||||
|         // Pause while seeking | ||||
|         on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { | ||||
|         bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { | ||||
|             const seek = event.currentTarget; | ||||
|  | ||||
|             const code = event.keyCode ? event.keyCode : event.which; | ||||
| @ -573,11 +550,7 @@ class Listeners { | ||||
|             const play = seek.hasAttribute('play-on-seeked'); | ||||
|  | ||||
|             // Done seeking | ||||
|             const done = [ | ||||
|                 'mouseup', | ||||
|                 'touchend', | ||||
|                 'keyup', | ||||
|             ].includes(event.type); | ||||
|             const done = ['mouseup', 'touchend', 'keyup'].includes(event.type); | ||||
|  | ||||
|             // If we're done seeking and it was playing, resume playback | ||||
|             if (play && done) { | ||||
| @ -590,7 +563,7 @@ class Listeners { | ||||
|         }); | ||||
|  | ||||
|         // Seek | ||||
|         on( | ||||
|         bind( | ||||
|             this.player.elements.inputs.seek, | ||||
|             inputEvent, | ||||
|             event => { | ||||
| @ -599,7 +572,7 @@ class Listeners { | ||||
|                 // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) | ||||
|                 let seekTo = seek.getAttribute('seek-value'); | ||||
|  | ||||
|                 if (utils.is.empty(seekTo)) { | ||||
|                 if (is.empty(seekTo)) { | ||||
|                     seekTo = seek.value; | ||||
|                 } | ||||
|  | ||||
| @ -612,8 +585,8 @@ class Listeners { | ||||
|  | ||||
|         // Current time invert | ||||
|         // Only if one time element is used for both currentTime and duration | ||||
|         if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) { | ||||
|             on(this.player.elements.display.currentTime, 'click', () => { | ||||
|         if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) { | ||||
|             bind(this.player.elements.display.currentTime, 'click', () => { | ||||
|                 // Do nothing if we're at the start | ||||
|                 if (this.player.currentTime === 0) { | ||||
|                     return; | ||||
| @ -626,7 +599,7 @@ class Listeners { | ||||
|         } | ||||
|  | ||||
|         // Volume | ||||
|         on( | ||||
|         bind( | ||||
|             this.player.elements.inputs.volume, | ||||
|             inputEvent, | ||||
|             event => { | ||||
| @ -637,33 +610,32 @@ class Listeners { | ||||
|  | ||||
|         // Polyfill for lower fill in <input type="range"> for webkit | ||||
|         if (browser.isWebkit) { | ||||
|             on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => { | ||||
|                 controls.updateRangeFill.call(this.player, event.target); | ||||
|             Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => { | ||||
|                 bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target)); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // Seek tooltip | ||||
|         on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event)); | ||||
|         bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => | ||||
|             controls.updateSeekTooltip.call(this.player, event), | ||||
|         ); | ||||
|  | ||||
|         // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) | ||||
|         on(this.player.elements.controls, 'mouseenter mouseleave', event => { | ||||
|         bind(this.player.elements.controls, 'mouseenter mouseleave', event => { | ||||
|             this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; | ||||
|         }); | ||||
|  | ||||
|         // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) | ||||
|         on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { | ||||
|             this.player.elements.controls.pressed = [ | ||||
|                 'mousedown', | ||||
|                 'touchstart', | ||||
|             ].includes(event.type); | ||||
|         bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { | ||||
|             this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); | ||||
|         }); | ||||
|  | ||||
|         // Focus in/out on controls | ||||
|         on(this.player.elements.controls, 'focusin focusout', event => { | ||||
|         bind(this.player.elements.controls, 'focusin focusout', event => { | ||||
|             const { config, elements, timers } = this.player; | ||||
|  | ||||
|             // Skip transition to prevent focus from scrolling the parent element | ||||
|             utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); | ||||
|             toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); | ||||
|  | ||||
|             // Toggle | ||||
|             ui.toggleControls.call(this.player, event.type === 'focusin'); | ||||
| @ -672,7 +644,7 @@ class Listeners { | ||||
|             if (event.type === 'focusin') { | ||||
|                 // Restore transition | ||||
|                 setTimeout(() => { | ||||
|                     utils.toggleClass(elements.controls, config.classNames.noTransition, false); | ||||
|                     toggleClass(elements.controls, config.classNames.noTransition, false); | ||||
|                 }, 0); | ||||
|  | ||||
|                 // Delay a little more for keyboard users | ||||
| @ -686,7 +658,7 @@ class Listeners { | ||||
|         }); | ||||
|  | ||||
|         // Mouse wheel for volume | ||||
|         on( | ||||
|         bind( | ||||
|             this.player.elements.inputs.volume, | ||||
|             'wheel', | ||||
|             event => { | ||||
| @ -719,7 +691,10 @@ class Listeners { | ||||
|                 } | ||||
|  | ||||
|                 // Don't break page scrolling at max and min | ||||
|                 if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) { | ||||
|                 if ( | ||||
|                     (direction === 1 && this.player.media.volume < 1) || | ||||
|                     (direction === -1 && this.player.media.volume > 0) | ||||
|                 ) { | ||||
|                     event.preventDefault(); | ||||
|                 } | ||||
|             }, | ||||
| @ -727,11 +702,6 @@ class Listeners { | ||||
|             false, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // Reset on destroy | ||||
|     clear() { | ||||
|         this.global(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default Listeners; | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
| import html5 from './html5'; | ||||
| import vimeo from './plugins/vimeo'; | ||||
| import youtube from './plugins/youtube'; | ||||
| import utils from './utils'; | ||||
| import { createElement, toggleClass, wrap } from './utils/elements'; | ||||
|  | ||||
| const media = { | ||||
|     // Setup media | ||||
| @ -17,50 +17,41 @@ const media = { | ||||
|         } | ||||
|  | ||||
|         // Add type class | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); | ||||
|         toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); | ||||
|  | ||||
|         // Add provider class | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); | ||||
|         toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), 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); | ||||
|             toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); | ||||
|         } | ||||
|  | ||||
|         // Inject the player wrapper | ||||
|         if (this.isVideo) { | ||||
|             // Create the wrapper div | ||||
|             this.elements.wrapper = utils.createElement('div', { | ||||
|             this.elements.wrapper = createElement('div', { | ||||
|                 class: this.config.classNames.video, | ||||
|             }); | ||||
|  | ||||
|             // Wrap the video in a container | ||||
|             utils.wrap(this.media, this.elements.wrapper); | ||||
|             wrap(this.media, this.elements.wrapper); | ||||
|  | ||||
|             // Faux poster container | ||||
|             this.elements.poster = utils.createElement('div', { | ||||
|             this.elements.poster = createElement('div', { | ||||
|                 class: this.config.classNames.poster, | ||||
|             }); | ||||
|  | ||||
|             this.elements.wrapper.appendChild(this.elements.poster); | ||||
|         } | ||||
|  | ||||
|         if (this.isEmbed) { | ||||
|             switch (this.provider) { | ||||
|                 case 'youtube': | ||||
|                     youtube.setup.call(this); | ||||
|                     break; | ||||
|  | ||||
|                 case 'vimeo': | ||||
|                     vimeo.setup.call(this); | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         } else if (this.isHTML5) { | ||||
|         if (this.isHTML5) { | ||||
|             html5.extend.call(this); | ||||
|         } else if (this.isYouTube) { | ||||
|             youtube.setup.call(this); | ||||
|         } else if (this.isVimeo) { | ||||
|             vimeo.setup.call(this); | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| @ -7,7 +7,12 @@ | ||||
| /* global google */ | ||||
|  | ||||
| import i18n from '../i18n'; | ||||
| import utils from '../utils'; | ||||
| import { createElement } from './../utils/elements'; | ||||
| import { triggerEvent } from './../utils/events'; | ||||
| import is from './../utils/is'; | ||||
| import loadScript from './../utils/loadScript'; | ||||
| import { formatTime } from './../utils/time'; | ||||
| import { buildUrlParams } from './../utils/urls'; | ||||
|  | ||||
| class Ads { | ||||
|     /** | ||||
| @ -44,7 +49,7 @@ class Ads { | ||||
|     } | ||||
|  | ||||
|     get enabled() { | ||||
|         return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId); | ||||
|         return this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -53,9 +58,8 @@ class Ads { | ||||
|     load() { | ||||
|         if (this.enabled) { | ||||
|             // Check if the Google IMA3 SDK is loaded or load it ourselves | ||||
|             if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) { | ||||
|                 utils | ||||
|                     .loadScript(this.player.config.urls.googleIMA.sdk) | ||||
|             if (!is.object(window.google) || !is.object(window.google.ima)) { | ||||
|                 loadScript(this.player.config.urls.googleIMA.sdk) | ||||
|                     .then(() => { | ||||
|                         this.ready(); | ||||
|                     }) | ||||
| @ -103,7 +107,7 @@ class Ads { | ||||
|  | ||||
|         const base = 'https://go.aniview.com/api/adserver6/vast/'; | ||||
|  | ||||
|         return `${base}?${utils.buildUrlParams(params)}`; | ||||
|         return `${base}?${buildUrlParams(params)}`; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -116,7 +120,7 @@ class Ads { | ||||
|      */ | ||||
|     setupIMA() { | ||||
|         // Create the container for our advertisements | ||||
|         this.elements.container = utils.createElement('div', { | ||||
|         this.elements.container = createElement('div', { | ||||
|             class: this.player.config.classNames.ads, | ||||
|         }); | ||||
|         this.player.elements.container.appendChild(this.elements.container); | ||||
| @ -146,7 +150,11 @@ class Ads { | ||||
|             this.loader = new google.ima.AdsLoader(this.elements.displayContainer); | ||||
|  | ||||
|             // Listen and respond to ads loaded and error events | ||||
|             this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false); | ||||
|             this.loader.addEventListener( | ||||
|                 google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, | ||||
|                 event => this.onAdsManagerLoaded(event), | ||||
|                 false, | ||||
|             ); | ||||
|             this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); | ||||
|  | ||||
|             // Request video ads | ||||
| @ -184,7 +192,7 @@ class Ads { | ||||
|         } | ||||
|  | ||||
|         const update = () => { | ||||
|             const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0)); | ||||
|             const time = formatTime(Math.max(this.manager.getRemainingTime(), 0)); | ||||
|             const label = `${i18n.get('advertisement', this.player.config)} - ${time}`; | ||||
|             this.elements.container.setAttribute('data-badge-text', label); | ||||
|         }; | ||||
| @ -212,14 +220,14 @@ class Ads { | ||||
|         this.cuePoints = this.manager.getCuePoints(); | ||||
|  | ||||
|         // Add advertisement cue's within the time line if available | ||||
|         if (!utils.is.empty(this.cuePoints)) { | ||||
|         if (!is.empty(this.cuePoints)) { | ||||
|             this.cuePoints.forEach(cuePoint => { | ||||
|                 if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) { | ||||
|                     const seekElement = this.player.elements.progress; | ||||
|  | ||||
|                     if (utils.is.element(seekElement)) { | ||||
|                     if (is.element(seekElement)) { | ||||
|                         const cuePercentage = 100 / this.player.duration * cuePoint; | ||||
|                         const cue = utils.createElement('span', { | ||||
|                         const cue = createElement('span', { | ||||
|                             class: this.player.config.classNames.cues, | ||||
|                         }); | ||||
|  | ||||
| @ -266,7 +274,7 @@ class Ads { | ||||
|         // Proxy event | ||||
|         const dispatchEvent = type => { | ||||
|             const event = `ads${type.replace(/_/g, '').toLowerCase()}`; | ||||
|             utils.dispatchEvent.call(this.player, this.player.media, event); | ||||
|             triggerEvent.call(this.player, this.player.media, event); | ||||
|         }; | ||||
|  | ||||
|         switch (event.type) { | ||||
| @ -393,7 +401,7 @@ class Ads { | ||||
|         this.player.on('seeked', () => { | ||||
|             const seekedTime = this.player.currentTime; | ||||
|  | ||||
|             if (utils.is.empty(this.cuePoints)) { | ||||
|             if (is.empty(this.cuePoints)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @ -530,9 +538,9 @@ class Ads { | ||||
|     trigger(event, ...args) { | ||||
|         const handlers = this.events[event]; | ||||
|  | ||||
|         if (utils.is.array(handlers)) { | ||||
|         if (is.array(handlers)) { | ||||
|             handlers.forEach(handler => { | ||||
|                 if (utils.is.function(handler)) { | ||||
|                 if (is.function(handler)) { | ||||
|                     handler.apply(this, args); | ||||
|                 } | ||||
|             }); | ||||
| @ -546,7 +554,7 @@ class Ads { | ||||
|      * @return {Ads} | ||||
|      */ | ||||
|     on(event, callback) { | ||||
|         if (!utils.is.array(this.events[event])) { | ||||
|         if (!is.array(this.events[event])) { | ||||
|             this.events[event] = []; | ||||
|         } | ||||
|  | ||||
| @ -577,7 +585,7 @@ class Ads { | ||||
|      * @param {string} from | ||||
|      */ | ||||
|     clearSafetyTimer(from) { | ||||
|         if (!utils.is.nullOrUndefined(this.safetyTimer)) { | ||||
|         if (!is.nullOrUndefined(this.safetyTimer)) { | ||||
|             this.player.debug.log(`Safety timer cleared from: ${from}`); | ||||
|  | ||||
|             clearTimeout(this.safetyTimer); | ||||
|  | ||||
| @ -5,7 +5,34 @@ | ||||
| import captions from './../captions'; | ||||
| import controls from './../controls'; | ||||
| import ui from './../ui'; | ||||
| import utils from './../utils'; | ||||
| import { createElement, replaceElement, toggleClass } from './../utils/elements'; | ||||
| import { triggerEvent } from './../utils/events'; | ||||
| import fetch from './../utils/fetch'; | ||||
| import is from './../utils/is'; | ||||
| import loadScript from './../utils/loadScript'; | ||||
| import { format, stripHTML } from './../utils/strings'; | ||||
| import { buildUrlParams } from './../utils/urls'; | ||||
|  | ||||
| // Parse Vimeo ID from URL | ||||
| function parseId(url) { | ||||
|     if (is.empty(url)) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     if (is.number(Number(url))) { | ||||
|         return url; | ||||
|     } | ||||
|  | ||||
|     const regex = /^.*(vimeo.com\/|video\/)(\d+).*/; | ||||
|     return url.match(regex) ? RegExp.$2 : url; | ||||
| } | ||||
|  | ||||
| // Get aspect ratio for dimensions | ||||
| function getAspectRatio(width, height) { | ||||
|     const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); | ||||
|     const ratio = getRatio(width, height); | ||||
|     return `${width / ratio}:${height / ratio}`; | ||||
| } | ||||
|  | ||||
| // Set playback state and trigger change (only on actual change) | ||||
| function assurePlaybackState(play) { | ||||
| @ -14,22 +41,21 @@ function assurePlaybackState(play) { | ||||
|     } | ||||
|     if (this.media.paused === play) { | ||||
|         this.media.paused = !play; | ||||
|         utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); | ||||
|         triggerEvent.call(this, this.media, play ? 'play' : 'pause'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const vimeo = { | ||||
|     setup() { | ||||
|         // Add embed class for responsive | ||||
|         utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); | ||||
|         toggleClass(this.elements.wrapper, this.config.classNames.embed, true); | ||||
|  | ||||
|         // Set intial ratio | ||||
|         vimeo.setAspectRatio.call(this); | ||||
|  | ||||
|         // Load the API if not already | ||||
|         if (!utils.is.object(window.Vimeo)) { | ||||
|             utils | ||||
|                 .loadScript(this.config.urls.vimeo.sdk) | ||||
|         if (!is.object(window.Vimeo)) { | ||||
|             loadScript(this.config.urls.vimeo.sdk) | ||||
|                 .then(() => { | ||||
|                     vimeo.ready.call(this); | ||||
|                 }) | ||||
| @ -44,8 +70,8 @@ const vimeo = { | ||||
|     // Set aspect ratio | ||||
|     // For Vimeo we have an extra 300% height <div> to hide the standard controls and UI | ||||
|     setAspectRatio(input) { | ||||
|         const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); | ||||
|         const padding = 100 / ratio[0] * ratio[1]; | ||||
|         const [x, y] = (is.string(input) ? input : this.config.ratio).split(':'); | ||||
|         const padding = 100 / x * y; | ||||
|         this.elements.wrapper.style.paddingBottom = `${padding}%`; | ||||
|  | ||||
|         if (this.supported.ui) { | ||||
| @ -73,34 +99,37 @@ const vimeo = { | ||||
|             gesture: 'media', | ||||
|             playsinline: !this.config.fullscreen.iosNative, | ||||
|         }; | ||||
|         const params = utils.buildUrlParams(options); | ||||
|         const params = buildUrlParams(options); | ||||
|  | ||||
|         // Get the source URL or ID | ||||
|         let source = player.media.getAttribute('src'); | ||||
|  | ||||
|         // Get from <div> if needed | ||||
|         if (utils.is.empty(source)) { | ||||
|         if (is.empty(source)) { | ||||
|             source = player.media.getAttribute(player.config.attributes.embed.id); | ||||
|         } | ||||
|  | ||||
|         const id = utils.parseVimeoId(source); | ||||
|         const id = parseId(source); | ||||
|  | ||||
|         // Build an iframe | ||||
|         const iframe = utils.createElement('iframe'); | ||||
|         const src = utils.format(player.config.urls.vimeo.iframe, id, params); | ||||
|         const iframe = createElement('iframe'); | ||||
|         const src = format(player.config.urls.vimeo.iframe, id, params); | ||||
|         iframe.setAttribute('src', src); | ||||
|         iframe.setAttribute('allowfullscreen', ''); | ||||
|         iframe.setAttribute('allowtransparency', ''); | ||||
|         iframe.setAttribute('allow', 'autoplay'); | ||||
|  | ||||
|         // Get poster, if already set | ||||
|         const { poster } = player; | ||||
|  | ||||
|         // Inject the package | ||||
|         const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer }); | ||||
|         const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); | ||||
|         wrapper.appendChild(iframe); | ||||
|         player.media = utils.replaceElement(wrapper, player.media); | ||||
|         player.media = replaceElement(wrapper, player.media); | ||||
|  | ||||
|         // Get poster image | ||||
|         utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => { | ||||
|             if (utils.is.empty(response)) { | ||||
|         fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => { | ||||
|             if (is.empty(response)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @ -111,7 +140,7 @@ const vimeo = { | ||||
|             url.pathname = `${url.pathname.split('_')[0]}.jpg`; | ||||
|  | ||||
|             // Set and show poster | ||||
|             ui.setPoster.call(player, url.href); | ||||
|             ui.setPoster.call(player, url.href).catch(() => {}); | ||||
|         }); | ||||
|  | ||||
|         // Setup instance | ||||
| @ -160,7 +189,7 @@ const vimeo = { | ||||
|  | ||||
|                 // Set seeking state and trigger event | ||||
|                 media.seeking = true; | ||||
|                 utils.dispatchEvent.call(player, media, 'seeking'); | ||||
|                 triggerEvent.call(player, media, 'seeking'); | ||||
|  | ||||
|                 // If paused, mute until seek is complete | ||||
|                 Promise.resolve(restorePause && embed.setVolume(0)) | ||||
| @ -187,7 +216,7 @@ const vimeo = { | ||||
|                     .setPlaybackRate(input) | ||||
|                     .then(() => { | ||||
|                         speed = input; | ||||
|                         utils.dispatchEvent.call(player, player.media, 'ratechange'); | ||||
|                         triggerEvent.call(player, player.media, 'ratechange'); | ||||
|                     }) | ||||
|                     .catch(error => { | ||||
|                         // Hide menu item (and menu if empty) | ||||
| @ -207,7 +236,7 @@ const vimeo = { | ||||
|             set(input) { | ||||
|                 player.embed.setVolume(input).then(() => { | ||||
|                     volume = input; | ||||
|                     utils.dispatchEvent.call(player, player.media, 'volumechange'); | ||||
|                     triggerEvent.call(player, player.media, 'volumechange'); | ||||
|                 }); | ||||
|             }, | ||||
|         }); | ||||
| @ -219,11 +248,11 @@ const vimeo = { | ||||
|                 return muted; | ||||
|             }, | ||||
|             set(input) { | ||||
|                 const toggle = utils.is.boolean(input) ? input : false; | ||||
|                 const toggle = is.boolean(input) ? input : false; | ||||
|  | ||||
|                 player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { | ||||
|                     muted = toggle; | ||||
|                     utils.dispatchEvent.call(player, player.media, 'volumechange'); | ||||
|                     triggerEvent.call(player, player.media, 'volumechange'); | ||||
|                 }); | ||||
|             }, | ||||
|         }); | ||||
| @ -235,7 +264,7 @@ const vimeo = { | ||||
|                 return loop; | ||||
|             }, | ||||
|             set(input) { | ||||
|                 const toggle = utils.is.boolean(input) ? input : player.config.loop.active; | ||||
|                 const toggle = is.boolean(input) ? input : player.config.loop.active; | ||||
|  | ||||
|                 player.embed.setLoop(toggle).then(() => { | ||||
|                     loop = toggle; | ||||
| @ -268,11 +297,8 @@ const vimeo = { | ||||
|         }); | ||||
|  | ||||
|         // Set aspect ratio based on video size | ||||
|         Promise.all([ | ||||
|             player.embed.getVideoWidth(), | ||||
|             player.embed.getVideoHeight(), | ||||
|         ]).then(dimensions => { | ||||
|             const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]); | ||||
|         Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { | ||||
|             const ratio = getAspectRatio(dimensions[0], dimensions[1]); | ||||
|             vimeo.setAspectRatio.call(this, ratio); | ||||
|         }); | ||||
|  | ||||
| @ -290,13 +316,13 @@ const vimeo = { | ||||
|         // Get current time | ||||
|         player.embed.getCurrentTime().then(value => { | ||||
|             currentTime = value; | ||||
|             utils.dispatchEvent.call(player, player.media, 'timeupdate'); | ||||
|             triggerEvent.call(player, player.media, 'timeupdate'); | ||||
|         }); | ||||
|  | ||||
|         // Get duration | ||||
|         player.embed.getDuration().then(value => { | ||||
|             player.media.duration = value; | ||||
|             utils.dispatchEvent.call(player, player.media, 'durationchange'); | ||||
|             triggerEvent.call(player, player.media, 'durationchange'); | ||||
|         }); | ||||
|  | ||||
|         // Get captions | ||||
| @ -306,7 +332,7 @@ const vimeo = { | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('cuechange', ({ cues = [] }) => { | ||||
|             const strippedCues = cues.map(cue => utils.stripHTML(cue.text)); | ||||
|             const strippedCues = cues.map(cue => stripHTML(cue.text)); | ||||
|             captions.updateCues.call(player, strippedCues); | ||||
|         }); | ||||
|  | ||||
| @ -315,11 +341,11 @@ const vimeo = { | ||||
|             player.embed.getPaused().then(paused => { | ||||
|                 assurePlaybackState.call(player, !paused); | ||||
|                 if (!paused) { | ||||
|                     utils.dispatchEvent.call(player, player.media, 'playing'); | ||||
|                     triggerEvent.call(player, player.media, 'playing'); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             if (utils.is.element(player.embed.element) && player.supported.ui) { | ||||
|             if (is.element(player.embed.element) && player.supported.ui) { | ||||
|                 const frame = player.embed.element; | ||||
|  | ||||
|                 // Fix keyboard focus issues | ||||
| @ -330,7 +356,7 @@ const vimeo = { | ||||
|  | ||||
|         player.embed.on('play', () => { | ||||
|             assurePlaybackState.call(player, true); | ||||
|             utils.dispatchEvent.call(player, player.media, 'playing'); | ||||
|             triggerEvent.call(player, player.media, 'playing'); | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('pause', () => { | ||||
| @ -340,16 +366,16 @@ const vimeo = { | ||||
|         player.embed.on('timeupdate', data => { | ||||
|             player.media.seeking = false; | ||||
|             currentTime = data.seconds; | ||||
|             utils.dispatchEvent.call(player, player.media, 'timeupdate'); | ||||
|             triggerEvent.call(player, player.media, 'timeupdate'); | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('progress', data => { | ||||
|             player.media.buffered = data.percent; | ||||
|             utils.dispatchEvent.call(player, player.media, 'progress'); | ||||
|             triggerEvent.call(player, player.media, 'progress'); | ||||
|  | ||||
|             // Check all loaded | ||||
|             if (parseInt(data.percent, 10) === 1) { | ||||
|                 utils.dispatchEvent.call(player, player.media, 'canplaythrough'); | ||||
|                 triggerEvent.call(player, player.media, 'canplaythrough'); | ||||
|             } | ||||
|  | ||||
|             // Get duration as if we do it before load, it gives an incorrect value | ||||
| @ -357,24 +383,24 @@ const vimeo = { | ||||
|             player.embed.getDuration().then(value => { | ||||
|                 if (value !== player.media.duration) { | ||||
|                     player.media.duration = value; | ||||
|                     utils.dispatchEvent.call(player, player.media, 'durationchange'); | ||||
|                     triggerEvent.call(player, player.media, 'durationchange'); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('seeked', () => { | ||||
|             player.media.seeking = false; | ||||
|             utils.dispatchEvent.call(player, player.media, 'seeked'); | ||||
|             triggerEvent.call(player, player.media, 'seeked'); | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('ended', () => { | ||||
|             player.media.paused = true; | ||||
|             utils.dispatchEvent.call(player, player.media, 'ended'); | ||||
|             triggerEvent.call(player, player.media, 'ended'); | ||||
|         }); | ||||
|  | ||||
|         player.embed.on('error', detail => { | ||||
|             player.media.error = detail; | ||||
|             utils.dispatchEvent.call(player, player.media, 'error'); | ||||
|             triggerEvent.call(player, player.media, 'error'); | ||||
|         }); | ||||
|  | ||||
|         // Rebuild UI | ||||
|  | ||||
| @ -4,64 +4,54 @@ | ||||
|  | ||||
| import controls from './../controls'; | ||||
| import ui from './../ui'; | ||||
| import utils from './../utils'; | ||||
| import { dedupe } from './../utils/arrays'; | ||||
| import { createElement, replaceElement, toggleClass } from './../utils/elements'; | ||||
| import { triggerEvent } from './../utils/events'; | ||||
| import fetch from './../utils/fetch'; | ||||
| import is from './../utils/is'; | ||||
| import loadImage from './../utils/loadImage'; | ||||
| import loadScript from './../utils/loadScript'; | ||||
| import { format, generateId } from './../utils/strings'; | ||||
|  | ||||
| // Parse YouTube ID from URL | ||||
| function parseId(url) { | ||||
|     if (is.empty(url)) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; | ||||
|     return url.match(regex) ? RegExp.$2 : url; | ||||
| } | ||||
|  | ||||
| // Standardise YouTube quality unit | ||||
| function mapQualityUnit(input) { | ||||
|     switch (input) { | ||||
|         case 'hd2160': | ||||
|             return 2160; | ||||
|     const qualities = { | ||||
|         hd2160: 2160, | ||||
|         hd1440: 1440, | ||||
|         hd1080: 1080, | ||||
|         hd720: 720, | ||||
|         large: 480, | ||||
|         medium: 360, | ||||
|         small: 240, | ||||
|         tiny: 144, | ||||
|     }; | ||||
|  | ||||
|         case 2160: | ||||
|             return 'hd2160'; | ||||
|     const entry = Object.entries(qualities).find(entry => entry.includes(input)); | ||||
|  | ||||
|         case 'hd1440': | ||||
|             return 1440; | ||||
|  | ||||
|         case 1440: | ||||
|             return 'hd1440'; | ||||
|  | ||||
|         case 'hd1080': | ||||
|             return 1080; | ||||
|  | ||||
|         case 1080: | ||||
|             return 'hd1080'; | ||||
|  | ||||
|         case 'hd720': | ||||
|             return 720; | ||||
|  | ||||
|         case 720: | ||||
|             return 'hd720'; | ||||
|  | ||||
|         case 'large': | ||||
|             return 480; | ||||
|  | ||||
|         case 480: | ||||
|             return 'large'; | ||||
|  | ||||
|         case 'medium': | ||||
|             return 360; | ||||
|  | ||||
|         case 360: | ||||
|             return 'medium'; | ||||
|  | ||||
|         case 'small': | ||||
|             return 240; | ||||
|  | ||||
|         case 240: | ||||
|             return 'small'; | ||||
|  | ||||
|         default: | ||||
|             return 'default'; | ||||
|     if (entry) { | ||||
|         // Get the match corresponding to the input | ||||
|         return entry.find(value => value !== input); | ||||
|     } | ||||
|  | ||||
|     return 'default'; | ||||
| } | ||||
|  | ||||
| function mapQualityUnits(levels) { | ||||
|     if (utils.is.empty(levels)) { | ||||
|     if (is.empty(levels)) { | ||||
|         return levels; | ||||
|     } | ||||
|  | ||||
|     return utils.dedupe(levels.map(level => mapQualityUnit(level))); | ||||
|     return dedupe(levels.map(level => mapQualityUnit(level))); | ||||
| } | ||||
|  | ||||
| // Set playback state and trigger change (only on actual change) | ||||
| @ -71,24 +61,24 @@ function assurePlaybackState(play) { | ||||
|     } | ||||
|     if (this.media.paused === play) { | ||||
|         this.media.paused = !play; | ||||
|         utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); | ||||
|         triggerEvent.call(this, this.media, play ? 'play' : 'pause'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const youtube = { | ||||
|     setup() { | ||||
|         // Add embed class for responsive | ||||
|         utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); | ||||
|         toggleClass(this.elements.wrapper, this.config.classNames.embed, true); | ||||
|  | ||||
|         // Set aspect ratio | ||||
|         youtube.setAspectRatio.call(this); | ||||
|  | ||||
|         // Setup API | ||||
|         if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) { | ||||
|         if (is.object(window.YT) && is.function(window.YT.Player)) { | ||||
|             youtube.ready.call(this); | ||||
|         } else { | ||||
|             // Load the API | ||||
|             utils.loadScript(this.config.urls.youtube.sdk).catch(error => { | ||||
|             loadScript(this.config.urls.youtube.sdk).catch(error => { | ||||
|                 this.debug.warn('YouTube API failed to load', error); | ||||
|             }); | ||||
|  | ||||
| @ -115,10 +105,10 @@ const youtube = { | ||||
|         // Try via undocumented API method first | ||||
|         // This method disappears now and then though... | ||||
|         // https://github.com/sampotts/plyr/issues/709 | ||||
|         if (utils.is.function(this.embed.getVideoData)) { | ||||
|         if (is.function(this.embed.getVideoData)) { | ||||
|             const { title } = this.embed.getVideoData(); | ||||
|  | ||||
|             if (utils.is.empty(title)) { | ||||
|             if (is.empty(title)) { | ||||
|                 this.config.title = title; | ||||
|                 ui.setTitle.call(this); | ||||
|                 return; | ||||
| @ -127,13 +117,12 @@ const youtube = { | ||||
|  | ||||
|         // Or via Google API | ||||
|         const key = this.config.keys.google; | ||||
|         if (utils.is.string(key) && !utils.is.empty(key)) { | ||||
|             const url = utils.format(this.config.urls.youtube.api, videoId, key); | ||||
|         if (is.string(key) && !is.empty(key)) { | ||||
|             const url = format(this.config.urls.youtube.api, videoId, key); | ||||
|  | ||||
|             utils | ||||
|                 .fetch(url) | ||||
|             fetch(url) | ||||
|                 .then(result => { | ||||
|                     if (utils.is.object(result)) { | ||||
|                     if (is.object(result)) { | ||||
|                         this.config.title = result.items[0].snippet.title; | ||||
|                         ui.setTitle.call(this); | ||||
|                     } | ||||
| @ -154,7 +143,7 @@ const youtube = { | ||||
|  | ||||
|         // Ignore already setup (race condition) | ||||
|         const currentId = player.media.getAttribute('id'); | ||||
|         if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) { | ||||
|         if (!is.empty(currentId) && currentId.startsWith('youtube-')) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -162,30 +151,36 @@ const youtube = { | ||||
|         let source = player.media.getAttribute('src'); | ||||
|  | ||||
|         // Get from <div> if needed | ||||
|         if (utils.is.empty(source)) { | ||||
|         if (is.empty(source)) { | ||||
|             source = player.media.getAttribute(this.config.attributes.embed.id); | ||||
|         } | ||||
|  | ||||
|         // Replace the <iframe> with a <div> due to YouTube API issues | ||||
|         const videoId = utils.parseYouTubeId(source); | ||||
|         const id = utils.generateId(player.provider); | ||||
|         const container = utils.createElement('div', { id }); | ||||
|         player.media = utils.replaceElement(container, player.media); | ||||
|         const videoId = parseId(source); | ||||
|         const id = generateId(player.provider); | ||||
|  | ||||
|         // Set poster image | ||||
|         // Get poster, if already set | ||||
|         const { poster } = player; | ||||
|  | ||||
|         // Replace media element | ||||
|         const container = createElement('div', { id, poster }); | ||||
|         player.media = replaceElement(container, player.media); | ||||
|  | ||||
|         // Id to poster wrapper | ||||
|         const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`; | ||||
|  | ||||
|         // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) | ||||
|         utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded | ||||
|             .catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 | ||||
|             .catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists | ||||
|         loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded | ||||
|             .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 | ||||
|             .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists | ||||
|             .then(image => ui.setPoster.call(player, image.src)) | ||||
|             .then(posterSrc => { | ||||
|                 // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) | ||||
|                 if (!posterSrc.includes('maxres')) { | ||||
|                     player.elements.poster.style.backgroundSize = 'cover'; | ||||
|                 } | ||||
|             }); | ||||
|             }) | ||||
|             .catch(() => {}); | ||||
|  | ||||
|         // Setup instance | ||||
|         // https://developers.google.com/youtube/iframe_api_reference | ||||
| @ -211,49 +206,26 @@ const youtube = { | ||||
|             }, | ||||
|             events: { | ||||
|                 onError(event) { | ||||
|                     // If we've already fired an error, don't do it again | ||||
|                     // YouTube fires onError twice | ||||
|                     if (utils.is.object(player.media.error)) { | ||||
|                         return; | ||||
|                     // YouTube may fire onError twice, so only handle it once | ||||
|                     if (!player.media.error) { | ||||
|                         const code = event.data; | ||||
|                         // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError | ||||
|                         const message = | ||||
|                             { | ||||
|                                 2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.', | ||||
|                                 5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.', | ||||
|                                 100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.', | ||||
|                                 101: 'The owner of the requested video does not allow it to be played in embedded players.', | ||||
|                                 150: 'The owner of the requested video does not allow it to be played in embedded players.', | ||||
|                             }[code] || 'An unknown error occured'; | ||||
|  | ||||
|                         player.media.error = { code, message }; | ||||
|  | ||||
|                         triggerEvent.call(player, player.media, 'error'); | ||||
|                     } | ||||
|  | ||||
|                     const detail = { | ||||
|                         code: event.data, | ||||
|                     }; | ||||
|  | ||||
|                     // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError | ||||
|                     switch (event.data) { | ||||
|                         case 2: | ||||
|                             detail.message = | ||||
|                                 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.'; | ||||
|                             break; | ||||
|  | ||||
|                         case 5: | ||||
|                             detail.message = | ||||
|                                 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.'; | ||||
|                             break; | ||||
|  | ||||
|                         case 100: | ||||
|                             detail.message = | ||||
|                                 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.'; | ||||
|                             break; | ||||
|  | ||||
|                         case 101: | ||||
|                         case 150: | ||||
|                             detail.message = 'The owner of the requested video does not allow it to be played in embedded players.'; | ||||
|                             break; | ||||
|  | ||||
|                         default: | ||||
|                             detail.message = 'An unknown error occured'; | ||||
|                             break; | ||||
|                     } | ||||
|  | ||||
|                     player.media.error = detail; | ||||
|  | ||||
|                     utils.dispatchEvent.call(player, player.media, 'error'); | ||||
|                 }, | ||||
|                 onPlaybackQualityChange() { | ||||
|                     utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { | ||||
|                     triggerEvent.call(player, player.media, 'qualitychange', false, { | ||||
|                         quality: player.media.quality, | ||||
|                     }); | ||||
|                 }, | ||||
| @ -264,7 +236,7 @@ const youtube = { | ||||
|                     // Get current speed | ||||
|                     player.media.playbackRate = instance.getPlaybackRate(); | ||||
|  | ||||
|                     utils.dispatchEvent.call(player, player.media, 'ratechange'); | ||||
|                     triggerEvent.call(player, player.media, 'ratechange'); | ||||
|                 }, | ||||
|                 onReady(event) { | ||||
|                     // Get the instance | ||||
| @ -305,7 +277,7 @@ const youtube = { | ||||
|  | ||||
|                             // Set seeking state and trigger event | ||||
|                             player.media.seeking = true; | ||||
|                             utils.dispatchEvent.call(player, player.media, 'seeking'); | ||||
|                             triggerEvent.call(player, player.media, 'seeking'); | ||||
|  | ||||
|                             // Seek after events sent | ||||
|                             instance.seekTo(time); | ||||
| @ -328,15 +300,7 @@ const youtube = { | ||||
|                             return mapQualityUnit(instance.getPlaybackQuality()); | ||||
|                         }, | ||||
|                         set(input) { | ||||
|                             const quality = input; | ||||
|  | ||||
|                             // Set via API | ||||
|                             instance.setPlaybackQuality(mapQualityUnit(quality)); | ||||
|  | ||||
|                             // Trigger request event | ||||
|                             utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { | ||||
|                                 quality, | ||||
|                             }); | ||||
|                             instance.setPlaybackQuality(mapQualityUnit(input)); | ||||
|                         }, | ||||
|                     }); | ||||
|  | ||||
| @ -349,7 +313,7 @@ const youtube = { | ||||
|                         set(input) { | ||||
|                             volume = input; | ||||
|                             instance.setVolume(volume * 100); | ||||
|                             utils.dispatchEvent.call(player, player.media, 'volumechange'); | ||||
|                             triggerEvent.call(player, player.media, 'volumechange'); | ||||
|                         }, | ||||
|                     }); | ||||
|  | ||||
| @ -360,10 +324,10 @@ const youtube = { | ||||
|                             return muted; | ||||
|                         }, | ||||
|                         set(input) { | ||||
|                             const toggle = utils.is.boolean(input) ? input : muted; | ||||
|                             const toggle = is.boolean(input) ? input : muted; | ||||
|                             muted = toggle; | ||||
|                             instance[toggle ? 'mute' : 'unMute'](); | ||||
|                             utils.dispatchEvent.call(player, player.media, 'volumechange'); | ||||
|                             triggerEvent.call(player, player.media, 'volumechange'); | ||||
|                         }, | ||||
|                     }); | ||||
|  | ||||
| @ -389,8 +353,8 @@ const youtube = { | ||||
|                         player.media.setAttribute('tabindex', -1); | ||||
|                     } | ||||
|  | ||||
|                     utils.dispatchEvent.call(player, player.media, 'timeupdate'); | ||||
|                     utils.dispatchEvent.call(player, player.media, 'durationchange'); | ||||
|                     triggerEvent.call(player, player.media, 'timeupdate'); | ||||
|                     triggerEvent.call(player, player.media, 'durationchange'); | ||||
|  | ||||
|                     // Reset timer | ||||
|                     clearInterval(player.timers.buffering); | ||||
| @ -402,7 +366,7 @@ const youtube = { | ||||
|  | ||||
|                         // 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'); | ||||
|                             triggerEvent.call(player, player.media, 'progress'); | ||||
|                         } | ||||
|  | ||||
|                         // Set last buffer point | ||||
| @ -413,7 +377,7 @@ const youtube = { | ||||
|                             clearInterval(player.timers.buffering); | ||||
|  | ||||
|                             // Trigger event | ||||
|                             utils.dispatchEvent.call(player, player.media, 'canplaythrough'); | ||||
|                             triggerEvent.call(player, player.media, 'canplaythrough'); | ||||
|                         } | ||||
|                     }, 200); | ||||
|  | ||||
| @ -427,15 +391,12 @@ const youtube = { | ||||
|                     // Reset timer | ||||
|                     clearInterval(player.timers.playing); | ||||
|  | ||||
|                     const seeked = player.media.seeking && [ | ||||
|                         1, | ||||
|                         2, | ||||
|                     ].includes(event.data); | ||||
|                     const seeked = player.media.seeking && [1, 2].includes(event.data); | ||||
|  | ||||
|                     if (seeked) { | ||||
|                         // Unset seeking and fire seeked event | ||||
|                         player.media.seeking = false; | ||||
|                         utils.dispatchEvent.call(player, player.media, 'seeked'); | ||||
|                         triggerEvent.call(player, player.media, 'seeked'); | ||||
|                     } | ||||
|  | ||||
|                     // Handle events | ||||
| @ -448,11 +409,11 @@ const youtube = { | ||||
|                     switch (event.data) { | ||||
|                         case -1: | ||||
|                             // Update scrubber | ||||
|                             utils.dispatchEvent.call(player, player.media, 'timeupdate'); | ||||
|                             triggerEvent.call(player, player.media, 'timeupdate'); | ||||
|  | ||||
|                             // Get loaded % from YouTube | ||||
|                             player.media.buffered = instance.getVideoLoadedFraction(); | ||||
|                             utils.dispatchEvent.call(player, player.media, 'progress'); | ||||
|                             triggerEvent.call(player, player.media, 'progress'); | ||||
|  | ||||
|                             break; | ||||
|  | ||||
| @ -465,7 +426,7 @@ const youtube = { | ||||
|                                 instance.stopVideo(); | ||||
|                                 instance.playVideo(); | ||||
|                             } else { | ||||
|                                 utils.dispatchEvent.call(player, player.media, 'ended'); | ||||
|                                 triggerEvent.call(player, player.media, 'ended'); | ||||
|                             } | ||||
|  | ||||
|                             break; | ||||
| @ -477,11 +438,11 @@ const youtube = { | ||||
|                             } else { | ||||
|                                 assurePlaybackState.call(player, true); | ||||
|  | ||||
|                                 utils.dispatchEvent.call(player, player.media, 'playing'); | ||||
|                                 triggerEvent.call(player, player.media, 'playing'); | ||||
|  | ||||
|                                 // Poll to get playback progress | ||||
|                                 player.timers.playing = setInterval(() => { | ||||
|                                     utils.dispatchEvent.call(player, player.media, 'timeupdate'); | ||||
|                                     triggerEvent.call(player, player.media, 'timeupdate'); | ||||
|                                 }, 50); | ||||
|  | ||||
|                                 // Check duration again due to YouTube bug | ||||
| @ -489,11 +450,14 @@ const youtube = { | ||||
|                                 // 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'); | ||||
|                                     triggerEvent.call(player, player.media, 'durationchange'); | ||||
|                                 } | ||||
|  | ||||
|                                 // Get quality | ||||
|                                 controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); | ||||
|                                 controls.setQualityMenu.call( | ||||
|                                     player, | ||||
|                                     mapQualityUnits(instance.getAvailableQualityLevels()), | ||||
|                                 ); | ||||
|                             } | ||||
|  | ||||
|                             break; | ||||
| @ -511,7 +475,7 @@ const youtube = { | ||||
|                             break; | ||||
|                     } | ||||
|  | ||||
|                     utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, { | ||||
|                     triggerEvent.call(player, player.elements.container, 'statechange', false, { | ||||
|                         code: event.data, | ||||
|                     }); | ||||
|                 }, | ||||
|  | ||||
							
								
								
									
										244
									
								
								src/js/plyr.js
									
									
									
									
									
								
							
							
						
						
									
										244
									
								
								src/js/plyr.js
									
									
									
									
									
								
							| @ -6,9 +6,10 @@ | ||||
| // ========================================================================== | ||||
|  | ||||
| import captions from './captions'; | ||||
| import defaults from './config/defaults'; | ||||
| import { getProviderByUrl, providers, types } from './config/types'; | ||||
| import Console from './console'; | ||||
| import controls from './controls'; | ||||
| import defaults from './defaults'; | ||||
| import Fullscreen from './fullscreen'; | ||||
| import Listeners from './listeners'; | ||||
| import media from './media'; | ||||
| @ -16,9 +17,14 @@ import Ads from './plugins/ads'; | ||||
| import source from './source'; | ||||
| import Storage from './storage'; | ||||
| import support from './support'; | ||||
| import { providers, types } from './types'; | ||||
| import ui from './ui'; | ||||
| import utils from './utils'; | ||||
| import { closest } from './utils/arrays'; | ||||
| import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements'; | ||||
| import { off, on, once, triggerEvent, unbindListeners } from './utils/events'; | ||||
| import is from './utils/is'; | ||||
| import loadSprite from './utils/loadSprite'; | ||||
| import { cloneDeep, extend } from './utils/objects'; | ||||
| import { parseUrl } from './utils/urls'; | ||||
|  | ||||
| // Private properties | ||||
| // TODO: Use a WeakMap for private globals | ||||
| @ -41,18 +47,18 @@ class Plyr { | ||||
|         this.media = target; | ||||
|  | ||||
|         // String selector passed | ||||
|         if (utils.is.string(this.media)) { | ||||
|         if (is.string(this.media)) { | ||||
|             this.media = document.querySelectorAll(this.media); | ||||
|         } | ||||
|  | ||||
|         // jQuery, NodeList or Array passed, use first element | ||||
|         if ((window.jQuery && this.media instanceof jQuery) || utils.is.nodeList(this.media) || utils.is.array(this.media)) { | ||||
|         if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) { | ||||
|             // eslint-disable-next-line | ||||
|             this.media = this.media[0]; | ||||
|         } | ||||
|  | ||||
|         // Set config | ||||
|         this.config = utils.extend( | ||||
|         this.config = extend( | ||||
|             {}, | ||||
|             defaults, | ||||
|             Plyr.defaults, | ||||
| @ -108,7 +114,7 @@ class Plyr { | ||||
|         this.debug.log('Support', support); | ||||
|  | ||||
|         // We need an element to setup | ||||
|         if (utils.is.nullOrUndefined(this.media) || !utils.is.element(this.media)) { | ||||
|         if (is.nullOrUndefined(this.media) || !is.element(this.media)) { | ||||
|             this.debug.error('Setup failed: no suitable element passed'); | ||||
|             return; | ||||
|         } | ||||
| @ -144,7 +150,6 @@ class Plyr { | ||||
|         // Embed properties | ||||
|         let iframe = null; | ||||
|         let url = null; | ||||
|         let params = null; | ||||
|  | ||||
|         // Different setup based on type | ||||
|         switch (type) { | ||||
| @ -153,10 +158,10 @@ class Plyr { | ||||
|                 iframe = this.media.querySelector('iframe'); | ||||
|  | ||||
|                 // <iframe> type | ||||
|                 if (utils.is.element(iframe)) { | ||||
|                 if (is.element(iframe)) { | ||||
|                     // Detect provider | ||||
|                     url = iframe.getAttribute('src'); | ||||
|                     this.provider = utils.getProviderByUrl(url); | ||||
|                     url = parseUrl(iframe.getAttribute('src')); | ||||
|                     this.provider = getProviderByUrl(url.toString()); | ||||
|  | ||||
|                     // Rework elements | ||||
|                     this.elements.container = this.media; | ||||
| @ -166,24 +171,20 @@ class Plyr { | ||||
|                     this.elements.container.className = ''; | ||||
|  | ||||
|                     // Get attributes from URL and set config | ||||
|                     params = utils.getUrlParams(url); | ||||
|                     if (!utils.is.empty(params)) { | ||||
|                         const truthy = [ | ||||
|                             '1', | ||||
|                             'true', | ||||
|                         ]; | ||||
|                     if (url.searchParams.length) { | ||||
|                         const truthy = ['1', 'true']; | ||||
|  | ||||
|                         if (truthy.includes(params.autoplay)) { | ||||
|                         if (truthy.includes(url.searchParams.get('autoplay'))) { | ||||
|                             this.config.autoplay = true; | ||||
|                         } | ||||
|                         if (truthy.includes(params.loop)) { | ||||
|                         if (truthy.includes(url.searchParams.get('loop'))) { | ||||
|                             this.config.loop.active = true; | ||||
|                         } | ||||
|  | ||||
|                         // TODO: replace fullscreen.iosNative with this playsinline config option | ||||
|                         // YouTube requires the playsinline in the URL | ||||
|                         if (this.isYouTube) { | ||||
|                             this.config.playsinline = truthy.includes(params.playsinline); | ||||
|                             this.config.playsinline = truthy.includes(url.searchParams.get('playsinline')); | ||||
|                         } else { | ||||
|                             this.config.playsinline = true; | ||||
|                         } | ||||
| @ -197,7 +198,7 @@ class Plyr { | ||||
|                 } | ||||
|  | ||||
|                 // Unsupported or missing provider | ||||
|                 if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) { | ||||
|                 if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) { | ||||
|                     this.debug.error('Setup failed: Invalid provider'); | ||||
|                     return; | ||||
|                 } | ||||
| @ -245,6 +246,8 @@ class Plyr { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.eventListeners = []; | ||||
|  | ||||
|         // Create listeners | ||||
|         this.listeners = new Listeners(this); | ||||
|  | ||||
| @ -255,9 +258,9 @@ class Plyr { | ||||
|         this.media.plyr = this; | ||||
|  | ||||
|         // Wrap media | ||||
|         if (!utils.is.element(this.elements.container)) { | ||||
|             this.elements.container = utils.createElement('div'); | ||||
|             utils.wrap(this.media, this.elements.container); | ||||
|         if (!is.element(this.elements.container)) { | ||||
|             this.elements.container = createElement('div'); | ||||
|             wrap(this.media, this.elements.container); | ||||
|         } | ||||
|  | ||||
|         // Add style hook | ||||
| @ -268,7 +271,7 @@ class Plyr { | ||||
|  | ||||
|         // Listen for events if debugging | ||||
|         if (this.config.debug) { | ||||
|             utils.on(this.elements.container, this.config.events.join(' '), event => { | ||||
|             on.call(this, this.elements.container, this.config.events.join(' '), event => { | ||||
|                 this.debug.log(`event: ${event.type}`); | ||||
|             }); | ||||
|         } | ||||
| @ -327,7 +330,7 @@ class Plyr { | ||||
|      * Play the media, or play the advertisement (if they are not blocked) | ||||
|      */ | ||||
|     play() { | ||||
|         if (!utils.is.function(this.media.play)) { | ||||
|         if (!is.function(this.media.play)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
| @ -339,7 +342,7 @@ class Plyr { | ||||
|      * Pause the media | ||||
|      */ | ||||
|     pause() { | ||||
|         if (!this.playing || !utils.is.function(this.media.pause)) { | ||||
|         if (!this.playing || !is.function(this.media.pause)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -380,7 +383,7 @@ class Plyr { | ||||
|      */ | ||||
|     togglePlay(input) { | ||||
|         // Toggle based on current state if nothing passed | ||||
|         const toggle = utils.is.boolean(input) ? input : !this.playing; | ||||
|         const toggle = is.boolean(input) ? input : !this.playing; | ||||
|  | ||||
|         if (toggle) { | ||||
|             this.play(); | ||||
| @ -396,7 +399,7 @@ class Plyr { | ||||
|         if (this.isHTML5) { | ||||
|             this.pause(); | ||||
|             this.restart(); | ||||
|         } else if (utils.is.function(this.media.stop)) { | ||||
|         } else if (is.function(this.media.stop)) { | ||||
|             this.media.stop(); | ||||
|         } | ||||
|     } | ||||
| @ -413,7 +416,7 @@ class Plyr { | ||||
|      * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime | ||||
|      */ | ||||
|     rewind(seekTime) { | ||||
|         this.currentTime = this.currentTime - (utils.is.number(seekTime) ? seekTime : this.config.seekTime); | ||||
|         this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -421,7 +424,7 @@ class Plyr { | ||||
|      * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime | ||||
|      */ | ||||
|     forward(seekTime) { | ||||
|         this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime); | ||||
|         this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -435,7 +438,7 @@ class Plyr { | ||||
|         } | ||||
|  | ||||
|         // Validate input | ||||
|         const inputIsValid = utils.is.number(input) && input > 0; | ||||
|         const inputIsValid = is.number(input) && input > 0; | ||||
|  | ||||
|         // Set | ||||
|         this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; | ||||
| @ -458,7 +461,7 @@ class Plyr { | ||||
|         const { buffered } = this.media; | ||||
|  | ||||
|         // YouTube / Vimeo return a float between 0-1 | ||||
|         if (utils.is.number(buffered)) { | ||||
|         if (is.number(buffered)) { | ||||
|             return buffered; | ||||
|         } | ||||
|  | ||||
| @ -502,17 +505,17 @@ class Plyr { | ||||
|         const max = 1; | ||||
|         const min = 0; | ||||
|  | ||||
|         if (utils.is.string(volume)) { | ||||
|         if (is.string(volume)) { | ||||
|             volume = Number(volume); | ||||
|         } | ||||
|  | ||||
|         // Load volume from storage if no value specified | ||||
|         if (!utils.is.number(volume)) { | ||||
|         if (!is.number(volume)) { | ||||
|             volume = this.storage.get('volume'); | ||||
|         } | ||||
|  | ||||
|         // Use config if all else fails | ||||
|         if (!utils.is.number(volume)) { | ||||
|         if (!is.number(volume)) { | ||||
|             ({ volume } = this.config); | ||||
|         } | ||||
|  | ||||
| @ -532,7 +535,7 @@ class Plyr { | ||||
|         this.media.volume = volume; | ||||
|  | ||||
|         // If muted, and we're increasing volume manually, reset muted state | ||||
|         if (!utils.is.empty(value) && this.muted && volume > 0) { | ||||
|         if (!is.empty(value) && this.muted && volume > 0) { | ||||
|             this.muted = false; | ||||
|         } | ||||
|     } | ||||
| @ -550,7 +553,7 @@ class Plyr { | ||||
|      */ | ||||
|     increaseVolume(step) { | ||||
|         const volume = this.media.muted ? 0 : this.volume; | ||||
|         this.volume = volume + (utils.is.number(step) ? step : 1); | ||||
|         this.volume = volume + (is.number(step) ? step : 1); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -559,7 +562,7 @@ class Plyr { | ||||
|      */ | ||||
|     decreaseVolume(step) { | ||||
|         const volume = this.media.muted ? 0 : this.volume; | ||||
|         this.volume = volume - (utils.is.number(step) ? step : 1); | ||||
|         this.volume = volume - (is.number(step) ? step : 1); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -570,12 +573,12 @@ class Plyr { | ||||
|         let toggle = mute; | ||||
|  | ||||
|         // Load muted state from storage | ||||
|         if (!utils.is.boolean(toggle)) { | ||||
|         if (!is.boolean(toggle)) { | ||||
|             toggle = this.storage.get('muted'); | ||||
|         } | ||||
|  | ||||
|         // Use config if all else fails | ||||
|         if (!utils.is.boolean(toggle)) { | ||||
|         if (!is.boolean(toggle)) { | ||||
|             toggle = this.config.muted; | ||||
|         } | ||||
|  | ||||
| @ -621,15 +624,15 @@ class Plyr { | ||||
|     set speed(input) { | ||||
|         let speed = null; | ||||
|  | ||||
|         if (utils.is.number(input)) { | ||||
|         if (is.number(input)) { | ||||
|             speed = input; | ||||
|         } | ||||
|  | ||||
|         if (!utils.is.number(speed)) { | ||||
|         if (!is.number(speed)) { | ||||
|             speed = this.storage.get('speed'); | ||||
|         } | ||||
|  | ||||
|         if (!utils.is.number(speed)) { | ||||
|         if (!is.number(speed)) { | ||||
|             speed = this.config.speed.selected; | ||||
|         } | ||||
|  | ||||
| @ -666,36 +669,31 @@ class Plyr { | ||||
|      * @param {number} input - Quality level | ||||
|      */ | ||||
|     set quality(input) { | ||||
|         let quality = null; | ||||
|         const config = this.config.quality; | ||||
|         const options = this.options.quality; | ||||
|  | ||||
|         if (!utils.is.empty(input)) { | ||||
|             quality = Number(input); | ||||
|         } | ||||
|  | ||||
|         if (!utils.is.number(quality)) { | ||||
|             quality = this.storage.get('quality'); | ||||
|         } | ||||
|  | ||||
|         if (!utils.is.number(quality)) { | ||||
|             quality = this.config.quality.selected; | ||||
|         } | ||||
|  | ||||
|         if (!utils.is.number(quality)) { | ||||
|             quality = this.config.quality.default; | ||||
|         } | ||||
|  | ||||
|         if (!this.options.quality.length) { | ||||
|         if (!options.length) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!this.options.quality.includes(quality)) { | ||||
|             const closest = utils.closest(this.options.quality, quality); | ||||
|             this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`); | ||||
|             quality = closest; | ||||
|         let quality = [ | ||||
|             !is.empty(input) && Number(input), | ||||
|             this.storage.get('quality'), | ||||
|             config.selected, | ||||
|             config.default, | ||||
|         ].find(is.number); | ||||
|  | ||||
|         if (!options.includes(quality)) { | ||||
|             const value = closest(options, quality); | ||||
|             this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`); | ||||
|             quality = value; | ||||
|         } | ||||
|  | ||||
|         // Trigger request event | ||||
|         triggerEvent.call(this, this.media, 'qualityrequested', false, { quality }); | ||||
|  | ||||
|         // Update config | ||||
|         this.config.quality.selected = quality; | ||||
|         config.selected = quality; | ||||
|  | ||||
|         // Set quality | ||||
|         this.media.quality = quality; | ||||
| @ -714,7 +712,7 @@ class Plyr { | ||||
|      * @param {boolean} input - Whether to loop or not | ||||
|      */ | ||||
|     set loop(input) { | ||||
|         const toggle = utils.is.boolean(input) ? input : this.config.loop.active; | ||||
|         const toggle = is.boolean(input) ? input : this.config.loop.active; | ||||
|         this.config.loop.active = toggle; | ||||
|         this.media.loop = toggle; | ||||
|  | ||||
| @ -794,7 +792,7 @@ class Plyr { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         ui.setPoster.call(this, input); | ||||
|         ui.setPoster.call(this, input, false).catch(() => {}); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -813,7 +811,7 @@ class Plyr { | ||||
|      * @param {boolean} input - Whether to autoplay or not | ||||
|      */ | ||||
|     set autoplay(input) { | ||||
|         const toggle = utils.is.boolean(input) ? input : this.config.autoplay; | ||||
|         const toggle = is.boolean(input) ? input : this.config.autoplay; | ||||
|         this.config.autoplay = toggle; | ||||
|     } | ||||
|  | ||||
| @ -829,41 +827,23 @@ class Plyr { | ||||
|      * @param {boolean} input - Whether to enable captions | ||||
|      */ | ||||
|     toggleCaptions(input) { | ||||
|         // If there's no full support | ||||
|         if (!this.supported.ui) { | ||||
|             return; | ||||
|         captions.toggle.call(this, input, false); | ||||
|         } | ||||
|  | ||||
|         // If the method is called without parameter, toggle based on current value | ||||
|         const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active); | ||||
|  | ||||
|         // Toggle state | ||||
|         this.elements.buttons.captions.pressed = active; | ||||
|  | ||||
|         // Add class hook | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active); | ||||
|  | ||||
|         // Update state and trigger event | ||||
|         if (active !== this.captions.active) { | ||||
|             this.captions.active = active; | ||||
|             utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the caption track by index | ||||
|      * @param {number} - Caption index | ||||
|      */ | ||||
|     set currentTrack(input) { | ||||
|         captions.set.call(this, input); | ||||
|         captions.set.call(this, input, false); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the current caption track index (-1 if disabled) | ||||
|      */ | ||||
|     get currentTrack() { | ||||
|         const { active, currentTrack } = this.captions; | ||||
|         return active ? currentTrack : -1; | ||||
|         const { toggled, currentTrack } = this.captions; | ||||
|         return toggled ? currentTrack : -1; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -872,7 +852,7 @@ class Plyr { | ||||
|      * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) | ||||
|      */ | ||||
|     set language(input) { | ||||
|         captions.setLanguage.call(this, input); | ||||
|         captions.setLanguage.call(this, input, false); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -899,7 +879,7 @@ class Plyr { | ||||
|         } | ||||
|  | ||||
|         // Toggle based on current state if not passed | ||||
|         const toggle = utils.is.boolean(input) ? input : this.pip === states.inline; | ||||
|         const toggle = is.boolean(input) ? input : this.pip === states.inline; | ||||
|  | ||||
|         // Toggle based on current state | ||||
|         this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); | ||||
| @ -935,22 +915,22 @@ class Plyr { | ||||
|         // Don't toggle if missing UI support or if it's audio | ||||
|         if (this.supported.ui && !this.isAudio) { | ||||
|             // Get state before change | ||||
|             const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls); | ||||
|             const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); | ||||
|  | ||||
|             // Negate the argument if not undefined since adding the class to hides the controls | ||||
|             const force = typeof toggle === 'undefined' ? undefined : !toggle; | ||||
|  | ||||
|             // Apply and get updated state | ||||
|             const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force); | ||||
|             const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); | ||||
|  | ||||
|             // Close menu | ||||
|             if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { | ||||
|             if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { | ||||
|                 controls.toggleMenu.call(this, false); | ||||
|             } | ||||
|             // Trigger event on change | ||||
|             if (hiding !== isHidden) { | ||||
|                 const eventName = hiding ? 'controlshidden' : 'controlsshown'; | ||||
|                 utils.dispatchEvent.call(this, this.media, eventName); | ||||
|                 triggerEvent.call(this, this.media, eventName); | ||||
|             } | ||||
|             return !hiding; | ||||
|         } | ||||
| @ -963,16 +943,23 @@ class Plyr { | ||||
|      * @param {function} callback - Callback for when event occurs | ||||
|      */ | ||||
|     on(event, callback) { | ||||
|         utils.on(this.elements.container, event, callback); | ||||
|         on.call(this, this.elements.container, event, callback); | ||||
|     } | ||||
|     /** | ||||
|      * Add event listeners once | ||||
|      * @param {string} event - Event type | ||||
|      * @param {function} callback - Callback for when event occurs | ||||
|      */ | ||||
|     once(event, callback) { | ||||
|         once.call(this, this.elements.container, event, callback); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove event listeners | ||||
|      * @param {string} event - Event type | ||||
|      * @param {function} callback - Callback for when event occurs | ||||
|      */ | ||||
|     off(event, callback) { | ||||
|         utils.off(this.elements.container, event, callback); | ||||
|         off(this.elements.container, event, callback); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -998,10 +985,10 @@ class Plyr { | ||||
|             if (soft) { | ||||
|                 if (Object.keys(this.elements).length) { | ||||
|                     // Remove elements | ||||
|                     utils.removeElement(this.elements.buttons.play); | ||||
|                     utils.removeElement(this.elements.captions); | ||||
|                     utils.removeElement(this.elements.controls); | ||||
|                     utils.removeElement(this.elements.wrapper); | ||||
|                     removeElement(this.elements.buttons.play); | ||||
|                     removeElement(this.elements.captions); | ||||
|                     removeElement(this.elements.controls); | ||||
|                     removeElement(this.elements.wrapper); | ||||
|  | ||||
|                     // Clear for GC | ||||
|                     this.elements.buttons.play = null; | ||||
| @ -1011,21 +998,21 @@ class Plyr { | ||||
|                 } | ||||
|  | ||||
|                 // Callback | ||||
|                 if (utils.is.function(callback)) { | ||||
|                 if (is.function(callback)) { | ||||
|                     callback(); | ||||
|                 } | ||||
|             } else { | ||||
|                 // Unbind listeners | ||||
|                 this.listeners.clear(); | ||||
|                 unbindListeners.call(this); | ||||
|  | ||||
|                 // Replace the container with the original element provided | ||||
|                 utils.replaceElement(this.elements.original, this.elements.container); | ||||
|                 replaceElement(this.elements.original, this.elements.container); | ||||
|  | ||||
|                 // Event | ||||
|                 utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true); | ||||
|                 triggerEvent.call(this, this.elements.original, 'destroyed', true); | ||||
|  | ||||
|                 // Callback | ||||
|                 if (utils.is.function(callback)) { | ||||
|                 if (is.function(callback)) { | ||||
|                     callback.call(this.elements.original); | ||||
|                 } | ||||
|  | ||||
| @ -1043,10 +1030,8 @@ class Plyr { | ||||
|         // Stop playback | ||||
|         this.stop(); | ||||
|  | ||||
|         // Type specific stuff | ||||
|         switch (`${this.provider}:${this.type}`) { | ||||
|             case 'html5:video': | ||||
|             case 'html5:audio': | ||||
|         // Provider specific stuff | ||||
|         if (this.isHTML5) { | ||||
|                 // Clear timeout | ||||
|                 clearTimeout(this.timers.loading); | ||||
|  | ||||
| @ -1055,25 +1040,19 @@ class Plyr { | ||||
|  | ||||
|                 // Clean up | ||||
|                 done(); | ||||
|  | ||||
|                 break; | ||||
|  | ||||
|             case 'youtube:video': | ||||
|         } else if (this.isYouTube) { | ||||
|                 // Clear timers | ||||
|                 clearInterval(this.timers.buffering); | ||||
|                 clearInterval(this.timers.playing); | ||||
|  | ||||
|                 // Destroy YouTube API | ||||
|                 if (this.embed !== null && utils.is.function(this.embed.destroy)) { | ||||
|             if (this.embed !== null && is.function(this.embed.destroy)) { | ||||
|                     this.embed.destroy(); | ||||
|                 } | ||||
|  | ||||
|                 // Clean up | ||||
|                 done(); | ||||
|  | ||||
|                 break; | ||||
|  | ||||
|             case 'vimeo:video': | ||||
|         } else if (this.isVimeo) { | ||||
|                 // Destroy Vimeo API | ||||
|                 // then clean up (wait, to prevent postmessage errors) | ||||
|                 if (this.embed !== null) { | ||||
| @ -1082,11 +1061,6 @@ class Plyr { | ||||
|  | ||||
|                 // Vimeo does not always return | ||||
|                 setTimeout(done, 200); | ||||
|  | ||||
|                 break; | ||||
|  | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -1114,7 +1088,7 @@ class Plyr { | ||||
|      * @param {string} [id] - Unique ID | ||||
|      */ | ||||
|     static loadSprite(url, id) { | ||||
|         return utils.loadSprite(url, id); | ||||
|         return loadSprite(url, id); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -1125,15 +1099,15 @@ class Plyr { | ||||
|     static setup(selector, options = {}) { | ||||
|         let targets = null; | ||||
|  | ||||
|         if (utils.is.string(selector)) { | ||||
|         if (is.string(selector)) { | ||||
|             targets = Array.from(document.querySelectorAll(selector)); | ||||
|         } else if (utils.is.nodeList(selector)) { | ||||
|         } else if (is.nodeList(selector)) { | ||||
|             targets = Array.from(selector); | ||||
|         } else if (utils.is.array(selector)) { | ||||
|             targets = selector.filter(utils.is.element); | ||||
|         } else if (is.array(selector)) { | ||||
|             targets = selector.filter(is.element); | ||||
|         } | ||||
|  | ||||
|         if (utils.is.empty(targets)) { | ||||
|         if (is.empty(targets)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
| @ -1141,6 +1115,6 @@ class Plyr { | ||||
|     } | ||||
| } | ||||
|  | ||||
| Plyr.defaults = utils.cloneDeep(defaults); | ||||
| Plyr.defaults = cloneDeep(defaults); | ||||
|  | ||||
| export default Plyr; | ||||
|  | ||||
| @ -2,23 +2,25 @@ | ||||
| // Plyr source update | ||||
| // ========================================================================== | ||||
|  | ||||
| import { providers } from './config/types'; | ||||
| import html5 from './html5'; | ||||
| import media from './media'; | ||||
| import support from './support'; | ||||
| import { providers } from './types'; | ||||
| import ui from './ui'; | ||||
| import utils from './utils'; | ||||
| import { createElement, insertElement, removeElement } from './utils/elements'; | ||||
| import is from './utils/is'; | ||||
| import { getDeep } from './utils/objects'; | ||||
|  | ||||
| const source = { | ||||
|     // Add elements to HTML5 media (source, tracks, etc) | ||||
|     insertElements(type, attributes) { | ||||
|         if (utils.is.string(attributes)) { | ||||
|             utils.insertElement(type, this.media, { | ||||
|         if (is.string(attributes)) { | ||||
|             insertElement(type, this.media, { | ||||
|                 src: attributes, | ||||
|             }); | ||||
|         } else if (utils.is.array(attributes)) { | ||||
|         } else if (is.array(attributes)) { | ||||
|             attributes.forEach(attribute => { | ||||
|                 utils.insertElement(type, this.media, attribute); | ||||
|                 insertElement(type, this.media, attribute); | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| @ -26,7 +28,7 @@ const source = { | ||||
|     // Update source | ||||
|     // Sources are not checked for support so be careful | ||||
|     change(input) { | ||||
|         if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) { | ||||
|         if (!getDeep(input, 'sources.length')) { | ||||
|             this.debug.warn('Invalid source format'); | ||||
|             return; | ||||
|         } | ||||
| @ -42,47 +44,34 @@ const source = { | ||||
|                 this.options.quality = []; | ||||
|  | ||||
|                 // Remove elements | ||||
|                 utils.removeElement(this.media); | ||||
|                 removeElement(this.media); | ||||
|                 this.media = null; | ||||
|  | ||||
|                 // Reset class name | ||||
|                 if (utils.is.element(this.elements.container)) { | ||||
|                 if (is.element(this.elements.container)) { | ||||
|                     this.elements.container.removeAttribute('class'); | ||||
|                 } | ||||
|  | ||||
|                 // Set the type and provider | ||||
|                 this.type = input.type; | ||||
|                 this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5; | ||||
|                 const { sources, type } = input; | ||||
|                 const [{ provider = providers.html5, src }] = sources; | ||||
|                 const tagName = provider === 'html5' ? type : 'div'; | ||||
|                 const attributes = provider === 'html5' ? {} : { src }; | ||||
|  | ||||
|                 // Check for support | ||||
|                 this.supported = support.check(this.type, this.provider, this.config.playsinline); | ||||
|  | ||||
|                 // Create new markup | ||||
|                 switch (`${this.provider}:${this.type}`) { | ||||
|                     case 'html5:video': | ||||
|                         this.media = utils.createElement('video'); | ||||
|                         break; | ||||
|  | ||||
|                     case 'html5:audio': | ||||
|                         this.media = utils.createElement('audio'); | ||||
|                         break; | ||||
|  | ||||
|                     case 'youtube:video': | ||||
|                     case 'vimeo:video': | ||||
|                         this.media = utils.createElement('div', { | ||||
|                             src: input.sources[0].src, | ||||
|                         }); | ||||
|                         break; | ||||
|  | ||||
|                     default: | ||||
|                         break; | ||||
|                 } | ||||
|                 Object.assign(this, { | ||||
|                     provider, | ||||
|                     type, | ||||
|                     // Check for support | ||||
|                     supported: support.check(type, provider, this.config.playsinline), | ||||
|                     // Create new element | ||||
|                     media: createElement(tagName, attributes), | ||||
|                 }); | ||||
|  | ||||
|                 // Inject the new element | ||||
|                 this.elements.container.appendChild(this.media); | ||||
|  | ||||
|                 // Autoplay the new source? | ||||
|                 if (utils.is.boolean(input.autoplay)) { | ||||
|                 if (is.boolean(input.autoplay)) { | ||||
|                     this.config.autoplay = input.autoplay; | ||||
|                 } | ||||
|  | ||||
| @ -94,7 +83,7 @@ const source = { | ||||
|                     if (this.config.autoplay) { | ||||
|                         this.media.setAttribute('autoplay', ''); | ||||
|                     } | ||||
|                     if (!utils.is.empty(input.poster)) { | ||||
|                     if (!is.empty(input.poster)) { | ||||
|                         this.poster = input.poster; | ||||
|                     } | ||||
|                     if (this.config.loop.active) { | ||||
| @ -113,7 +102,7 @@ const source = { | ||||
|  | ||||
|                 // Set new sources for html5 | ||||
|                 if (this.isHTML5) { | ||||
|                     source.insertElements.call(this, 'source', input.sources); | ||||
|                     source.insertElements.call(this, 'source', sources); | ||||
|                 } | ||||
|  | ||||
|                 // Set video title | ||||
|  | ||||
| @ -2,7 +2,8 @@ | ||||
| // Plyr storage | ||||
| // ========================================================================== | ||||
|  | ||||
| import utils from './utils'; | ||||
| import is from './utils/is'; | ||||
| import { extend } from './utils/objects'; | ||||
|  | ||||
| class Storage { | ||||
|     constructor(player) { | ||||
| @ -37,13 +38,13 @@ class Storage { | ||||
|  | ||||
|         const store = window.localStorage.getItem(this.key); | ||||
|  | ||||
|         if (utils.is.empty(store)) { | ||||
|         if (is.empty(store)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         const json = JSON.parse(store); | ||||
|  | ||||
|         return utils.is.string(key) && key.length ? json[key] : json; | ||||
|         return is.string(key) && key.length ? json[key] : json; | ||||
|     } | ||||
|  | ||||
|     set(object) { | ||||
| @ -53,7 +54,7 @@ class Storage { | ||||
|         } | ||||
|  | ||||
|         // Can only store objectst | ||||
|         if (!utils.is.object(object)) { | ||||
|         if (!is.object(object)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -61,12 +62,12 @@ class Storage { | ||||
|         let storage = this.get(); | ||||
|  | ||||
|         // Default to empty object | ||||
|         if (utils.is.empty(storage)) { | ||||
|         if (is.empty(storage)) { | ||||
|             storage = {}; | ||||
|         } | ||||
|  | ||||
|         // Update the working copy of the values | ||||
|         utils.extend(storage, object); | ||||
|         extend(storage, object); | ||||
|  | ||||
|         // Update storage | ||||
|         window.localStorage.setItem(this.key, JSON.stringify(storage)); | ||||
|  | ||||
| @ -2,7 +2,19 @@ | ||||
| // Plyr support checks | ||||
| // ========================================================================== | ||||
|  | ||||
| import utils from './utils'; | ||||
| import { transitionEndEvent } from './utils/animation'; | ||||
| import browser from './utils/browser'; | ||||
| import { createElement } from './utils/elements'; | ||||
| import is from './utils/is'; | ||||
|  | ||||
| // Default codecs for checking mimetype support | ||||
| const defaultCodecs = { | ||||
|     'audio/ogg': 'vorbis', | ||||
|     'audio/wav': '1', | ||||
|     'video/webm': 'vp8, vorbis', | ||||
|     'video/mp4': 'avc1.42E01E, mp4a.40.2', | ||||
|     'video/ogg': 'theora', | ||||
| }; | ||||
|  | ||||
| // Check for feature support | ||||
| const support = { | ||||
| @ -13,32 +25,9 @@ const support = { | ||||
|     // Check for support | ||||
|     // Basic functionality vs full UI | ||||
|     check(type, provider, playsinline) { | ||||
|         let api = false; | ||||
|         let ui = false; | ||||
|         const browser = utils.getBrowser(); | ||||
|         const canPlayInline = browser.isIPhone && playsinline && support.playsinline; | ||||
|  | ||||
|         switch (`${provider}:${type}`) { | ||||
|             case 'html5:video': | ||||
|                 api = support.video; | ||||
|                 ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline); | ||||
|                 break; | ||||
|  | ||||
|             case 'html5:audio': | ||||
|                 api = support.audio; | ||||
|                 ui = api && support.rangeInput; | ||||
|                 break; | ||||
|  | ||||
|             case 'youtube:video': | ||||
|             case 'vimeo:video': | ||||
|                 api = true; | ||||
|                 ui = support.rangeInput && (!browser.isIPhone || canPlayInline); | ||||
|                 break; | ||||
|  | ||||
|             default: | ||||
|                 api = support.audio && support.video; | ||||
|                 ui = api && support.rangeInput; | ||||
|         } | ||||
|         const api = support[type] || provider !== 'html5'; | ||||
|         const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline); | ||||
|  | ||||
|         return { | ||||
|             api, | ||||
| @ -48,14 +37,11 @@ const support = { | ||||
|  | ||||
|     // Picture-in-picture support | ||||
|     // Safari only currently | ||||
|     pip: (() => { | ||||
|         const browser = utils.getBrowser(); | ||||
|         return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode); | ||||
|     })(), | ||||
|     pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(), | ||||
|  | ||||
|     // Airplay support | ||||
|     // Safari only currently | ||||
|     airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent), | ||||
|     airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent), | ||||
|  | ||||
|     // Inline playback support | ||||
|     // https://webkit.org/blog/6784/new-video-policies-for-ios/ | ||||
| @ -64,83 +50,34 @@ const support = { | ||||
|     // Check for mime type support against a player instance | ||||
|     // Credits: http://diveintohtml5.info/everything.html | ||||
|     // Related: http://www.leanbackplayer.com/test/h5mt.html | ||||
|     mime(type) { | ||||
|         const { media } = this; | ||||
|  | ||||
|         try { | ||||
|             // Bail if no checking function | ||||
|             if (!this.isHTML5 || !utils.is.function(media.canPlayType)) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             // Check directly if codecs specified | ||||
|             if (type.includes('codecs=')) { | ||||
|                 return media.canPlayType(type).replace(/no/, ''); | ||||
|             } | ||||
|  | ||||
|             // Type specific checks | ||||
|             if (this.isVideo) { | ||||
|                 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 (this.isAudio) { | ||||
|                 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) { | ||||
|     mime(inputType) { | ||||
|         const [mediaType] = inputType.split('/'); | ||||
|         if (!this.isHTML5 || mediaType !== this.type) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // If we got this far, we're stuffed | ||||
|         return false; | ||||
|         let type; | ||||
|         if (inputType && inputType.includes('codecs=')) { | ||||
|             // Use input directly | ||||
|             type = inputType; | ||||
|         } else if (inputType === 'audio/mpeg') { | ||||
|             // Skip codec | ||||
|             type = 'audio/mpeg;'; | ||||
|         } else if (inputType in defaultCodecs) { | ||||
|             // Use codec | ||||
|             type = `${inputType}; codecs="${defaultCodecs[inputType]}"`; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); | ||||
|         } catch (err) { | ||||
|             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); | ||||
|             window.removeEventListener('test', null, options); | ||||
|         } catch (e) { | ||||
|             // Do nothing | ||||
|         } | ||||
|  | ||||
|         return supported; | ||||
|     })(), | ||||
|  | ||||
|     // <input type="range"> Sliders | ||||
|     rangeInput: (() => { | ||||
|         const range = document.createElement('input'); | ||||
| @ -153,7 +90,7 @@ const support = { | ||||
|     touch: 'ontouchstart' in document.documentElement, | ||||
|  | ||||
|     // Detect transitions support | ||||
|     transitions: utils.transitionEndEvent !== false, | ||||
|     transitions: transitionEndEvent !== false, | ||||
|  | ||||
|     // Reduced motion iOS & MacOS setting | ||||
|     // https://webkit.org/blog/7551/responsive-design-for-motion/ | ||||
|  | ||||
							
								
								
									
										120
									
								
								src/js/ui.js
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								src/js/ui.js
									
									
									
									
									
								
							| @ -6,15 +6,16 @@ import captions from './captions'; | ||||
| import controls from './controls'; | ||||
| import i18n from './i18n'; | ||||
| import support from './support'; | ||||
| import utils from './utils'; | ||||
|  | ||||
| // Sniff out the browser | ||||
| const browser = utils.getBrowser(); | ||||
| import browser from './utils/browser'; | ||||
| import { getElement, toggleClass } from './utils/elements'; | ||||
| import { ready, triggerEvent } from './utils/events'; | ||||
| import is from './utils/is'; | ||||
| import loadImage from './utils/loadImage'; | ||||
|  | ||||
| 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); | ||||
|         toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); | ||||
|         toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); | ||||
|     }, | ||||
|  | ||||
|     // Toggle native HTML5 media controls | ||||
| @ -44,7 +45,7 @@ const ui = { | ||||
|         } | ||||
|  | ||||
|         // Inject custom controls if not present | ||||
|         if (!utils.is.element(this.elements.controls)) { | ||||
|         if (!is.element(this.elements.controls)) { | ||||
|             // Inject custom controls | ||||
|             controls.inject.call(this); | ||||
|  | ||||
| @ -85,31 +86,35 @@ const ui = { | ||||
|         ui.checkPlaying.call(this); | ||||
|  | ||||
|         // Check for picture-in-picture support | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); | ||||
|         toggleClass( | ||||
|             this.elements.container, | ||||
|             this.config.classNames.pip.supported, | ||||
|             support.pip && this.isHTML5 && this.isVideo, | ||||
|         ); | ||||
|  | ||||
|         // Check for airplay support | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); | ||||
|         toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); | ||||
|  | ||||
|         // Add iOS class | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); | ||||
|         toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); | ||||
|  | ||||
|         // Add touch class | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); | ||||
|         toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); | ||||
|  | ||||
|         // Ready for API calls | ||||
|         this.ready = true; | ||||
|  | ||||
|         // Ready event at end of execution stack | ||||
|         setTimeout(() => { | ||||
|             utils.dispatchEvent.call(this, this.media, 'ready'); | ||||
|             triggerEvent.call(this, this.media, 'ready'); | ||||
|         }, 0); | ||||
|  | ||||
|         // Set the title | ||||
|         ui.setTitle.call(this); | ||||
|  | ||||
|         // Assure the poster image is set, if the property was added before the element was created | ||||
|         if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) { | ||||
|             ui.setPoster.call(this, this.poster); | ||||
|         if (this.poster) { | ||||
|             ui.setPoster.call(this, this.poster, false).catch(() => {}); | ||||
|         } | ||||
|  | ||||
|         // Manually set the duration if user has overridden it. | ||||
| @ -125,12 +130,12 @@ const ui = { | ||||
|         let label = i18n.get('play', this.config); | ||||
|  | ||||
|         // 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)) { | ||||
|         if (is.string(this.config.title) && !is.empty(this.config.title)) { | ||||
|             label += `, ${this.config.title}`; | ||||
|         } | ||||
|  | ||||
|         // If there's a play button, set label | ||||
|         if (utils.is.nodeList(this.elements.buttons.play)) { | ||||
|         if (is.nodeList(this.elements.buttons.play)) { | ||||
|             Array.from(this.elements.buttons.play).forEach(button => { | ||||
|                 button.setAttribute('aria-label', label); | ||||
|             }); | ||||
| @ -139,14 +144,14 @@ const ui = { | ||||
|         // Set iframe title | ||||
|         // https://github.com/sampotts/plyr/issues/124 | ||||
|         if (this.isEmbed) { | ||||
|             const iframe = utils.getElement.call(this, 'iframe'); | ||||
|             const iframe = getElement.call(this, 'iframe'); | ||||
|  | ||||
|             if (!utils.is.element(iframe)) { | ||||
|             if (!is.element(iframe)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Default to media type | ||||
|             const title = !utils.is.empty(this.config.title) ? this.config.title : 'video'; | ||||
|             const title = !is.empty(this.config.title) ? this.config.title : 'video'; | ||||
|             const format = i18n.get('frameTitle', this.config); | ||||
|  | ||||
|             iframe.setAttribute('title', format.replace('{title}', title)); | ||||
| @ -155,44 +160,58 @@ const ui = { | ||||
|  | ||||
|     // Toggle poster | ||||
|     togglePoster(enable) { | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); | ||||
|         toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); | ||||
|     }, | ||||
|  | ||||
|     // Set the poster image (async) | ||||
|     setPoster(poster) { | ||||
|         // Set property regardless of validity | ||||
|         this.media.setAttribute('poster', poster); | ||||
|  | ||||
|         // Bail if element is missing | ||||
|         if (!utils.is.element(this.elements.poster)) { | ||||
|             return Promise.reject(); | ||||
|     // Used internally for the poster setter, with the passive option forced to false | ||||
|     setPoster(poster, passive = true) { | ||||
|         // Don't override if call is passive | ||||
|         if (passive && this.poster) { | ||||
|             return Promise.reject(new Error('Poster already set')); | ||||
|         } | ||||
|  | ||||
|         // Load the image, and set poster if successful | ||||
|         const loadPromise = utils.loadImage(poster).then(() => { | ||||
|             this.elements.poster.style.backgroundImage = `url('${poster}')`; | ||||
|             Object.assign(this.elements.poster.style, { | ||||
|                 backgroundImage: `url('${poster}')`, | ||||
|                 // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) | ||||
|                 backgroundSize: '', | ||||
|             }); | ||||
|             ui.togglePoster.call(this, true); | ||||
|             return poster; | ||||
|         }); | ||||
|         // Set property synchronously to respect the call order | ||||
|         this.media.setAttribute('poster', poster); | ||||
|  | ||||
|         // Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video) | ||||
|         loadPromise.catch(() => ui.togglePoster.call(this, false)); | ||||
|  | ||||
|         // Return the promise so the caller can use it as well | ||||
|         return loadPromise; | ||||
|         // Wait until ui is ready | ||||
|         return ( | ||||
|             ready | ||||
|                 .call(this) | ||||
|                 // Load image | ||||
|                 .then(() => loadImage(poster)) | ||||
|                 .catch(err => { | ||||
|                     // Hide poster on error unless it's been set by another call | ||||
|                     if (poster === this.poster) { | ||||
|                         ui.togglePoster.call(this, false); | ||||
|                     } | ||||
|                     // Rethrow | ||||
|                     throw err; | ||||
|                 }) | ||||
|                 .then(() => { | ||||
|                     // Prevent race conditions | ||||
|                     if (poster !== this.poster) { | ||||
|                         throw new Error('setPoster cancelled by later call to setPoster'); | ||||
|                     } | ||||
|                 }) | ||||
|                 .then(() => { | ||||
|                     Object.assign(this.elements.poster.style, { | ||||
|                         backgroundImage: `url('${poster}')`, | ||||
|                         // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) | ||||
|                         backgroundSize: '', | ||||
|                     }); | ||||
|                     ui.togglePoster.call(this, true); | ||||
|                     return poster; | ||||
|                 }) | ||||
|         ); | ||||
|     }, | ||||
|  | ||||
|     // Check playing state | ||||
|     checkPlaying(event) { | ||||
|         // Class hooks | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing); | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused); | ||||
|         utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); | ||||
|         toggleClass(this.elements.container, this.config.classNames.playing, this.playing); | ||||
|         toggleClass(this.elements.container, this.config.classNames.paused, this.paused); | ||||
|         toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); | ||||
|  | ||||
|         // Set state | ||||
|         Array.from(this.elements.buttons.play).forEach(target => { | ||||
| @ -200,7 +219,7 @@ const ui = { | ||||
|         }); | ||||
|  | ||||
|         // Only update controls on non timeupdate events | ||||
|         if (utils.is.event(event) && event.type === 'timeupdate') { | ||||
|         if (is.event(event) && event.type === 'timeupdate') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -210,10 +229,7 @@ const ui = { | ||||
|  | ||||
|     // Check if media is loading | ||||
|     checkLoading(event) { | ||||
|         this.loading = [ | ||||
|             'stalled', | ||||
|             'waiting', | ||||
|         ].includes(event.type); | ||||
|         this.loading = ['stalled', 'waiting'].includes(event.type); | ||||
|  | ||||
|         // Clear timer | ||||
|         clearTimeout(this.timers.loading); | ||||
| @ -221,7 +237,7 @@ const ui = { | ||||
|         // Timer to prevent flicker when seeking | ||||
|         this.timers.loading = setTimeout(() => { | ||||
|             // Update progress bar loading class state | ||||
|             utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading); | ||||
|             toggleClass(this.elements.container, this.config.classNames.loading, this.loading); | ||||
|  | ||||
|             // Update controls visibility | ||||
|             ui.toggleControls.call(this); | ||||
|  | ||||
							
								
								
									
										853
									
								
								src/js/utils.js
									
									
									
									
									
								
							
							
						
						
									
										853
									
								
								src/js/utils.js
									
									
									
									
									
								
							| @ -1,853 +0,0 @@ | ||||
| // ========================================================================== | ||||
| // Plyr utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import loadjs from 'loadjs'; | ||||
| import Storage from './storage'; | ||||
| import support from './support'; | ||||
| import { providers } from './types'; | ||||
|  | ||||
| const utils = { | ||||
|     // Check variable types | ||||
|     is: { | ||||
|         object(input) { | ||||
|             return utils.getConstructor(input) === Object; | ||||
|         }, | ||||
|         number(input) { | ||||
|             return utils.getConstructor(input) === Number && !Number.isNaN(input); | ||||
|         }, | ||||
|         string(input) { | ||||
|             return utils.getConstructor(input) === String; | ||||
|         }, | ||||
|         boolean(input) { | ||||
|             return utils.getConstructor(input) === Boolean; | ||||
|         }, | ||||
|         function(input) { | ||||
|             return utils.getConstructor(input) === Function; | ||||
|         }, | ||||
|         array(input) { | ||||
|             return !utils.is.nullOrUndefined(input) && Array.isArray(input); | ||||
|         }, | ||||
|         weakMap(input) { | ||||
|             return utils.is.instanceof(input, WeakMap); | ||||
|         }, | ||||
|         nodeList(input) { | ||||
|             return utils.is.instanceof(input, NodeList); | ||||
|         }, | ||||
|         element(input) { | ||||
|             return utils.is.instanceof(input, Element); | ||||
|         }, | ||||
|         textNode(input) { | ||||
|             return utils.getConstructor(input) === Text; | ||||
|         }, | ||||
|         event(input) { | ||||
|             return utils.is.instanceof(input, Event); | ||||
|         }, | ||||
|         cue(input) { | ||||
|             return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue); | ||||
|         }, | ||||
|         track(input) { | ||||
|             return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind)); | ||||
|         }, | ||||
|         url(input) { | ||||
|             return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); | ||||
|         }, | ||||
|         nullOrUndefined(input) { | ||||
|             return input === null || typeof input === 'undefined'; | ||||
|         }, | ||||
|         empty(input) { | ||||
|             return ( | ||||
|                 utils.is.nullOrUndefined(input) || | ||||
|                 ((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) || | ||||
|                 (utils.is.object(input) && !Object.keys(input).length) | ||||
|             ); | ||||
|         }, | ||||
|         instanceof(input, constructor) { | ||||
|             return Boolean(input && constructor && input instanceof constructor); | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     getConstructor(input) { | ||||
|         return !utils.is.nullOrUndefined(input) ? input.constructor : null; | ||||
|     }, | ||||
|  | ||||
|     // 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), | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     // Fetch wrapper | ||||
|     // Using XHR to avoid issues with older browsers | ||||
|     fetch(url, responseType = 'text') { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             try { | ||||
|                 const request = new XMLHttpRequest(); | ||||
|  | ||||
|                 // Check for CORS support | ||||
|                 if (!('withCredentials' in request)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 request.addEventListener('load', () => { | ||||
|                     if (responseType === 'text') { | ||||
|                         try { | ||||
|                             resolve(JSON.parse(request.responseText)); | ||||
|                         } catch (e) { | ||||
|                             resolve(request.responseText); | ||||
|                         } | ||||
|                     } else { | ||||
|                         resolve(request.response); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 request.addEventListener('error', () => { | ||||
|                     throw new Error(request.statusText); | ||||
|                 }); | ||||
|  | ||||
|                 request.open('GET', url, true); | ||||
|  | ||||
|                 // Set the required response type | ||||
|                 request.responseType = responseType; | ||||
|  | ||||
|                 request.send(); | ||||
|             } catch (e) { | ||||
|                 reject(e); | ||||
|             } | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     // Load image avoiding xhr/fetch CORS issues | ||||
|     // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded. | ||||
|     // By default it checks if it is at least 1px, but you can add a second argument to change this. | ||||
|     loadImage(src, minWidth = 1) { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             const image = new Image(); | ||||
|             const handler = () => { | ||||
|                 delete image.onload; | ||||
|                 delete image.onerror; | ||||
|                 (image.naturalWidth >= minWidth ? resolve : reject)(image); | ||||
|             }; | ||||
|             Object.assign(image, {onload: handler, onerror: handler, src}); | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     // Load an external script | ||||
|     loadScript(url) { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             loadjs(url, { | ||||
|                 success: resolve, | ||||
|                 error: reject, | ||||
|             }); | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     // Load an external SVG sprite | ||||
|     loadSprite(url, id) { | ||||
|         if (!utils.is.string(url)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const prefix = 'cache'; | ||||
|         const hasId = utils.is.string(id); | ||||
|         let isCached = false; | ||||
|  | ||||
|         const exists = () => document.getElementById(id) !== null; | ||||
|  | ||||
|         const update = (container, data) => { | ||||
|             container.innerHTML = data; | ||||
|  | ||||
|             // Check again incase of race condition | ||||
|             if (hasId && exists()) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Inject the SVG to the body | ||||
|             document.body.insertAdjacentElement('afterbegin', container); | ||||
|         }; | ||||
|  | ||||
|         // Only load once if ID set | ||||
|         if (!hasId || !exists()) { | ||||
|             const useStorage = Storage.supported; | ||||
|  | ||||
|             // Create container | ||||
|             const container = document.createElement('div'); | ||||
|             utils.toggleHidden(container, true); | ||||
|  | ||||
|             if (hasId) { | ||||
|                 container.setAttribute('id', id); | ||||
|             } | ||||
|  | ||||
|             // Check in cache | ||||
|             if (useStorage) { | ||||
|                 const cached = window.localStorage.getItem(`${prefix}-${id}`); | ||||
|                 isCached = cached !== null; | ||||
|  | ||||
|                 if (isCached) { | ||||
|                     const data = JSON.parse(cached); | ||||
|                     update(container, data.content); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Get the sprite | ||||
|             utils | ||||
|                 .fetch(url) | ||||
|                 .then(result => { | ||||
|                     if (utils.is.empty(result)) { | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     if (useStorage) { | ||||
|                         window.localStorage.setItem( | ||||
|                             `${prefix}-${id}`, | ||||
|                             JSON.stringify({ | ||||
|                                 content: result, | ||||
|                             }), | ||||
|                         ); | ||||
|                     } | ||||
|  | ||||
|                     update(container, result); | ||||
|                 }) | ||||
|                 .catch(() => {}); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Generate a random ID | ||||
|     generateId(prefix) { | ||||
|         return `${prefix}-${Math.floor(Math.random() * 10000)}`; | ||||
|     }, | ||||
|  | ||||
|     // 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); | ||||
|                 } | ||||
|             }); | ||||
|     }, | ||||
|  | ||||
|     // 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.innerText = text; | ||||
|         } | ||||
|  | ||||
|         // Return built element | ||||
|         return element; | ||||
|     }, | ||||
|  | ||||
|     // Inaert an element after another | ||||
|     insertAfter(element, target) { | ||||
|         target.parentNode.insertBefore(element, target.nextSibling); | ||||
|     }, | ||||
|  | ||||
|     // Insert a DocumentFragment | ||||
|     insertElement(type, parent, attributes, text) { | ||||
|         // Inject the new <element> | ||||
|         parent.appendChild(utils.createElement(type, attributes, text)); | ||||
|     }, | ||||
|  | ||||
|     // Remove element(s) | ||||
|     removeElement(element) { | ||||
|         if (utils.is.nodeList(element) || utils.is.array(element)) { | ||||
|             Array.from(element).forEach(utils.removeElement); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!utils.is.element(element) || !utils.is.element(element.parentNode)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         element.parentNode.removeChild(element); | ||||
|     }, | ||||
|  | ||||
|     // Remove all child elements | ||||
|     emptyElement(element) { | ||||
|         let { length } = element.childNodes; | ||||
|  | ||||
|         while (length > 0) { | ||||
|             element.removeChild(element.lastChild); | ||||
|             length -= 1; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Replace element | ||||
|     replaceElement(newChild, oldChild) { | ||||
|         if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         oldChild.parentNode.replaceChild(newChild, oldChild); | ||||
|  | ||||
|         return newChild; | ||||
|     }, | ||||
|  | ||||
|     // Set attributes | ||||
|     setAttributes(element, attributes) { | ||||
|         if (!utils.is.element(element) || utils.is.empty(attributes)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         Object.entries(attributes).forEach(([ | ||||
|             key, | ||||
|             value, | ||||
|         ]) => { | ||||
|             element.setAttribute(key, value); | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     // 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 hidden | ||||
|     toggleHidden(element, hidden) { | ||||
|         if (!utils.is.element(element)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let hide = hidden; | ||||
|  | ||||
|         if (!utils.is.boolean(hide)) { | ||||
|             hide = !element.hasAttribute('hidden'); | ||||
|         } | ||||
|  | ||||
|         if (hide) { | ||||
|             element.setAttribute('hidden', ''); | ||||
|         } else { | ||||
|             element.removeAttribute('hidden'); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Mirror Element.classList.toggle, with IE compatibility for "force" argument | ||||
|     toggleClass(element, className, force) { | ||||
|         if (utils.is.element(element)) { | ||||
|             let method = 'toggle'; | ||||
|             if (typeof force !== 'undefined') { | ||||
|                 method = force ? 'add' : 'remove'; | ||||
|             } | ||||
|  | ||||
|             element.classList[method](className); | ||||
|             return element.classList.contains(className); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     }, | ||||
|  | ||||
|     // Has class name | ||||
|     hasClass(element, className) { | ||||
|         return utils.is.element(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); | ||||
|     }, | ||||
|  | ||||
|     // 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(element = null, toggle = false) { | ||||
|         if (!utils.is.element(element)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); | ||||
|         const first = focusable[0]; | ||||
|         const last = focusable[focusable.length - 1]; | ||||
|  | ||||
|         const trap = event => { | ||||
|             // Bail if not tab key or not fullscreen | ||||
|             if (event.key !== 'Tab' || event.keyCode !== 9) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Get the current focused element | ||||
|             const focused = utils.getFocusElement(); | ||||
|  | ||||
|             if (focused === last && !event.shiftKey) { | ||||
|                 // Move focus to first element that can be tabbed if Shift isn't used | ||||
|                 first.focus(); | ||||
|                 event.preventDefault(); | ||||
|             } else if (focused === first && event.shiftKey) { | ||||
|                 // Move focus to last element that can be tabbed if Shift is used | ||||
|                 last.focus(); | ||||
|                 event.preventDefault(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         if (toggle) { | ||||
|             utils.on(this.elements.container, 'keydown', trap, false); | ||||
|         } else { | ||||
|             utils.off(this.elements.container, 'keydown', trap, false); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Toggle event listener | ||||
|     toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) { | ||||
|         // Bail if no elemetns, event, or callback | ||||
|         if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // If a nodelist is passed, call itself on each node | ||||
|         if (utils.is.nodeList(elements) || utils.is.array(elements)) { | ||||
|             // 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 the capture boolean for browsers with no passive listener support | ||||
|         let options = capture; | ||||
|  | ||||
|         // If passive events listeners are supported | ||||
|         if (support.passiveListeners) { | ||||
|             options = { | ||||
|                 // Whether the listener can be passive (i.e. default never prevented) | ||||
|                 passive, | ||||
|                 // Whether the listener is a capturing listener or not | ||||
|                 capture, | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         // 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 = true, capture = false) { | ||||
|         utils.toggleListener(element, events, callback, true, passive, capture); | ||||
|     }, | ||||
|  | ||||
|     // Unbind event handler | ||||
|     off(element, events = '', callback, passive = true, capture = false) { | ||||
|         utils.toggleListener(element, events, callback, false, passive, capture); | ||||
|     }, | ||||
|  | ||||
|     // Trigger event | ||||
|     dispatchEvent(element, type = '', bubbles = false, detail = {}) { | ||||
|         // Bail if no element | ||||
|         if (!utils.is.element(element) || utils.is.empty(type)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Create and dispatch the event | ||||
|         const event = new CustomEvent(type, { | ||||
|             bubbles, | ||||
|             detail: Object.assign({}, detail, { | ||||
|                 plyr: this, | ||||
|             }), | ||||
|         }); | ||||
|  | ||||
|         // Dispatch the event | ||||
|         element.dispatchEvent(event); | ||||
|     }, | ||||
|  | ||||
|     // Format string | ||||
|     format(input, ...args) { | ||||
|         if (utils.is.empty(input)) { | ||||
|             return input; | ||||
|         } | ||||
|  | ||||
|         return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : '')); | ||||
|     }, | ||||
|  | ||||
|     // Get percentage | ||||
|     getPercentage(current, max) { | ||||
|         if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         return (current / max * 100).toFixed(2); | ||||
|     }, | ||||
|  | ||||
|     // Time helpers | ||||
|     getHours(value) { | ||||
|         return parseInt((value / 60 / 60) % 60, 10); | ||||
|     }, | ||||
|     getMinutes(value) { | ||||
|         return parseInt((value / 60) % 60, 10); | ||||
|     }, | ||||
|     getSeconds(value) { | ||||
|         return parseInt(value % 60, 10); | ||||
|     }, | ||||
|  | ||||
|     // Format time to UI friendly string | ||||
|     formatTime(time = 0, displayHours = false, inverted = false) { | ||||
|         // Bail if the value isn't a number | ||||
|         if (!utils.is.number(time)) { | ||||
|             return utils.formatTime(null, displayHours, inverted); | ||||
|         } | ||||
|  | ||||
|         // Format time component to add leading zero | ||||
|         const format = value => `0${value}`.slice(-2); | ||||
|  | ||||
|         // Breakdown to hours, mins, secs | ||||
|         let hours = utils.getHours(time); | ||||
|         const mins = utils.getMinutes(time); | ||||
|         const secs = utils.getSeconds(time); | ||||
|  | ||||
|         // Do we need to display hours? | ||||
|         if (displayHours || hours > 0) { | ||||
|             hours = `${hours}:`; | ||||
|         } else { | ||||
|             hours = ''; | ||||
|         } | ||||
|  | ||||
|         // Render | ||||
|         return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`; | ||||
|     }, | ||||
|  | ||||
|     // Replace all occurances of a string in a string | ||||
|     replaceAll(input = '', find = '', replace = '') { | ||||
|         return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); | ||||
|     }, | ||||
|  | ||||
|     // Convert to title case | ||||
|     toTitleCase(input = '') { | ||||
|         return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()); | ||||
|     }, | ||||
|  | ||||
|     // Convert string to pascalCase | ||||
|     toPascalCase(input = '') { | ||||
|         let string = input.toString(); | ||||
|  | ||||
|         // Convert kebab case | ||||
|         string = utils.replaceAll(string, '-', ' '); | ||||
|  | ||||
|         // Convert snake case | ||||
|         string = utils.replaceAll(string, '_', ' '); | ||||
|  | ||||
|         // Convert to title case | ||||
|         string = utils.toTitleCase(string); | ||||
|  | ||||
|         // Convert to pascal case | ||||
|         return utils.replaceAll(string, ' ', ''); | ||||
|     }, | ||||
|  | ||||
|     // Convert string to pascalCase | ||||
|     toCamelCase(input = '') { | ||||
|         let string = input.toString(); | ||||
|  | ||||
|         // Convert to pascal case | ||||
|         string = utils.toPascalCase(string); | ||||
|  | ||||
|         // Convert first character to lowercase | ||||
|         return string.charAt(0).toLowerCase() + string.slice(1); | ||||
|     }, | ||||
|  | ||||
|     // Deep extend destination object with N more objects | ||||
|     extend(target = {}, ...sources) { | ||||
|         if (!sources.length) { | ||||
|             return target; | ||||
|         } | ||||
|  | ||||
|         const source = sources.shift(); | ||||
|  | ||||
|         if (!utils.is.object(source)) { | ||||
|             return target; | ||||
|         } | ||||
|  | ||||
|         Object.keys(source).forEach(key => { | ||||
|             if (utils.is.object(source[key])) { | ||||
|                 if (!Object.keys(target).includes(key)) { | ||||
|                     Object.assign(target, { [key]: {} }); | ||||
|                 } | ||||
|  | ||||
|                 utils.extend(target[key], source[key]); | ||||
|             } else { | ||||
|                 Object.assign(target, { [key]: source[key] }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return utils.extend(target, ...sources); | ||||
|     }, | ||||
|  | ||||
|     // Remove duplicates in an array | ||||
|     dedupe(array) { | ||||
|         if (!utils.is.array(array)) { | ||||
|             return array; | ||||
|         } | ||||
|  | ||||
|         return array.filter((item, index) => array.indexOf(item) === index); | ||||
|     }, | ||||
|  | ||||
|     // Clone nested objects | ||||
|     cloneDeep(object) { | ||||
|         return JSON.parse(JSON.stringify(object)); | ||||
|     }, | ||||
|  | ||||
|     // Get a nested value in an object | ||||
|     getDeep(object, path) { | ||||
|         return path.split('.').reduce((obj, key) => obj && obj[key], object); | ||||
|     }, | ||||
|  | ||||
|     // Get the closest value in an array | ||||
|     closest(array, value) { | ||||
|         if (!utils.is.array(array) || !array.length) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev)); | ||||
|     }, | ||||
|  | ||||
|     // Get the provider for a given URL | ||||
|     getProviderByUrl(url) { | ||||
|         // YouTube | ||||
|         if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) { | ||||
|             return providers.youtube; | ||||
|         } | ||||
|  | ||||
|         // Vimeo | ||||
|         if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { | ||||
|             return providers.vimeo; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     }, | ||||
|  | ||||
|     // Parse YouTube ID from URL | ||||
|     parseYouTubeId(url) { | ||||
|         if (utils.is.empty(url)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         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.empty(url)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         if (utils.is.number(Number(url))) { | ||||
|             return url; | ||||
|         } | ||||
|  | ||||
|         const regex = /^.*(vimeo.com\/|video\/)(\d+).*/; | ||||
|         return url.match(regex) ? RegExp.$2 : url; | ||||
|     }, | ||||
|  | ||||
|     // Convert a URL to a location object | ||||
|     parseUrl(url) { | ||||
|         const parser = document.createElement('a'); | ||||
|         parser.href = url; | ||||
|         return parser; | ||||
|     }, | ||||
|  | ||||
|     // Get URL query parameters | ||||
|     getUrlParams(input) { | ||||
|         let search = input; | ||||
|  | ||||
|         // Parse URL if needed | ||||
|         if (input.startsWith('http://') || input.startsWith('https://')) { | ||||
|             ({ search } = utils.parseUrl(input)); | ||||
|         } | ||||
|  | ||||
|         if (utils.is.empty(search)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         const hashes = search.slice(search.indexOf('?') + 1).split('&'); | ||||
|  | ||||
|         return hashes.reduce((params, hash) => { | ||||
|             const [ | ||||
|                 key, | ||||
|                 val, | ||||
|             ] = hash.split('='); | ||||
|  | ||||
|             return Object.assign(params, { [key]: decodeURIComponent(val) }); | ||||
|         }, {}); | ||||
|     }, | ||||
|  | ||||
|     // Convert object to URL parameters | ||||
|     buildUrlParams(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; | ||||
|     }, | ||||
|  | ||||
|     // Like outerHTML, but also works for DocumentFragment | ||||
|     getHTML(element) { | ||||
|         const wrapper = document.createElement('div'); | ||||
|         wrapper.appendChild(element); | ||||
|         return wrapper.innerHTML; | ||||
|     }, | ||||
|  | ||||
|     // Get aspect ratio for dimensions | ||||
|     getAspectRatio(width, height) { | ||||
|         const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); | ||||
|         const ratio = getRatio(width, height); | ||||
|         return `${width / ratio}:${height / ratio}`; | ||||
|     }, | ||||
|  | ||||
|     // Get the transition end event | ||||
|     get transitionEndEvent() { | ||||
|         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 utils.is.string(type) ? events[type] : false; | ||||
|     }, | ||||
|  | ||||
|     // Force repaint of element | ||||
|     repaint(element) { | ||||
|         setTimeout(() => { | ||||
|             utils.toggleHidden(element, true); | ||||
|             element.offsetHeight; // eslint-disable-line | ||||
|             utils.toggleHidden(element, false); | ||||
|         }, 0); | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default utils; | ||||
							
								
								
									
										30
									
								
								src/js/utils/animation.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/js/utils/animation.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| // ========================================================================== | ||||
| // Animation utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import { toggleHidden } from './elements'; | ||||
| import is from './is'; | ||||
|  | ||||
| export const transitionEndEvent = (() => { | ||||
|     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 is.string(type) ? events[type] : false; | ||||
| })(); | ||||
|  | ||||
| // Force repaint of element | ||||
| export function repaint(element) { | ||||
|     setTimeout(() => { | ||||
|         toggleHidden(element, true); | ||||
|         element.offsetHeight; // eslint-disable-line | ||||
|         toggleHidden(element, false); | ||||
|     }, 0); | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/js/utils/arrays.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/js/utils/arrays.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| // ========================================================================== | ||||
| // Array utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import is from './is'; | ||||
|  | ||||
| // Remove duplicates in an array | ||||
| export function dedupe(array) { | ||||
|     if (!is.array(array)) { | ||||
|         return array; | ||||
|     } | ||||
|  | ||||
|     return array.filter((item, index) => array.indexOf(item) === index); | ||||
| } | ||||
|  | ||||
| // Get the closest value in an array | ||||
| export function closest(array, value) { | ||||
|     if (!is.array(array) || !array.length) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev)); | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/js/utils/browser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/js/utils/browser.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| // ========================================================================== | ||||
| // Browser sniffing | ||||
| // Unfortunately, due to mixed support, UA sniffing is required | ||||
| // ========================================================================== | ||||
|  | ||||
| const browser = { | ||||
|     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), | ||||
| }; | ||||
|  | ||||
| export default browser; | ||||
							
								
								
									
										285
									
								
								src/js/utils/elements.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								src/js/utils/elements.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,285 @@ | ||||
| // ========================================================================== | ||||
| // Element utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import { toggleListener } from './events'; | ||||
| import is from './is'; | ||||
|  | ||||
| // Wrap an element | ||||
| export function 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); | ||||
|             } | ||||
|         }); | ||||
| } | ||||
|  | ||||
| // Set attributes | ||||
| export function setAttributes(element, attributes) { | ||||
|     if (!is.element(element) || is.empty(attributes)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Assume null and undefined attributes should be left out, | ||||
|     // Setting them would otherwise convert them to "null" and "undefined" | ||||
|     Object.entries(attributes) | ||||
|         .filter(([, value]) => !is.nullOrUndefined(value)) | ||||
|         .forEach(([key, value]) => element.setAttribute(key, value)); | ||||
| } | ||||
|  | ||||
| // Create a DocumentFragment | ||||
| export function createElement(type, attributes, text) { | ||||
|     // Create a new <element> | ||||
|     const element = document.createElement(type); | ||||
|  | ||||
|     // Set all passed attributes | ||||
|     if (is.object(attributes)) { | ||||
|         setAttributes(element, attributes); | ||||
|     } | ||||
|  | ||||
|     // Add text node | ||||
|     if (is.string(text)) { | ||||
|         element.innerText = text; | ||||
|     } | ||||
|  | ||||
|     // Return built element | ||||
|     return element; | ||||
| } | ||||
|  | ||||
| // Inaert an element after another | ||||
| export function insertAfter(element, target) { | ||||
|     target.parentNode.insertBefore(element, target.nextSibling); | ||||
| } | ||||
|  | ||||
| // Insert a DocumentFragment | ||||
| export function insertElement(type, parent, attributes, text) { | ||||
|     // Inject the new <element> | ||||
|     parent.appendChild(createElement(type, attributes, text)); | ||||
| } | ||||
|  | ||||
| // Remove element(s) | ||||
| export function removeElement(element) { | ||||
|     if (is.nodeList(element) || is.array(element)) { | ||||
|         Array.from(element).forEach(removeElement); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (!is.element(element) || !is.element(element.parentNode)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     element.parentNode.removeChild(element); | ||||
| } | ||||
|  | ||||
| // Remove all child elements | ||||
| export function emptyElement(element) { | ||||
|     let { length } = element.childNodes; | ||||
|  | ||||
|     while (length > 0) { | ||||
|         element.removeChild(element.lastChild); | ||||
|         length -= 1; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Replace element | ||||
| export function replaceElement(newChild, oldChild) { | ||||
|     if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     oldChild.parentNode.replaceChild(newChild, oldChild); | ||||
|  | ||||
|     return newChild; | ||||
| } | ||||
|  | ||||
| // Get an attribute object from a string selector | ||||
| export function getAttributesFromSelector(sel, existingAttributes) { | ||||
|     // For example: | ||||
|     // '.test' to { class: 'test' } | ||||
|     // '#test' to { id: 'test' } | ||||
|     // '[data-test="test"]' to { 'data-test': 'test' } | ||||
|  | ||||
|     if (!is.string(sel) || 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 (is.object(existing) && 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 hidden | ||||
| export function toggleHidden(element, hidden) { | ||||
|     if (!is.element(element)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     let hide = hidden; | ||||
|  | ||||
|     if (!is.boolean(hide)) { | ||||
|         hide = !element.hasAttribute('hidden'); | ||||
|     } | ||||
|  | ||||
|     if (hide) { | ||||
|         element.setAttribute('hidden', ''); | ||||
|     } else { | ||||
|         element.removeAttribute('hidden'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Mirror Element.classList.toggle, with IE compatibility for "force" argument | ||||
| export function toggleClass(element, className, force) { | ||||
|     if (is.element(element)) { | ||||
|         let method = 'toggle'; | ||||
|         if (typeof force !== 'undefined') { | ||||
|             method = force ? 'add' : 'remove'; | ||||
|         } | ||||
|  | ||||
|         element.classList[method](className); | ||||
|         return element.classList.contains(className); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
| } | ||||
|  | ||||
| // Has class name | ||||
| export function hasClass(element, className) { | ||||
|     return is.element(element) && element.classList.contains(className); | ||||
| } | ||||
|  | ||||
| // Element matches selector | ||||
| export function 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 | ||||
| export function getElements(selector) { | ||||
|     return this.elements.container.querySelectorAll(selector); | ||||
| } | ||||
|  | ||||
| // Find a single element | ||||
| export function getElement(selector) { | ||||
|     return this.elements.container.querySelector(selector); | ||||
| } | ||||
|  | ||||
| // Get the focused element | ||||
| export function getFocusElement() { | ||||
|     let focused = document.activeElement; | ||||
|  | ||||
|     if (!focused || focused === document.body) { | ||||
|         focused = null; | ||||
|     } else { | ||||
|         focused = document.querySelector(':focus'); | ||||
|     } | ||||
|  | ||||
|     return focused; | ||||
| } | ||||
|  | ||||
| // Trap focus inside container | ||||
| export function trapFocus(element = null, toggle = false) { | ||||
|     if (!is.element(element)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); | ||||
|     const first = focusable[0]; | ||||
|     const last = focusable[focusable.length - 1]; | ||||
|  | ||||
|     const trap = event => { | ||||
|         // Bail if not tab key or not fullscreen | ||||
|         if (event.key !== 'Tab' || event.keyCode !== 9) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Get the current focused element | ||||
|         const focused = getFocusElement(); | ||||
|  | ||||
|         if (focused === last && !event.shiftKey) { | ||||
|             // Move focus to first element that can be tabbed if Shift isn't used | ||||
|             first.focus(); | ||||
|             event.preventDefault(); | ||||
|         } else if (focused === first && event.shiftKey) { | ||||
|             // Move focus to last element that can be tabbed if Shift is used | ||||
|             last.focus(); | ||||
|             event.preventDefault(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); | ||||
| } | ||||
							
								
								
									
										120
									
								
								src/js/utils/events.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/js/utils/events.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| // ========================================================================== | ||||
| // Event utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import is from './is'; | ||||
|  | ||||
| // Check for passive event listener support | ||||
| // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md | ||||
| // https://www.youtube.com/watch?v=NPM6172J22g | ||||
| const supportsPassiveListeners = (() => { | ||||
|     // 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); | ||||
|         window.removeEventListener('test', null, options); | ||||
|     } catch (e) { | ||||
|         // Do nothing | ||||
|     } | ||||
|  | ||||
|     return supported; | ||||
| })(); | ||||
|  | ||||
| // Toggle event listener | ||||
| export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { | ||||
|     // Bail if no element, event, or callback | ||||
|     if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Allow multiple events | ||||
|     const events = event.split(' '); | ||||
|  | ||||
|     // Build options | ||||
|     // Default to just the capture boolean for browsers with no passive listener support | ||||
|     let options = capture; | ||||
|  | ||||
|     // If passive events listeners are supported | ||||
|     if (supportsPassiveListeners) { | ||||
|         options = { | ||||
|             // Whether the listener can be passive (i.e. default never prevented) | ||||
|             passive, | ||||
|             // Whether the listener is a capturing listener or not | ||||
|             capture, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // If a single node is passed, bind the event listener | ||||
|     events.forEach(type => { | ||||
|         if (this && this.eventListeners && toggle) { | ||||
|             // Cache event listener | ||||
|             this.eventListeners.push({ element, type, callback, options }); | ||||
|         } | ||||
|  | ||||
|         element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Bind event handler | ||||
| export function on(element, events = '', callback, passive = true, capture = false) { | ||||
|     toggleListener.call(this, element, events, callback, true, passive, capture); | ||||
| } | ||||
|  | ||||
| // Unbind event handler | ||||
| export function off(element, events = '', callback, passive = true, capture = false) { | ||||
|     toggleListener.call(this, element, events, callback, false, passive, capture); | ||||
| } | ||||
|  | ||||
| // Bind once-only event handler | ||||
| export function once(element, events = '', callback, passive = true, capture = false) { | ||||
|     function onceCallback(...args) { | ||||
|         off(element, events, onceCallback, passive, capture); | ||||
|         callback.apply(this, args); | ||||
|     } | ||||
|  | ||||
|     toggleListener.call(this, element, events, onceCallback, true, passive, capture); | ||||
| } | ||||
|  | ||||
| // Trigger event | ||||
| export function triggerEvent(element, type = '', bubbles = false, detail = {}) { | ||||
|     // Bail if no element | ||||
|     if (!is.element(element) || is.empty(type)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Create and dispatch the event | ||||
|     const event = new CustomEvent(type, { | ||||
|         bubbles, | ||||
|         detail: Object.assign({}, detail, { | ||||
|             plyr: this, | ||||
|         }), | ||||
|     }); | ||||
|  | ||||
|     // Dispatch the event | ||||
|     element.dispatchEvent(event); | ||||
| } | ||||
|  | ||||
| // Unbind all cached event listeners | ||||
| export function unbindListeners() { | ||||
|     if (this && this.eventListeners) { | ||||
|         this.eventListeners.forEach(item => { | ||||
|             const { element, type, callback, options } = item; | ||||
|             element.removeEventListener(type, callback, options); | ||||
|         }); | ||||
|  | ||||
|         this.eventListeners = []; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Run method when / if player is ready | ||||
| export function ready() { | ||||
|     return new Promise( | ||||
|         resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)), | ||||
|     ).then(() => {}); | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/js/utils/fetch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/js/utils/fetch.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| // ========================================================================== | ||||
| // Fetch wrapper | ||||
| // Using XHR to avoid issues with older browsers | ||||
| // ========================================================================== | ||||
|  | ||||
| export default function fetch(url, responseType = 'text') { | ||||
|     return new Promise((resolve, reject) => { | ||||
|         try { | ||||
|             const request = new XMLHttpRequest(); | ||||
|  | ||||
|             // Check for CORS support | ||||
|             if (!('withCredentials' in request)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             request.addEventListener('load', () => { | ||||
|                 if (responseType === 'text') { | ||||
|                     try { | ||||
|                         resolve(JSON.parse(request.responseText)); | ||||
|                     } catch (e) { | ||||
|                         resolve(request.responseText); | ||||
|                     } | ||||
|                 } else { | ||||
|                     resolve(request.response); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             request.addEventListener('error', () => { | ||||
|                 throw new Error(request.statusText); | ||||
|             }); | ||||
|  | ||||
|             request.open('GET', url, true); | ||||
|  | ||||
|             // Set the required response type | ||||
|             request.responseType = responseType; | ||||
|  | ||||
|             request.send(); | ||||
|         } catch (e) { | ||||
|             reject(e); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										67
									
								
								src/js/utils/is.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/js/utils/is.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | ||||
| // ========================================================================== | ||||
| // Type checking utils | ||||
| // ========================================================================== | ||||
|  | ||||
| const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null); | ||||
|  | ||||
| const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor); | ||||
|  | ||||
| const is = { | ||||
|     object(input) { | ||||
|         return getConstructor(input) === Object; | ||||
|     }, | ||||
|     number(input) { | ||||
|         return getConstructor(input) === Number && !Number.isNaN(input); | ||||
|     }, | ||||
|     string(input) { | ||||
|         return getConstructor(input) === String; | ||||
|     }, | ||||
|     boolean(input) { | ||||
|         return getConstructor(input) === Boolean; | ||||
|     }, | ||||
|     function(input) { | ||||
|         return getConstructor(input) === Function; | ||||
|     }, | ||||
|     array(input) { | ||||
|         return !is.nullOrUndefined(input) && Array.isArray(input); | ||||
|     }, | ||||
|     weakMap(input) { | ||||
|         return instanceOf(input, WeakMap); | ||||
|     }, | ||||
|     nodeList(input) { | ||||
|         return instanceOf(input, NodeList); | ||||
|     }, | ||||
|     element(input) { | ||||
|         return instanceOf(input, Element); | ||||
|     }, | ||||
|     textNode(input) { | ||||
|         return getConstructor(input) === Text; | ||||
|     }, | ||||
|     event(input) { | ||||
|         return instanceOf(input, Event); | ||||
|     }, | ||||
|     cue(input) { | ||||
|         return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); | ||||
|     }, | ||||
|     track(input) { | ||||
|         return instanceOf(input, TextTrack) || (!is.nullOrUndefined(input) && is.string(input.kind)); | ||||
|     }, | ||||
|     url(input) { | ||||
|         return ( | ||||
|             !is.nullOrUndefined(input) && | ||||
|             /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input) | ||||
|         ); | ||||
|     }, | ||||
|     nullOrUndefined(input) { | ||||
|         return input === null || typeof input === 'undefined'; | ||||
|     }, | ||||
|     empty(input) { | ||||
|         return ( | ||||
|             is.nullOrUndefined(input) || | ||||
|             ((is.string(input) || is.array(input) || is.nodeList(input)) && !input.length) || | ||||
|             (is.object(input) && !Object.keys(input).length) | ||||
|         ); | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default is; | ||||
							
								
								
									
										19
									
								
								src/js/utils/loadImage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/js/utils/loadImage.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| // ========================================================================== | ||||
| // Load image avoiding xhr/fetch CORS issues | ||||
| // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded | ||||
| // By default it checks if it is at least 1px, but you can add a second argument to change this | ||||
| // ========================================================================== | ||||
|  | ||||
| export default function loadImage(src, minWidth = 1) { | ||||
|     return new Promise((resolve, reject) => { | ||||
|         const image = new Image(); | ||||
|  | ||||
|         const handler = () => { | ||||
|             delete image.onload; | ||||
|             delete image.onerror; | ||||
|             (image.naturalWidth >= minWidth ? resolve : reject)(image); | ||||
|         }; | ||||
|  | ||||
|         Object.assign(image, { onload: handler, onerror: handler, src }); | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/js/utils/loadScript.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/js/utils/loadScript.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| // ========================================================================== | ||||
| // Load an external script | ||||
| // ========================================================================== | ||||
|  | ||||
| import loadjs from 'loadjs'; | ||||
|  | ||||
| export default function loadScript(url) { | ||||
|     return new Promise((resolve, reject) => { | ||||
|         loadjs(url, { | ||||
|             success: resolve, | ||||
|             error: reject, | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										75
									
								
								src/js/utils/loadSprite.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/js/utils/loadSprite.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | ||||
| // ========================================================================== | ||||
| // Sprite loader | ||||
| // ========================================================================== | ||||
|  | ||||
| import Storage from './../storage'; | ||||
| import is from './is'; | ||||
|  | ||||
| // Load an external SVG sprite | ||||
| export default function loadSprite(url, id) { | ||||
|     if (!is.string(url)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const prefix = 'cache'; | ||||
|     const hasId = is.string(id); | ||||
|     let isCached = false; | ||||
|  | ||||
|     const exists = () => document.getElementById(id) !== null; | ||||
|  | ||||
|     const update = (container, data) => { | ||||
|         container.innerHTML = data; | ||||
|  | ||||
|         // Check again incase of race condition | ||||
|         if (hasId && exists()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Inject the SVG to the body | ||||
|         document.body.insertAdjacentElement('afterbegin', container); | ||||
|     }; | ||||
|  | ||||
|     // Only load once if ID set | ||||
|     if (!hasId || !exists()) { | ||||
|         const useStorage = Storage.supported; | ||||
|  | ||||
|         // Create container | ||||
|         const container = document.createElement('div'); | ||||
|         container.setAttribute('hidden', ''); | ||||
|  | ||||
|         if (hasId) { | ||||
|             container.setAttribute('id', id); | ||||
|         } | ||||
|  | ||||
|         // Check in cache | ||||
|         if (useStorage) { | ||||
|             const cached = window.localStorage.getItem(`${prefix}-${id}`); | ||||
|             isCached = cached !== null; | ||||
|  | ||||
|             if (isCached) { | ||||
|                 const data = JSON.parse(cached); | ||||
|                 update(container, data.content); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Get the sprite | ||||
|         fetch(url) | ||||
|             .then(result => { | ||||
|                 if (is.empty(result)) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (useStorage) { | ||||
|                     window.localStorage.setItem( | ||||
|                         `${prefix}-${id}`, | ||||
|                         JSON.stringify({ | ||||
|                             content: result, | ||||
|                         }), | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 update(container, result); | ||||
|             }) | ||||
|             .catch(() => {}); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/js/utils/objects.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/js/utils/objects.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| // ========================================================================== | ||||
| // Object utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import is from './is'; | ||||
|  | ||||
| // Clone nested objects | ||||
| export function cloneDeep(object) { | ||||
|     return JSON.parse(JSON.stringify(object)); | ||||
| } | ||||
|  | ||||
| // Get a nested value in an object | ||||
| export function getDeep(object, path) { | ||||
|     return path.split('.').reduce((obj, key) => obj && obj[key], object); | ||||
| } | ||||
|  | ||||
| // Deep extend destination object with N more objects | ||||
| export function extend(target = {}, ...sources) { | ||||
|     if (!sources.length) { | ||||
|         return target; | ||||
|     } | ||||
|  | ||||
|     const source = sources.shift(); | ||||
|  | ||||
|     if (!is.object(source)) { | ||||
|         return target; | ||||
|     } | ||||
|  | ||||
|     Object.keys(source).forEach(key => { | ||||
|         if (is.object(source[key])) { | ||||
|             if (!Object.keys(target).includes(key)) { | ||||
|                 Object.assign(target, { [key]: {} }); | ||||
|             } | ||||
|  | ||||
|             extend(target[key], source[key]); | ||||
|         } else { | ||||
|             Object.assign(target, { [key]: source[key] }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     return extend(target, ...sources); | ||||
| } | ||||
							
								
								
									
										85
									
								
								src/js/utils/strings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/js/utils/strings.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| // ========================================================================== | ||||
| // String utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import is from './is'; | ||||
|  | ||||
| // Generate a random ID | ||||
| export function generateId(prefix) { | ||||
|     return `${prefix}-${Math.floor(Math.random() * 10000)}`; | ||||
| } | ||||
|  | ||||
| // Format string | ||||
| export function format(input, ...args) { | ||||
|     if (is.empty(input)) { | ||||
|         return input; | ||||
|     } | ||||
|  | ||||
|     return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString()); | ||||
| } | ||||
|  | ||||
| // Get percentage | ||||
| export function getPercentage(current, max) { | ||||
|     if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     return (current / max * 100).toFixed(2); | ||||
| } | ||||
|  | ||||
| // Replace all occurances of a string in a string | ||||
| export function replaceAll(input = '', find = '', replace = '') { | ||||
|     return input.replace( | ||||
|         new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), | ||||
|         replace.toString(), | ||||
|     ); | ||||
| } | ||||
|  | ||||
| // Convert to title case | ||||
| export function toTitleCase(input = '') { | ||||
|     return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()); | ||||
| } | ||||
|  | ||||
| // Convert string to pascalCase | ||||
| export function toPascalCase(input = '') { | ||||
|     let string = input.toString(); | ||||
|  | ||||
|     // Convert kebab case | ||||
|     string = replaceAll(string, '-', ' '); | ||||
|  | ||||
|     // Convert snake case | ||||
|     string = replaceAll(string, '_', ' '); | ||||
|  | ||||
|     // Convert to title case | ||||
|     string = toTitleCase(string); | ||||
|  | ||||
|     // Convert to pascal case | ||||
|     return replaceAll(string, ' ', ''); | ||||
| } | ||||
|  | ||||
| // Convert string to pascalCase | ||||
| export function toCamelCase(input = '') { | ||||
|     let string = input.toString(); | ||||
|  | ||||
|     // Convert to pascal case | ||||
|     string = toPascalCase(string); | ||||
|  | ||||
|     // Convert first character to lowercase | ||||
|     return string.charAt(0).toLowerCase() + string.slice(1); | ||||
| } | ||||
|  | ||||
| // Remove HTML from a string | ||||
| export function stripHTML(source) { | ||||
|     const fragment = document.createDocumentFragment(); | ||||
|     const element = document.createElement('div'); | ||||
|     fragment.appendChild(element); | ||||
|     element.innerHTML = source; | ||||
|     return fragment.firstChild.innerText; | ||||
| } | ||||
|  | ||||
| // Like outerHTML, but also works for DocumentFragment | ||||
| export function getHTML(element) { | ||||
|     const wrapper = document.createElement('div'); | ||||
|     wrapper.appendChild(element); | ||||
|     return wrapper.innerHTML; | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/js/utils/time.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/js/utils/time.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| // ========================================================================== | ||||
| // Time utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import is from './is'; | ||||
|  | ||||
| // Time helpers | ||||
| export const getHours = value => parseInt((value / 60 / 60) % 60, 10); | ||||
| export const getMinutes = value => parseInt((value / 60) % 60, 10); | ||||
| export const getSeconds = value => parseInt(value % 60, 10); | ||||
|  | ||||
| // Format time to UI friendly string | ||||
| export function formatTime(time = 0, displayHours = false, inverted = false) { | ||||
|     // Bail if the value isn't a number | ||||
|     if (!is.number(time)) { | ||||
|         return formatTime(null, displayHours, inverted); | ||||
|     } | ||||
|  | ||||
|     // Format time component to add leading zero | ||||
|     const format = value => `0${value}`.slice(-2); | ||||
|  | ||||
|     // Breakdown to hours, mins, secs | ||||
|     let hours = getHours(time); | ||||
|     const mins = getMinutes(time); | ||||
|     const secs = getSeconds(time); | ||||
|  | ||||
|     // Do we need to display hours? | ||||
|     if (displayHours || hours > 0) { | ||||
|         hours = `${hours}:`; | ||||
|     } else { | ||||
|         hours = ''; | ||||
|     } | ||||
|  | ||||
|     // Render | ||||
|     return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`; | ||||
| } | ||||
							
								
								
									
										39
									
								
								src/js/utils/urls.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/js/utils/urls.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| // ========================================================================== | ||||
| // URL utils | ||||
| // ========================================================================== | ||||
|  | ||||
| import is from './is'; | ||||
|  | ||||
| /** | ||||
|  * Parse a string to a URL object | ||||
|  * @param {string} input - the URL to be parsed | ||||
|  * @param {boolean} safe - failsafe parsing | ||||
|  */ | ||||
| export function parseUrl(input, safe = true) { | ||||
|     let url = input; | ||||
|  | ||||
|     if (safe) { | ||||
|         const parser = document.createElement('a'); | ||||
|         parser.href = url; | ||||
|         url = parser.href; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|         return new URL(url); | ||||
|     } catch (e) { | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Convert object to URLSearchParams | ||||
| export function buildUrlParams(input) { | ||||
|     const params = new URLSearchParams(); | ||||
|  | ||||
|     if (is.object(input)) { | ||||
|         Object.entries(input).forEach(([key, value]) => { | ||||
|             params.set(key, value); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return params; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user