<!DOCTYPE html><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>Tech</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"/>

API Index

Tech Methods

Class Methods

addRemoteTextTrack

addTextTrack

buffered

bufferedPercent

dispose

emulateTextTracks

error

getComponent

initTextTrackListeners

manualProgressOff

manualProgressOn

manualTimeUpdatesOff

manualTimeUpdatesOn

onDurationChange

played

registerComponent

remoteTextTrackEls

remoteTextTracks

removeRemoteTextTrack

reset

setCurrentTime

setPoster

stopTrackingCurrentTime

stopTrackingProgress

textTracks

trackCurrentTime

trackProgress

Inherited Methods from Component

$

$$

addChild

addClass

buildCSSClass

children

clearInterval

clearTimeout

contentEl

createEl

dimensions

el

enableTouchActivity

extend

getChild

getChildById

hasClass

height

hide

id

initChildren

name

off

on

one

options

player

ready

removeChild

removeClass

setInterval

setTimeout

show

toggleClass

trigger

triggerReady

width

Tech

Base class for media (HTML5 Video, Flash) controllers

DEFINED IN: tech.js line number: 21

EXTENDS: component.js

Constructor

Tech( [options],[ready] )

Parameters

name	Type	Required	Description

options	Object	no	Options object

ready	function	no	Ready callback function

Methods

addRemoteTextTrack( options )

Creates a remote text track object and returns a emulated html track element

Parameters

name	Type	Required	Description

options	Object	yes	The object should contain values for
kind, language, label and src (location of the WebVTT file)

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

[back to top]

addTextTrack( kind, [label], [language] )

Creates and returns a remote text track object

Parameters

name	Type	Required	Description

kind	String	yes	Text track kind (subtitles, captions, descriptions
                                      chapters and metadata)

label	String	no	Label to identify the text track

language	String	no	Two letter language abbreviation

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

[back to top]

buffered()

Create and get TimeRange object for buffering

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

[back to top]

bufferedPercent()

Get buffered percent

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

[back to top]

dispose()

Turn off any manual progress or timeupdate tracking

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

[back to top]

emulateTextTracks()

Emulate texttracks

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

[back to top]

error( [err] )

When invoked without an argument, returns a MediaError object
representing the current error state of the player or null if
there is no error. When invoked with an argument, set the current
error state of the player.

Parameters

name	Type	Required	Description

err	MediaError	no	Optional an error object

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

[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/tech/tech.js line number: 509

[back to top]

initTextTrackListeners()

Initialize texttrack listeners

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

[back to top]

manualProgressOff()

Turn off progress events

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

[back to top]

manualProgressOn()

Turn on progress events

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

[back to top]

manualTimeUpdatesOff()

Remove event listeners for on play and pause and tracking current time

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

[back to top]

manualTimeUpdatesOn()

Set event listeners for on play and pause and tracking current time

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

[back to top]

onDurationChange()

Update duration

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

[back to top]

played()

Return the time ranges that have been played through for the
current source. This implementation is incomplete. It does not
track the played time ranges, only whether the source has played
at all or not.

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

[back to top]

static registerComponent( name, tech )

Registers a Tech

Parameters

name	Type	Required	Description

name	String	yes	Name of the Tech to register

tech	Object	yes	The tech to register

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

[back to top]

remoteTextTrackEls()

Get remote htmltrackelements

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

[back to top]

remoteTextTracks()

Get remote texttracks

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

[back to top]

removeRemoteTextTrack( track )

Remove remote texttrack

Parameters

name	Type	Required	Description

track	TextTrackObject	yes	Texttrack to remove

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

[back to top]

reset()

Reset the tech. Removes all sources and resets readyState.

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

[back to top]

setCurrentTime()

Set current time

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

[back to top]

setPoster()

Provide a default setPoster method for techs
Poster support for techs should be optional, so we don't want techs to
break if they don't have a way to set a poster.

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

[back to top]

stopTrackingCurrentTime()

Turn off play progress tracking (when paused or dragging)

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

[back to top]

stopTrackingProgress()

Stops tracking progress by clearing progress interval

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

[back to top]

textTracks()

Get texttracks

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

[back to top]

trackCurrentTime()

Tracks current time

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

[back to top]

trackProgress()

Track progress

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

[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]

createEl( [tagName], [properties], [attributes] )

Create the component's DOM element

Parameters

name	Type	Required	Description

tagName	String	no	Element's node type. e.g. 'div'

properties	Object	no	An object of properties that should be set

attributes	Object	no	An object of attributes that should be set

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

[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]

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]

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]

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]


keep keep replace replace replace replace replace replace replace replace replace replace replace keep keep keep keep

assert.equal(errorSpy.callCount, 1, 'vttjserror handler not called again');

/* eslint-env qunit */
import window from 'global/window';
import EventTarget from '../../../src/js/event-target.js';
import TrackBaseline from './track-baseline';
import TechFaker from '../tech/tech-faker';
import TextTrack from '../../../src/js/tracks/text-track.js';
import TestHelpers from '../test-helpers.js';
import sinon from 'sinon';
import log from '../../../src/js/utils/log.js';

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

// do baseline track testing
TrackBaseline(TextTrack, {
  id: '1',
  kind: 'subtitles',
  mode: 'disabled',
  label: 'English',
  language: 'en'
  // tech is added in baseline
  // tech: new TechFaker()
});

QUnit.test('requires a tech', function(assert) {
  const error = new Error('A tech was not provided.');

  assert.throws(() => new TextTrack({}), error, 'a tech is required');
  assert.throws(() => new TextTrack({tech: null}), error, 'a tech is required');
});

QUnit.test('can create a TextTrack with a mode property', function(assert) {
  const mode = 'disabled';
  const tt = new TextTrack({
    mode,
    tech: this.tech
  });

  assert.equal(tt.mode, mode, 'we have a mode');
});

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

  assert.equal(tt.kind, 'subtitles', 'kind defaulted to subtitles');
  assert.equal(tt.mode, 'disabled', 'mode defaulted to disabled');
  assert.equal(tt.label, '', 'label defaults to empty string');
  assert.equal(tt.language, '', 'language defaults to empty string');
});

QUnit.test('kind can only be one of several options, defaults to subtitles', function(assert) {
  let tt = new TextTrack({
    tech: this.tech,
    kind: 'foo'
  });

  assert.equal(tt.kind, 'subtitles', 'the kind is set to subtitles, not foo');
  assert.notEqual(tt.kind, 'foo', 'the kind is set to subtitles, not foo');

  tt = new TextTrack({
    tech: this.tech,
    kind: 'subtitles'
  });

  assert.equal(tt.kind, 'subtitles', 'the kind is set to subtitles');

  tt = new TextTrack({
    tech: this.tech,
    kind: 'captions'
  });

  assert.equal(tt.kind, 'captions', 'the kind is set to captions');

  tt = new TextTrack({
    tech: this.tech,
    kind: 'descriptions'
  });

  assert.equal(tt.kind, 'descriptions', 'the kind is set to descriptions');

  tt = new TextTrack({
    tech: this.tech,
    kind: 'chapters'
  });

  assert.equal(tt.kind, 'chapters', 'the kind is set to chapters');

  tt = new TextTrack({
    tech: this.tech,
    kind: 'metadata'
  });

  assert.equal(tt.kind, 'metadata', 'the kind is set to metadata');
});

QUnit.test('mode can only be one of several options, defaults to disabled', function(assert) {
  let tt = new TextTrack({
    tech: this.tech,
    mode: 'foo'
  });

  assert.equal(tt.mode, 'disabled', 'the mode is set to disabled, not foo');
  assert.notEqual(tt.mode, 'foo', 'the mode is set to disabld, not foo');

  tt = new TextTrack({
    tech: this.tech,
    mode: 'disabled'
  });

  assert.equal(tt.mode, 'disabled', 'the mode is set to disabled');

  tt = new TextTrack({
    tech: this.tech,
    mode: 'hidden'
  });

  assert.equal(tt.mode, 'hidden', 'the mode is set to hidden');

  tt = new TextTrack({
    tech: this.tech,
    mode: 'showing'
  });

  assert.equal(tt.mode, 'showing', 'the mode is set to showing');
});

QUnit.test('cue and activeCues are read only', function(assert) {
  const mode = 'disabled';
  const tt = new TextTrack({
    mode,
    tech: this.tech
  });

  tt.cues = 'foo';
  tt.activeCues = 'bar';

  assert.notEqual(tt.cues, 'foo', 'cues is still original value');
  assert.notEqual(tt.activeCues, 'bar', 'activeCues is still original value');
});

QUnit.test('mode can only be set to a few options', function(assert) {
  const tt = new TextTrack({
    tech: this.tech
  });

  tt.mode = 'foo';

  assert.notEqual(tt.mode, 'foo', 'the mode is still the old value, disabled');
  assert.equal(tt.mode, 'disabled', 'still on the default mode, disabled');

  tt.mode = 'hidden';
  assert.equal(tt.mode, 'hidden', 'mode set to hidden');

  tt.mode = 'bar';
  assert.notEqual(tt.mode, 'bar', 'the mode is still the old value, hidden');
  assert.equal(tt.mode, 'hidden', 'still on the previous mode, hidden');

  tt.mode = 'showing';
  assert.equal(tt.mode, 'showing', 'mode set to showing');

  tt.mode = 'baz';
  assert.notEqual(tt.mode, 'baz', 'the mode is still the old value, showing');
  assert.equal(tt.mode, 'showing', 'still on the previous mode, showing');
});

QUnit.test('cues and activeCues return a TextTrackCueList', function(assert) {
  const tt = new TextTrack({
    tech: this.tech
  });

  assert.ok(tt.cues.getCueById, 'cues are a TextTrackCueList');
  assert.ok(tt.activeCues.getCueById, 'activeCues are a TextTrackCueList');
});

QUnit.test('cues can be added and removed from a TextTrack', function(assert) {
  const tt = new TextTrack({
    tech: this.tech
  });
  const cues = tt.cues;

  assert.equal(cues.length, 0, 'start with zero cues');

  tt.addCue({id: '1'});

  assert.equal(cues.length, 1, 'we have one cue');

  tt.removeCue(cues.getCueById('1'));

  assert.equal(cues.length, 0, 'we have removed our one cue');

  tt.addCue({id: '1'});
  tt.addCue({id: '2'});
  tt.addCue({id: '3'});

  assert.equal(cues.length, 3, 'we now have 3 cues');
});

QUnit.test('original cue can be used to remove cue from cues list', function(assert) {
  const tt = new TextTrack({
    tech: this.tech
  });
  const Cue = window.VTTCue ||
              window.vttjs && window.vttjs.VTTCue ||
              window.TextTrackCue;

  const cue1 = new Cue(0, 1, 'some-cue');

  assert.equal(tt.cues.length, 0, 'start with zero cues');
  tt.addCue(cue1);
  assert.equal(tt.cues.length, 1, 'we have one cue');

  tt.removeCue(cue1);
  assert.equal(tt.cues.length, 0, 'we have removed cue1');
});

QUnit.test('can only remove one cue at a time', function(assert) {
  const tt = new TextTrack({
    tech: this.tech
  });
  const Cue = window.VTTCue ||
              window.vttjs && window.vttjs.VTTCue ||
              window.TextTrackCue;

  const cue1 = new Cue(0, 1, 'some-cue');

  assert.equal(tt.cues.length, 0, 'start with zero cues');
  tt.addCue(cue1);
  tt.addCue(cue1);
  assert.equal(tt.cues.length, 2, 'we have two cues');

  tt.removeCue(cue1);
  assert.equal(tt.cues.length, 1, 'we have removed one instance of cue1');

  tt.removeCue(cue1);
  assert.equal(tt.cues.length, 0, 'we have removed the other instance of cue1');
});

QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
  const done = assert.async();
  const player = TestHelpers.makePlayer({techfaker: {autoReady: false}});
  let changes = 0;
  const tt = new TextTrack({
    tech: player.tech_,
    mode: 'showing'
  });
  const cuechangeHandler = function() {
    changes++;
  };

  tt.addCue({
    id: '1',
    startTime: 0,
    endTime: 5
  });

  tt.oncuechange = cuechangeHandler;
  tt.addEventListener('cuechange', cuechangeHandler);

  player.tech_.currentTime = function() {
    return 0;
  };

  player.tech_.trigger('timeupdate');
  assert.equal(changes, 0, 'a cuechange event is not triggered');

  player.tech_.on('ready', function() {
    player.tech_.currentTime = function() {
      return 0.2;
    };

    player.tech_.trigger('timeupdate');

    assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange');

    tt.off();
    player.dispose();
    done();
  });
  player.tech_.triggerReady();
});

QUnit.test('fires cuechange when cues become active and inactive', function(assert) {
  const player = TestHelpers.makePlayer();
  let changes = 0;
  const tt = new TextTrack({
    tech: player.tech_,
    mode: 'showing'
  });
  const cuechangeHandler = function() {
    changes++;
  };

  tt.addCue({
    id: '1',
    startTime: 1,
    endTime: 5
  });

  tt.oncuechange = cuechangeHandler;
  tt.addEventListener('cuechange', cuechangeHandler);

  player.tech_.currentTime = function() {
    return 2;
  };

  player.tech_.trigger('timeupdate');

  assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange');

  player.tech_.currentTime = function() {
    return 7;
  };

  player.tech_.trigger('timeupdate');

  assert.equal(changes, 4, 'a cuechange event trigger addEventListener and oncuechange');

  tt.off();
  player.dispose();
});

QUnit.test('enabled and disabled cuechange handler when changing mode to hidden', function(assert) {
  const player = TestHelpers.makePlayer();
  let changes = 0;
  const tt = new TextTrack({
    tech: player.tech_
  });
  const cuechangeHandler = function() {
    changes++;
  };

  tt.mode = 'hidden';

  tt.addCue({
    id: '1',
    startTime: 1,
    endTime: 5
  });

  tt.addEventListener('cuechange', cuechangeHandler);

  player.tech_.currentTime = function() {
    return 2;
  };
  player.tech_.trigger('timeupdate');

  assert.equal(changes, 1, 'a cuechange event trigger');

  changes = 0;
  tt.mode = 'disabled';

  player.tech_.currentTime = function() {
    return 7;
  };
  player.tech_.trigger('timeupdate');

  assert.equal(changes, 0, 'NO cuechange event trigger');

  tt.off();
  player.dispose();
});

QUnit.test('enabled and disabled cuechange handler when changing mode to showing', function(assert) {
  const player = TestHelpers.makePlayer();
  let changes = 0;
  const tt = new TextTrack({
    tech: player.tech_
  });
  const cuechangeHandler = function() {
    changes++;
  };

  tt.mode = 'showing';

  tt.addCue({
    id: '1',
    startTime: 1,
    endTime: 5
  });

  tt.addEventListener('cuechange', cuechangeHandler);

  player.tech_.currentTime = function() {
    return 2;
  };
  player.tech_.trigger('timeupdate');

  assert.equal(changes, 1, 'a cuechange event trigger');

  changes = 0;
  tt.mode = 'disabled';

  player.tech_.currentTime = function() {
    return 7;
  };
  player.tech_.trigger('timeupdate');

  assert.equal(changes, 0, 'NO cuechange event trigger');

  tt.off();
  player.dispose();
});

QUnit.test('tracks are parsed if vttjs is loaded', function(assert) {
  const clock = sinon.useFakeTimers();
  const oldVTT = window.WebVTT;
  let parserCreated = false;
  const reqs = [];

  window.xhr.onCreate = function(req) {
    reqs.push(req);
  };

  window.WebVTT = () => {};
  window.WebVTT.StringDecoder = () => {};
  window.WebVTT.Parser = () => {
    parserCreated = true;
    return {
      oncue() {},
      onparsingerror() {},
      onflush() {},
      parse() {},
      flush() {}
    };
  };

  const tt = new TextTrack({
    tech: this.tech,
    src: 'http://example.com'
  });

  reqs.pop().respond(200, null, 'WEBVTT\n');

  assert.ok(parserCreated, 'WebVTT is loaded, so we can just parse');

  clock.restore();
  tt.off();
  window.WebVTT = oldVTT;
});

QUnit.test('tracks are parsed once vttjs is loaded', function(assert) {
  const clock = sinon.useFakeTimers();
  const oldVTT = window.WebVTT;
  let parserCreated = false;
  const reqs = [];

  window.xhr.onCreate = function(req) {
    reqs.push(req);
  };

  window.WebVTT = true;

  const testTech = new EventTarget();

  testTech.textTracks = () => {};
  testTech.currentTime = () => {};

  const tt = new TextTrack({
    tech: testTech,
    src: 'http://example.com'
  });

  reqs.pop().respond(200, null, 'WEBVTT\n');

  assert.ok(!parserCreated, 'WebVTT is not loaded, do not try to parse yet');

  clock.tick(100);
  assert.ok(!parserCreated, 'WebVTT still not loaded, do not try to parse yet');

  window.WebVTT = () => {};
  window.WebVTT.StringDecoder = () => {};
  window.WebVTT.Parser = () => {
    parserCreated = true;
    return {
      oncue() {},
      onparsingerror() {},
      onflush() {},
      parse() {},
      flush() {}
    };
  };

  testTech.trigger('vttjsloaded');
  assert.ok(parserCreated, 'WebVTT is loaded, so we can parse now');

  clock.restore();
  tt.off();
  testTech.off();
  window.WebVTT = oldVTT;
});

QUnit.test('stops processing if vttjs loading errored out', function(assert) {
  const clock = sinon.useFakeTimers();
  const errorSpy = sinon.spy();
  const oldVTT = window.WebVTT;
  const oldLogError = log.error;
  const parserCreated = false;
  const reqs = [];

  window.xhr.onCreate = function(req) {
    reqs.push(req);
  };

  log.error = errorSpy;

  window.WebVTT = true;

  const testTech = new EventTarget();

  testTech.textTracks = () => {};
  testTech.currentTime = () => {};

  sinon.stub(testTech, 'off');
  testTech.off.withArgs('vttjsloaded');

  const tt = new TextTrack({
    tech: testTech,
    src: 'http://example.com'
  });

  reqs.pop().respond(200, null, 'WEBVTT\n');

  assert.ok(!parserCreated, 'WebVTT is not loaded, do not try to parse yet');

  testTech.trigger('vttjserror');
  const offSpyCall = testTech.off.getCall(0);

  assert.ok(errorSpy.called, 'vttjs failed to load, so log.error was called');
  if (errorSpy.called) {
    assert.ok(
      /^vttjs failed to load, stopping trying to process/.test(errorSpy.getCall(0).args[0]),
      'log.error was called with the expected message'
    );
  }
  assert.ok(!parserCreated, 'WebVTT is not loaded, do not try to parse yet');
  assert.ok(offSpyCall, 'tech.off was called');

  clock.restore();
  window.WebVTT = oldVTT;
  tt.off();
  testTech.off.restore();
  testTech.off();
  log.error = oldLogError;
});


/**
 * @file url.js
 * @module url
 */
import document from 'global/document';
import window from 'global/window';

/**
 * @typedef {Object} url:URLObject
 *
 * @property {string} protocol
 *           The protocol of the url that was parsed.
 *
 * @property {string} hostname
 *           The hostname of the url that was parsed.
 *
 * @property {string} port
 *           The port of the url that was parsed.
 *
 * @property {string} pathname
 *           The pathname of the url that was parsed.
 *
 * @property {string} search
 *           The search query of the url that was parsed.
 *
 * @property {string} hash
 *           The hash of the url that was parsed.
 *
 * @property {string} host
 *           The host of the url that was parsed.
 */

/**
 * Resolve and parse the elements of a URL.
 *
 * @function
 * @param    {String} url
 *           The url to parse
 *
 * @return   {url:URLObject}
 *           An object of url details
 */
export const parseUrl = function(url) {
  const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];

  // add the url to an anchor and let the browser parse the URL
  let a = document.createElement('a');

  a.href = url;

  // IE8 (and 9?) Fix
  // ie8 doesn't parse the URL correctly until the anchor is actually
  // added to the body, and an innerHTML is needed to trigger the parsing
  const addToBody = (a.host === '' && a.protocol !== 'file:');
  let div;

  if (addToBody) {
    div = document.createElement('div');
    div.innerHTML = `<a href="${url}"></a>`;
    a = div.firstChild;
    // prevent the div from affecting layout
    div.setAttribute('style', 'display:none; position:absolute;');
    document.body.appendChild(div);
  }

  // Copy the specific URL properties to a new object
  // This is also needed for IE8 because the anchor loses its
  // properties when it's removed from the dom
  const details = {};

  for (let i = 0; i < props.length; i++) {
    details[props[i]] = a[props[i]];
  }

  // IE9 adds the port to the host property unlike everyone else. If
  // a port identifier is added for standard ports, strip it.
  if (details.protocol === 'http:') {
    details.host = details.host.replace(/:80$/, '');
  }

  if (details.protocol === 'https:') {
    details.host = details.host.replace(/:443$/, '');
  }

  if (!details.protocol) {
    details.protocol = window.location.protocol;
  }

  if (addToBody) {
    document.body.removeChild(div);
  }

  return details;
};

/**
 * Get absolute version of relative URL. Used to tell Flash the correct URL.
 *
 * @function
 * @param    {string} url
 *           URL to make absolute
 *
 * @return   {string}
 *           Absolute URL
 *
 * @see      http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
 */
export const getAbsoluteURL = function(url) {
  // Check if absolute URL
  if (!url.match(/^https?:\/\//)) {
    // Convert to absolute URL. Flash hosted off-site needs an absolute URL.
    const div = document.createElement('div');

    div.innerHTML = `<a href="${url}">x</a>`;
    url = div.firstChild.href;
  }

  return url;
};

/**
 * Returns the extension of the passed file name. It will return an empty string
 * if passed an invalid path.
 *
 * @function
 * @param    {string} path
 *           The fileName path like '/path/to/file.mp4'
 *
 * @return  {string}
 *           The extension in lower case or an empty string if no
 *           extension could be found.
 */
export const getFileExtension = function(path) {
  if (typeof path === 'string') {
    const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
    const pathParts = splitPathRe.exec(path);

    if (pathParts) {
      return pathParts.pop().toLowerCase();
    }
  }

  return '';
};

/**
 * Returns whether the url passed is a cross domain request or not.
 *
 * @function
 * @param    {string} url
 *           The url to check.
 *
 * @return   {boolean}
 *           Whether it is a cross domain request or not.
 */
export const isCrossOrigin = function(url) {
  const winLoc = window.location;
  const urlInfo = parseUrl(url);

  // IE8 protocol relative urls will return ':' for protocol
  const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol;

  // Check if url is for another domain/origin
  // IE8 doesn't know location.origin, so we won't rely on it here
  const crossOrigin = (srcProtocol + urlInfo.host) !== (winLoc.protocol + winLoc.host);

  return crossOrigin;
};


// Fake a media playback tech controller so that player tests
// can run without HTML5 or Flash, of which PhantomJS supports neither.
import Tech from '../../../src/js/tech/tech.js';
import {createTimeRanges} from '../../../src/js/utils/time-ranges.js';
/**
 * @class
 */
class TechFaker extends Tech {

  constructor(options, handleReady) {
    super(options, handleReady);

    this.featuresPlaybackRate = true;
    this.defaultPlaybackRate_ = 1;
    this.playbackRate_ = 1;
    this.currentTime_ = 0;

    if (this.options_ && this.options_.sourceset) {
      this.fakeSourceset();
    }
    if (!options || options.autoReady !== false) {
      this.triggerReady();
    }
  }

  createEl() {
    const el = super.createEl('div', {
      className: 'vjs-tech'
    });

    return el;
  }

  // fake a poster attribute to mimic the video element
  poster() {
    return this.el().poster;
  }
  setPoster(val) {
    this.el().poster = val;
    this.trigger('posterchange');
  }

  setControls(val) {}

  setVolume(value) {
    this.volume_ = value;
  }

  setMuted() {}

  setAutoplay(v) {
    if (!v) {
      this.options_.autoplay = false;
    }

    this.options_.autoplay = true;
  }

  defaultPlaybackRate(value) {
    if (value !== undefined) {
      this.defaultPlaybackRate_ = parseFloat(value);
    }
    return this.defaultPlaybackRate_;
  }

  setPlaybackRate(value) {
    const last = this.playbackRate_;

    this.playbackRate_ = parseFloat(value);

    if (value !== last) {
      this.trigger('ratechange');
    }
  }

  playbackRate() {
    return this.playbackRate_;
  }

  setCurrentTime(value) {
    const last = this.currentTime_;

    this.currentTime_ = parseFloat(value);

    if (value !== last) {
      this.trigger('timeupdate');
    }
  }

  currentTime() {
    return this.currentTime_;
  }

  seekable() {
    return createTimeRanges(0, 0);
  }
  seeking() {
    return false;
  }
  fakeSourceset() {
    this.el_.src = this.options_.sourceset;
    this.el_.setAttribute('src', this.options_.sourceset);
    super.triggerSourceset(this.options_.sourceset);
  }
  src(src) {
    if (typeof src !== 'undefined' && this.options_ && this.options_.sourceset) {
      this.fakeSourceset();
    }
    return 'movie.mp4';
  }
  load() {
  }
  currentSrc() {
    return 'movie.mp4';
  }
  volume() {
    return this.volume_ || 0;
  }
  muted() {
    return false;
  }
  autoplay() {
    return this.options_.autoplay || false;
  }
  pause() {
    return false;
  }
  paused() {
    return true;
  }
  loop() {
    return false;
  }
  play() {
    this.trigger('play');
  }
  supportsFullScreen() {
    return false;
  }
  buffered() {
    return {};
  }
  duration() {
    return {};
  }
  networkState() {
    return 0;
  }
  readyState() {
    return 0;
  }
  controls() {
    return false;
  }

  // Support everything except for "video/unsupported-format"
  static isSupported() {
    return true;
  }
  static canPlayType(type) {
    return (type !== 'video/unsupported-format' ? 'maybe' : '');
  }
  static canPlaySource(srcObj) {
    return srcObj.type !== 'video/unsupported-format';
  }
}

Tech.registerTech('TechFaker', TechFaker);
export default TechFaker;


<!DOCTYPE html>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<link rel="stylesheet" type="text/css" href="../css/guides.css">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700,700italic,600,600italic' rel='stylesheet' type='text/css'><link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.8.0/styles/solarized_light.min.css" />

Tracks

There are currently three types of tracks

AudioTracks - allows the selection of alternative AudioTracks for a video

VideoTracks - allows the selection of an alternative VideoTrack for a video

TextTracks - Text Tracks are used to display subtitles and captions, and add a menu for navigating between chapters in a video.


const placeholder: Placeholder = memoizedPlaceholder(

keep keep keep replace replace keep keep keep keep

// @flow
/* eslint-disable react/no-multi-comp */
import React, { Component } from 'react';
import { mount } from 'enzyme';
import Droppable, { makeSelector } from '../../../src/view/droppable/connected-droppable';
import noImpact from '../../../src/state/no-impact';
import { getDraggableDimension } from '../../../src/state/dimension';
import { withStore } from '../../utils/get-context-options';
import getClientRect from '../../../src/state/get-client-rect';
import type {
  Phase,
  DragState,
  PendingDrop,
  Position,
  DraggableId,
  DroppableId,
  InitialDrag,
  DragImpact,
  DropResult,
  CurrentDrag,
  DraggableDimension,
  DraggableLocation,
  InitialDragLocation,
  CurrentDragLocation,
} from '../../../src/types';
import type {
  MapProps,
  Provided,
  Selector,
  Placeholder,
} from '../../../src/view/droppable/droppable-types';

type ExecuteArgs = {|
  droppableId: DroppableId,
  phase: Phase,
  drag: ?DragState,
  pending: ?PendingDrop,
  draggable: ?DraggableDimension,
  // being simple and defaulting to false (droppable is enabled)
  isDropDisabled?: boolean,
|}

const execute = (selector: Selector) =>
  ({ phase, drag, draggable, pending, droppableId, isDropDisabled = false }: ExecuteArgs) =>
    selector.resultFunc(
      phase, drag, draggable, pending, droppableId, isDropDisabled,
    );

const defaultMapProps: MapProps = {
  isDraggingOver: false,
  placeholder: null,
};

const homeDroppableId: DroppableId = 'home-id';
const foreignDroppableId: DroppableId = 'foreign-droppable';
const draggableId: DraggableId = 'drag-1';
const origin: Position = { x: 0, y: 0 };

type DragArgs = {|
  isDraggingOver: false | 'home' | 'foreign'
|}

const draggable: DraggableDimension = getDraggableDimension({
  id: draggableId,
  droppableId: homeDroppableId,
  clientRect: getClientRect({
    top: 100,
    left: 0,
    right: 100,
    bottom: 200,
  }),
});

const placeholder: Placeholder = {
  width: draggable.page.withoutMargin.width,
  height: draggable.page.withoutMargin.height,
};

const perform = (() => {
  const initial: InitialDrag = (() => {
    const client: InitialDragLocation = {
      selection: draggable.client.withoutMargin.center,
      center: draggable.client.withoutMargin.center,
    };

    const page: InitialDragLocation = {
      selection: draggable.page.withoutMargin.center,
      center: draggable.page.withoutMargin.center,
    };

    const value: InitialDrag = {
      source: {
        index: 0,
        droppableId: homeDroppableId,
      },
      client,
      page,
      windowScroll: origin,
      withinDroppable: {
        center: page.center,
      },
    };

    return value;
  })();

  const current: CurrentDrag = (() => {
    const client: CurrentDragLocation = {
      selection: initial.client.center,
      center: initial.client.center,
      offset: origin,
    };

    const page: CurrentDragLocation = {
      selection: initial.page.center,
      center: initial.page.center,
      offset: origin,
    };

    const value: CurrentDrag = {
      id: draggableId,
      type: 'TYPE',
      client,
      page,
      windowScroll: origin,
      withinDroppable: {
        center: page.center,
      },
      shouldAnimate: true,
      isScrollAllowed: true,
    };
    return value;
  })();

  const homeDestination: DraggableLocation = {
    index: initial.source.index + 1,
    droppableId: homeDroppableId,
  };
  const foreignDestination: DraggableLocation = {
    index: 0,
    droppableId: foreignDroppableId,
  };

  const dragOverHomeImpact: DragImpact = {
    movement: {
      draggables: [draggableId],
      amount: {
        y: draggable.page.withMargin.height,
        x: 0,
      },
      isBeyondStartPosition: true,
    },
    direction: 'vertical',
    destination: homeDestination,
  };

  const dragOverForeignImpact: DragImpact = {
    movement: {
      draggables: [],
      amount: {
        y: draggable.page.withMargin.height,
        x: 0,
      },
      isBeyondStartPosition: false,
    },
    direction: 'vertical',
    destination: foreignDestination,
  };

  const drag = ({ isDraggingOver }: DragArgs): DragState => {
    const impact: DragImpact = (() => {
      if (isDraggingOver === 'home') {
        return dragOverHomeImpact;
      }
      if (isDraggingOver === 'foreign') {
        return dragOverForeignImpact;
      }
      return noImpact;
    })();
    const state: DragState = {
      current,
      impact,
      initial,
    };
    return state;
  };

  const drop = ({ isDraggingOver }: DragArgs): PendingDrop => {
    // some made up position
    const dontCare: Position = {
      x: 100,
      y: 20,
    };

    const destination: ?DraggableLocation = (() => {
      if (isDraggingOver === 'home') {
        return homeDestination;
      }
      if (isDraggingOver === 'foreign') {
        return foreignDestination;
      }
      return null;
    })();

    const impact: DragImpact = drag({ isDraggingOver }).impact;

    const result: DropResult = {
      draggableId,
      type: 'TYPE',
      source: initial.source,
      destination,
    };

    const pending: PendingDrop = {
      trigger: 'DROP',
      newHomeOffset: dontCare,
      impact,
      result,
    };

    return pending;
  };

  return { drag, drop };
})();

describe('Droppable - connected', () => {
  describe('selector', () => {
    beforeEach(() => {
      jest.spyOn(console, 'error').mockImplementation(() => {});
    });

    afterEach(() => {
      console.error.mockRestore();
    });

    describe('dropping is disabled', () => {
      const phases: Phase[] = ['IDLE', 'COLLECTING_DIMENSIONS', 'DRAGGING', 'DROP_ANIMATING', 'DROP_COMPLETE'];

      it('should always return the default props', () => {
        phases.forEach((phase: Phase) => {
          const props: MapProps = execute(makeSelector())({
            phase,
            drag: null,
            pending: null,
            draggable: null,
            droppableId: homeDroppableId,
            isDropDisabled: true,
          });

          expect(props).toEqual(defaultMapProps);
        });
      });

      it('should not break memoization on multiple calls', () => {
        phases.forEach((phase: Phase) => {
          const selector = makeSelector();

          const first: MapProps = execute(selector)({
            phase,
            drag: null,
            pending: null,
            draggable: null,
            droppableId: homeDroppableId,
            isDropDisabled: true,
          });
          const second: MapProps = execute(selector)({
            phase,
            drag: null,
            pending: null,
            draggable: null,
            droppableId: homeDroppableId,
            isDropDisabled: true,
          });

          // checking object equality
          expect(first).toBe(second);
        });
      });

      it('should not break memoization between phases', () => {
        let previous: MapProps;
        const selector = makeSelector();

        phases.forEach((phase: Phase) => {
          const result: MapProps = execute(selector)({
            phase,
            drag: null,
            pending: null,
            draggable: null,
            droppableId: homeDroppableId,
            isDropDisabled: true,
          });

          // seed previous
          if (!previous) {
            previous = result;
            return;
          }

          // checking object equality
          expect(result).toBe(previous);
          expect(result).toEqual(defaultMapProps);
        });
      });
    });

    describe('while dragging', () => {
      it('should log an error an return the default props if no drag is available', () => {
        const props: MapProps = execute(makeSelector())({
          phase: 'DRAGGING',
          drag: null,
          draggable: null,
          pending: null,
          droppableId: homeDroppableId,
        });

        expect(props).toEqual(defaultMapProps);
        expect(console.error).toHaveBeenCalled();
      });

      describe('over home droppable', () => {
        it('should return that it is dragging over', () => {
          const expected: MapProps = {
            isDraggingOver: true,
            placeholder: null,
          };

          const props: MapProps = execute(makeSelector())({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: 'home' }),
            draggable,
            pending: null,
            droppableId: homeDroppableId,
          });

          expect(props).toEqual(expected);
        });

        it('should not break memoization on multiple drags', () => {
          const selector = makeSelector();
          const expected: MapProps = {
            isDraggingOver: true,
            placeholder: null,
          };

          const props1: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: 'home' }),
            pending: null,
            draggable,
            droppableId: homeDroppableId,
          });
          const props2: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: 'home' }),
            pending: null,
            draggable,
            droppableId: homeDroppableId,
          });

          // checking object equality
          expect(props1).toBe(props2);
          expect(props1).toEqual(expected);
          expect(props2).toEqual(expected);
        });
      });

      describe('over foreign droppable', () => {
        it('should return that it is dragging over and a placeholder', () => {
          const expected: MapProps = {
            isDraggingOver: true,
            placeholder,
          };

          const props: MapProps = execute(makeSelector())({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: 'foreign' }),
            draggable,
            pending: null,
            droppableId: foreignDroppableId,
          });

          expect(props).toEqual(expected);
        });

        it('should not break memoization on multiple drags', () => {
          const selector = makeSelector();
          const expected: MapProps = {
            isDraggingOver: true,
            placeholder,
          };

          const props1: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: 'foreign' }),
            pending: null,
            draggable,
            droppableId: foreignDroppableId,
          });
          const props2: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: 'foreign' }),
            pending: null,
            draggable,
            droppableId: foreignDroppableId,
          });

          // checking object equality
          expect(props1).toBe(props2);
          expect(props1.placeholder).toBe(props2.placeholder);
          expect(props1).toEqual(expected);
          expect(props2).toEqual(expected);
        });
      });

      describe('not dragging over', () => {
        it('should return that it is not dragging over', () => {
          const expected: MapProps = {
            isDraggingOver: false,
            placeholder: null,
          };

          const props: MapProps = execute(makeSelector())({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: false }),
            pending: null,
            draggable,
            droppableId: homeDroppableId,
          });

          expect(props).toEqual(expected);
        });

        it('should not break memoization on multiple drags', () => {
          const selector = makeSelector();
          const expected: MapProps = {
            isDraggingOver: false,
            placeholder: null,
          };

          const props1: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: false }),
            pending: null,
            draggable,
            droppableId: homeDroppableId,
          });
          const props2: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: false }),
            pending: null,
            draggable,
            droppableId: homeDroppableId,
          });

          // checking object equality
          expect(props1).toBe(props2);
          expect(props1).toEqual(expected);
          expect(props2).toEqual(expected);
        });
      });
    });

    describe('while drop animating', () => {
      it('should log an error an return the default props if no pending drop is available', () => {
        const props: MapProps = execute(makeSelector())({
          phase: 'DROP_ANIMATING',
          drag: null,
          pending: null,
          draggable,
          droppableId: homeDroppableId,
        });

        expect(props).toEqual(defaultMapProps);
        expect(console.error).toHaveBeenCalled();
      });

      describe('was dragging over home droppable', () => {
        it('should return that it is dragging over', () => {
          const expected: MapProps = {
            isDraggingOver: true,
            placeholder: null,
          };

          const props: MapProps = execute(makeSelector())({
            phase: 'DROP_ANIMATING',
            drag: null,
            draggable,
            pending: perform.drop({ isDraggingOver: 'home' }),
            droppableId: homeDroppableId,
          });

          expect(props).toEqual(expected);
        });

        it('should not break memoization from a previous DRAGGING phase', () => {
          const selector = makeSelector();
          const expected: MapProps = {
            isDraggingOver: true,
            placeholder: null,
          };

          const dragging: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: 'home' }),
            pending: null,
            draggable,
            droppableId: homeDroppableId,
          });
          const dropAnimating: MapProps = execute(selector)({
            phase: 'DROP_ANIMATING',
            drag: null,
            pending: perform.drop({ isDraggingOver: 'home' }),
            draggable,
            droppableId: homeDroppableId,
          });

          expect(dragging).toEqual(expected);
          expect(dropAnimating).toEqual(expected);
          // checking object equality
          expect(dragging).toBe(dropAnimating);
        });
      });

      describe('was dragging over foreign droppable', () => {
        it('should return that it is dragging over and provide a placeholder', () => {
          const expected: MapProps = {
            isDraggingOver: true,
            placeholder,
          };

          const props: MapProps = execute(makeSelector())({
            phase: 'DROP_ANIMATING',
            drag: null,
            pending: perform.drop({ isDraggingOver: 'foreign' }),
            draggable,
            droppableId: foreignDroppableId,
          });

          expect(props).toEqual(expected);
        });

        it('should not break memoization from a previous DRAGGING phase', () => {
          const selector = makeSelector();
          const expected: MapProps = {
            isDraggingOver: true,
            placeholder,
          };

          const dragging: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: 'foreign' }),
            pending: null,
            draggable,
            droppableId: foreignDroppableId,
          });
          const dropAnimating: MapProps = execute(selector)({
            phase: 'DROP_ANIMATING',
            drag: null,
            pending: perform.drop({ isDraggingOver: 'foreign' }),
            draggable,
            droppableId: foreignDroppableId,
          });

          expect(dragging).toEqual(expected);
          expect(dropAnimating).toEqual(expected);
          // checking object equality
          expect(dragging).toBe(dropAnimating);
        });
      });

      describe('not dragging over', () => {
        it('should return that it is not dragging over', () => {
          const expected: MapProps = {
            isDraggingOver: false,
            placeholder: null,
          };

          const props: MapProps = execute(makeSelector())({
            phase: 'DROP_ANIMATING',
            drag: null,
            draggable,
            pending: perform.drop({ isDraggingOver: false }),
            droppableId: homeDroppableId,
          });

          expect(props).toEqual(expected);
        });

        it('should not break memoization from a previous DRAGGING phase', () => {
          const selector = makeSelector();
          const expected: MapProps = {
            isDraggingOver: false,
            placeholder: null,
          };

          const dragging: MapProps = execute(selector)({
            phase: 'DRAGGING',
            drag: perform.drag({ isDraggingOver: false }),
            pending: null,
            draggable,
            droppableId: homeDroppableId,
          });
          const dropAnimating: MapProps = execute(selector)({
            phase: 'DROP_ANIMATING',
            drag: null,
            pending: perform.drop({ isDraggingOver: false }),
            draggable,
            droppableId: homeDroppableId,
          });

          expect(dragging).toEqual(expected);
          expect(dropAnimating).toEqual(expected);
          // checking object equality
          expect(dragging).toBe(dropAnimating);
        });
      });
    });

    describe('other phases', () => {
      const others: Phase[] = ['IDLE', 'COLLECTING_DIMENSIONS', 'DROP_COMPLETE'];

      it('should return the default props', () => {
        const selector = makeSelector();

        others.forEach((phase: Phase): void => {
          const props: MapProps = execute(selector)({
            phase,
            drag: null,
            pending: null,
            draggable: null,
            droppableId: homeDroppableId,
          });

          expect(props).toEqual(defaultMapProps);
        });
      });

      it('should not break memoization on multiple calls', () => {
        const selector = makeSelector();

        others.forEach((phase: Phase): void => {
          const first: MapProps = execute(selector)({
            phase,
            drag: null,
            pending: null,
            draggable: null,
            droppableId: homeDroppableId,
          });
          const second: MapProps = execute(selector)({
            phase,
            drag: null,
            pending: null,
            draggable: null,
            droppableId: homeDroppableId,
          });

          expect(first).toEqual(defaultMapProps);
          expect(second).toEqual(defaultMapProps);
          // checking object equality
          expect(first).toBe(second);
        });
      });
    });
  });

  describe('child render behavior', () => {
    class Person extends Component {
      props: {
        name: string,
        provided: Provided
      }

      render() {
        const { provided, name } = this.props;
        return (
          <div ref={ref => provided.innerRef(ref)}>
            hello {name}
          </div>
        );
      }
    }

    class App extends Component {
      props: {
        currentUser: string,
      }

      render() {
        return (
          <Droppable droppableId="drop-1">
            {(provided: Provided) => (
              <Person
                name={this.props.currentUser}
                provided={provided}
              />
            )}
          </Droppable>
        );
      }
    }

    beforeEach(() => {
      jest.spyOn(Person.prototype, 'render');
    });

    afterEach(() => {
      Person.prototype.render.mockRestore();
    });

    it('should render the child function when the parent renders', () => {
      const wrapper = mount(<App currentUser="Jake" />, withStore());

      // initial render causes two renders due to setting child ref
      expect(Person.prototype.render).toHaveBeenCalledTimes(2);
      expect(wrapper.find(Person).props().name).toBe('Jake');
    });

    it('should render the child function when the parent re-renders', () => {
      const wrapper = mount(<App currentUser="Jake" />, withStore());

      wrapper.update();

      // initial render causes two renders due to setting child ref
      expect(Person.prototype.render).toHaveBeenCalledTimes(3);
      expect(wrapper.find(Person).props().name).toBe('Jake');
    });

    it('should render the child function when the parents props changes that cause a re-render', () => {
      const wrapper = mount(<App currentUser="Jake" />, withStore());

      wrapper.setProps({
        currentUser: 'Finn',
      });

      // initial render causes two renders due to setting child ref
      expect(Person.prototype.render).toHaveBeenCalledTimes(3);
      expect(wrapper.find(Person).props().name).toBe('Finn');
    });
  });
});


// @flow
import { closest } from '../position';
import isWithin from '../is-within';
import type {
  Axis,
  Position,
  DroppableId,
  DimensionFragment,
  DroppableDimension,
  DroppableDimensionMap,
} from '../../types';

const getCorners = (droppable: DroppableDimension): Position[] => {
  const fragment: DimensionFragment = droppable.page.withMargin;

  return [
    { x: fragment.left, y: fragment.top },
    { x: fragment.right, y: fragment.top },
    { x: fragment.left, y: fragment.bottom },
    { x: fragment.right, y: fragment.bottom },
  ];
};

type GetBestDroppableArgs = {|
  isMovingForward: boolean,
  // the current position of the dragging item
  pageCenter: Position,
  // the home of the draggable
  source: DroppableDimension,
  // all the droppables in the system
  droppables: DroppableDimensionMap,
|}

export default ({
  isMovingForward,
  pageCenter,
  source,
  droppables,
}: GetBestDroppableArgs): ?DroppableDimension => {
  const axis: Axis = source.axis;

  const candidates: DroppableDimension[] = Object.keys(droppables)
    .map((id: DroppableId) => droppables[id])
    // Remove the source droppable from the list
    .filter((droppable: DroppableDimension): boolean => droppable !== source)
    // Remove any options that are not enabled
    .filter((droppable: DroppableDimension): boolean => droppable.isEnabled)
    // Get only droppables that are on the desired side
    .filter((droppable: DroppableDimension): boolean => {
      if (isMovingForward) {
        // is the droppable in front of the source on the cross axis?
        return source.page.withMargin[axis.crossAxisEnd] <=
          droppable.page.withMargin[axis.crossAxisStart];
      }
      // is the droppable behind the source on the cross axis?
      return droppable.page.withMargin[axis.crossAxisEnd] <=
        source.page.withMargin[axis.crossAxisStart];
    })
    // Must have some overlap on the main axis
    .filter((droppable: DroppableDimension): boolean => {
      const sourceFragment: DimensionFragment = source.page.withMargin;
      const destinationFragment: DimensionFragment = droppable.page.withMargin;

      const isBetweenSourceBounds = isWithin(
        sourceFragment[axis.start],
        sourceFragment[axis.end]
      );
      const isBetweenDestinationBounds = isWithin(
        destinationFragment[axis.start],
        destinationFragment[axis.end]
      );

      return isBetweenSourceBounds(destinationFragment[axis.start]) ||
        isBetweenSourceBounds(destinationFragment[axis.end]) ||
        isBetweenDestinationBounds(sourceFragment[axis.start]) ||
        isBetweenDestinationBounds(sourceFragment[axis.end]);
    })
    // Sort on the cross axis
    .sort((a: DroppableDimension, b: DroppableDimension) => {
      const first: number = a.page.withMargin[axis.crossAxisStart];
      const second: number = b.page.withMargin[axis.crossAxisStart];

      if (isMovingForward) {
        return first - second;
      }
      return second - first;
    })
    // Find the droppables that have the same cross axis value as the first item
    .filter((droppable: DroppableDimension, index: number, array: DroppableDimension[]): boolean =>
      droppable.page.withMargin[axis.crossAxisStart] ===
      array[0].page.withMargin[axis.crossAxisStart]
    );

  // no possible candidates
  if (!candidates.length) {
    return null;
  }

  // only one result - all done!
  if (candidates.length === 1) {
    return candidates[0];
  }

  // At this point we have a number of candidates that
  // all have the same axis.crossAxisStart value.

  // Check to see if the center position is within the size of a Droppable on the main axis
  const contains: DroppableDimension[] = candidates
    .filter((droppable: DroppableDimension) => {
      const isWithinDroppable = isWithin(
        droppable.page.withMargin[axis.start],
        droppable.page.withMargin[axis.end]
      );
      return isWithinDroppable(pageCenter[axis.line]);
    });

  if (contains.length === 1) {
    return contains[0];
  }

  // The center point of the draggable falls on the boundary between two droppables
  if (contains.length > 1) {
    // sort on the main axis and choose the first
    return contains.sort((a: DroppableDimension, b: DroppableDimension) => (
      a.page.withMargin[axis.start] - b.page.withMargin[axis.start]
    ))[0];
  }

  // The center is not contained within any droppable
  // 1. Find the candidate that has the closest corner
  // 2. If there is a tie - choose the one that is first on the main axis
  return candidates.sort((a: DroppableDimension, b: DroppableDimension) => {
    const first = closest(pageCenter, getCorners(a));
    const second = closest(pageCenter, getCorners(b));

    // if the distances are not equal - choose the shortest
    if (first !== second) {
      return first - second;
    }

    // They both have the same distance -
    // choose the one that is first on the main axis
    return a.page.withMargin[axis.start] - b.page.withMargin[axis.start];
  })[0];
};


describe('createStore', () => {
  it('should add redux devtools if they are available and the application is in dev mode', () => {

  });

  it('should not add redux devtools if they are not available', () => {

  });

  it('should not add redux devtools if the application is not in dev mode', () => {

  });
});


// @flow
import type { Position } from '../types';

export default (): Position => ({
  x: window.pageXOffset,
  y: window.pageYOffset,
});


DraggableLocation,
InitialDragLocation,
CurrentDragLocation,
} from '../../../src/types';
import type {
MapProps,
Provided,

// @flow
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import memoizeOne from 'memoize-one';
import { storeKey } from '../context-keys';
import {
  dragSelector,
  pendingDropSelector,
  phaseSelector,
  draggingDraggableSelector,
} from '../../state/selectors';
import Droppable from './droppable';
import type {
  Phase,
  PendingDrop,
  DragState,
  State,
  DroppableId,
  DraggableLocation,
  DraggableDimension,
} from '../../types';
import type {
  OwnProps,
  MapProps,
  Selector,
  Placeholder,
} from './droppable-types';

export const makeSelector = (): Selector => {
  const idSelector = (state: State, ownProps: OwnProps) =>
    ownProps.droppableId;
  const isDropDisabledSelector = (state: State, ownProps: OwnProps) =>
    ownProps.isDropDisabled || false;

  const getIsDraggingOver = memoizeOne(
    (id: DroppableId, destination: ?DraggableLocation): boolean => {
      if (!destination) {
        return false;
      }
      return destination.droppableId === id;
    },
  );

  const memoizedPlaceholder = memoizeOne((width: number, height: number): Placeholder => ({
    width, height,
  }));

  const getPlaceholder = memoizeOne(
    (id: DroppableId,
      source: DraggableLocation,
      destination: ?DraggableLocation,
      draggable: ?DraggableDimension
    ): ?Placeholder => {
      if (!destination) {
        return null;
      }
      // no placeholder needed for this droppable
      if (destination.droppableId !== id) {
        return null;
      }

      // no placeholder needed when dragging over the source list
      if (source.droppableId === destination.droppableId) {
        return null;
      }

      if (!draggable) {
        return null;
      }

      const placeholder: Placeholder = memoizedPlaceholder(
        draggable.page.withMargin.width,
        draggable.page.withMargin.height,
      );

      return placeholder;
    }
  );

  const getMapProps = memoizeOne(
    (isDraggingOver: boolean, placeholder: ?Placeholder): MapProps => ({
      isDraggingOver,
      placeholder,
    })
  );

  return createSelector(
    [phaseSelector,
      dragSelector,
      draggingDraggableSelector,
      pendingDropSelector,
      idSelector,
      isDropDisabledSelector,
    ],
    (phase: Phase,
      drag: ?DragState,
      draggable: ?DraggableDimension,
      pending: ?PendingDrop,
      id: DroppableId,
      isDropDisabled: boolean,
    ): MapProps => {
      if (isDropDisabled) {
        return getMapProps(false, null);
      }

      if (phase === 'DRAGGING') {
        if (!drag) {
          console.error('cannot determine dragging over as there is not drag');
          return getMapProps(false, null);
        }

        const isDraggingOver = getIsDraggingOver(id, drag.impact.destination);

        const placeholder: ?Placeholder = getPlaceholder(
          id,
          drag.initial.source,
          drag.impact.destination,
          draggable
        );
        return getMapProps(isDraggingOver, placeholder);
      }

      if (phase === 'DROP_ANIMATING') {
        if (!pending) {
          console.error('cannot determine dragging over as there is no pending result');
          return getMapProps(false, null);
        }

        const isDraggingOver = getIsDraggingOver(id, pending.impact.destination);
        const placeholder: ?Placeholder = getPlaceholder(
          id,
          pending.result.source,
          pending.result.destination,
          draggable
        );
        return getMapProps(isDraggingOver, placeholder);
      }

      return getMapProps(false, null);
    },
  );
};

const makeMapStateToProps = () => {
  const selector = makeSelector();
  return (state: State, props: OwnProps) => selector(state, props);
};

// Leaning heavily on the default shallow equality checking
// that `connect` provides.
// It avoids needing to do it own within `Droppable`
export default connect(
  // returning a function to ensure each
  // Droppable gets its own selector
  makeMapStateToProps,
  null,
  null,
  { storeKey },
)(Droppable);


module.exports = {
  setupFiles: ['./test/setup.js'],
};


// @flow
import React from 'react';
import { storiesOf } from '@storybook/react';
import styled from 'styled-components';
import QuoteApp from './src/vertical/quote-app';
import { quotes, getQuotes } from './src/data';
import { grid } from './src/constants';
import type { Quote } from './src/types';

const bigData: Quote[] = getQuotes(40);

const ScrollContainer = styled.div`
  box-sizing: border-box;
  background: lightgrey;
  padding: ${grid * 2}px;
  overflow-y: scroll;
  width: 500px;

  height: 100vh;
  position: relative;
`;

const Title = styled.h4`
  text-align: center;
  margin-bottom: ${grid}px;
`;

storiesOf('single vertical list', module)
  .add('simple example', () => (
    <QuoteApp initial={quotes} />
  ))
  .add('with window scrolling', () => (
    <QuoteApp
      initial={bigData}
    />
  ))
  .add('Droppable is a scroll container', () => (
    <QuoteApp
      initial={bigData}
      listStyle={{
        overflowY: 'scroll',
        maxHeight: '80vh',
        position: 'relative',
      }}
    />
  ))
  .add('window scrolling and a Droppable scroll container', () => (
    <QuoteApp
      initial={bigData}
      listStyle={{
        overflowY: 'scroll',
        maxHeight: '120vh',
        position: 'relative',
      }}
    />
  ))
  .add('within a larger scroll container', () => (
    <ScrollContainer>
      <Title>List is within a larger scroll container</Title>
      <QuoteApp
        initial={bigData}
      />
    </ScrollContainer>
  ));


// @flow
export { default } from './connected-draggable-dimension-publisher';


isDraggingOver: false | 'home' | 'foreign'

const draggable: DraggableDimension = getDraggableDimension({


import React, { Component } from 'react';
import PropTypes from 'prop-types';
import type { Props, Provided, StateSnapshot, DefaultProps } from './droppable-types';
import type { DroppableId, HTMLElement } from '../../types';
import DroppableDimensionPublisher from '../droppable-dimension-publisher/';
import Placeholder from '../placeholder/';
import { droppableIdKey } from '../context-keys';

type State = {|
  ref: ?HTMLElement,
|}

type Context = {|
  [droppableIdKey]: DroppableId
|}

export default class Droppable extends Component {
  /* eslint-disable react/sort-comp */
  props: Props
  state: State

  state: State = {
    ref: null,
  }

  static defaultProps: DefaultProps = {
    type: 'DEFAULT',
    isDropDisabled: false,
    direction: 'vertical',
  }

  // Need to declare childContextTypes without flow
  // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22
  static childContextTypes = {
    [droppableIdKey]: PropTypes.string.isRequired,
  }

  getChildContext(): Context {
    const value: Context = {
      [droppableIdKey]: this.props.droppableId,
    };
    return value;
  }
  /* eslint-enable */

  // React calls ref callback twice for every render
  // https://github.com/facebook/react/pull/8333/files
  setRef = (ref: ?HTMLElement) => {
    // TODO: need to clear this.state.ref on unmount
    if (ref === null) {
      return;
    }

    if (ref === this.state.ref) {
      return;
    }

    // need to trigger a child render when ref changes
    this.setState({
      ref,
    });
  }

  getPlaceholder() {
    if (!this.props.placeholder) {
      return null;
    }

    return (
      <Placeholder
        height={this.props.placeholder.height}
        width={this.props.placeholder.width}
      />
    );
  }

  render() {
    const {
      children,
      direction,
      droppableId,
      isDraggingOver,
      isDropDisabled,
      type,
    } = this.props;
    const provided: Provided = {
      innerRef: this.setRef,
      placeholder: this.getPlaceholder(),
    };
    const snapshot: StateSnapshot = {
      isDraggingOver,
    };

    return (
      <DroppableDimensionPublisher
        droppableId={droppableId}
        direction={direction}
        isDropDisabled={isDropDisabled}
        type={type}
        targetRef={this.state.ref}
      >
        {children(provided, snapshot)}
      </DroppableDimensionPublisher>
    );
  }
}


// @flow
export { default } from './connected-droppable';


// @flow
import { vertical, horizontal } from './axis';
import getClientRect from './get-client-rect';
import type {
  DroppableId,
  DraggableId,
  Position,
  DraggableDimension,
  DroppableDimension,
  Direction,
  DimensionFragment,
  Spacing,
  ClientRect,
} from '../types';

const origin: Position = { x: 0, y: 0 };

export const noSpacing: Spacing = {
  top: 0,
  right: 0,
  bottom: 0,
  left: 0,
};

const getWithPosition = (clientRect: ClientRect, point: Position): ClientRect => {
  const { top, right, bottom, left } = clientRect;
  return getClientRect({
    top: top + point.y,
    left: left + point.x,
    bottom: bottom + point.y,
    right: right + point.x,
  });
};

const getWithSpacing = (clientRect: ClientRect, spacing: Spacing): ClientRect => {
  const { top, right, bottom, left } = clientRect;
  return getClientRect({
    top: top - spacing.top,
    left: left - spacing.left,
    bottom: bottom + spacing.bottom,
    right: right + spacing.right,
  });
};

const getFragment = (
  initial: ClientRect | DimensionFragment,
  point?: Position = origin,
): DimensionFragment => {
  const rect: ClientRect = getClientRect({
    top: initial.top + point.y,
    left: initial.left + point.x,
    bottom: initial.bottom + point.y,
    right: initial.right + point.x,
  });

  return {
    top: rect.top,
    right: rect.right,
    bottom: rect.bottom,
    left: rect.left,
    width: rect.width,
    height: rect.height,
    center: {
      x: (rect.right + rect.left) / 2,
      y: (rect.bottom + rect.top) / 2,
    },
  };
};

type GetDraggableArgs = {|
  id: DraggableId,
  droppableId: DroppableId,
  clientRect: ClientRect,
  margin?: Spacing,
  windowScroll?: Position,
|};

export const getDraggableDimension = ({
  id,
  droppableId,
  clientRect,
  margin = noSpacing,
  windowScroll = origin,
}: GetDraggableArgs): DraggableDimension => {
  const withScroll = getWithPosition(clientRect, windowScroll);

  const dimension: DraggableDimension = {
    id,
    droppableId,
    // on the viewport
    client: {
      withoutMargin: getFragment(clientRect),
      withMargin: getFragment(getWithSpacing(clientRect, margin)),
    },
    // with scroll
    page: {
      withoutMargin: getFragment(withScroll),
      withMargin: getFragment(getWithSpacing(withScroll, margin)),
    },
  };

  return dimension;
};

type GetDroppableArgs = {|
  id: DroppableId,
  clientRect: ClientRect,
  direction?: Direction,
  margin?: Spacing,
  padding?: Spacing,
  windowScroll?: Position,
  scroll ?: Position,
  // Whether or not the droppable is currently enabled (can change at during a drag)
  // defaults to true
  isEnabled?: boolean,
|}

const add = (spacing1: Spacing, spacing2: Spacing): Spacing => ({
  top: spacing1.top + spacing2.top,
  left: spacing1.left + spacing2.left,
  right: spacing1.right + spacing2.right,
  bottom: spacing1.bottom + spacing2.bottom,
});

export const getDroppableDimension = ({
  id,
  clientRect,
  direction = 'vertical',
  margin = noSpacing,
  padding = noSpacing,
  windowScroll = origin,
  scroll = origin,
  isEnabled = true,
}: GetDroppableArgs): DroppableDimension => {
  const withMargin = getWithSpacing(clientRect, margin);
  const withWindowScroll = getWithPosition(clientRect, windowScroll);

  const dimension: DroppableDimension = {
    id,
    isEnabled,
    axis: direction === 'vertical' ? vertical : horizontal,
    scroll: {
      initial: scroll,
      // when we start the current scroll is the initial scroll
      current: scroll,
    },
    client: {
      withoutMargin: getFragment(clientRect),
      withMargin: getFragment(withMargin),
      withMarginAndPadding: getFragment(getWithSpacing(withMargin, padding)),
    },
    page: {
      withoutMargin: getFragment(withWindowScroll),
      withMargin: getFragment(getWithSpacing(withWindowScroll, margin)),
      withMarginAndPadding: getFragment(getWithSpacing(withWindowScroll, add(margin, padding))),
    },
  };

  return dimension;
};


// @flow
import type { PropType } from 'babel-plugin-react-flow-props-to-prop-types';
import {
  publishDroppableDimension,
  updateDroppableDimensionIsEnabled,
  updateDroppableDimensionScroll,
} from '../../state/action-creators';
import type {
  DroppableId,
  TypeId,
  ReactElement,
  HTMLElement,
  Direction,
} from '../../types';

export type MapProps = {|
  shouldPublish: boolean,
|}

export type DispatchProps = {|
  publish: PropType<typeof publishDroppableDimension, Function>,
  updateIsEnabled: PropType<typeof updateDroppableDimensionIsEnabled, Function>,
  updateScroll: PropType<typeof updateDroppableDimensionScroll, Function>,
|}

export type OwnProps = {|
  droppableId: DroppableId,
  direction: Direction,
  isDropDisabled: boolean,
  type: TypeId,
  targetRef: ?HTMLElement,
  children?: ReactElement,
|}

export type Props = {
  ...MapProps,
  ...DispatchProps,
  ...OwnProps
}

// Having issues getting the correct reselect type
// export type Selector = OutputSelector<State, OwnProps, MapProps>;
export type Selector = Function;


// @flow
import React from 'react';
import { mount } from 'enzyme';
import Moveable from '../../../src/view/moveable/';
import type { Position } from '../../../src/types';
import type { Speed, Style } from '../../../src/view/moveable/moveable-types';

describe('Moveable', () => {
  let wrapper;
  let childFn;

  beforeAll(() => { // eslint-disable-line no-undef
    requestAnimationFrame.reset();
    childFn = jest.fn(() =>
hi there
);
  });

  beforeEach(() => {
    jest.useFakeTimers();
    wrapper = mount(
      <Moveable
        speed="STANDARD"
      >
        {childFn}
      </Moveable>,
    );
  });

  afterEach(() => {
    jest.useRealTimers();
    requestAnimationFrame.reset();
  });

  const moveTo = (point: Position, speed?: Speed = 'STANDARD', onMoveEnd?: () => void) => {
    wrapper.setProps({
      destination: point,
      onMoveEnd,
      speed,
    });

    // flush the animation
    requestAnimationFrame.flush();

    // callback is called on the next tick after
    // the animation is finished.
    jest.runOnlyPendingTimers();
  };

  const getStyle = (point: Position) => {
    const style: Style = {
      transform: `translate(${point.x}px, ${point.y}px)`,
    };
    return style;
  };

  it('should move to the provided destination', () => {
    const destination: Position = {
      x: 100,
      y: 200,
    };

    moveTo(destination);

    expect(childFn).toHaveBeenCalledWith(getStyle(destination));
  });

  it('should call onMoveEnd when the movement is finished', () => {
    const myMock = jest.fn();
    const destination: Position = {
      x: 100,
      y: 200,
    };

    moveTo(destination, 'STANDARD', myMock);

    expect(myMock).toHaveBeenCalled();
  });

  it('should move instantly if required', () => {
    const myMock = jest.fn();
    const destination: Position = {
      x: 100,
      y: 200,
    };

    // Only releasing one frame
    // ReactMotion uses this to trigger the initial animation
    requestAnimationFrame.step();

    wrapper.setProps({
      speed: 'INSTANT',
      destination,
      onMoveEnd: myMock,
    });

    // Only releasing one frame
    requestAnimationFrame.flush();

    // onMoveEnd fired after a tick
    jest.runOnlyPendingTimers();

    expect(childFn).toHaveBeenCalledWith(getStyle(destination));
    expect(myMock).toHaveBeenCalled();
  });

  it('should allow multiple movements', () => {
    const positions: Array<Position> = [
      { x: 100, y: 100 },
      { x: 400, y: 200 },
      { x: 10, y: -20 },
    ];

    positions.forEach((position: Position) => {
      moveTo(position);

      expect(childFn).toBeCalledWith(getStyle(position));
    });
  });

  it('should return no movement if the item is at the origin', () => {
    const expected: Style = {
      transform: null,
    };

    moveTo({ x: 0, y: 0 });

    expect(childFn).toHaveBeenCalledWith(expected);
  });
});


[ignore]
.*/node_modules/styled-components/.*

[options]

# Provides a way to suppress flow errors in the following line.
# Example: // $FlowFixMe: This following line is borked because of reasons.
# Example: // $ExpectError: This following line is correctly ignored by flow.
suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe
suppress_comment= \\(.\\|\n\\)*\\$ExpectError