Add 'passive' flag to internal captions methods to avoid overriding user preferences, support multiple browser languages (get first match) and improve comments

This commit is contained in:
Albin Larsson 2018-06-15 06:06:16 +02:00
parent cf5f77c709
commit 19e412a73a
2 changed files with 104 additions and 57 deletions

View File

@ -11,6 +11,7 @@ import { createElement, emptyElement, getAttributesFromSelector, insertAfter, re
import { on, triggerEvent } from './utils/events'; import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch'; import fetch from './utils/fetch';
import is from './utils/is'; import is from './utils/is';
import { dedupe } from './utils/arrays';
import { getHTML } from './utils/strings'; import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls'; import { parseUrl } from './utils/urls';
@ -63,21 +64,34 @@ const captions = {
}); });
} }
// Try to load the value from storage // Get and set initial data
let active = this.storage.get('captions'); // The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config
// * toggled: The real captions state
// Otherwise fall back to the default config const languages = dedupe(Array.from(navigator.languages || navigator.userLanguage)
.map(language => language.split('-')[0]));
let language = this.storage.get('language') || this.config.captions.language;
// Use first browser language when language is 'auto'
if (language === 'auto') {
[language] = languages;
}
let active = this.storage.get('captions');
if (!is.boolean(active)) { if (!is.boolean(active)) {
({ active } = this.config.captions); ({ active } = this.config.captions);
} }
// Get language from storage, fallback to config Object.assign(this.captions, {
let language = this.storage.get('language') || this.config.captions.language; toggled: false,
if (language === 'auto') { active,
[language] = (navigator.language || navigator.userLanguage).split('-'); language,
} languages,
// Set language and show if active });
captions.setLanguage.call(this, language, active);
// Watch changes to textTracks and update captions menu // Watch changes to textTracks and update captions menu
if (this.isHTML5) { if (this.isHTML5) {
@ -89,10 +103,12 @@ const captions = {
setTimeout(captions.update.bind(this), 0); setTimeout(captions.update.bind(this), 0);
}, },
// Update available language options in settings based on tracks
update() { update() {
const tracks = captions.getTracks.call(this, true); const tracks = captions.getTracks.call(this, true);
// Get the wanted language // Get the wanted language
const { language, meta } = this.captions; const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// Handle tracks (add event listener and "pseudo"-default) // Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) { if (this.isHTML5 && this.isVideo) {
@ -111,12 +127,10 @@ const captions = {
}); });
} }
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode); // Update language first time it matches, or if the previous matching track was removed
const firstMatch = this.language !== language && tracks.find(track => track.language === language); if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
captions.setLanguage.call(this, language);
// Update language if removed or first matching track added captions.toggle.call(this, active && languageExists);
if (trackRemoved || firstMatch) {
captions.setLanguage.call(this, language, this.config.captions.active);
} }
// Enable or disable captions based on track length // Enable or disable captions based on track length
@ -128,44 +142,69 @@ const captions = {
} }
}, },
// Used internally for toggleCaptions() // Toggle captions display
toggle(input) { // Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support // If there's no full support
if (!this.supported.ui) { if (!this.supported.ui) {
return; return;
} }
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
// Get the next state
// If the method is called without parameter, toggle based on current value // If the method is called without parameter, toggle based on current value
const active = is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active); const active = is.nullOrUndefined(input) ? !toggled : input;
// Update state and trigger event
if (active !== toggled) {
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [
this.captions.language,
...this.captions.languages,
], true);
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
// Toggle state // Toggle state
toggleState(this.elements.buttons.captions, active); toggleState(this.elements.buttons.captions, active);
// Add class hook // Add class hook
toggleClass(this.elements.container, this.config.classNames.captions.active, active); toggleClass(this.elements.container, activeClass, active);
// Update state and trigger event this.captions.toggled = active;
if (active !== this.captions.active) {
this.captions.active = active;
// Update UI // Update settings menu
controls.updateSetting.call(this, 'captions'); controls.updateSetting.call(this, 'captions');
// Save to storage // When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active }); this.storage.set({ captions: active });
}
// Trigger event (not used internally) // Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
} }
}, },
// Used internally for currentTrack setter // Set captions by track index
set(index, setLanguage = true, show = true) { // Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
// Disable captions if setting to -1 // Disable captions if setting to -1
if (index === -1) { if (index === -1) {
this.toggleCaptions(false); captions.toggle.call(this, false, passive);
return; return;
} }
@ -181,15 +220,19 @@ const captions = {
if (this.captions.currentTrack !== index) { if (this.captions.currentTrack !== index) {
this.captions.currentTrack = index; this.captions.currentTrack = index;
const track = captions.getCurrentTrack.call(this); const track = tracks[index];
const { language } = track || {}; const { language } = track || {};
// Store reference to node for invalidation on remove // Store reference to node for invalidation on remove
this.captions.currentTrackNode = track; this.captions.currentTrackNode = track;
// Prevent setting language in some cases, since it can violate user's intentions // Update settings menu
if (setLanguage) { controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language; this.captions.language = language;
this.storage.set({ language });
} }
// Handle Vimeo captions // Handle Vimeo captions
@ -197,13 +240,7 @@ const captions = {
this.embed.enableTextTrack(language); this.embed.enableTextTrack(language);
} }
// Update UI // Trigger event
controls.updateSetting.call(this, 'captions');
// Save to storage
this.storage.set({ language });
// Trigger event (not used internally)
triggerEvent.call(this, this.media, 'languagechange'); triggerEvent.call(this, this.media, 'languagechange');
} }
@ -213,13 +250,12 @@ const captions = {
} }
// Show captions // Show captions
if (show) { captions.toggle.call(this, true, passive);
this.toggleCaptions(true);
}
}, },
// Used internally for language setter // Set captions by language
setLanguage(language, show = true) { // Used internally for the language setter with the passive option forced to false
setLanguage(language, passive = true) {
if (!is.string(language)) { if (!is.string(language)) {
this.debug.warn('Invalid language argument', language); this.debug.warn('Invalid language argument', language);
return; return;
@ -229,8 +265,8 @@ const captions = {
// Set currentTrack // Set currentTrack
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const track = captions.getCurrentTrack.call(this, true); const track = captions.findTrack.call(this, [language]);
captions.set.call(this, tracks.indexOf(track), false, show); captions.set.call(this, tracks.indexOf(track), passive);
}, },
// Get current valid caption tracks // Get current valid caption tracks
@ -247,19 +283,30 @@ const captions = {
].includes(track.kind)); ].includes(track.kind));
}, },
// Get the current track for the current language // Match tracks based on languages and get the first
getCurrentTrack(fromLanguage = false) { findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0]; let track;
languages.every(language => {
track = sorted.find(track => track.language === language);
return !track; // Break iteration if there is a match
});
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
// Get the current track
getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
}, },
// Get UI label for track // Get UI label for track
getLabel(track) { getLabel(track) {
let currentTrack = track; let currentTrack = track;
if (!is.track(currentTrack) && support.textTracks && this.captions.active) { if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
currentTrack = captions.getCurrentTrack.call(this); currentTrack = captions.getCurrentTrack.call(this);
} }

4
src/js/controls.js vendored
View File

@ -848,7 +848,7 @@ const controls = {
// Generate options data // Generate options data
const options = tracks.map((track, value) => ({ const options = tracks.map((track, value) => ({
value, value,
checked: this.captions.active && this.currentTrack === value, checked: this.captions.toggled && this.currentTrack === value,
title: captions.getLabel.call(this, track), title: captions.getLabel.call(this, track),
badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()), badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),
list, list,
@ -858,7 +858,7 @@ const controls = {
// Add the "Disabled" option to turn off captions // Add the "Disabled" option to turn off captions
options.unshift({ options.unshift({
value: -1, value: -1,
checked: !this.captions.active, checked: !this.captions.toggled,
title: i18n.get('disabled', this.config), title: i18n.get('disabled', this.config),
list, list,
type: 'language', type: 'language',