diff options
Diffstat (limited to 'javascript/videojs/src/js/control-bar/text-track-controls')
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; |
