diff options
author | Sam Potts <sam@potts.es> | 2019-04-25 12:11:06 +1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-25 12:11:06 +1000 |
commit | e644eeb5b62e6e2eb915d5915cb65ba8af88c05e (patch) | |
tree | 60f96b088ce8490429199163a5e71768b8a9b52c /src/js/controls.js | |
parent | 2bd08cdc28bf55b7f7a169b02eb288d7b197b8d6 (diff) | |
parent | 5ddd9e02def654bb677c988403dbefbc4a32787c (diff) | |
download | plyr-e644eeb5b62e6e2eb915d5915cb65ba8af88c05e.tar.lz plyr-e644eeb5b62e6e2eb915d5915cb65ba8af88c05e.tar.xz plyr-e644eeb5b62e6e2eb915d5915cb65ba8af88c05e.zip |
Merge pull request #1423 from sampotts/develop
v3.5.4
Diffstat (limited to 'src/js/controls.js')
-rw-r--r-- | src/js/controls.js | 562 |
1 files changed, 293 insertions, 269 deletions
diff --git a/src/js/controls.js b/src/js/controls.js index 73903e16..9a960b38 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -10,20 +10,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, - matches, - removeElement, - setAttributes, - setFocus, - toggleClass, - toggleHidden, -} from './utils/elements'; +import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, setFocus, toggleClass, toggleHidden } from './utils/elements'; import { off, on } from './utils/events'; import i18n from './utils/i18n'; import is from './utils/is'; @@ -172,7 +159,7 @@ const controls = { // Create a <button> createButton(buttonType, attr) { - const attributes = Object.assign({}, attr); + const attributes = extend({}, attr); let type = toCamelCase(buttonType); const props = { @@ -198,8 +185,10 @@ const controls = { // Set class name if (Object.keys(attributes).includes('class')) { - if (!attributes.class.includes(this.config.classNames.control)) { - attributes.class += ` ${this.config.classNames.control}`; + if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) { + extend(attributes, { + class: `${attributes.class} ${this.config.classNames.control}`, + }); } } else { attributes.class = this.config.classNames.control; @@ -377,13 +366,13 @@ const controls = { }, // Create time display - createTime(type) { - const attributes = getAttributesFromSelector(this.config.selectors.display[type]); + createTime(type, attrs) { + const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs); const container = createElement( 'div', extend(attributes, { - class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(), + class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(), 'aria-label': i18n.get(type, this.config), }), '00:00', @@ -1138,7 +1127,10 @@ const controls = { } else if (is.keyboardEvent(input) && input.which === 27) { show = false; } else if (is.event(input)) { - const isMenuItem = popup.contains(input.target); + // If Plyr is in a shadowDOM, the event target is set to the component, instead of the + // Element in the shadowDOM. The path, if available, is complete. + const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target; + const isMenuItem = popup.contains(target); // If the click was inside the menu or if the click // wasn't the button or menu item and we're trying to @@ -1191,7 +1183,7 @@ const controls = { // Show a panel in the menu showMenuPanel(type = '', tabFocus = false) { - const target = document.getElementById(`plyr-settings-${this.id}-${type}`); + const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`); // Nothing to show, bail if (!is.element(target)) { @@ -1244,8 +1236,8 @@ const controls = { controls.focusFirstMenuItem.call(this, target, tabFocus); }, - // Set the download link - setDownloadLink() { + // Set the download URL + setDownloadUrl() { const button = this.elements.buttons.download; // Bail if no button @@ -1253,324 +1245,356 @@ const controls = { return; } - // Set download link + // Set attribute button.setAttribute('href', this.download); }, // Build the default HTML - // TODO: Set order based on order in the config.controls array? create(data) { - // Create the container - const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); + const { + bindMenuItemShortcuts, + createButton, + createProgress, + createRange, + createTime, + setQualityMenu, + setSpeedMenu, + showMenuPanel, + } = controls; + this.elements.controls = null; - // Restart button - if (this.config.controls.includes('restart')) { - container.appendChild(controls.createButton.call(this, 'restart')); + // Larger overlaid play button + if (this.config.controls.includes('play-large')) { + this.elements.container.appendChild(createButton.call(this, 'play-large')); } - // Rewind button - if (this.config.controls.includes('rewind')) { - container.appendChild(controls.createButton.call(this, 'rewind')); - } + // Create the container + const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); + this.elements.controls = container; - // Play/Pause button - if (this.config.controls.includes('play')) { - container.appendChild(controls.createButton.call(this, 'play')); - } + // Default item attributes + const defaultAttributes = { class: 'plyr__controls__item' }; - // Fast forward button - if (this.config.controls.includes('fast-forward')) { - container.appendChild(controls.createButton.call(this, 'fast-forward')); - } + // Loop through controls in order + dedupe(this.config.controls).forEach(control => { + // Restart button + if (control === 'restart') { + container.appendChild(createButton.call(this, 'restart', defaultAttributes)); + } - // Progress - if (this.config.controls.includes('progress')) { - const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); + // Rewind button + if (control === 'rewind') { + container.appendChild(createButton.call(this, 'rewind', defaultAttributes)); + } - // Seek range slider - progress.appendChild( - controls.createRange.call(this, 'seek', { - id: `plyr-seek-${data.id}`, - }), - ); + // Play/Pause button + if (control === 'play') { + container.appendChild(createButton.call(this, 'play', defaultAttributes)); + } - // Buffer progress - progress.appendChild(controls.createProgress.call(this, 'buffer')); + // Fast forward button + if (control === 'fast-forward') { + container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes)); + } - // TODO: Add loop display indicator + // Progress + if (control === 'progress') { + const progressContainer = createElement('div', { + class: `${defaultAttributes.class} plyr__progress__container`, + }); - // Seek tooltip - if (this.config.tooltips.seek) { - const tooltip = createElement( - 'span', - { - class: this.config.classNames.tooltip, - }, - '00:00', - ); + const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); - progress.appendChild(tooltip); - this.elements.display.seekTooltip = tooltip; - } + // Seek range slider + progress.appendChild( + createRange.call(this, 'seek', { + id: `plyr-seek-${data.id}`, + }), + ); - this.elements.progress = progress; - container.appendChild(this.elements.progress); - } + // Buffer progress + progress.appendChild(createProgress.call(this, 'buffer')); - // Media current time display - if (this.config.controls.includes('current-time')) { - container.appendChild(controls.createTime.call(this, 'currentTime')); - } + // TODO: Add loop display indicator - // Media duration display - if (this.config.controls.includes('duration')) { - container.appendChild(controls.createTime.call(this, 'duration')); - } + // Seek tooltip + if (this.config.tooltips.seek) { + const tooltip = createElement( + 'span', + { + class: this.config.classNames.tooltip, + }, + '00:00', + ); - // Volume controls - if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) { - const volume = createElement('div', { - class: 'plyr__volume', - }); + progress.appendChild(tooltip); + this.elements.display.seekTooltip = tooltip; + } - // Toggle mute button - if (this.config.controls.includes('mute')) { - volume.appendChild(controls.createButton.call(this, 'mute')); + this.elements.progress = progress; + progressContainer.appendChild(this.elements.progress); + container.appendChild(progressContainer); } - // Volume range control - if (this.config.controls.includes('volume')) { - // Set the attributes - const attributes = { - max: 1, - step: 0.05, - value: this.config.volume, - }; - - // Create the volume range slider - volume.appendChild( - controls.createRange.call( - this, - 'volume', - extend(attributes, { - id: `plyr-volume-${data.id}`, - }), - ), - ); - - this.elements.volume = volume; + // Media current time display + if (control === 'current-time') { + container.appendChild(createTime.call(this, 'currentTime', defaultAttributes)); } - container.appendChild(volume); - } - - // Toggle captions button - if (this.config.controls.includes('captions')) { - container.appendChild(controls.createButton.call(this, 'captions')); - } + // Media duration display + if (control === 'duration') { + container.appendChild(createTime.call(this, 'duration', defaultAttributes)); + } - // Settings button / menu - if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) { - const control = createElement('div', { - class: 'plyr__menu', - hidden: '', - }); + // Volume controls + if (control === 'mute' || control === 'volume') { + let { volume } = this.elements; - control.appendChild( - controls.createButton.call(this, 'settings', { - 'aria-haspopup': true, - 'aria-controls': `plyr-settings-${data.id}`, - 'aria-expanded': false, - }), - ); + // Create the volume container if needed + if (!is.element(volume) || !container.contains(volume)) { + volume = createElement( + 'div', + extend({}, defaultAttributes, { + class: `${defaultAttributes.class} plyr__volume`.trim(), + }), + ); - const popup = createElement('div', { - class: 'plyr__menu__container', - id: `plyr-settings-${data.id}`, - hidden: '', - }); + this.elements.volume = volume; - const inner = createElement('div'); + container.appendChild(volume); + } - const home = createElement('div', { - id: `plyr-settings-${data.id}-home`, - }); + // Toggle mute button + if (control === 'mute') { + volume.appendChild(createButton.call(this, 'mute')); + } - // Create the menu - const menu = createElement('div', { - role: 'menu', - }); + // Volume range control + if (control === 'volume') { + // Set the attributes + const attributes = { + max: 1, + step: 0.05, + value: this.config.volume, + }; + + // Create the volume range slider + volume.appendChild( + createRange.call( + this, + 'volume', + extend(attributes, { + id: `plyr-volume-${data.id}`, + }), + ), + ); + } + } - home.appendChild(menu); - inner.appendChild(home); - this.elements.settings.panels.home = home; + // Toggle captions button + if (control === 'captions') { + container.appendChild(createButton.call(this, 'captions', defaultAttributes)); + } - // Build the menu items - this.config.settings.forEach(type => { - // TODO: bundle this with the createMenuItem helper and bindings - const menuItem = createElement( - 'button', - extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { - type: 'button', - class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, - role: 'menuitem', - 'aria-haspopup': true, + // Settings button / menu + if (control === 'settings' && !is.empty(this.config.settings)) { + const control = createElement( + 'div', + extend({}, defaultAttributes, { + class: `${defaultAttributes.class} plyr__menu`.trim(), hidden: '', }), ); - // Bind menu shortcuts for keyboard users - controls.bindMenuItemShortcuts.call(this, menuItem, type); + control.appendChild( + createButton.call(this, 'settings', { + 'aria-haspopup': true, + 'aria-controls': `plyr-settings-${data.id}`, + 'aria-expanded': false, + }), + ); - // Show menu on click - on(menuItem, 'click', () => { - controls.showMenuPanel.call(this, type, false); + const popup = createElement('div', { + class: 'plyr__menu__container', + id: `plyr-settings-${data.id}`, + hidden: '', }); - const flex = createElement('span', null, i18n.get(type, this.config)); + const inner = createElement('div'); - const value = createElement('span', { - class: this.config.classNames.menu.value, + const home = createElement('div', { + id: `plyr-settings-${data.id}-home`, }); - // Speed contains HTML entities - value.innerHTML = data[type]; + // Create the menu + const menu = createElement('div', { + role: 'menu', + }); - flex.appendChild(value); - menuItem.appendChild(flex); - menu.appendChild(menuItem); + home.appendChild(menu); + inner.appendChild(home); + this.elements.settings.panels.home = home; + + // Build the menu items + this.config.settings.forEach(type => { + // TODO: bundle this with the createMenuItem helper and bindings + const menuItem = createElement( + 'button', + extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { + type: 'button', + class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, + role: 'menuitem', + 'aria-haspopup': true, + hidden: '', + }), + ); - // Build the panes - const pane = createElement('div', { - id: `plyr-settings-${data.id}-${type}`, - hidden: '', - }); + // Bind menu shortcuts for keyboard users + bindMenuItemShortcuts.call(this, menuItem, type); - // Back button - const backButton = createElement('button', { - type: 'button', - class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, - }); + // Show menu on click + on(menuItem, 'click', () => { + showMenuPanel.call(this, type, false); + }); - // Visible label - backButton.appendChild( - createElement( - 'span', - { - 'aria-hidden': true, - }, - i18n.get(type, this.config), - ), - ); + const flex = createElement('span', null, i18n.get(type, this.config)); - // Screen reader label - backButton.appendChild( - createElement( - 'span', - { - class: this.config.classNames.hidden, - }, - i18n.get('menuBack', this.config), - ), - ); + const value = createElement('span', { + class: this.config.classNames.menu.value, + }); - // Go back via keyboard - on( - pane, - 'keydown', - event => { - // We only care about <- - if (event.which !== 37) { - return; - } + // Speed contains HTML entities + value.innerHTML = data[type]; - // Prevent seek - event.preventDefault(); - event.stopPropagation(); + flex.appendChild(value); + menuItem.appendChild(flex); + menu.appendChild(menuItem); - // Show the respective menu - controls.showMenuPanel.call(this, 'home', true); - }, - false, - ); + // Build the panes + const pane = createElement('div', { + id: `plyr-settings-${data.id}-${type}`, + hidden: '', + }); - // Go back via button click - on(backButton, 'click', () => { - controls.showMenuPanel.call(this, 'home', false); - }); + // Back button + const backButton = createElement('button', { + type: 'button', + class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, + }); + + // Visible label + backButton.appendChild( + createElement( + 'span', + { + 'aria-hidden': true, + }, + i18n.get(type, this.config), + ), + ); + + // Screen reader label + backButton.appendChild( + createElement( + 'span', + { + class: this.config.classNames.hidden, + }, + i18n.get('menuBack', this.config), + ), + ); + + // Go back via keyboard + on( + pane, + 'keydown', + event => { + // We only care about <- + if (event.which !== 37) { + return; + } - // Add to pane - pane.appendChild(backButton); + // Prevent seek + event.preventDefault(); + event.stopPropagation(); - // Menu - pane.appendChild( - createElement('div', { - role: 'menu', - }), - ); + // Show the respective menu + showMenuPanel.call(this, 'home', true); + }, + false, + ); - inner.appendChild(pane); + // Go back via button click + on(backButton, 'click', () => { + showMenuPanel.call(this, 'home', false); + }); - this.elements.settings.buttons[type] = menuItem; - this.elements.settings.panels[type] = pane; - }); + // Add to pane + pane.appendChild(backButton); - popup.appendChild(inner); - control.appendChild(popup); - container.appendChild(control); + // Menu + pane.appendChild( + createElement('div', { + role: 'menu', + }), + ); - this.elements.settings.popup = popup; - this.elements.settings.menu = control; - } + inner.appendChild(pane); - // Picture in picture button - if (this.config.controls.includes('pip') && support.pip) { - container.appendChild(controls.createButton.call(this, 'pip')); - } + this.elements.settings.buttons[type] = menuItem; + this.elements.settings.panels[type] = pane; + }); - // Airplay button - if (this.config.controls.includes('airplay') && support.airplay) { - container.appendChild(controls.createButton.call(this, 'airplay')); - } + popup.appendChild(inner); + control.appendChild(popup); + container.appendChild(control); - // Download button - if (this.config.controls.includes('download')) { - const attributes = { - element: 'a', - href: this.download, - target: '_blank', - }; + this.elements.settings.popup = popup; + this.elements.settings.menu = control; + } - const { download } = this.config.urls; + // Picture in picture button + if (control === 'pip' && support.pip) { + container.appendChild(createButton.call(this, 'pip', defaultAttributes)); + } - if (!is.url(download) && this.isEmbed) { - extend(attributes, { - icon: `logo-${this.provider}`, - label: this.provider, - }); + // Airplay button + if (control === 'airplay' && support.airplay) { + container.appendChild(createButton.call(this, 'airplay', defaultAttributes)); } - container.appendChild(controls.createButton.call(this, 'download', attributes)); - } + // Download button + if (control === 'download') { + const attributes = extend({}, defaultAttributes, { + element: 'a', + href: this.download, + target: '_blank', + }); - // Toggle fullscreen button - if (this.config.controls.includes('fullscreen')) { - container.appendChild(controls.createButton.call(this, 'fullscreen')); - } + const { download } = this.config.urls; - // Larger overlaid play button - if (this.config.controls.includes('play-large')) { - this.elements.container.appendChild(controls.createButton.call(this, 'play-large')); - } + if (!is.url(download) && this.isEmbed) { + extend(attributes, { + icon: `logo-${this.provider}`, + label: this.provider, + }); + } - this.elements.controls = container; + container.appendChild(createButton.call(this, 'download', attributes)); + } + + // Toggle fullscreen button + if (control === 'fullscreen') { + container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes)); + } + }); // Set available quality levels if (this.isHTML5) { - controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); + setQualityMenu.call(this, html5.getQualityOptions.call(this)); } - controls.setSpeedMenu.call(this); + setSpeedMenu.call(this); return container; }, |