/**
 * @file tech.js
 */

import Component from '../component';
import mergeOptions from '../utils/merge-options.js';
import * as Fn from '../utils/fn.js';
import log from '../utils/log.js';
import { createTimeRange } from '../utils/time-ranges.js';
import { bufferedPercent } from '../utils/buffer.js';
import MediaError from '../media-error.js';
import window from 'global/window';
import document from 'global/document';
import {isPlain} from '../utils/obj';
import * as TRACK_TYPES from '../tracks/track-types';
import {toTitleCase, toLowerCase} from '../utils/string-cases.js';
import vtt from 'videojs-vtt.js';

/**
 * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
 * that just contains the src url alone.
 * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
   * `var SourceString = 'http://example.com/some-video.mp4';`
 *
 * @typedef {Object|string} Tech~SourceObject
 *
 * @property {string} src
 *           The url to the source
 *
 * @property {string} type
 *           The mime type of the source
 */

/**
 * A function used by {@link Tech} to create a new {@link TextTrack}.
 *
 * @private
 *
 * @param {Tech} self
 *        An instance of the Tech class.
 *
 * @param {string} kind
 *        `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
 *
 * @param {string} [label]
 *        Label to identify the text track
 *
 * @param {string} [language]
 *        Two letter language abbreviation
 *
 * @param {Object} [options={}]
 *        An object with additional text track options
 *
 * @return {TextTrack}
 *          The text track that was created.
 */
function createTrackHelper(self, kind, label, language, options = {}) {
  const tracks = self.textTracks();

  options.kind = kind;

  if (label) {
    options.label = label;
  }
  if (language) {
    options.language = language;
  }
  options.tech = self;

  const track = new TRACK_TYPES.ALL.text.TrackClass(options);

  tracks.addTrack(track);

  return track;
}

/**
 * This is the base class for media playback technology controllers, such as
 * {@link HTML5}
 *
 * @extends Component
 */
class Tech extends Component {

  /**
  * Create an instance of this Tech.
  *
  * @param {Object} [options]
  *        The key/value store of player options.
  *
  * @param {Component~ReadyCallback} ready
  *        Callback function to call when the `HTML5` Tech is ready.
  */
  constructor(options = {}, ready = function() {}) {
    // we don't want the tech to report user activity automatically.
    // This is done manually in addControlsListeners
    options.reportTouchActivity = false;
    super(null, options, ready);

    this.onDurationChange_ = (e) => this.onDurationChange(e);
    this.trackProgress_ = (e) => this.trackProgress(e);
    this.trackCurrentTime_ = (e) => this.trackCurrentTime(e);
    this.stopTrackingCurrentTime_ = (e) => this.stopTrackingCurrentTime(e);
    this.disposeSourceHandler_ = (e) => this.disposeSourceHandler(e);

    // keep track of whether the current source has played at all to
    // implement a very limited played()
    this.hasStarted_ = false;
    this.on('playing', function() {
      this.hasStarted_ = true;
    });
    this.on('loadstart', function() {
      this.hasStarted_ = false;
    });

    TRACK_TYPES.ALL.names.forEach((name) => {
      const props = TRACK_TYPES.ALL[name];

      if (options && options[props.getterName]) {
        this[props.privateName] = options[props.getterName];
      }
    });

    // Manually track progress in cases where the browser/tech doesn't report it.
    if (!this.featuresProgressEvents) {
      this.manualProgressOn();
    }

    // Manually track timeupdates in cases where the browser/tech doesn't report it.
    if (!this.featuresTimeupdateEvents) {
      this.manualTimeUpdatesOn();
    }

    ['Text', 'Audio', 'Video'].forEach((track) => {
      if (options[`native${track}Tracks`] === false) {
        this[`featuresNative${track}Tracks`] = false;
      }
    });

    if (options.nativeCaptions === false || options.nativeTextTracks === false) {
      this.featuresNativeTextTracks = false;
    } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
      this.featuresNativeTextTracks = true;
    }

    if (!this.featuresNativeTextTracks) {
      this.emulateTextTracks();
    }

    this.preloadTextTracks = options.preloadTextTracks !== false;

    this.autoRemoteTextTracks_ = new TRACK_TYPES.ALL.text.ListClass();

    this.initTrackListeners();

    // Turn on component tap events only if not using native controls
    if (!options.nativeControlsForTouch) {
      this.emitTapEvents();
    }

    if (this.constructor) {
      this.name_ = this.constructor.name || 'Unknown Tech';
    }
  }

  /**
   * A special function to trigger source set in a way that will allow player
   * to re-trigger if the player or tech are not ready yet.
   *
   * @fires Tech#sourceset
   * @param {string} src The source string at the time of the source changing.
   */
  triggerSourceset(src) {
    if (!this.isReady_) {
      // on initial ready we have to trigger source set
      // 1ms after ready so that player can watch for it.
      this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
    }

    /**
     * Fired when the source is set on the tech causing the media element
     * to reload.
     *
     * @see {@link Player#event:sourceset}
     * @event Tech#sourceset
     * @type {EventTarget~Event}
     */
    this.trigger({
      src,
      type: 'sourceset'
    });
  }

  /* Fallbacks for unsupported event types
  ================================================================================ */

  /**
   * Polyfill the `progress` event for browsers that don't support it natively.
   *
   * @see {@link Tech#trackProgress}
   */
  manualProgressOn() {
    this.on('durationchange', this.onDurationChange_);

    this.manualProgress = true;

    // Trigger progress watching when a source begins loading
    this.one('ready', this.trackProgress_);
  }

  /**
   * Turn off the polyfill for `progress` events that was created in
   * {@link Tech#manualProgressOn}
   */
  manualProgressOff() {
    this.manualProgress = false;
    this.stopTrackingProgress();

    this.off('durationchange', this.onDurationChange_);
  }

  /**
   * This is used to trigger a `progress` event when the buffered percent changes. It
   * sets an interval function that will be called every 500 milliseconds to check if the
   * buffer end percent has changed.
   *
   * > This function is called by {@link Tech#manualProgressOn}
   *
   * @param {EventTarget~Event} event
   *        The `ready` event that caused this to run.
   *
   * @listens Tech#ready
   * @fires Tech#progress
   */
  trackProgress(event) {
    this.stopTrackingProgress();
    this.progressInterval = this.setInterval(Fn.bind(this, function() {
      // Don't trigger unless buffered amount is greater than last time

      const numBufferedPercent = this.bufferedPercent();

      if (this.bufferedPercent_ !== numBufferedPercent) {
        /**
         * See {@link Player#progress}
         *
         * @event Tech#progress
         * @type {EventTarget~Event}
         */
        this.trigger('progress');
      }

      this.bufferedPercent_ = numBufferedPercent;

      if (numBufferedPercent === 1) {
        this.stopTrackingProgress();
      }
    }), 500);
  }

  /**
   * Update our internal duration on a `durationchange` event by calling
   * {@link Tech#duration}.
   *
   * @param {EventTarget~Event} event
   *        The `durationchange` event that caused this to run.
   *
   * @listens Tech#durationchange
   */
  onDurationChange(event) {
    this.duration_ = this.duration();
  }

  /**
   * Get and create a `TimeRange` object for buffering.
   *
   * @return {TimeRange}
   *         The time range object that was created.
   */
  buffered() {
    return createTimeRange(0, 0);
  }

  /**
   * Get the percentage of the current video that is currently buffered.
   *
   * @return {number}
   *         A number from 0 to 1 that represents the decimal percentage of the
   *         video that is buffered.
   *
   */
  bufferedPercent() {
    return bufferedPercent(this.buffered(), this.duration_);
  }

  /**
   * Turn off the polyfill for `progress` events that was created in
   * {@link Tech#manualProgressOn}
   * Stop manually tracking progress events by clearing the interval that was set in
   * {@link Tech#trackProgress}.
   */
  stopTrackingProgress() {
    this.clearInterval(this.progressInterval);
  }

  /**
   * Polyfill the `timeupdate` event for browsers that don't support it.
   *
   * @see {@link Tech#trackCurrentTime}
   */
  manualTimeUpdatesOn() {
    this.manualTimeUpdates = true;

    this.on('play', this.trackCurrentTime_);
    this.on('pause', this.stopTrackingCurrentTime_);
  }

  /**
   * Turn off the polyfill for `timeupdate` events that was created in
   * {@link Tech#manualTimeUpdatesOn}
   */
  manualTimeUpdatesOff() {
    this.manualTimeUpdates = false;
    this.stopTrackingCurrentTime();
    this.off('play', this.trackCurrentTime_);
    this.off('pause', this.stopTrackingCurrentTime_);
  }

  /**
   * Sets up an interval function to track current time and trigger `timeupdate` every
   * 250 milliseconds.
   *
   * @listens Tech#play
   * @triggers Tech#timeupdate
   */
  trackCurrentTime() {
    if (this.currentTimeInterval) {
      this.stopTrackingCurrentTime();
    }
    this.currentTimeInterval = this.setInterval(function() {
      /**
       * Triggered at an interval of 250ms to indicated that time is passing in the video.
       *
       * @event Tech#timeupdate
       * @type {EventTarget~Event}
       */
      this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });

    // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
    }, 250);
  }

  /**
   * Stop the interval function created in {@link Tech#trackCurrentTime} so that the
   * `timeupdate` event is no longer triggered.
   *
   * @listens {Tech#pause}
   */
  stopTrackingCurrentTime() {
    this.clearInterval(this.currentTimeInterval);

    // #1002 - if the video ends right before the next timeupdate would happen,
    // the progress bar won't make it all the way to the end
    this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
  }

  /**
   * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
   * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
   *
   * @fires Component#dispose
   */
  dispose() {

    // clear out all tracks because we can't reuse them between techs
    this.clearTracks(TRACK_TYPES.NORMAL.names);

    // Turn off any manual progress or timeupdate tracking
    if (this.manualProgress) {
      this.manualProgressOff();
    }

    if (this.manualTimeUpdates) {
      this.manualTimeUpdatesOff();
    }

    super.dispose();
  }

  /**
   * Clear out a single `TrackList` or an array of `TrackLists` given their names.
   *
   * > Note: Techs without source handlers should call this between sources for `video`
   *         & `audio` tracks. You don't want to use them between tracks!
   *
   * @param {string[]|string} types
   *        TrackList names to clear, valid names are `video`, `audio`, and
   *        `text`.
   */
  clearTracks(types) {
    types = [].concat(types);
    // clear out all tracks because we can't reuse them between techs
    types.forEach((type) => {
      const list = this[`${type}Tracks`]() || [];
      let i = list.length;

      while (i--) {
        const track = list[i];

        if (type === 'text') {
          this.removeRemoteTextTrack(track);
        }
        list.removeTrack(track);
      }
    });
  }

  /**
   * Remove any TextTracks added via addRemoteTextTrack that are
   * flagged for automatic garbage collection
   */
  cleanupAutoTextTracks() {
    const list = this.autoRemoteTextTracks_ || [];
    let i = list.length;

    while (i--) {
      const track = list[i];

      this.removeRemoteTextTrack(track);
    }
  }

  /**
   * Reset the tech, which will removes all sources and reset the internal readyState.
   *
   * @abstract
   */
  reset() {}

  /**
   * Get the value of `crossOrigin` from the tech.
   *
   * @abstract
   *
   * @see {Html5#crossOrigin}
   */
  crossOrigin() {}

  /**
   * Set the value of `crossOrigin` on the tech.
   *
   * @abstract
   *
   * @param {string} crossOrigin the crossOrigin value
   * @see {Html5#setCrossOrigin}
   */
  setCrossOrigin() {}

  /**
   * Get or set an error on the Tech.
   *
   * @param {MediaError} [err]
   *        Error to set on the Tech
   *
   * @return {MediaError|null}
   *         The current error object on the tech, or null if there isn't one.
   */
  error(err) {
    if (err !== undefined) {
      this.error_ = new MediaError(err);
      this.trigger('error');
    }
    return this.error_;
  }

  /**
   * Returns the `TimeRange`s that have been played through for the current source.
   *
   * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
   *         It only checks whether the source has played at all or not.
   *
   * @return {TimeRange}
   *         - A single time range if this video has played
   *         - An empty set of ranges if not.
   */
  played() {
    if (this.hasStarted_) {
      return createTimeRange(0, 0);
    }
    return createTimeRange();
  }

  /**
   * Start playback
   *
   * @abstract
   *
   * @see {Html5#play}
   */
  play() {}

  /**
   * Set whether we are scrubbing or not
   *
   * @abstract
   *
   * @see {Html5#setScrubbing}
   */
  setScrubbing() {}

  /**
   * Get whether we are scrubbing or not
   *
   * @abstract
   *
   * @see {Html5#scrubbing}
   */
  scrubbing() {}

  /**
   * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
   * previously called.
   *
   * @fires Tech#timeupdate
   */
  setCurrentTime() {
    // improve the accuracy of manual timeupdates
    if (this.manualTimeUpdates) {
      /**
       * A manual `timeupdate` event.
       *
       * @event Tech#timeupdate
       * @type {EventTarget~Event}
       */
      this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
    }
  }

  /**
   * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
   * {@link TextTrackList} events.
   *
   * This adds {@link EventTarget~EventListeners} for `addtrack`, and  `removetrack`.
   *
   * @fires Tech#audiotrackchange
   * @fires Tech#videotrackchange
   * @fires Tech#texttrackchange
   */
  initTrackListeners() {
    /**
      * Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
      *
      * @event Tech#audiotrackchange
      * @type {EventTarget~Event}
      */

    /**
      * Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
      *
      * @event Tech#videotrackchange
      * @type {EventTarget~Event}
      */

    /**
      * Triggered when tracks are added or removed on the Tech {@link TextTrackList}
      *
      * @event Tech#texttrackchange
      * @type {EventTarget~Event}
      */
    TRACK_TYPES.NORMAL.names.forEach((name) => {
      const props = TRACK_TYPES.NORMAL[name];
      const trackListChanges = () => {
        this.trigger(`${name}trackchange`);
      };

      const tracks = this[props.getterName]();

      tracks.addEventListener('removetrack', trackListChanges);
      tracks.addEventListener('addtrack', trackListChanges);

      this.on('dispose', () => {
        tracks.removeEventListener('removetrack', trackListChanges);
        tracks.removeEventListener('addtrack', trackListChanges);
      });
    });
  }

  /**
   * Emulate TextTracks using vtt.js if necessary
   *
   * @fires Tech#vttjsloaded
   * @fires Tech#vttjserror
   */
  addWebVttScript_() {
    if (window.WebVTT) {
      return;
    }

    // Initially, Tech.el_ is a child of a dummy-div wait until the Component system
    // signals that the Tech is ready at which point Tech.el_ is part of the DOM
    // before inserting the WebVTT script
    if (document.body.contains(this.el())) {

      // load via require if available and vtt.js script location was not passed in
      // as an option. novtt builds will turn the above require call into an empty object
      // which will cause this if check to always fail.
      if (!this.options_['vtt.js'] && isPlain(vtt) && Object.keys(vtt).length > 0) {
        this.trigger('vttjsloaded');
        return;
      }

      // load vtt.js via the script location option or the cdn of no location was
      // passed in
      const script = document.createElement('script');

      script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
      script.onload = () => {
        /**
         * Fired when vtt.js is loaded.
         *
         * @event Tech#vttjsloaded
         * @type {EventTarget~Event}
         */
        this.trigger('vttjsloaded');
      };
      script.onerror = () => {
        /**
         * Fired when vtt.js was not loaded due to an error
         *
         * @event Tech#vttjsloaded
         * @type {EventTarget~Event}
         */
        this.trigger('vttjserror');
      };
      this.on('dispose', () => {
        script.onload = null;
        script.onerror = null;
      });
      // but have not loaded yet and we set it to true before the inject so that
      // we don't overwrite the injected window.WebVTT if it loads right away
      window.WebVTT = true;
      this.el().parentNode.appendChild(script);
    } else {
      this.ready(this.addWebVttScript_);
    }

  }

  /**
   * Emulate texttracks
   *
   */
  emulateTextTracks() {
    const tracks = this.textTracks();
    const remoteTracks = this.remoteTextTracks();
    const handleAddTrack = (e) => tracks.addTrack(e.track);
    const handleRemoveTrack = (e) => tracks.removeTrack(e.track);

    remoteTracks.on('addtrack', handleAddTrack);
    remoteTracks.on('removetrack', handleRemoveTrack);

    this.addWebVttScript_();

    const updateDisplay = () => this.trigger('texttrackchange');

    const textTracksChanges = () => {
      updateDisplay();

      for (let i = 0; i < tracks.length; i++) {
        const track = tracks[i];

        track.removeEventListener('cuechange', updateDisplay);
        if (track.mode === 'showing') {
          track.addEventListener('cuechange', updateDisplay);
        }
      }
    };

    textTracksChanges();
    tracks.addEventListener('change', textTracksChanges);
    tracks.addEventListener('addtrack', textTracksChanges);
    tracks.addEventListener('removetrack', textTracksChanges);

    this.on('dispose', function() {
      remoteTracks.off('addtrack', handleAddTrack);
      remoteTracks.off('removetrack', handleRemoveTrack);
      tracks.removeEventListener('change', textTracksChanges);
      tracks.removeEventListener('addtrack', textTracksChanges);
      tracks.removeEventListener('removetrack', textTracksChanges);

      for (let i = 0; i < tracks.length; i++) {
        const track = tracks[i];

        track.removeEventListener('cuechange', updateDisplay);
      }
    });
  }

  /**
   * Create and returns a remote {@link TextTrack} object.
   *
   * @param {string} kind
   *        `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
   *
   * @param {string} [label]
   *        Label to identify the text track
   *
   * @param {string} [language]
   *        Two letter language abbreviation
   *
   * @return {TextTrack}
   *         The TextTrack that gets created.
   */
  addTextTrack(kind, label, language) {
    if (!kind) {
      throw new Error('TextTrack kind is required but was not provided');
    }

    return createTrackHelper(this, kind, label, language);
  }

  /**
   * Create an emulated TextTrack for use by addRemoteTextTrack
   *
   * This is intended to be overridden by classes that inherit from
   * Tech in order to create native or custom TextTracks.
   *
   * @param {Object} options
   *        The object should contain the options to initialize the TextTrack with.
   *
   * @param {string} [options.kind]
   *        `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
   *
   * @param {string} [options.label].
   *        Label to identify the text track
   *
   * @param {string} [options.language]
   *        Two letter language abbreviation.
   *
   * @return {HTMLTrackElement}
   *         The track element that gets created.
   */
  createRemoteTextTrack(options) {
    const track = mergeOptions(options, {
      tech: this
    });

    return new TRACK_TYPES.REMOTE.remoteTextEl.TrackClass(track);
  }

  /**
   * Creates a remote text track object and returns an html track element.
   *
   * > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
   *
   * @param {Object} options
   *        See {@link Tech#createRemoteTextTrack} for more detailed properties.
   *
   * @param {boolean} [manualCleanup=true]
   *        - When false: the TextTrack will be automatically removed from the video
   *          element whenever the source changes
   *        - When True: The TextTrack will have to be cleaned up manually
   *
   * @return {HTMLTrackElement}
   *         An Html Track Element.
   *
   * @deprecated The default functionality for this function will be equivalent
   *             to "manualCleanup=false" in the future. The manualCleanup parameter will
   *             also be removed.
   */
  addRemoteTextTrack(options = {}, manualCleanup) {
    const htmlTrackElement = this.createRemoteTextTrack(options);

    if (manualCleanup !== true && manualCleanup !== false) {
      // deprecation warning
      log.warn('Calling addRemoteTextTrack without explicitly setting the "manualCleanup" parameter to `true` is deprecated and default to `false` in future version of video.js');
      manualCleanup = true;
    }

    // store HTMLTrackElement and TextTrack to remote list
    this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
    this.remoteTextTracks().addTrack(htmlTrackElement.track);

    if (manualCleanup !== true) {
      // create the TextTrackList if it doesn't exist
      this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
    }

    return htmlTrackElement;
  }

  /**
   * Remove a remote text track from the remote `TextTrackList`.
   *
   * @param {TextTrack} track
   *        `TextTrack` to remove from the `TextTrackList`
   */
  removeRemoteTextTrack(track) {
    const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);

    // remove HTMLTrackElement and TextTrack from remote list
    this.remoteTextTrackEls().removeTrackElement_(trackElement);
    this.remoteTextTracks().removeTrack(track);
    this.autoRemoteTextTracks_.removeTrack(track);
  }

  /**
   * Gets available media playback quality metrics as specified by the W3C's Media
   * Playback Quality API.
   *
   * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
   *
   * @return {Object}
   *         An object with supported media playback quality metrics
   *
   * @abstract
   */
  getVideoPlaybackQuality() {
    return {};
  }

  /**
   * Attempt to create a floating video window always on top of other windows
   * so that users may continue consuming media while they interact with other
   * content sites, or applications on their device.
   *
   * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
   *
   * @return {Promise|undefined}
   *         A promise with a Picture-in-Picture window if the browser supports
   *         Promises (or one was passed in as an option). It returns undefined
   *         otherwise.
   *
   * @abstract
   */
  requestPictureInPicture() {
    const PromiseClass = this.options_.Promise || window.Promise;

    if (PromiseClass) {
      return PromiseClass.reject();
    }
  }

  /**
   * A method to check for the value of the 'disablePictureInPicture' <video> property.
   * Defaults to true, as it should be considered disabled if the tech does not support pip
   *
   * @abstract
   */
  disablePictureInPicture() {
    return true;
  }

  /**
   * A method to set or unset the 'disablePictureInPicture' <video> property.
   *
   * @abstract
   */
  setDisablePictureInPicture() {}

  /**
   * A method to set a poster from a `Tech`.
   *
   * @abstract
   */
  setPoster() {}

  /**
   * A method to check for the presence of the 'playsinline' <video> attribute.
   *
   * @abstract
   */
  playsinline() {}

  /**
   * A method to set or unset the 'playsinline' <video> attribute.
   *
   * @abstract
   */
  setPlaysinline() {}

  /**
   * Attempt to force override of native audio tracks.
   *
   * @param {boolean} override - If set to true native audio will be overridden,
   * otherwise native audio will potentially be used.
   *
   * @abstract
   */
  overrideNativeAudioTracks() {}

  /**
   * Attempt to force override of native video tracks.
   *
   * @param {boolean} override - If set to true native video will be overridden,
   * otherwise native video will potentially be used.
   *
   * @abstract
   */
  overrideNativeVideoTracks() {}

  /*
   * Check if the tech can support the given mime-type.
   *
   * The base tech does not support any type, but source handlers might
   * overwrite this.
   *
   * @param  {string} type
   *         The mimetype to check for support
   *
   * @return {string}
   *         'probably', 'maybe', or empty string
   *
   * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
   *
   * @abstract
   */
  canPlayType() {
    return '';
  }

  /**
   * Check if the type is supported by this tech.
   *
   * The base tech does not support any type, but source handlers might
   * overwrite this.
   *
   * @param {string} type
   *        The media type to check
   * @return {string} Returns the native video element's response
   */
  static canPlayType() {
    return '';
  }

  /**
   * Check if the tech can support the given source
   *
   * @param {Object} srcObj
   *        The source object
   * @param {Object} options
   *        The options passed to the tech
   * @return {string} 'probably', 'maybe', or '' (empty string)
   */
  static canPlaySource(srcObj, options) {
    return Tech.canPlayType(srcObj.type);
  }

  /*
   * Return whether the argument is a Tech or not.
   * Can be passed either a Class like `Html5` or a instance like `player.tech_`
   *
   * @param {Object} component
   *        The item to check
   *
   * @return {boolean}
   *         Whether it is a tech or not
   *         - True if it is a tech
   *         - False if it is not
   */
  static isTech(component) {
    return component.prototype instanceof Tech ||
           component instanceof Tech ||
           component === Tech;
  }

  /**
   * Registers a `Tech` into a shared list for videojs.
   *
   * @param {string} name
   *        Name of the `Tech` to register.
   *
   * @param {Object} tech
   *        The `Tech` class to register.
   */
  static registerTech(name, tech) {
    if (!Tech.techs_) {
      Tech.techs_ = {};
    }

    if (!Tech.isTech(tech)) {
      throw new Error(`Tech ${name} must be a Tech`);
    }

    if (!Tech.canPlayType) {
      throw new Error('Techs must have a static canPlayType method on them');
    }
    if (!Tech.canPlaySource) {
      throw new Error('Techs must have a static canPlaySource method on them');
    }

    name = toTitleCase(name);

    Tech.techs_[name] = tech;
    Tech.techs_[toLowerCase(name)] = tech;
    if (name !== 'Tech') {
      // camel case the techName for use in techOrder
      Tech.defaultTechOrder_.push(name);
    }
    return tech;
  }

  /**
   * Get a `Tech` from the shared list by name.
   *
   * @param {string} name
   *        `camelCase` or `TitleCase` name of the Tech to get
   *
   * @return {Tech|undefined}
   *         The `Tech` or undefined if there was no tech with the name requested.
   */
  static getTech(name) {
    if (!name) {
      return;
    }

    if (Tech.techs_ && Tech.techs_[name]) {
      return Tech.techs_[name];
    }

    name = toTitleCase(name);

    if (window && window.videojs && window.videojs[name]) {
      log.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`);
      return window.videojs[name];
    }
  }
}

/**
 * Get the {@link VideoTrackList}
 *
 * @returns {VideoTrackList}
 * @method Tech.prototype.videoTracks
 */

/**
 * Get the {@link AudioTrackList}
 *
 * @returns {AudioTrackList}
 * @method Tech.prototype.audioTracks
 */

/**
 * Get the {@link TextTrackList}
 *
 * @returns {TextTrackList}
 * @method Tech.prototype.textTracks
 */

/**
 * Get the remote element {@link TextTrackList}
 *
 * @returns {TextTrackList}
 * @method Tech.prototype.remoteTextTracks
 */

/**
 * Get the remote element {@link HtmlTrackElementList}
 *
 * @returns {HtmlTrackElementList}
 * @method Tech.prototype.remoteTextTrackEls
 */

TRACK_TYPES.ALL.names.forEach(function(name) {
  const props = TRACK_TYPES.ALL[name];

  Tech.prototype[props.getterName] = function() {
    this[props.privateName] = this[props.privateName] || new props.ListClass();
    return this[props.privateName];
  };
});

/**
 * List of associated text tracks
 *
 * @type {TextTrackList}
 * @private
 * @property Tech#textTracks_
 */

/**
 * List of associated audio tracks.
 *
 * @type {AudioTrackList}
 * @private
 * @property Tech#audioTracks_
 */

/**
 * List of associated video tracks.
 *
 * @type {VideoTrackList}
 * @private
 * @property Tech#videoTracks_
 */

/**
 * Boolean indicating whether the `Tech` supports volume control.
 *
 * @type {boolean}
 * @default
 */
Tech.prototype.featuresVolumeControl = true;

/**
 * Boolean indicating whether the `Tech` supports muting volume.
 *
 * @type {bolean}
 * @default
 */
Tech.prototype.featuresMuteControl = true;

/**
 * Boolean indicating whether the `Tech` supports fullscreen resize control.
 * Resizing plugins using request fullscreen reloads the plugin
 *
 * @type {boolean}
 * @default
 */
Tech.prototype.featuresFullscreenResize = false;

/**
 * Boolean indicating whether the `Tech` supports changing the speed at which the video
 * plays. Examples:
 *   - Set player to play 2x (twice) as fast
 *   - Set player to play 0.5x (half) as fast
 *
 * @type {boolean}
 * @default
 */
Tech.prototype.featuresPlaybackRate = false;

/**
 * Boolean indicating whether the `Tech` supports the `progress` event. This is currently
 * not triggered by video-js-swf. This will be used to determine if
 * {@link Tech#manualProgressOn} should be called.
 *
 * @type {boolean}
 * @default
 */
Tech.prototype.featuresProgressEvents = false;

/**
 * Boolean indicating whether the `Tech` supports the `sourceset` event.
 *
 * A tech should set this to `true` and then use {@link Tech#triggerSourceset}
 * to trigger a {@link Tech#event:sourceset} at the earliest time after getting
 * a new source.
 *
 * @type {boolean}
 * @default
 */
Tech.prototype.featuresSourceset = false;

/**
 * Boolean indicating whether the `Tech` supports the `timeupdate` event. This is currently
 * not triggered by video-js-swf. This will be used to determine if
 * {@link Tech#manualTimeUpdates} should be called.
 *
 * @type {boolean}
 * @default
 */
Tech.prototype.featuresTimeupdateEvents = false;

/**
 * Boolean indicating whether the `Tech` supports the native `TextTrack`s.
 * This will help us integrate with native `TextTrack`s if the browser supports them.
 *
 * @type {boolean}
 * @default
 */
Tech.prototype.featuresNativeTextTracks = false;

/**
 * A functional mixin for techs that want to use the Source Handler pattern.
 * Source handlers are scripts for handling specific formats.
 * The source handler pattern is used for adaptive formats (HLS, DASH) that
 * manually load video data and feed it into a Source Buffer (Media Source Extensions)
 * Example: `Tech.withSourceHandlers.call(MyTech);`
 *
 * @param {Tech} _Tech
 *        The tech to add source handler functions to.
 *
 * @mixes Tech~SourceHandlerAdditions
 */
Tech.withSourceHandlers = function(_Tech) {

  /**
   * Register a source handler
   *
   * @param {Function} handler
   *        The source handler class
   *
   * @param {number} [index]
   *        Register it at the following index
   */
  _Tech.registerSourceHandler = function(handler, index) {
    let handlers = _Tech.sourceHandlers;

    if (!handlers) {
      handlers = _Tech.sourceHandlers = [];
    }

    if (index === undefined) {
      // add to the end of the list
      index = handlers.length;
    }

    handlers.splice(index, 0, handler);
  };

  /**
   * Check if the tech can support the given type. Also checks the
   * Techs sourceHandlers.
   *
   * @param {string} type
   *         The mimetype to check.
   *
   * @return {string}
   *         'probably', 'maybe', or '' (empty string)
   */
  _Tech.canPlayType = function(type) {
    const handlers = _Tech.sourceHandlers || [];
    let can;

    for (let i = 0; i < handlers.length; i++) {
      can = handlers[i].canPlayType(type);

      if (can) {
        return can;
      }
    }

    return '';
  };

  /**
   * Returns the first source handler that supports the source.
   *
   * TODO: Answer question: should 'probably' be prioritized over 'maybe'
   *
   * @param {Tech~SourceObject} source
   *        The source object
   *
   * @param {Object} options
   *        The options passed to the tech
   *
   * @return {SourceHandler|null}
   *          The first source handler that supports the source or null if
   *          no SourceHandler supports the source
   */
  _Tech.selectSourceHandler = function(source, options) {
    const handlers = _Tech.sourceHandlers || [];
    let can;

    for (let i = 0; i < handlers.length; i++) {
      can = handlers[i].canHandleSource(source, options);

      if (can) {
        return handlers[i];
      }
    }

    return null;
  };

  /**
   * Check if the tech can support the given source.
   *
   * @param {Tech~SourceObject} srcObj
   *        The source object
   *
   * @param {Object} options
   *        The options passed to the tech
   *
   * @return {string}
   *         'probably', 'maybe', or '' (empty string)
   */
  _Tech.canPlaySource = function(srcObj, options) {
    const sh = _Tech.selectSourceHandler(srcObj, options);

    if (sh) {
      return sh.canHandleSource(srcObj, options);
    }

    return '';
  };

  /**
   * When using a source handler, prefer its implementation of
   * any function normally provided by the tech.
   */
  const deferrable = [
    'seekable',
    'seeking',
    'duration'
  ];

  /**
   * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable
   * function if it exists, with a fallback to the Techs seekable function.
   *
   * @method _Tech.seekable
   */

  /**
   * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration
   * function if it exists, otherwise it will fallback to the techs duration function.
   *
   * @method _Tech.duration
   */

  deferrable.forEach(function(fnName) {
    const originalFn = this[fnName];

    if (typeof originalFn !== 'function') {
      return;
    }

    this[fnName] = function() {
      if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
        return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
      }
      return originalFn.apply(this, arguments);
    };
  }, _Tech.prototype);

  /**
   * Create a function for setting the source using a source object
   * and source handlers.
   * Should never be called unless a source handler was found.
   *
   * @param {Tech~SourceObject} source
   *        A source object with src and type keys
   */
  _Tech.prototype.setSource = function(source) {
    let sh = _Tech.selectSourceHandler(source, this.options_);

    if (!sh) {
      // Fall back to a native source hander when unsupported sources are
      // deliberately set
      if (_Tech.nativeSourceHandler) {
        sh = _Tech.nativeSourceHandler;
      } else {
        log.error('No source handler found for the current source.');
      }
    }

    // Dispose any existing source handler
    this.disposeSourceHandler();
    this.off('dispose', this.disposeSourceHandler_);

    if (sh !== _Tech.nativeSourceHandler) {
      this.currentSource_ = source;
    }

    this.sourceHandler_ = sh.handleSource(source, this, this.options_);
    this.one('dispose', this.disposeSourceHandler_);
  };

  /**
   * Clean up any existing SourceHandlers and listeners when the Tech is disposed.
   *
   * @listens Tech#dispose
   */
  _Tech.prototype.disposeSourceHandler = function() {
    // if we have a source and get another one
    // then we are loading something new
    // than clear all of our current tracks
    if (this.currentSource_) {
      this.clearTracks(['audio', 'video']);
      this.currentSource_ = null;
    }

    // always clean up auto-text tracks
    this.cleanupAutoTextTracks();

    if (this.sourceHandler_) {

      if (this.sourceHandler_.dispose) {
        this.sourceHandler_.dispose();
      }

      this.sourceHandler_ = null;
    }
  };

};

// The base Tech class needs to be registered as a Component. It is the only
// Tech that can be registered as a Component.
Component.registerComponent('Tech', Tech);
Tech.registerTech('Tech', Tech);

/**
 * A list of techs that should be added to techOrder on Players
 *
 * @private
 */
Tech.defaultTechOrder_ = [];

export default Tech;


.video-js.vjs-4-3 {
@include apply-aspect-ratio(4, 3);
}
.video-js.vjs-fill {
width: 100%;
height: 100%;

.video-js.vjs-9-16 {
@include apply-aspect-ratio(9, 16);
}
.video-js.vjs-1-1 {
@include apply-aspect-ratio(1, 1);
}

# Layout

Video.js generally lays out the player to the dimensions that are set as attributes or via CSS, like other DOM elements. However, we provide a few ways to make the player be more fluid.

## Fluid Mode

Video.js has a fluid mode that keeps the player sized to a particular aspect ratio.

By default, fluid mode will use the intrinsic size of the video once loaded but you can change it with classes or with the `aspectRatio` option.

Enabling fluid mode will disable fill mode. If both are enabled, fluid mode takes precedence.

You can enable fluid in a few ways:

* Add `vjs-fluid`, `vjs-16-9`, or `vjs-4-3` as a class to the player element.
* pass `fluid` option to the player.
* call `player.fluid(true)`.
* pass `aspectRatio` option to the player.
* call `player.aspectRatio('16:9')`.

### Classes

There are three classes associated with fluid mode, `vjs-fluid`, `vjs-16-9`, and `vjs-4-3`.

`vjs-fluid` turns on the general fluid mode which will wait for the video to load to calculate the aspect ratio of the video.

Alternatively, because 16:9 and 4:3 aspect ratios are so common, we provided them as classes by default for you to use if you know that your videos are 16:9 or 4:3.

### Enabling Fluid Mode

You can pass in the `fluid` option to the player or call `player.fluid(true)`. This will enable the generic fluid mode.

```js
var player = videojs('vid1', {
  fluid: true
});
```

```js
var player = videojs('vid2');

player.fluid(true);
```

### Setting Aspect Ratio

You can specify an aspect ratio for us to use if you don't want to use the intrinsic values from the video element or if you have a specific ratio in mind. It works as either a method call or an option to the player.

This option is in the form of two integers separated by a colon like so `16:9` or `4:3`.

```js
// make a vertical video
var player = videojs('vid1', {
  aspectRatio: '9:16'
});
```

```js
var player = videojs('vid2');

// make a square video
player.aspectRatio('1:1');
```

### Disabling Fluid Mode

You can disable fluid mode by remove the associated classes or by calling passing in `false` to the method.

```js
player.fluid(false);
```

## Fill Mode

Fill mode will make the player fit and fill out its container. This is often useful if you have a responsive website and already have a container for Video.js that resizes properly to your design. It can be set either via a class or an option.

If fill is enabled, it'll turn off fluid mode. If the player is configured with both fluid and fill options, fluid mode takes precedence.

### Class

There's just one class for this one: `vjs-fill`. When available, Video.js will enter fill mode.

### Enabling Fill Mode

You can pass in the `fill` option to the player or call `player.fill(true)`. This will enable fill mode.

```js
var player = videojs('vid1', {
  fill: true
});
```

```js
var player = videojs('vid2');

player.fill(true);
```

### Disabling Fill Mode

You can disable fill mode by removing the associated class or by passing `false` in to the method.

```js
player.fill(false);
```

## Responsive Mode

Responsive mode will make the player's UI customize itself based on the size of the player. This is useful if you have embeds of varying sizes - or if you want a fluid/fill player to adjust its UI based on its size.

Responsive mode is independent of fluid mode or fill mode - it only deals with the arrangements of the UI within the player, not with the size of the player. However, it is often useful to use responsive mode in conjunction with either fluid mode or fill mode!

### Class

A player in responsive mode will add and remove classes based on its size breakpoints. The default breakpoints, classes, and sizes are outlined below:

| Name     | Class                | Min. Width | Max. Width |
| -------- | -------------------- | ---------- | ---------- |
| `tiny`   | `vjs-layout-tiny`    | 0          | 210        |
| `xsmall` | `vjs-layout-x-small` | 211        | 320        |
| `small`  | `vjs-layout-small`   | 321        | 425        |
| `medium` | `vjs-layout-medium`  | 426        | 768        |
| `large`  | `vjs-layout-large`   | 769        | 1440       |
| `xlarge` | `vjs-layout-x-large` | 1441       | 2560       |
| `huge`   | `vjs-layout-huge`    | 2561       | Infinity   |

### Enabling Responsive Mode

You can enable responsive mode by passing the `responsive` option or by calling `player.responsive(true)`.

```js
var player = videojs('vid1', {
  responsive: true
});
```

```js
var player = videojs('vid2');

player.responsive(true);
```

### Disabling Responsive Mode

You can disable responsive mode by passing `false` to the method.

```js
player.responsive(false);
```

### Customizing Breakpoints

The default breakpoints can be customized by passing the `breakpoints` option or by calling `player.breakpoints({...})`.

```js
var player = videojs('vid1', {
  breakpoints: {
    medium: 500
  }
});
```

```js
var player = videojs('vid2');

player.breakpoints({
  medium: 500
});
```

The breakpoints object should have keys matching the **Name** from the table above and values matching the **Max. Width** from the table above. The **Min. Width** is calculated by adding one to the previous breakpoint's **Max. Width**.

Anytime breakpoints are customized, previous customizations are discarded.

### Restoring Default Breakpoints

The default breakpoints can be restored by calling `player.breakpoints(true)`.

```js
var player = videojs('vid1');

player.breakpoints(true);
```

This is only useful if breakpoints had previously been customized.


<!DOCTYPE html><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>Slider</title><link href="http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700" rel="stylesheet" type="text/css"/> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/atelier-forest.light.min.css"/><link rel="stylesheet" type="text/css" href="css/api-docs.css"/><link rel="canonical" href="https://docs.videojs.com/Slider.html">

This documentation is for an outdated version of Video.js. See documentation for the current release.

API Index

Slider Methods

Class Methods

calculateDistance

createEl

handleBlur

handleClick

handleFocus

handleKeyPress

handleMouseDown

handleMouseMove

handleMouseUp

update

vertical

Inherited Methods from Component

$

$$

addChild

addClass

buildCSSClass

children

clearInterval

clearTimeout

contentEl

dimensions

dispose

el

enableTouchActivity

extend

getChild

getChildById

getComponent

hasClass

height

hide

id

initChildren

name

off

on

one

options

player

ready

registerComponent

removeChild

removeClass

setInterval

setTimeout

show

toggleClass

trigger

triggerReady

width

Slider

The base functionality for sliders like the volume bar and seek bar

DEFINED IN: slider.js line number: 9

EXTENDS: component.js

Constructor

Slider( player,[options] )

Parameters

name	Type	Required	Description

player	Player|Object

options	Object

Methods

calculateDistance( event )

Calculate distance for slider

Parameters

name	Type	Required	Description

event	Object	yes	Event object

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 149

[back to top]

createEl( type, [props] )

Create the component's DOM element

Parameters

name	Type	Required	Description

type	String	yes	Type of element to create

props	Object	no	List of properties in Object form

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 38

[back to top]

handleBlur()

Handle on blur for slider

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 188

[back to top]

handleClick( event )

Listener for click events on slider, used to prevent clicks
  from bubbling up to parent elements like button menus.

Parameters

name	Type	Required	Description

event	Object	yes	Event object

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 197

[back to top]

handleFocus()

Handle on focus for slider

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 163

[back to top]

handleKeyPress( event )

Handle key press for slider

Parameters

name	Type	Required	Description

event	Object	yes	Event object

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 172

[back to top]

handleMouseDown( event )

Handle mouse down on slider

Parameters

name	Type	Required	Description

event	Object	yes	Mouse down event object

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 64

[back to top]

handleMouseMove()

To be overridden by a subclass

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 85

[back to top]

handleMouseUp()

Handle mouse up on Slider

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 92

[back to top]

update()

Update slider

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 111

[back to top]

vertical( bool )

Get/set if slider is horizontal for vertical

Parameters

name	Type	Required	Description

bool	Boolean	yes	True if slider is vertical, false is horizontal

Defined in https://github.com/videojs/video.js/blob/master/src/js/slider/slider.js line number: 209

[back to top]

$( selector, [context] )

Finds a single DOM element matching selector within the component's
contentEl or another custom context.

Parameters

name	Type	Required	Description

selector	String	yes	A valid CSS selector, which will be passed to querySelector.

context	Element|String	no	A DOM element within which to query. Can also be a selector
        string in which case the first matching element will be used
        as context. If missing (or no element matches selector), falls
        back to document.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 841

[back to top]

$$( selector, [context] )

Finds a all DOM elements matching selector within the component's
contentEl or another custom context.

Parameters

name	Type	Required	Description

selector	String	yes	A valid CSS selector, which will be passed to querySelectorAll.

context	Element|String	no	A DOM element within which to query. Can also be a selector
        string in which case the first matching element will be used
        as context. If missing (or no element matches selector), falls
        back to document.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 861

[back to top]

addChild( child, [options] )

Adds a child component inside this component

    myComponent.el();
    // ->

    myComponent.children();
    // [empty array]

    var myButton = myComponent.addChild('MyButton');
    // ->

myButton

    // -> myButton === myComponent.children()[0];

Pass in options for child constructors and options for children of the child

    var myButton = myComponent.addChild('MyButton', {
      text: 'Press Me',
      buttonChildExample: {
        buttonChildOption: true
      }
    });

Parameters

name	Type	Required	Description

child	String|Component	yes	The class name or instance of a child to add

options	Object	no	Options, including options to be passed to children of the child.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 315

[back to top]

addClass( classToAdd )

Add a CSS class name to the component's element

Parameters

name	Type	Required	Description

classToAdd	String	yes	Classname to add

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 892

[back to top]

buildCSSClass()

Allows sub components to stack CSS class names

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 593

[back to top]

children()

Get an array of all child components

    var kids = myComponent.children();

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 282

[back to top]

clearInterval( intervalId )

Clears an interval and removes the associated dispose listener

Parameters

name	Type	Required	Description

intervalId	Number	yes	The id of the interval to clear

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1295

[back to top]

clearTimeout( timeoutId )

Clears a timeout and removes the associated dispose listener

Parameters

name	Type	Required	Description

timeoutId	Number	yes	The id of the timeout to clear

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1252

[back to top]

contentEl()

Return the component's DOM element where children are inserted.
Will either be the same as el() or a new element defined in createEl().

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 245

[back to top]

dimensions( width, height )

Set both width and height at the same time

Parameters

name	Type	Required	Description

width	Number|String	yes	Width of player

height	Number|String	yes	Height of player

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1015

[back to top]

dispose()

Dispose of the component and all child components

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 101

[back to top]

el()

Get the component's DOM element

    var domEl = myComponent.el();

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 195

[back to top]

enableTouchActivity()

Report user touch activity when touch events occur
User activity is used to determine when controls should show/hide. It's
relatively simple when it comes to mouse events, because any mouse event
should show the controls. So we capture mouse events that bubble up to the
player and report activity when that happens.
With touch events it isn't as easy. We can't rely on touch events at the
player level, because a tap (touchstart + touchend) on the video itself on
mobile devices is meant to turn controls off (and on). User activity is
checked asynchronously, so what could happen is a tap event on the video
turns the controls off, then the touchend event bubbles up to the player,
which if it reported user activity, would turn the controls right back on.
(We also don't want to completely block touch events from bubbling up)
Also a touchmove, touch+hold, and anything other than a tap is not supposed
to turn the controls back on on a mobile device.
Here we're setting the default component behavior to report user activity
whenever touch events happen, and this can be turned off by components that
want touch events to act differently.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1174

[back to top]

static extend( props ) (deprecated)

Sets up the constructor using the supplied init method
or uses the init of the parent object

Parameters

name	Type	Required	Description

props	Object	yes	An object of properties

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1350

[back to top]

getChild()

Returns a child component with the provided name

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 305

[back to top]

getChildById()

Returns a child component with the provided ID

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 295

[back to top]

static getComponent( name )

Gets a component by name

Parameters

name	Type	Required	Description

name	String	yes	Name of the component to get

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1331

[back to top]

hasClass( classToCheck )

Check if a component's element has a CSS class name

Parameters

name	Type	Required	Description

classToCheck	String	yes	Classname to check

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 881

[back to top]

height( num, [skipListeners] )

Get or set the height of the component (CSS values)
Setting the video tag dimension values only works with values in pixels.
Percent values will not work.
Some percents can be used, but width()/height() will return the number + %,
not the actual computed width/height.

Parameters

name	Type	Required	Description

num	Number|String	yes	New component height

skipListeners	Boolean	no	Skip the resize event trigger

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 998

[back to top]

hide()

Hide the component element if currently showing

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 944

[back to top]

id()

Get the component's ID

    var id = myComponent.id();

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 256

[back to top]

initChildren()

Add and initialize default child components from options

    // when an instance of MyComponent is created, all children in options
    // will be added to the instance by their name strings and options
    MyComponent.prototype.options_ = {
      children: [
        'myChildComponent'
      ],
      myChildComponent: {
        myChildOption: true
      }
    };

    // Or when creating the component
    var myComp = new MyComponent(player, {
      children: [
        'myChildComponent'
      ],
      myChildComponent: {
        myChildOption: true
      }
    });

The children option can also be an array of
child options objects (that also include a 'name' key).
This can be used if you have two child components of the
same type that need different options.

    var myComp = new MyComponent(player, {
      children: [
        'button',
        {
          name: 'button',
          someOtherOption: true
        },
        {
          name: 'button',
          someOtherOption: false
        }
      ]
    });

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 455

[back to top]

name()

Get the component's name. The name is often used to reference the component.

    var name = myComponent.name();

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 269

[back to top]

off( first, second, [third] )

Remove an event listener from this component's element

    myComponent.off('eventType', myFunc);

If myFunc is excluded, ALL listeners for the event type will be removed.
If eventType is excluded, ALL listeners will be removed from the component.
Alternatively you can use off to remove listeners that were added to other
elements or components using myComponent.on(otherComponent....
In this case both the event type and listener function are REQUIRED.

    myComponent.off(otherElement, 'eventType', myFunc);
    myComponent.off(otherComponent, 'eventType', myFunc);

Parameters

name	Type	Required	Description

first	String|Component	yes	The event type or other component

second	function|String	yes	The listener function or event type

third	function	no	The listener for other component

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 680

[back to top]

on( first, second, third )

Add an event listener to this component's element

    var myFunc = function(){
      var myComponent = this;
      // Do something when the event is fired
    };

    myComponent.on('eventType', myFunc);

The context of myFunc will be myComponent unless previously bound.
Alternatively, you can add a listener to another element or component.

    myComponent.on(otherElement, 'eventName', myFunc);
    myComponent.on(otherComponent, 'eventName', myFunc);

The benefit of using this over VjsEvents.on(otherElement, 'eventName', myFunc)
and otherComponent.on('eventName', myFunc) is that this way the listeners
will be automatically cleaned up when either component is disposed.
It will also bind myComponent as the context of myFunc.
NOTE: When using this on elements in the page other than window
and document (both permanent), if you remove the element from the DOM
you need to call myComponent.trigger(el, 'dispose') on it to clean up
references to it and allow the browser to garbage collect it.

Parameters

name	Type	Required	Description

first	String|Component	yes	The event type or other component

second	function|String	yes	The event handler or event type

third	function	yes	The event handler

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 605

[back to top]

one( first, second, [third] )

Add an event listener to be triggered only once and then removed

    myComponent.one('eventName', myFunc);

Alternatively you can add a listener to another element or component
that will be triggered only once.

    myComponent.one(otherElement, 'eventName', myFunc);
    myComponent.one(otherComponent, 'eventName', myFunc);

Parameters

name	Type	Required	Description

first	String|Component	yes	The event type or other component

second	function|String	yes	The listener function or event type

third	function	no	The listener function for other component

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 728

[back to top]

options( obj )

Deep merge of options objects
Whenever a property is an object on both options objects
the two properties will be merged using mergeOptions.

    Parent.prototype.options_ = {
      optionSet: {
        'childOne': { 'foo': 'bar', 'asdf': 'fdsa' },
        'childTwo': {},
        'childThree': {}
      }
    }
    newOptions = {
      optionSet: {
        'childOne': { 'foo': 'baz', 'abc': '123' }
        'childTwo': null,
        'childFour': {}
      }
    }

    this.options(newOptions);

RESULT

    {
      optionSet: {
        'childOne': { 'foo': 'baz', 'asdf': 'fdsa', 'abc': '123' },
        'childTwo': null, // Disabled. Won't be initialized.
        'childThree': {},
        'childFour': {}
      }
    }

Parameters

name	Type	Required	Description

obj	Object	yes	Object of new option values

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 145

[back to top]

player()

Return the component's player

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 135

[back to top]

ready( fn, sync )

Bind a listener to the component's ready state.
Different from event listeners in that if the ready event has already happened
it will trigger the function immediately.

Parameters

name	Type	Required	Description

fn	function	yes	Ready listener

sync	Boolean	yes	Exec the listener synchronously if component is ready

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 787

[back to top]

static registerComponent( name, comp )

Registers a component

Parameters

name	Type	Required	Description

name	String	yes	Name of the component to register

comp	Object	yes	The component to register

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1314

[back to top]

removeChild( component )

Remove a child component from this component's list of children, and the
child component's element from this component's element

Parameters

name	Type	Required	Description

component	Component	yes	Component to remove

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 415

[back to top]

removeClass( classToRemove )

Remove a CSS class name from the component's element

Parameters

name	Type	Required	Description

classToRemove	String	yes	Classname to remove

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 904

[back to top]

setInterval( fn, interval )

Creates an interval and sets up disposal automatically.

Parameters

name	Type	Required	Description

fn	function	yes	The function to run every N seconds.

interval	Number	yes	Number of ms to delay before executing specified function.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1271

[back to top]

setTimeout( fn, timeout )

Creates timeout and sets up disposal automatically.

Parameters

name	Type	Required	Description

fn	function	yes	The function to run after the timeout.

timeout	Number	yes	Number of ms to delay before executing specified function.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1227

[back to top]

show()

Show the component element if hidden

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 933

[back to top]

toggleClass( classToToggle, [predicate] )

Add or remove a CSS class name from the component's element

Parameters

name	Type	Required	Description

classToToggle	String

predicate	Boolean|function	no	Can be a function that returns a Boolean. If true, the class
        will be added; if false, the class will be removed. If not
        given, the class will be added if not present and vice versa.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 916

[back to top]

trigger( event, [hash] )

Trigger an event on an element

    myComponent.trigger('eventName');
    myComponent.trigger({'type':'eventName'});
    myComponent.trigger('eventName', {data: 'some data'});
    myComponent.trigger({'type':'eventName'}, {data: 'some data'});

Parameters

name	Type	Required	Description

event	Event|Object|String	yes	A string (the type) or an event object with a type attribute

hash	Object	no	data hash to pass along with the event

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 768

[back to top]

triggerReady()

Trigger the ready listeners

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 814

[back to top]

width( num, skipListeners )

Set or get the width of the component (CSS values)
Setting the video tag dimension values only works with values in pixels.
Percent values will not work.
Some percents can be used, but width()/height() will return the number + %,
not the actual computed width/height.

Parameters

name	Type	Required	Description

num	Number|String	yes	Optional width number

skipListeners	Boolean	yes	Skip the 'resize' event trigger

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 981

[back to top]


<!DOCTYPE html>

        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title></title>

        <link href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700' rel='stylesheet' type='text/css'>

        <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/atelier-forest.light.min.css">
        <link rel="stylesheet" type="text/css" href="css/api-docs.css">

    <link rel="canonical" href="https://docs.videojs.com">

This documentation is for an outdated version of Video.js. See documentation for the current release.


/* eslint-env qunit */
import HTMLTrackElement from '../../../src/js/tracks/html-track-element.js';
import TechFaker from '../tech/tech-faker';

QUnit.module('HTML Track Element', {
  beforeEach() {
    this.tech = new TechFaker();
  },
  afterEach() {
    this.tech.dispose();
    this.tech = null;
  }
});

QUnit.test('html track element requires a tech', function(assert) {
  assert.throws(
    function() {
      return new HTMLTrackElement();
    },
    new Error('A tech was not provided.'),
    'a tech is required for html track element'
  );
});

QUnit.test('can create a html track element with various properties', function(assert) {
  const kind = 'chapters';
  const label = 'English';
  const language = 'en';

  const htmlTrackElement = new HTMLTrackElement({
    kind,
    label,
    language,
    tech: this.tech
  });

  assert.equal(typeof htmlTrackElement.default, 'undefined', 'we have a default');
  assert.equal(htmlTrackElement.kind, kind, 'we have a kind');
  assert.equal(htmlTrackElement.label, label, 'we have a label');
  assert.equal(htmlTrackElement.readyState, 0, 'we have a readyState');
  assert.equal(htmlTrackElement.srclang, language, 'we have a srclang');

  htmlTrackElement.track.off();
});

QUnit.test('defaults when items not provided', function(assert) {
  const htmlTrackElement = new HTMLTrackElement({
    tech: this.tech
  });

  assert.equal(typeof htmlTrackElement.default, 'undefined', 'we have a default');
  assert.equal(htmlTrackElement.kind, 'subtitles', 'we have a kind');
  assert.equal(htmlTrackElement.label, '', 'we have a label');
  assert.equal(htmlTrackElement.readyState, 0, 'we have a readyState');
  assert.equal(typeof htmlTrackElement.src, 'undefined', 'we have a src');
  assert.equal(htmlTrackElement.srclang, '', 'we have a srclang');
  assert.equal(htmlTrackElement.track.cues.length, 0, 'we have a track');

  htmlTrackElement.track.off();
});

QUnit.test('fires loadeddata when track cues become populated', function(assert) {
  let changes = 0;
  const loadHandler = function() {
    changes++;
  };
  const htmlTrackElement = new HTMLTrackElement({
    tech: this.tech
  });

  htmlTrackElement.addEventListener('load', loadHandler);

  // trigger loaded cues event
  htmlTrackElement.track.trigger('loadeddata');

  assert.equal(changes, 1, 'a loadeddata event trigger addEventListener');
  assert.equal(htmlTrackElement.readyState, 2, 'readyState is loaded');

  htmlTrackElement.track.off();
  htmlTrackElement.off('load');
});


keep keep keep keep add keep keep keep keep

import type { IRuleResult } from '@stoplight/spectral-core';
import { generate, runTests, Test } from 'insomnia-testing';
import path from 'path';
import { ActionFunction, redirect } from 'react-router-dom';

import * as session from '../../account/session';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../../common/constants';
import { database } from '../../common/database';
import { importResources, scanResources } from '../../common/import';
import * as models from '../../models';
import * as workspaceOperations from '../../models/helpers/workspace-operations';
import { DEFAULT_ORGANIZATION_ID } from '../../models/organization';
import { DEFAULT_PROJECT_ID, isRemoteProject } from '../../models/project';
import { UnitTest } from '../../models/unit-test';
import { isCollection } from '../../models/workspace';
import { getSendRequestCallback } from '../../network/unit-test-feature';
import { initializeLocalBackendProjectAndMarkForSync } from '../../sync/vcs/initialize-backend-project';
import { getVCS } from '../../sync/vcs/vcs';
import { invariant } from '../../utils/invariant';
import { SegmentEvent, trackSegmentEvent } from '../analytics';

// Project
export const createNewProjectAction: ActionFunction = async ({ request, params }) => {
  const { organizationId } = params;
  invariant(organizationId, 'Organization ID is required');
  const formData = await request.formData();
  const name = formData.get('name');
  invariant(typeof name === 'string', 'Name is required');
  const project = await models.project.create({ name });
  trackSegmentEvent(SegmentEvent.projectLocalCreate);
  return redirect(`/organization/${organizationId}/project/${project._id}`);
};

export const renameProjectAction: ActionFunction = async ({
  request,
  params,
}) => {
  const formData = await request.formData();

  const name = formData.get('name');
  invariant(typeof name === 'string', 'Name is required');

  const { projectId } = params;
  invariant(projectId, 'Project ID is required');

  const project = await models.project.getById(projectId);

  invariant(project, 'Project not found');

  invariant(
    !isRemoteProject(project),
    'Cannot rename remote project',
  );

  await models.project.update(project, { name });
};

export const deleteProjectAction: ActionFunction = async ({ params }) => {
  const { organizationId, projectId } = params;
  invariant(organizationId, 'Organization ID is required');
  invariant(projectId, 'Project ID is required');
  const project = await models.project.getById(projectId);
  invariant(project, 'Project not found');

  await models.stats.incrementDeletedRequestsForDescendents(project);
  await models.project.remove(project);

  trackSegmentEvent(SegmentEvent.projectLocalDelete);

  return redirect(`/organization/${DEFAULT_ORGANIZATION_ID}/project/${DEFAULT_PROJECT_ID}`);
};

// Workspace
export const createNewWorkspaceAction: ActionFunction = async ({
  params,
  request,
}) => {
  const { organizationId, projectId } = params;
  invariant(organizationId, 'Organization ID is required');
  invariant(projectId, 'Project ID is required');

  const project = await models.project.getById(projectId);

  invariant(project, 'Project not found');

  const formData = await request.formData();

  const name = formData.get('name');
  invariant(typeof name === 'string', 'Name is required');

  const scope = formData.get('scope');
  invariant(scope === 'design' || scope === 'collection', 'Scope is required');

  const flushId = await database.bufferChanges();

  const workspace = await models.workspace.create({
    name,
    scope,
    parentId: projectId,
  });

  // Create default env, cookie jar, and meta
  await models.environment.getOrCreateForParentId(workspace._id);
  await models.cookieJar.getOrCreateForParentId(workspace._id);
  await models.workspaceMeta.getOrCreateByParentId(workspace._id);

  await database.flushChanges(flushId);
  if (session.isLoggedIn() && isRemoteProject(project) && isCollection(workspace)) {
    const vcs = getVCS();
    if (vcs) {
      initializeLocalBackendProjectAndMarkForSync({
        vcs,
        workspace,
      });
    }
  }

  trackSegmentEvent(
    isCollection(workspace)
      ? SegmentEvent.collectionCreate
      : SegmentEvent.documentCreate
  );

  return redirect(
    `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/${
      workspace.scope === 'collection' ? ACTIVITY_DEBUG : ACTIVITY_SPEC
    }`
  );
};

export const deleteWorkspaceAction: ActionFunction = async ({
  params,
  request,
}) => {
  const { organizationId, projectId } = params;
  invariant(projectId, 'projectId is required');

  const project = await models.project.getById(projectId);
  invariant(project, 'Project not found');

  const formData = await request.formData();

  const workspaceId = formData.get('workspaceId');
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');

  const workspace = await models.workspace.getById(workspaceId);
  invariant(workspace, 'Workspace not found');

  await models.stats.incrementDeletedRequestsForDescendents(workspace);
  await models.workspace.remove(workspace);

  try {
    const vcs = getVCS();
    if (vcs) {
      const backendProject = await vcs._getBackendProjectByRootDocument(workspace._id);
      await vcs._removeProject(backendProject);

      console.log({ projectsLOCAL: await vcs.localBackendProjects() });
    }
  } catch (err) {
    console.warn('Failed to remove project from VCS', err);
  }

  return redirect(`/organization/${organizationId}/project/${projectId}`);
};

export const duplicateWorkspaceAction: ActionFunction = async ({ request, params }) => {
  const { organizationId } = params;
  invariant(organizationId, 'Organization Id is required');
  const formData = await request.formData();
  const projectId = formData.get('projectId');
  invariant(typeof projectId === 'string', 'Project ID is required');

  const workspaceId = formData.get('workspaceId');
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');

  const workspace = await models.workspace.getById(workspaceId);
  invariant(workspace, 'Workspace not found');

  const name = formData.get('name') || '';
  invariant(typeof name === 'string', 'Name is required');

  const duplicateToProject = await models.project.getById(projectId);
  invariant(duplicateToProject, 'Project not found');

  const newWorkspace = await workspaceOperations.duplicate(workspace, {
    name,
    parentId: projectId,
  });

  // Create default env, cookie jar, and meta
  await models.environment.getOrCreateForParentId(newWorkspace._id);
  await models.cookieJar.getOrCreateForParentId(newWorkspace._id);
  await models.workspaceMeta.getOrCreateByParentId(newWorkspace._id);

  try {
    // Mark for sync if logged in and in the expected project
    const vcs = getVCS();
    if (session.isLoggedIn() && vcs && isRemoteProject(duplicateToProject)) {
      await initializeLocalBackendProjectAndMarkForSync({
        vcs: vcs.newInstance(),
        workspace: newWorkspace,
      });
    }
  } catch (e) {
    console.warn('Failed to initialize local backend project', e);
  }

  return redirect(
    `/organization/${organizationId}/project/${projectId}/workspace/${newWorkspace._id}/${
      newWorkspace.scope === 'collection' ? ACTIVITY_DEBUG : ACTIVITY_SPEC
    }`
  );
};

export const updateWorkspaceAction: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const workspaceId = formData.get('workspaceId');
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');

  const name = formData.get('name');
  invariant(typeof name === 'string', 'Name is required');

  const description = formData.get('description');
  if (description) {
    invariant(typeof description === 'string', 'Description is required');
  }

  const workspace = await models.workspace.getById(workspaceId);
  invariant(workspace, 'Workspace not found');

  if (workspace.scope === 'design') {
    const apiSpec = await models.apiSpec.getByParentId(workspaceId);
    invariant(apiSpec, 'No Api Spec found for this workspace');

    await models.apiSpec.update(apiSpec, {
      fileName: name,
    });
  }

  await models.workspace.update(workspace, {
    name,
    description: description || workspace.description,
  });
};

// Test Suite
export const createNewTestSuiteAction: ActionFunction = async ({
  request,
  params,
}) => {
  const { organizationId, workspaceId, projectId } = params;
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');
  const formData = await request.formData();
  const name = formData.get('name');
  invariant(typeof name === 'string', 'Name is required');

  const unitTestSuite = await models.unitTestSuite.create({
    parentId: workspaceId,
    name,
  });

  trackSegmentEvent(SegmentEvent.testSuiteCreate);

  return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}`);
};

export const deleteTestSuiteAction: ActionFunction = async ({ params }) => {
  const { organizationId, workspaceId, projectId, testSuiteId } = params;
  invariant(typeof testSuiteId === 'string', 'Test Suite ID is required');
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');
  invariant(typeof projectId === 'string', 'Project ID is required');

  const unitTestSuite = await models.unitTestSuite.getById(testSuiteId);

  invariant(unitTestSuite, 'Test Suite not found');

  await models.unitTestSuite.remove(unitTestSuite);

  trackSegmentEvent(SegmentEvent.testSuiteDelete);

  return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test`);
};

export const runAllTestsAction: ActionFunction = async ({
  params,
}) => {
  const { organizationId, projectId, workspaceId, testSuiteId } = params;
  invariant(typeof projectId === 'string', 'Project ID is required');
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');
  invariant(typeof testSuiteId === 'string', 'Test Suite ID is required');

  const unitTests = await database.find<UnitTest>(models.unitTest.type, {
    parentId: testSuiteId,
  });
  invariant(unitTests, 'No unit tests found');
  console.log('unitTests', unitTests);

  const tests: Test[] = unitTests
    .filter(t => t !== null)
    .map(t => ({
      name: t.name,
      code: t.code,
      defaultRequestId: t.requestId,
    }));

  const src = generate([{ name: 'My Suite', suites: [], tests }]);

  const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(
    workspaceId
  );

  const sendRequest = getSendRequestCallback(
    workspaceMeta?.activeEnvironmentId || undefined
  );

  const results = await runTests(src, { sendRequest });

  const testResult = await models.unitTestResult.create({
    results,
    parentId: workspaceId,
  });

  trackSegmentEvent(SegmentEvent.unitTestRun);

  return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${testSuiteId}/test-result/${testResult._id}`);
};

export const renameTestSuiteAction: ActionFunction = async ({ request, params }) => {
  const { workspaceId, projectId, testSuiteId } = params;
  invariant(typeof testSuiteId === 'string', 'Test Suite ID is required');
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');
  invariant(typeof projectId === 'string', 'Project ID is required');

  const formData = await request.formData();
  const name = formData.get('name');
  invariant(typeof name === 'string', 'Name is required');

  const unitTestSuite = await database.getWhere(models.unitTestSuite.type, {
    _id: testSuiteId,
  });

  invariant(unitTestSuite, 'Test Suite not found');

  await models.unitTestSuite.update(unitTestSuite, { name });
};

// Unit Test
export const createNewTestAction: ActionFunction = async ({ request, params }) => {
  const { testSuiteId } = params;
  invariant(typeof testSuiteId === 'string', 'Test Suite ID is required');
  const formData = await request.formData();

  const name = formData.get('name');
  invariant(typeof name === 'string', 'Name is required');

  await models.unitTest.create({
    parentId: testSuiteId,
    code: `const response1 = await insomnia.send();
expect(response1.status).to.equal(200);`,
    name,
  });

  trackSegmentEvent(SegmentEvent.unitTestCreate);
};

export const deleteTestAction: ActionFunction = async ({ params }) => {
  const { testId } = params;
  invariant(typeof testId === 'string', 'Test ID is required');

  const unitTest = await database.getWhere<UnitTest>(models.unitTest.type, {
    _id: testId,
  });

  invariant(unitTest, 'Test not found');

  await models.unitTest.remove(unitTest);
  trackSegmentEvent(SegmentEvent.unitTestDelete);
};

export const updateTestAction: ActionFunction = async ({ request, params }) => {
  const { testId } = params;
  const formData = await request.formData();
  invariant(typeof testId === 'string', 'Test ID is required');
  const code = formData.get('code');
  invariant(typeof code === 'string', 'Code is required');
  const name = formData.get('name');
  invariant(typeof name === 'string', 'Name is required');
  const requestId = formData.get('requestId');

  if (requestId) {
    invariant(typeof requestId === 'string', 'Request ID is required');
  }

  const unitTest = await database.getWhere<UnitTest>(models.unitTest.type, {
    _id: testId,
  });
  invariant(unitTest, 'Test not found');

  await models.unitTest.update(unitTest, { name, code, requestId: requestId || null });
};

export const runTestAction: ActionFunction = async ({ params }) => {
  const { organizationId, projectId, workspaceId, testSuiteId, testId } = params;
  invariant(typeof testId === 'string', 'Test ID is required');

  const unitTest = await database.getWhere<UnitTest>(models.unitTest.type, {
    _id: testId,
  });
  invariant(unitTest, 'Test not found');

  const tests: Test[] = [
    {
      name: unitTest.name,
      code: unitTest.code,
      defaultRequestId: unitTest.requestId,
    },
  ];
  const src = generate([{ name: 'My Suite', suites: [], tests }]);
  const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(
    unitTest.parentId
  );

  const sendRequest = getSendRequestCallback(
    workspaceMeta?.activeEnvironmentId || undefined
  );

  const results = await runTests(src, { sendRequest });

  const testResult = await models.unitTestResult.create({
    results,
    parentId: unitTest.parentId,
  });

  trackSegmentEvent(SegmentEvent.unitTestRun);

  return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${testSuiteId}/test-result/${testResult._id}`);
};

// Api Spec
export const updateApiSpecAction: ActionFunction = async ({
  request,
  params,
}) => {
  const { workspaceId } = params;
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');
  const formData = await request.formData();
  const contents = formData.get('contents');
  const fromSync = Boolean(formData.get('fromSync'));

  invariant(typeof contents === 'string', 'Contents is required');

  const apiSpec = await models.apiSpec.getByParentId(workspaceId);

  invariant(apiSpec, 'API Spec not found');
  await database.update({
    ...apiSpec,
    modified: Date.now(),
    created: fromSync ? Date.now() : apiSpec.created,
    contents,
  }, fromSync);
};

export const generateCollectionFromApiSpecAction: ActionFunction = async ({
  params,
}) => {
  const { organizationId, projectId, workspaceId } = params;

  invariant(typeof projectId === 'string', 'Project ID is required');
  invariant(typeof workspaceId === 'string', 'Workspace ID is required');

  const apiSpec = await models.apiSpec.getByParentId(workspaceId);

  if (!apiSpec) {
    throw new Error('No API Specification was found');
  }

  const workspace = await models.workspace.getById(workspaceId);

  invariant(workspace, 'Workspace not found');

  const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId);

  const isLintError = (result: IRuleResult) => result.severity === 0;
  const rulesetPath = path.join(
    process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'),
    `version-control/git/${workspaceMeta?.gitRepositoryId}/other/.spectral.yaml`,
  );

  const results = (await window.main.spectralRun({ contents: apiSpec.contents, rulesetPath })).filter(isLintError);
  if (apiSpec.contents && results && results.length) {
    throw new Error('Error Generating Configuration');
  }

  await scanResources({
    content: apiSpec.contents,
  });

  await importResources({
    projectId,
    workspaceId,
  });

  return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_DEBUG}`);
};


insomnia-plugin-uuid

Insomnia uuid template tag


openapi: 3.0.1
info:
  title: Swagger Petstore
  license:
    name: MIT
  version: 1.0.0
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets:
    get:
      tags:
        - pets
      summary: List all pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          schema:
            type: integer
            format: int32
      responses:
        200:
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pets'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      tags:
        - pets
      summary: Create a pet
      operationId: createPets
      responses:
        201:
          description: Null response
          content: {}
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /pets/{petId}:
    get:
      tags:
        - pets
      summary: Info for a specific pet
      operationId: showPetById
      parameters:
        - name: petId
          in: path
          description: The id of the pet to retrieve
          required: true
          schema:
            type: string
      responses:
        200:
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pets'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Pets:
      type: array
      items:
        $ref: '#/components/schemas/Pet'
    Error:
      required:
        - code
        - message
      type: object
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string
    Pet:
      required:
        - id
        - name
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string


description: description || workspace.description,

export const createNewTestSuiteAction: ActionFunction = async ({


.sidebar {
  position: relative;
  display: grid;
  grid-template-rows: auto auto fit-content(20%) auto minmax(0, 1fr) auto;
  grid-template-columns: 100%;
  z-index: 10;

  background-color: var(--color-bg);
  color: var(--color-font);

  width: 100%;
  height: 100%;

  & > .sidebar__list:last-child {
    padding-bottom: var(--padding-md);
  }

  &.sidebar--collapsed > * > * {
    display: none;
  }

  &.sidebar--hidden {
    // It proved difficult to keep the invisible children from overflowing when
    // hidden, so we're just going to bump it way to the left to be sure.
    left: -9999px;
  }

  &:focus {
    outline: none;
  }

  // ~~~~~~~~~~~~~~ //
  // Sidebar Header //
  // ~~~~~~~~~~~~~~ //

  .sidebar__header {
    border-top: 1px solid var(--hl-md);
    border-bottom: 1px solid var(--hl-md);
    box-sizing: border-box;
    background-color: var(--color-bg);
    color: var(--color-font);
    width: 100%;
    height: var(--nav-height);

    h1 {
      margin: 0;
    }

    h1,
    h1 * {
      font-size: var(--font-size-xl);
    }
  }

  // ~~~~~~~~~~~~~~~ //
  // Sidebar Toolbar //
  // ~~~~~~~~~~~~~~~ //

  .sidebar__filter {
    // Nothing yet
    display: grid;
    font-size: var(--font-size-sm);
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: center;
    box-sizing: border-box;
    padding: var(--padding-sm) 0 var(--padding-xxs) var(--padding-sm);

    .btn {
      padding: 0 calc(var(--padding-md) - var(--padding-xs));
      margin-right: var(--padding-xxs);
      border-radius: var(--radius-md);
      margin-left: var(--padding-xxs);
      margin-bottom: var(--padding-sm);
      height: var(--line-height-xxs);
    }

    .form-control input {
      margin-top: 0;
      height: auto;
      background: transparent;
    }
  }

  .sidebar__menu {
    box-sizing: border-box;
    width: 100%;
    display: flex;
    align-items: center;
    flex-direction: row;
    flex-wrap: wrap;
    font-size: var(--font-size-sm);
    color: var(--hl);
    padding-left: var(--padding-xs);
    padding-right: var(--padding-xs);
    height: var(--height-nav);

    &--insomnia-HACK {
      height: var(--line-height-md);
    }

    // Make it scroll when too skinny
    overflow: auto;

    &::-webkit-scrollbar {
      display: none;
    }

    & > * {
      flex: 1;
      margin-top: var(--padding-xs);
      margin-bottom: var(--padding-xs);
      max-width: 100%;
    }

    & > .dropdown > * {
      width: 100%;
    }

    & > *:first-child {
      margin-right: var(--padding-xxs);
    }

    & > *:last-child {
      margin-left: var(--padding-xxs);
    }

    .sidebar__menu__thing {
      display: flex;
      flex-direction: row;
      justify-content: center;
      align-items: center;

      & > .sidebar__menu__thing__text {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      i.fa {
        // Bump the drop down caret down a bit
        position: relative;
        top: 1px;
      }
    }

    .btn {
      border-radius: 900px;
      height: var(--line-height-xxs);
      padding-left: var(--padding-xxs);
      padding-right: var(--padding-xxs);
      color: var(--color-font);

      &:hover,
      &:focus {
        opacity: 1;
      }
    }
  }

  .sidebar__top {
    background: var(--color-bg);
    color: var(--color-font);
  }

  &.sidebar--skinny .sidebar__filter {
    grid-template-columns: 100%;
    padding-right: var(--padding-sm);

    & > *:first-child {
      margin-right: var(--padding-xs);
    }

    & > *:last-child {
      display: none;
    }
  }

  // ~~~~~~~~~~~~ //
  // Sidebar List //
  // ~~~~~~~~~~~~ //

  .sidebar__list-root {
    box-shadow: inset 0 2rem 2rem -2rem rgba(0, 0, 0, 0.03);

    overflow-y: auto;

    &::-webkit-scrollbar {
      display: none;
    }

    &:hover::-webkit-scrollbar {
      display: block;
    }
  }

  .sidebar__list-separator {
    height: 2px;
    background-color: var(--hl-sm);
    margin: var(--padding-sm) 0;

    &--invisible {
      opacity: 0;
    }
  }

  .sidebar__list--collapsed > * {
    display: none;
  }

  // ~~~~~~~~~~~ //
  // Sidebar Row //
  // ~~~~~~~~~~~ //

  .sidebar__row {
    position: relative;

    &.sidebar__row--dragging {
      // Set opacity on children so we can still see the separator
      & > * {
        opacity: 0.3;
      }
    }

    &.sidebar__row--dragging-below::after,
    &.sidebar__row--dragging-above::before {
      position: absolute;
      height: 0;
      right: 0;
      left: 0;
      border-bottom: 2px dotted var(--color-surprise);
      content: ' ';
      display: block;
    }
  }

  // ~~~~~~~~~~~~ //
  // Sidebar Item //
  // ~~~~~~~~~~~~ //

  .sidebar__item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-rows: var(--line-height-xs);

    & > button {
      color: var(--hl);
    }

    & > * {
      height: 100%;
    }

    &.sidebar__item--big {
      grid-template-rows: var(--line-height-sm);
    }

    &.sidebar__item--active > button {
      color: var(--color-font);

      .tag {
        opacity: 1;
      }
    }

    & > button {
      border-left: var(--padding-xxs) solid transparent;
    }

    &.sidebar__item--active.sidebar__item--request,
    &:focus,
    &:hover {
      background-color: var(--hl-xs);
      transition: background-color 0ms;
    }

    &.sidebar__item--active.sidebar__item--request > button {
      border-left: var(--padding-xxs) solid var(--color-surprise);
    }

    .tag {
      padding-left: 0;
      text-align: left;
      width: 3em;
    }
  }

  // ~~~~~~~~~~~~~~~~~ //
  // Sidebar Clickable //
  // ~~~~~~~~~~~~~~~~~ //

  .sidebar__clickable {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    height: 100%;

    & > span {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;

      &.empty {
        font-style: italic;
        opacity: 0.5;
      }
    }
  }

  // Padding for nested folders

  .sidebar__list .sidebar__clickable {
    padding-left: var(--padding-sm);
  }

  .sidebar__list .sidebar__list .sidebar__clickable {
    padding-left: calc(var(--padding-sm) + var(--padding-md) * 1);
  }

  .sidebar__list .sidebar__list .sidebar__list .sidebar__clickable {
    padding-left: calc(var(--padding-sm) + var(--padding-md) * 2);
  }

  .sidebar__list .sidebar__list .sidebar__list .sidebar__list .sidebar__clickable {
    padding-left: calc(var(--padding-sm) + var(--padding-md) * 3);
  }

  .sidebar__list .sidebar__list .sidebar__list .sidebar__list .sidebar__list .sidebar__clickable {
    padding-left: calc(var(--padding-sm) + var(--padding-md) * 4);
  }

  .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__clickable {
    padding-left: calc(var(--padding-sm) + var(--padding-md) * 5);
  }

  .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__list
    .sidebar__clickable {
    padding-left: calc(var(--padding-sm) + var(--padding-md) * 6);
  }

  // ~~~~~~~~~~~~~~~ //
  // Sidebar Expand  //
  // ~~~~~~~~~~~~~~~ //

  .sidebar__expand {
    display: none;
    height: 100%;

    &.sidebar__expand-hint {
      display: flex;
      align-items: center;
    }
  }

  // ~~~~~~~~~~~~~~~ //
  // Sidebar Actions //
  // ~~~~~~~~~~~~~~~ //

  .sidebar__actions {
    & > * {
      height: 100%;
    }

    & > button,
    & > .dropdown > button {
      display: none !important;
      color: var(--hl);
      height: 100%;
      padding: 0 var(--padding-sm);
    }

    & > button:hover,
    & .dropdown > button:focus,
    & > .dropdown:hover > button {
      color: var(--color-font);
    }
  }

  .sidebar__item:hover .sidebar__actions > button,
  .sidebar__item:hover .sidebar__actions > .dropdown > button {
    display: initial !important;
  }

  .sidebar__item__icon-pin {
    display: flex;
    align-items: center;
    color: var(--hl-xl);
    height: 100%;
    padding: 0 var(--padding-sm);
  }

  .sidebar__item:hover .sidebar__item__icon-pin {
    display: none;
  }

  // ~~~~~~~~~~~~~~ //
  // Sidebar Footer //
  // ~~~~~~~~~~~~~~ //

  .sidebar__footer {
    height: var(--line-height-xs);
    border-top: 1px solid var(--hl-md);
    width: 100%;
    grid-row-start: -1;
  }

  .sidebar__footer > button,
  .sidebar__footer .dropdown > button {
    font-size: var(--font-size-sm);
    padding: var(--padding-xs) var(--padding-md);
    height: 100%;
    margin: 0;
    color: var(--hl);
    box-sizing: border-box;
    overflow: hidden;
  }
}


import { DOMParser } from 'xmldom';
import xpath, { SelectedValue } from 'xpath';

/**
 * Query an XML blob with XPath
 */
export const queryXPath = (xml: string, query?: string) => {
  const dom = new DOMParser().parseFromString(xml);
  let selectedValues: SelectedValue[] = [];
  if (query === undefined) {
    throw new Error('Must pass an XPath query.');
  }
  try {
    selectedValues = xpath.select(query, dom);
  } catch (err) {
    throw new Error(`Invalid XPath query: ${query}`);
  }
  const output = [];
  // Functions return plain strings
  if (typeof selectedValues === 'string') {
    output.push({
      outer: selectedValues,
      inner: selectedValues,
    });
  } else {
    for (const selectedValue of selectedValues || []) {
      switch (selectedValue.constructor.name) {
        case 'Attr':
          output.push({
            outer: (selectedValue as Attr).toString().trim(),
            inner: (selectedValue as Attr).nodeValue,
          });
          break;

        case 'Element':
          output.push({
            outer: (selectedValue as Node).toString().trim(),
            inner: (selectedValue as Node).childNodes.toString(),
          });
          break;

        case 'Text':
          output.push({
            outer: (selectedValue as Text).toString().trim(),
            inner: (selectedValue as Text).toString().trim(),
          });
          break;

        default:
          break;
      }
    }
  }
  return output;
};


Digest Authentication Test

A collection for testing


invariant(unitTestSuite, 'Test Suite not found');

await models.unitTestSuite.update(unitTestSuite, { name });