summaryrefslogtreecommitdiff
path: root/javascript/videojs/src/js/control-bar/text-track-controls
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/videojs/src/js/control-bar/text-track-controls')
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/caption-settings-menu-item.js71
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/captions-button.js87
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/chapters-button.js224
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/chapters-track-menu-item.js60
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/descriptions-button.js105
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/off-text-track-menu-item.js115
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/subs-caps-button.js99
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/subs-caps-menu-item.js43
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/subtitles-button.js66
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/text-track-button.js93
-rw-r--r--javascript/videojs/src/js/control-bar/text-track-controls/text-track-menu-item.js182
11 files changed, 1145 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/caption-settings-menu-item.js b/javascript/videojs/src/js/control-bar/text-track-controls/caption-settings-menu-item.js
new file mode 100644
index 0000000..5d08e6d
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/caption-settings-menu-item.js
@@ -0,0 +1,71 @@
+/**
+ * @file caption-settings-menu-item.js
+ */
+import TextTrackMenuItem from './text-track-menu-item.js';
+import Component from '../../component.js';
+
+/** @import Player from '../../player' */
+
+/**
+ * The menu item for caption track settings menu
+ *
+ * @extends TextTrackMenuItem
+ */
+class CaptionSettingsMenuItem extends TextTrackMenuItem {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ */
+ constructor(player, options) {
+ options.track = {
+ player,
+ kind: options.kind,
+ label: options.kind + ' settings',
+ selectable: false,
+ default: false,
+ mode: 'disabled'
+ };
+
+ // CaptionSettingsMenuItem has no concept of 'selected'
+ options.selectable = false;
+
+ options.name = 'CaptionSettingsMenuItem';
+
+ super(player, options);
+ this.addClass('vjs-texttrack-settings');
+ this.controlText(', opens ' + options.kind + ' settings dialog');
+ }
+
+ /**
+ * This gets called when an `CaptionSettingsMenuItem` is "clicked". See
+ * {@link ClickableComponent} for more detailed information on what a click can be.
+ *
+ * @param {Event} [event]
+ * The `keydown`, `tap`, or `click` event that caused this function to be
+ * called.
+ *
+ * @listens tap
+ * @listens click
+ */
+ handleClick(event) {
+ this.player().getChild('textTrackSettings').open();
+ }
+
+ /**
+ * Update control text and label on languagechange
+ */
+ handleLanguagechange() {
+ this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings');
+
+ super.handleLanguagechange();
+ }
+}
+
+Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
+export default CaptionSettingsMenuItem;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/captions-button.js b/javascript/videojs/src/js/control-bar/text-track-controls/captions-button.js
new file mode 100644
index 0000000..8f5f6da
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/captions-button.js
@@ -0,0 +1,87 @@
+/**
+ * @file captions-button.js
+ */
+import TextTrackButton from './text-track-button.js';
+import Component from '../../component.js';
+import CaptionSettingsMenuItem from './caption-settings-menu-item.js';
+
+/** @import Player from '../../player' */
+
+/**
+ * The button component for toggling and selecting captions
+ *
+ * @extends TextTrackButton
+ */
+class CaptionsButton extends TextTrackButton {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ *
+ * @param {Function} [ready]
+ * The function to call when this component is ready.
+ */
+ constructor(player, options, ready) {
+ super(player, options, ready);
+
+ this.setIcon('captions');
+ }
+
+ /**
+ * Builds the default DOM `className`.
+ *
+ * @return {string}
+ * The DOM `className` for this object.
+ */
+ buildCSSClass() {
+ return `vjs-captions-button ${super.buildCSSClass()}`;
+ }
+
+ buildWrapperCSSClass() {
+ return `vjs-captions-button ${super.buildWrapperCSSClass()}`;
+ }
+
+ /**
+ * Create caption menu items
+ *
+ * @return {CaptionSettingsMenuItem[]}
+ * The array of current menu items.
+ */
+ createItems() {
+ const items = [];
+
+ if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) &&
+ this.player().getChild('textTrackSettings')) {
+ items.push(new CaptionSettingsMenuItem(this.player_, {kind: this.kind_}));
+
+ this.hideThreshold_ += 1;
+ }
+
+ return super.createItems(items);
+ }
+
+}
+
+/**
+ * `kind` of TextTrack to look for to associate it with this menu.
+ *
+ * @type {string}
+ * @private
+ */
+CaptionsButton.prototype.kind_ = 'captions';
+
+/**
+ * The text that should display over the `CaptionsButton`s controls. Added for localization.
+ *
+ * @type {string}
+ * @protected
+ */
+CaptionsButton.prototype.controlText_ = 'Captions';
+
+Component.registerComponent('CaptionsButton', CaptionsButton);
+export default CaptionsButton;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/chapters-button.js b/javascript/videojs/src/js/control-bar/text-track-controls/chapters-button.js
new file mode 100644
index 0000000..8366179
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/chapters-button.js
@@ -0,0 +1,224 @@
+/**
+ * @file chapters-button.js
+ */
+import TextTrackButton from './text-track-button.js';
+import Component from '../../component.js';
+import ChaptersTrackMenuItem from './chapters-track-menu-item.js';
+import {toTitleCase} from '../../utils/str.js';
+
+/** @import Player from '../../player' */
+/** @import Menu from '../../menu/menu' */
+/** @import TextTrack from '../../tracks/text-track' */
+/** @import TextTrackMenuItem from '../text-track-controls/text-track-menu-item' */
+
+/**
+ * The button component for toggling and selecting chapters
+ * Chapters act much differently than other text tracks
+ * Cues are navigation vs. other tracks of alternative languages
+ *
+ * @extends TextTrackButton
+ */
+class ChaptersButton extends TextTrackButton {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ *
+ * @param {Function} [ready]
+ * The function to call when this function is ready.
+ */
+ constructor(player, options, ready) {
+ super(player, options, ready);
+
+ this.setIcon('chapters');
+
+ this.selectCurrentItem_ = () => {
+ this.items.forEach(item => {
+ item.selected(this.track_.activeCues[0] === item.cue);
+ });
+ };
+ }
+
+ /**
+ * Builds the default DOM `className`.
+ *
+ * @return {string}
+ * The DOM `className` for this object.
+ */
+ buildCSSClass() {
+ return `vjs-chapters-button ${super.buildCSSClass()}`;
+ }
+
+ buildWrapperCSSClass() {
+ return `vjs-chapters-button ${super.buildWrapperCSSClass()}`;
+ }
+
+ /**
+ * Update the menu based on the current state of its items.
+ *
+ * @param {Event} [event]
+ * An event that triggered this function to run.
+ *
+ * @listens TextTrackList#addtrack
+ * @listens TextTrackList#removetrack
+ * @listens TextTrackList#change
+ */
+ update(event) {
+ if (event && event.track && event.track.kind !== 'chapters') {
+ return;
+ }
+
+ const track = this.findChaptersTrack();
+
+ if (track !== this.track_) {
+ this.setTrack(track);
+ super.update();
+ } else if (!this.items || (track && track.cues && track.cues.length !== this.items.length)) {
+ // Update the menu initially or if the number of cues has changed since set
+ super.update();
+ }
+ }
+
+ /**
+ * Set the currently selected track for the chapters button.
+ *
+ * @param {TextTrack} track
+ * The new track to select. Nothing will change if this is the currently selected
+ * track.
+ */
+ setTrack(track) {
+ if (this.track_ === track) {
+ return;
+ }
+
+ if (!this.updateHandler_) {
+ this.updateHandler_ = this.update.bind(this);
+ }
+
+ // here this.track_ refers to the old track instance
+ if (this.track_) {
+ const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
+
+ if (remoteTextTrackEl) {
+ remoteTextTrackEl.removeEventListener('load', this.updateHandler_);
+ }
+
+ this.track_.removeEventListener('cuechange', this.selectCurrentItem_);
+
+ this.track_ = null;
+ }
+
+ this.track_ = track;
+
+ // here this.track_ refers to the new track instance
+ if (this.track_) {
+ this.track_.mode = 'hidden';
+
+ const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
+
+ if (remoteTextTrackEl) {
+ remoteTextTrackEl.addEventListener('load', this.updateHandler_);
+ }
+
+ this.track_.addEventListener('cuechange', this.selectCurrentItem_);
+ }
+ }
+
+ /**
+ * Find the track object that is currently in use by this ChaptersButton
+ *
+ * @return {TextTrack|undefined}
+ * The current track or undefined if none was found.
+ */
+ findChaptersTrack() {
+ const tracks = this.player_.textTracks() || [];
+
+ for (let i = tracks.length - 1; i >= 0; i--) {
+ // We will always choose the last track as our chaptersTrack
+ const track = tracks[i];
+
+ if (track.kind === this.kind_) {
+ return track;
+ }
+ }
+ }
+
+ /**
+ * Get the caption for the ChaptersButton based on the track label. This will also
+ * use the current tracks localized kind as a fallback if a label does not exist.
+ *
+ * @return {string}
+ * The tracks current label or the localized track kind.
+ */
+ getMenuCaption() {
+ if (this.track_ && this.track_.label) {
+ return this.track_.label;
+ }
+ return this.localize(toTitleCase(this.kind_));
+ }
+
+ /**
+ * Create menu from chapter track
+ *
+ * @return {Menu}
+ * New menu for the chapter buttons
+ */
+ createMenu() {
+ this.options_.title = this.getMenuCaption();
+ return super.createMenu();
+ }
+
+ /**
+ * Create a menu item for each text track
+ *
+ * @return {TextTrackMenuItem[]}
+ * Array of menu items
+ */
+ createItems() {
+ const items = [];
+
+ if (!this.track_) {
+ return items;
+ }
+
+ const cues = this.track_.cues;
+
+ if (!cues) {
+ return items;
+ }
+
+ for (let i = 0, l = cues.length; i < l; i++) {
+ const cue = cues[i];
+ const mi = new ChaptersTrackMenuItem(this.player_, { track: this.track_, cue });
+
+ items.push(mi);
+ }
+
+ return items;
+ }
+
+}
+
+/**
+ * `kind` of TextTrack to look for to associate it with this menu.
+ *
+ * @type {string}
+ * @private
+ */
+ChaptersButton.prototype.kind_ = 'chapters';
+
+/**
+ * The text that should display over the `ChaptersButton`s controls. Added for localization.
+ *
+ * @type {string}
+ * @protected
+ */
+ChaptersButton.prototype.controlText_ = 'Chapters';
+
+Component.registerComponent('ChaptersButton', ChaptersButton);
+export default ChaptersButton;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/chapters-track-menu-item.js b/javascript/videojs/src/js/control-bar/text-track-controls/chapters-track-menu-item.js
new file mode 100644
index 0000000..2463564
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/chapters-track-menu-item.js
@@ -0,0 +1,60 @@
+/**
+ * @file chapters-track-menu-item.js
+ */
+import MenuItem from '../../menu/menu-item.js';
+import Component from '../../component.js';
+
+/** @import Player from '../../player' */
+
+/**
+ * The chapter track menu item
+ *
+ * @extends MenuItem
+ */
+class ChaptersTrackMenuItem extends MenuItem {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ */
+ constructor(player, options) {
+ const track = options.track;
+ const cue = options.cue;
+ const currentTime = player.currentTime();
+
+ // Modify options for parent MenuItem class's init.
+ options.selectable = true;
+ options.multiSelectable = false;
+ options.label = cue.text;
+ options.selected = (cue.startTime <= currentTime && currentTime < cue.endTime);
+ super(player, options);
+
+ this.track = track;
+ this.cue = cue;
+ }
+
+ /**
+ * This gets called when an `ChaptersTrackMenuItem` is "clicked". See
+ * {@link ClickableComponent} for more detailed information on what a click can be.
+ *
+ * @param {Event} [event]
+ * The `keydown`, `tap`, or `click` event that caused this function to be
+ * called.
+ *
+ * @listens tap
+ * @listens click
+ */
+ handleClick(event) {
+ super.handleClick();
+ this.player_.currentTime(this.cue.startTime);
+ }
+
+}
+
+Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
+export default ChaptersTrackMenuItem;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/descriptions-button.js b/javascript/videojs/src/js/control-bar/text-track-controls/descriptions-button.js
new file mode 100644
index 0000000..320cc35
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/descriptions-button.js
@@ -0,0 +1,105 @@
+/**
+ * @file descriptions-button.js
+ */
+import TextTrackButton from './text-track-button.js';
+import Component from '../../component.js';
+import * as Fn from '../../utils/fn.js';
+
+/** @import Player from '../../player' */
+
+/**
+ * The button component for toggling and selecting descriptions
+ *
+ * @extends TextTrackButton
+ */
+class DescriptionsButton extends TextTrackButton {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ *
+ * @param {Function} [ready]
+ * The function to call when this component is ready.
+ */
+ constructor(player, options, ready) {
+ super(player, options, ready);
+
+ this.setIcon('audio-description');
+
+ const tracks = player.textTracks();
+ const changeHandler = Fn.bind_(this, this.handleTracksChange);
+
+ tracks.addEventListener('change', changeHandler);
+ this.on('dispose', function() {
+ tracks.removeEventListener('change', changeHandler);
+ });
+ }
+
+ /**
+ * Handle text track change
+ *
+ * @param {Event} event
+ * The event that caused this function to run
+ *
+ * @listens TextTrackList#change
+ */
+ handleTracksChange(event) {
+ const tracks = this.player().textTracks();
+ let disabled = false;
+
+ // Check whether a track of a different kind is showing
+ for (let i = 0, l = tracks.length; i < l; i++) {
+ const track = tracks[i];
+
+ if (track.kind !== this.kind_ && track.mode === 'showing') {
+ disabled = true;
+ break;
+ }
+ }
+
+ // If another track is showing, disable this menu button
+ if (disabled) {
+ this.disable();
+ } else {
+ this.enable();
+ }
+ }
+
+ /**
+ * Builds the default DOM `className`.
+ *
+ * @return {string}
+ * The DOM `className` for this object.
+ */
+ buildCSSClass() {
+ return `vjs-descriptions-button ${super.buildCSSClass()}`;
+ }
+
+ buildWrapperCSSClass() {
+ return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`;
+ }
+}
+
+/**
+ * `kind` of TextTrack to look for to associate it with this menu.
+ *
+ * @type {string}
+ * @private
+ */
+DescriptionsButton.prototype.kind_ = 'descriptions';
+
+/**
+ * The text that should display over the `DescriptionsButton`s controls. Added for localization.
+ *
+ * @type {string}
+ * @protected
+ */
+DescriptionsButton.prototype.controlText_ = 'Descriptions';
+
+Component.registerComponent('DescriptionsButton', DescriptionsButton);
+export default DescriptionsButton;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/off-text-track-menu-item.js b/javascript/videojs/src/js/control-bar/text-track-controls/off-text-track-menu-item.js
new file mode 100644
index 0000000..7f117a1
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/off-text-track-menu-item.js
@@ -0,0 +1,115 @@
+/**
+ * @file off-text-track-menu-item.js
+ */
+import TextTrackMenuItem from './text-track-menu-item.js';
+import Component from '../../component.js';
+
+/** @import Player from '../../player' */
+
+/**
+ * A special menu item for turning off a specific type of text track
+ *
+ * @extends TextTrackMenuItem
+ */
+class OffTextTrackMenuItem extends TextTrackMenuItem {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ */
+ constructor(player, options) {
+ // Create pseudo track info
+ // Requires options['kind']
+ options.track = {
+ player,
+ // it is no longer necessary to store `kind` or `kinds` on the track itself
+ // since they are now stored in the `kinds` property of all instances of
+ // TextTrackMenuItem, but this will remain for backwards compatibility
+ kind: options.kind,
+ kinds: options.kinds,
+ default: false,
+ mode: 'disabled'
+ };
+
+ if (!options.kinds) {
+ options.kinds = [options.kind];
+ }
+
+ if (options.label) {
+ options.track.label = options.label;
+ } else {
+ options.track.label = options.kinds.join(' and ') + ' off';
+ }
+
+ // MenuItem is selectable
+ options.selectable = true;
+ // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
+ options.multiSelectable = false;
+
+ super(player, options);
+ }
+
+ /**
+ * Handle text track change
+ *
+ * @param {Event} event
+ * The event that caused this function to run
+ */
+ handleTracksChange(event) {
+ const tracks = this.player().textTracks();
+ let shouldBeSelected = true;
+
+ for (let i = 0, l = tracks.length; i < l; i++) {
+ const track = tracks[i];
+
+ if ((this.options_.kinds.indexOf(track.kind) > -1) && track.mode === 'showing') {
+ shouldBeSelected = false;
+ break;
+ }
+ }
+
+ // Prevent redundant selected() calls because they may cause
+ // screen readers to read the appended control text unnecessarily
+ if (shouldBeSelected !== this.isSelected_) {
+ this.selected(shouldBeSelected);
+ }
+ }
+
+ handleSelectedLanguageChange(event) {
+ const tracks = this.player().textTracks();
+ let allHidden = true;
+
+ for (let i = 0, l = tracks.length; i < l; i++) {
+ const track = tracks[i];
+
+ if ((['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1) && track.mode === 'showing') {
+ allHidden = false;
+ break;
+ }
+ }
+
+ if (allHidden) {
+ this.player_.cache_.selectedLanguage = {
+ enabled: false
+ };
+ }
+ }
+
+ /**
+ * Update control text and label on languagechange
+ */
+ handleLanguagechange() {
+ this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label);
+
+ super.handleLanguagechange();
+ }
+
+}
+
+Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
+export default OffTextTrackMenuItem;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/subs-caps-button.js b/javascript/videojs/src/js/control-bar/text-track-controls/subs-caps-button.js
new file mode 100644
index 0000000..576b4d0
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/subs-caps-button.js
@@ -0,0 +1,99 @@
+/**
+ * @file sub-caps-button.js
+ */
+import TextTrackButton from './text-track-button.js';
+import Component from '../../component.js';
+import CaptionSettingsMenuItem from './caption-settings-menu-item.js';
+import SubsCapsMenuItem from './subs-caps-menu-item.js';
+import {toTitleCase} from '../../utils/str.js';
+
+/** @import Player from '../../player' */
+
+/**
+ * The button component for toggling and selecting captions and/or subtitles
+ *
+ * @extends TextTrackButton
+ */
+class SubsCapsButton extends TextTrackButton {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ *
+ * @param {Function} [ready]
+ * The function to call when this component is ready.
+ */
+ constructor(player, options = {}) {
+ super(player, options);
+
+ // Although North America uses "captions" in most cases for
+ // "captions and subtitles" other locales use "subtitles"
+ this.label_ = 'subtitles';
+ this.setIcon('subtitles');
+ if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
+ this.label_ = 'captions';
+ this.setIcon('captions');
+ }
+ this.menuButton_.controlText(toTitleCase(this.label_));
+ }
+
+ /**
+ * Builds the default DOM `className`.
+ *
+ * @return {string}
+ * The DOM `className` for this object.
+ */
+ buildCSSClass() {
+ return `vjs-subs-caps-button ${super.buildCSSClass()}`;
+ }
+
+ buildWrapperCSSClass() {
+ return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`;
+ }
+
+ /**
+ * Create caption/subtitles menu items
+ *
+ * @return {CaptionSettingsMenuItem[]}
+ * The array of current menu items.
+ */
+ createItems() {
+ let items = [];
+
+ if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) &&
+ this.player().getChild('textTrackSettings')) {
+ items.push(new CaptionSettingsMenuItem(this.player_, {kind: this.label_}));
+
+ this.hideThreshold_ += 1;
+ }
+
+ items = super.createItems(items, SubsCapsMenuItem);
+ return items;
+ }
+
+}
+
+/**
+ * `kind`s of TextTrack to look for to associate it with this menu.
+ *
+ * @type {array}
+ * @private
+ */
+SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
+
+/**
+ * The text that should display over the `SubsCapsButton`s controls.
+ *
+ *
+ * @type {string}
+ * @protected
+ */
+SubsCapsButton.prototype.controlText_ = 'Subtitles';
+
+Component.registerComponent('SubsCapsButton', SubsCapsButton);
+export default SubsCapsButton;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/subs-caps-menu-item.js b/javascript/videojs/src/js/control-bar/text-track-controls/subs-caps-menu-item.js
new file mode 100644
index 0000000..7bcbe30
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/subs-caps-menu-item.js
@@ -0,0 +1,43 @@
+/**
+ * @file subs-caps-menu-item.js
+ */
+import TextTrackMenuItem from './text-track-menu-item.js';
+import Component from '../../component.js';
+import {createEl} from '../../utils/dom.js';
+
+/**
+ * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles
+ * in the SubsCapsMenu.
+ *
+ * @extends TextTrackMenuItem
+ */
+class SubsCapsMenuItem extends TextTrackMenuItem {
+
+ createEl(type, props, attrs) {
+ const el = super.createEl(type, props, attrs);
+ const parentSpan = el.querySelector('.vjs-menu-item-text');
+
+ if (this.options_.track.kind === 'captions') {
+ if (this.player_.options_.experimentalSvgIcons) {
+ this.setIcon('captions', el);
+ } else {
+ parentSpan.appendChild(createEl('span', {
+ className: 'vjs-icon-placeholder'
+ }, {
+ 'aria-hidden': true
+ }));
+ }
+ parentSpan.appendChild(createEl('span', {
+ className: 'vjs-control-text',
+ // space added as the text will visually flow with the
+ // label
+ textContent: ` ${this.localize('Captions')}`
+ }));
+ }
+
+ return el;
+ }
+}
+
+Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem);
+export default SubsCapsMenuItem;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/subtitles-button.js b/javascript/videojs/src/js/control-bar/text-track-controls/subtitles-button.js
new file mode 100644
index 0000000..f7c530c
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/subtitles-button.js
@@ -0,0 +1,66 @@
+/**
+ * @file subtitles-button.js
+ */
+import TextTrackButton from './text-track-button.js';
+import Component from '../../component.js';
+
+/** @import Player from '../../player' */
+
+/**
+ * The button component for toggling and selecting subtitles
+ *
+ * @extends TextTrackButton
+ */
+class SubtitlesButton extends TextTrackButton {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ *
+ * @param {Function} [ready]
+ * The function to call when this component is ready.
+ */
+ constructor(player, options, ready) {
+ super(player, options, ready);
+
+ this.setIcon('subtitles');
+ }
+
+ /**
+ * Builds the default DOM `className`.
+ *
+ * @return {string}
+ * The DOM `className` for this object.
+ */
+ buildCSSClass() {
+ return `vjs-subtitles-button ${super.buildCSSClass()}`;
+ }
+
+ buildWrapperCSSClass() {
+ return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`;
+ }
+}
+
+/**
+ * `kind` of TextTrack to look for to associate it with this menu.
+ *
+ * @type {string}
+ * @private
+ */
+SubtitlesButton.prototype.kind_ = 'subtitles';
+
+/**
+ * The text that should display over the `SubtitlesButton`s controls. Added for localization.
+ *
+ * @type {string}
+ * @protected
+ */
+SubtitlesButton.prototype.controlText_ = 'Subtitles';
+
+Component.registerComponent('SubtitlesButton', SubtitlesButton);
+export default SubtitlesButton;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/text-track-button.js b/javascript/videojs/src/js/control-bar/text-track-controls/text-track-button.js
new file mode 100644
index 0000000..0744986
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/text-track-button.js
@@ -0,0 +1,93 @@
+/**
+ * @file text-track-button.js
+ */
+import TrackButton from '../track-button.js';
+import Component from '../../component.js';
+import TextTrackMenuItem from './text-track-menu-item.js';
+import OffTextTrackMenuItem from './off-text-track-menu-item.js';
+
+/** @import Player from '../../player' */
+
+/**
+ * The base class for buttons that toggle specific text track types (e.g. subtitles)
+ *
+ * @extends MenuButton
+ */
+class TextTrackButton extends TrackButton {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options={}]
+ * The key/value store of player options.
+ */
+ constructor(player, options = {}) {
+ options.tracks = player.textTracks();
+
+ super(player, options);
+ }
+
+ /**
+ * Create a menu item for each text track
+ *
+ * @param {TextTrackMenuItem[]} [items=[]]
+ * Existing array of items to use during creation
+ *
+ * @return {TextTrackMenuItem[]}
+ * Array of menu items that were created
+ */
+ createItems(items = [], TrackMenuItem = TextTrackMenuItem) {
+
+ // Label is an override for the [track] off label
+ // USed to localise captions/subtitles
+ let label;
+
+ if (this.label_) {
+ label = `${this.label_} off`;
+ }
+ // Add an OFF menu item to turn all tracks off
+ items.push(new OffTextTrackMenuItem(this.player_, {
+ kinds: this.kinds_,
+ kind: this.kind_,
+ label
+ }));
+
+ this.hideThreshold_ += 1;
+
+ const tracks = this.player_.textTracks();
+
+ if (!Array.isArray(this.kinds_)) {
+ this.kinds_ = [this.kind_];
+ }
+
+ for (let i = 0; i < tracks.length; i++) {
+ const track = tracks[i];
+
+ // only add tracks that are of an appropriate kind and have a label
+ if (this.kinds_.indexOf(track.kind) > -1) {
+
+ const item = new TrackMenuItem(this.player_, {
+ track,
+ kinds: this.kinds_,
+ kind: this.kind_,
+ // MenuItem is selectable
+ selectable: true,
+ // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
+ multiSelectable: false
+ });
+
+ item.addClass(`vjs-${track.kind}-menu-item`);
+ items.push(item);
+ }
+ }
+
+ return items;
+ }
+
+}
+
+Component.registerComponent('TextTrackButton', TextTrackButton);
+export default TextTrackButton;
diff --git a/javascript/videojs/src/js/control-bar/text-track-controls/text-track-menu-item.js b/javascript/videojs/src/js/control-bar/text-track-controls/text-track-menu-item.js
new file mode 100644
index 0000000..757e18b
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/text-track-controls/text-track-menu-item.js
@@ -0,0 +1,182 @@
+/**
+ * @file text-track-menu-item.js
+ */
+import MenuItem from '../../menu/menu-item.js';
+import Component from '../../component.js';
+import window from 'global/window';
+import document from 'global/document';
+
+/** @import Player from '../../player' */
+
+/**
+ * The specific menu item type for selecting a language within a text track kind
+ *
+ * @extends MenuItem
+ */
+class TextTrackMenuItem extends MenuItem {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ */
+ constructor(player, options) {
+ const track = options.track;
+ const tracks = player.textTracks();
+
+ // Modify options for parent MenuItem class's init.
+ options.label = track.label || track.language || 'Unknown';
+ options.selected = track.mode === 'showing';
+
+ super(player, options);
+
+ this.track = track;
+ // Determine the relevant kind(s) of tracks for this component and filter
+ // out empty kinds.
+ this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean);
+
+ const changeHandler = (...args) => {
+ this.handleTracksChange.apply(this, args);
+ };
+ const selectedLanguageChangeHandler = (...args) => {
+ this.handleSelectedLanguageChange.apply(this, args);
+ };
+
+ player.on(['loadstart', 'texttrackchange'], changeHandler);
+ tracks.addEventListener('change', changeHandler);
+ tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
+ this.on('dispose', function() {
+ player.off(['loadstart', 'texttrackchange'], changeHandler);
+ tracks.removeEventListener('change', changeHandler);
+ tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
+ });
+
+ // iOS7 doesn't dispatch change events to TextTrackLists when an
+ // associated track's mode changes. Without something like
+ // Object.observe() (also not present on iOS7), it's not
+ // possible to detect changes to the mode attribute and polyfill
+ // the change event. As a poor substitute, we manually dispatch
+ // change events whenever the controls modify the mode.
+ if (tracks.onchange === undefined) {
+ let event;
+
+ this.on(['tap', 'click'], function() {
+ if (typeof window.Event !== 'object') {
+ // Android 2.3 throws an Illegal Constructor error for window.Event
+ try {
+ event = new window.Event('change');
+ } catch (err) {
+ // continue regardless of error
+ }
+ }
+
+ if (!event) {
+ event = document.createEvent('Event');
+ event.initEvent('change', true, true);
+ }
+
+ tracks.dispatchEvent(event);
+ });
+ }
+
+ // set the default state based on current tracks
+ this.handleTracksChange();
+ }
+
+ /**
+ * This gets called when an `TextTrackMenuItem` is "clicked". See
+ * {@link ClickableComponent} for more detailed information on what a click can be.
+ *
+ * @param {Event} event
+ * The `keydown`, `tap`, or `click` event that caused this function to be
+ * called.
+ *
+ * @listens tap
+ * @listens click
+ */
+ handleClick(event) {
+ const referenceTrack = this.track;
+ const tracks = this.player_.textTracks();
+
+ super.handleClick(event);
+
+ if (!tracks) {
+ return;
+ }
+
+ for (let i = 0; i < tracks.length; i++) {
+ const track = tracks[i];
+
+ // If the track from the text tracks list is not of the right kind,
+ // skip it. We do not want to affect tracks of incompatible kind(s).
+ if (this.kinds.indexOf(track.kind) === -1) {
+ continue;
+ }
+
+ // If this text track is the component's track and it is not showing,
+ // set it to showing.
+ if (track === referenceTrack) {
+ if (track.mode !== 'showing') {
+ track.mode = 'showing';
+ }
+
+ // If this text track is not the component's track and it is not
+ // disabled, set it to disabled.
+ } else if (track.mode !== 'disabled') {
+ track.mode = 'disabled';
+ }
+ }
+ }
+
+ /**
+ * Handle text track list change
+ *
+ * @param {Event} event
+ * The `change` event that caused this function to be called.
+ *
+ * @listens TextTrackList#change
+ */
+ handleTracksChange(event) {
+ const shouldBeSelected = this.track.mode === 'showing';
+
+ // Prevent redundant selected() calls because they may cause
+ // screen readers to read the appended control text unnecessarily
+ if (shouldBeSelected !== this.isSelected_) {
+ this.selected(shouldBeSelected);
+ }
+ }
+
+ handleSelectedLanguageChange(event) {
+ if (this.track.mode === 'showing') {
+ const selectedLanguage = this.player_.cache_.selectedLanguage;
+
+ // Don't replace the kind of track across the same language
+ if (selectedLanguage && selectedLanguage.enabled &&
+ selectedLanguage.language === this.track.language &&
+ selectedLanguage.kind !== this.track.kind) {
+ return;
+ }
+
+ this.player_.cache_.selectedLanguage = {
+ enabled: true,
+ language: this.track.language,
+ kind: this.track.kind
+ };
+ }
+ }
+
+ dispose() {
+ // remove reference to track object on dispose
+ this.track = null;
+
+ super.dispose();
+ }
+
+}
+
+Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
+export default TextTrackMenuItem;