<commit_before>// Copyright 2014-2020, University of Colorado Boulder

/**
 * Shows a readout of the elapsed time, with play and pause buttons.  By default there are no units (which could be used
 * if all of a simulations time units are in 'seconds'), or you can specify a selection of units to choose from.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Anton Ulyanov (Mlearner)
 */

import Bounds2 from '../../dot/js/Bounds2.js';
import Utils from '../../dot/js/Utils.js';
import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js';
import merge from '../../phet-core/js/merge.js';
import StringUtils from '../../phetcommon/js/util/StringUtils.js';
import DragListener from '../../scenery/js/listeners/DragListener.js';
import Circle from '../../scenery/js/nodes/Circle.js';
import HBox from '../../scenery/js/nodes/HBox.js';
import Node from '../../scenery/js/nodes/Node.js';
import Path from '../../scenery/js/nodes/Path.js';
import VBox from '../../scenery/js/nodes/VBox.js';
import BooleanRectangularToggleButton from '../../sun/js/buttons/BooleanRectangularToggleButton.js';
import RectangularPushButton from '../../sun/js/buttons/RectangularPushButton.js';
import Tandem from '../../tandem/js/Tandem.js';
import DragBoundsProperty from './DragBoundsProperty.js';
import NumberDisplay from './NumberDisplay.js';
import PauseIconShape from './PauseIconShape.js';
import PhetFont from './PhetFont.js';
import PlayIconShape from './PlayIconShape.js';
import sceneryPhet from './sceneryPhet.js';
import sceneryPhetStrings from './sceneryPhetStrings.js';
import ShadedRectangle from './ShadedRectangle.js';
import Stopwatch from './Stopwatch.js';
import UTurnArrowShape from './UTurnArrowShape.js';

class StopwatchNode extends Node {

  /**
   * @param {Stopwatch} stopwatch
   * @param {Object} [options]
   */
  constructor( stopwatch, options ) {
    assert && assert( stopwatch instanceof Stopwatch, `invalid stopwatch: ${stopwatch}` );

    options = merge( {

      cursor: 'pointer',
      iconHeight: 10,
      numberDisplayRange: Stopwatch.ZERO_TO_ALMOST_SIXTY, // Just for sizing the display, sized for 59:59.99 (mm:ss) or 3599.99 (decimal)
      iconFill: 'black',
      iconLineWidth: 1,
      backgroundBaseColor: 'rgb( 80, 130, 230 )',
      buttonBaseColor: '#DFE0E1',
      xSpacing: 6, // horizontal space between the buttons
      ySpacing: 6, // vertical space between readout and buttons
      xMargin: 8,
      yMargin: 8,

      // options propagated to the NumberDisplay
      numberDisplayOptions: {
        numberFormatter: StopwatchNode.RICH_TEXT_MINUTES_AND_SECONDS,
        useRichText: true,
        textOptions: {
          font: StopwatchNode.DEFAULT_FONT
        },
        align: 'right',
        cornerRadius: 4,
        xMargin: 4,
        yMargin: 2,
        pickable: false // allow dragging by the number display
      },

      // {Property.<Bounds2>|null} If provided, the stopwatch is draggable within the bounds.  If null, the stopwatch is not draggable.
      dragBoundsProperty: null,

      // Tandem is required to make sure the buttons are instrumented
      tandem: Tandem.REQUIRED,

      // options propagated to the DragListener.
      dragListenerOptions: {
        start: _.noop
      }
    }, options );
    assert && assert( !options.hasOwnProperty( 'maxValue' ), 'options.maxValue no longer supported' );

    assert && assert( options.xSpacing >= 0, 'Buttons cannot overlap' );
    assert && assert( options.ySpacing >= 0, 'Buttons cannot overlap the readout' );

    const numberDisplay = new NumberDisplay( stopwatch.timeProperty, options.numberDisplayRange, options.numberDisplayOptions );

    // Buttons ----------------------------------------------------------------------------

    const resetPath = new Path( new UTurnArrowShape( options.iconHeight ), {
      fill: options.iconFill
    } );

    const playIconHeight = resetPath.height;
    const playIconWidth = 0.8 * playIconHeight;
    const playPath = new Path( new PlayIconShape( playIconWidth, playIconHeight ), {
      fill: options.iconFill
    } );

    const pausePath = new Path( new PauseIconShape( 0.75 * playIconWidth, playIconHeight ), {
      fill: options.iconFill
    } );

    const playPauseButton = new BooleanRectangularToggleButton( pausePath, playPath, stopwatch.isRunningProperty, {
      baseColor: options.buttonBaseColor,
      tandem: options.tandem.createTandem( 'playPauseButton' )
    } );

    const resetButton = new RectangularPushButton( {
      listener: () => {
        stopwatch.isRunningProperty.set( false );
        stopwatch.timeProperty.set( 0 );
      },
      content: resetPath,
      baseColor: options.buttonBaseColor,
      tandem: options.tandem.createTandem( 'resetButton' )
    } );

    const contents = new VBox( {
      spacing: options.ySpacing,
      children: [
        numberDisplay,
        new HBox( {
          spacing: options.xSpacing,
          children: [ resetButton, playPauseButton ]
        } )
      ]
    } );

    // Background panel ----------------------------------------------------------------------------

    const backgroundNode = new ShadedRectangle( new Bounds2( 0, 0,
      contents.width + 2 * options.xMargin, contents.height + 2 * options.yMargin ), {
      baseColor: options.backgroundBaseColor
    } );
    contents.center = backgroundNode.center;

    assert && assert( !options.children, 'StopwatchNode sets children' );
    options.children = [ backgroundNode, contents ];

    super( options );

    // Disable the reset button when time is zero, and enable the play/pause button when not at the max time
    const timeListener = time => {
      resetButton.enabled = time > 0;
      playPauseButton.enabled = time < stopwatch.timeProperty.range.max;
    };
    stopwatch.timeProperty.link( timeListener );

    // Put a red dot at the origin, for debugging layout.
    if ( phet.chipper.queryParameters.dev ) {
      this.addChild( new Circle( 3, { fill: 'red' } ) );
    }

    const stopwatchVisibleListener = visible => {
      this.visible = visible;
      if ( visible ) {
        this.moveToFront();
      }
      else {

        // interrupt user interactions when the stopwatch is made invisible
        this.interruptSubtreeInput();
      }
    };
    stopwatch.isVisibleProperty.link( stopwatchVisibleListener );

    // Move to the stopwatch's position
    const stopwatchPositionListener = position => this.setTranslation( position );
    stopwatch.positionProperty.link( stopwatchPositionListener );

    // @public (read-only) {DragListener|null} -- reassigned below, if draggable.  Can be used for forwarding press
    // events when dragging out of a toolbox.
    this.dragListener = null;

    let adjustedDragBoundsProperty = null;

    if ( options.dragBoundsProperty ) {

      // drag bounds, adjusted to keep this entire Node inside visible bounds
      adjustedDragBoundsProperty = new DragBoundsProperty( this, options.dragBoundsProperty );

      // interrupt user interactions when the visible bounds changes, such as a device orientation change or window resize
      options.dragBoundsProperty.link( () => this.interruptSubtreeInput() );

      // If the stopwatch is outside the drag bounds, move it inside.
      adjustedDragBoundsProperty.link( dragBounds => {
        if ( !dragBounds.containsPoint( stopwatch.positionProperty.value ) ) {
          stopwatch.positionProperty.value = dragBounds.closestPointTo( stopwatch.positionProperty.value );
        }
      } );

      // dragging, added to background so that other UI components get input events on touch devices
      const dragListenerOptions = merge( {
        targetNode: this,
        positionProperty: stopwatch.positionProperty,
        dragBoundsProperty: adjustedDragBoundsProperty,
        tandem: options.tandem.createTandem( 'dragListener' )
      }, options.dragListenerOptions );

      // Add moveToFront to any start function that the client provided.
      const optionsStart = dragListenerOptions.start;
      dragListenerOptions.start = () => {
        this.moveToFront();
        optionsStart();
      };

      // Dragging, added to background so that other UI components get input events on touch devices.
      // If added to 'this', touchSnag will lock out listeners for other UI components.
      this.dragListener = new DragListener( dragListenerOptions );
      backgroundNode.addInputListener( this.dragListener );

      // Move to front on pointer down, anywhere on this Node, including interactive subcomponents.
      this.addInputListener( {
        down: () => this.moveToFront()
      } );
    }

    this.addLinkedElement( stopwatch, {
      tandem: options.tandem.createTandem( 'stopwatch' )
    } );

    // @private
    this.disposeStopwatchNode = () => {
      stopwatch.isVisibleProperty.unlink( stopwatchVisibleListener );
      stopwatch.timeProperty.unlink( timeListener );
      stopwatch.positionProperty.unlink( stopwatchPositionListener );

      numberDisplay.dispose();
      resetButton.dispose();
      playPauseButton.dispose();

      if ( this.dragListener ) {
        backgroundNode.removeInputListener( this.dragListener );
        this.dragListener.dispose();
      }

      adjustedDragBoundsProperty && adjustedDragBoundsProperty.dispose();
    };

    // @private
    this.numberDisplay = numberDisplay;

    // support for binder documentation, stripped out in builds and only runs when ?binder is specified
    assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'scenery-phet', 'StopwatchNode', this );
  }

  /**
   * @param {function(number):string} numberFormatter
   * @public
   */
  setNumberFormatter( numberFormatter ) {
    this.numberDisplay.setNumberFormatter( numberFormatter );
  }

  // @public - redraw the text when something other than the numberProperty changes (such as units, formatter, etc).
  redrawNumberDisplay() {
    this.numberDisplay.recomputeText();
  }

  /**
   * Release resources when no longer be used.
   * @public
   * @override
   */
  dispose() {
    this.disposeStopwatchNode();
    super.dispose();
  }

  /**
   * Gets the centiseconds (hundredths-of-a-second) string for a time value.
   * @public (read-only, unit-tests)
   * @param {number} time
   * @param {number} numberDecimalPlaces
   * @returns {string}
   */
  static getDecimalPlaces( time, numberDecimalPlaces ) {

    const max = Math.pow( 10, numberDecimalPlaces );

    // Round to the nearest centisecond, see https://github.com/phetsims/masses-and-springs/issues/156
    time = Utils.roundSymmetric( time * max ) / max;

    // Rounding after mod, in case there is floating-point error
    let decimalValue = `${Utils.roundSymmetric( time % 1 * max )}`;
    while ( decimalValue.length < numberDecimalPlaces ) {
      decimalValue = `0${decimalValue}`;
    }
    return `.${decimalValue}`;
  }

  /**
   * Creates a custom value for options.numberDisplayOptions.numberFormatter, passed to NumberDisplay.
   * @public
   * @param {Object} [options]
   * @returns {function(time:number):string} - see NumberDisplay options.numberFormatter
   */
  static createRichTextNumberFormatter( options ) {

    options = merge( {

      // If true, the time value is converted to minutes and seconds, and the format looks like 59:59.00.
      // If false, time is formatted as a decimal value, like 123.45
      showAsMinutesAndSeconds: true,
      numberOfDecimalPlaces: 2,
      bigNumberFont: 20,
      smallNumberFont: 14,
      unitsFont: 14,
      units: '',

      // Units cannot be baked into the i18n string because they can change independently
      valueUnitsPattern: sceneryPhetStrings.stopwatchValueUnitsPattern
    }, options );

    return time => {
      const minutesAndSeconds = options.showAsMinutesAndSeconds ? toMinutesAndSeconds( time ) : Math.floor( time );
      const centiseconds = StopwatchNode.getDecimalPlaces( time, options.numberOfDecimalPlaces );

      // Single quotes around CSS style so the double-quotes in the CSS font family work. Himalaya doesn't like "
      // See https://github.com/phetsims/collision-lab/issues/140.
      return StringUtils.fillIn( options.valueUnitsPattern, {
        value: `${minutesAndSeconds}${centiseconds}`,
        units: `<span style='font-size: ${options.unitsFont}px; font-family:${StopwatchNode.NUMBER_FONT_FAMILY};'>${options.units}</span>`
      } );
    };
  }
}

// @public - We used to use Lucida Console, Arial, but Arial has smaller number width for "11" and hence was causing
// jitter. Neither Trebuchet MS and Lucida Grande is a monospace font, but the digits all appear to be monospace.
// Use Trebuchet first, since it has broader cross-platform support.
// Another advantage of using a non-monospace font (that has monospace digits) is that the : and . symbols aren't as
// wide as the numerals.  @ariel-phet and @samreid tested this combination of families on Mac/Chrome and Windows/Chrome
// and it seemed to work nicely, with no jitter.
StopwatchNode.NUMBER_FONT_FAMILY = '"Trebuchet MS", "Lucida Grande", monospace';

// @public
StopwatchNode.DEFAULT_FONT = new PhetFont( { size: 20, family: StopwatchNode.NUMBER_FONT_FAMILY } );

/**
 * A value for options.numberDisplayOptions.numberFormatter where time is interpreted as minutes and seconds.
 * The format is MM:SS.CC, where M=minutes, S=seconds, C=centiseconds. The returned string is plain text, so all
 * digits will be the same size, and the client is responsible for setting the font size.
 *
 * @public
 * @static
 * @param {number} time
 * @returns {string} - plain text
 */
StopwatchNode.PLAIN_TEXT_MINUTES_AND_SECONDS = time => {
  const minutesAndSeconds = toMinutesAndSeconds( time );
  const centiseconds = StopwatchNode.getDecimalPlaces( time, 2 );
  return minutesAndSeconds + centiseconds;
};

/**
 * A value for options.numberDisplayOptions.numberFormatter where time is interpreted as minutes and seconds.
 * The format is format MM:SS.cc, where M=minutes, S=seconds, c=centiseconds. The string returned is in RichText
 * format, with the 'c' digits in a smaller font.
 *
 * @public
 * @static
 * @param {number} time
 * @returns {string} - RichText format
 */
StopwatchNode.RICH_TEXT_MINUTES_AND_SECONDS = StopwatchNode.createRichTextNumberFormatter( {
  showAsMinutesAndSeconds: true,
  numberOfDecimalPlaces: 2
} );

/**
 * Converts a time to a string in {{minutes}}:{{seconds}} format.
 * @param {number} time
 * @returns {string}
 */
function toMinutesAndSeconds( time ) {

  // Round to the nearest centi-part (if time is in seconds, this would be centiseconds)
  // see https://github.com/phetsims/masses-and-springs/issues/156
  time = Utils.roundSymmetric( time * 100 ) / 100;

  // When showing units, don't show the "00:" prefix, see https://github.com/phetsims/scenery-phet/issues/378
  const timeInSeconds = time;

  // If no units are provided, then we assume the time is in seconds, and should be shown in mm:ss.cs
  let minutes = Math.floor( timeInSeconds / 60 );
  let seconds = Math.floor( timeInSeconds ) % 60;

  if ( seconds < 10 ) {
    seconds = `0${seconds}`;
  }
  if ( minutes < 10 ) {
    minutes = `0${minutes}`;
  }
  return `${minutes}:${seconds}`;
}

sceneryPhet.register( 'StopwatchNode', StopwatchNode );
export default StopwatchNode;<commit_msg>restore this.dragTarget, https://github.com/phetsims/scenery-phet/issues/659<commit_after>// Copyright 2014-2020, University of Colorado Boulder

/**
 * Shows a readout of the elapsed time, with play and pause buttons.  By default there are no units (which could be used
 * if all of a simulations time units are in 'seconds'), or you can specify a selection of units to choose from.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Anton Ulyanov (Mlearner)
 */

import Bounds2 from '../../dot/js/Bounds2.js';
import Utils from '../../dot/js/Utils.js';
import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js';
import merge from '../../phet-core/js/merge.js';
import StringUtils from '../../phetcommon/js/util/StringUtils.js';
import DragListener from '../../scenery/js/listeners/DragListener.js';
import Circle from '../../scenery/js/nodes/Circle.js';
import HBox from '../../scenery/js/nodes/HBox.js';
import Node from '../../scenery/js/nodes/Node.js';
import Path from '../../scenery/js/nodes/Path.js';
import VBox from '../../scenery/js/nodes/VBox.js';
import BooleanRectangularToggleButton from '../../sun/js/buttons/BooleanRectangularToggleButton.js';
import RectangularPushButton from '../../sun/js/buttons/RectangularPushButton.js';
import Tandem from '../../tandem/js/Tandem.js';
import DragBoundsProperty from './DragBoundsProperty.js';
import NumberDisplay from './NumberDisplay.js';
import PauseIconShape from './PauseIconShape.js';
import PhetFont from './PhetFont.js';
import PlayIconShape from './PlayIconShape.js';
import sceneryPhet from './sceneryPhet.js';
import sceneryPhetStrings from './sceneryPhetStrings.js';
import ShadedRectangle from './ShadedRectangle.js';
import Stopwatch from './Stopwatch.js';
import UTurnArrowShape from './UTurnArrowShape.js';

class StopwatchNode extends Node {

  /**
   * @param {Stopwatch} stopwatch
   * @param {Object} [options]
   */
  constructor( stopwatch, options ) {
    assert && assert( stopwatch instanceof Stopwatch, `invalid stopwatch: ${stopwatch}` );

    options = merge( {

      cursor: 'pointer',
      iconHeight: 10,
      numberDisplayRange: Stopwatch.ZERO_TO_ALMOST_SIXTY, // Just for sizing the display, sized for 59:59.99 (mm:ss) or 3599.99 (decimal)
      iconFill: 'black',
      iconLineWidth: 1,
      backgroundBaseColor: 'rgb( 80, 130, 230 )',
      buttonBaseColor: '#DFE0E1',
      xSpacing: 6, // horizontal space between the buttons
      ySpacing: 6, // vertical space between readout and buttons
      xMargin: 8,
      yMargin: 8,

      // options propagated to the NumberDisplay
      numberDisplayOptions: {
        numberFormatter: StopwatchNode.RICH_TEXT_MINUTES_AND_SECONDS,
        useRichText: true,
        textOptions: {
          font: StopwatchNode.DEFAULT_FONT
        },
        align: 'right',
        cornerRadius: 4,
        xMargin: 4,
        yMargin: 2,
        pickable: false // allow dragging by the number display
      },

      // {Property.<Bounds2>|null} If provided, the stopwatch is draggable within the bounds.  If null, the stopwatch is not draggable.
      dragBoundsProperty: null,

      // Tandem is required to make sure the buttons are instrumented
      tandem: Tandem.REQUIRED,

      // options propagated to the DragListener.
      dragListenerOptions: {
        start: _.noop
      }
    }, options );
    assert && assert( !options.hasOwnProperty( 'maxValue' ), 'options.maxValue no longer supported' );

    assert && assert( options.xSpacing >= 0, 'Buttons cannot overlap' );
    assert && assert( options.ySpacing >= 0, 'Buttons cannot overlap the readout' );

    const numberDisplay = new NumberDisplay( stopwatch.timeProperty, options.numberDisplayRange, options.numberDisplayOptions );

    // Buttons ----------------------------------------------------------------------------

    const resetPath = new Path( new UTurnArrowShape( options.iconHeight ), {
      fill: options.iconFill
    } );

    const playIconHeight = resetPath.height;
    const playIconWidth = 0.8 * playIconHeight;
    const playPath = new Path( new PlayIconShape( playIconWidth, playIconHeight ), {
      fill: options.iconFill
    } );

    const pausePath = new Path( new PauseIconShape( 0.75 * playIconWidth, playIconHeight ), {
      fill: options.iconFill
    } );

    const playPauseButton = new BooleanRectangularToggleButton( pausePath, playPath, stopwatch.isRunningProperty, {
      baseColor: options.buttonBaseColor,
      tandem: options.tandem.createTandem( 'playPauseButton' )
    } );

    const resetButton = new RectangularPushButton( {
      listener: () => {
        stopwatch.isRunningProperty.set( false );
        stopwatch.timeProperty.set( 0 );
      },
      content: resetPath,
      baseColor: options.buttonBaseColor,
      tandem: options.tandem.createTandem( 'resetButton' )
    } );

    const contents = new VBox( {
      spacing: options.ySpacing,
      children: [
        numberDisplay,
        new HBox( {
          spacing: options.xSpacing,
          children: [ resetButton, playPauseButton ]
        } )
      ]
    } );

    // Background panel ----------------------------------------------------------------------------

    const backgroundNode = new ShadedRectangle( new Bounds2( 0, 0,
      contents.width + 2 * options.xMargin, contents.height + 2 * options.yMargin ), {
      baseColor: options.backgroundBaseColor
    } );
    contents.center = backgroundNode.center;

    assert && assert( !options.children, 'StopwatchNode sets children' );
    options.children = [ backgroundNode, contents ];

    super( options );

    // @public (read-only) - Target for drag listeners
    this.dragTarget = backgroundNode;

    // Disable the reset button when time is zero, and enable the play/pause button when not at the max time
    const timeListener = time => {
      resetButton.enabled = time > 0;
      playPauseButton.enabled = time < stopwatch.timeProperty.range.max;
    };
    stopwatch.timeProperty.link( timeListener );

    // Put a red dot at the origin, for debugging layout.
    if ( phet.chipper.queryParameters.dev ) {
      this.addChild( new Circle( 3, { fill: 'red' } ) );
    }

    const stopwatchVisibleListener = visible => {
      this.visible = visible;
      if ( visible ) {
        this.moveToFront();
      }
      else {

        // interrupt user interactions when the stopwatch is made invisible
        this.interruptSubtreeInput();
      }
    };
    stopwatch.isVisibleProperty.link( stopwatchVisibleListener );

    // Move to the stopwatch's position
    const stopwatchPositionListener = position => this.setTranslation( position );
    stopwatch.positionProperty.link( stopwatchPositionListener );

    // @public (read-only) {DragListener|null} -- reassigned below, if draggable.  Can be used for forwarding press
    // events when dragging out of a toolbox.
    this.dragListener = null;

    let adjustedDragBoundsProperty = null;

    if ( options.dragBoundsProperty ) {

      // drag bounds, adjusted to keep this entire Node inside visible bounds
      adjustedDragBoundsProperty = new DragBoundsProperty( this, options.dragBoundsProperty );

      // interrupt user interactions when the visible bounds changes, such as a device orientation change or window resize
      options.dragBoundsProperty.link( () => this.interruptSubtreeInput() );

      // If the stopwatch is outside the drag bounds, move it inside.
      adjustedDragBoundsProperty.link( dragBounds => {
        if ( !dragBounds.containsPoint( stopwatch.positionProperty.value ) ) {
          stopwatch.positionProperty.value = dragBounds.closestPointTo( stopwatch.positionProperty.value );
        }
      } );

      // dragging, added to background so that other UI components get input events on touch devices
      const dragListenerOptions = merge( {
        targetNode: this,
        positionProperty: stopwatch.positionProperty,
        dragBoundsProperty: adjustedDragBoundsProperty,
        tandem: options.tandem.createTandem( 'dragListener' )
      }, options.dragListenerOptions );

      // Add moveToFront to any start function that the client provided.
      const optionsStart = dragListenerOptions.start;
      dragListenerOptions.start = () => {
        this.moveToFront();
        optionsStart();
      };

      // Dragging, added to background so that other UI components get input events on touch devices.
      // If added to 'this', touchSnag will lock out listeners for other UI components.
      this.dragListener = new DragListener( dragListenerOptions );
      this.dragTarget.addInputListener( this.dragListener );

      // Move to front on pointer down, anywhere on this Node, including interactive subcomponents.
      this.addInputListener( {
        down: () => this.moveToFront()
      } );
    }

    this.addLinkedElement( stopwatch, {
      tandem: options.tandem.createTandem( 'stopwatch' )
    } );

    // @private
    this.disposeStopwatchNode = () => {
      stopwatch.isVisibleProperty.unlink( stopwatchVisibleListener );
      stopwatch.timeProperty.unlink( timeListener );
      stopwatch.positionProperty.unlink( stopwatchPositionListener );

      numberDisplay.dispose();
      resetButton.dispose();
      playPauseButton.dispose();

      if ( this.dragListener ) {
        this.dragTarget.removeInputListener( this.dragListener );
        this.dragListener.dispose();
      }

      adjustedDragBoundsProperty && adjustedDragBoundsProperty.dispose();
    };

    // @private
    this.numberDisplay = numberDisplay;

    // support for binder documentation, stripped out in builds and only runs when ?binder is specified
    assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'scenery-phet', 'StopwatchNode', this );
  }

  /**
   * @param {function(number):string} numberFormatter
   * @public
   */
  setNumberFormatter( numberFormatter ) {
    this.numberDisplay.setNumberFormatter( numberFormatter );
  }

  // @public - redraw the text when something other than the numberProperty changes (such as units, formatter, etc).
  redrawNumberDisplay() {
    this.numberDisplay.recomputeText();
  }

  /**
   * Release resources when no longer be used.
   * @public
   * @override
   */
  dispose() {
    this.disposeStopwatchNode();
    super.dispose();
  }

  /**
   * Gets the centiseconds (hundredths-of-a-second) string for a time value.
   * @public (read-only, unit-tests)
   * @param {number} time
   * @param {number} numberDecimalPlaces
   * @returns {string}
   */
  static getDecimalPlaces( time, numberDecimalPlaces ) {

    const max = Math.pow( 10, numberDecimalPlaces );

    // Round to the nearest centisecond, see https://github.com/phetsims/masses-and-springs/issues/156
    time = Utils.roundSymmetric( time * max ) / max;

    // Rounding after mod, in case there is floating-point error
    let decimalValue = `${Utils.roundSymmetric( time % 1 * max )}`;
    while ( decimalValue.length < numberDecimalPlaces ) {
      decimalValue = `0${decimalValue}`;
    }
    return `.${decimalValue}`;
  }

  /**
   * Creates a custom value for options.numberDisplayOptions.numberFormatter, passed to NumberDisplay.
   * @public
   * @param {Object} [options]
   * @returns {function(time:number):string} - see NumberDisplay options.numberFormatter
   */
  static createRichTextNumberFormatter( options ) {

    options = merge( {

      // If true, the time value is converted to minutes and seconds, and the format looks like 59:59.00.
      // If false, time is formatted as a decimal value, like 123.45
      showAsMinutesAndSeconds: true,
      numberOfDecimalPlaces: 2,
      bigNumberFont: 20,
      smallNumberFont: 14,
      unitsFont: 14,
      units: '',

      // Units cannot be baked into the i18n string because they can change independently
      valueUnitsPattern: sceneryPhetStrings.stopwatchValueUnitsPattern
    }, options );

    return time => {
      const minutesAndSeconds = options.showAsMinutesAndSeconds ? toMinutesAndSeconds( time ) : Math.floor( time );
      const centiseconds = StopwatchNode.getDecimalPlaces( time, options.numberOfDecimalPlaces );

      // Single quotes around CSS style so the double-quotes in the CSS font family work. Himalaya doesn't like "
      // See https://github.com/phetsims/collision-lab/issues/140.
      return StringUtils.fillIn( options.valueUnitsPattern, {
        value: `${minutesAndSeconds}${centiseconds}`,
        units: `<span style='font-size: ${options.unitsFont}px; font-family:${StopwatchNode.NUMBER_FONT_FAMILY};'>${options.units}</span>`
      } );
    };
  }
}

// @public - We used to use Lucida Console, Arial, but Arial has smaller number width for "11" and hence was causing
// jitter. Neither Trebuchet MS and Lucida Grande is a monospace font, but the digits all appear to be monospace.
// Use Trebuchet first, since it has broader cross-platform support.
// Another advantage of using a non-monospace font (that has monospace digits) is that the : and . symbols aren't as
// wide as the numerals.  @ariel-phet and @samreid tested this combination of families on Mac/Chrome and Windows/Chrome
// and it seemed to work nicely, with no jitter.
StopwatchNode.NUMBER_FONT_FAMILY = '"Trebuchet MS", "Lucida Grande", monospace';

// @public
StopwatchNode.DEFAULT_FONT = new PhetFont( { size: 20, family: StopwatchNode.NUMBER_FONT_FAMILY } );

/**
 * A value for options.numberDisplayOptions.numberFormatter where time is interpreted as minutes and seconds.
 * The format is MM:SS.CC, where M=minutes, S=seconds, C=centiseconds. The returned string is plain text, so all
 * digits will be the same size, and the client is responsible for setting the font size.
 *
 * @public
 * @static
 * @param {number} time
 * @returns {string} - plain text
 */
StopwatchNode.PLAIN_TEXT_MINUTES_AND_SECONDS = time => {
  const minutesAndSeconds = toMinutesAndSeconds( time );
  const centiseconds = StopwatchNode.getDecimalPlaces( time, 2 );
  return minutesAndSeconds + centiseconds;
};

/**
 * A value for options.numberDisplayOptions.numberFormatter where time is interpreted as minutes and seconds.
 * The format is format MM:SS.cc, where M=minutes, S=seconds, c=centiseconds. The string returned is in RichText
 * format, with the 'c' digits in a smaller font.
 *
 * @public
 * @static
 * @param {number} time
 * @returns {string} - RichText format
 */
StopwatchNode.RICH_TEXT_MINUTES_AND_SECONDS = StopwatchNode.createRichTextNumberFormatter( {
  showAsMinutesAndSeconds: true,
  numberOfDecimalPlaces: 2
} );

/**
 * Converts a time to a string in {{minutes}}:{{seconds}} format.
 * @param {number} time
 * @returns {string}
 */
function toMinutesAndSeconds( time ) {

  // Round to the nearest centi-part (if time is in seconds, this would be centiseconds)
  // see https://github.com/phetsims/masses-and-springs/issues/156
  time = Utils.roundSymmetric( time * 100 ) / 100;

  // When showing units, don't show the "00:" prefix, see https://github.com/phetsims/scenery-phet/issues/378
  const timeInSeconds = time;

  // If no units are provided, then we assume the time is in seconds, and should be shown in mm:ss.cs
  let minutes = Math.floor( timeInSeconds / 60 );
  let seconds = Math.floor( timeInSeconds ) % 60;

  if ( seconds < 10 ) {
    seconds = `0${seconds}`;
  }
  if ( minutes < 10 ) {
    minutes = `0${minutes}`;
  }
  return `${minutes}:${seconds}`;
}

sceneryPhet.register( 'StopwatchNode', StopwatchNode );
export default StopwatchNode;<|endoftext|>


<commit_before>/*
 * Video.js Hotkeys
 * https://github.com/ctd1500/videojs-hotkeys
 *
 * Copyright (c) 2015 Chris Dougherty
 * Licensed under the Apache-2.0 license.
 */

(function(window, videojs) {
  'use strict';

  window['videojs_hotkeys'] = { version: "0.2.8" };

  var hotkeys = function(options) {
    var player = this;
    var pQuerySlector = player.el().querySelector;

    var playPauseKey = function(e) {
      return (e.which === 32);
    };

    var rewindKey = function(e) {
      // Left Arrow
      return (e.which === 37);
    };

    var forwardKey = function(e) {
      // Right Arrow
      return (e.which === 39);
    };

    var volumeUpKey = function(e) {
      // Up Arrow
      return (e.which === 38);
    };

    var volumeDownKey = function(e) {
      // Down Arrow
      return (e.which === 40);
    };

    var muteKey = function(e) {
      // M key
      return (e.which === 77);
    };

    var fullscreenKey = function(e) {
      // F key
      return (e.which === 70);
    };

    var def_options = {
      volumeStep: 0.1,
      seekStep: 5,
      enableMute: true,
      enableFullscreen: true,
      enableNumbers: true,
      enableJogStyle: false,
      alwaysCaptureHotkeys: false,
      playPauseKey: playPauseKey,
      rewindKey: rewindKey,
      forwardKey: forwardKey,
      volumeUpKey: volumeUpKey,
      volumeDownKey: volumeDownKey,
      muteKey: muteKey,
      fullscreenKey: fullscreenKey
    };

    // Use built-in merge function from Video.js v5.0+ or v4.4.0+
    var mergeOptions = videojs.mergeOptions || videojs.util.mergeOptions;
    options = mergeOptions(def_options, options || {});

    var volumeStep = options.volumeStep;
    var seekStep = options.seekStep;
    var enableMute = options.enableMute;
    var enableFull = options.enableFullscreen;
    var enableNumbers = options.enableNumbers;
    var enableJogStyle = options.enableJogStyle;
    var alwaysCaptureHotkeys = options.alwaysCaptureHotkeys;

    // Set default player tabindex to handle keydown and doubleclick events
    if (!player.el().hasAttribute('tabIndex')) {
      player.el().setAttribute('tabIndex', '-1');
    }

    if (alwaysCaptureHotkeys) {
      player.one('play', function() {
        player.el().focus(); // Fixes the .vjs-big-play-button handing focus back to body instead of the player
      });
    }

    player.on('play', function() {
      // Fix allowing the YouTube plugin to have hotkey support.
      var ifblocker = player.el().querySelector('.iframeblocker');
      if (ifblocker && ifblocker.style.display === '') {
        ifblocker.style.display = "block";
        ifblocker.style.bottom = "39px";
      }
    });

    var keyDown = function keyDown(event) {

      var ewhich = event.which, curTime;
      var ePreventDefault = event.preventDefault;
      // When controls are disabled, hotkeys will be disabled as well
      if (player.controls()) {

        // Don't catch keys if any control buttons are focused, unless alwaysCaptureHotkeys is true
        var activeEl = document.activeElement;
        if (alwaysCaptureHotkeys ||
            activeEl == player.el() ||
            activeEl == pQuerySlector('.vjs-tech') ||
            activeEl == pQuerySlector('.vjs-control-bar') ||
            activeEl == pQuerySlector('.iframeblocker')) {

          switch (checkKeys(event)) {

            // Spacebar toggles play/pause
            case "play":
              ePreventDefault();
              if (alwaysCaptureHotkeys) {
                // Prevent control activation with space
                event.stopPropagation();
              }

              if (player.paused()) {
                player.play();
              } else {
                player.pause();
              }
              break;

            // Seeking with the left/right arrow keys
            case "rewind": // Seek Backward
              ePreventDefault();
              curTime = player.currentTime() - seekStep;
              // The flash player tech will allow you to seek into negative
              // numbers and break the seekbar, so try to prevent that.
              if (player.currentTime() <= seekStep) {
                curTime = 0;
              }
              player.currentTime(curTime);
              break;
            case "forward": // Seek Forward
              ePreventDefault();
              player.currentTime(player.currentTime() + seekStep);
              break;

            // Volume control with the up/down arrow keys
            case "volumeDown":
              ePreventDefault();
              if (!enableJogStyle) {
                player.volume(player.volume() - volumeStep);
              } else {
                curTime = player.currentTime() - 1;
                if (player.currentTime() <= 1) {
                  curTime = 0;
                }
                player.currentTime(curTime);
              }
              break;
            case "volumeUp":
              ePreventDefault();
              if (!enableJogStyle) {
                player.volume(player.volume() + volumeStep);
              } else {
                player.currentTime(player.currentTime() + 1);
              }
              break;

            // Toggle Mute with the M key
            case "mute":
              if (enableMute) {
                if (player.muted()) {
                  player.muted(false);
                } else {
                  player.muted(true);
                }
              }
              break;

            // Toggle Fullscreen with the F key
            case  "fullscreen":
              if (enableFull) {
                if (player.isFullscreen()) {
                  player.exitFullscreen();
                } else {
                  player.requestFullscreen();
                }
              }
              break;

            default:
              // Number keys from 0-9 skip to a percentage of the video. 0 is 0% and 9 is 90%
              if ((ewhich > 47 && ewhich < 59) || (ewhich > 95 && ewhich < 106)) {
                if (enableNumbers) {
                  var sub = 48;
                  if (ewhich > 95) {
                    sub = 96;
                  }
                  var number = ewhich - sub;
                  ePreventDefault();
                  player.currentTime(player.duration() * number * 0.1);
                }
              }
          }
        }
      }
    };

    var doubleClick = function doubleClick(event) {

      // When controls are disabled, hotkeys will be disabled as well
      if (player.controls()) {

        // Don't catch clicks if any control buttons are focused
        var activeEl = event.relatedTarget || event.toElement || document.activeElement;
        if (activeEl == player.el() ||
            activeEl == pQuerySlector('.vjs-tech') ||
            activeEl == pQuerySlector('.iframeblocker')) {

          if (enableFull) {
            if (player.isFullscreen()) {
              player.exitFullscreen();
            } else {
              player.requestFullscreen();
            }
          }
        }
      }
    };

    var checkKeys = function checkKeys(e) {
      // Allow some modularity in defining custom hotkeys

      // Play/Pause check
      if (options.playPauseKey(e)) {
        return "play";
      }

      // Seek Backward check
      if (options.rewindKey(e)) {
        return "rewind";
      }

      // Seek Forward check
      if (options.forwardKey(e)) {
        return "forward";
      }

      // Volume Up check
      if (options.volumeUpKey(e)) {
        return "volumeUp";
      }

      // Volume Down check
      if (options.volumeDownKey(e)) {
        return "volumeDown";
      }

      // Mute check
      if (options.muteKey(e)) {
        return "mute";
      }

      // Fullscreen check
      if (options.fullscreenKey(e)) {
        return "fullscreen";
      }
    };

    player.on('keydown', keyDown);
    player.on('dblclick', doubleClick);

    return this;
  };

  videojs.plugin('hotkeys', hotkeys);

})(window, window.videojs);
<commit_msg>Fixing some formatting and variables<commit_after>/*
 * Video.js Hotkeys
 * https://github.com/ctd1500/videojs-hotkeys
 *
 * Copyright (c) 2015 Chris Dougherty
 * Licensed under the Apache-2.0 license.
 */

(function(window, videojs) {
  'use strict';

  window['videojs_hotkeys'] = { version: "0.2.8" };

  var hotkeys = function(options) {
    var player = this;
    var pEl = player.el();

    var playPauseKey = function(e) {
      return (e.which === 32);
    };
    var rewindKey = function(e) {
      // Left Arrow
      return (e.which === 37);
    };
    var forwardKey = function(e) {
      // Right Arrow
      return (e.which === 39);
    };
    var volumeUpKey = function(e) {
      // Up Arrow
      return (e.which === 38);
    };
    var volumeDownKey = function(e) {
      // Down Arrow
      return (e.which === 40);
    };
    var muteKey = function(e) {
      // M key
      return (e.which === 77);
    };
    var fullscreenKey = function(e) {
      // F key
      return (e.which === 70);
    };

    var def_options = {
      volumeStep: 0.1,
      seekStep: 5,
      enableMute: true,
      enableFullscreen: true,
      enableNumbers: true,
      enableJogStyle: false,
      alwaysCaptureHotkeys: false,
      playPauseKey: playPauseKey,
      rewindKey: rewindKey,
      forwardKey: forwardKey,
      volumeUpKey: volumeUpKey,
      volumeDownKey: volumeDownKey,
      muteKey: muteKey,
      fullscreenKey: fullscreenKey
    };

    // Use built-in merge function from Video.js v5.0+ or v4.4.0+
    var mergeOptions = videojs.mergeOptions || videojs.util.mergeOptions;
    options = mergeOptions(def_options, options || {});

    var volumeStep = options.volumeStep;
    var seekStep = options.seekStep;
    var enableMute = options.enableMute;
    var enableFull = options.enableFullscreen;
    var enableNumbers = options.enableNumbers;
    var enableJogStyle = options.enableJogStyle;
    var alwaysCaptureHotkeys = options.alwaysCaptureHotkeys;

    // Set default player tabindex to handle keydown and doubleclick events
    if (!pEl.hasAttribute('tabIndex')) {
      pEl.setAttribute('tabIndex', '-1');
    }

    if (alwaysCaptureHotkeys) {
      player.one('play', function() {
        pEl.focus(); // Fixes the .vjs-big-play-button handing focus back to body instead of the player
      });
    }

    player.on('play', function() {
      // Fix allowing the YouTube plugin to have hotkey support.
      var ifblocker = pEl.querySelector('.iframeblocker');
      if (ifblocker && ifblocker.style.display === '') {
        ifblocker.style.display = "block";
        ifblocker.style.bottom = "39px";
      }
    });

    var keyDown = function keyDown(event) {

      var ewhich = event.which, curTime;
      var ePreventDefault = event.preventDefault;
      // When controls are disabled, hotkeys will be disabled as well
      if (player.controls()) {

        // Don't catch keys if any control buttons are focused, unless alwaysCaptureHotkeys is true
        var activeEl = document.activeElement;
        if (alwaysCaptureHotkeys ||
            activeEl == pEl ||
            activeEl == pEl.querySelector('.vjs-tech') ||
            activeEl == pEl.querySelector('.vjs-control-bar') ||
            activeEl == pEl.querySelector('.iframeblocker')) {

          switch (checkKeys(event)) {

            // Spacebar toggles play/pause
            case "play":
              ePreventDefault();
              if (alwaysCaptureHotkeys) {
                // Prevent control activation with space
                event.stopPropagation();
              }

              if (player.paused()) {
                player.play();
              } else {
                player.pause();
              }
              break;

            // Seeking with the left/right arrow keys
            case "rewind": // Seek Backward
              ePreventDefault();
              curTime = player.currentTime() - seekStep;
              // The flash player tech will allow you to seek into negative
              // numbers and break the seekbar, so try to prevent that.
              if (player.currentTime() <= seekStep) {
                curTime = 0;
              }
              player.currentTime(curTime);
              break;
            case "forward": // Seek Forward
              ePreventDefault();
              player.currentTime(player.currentTime() + seekStep);
              break;

            // Volume control with the up/down arrow keys
            case "volumeDown":
              ePreventDefault();
              if (!enableJogStyle) {
                player.volume(player.volume() - volumeStep);
              } else {
                curTime = player.currentTime() - 1;
                if (player.currentTime() <= 1) {
                  curTime = 0;
                }
                player.currentTime(curTime);
              }
              break;
            case "volumeUp":
              ePreventDefault();
              if (!enableJogStyle) {
                player.volume(player.volume() + volumeStep);
              } else {
                player.currentTime(player.currentTime() + 1);
              }
              break;

            // Toggle Mute with the M key
            case "mute":
              if (enableMute) {
                if (player.muted()) {
                  player.muted(false);
                } else {
                  player.muted(true);
                }
              }
              break;

            // Toggle Fullscreen with the F key
            case  "fullscreen":
              if (enableFull) {
                if (player.isFullscreen()) {
                  player.exitFullscreen();
                } else {
                  player.requestFullscreen();
                }
              }
              break;

            default:
              // Number keys from 0-9 skip to a percentage of the video. 0 is 0% and 9 is 90%
              if ((ewhich > 47 && ewhich < 59) || (ewhich > 95 && ewhich < 106)) {
                if (enableNumbers) {
                  var sub = 48;
                  if (ewhich > 95) {
                    sub = 96;
                  }
                  var number = ewhich - sub;
                  ePreventDefault();
                  player.currentTime(player.duration() * number * 0.1);
                }
              }
          }
        }
      }
    };

    var doubleClick = function doubleClick(event) {

      // When controls are disabled, hotkeys will be disabled as well
      if (player.controls()) {

        // Don't catch clicks if any control buttons are focused
        var activeEl = event.relatedTarget || event.toElement || document.activeElement;
        if (activeEl == pEl ||
            activeEl == pEl.querySelector('.vjs-tech') ||
            activeEl == pEl.querySelector('.iframeblocker')) {

          if (enableFull) {
            if (player.isFullscreen()) {
              player.exitFullscreen();
            } else {
              player.requestFullscreen();
            }
          }
        }
      }
    };

    var checkKeys = function checkKeys(e) {
      // Allow some modularity in defining custom hotkeys

      // Play/Pause check
      if (options.playPauseKey(e)) {
        return "play";
      }

      // Seek Backward check
      if (options.rewindKey(e)) {
        return "rewind";
      }

      // Seek Forward check
      if (options.forwardKey(e)) {
        return "forward";
      }

      // Volume Up check
      if (options.volumeUpKey(e)) {
        return "volumeUp";
      }

      // Volume Down check
      if (options.volumeDownKey(e)) {
        return "volumeDown";
      }

      // Mute check
      if (options.muteKey(e)) {
        return "mute";
      }

      // Fullscreen check
      if (options.fullscreenKey(e)) {
        return "fullscreen";
      }
    };

    player.on('keydown', keyDown);
    player.on('dblclick', doubleClick);

    return this;
  };

  videojs.plugin('hotkeys', hotkeys);

})(window, window.videojs);
<|endoftext|>


<commit_before>var config = require('./config')
,   request = require('request')
,   ERRORS = config.ERRORS
,   format = require('util').format
,   xtend = require('xtend')
,   utils = require('./utils')
,   without = utils.without
,   each = utils.each
,   normalize = utils.normalize
,   countTokens = utils.countTokens
,   Chain = require('./chain')

function TmdbApi(key){
    if(!key) throw new Error(ERRORS.KEY_MISSING)
    this.apiKey = key
    //TODO: Make this less terrible
    createTokenedMethods(this, without('movie',Object.keys(config.endpoints.movie)).map(function(val){
        return {name : val, methodName : 'movie'+normalize(val), uri : config.endpoints.movie[val] }
    }))
    createTokenedMethods(this, without('collection',Object.keys(config.endpoints.collection)).map(function(val){
        return {name : val, methodName : 'collection'+normalize(val), uri : config.endpoints.collection[val] }
    }))
    createTokenedMethods(this, without('company',Object.keys(config.endpoints.company)).map(function(val){
        return {name : val, methodName : 'company'+normalize(val), uri : config.endpoints.company[val] }
    }))
    this.changes = genericTyped.bind(this, config.endpoints.changes.changes, 'movie')
    this.movie = genericChain.bind(this
        ,   config.endpoints.movie.movie
        ,   without(['movie', 'top_rated', 'popular', 'upcoming', 'now_playing', 'latest'], Object.keys(config.endpoints.movie)))
}

/**
    Genres
    Returns the list of movie genres.
    @sig Genres(function(err,data,response))
    @sig Genres([Integer] Id,function(err,data,response))
    @sig Genres([Integer] Id, [Object] Options, function(err,data,response))
    @Desc Fetch all movies with the specified genre.(Note this acheives the same effect as Genres(Integer))
*/
TmdbApi.prototype.genres = function(id,opts,callback) {
    var args = Array.prototype.slice.call(arguments)
    if(args.length === 1){
        callback = id
        id = null
    }else if(args.length === 2){
        callback = opts
        opts = {}
    }

    if(null == id){
        return this._execRequest({
            uri : config.baseUrl + config.endpoints.genres.genreList
        }, callback)
    }
    this._execRequest({
        uri : config.baseUrl + format(config.endpoints.genres.movies, id),
        qs : opts
    }, callback)
}

/**
    Collection
    @Desc Chainable. Get the collection the specific collection id belongs to.
    @sig collection([Integer] id, [Object] opts, function(err,data,response))
*/
TmdbApi.prototype.collection = function(id,opts,callback) {
    return new Chain(this
        , this.formUri(format(config.endpoints.collection.collection, id))
        , without('collection', Object.keys(config.endpoints.collection))
        , opts
        , callback)
}

/**
    Company
    @Desc Chainable. Get company info
    @sig collection([Integer] id, [Object] opts, function(err,data,response))
*/
TmdbApi.prototype.company = function(id,opts,callback) {
    return new Chain(this
        ,this.formUri(format(config.endpoints.company.company,id))
        ,without('company', Object.keys(config.endpoints.company))
        ,opts
        ,callback)
}

function createTokenedMethods(ctx, methodList) {
    methodList.forEach(function(val){
        switch(countTokens(val.uri)){
            case 0:
            case 1:
            default:
                return this[val.methodName] = function(token,opts,callback){
                    return genericToken(val.uri,token,opts,callback)
                }
            case 2:
                return this[val.methodName] = function(token,tokenB,opts,callback){
                    return genericToken(val.uri,token,tokenB,opts,callback)
                }
            case 3:
                return this[val.methodName] = function(token,tokenB,tokenC,opts,callback){
                    return genericToken(val.uri,token,tokenB,tokenC,opts,callback)
                }
        }
    },ctx)
}

function genericChain(uri,methodList,id,opts,callback){
    return new Chain(this
        ,   this.formUri(format(uri,id))
        ,   methodList
        ,   opts
        ,   callback)
}

function genericTyped(uri,defaultType,type,opts,callback) {
    var args = Array.prototype.slice.call(arguments)
    if(typeof type === 'function'){
        callback = type
        type = defaultType
        opts = {}
    }else if(typeof opts === 'function'){
        callback = opts
        opts = {}
    }else if(typeof type === 'object'){
        callback = opts
        opts = type
        type = defaultType
    }
    return this._execRequest({
        uri : config.baseUrl + format(uri, type),
        qs : opts
    }, callback)
}

function genericToken(uri) {
    var args = Array.prototype.slice.call(arguments)
    ,   callback = args.pop()
    ,   opts = args.pop()
    ,   tokens = args.slice(0)
    if(typeof opts !== 'object'){
        tokens.push(opts)
        opts = {}
    }
    tokens.unshift(uri)
    var uri = this.formUri(format.apply(null,tokens))
        this._execRequest({
            uri : uri,
            qs : opts
        }, callback)
}

TmdbApi.prototype.formUri = function(uri) {
    return config.baseUrl + uri
}

TmdbApi.prototype._execRequest = function(opts,callback) {
    if(null == opts.qs) opts.qs = {}
    if(null == opts.method) opts.method = 'GET'
    opts.qs.api_key = this.apiKey
    request(opts,function(err,body,response){
        if(err) return callback(err,response, body, opts, this)
        return callback(err,JSON.parse(response),body, opts, this)
    })
}

module.exports = TmdbApi
<commit_msg>Updates and fixes<commit_after>var config = require('./config')
,   request = require('request')
,   ERRORS = config.ERRORS
,   format = require('util').format
,   xtend = require('xtend')
,   utils = require('./utils')
,   without = utils.without
,   each = utils.each
,   normalize = utils.normalize
,   countTokens = utils.countTokens
,   Chain = require('./chain')

function TmdbApi(key){
    if(!key) throw new Error(ERRORS.KEY_MISSING)
    this.apiKey = key
    //TODO: Make this less terrible
    createTokenedMethods(this, without('movie',Object.keys(config.endpoints.movie)).map(function(val){
        return {name : val, methodName : 'movie'+normalize(val), uri : config.endpoints.movie[val] }
    }))
    createTokenedMethods(this, without('collection',Object.keys(config.endpoints.collection)).map(function(val){
        return {name : val, methodName : 'collection'+normalize(val), uri : config.endpoints.collection[val] }
    }))
    createTokenedMethods(this, without('company',Object.keys(config.endpoints.company)).map(function(val){
        return {name : val, methodName : 'company'+normalize(val), uri : config.endpoints.company[val] }
    }))
    createTypedMethods(this
        ,   {
                name : config.endpoints.changes.changes,
                methodName : 'changes',
                uri : config.endpoints.changes.changes,
                defaultType : 'movie'
            }
        ,   'movie')
    this.movie = genericChain.bind(this
        ,   config.endpoints.movie.movie
        ,   without(['movie', 'top_rated', 'popular', 'upcoming', 'now_playing', 'latest'], Object.keys(config.endpoints.movie)))
}

/**
    Genres
    Returns the list of movie genres.
    @sig Genres(function(err,data,response))
    @sig Genres([Integer] Id,function(err,data,response))
    @sig Genres([Integer] Id, [Object] Options, function(err,data,response))
    @Desc Fetch all movies with the specified genre.(Note this acheives the same effect as Genres(Integer))
*/
TmdbApi.prototype.genres = function(id,opts,callback) {
    var args = Array.prototype.slice.call(arguments)
    if(args.length === 1){
        callback = id
        id = null
    }else if(args.length === 2){
        callback = opts
        opts = {}
    }

    if(null == id){
        return this._execRequest({
            uri : config.host + config.apiVer + config.endpoints.genres.genreList
        }, callback)
    }
    this._execRequest({
        uri : config.host + config.apiVer + format(config.endpoints.genres.movies, id),
        qs : opts
    }, callback)
}

/**
    Collection
    @Desc Chainable. Get the collection the specific collection id belongs to.
    @sig collection([Integer] id, [Object] opts, function(err,data,response))
*/
TmdbApi.prototype.collection = function(id,opts,callback) {
    return new Chain(this
        , this.formUri(format(config.endpoints.collection.collection, id))
        , without('collection', Object.keys(config.endpoints.collection))
        , opts
        , callback)
}

/**
    Company
    @Desc Chainable. Get company info
    @sig collection([Integer] id, [Object] opts, function(err,data,response))
*/
TmdbApi.prototype.company = function(id,opts,callback) {
    return new Chain(this
        ,this.formUri(format(config.endpoints.company.company,id))
        ,without('company', Object.keys(config.endpoints.company))
        ,opts
        ,callback)
}

function createTokenedMethods(api, methodList) {
    methodList.forEach(function(val){
        switch(countTokens(val.uri)){
            case 0:
            case 1:
            default:
                return this[val.methodName] = function(token,opts,callback){
                    if(opts) return genericToken(this,val.uri,token,opts)
                    return genericToken(this,val.uri,token,opts,callback)
                }
            case 2:
                return this[val.methodName] = function(token,tokenB,opts,callback){
                    if(opts) return genericToken(this,val.uri,token,tokenB,opts)
                    return genericToken(this,val.uri,token,tokenB,opts,callback)
                }
            case 3:
                return this[val.methodName] = function(token,tokenB,tokenC,opts,callback){
                    if(opts) return genericToken(this,val.uri,token,tokenB,tokenC,opts)
                    return genericToken(this,val.uri,token,tokenB,tokenC,opts,callback)
                }
        }
    },api)
}

function createTypedMethods(api,methodList,defaultType){
    if(!Array.isArray(methodList)) methodList = [methodList]
    methodList.forEach(function(val){
        this[val.methodName] = function(type,opts,callback){
            if(arguments.length === 1) return genericTyped(this,val.uri,val.defaultType,type)
            if(arguments.length === 2) return genericTyped(this,val.uri,val.defaultType,type, opts)
            return genericTyped(this, val.uri, val.defaultType, type, opts, callback)
        }
    }, api)
}

function genericChain(uri,methodList,id,opts,callback){
    return new Chain(this
        ,   this.formUri(format(uri,id))
        ,   methodList
        ,   opts
        ,   callback)
}

function genericTyped(api, uri,defaultType,type,opts,callback) {
    var args = Array.prototype.slice.call(arguments)
    if(typeof type === 'function'){
        callback = type
        type = defaultType
        opts = {}
    }else if(typeof opts === 'function'){
        callback = opts
        opts = {}
    }else if(typeof type === 'object'){
        callback = opts
        opts = type
        type = defaultType
    }
    return api._execRequest({
        uri : config.host + config.apiVer + format(uri, type),
        qs : opts
    }, callback)
}

function genericToken(api,uri) {
    var args = Array.prototype.slice.call(arguments,2)
    ,   callback = args.pop()
    ,   opts = args.pop()
    ,   tokens = args.slice(0)
    if(typeof opts !== 'object'){
        tokens.push(opts)
        opts = {}
    }
    if(isNaN(tokens[0])) {
        if(!callback){
            for(var i = 0, l =args.length; i < l; i++){
                if(typeof args[i] === 'function') return args[i](config.ERRORS.NO_TOKEN)
            }
        }
        return callback(config.ERRORS.NO_TOKEN)
    }
    var uri = api.formUri(format.apply(null,[uri].concat(tokens)))
        api._execRequest({
            uri : uri,
            qs : opts
        }, callback)
}

TmdbApi.prototype.formUri = function(uri) {
    return config.host + config.apiVer + uri
}

TmdbApi.prototype._execRequest = function(opts,callback) {
    if(null == opts.qs) opts.qs = {}
    if(null == opts.method) opts.method = 'GET'
    opts.qs.api_key = this.apiKey
    request(opts,function(err,body,response){
        if(err) return callback(err,response, body, opts, this)
        return callback(err,JSON.parse(response),body, opts, this)
    })
}

module.exports = TmdbApi
<|endoftext|>


<commit_before>Util.Fix_Keys = function()
{
};

Util.Fix_Keys.NO_MERGE = /^(BODY|HEAD|TABLE|TBODY|THEAD|TR|TH|TD)$/;
Util.Fix_Keys.fix_delete_and_backspace = function(e, win)
{
	function is_not_at_end_of_body(rng)
	{
		var start_container = rng.startContainer;
		var start_offset = rng.startOffset;
		var rng2 = Util.Range.create_range(sel);
		rng2.selectNodeContents(start_container.ownerDocument.getElementsByTagName('BODY')[0]);
		rng2.setStart(start_container, start_offset);
		var ret = rng2.toString().length > 0;// != '';
		return ret;
	}

	function is_not_at_beg_of_body(rng)
	{
		var start_container = rng.startContainer;
		var start_offset = rng.startOffset;
		var rng2 = Util.Range.create_range(sel);
		rng2.selectNodeContents(start_container.ownerDocument.getElementsByTagName('BODY')[0]);
		rng2.setEnd(start_container, start_offset);
		var ret = rng2.toString().length > 0;// != '';
		return ret;
	}

	function move_selection_to_end(node, sel)
	{
		var rightmost = Util.Node.get_rightmost_descendent(node);
		Util.Selection.select_node(sel, rightmost);
		Util.Selection.collapse(sel, false); // to end
	}

	function remove_trailing_br(node)
	{
		if ( node.lastChild != null &&
			 node.lastChild.nodeType == Util.Node.ELEMENT_NODE &&
			 node.lastChild.tagName == 'BR' )
		{
			node.removeChild(node.lastChild);
		}
	}

	function merge_blocks(one, two)
	{
		while (two.firstChild)
			one.appendChild(two.firstChild);
		two.parentNode.removeChild(two);
	}

	/*
	 * If the node is a special Loki container (e.g. for a horizontal rule),
	 * we shouldn't merge with it.
	 */
	function is_container(node)
	{
		return (node && node.nodeType == Util.Node.ELEMENT_NODE &&
			node.getAttribute('loki:container'));
	}

	function is_empty_block(node)
	{
		return (Util.Node.is_block_level_element(node) &&
			Util.Node.is_basically_empty(node));
	}

	function is_unmergable(node)
	{
		return (is_container(node) ||
			is_empty_block(node) ||
			Util.Element.empty_tag(node));
	}

	function do_merge(one, two, sel)
	{
		function handle_unmergable(node)
		{
			if (is_unmergable(node)) {
				node.parentNode.removeChild(node);
				return true;
			}

			return false;
		}

		var tags = Util.Fix_Keys.NO_MERGE;
		if (!one || !two || one.nodeName.match(tags) || two.nodeName.match(tags)) {
			return;
		} else if (handle_unmergable(one) || handle_unmergable(two)) {
			return;
		} else {
			remove_trailing_br(one);
			move_selection_to_end(one, sel);
			merge_blocks(one, two);
			e.preventDefault();
		}
	}

	function remove_container(container)
	{
		container.parentNode.removeChild(container);
		e.preventDefault();
	}

	function remove_if_container(node)
	{
		if (is_container(node))
			remove_container(node);
	}

	var sel = Util.Selection.get_selection(win);
	var rng = Util.Range.create_range(sel);
	var cur_block;
	try {
	    cur_block = Util.Range.get_nearest_bl_ancestor_element(rng);
	} catch (e) {
	    cur_nlock = null;
	}

	function get_neighbor_element(direction)
	{
		if (rng.startContainer != rng.endContainer || rng.startOffset != rng.endOffset)
			return null;

		if (rng.startContainer.nodeType == Util.Node.TEXT_NODE) {
		    if (direction == Util.Node.NEXT) {
		        if (rng.endOffset == rng.endContainer.nodeValue.length)
		            return rng.endContainer.nextSibling;
		    } else if (direction == Util.Node.PREVIOUS) {
		        if (rng.startOffset == 0)
		            return rng.startContainer.previousSibling;
		    }

		    // If we're in the middle of a text node; well, how did we reach
		    // this code?
		    return null;
		}

		if (direction == Util.Node.NEXT && rng.endContainer.childNodes[rng.endOffset])
			return rng.endContainer.childNodes[rng.endOffset];
		else if (direction == Util.Node.PREVIOUS && rng.startContainer.childNodes[rng.startOffset - 1])
			return rng.startContainer.childNodes[rng.startOffset - 1];
		else
			return null;
	}

	function is_named_anchor(element) {
	    return (element && element.tagName == 'A' && element.name &&
	        !Util.Node.get_last_non_whitespace_child_node(element));
	}

	function remove_anchor(anchor) {
	    var id = anchor.id, sibling = anchor.previousSibling, i, images;

	    function is_marker(node) {
	        return (node.nodeName == 'IMG' &&
	            node.getAttribute('loki:anchor.id') == id);
	    }

	    if (is_marker(sibling)) {
	        // easy case: marker is in its original position, we avoid a DOM
	        // search
	        sibling.parentNode.removeChild(sibling);
	    } else {
	        images = anchor.ownerDocument.getElementsByTagName('IMG');
	        for (i = 0; i < images.length; i++) {
	            if (is_marker(images[i])) {
	                images[i].parentNode.removeChild(images[i]);
	                break;
	            }
	        }
	    }

	    anchor.parentNode.removeChild(anchor);
	}

	if ( rng.collapsed == true && !e.shiftKey )
	{
		var neighbor = null;

		if (e.keyCode == e.DOM_VK_DELETE) {
		    neighbor = get_neighbor_element(Util.Node.NEXT);
		    if (is_named_anchor(neighbor)) {
		        remove_anchor(neighbor);
		    } else if (cur_block && Util.Range.is_at_end_of_block(rng, cur_block)) {
				do_merge(cur_block, Util.Node.next_element_sibling(cur_block), sel);
			} else if (Util.Range.is_at_end_of_text(rng) && is_container(rng.endContainer.nextSibling)) {
				remove_container(rng.endContainer.nextSibling);
			} else if (neighbor) {
				remove_if_container(neighbor);
			}
		} else if (e.keyCode == e.DOM_VK_BACK_SPACE) {
		    neighbor = get_neighbor_element(Util.Node.PREVIOUS);

			if (is_named_anchor(neighbor)) {
		        remove_anchor(neighbor);
			} else if (cur_block && Util.Range.is_at_beg_of_block(rng, cur_block) && rng.isPointInRange(rng.startContainer, 0)) {
				// Both the above range tests are necessary to avoid
    			// merge on B's here:
s|ah

				do_merge(Util.Node.previous_element_sibling(cur_block), cur_block, sel);
			} else if (Util.Range.is_at_beg_of_text(rng) && is_container(rng.startContainer.previousSibling)) {
				remove_container(rng.endContainer.nextSibling);
			} else if (neighbor) {
				remove_if_container(neighbor);
			}
		}
	}

	return;
	//mb('rng.startContainer, rng.startContainer.parentNode.lastChild, rng.startContainer.parentNode.firstChild, rng.startOffset, rng.startContainer.length, sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset, rng, sel', [rng.startContainer, rng.startContainer.parentNode.lastChild, rng.startContainer.parentNode.firstChild, rng.startOffset, rng.startContainer.length, sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset, rng, sel]);
};

Util.Fix_Keys.fix_enter_ie = function(e, win, loki)
{
	// Do nothing if enter not pressed
	if (!( !e.shiftKey && e.keyCode == 13 ))
		return true;

	var sel = Util.Selection.get_selection(win);
	var rng = Util.Range.create_range(sel);
	var cur_block = Util.Range.get_nearest_bl_ancestor_element(rng);

	if ( cur_block && cur_block.nodeName == 'PRE' )
	{
		var br_helper = (new UI.BR_Helper).init(loki);
		br_helper.insert_br();
		return false; // prevent default
	}

	// else
	return true; // don't prevent default
};
<commit_msg>Fixing a function call.<commit_after>Util.Fix_Keys = function()
{
};

Util.Fix_Keys.NO_MERGE = /^(BODY|HEAD|TABLE|TBODY|THEAD|TR|TH|TD)$/;
Util.Fix_Keys.fix_delete_and_backspace = function(e, win)
{
	function is_not_at_end_of_body(rng)
	{
		var start_container = rng.startContainer;
		var start_offset = rng.startOffset;
		var rng2 = Util.Range.create_range(sel);
		rng2.selectNodeContents(start_container.ownerDocument.getElementsByTagName('BODY')[0]);
		rng2.setStart(start_container, start_offset);
		var ret = rng2.toString().length > 0;// != '';
		return ret;
	}

	function is_not_at_beg_of_body(rng)
	{
		var start_container = rng.startContainer;
		var start_offset = rng.startOffset;
		var rng2 = Util.Range.create_range(sel);
		rng2.selectNodeContents(start_container.ownerDocument.getElementsByTagName('BODY')[0]);
		rng2.setEnd(start_container, start_offset);
		var ret = rng2.toString().length > 0;// != '';
		return ret;
	}

	function move_selection_to_end(node, sel)
	{
		var rightmost = Util.Node.get_rightmost_descendent(node);
		Util.Selection.select_node(sel, rightmost);
		Util.Selection.collapse(sel, false); // to end
	}

	function remove_trailing_br(node)
	{
		if ( node.lastChild != null &&
			 node.lastChild.nodeType == Util.Node.ELEMENT_NODE &&
			 node.lastChild.tagName == 'BR' )
		{
			node.removeChild(node.lastChild);
		}
	}

	function merge_blocks(one, two)
	{
		while (two.firstChild)
			one.appendChild(two.firstChild);
		two.parentNode.removeChild(two);
	}

	/*
	 * If the node is a special Loki container (e.g. for a horizontal rule),
	 * we shouldn't merge with it.
	 */
	function is_container(node)
	{
		return (node && node.nodeType == Util.Node.ELEMENT_NODE &&
			node.getAttribute('loki:container'));
	}

	function is_empty_block(node)
	{
		return (Util.Node.is_block_level_element(node) &&
			Util.Element.is_basically_empty(node));
	}

	function is_unmergable(node)
	{
		return (is_container(node) ||
			is_empty_block(node) ||
			Util.Element.empty_tag(node));
	}

	function do_merge(one, two, sel)
	{
		function handle_unmergable(node)
		{
			if (is_unmergable(node)) {
				node.parentNode.removeChild(node);
				return true;
			}

			return false;
		}

		var tags = Util.Fix_Keys.NO_MERGE;
		if (!one || !two || one.nodeName.match(tags) || two.nodeName.match(tags)) {
			return;
		} else if (handle_unmergable(one) || handle_unmergable(two)) {
			return;
		} else {
			remove_trailing_br(one);
			move_selection_to_end(one, sel);
			merge_blocks(one, two);
			e.preventDefault();
		}
	}

	function remove_container(container)
	{
		container.parentNode.removeChild(container);
		e.preventDefault();
	}

	function remove_if_container(node)
	{
		if (is_container(node))
			remove_container(node);
	}

	var sel = Util.Selection.get_selection(win);
	var rng = Util.Range.create_range(sel);
	var cur_block;
	try {
	    cur_block = Util.Range.get_nearest_bl_ancestor_element(rng);
	} catch (e) {
	    cur_nlock = null;
	}

	function get_neighbor_element(direction)
	{
		if (rng.startContainer != rng.endContainer || rng.startOffset != rng.endOffset)
			return null;

		if (rng.startContainer.nodeType == Util.Node.TEXT_NODE) {
		    if (direction == Util.Node.NEXT) {
		        if (rng.endOffset == rng.endContainer.nodeValue.length)
		            return rng.endContainer.nextSibling;
		    } else if (direction == Util.Node.PREVIOUS) {
		        if (rng.startOffset == 0)
		            return rng.startContainer.previousSibling;
		    }

		    // If we're in the middle of a text node; well, how did we reach
		    // this code?
		    return null;
		}

		if (direction == Util.Node.NEXT && rng.endContainer.childNodes[rng.endOffset])
			return rng.endContainer.childNodes[rng.endOffset];
		else if (direction == Util.Node.PREVIOUS && rng.startContainer.childNodes[rng.startOffset - 1])
			return rng.startContainer.childNodes[rng.startOffset - 1];
		else
			return null;
	}

	function is_named_anchor(element) {
	    return (element && element.tagName == 'A' && element.name &&
	        !Util.Node.get_last_non_whitespace_child_node(element));
	}

	function remove_anchor(anchor) {
	    var id = anchor.id, sibling = anchor.previousSibling, i, images;

	    function is_marker(node) {
	        return (node.nodeName == 'IMG' &&
	            node.getAttribute('loki:anchor.id') == id);
	    }

	    if (is_marker(sibling)) {
	        // easy case: marker is in its original position, we avoid a DOM
	        // search
	        sibling.parentNode.removeChild(sibling);
	    } else {
	        images = anchor.ownerDocument.getElementsByTagName('IMG');
	        for (i = 0; i < images.length; i++) {
	            if (is_marker(images[i])) {
	                images[i].parentNode.removeChild(images[i]);
	                break;
	            }
	        }
	    }

	    anchor.parentNode.removeChild(anchor);
	}

	if ( rng.collapsed == true && !e.shiftKey )
	{
		var neighbor = null;

		if (e.keyCode == e.DOM_VK_DELETE) {
		    neighbor = get_neighbor_element(Util.Node.NEXT);
		    if (is_named_anchor(neighbor)) {
		        remove_anchor(neighbor);
		    } else if (cur_block && Util.Range.is_at_end_of_block(rng, cur_block)) {
				do_merge(cur_block, Util.Node.next_element_sibling(cur_block), sel);
			} else if (Util.Range.is_at_end_of_text(rng) && is_container(rng.endContainer.nextSibling)) {
				remove_container(rng.endContainer.nextSibling);
			} else if (neighbor) {
				remove_if_container(neighbor);
			}
		} else if (e.keyCode == e.DOM_VK_BACK_SPACE) {
		    neighbor = get_neighbor_element(Util.Node.PREVIOUS);

			if (is_named_anchor(neighbor)) {
		        remove_anchor(neighbor);
			} else if (cur_block && Util.Range.is_at_beg_of_block(rng, cur_block) && rng.isPointInRange(rng.startContainer, 0)) {
				// Both the above range tests are necessary to avoid
    			// merge on B's here:
s|ah

				do_merge(Util.Node.previous_element_sibling(cur_block), cur_block, sel);
			} else if (Util.Range.is_at_beg_of_text(rng) && is_container(rng.startContainer.previousSibling)) {
				remove_container(rng.endContainer.nextSibling);
			} else if (neighbor) {
				remove_if_container(neighbor);
			}
		}
	}

	return;
	//mb('rng.startContainer, rng.startContainer.parentNode.lastChild, rng.startContainer.parentNode.firstChild, rng.startOffset, rng.startContainer.length, sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset, rng, sel', [rng.startContainer, rng.startContainer.parentNode.lastChild, rng.startContainer.parentNode.firstChild, rng.startOffset, rng.startContainer.length, sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset, rng, sel]);
};

Util.Fix_Keys.fix_enter_ie = function(e, win, loki)
{
	// Do nothing if enter not pressed
	if (!( !e.shiftKey && e.keyCode == 13 ))
		return true;

	var sel = Util.Selection.get_selection(win);
	var rng = Util.Range.create_range(sel);
	var cur_block = Util.Range.get_nearest_bl_ancestor_element(rng);

	if ( cur_block && cur_block.nodeName == 'PRE' )
	{
		var br_helper = (new UI.BR_Helper).init(loki);
		br_helper.insert_br();
		return false; // prevent default
	}

	// else
	return true; // don't prevent default
};
<|endoftext|>


<commit_before>var request = require('request'),
  xtend = require('xtend'),
  parseLinks = require('parse-links'),
  url = require('url');

var api = module.exports;
var Connection = require('./Connection');

api.getConnections = function(accessToken, done) {
  request.get({
    url: this.apiUrl + '/connections',
    qs: { access_token: accessToken },
    headers: {"Accept": "application/xml"},
  }, function (err, r, body) {
    if (err) { return done(err); }

    if (r.statusCode.toString().substr(0, 1) !== '2'){
      return done(new Error(body));
    }

    try {
      var result = JSON.parse(body).map(function(c) {
        return new Connection(this, accessToken, c);
      });
      done(null, result);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.getSocialConnections = function(accessToken, done) {
  request.get({
    url: this.apiUrl + '/connections',
    qs: { access_token: accessToken, only_socials: true },
    headers: {"Accept": "application/xml"}
  }, function (err, r, body) {
    if (err) { return done(err); }

    if (r.statusCode.toString().substr(0, 1) !== '2'){
      return done(new Error(body));
    }
    try {
      var result = JSON.parse(body).map(function(c) {
        return new Connection(this, accessToken, c);
      });
      done(null, result);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.getEnterpriseConnections = function(accessToken, done) {
  request.get({
    url: this.apiUrl + '/connections',
    qs: {
      access_token: accessToken,
      only_enterprise: true
    },
    headers: {"Accept": "application/xml"}
  }, function (err, r, body) {
    if (err) { return done(err); }

    if (r.statusCode.toString().substr(0, 1) !== '2'){
      return done(new Error(body));
    }
    try {
      var result = JSON.parse(body).map(function(c) {
        return new Connection(this, accessToken, c);
      });
      done(null, result);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.getSocialUsers = function (accessToken, options, done) {
  if(typeof options === 'function'){
    done = options;
    options = {};
  }
  options = xtend({social: true}, options);
  return api.getUsers.bind(this)(accessToken, options, done);
};

api.getEnterpriseUsers = function (accessToken, options, done) {
  if(typeof options === 'function'){
    done = options;
    options = {};
  }
  options = xtend({social: false}, options);
  return api.getUsers.bind(this)(accessToken, options, done);
};

api.getUsers = function(accessToken, options, done) {
  var nextPageLink, req, connection;

  if(typeof options === 'function'){
    done = options;
    options = {};
  }else{
    options = options || {};
    if(options.perPage){
      options.per_page = options.perPage;
      delete options.perPage;
    }
    if(options.page){
      nextPageLink = options.page;
      delete options.page;
    }
    if(options.connection){
      connection = options.connection;
      delete options.connection;
    }
  }

  if(nextPageLink){
    req = { url: url.parse(nextPageLink) };
  }else {
    req = { qs: xtend({ access_token: accessToken }, options) };
    if (connection) {
      req.url = this.apiUrl + '/connections/' + connection + '/users';
    } else if (options.social) {
      req.url = this.apiUrl + '/socialconnections/users';
      delete options.social;
    } else if (options.social === false) {
      req.url = this.apiUrl + '/enterpriseconnections/users';
      delete options.social;
    } else {
      req.url = this.apiUrl + '/users';
    }
  }

  request.get(req, function (err, r, body) {
    if (err) return done(err);

    if (r.statusCode.toString().substr(0, 1) !== '2'){
      return done(new Error(body));
    }

    try {
      var results = JSON.parse(body);
      if(r.headers.link){

        var links = parseLinks(r.headers.link);

        if(links.next){
          Object.defineProperty(results, 'nextPageLink' , {
            enumerable: false,
            writeable: false,
            value: links.next
          });
        }

      }
      done(null, results);
    } catch(e) {
      done(e);
    }

  });
};

api.updateTemplate = function (accessToken, strategy, options, done) {
  request.put({
    url: this.apiUrl + '/connection-templates/' + strategy,
    qs: { access_token: accessToken },
    json: options
  }, function (err, r, body) {
    if (err) return done(err);
    if(r.statusCode === 400) {
      return done(new Error(body.detail || body));
    }
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done();
  });
};

api.getTemplate = function (accessToken, strategy, done) {
  request.get({
    url: this.apiUrl + '/connection-templates/' + strategy,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done(null, JSON.parse(body));
  });
};

api.getStrategies = function (accessToken, done) {
  request.get({
    url: this.apiUrl + '/strategies',
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done(null, JSON.parse(body));
  });
};

api.getConnection = function(accessToken, name, done) {
  request.get({
    url: this.apiUrl + '/connections/' + name,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode === 404) return done(null, null);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));

    try {
      var data = JSON.parse(body);
      var connection = new Connection(this, accessToken, data);
      done(null, connection);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.createConnection = function (accessToken, connection, done) {
  request.post({
    url:  this.apiUrl + '/connections',
    qs:   { access_token: accessToken },
    json: connection
  }, function (err, r, body) {
    if (err) { return done(err); }
    if(r.statusCode === 400) {
      return done(new Error(body.detail || body));
    }
    done(null, body);
  });
};

api.deleteConnection = function(accessToken, name, done)
{
  request.del({
    url: this.apiUrl + '/connections/' + name,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode === 404) return done(null, null);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));

    try {
      var data = JSON.parse(body);
      done(null, data);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.updateConnection = function (accessToken, connection, done) {
  request.put({
    url:  this.apiUrl + '/connections/' + connection.name,
    qs:   { access_token: accessToken },
    json: connection
  }, function (err, r, body) {
    if (err) { return done(err); }
    if(r.statusCode === 400) {
      return done(new Error(body.detail || body));
    }
    done(null, body);
  });
};

api.deleteTenant = function (accessToken, name, done) {
  request.del({
    url: this.apiUrl + '/tenants/' + name,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode === 404) return done(null, null);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done();
  }.bind(this));
};

api.createClient = function (accessToken, options, done) {
  request.post({
    url:  this.apiUrl + '/clients',
    qs:   { access_token: accessToken },
    json: options
  }, function (err, r, body) {
    if (err) { return done(err); }
    if (r.statusCode === 400) {
      return done(new Error(body.detail || body));
    }

    done(null, body);
  });
};

api.deleteClient = function (accessToken, name, done) {
  request.del({
    url: this.apiUrl + '/tenants/' + name,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode === 404) return done(null, null);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done();
  }.bind(this));
};
<commit_msg>methods to getClients (all, by clientId, by userId)<commit_after>var request = require('request'),
  xtend = require('xtend'),
  parseLinks = require('parse-links'),
  url = require('url');

var api = module.exports;
var Connection = require('./Connection');

api.getConnections = function(accessToken, done) {
  request.get({
    url: this.apiUrl + '/connections',
    qs: { access_token: accessToken },
    headers: {"Accept": "application/xml"},
  }, function (err, r, body) {
    if (err) { return done(err); }

    if (r.statusCode.toString().substr(0, 1) !== '2'){
      return done(new Error(body));
    }

    try {
      var result = JSON.parse(body).map(function(c) {
        return new Connection(this, accessToken, c);
      });
      done(null, result);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.getSocialConnections = function(accessToken, done) {
  request.get({
    url: this.apiUrl + '/connections',
    qs: { access_token: accessToken, only_socials: true },
    headers: {"Accept": "application/xml"}
  }, function (err, r, body) {
    if (err) { return done(err); }

    if (r.statusCode.toString().substr(0, 1) !== '2'){
      return done(new Error(body));
    }
    try {
      var result = JSON.parse(body).map(function(c) {
        return new Connection(this, accessToken, c);
      });
      done(null, result);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.getEnterpriseConnections = function(accessToken, done) {
  request.get({
    url: this.apiUrl + '/connections',
    qs: {
      access_token: accessToken,
      only_enterprise: true
    },
    headers: {"Accept": "application/xml"}
  }, function (err, r, body) {
    if (err) { return done(err); }

    if (r.statusCode.toString().substr(0, 1) !== '2'){
      return done(new Error(body));
    }
    try {
      var result = JSON.parse(body).map(function(c) {
        return new Connection(this, accessToken, c);
      });
      done(null, result);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.getSocialUsers = function (accessToken, options, done) {
  if(typeof options === 'function'){
    done = options;
    options = {};
  }
  options = xtend({social: true}, options);
  return api.getUsers.bind(this)(accessToken, options, done);
};

api.getEnterpriseUsers = function (accessToken, options, done) {
  if(typeof options === 'function'){
    done = options;
    options = {};
  }
  options = xtend({social: false}, options);
  return api.getUsers.bind(this)(accessToken, options, done);
};

api.getUsers = function(accessToken, options, done) {
  var nextPageLink, req, connection;

  if(typeof options === 'function'){
    done = options;
    options = {};
  }else{
    options = options || {};
    if(options.perPage){
      options.per_page = options.perPage;
      delete options.perPage;
    }
    if(options.page){
      nextPageLink = options.page;
      delete options.page;
    }
    if(options.connection){
      connection = options.connection;
      delete options.connection;
    }
  }

  if(nextPageLink){
    req = { url: url.parse(nextPageLink) };
  }else {
    req = { qs: xtend({ access_token: accessToken }, options) };
    if (connection) {
      req.url = this.apiUrl + '/connections/' + connection + '/users';
    } else if (options.social) {
      req.url = this.apiUrl + '/socialconnections/users';
      delete options.social;
    } else if (options.social === false) {
      req.url = this.apiUrl + '/enterpriseconnections/users';
      delete options.social;
    } else {
      req.url = this.apiUrl + '/users';
    }
  }

  request.get(req, function (err, r, body) {
    if (err) return done(err);

    if (r.statusCode.toString().substr(0, 1) !== '2'){
      return done(new Error(body));
    }

    try {
      var results = JSON.parse(body);
      if(r.headers.link){

        var links = parseLinks(r.headers.link);

        if(links.next){
          Object.defineProperty(results, 'nextPageLink' , {
            enumerable: false,
            writeable: false,
            value: links.next
          });
        }

      }
      done(null, results);
    } catch(e) {
      done(e);
    }

  });
};

api.updateTemplate = function (accessToken, strategy, options, done) {
  request.put({
    url: this.apiUrl + '/connection-templates/' + strategy,
    qs: { access_token: accessToken },
    json: options
  }, function (err, r, body) {
    if (err) return done(err);
    if(r.statusCode === 400) {
      return done(new Error(body.detail || body));
    }
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done();
  });
};

api.getTemplate = function (accessToken, strategy, done) {
  request.get({
    url: this.apiUrl + '/connection-templates/' + strategy,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done(null, JSON.parse(body));
  });
};

api.getStrategies = function (accessToken, done) {
  request.get({
    url: this.apiUrl + '/strategies',
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done(null, JSON.parse(body));
  });
};

api.getConnection = function(accessToken, name, done) {
  request.get({
    url: this.apiUrl + '/connections/' + name,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode === 404) return done(null, null);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));

    try {
      var data = JSON.parse(body);
      var connection = new Connection(this, accessToken, data);
      done(null, connection);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.createConnection = function (accessToken, connection, done) {
  request.post({
    url:  this.apiUrl + '/connections',
    qs:   { access_token: accessToken },
    json: connection
  }, function (err, r, body) {
    if (err) { return done(err); }
    if(r.statusCode === 400) {
      return done(new Error(body.detail || body));
    }
    done(null, body);
  });
};

api.deleteConnection = function(accessToken, name, done)
{
  request.del({
    url: this.apiUrl + '/connections/' + name,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode === 404) return done(null, null);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));

    try {
      var data = JSON.parse(body);
      done(null, data);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

api.updateConnection = function (accessToken, connection, done) {
  request.put({
    url:  this.apiUrl + '/connections/' + connection.name,
    qs:   { access_token: accessToken },
    json: connection
  }, function (err, r, body) {
    if (err) { return done(err); }
    if(r.statusCode === 400) {
      return done(new Error(body.detail || body));
    }
    done(null, body);
  });
};

api.deleteTenant = function (accessToken, name, done) {
  request.del({
    url: this.apiUrl + '/tenants/' + name,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode === 404) return done(null, null);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done();
  }.bind(this));
};

api.createClient = function (accessToken, options, done) {
  request.post({
    url:  this.apiUrl + '/clients',
    qs:   { access_token: accessToken },
    json: options
  }, function (err, r, body) {
    if (err) { return done(err); }
    if (r.statusCode === 400) {
      return done(new Error(body.detail || body));
    }

    done(null, body);
  });
};

api.deleteClient = function (accessToken, name, done) {
  request.del({
    url: this.apiUrl + '/tenants/' + name,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode === 404) return done(null, null);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done();
  }.bind(this));
};

api.getClients = function (accessToken, clientID, done) {
  if (typeof clientID === 'function') {
    done = clientID;
    clientID = '';
  }

  request.get({
    url: this.apiUrl + '/clients/' + clientID,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done(null, JSON.parse(body));
  });
};

api.getClientsByUserId = function (accessToken, userId, done) {
  request.get({
    url: this.apiUrl + '/clients/?userId=' + userId,
    qs: { access_token: accessToken }
  }, function (err, r, body) {
    if (err) return done(err);
    if (r.statusCode.toString().substr(0, 1) !== '2') return done(new Error(body));
    done(null, JSON.parse(body));
  });
};
<|endoftext|>