diff options
Diffstat (limited to 'src/js')
-rw-r--r-- | src/js/config/defaults.js | 3 | ||||
-rw-r--r-- | src/js/controls.js | 470 | ||||
-rw-r--r-- | src/js/listeners.js | 149 | ||||
-rw-r--r-- | src/js/plyr.js | 7 | ||||
-rw-r--r-- | src/js/utils/elements.js | 13 |
5 files changed, 306 insertions, 336 deletions
diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 1e90a4f0..a898755d 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -354,6 +354,9 @@ const defaults = { isTouch: 'plyr--is-touch', uiSupported: 'plyr--full-ui', noTransition: 'plyr--no-transition', + display: { + time: 'plyr__time', + }, menu: { value: 'plyr__menu__value', badge: 'plyr__badge', diff --git a/src/js/controls.js b/src/js/controls.js index d79aaee7..710ee93b 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -9,19 +9,7 @@ import support from './support'; import { repaint, transitionEndEvent } from './utils/animation'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; -import { - createElement, - emptyElement, - getAttributesFromSelector, - getElement, - getElements, - hasClass, - removeElement, - setAttributes, - toggleClass, - toggleHidden, - matches, -} from './utils/elements'; +import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, toggleClass, toggleHidden } from './utils/elements'; import { off, on } from './utils/events'; import is from './utils/is'; import loadSprite from './utils/loadSprite'; @@ -360,7 +348,7 @@ const controls = { const container = createElement( 'div', extend(attributes, { - class: `plyr__time ${attributes.class}`, + class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(), 'aria-label': i18n.get(type, this.config), }), '00:00', @@ -374,34 +362,75 @@ const controls = { // Create a settings menu item createMenuItem({ value, list, type, title, badge = null, checked = false }) { - const item = createElement('li'); + const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); - const label = createElement('label', { - class: this.config.classNames.control, - }); - - const radio = createElement( - 'input', - extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { - type: 'radio', - name: `plyr-${type}`, + const item = createElement( + 'button', + extend(attributes, { + type: 'button', + role: 'menuitemradio', + class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(), + 'aria-checked': checked, value, - checked, - class: 'plyr__sr-only', }), ); - const faux = createElement('span', { hidden: '' }); + const flex = createElement('span'); - label.appendChild(radio); - label.appendChild(faux); - label.insertAdjacentHTML('beforeend', title); + // We have to set as HTML incase of special characters + flex.innerHTML = title; if (is.element(badge)) { - label.appendChild(badge); + flex.appendChild(badge); } - item.appendChild(label); + item.appendChild(flex); + + Object.defineProperty(item, 'checked', { + enumerable: true, + get() { + return item.getAttribute('aria-checked') === 'true'; + }, + set(checked) { + // Ensure exclusivity + if (checked) { + Array.from(item.parentNode.children) + .filter(node => matches(node, '[role="menuitemradio"]')) + .forEach(node => node.setAttribute('aria-checked', 'false')); + } + + item.setAttribute('aria-checked', checked ? 'true' : 'false'); + }, + }); + + this.listeners.bind( + item, + 'click', + () => { + item.checked = true; + + switch (type) { + case 'language': + this.currentTrack = Number(value); + break; + + case 'quality': + this.quality = value; + break; + + case 'speed': + this.speed = parseFloat(value); + break; + + default: + break; + } + + controls.showMenuPanel.call(this, 'home'); + }, + type, + ); + list.appendChild(item); }, @@ -656,19 +685,97 @@ const controls = { }, // Hide/show a tab - toggleTab(setting, toggle) { - toggleHidden(this.elements.settings.tabs[setting], !toggle); + toggleMenuButton(setting, toggle) { + toggleHidden(this.elements.settings.buttons[setting], !toggle); + }, + + // Update the selected setting + updateSetting(setting, container, input) { + const pane = this.elements.settings.panels[setting]; + let value = null; + let list = container; + + if (setting === 'captions') { + value = this.currentTrack; + } else { + value = !is.empty(input) ? input : this[setting]; + + // Get default + if (is.empty(value)) { + value = this.config[setting].default; + } + + // Unsupported value + if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { + this.debug.warn(`Unsupported value of '${value}' for ${setting}`); + return; + } + + // Disabled value + if (!this.config[setting].options.includes(value)) { + this.debug.warn(`Disabled value of '${value}' for ${setting}`); + return; + } + } + + // Get the list if we need to + if (!is.element(list)) { + list = pane && pane.querySelector('[role="menu"]'); + } + + // If there's no list it means it's not been rendered... + if (!is.element(list)) { + return; + } + + // Update the label + const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`); + label.innerHTML = controls.getLabel.call(this, setting, value); + + // Find the radio option and check it + const target = list && list.querySelector(`[value="${value}"]`); + + if (is.element(target)) { + target.checked = true; + } + }, + + // Translate a value into a nice label + getLabel(setting, value) { + switch (setting) { + case 'speed': + return value === 1 ? i18n.get('normal', this.config) : `${value}×`; + + case 'quality': + if (is.number(value)) { + const label = i18n.get(`qualityLabel.${value}`, this.config); + + if (!label.length) { + return `${value}p`; + } + + return label; + } + + return toTitleCase(value); + + case 'captions': + return captions.getLabel.call(this); + + default: + return null; + } }, // Set the quality menu setQualityMenu(options) { // Menu required - if (!is.element(this.elements.settings.panes.quality)) { + if (!is.element(this.elements.settings.panels.quality)) { return; } const type = 'quality'; - const list = this.elements.settings.panes.quality.querySelector('ul'); + const list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); // Set options if passed and filter based on uniqueness and config if (is.array(options)) { @@ -677,7 +784,10 @@ const controls = { // Toggle the pane and tab const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; - controls.toggleTab.call(this, type, toggle); + controls.toggleMenuButton.call(this, type, toggle); + + // Empty the menu + emptyElement(list); // Check if we need to toggle the parent controls.checkMenu.call(this); @@ -687,9 +797,6 @@ const controls = { return; } - // Empty the menu - emptyElement(list); - // Get the badge HTML for HD, 4K etc const getBadge = quality => { const label = i18n.get(`qualityBadge.${quality}`, this.config); @@ -720,101 +827,23 @@ const controls = { controls.updateSetting.call(this, type, list); }, - // Translate a value into a nice label - getLabel(setting, value) { - switch (setting) { - case 'speed': - return value === 1 ? i18n.get('normal', this.config) : `${value}×`; - - case 'quality': - if (is.number(value)) { - const label = i18n.get(`qualityLabel.${value}`, this.config); - - if (!label.length) { - return `${value}p`; - } - - return label; - } - - return toTitleCase(value); - - case 'captions': - return captions.getLabel.call(this); - - default: - return null; - } - }, - - // Update the selected setting - updateSetting(setting, container, input) { - const pane = this.elements.settings.panes[setting]; - let value = null; - let list = container; - - if (setting === 'captions') { - value = this.currentTrack; - } else { - value = !is.empty(input) ? input : this[setting]; - - // Get default - if (is.empty(value)) { - value = this.config[setting].default; - } - - // Unsupported value - if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { - this.debug.warn(`Unsupported value of '${value}' for ${setting}`); - return; - } - - // Disabled value - if (!this.config[setting].options.includes(value)) { - this.debug.warn(`Disabled value of '${value}' for ${setting}`); - return; - } - } - - // Get the list if we need to - if (!is.element(list)) { - list = pane && pane.querySelector('ul'); - } - - // If there's no list it means it's not been rendered... - if (!is.element(list)) { - return; - } - - // Update the label - const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`); - label.innerHTML = controls.getLabel.call(this, setting, value); - - // Find the radio option and check it - const target = list && list.querySelector(`input[value="${value}"]`); - - if (is.element(target)) { - target.checked = true; - } - }, - // Set the looping options /* setLoopMenu() { // Menu required - if (!is.element(this.elements.settings.panes.loop)) { + if (!is.element(this.elements.settings.panels.loop)) { return; } const options = ['start', 'end', 'all', 'reset']; - const list = this.elements.settings.panes.loop.querySelector('ul'); + const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); // Show the pane and tab - toggleHidden(this.elements.settings.tabs.loop, false); - toggleHidden(this.elements.settings.panes.loop, false); + toggleHidden(this.elements.settings.buttons.loop, false); + toggleHidden(this.elements.settings.panels.loop, false); // Toggle the pane and tab const toggle = !is.empty(this.loop.options); - controls.toggleTab.call(this, 'loop', toggle); + controls.toggleMenuButton.call(this, 'loop', toggle); // Empty the menu emptyElement(list); @@ -847,13 +876,19 @@ const controls = { // Set a list of available captions languages setCaptionsMenu() { + // Menu required + if (!is.element(this.elements.settings.panels.captions)) { + return; + } + // TODO: Captions or language? Currently it's mixed const type = 'captions'; - const list = this.elements.settings.panes.captions.querySelector('ul'); + const list = this.elements.settings.panels.captions.querySelector('[role="menu"]'); const tracks = captions.getTracks.call(this); + const toggle = Boolean(tracks.length); // Toggle the pane and tab - controls.toggleTab.call(this, type, tracks.length); + controls.toggleMenuButton.call(this, type, toggle); // Empty the menu emptyElement(list); @@ -862,7 +897,7 @@ const controls = { controls.checkMenu.call(this); // If there's no captions, bail - if (!tracks.length) { + if (!toggle) { return; } @@ -893,17 +928,13 @@ const controls = { // Set a list of available captions languages setSpeedMenu(options) { - // Do nothing if not selected - if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) { - return; - } - // Menu required - if (!is.element(this.elements.settings.panes.speed)) { + if (!is.element(this.elements.settings.panels.speed)) { return; } const type = 'speed'; + const list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options if (is.array(options)) { @@ -917,7 +948,10 @@ const controls = { // Toggle the pane and tab const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; - controls.toggleTab.call(this, type, toggle); + controls.toggleMenuButton.call(this, type, toggle); + + // Empty the menu + emptyElement(list); // Check if we need to toggle the parent controls.checkMenu.call(this); @@ -927,12 +961,6 @@ const controls = { return; } - // Get the list to populate - const list = this.elements.settings.panes.speed.querySelector('ul'); - - // Empty the menu - emptyElement(list); - // Create items this.options.speed.forEach(speed => { controls.createMenuItem.call(this, { @@ -948,26 +976,26 @@ const controls = { // Check if we need to hide/show the settings menu checkMenu() { - const { tabs } = this.elements.settings; - const visible = !is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden); + const { buttons } = this.elements.settings; + const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden); toggleHidden(this.elements.settings.menu, !visible); }, // Show/hide menu toggleMenu(event) { - const { form } = this.elements.settings; + const { popup } = this.elements.settings; const button = this.elements.buttons.settings; // Menu and button are required - if (!is.element(form) || !is.element(button)) { + if (!is.element(popup) || !is.element(button)) { return; } - const show = is.boolean(event) ? event : is.element(form) && form.hasAttribute('hidden'); + const show = is.boolean(event) ? event : is.element(popup) && popup.hasAttribute('hidden'); if (is.event(event)) { - const isMenuItem = is.element(form) && form.contains(event.target); + const isMenuItem = is.element(popup) && popup.contains(event.target); const isButton = event.target === this.elements.buttons.settings; // If the click was inside the form or if the click @@ -988,31 +1016,25 @@ const controls = { button.setAttribute('aria-expanded', show); } - if (is.element(form)) { - toggleHidden(form, !show); + if (is.element(popup)) { + toggleHidden(popup, !show); toggleClass(this.elements.container, this.config.classNames.menu.open, show); if (show) { - form.removeAttribute('tabindex'); + popup.removeAttribute('tabindex'); } else { - form.setAttribute('tabindex', -1); + popup.setAttribute('tabindex', -1); } } }, - // Get the natural size of a tab - getTabSize(tab) { + // Get the natural size of a menu panel + getMenuSize(tab) { const clone = tab.cloneNode(true); clone.style.position = 'absolute'; clone.style.opacity = 0; clone.removeAttribute('hidden'); - // Prevent input's being unchecked due to the name being identical - Array.from(clone.querySelectorAll('input[name]')).forEach(input => { - const name = input.getAttribute('name'); - input.setAttribute('name', `${name}-clone`); - }); - // Append to parent so we get the "real" size tab.parentNode.appendChild(clone); @@ -1029,31 +1051,18 @@ const controls = { }; }, - // Toggle Menu - showTab(target = '') { - const { menu } = this.elements.settings; - const pane = document.getElementById(target); + // Show a panel in the menu + showMenuPanel(type = '') { + const target = document.getElementById(`plyr-settings-${this.id}-${type}`); // Nothing to show, bail - if (!is.element(pane)) { - return; - } - - // Are we targeting a tab? If not, bail - const isTab = pane.getAttribute('role') === 'tabpanel'; - if (!isTab) { + if (!is.element(target)) { return; } - // Hide all other tabs - // Get other tabs - const current = menu.querySelector('[role="tabpanel"]:not([hidden])'); - const container = current.parentNode; - - // Set other toggles to be expanded false - Array.from(menu.querySelectorAll(`[aria-controls="${current.getAttribute('id')}"]`)).forEach(toggle => { - toggle.setAttribute('aria-expanded', false); - }); + // Hide all other panels + const container = target.parentNode; + const current = Array.from(container.children).find(node => !node.hidden); // If we can do fancy animations, we'll animate the height/width if (support.transitions && !support.reducedMotion) { @@ -1062,12 +1071,12 @@ const controls = { container.style.height = `${current.scrollHeight}px`; // Get potential sizes - const size = controls.getTabSize.call(this, pane); + const size = controls.getMenuSize.call(this, target); // Restore auto height/width - const restore = e => { + const restore = event => { // We're only bothered about height and width on the container - if (e.target !== container || !['width', 'height'].includes(e.propertyName)) { + if (event.target !== container || !['width', 'height'].includes(event.propertyName)) { return; } @@ -1089,19 +1098,15 @@ const controls = { // Set attributes on current tab toggleHidden(current, true); - current.setAttribute('tabindex', -1); // Set attributes on target - toggleHidden(pane, false); - - const tabs = getElements.call(this, `[aria-controls="${target}"]`); - Array.from(tabs).forEach(tab => { - tab.setAttribute('aria-expanded', true); - }); - pane.removeAttribute('tabindex'); + toggleHidden(target, false); // Focus the first item - pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus(); + const firstItem = target.querySelector('[role^="menuitem"]'); + if (firstItem) { + firstItem.focus(); + } }, // Build the default HTML @@ -1220,12 +1225,12 @@ const controls = { // Settings button / menu if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) { - const menu = createElement('div', { + const control = createElement('div', { class: 'plyr__menu', hidden: '', }); - menu.appendChild( + control.appendChild( controls.createButton.call(this, 'settings', { id: `plyr-settings-toggle-${data.id}`, 'aria-haspopup': true, @@ -1234,48 +1239,39 @@ const controls = { }), ); - const form = createElement('form', { + const popup = createElement('div', { class: 'plyr__menu__container', id: `plyr-settings-${data.id}`, hidden: '', 'aria-labelled-by': `plyr-settings-toggle-${data.id}`, - role: 'tablist', - tabindex: -1, }); const inner = createElement('div'); const home = createElement('div', { id: `plyr-settings-${data.id}-home`, - 'aria-labelled-by': `plyr-settings-toggle-${data.id}`, - role: 'tabpanel', }); - // Create the tab list - const tabs = createElement('ul', { - role: 'tablist', + // Create the menu + const menu = createElement('div', { + role: 'menu', }); - // Build the tabs + // Build the menu items this.config.settings.forEach(type => { - const tab = createElement('li', { - role: 'tab', - hidden: '', - }); - - const button = createElement( + const menuItem = createElement( 'button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { type: 'button', class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, - id: `plyr-settings-${data.id}-${type}-tab`, + role: 'menuitem', 'aria-haspopup': true, - 'aria-controls': `plyr-settings-${data.id}-${type}`, - 'aria-expanded': false, + hidden: '', }), - i18n.get(type, this.config), ); + const flex = createElement('span', null, i18n.get(type, this.config)); + const value = createElement('span', { class: this.config.classNames.menu.value, }); @@ -1283,54 +1279,56 @@ const controls = { // Speed contains HTML entities value.innerHTML = data[type]; - button.appendChild(value); - tab.appendChild(button); - tabs.appendChild(tab); + flex.appendChild(value); + menuItem.appendChild(flex); + menu.appendChild(menuItem); - this.elements.settings.tabs[type] = tab; - }); - - home.appendChild(tabs); - inner.appendChild(home); - - // Build the panes - this.config.settings.forEach(type => { + // Build the panes const pane = createElement('div', { id: `plyr-settings-${data.id}-${type}`, hidden: '', - 'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`, - role: 'tabpanel', - tabindex: -1, }); + // Back button const back = createElement( 'button', { type: 'button', class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, - 'aria-haspopup': true, - 'aria-controls': `plyr-settings-${data.id}-home`, - 'aria-expanded': false, }, i18n.get(type, this.config), ); - + back.addEventListener('click', () => { + controls.showMenuPanel.call(this, 'home'); + }); pane.appendChild(back); - const options = createElement('ul'); + // Menu + pane.appendChild( + createElement('div', { + role: 'menu', + }), + ); - pane.appendChild(options); inner.appendChild(pane); - this.elements.settings.panes[type] = pane; + menuItem.addEventListener('click', () => { + controls.showMenuPanel.call(this, type); + }); + + this.elements.settings.buttons[type] = menuItem; + this.elements.settings.panels[type] = pane; }); - form.appendChild(inner); - menu.appendChild(form); - container.appendChild(menu); + home.appendChild(menu); + inner.appendChild(home); + + popup.appendChild(inner); + control.appendChild(popup); + container.appendChild(control); - this.elements.settings.form = form; - this.elements.settings.menu = menu; + this.elements.settings.popup = popup; + this.elements.settings.menu = control; } // Picture in picture button diff --git a/src/js/listeners.js b/src/js/listeners.js index 7615e365..05a12147 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -243,7 +243,8 @@ class Listeners { // Clear timer clearTimeout(this.player.timers.controls); - // Timer to prevent flicker when seeking + + // Set new timer to prevent flicker when seeking this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); }, ); @@ -394,60 +395,60 @@ class Listeners { }); } - // Listen for control events - controls() { - // IE doesn't support input event, so we fallback to change - const inputEvent = browser.isIE ? 'change' : 'input'; + // Run default and custom handlers + proxy(event, defaultHandler, customHandlerKey) { + const customHandler = this.player.config.listeners[customHandlerKey]; + const hasCustomHandler = is.function(customHandler); + let returned = true; - // Run default and custom handlers - const proxy = (event, defaultHandler, customHandlerKey) => { - const customHandler = this.player.config.listeners[customHandlerKey]; - const hasCustomHandler = is.function(customHandler); - let returned = true; + // Execute custom handler + if (hasCustomHandler) { + returned = customHandler.call(this.player, event); + } - // Execute custom handler - if (hasCustomHandler) { - returned = customHandler.call(this.player, event); - } + // Only call default handler if not prevented in custom handler + if (returned && is.function(defaultHandler)) { + defaultHandler.call(this.player, event); + } + } - // Only call default handler if not prevented in custom handler - if (returned && is.function(defaultHandler)) { - defaultHandler.call(this.player, event); - } - }; + // Trigger custom and default handlers + bind(element, type, defaultHandler, customHandlerKey, passive = true) { + const customHandler = this.player.config.listeners[customHandlerKey]; + const hasCustomHandler = is.function(customHandler); - // Trigger custom and default handlers - const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => { - const customHandler = this.player.config.listeners[customHandlerKey]; - const hasCustomHandler = is.function(customHandler); + on.call( + this.player, + element, + type, + event => this.proxy(event, defaultHandler, customHandlerKey), + passive && !hasCustomHandler, + ); + } - on.call( - this.player, - element, - type, - event => proxy(event, defaultHandler, customHandlerKey), - passive && !hasCustomHandler, - ); - }; + // Listen for control events + controls() { + // IE doesn't support input event, so we fallback to change + const inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle if (this.player.elements.buttons.play) { Array.from(this.player.elements.buttons.play).forEach(button => { - bind(button, 'click', this.player.togglePlay, 'play'); + this.bind(button, 'click', this.player.togglePlay, 'play'); }); } // Pause - bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); + this.bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); // Rewind - bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); + this.bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); // Rewind - bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); + this.bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); // Mute toggle - bind( + this.bind( this.player.elements.buttons.mute, 'click', () => { @@ -457,10 +458,10 @@ class Listeners { ); // Captions toggle - bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions()); + this.bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions()); // Fullscreen toggle - bind( + this.bind( this.player.elements.buttons.fullscreen, 'click', () => { @@ -470,7 +471,7 @@ class Listeners { ); // Picture-in-Picture - bind( + this.bind( this.player.elements.buttons.pip, 'click', () => { @@ -480,66 +481,22 @@ class Listeners { ); // Airplay - bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); + this.bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); // Settings menu - bind(this.player.elements.buttons.settings, 'click', event => { + this.bind(this.player.elements.buttons.settings, 'click', event => { controls.toggleMenu.call(this.player, event); }); - // Settings menu - bind(this.player.elements.settings.form, 'click', event => { - event.stopPropagation(); - - // Go back to home tab on click - const showHomeTab = () => { - const id = `plyr-settings-${this.player.id}-home`; - controls.showTab.call(this.player, id); - }; - - // Settings menu items - use event delegation as items are added/removed - if (matches(event.target, this.player.config.selectors.inputs.language)) { - proxy( - event, - () => { - this.player.currentTrack = Number(event.target.value); - showHomeTab(); - }, - 'language', - ); - } else if (matches(event.target, this.player.config.selectors.inputs.quality)) { - proxy( - event, - () => { - this.player.quality = event.target.value; - showHomeTab(); - }, - 'quality', - ); - } else if (matches(event.target, this.player.config.selectors.inputs.speed)) { - proxy( - event, - () => { - this.player.speed = parseFloat(event.target.value); - showHomeTab(); - }, - 'speed', - ); - } else { - const tab = event.target; - controls.showTab.call(this.player, tab.getAttribute('aria-controls')); - } - }); - // Set range input alternative "value", which matches the tooltip time (#954) - bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => { + this.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 - bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { + this.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; @@ -565,7 +522,7 @@ class Listeners { }); // Seek - bind( + this.bind( this.player.elements.inputs.seek, inputEvent, event => { @@ -588,7 +545,7 @@ class Listeners { // Current time invert // Only if one time element is used for both currentTime and duration if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) { - bind(this.player.elements.display.currentTime, 'click', () => { + this.bind(this.player.elements.display.currentTime, 'click', () => { // Do nothing if we're at the start if (this.player.currentTime === 0) { return; @@ -601,7 +558,7 @@ class Listeners { } // Volume - bind( + this.bind( this.player.elements.inputs.volume, inputEvent, event => { @@ -613,27 +570,27 @@ class Listeners { // Polyfill for lower fill in <input type="range"> for webkit if (browser.isWebkit) { Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => { - bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target)); + this.bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target)); }); } // Seek tooltip - bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => + this.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) - bind(this.player.elements.controls, 'mouseenter mouseleave', event => { + this.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) - bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { + this.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 - bind(this.player.elements.controls, 'focusin focusout', event => { + this.bind(this.player.elements.controls, 'focusin focusout', event => { const { config, elements, timers } = this.player; // Skip transition to prevent focus from scrolling the parent element @@ -660,7 +617,7 @@ class Listeners { }); // Mouse wheel for volume - bind( + this.bind( this.player.elements.inputs.volume, 'wheel', event => { diff --git a/src/js/plyr.js b/src/js/plyr.js index 374251e6..9f3def07 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -75,16 +75,17 @@ class Plyr { // Elements cache this.elements = { container: null, + captions: null, buttons: {}, display: {}, progress: {}, inputs: {}, settings: { + popup: null, menu: null, - panes: {}, - tabs: {}, + panels: {}, + buttons: {}, }, - captions: null, }; // Captions diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 69e4d46c..7b58c9ff 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -70,12 +70,19 @@ export function createElement(type, attributes, text) { // Inaert an element after another export function insertAfter(element, target) { + if (!is.element(element) || !is.element(target)) { + return; + } + target.parentNode.insertBefore(element, target.nextSibling); } // Insert a DocumentFragment export function insertElement(type, parent, attributes, text) { - // Inject the new <element> + if (!is.element(parent)) { + return; + } + parent.appendChild(createElement(type, attributes, text)); } @@ -95,6 +102,10 @@ export function removeElement(element) { // Remove all child elements export function emptyElement(element) { + if (!is.element(element)) { + return; + } + let { length } = element.childNodes; while (length > 0) { |