control-bar_text-track-controls_chapters-button.js

/**
 * @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';

/**
 * 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 { import('../../player').default } 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 { import('../../menu/menu').default }
   *         New menu for the chapter buttons
   */
  createMenu() {
    this.options_.title = this.getMenuCaption();
    return super.createMenu();
  }

  /**
   * Create a menu item for each text track
   *
   * @return  { import('./text-track-menu-item').default[] }
   *         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;