diff options
-rw-r--r-- | package.json | 6 | ||||
-rw-r--r-- | src/js/config/defaults.js | 5 | ||||
-rw-r--r-- | src/js/config/types.js | 2 | ||||
-rw-r--r-- | src/js/controls.js | 139 | ||||
-rw-r--r-- | src/js/listeners.js | 19 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 3 | ||||
-rw-r--r-- | src/sprite/plyr-download.svg | 6 | ||||
-rw-r--r-- | src/sprite/plyr-logo-vimeo.svg | 4 | ||||
-rw-r--r-- | src/sprite/plyr-logo-youtube.svg | 4 |
9 files changed, 133 insertions, 55 deletions
diff --git a/package.json b/package.json index 0c65d92b..fa29602b 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "babel-core": "^6.26.3", - "babel-eslint": "^9.0.0", + "babel-eslint": "^10.0.0", "@babel/preset-env": "^7.1.0", "del": "^3.0.0", "eslint": "^5.6.0", @@ -64,7 +64,7 @@ "gulp-svgstore": "^7.0.0", "gulp-uglify-es": "^1.0.4", "gulp-util": "^3.0.8", - "postcss-custom-properties": "^8.0.5", + "postcss-custom-properties": "^8.0.6", "prettier-eslint": "^8.8.2", "prettier-stylelint": "^0.4.2", "remark-cli": "^5.0.0", @@ -73,7 +73,7 @@ "rollup-plugin-commonjs": "^9.1.8", "rollup-plugin-node-resolve": "^3.4.0", "run-sequence": "^2.2.1", - "stylelint": "^9.5.0", + "stylelint": "^9.6.0", "stylelint-config-prettier": "^4.0.0", "stylelint-config-recommended": "^2.1.0", "stylelint-config-sass-guidelines": "^5.2.0", diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index e6e2d7c1..5e2fc4a9 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -133,6 +133,7 @@ const defaults = { 'settings', 'pip', 'airplay', + 'download', 'fullscreen', ], settings: ['captions', 'quality', 'speed'], @@ -155,6 +156,7 @@ const defaults = { unmute: 'Unmute', enableCaptions: 'Enable captions', disableCaptions: 'Disable captions', + download: 'Download', enterFullscreen: 'Enter fullscreen', exitFullscreen: 'Exit fullscreen', frameTitle: 'Player for {title}', @@ -210,6 +212,7 @@ const defaults = { mute: null, volume: null, captions: null, + download: null, fullscreen: null, pip: null, airplay: null, @@ -245,6 +248,7 @@ const defaults = { 'cuechange', // Custom events + 'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', @@ -290,6 +294,7 @@ const defaults = { fastForward: '[data-plyr="fast-forward"]', mute: '[data-plyr="mute"]', captions: '[data-plyr="captions"]', + download: '[data-plyr="download"]', fullscreen: '[data-plyr="fullscreen"]', pip: '[data-plyr="pip"]', airplay: '[data-plyr="airplay"]', diff --git a/src/js/config/types.js b/src/js/config/types.js index 13303573..c9d50937 100644 --- a/src/js/config/types.js +++ b/src/js/config/types.js @@ -15,7 +15,7 @@ export const types = { /** * Get provider by URL - * @param {string} url + * @param {String} url */ export function getProviderByUrl(url) { // YouTube diff --git a/src/js/controls.js b/src/js/controls.js index 661ceb32..785f100d 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -122,17 +122,13 @@ const controls = { }, // Create hidden text label - createLabel(type, attr = {}) { - // Skip i18n for abbreviations and brand names - const universals = { - pip: 'PIP', - airplay: 'AirPlay', - }; - const text = universals[type] || i18n.get(type, this.config); + createLabel(key, attr = {}) { + const text = i18n.get(key, this.config); const attributes = Object.assign({}, attr, { class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '), }); + return createElement('span', attributes, text); }, @@ -161,21 +157,32 @@ const controls = { // Create a <button> createButton(buttonType, attr) { - const button = createElement('button'); const attributes = Object.assign({}, attr); let type = toCamelCase(buttonType); - let toggle = false; - let label; - let icon; - let labelPressed; - let iconPressed; + const props = { + element: 'button', + toggle: false, + label: null, + icon: null, + labelPressed: null, + iconPressed: null, + }; - if (!('type' in attributes)) { + ['element', 'icon', 'label'].forEach(key => { + if (Object.keys(attributes).includes(key)) { + props[key] = attributes[key]; + delete attributes[key]; + } + }); + + // Default to 'button' type to prevent form submission + if (props.element === 'button' && !Object.keys(attributes).includes('type')) { attributes.type = 'button'; } - if ('class' in attributes) { + // Set class name + if (Object.keys(attributes).includes('class')) { if (!attributes.class.includes(this.config.classNames.control)) { attributes.class += ` ${this.config.classNames.control}`; } @@ -186,82 +193,87 @@ const controls = { // Large play button switch (buttonType) { case 'play': - toggle = true; - label = 'play'; - labelPressed = 'pause'; - icon = 'play'; - iconPressed = 'pause'; + props.toggle = true; + props.label = 'play'; + props.labelPressed = 'pause'; + props.icon = 'play'; + props.iconPressed = 'pause'; break; case 'mute': - toggle = true; - label = 'mute'; - labelPressed = 'unmute'; - icon = 'volume'; - iconPressed = 'muted'; + props.toggle = true; + props.label = 'mute'; + props.labelPressed = 'unmute'; + props.icon = 'volume'; + props.iconPressed = 'muted'; break; case 'captions': - toggle = true; - label = 'enableCaptions'; - labelPressed = 'disableCaptions'; - icon = 'captions-off'; - iconPressed = 'captions-on'; + props.toggle = true; + props.label = 'enableCaptions'; + props.labelPressed = 'disableCaptions'; + props.icon = 'captions-off'; + props.iconPressed = 'captions-on'; break; case 'fullscreen': - toggle = true; - label = 'enterFullscreen'; - labelPressed = 'exitFullscreen'; - icon = 'enter-fullscreen'; - iconPressed = 'exit-fullscreen'; + props.toggle = true; + props.label = 'enterFullscreen'; + props.labelPressed = 'exitFullscreen'; + props.icon = 'enter-fullscreen'; + props.iconPressed = 'exit-fullscreen'; break; case 'play-large': attributes.class += ` ${this.config.classNames.control}--overlaid`; type = 'play'; - label = 'play'; - icon = 'play'; + props.label = 'play'; + props.icon = 'play'; break; default: - label = type; - icon = buttonType; + if (is.empty(props.label)) { + props.label = type; + } + if (is.empty(props.icon)) { + props.icon = buttonType; + } } + const button = createElement(props.element); + // Setup toggle icon and labels - if (toggle) { + if (props.toggle) { // Icon button.appendChild( - controls.createIcon.call(this, iconPressed, { + controls.createIcon.call(this, props.iconPressed, { class: 'icon--pressed', }), ); button.appendChild( - controls.createIcon.call(this, icon, { + controls.createIcon.call(this, props.icon, { class: 'icon--not-pressed', }), ); // Label/Tooltip button.appendChild( - controls.createLabel.call(this, labelPressed, { + controls.createLabel.call(this, props.labelPressed, { class: 'label--pressed', }), ); button.appendChild( - controls.createLabel.call(this, label, { + controls.createLabel.call(this, props.label, { class: 'label--not-pressed', }), ); } else { - button.appendChild(controls.createIcon.call(this, icon)); - button.appendChild(controls.createLabel.call(this, label)); + button.appendChild(controls.createIcon.call(this, props.icon)); + button.appendChild(controls.createLabel.call(this, props.label)); } - // Merge attributes + // Merge and set attributes extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); - setAttributes(button, attributes); // We have multiple play buttons @@ -1214,6 +1226,15 @@ const controls = { controls.focusFirstMenuItem.call(this, target, tabFocus); }, + // Set the download link + setDownloadLink() { + // Set download link + const { download } = this.elements.buttons; + if (is.element(download)) { + download.setAttribute('href', this.source); + } + }, + // Build the default HTML // TODO: Set order based on order in the config.controls array? create(data) { @@ -1490,6 +1511,28 @@ const controls = { container.appendChild(controls.createButton.call(this, 'airplay')); } + // Download button + if (this.config.controls.includes('download')) { + const attributes = { + element: 'a', + href: this.source, + target: '_blank', + }; + + if (this.isHTML5) { + extend(attributes, { + download: '', + }); + } else if (this.isEmbed) { + extend(attributes, { + icon: `logo-${this.provider}`, + label: this.provider, + }); + } + + container.appendChild(controls.createButton.call(this, 'download', attributes)); + } + // Toggle fullscreen button if (this.config.controls.includes('fullscreen')) { container.appendChild(controls.createButton.call(this, 'fullscreen')); diff --git a/src/js/listeners.js b/src/js/listeners.js index f938d9a2..31d74af6 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -431,6 +431,11 @@ class Listeners { controls.updateSetting.call(player, 'quality', null, event.detail.quality); }); + // Update download link + on.call(player, player.media, 'ready qualitychange', () => { + controls.setDownloadLink.call(player); + }); + // Proxy events to container // Bubble up key events for Edge const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' '); @@ -517,6 +522,16 @@ class Listeners { // Captions toggle this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions()); + // Download + this.bind( + elements.buttons.download, + 'click', + () => { + triggerEvent.call(player, player.media, 'download'); + }, + 'download', + ); + // Fullscreen toggle this.bind( elements.buttons.fullscreen, @@ -698,7 +713,7 @@ class Listeners { }); // Show controls when they receive focus (e.g., when using keyboard tab key) - this.bind(elements.controls, 'focusin', event => { + this.bind(elements.controls, 'focusin', () => { const { config, elements, timers } = player; // Skip transition to prevent focus from scrolling the parent element @@ -712,7 +727,7 @@ class Listeners { toggleClass(elements.controls, config.classNames.noTransition, false); }, 0); - // Delay a little more for keyboard users + // Delay a little more for mouse users const delay = this.touch ? 3000 : 4000; // Clear timer diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 4cbdb5c4..3c3dee20 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -71,7 +71,7 @@ const vimeo = { // For Vimeo we have an extra 300% height <div> to hide the standard controls and UI setAspectRatio(input) { const [x, y] = (is.string(input) ? input : this.config.ratio).split(':'); - const padding = 100 / x * y; + const padding = (100 / x) * y; this.elements.wrapper.style.paddingBottom = `${padding}%`; if (this.supported.ui) { @@ -278,6 +278,7 @@ const vimeo = { .getVideoUrl() .then(value => { currentSrc = value; + controls.setDownloadLink.call(player); }) .catch(error => { this.debug.warn(error); diff --git a/src/sprite/plyr-download.svg b/src/sprite/plyr-download.svg new file mode 100644 index 00000000..1d971a40 --- /dev/null +++ b/src/sprite/plyr-download.svg @@ -0,0 +1,6 @@ +<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"> + <g transform="translate(2 1)"> + <path d="M7,12 C7.3,12 7.5,11.9 7.7,11.7 L13.4,6 L12,4.6 L8,8.6 L8,0 L6,0 L6,8.6 L2,4.6 L0.6,6 L6.3,11.7 C6.5,11.9 6.7,12 7,12 Z" /> + <rect width="14" height="2" y="14" /> + </g> +</svg>
\ No newline at end of file diff --git a/src/sprite/plyr-logo-vimeo.svg b/src/sprite/plyr-logo-vimeo.svg new file mode 100644 index 00000000..de2f9ee6 --- /dev/null +++ b/src/sprite/plyr-logo-vimeo.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"> + <path d="M16,3.3 C15.9,4.9 14.8,7 12.7,9.7 C10.5,12.5 8.7,13.9 7.2,13.9 C6.3,13.9 5.5,13 4.8,11.3 C4,8.9 3.4,4 2,4 C1.9,4 1.5,4.3 0.8,4.8 L0,3.8 C0.8,3.1 3.5,0.4 4.7,0.3 C5.9,0.2 6.7,1 7,2.8 C7.3,4.8 7.8,8.9 8.8,8.9 C9.7,8.9 11.3,5.5 11.4,4.9 C11.5,4 11.1,3 9.1,3.8 C9.9,1.2 11.4,-8.8817842e-16 13.6,-8.8817842e-16 C15.3,0.1 16.1,1.2 16,3.3 Z" + transform="translate(1 2)" /> +</svg>
\ No newline at end of file diff --git a/src/sprite/plyr-logo-youtube.svg b/src/sprite/plyr-logo-youtube.svg new file mode 100644 index 00000000..3bec1531 --- /dev/null +++ b/src/sprite/plyr-logo-youtube.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"> + <path d="M15.8,2.8 C15.6,1.5 15,0.6 13.6,0.4 C11.4,0 8,0 8,0 C8,0 4.6,0 2.4,0.4 C1,0.6 0.3,1.5 0.2,2.8 C0,4.1 0,6 0,6 C0,6 0,7.9 0.2,9.2 C0.4,10.5 1,11.4 2.4,11.6 C4.6,12 8,12 8,12 C8,12 11.4,12 13.6,11.6 C15,11.3 15.6,10.5 15.8,9.2 C16,7.9 16,6 16,6 C16,6 16,4.1 15.8,2.8 Z M6,9 L6,3 L11,6 L6,9 Z" + transform="translate(1 3)" /> +</svg>
\ No newline at end of file |