diff options
Diffstat (limited to 'src/js/controls.js')
-rw-r--r-- | src/js/controls.js | 574 |
1 files changed, 304 insertions, 270 deletions
diff --git a/src/js/controls.js b/src/js/controls.js index 73903e16..43a92140 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -4,6 +4,7 @@ // ========================================================================== import RangeTouch from 'rangetouch'; + import captions from './captions'; import html5 from './html5'; import support from './support'; @@ -105,7 +106,6 @@ const controls = { const namespace = 'http://www.w3.org/2000/svg'; const iconUrl = controls.getIconUrl.call(this); const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; - // Create <svg> const icon = document.createElementNS(namespace, 'svg'); setAttributes( @@ -172,7 +172,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 +198,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 +379,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', @@ -491,15 +493,15 @@ const controls = { get() { return menuItem.getAttribute('aria-checked') === 'true'; }, - set(checked) { + set(check) { // Ensure exclusivity - if (checked) { + if (check) { Array.from(menuItem.parentNode.children) .filter(node => matches(node, '[role="menuitemradio"]')) .forEach(node => node.setAttribute('aria-checked', 'false')); } - menuItem.setAttribute('aria-checked', checked ? 'true' : 'false'); + menuItem.setAttribute('aria-checked', check ? 'true' : 'false'); }, }); @@ -607,17 +609,17 @@ const controls = { let value = 0; const setProgress = (target, input) => { - const value = is.number(input) ? input : 0; + const val = is.number(input) ? input : 0; const progress = is.element(target) ? target : this.elements.display.buffer; // Update value and label if (is.element(progress)) { - progress.value = value; + progress.value = val; // Update text label inside const label = progress.getElementsByTagName('span')[0]; if (is.element(label)) { - label.childNodes[0].nodeValue = value; + label.childNodes[0].nodeValue = val; } } }; @@ -699,14 +701,8 @@ const controls = { return; } - // Calculate percentage - let percent = 0; - const clientRect = this.elements.progress.getBoundingClientRect(); const visible = `${this.config.classNames.tooltip}--visible`; - - const toggle = toggle => { - toggleClass(this.elements.display.seekTooltip, visible, toggle); - }; + const toggle = show => toggleClass(this.elements.display.seekTooltip, visible, show); // Hide on touch if (this.touch) { @@ -715,6 +711,9 @@ const controls = { } // Determine percentage, if already visible + let percent = 0; + const clientRect = this.elements.progress.getBoundingClientRect(); + if (is.event(event)) { percent = (100 / clientRect.width) * (event.pageX - clientRect.left); } else if (hasClass(this.elements.display.seekTooltip, visible)) { @@ -1111,7 +1110,7 @@ const controls = { let target = pane; if (!is.element(target)) { - target = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); + target = Object.values(this.elements.settings.panels).find(p => !p.hidden); } const firstItem = target.querySelector('[role^="menuitem"]'); @@ -1138,7 +1137,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 +1193,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 +1246,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 +1255,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)); + } + + // Fast forward button + if (control === 'fast-forward') { + container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes)); + } - // Buffer progress - progress.appendChild(controls.createProgress.call(this, 'buffer')); + // Progress + if (control === 'progress') { + const progressContainer = createElement('div', { + class: `${defaultAttributes.class} plyr__progress__container`, + }); - // TODO: Add loop display indicator + const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); - // Seek tooltip - if (this.config.tooltips.seek) { - const tooltip = createElement( - 'span', - { - class: this.config.classNames.tooltip, - }, - '00:00', + // Seek range slider + progress.appendChild( + createRange.call(this, 'seek', { + id: `plyr-seek-${data.id}`, + }), ); - progress.appendChild(tooltip); - this.elements.display.seekTooltip = tooltip; - } + // Buffer progress + progress.appendChild(createProgress.call(this, 'buffer')); - this.elements.progress = progress; - container.appendChild(this.elements.progress); - } + // TODO: Add loop display indicator - // Media current time display - if (this.config.controls.includes('current-time')) { - container.appendChild(controls.createTime.call(this, 'currentTime')); - } - - // 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 wrapper = createElement( + 'div', + extend({}, defaultAttributes, { + class: `${defaultAttributes.class} plyr__menu`.trim(), hidden: '', }), ); - // Bind menu shortcuts for keyboard users - controls.bindMenuItemShortcuts.call(this, menuItem, type); + wrapper.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); + wrapper.appendChild(popup); + container.appendChild(wrapper); - // 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 = wrapper; + } - 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; }, |