Added download button
This commit is contained in:
parent
515ae32160
commit
fac134dd95
@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
"babel-eslint": "^9.0.0",
|
"babel-eslint": "^10.0.0",
|
||||||
"@babel/preset-env": "^7.1.0",
|
"@babel/preset-env": "^7.1.0",
|
||||||
"del": "^3.0.0",
|
"del": "^3.0.0",
|
||||||
"eslint": "^5.6.0",
|
"eslint": "^5.6.0",
|
||||||
@ -64,7 +64,7 @@
|
|||||||
"gulp-svgstore": "^7.0.0",
|
"gulp-svgstore": "^7.0.0",
|
||||||
"gulp-uglify-es": "^1.0.4",
|
"gulp-uglify-es": "^1.0.4",
|
||||||
"gulp-util": "^3.0.8",
|
"gulp-util": "^3.0.8",
|
||||||
"postcss-custom-properties": "^8.0.5",
|
"postcss-custom-properties": "^8.0.6",
|
||||||
"prettier-eslint": "^8.8.2",
|
"prettier-eslint": "^8.8.2",
|
||||||
"prettier-stylelint": "^0.4.2",
|
"prettier-stylelint": "^0.4.2",
|
||||||
"remark-cli": "^5.0.0",
|
"remark-cli": "^5.0.0",
|
||||||
@ -73,7 +73,7 @@
|
|||||||
"rollup-plugin-commonjs": "^9.1.8",
|
"rollup-plugin-commonjs": "^9.1.8",
|
||||||
"rollup-plugin-node-resolve": "^3.4.0",
|
"rollup-plugin-node-resolve": "^3.4.0",
|
||||||
"run-sequence": "^2.2.1",
|
"run-sequence": "^2.2.1",
|
||||||
"stylelint": "^9.5.0",
|
"stylelint": "^9.6.0",
|
||||||
"stylelint-config-prettier": "^4.0.0",
|
"stylelint-config-prettier": "^4.0.0",
|
||||||
"stylelint-config-recommended": "^2.1.0",
|
"stylelint-config-recommended": "^2.1.0",
|
||||||
"stylelint-config-sass-guidelines": "^5.2.0",
|
"stylelint-config-sass-guidelines": "^5.2.0",
|
||||||
|
@ -133,6 +133,7 @@ const defaults = {
|
|||||||
'settings',
|
'settings',
|
||||||
'pip',
|
'pip',
|
||||||
'airplay',
|
'airplay',
|
||||||
|
'download',
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
],
|
],
|
||||||
settings: ['captions', 'quality', 'speed'],
|
settings: ['captions', 'quality', 'speed'],
|
||||||
@ -155,6 +156,7 @@ const defaults = {
|
|||||||
unmute: 'Unmute',
|
unmute: 'Unmute',
|
||||||
enableCaptions: 'Enable captions',
|
enableCaptions: 'Enable captions',
|
||||||
disableCaptions: 'Disable captions',
|
disableCaptions: 'Disable captions',
|
||||||
|
download: 'Download',
|
||||||
enterFullscreen: 'Enter fullscreen',
|
enterFullscreen: 'Enter fullscreen',
|
||||||
exitFullscreen: 'Exit fullscreen',
|
exitFullscreen: 'Exit fullscreen',
|
||||||
frameTitle: 'Player for {title}',
|
frameTitle: 'Player for {title}',
|
||||||
@ -210,6 +212,7 @@ const defaults = {
|
|||||||
mute: null,
|
mute: null,
|
||||||
volume: null,
|
volume: null,
|
||||||
captions: null,
|
captions: null,
|
||||||
|
download: null,
|
||||||
fullscreen: null,
|
fullscreen: null,
|
||||||
pip: null,
|
pip: null,
|
||||||
airplay: null,
|
airplay: null,
|
||||||
@ -245,6 +248,7 @@ const defaults = {
|
|||||||
'cuechange',
|
'cuechange',
|
||||||
|
|
||||||
// Custom events
|
// Custom events
|
||||||
|
'download',
|
||||||
'enterfullscreen',
|
'enterfullscreen',
|
||||||
'exitfullscreen',
|
'exitfullscreen',
|
||||||
'captionsenabled',
|
'captionsenabled',
|
||||||
@ -290,6 +294,7 @@ const defaults = {
|
|||||||
fastForward: '[data-plyr="fast-forward"]',
|
fastForward: '[data-plyr="fast-forward"]',
|
||||||
mute: '[data-plyr="mute"]',
|
mute: '[data-plyr="mute"]',
|
||||||
captions: '[data-plyr="captions"]',
|
captions: '[data-plyr="captions"]',
|
||||||
|
download: '[data-plyr="download"]',
|
||||||
fullscreen: '[data-plyr="fullscreen"]',
|
fullscreen: '[data-plyr="fullscreen"]',
|
||||||
pip: '[data-plyr="pip"]',
|
pip: '[data-plyr="pip"]',
|
||||||
airplay: '[data-plyr="airplay"]',
|
airplay: '[data-plyr="airplay"]',
|
||||||
|
@ -15,7 +15,7 @@ export const types = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get provider by URL
|
* Get provider by URL
|
||||||
* @param {string} url
|
* @param {String} url
|
||||||
*/
|
*/
|
||||||
export function getProviderByUrl(url) {
|
export function getProviderByUrl(url) {
|
||||||
// YouTube
|
// YouTube
|
||||||
|
139
src/js/controls.js
vendored
139
src/js/controls.js
vendored
@ -122,17 +122,13 @@ const controls = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Create hidden text label
|
// Create hidden text label
|
||||||
createLabel(type, attr = {}) {
|
createLabel(key, attr = {}) {
|
||||||
// Skip i18n for abbreviations and brand names
|
const text = i18n.get(key, this.config);
|
||||||
const universals = {
|
|
||||||
pip: 'PIP',
|
|
||||||
airplay: 'AirPlay',
|
|
||||||
};
|
|
||||||
const text = universals[type] || i18n.get(type, this.config);
|
|
||||||
|
|
||||||
const attributes = Object.assign({}, attr, {
|
const attributes = Object.assign({}, attr, {
|
||||||
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '),
|
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '),
|
||||||
});
|
});
|
||||||
|
|
||||||
return createElement('span', attributes, text);
|
return createElement('span', attributes, text);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -161,21 +157,32 @@ const controls = {
|
|||||||
|
|
||||||
// Create a <button>
|
// Create a <button>
|
||||||
createButton(buttonType, attr) {
|
createButton(buttonType, attr) {
|
||||||
const button = createElement('button');
|
|
||||||
const attributes = Object.assign({}, attr);
|
const attributes = Object.assign({}, attr);
|
||||||
let type = toCamelCase(buttonType);
|
let type = toCamelCase(buttonType);
|
||||||
|
|
||||||
let toggle = false;
|
const props = {
|
||||||
let label;
|
element: 'button',
|
||||||
let icon;
|
toggle: false,
|
||||||
let labelPressed;
|
label: null,
|
||||||
let iconPressed;
|
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';
|
attributes.type = 'button';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('class' in attributes) {
|
// Set class name
|
||||||
|
if (Object.keys(attributes).includes('class')) {
|
||||||
if (!attributes.class.includes(this.config.classNames.control)) {
|
if (!attributes.class.includes(this.config.classNames.control)) {
|
||||||
attributes.class += ` ${this.config.classNames.control}`;
|
attributes.class += ` ${this.config.classNames.control}`;
|
||||||
}
|
}
|
||||||
@ -186,82 +193,87 @@ const controls = {
|
|||||||
// Large play button
|
// Large play button
|
||||||
switch (buttonType) {
|
switch (buttonType) {
|
||||||
case 'play':
|
case 'play':
|
||||||
toggle = true;
|
props.toggle = true;
|
||||||
label = 'play';
|
props.label = 'play';
|
||||||
labelPressed = 'pause';
|
props.labelPressed = 'pause';
|
||||||
icon = 'play';
|
props.icon = 'play';
|
||||||
iconPressed = 'pause';
|
props.iconPressed = 'pause';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mute':
|
case 'mute':
|
||||||
toggle = true;
|
props.toggle = true;
|
||||||
label = 'mute';
|
props.label = 'mute';
|
||||||
labelPressed = 'unmute';
|
props.labelPressed = 'unmute';
|
||||||
icon = 'volume';
|
props.icon = 'volume';
|
||||||
iconPressed = 'muted';
|
props.iconPressed = 'muted';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'captions':
|
case 'captions':
|
||||||
toggle = true;
|
props.toggle = true;
|
||||||
label = 'enableCaptions';
|
props.label = 'enableCaptions';
|
||||||
labelPressed = 'disableCaptions';
|
props.labelPressed = 'disableCaptions';
|
||||||
icon = 'captions-off';
|
props.icon = 'captions-off';
|
||||||
iconPressed = 'captions-on';
|
props.iconPressed = 'captions-on';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fullscreen':
|
case 'fullscreen':
|
||||||
toggle = true;
|
props.toggle = true;
|
||||||
label = 'enterFullscreen';
|
props.label = 'enterFullscreen';
|
||||||
labelPressed = 'exitFullscreen';
|
props.labelPressed = 'exitFullscreen';
|
||||||
icon = 'enter-fullscreen';
|
props.icon = 'enter-fullscreen';
|
||||||
iconPressed = 'exit-fullscreen';
|
props.iconPressed = 'exit-fullscreen';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'play-large':
|
case 'play-large':
|
||||||
attributes.class += ` ${this.config.classNames.control}--overlaid`;
|
attributes.class += ` ${this.config.classNames.control}--overlaid`;
|
||||||
type = 'play';
|
type = 'play';
|
||||||
label = 'play';
|
props.label = 'play';
|
||||||
icon = 'play';
|
props.icon = 'play';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
label = type;
|
if (is.empty(props.label)) {
|
||||||
icon = buttonType;
|
props.label = type;
|
||||||
|
}
|
||||||
|
if (is.empty(props.icon)) {
|
||||||
|
props.icon = buttonType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const button = createElement(props.element);
|
||||||
|
|
||||||
// Setup toggle icon and labels
|
// Setup toggle icon and labels
|
||||||
if (toggle) {
|
if (props.toggle) {
|
||||||
// Icon
|
// Icon
|
||||||
button.appendChild(
|
button.appendChild(
|
||||||
controls.createIcon.call(this, iconPressed, {
|
controls.createIcon.call(this, props.iconPressed, {
|
||||||
class: 'icon--pressed',
|
class: 'icon--pressed',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
button.appendChild(
|
button.appendChild(
|
||||||
controls.createIcon.call(this, icon, {
|
controls.createIcon.call(this, props.icon, {
|
||||||
class: 'icon--not-pressed',
|
class: 'icon--not-pressed',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Label/Tooltip
|
// Label/Tooltip
|
||||||
button.appendChild(
|
button.appendChild(
|
||||||
controls.createLabel.call(this, labelPressed, {
|
controls.createLabel.call(this, props.labelPressed, {
|
||||||
class: 'label--pressed',
|
class: 'label--pressed',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
button.appendChild(
|
button.appendChild(
|
||||||
controls.createLabel.call(this, label, {
|
controls.createLabel.call(this, props.label, {
|
||||||
class: 'label--not-pressed',
|
class: 'label--not-pressed',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
button.appendChild(controls.createIcon.call(this, icon));
|
button.appendChild(controls.createIcon.call(this, props.icon));
|
||||||
button.appendChild(controls.createLabel.call(this, label));
|
button.appendChild(controls.createLabel.call(this, props.label));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge attributes
|
// Merge and set attributes
|
||||||
extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
|
extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
|
||||||
|
|
||||||
setAttributes(button, attributes);
|
setAttributes(button, attributes);
|
||||||
|
|
||||||
// We have multiple play buttons
|
// We have multiple play buttons
|
||||||
@ -1214,6 +1226,15 @@ const controls = {
|
|||||||
controls.focusFirstMenuItem.call(this, target, tabFocus);
|
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
|
// Build the default HTML
|
||||||
// TODO: Set order based on order in the config.controls array?
|
// TODO: Set order based on order in the config.controls array?
|
||||||
create(data) {
|
create(data) {
|
||||||
@ -1490,6 +1511,28 @@ const controls = {
|
|||||||
container.appendChild(controls.createButton.call(this, 'airplay'));
|
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
|
// Toggle fullscreen button
|
||||||
if (this.config.controls.includes('fullscreen')) {
|
if (this.config.controls.includes('fullscreen')) {
|
||||||
container.appendChild(controls.createButton.call(this, 'fullscreen'));
|
container.appendChild(controls.createButton.call(this, 'fullscreen'));
|
||||||
|
@ -431,6 +431,11 @@ class Listeners {
|
|||||||
controls.updateSetting.call(player, 'quality', null, event.detail.quality);
|
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
|
// Proxy events to container
|
||||||
// Bubble up key events for Edge
|
// Bubble up key events for Edge
|
||||||
const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
|
const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
|
||||||
@ -517,6 +522,16 @@ class Listeners {
|
|||||||
// Captions toggle
|
// Captions toggle
|
||||||
this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());
|
this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());
|
||||||
|
|
||||||
|
// Download
|
||||||
|
this.bind(
|
||||||
|
elements.buttons.download,
|
||||||
|
'click',
|
||||||
|
() => {
|
||||||
|
triggerEvent.call(player, player.media, 'download');
|
||||||
|
},
|
||||||
|
'download',
|
||||||
|
);
|
||||||
|
|
||||||
// Fullscreen toggle
|
// Fullscreen toggle
|
||||||
this.bind(
|
this.bind(
|
||||||
elements.buttons.fullscreen,
|
elements.buttons.fullscreen,
|
||||||
@ -698,7 +713,7 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Show controls when they receive focus (e.g., when using keyboard tab key)
|
// 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;
|
const { config, elements, timers } = player;
|
||||||
|
|
||||||
// Skip transition to prevent focus from scrolling the parent element
|
// Skip transition to prevent focus from scrolling the parent element
|
||||||
@ -712,7 +727,7 @@ class Listeners {
|
|||||||
toggleClass(elements.controls, config.classNames.noTransition, false);
|
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Delay a little more for keyboard users
|
// Delay a little more for mouse users
|
||||||
const delay = this.touch ? 3000 : 4000;
|
const delay = this.touch ? 3000 : 4000;
|
||||||
|
|
||||||
// Clear timer
|
// Clear timer
|
||||||
|
@ -71,7 +71,7 @@ const vimeo = {
|
|||||||
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
|
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
|
||||||
setAspectRatio(input) {
|
setAspectRatio(input) {
|
||||||
const [x, y] = (is.string(input) ? input : this.config.ratio).split(':');
|
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}%`;
|
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
||||||
|
|
||||||
if (this.supported.ui) {
|
if (this.supported.ui) {
|
||||||
@ -278,6 +278,7 @@ const vimeo = {
|
|||||||
.getVideoUrl()
|
.getVideoUrl()
|
||||||
.then(value => {
|
.then(value => {
|
||||||
currentSrc = value;
|
currentSrc = value;
|
||||||
|
controls.setDownloadLink.call(player);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.debug.warn(error);
|
this.debug.warn(error);
|
||||||
|
6
src/sprite/plyr-download.svg
Normal file
6
src/sprite/plyr-download.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 325 B |
4
src/sprite/plyr-logo-vimeo.svg
Normal file
4
src/sprite/plyr-logo-vimeo.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 470 B |
4
src/sprite/plyr-logo-youtube.svg
Normal file
4
src/sprite/plyr-logo-youtube.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 425 B |
Loading…
x
Reference in New Issue
Block a user