/* eslint-env qunit */
/**
 * These tests run on the minified, window.videojs and ensure the needed
 * APIs still exist
 */
import document from 'global/document';
import window from 'global/window';
const videojs = window.videojs;

QUnit.module('Player API');
QUnit.test('videojs should exist on the window', function(assert) {
  assert.ok(window.videojs, 'videojs exists on the window');
});

QUnit.test('should be able to access expected player API methods', function(assert) {
  const player = videojs.getComponent('Player').prototype;

  // Native HTML5 Methods
  assert.ok(player.error, 'error exists');
  assert.ok(player.src, 'src exists');
  assert.ok(player.currentSrc, 'currentSrc exists');
  assert.ok(player.buffered, 'buffered exists');
  assert.ok(player.load, 'load exists');
  assert.ok(player.seeking, 'seeking exists');
  assert.ok(player.currentTime, 'currentTime exists');
  assert.ok(player.duration, 'duration exists');
  assert.ok(player.paused, 'paused exists');
  assert.ok(player.ended, 'ended exists');
  assert.ok(player.autoplay, 'autoplay exists');
  assert.ok(player.loop, 'loop exists');
  assert.ok(player.play, 'play exists');
  assert.ok(player.pause, 'pause exists');
  assert.ok(player.controls, 'controls exists');
  assert.ok(player.volume, 'volume exists');
  assert.ok(player.muted, 'muted exists');
  assert.ok(player.width, 'width exists');
  assert.ok(player.height, 'height exists');
  assert.ok(player.poster, 'poster exists');
  assert.ok(player.textTracks, 'textTracks exists');
  assert.ok(player.requestFullscreen, 'requestFullscreen exists');
  assert.ok(player.exitFullscreen, 'exitFullscreen exists');
  assert.ok(player.disablePictureInPicture, 'disablePictureInPicture exists');
  assert.ok(player.requestPictureInPicture, 'requestPictureInPicture exists');
  assert.ok(player.exitPictureInPicture, 'exitPictureInPicture exists');
  assert.ok(player.playbackRate, 'playbackRate exists');
  assert.ok(player.networkState, 'networkState exists');
  assert.ok(player.readyState, 'readyState exists');

  // Unsupported Native HTML5 Methods
  // assert.ok(player.canPlayType, 'canPlayType exists');
  // assert.ok(player.startTime, 'startTime exists');
  // assert.ok(player.defaultPlaybackRate, 'defaultPlaybackRate exists');
  // assert.ok(player.playbackRate, 'playbackRate exists');
  // assert.ok(player.played, 'played exists');
  // assert.ok(player.seekable, 'seekable exists');
  // assert.ok(player.videoWidth, 'videoWidth exists');
  // assert.ok(player.videoHeight, 'videoHeight exists');

  // Additional player methods
  assert.ok(player.bufferedPercent, 'bufferedPercent exists');
  assert.ok(player.reportUserActivity, 'reportUserActivity exists');
  assert.ok(player.userActive, 'userActive exists');
  assert.ok(player.usingNativeControls, 'usingNativeControls exists');
  assert.ok(player.isFullscreen, 'isFullscreen exists');
  assert.ok(player.getVideoPlaybackQuality, 'getVideoPlaybackQuality exists');

  // Track methods
  assert.ok(player.audioTracks, 'audioTracks exists');
  assert.ok(player.videoTracks, 'videoTracks exists');
  assert.ok(player.textTracks, 'textTracks exists');
  assert.ok(player.remoteTextTrackEls, 'remoteTextTrackEls exists');
  assert.ok(player.remoteTextTracks, 'remoteTextTracks exists');
  assert.ok(player.addTextTrack, 'addTextTrack exists');
  assert.ok(player.addRemoteTextTrack, 'addRemoteTextTrack exists');
  assert.ok(player.removeRemoteTextTrack, 'removeRemoteTextTrack exists');

  // Deprecated methods that should still exist
  assert.ok(player.requestFullScreen, 'requestFullScreen exists');
  assert.ok(player.isFullScreen, 'isFullScreen exists');
  assert.ok(player.cancelFullScreen, 'cancelFullScreen exists');
});

QUnit.test('should be able to access expected component API methods', function(assert) {
  const Component = videojs.getComponent('Component');
  const comp = new Component({
    id() {
      return 1;
    },
    reportUserActivity() {}
  });

  // Component methods
  assert.ok(comp.player, 'player exists');
  assert.ok(comp.options, 'options exists');
  assert.ok(comp.init, 'init exists');
  assert.ok(comp.dispose, 'dispose exists');
  assert.ok(comp.createEl, 'createEl exists');
  assert.ok(comp.contentEl, 'contentEl exists');
  assert.ok(comp.el, 'el exists');
  assert.ok(comp.addChild, 'addChild exists');
  assert.ok(comp.getChild, 'getChild exists');
  assert.ok(comp.getChildById, 'getChildById exists');
  assert.ok(comp.children, 'children exists');
  assert.ok(comp.initChildren, 'initChildren exists');
  assert.ok(comp.removeChild, 'removeChild exists');
  assert.ok(comp.on, 'on exists');
  assert.ok(comp.off, 'off exists');
  assert.ok(comp.one, 'one exists');
  assert.ok(comp.trigger, 'trigger exists');
  assert.ok(comp.triggerReady, 'triggerReady exists');
  assert.ok(comp.show, 'show exists');
  assert.ok(comp.hide, 'hide exists');
  assert.ok(comp.width, 'width exists');
  assert.ok(comp.height, 'height exists');
  assert.ok(comp.dimensions, 'dimensions exists');
  assert.ok(comp.ready, 'ready exists');
  assert.ok(comp.addClass, 'addClass exists');
  assert.ok(comp.removeClass, 'removeClass exists');
  assert.ok(comp.buildCSSClass, 'buildCSSClass exists');
  assert.ok(comp.setInterval, 'setInterval exists');
  assert.ok(comp.clearInterval, 'clearInterval exists');
  assert.ok(comp.setTimeout, 'setTimeout exists');
  assert.ok(comp.clearTimeout, 'clearTimeout exists');

  comp.dispose();
});

QUnit.test('should be able to access expected MediaTech API methods', function(assert) {
  const media = videojs.getComponent('Tech');
  const mediaProto = media.prototype;
  const html5 = videojs.getComponent('Html5');
  const html5Proto = html5.prototype;

  assert.ok(mediaProto.setPoster, 'setPoster should exist on the Media tech');
  assert.ok(html5Proto.setPoster, 'setPoster should exist on the HTML5 tech');

  assert.ok(html5.patchCanPlayType, 'patchCanPlayType should exist for HTML5');
  assert.ok(html5.unpatchCanPlayType, 'unpatchCanPlayType should exist for HTML5');

  // Source Handler Functions
  assert.ok(media.withSourceHandlers, 'withSourceHandlers should exist for Media Tech');

  assert.ok(html5.canPlaySource, 'canPlaySource should exist for HTML5');
  assert.ok(html5.registerSourceHandler, 'registerSourceHandler should exist for Html5');
  assert.ok(html5.selectSourceHandler, 'selectSourceHandler should exist for Html5');
  assert.ok(html5.prototype.setSource, 'setSource should exist for Html5');
  assert.ok(html5.prototype.disposeSourceHandler,
           'disposeSourceHandler should exist for Html5');
});

QUnit.test('should export ready api call to public', function(assert) {
  const videoTag = testHelperMakeTag();
  const fixture = document.getElementById('qunit-fixture');

  fixture.appendChild(videoTag);

  const player = videojs('example_1');

  assert.ok(player.ready !== undefined, 'ready callback is defined');
  player.dispose();
});

QUnit.test('should export useful components to the public', function(assert) {
  assert.ok(videojs.browser.TOUCH_ENABLED !== undefined,
           'Touch detection should be public');
  assert.ok(videojs.getComponent('ControlBar'), 'ControlBar should be public');
  assert.ok(videojs.getComponent('Button'), 'Button should be public');
  assert.ok(videojs.getComponent('PlayToggle'), 'PlayToggle should be public');
  assert.ok(videojs.getComponent('PictureInPictureToggle'), 'PictureInPictureToggle should be public');
  assert.ok(videojs.getComponent('FullscreenToggle'), 'FullscreenToggle should be public');
  assert.ok(videojs.getComponent('BigPlayButton'), 'BigPlayButton should be public');
  assert.ok(videojs.getComponent('LoadingSpinner'), 'LoadingSpinner should be public');
  assert.ok(videojs.getComponent('CurrentTimeDisplay'),
           'CurrentTimeDisplay should be public');
  assert.ok(videojs.getComponent('DurationDisplay'), 'DurationDisplay should be public');
  assert.ok(videojs.getComponent('TimeDivider'), 'TimeDivider should be public');
  assert.ok(videojs.getComponent('RemainingTimeDisplay'),
           'RemainingTimeDisplay should be public');
  assert.ok(videojs.getComponent('Slider'), 'Slider should be public');
  assert.ok(videojs.getComponent('ProgressControl'), 'ProgressControl should be public');
  assert.ok(videojs.getComponent('SeekBar'), 'SeekBar should be public');
  assert.ok(videojs.getComponent('LoadProgressBar'), 'LoadProgressBar should be public');
  assert.ok(videojs.getComponent('PlayProgressBar'), 'PlayProgressBar should be public');
  assert.ok(videojs.getComponent('VolumeControl'), 'VolumeControl should be public');
  assert.ok(videojs.getComponent('VolumeBar'), 'VolumeBar should be public');
  assert.ok(videojs.getComponent('VolumeLevel'), 'VolumeLevel should be public');
  assert.ok(videojs.getComponent('VolumeMenuButton'), 'VolumeMenuButton should be public');
  assert.ok(videojs.getComponent('MuteToggle'), 'MuteToggle should be public');
  assert.ok(videojs.getComponent('PosterImage'), 'PosterImage should be public');
  assert.ok(videojs.getComponent('Menu'), 'Menu should be public');
  assert.ok(videojs.getComponent('MenuItem'), 'MenuItem should be public');
  assert.ok(videojs.getComponent('MenuButton'), 'MenuButton should be public');
  assert.ok(videojs.getComponent('PlaybackRateMenuButton'),
           'PlaybackRateMenuButton should be public');

  assert.ok(videojs.getComponent('CaptionSettingsMenuItem'),
           'CaptionSettingsMenuItem should be public');
  assert.ok(videojs.getComponent('OffTextTrackMenuItem'),
           'OffTextTrackMenuItem should be public');
  assert.ok(videojs.getComponent('TextTrackMenuItem'),
           'TextTrackMenuItem should be public');
  assert.ok(videojs.getComponent('TextTrackDisplay'), 'TextTrackDisplay should be public');
  assert.ok(videojs.getComponent('TextTrackButton'), 'TextTrackButton should be public');
  assert.ok(videojs.getComponent('CaptionsButton'), 'CaptionsButton should be public');
  assert.ok(videojs.getComponent('SubtitlesButton'), 'SubtitlesButton should be public');
  assert.ok(videojs.getComponent('DescriptionsButton'),
           'DescriptionsButton should be public');
  assert.ok(videojs.getComponent('ChaptersButton'), 'ChaptersButton should be public');
  assert.ok(videojs.getComponent('ChaptersTrackMenuItem'),
           'ChaptersTrackMenuItem should be public');

  assert.ok(videojs.mergeOptions, 'mergeOptions should be public');
});

QUnit.test('should be able to initialize player twice on the same tag using string reference', function(assert) {
  const videoTag = testHelperMakeTag();
  const id = videoTag.id;
  const fixture = document.getElementById('qunit-fixture');

  fixture.appendChild(videoTag);

  const player = videojs('example_1');

  player.dispose();
  assert.ok(!document.getElementById(id), 'element is removed');

  videoTag = testHelperMakeTag();
  fixture.appendChild(videoTag);

  player = videojs('example_1');
  player.dispose();
});

QUnit.test('videojs.getPlayers() should be available after minification', function(assert) {
  const videoTag = testHelperMakeTag();
  const id = videoTag.id;
  const fixture = document.getElementById('qunit-fixture');

  fixture.appendChild(videoTag);

  const player = videojs(id);

  assert.ok(videojs.getPlayers()[id] === player, 'videojs.getPlayers() is available');

  player.dispose();
});

QUnit.test('component can be subclassed externally', function(assert) {
  const Component = videojs.getComponent('Component');
  const ControlBar = videojs.getComponent('ControlBar');

  class TestComponent extends Component {
    reportUserActivity() {}
    textTracks() {
      return {
        addEventListener: Function.prototype,
        removeEventListener: Function.prototype
      };
    }
  }

  const player = new TestComponent({
    id() {},
    reportUserActivity() {}
  });

  assert.ok(new ControlBar(player), 'created a control bar without throwing');
});

function testHelperMakeTag() {
  const videoTag = document.createElement('video');

  videoTag.id = 'example_1';
  videoTag.className = 'video-js';
  return videoTag;
}

QUnit.test('should extend Component', function(assert) {
  const Component = videojs.getComponent('Component');

  class MyComponent extends Component {
    constructor() {
      this.bar = true;
    }
    foo() {
      return true;
    }
  }

  const myComponent = new MyComponent();

  assert.ok(myComponent instanceof Component, 'creates an instance of Component');
  assert.ok(myComponent instanceof MyComponent, 'creates an instance of MyComponent');
  assert.ok(myComponent.bar, 'the constructor function is used');
  assert.ok(myComponent.foo(), 'instance methods are applied');

  class NoMethods extends Component {}

  const noMethods = new NoMethods({});

  assert.ok(noMethods.on, 'should extend component with no methods or constructor');

  myComponent.dispose();
});


// Move captions down when controls aren't being shown

// Hide if an error occurs
.vjs-error .vjs-text-track-display {
display: none;
}

### 🆕🐥☝ First Timers Only.

This issue is reserved for people who never contributed to Open Source before. We know that the process of creating a pull request is the biggest barrier for new contributors. This issue is for you 💝

[About First Timers Only](http://www.firsttimersonly.com/).

### 🤔 What you will need to know.

Nothing. This issue is meant to welcome you to Open Source :) We are happy to walk you through the process.

### 📋 Step by Step

- [ ] 🙋 **Claim this issue**: Comment below.

 Once claimed, we'll remove the 'unclaimed' label from the issue so others will know not to work on it.

- [ ] 📝 **Update** the file [$FILENAME]($BRANCH_URL) in the `$REPO` repository (press the little pen Icon) and edit the line as shown below.

``````diff
$DIFF
``````

- [ ] 💾 **Commit** your changes

- [ ] 🔀 **Start a Pull Request**. There are two ways how you can start a pull request:

1. If you are familiar with the terminal or would like to learn it, [here is a great tutorial](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) on how to send a pull request using the terminal.

2. You can [edit files directly in your browser](https://help.github.com/articles/editing-files-in-your-repository/)

- [ ] 🏁 **Done** Ask in comments for a review :)

### 🤔❓ Questions

Leave a comment below! Alternatively, drop by our [slack's #discussion channel](http://slack.videojs.com).

This issue was created by [First-Timers-Bot](https://github.com/hoodiehq/first-timers-bot).


position: absolute;
top: 0;
width: 100%;
}
.vjs-title-bar-title,
.vjs-title-bar-description {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;

keep keep keep keep add keep keep keep keep keep

// Hide if an error occurs
.vjs-error .vjs-title-bar {
display: none;
}

.vjs-modal-dialog.vjs-text-track-settings {
  background-color: $primary-background-color;
  background-color: rgba($primary-background-color, 0.75);
  color: $primary-foreground-color;
  height: 70%;
}

// Layout divs
.vjs-text-track-settings .vjs-modal-dialog-content {
  display: table;
}

.vjs-text-track-settings .vjs-track-settings-colors,
.vjs-text-track-settings .vjs-track-settings-font,
.vjs-text-track-settings .vjs-track-settings-controls {
  display: table-cell;
}

.vjs-text-track-settings .vjs-track-settings-controls {
  text-align: right;
  vertical-align: bottom;
}

// code that will only run if CSS Grid is supported by the browser
@supports (display: grid) {
  .vjs-text-track-settings .vjs-modal-dialog-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr;
    // Flex and Grid for Firefox, IE, and Edge remove the bottom padding/margin in a container as size decreases
    // so we add bottom padding/margin to the last item in the grid instead of here
    // see https://stackoverflow.com/a/23754080
    padding: 20px 24px 0px 24px;
  }

  // see the comment for padding above
  .vjs-track-settings-controls .vjs-default-button {
    margin-bottom: 20px;
  }

  .vjs-text-track-settings .vjs-track-settings-controls {
    // make this take up both columns
    grid-column: 1 / -1;
  }

  // 1 column for small players
  .vjs-layout-small .vjs-text-track-settings .vjs-modal-dialog-content ,
  .vjs-layout-x-small .vjs-text-track-settings .vjs-modal-dialog-content,
  .vjs-layout-tiny .vjs-text-track-settings .vjs-modal-dialog-content {
    grid-template-columns: 1fr;
  }

}

// Form elements
.vjs-text-track-settings select {
  font-size: inherit;
}

.vjs-track-setting > select {
  margin-right: 1em;
  margin-bottom: 0.5em;
}

.vjs-text-track-settings fieldset {
  margin: 10px;
  border: none;
}

.vjs-text-track-settings fieldset span {
  display: inline-block;
  padding: 0 .6em .8em;
}

// style the second select for text colors
.vjs-text-track-settings fieldset span > select {
  max-width: 7.3em;
}

.vjs-text-track-settings legend {
  color: $primary-foreground-color;
  font-weight: bold;
  font-size: 1.2em;
}

.vjs-text-track-settings .vjs-label {
  margin: 0 .5em .5em 0;
}

.vjs-track-settings-controls button:focus,
.vjs-track-settings-controls button:active {
  outline-style: solid;
  outline-width: medium;
  background-image: linear-gradient(0deg, $primary-foreground-color 88%, $secondary-background-color 100%);
}

.vjs-track-settings-controls button:hover {
  color: rgba(#2B333F, 0.75);
}

.vjs-track-settings-controls button {
  background-color: $primary-foreground-color;
  background-image: linear-gradient(-180deg, $primary-foreground-color 88%, $secondary-background-color 100%);
  color: #2B333F;
  cursor: pointer;
  border-radius: 2px;
}

.vjs-track-settings-controls .vjs-default-button {
  margin-right: 1em;
}


const AccessSniff = require('access-sniff');
const path = require('path');

const testFiles = [
  path.join(__dirname, '..', 'sandbox', 'descriptions.test-a11y.html')
];

const options = {
  accessibilityLevel: 'WCAG2AA',
  reportLevels: {
    notice: false,
    warning: true,
    error: true
  },
  ignore: [
    // Ignore warning about contrast of the "vjs-no-js" fallback link
    'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.BgImage'
  ]
};

AccessSniff.default(testFiles, options).then(function(report) {
  AccessSniff.report(report);
}).catch(function() {

  // there were errors, which are already reported, exit with an error
  process.exit(1);
});


/* eslint-disable no-var */
/* eslint-env qunit */
var videojs = require('../../');

QUnit.module('Browserify Require');
QUnit.test('videojs should be requirable and bundled via browserify', function(assert) {
  assert.ok(videojs, 'videojs is required properly');
});


* @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false

handleMouseMove(event, mouseDown = false) {

if (!mouseDown && !this.player_.scrubbing()) {

keep keep keep keep keep replace keep keep keep keep keep

if (!Dom.isSingleLeftClick(event) || isNaN(this.player_.duration())) {

/* eslint-env qunit */
import TestHelpers from './test-helpers.js';
import {createTimeRanges} from '../../src/js/utils/time.js';
import sinon from 'sinon';
QUnit.module('player reset-ui');

QUnit.test('Calling resetProgressBar should reset the components displaying time related values', function(assert) {
  const clock = sinon.useFakeTimers();
  const player = TestHelpers.makePlayer({controls: true});

  clock.tick(1000);
  player.trigger('ready');
  player.buffered = function() {
    return createTimeRanges(0, 5);
  };
  player.duration(40);
  player.currentTime(20);

  const {
    currentTimeDisplay,
    durationDisplay,
    remainingTimeDisplay,
    progressControl: {
      seekBar
    }
  } = player.controlBar;

  clock.tick(30);

  player.trigger('timeupdate');
  player.trigger('progress');

  clock.tick(30);

  // Current time display
  assert.equal(currentTimeDisplay.textNode_.textContent, '0:20', 'current time display is 0:20');
  // Duration display
  assert.equal(durationDisplay.textNode_.textContent, '0:40', 'duration display is 0:40');
  // Remaining time display
  assert.equal(remainingTimeDisplay.textNode_.textContent, '0:20', 'remaining time display is 0:20');
  // Seek bar
  assert.equal(seekBar.getProgress(), '0.5', 'seek bar progress is 0.5');
  assert.equal(seekBar.getAttribute('aria-valuetext'), '0:20 of 0:40', 'seek bar progress holder aria value text is 0:20 of 0:40');
  assert.equal(seekBar.getAttribute('aria-valuenow'), '50.00', 'seek bar progress holder aria value now is 50.00');
  // Load progress
  assert.equal(seekBar.loadProgressBar.el().textContent, 'Loaded: 12.50%', 'load progress bar textContent is Loaded: 12.50%');
  assert.equal(seekBar.loadProgressBar.el().style.width, '12.5%', 'load progress bar width is 12.5%');
  // Play progress
  assert.equal(seekBar.playProgressBar.el().textContent, '0:20', 'player progress bar textContent is 0:20');
  assert.equal(seekBar.playProgressBar.el().style.width, '50%', 'player progress bar width is 50%');
  assert.equal(seekBar.playProgressBar.timeTooltip.el().textContent, '0:20', 'player progress bar time tooltip is 0:20');

  // Do reset
  player.resetProgressBar_();
  player.duration(0);
  clock.tick(30);

  assert.equal(player.currentTime(), 0, 'player current time is 0');

  // Current time display
  assert.equal(currentTimeDisplay.textNode_.textContent, '0:00', 'current time display is 0:00');
  // Duration display
  assert.equal(durationDisplay.textNode_.textContent, '0:00', 'duration display is 0:00');
  // Remaining time display
  assert.equal(remainingTimeDisplay.textNode_.textContent, '0:00', 'remaining time display is 0:00');
  // Seek bar
  assert.equal(seekBar.getProgress(), '0', 'seek bar progress is 0');
  assert.equal(seekBar.getAttribute('aria-valuetext'), '0:00 of 0:00', 'seek bar progress holder aria value text is 0:00 of 0:00');
  assert.equal(seekBar.getAttribute('aria-valuenow'), '0.00', 'seek bar progress holder aria value now is 0.00');
  // Load progress
  assert.equal(seekBar.loadProgressBar.el().textContent, 'Loaded: 0.00%', 'load progress bar textContent is Loaded: 0.00%');
  assert.equal(seekBar.loadProgressBar.el().style.width, '0%', 'load progress bar width is 0%');
  // Play progress
  assert.equal(seekBar.playProgressBar.el().textContent, '0:00', 'player progress bar textContent is 0:00');
  assert.equal(seekBar.playProgressBar.el().style.width, '0%', 'player progress bar width is 0%');
  assert.equal(seekBar.playProgressBar.timeTooltip.el().textContent, '0:00', 'player progress bar time tooltip is 0:00');

  clock.restore();
  player.dispose();
});

QUnit.test('Calling resetProgressBar_ player method when no durationDisplay or remainingTimeDisplay controls exist', function(assert) {
  const player = TestHelpers.makePlayer({
    children: {
      controlBar: {}
    },
    controlBar: {
      children: {
        playToggle: {}
      }
    }
  });

  player.currentTime(20);
  player.resetProgressBar_();

  assert.equal(
    player.currentTime(), 0,
    'player current time is 0'
  );
  player.dispose();
});

QUnit.test('Calling resetPlaybackRate player method should place play rate at 1x', function(assert) {
  const player = TestHelpers.makePlayer({techOrder: ['html5']});

  player.playbackRate(2);
  player.handleTechRateChange_();
  player.resetPlaybackRate_();
  const defaultRate = player.defaultPlaybackRate();

  assert.equal(
    player.controlBar.playbackRateMenuButton.labelEl_.textContent, defaultRate + 'x',
    'Playback rate is the default one on the UI'
  );
  assert.equal(
    player.playbackRate(), defaultRate,
    'Playback rate is the default one on the player object'
  );
  player.dispose();
});

QUnit.test('Calling resetVolumeBar player method should reset volume bar', function(assert) {
  const player = TestHelpers.makePlayer({ techOrder: ['html5'] });

  player.volume(0.5);

  player.trigger('volumechange');

  assert.equal(player.controlBar.volumePanel.volumeControl.volumeBar.el_.getAttribute('aria-valuenow'), 50, 'UI value of VolumeBar is 50');

  player.resetVolumeBar_();

  assert.equal(player.controlBar.volumePanel.volumeControl.volumeBar.el_.getAttribute('aria-valuenow'), 100, 'UI value of VolumeBar is 100');

  player.dispose();
});


/**
 * Player Component - Base class for all UI objects
 *
 * @file component.js
 */
import window from 'global/window';
import evented from './mixins/evented';
import stateful from './mixins/stateful';
import * as Dom from './utils/dom.js';
import * as Fn from './utils/fn.js';
import * as Guid from './utils/guid.js';
import {toTitleCase, toLowerCase} from './utils/str.js';
import {merge} from './utils/obj.js';
import keycode from 'keycode';

/**
 * Base class for all UI Components.
 * Components are UI objects which represent both a javascript object and an element
 * in the DOM. They can be children of other components, and can have
 * children themselves.
 *
 * Components can also use methods from {@link EventTarget}
 */
class Component {

  /**
   * A callback that is called when a component is ready. Does not have any
   * parameters and any callback value will be ignored.
   *
   * @callback ReadyCallback
   * @this Component
   */

  /**
   * Creates an instance of this class.
   *
   * @param { import('./player').default } player
   *        The `Player` that this class should be attached to.
   *
   * @param {Object} [options]
   *        The key/value store of component options.
   *
   * @param {Object[]} [options.children]
   *        An array of children objects to initialize this component with. Children objects have
   *        a name property that will be used if more than one component of the same type needs to be
   *        added.
   *
   * @param  {string} [options.className]
   *         A class or space separated list of classes to add the component
   *
   * @param {ReadyCallback} [ready]
   *        Function that gets called when the `Component` is ready.
   */
  constructor(player, options, ready) {

    // The component might be the player itself and we can't pass `this` to super
    if (!player && this.play) {
      this.player_ = player = this; // eslint-disable-line
    } else {
      this.player_ = player;
    }

    this.isDisposed_ = false;

    // Hold the reference to the parent component via `addChild` method
    this.parentComponent_ = null;

    // Make a copy of prototype.options_ to protect against overriding defaults
    this.options_ = merge({}, this.options_);

    // Updated options with supplied options
    options = this.options_ = merge(this.options_, options);

    // Get ID from options or options element if one is supplied
    this.id_ = options.id || (options.el && options.el.id);

    // If there was no ID from the options, generate one
    if (!this.id_) {
      // Don't require the player ID function in the case of mock players
      const id = player && player.id && player.id() || 'no_player';

      this.id_ = `${id}_component_${Guid.newGUID()}`;
    }

    this.name_ = options.name || null;

    // Create element if one wasn't provided in options
    if (options.el) {
      this.el_ = options.el;
    } else if (options.createEl !== false) {
      this.el_ = this.createEl();
    }

    if (options.className && this.el_) {
      options.className.split(' ').forEach(c => this.addClass(c));
    }

    // Remove the placeholder event methods. If the component is evented, the
    // real methods are added next
    ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
      this[fn] = undefined;
    });

    // if evented is anything except false, we want to mixin in evented
    if (options.evented !== false) {
      // Make this an evented object and use `el_`, if available, as its event bus
      evented(this, {eventBusKey: this.el_ ? 'el_' : null});

      this.handleLanguagechange = this.handleLanguagechange.bind(this);
      this.on(this.player_, 'languagechange', this.handleLanguagechange);
    }
    stateful(this, this.constructor.defaultState);

    this.children_ = [];
    this.childIndex_ = {};
    this.childNameIndex_ = {};

    this.setTimeoutIds_ = new Set();
    this.setIntervalIds_ = new Set();
    this.rafIds_ = new Set();
    this.namedRafs_ = new Map();
    this.clearingTimersOnDispose_ = false;

    // Add any child components in options
    if (options.initChildren !== false) {
      this.initChildren();
    }

    // Don't want to trigger ready here or it will go before init is actually
    // finished for all children that run this constructor
    this.ready(ready);

    if (options.reportTouchActivity !== false) {
      this.enableTouchActivity();
    }

  }

  // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
  // They are replaced or removed in the constructor

  /**
   * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
   * function that will get called when an event with a certain name gets triggered.
   *
   * @param {string|string[]} type
   *        An event name or an array of event names.
   *
   * @param {Function} fn
   *        The function to call with `EventTarget`s
   */
  on(type, fn) {}

  /**
   * Removes an `event listener` for a specific event from an instance of `EventTarget`.
   * This makes it so that the `event listener` will no longer get called when the
   * named event happens.
   *
   * @param {string|string[]} type
   *        An event name or an array of event names.
   *
   * @param {Function} fn
   *        The function to remove.
   */
  off(type, fn) {}

  /**
   * This function will add an `event listener` that gets triggered only once. After the
   * first trigger it will get removed. This is like adding an `event listener`
   * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
   *
   * @param {string|string[]} type
   *        An event name or an array of event names.
   *
   * @param {Function} fn
   *        The function to be called once for each event name.
   */
  one(type, fn) {}

  /**
   * This function will add an `event listener` that gets triggered only once and is
   * removed from all events. This is like adding an array of `event listener`s
   * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
   * first time it is triggered.
   *
   * @param {string|string[]} type
   *        An event name or an array of event names.
   *
   * @param {Function} fn
   *        The function to be called once for each event name.
   */
  any(type, fn) {}

  /**
   * This function causes an event to happen. This will then cause any `event listeners`
   * that are waiting for that event, to get called. If there are no `event listeners`
   * for an event then nothing will happen.
   *
   * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
   * Trigger will also call the `on` + `uppercaseEventName` function.
   *
   * Example:
   * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
   * `onClick` if it exists.
   *
   * @param {string|Event|Object} event
   *        The name of the event, an `Event`, or an object with a key of type set to
   *        an event name.
   */
  trigger(event) {}

  /**
   * Dispose of the `Component` and all child components.
   *
   * @fires Component#dispose
   *
   * @param {Object} options
   * @param {Element} options.originalEl element with which to replace player element
   */
  dispose(options = {}) {

    // Bail out if the component has already been disposed.
    if (this.isDisposed_) {
      return;
    }

    if (this.readyQueue_) {
      this.readyQueue_.length = 0;
    }

    /**
     * Triggered when a `Component` is disposed.
     *
     * @event Component#dispose
     * @type {Event}
     *
     * @property {boolean} [bubbles=false]
     *           set to false so that the dispose event does not
     *           bubble up
     */
    this.trigger({type: 'dispose', bubbles: false});

    this.isDisposed_ = true;

    // Dispose all children.
    if (this.children_) {
      for (let i = this.children_.length - 1; i >= 0; i--) {
        if (this.children_[i].dispose) {
          this.children_[i].dispose();
        }
      }
    }

    // Delete child references
    this.children_ = null;
    this.childIndex_ = null;
    this.childNameIndex_ = null;

    this.parentComponent_ = null;

    if (this.el_) {
      // Remove element from DOM
      if (this.el_.parentNode) {
        if (options.restoreEl) {
          this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
        } else {
          this.el_.parentNode.removeChild(this.el_);
        }
      }

      this.el_ = null;
    }

    // remove reference to the player after disposing of the element
    this.player_ = null;
  }

  /**
   * Determine whether or not this component has been disposed.
   *
   * @return {boolean}
   *         If the component has been disposed, will be `true`. Otherwise, `false`.
   */
  isDisposed() {
    return Boolean(this.isDisposed_);
  }

  /**
   * Return the {@link Player} that the `Component` has attached to.
   *
   * @return { import('./player').default }
   *         The player that this `Component` has attached to.
   */
  player() {
    return this.player_;
  }

  /**
   * Deep merge of options objects with new options.
   * > Note: When both `obj` and `options` contain properties whose values are objects.
   *         The two properties get merged using {@link module:obj.merge}
   *
   * @param {Object} obj
   *        The object that contains new options.
   *
   * @return {Object}
   *         A new object of `this.options_` and `obj` merged together.
   */
  options(obj) {
    if (!obj) {
      return this.options_;
    }

    this.options_ = merge(this.options_, obj);
    return this.options_;
  }

  /**
   * Get the `Component`s DOM element
   *
   * @return {Element}
   *         The DOM element for this `Component`.
   */
  el() {
    return this.el_;
  }

  /**
   * Create the `Component`s DOM element.
   *
   * @param {string} [tagName]
   *        Element's DOM node type. e.g. 'div'
   *
   * @param {Object} [properties]
   *        An object of properties that should be set.
   *
   * @param {Object} [attributes]
   *        An object of attributes that should be set.
   *
   * @return {Element}
   *         The element that gets created.
   */
  createEl(tagName, properties, attributes) {
    return Dom.createEl(tagName, properties, attributes);
  }

  /**
   * Localize a string given the string in english.
   *
   * If tokens are provided, it'll try and run a simple token replacement on the provided string.
   * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
   *
   * If a `defaultValue` is provided, it'll use that over `string`,
   * if a value isn't found in provided language files.
   * This is useful if you want to have a descriptive key for token replacement
   * but have a succinct localized string and not require `en.json` to be included.
   *
   * Currently, it is used for the progress bar timing.
   * ```js
   * {
   *   "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
   * }
   * ```
   * It is then used like so:
   * ```js
   * this.localize('progress bar timing: currentTime={1} duration{2}',
   *               [this.player_.currentTime(), this.player_.duration()],
   *               '{1} of {2}');
   * ```
   *
   * Which outputs something like: `01:23 of 24:56`.
   *
   *
   * @param {string} string
   *        The string to localize and the key to lookup in the language files.
   * @param {string[]} [tokens]
   *        If the current item has token replacements, provide the tokens here.
   * @param {string} [defaultValue]
   *        Defaults to `string`. Can be a default value to use for token replacement
   *        if the lookup key is needed to be separate.
   *
   * @return {string}
   *         The localized string or if no localization exists the english string.
   */
  localize(string, tokens, defaultValue = string) {

    const code = this.player_.language && this.player_.language();
    const languages = this.player_.languages && this.player_.languages();
    const language = languages && languages[code];
    const primaryCode = code && code.split('-')[0];
    const primaryLang = languages && languages[primaryCode];

    let localizedString = defaultValue;

    if (language && language[string]) {
      localizedString = language[string];
    } else if (primaryLang && primaryLang[string]) {
      localizedString = primaryLang[string];
    }

    if (tokens) {
      localizedString = localizedString.replace(/\{(\d+)\}/g, function(match, index) {
        const value = tokens[index - 1];
        let ret = value;

        if (typeof value === 'undefined') {
          ret = match;
        }

        return ret;
      });
    }

    return localizedString;
  }

  /**
   * Handles language change for the player in components. Should be overridden by sub-components.
   *
   * @abstract
   */
  handleLanguagechange() {}

  /**
   * Return the `Component`s DOM element. This is where children get inserted.
   * This will usually be the the same as the element returned in {@link Component#el}.
   *
   * @return {Element}
   *         The content element for this `Component`.
   */
  contentEl() {
    return this.contentEl_ || this.el_;
  }

  /**
   * Get this `Component`s ID
   *
   * @return {string}
   *         The id of this `Component`
   */
  id() {
    return this.id_;
  }

  /**
   * Get the `Component`s name. The name gets used to reference the `Component`
   * and is set during registration.
   *
   * @return {string}
   *         The name of this `Component`.
   */
  name() {
    return this.name_;
  }

  /**
   * Get an array of all child components
   *
   * @return {Array}
   *         The children
   */
  children() {
    return this.children_;
  }

  /**
   * Returns the child `Component` with the given `id`.
   *
   * @param {string} id
   *        The id of the child `Component` to get.
   *
   * @return {Component|undefined}
   *         The child `Component` with the given `id` or undefined.
   */
  getChildById(id) {
    return this.childIndex_[id];
  }

  /**
   * Returns the child `Component` with the given `name`.
   *
   * @param {string} name
   *        The name of the child `Component` to get.
   *
   * @return {Component|undefined}
   *         The child `Component` with the given `name` or undefined.
   */
  getChild(name) {
    if (!name) {
      return;
    }

    return this.childNameIndex_[name];
  }

  /**
   * Returns the descendant `Component` following the givent
   * descendant `names`. For instance ['foo', 'bar', 'baz'] would
   * try to get 'foo' on the current component, 'bar' on the 'foo'
   * component and 'baz' on the 'bar' component and return undefined
   * if any of those don't exist.
   *
   * @param {...string[]|...string} names
   *        The name of the child `Component` to get.
   *
   * @return {Component|undefined}
   *         The descendant `Component` following the given descendant
   *         `names` or undefined.
   */
  getDescendant(...names) {
    // flatten array argument into the main array
    names = names.reduce((acc, n) => acc.concat(n), []);

    let currentChild = this;

    for (let i = 0; i < names.length; i++) {
      currentChild = currentChild.getChild(names[i]);

      if (!currentChild || !currentChild.getChild) {
        return;
      }
    }

    return currentChild;
  }

  /**
   * Add a child `Component` inside the current `Component`.
   *
   *
   * @param {string|Component} child
   *        The name or instance of a child to add.
   *
   * @param {Object} [options={}]
   *        The key/value store of options that will get passed to children of
   *        the child.
   *
   * @param {number} [index=this.children_.length]
   *        The index to attempt to add a child into.
   *
   * @return {Component}
   *         The `Component` that gets added as a child. When using a string the
   *         `Component` will get created by this process.
   */
  addChild(child, options = {}, index = this.children_.length) {
    let component;
    let componentName;

    // If child is a string, create component with options
    if (typeof child === 'string') {
      componentName = toTitleCase(child);

      const componentClassName = options.componentClass || componentName;

      // Set name through options
      options.name = componentName;

      // Create a new object & element for this controls set
      // If there's no .player_, this is a player
      const ComponentClass = Component.getComponent(componentClassName);

      if (!ComponentClass) {
        throw new Error(`Component ${componentClassName} does not exist`);
      }

      // data stored directly on the videojs object may be
      // misidentified as a component to retain
      // backwards-compatibility with 4.x. check to make sure the
      // component class can be instantiated.
      if (typeof ComponentClass !== 'function') {
        return null;
      }

      component = new ComponentClass(this.player_ || this, options);

    // child is a component instance
    } else {
      component = child;
    }

    if (component.parentComponent_) {
      component.parentComponent_.removeChild(component);
    }
    this.children_.splice(index, 0, component);
    component.parentComponent_ = this;

    if (typeof component.id === 'function') {
      this.childIndex_[component.id()] = component;
    }

    // If a name wasn't used to create the component, check if we can use the
    // name function of the component
    componentName = componentName || (component.name && toTitleCase(component.name()));

    if (componentName) {
      this.childNameIndex_[componentName] = component;
      this.childNameIndex_[toLowerCase(componentName)] = component;
    }

    // Add the UI object's element to the container div (box)
    // Having an element is not required
    if (typeof component.el === 'function' && component.el()) {
      // If inserting before a component, insert before that component's element
      let refNode = null;

      if (this.children_[index + 1]) {
        // Most children are components, but the video tech is an HTML element
        if (this.children_[index + 1].el_) {
          refNode = this.children_[index + 1].el_;
        } else if (Dom.isEl(this.children_[index + 1])) {
          refNode = this.children_[index + 1];
        }
      }

      this.contentEl().insertBefore(component.el(), refNode);
    }

    // Return so it can stored on parent object if desired.
    return component;
  }

  /**
   * Remove a child `Component` from this `Component`s list of children. Also removes
   * the child `Component`s element from this `Component`s element.
   *
   * @param {Component} component
   *        The child `Component` to remove.
   */
  removeChild(component) {
    if (typeof component === 'string') {
      component = this.getChild(component);
    }

    if (!component || !this.children_) {
      return;
    }

    let childFound = false;

    for (let i = this.children_.length - 1; i >= 0; i--) {
      if (this.children_[i] === component) {
        childFound = true;
        this.children_.splice(i, 1);
        break;
      }
    }

    if (!childFound) {
      return;
    }

    component.parentComponent_ = null;

    this.childIndex_[component.id()] = null;
    this.childNameIndex_[toTitleCase(component.name())] = null;
    this.childNameIndex_[toLowerCase(component.name())] = null;

    const compEl = component.el();

    if (compEl && compEl.parentNode === this.contentEl()) {
      this.contentEl().removeChild(component.el());
    }
  }

  /**
   * Add and initialize default child `Component`s based upon options.
   */
  initChildren() {
    const children = this.options_.children;

    if (children) {
      // `this` is `parent`
      const parentOptions = this.options_;

      const handleAdd = (child) => {
        const name = child.name;
        let opts = child.opts;

        // Allow options for children to be set at the parent options
        // e.g. videojs(id, { controlBar: false });
        // instead of videojs(id, { children: { controlBar: false });
        if (parentOptions[name] !== undefined) {
          opts = parentOptions[name];
        }

        // Allow for disabling default components
        // e.g. options['children']['posterImage'] = false
        if (opts === false) {
          return;
        }

        // Allow options to be passed as a simple boolean if no configuration
        // is necessary.
        if (opts === true) {
          opts = {};
        }

        // We also want to pass the original player options
        // to each component as well so they don't need to
        // reach back into the player for options later.
        opts.playerOptions = this.options_.playerOptions;

        // Create and add the child component.
        // Add a direct reference to the child by name on the parent instance.
        // If two of the same component are used, different names should be supplied
        // for each
        const newChild = this.addChild(name, opts);

        if (newChild) {
          this[name] = newChild;
        }
      };

      // Allow for an array of children details to passed in the options
      let workingChildren;
      const Tech = Component.getComponent('Tech');

      if (Array.isArray(children)) {
        workingChildren = children;
      } else {
        workingChildren = Object.keys(children);
      }

      workingChildren
      // children that are in this.options_ but also in workingChildren  would
      // give us extra children we do not want. So, we want to filter them out.
        .concat(Object.keys(this.options_)
          .filter(function(child) {
            return !workingChildren.some(function(wchild) {
              if (typeof wchild === 'string') {
                return child === wchild;
              }
              return child === wchild.name;
            });
          }))
        .map((child) => {
          let name;
          let opts;

          if (typeof child === 'string') {
            name = child;
            opts = children[name] || this.options_[name] || {};
          } else {
            name = child.name;
            opts = child;
          }

          return {name, opts};
        })
        .filter((child) => {
        // we have to make sure that child.name isn't in the techOrder since
        // techs are registered as Components but can't aren't compatible
        // See https://github.com/videojs/video.js/issues/2772
          const c = Component.getComponent(child.opts.componentClass ||
                                       toTitleCase(child.name));

          return c && !Tech.isTech(c);
        })
        .forEach(handleAdd);
    }
  }

  /**
   * Builds the default DOM class name. Should be overridden by sub-components.
   *
   * @return {string}
   *         The DOM class name for this object.
   *
   * @abstract
   */
  buildCSSClass() {
    // Child classes can include a function that does:
    // return 'CLASS NAME' + this._super();
    return '';
  }

  /**
   * 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.
   *
   * @param {ReadyCallback} fn
   *        Function that gets called when the `Component` is ready.
   *
   * @return {Component}
   *         Returns itself; method can be chained.
   */
  ready(fn, sync = false) {
    if (!fn) {
      return;
    }

    if (!this.isReady_) {
      this.readyQueue_ = this.readyQueue_ || [];
      this.readyQueue_.push(fn);
      return;
    }

    if (sync) {
      fn.call(this);
    } else {
      // Call the function asynchronously by default for consistency
      this.setTimeout(fn, 1);
    }
  }

  /**
   * Trigger all the ready listeners for this `Component`.
   *
   * @fires Component#ready
   */
  triggerReady() {
    this.isReady_ = true;

    // Ensure ready is triggered asynchronously
    this.setTimeout(function() {
      const readyQueue = this.readyQueue_;

      // Reset Ready Queue
      this.readyQueue_ = [];

      if (readyQueue && readyQueue.length > 0) {
        readyQueue.forEach(function(fn) {
          fn.call(this);
        }, this);
      }

      // Allow for using event listeners also
      /**
       * Triggered when a `Component` is ready.
       *
       * @event Component#ready
       * @type {Event}
       */
      this.trigger('ready');
    }, 1);
  }

  /**
   * Find a single DOM element matching a `selector`. This can be within the `Component`s
   * `contentEl()` or another custom context.
   *
   * @param {string} selector
   *        A valid CSS selector, which will be passed to `querySelector`.
   *
   * @param {Element|string} [context=this.contentEl()]
   *        A DOM element within which to query. Can also be a selector string in
   *        which case the first matching element will get used as context. If
   *        missing `this.contentEl()` gets used. If  `this.contentEl()` returns
   *        nothing it falls back to `document`.
   *
   * @return {Element|null}
   *         the dom element that was found, or null
   *
   * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   */
  $(selector, context) {
    return Dom.$(selector, context || this.contentEl());
  }

  /**
   * Finds all DOM element matching a `selector`. This can be within the `Component`s
   * `contentEl()` or another custom context.
   *
   * @param {string} selector
   *        A valid CSS selector, which will be passed to `querySelectorAll`.
   *
   * @param {Element|string} [context=this.contentEl()]
   *        A DOM element within which to query. Can also be a selector string in
   *        which case the first matching element will get used as context. If
   *        missing `this.contentEl()` gets used. If  `this.contentEl()` returns
   *        nothing it falls back to `document`.
   *
   * @return {NodeList}
   *         a list of dom elements that were found
   *
   * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   */
  $$(selector, context) {
    return Dom.$$(selector, context || this.contentEl());
  }

  /**
   * Check if a component's element has a CSS class name.
   *
   * @param {string} classToCheck
   *        CSS class name to check.
   *
   * @return {boolean}
   *         - True if the `Component` has the class.
   *         - False if the `Component` does not have the class`
   */
  hasClass(classToCheck) {
    return Dom.hasClass(this.el_, classToCheck);
  }

  /**
   * Add a CSS class name to the `Component`s element.
   *
   * @param {...string} classesToAdd
   *        One or more CSS class name to add.
   */
  addClass(...classesToAdd) {
    Dom.addClass(this.el_, ...classesToAdd);
  }

  /**
   * Remove a CSS class name from the `Component`s element.
   *
   * @param {...string} classesToRemove
   *        One or more CSS class name to remove.
   */
  removeClass(...classesToRemove) {
    Dom.removeClass(this.el_, ...classesToRemove);
  }

  /**
   * Add or remove a CSS class name from the component's element.
   * - `classToToggle` gets added when {@link Component#hasClass} would return false.
   * - `classToToggle` gets removed when {@link Component#hasClass} would return true.
   *
   * @param  {string} classToToggle
   *         The class to add or remove based on (@link Component#hasClass}
   *
   * @param  {boolean|Dom~predicate} [predicate]
   *         An {@link Dom~predicate} function or a boolean
   */
  toggleClass(classToToggle, predicate) {
    Dom.toggleClass(this.el_, classToToggle, predicate);
  }

  /**
   * Show the `Component`s element if it is hidden by removing the
   * 'vjs-hidden' class name from it.
   */
  show() {
    this.removeClass('vjs-hidden');
  }

  /**
   * Hide the `Component`s element if it is currently showing by adding the
   * 'vjs-hidden` class name to it.
   */
  hide() {
    this.addClass('vjs-hidden');
  }

  /**
   * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
   * class name to it. Used during fadeIn/fadeOut.
   *
   * @private
   */
  lockShowing() {
    this.addClass('vjs-lock-showing');
  }

  /**
   * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
   * class name from it. Used during fadeIn/fadeOut.
   *
   * @private
   */
  unlockShowing() {
    this.removeClass('vjs-lock-showing');
  }

  /**
   * Get the value of an attribute on the `Component`s element.
   *
   * @param {string} attribute
   *        Name of the attribute to get the value from.
   *
   * @return {string|null}
   *         - The value of the attribute that was asked for.
   *         - Can be an empty string on some browsers if the attribute does not exist
   *           or has no value
   *         - Most browsers will return null if the attribute does not exist or has
   *           no value.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
   */
  getAttribute(attribute) {
    return Dom.getAttribute(this.el_, attribute);
  }

  /**
   * Set the value of an attribute on the `Component`'s element
   *
   * @param {string} attribute
   *        Name of the attribute to set.
   *
   * @param {string} value
   *        Value to set the attribute to.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
   */
  setAttribute(attribute, value) {
    Dom.setAttribute(this.el_, attribute, value);
  }

  /**
   * Remove an attribute from the `Component`s element.
   *
   * @param {string} attribute
   *        Name of the attribute to remove.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
   */
  removeAttribute(attribute) {
    Dom.removeAttribute(this.el_, attribute);
  }

  /**
   * Get or set the width of the component based upon the CSS styles.
   * See {@link Component#dimension} for more detailed information.
   *
   * @param {number|string} [num]
   *        The width that you want to set postfixed with '%', 'px' or nothing.
   *
   * @param {boolean} [skipListeners]
   *        Skip the componentresize event trigger
   *
   * @return {number|string}
   *         The width when getting, zero if there is no width. Can be a string
   *           postpixed with '%' or 'px'.
   */
  width(num, skipListeners) {
    return this.dimension('width', num, skipListeners);
  }

  /**
   * Get or set the height of the component based upon the CSS styles.
   * See {@link Component#dimension} for more detailed information.
   *
   * @param {number|string} [num]
   *        The height that you want to set postfixed with '%', 'px' or nothing.
   *
   * @param {boolean} [skipListeners]
   *        Skip the componentresize event trigger
   *
   * @return {number|string}
   *         The width when getting, zero if there is no width. Can be a string
   *         postpixed with '%' or 'px'.
   */
  height(num, skipListeners) {
    return this.dimension('height', num, skipListeners);
  }

  /**
   * Set both the width and height of the `Component` element at the same time.
   *
   * @param  {number|string} width
   *         Width to set the `Component`s element to.
   *
   * @param  {number|string} height
   *         Height to set the `Component`s element to.
   */
  dimensions(width, height) {
    // Skip componentresize listeners on width for optimization
    this.width(width, true);
    this.height(height);
  }

  /**
   * Get or set width or height of the `Component` element. This is the shared code
   * for the {@link Component#width} and {@link Component#height}.
   *
   * Things to know:
   * - If the width or height in an number this will return the number postfixed with 'px'.
   * - If the width/height is a percent this will return the percent postfixed with '%'
   * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
   *   defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
   *   See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
   *   for more information
   * - If you want the computed style of the component, use {@link Component#currentWidth}
   *   and {@link {Component#currentHeight}
   *
   * @fires Component#componentresize
   *
   * @param {string} widthOrHeight
   8        'width' or 'height'
   *
   * @param  {number|string} [num]
   8         New dimension
   *
   * @param  {boolean} [skipListeners]
   *         Skip componentresize event trigger
   *
   * @return {number}
   *         The dimension when getting or 0 if unset
   */
  dimension(widthOrHeight, num, skipListeners) {
    if (num !== undefined) {
      // Set to zero if null or literally NaN (NaN !== NaN)
      if (num === null || num !== num) {
        num = 0;
      }

      // Check if using css width/height (% or px) and adjust
      if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
        this.el_.style[widthOrHeight] = num;
      } else if (num === 'auto') {
        this.el_.style[widthOrHeight] = '';
      } else {
        this.el_.style[widthOrHeight] = num + 'px';
      }

      // skipListeners allows us to avoid triggering the resize event when setting both width and height
      if (!skipListeners) {
        /**
         * Triggered when a component is resized.
         *
         * @event Component#componentresize
         * @type {Event}
         */
        this.trigger('componentresize');
      }

      return;
    }

    // Not setting a value, so getting it
    // Make sure element exists
    if (!this.el_) {
      return 0;
    }

    // Get dimension value from style
    const val = this.el_.style[widthOrHeight];
    const pxIndex = val.indexOf('px');

    if (pxIndex !== -1) {
      // Return the pixel value with no 'px'
      return parseInt(val.slice(0, pxIndex), 10);
    }

    // No px so using % or no style was set, so falling back to offsetWidth/height
    // If component has display:none, offset will return 0
    // TODO: handle display:none and no dimension style using px
    return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
  }

  /**
   * Get the computed width or the height of the component's element.
   *
   * Uses `window.getComputedStyle`.
   *
   * @param {string} widthOrHeight
   *        A string containing 'width' or 'height'. Whichever one you want to get.
   *
   * @return {number}
   *         The dimension that gets asked for or 0 if nothing was set
   *         for that dimension.
   */
  currentDimension(widthOrHeight) {
    let computedWidthOrHeight = 0;

    if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
      throw new Error('currentDimension only accepts width or height value');
    }

    computedWidthOrHeight = Dom.computedStyle(this.el_, widthOrHeight);

    // remove 'px' from variable and parse as integer
    computedWidthOrHeight = parseFloat(computedWidthOrHeight);

    // if the computed value is still 0, it's possible that the browser is lying
    // and we want to check the offset values.
    // This code also runs wherever getComputedStyle doesn't exist.
    if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
      const rule = `offset${toTitleCase(widthOrHeight)}`;

      computedWidthOrHeight = this.el_[rule];
    }

    return computedWidthOrHeight;
  }

  /**
   * An object that contains width and height values of the `Component`s
   * computed style. Uses `window.getComputedStyle`.
   *
   * @typedef {Object} Component~DimensionObject
   *
   * @property {number} width
   *           The width of the `Component`s computed style.
   *
   * @property {number} height
   *           The height of the `Component`s computed style.
   */

  /**
   * Get an object that contains computed width and height values of the
   * component's element.
   *
   * Uses `window.getComputedStyle`.
   *
   * @return {Component~DimensionObject}
   *         The computed dimensions of the component's element.
   */
  currentDimensions() {
    return {
      width: this.currentDimension('width'),
      height: this.currentDimension('height')
    };
  }

  /**
   * Get the computed width of the component's element.
   *
   * Uses `window.getComputedStyle`.
   *
   * @return {number}
   *         The computed width of the component's element.
   */
  currentWidth() {
    return this.currentDimension('width');
  }

  /**
   * Get the computed height of the component's element.
   *
   * Uses `window.getComputedStyle`.
   *
   * @return {number}
   *         The computed height of the component's element.
   */
  currentHeight() {
    return this.currentDimension('height');
  }

  /**
   * Set the focus to this component
   */
  focus() {
    this.el_.focus();
  }

  /**
   * Remove the focus from this component
   */
  blur() {
    this.el_.blur();
  }

  /**
   * When this Component receives a `keydown` event which it does not process,
   *  it passes the event to the Player for handling.
   *
   * @param {KeyboardEvent} event
   *        The `keydown` event that caused this function to be called.
   */
  handleKeyDown(event) {
    if (this.player_) {

      // We only stop propagation here because we want unhandled events to fall
      // back to the browser. Exclude Tab for focus trapping.
      if (!keycode.isEventKey(event, 'Tab')) {
        event.stopPropagation();
      }
      this.player_.handleKeyDown(event);
    }
  }

  /**
   * Many components used to have a `handleKeyPress` method, which was poorly
   * named because it listened to a `keydown` event. This method name now
   * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
   * will not see their method calls stop working.
   *
   * @param {Event} event
   *        The event that caused this function to be called.
   */
  handleKeyPress(event) {
    this.handleKeyDown(event);
  }

  /**
   * Emit a 'tap' events when touch event support gets detected. This gets used to
   * support toggling the controls through a tap on the video. They get enabled
   * because every sub-component would have extra overhead otherwise.
   *
   * @private
   * @fires Component#tap
   * @listens Component#touchstart
   * @listens Component#touchmove
   * @listens Component#touchleave
   * @listens Component#touchcancel
   * @listens Component#touchend

   */
  emitTapEvents() {
    // Track the start time so we can determine how long the touch lasted
    let touchStart = 0;
    let firstTouch = null;

    // Maximum movement allowed during a touch event to still be considered a tap
    // Other popular libs use anywhere from 2 (hammer.js) to 15,
    // so 10 seems like a nice, round number.
    const tapMovementThreshold = 10;

    // The maximum length a touch can be while still being considered a tap
    const touchTimeThreshold = 200;

    let couldBeTap;

    this.on('touchstart', function(event) {
      // If more than one finger, don't consider treating this as a click
      if (event.touches.length === 1) {
        // Copy pageX/pageY from the object
        firstTouch = {
          pageX: event.touches[0].pageX,
          pageY: event.touches[0].pageY
        };
        // Record start time so we can detect a tap vs. "touch and hold"
        touchStart = window.performance.now();
        // Reset couldBeTap tracking
        couldBeTap = true;
      }
    });

    this.on('touchmove', function(event) {
      // If more than one finger, don't consider treating this as a click
      if (event.touches.length > 1) {
        couldBeTap = false;
      } else if (firstTouch) {
        // Some devices will throw touchmoves for all but the slightest of taps.
        // So, if we moved only a small distance, this could still be a tap
        const xdiff = event.touches[0].pageX - firstTouch.pageX;
        const ydiff = event.touches[0].pageY - firstTouch.pageY;
        const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);

        if (touchDistance > tapMovementThreshold) {
          couldBeTap = false;
        }
      }
    });

    const noTap = function() {
      couldBeTap = false;
    };

    // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
    this.on('touchleave', noTap);
    this.on('touchcancel', noTap);

    // When the touch ends, measure how long it took and trigger the appropriate
    // event
    this.on('touchend', function(event) {
      firstTouch = null;
      // Proceed only if the touchmove/leave/cancel event didn't happen
      if (couldBeTap === true) {
        // Measure how long the touch lasted
        const touchTime = window.performance.now() - touchStart;

        // Make sure the touch was less than the threshold to be considered a tap
        if (touchTime < touchTimeThreshold) {
          // Don't let browser turn this into a click
          event.preventDefault();
          /**
           * Triggered when a `Component` is tapped.
           *
           * @event Component#tap
           * @type {MouseEvent}
           */
          this.trigger('tap');
          // It may be good to copy the touchend event object and change the
          // type to tap, if the other event properties aren't exact after
          // Events.fixEvent runs (e.g. event.target)
        }
      }
    });
  }

  /**
   * This function reports user activity whenever touch events happen. This can get
   * turned off by any sub-components that wants touch events to act another way.
   *
   * Report user touch activity when touch events occur. User activity gets used to
   * determine when controls should show/hide. It is 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 as `touchstart` and `touchend` toggle player
   * controls. So touch events can't help us at the player level either.
   *
   * User activity gets 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.
   * Furthermore a `touchmove` event and anything other than a tap, should not turn
   * controls back on.
   *
   * @listens Component#touchstart
   * @listens Component#touchmove
   * @listens Component#touchend
   * @listens Component#touchcancel
   */
  enableTouchActivity() {
    // Don't continue if the root player doesn't support reporting user activity
    if (!this.player() || !this.player().reportUserActivity) {
      return;
    }

    // listener for reporting that the user is active
    const report = Fn.bind_(this.player(), this.player().reportUserActivity);

    let touchHolding;

    this.on('touchstart', function() {
      report();
      // For as long as the they are touching the device or have their mouse down,
      // we consider them active even if they're not moving their finger or mouse.
      // So we want to continue to update that they are active
      this.clearInterval(touchHolding);
      // report at the same interval as activityCheck
      touchHolding = this.setInterval(report, 250);
    });

    const touchEnd = function(event) {
      report();
      // stop the interval that maintains activity if the touch is holding
      this.clearInterval(touchHolding);
    };

    this.on('touchmove', report);
    this.on('touchend', touchEnd);
    this.on('touchcancel', touchEnd);
  }

  /**
   * A callback that has no parameters and is bound into `Component`s context.
   *
   * @callback Component~GenericCallback
   * @this Component
   */

  /**
   * Creates a function that runs after an `x` millisecond timeout. This function is a
   * wrapper around `window.setTimeout`. There are a few reasons to use this one
   * instead though:
   * 1. It gets cleared via  {@link Component#clearTimeout} when
   *    {@link Component#dispose} gets called.
   * 2. The function callback will gets turned into a {@link Component~GenericCallback}
   *
   * > Note: You can't use `window.clearTimeout` on the id returned by this function. This
   *         will cause its dispose listener not to get cleaned up! Please use
   *         {@link Component#clearTimeout} or {@link Component#dispose} instead.
   *
   * @param {Component~GenericCallback} fn
   *        The function that will be run after `timeout`.
   *
   * @param {number} timeout
   *        Timeout in milliseconds to delay before executing the specified function.
   *
   * @return {number}
   *         Returns a timeout ID that gets used to identify the timeout. It can also
   *         get used in {@link Component#clearTimeout} to clear the timeout that
   *         was set.
   *
   * @listens Component#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
   */
  setTimeout(fn, timeout) {
    // declare as variables so they are properly available in timeout function
    // eslint-disable-next-line
    var timeoutId, disposeFn;

    fn = Fn.bind_(this, fn);

    this.clearTimersOnDispose_();

    timeoutId = window.setTimeout(() => {
      if (this.setTimeoutIds_.has(timeoutId)) {
        this.setTimeoutIds_.delete(timeoutId);
      }
      fn();
    }, timeout);

    this.setTimeoutIds_.add(timeoutId);

    return timeoutId;
  }

  /**
   * Clears a timeout that gets created via `window.setTimeout` or
   * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
   * use this function instead of `window.clearTimout`. If you don't your dispose
   * listener will not get cleaned up until {@link Component#dispose}!
   *
   * @param {number} timeoutId
   *        The id of the timeout to clear. The return value of
   *        {@link Component#setTimeout} or `window.setTimeout`.
   *
   * @return {number}
   *         Returns the timeout id that was cleared.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
   */
  clearTimeout(timeoutId) {
    if (this.setTimeoutIds_.has(timeoutId)) {
      this.setTimeoutIds_.delete(timeoutId);
      window.clearTimeout(timeoutId);
    }

    return timeoutId;
  }

  /**
   * Creates a function that gets run every `x` milliseconds. This function is a wrapper
   * around `window.setInterval`. There are a few reasons to use this one instead though.
   * 1. It gets cleared via  {@link Component#clearInterval} when
   *    {@link Component#dispose} gets called.
   * 2. The function callback will be a {@link Component~GenericCallback}
   *
   * @param {Component~GenericCallback} fn
   *        The function to run every `x` seconds.
   *
   * @param {number} interval
   *        Execute the specified function every `x` milliseconds.
   *
   * @return {number}
   *         Returns an id that can be used to identify the interval. It can also be be used in
   *         {@link Component#clearInterval} to clear the interval.
   *
   * @listens Component#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
   */
  setInterval(fn, interval) {
    fn = Fn.bind_(this, fn);

    this.clearTimersOnDispose_();

    const intervalId = window.setInterval(fn, interval);

    this.setIntervalIds_.add(intervalId);

    return intervalId;
  }

  /**
   * Clears an interval that gets created via `window.setInterval` or
   * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
   * use this function instead of `window.clearInterval`. If you don't your dispose
   * listener will not get cleaned up until {@link Component#dispose}!
   *
   * @param {number} intervalId
   *        The id of the interval to clear. The return value of
   *        {@link Component#setInterval} or `window.setInterval`.
   *
   * @return {number}
   *         Returns the interval id that was cleared.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
   */
  clearInterval(intervalId) {
    if (this.setIntervalIds_.has(intervalId)) {
      this.setIntervalIds_.delete(intervalId);
      window.clearInterval(intervalId);
    }

    return intervalId;
  }

  /**
   * Queues up a callback to be passed to requestAnimationFrame (rAF), but
   * with a few extra bonuses:
   *
   * - Supports browsers that do not support rAF by falling back to
   *   {@link Component#setTimeout}.
   *
   * - The callback is turned into a {@link Component~GenericCallback} (i.e.
   *   bound to the component).
   *
   * - Automatic cancellation of the rAF callback is handled if the component
   *   is disposed before it is called.
   *
   * @param  {Component~GenericCallback} fn
   *         A function that will be bound to this component and executed just
   *         before the browser's next repaint.
   *
   * @return {number}
   *         Returns an rAF ID that gets used to identify the timeout. It can
   *         also be used in {@link Component#cancelAnimationFrame} to cancel
   *         the animation frame callback.
   *
   * @listens Component#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
   */
  requestAnimationFrame(fn) {
    this.clearTimersOnDispose_();

    // declare as variables so they are properly available in rAF function
    // eslint-disable-next-line
    var id;
    fn = Fn.bind_(this, fn);

    id = window.requestAnimationFrame(() => {
      if (this.rafIds_.has(id)) {
        this.rafIds_.delete(id);
      }
      fn();
    });
    this.rafIds_.add(id);

    return id;
  }

  /**
   * Request an animation frame, but only one named animation
   * frame will be queued. Another will never be added until
   * the previous one finishes.
   *
   * @param {string} name
   *        The name to give this requestAnimationFrame
   *
   * @param  {Component~GenericCallback} fn
   *         A function that will be bound to this component and executed just
   *         before the browser's next repaint.
   */
  requestNamedAnimationFrame(name, fn) {
    if (this.namedRafs_.has(name)) {
      return;
    }
    this.clearTimersOnDispose_();

    fn = Fn.bind_(this, fn);

    const id = this.requestAnimationFrame(() => {
      fn();
      if (this.namedRafs_.has(name)) {
        this.namedRafs_.delete(name);
      }
    });

    this.namedRafs_.set(name, id);

    return name;
  }

  /**
   * Cancels a current named animation frame if it exists.
   *
   * @param {string} name
   *        The name of the requestAnimationFrame to cancel.
   */
  cancelNamedAnimationFrame(name) {
    if (!this.namedRafs_.has(name)) {
      return;
    }

    this.cancelAnimationFrame(this.namedRafs_.get(name));
    this.namedRafs_.delete(name);
  }

  /**
   * Cancels a queued callback passed to {@link Component#requestAnimationFrame}
   * (rAF).
   *
   * If you queue an rAF callback via {@link Component#requestAnimationFrame},
   * use this function instead of `window.cancelAnimationFrame`. If you don't,
   * your dispose listener will not get cleaned up until {@link Component#dispose}!
   *
   * @param {number} id
   *        The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
   *
   * @return {number}
   *         Returns the rAF ID that was cleared.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
   */
  cancelAnimationFrame(id) {
    if (this.rafIds_.has(id)) {
      this.rafIds_.delete(id);
      window.cancelAnimationFrame(id);
    }

    return id;

  }

  /**
   * A function to setup `requestAnimationFrame`, `setTimeout`,
   * and `setInterval`, clearing on dispose.
   *
   * > Previously each timer added and removed dispose listeners on it's own.
   * For better performance it was decided to batch them all, and use `Set`s
   * to track outstanding timer ids.
   *
   * @private
   */
  clearTimersOnDispose_() {
    if (this.clearingTimersOnDispose_) {
      return;
    }

    this.clearingTimersOnDispose_ = true;
    this.one('dispose', () => {
      [
        ['namedRafs_', 'cancelNamedAnimationFrame'],
        ['rafIds_', 'cancelAnimationFrame'],
        ['setTimeoutIds_', 'clearTimeout'],
        ['setIntervalIds_', 'clearInterval']
      ].forEach(([idName, cancelName]) => {
        // for a `Set` key will actually be the value again
        // so forEach((val, val) =>` but for maps we want to use
        // the key.
        this[idName].forEach((val, key) => this[cancelName](key));
      });

      this.clearingTimersOnDispose_ = false;
    });
  }

  /**
   * Register a `Component` with `videojs` given the name and the component.
   *
   * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
   *         should be registered using {@link Tech.registerTech} or
   *         {@link videojs:videojs.registerTech}.
   *
   * > NOTE: This function can also be seen on videojs as
   *         {@link videojs:videojs.registerComponent}.
   *
   * @param {string} name
   *        The name of the `Component` to register.
   *
   * @param {Component} ComponentToRegister
   *        The `Component` class to register.
   *
   * @return {Component}
   *         The `Component` that was registered.
   */
  static registerComponent(name, ComponentToRegister) {
    if (typeof name !== 'string' || !name) {
      throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
    }

    const Tech = Component.getComponent('Tech');

    // We need to make sure this check is only done if Tech has been registered.
    const isTech = Tech && Tech.isTech(ComponentToRegister);
    const isComp = Component === ComponentToRegister ||
      Component.prototype.isPrototypeOf(ComponentToRegister.prototype);

    if (isTech || !isComp) {
      let reason;

      if (isTech) {
        reason = 'techs must be registered using Tech.registerTech()';
      } else {
        reason = 'must be a Component subclass';
      }

      throw new Error(`Illegal component, "${name}"; ${reason}.`);
    }

    name = toTitleCase(name);

    if (!Component.components_) {
      Component.components_ = {};
    }

    const Player = Component.getComponent('Player');

    if (name === 'Player' && Player && Player.players) {
      const players = Player.players;
      const playerNames = Object.keys(players);

      // If we have players that were disposed, then their name will still be
      // in Players.players. So, we must loop through and verify that the value
      // for each item is not null. This allows registration of the Player component
      // after all players have been disposed or before any were created.
      if (players &&
          playerNames.length > 0 &&
          playerNames.map((pname) => players[pname]).every(Boolean)) {
        throw new Error('Can not register Player component after player has been created.');
      }
    }

    Component.components_[name] = ComponentToRegister;
    Component.components_[toLowerCase(name)] = ComponentToRegister;

    return ComponentToRegister;
  }

  /**
   * Get a `Component` based on the name it was registered with.
   *
   * @param {string} name
   *        The Name of the component to get.
   *
   * @return {Component}
   *         The `Component` that got registered under the given name.
   */
  static getComponent(name) {
    if (!name || !Component.components_) {
      return;
    }

    return Component.components_[name];
  }
}

Component.registerComponent('Component', Component);

export default Component;