aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/captions.js3
-rw-r--r--src/js/config/defaults.js7
-rw-r--r--src/js/controls.js116
-rw-r--r--src/js/fullscreen.js8
-rw-r--r--src/js/plyr.js47
-rw-r--r--src/js/ui.js11
-rw-r--r--src/js/utils/elements.js22
-rw-r--r--src/js/utils/time.js2
-rw-r--r--src/sass/components/control.scss8
9 files changed, 113 insertions, 111 deletions
diff --git a/src/js/captions.js b/src/js/captions.js
index 28d5cb91..732b2e38 100644
--- a/src/js/captions.js
+++ b/src/js/captions.js
@@ -15,7 +15,6 @@ import {
insertAfter,
removeElement,
toggleClass,
- toggleState,
} from './utils/elements';
import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch';
@@ -193,7 +192,7 @@ const captions = {
}
// Toggle state
- toggleState(this.elements.buttons.captions, active);
+ this.elements.buttons.captions.pressed = active;
// Add class hook
toggleClass(this.elements.container, activeClass, active);
diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js
index 0e07b75c..1e90a4f0 100644
--- a/src/js/config/defaults.js
+++ b/src/js/config/defaults.js
@@ -18,6 +18,10 @@ const defaults = {
// Only allow one media playing at once (vimeo only)
autopause: true,
+ // Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
+ // TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
+ playsinline: true,
+
// Default time to skip when rewind/fast forward
seekTime: 10,
@@ -153,6 +157,7 @@ const defaults = {
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
+ seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
@@ -167,6 +172,7 @@ const defaults = {
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
+ menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
@@ -334,6 +340,7 @@ const defaults = {
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
+ controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
diff --git a/src/js/controls.js b/src/js/controls.js
index d8d770a5..d79aaee7 100644
--- a/src/js/controls.js
+++ b/src/js/controls.js
@@ -20,7 +20,7 @@ import {
setAttributes,
toggleClass,
toggleHidden,
- toggleState,
+ matches,
} from './utils/elements';
import { off, on } from './utils/events';
import is from './utils/is';
@@ -29,6 +29,7 @@ import { extend } from './utils/objects';
import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';
import { formatTime, getHours } from './utils/time';
+// TODO: Don't export a massive object - break down and create class
const controls = {
// Get icon URL
getIconUrl() {
@@ -41,8 +42,7 @@ const controls = {
};
},
- // Find the UI controls and store references in custom controls
- // TODO: Allow settings menus with custom controls
+ // Find the UI controls
findElements() {
try {
this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);
@@ -139,12 +139,11 @@ const controls = {
pip: 'PIP',
airplay: 'AirPlay',
};
-
const text = universals[type] || i18n.get(type, this.config);
+
const attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '),
});
-
return createElement('span', attributes, text);
},
@@ -250,9 +249,6 @@ const controls = {
// Label/Tooltip
button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' }));
button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' }));
-
- // Add aria attributes
- attributes['aria-pressed'] = false;
} else {
button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label));
@@ -274,22 +270,23 @@ const controls = {
this.elements.buttons[type] = button;
}
+ // Toggle classname when pressed property is set
+ const className = this.config.classNames.controlPressed;
+ Object.defineProperty(button, 'pressed', {
+ enumerable: true,
+ get() {
+ return hasClass(button, className);
+ },
+ set(pressed = false) {
+ toggleClass(button, className, pressed);
+ },
+ });
+
return button;
},
// Create an <input type='range'>
createRange(type, attributes) {
- // Seek label
- const label = createElement(
- 'label',
- {
- for: attributes.id,
- id: `${attributes.id}-label`,
- class: this.config.classNames.hidden,
- },
- i18n.get(type, this.config),
- );
-
// Seek input
const input = createElement(
'input',
@@ -304,7 +301,7 @@ const controls = {
autocomplete: 'off',
// A11y fixes for https://github.com/sampotts/plyr/issues/905
role: 'slider',
- 'aria-labelledby': `${attributes.id}-label`,
+ 'aria-label': i18n.get(type, this.config),
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 0,
@@ -318,10 +315,7 @@ const controls = {
// Set the fill for webkit now
controls.updateRangeFill.call(this, input);
- return {
- label,
- input,
- };
+ return input;
},
// Create a <progress>
@@ -349,7 +343,6 @@ const controls = {
played: 'played',
buffer: 'buffered',
}[type];
-
const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';
progress.innerText = `% ${suffix.toLowerCase()}`;
@@ -412,6 +405,19 @@ const controls = {
list.appendChild(item);
},
+ // Format a time for display
+ formatTime(time = 0, inverted = false) {
+ // Bail if the value isn't a number
+ if (!is.number(time)) {
+ return time;
+ }
+
+ // Always display hours if duration is over an hour
+ const forceHours = getHours(this.duration) > 0;
+
+ return formatTime(time, forceHours, inverted);
+ },
+
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
@@ -419,11 +425,8 @@ const controls = {
return;
}
- // Always display hours if duration is over an hour
- const forceHours = getHours(this.duration) > 0;
-
// eslint-disable-next-line no-param-reassign
- target.innerText = formatTime(time, forceHours, inverted);
+ target.innerText = controls.formatTime(time, inverted);
},
// Update volume UI and storage
@@ -439,7 +442,7 @@ const controls = {
// Update mute state
if (is.element(this.elements.buttons.mute)) {
- toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
+ this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
}
},
@@ -518,8 +521,23 @@ const controls = {
return;
}
- // Set aria value for https://github.com/sampotts/plyr/issues/905
- range.setAttribute('aria-valuenow', range.value);
+ // Set aria values for https://github.com/sampotts/plyr/issues/905
+ if (matches(range, this.config.selectors.inputs.seek)) {
+ range.setAttribute('aria-valuenow', this.currentTime);
+ const currentTime = controls.formatTime(this.currentTime);
+ const duration = controls.formatTime(this.duration);
+ const format = i18n.get('seekLabel', this.config);
+ range.setAttribute(
+ 'aria-valuetext',
+ format.replace('{currentTime}', currentTime).replace('{duration}', duration),
+ );
+ } else if (matches(range, this.config.selectors.inputs.volume)) {
+ const percent = range.value * 100;
+ range.setAttribute('aria-valuenow', percent);
+ range.setAttribute('aria-valuetext', `${percent}%`);
+ } else {
+ range.setAttribute('aria-valuenow', range.value);
+ }
// WebKit only
if (!browser.isWebkit) {
@@ -610,11 +628,16 @@ const controls = {
// Show the duration on metadataloaded or durationchange events
durationUpdate() {
- // Bail if no ui or durationchange event triggered after playing/seek when invertTime is false
+ // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {
return;
}
+ // Update ARIA values
+ if (is.element(this.elements.inputs.seek)) {
+ this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
+ }
+
// If there's a spot to display duration
const hasDuration = is.element(this.elements.display.duration);
@@ -1117,11 +1140,11 @@ const controls = {
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
// Seek range slider
- const seek = controls.createRange.call(this, 'seek', {
- id: `plyr-seek-${data.id}`,
- });
- progress.appendChild(seek.label);
- progress.appendChild(seek.input);
+ progress.appendChild(
+ controls.createRange.call(this, 'seek', {
+ id: `plyr-seek-${data.id}`,
+ }),
+ );
// Buffer progress
progress.appendChild(controls.createProgress.call(this, 'buffer'));
@@ -1175,15 +1198,15 @@ const controls = {
};
// Create the volume range slider
- const range = controls.createRange.call(
- this,
- 'volume',
- extend(attributes, {
- id: `plyr-volume-${data.id}`,
- }),
+ volume.appendChild(
+ controls.createRange.call(
+ this,
+ 'volume',
+ extend(attributes, {
+ id: `plyr-volume-${data.id}`,
+ }),
+ ),
);
- volume.appendChild(range.label);
- volume.appendChild(range.input);
this.elements.volume = volume;
@@ -1448,7 +1471,6 @@ const controls = {
Array.from(labels).forEach(label => {
toggleClass(label, this.config.classNames.hidden, false);
toggleClass(label, this.config.classNames.tooltip, true);
- label.setAttribute('role', 'tooltip');
});
}
},
diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js
index ded581f9..d9cba17f 100644
--- a/src/js/fullscreen.js
+++ b/src/js/fullscreen.js
@@ -4,7 +4,7 @@
// ==========================================================================
import browser from './utils/browser';
-import { hasClass, toggleClass, toggleState, trapFocus } from './utils/elements';
+import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
@@ -16,7 +16,7 @@ function onChange() {
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
- toggleState(button, this.active);
+ button.pressed = this.active;
}
// Trigger an event
@@ -68,8 +68,8 @@ class Fullscreen {
document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
- // TODO: Filter for target??
- onChange.call(this);
+ // TODO: Filter for target??
+ onChange.call(this);
},
);
diff --git a/src/js/plyr.js b/src/js/plyr.js
index 7ecb810a..93fccbfa 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -263,9 +263,6 @@ class Plyr {
wrap(this.media, this.elements.container);
}
- // Allow focus to be captured
- this.elements.container.setAttribute('tabindex', 0);
-
// Add style hook
ui.addStyleHook.call(this);
@@ -831,7 +828,7 @@ class Plyr {
*/
toggleCaptions(input) {
captions.toggle.call(this, input, false);
- }
+ }
/**
* Set the caption track by index
@@ -1035,35 +1032,35 @@ class Plyr {
// Provider specific stuff
if (this.isHTML5) {
- // Clear timeout
- clearTimeout(this.timers.loading);
+ // Clear timeout
+ clearTimeout(this.timers.loading);
- // Restore native video controls
- ui.toggleNativeControls.call(this, true);
+ // Restore native video controls
+ ui.toggleNativeControls.call(this, true);
- // Clean up
- done();
+ // Clean up
+ done();
} else if (this.isYouTube) {
- // Clear timers
- clearInterval(this.timers.buffering);
- clearInterval(this.timers.playing);
+ // Clear timers
+ clearInterval(this.timers.buffering);
+ clearInterval(this.timers.playing);
- // Destroy YouTube API
+ // Destroy YouTube API
if (this.embed !== null && is.function(this.embed.destroy)) {
- this.embed.destroy();
- }
+ this.embed.destroy();
+ }
- // Clean up
- done();
+ // Clean up
+ done();
} else if (this.isVimeo) {
- // Destroy Vimeo API
- // then clean up (wait, to prevent postmessage errors)
- if (this.embed !== null) {
- this.embed.unload().then(done);
- }
+ // Destroy Vimeo API
+ // then clean up (wait, to prevent postmessage errors)
+ if (this.embed !== null) {
+ this.embed.unload().then(done);
+ }
- // Vimeo does not always return
- setTimeout(done, 200);
+ // Vimeo does not always return
+ setTimeout(done, 200);
}
}
diff --git a/src/js/ui.js b/src/js/ui.js
index e0d7c6ae..b77ee131 100644
--- a/src/js/ui.js
+++ b/src/js/ui.js
@@ -7,7 +7,7 @@ import controls from './controls';
import i18n from './i18n';
import support from './support';
import browser from './utils/browser';
-import { getElement, toggleClass, toggleState } from './utils/elements';
+import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import is from './utils/is';
import loadImage from './utils/loadImage';
@@ -132,9 +132,6 @@ const ui = {
// If there's a media title set, use that for the label
if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`;
-
- // Set container label
- this.elements.container.setAttribute('aria-label', this.config.title);
}
// If there's a play button, set label
@@ -216,8 +213,10 @@ const ui = {
toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
- // Set ARIA state
- toggleState(this.elements.buttons.play, this.playing);
+ // Set state
+ Array.from(this.elements.buttons.play).forEach(target => {
+ target.pressed = this.playing;
+ });
// Only update controls on non timeupdate events
if (is.event(event) && event.type === 'timeupdate') {
diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js
index 19e98f6f..69e4d46c 100644
--- a/src/js/utils/elements.js
+++ b/src/js/utils/elements.js
@@ -283,25 +283,3 @@ export function trapFocus(element = null, toggle = false) {
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
}
-
-// Toggle aria-pressed state on a toggle button
-// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
-export function toggleState(element, input) {
- // If multiple elements passed
- if (is.array(element) || is.nodeList(element)) {
- Array.from(element).forEach(target => toggleState(target, input));
- return;
- }
-
- // Bail if no target
- if (!is.element(element)) {
- return;
- }
-
- // Get state
- const pressed = element.getAttribute('aria-pressed') === 'true';
- const state = is.boolean(input) ? input : !pressed;
-
- // Set the attribute on target
- element.setAttribute('aria-pressed', state);
-}
diff --git a/src/js/utils/time.js b/src/js/utils/time.js
index 0c9fce64..7c9860fd 100644
--- a/src/js/utils/time.js
+++ b/src/js/utils/time.js
@@ -32,5 +32,5 @@ export function formatTime(time = 0, displayHours = false, inverted = false) {
}
// Render
- return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
+ return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
}
diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss
index 52716805..cfef1b3a 100644
--- a/src/sass/components/control.scss
+++ b/src/sass/components/control.scss
@@ -34,10 +34,10 @@
}
// Change icons on state change
-.plyr__control[aria-pressed='false'] .icon--pressed,
-.plyr__control[aria-pressed='true'] .icon--not-pressed,
-.plyr__control[aria-pressed='false'] .label--pressed,
-.plyr__control[aria-pressed='true'] .label--not-pressed {
+.plyr__control:not(.plyr__control--pressed) .icon--pressed,
+.plyr__control.plyr__control--pressed .icon--not-pressed,
+.plyr__control:not(.plyr__control--pressed) .label--pressed,
+.plyr__control.plyr__control--pressed .label--not-pressed {
display: none;
}