<commit_before>// Copyright 2013-2015, University of Colorado Boulder

/**
 * Horizontal slider.
 *
 * Moved from beers-law-lab/EvaporationSlider on 9/15/2013
 * see https://github.com/phetsims/sun/issues/9
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Input = require( 'SCENERY/input/Input' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Property = require( 'AXON/Property' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var TandemDragHandler = require( 'SUN/TandemDragHandler' );
  var sun = require( 'SUN/sun' );
  var Util = require( 'DOT/Util' );

  /**
   * @param {Property.<number>} valueProperty
   * @param { {min:number, max:number} } range
   * @param {Object} [options]
   * @constructor
   */
  function HSlider( valueProperty, range, options ) {

    var thisSlider = this;
    Node.call( thisSlider );

    options = _.extend( {
      // track
      trackSize: new Dimension2( 100, 5 ),
      trackFill: 'white',
      trackStroke: 'black',
      trackLineWidth: 1,
      // {Node} optional thumb, replaces the default. Client is responsible for highlighting and disabling. Centered in the track.
      // If you are using the default thumb, see ThumbNode constructor for additional pass-through options.
      thumbNode: null,
      // ticks
      tickLabelSpacing: 6,
      majorTickLength: 25,
      majorTickStroke: 'black',
      majorTickLineWidth: 1,
      minorTickLength: 10,
      minorTickStroke: 'black',
      minorTickLineWidth: 1,
      // other
      cursor: 'pointer',
      enabledProperty: new Property( true ),
      startDrag: function() {}, // called when a drag sequence starts
      endDrag: function() {}, // called when a drag sequence ends
      constrainValue: function( value ) { return value; }, // called before valueProperty is set
      // tandem
      tandem: null
    }, options );
    this.options = options; // @private TODO save only the options that are needed by prototype functions
    this.enabledProperty = options.enabledProperty;

    // @private ticks are added to these parents, so they are behind the knob
    thisSlider.majorTicksParent = new Node();
    thisSlider.minorTicksParent = new Node();
    thisSlider.addChild( thisSlider.majorTicksParent );
    thisSlider.addChild( thisSlider.minorTicksParent );

    // @private mapping between value and track position
    thisSlider.valueToPosition = new LinearFunction( range.min, range.max, 0, options.trackSize.width, true /* clamp */ );

    // @private track
    thisSlider.track = new Rectangle( 0, 0, options.trackSize.width, options.trackSize.height,
      { fill: options.trackFill, stroke: options.trackStroke, lineWidth: options.trackLineWidth } );
    thisSlider.addChild( thisSlider.track );

    // click in the track to change the value, continue dragging if desired
    var trackInputListener = new TandemDragHandler( {
      tandem: options.tandem ? options.tandem.createTandem( 'trackInputListener' ) : null,

      /**
       *
       * @param event
       * @param trail
       */
      handleTrackEvent: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          var transform = trail.subtrailTo( thisSlider ).getTransform();
          var x = transform.inversePosition2( event.pointer.point ).x;
          var value = thisSlider.valueToPosition.inverse( x );
          var newValue = options.constrainValue( value );
          valueProperty.set( newValue );
        }
      },

      start: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          options.startDrag();
          this.handleTrackEvent( event, trail );
        }
      },

      drag: function( event, trail ) {

        // Reuse the same handleTrackEvent but make sure the startedCallbacks call is made before the value changes
        this.handleTrackEvent( event, trail );
      },

      end: function() {
        if ( thisSlider.enabledProperty.get() ) {
          options.endDrag();
        }
      }
    } );
    thisSlider.track.addInputListener( trackInputListener );

    // thumb
    var thumbNode = options.thumbNode || new ThumbNode( this.enabledProperty, options );

    thumbNode.centerY = thisSlider.track.centerY;
    thisSlider.addChild( thumbNode );

    // thumb touch area
    if ( !options.thumbNode ) {
      var dx = 0.5 * thumbNode.width;
      var dy = 0.25 * thumbNode.height;
      thumbNode.touchArea = Shape.rectangle( ( -thumbNode.width / 2 ) - dx, ( -thumbNode.height / 2 ) - dy, thumbNode.width + dx + dx, thumbNode.height + dy + dy );
    }

    // update value when thumb is dragged
    var thumbInputListener = new TandemDragHandler( {

      tandem: options.tandem ? options.tandem.createTandem( 'thumbInputListener' ) : null,

      // x-offset between initial click and thumb's origin
      clickXOffset: 0,

      allowTouchSnag: true,

      start: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          options.startDrag();

          var transform = trail.subtrailTo( thisSlider ).getTransform();
          this.clickXOffset = transform.inversePosition2( event.pointer.point ).x - thumbNode.x;
        }
      },

      drag: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          var transform = trail.subtrailTo( thisSlider ).getTransform(); // we only want the transform to our parent
          var x = transform.inversePosition2( event.pointer.point ).x - this.clickXOffset;
          var newValue = thisSlider.valueToPosition.inverse( x );

          valueProperty.set( options.constrainValue( newValue ) );
        }
      },

      end: function() {
        if ( thisSlider.enabledProperty.get() ) {
          options.endDrag();
        }
      }
    } );
    thumbNode.addInputListener( thumbInputListener );

    //TODO This is experimental code. Decide how this affects data collection.
    // Keyboard accessibility
    thumbNode.addInputListener( {
      keydown: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          var keyCode = event.domEvent.keyCode;
          var delta = keyCode === Input.KEY_LEFT_ARROW || keyCode === Input.KEY_DOWN_ARROW ? -1 :
                      keyCode === Input.KEY_RIGHT_ARROW || keyCode === Input.KEY_UP_ARROW ? +1 :
                      0;
          var clampedValue = Util.clamp( valueProperty.get() + (range.max - range.min) * 0.1 * delta, range.min, range.max );
          valueProperty.set( options.constrainValue( clampedValue ) );
        }
      }
    } );

    // @private enable/disable
    var enabledObserver = function( enabled ) {
      thisSlider.cursor = thisSlider.enabledProperty.get() ? options.cursor : 'default';
      if ( !enabled ) {
        if ( thumbInputListener.dragging ) { thumbInputListener.endDrag(); }
        if ( trackInputListener.dragging ) { trackInputListener.endDrag(); }
      }
    };
    thisSlider.enabledProperty.link( enabledObserver ); // must be unlinked in disposeHSlider

    // @private update thumb location when value changes
    var valueObserver = function( value ) {
      thumbNode.centerX = thisSlider.valueToPosition( value );
    };
    valueProperty.link( valueObserver ); // must be unlinked in disposeHSlider

    // @private Called by dispose
    this.disposeHSlider = function() {
      thumbNode.dispose && thumbNode.dispose(); // in case a custom thumb is provided via options.thumbNode that doesn't implement dispose
      valueProperty.unlink( valueObserver );
      thisSlider.enabledProperty.unlink( enabledObserver );
      options.tandem && options.tandem.removeInstance( thisSlider );
      trackInputListener.dispose();
      thumbInputListener.dispose();
    };

    thisSlider.mutate( options );

    options.tandem && options.tandem.addInstance( this );
  }

  sun.register( 'HSlider', HSlider );

  inherit( Node, HSlider, {

    // @public - ensures that this object is eligible for GC
    dispose: function() {
      this.disposeHSlider();
    },

    /**
     * Adds a major tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     * @public
     */
    addMajorTick: function( value, label ) {
      this.addTick( this.majorTicksParent, value, label, this.options.majorTickLength, this.options.majorTickStroke, this.options.majorTickLineWidth );
    },

    /**
     * Adds a minor tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     * @public
     */
    addMinorTick: function( value, label ) {
      this.addTick( this.minorTicksParent, value, label, this.options.minorTickLength, this.options.minorTickStroke, this.options.minorTickLineWidth );
    },

    /*
     * Adds a tick mark above the track.
     * @param {Node} parent
     * @param {number} value
     * @param {Node} [label] optional
     * @param {number} length
     * @param {number} stroke
     * @param {number} lineWidth
     * @private
     */
    addTick: function( parent, value, label, length, stroke, lineWidth ) {
      var labelX = this.valueToPosition( value );
      // ticks
      var tick = new Path( new Shape()
        .moveTo( labelX, this.track.top )
        .lineTo( labelX, this.track.top - length ),
        { stroke: stroke, lineWidth: lineWidth } );
      parent.addChild( tick );
      // label
      if ( label ) {
        parent.addChild( label );
        label.centerX = tick.centerX;
        label.bottom = tick.top - this.options.tickLabelSpacing;
      }
    },

    // @public
    setEnabled: function( enabled ) { this.enabledProperty.value = enabled; },
    set enabled( value ) { this. setEnabled( value ); },

    // @public
    getEnabled: function() { return this.enabledProperty.value; },
    get enabled() { return this.getEnabled(); },

    // @public - Sets visibility of major ticks.
    setMajorTicksVisible: function( visible ) {
      this.majorTicksParent.visible = visible;
    },
    set majorTicksVisible( value ) { this.setMajorTicksVisible( value ); },

    // @public - Gets visibility of major ticks.
    getMajorTicksVisible: function() {
      return this.majorTicksParent.visible;
    },
    get majorTicksVisible() { return this.getMajorTicksVisible(); },

    // @public - Sets visibility of minor ticks.
    setMinorTicksVisible: function( visible ) {
      this.minorTicksParent.visible = visible;
    },
    set minorTicksVisible( value ) { this.setMinorTicksVisible( value ); },

    // @public - Gets visibility of minor ticks.
    getMinorTicksVisible: function() {
      return this.minorTicksParent.visible;
    },
    get minorTicksVisible() { return this.getMinorTicksVisible(); }
  } );

  /**
   * Default thumb, a rectangle with a vertical white line down the center
   * @param {Property.<boolean>} enabledProperty
   * @param {Object} [options] see HSlider constructor
   * @constructor
   * @private
   */
  function ThumbNode( enabledProperty, options ) {

    options = _.extend( {
      // default thumb (ignored if thumbNode is provided)
      thumbSize: new Dimension2( 22, 45 ),
      thumbFillEnabled: 'rgb(50,145,184)',
      thumbFillHighlighted: 'rgb(71,207,255)',
      thumbFillDisabled: '#F0F0F0',
      thumbStroke: 'black',
      thumbLineWidth: 1,
      thumbCenterLineStroke: 'white'
    }, options );

    var thisNode = this;

    // rectangle
    var arcWidth = 0.25 * options.thumbSize.width;
    Rectangle.call( thisNode,
      -options.thumbSize.width / 2, -options.thumbSize.height / 2,
      options.thumbSize.width, options.thumbSize.height,
      arcWidth, arcWidth,
      {
        fill: enabledProperty.get() ? options.thumbFillEnabled : options.thumbFillDisabled,
        stroke: options.thumbStroke,
        lineWidth: options.thumbLineWidth,
        cachedPaints: [
          options.thumbFillHighlighted, options.thumbFillEnabled, options.thumbFillDisabled
        ]
      } );

    // vertical line down the center
    var centerLineYMargin = 3;
    thisNode.addChild( new Path( Shape.lineSegment(
      0, -( options.thumbSize.height / 2 ) + centerLineYMargin,
      0, ( options.thumbSize.height / 2 ) - centerLineYMargin ),
      { stroke: options.thumbCenterLineStroke } ) );

    // highlight thumb on pointer over
    thisNode.addInputListener( new ButtonListener( {
      over: function( event ) {
        if ( enabledProperty.get() ) { thisNode.fill = options.thumbFillHighlighted; }
      },
      up: function( event ) {
        if ( enabledProperty.get() ) { thisNode.fill = options.thumbFillEnabled; }
      }
    } ) );

    // @private enable/disable the look of the thumb
    var enabledObserver = function( enabled ) {
      thisNode.fill = enabled ? options.thumbFillEnabled : options.thumbFillDisabled;
    };
    enabledProperty.link( enabledObserver ); // must be unlinked in disposeThumbNode

    // @private Called by dispose
    this.disposeThumbNode = function() {
      enabledProperty.unlink( enabledObserver );
    };
  }

  sun.register( 'HSlider.ThumbNode', ThumbNode );

  inherit( Rectangle, ThumbNode, {

    // @public - Ensures that this object is eligible for GC.
    dispose: function() {
      this.disposeThumbNode();
    }
  } );

  return HSlider;
} );<commit_msg>fix SimpleDragHandler in HSlider, https://github.com/phetsims/scenery/issues/513<commit_after>// Copyright 2013-2015, University of Colorado Boulder

/**
 * Horizontal slider.
 *
 * Moved from beers-law-lab/EvaporationSlider on 9/15/2013
 * see https://github.com/phetsims/sun/issues/9
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Input = require( 'SCENERY/input/Input' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Property = require( 'AXON/Property' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var TandemDragHandler = require( 'SUN/TandemDragHandler' );
  var sun = require( 'SUN/sun' );
  var Util = require( 'DOT/Util' );

  /**
   * @param {Property.<number>} valueProperty
   * @param { {min:number, max:number} } range
   * @param {Object} [options]
   * @constructor
   */
  function HSlider( valueProperty, range, options ) {

    var thisSlider = this;
    Node.call( thisSlider );

    options = _.extend( {
      // track
      trackSize: new Dimension2( 100, 5 ),
      trackFill: 'white',
      trackStroke: 'black',
      trackLineWidth: 1,
      // {Node} optional thumb, replaces the default. Client is responsible for highlighting and disabling. Centered in the track.
      // If you are using the default thumb, see ThumbNode constructor for additional pass-through options.
      thumbNode: null,
      // ticks
      tickLabelSpacing: 6,
      majorTickLength: 25,
      majorTickStroke: 'black',
      majorTickLineWidth: 1,
      minorTickLength: 10,
      minorTickStroke: 'black',
      minorTickLineWidth: 1,
      // other
      cursor: 'pointer',
      enabledProperty: new Property( true ),
      startDrag: function() {}, // called when a drag sequence starts
      endDrag: function() {}, // called when a drag sequence ends
      constrainValue: function( value ) { return value; }, // called before valueProperty is set
      // tandem
      tandem: null
    }, options );
    this.options = options; // @private TODO save only the options that are needed by prototype functions
    this.enabledProperty = options.enabledProperty;

    // @private ticks are added to these parents, so they are behind the knob
    thisSlider.majorTicksParent = new Node();
    thisSlider.minorTicksParent = new Node();
    thisSlider.addChild( thisSlider.majorTicksParent );
    thisSlider.addChild( thisSlider.minorTicksParent );

    // @private mapping between value and track position
    thisSlider.valueToPosition = new LinearFunction( range.min, range.max, 0, options.trackSize.width, true /* clamp */ );

    // @private track
    thisSlider.track = new Rectangle( 0, 0, options.trackSize.width, options.trackSize.height,
      { fill: options.trackFill, stroke: options.trackStroke, lineWidth: options.trackLineWidth } );
    thisSlider.addChild( thisSlider.track );

    // click in the track to change the value, continue dragging if desired
    var handleTrackEvent = function( event, trail ) {
      if ( thisSlider.enabledProperty.get() ) {
        var transform = trail.subtrailTo( thisSlider ).getTransform();
        var x = transform.inversePosition2( event.pointer.point ).x;
        var value = thisSlider.valueToPosition.inverse( x );
        var newValue = options.constrainValue( value );
        valueProperty.set( newValue );
      }
    };
    var trackInputListener = new TandemDragHandler( {
      tandem: options.tandem ? options.tandem.createTandem( 'trackInputListener' ) : null,

      start: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          options.startDrag();
          handleTrackEvent( event, trail );
        }
      },

      drag: function( event, trail ) {

        // Reuse the same handleTrackEvent but make sure the startedCallbacks call is made before the value changes
        handleTrackEvent( event, trail );
      },

      end: function() {
        if ( thisSlider.enabledProperty.get() ) {
          options.endDrag();
        }
      }
    } );
    thisSlider.track.addInputListener( trackInputListener );

    // thumb
    var thumbNode = options.thumbNode || new ThumbNode( this.enabledProperty, options );

    thumbNode.centerY = thisSlider.track.centerY;
    thisSlider.addChild( thumbNode );

    // thumb touch area
    if ( !options.thumbNode ) {
      var dx = 0.5 * thumbNode.width;
      var dy = 0.25 * thumbNode.height;
      thumbNode.touchArea = Shape.rectangle( ( -thumbNode.width / 2 ) - dx, ( -thumbNode.height / 2 ) - dy, thumbNode.width + dx + dx, thumbNode.height + dy + dy );
    }

    // update value when thumb is dragged
    var clickXOffset = 0; // x-offset between initial click and thumb's origin
    var thumbInputListener = new TandemDragHandler( {

      tandem: options.tandem ? options.tandem.createTandem( 'thumbInputListener' ) : null,

      allowTouchSnag: true,

      start: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          options.startDrag();

          var transform = trail.subtrailTo( thisSlider ).getTransform();
          clickXOffset = transform.inversePosition2( event.pointer.point ).x - thumbNode.x;
        }
      },

      drag: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          var transform = trail.subtrailTo( thisSlider ).getTransform(); // we only want the transform to our parent
          var x = transform.inversePosition2( event.pointer.point ).x - clickXOffset;
          var newValue = thisSlider.valueToPosition.inverse( x );

          valueProperty.set( options.constrainValue( newValue ) );
        }
      },

      end: function() {
        if ( thisSlider.enabledProperty.get() ) {
          options.endDrag();
        }
      }
    } );
    thumbNode.addInputListener( thumbInputListener );

    //TODO This is experimental code. Decide how this affects data collection.
    // Keyboard accessibility
    thumbNode.addInputListener( {
      keydown: function( event, trail ) {
        if ( thisSlider.enabledProperty.get() ) {
          var keyCode = event.domEvent.keyCode;
          var delta = keyCode === Input.KEY_LEFT_ARROW || keyCode === Input.KEY_DOWN_ARROW ? -1 :
                      keyCode === Input.KEY_RIGHT_ARROW || keyCode === Input.KEY_UP_ARROW ? +1 :
                      0;
          var clampedValue = Util.clamp( valueProperty.get() + (range.max - range.min) * 0.1 * delta, range.min, range.max );
          valueProperty.set( options.constrainValue( clampedValue ) );
        }
      }
    } );

    // @private enable/disable
    var enabledObserver = function( enabled ) {
      thisSlider.cursor = thisSlider.enabledProperty.get() ? options.cursor : 'default';
      if ( !enabled ) {
        if ( thumbInputListener.dragging ) { thumbInputListener.endDrag(); }
        if ( trackInputListener.dragging ) { trackInputListener.endDrag(); }
      }
    };
    thisSlider.enabledProperty.link( enabledObserver ); // must be unlinked in disposeHSlider

    // @private update thumb location when value changes
    var valueObserver = function( value ) {
      thumbNode.centerX = thisSlider.valueToPosition( value );
    };
    valueProperty.link( valueObserver ); // must be unlinked in disposeHSlider

    // @private Called by dispose
    this.disposeHSlider = function() {
      thumbNode.dispose && thumbNode.dispose(); // in case a custom thumb is provided via options.thumbNode that doesn't implement dispose
      valueProperty.unlink( valueObserver );
      thisSlider.enabledProperty.unlink( enabledObserver );
      options.tandem && options.tandem.removeInstance( thisSlider );
      trackInputListener.dispose();
      thumbInputListener.dispose();
    };

    thisSlider.mutate( options );

    options.tandem && options.tandem.addInstance( this );
  }

  sun.register( 'HSlider', HSlider );

  inherit( Node, HSlider, {

    // @public - ensures that this object is eligible for GC
    dispose: function() {
      this.disposeHSlider();
    },

    /**
     * Adds a major tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     * @public
     */
    addMajorTick: function( value, label ) {
      this.addTick( this.majorTicksParent, value, label, this.options.majorTickLength, this.options.majorTickStroke, this.options.majorTickLineWidth );
    },

    /**
     * Adds a minor tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     * @public
     */
    addMinorTick: function( value, label ) {
      this.addTick( this.minorTicksParent, value, label, this.options.minorTickLength, this.options.minorTickStroke, this.options.minorTickLineWidth );
    },

    /*
     * Adds a tick mark above the track.
     * @param {Node} parent
     * @param {number} value
     * @param {Node} [label] optional
     * @param {number} length
     * @param {number} stroke
     * @param {number} lineWidth
     * @private
     */
    addTick: function( parent, value, label, length, stroke, lineWidth ) {
      var labelX = this.valueToPosition( value );
      // ticks
      var tick = new Path( new Shape()
        .moveTo( labelX, this.track.top )
        .lineTo( labelX, this.track.top - length ),
        { stroke: stroke, lineWidth: lineWidth } );
      parent.addChild( tick );
      // label
      if ( label ) {
        parent.addChild( label );
        label.centerX = tick.centerX;
        label.bottom = tick.top - this.options.tickLabelSpacing;
      }
    },

    // @public
    setEnabled: function( enabled ) { this.enabledProperty.value = enabled; },
    set enabled( value ) { this. setEnabled( value ); },

    // @public
    getEnabled: function() { return this.enabledProperty.value; },
    get enabled() { return this.getEnabled(); },

    // @public - Sets visibility of major ticks.
    setMajorTicksVisible: function( visible ) {
      this.majorTicksParent.visible = visible;
    },
    set majorTicksVisible( value ) { this.setMajorTicksVisible( value ); },

    // @public - Gets visibility of major ticks.
    getMajorTicksVisible: function() {
      return this.majorTicksParent.visible;
    },
    get majorTicksVisible() { return this.getMajorTicksVisible(); },

    // @public - Sets visibility of minor ticks.
    setMinorTicksVisible: function( visible ) {
      this.minorTicksParent.visible = visible;
    },
    set minorTicksVisible( value ) { this.setMinorTicksVisible( value ); },

    // @public - Gets visibility of minor ticks.
    getMinorTicksVisible: function() {
      return this.minorTicksParent.visible;
    },
    get minorTicksVisible() { return this.getMinorTicksVisible(); }
  } );

  /**
   * Default thumb, a rectangle with a vertical white line down the center
   * @param {Property.<boolean>} enabledProperty
   * @param {Object} [options] see HSlider constructor
   * @constructor
   * @private
   */
  function ThumbNode( enabledProperty, options ) {

    options = _.extend( {
      // default thumb (ignored if thumbNode is provided)
      thumbSize: new Dimension2( 22, 45 ),
      thumbFillEnabled: 'rgb(50,145,184)',
      thumbFillHighlighted: 'rgb(71,207,255)',
      thumbFillDisabled: '#F0F0F0',
      thumbStroke: 'black',
      thumbLineWidth: 1,
      thumbCenterLineStroke: 'white'
    }, options );

    var thisNode = this;

    // rectangle
    var arcWidth = 0.25 * options.thumbSize.width;
    Rectangle.call( thisNode,
      -options.thumbSize.width / 2, -options.thumbSize.height / 2,
      options.thumbSize.width, options.thumbSize.height,
      arcWidth, arcWidth,
      {
        fill: enabledProperty.get() ? options.thumbFillEnabled : options.thumbFillDisabled,
        stroke: options.thumbStroke,
        lineWidth: options.thumbLineWidth,
        cachedPaints: [
          options.thumbFillHighlighted, options.thumbFillEnabled, options.thumbFillDisabled
        ]
      } );

    // vertical line down the center
    var centerLineYMargin = 3;
    thisNode.addChild( new Path( Shape.lineSegment(
      0, -( options.thumbSize.height / 2 ) + centerLineYMargin,
      0, ( options.thumbSize.height / 2 ) - centerLineYMargin ),
      { stroke: options.thumbCenterLineStroke } ) );

    // highlight thumb on pointer over
    thisNode.addInputListener( new ButtonListener( {
      over: function( event ) {
        if ( enabledProperty.get() ) { thisNode.fill = options.thumbFillHighlighted; }
      },
      up: function( event ) {
        if ( enabledProperty.get() ) { thisNode.fill = options.thumbFillEnabled; }
      }
    } ) );

    // @private enable/disable the look of the thumb
    var enabledObserver = function( enabled ) {
      thisNode.fill = enabled ? options.thumbFillEnabled : options.thumbFillDisabled;
    };
    enabledProperty.link( enabledObserver ); // must be unlinked in disposeThumbNode

    // @private Called by dispose
    this.disposeThumbNode = function() {
      enabledProperty.unlink( enabledObserver );
    };
  }

  sun.register( 'HSlider.ThumbNode', ThumbNode );

  inherit( Rectangle, ThumbNode, {

    // @public - Ensures that this object is eligible for GC.
    dispose: function() {
      this.disposeThumbNode();
    }
  } );

  return HSlider;
} );<|endoftext|>


<commit_before>/* global describe */
/* global it */
var assert = require('assert');
var Promise = require('bluebird');
var readline = require('readline');
var rewire = require('rewire');
var sinon = require('sinon');
var App = rewire('../../lib/auth/app');

describe('App', function() {

  describe('#new', function() {
    it('should have default values for options', function() {
      var app = new App({ clientId: 123 });
      assert.equal(app.clientSecret, null);
      assert.equal(app.redirectUri, null);
      assert.equal(app.scope, 'default');
      assert.equal(app.asanaBaseUrl, 'https://app.asana.com/');
    });
    it('should store options', function() {
      var app = new App({
        clientId: 'fake_id',
        clientSecret: 'fake_secret',
        redirectUri: 'fake_uri',
        scope: 'fake_scope',
        asanaBaseUrl: 'fake_asana_url'
      });
      assert.equal(app.clientId, 'fake_id');
      assert.equal(app.clientSecret, 'fake_secret');
      assert.equal(app.redirectUri, 'fake_uri');
      assert.equal(app.scope, 'fake_scope');
      assert.equal(app.asanaBaseUrl, 'fake_asana_url');
    });
  });

  describe('#accessTokenFromCode', function() {

    function createApp() {
      return new App({
        clientId: 'fake_id',
        clientSecret: 'fake_secret',
        redirectUri: 'fake_uri',
        scope: 'fake_scope'
      });
    }

    it('should make post request for token', function() {
      var request = sinon.mock();
      request.once();
      App.__set__('request', request);

      var app = createApp();
      app.asanaTokenUrl = sinon.stub().returns('token_url');
      app.accessTokenFromCode('code');

      var params = request.args[0][0];
      assert.equal(params.method, 'POST');
      assert.equal(params.url, 'token_url');
    });

    it('should resolve to token on success', function() {
      var request = sinon.stub();
      App.__set__('request', request);

      var app = createApp();
      var payload = { access_token: 123 };
      var response = app.accessTokenFromCode('code');
      request.callArgWith(
          1, null, { statusCode: 200 }, JSON.stringify(payload));
      return response.then(function(value) {
        assert.deepEqual(value, payload);
      });
    });

    it('should reject with error on failure', function() {
      var request = sinon.stub();
      App.__set__('request', request);
      var app = createApp();
      var response = app.accessTokenFromCode('code');
      request.callArgWith(
          1, 'error', null, 'payload');
      return response.then(function(value) {
        assert(false, 'request should not have succeeded');
      }).catch(function(e) {
        assert.equal(e, 'error');
      });
    });
  });

});<commit_msg>Fixed app spec lint errors<commit_after>/* global describe */
/* global it */
var assert = require('assert');
var rewire = require('rewire');
var sinon = require('sinon');
var App = rewire('../../lib/auth/app');

describe('App', function() {

  describe('#new', function() {
    it('should have default values for options', function() {
      var app = new App({
        clientId: 123
      });
      assert.equal(app.clientSecret, null);
      assert.equal(app.redirectUri, null);
      assert.equal(app.scope, 'default');
      assert.equal(app.asanaBaseUrl, 'https://app.asana.com/');
    });
    it('should store options', function() {
      var app = new App({
        clientId: 'fake_id',
        clientSecret: 'fake_secret',
        redirectUri: 'fake_uri',
        scope: 'fake_scope',
        asanaBaseUrl: 'fake_asana_url'
      });
      assert.equal(app.clientId, 'fake_id');
      assert.equal(app.clientSecret, 'fake_secret');
      assert.equal(app.redirectUri, 'fake_uri');
      assert.equal(app.scope, 'fake_scope');
      assert.equal(app.asanaBaseUrl, 'fake_asana_url');
    });
  });

  describe('#accessTokenFromCode', function() {

    function createApp() {
      return new App({
        clientId: 'fake_id',
        clientSecret: 'fake_secret',
        redirectUri: 'fake_uri',
        scope: 'fake_scope'
      });
    }

    it('should make post request for token', function() {
      var request = sinon.mock();
      request.once();
      App.__set__('request', request);

      var app = createApp();
      app.asanaTokenUrl = sinon.stub().returns('token_url');
      app.accessTokenFromCode('code');

      var params = request.args[0][0];
      assert.equal(params.method, 'POST');
      assert.equal(params.url, 'token_url');
    });

    it('should resolve to token on success', function() {
      var request = sinon.stub();
      App.__set__('request', request);

      var app = createApp();
      var payload = {
        'access_token': 123
      };
      var response = app.accessTokenFromCode('code');
      request.callArgWith(
        1, null, {
          statusCode: 200
        }, JSON.stringify(payload));
      return response.then(function(value) {
        assert.deepEqual(value, payload);
      });
    });

    it('should reject with error on failure', function() {
      var request = sinon.stub();
      App.__set__('request', request);
      var app = createApp();
      var response = app.accessTokenFromCode('code');
      request.callArgWith(
        1, 'error', null, 'payload');
      return response.then(function() {
        assert(false, 'request should not have succeeded');
      }).catch(function(e) {
        assert.equal(e, 'error');
      });
    });
  });

});<|endoftext|>


<commit_before>describe("Essential.Behavior", function () {
  context("given a DOM element", function () {

    beforeEach(function () {
      setDocumentContents("test/fixtures/single_element.html");
      this.domElement = document.getElementById("carousel");
      this.behavior = customBehavior.new(this.domElement);
      Helper.flag = 0;
    });

    it("stores the element in the `el` variable on initialization", function () {
      expect(this.behavior.el).to.be.eq(this.domElement);
    });

    context("given a hash of events", function () {

      it("delegates the events contained in the hash to `this.el` if no selector is present", function () {
        this.domElement.dispatchEvent(Events.click());
        expect(Helper.flag).to.not.equal(0);
      });

      it("delegates the events contained in the hash to an element inside `this.el` if a selector is present", function () {
        var nestedElement = this.domElement.getElementsByTagName("li")[0];
        nestedElement.dispatchEvent(Events.click());
        expect(Helper.flag).to.not.equal(0);
      });

      it("only delegates events to elements contained by `this.el`", function () {
        var menu = document.getElementById("menu");
        var outsideElement = menu.getElementsByTagName("li")[0];
        outsideElement.dispatchEvent(Events.click());
        expect(Helper.flag).to.be.equal(0);
      });

      it("pass off if an event in the hash is not defined", function () {
        expect(this.behavior).to.be.ok;
      });
    });

    context("without a hash of events", function() {
      it("just ignores the existence of events", function() {
        var behavior = Essential.Behavior.new(this.domElement);
        expect(behavior).to.be.ok;
      });
    });
  });
});
<commit_msg>Adds tests for behavior init function.<commit_after>describe("Essential.Behavior", function () {
  context("given a DOM element", function () {

    beforeEach(function () {
      setDocumentContents("test/fixtures/single_element.html");
      this.domElement = document.getElementById("carousel");
      this.behavior = customBehavior.new(this.domElement);
      Helper.flag = 0;
    });

    it("stores the element in the `el` variable on initialization", function () {
      expect(this.behavior.el).to.be.eq(this.domElement);
    });

    it("executes `init` function on initialization", function () {
      var initFlag = 0,
          behaviorWithInit = Essential.Behavior.extend({
            init: function() {
              initFlag++;
            }
          });
      var behavior = behaviorWithInit.new(this.domElement);
      expect(initFlag).to.be.equal(1);
    });

    context("given a hash of events", function () {

      it("delegates the events contained in the hash to `this.el` if no selector is present", function () {
        this.domElement.dispatchEvent(Events.click());
        expect(Helper.flag).to.not.equal(0);
      });

      it("delegates the events contained in the hash to an element inside `this.el` if a selector is present", function () {
        var nestedElement = this.domElement.getElementsByTagName("li")[0];
        nestedElement.dispatchEvent(Events.click());
        expect(Helper.flag).to.not.equal(0);
      });

      it("only delegates events to elements contained by `this.el`", function () {
        var menu = document.getElementById("menu");
        var outsideElement = menu.getElementsByTagName("li")[0];
        outsideElement.dispatchEvent(Events.click());
        expect(Helper.flag).to.be.equal(0);
      });

      it("pass off if an event in the hash is not defined", function () {
        expect(this.behavior).to.be.ok;
      });
    });

    context("without a hash of events", function() {
      it("just ignores the existence of events", function() {
        var behavior = Essential.Behavior.new(this.domElement);
        expect(behavior).to.be.ok;
      });
    });
  });
});
<|endoftext|>


<commit_before>/* global using */

using("WSE.commands", "WSE.functions", "WSE.tools::warn").
define("WSE.Trigger", function (commands, functions, warn) {

    "use strict";

    function Trigger (trigger, interpreter) {

        var self = this, fn;

        this.name = trigger.getAttribute("name") || null;
        this.event = trigger.getAttribute("event") || null;
        this.special = trigger.getAttribute("special") || null;
        this.fnName = trigger.getAttribute("function") || null;
        this.scene = trigger.getAttribute("scene") || null;
        this.interpreter = interpreter;
        this.isSubscribed = false;

        if (this.name === null) {
            warn(interpreter.bus, "No 'name' attribute specified on 'trigger' element.", trigger);
            return;
        }

        if (this.event === null) {

            warn(interpreter.bus, "No 'event' attribute specified on 'trigger' element '" +
                this.name + "'.", trigger);

            return;
        }

        if (this.special === null && this.fnName === null && this.scene === null) {

            warn(interpreter.bus, "No suitable action or function found for trigger element '" +
                this.name + "'.", trigger);

            return;
        }

        if (this.scene) {

            fn = function () {
                console.log('Triggering event "' + self.event + '"...');
                commands.sub(trigger, interpreter);
                interpreter.index = 0;
                interpreter.currentElement = 0;
                interpreter.next();
            };
        }

        this.isKeyEvent = false;
        this.key = null;

        if (this.special !== null && this.special !== "next") {

            warn(interpreter.bus, "Unknown special specified on trigger element '" +
                this.name + "'.", trigger);

            return;
        }

        if (this.special === "next") {

            fn = function () {

                if (self.interpreter.state === "pause" || self.interpreter.waitCounter > 0) {
                    return;
                }

                self.interpreter.next();
            };
        }

        if (this.fnName !== null) {

            if (typeof functions[this.fnName] !== "function") {

                warn(interpreter.bus, "Unknown function specified on trigger element '" +
                    this.name + "'.", trigger);

                return;
            }

            fn = function () {
                functions[self.fnName](self.interpreter, trigger);
            };
        }

        switch (this.event) {

            case "keyup":
            case "keydown":
            case "keypress":

                this.isKeyEvent = true;
                this.key = trigger.getAttribute("key") || null;

                if (this.key === null) {

                    warn(interpreter.bus, "No 'key' attribute specified on trigger element '" +
                        this.name + "'.", trigger);

                    return;
                }

                if (
                    typeof interpreter.game.keys.keys[this.key] === "undefined" ||
                    interpreter.game.keys.keys[this.key] === null
                ) {

                    warn(interpreter.bus, "Unknown key '" + this.key + "' specified on trigger " +
                        "element '" + this.name + "'.", trigger);

                    return;
                }

                this.fn = function (data) {

                    if (data.keys[self.key].kc !== data.event.keyCode) {
                        return;
                    }

                    if (interpreter.keysDisabled > 0) {
                        return;
                    }

                    fn();
                };

                return;

            default:
                this.fn = fn;
        }
    };

    Trigger.prototype.activate = function () {

        if (this.isSubscribed === true) {
            return;
        }

        this.interpreter.bus.subscribe(this.fn, this.event);
        this.isSubscribed = true;
    };

    Trigger.prototype.deactivate = function () {

        if (this.isSubscribed === false) {
            return;
        }

        this.interpreter.bus.unsubscribe(this.fn, this.event);
        this.isSubscribed = false;
    };

    return Trigger;

});<commit_msg>Removed semicolon that wasn't needed.<commit_after>/* global using */

using("WSE.commands", "WSE.functions", "WSE.tools::warn").
define("WSE.Trigger", function (commands, functions, warn) {

    "use strict";

    function Trigger (trigger, interpreter) {

        var self = this, fn;

        this.name = trigger.getAttribute("name") || null;
        this.event = trigger.getAttribute("event") || null;
        this.special = trigger.getAttribute("special") || null;
        this.fnName = trigger.getAttribute("function") || null;
        this.scene = trigger.getAttribute("scene") || null;
        this.interpreter = interpreter;
        this.isSubscribed = false;

        if (this.name === null) {
            warn(interpreter.bus, "No 'name' attribute specified on 'trigger' element.", trigger);
            return;
        }

        if (this.event === null) {

            warn(interpreter.bus, "No 'event' attribute specified on 'trigger' element '" +
                this.name + "'.", trigger);

            return;
        }

        if (this.special === null && this.fnName === null && this.scene === null) {

            warn(interpreter.bus, "No suitable action or function found for trigger element '" +
                this.name + "'.", trigger);

            return;
        }

        if (this.scene) {

            fn = function () {
                console.log('Triggering event "' + self.event + '"...');
                commands.sub(trigger, interpreter);
                interpreter.index = 0;
                interpreter.currentElement = 0;
                interpreter.next();
            };
        }

        this.isKeyEvent = false;
        this.key = null;

        if (this.special !== null && this.special !== "next") {

            warn(interpreter.bus, "Unknown special specified on trigger element '" +
                this.name + "'.", trigger);

            return;
        }

        if (this.special === "next") {

            fn = function () {

                if (self.interpreter.state === "pause" || self.interpreter.waitCounter > 0) {
                    return;
                }

                self.interpreter.next();
            };
        }

        if (this.fnName !== null) {

            if (typeof functions[this.fnName] !== "function") {

                warn(interpreter.bus, "Unknown function specified on trigger element '" +
                    this.name + "'.", trigger);

                return;
            }

            fn = function () {
                functions[self.fnName](self.interpreter, trigger);
            };
        }

        switch (this.event) {

            case "keyup":
            case "keydown":
            case "keypress":

                this.isKeyEvent = true;
                this.key = trigger.getAttribute("key") || null;

                if (this.key === null) {

                    warn(interpreter.bus, "No 'key' attribute specified on trigger element '" +
                        this.name + "'.", trigger);

                    return;
                }

                if (
                    typeof interpreter.game.keys.keys[this.key] === "undefined" ||
                    interpreter.game.keys.keys[this.key] === null
                ) {

                    warn(interpreter.bus, "Unknown key '" + this.key + "' specified on trigger " +
                        "element '" + this.name + "'.", trigger);

                    return;
                }

                this.fn = function (data) {

                    if (data.keys[self.key].kc !== data.event.keyCode) {
                        return;
                    }

                    if (interpreter.keysDisabled > 0) {
                        return;
                    }

                    fn();
                };

                return;

            default:
                this.fn = fn;
        }
    }

    Trigger.prototype.activate = function () {

        if (this.isSubscribed === true) {
            return;
        }

        this.interpreter.bus.subscribe(this.fn, this.event);
        this.isSubscribed = true;
    };

    Trigger.prototype.deactivate = function () {

        if (this.isSubscribed === false) {
            return;
        }

        this.interpreter.bus.unsubscribe(this.fn, this.event);
        this.isSubscribed = false;
    };

    return Trigger;

});<|endoftext|>


<commit_before>
// Description:
// Author: Andrii Sergiienko
// Created: Sun Apr  5 15:15:20 2015 (+0300)
// Version: 1.9-beta

/*
   options.localVideoID = string ID of the video tag for showing local video
   options.remoteVideoID = string ID of the video tag for showing remote video
   options.getRemoteVideoID = call back function which is called when we need to get remote video tag ID from the client
   options.roomID = string word that identifies room parameter in the GET request. If not set, equal to 'room'
   options.roomName = string that that identifies room name. If not set, room name will be requested from the signaling server
   options.pc_config
   options.constraints
   options.sdp_constraints
   options.sandbox = if set, don't make calls, just get user media and stop
   options.own_signaler = URL to the WebRTCO signaler installed on the customer's side
   options.call_back = it is a function that will be called when we need to notify client on an event (error happened, created room etc.)
*/

var WebRTCO = function(options) {
    var API = {};
    var signaling_url = typeof options.own_signaler !== 'undefined' ? options.own_signaler : "wss://www.oslikas.com/WebRTCO-app/signaling/1.9/";
    if (typeof options === 'undefined') {
	    console.error("You should pass options. Please, refer to the documentation.");
	    return;
    }
    // we store in here set of PeerConnection objects (one per peer)
    var Peers = [];
    // we use basic default contraints when custom constraints are not provided
    var default_constraints = {
        audio: {
            mandatory: {
            },
            optional: []
        },
	    video: {
	        mandatory: {
                minWidth: 640,
                minHeight: 480,
                maxWidth: 1080,
                maxHeight: 720
            },
	        optional: []
	    }
    };
    var default_pc_config = {
	    iceServers: [
	        {url: 'stun:stun1.l.google.com:19302'},
	        {url: 'stun:srun2.l.google.com:19302'},
	        {url: 'stun:stun.l.google.com:19302'}
	    ]
    };
    var default_sdp_constraints = {
        mandatory: {
            OfferToReceiveAudio: true,
            OfferToReceiveVideo: true
        },
        optional: []
    };

    /* Look at the options and do initialization */
    var pc_config = typeof options.pc_config !== 'undefined' ? options.pc_config : default_pc_config;
    var constraints = typeof options.constraints !== 'undefined' ? options.constraints : default_constraints;
    var sdpConstraints = typeof options.sdp_constraints !== 'undefined' ? options.sdp_constraints : default_sdp_constraints;
    var localStream = null;
    var roomID = typeof options.roomID !== 'undefined' ? options.roomID : 'room';
    var sandbox = typeof options.sandbox !== 'undefined' ? true : false;
    var call_back = typeof options.call_back !== 'undefined' ? options.call_back : undefined;

    function getRemoteVideo() {
        if (typeof options.remoteVideoID !== 'undefined') return options.remoteVideoID;
        if (typeof options.getRemoteVideoID !== 'undefined') return options.getRemoteVideoID();
        pushCallBack(ErrorCodes.REMOTE_ID);
        return null;
    }

    /*
     * Error codes and call back
     */
    var ErrorCodes = {
        // New room created, got the room URL
        ENTERED_ROOM: 100,
        // Unknown error
        UNKNOWN: 10001,
        // Can't create peer connection
        PEER_CONNECTION: 1002,
        // Can't access user media
        MEDIA: 1003,
        // Can't reach signaling server
        SIGNALING: 1004,
        // Room is full
        ROOM_IS_FULL: 1005,
        // Remote Video ID is not defined
        REMOTE_ID: 1006
    };
    API.ErrorCodes = ErrorCodes;

    function pushCallback(code, message) {
        if (undefined === call_back) return;
        var msg = {code: code};
        if (typeof message !== undefined) msg.message = message;
        call_back(msg);
    };
    /* end of ErrorCodes */

    /*
     * Utilities
     */
    var Utils = (function() {
	    var API = {};
	    API.getRoomName = function() {
	        var s = window.location.search;
	        var o = {};
	        s.replace(
		        new RegExp ("([^?=&]+)(=([^&]*))?", "g"),
		        function ($0, $1, $2, $3) { o[$1] = $3; }
	        );
	        if (o[roomID]) return o[roomID];
            return undefined;
	    };
        API.makeRandID = function(limit) {
            var rand_text = "";
            var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
            for(var i = 0; i < limit; i++)
                rand_text += possible.charAt(Math.floor(Math.random() * possible.length));
	        return rand_text;
        }
	    return API;
    }());
    /* end of Utils object */

    /*
     * Peer Connection object
     *
     *
     */
    var PeerConnection = function(remoteVideoID) {
        var API = {};
        var remoteVideo = document.getElementById(remoteVideoID);
	    var pc_constraints = {};
        var peer;
        API.setPeer = function(p) {
            peer = p;
        };
	    function onIceCandidate(e) {
	        if (e.candidate) send2peer({type: 'candidate', label: e.candidate.sdpMLineIndex,
					                   id: e.candidate.sdpMid, candidate: e.candidate.candidate});
//	        else console.debug("End of candidates");
	    };

        function send2peer(message) {
            Signaling.Send({type: 'TO', value: peer, message: message});
        }

	    var onRemoteStreamAdded = function(e) {
	        //TODO: calling of attachstream should be in media object (a callback?)
	        attachMediaStream(remoteVideo, e.stream);
//	        console.debug("Remote stream added");
	    };

	    function onRemoteStreamRemoved(e) {
//	        console.debug("Remote stream removed");
	    };

	    API.doCall = function() {
//	        console.debug("Sending offer to the peer. Constraints: " + JSON.stringify(pc_constraints));
            createDataChannel();
	        pc.createOffer(setLocalAndSendMessage, onError, pc_constraints);
	    };

	    function doAnswer() {
//	        console.debug("Sending answer to the peer");
	        pc.createAnswer(setLocalAndSendMessage, onError, sdpConstraints); //TODO: sdpConstraints should be passed here?
	    };

	    function onError(e) {
            pushCallback(ErrorCodes.PEER_CONNECTION, e.toString());
//	        console.debug("Something happened when sending an answer or an offer: " + e.toString());
	    };

	    function setLocalAndSendMessage(sdp) {
	        pc.setLocalDescription(sdp);
	        send2peer(sdp);
	    };

	    API.onOffer = function(m) {
	        pc.setRemoteDescription(new RTCSessionDescription(m));
	        doAnswer();
	    };

	    API.onAnswer = function(m) {
	        pc.setRemoteDescription(new RTCSessionDescription(m));
	    };

	    API.onCandidate = function(m) {
	        var candidate = new RTCIceCandidate({sdpMLineIndex: m.label, candidate: m.candidate});
	        pc.addIceCandidate(candidate);
	    };

        API.addStream = function(stream) {
            pc.addStream(stream);
        }

        // Data Channel
        var data_channel = null;
        function onDataChannel(event) {
            console.info("Received data channel creating request");
            data_channel = event.channel;
            initDataChannel();
        };
        function createDataChannel() {
            try {
                data_channel = pc.createDataChannel("datachannel_" + peer);
            } catch (e) {
//                console.error("Error creating data channel " + e);
                return;
            }
            initDataChannel();
        };
        function initDataChannel() {
            data_channel.onopen = onDataChannelOpen;
            data_channel.onclose = onDataChannelClose;
            data_channel.onmessage = onReceiveMessageCallback;
        };
        function send2dataChannel(message) {
            if (null !== data_channel) data_channel.send(message);
        };
        function onDataChannelOpen(event) {
            console.info("Data channel opened");
//            send2dataChannel("hello to " + peer + " !");
        };
        function onDataChannelClose(event) {
            data_channel = null;
//            console.info("Data channel closed");
        };
        function onReceiveMessageCallback(event) {
//            console.info("data message: " + event.data);
        }
        // end of Data Channel

	    try {
	        pc = new RTCPeerConnection(pc_config, {});
	        pc.onicecandidate = onIceCandidate;
            pc.onaddstream = onRemoteStreamAdded;
	        pc.onremovestream = onRemoteStreamRemoved;
            pc.ondatachannel = onDataChannel;
//	        console.debug("Peer connection created");
	    } catch (e) {
	        pc = null;
            pushCallback(ErrorCodes.PEER_CONNECTION, e.message);
//	        console.debug("Failed to create peer connection: " + e.message);
	    }
        return API;
    };
    /* end of peer connection object */

    API.Adapter = new WebRTCOAdapter();
    /*
     * WebRTC media object
     *
     * localVideoID - (mandatory) - ID of the video tag
     * _constraints - (optional) - media access constraints
     *
     */
    API.Media = (function(localVideoID) {
        var API = {};
	    var localVideo = document.getElementById(localVideoID);
	    function doGetUserMedia() {
	        try {
//		        console.debug("Requesting access to media...");
		        getUserMedia(constraints, onSuccess, onError);
	        } catch (e) {
                pushCallback(ErrorCodes.MEDIA, e.message);
//		        console.error("Media access failed with error: " + e.message);
		        return;
	        }
	    };
	    function onSuccess(stream) {
//	        console.debug("Media access granted");
            localVideo.muted = true;
	        localStream = stream;
	        attachMediaStream(localVideo, localStream);
	    };
	    function onError(e) {
            pushCallback(ErrorCodes.MEDIA, e.toString());
//	        console.error("Failed to get media access: " + e);
	    };
        API.getLocalStream = function() {
            return localStream;
        }
	    doGetUserMedia();
        return API;
    }(options.localVideoID));
    /* end of media object */

    /*
     * Signaling object
     *
     *
     */
    var Signaling;
    if (!sandbox)
        Signaling = (function() {
	    var API = {};
	    var ready = false;
        var channel = null;

	    function onOpened() {
//	        console.debug("Signaling channel opened");
	        ready = true;
	        var room = Utils.getRoomName();
	        if (room !== undefined) {
		        initiator = true;
	        } else {
                room = Utils.makeRandID(5);
//                console.debug("Room name: " + room);
		        initiator = false;
	        }
            API.Send({type: 'ROOM_ENTER', value: room + ''});
	    };

	    function onMessage(m) {
//	        console.debug('S->C: ' + m.data);
	        processMessage(m.data);
	    };

	    function onError(e) {
            pushCallback(ErrorCodes.SIGNALING, e.message);
//	        console.debug("Error on signaling channel");
	    };

	    function onClosed() {
//	        console.debug("Signaling channel has been closed");
	        ready = false;
	    };

	    function processMessage(m) {
	        var msg = JSON.parse(m);
//            console.debug(m);
	        if (msg.type == 'ROOM_IS_FULL') {
                pushCallback(ErrorCodes.ROOM_IS_FULL);
            } else if (msg.type == 'ENTERED_ROOM') {
                var room_url = window.location + "?" + roomID + "=" + msg.roomid;
                pushCallback(ErrorCodes.ENTERED_ROOM, room_url);
            } else if (msg.type == 'NEW_PEER') {
                var FromPid = msg.pid;
                Peers[FromPid] = new PeerConnection(getRemoteVideo());
                Peers[FromPid].addStream(localStream);
                Peers[FromPid].setPeer(FromPid);
                Peers[FromPid].doCall();
            } else if (msg.type = 'FROM') {
                var peer = msg.value;
                msg = msg.message;
                if (msg.type == 'offer') {
                    if (Peers.indexOf(peer) <= -1) {
                        Peers[peer] = new PeerConnection(getRemoteVideo());
                        Peers[peer].addStream(localStream);
                        Peers[peer].setPeer(peer);
                        Peers[peer].onOffer(msg);
                    }
                } else if (msg.type == 'answer') {
                    Peers[peer].onAnswer(msg);
                } else if (msg.type == 'candidate') {
                    Peers[peer].onCandidate(msg);
                }
            }
	    };

	    API.Send = function(m) {
	        if (!ready) return;
	        var s = JSON.stringify(m);
//	        console.debug('C->S: ' + s);
	        channel.send(s);
	    };

        function init() {
            if (null !== localStream) {
	            channel = new WebSocket(signaling_url);
	            channel.onopen = onOpened;
	            channel.onerror = onError;
	            channel.onmessage = onMessage;
	            channel.onclose = onClosed;
            } else setTimeout(init, 1000);
        };
        init();
	    return API;
    }());
    /* end of signaling object */

    return API;
};
/* end of WebRTCO object */

/*
 * WebRTC Adapter
 *
 */
var WebRTCOAdapter = function() {
    var API = {};
    if (navigator.mozGetUserMedia) {
        window.RTCPeerConnection = mozRTCPeerConnection;
        window.RTCSessionDescription = mozRTCSessionDescription;
        window.RTCIceCandidate = mozRTCIceCandidate;
        window.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
        window.attachMediaStream = function(element, stream) {
            element.mozSrcObject = stream;
            element.play();
        };
        window.reattachMediaStream = function(to, from) {
            to.mozSrcObject = from.mozSrcObject;
            to.play();
        };
    } else if (navigator.webkitGetUserMedia) {
        window.RTCPeerConnection = webkitRTCPeerConnection;
        window.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
        window.attachMediaStream = function(element, stream) {
            element.src = URL.createObjectURL(stream);
        };
        window.reattachMediaStream = function(to, from) {
            to.src = from.src;
        };
        if (!webkitMediaStream.prototype.getVideoTracks) {
            webkitMediaStream.prototype.getVideoTracks = function() {
                return this.videoTracks;
            };
            webkitMediaStream.prototype.getAudioTracks = function() {
                return this.audioTracks;
            };
        }
        if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
            webkitRTCPeerConnection.prototype.getLocalStreams = function() {
                return this.localStreams;
            };
            webkitRTCPeerConnection.prototype.getRemoteStreams = function() {
                return this.remoteStreams;
            };
        }
    }

    var ua=navigator.userAgent,tem,M=ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
    if(/trident/i.test(M[1])) {
        tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
        API.name = 'IE ';
        API.version = (tem[1]||'');
    } else if(M[1] === 'Chrome') {
        tem = ua.match(/\bOPR\/(\d+)/);
        if (tem != null) {
            API.name = 'Opera';
            API.version = tem[1]
        }
    }
    if (typeof API.name === 'undefined') {
        M = M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?'];
        if ((tem = ua.match(/version\/(\d+)/i)) != null) {
            M.splice(1,1,tem[1]);
        }
        API.name = M[0];
        API.version = M[1];
    }
    return API;
};
    /* end of Adapter object */
<commit_msg>fix #1 call back the client when we can not create the channel<commit_after>
// Description:
// Author: Andrii Sergiienko
// Created: Sun Apr  5 15:15:20 2015 (+0300)
// Version: 1.9-beta

/*
   options.localVideoID = string ID of the video tag for showing local video
   options.remoteVideoID = string ID of the video tag for showing remote video
   options.getRemoteVideoID = call back function which is called when we need to get remote video tag ID from the client
   options.roomID = string word that identifies room parameter in the GET request. If not set, equal to 'room'
   options.roomName = string that that identifies room name. If not set, room name will be requested from the signaling server
   options.pc_config
   options.constraints
   options.sdp_constraints
   options.sandbox = if set, don't make calls, just get user media and stop
   options.own_signaler = URL to the WebRTCO signaler installed on the customer's side
   options.call_back = it is a function that will be called when we need to notify client on an event (error happened, created room etc.)
   options.no_data_channel - if set, don't create data channel (created by default)
*/

var WebRTCO = function(options) {
    var API = {};
    var signaling_url = typeof options.own_signaler !== 'undefined' ? options.own_signaler : "wss://www.oslikas.com/WebRTCO-app/signaling/1.9/";
    if (typeof options === 'undefined') {
	    console.error("You should pass options. Please, refer to the documentation.");
	    return;
    }
    // we store in here set of PeerConnection objects (one per peer)
    var Peers = [];
    // we use basic default contraints when custom constraints are not provided
    var default_constraints = {
        audio: {
            mandatory: {
            },
            optional: []
        },
	    video: {
	        mandatory: {
                minWidth: 640,
                minHeight: 480,
                maxWidth: 1080,
                maxHeight: 720
            },
	        optional: []
	    }
    };
    var default_pc_config = {
	    iceServers: [
	        {url: 'stun:stun1.l.google.com:19302'},
	        {url: 'stun:srun2.l.google.com:19302'},
	        {url: 'stun:stun.l.google.com:19302'}
	    ]
    };
    var default_sdp_constraints = {
        mandatory: {
            OfferToReceiveAudio: true,
            OfferToReceiveVideo: true
        },
        optional: []
    };

    /* Look at the options and do initialization */
    var pc_config = typeof options.pc_config !== 'undefined' ? options.pc_config : default_pc_config;
    var constraints = typeof options.constraints !== 'undefined' ? options.constraints : default_constraints;
    var sdpConstraints = typeof options.sdp_constraints !== 'undefined' ? options.sdp_constraints : default_sdp_constraints;
    var localStream = null;
    var roomID = typeof options.roomID !== 'undefined' ? options.roomID : 'room';
    var sandbox = typeof options.sandbox !== 'undefined' ? true : false;
    var call_back = typeof options.call_back !== 'undefined' ? options.call_back : undefined;

    function getRemoteVideo() {
        if (typeof options.remoteVideoID !== 'undefined') return options.remoteVideoID;
        if (typeof options.getRemoteVideoID !== 'undefined') return options.getRemoteVideoID();
        pushCallBack(ErrorCodes.REMOTE_ID);
        return null;
    }

    /*
     * Error codes and call back
     */
    var ErrorCodes = {
        // New room created, got the room URL
        ENTERED_ROOM: 100,
        // Unknown error
        UNKNOWN: 10001,
        // Can't create peer connection
        PEER_CONNECTION: 1002,
        // Can't access user media
        MEDIA: 1003,
        // Can't reach signaling server
        SIGNALING: 1004,
        // Room is full
        ROOM_IS_FULL: 1005,
        // Remote Video ID is not defined
        REMOTE_ID: 1006,
        // Can't create data channel
        DATA_CHANNEL: 1007
    };
    API.ErrorCodes = ErrorCodes;

    function pushCallback(code, message) {
        if (undefined === call_back) return;
        var msg = {code: code};
        if (typeof message !== undefined) msg.message = message;
        call_back(msg);
    };
    /* end of ErrorCodes */

    /*
     * Utilities
     */
    var Utils = (function() {
	    var API = {};
	    API.getRoomName = function() {
	        var s = window.location.search;
	        var o = {};
	        s.replace(
		        new RegExp ("([^?=&]+)(=([^&]*))?", "g"),
		        function ($0, $1, $2, $3) { o[$1] = $3; }
	        );
	        if (o[roomID]) return o[roomID];
            return undefined;
	    };
        API.makeRandID = function(limit) {
            var rand_text = "";
            var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
            for(var i = 0; i < limit; i++)
                rand_text += possible.charAt(Math.floor(Math.random() * possible.length));
	        return rand_text;
        }
	    return API;
    }());
    /* end of Utils object */

    /*
     * Peer Connection object
     *
     *
     */
    var PeerConnection = function(remoteVideoID) {
        var API = {};
        var remoteVideo = document.getElementById(remoteVideoID);
	    var pc_constraints = {};
        var peer;
        API.setPeer = function(p) {
            peer = p;
        };
	    function onIceCandidate(e) {
	        if (e.candidate) send2peer({type: 'candidate', label: e.candidate.sdpMLineIndex,
					                   id: e.candidate.sdpMid, candidate: e.candidate.candidate});
//	        else console.debug("End of candidates");
	    };

        function send2peer(message) {
            Signaling.Send({type: 'TO', value: peer, message: message});
        }

	    var onRemoteStreamAdded = function(e) {
	        //TODO: calling of attachstream should be in media object (a callback?)
	        attachMediaStream(remoteVideo, e.stream);
//	        console.debug("Remote stream added");
	    };

	    function onRemoteStreamRemoved(e) {
//	        console.debug("Remote stream removed");
	    };

	    API.doCall = function() {
//	        console.debug("Sending offer to the peer. Constraints: " + JSON.stringify(pc_constraints));
            if (typeof options.no_data_channel === 'undefined') createDataChannel();
	        pc.createOffer(setLocalAndSendMessage, onError, pc_constraints);
	    };

	    function doAnswer() {
//	        console.debug("Sending answer to the peer");
	        pc.createAnswer(setLocalAndSendMessage, onError, sdpConstraints); //TODO: sdpConstraints should be passed here?
	    };

	    function onError(e) {
            pushCallback(ErrorCodes.PEER_CONNECTION, e.toString());
//	        console.debug("Something happened when sending an answer or an offer: " + e.toString());
	    };

	    function setLocalAndSendMessage(sdp) {
	        pc.setLocalDescription(sdp);
	        send2peer(sdp);
	    };

	    API.onOffer = function(m) {
	        pc.setRemoteDescription(new RTCSessionDescription(m));
	        doAnswer();
	    };

	    API.onAnswer = function(m) {
	        pc.setRemoteDescription(new RTCSessionDescription(m));
	    };

	    API.onCandidate = function(m) {
	        var candidate = new RTCIceCandidate({sdpMLineIndex: m.label, candidate: m.candidate});
	        pc.addIceCandidate(candidate);
	    };

        API.addStream = function(stream) {
            pc.addStream(stream);
        }

        // Data Channel
        var data_channel = null;
        function onDataChannel(event) {
            console.info("Received data channel creating request");
            data_channel = event.channel;
            initDataChannel();
        };
        function createDataChannel() {
            try {
                data_channel = pc.createDataChannel("datachannel_" + peer);
            } catch (e) {
                pushCallback(ErrorCodes.DATA_CHANNEL,e);
                data_channel = null;
//                console.error("Error creating data channel " + e);
                return;
            }
            initDataChannel();
        };
        function initDataChannel() {
            data_channel.onopen = onDataChannelOpen;
            data_channel.onclose = onDataChannelClose;
            data_channel.onmessage = onReceiveMessageCallback;
        };
        function send2dataChannel(message) {
            if (null !== data_channel) data_channel.send(message);
        };
        function onDataChannelOpen(event) {
//            console.info("Data channel opened");
//            send2dataChannel("hello to " + peer + " !");
        };
        function onDataChannelClose(event) {
            data_channel = null;
//            console.info("Data channel closed");
        };
        function onReceiveMessageCallback(event) {
//            console.info("data message: " + event.data);
        };
        // end of Data Channel

	    try {
	        pc = new RTCPeerConnection(pc_config, {});
	        pc.onicecandidate = onIceCandidate;
            pc.onaddstream = onRemoteStreamAdded;
	        pc.onremovestream = onRemoteStreamRemoved;
            pc.ondatachannel = onDataChannel;
//	        console.debug("Peer connection created");
	    } catch (e) {
	        pc = null;
            pushCallback(ErrorCodes.PEER_CONNECTION, e.message);
//	        console.debug("Failed to create peer connection: " + e.message);
	    }
        return API;
    };
    /* end of peer connection object */

    API.Adapter = new WebRTCOAdapter();
    /*
     * WebRTC media object
     *
     * localVideoID - (mandatory) - ID of the video tag
     * _constraints - (optional) - media access constraints
     *
     */
    API.Media = (function(localVideoID) {
        var API = {};
	    var localVideo = document.getElementById(localVideoID);
	    function doGetUserMedia() {
	        try {
//		        console.debug("Requesting access to media...");
		        getUserMedia(constraints, onSuccess, onError);
	        } catch (e) {
                pushCallback(ErrorCodes.MEDIA, e.message);
//		        console.error("Media access failed with error: " + e.message);
		        return;
	        }
	    };
	    function onSuccess(stream) {
//	        console.debug("Media access granted");
            localVideo.muted = true;
	        localStream = stream;
	        attachMediaStream(localVideo, localStream);
	    };
	    function onError(e) {
            pushCallback(ErrorCodes.MEDIA, e.toString());
//	        console.error("Failed to get media access: " + e);
	    };
        API.getLocalStream = function() {
            return localStream;
        }
	    doGetUserMedia();
        return API;
    }(options.localVideoID));
    /* end of media object */

    /*
     * Signaling object
     *
     *
     */
    var Signaling;
    if (!sandbox)
        Signaling = (function() {
	    var API = {};
	    var ready = false;
        var channel = null;

	    function onOpened() {
//	        console.debug("Signaling channel opened");
	        ready = true;
	        var room = Utils.getRoomName();
	        if (room !== undefined) {
		        initiator = true;
	        } else {
                room = Utils.makeRandID(5);
//                console.debug("Room name: " + room);
		        initiator = false;
	        }
            API.Send({type: 'ROOM_ENTER', value: room + ''});
	    };

	    function onMessage(m) {
//	        console.debug('S->C: ' + m.data);
	        processMessage(m.data);
	    };

	    function onError(e) {
            pushCallback(ErrorCodes.SIGNALING, e.message);
//	        console.debug("Error on signaling channel");
	    };

	    function onClosed() {
//	        console.debug("Signaling channel has been closed");
	        ready = false;
	    };

	    function processMessage(m) {
	        var msg = JSON.parse(m);
//            console.debug(m);
	        if (msg.type == 'ROOM_IS_FULL') {
                pushCallback(ErrorCodes.ROOM_IS_FULL);
            } else if (msg.type == 'ENTERED_ROOM') {
                var room_url = window.location + "?" + roomID + "=" + msg.roomid;
                pushCallback(ErrorCodes.ENTERED_ROOM, room_url);
            } else if (msg.type == 'NEW_PEER') {
                var FromPid = msg.pid;
                Peers[FromPid] = new PeerConnection(getRemoteVideo());
                Peers[FromPid].addStream(localStream);
                Peers[FromPid].setPeer(FromPid);
                Peers[FromPid].doCall();
            } else if (msg.type = 'FROM') {
                var peer = msg.value;
                msg = msg.message;
                if (msg.type == 'offer') {
                    if (Peers.indexOf(peer) <= -1) {
                        Peers[peer] = new PeerConnection(getRemoteVideo());
                        Peers[peer].addStream(localStream);
                        Peers[peer].setPeer(peer);
                        Peers[peer].onOffer(msg);
                    }
                } else if (msg.type == 'answer') {
                    Peers[peer].onAnswer(msg);
                } else if (msg.type == 'candidate') {
                    Peers[peer].onCandidate(msg);
                }
            }
	    };

	    API.Send = function(m) {
	        if (!ready) return;
	        var s = JSON.stringify(m);
//	        console.debug('C->S: ' + s);
	        channel.send(s);
	    };

        function init() {
            if (null !== localStream) {
	            channel = new WebSocket(signaling_url);
	            channel.onopen = onOpened;
	            channel.onerror = onError;
	            channel.onmessage = onMessage;
	            channel.onclose = onClosed;
            } else setTimeout(init, 1000);
        };
        init();
	    return API;
    }());
    /* end of signaling object */

    return API;
};
/* end of WebRTCO object */

/*
 * WebRTC Adapter
 *
 */
var WebRTCOAdapter = function() {
    var API = {};
    if (navigator.mozGetUserMedia) {
        window.RTCPeerConnection = mozRTCPeerConnection;
        window.RTCSessionDescription = mozRTCSessionDescription;
        window.RTCIceCandidate = mozRTCIceCandidate;
        window.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
        window.attachMediaStream = function(element, stream) {
            element.mozSrcObject = stream;
            element.play();
        };
        window.reattachMediaStream = function(to, from) {
            to.mozSrcObject = from.mozSrcObject;
            to.play();
        };
    } else if (navigator.webkitGetUserMedia) {
        window.RTCPeerConnection = webkitRTCPeerConnection;
        window.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
        window.attachMediaStream = function(element, stream) {
            element.src = URL.createObjectURL(stream);
        };
        window.reattachMediaStream = function(to, from) {
            to.src = from.src;
        };
        if (!webkitMediaStream.prototype.getVideoTracks) {
            webkitMediaStream.prototype.getVideoTracks = function() {
                return this.videoTracks;
            };
            webkitMediaStream.prototype.getAudioTracks = function() {
                return this.audioTracks;
            };
        }
        if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
            webkitRTCPeerConnection.prototype.getLocalStreams = function() {
                return this.localStreams;
            };
            webkitRTCPeerConnection.prototype.getRemoteStreams = function() {
                return this.remoteStreams;
            };
        }
    }

    var ua=navigator.userAgent,tem,M=ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
    if(/trident/i.test(M[1])) {
        tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
        API.name = 'IE ';
        API.version = (tem[1]||'');
    } else if(M[1] === 'Chrome') {
        tem = ua.match(/\bOPR\/(\d+)/);
        if (tem != null) {
            API.name = 'Opera';
            API.version = tem[1]
        }
    }
    if (typeof API.name === 'undefined') {
        M = M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?'];
        if ((tem = ua.match(/version\/(\d+)/i)) != null) {
            M.splice(1,1,tem[1]);
        }
        API.name = M[0];
        API.version = M[1];
    }
    return API;
};
    /* end of Adapter object */
<|endoftext|>


<commit_before>
var appViews = {

  switchView: function(name, body) {
    $('#display-title').html(name);
    $('#display').html(body);
  },

  deviceOff: function() {
    var html = new EJS({url: 'views/device_off.ejs'}).render({});
    this.switchView('device_off', html);
  },

  channelList: function(channelList) {
    var html = new EJS({url: 'views/channel_list.ejs'}).render({
      channels: channelList
    });
    this.switchView('channel_list', html);
  },

  displayModeSelection: function(availableModes) {
    var html = new EJS({url: 'views/mode_selection.ejs'}).render({availableModes: availableModes});
    this.switchView('mode_selection', html);
  },

  displayUserCode: function(userCode, verificationUri) {
    var html = new EJS({url: 'views/user_code.ejs'}).render({
      user_code: userCode, verification_uri: verificationUri
    });
    this.switchView('user_code', html);
  },

  displayProgress: function(message) {
    var html = new EJS({url: 'views/progress.ejs'}).render({
      'message': message
    });
    this.switchView('Progress: '+ message, html);
  },

  successfulPairing: function(accessToken, mode) {
    var html = new EJS({url: 'views/success.ejs'}).render({
      message: 'The device is in ' + mode + '. Here is the access token: '+ accessToken
    });
    this.switchView('Device paired', html);
  },

  error: function(errorMessage) {
    var html = new EJS({url: 'views/error.ejs'}).render({
      message: errorMessage
    });
    this.switchView('error', html);
  }

};

var appFsm = new machina.Fsm({

  initialize: function() {

    var self = this;
    $('#power_btn').click(function() { self.handle('switch_on'); });
    $('#reset_btn').click(function() {
      storage.reset();
      Logger.info('**** RESET STORAGE ****');
      self.transition('DEVICE_OFF');
    });

    self.on('*', function(message, options) {
      if(message === 'transition') {
        if(options.action) {
          Logger.debug('[FSM] ', message, ': ', options.fromState, ' -> ', options.action, ' -> ', options.toState );
        } else {
          Logger.debug('[FSM] ', message, ': ', options.fromState, ' -> ', options.toState );
        }
      }
    });
  },

  error: function(err) {
    Logger.error(err);
    appViews.error(err.message);
    this.transition('ERROR');
  },

  initialState: 'DEVICE_OFF',

  states : {

    'DEVICE_OFF': {
      _onEnter: function() {
        appViews.deviceOff();
      },

      'switch_on': function() {
        this.transition('SCANNING');
      }
    },

    'SCANNING': {
      _onEnter: function() {
        appViews.displayProgress('Scanning...');

        this.handle('getChannelList');

      },
      'getChannelList': function() {
        storage.volatile.put('channel_list', [{name: 'BBC1', data:''}, {name:'BBC2', data:''}]);

        var self = this;
        setTimeout(function() {
          var channelList = storage.volatile.get('channel_list');
          if(!channelList || channelList.length === 0) {
            return error({message: 'Unable to discover any channel'});
          }
          self.transition('CHANNEL_LIST');
        }, 100);
      }
    },

    'CHANNEL_LIST': {
      _onEnter: function() {
        appViews.channelList(storage.volatile.get('channel_list'));

        var self = this;
        $('.channel-list>a').click(function() {
          self.handle('onChannelClick',  $(this).attr('data-channel'));
        });
      },

      'onChannelClick': function(channel) {
        var self = this;

        var mode = storage.persistent.get('mode');
        var serviceProvider = channel;
        storage.volatile.put('current_channel', channel);

        if (storage.persistent.get('client_information')) {
          // TODO: Check according to the SP.

          if(storage.persistent.getValue('access_token', serviceProvider)) {
            self.transition('SUCCESSFUL_PAIRING');
          } else {
            var pairingCode = storage.persistent.getValue('pairing_code', serviceProvider);
            if(mode === 'PAIRING_MODE') {
              if(!pairingCode) {
                self.transition('AUTHORIZATION_INIT');
              } else {
                self.transition('AUTHORIZATION_PENDING');
              }

            }
            else if(mode === 'CLIENT_MODE') {
              self.transition('CLIENT_AUTH_INIT');
            }
            else {
              self.transition('MODE_SELECTION');
            }
          }
        } else {
          self.transition('CLIENT_REGISTRATION');
        }
      }
    },

    'CLIENT_REGISTRATION': {
      _onEnter: function() {
        appViews.displayProgress('Client registration');
        var self = this;

        cpaProtocol.registerClient('Demo Client', 'cpa-client', '1.0.1', function(err, statusCode, body) {
          if(err) {
            return error(err);
          }
          self.transition('MODE_SELECTION');
        });
      }
    },

    'MODE_SELECTION': {
      _onEnter: function() {

        cpaProtocol.getAvailableModes('https://sp/', function(err, availableModes) {
          if(err) {
            return self.error(err);
          }
          appViews.displayModeSelection(availableModes);
        });

        var self = this;
        $('a.list-group-item').click(function() {
          self.handle('onModeClick',  $(this).attr('data-mode'));
        });
      },

      'onModeClick': function(mode) {
        var self = this;
        storage.persistent.put('mode', mode);

        // TODO: Check according to the SP.
        if(mode === 'PAIRING_MODE') {
          self.transition('AUTHORIZATION_INIT');
        }
        else if(mode === 'CLIENT_MODE') {
          self.transition('CLIENT_AUTH_INIT');
        }
        else {
          return error(new Error('Unknown mode'));
        }

      }
    },

    'AUTHORIZATION_INIT': {
      _onEnter: function() {
        var self = this;

        var clientId = storage.persistent.get('client_information').client_id;
        var serviceProvider = storage.volatile.get('current_channel');

        cpaProtocol.requestUserCode(clientId, serviceProvider, function(err){
          if(err) {
            return self.error(err);
          }

          self.transition('AUTHORIZATION_PENDING');
        });
      }
    },

    'CLIENT_AUTH_INIT': {
      _onEnter: function() {
        var self = this;

        var client = storage.persistent.get('client_information');
        var serviceProvider = storage.volatile.get('current_channel');

        cpaProtocol.requestClientAccessToken(client.client_id,
          client.client_secret,
          serviceProvider,
          function(err){
            if(err) {
              return self.error(err);
            }
            self.transition('SUCCESSFUL_PAIRING');
        });
      }
    },

    'AUTHORIZATION_PENDING': {
      _onEnter: function(){
        var self = this;
        var serviceProvider = storage.volatile.get('current_channel');
        var pairingCode = storage.persistent.getValue('pairing_code', serviceProvider);

        appViews.displayUserCode(pairingCode.user_code, pairingCode.verification_uri);
        $('#verify_code_btn').click(function() { self.handle('onValidatePairingClick'); });
      },

      'onValidatePairingClick': function() {
        this.transition('AUTHORIZATION_CHECK');
      }
    },

    'AUTHORIZATION_CHECK': {
      _onEnter: function() {
        var self = this;
        var serviceProvider = storage.volatile.get('current_channel');
        var pairingCode = storage.persistent.getValue('pairing_code', serviceProvider);
        var clientInformation = storage.persistent.get('client_information');

        cpaProtocol.requestAccessToken(clientInformation.client_id,
          pairingCode.device_code, serviceProvider, function(err, authorizationPending){
            if(err) {
              self.error(err);
            } else if(authorizationPending) {
              alert('Go to the website');
              self.transition('AUTHORIZATION_PENDING');
            } else {
              self.transition('SUCCESSFUL_PAIRING');
            }
          });
      }
    },

    'SUCCESSFUL_PAIRING': {
      _onEnter: function() {
        var serviceProvider = storage.volatile.get('current_channel');
        var accessToken = storage.persistent.getValue('access_token', serviceProvider);
        var mode = storage.persistent.get('mode');

        appViews.successfulPairing(accessToken.token, mode);

        $('#trig-with-btn').click(function(){
          requestHelper.get(config.service_provider_url[serviceProvider] + '/resource', accessToken.token)
            .success(function(data, textStatus, jqXHR) {
              Logger.info('Reply ' + jqXHR.status + '(' + textStatus + '): ', data);
              alert(data.message);
            })
            .fail(function(jqXHR, textStatus) {
              Logger.error('Reply ' + jqXHR.status + '(' + textStatus + '): ', 'invalid request');
              alert('invalid request');
            });
        });

        $('#trig-without-btn').click(function(){
          requestHelper.get(config.service_provider_url[serviceProvider] + '/resource', null)
            .success(function(data, textStatus, jqXHR) {
              Logger.info('Reply ' + jqXHR.status + '(' + textStatus + '): ', data);
              alert(data.message);
            })
            .fail(function(jqXHR, textStatus) {
              Logger.error('Reply ' + jqXHR.status + '(' + textStatus + '): ', 'invalid request');
              alert('invalid request');
            });
        });
      }
    },

    'ERROR': {
      _onEnter: function() {
        Logger.error('end');

      }
    }
  }
});

//appFsm.handle('switch_on');
<commit_msg>Rename PAIRING_MODE to USER_MODE<commit_after>
var appViews = {

  switchView: function(name, body) {
    $('#display-title').html(name);
    $('#display').html(body);
  },

  deviceOff: function() {
    var html = new EJS({url: 'views/device_off.ejs'}).render({});
    this.switchView('device_off', html);
  },

  channelList: function(channelList) {
    var html = new EJS({url: 'views/channel_list.ejs'}).render({
      channels: channelList
    });
    this.switchView('channel_list', html);
  },

  displayModeSelection: function(availableModes) {
    var html = new EJS({url: 'views/mode_selection.ejs'}).render({availableModes: availableModes});
    this.switchView('mode_selection', html);
  },

  displayUserCode: function(userCode, verificationUri) {
    var html = new EJS({url: 'views/user_code.ejs'}).render({
      user_code: userCode, verification_uri: verificationUri
    });
    this.switchView('user_code', html);
  },

  displayProgress: function(message) {
    var html = new EJS({url: 'views/progress.ejs'}).render({
      'message': message
    });
    this.switchView('Progress: '+ message, html);
  },

  successfulPairing: function(accessToken, mode) {
    var html = new EJS({url: 'views/success.ejs'}).render({
      message: 'The device is in ' + mode + '. Here is the access token: '+ accessToken
    });
    this.switchView('Device paired', html);
  },

  error: function(errorMessage) {
    var html = new EJS({url: 'views/error.ejs'}).render({
      message: errorMessage
    });
    this.switchView('error', html);
  }

};

var appFsm = new machina.Fsm({

  initialize: function() {

    var self = this;
    $('#power_btn').click(function() { self.handle('switch_on'); });
    $('#reset_btn').click(function() {
      storage.reset();
      Logger.info('**** RESET STORAGE ****');
      self.transition('DEVICE_OFF');
    });

    self.on('*', function(message, options) {
      if(message === 'transition') {
        if(options.action) {
          Logger.debug('[FSM] ', message, ': ', options.fromState, ' -> ', options.action, ' -> ', options.toState );
        } else {
          Logger.debug('[FSM] ', message, ': ', options.fromState, ' -> ', options.toState );
        }
      }
    });
  },

  error: function(err) {
    Logger.error(err);
    appViews.error(err.message);
    this.transition('ERROR');
  },

  initialState: 'DEVICE_OFF',

  states : {

    'DEVICE_OFF': {
      _onEnter: function() {
        appViews.deviceOff();
      },

      'switch_on': function() {
        this.transition('SCANNING');
      }
    },

    'SCANNING': {
      _onEnter: function() {
        appViews.displayProgress('Scanning...');

        this.handle('getChannelList');

      },
      'getChannelList': function() {
        storage.volatile.put('channel_list', [{name: 'BBC1', data:''}, {name:'BBC2', data:''}]);

        var self = this;
        setTimeout(function() {
          var channelList = storage.volatile.get('channel_list');
          if(!channelList || channelList.length === 0) {
            return error({message: 'Unable to discover any channel'});
          }
          self.transition('CHANNEL_LIST');
        }, 100);
      }
    },

    'CHANNEL_LIST': {
      _onEnter: function() {
        appViews.channelList(storage.volatile.get('channel_list'));

        var self = this;
        $('.channel-list>a').click(function() {
          self.handle('onChannelClick',  $(this).attr('data-channel'));
        });
      },

      'onChannelClick': function(channel) {
        var self = this;

        var mode = storage.persistent.get('mode');
        var serviceProvider = channel;
        storage.volatile.put('current_channel', channel);

        if (storage.persistent.get('client_information')) {
          // TODO: Check according to the SP.

          if(storage.persistent.getValue('access_token', serviceProvider)) {
            self.transition('SUCCESSFUL_PAIRING');
          } else {
            var pairingCode = storage.persistent.getValue('pairing_code', serviceProvider);
            if(mode === 'USER_MODE') {
              if(!pairingCode) {
                self.transition('AUTHORIZATION_INIT');
              } else {
                self.transition('AUTHORIZATION_PENDING');
              }

            }
            else if(mode === 'CLIENT_MODE') {
              self.transition('CLIENT_AUTH_INIT');
            }
            else {
              self.transition('MODE_SELECTION');
            }
          }
        } else {
          self.transition('CLIENT_REGISTRATION');
        }
      }
    },

    'CLIENT_REGISTRATION': {
      _onEnter: function() {
        appViews.displayProgress('Client registration');
        var self = this;

        cpaProtocol.registerClient('Demo Client', 'cpa-client', '1.0.1', function(err, statusCode, body) {
          if(err) {
            return error(err);
          }
          self.transition('MODE_SELECTION');
        });
      }
    },

    'MODE_SELECTION': {
      _onEnter: function() {

        cpaProtocol.getAvailableModes('https://sp/', function(err, availableModes) {
          if(err) {
            return self.error(err);
          }
          appViews.displayModeSelection(availableModes);
        });

        var self = this;
        $('a.list-group-item').click(function() {
          self.handle('onModeClick',  $(this).attr('data-mode'));
        });
      },

      'onModeClick': function(mode) {
        var self = this;
        storage.persistent.put('mode', mode);

        // TODO: Check according to the SP.
        if(mode === 'USER_MODE') {
          self.transition('AUTHORIZATION_INIT');
        }
        else if(mode === 'CLIENT_MODE') {
          self.transition('CLIENT_AUTH_INIT');
        }
        else {
          return error(new Error('Unknown mode'));
        }

      }
    },

    'AUTHORIZATION_INIT': {
      _onEnter: function() {
        var self = this;

        var clientId = storage.persistent.get('client_information').client_id;
        var serviceProvider = storage.volatile.get('current_channel');

        cpaProtocol.requestUserCode(clientId, serviceProvider, function(err){
          if(err) {
            return self.error(err);
          }

          self.transition('AUTHORIZATION_PENDING');
        });
      }
    },

    'CLIENT_AUTH_INIT': {
      _onEnter: function() {
        var self = this;

        var client = storage.persistent.get('client_information');
        var serviceProvider = storage.volatile.get('current_channel');

        cpaProtocol.requestClientAccessToken(client.client_id,
          client.client_secret,
          serviceProvider,
          function(err){
            if(err) {
              return self.error(err);
            }
            self.transition('SUCCESSFUL_PAIRING');
        });
      }
    },

    'AUTHORIZATION_PENDING': {
      _onEnter: function(){
        var self = this;
        var serviceProvider = storage.volatile.get('current_channel');
        var pairingCode = storage.persistent.getValue('pairing_code', serviceProvider);

        appViews.displayUserCode(pairingCode.user_code, pairingCode.verification_uri);
        $('#verify_code_btn').click(function() { self.handle('onValidatePairingClick'); });
      },

      'onValidatePairingClick': function() {
        this.transition('AUTHORIZATION_CHECK');
      }
    },

    'AUTHORIZATION_CHECK': {
      _onEnter: function() {
        var self = this;
        var serviceProvider = storage.volatile.get('current_channel');
        var pairingCode = storage.persistent.getValue('pairing_code', serviceProvider);
        var clientInformation = storage.persistent.get('client_information');

        cpaProtocol.requestAccessToken(clientInformation.client_id,
          pairingCode.device_code, serviceProvider, function(err, authorizationPending){
            if(err) {
              self.error(err);
            } else if(authorizationPending) {
              alert('Go to the website');
              self.transition('AUTHORIZATION_PENDING');
            } else {
              self.transition('SUCCESSFUL_PAIRING');
            }
          });
      }
    },

    'SUCCESSFUL_PAIRING': {
      _onEnter: function() {
        var serviceProvider = storage.volatile.get('current_channel');
        var accessToken = storage.persistent.getValue('access_token', serviceProvider);
        var mode = storage.persistent.get('mode');

        appViews.successfulPairing(accessToken.token, mode);

        $('#trig-with-btn').click(function(){
          requestHelper.get(config.service_provider_url[serviceProvider] + '/resource', accessToken.token)
            .success(function(data, textStatus, jqXHR) {
              Logger.info('Reply ' + jqXHR.status + '(' + textStatus + '): ', data);
              alert(data.message);
            })
            .fail(function(jqXHR, textStatus) {
              Logger.error('Reply ' + jqXHR.status + '(' + textStatus + '): ', 'invalid request');
              alert('invalid request');
            });
        });

        $('#trig-without-btn').click(function(){
          requestHelper.get(config.service_provider_url[serviceProvider] + '/resource', null)
            .success(function(data, textStatus, jqXHR) {
              Logger.info('Reply ' + jqXHR.status + '(' + textStatus + '): ', data);
              alert(data.message);
            })
            .fail(function(jqXHR, textStatus) {
              Logger.error('Reply ' + jqXHR.status + '(' + textStatus + '): ', 'invalid request');
              alert('invalid request');
            });
        });
      }
    },

    'ERROR': {
      _onEnter: function() {
        Logger.error('end');

      }
    }
  }
});

//appFsm.handle('switch_on');
<|endoftext|>