diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/config/defaults.js | 6 | ||||
-rw-r--r-- | src/js/controls.js | 23 | ||||
-rw-r--r-- | src/js/fullscreen.js | 173 | ||||
-rw-r--r-- | src/js/html5.js | 10 | ||||
-rw-r--r-- | src/js/listeners.js | 19 | ||||
-rw-r--r-- | src/js/plugins/preview-thumbnails.js | 4 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 6 | ||||
-rw-r--r-- | src/js/plyr.d.ts | 63 | ||||
-rw-r--r-- | src/js/plyr.js | 2 | ||||
-rw-r--r-- | src/js/plyr.polyfilled.js | 2 | ||||
-rw-r--r-- | src/js/utils/arrays.js | 8 | ||||
-rw-r--r-- | src/js/utils/elements.js | 34 |
12 files changed, 196 insertions, 154 deletions
diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 90a74a23..bf0f8c42 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -61,7 +61,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.5.7-beta.0/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.5.7/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', @@ -69,6 +69,7 @@ const defaults = { // Quality default quality: { default: 576, + // The options to display in the UI, if available for the source media options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240], forced: false, onChange: null, @@ -84,7 +85,8 @@ const defaults = { // Speed default and options to display speed: { selected: 1, - options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], + // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x) + options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4], }, // Keyboard shortcut settings diff --git a/src/js/controls.js b/src/js/controls.js index 15c82716..1cce51f6 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -9,7 +9,7 @@ import captions from './captions'; import html5 from './html5'; import support from './support'; import { repaint, transitionEndEvent } from './utils/animation'; -import { dedupe } from './utils/arrays'; +import { dedupe, fillRange } from './utils/arrays'; import browser from './utils/browser'; import { createElement, @@ -1044,7 +1044,7 @@ const controls = { }, // Set a list of available captions languages - setSpeedMenu(options) { + setSpeedMenu() { // Menu required if (!is.element(this.elements.settings.panels.speed)) { return; @@ -1053,16 +1053,14 @@ const controls = { const type = 'speed'; const list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); - // Set the speed options - if (is.array(options)) { - this.options.speed = options; - } else if (this.isHTML5 || this.isVimeo) { - this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; + // Determine options to display + // Vimeo and YouTube limit to 0.5x-2x + if (this.isVimeo || this.isYouTube) { + this.options.speed = fillRange(0.5, 2, 0.25).filter(s => this.config.speed.options.includes(s)); + } else { + this.options.speed = this.config.speed.options; } - // Set options if passed and filter based on config - this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed)); - // Toggle the pane and tab const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; controls.toggleMenuButton.call(this, type, toggle); @@ -1582,6 +1580,11 @@ const controls = { target: '_blank', }); + // Set download attribute for HTML5 only + if (this.isHTML5) { + attributes.download = ''; + } + const { download } = this.config.urls; if (!is.url(download) && this.isEmbed) { diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 7ae3ff17..c74b3406 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -5,79 +5,10 @@ // ========================================================================== import browser from './utils/browser'; -import { hasClass, toggleClass, trapFocus } from './utils/elements'; +import { getElements, hasClass, toggleClass } from './utils/elements'; import { on, triggerEvent } from './utils/events'; import is from './utils/is'; -function onChange() { - if (!this.enabled) { - return; - } - - // Update toggle button - const button = this.player.elements.buttons.fullscreen; - if (is.element(button)) { - button.pressed = this.active; - } - - // Trigger an event - triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); - - // Trap focus in container - if (!browser.isIos) { - trapFocus.call(this.player, this.target, this.active); - } -} - -function toggleFallback(toggle = false) { - // Store or restore scroll position - if (toggle) { - this.scrollPosition = { - x: window.scrollX || 0, - y: window.scrollY || 0, - }; - } else { - window.scrollTo(this.scrollPosition.x, this.scrollPosition.y); - } - - // Toggle scroll - document.body.style.overflow = toggle ? 'hidden' : ''; - - // Toggle class hook - toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); - - // Force full viewport on iPhone X+ - if (browser.isIos) { - let viewport = document.head.querySelector('meta[name="viewport"]'); - const property = 'viewport-fit=cover'; - - // Inject the viewport meta if required - if (!viewport) { - viewport = document.createElement('meta'); - viewport.setAttribute('name', 'viewport'); - } - - // Check if the property already exists - const hasProperty = is.string(viewport.content) && viewport.content.includes(property); - - if (toggle) { - this.cleanupViewport = !hasProperty; - - if (!hasProperty) { - viewport.content += `,${property}`; - } - } else if (this.cleanupViewport) { - viewport.content = viewport.content - .split(',') - .filter(part => part.trim() !== property) - .join(','); - } - } - - // Toggle button and fire events - onChange.call(this); -} - class Fullscreen { constructor(player) { // Keep reference to parent @@ -101,7 +32,7 @@ class Fullscreen { this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { // TODO: Filter for target?? - onChange.call(this); + this.onChange(); }, ); @@ -115,6 +46,9 @@ class Fullscreen { this.toggle(); }); + // Tap focus when in fullscreen + on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event)); + // Update the UI this.update(); } @@ -194,6 +128,97 @@ class Fullscreen { : this.player.elements.container; } + onChange() { + if (!this.enabled) { + return; + } + + // Update toggle button + const button = this.player.elements.buttons.fullscreen; + if (is.element(button)) { + button.pressed = this.active; + } + + // Trigger an event + triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); + } + + toggleFallback(toggle = false) { + // Store or restore scroll position + if (toggle) { + this.scrollPosition = { + x: window.scrollX || 0, + y: window.scrollY || 0, + }; + } else { + window.scrollTo(this.scrollPosition.x, this.scrollPosition.y); + } + + // Toggle scroll + document.body.style.overflow = toggle ? 'hidden' : ''; + + // Toggle class hook + toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); + + // Force full viewport on iPhone X+ + if (browser.isIos) { + let viewport = document.head.querySelector('meta[name="viewport"]'); + const property = 'viewport-fit=cover'; + + // Inject the viewport meta if required + if (!viewport) { + viewport = document.createElement('meta'); + viewport.setAttribute('name', 'viewport'); + } + + // Check if the property already exists + const hasProperty = is.string(viewport.content) && viewport.content.includes(property); + + if (toggle) { + this.cleanupViewport = !hasProperty; + + if (!hasProperty) { + viewport.content += `,${property}`; + } + } else if (this.cleanupViewport) { + viewport.content = viewport.content + .split(',') + .filter(part => part.trim() !== property) + .join(','); + } + } + + // Toggle button and fire events + this.onChange(); + } + + // Trap focus inside container + trapFocus(event) { + // Bail if iOS, not active, not the tab key + if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) { + return; + } + + // Get the current focused element + const focused = document.activeElement; + const focusable = getElements.call( + this.player, + 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]', + ); + const [first] = focusable; + const last = focusable[focusable.length - 1]; + + 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(); + } + } + // Update UI update() { if (this.enabled) { @@ -226,9 +251,9 @@ class Fullscreen { if (browser.isIos && this.player.config.fullscreen.iosNative) { this.target.webkitEnterFullscreen(); } else if (!Fullscreen.native || this.forceFallback) { - toggleFallback.call(this, true); + this.toggleFallback(true); } else if (!this.prefix) { - this.target.requestFullscreen({ navigationUI: "hide" }); + this.target.requestFullscreen({ navigationUI: 'hide' }); } else if (!is.empty(this.prefix)) { this.target[`${this.prefix}Request${this.property}`](); } @@ -245,7 +270,7 @@ class Fullscreen { this.target.webkitExitFullscreen(); this.player.play(); } else if (!Fullscreen.native || this.forceFallback) { - toggleFallback.call(this, false); + this.toggleFallback(false); } else if (!this.prefix) { (document.cancelFullScreen || document.exitFullscreen).call(document); } else if (!is.empty(this.prefix)) { diff --git a/src/js/html5.js b/src/js/html5.js index 1173bcbe..d1e82489 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -65,6 +65,10 @@ const html5 = { return source && Number(source.getAttribute('size')); }, set(input) { + if (player.quality === input) { + return; + } + // If we're using an an external handler... if (player.config.quality.forced && is.function(player.config.quality.onChange)) { player.config.quality.onChange(input); @@ -80,7 +84,7 @@ const html5 = { } // Get current state - const { currentTime, paused, preload, readyState } = player.media; + const { currentTime, paused, preload, readyState, playbackRate } = player.media; // Set new source player.media.src = source.getAttribute('src'); @@ -89,10 +93,8 @@ const html5 = { if (preload !== 'none' || readyState) { // Restore time player.once('loadedmetadata', () => { - if (player.currentTime === 0) { - return; - } + player.speed = playbackRate; player.currentTime = currentTime; // Resume playing diff --git a/src/js/listeners.js b/src/js/listeners.js index f68245e4..6a0046ee 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -599,12 +599,19 @@ class Listeners { this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle - this.bind(elements.buttons.settings, 'click', event => { - // Prevent the document click listener closing the menu - event.stopPropagation(); + this.bind( + elements.buttons.settings, + 'click', + event => { + // Prevent the document click listener closing the menu + event.stopPropagation(); + event.preventDefault(); - controls.toggleMenu.call(player, event); - }); + controls.toggleMenu.call(player, event); + }, + null, + false + ); // Can't be passive as we're preventing default // Settings menu - keyboard toggle // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus @@ -725,7 +732,7 @@ class Listeners { }); // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering - this.bind(elements.progress, 'mouseleave click', () => { + this.bind(elements.progress, 'mouseleave touchend click', () => { const { previewThumbnails } = player; if (previewThumbnails && previewThumbnails.loaded) { diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js index 8256c811..e5378bd3 100644 --- a/src/js/plugins/preview-thumbnails.js +++ b/src/js/plugins/preview-thumbnails.js @@ -239,8 +239,8 @@ class PreviewThumbnails { } startScrubbing(event) { - // Only act on left mouse button (0), or touch device (event.button is false) - if (event.button === false || event.button === 0) { + // Only act on left mouse button (0), or touch device (event.button does not exist or is false) + if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) { this.mouseDown = true; // Wait until media has a duration diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 8df5ad15..9529f2cd 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -196,12 +196,6 @@ const vimeo = { .then(() => { speed = input; triggerEvent.call(player, player.media, 'ratechange'); - }) - .catch(error => { - // Hide menu item (and menu if empty) - if (error.name === 'Error') { - controls.setSpeedMenu.call(player, []); - } }); }, }); diff --git a/src/js/plyr.d.ts b/src/js/plyr.d.ts index 4f64898f..cd204a6f 100644 --- a/src/js/plyr.d.ts +++ b/src/js/plyr.d.ts @@ -6,7 +6,6 @@ export = Plyr; export as namespace Plyr; - declare class Plyr { /** * Setup a new instance @@ -201,17 +200,26 @@ declare class Plyr { /** * Add an event listener for the specified event. */ - on(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void; + on( + event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, + callback: (this: this, event: Plyr.PlyrEvent) => void, + ): void; /** * Add an event listener for the specified event once. */ - once(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void; + once( + event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, + callback: (this: this, event: Plyr.PlyrEvent) => void, + ): void; /** * Remove an event listener for the specified event. */ - off(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void; + off( + event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, + callback: (this: this, event: Plyr.PlyrEvent) => void, + ): void; /** * Check support for a mime type. @@ -225,12 +233,39 @@ declare class Plyr { } declare namespace Plyr { - type MediaType = "audio" | "video"; - type Provider = "html5" | "youtube" | "vimeo"; - type StandardEvent = "progress" | "playing" | "play" | "pause" | "timeupdate" | "volumechange" | "seeking" | "seeked" | "ratechange" | "ended" | "enterfullscreen" | "exitfullscreen" - | "captionsenabled" | "captionsdisabled" | "languagechange" | "controlshidden" | "controlsshown" | "ready"; - type Html5Event = "loadstart" | "loadeddata" | "loadedmetadata" | "canplay" | "canplaythrough" | "stalled" | "waiting" | "emptied" | "cuechange" | "error"; - type YoutubeEvent = "statechange" | "qualitychange" | "qualityrequested"; + type MediaType = 'audio' | 'video'; + type Provider = 'html5' | 'youtube' | 'vimeo'; + type StandardEvent = + | 'progress' + | 'playing' + | 'play' + | 'pause' + | 'timeupdate' + | 'volumechange' + | 'seeking' + | 'seeked' + | 'ratechange' + | 'ended' + | 'enterfullscreen' + | 'exitfullscreen' + | 'captionsenabled' + | 'captionsdisabled' + | 'languagechange' + | 'controlshidden' + | 'controlsshown' + | 'ready'; + type Html5Event = + | 'loadstart' + | 'loadeddata' + | 'loadedmetadata' + | 'canplay' + | 'canplaythrough' + | 'stalled' + | 'waiting' + | 'emptied' + | 'cuechange' + | 'error'; + type YoutubeEvent = 'statechange' | 'qualitychange' | 'qualityrequested'; interface FullscreenControl { /** @@ -393,7 +428,7 @@ declare namespace Plyr { * Allows binding of event listeners to the controls before the default handlers. See the defaults.js for available listeners. * If your handler prevents default on the event (event.preventDefault()), the default handler will not fire. */ - listeners?: {[key: string]: (error: PlyrEvent) => void}; + listeners?: { [key: string]: (error: PlyrEvent) => void }; /** * active: Toggles if captions should be active by default. language: Sets the default language to load (if available). 'auto' uses the browser language. @@ -418,7 +453,7 @@ declare namespace Plyr { storage?: StorageOptions; /** - * selected: The default speed for playback. options: Options to display in the menu. Most browsers will refuse to play slower than 0.5. + * selected: The default speed for playback. options: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. */ speed?: SpeedOptions; @@ -527,7 +562,7 @@ declare namespace Plyr { size?: number; } - type TrackKind = "subtitles" | "captions" | "descriptions" | "chapters" | "metadata"; + type TrackKind = 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata'; interface Track { /** * Indicates how the text track is meant to be used @@ -550,7 +585,7 @@ declare namespace Plyr { } interface PlyrEvent extends CustomEvent { - readonly detail: { readonly plyr: Plyr; }; + readonly detail: { readonly plyr: Plyr }; } interface Support { diff --git a/src/js/plyr.js b/src/js/plyr.js index 8dc2b388..b5c4612c 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.5.7-beta.0 +// plyr.js v3.5.7 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 03e9a0f4..8b86a644 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.5.7-beta.0 +// plyr.js v3.5.7 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/utils/arrays.js b/src/js/utils/arrays.js index 69ef242c..c0d69626 100644 --- a/src/js/utils/arrays.js +++ b/src/js/utils/arrays.js @@ -21,3 +21,11 @@ export function closest(array, value) { return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev)); } + +export function fillRange(start, end, step = 1) { + const len = Math.floor((end - start) / step) + 1; + + return Array(len) + .fill() + .map((_, idx) => start + idx * step); +} diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 4f10938e..b88aad0c 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -2,7 +2,6 @@ // Element utils // ========================================================================== -import { toggleListener } from './events'; import is from './is'; import { extend } from './objects'; @@ -248,39 +247,6 @@ export function getElement(selector) { return this.elements.container.querySelector(selector); } -// 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 = document.activeElement; - - 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); -} - // Set focus and tab focus class export function setFocus(element = null, tabFocus = false) { if (!is.element(element)) { |