From 4c1337b4c5e86e22c47dac1d74e3b3298bbc01cb Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 03:21:18 +0200 Subject: [PATCH 01/10] Assure type safety in getSources() and getQualityOptions() (always return arrays), and remove external conditions and type conversion no longer needed --- src/js/html5.js | 53 ++++++++++--------------------------------------- 1 file changed, 11 insertions(+), 42 deletions(-) diff --git a/src/js/html5.js b/src/js/html5.js index 63596cfc..a7ff0bd9 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -8,35 +8,21 @@ import utils from './utils'; const html5 = { getSources() { if (!this.isHTML5) { - return null; + return []; } - return this.media.querySelectorAll('source'); + return Array.from(this.media.querySelectorAll('source')); }, // 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 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; - } + // Get sizes from elements + const sizes = html5.getSources.call(this) + .map(source => Number(source.getAttribute('size'))) + .filter(Boolean); // Reduce to unique list - return utils.dedupe(sizes.map(source => Number(source.getAttribute('size')))); + return utils.dedupe(sizes); }, extend() { @@ -51,34 +37,17 @@ 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)) { - 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; - } + const matches = sources.filter(source => Number(source.getAttribute('size')) === input); // Get supported sources const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type'))); From 62c263bda32434df26b5e63fc646cfe294c98449 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 04:22:40 +0200 Subject: [PATCH 02/10] Replace quality setter conditions with Array.find() --- src/js/plyr.js | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/js/plyr.js b/src/js/plyr.js index 181eff9e..e2fce1fe 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -669,36 +669,28 @@ 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); + let quality = ([ + !utils.is.empty(input) && Number(input), + this.storage.get('quality'), + config.selected, + config.default, + ]).find(utils.is.number); + + if (!options.includes(quality)) { + const closest = utils.closest(options, quality); this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`); quality = closest; } // Update config - this.config.quality.selected = quality; + config.selected = quality; // Set quality this.media.quality = quality; From 41184b82ee8eab9ba7a7a14e514db325812d4965 Mon Sep 17 00:00:00 2001 From: azu Date: Mon, 11 Jun 2018 19:12:35 +0900 Subject: [PATCH 03/10] Fix markdown in README --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 6ffe8bc0..249918ca 100644 --- a/readme.md +++ b/readme.md @@ -215,7 +215,7 @@ You can specify a range of arguments for the constructor to use: * A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) * A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) -* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList) +* A [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) * A [jQuery](https://jquery.com) object _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below. From f15e07f7f54975caf41c975d06138d3846d22c03 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 20:21:37 +0200 Subject: [PATCH 04/10] Simplify logic in youtube.mapQualityUnit (not that it matters much now) --- src/js/plugins/youtube.js | 60 +++++++++++---------------------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 9b067c8a..c759d8d2 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -8,52 +8,26 @@ import utils from './../utils'; // 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) { From ed606c28abec076ba164ec600a743a2bdd3307f2 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 06:34:02 +0200 Subject: [PATCH 05/10] Filter out unsupported mimetypes in getSources() instead of the quality setter --- src/js/html5.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/js/html5.js b/src/js/html5.js index a7ff0bd9..8f23b3c1 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -11,7 +11,10 @@ const html5 = { return []; } - return Array.from(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 @@ -46,14 +49,11 @@ const html5 = { // Get sources const sources = html5.getSources.call(player); - // Get matches for requested size - const matches = sources.filter(source => Number(source.getAttribute('size')) === input); + // Get first match for requested size + const source = sources.find(source => Number(source.getAttribute('size')) === input); - // Get supported sources - const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type'))); - - // No supported sources - if (utils.is.empty(supported)) { + // No matching source found + if (!source) { return; } @@ -66,7 +66,7 @@ const html5 = { const { currentTime, playing } = player; // Set new source - player.media.src = supported[0].getAttribute('src'); + player.media.src = source.getAttribute('src'); // Restore time const onLoadedMetaData = () => { From 81ee3f759c86ded1967555ab22cfef8cd16607b5 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 08:56:13 +0200 Subject: [PATCH 06/10] Remove todo comment about Vimeo support for setting quality (they don't support it) --- src/js/controls.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/js/controls.js b/src/js/controls.js index 058e636f..dbc88a3a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -632,7 +632,6 @@ const controls = { }, // Set the quality menu - // TODO: Vimeo support setQualityMenu(options) { // Menu required if (!utils.is.element(this.elements.settings.panes.quality)) { From 6d2dad58108d4c57e573a70872136c8dbb635d74 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 20:41:53 +0200 Subject: [PATCH 07/10] Trigger qualityrequested event unconditionally when trying to set it (needed for streaming libraries to be able to listen) --- src/js/html5.js | 5 ----- src/js/plugins/youtube.js | 10 +--------- src/js/plyr.js | 3 +++ 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/js/html5.js b/src/js/html5.js index 8f23b3c1..9931ae93 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -57,11 +57,6 @@ const html5 = { return; } - // Trigger change event - utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { - quality: input, - }); - // Get current state const { currentTime, playing } = player; diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index c759d8d2..f7458bcb 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -302,15 +302,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)); }, }); diff --git a/src/js/plyr.js b/src/js/plyr.js index e2fce1fe..46fed3b2 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -689,6 +689,9 @@ class Plyr { quality = closest; } + // Trigger request event + utils.dispatchEvent.call(this, this.media, 'qualityrequested', false, { quality }); + // Update config config.selected = quality; From db95b3234fd38e5dd71d00876c925514960e63fc Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 11 Jun 2018 17:00:34 +0200 Subject: [PATCH 08/10] Move uniqueness filter from getQualityOptions to setQualityMenu --- src/js/controls.js | 5 +++-- src/js/html5.js | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/js/controls.js b/src/js/controls.js index dbc88a3a..8fb2b7b7 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -641,9 +641,10 @@ const controls = { const type = 'quality'; const list = this.elements.settings.panes.quality.querySelector('ul'); - // Set options if passed and filter based on config + // Set options if passed and filter based on uniqueness and config if (utils.is.array(options)) { - this.options.quality = options.filter(quality => this.config.quality.options.includes(quality)); + this.options.quality = utils.dedupe(options) + .filter(quality => this.config.quality.options.includes(quality)); } // Toggle the pane and tab diff --git a/src/js/html5.js b/src/js/html5.js index 9931ae93..fb2bc359 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -20,12 +20,9 @@ const html5 = { // Get quality levels getQualityOptions() { // Get sizes from elements - const sizes = html5.getSources.call(this) + return html5.getSources.call(this) .map(source => Number(source.getAttribute('size'))) .filter(Boolean); - - // Reduce to unique list - return utils.dedupe(sizes); }, extend() { From ee4c044d2746ffc3cb5bd5de5fe6eab6b336a11c Mon Sep 17 00:00:00 2001 From: BoHong Li Date: Tue, 12 Jun 2018 11:35:31 +0800 Subject: [PATCH 09/10] fix: html5.cancelRequest not remove source tag correctly --- src/js/html5.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/html5.js b/src/js/html5.js index fb2bc359..c828dfaf 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -91,7 +91,7 @@ const html5 = { } // Remove child sources - utils.removeElement(html5.getSources()); + utils.removeElement(html5.getSources.call(this)); // Set blank video src attribute // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error From 87170ab46080ae0d82afd1b39ab3fdf2e3ff1e62 Mon Sep 17 00:00:00 2001 From: cky <576779975@qq.com> Date: Tue, 12 Jun 2018 20:55:31 +0800 Subject: [PATCH 10/10] remove event listeners in destroy, add once method --- readme.md | 1 + src/js/captions.js | 4 +-- src/js/controls.js | 4 +-- src/js/fullscreen.js | 4 +-- src/js/html5.js | 3 +-- src/js/listeners.js | 60 +++++++++++++++++++------------------------- src/js/plyr.js | 18 +++++++++---- src/js/utils.js | 32 +++++++++++++++++++---- 8 files changed, 73 insertions(+), 53 deletions(-) diff --git a/readme.md b/readme.md index 249918ca..9dece860 100644 --- a/readme.md +++ b/readme.md @@ -367,6 +367,7 @@ player.fullscreen.enter(); // Enter fullscreen | `airplay()` | - | Trigger the airplay dialog on supported devices. | | `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. | | `on(event, function)` | String, Function | Add an event listener for the specified event. | +| `once(event, function)` | String, Function | Add an event listener for the specified event once. | | `off(event, function)` | String, Function | Remove an event listener for the specified event. | | `supports(type)` | String | Check support for a mime type. | | `destroy()` | - | Destroy the instance and garbage collect any elements. | diff --git a/src/js/captions.js b/src/js/captions.js index bafcf87e..18f4cbd3 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -80,7 +80,7 @@ const captions = { // 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)); + utils.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) @@ -107,7 +107,7 @@ const captions = { track.mode = 'hidden'; // Add event listener for cue changes - utils.on(track, 'cuechange', () => captions.updateCues.call(this)); + utils.on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); }); } diff --git a/src/js/controls.js b/src/js/controls.js index 8fb2b7b7..1301084a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1063,12 +1063,10 @@ const controls = { container.style.width = ''; container.style.height = ''; - // Only listen once - utils.off(container, utils.transitionEndEvent, restore); }; // Listen for the transition finishing and restore auto height/width - utils.on(container, utils.transitionEndEvent, restore); + utils.once(container, utils.transitionEndEvent, restore); // Set dimensions to target container.style.width = `${size.width}px`; diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 000ba706..50681c73 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -62,13 +62,13 @@ class Fullscreen { // Register event listeners // Handle event (incase user presses escape etc) - utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { + utils.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 => { + utils.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)) { return; diff --git a/src/js/html5.js b/src/js/html5.js index fb2bc359..3b0b8c71 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -63,9 +63,8 @@ const html5 = { // 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(); diff --git a/src/js/listeners.js b/src/js/listeners.js index c391ea4c..56d0d177 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -197,39 +197,36 @@ class Listeners { // Add touch class utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); - // Clean up - utils.off(document.body, 'touchstart', this.firstTouch); } // Global window & document listeners global(toggle = true) { // Keyboard shortcuts if (this.player.config.keyboard.global) { - utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false); + utils.toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false); } // Click anywhere closes menu - utils.toggleListener(document.body, 'click', this.toggleMenu, toggle); + utils.toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle); // Detect touch by events - utils.on(document.body, 'touchstart', this.firstTouch); + utils.once(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); + utils.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.on.call(this.player, this.player.elements.container, 'focusout', event => { utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false); }); - // Add classname to tabbed elements - utils.on(this.player.elements.container, 'keydown', event => { + utils.on.call(this.player, this.player.elements.container, 'keydown', event => { if (event.keyCode !== 9) { return; } @@ -242,7 +239,7 @@ class Listeners { }); // Toggle controls on mouse events and entering fullscreen - utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { + utils.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 @@ -276,20 +273,20 @@ class Listeners { // Listen for media events media() { // Time change on media - utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event)); + utils.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)); + utils.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.on.call(this.player, this.player.media, 'loadeddata', () => { utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio); utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); }); // Handle the media finishing - utils.on(this.player.media, 'ended', () => { + utils.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 +295,20 @@ class Listeners { }); // Check for buffer progress - utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); + utils.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)); + utils.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)); + utils.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)); + utils.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', () => { + utils.on.call(this.player, this.player.media, 'playing', () => { if (!this.player.ads) { return; } @@ -334,7 +331,7 @@ class Listeners { } // On click play, pause ore restart - utils.on(wrapper, 'click', () => { + utils.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 +350,7 @@ class Listeners { // Disable right click if (this.player.supported.ui && this.player.config.disableContextMenu) { - utils.on( + utils.on.call(this.player, this.player.elements.wrapper, 'contextmenu', event => { @@ -364,13 +361,13 @@ class Listeners { } // Volume change - utils.on(this.player.media, 'volumechange', () => { + utils.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', () => { + utils.on.call(this.player, this.player.media, 'ratechange', () => { // Update UI controls.updateSetting.call(this.player, 'speed'); @@ -379,19 +376,19 @@ class Listeners { }); // Quality request - utils.on(this.player.media, 'qualityrequested', event => { + utils.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 => { + utils.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', () => { + utils.on.call(this.player, this.player.media, 'languagechange', () => { // Update UI controls.updateSetting.call(this.player, 'captions'); @@ -400,7 +397,7 @@ class Listeners { }); // Captions toggle - utils.on(this.player.media, 'captionsenabled captionsdisabled', () => { + utils.on.call(this.player, this.player.media, 'captionsenabled captionsdisabled', () => { // Update UI controls.updateSetting.call(this.player, 'captions'); @@ -410,7 +407,7 @@ class Listeners { // Proxy events to container // Bubble up key events for Edge - utils.on(this.player.media, this.player.config.events.concat([ + utils.on.call(this.player, this.player.media, this.player.config.events.concat([ 'keyup', 'keydown', ]).join(' '), event => { @@ -452,7 +449,7 @@ class Listeners { const customHandler = this.player.config.listeners[customHandlerKey]; const hasCustomHandler = utils.is.function(customHandler); - utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); + utils.on.call(this.player, element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); }; // Play/pause toggle @@ -727,11 +724,6 @@ class Listeners { false, ); } - - // Reset on destroy - clear() { - this.global(false); - } } export default Listeners; diff --git a/src/js/plyr.js b/src/js/plyr.js index 71ca363e..2d2267da 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -245,6 +245,8 @@ class Plyr { return; } + this.eventListeners = []; + // Create listeners this.listeners = new Listeners(this); @@ -271,7 +273,7 @@ class Plyr { // Listen for events if debugging if (this.config.debug) { - utils.on(this.elements.container, this.config.events.join(' '), event => { + utils.on.call(this, this.elements.container, this.config.events.join(' '), event => { this.debug.log(`event: ${event.type}`); }); } @@ -961,9 +963,16 @@ class Plyr { * @param {function} callback - Callback for when event occurs */ on(event, callback) { - utils.on(this.elements.container, event, callback); + utils.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) { + utils.once(this.elements.container, event, callback); } - /** * Remove event listeners * @param {string} event - Event type @@ -1014,8 +1023,7 @@ class Plyr { } } else { // Unbind listeners - this.listeners.clear(); - + utils.cleanupEventListeners.call(this); // Replace the container with the original element provided utils.replaceElement(this.elements.original, this.elements.container); diff --git a/src/js/utils.js b/src/js/utils.js index c36763dd..b2a06204 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -494,14 +494,14 @@ const utils = { }; if (toggle) { - utils.on(this.elements.container, 'keydown', trap, false); + utils.on.call(this, 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) { + toggleListener(elements, event, callback, toggle = false, passive = true, capture = false, once = false) { // Bail if no elemetns, event, or callback if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) { return; @@ -512,7 +512,7 @@ const utils = { // Create listener for each node Array.from(elements).forEach(element => { if (element instanceof Node) { - utils.toggleListener.call(null, element, event, callback, toggle, passive, capture); + utils.toggleListener.call(this, element, event, callback, toggle, passive, capture, once); } }); @@ -538,13 +538,35 @@ const utils = { // If a single node is passed, bind the event listener events.forEach(type => { + if (this && this.eventListeners && toggle && !once) { + // cache event listener + this.eventListeners.push({ elements, type, callback, options }); + } elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); }); }, - + // remove all cached event listeners + cleanupEventListeners() { + if (this && this.eventListeners) { + this.eventListeners.forEach(item => { + const { elements, type, callback, options } = item; + elements.removeEventListener(type, callback, options); + }); + this.eventListeners = []; + } + }, // Bind event handler on(element, events = '', callback, passive = true, capture = false) { - utils.toggleListener(element, events, callback, true, passive, capture); + utils.toggleListener.call(this, element, events, callback, true, passive, capture); + }, + + // Bind event handler once + once(element, events = '', callback, passive = true, capture = false) { + function onceCallback(...args) { + utils.off(element, events, onceCallback, passive, capture); + callback.apply(this, args); + } + utils.toggleListener(element, events, onceCallback, true, passive, capture, true); }, // Unbind event handler