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

/**
 * A Screen is the largest chunk of a simulation. (Java sims used the term Module, but that term
 * is too overloaded to use with JavaScript and Git.)
 *
 * When creating a Sim, Screens are supplied as the arguments. They can be specified as object literals or through
 * instances of this class. This class may centralize default behavior or state for Screens in the future, but right
 * now it only allows you to create Sims without using named parameter object literals.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const BooleanProperty = require( 'AXON/BooleanProperty' );
  const Dimension2 = require( 'DOT/Dimension2' );
  const inherit = require( 'PHET_CORE/inherit' );
  const joist = require( 'JOIST/joist' );
  const JoistA11yStrings = require( 'JOIST/JoistA11yStrings' );
  const merge = require( 'PHET_CORE/merge' );
  const Path = require( 'SCENERY/nodes/Path' );
  const Property = require( 'AXON/Property' );
  const Rectangle = require( 'SCENERY/nodes/Rectangle' );
  const Shape = require( 'KITE/Shape' );
  const StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  const Tandem = require( 'TANDEM/Tandem' );

  // a11y strings
  const screenNamePatternString = JoistA11yStrings.screenNamePattern.value;
  const screenSimPatternString = JoistA11yStrings.screenSimPattern.value;
  const simScreenString = JoistA11yStrings.simScreen.value;

  // constants
  const MINIMUM_HOME_SCREEN_ICON_SIZE = new Dimension2( 548, 373 );
  const MINIMUM_NAVBAR_ICON_SIZE = new Dimension2( 147, 100 );
  const NAVBAR_ICON_ASPECT_RATIO = MINIMUM_NAVBAR_ICON_SIZE.width / MINIMUM_NAVBAR_ICON_SIZE.height;
  const HOME_SCREEN_ICON_ASPECT_RATIO = MINIMUM_HOME_SCREEN_ICON_SIZE.width / MINIMUM_HOME_SCREEN_ICON_SIZE.height;
  const ICON_ASPECT_RATIO_TOLERANCE = 5E-3; // how close to the ideal aspect ratio an icon must be

  // Home screen and navigation bar icons must have the same aspect ratio, see https://github.com/phetsims/joist/issues/76
  assert && assert( Math.abs( HOME_SCREEN_ICON_ASPECT_RATIO - HOME_SCREEN_ICON_ASPECT_RATIO ) < ICON_ASPECT_RATIO_TOLERANCE,
    'MINIMUM_HOME_SCREEN_ICON_SIZE and MINIMUM_NAVBAR_ICON_SIZE must have the same aspect ratio' );

  /**
   * @param {function} createModel
   * @param {function:Object } createView - function( model )
   * @param {Object} [options]
   * @constructor
   */
  function Screen( createModel, createView, options ) {

    options = merge( {

      // {string|null} name of the sim, as displayed to the user.
      // For single-screen sims, there is no home screen or navigation bar, and null is OK.
      // For multi-screen sims, this must be provided.
      name: null,

      // {Property.<Color|string>} background color of the Screen
      backgroundColorProperty: new Property( 'white' ),

      // {Node|null} icon shown on the home screen. If null, then a default is created.
      // For single-screen sims, there is no home screen and the default is OK.
      homeScreenIcon: null,

      // {boolean} whether to draw a frame around the small icons on home screen
      showUnselectedHomeScreenIconFrame: false,

      // {Node|null} icon shown in the navigation bar. If null, then the home screen icon will be used, scaled to fit.
      navigationBarIcon: null,

      // {string|null} show a frame around the screen icon when the navbar's background fill is this color
      // 'black', 'white', or null (no frame)
      showScreenIconFrameForNavigationBarFill: null,

      // dt cap in seconds, see https://github.com/phetsims/joist/issues/130
      maxDT: 0.5,

      tandem: Tandem.REQUIRED,

      // a11y - The description that is used when interacting with screen icons/buttons in joist.
      // This is often a full but short sentence with a period at the end of it.
      descriptionContent: null
    }, options );

    assert && assert( _.includes( [ 'black', 'white', null ], options.showScreenIconFrameForNavigationBarFill ),
      'invalid showScreenIconFrameForNavigationBarFill: ' + options.showScreenIconFrameForNavigationBarFill );

    // Create a default homeScreenIcon, using the Screen's background color
    if ( !options.homeScreenIcon ) {
      options.homeScreenIcon = new Rectangle( 0, 0, MINIMUM_HOME_SCREEN_ICON_SIZE.width, MINIMUM_HOME_SCREEN_ICON_SIZE.height, {
        fill: options.backgroundColorProperty.value,
        stroke: 'black'
      } );
    }

    // navigationBarIcon defaults to homeScreenIcon, and will be scaled down
    if ( !options.navigationBarIcon ) {
      options.navigationBarIcon = options.homeScreenIcon;
    }

    // Validate icon sizes
    validateIconSize( options.homeScreenIcon, MINIMUM_HOME_SCREEN_ICON_SIZE, HOME_SCREEN_ICON_ASPECT_RATIO, 'homeScreenIcon' );
    validateIconSize( options.navigationBarIcon, MINIMUM_NAVBAR_ICON_SIZE, NAVBAR_ICON_ASPECT_RATIO, 'navigationBarIcon' );

    if ( assert && options.tandem.supplied ) {
      assert && assert( _.endsWith( options.tandem.phetioID, 'Screen' ), 'Screen tandems should end with Screen suffix' );
    }

    // @public (read-only, joist)
    this.screenTandem = options.tandem;

    assert && assert( !options.backgroundColor, 'Please provide backgroundColorProperty instead' );

    // @public
    this.backgroundColorProperty = options.backgroundColorProperty;

    // @public (read-only)
    this.name = options.name;
    this.homeScreenIcon = options.homeScreenIcon;
    this.navigationBarIcon = options.navigationBarIcon;
    this.showUnselectedHomeScreenIconFrame = options.showUnselectedHomeScreenIconFrame;
    this.showScreenIconFrameForNavigationBarFill = options.showScreenIconFrameForNavigationBarFill;

    // @public (read-only, joist)
    this.maxDT = options.maxDT;

    // @private
    this.createModel = createModel;
    this.createView = createView;

    // Construction of the model and view are delayed and controlled to enable features like
    // a) faster loading when only loading certain screens
    // b) showing a loading progress bar <not implemented>
    this._model = null; // @private
    this._view = null;  // @private

    // @public {Property.<boolean>} indicates whether the Screen is active. Clients can read this, joist sets it.
    // To prevent potential visual glitches, the value should change only while the screen's view is invisible.
    // That is: transitions from false to true before a Screen becomes visible, and from true to false after a Screen becomes invisible.
    this.activeProperty = new BooleanProperty( true, {
      tandem: options.tandem.createTandem( 'activeProperty' ),
      phetioReadOnly: true,
      phetioDocumentation: 'Indicates whether the screen is active.  For single-screen simulations, the screen is always active.'
    } );

    // @public (a11y) - used to set the ScreenView's descriptionContent. This is a bit of a misnomer because Screen is
    // not a Node subtype, so this is a value property rather than a setter.
    this.descriptionContent = '';
    if ( options.descriptionContent ) {
      this.descriptionContent = options.descriptionContent;
    }
    else if ( this.name ) {
      this.descriptionContent = StringUtils.fillIn( screenNamePatternString, {
        name: this.name
      } );
    }
    else {
      this.descriptionContent = simScreenString; // fall back on generic name
    }

    const self = this;
    assert && this.activeProperty.lazyLink( function() {
      assert( self._view, 'isActive should not change before the Screen view has been initialized' );

      // In phet-io mode, the state of a sim can be set without a deterministic order. The activeProperty could be
      // changed before the view's visibility is set.
      if ( !phet.phetio ) {
        assert( !self._view.isVisible(), 'isActive should not change while the Screen view is visible' );
      }
    } );
  }

  joist.register( 'Screen', Screen );

  /**
   * Convenience function to validate the sizes for the home screen icon and navigation bar icon.
   * @param {Node} icon - the icon to validate
   * @param {Dimension2} minimumSize - the minimum allowed size for the icon
   * @param {number} aspectRatio - the required aspect ratio
   * @param {string} name - the name of the icon type (for assert messages)
   */
  var validateIconSize = function( icon, minimumSize, aspectRatio, name ) {
    assert && assert( icon.width >= minimumSize.width, `${name} width is too small: ${icon.width} < ${minimumSize.width}` );
    assert && assert( icon.height >= minimumSize.height, `${name} height is too small: ${icon.height} < ${minimumSize.height}` );

    // Validate home screen aspect ratio
    const actualAspectRatio = icon.width / icon.height;
    assert && assert(
      Math.abs( aspectRatio - actualAspectRatio ) < ICON_ASPECT_RATIO_TOLERANCE,
      name + ' has invalid aspect ratio: ' + actualAspectRatio
    );
  };

  /**
   * For showing ScreenView layoutBounds with 'dev' query parameter.
   * @param {Bounds2} layoutBounds
   * @returns {Node}
   */
  const devCreateLayoutBoundsNode = function( layoutBounds ) {
    return new Path( Shape.bounds( layoutBounds ), {
      stroke: 'red',
      lineWidth: 3,
      pickable: false
    } );
  };

  /**
   * For showing ScreenView visible bounds with 'showVisibleBounds' query parameter.
   * @param {ScreenView} screenView
   * @returns {Node}
   */
  const devCreateVisibleBoundsNode = function( screenView ) {
    const path = new Path( Shape.bounds( screenView.visibleBoundsProperty.value ), {
      stroke: 'blue',
      lineWidth: 6,
      pickable: false
    } );
    screenView.visibleBoundsProperty.link( function( visibleBounds ) {
      path.shape = Shape.bounds( visibleBounds );
    } );
    return path;
  };

  return inherit( Object, Screen, {

    // @public
    reset: function() {
      // Background color not reset, as it's a responsibility of the code that changes the property
    },

    // @public - Returns the model (if it has been constructed)
    get model() {
      assert && assert( this._model, 'Model has not yet been constructed' );
      return this._model;
    },

    // @public - Returns the view (if it has been constructed)
    get view() {
      assert && assert( this._view, 'View has not yet been constructed' );
      return this._view;
    },

    /**
     * Initialize the model.  Clients should use either this or initializeModelAndView
     * Clients may want to use this method to gain more control over the creation process
     * @public (joist-internal)
     */
    initializeModel: function() {
      assert && assert( this._model === null, 'there was already a model' );
      this._model = this.createModel();
    },

    /**
     * Initialize the view.  Clients should use either this or initializeModelAndView
     * Clients may want to use this method to gain more control over the creation process
     * @public (joist-internal)
     * @param {string} [simName] - The display name of the sim, used for a11y. Not provided for the home screen.
     * @param {number} [numberOfScreens] - the number of screens in the whole sim.
     */
    initializeView: function( simName, numberOfScreens ) {
      assert && assert( this._view === null, 'there was already a view' );
      this._view = this.createView( this.model );
      this._view.setVisible( false ); // a Screen is invisible until selected

      // Show the home screen's layoutBounds
      if ( phet.chipper.queryParameters.dev ) {
        this._view.addChild( devCreateLayoutBoundsNode( this._view.layoutBounds ) );
      }

      // For debugging, make it possible to see the visibleBounds.  This is not included with ?dev since
      // it should just be equal to what you see.
      if ( phet.chipper.queryParameters.showVisibleBounds ) {
        this._view.addChild( devCreateVisibleBoundsNode( this._view ) );
      }

      // Set the accessible label for the screen. If simName is not provided, then we are creating the home screen.
      if ( simName ) {

        // Single screen sims don't need screen names, instead just show the title of the sim.
        // Using total screens for sim breaks modularity a bit, but it also is needed as that parameter changes the
        // labelling of this screen, see https://github.com/phetsims/joist/issues/496
        if ( numberOfScreens === 1 ) {
          this._view.labelContent = simName;
        }
        else {

          // Like "My Awesome Screen" because "My Awesome" is the name of the screen.
          const screenNameWithScreen = StringUtils.fillIn( screenNamePatternString, {
            name: this.name
          } );

          // initialize proper PDOM labelling for ScreenView
          this._view.labelContent = StringUtils.fillIn( screenSimPatternString, {
            screenName: screenNameWithScreen,
            simName: simName
          } );
        }

        // if there is a screenSummaryNode, then set its intro string now
        this._view.setScreenSummaryIntroString( simName, numberOfScreens );
      }
      assert && this._view.accessibleAudit();
    },

    // Initialize both the model and view
    initializeModelAndView: function() {
      this.initializeModel();
      this.initializeView();
    }
  }, {

    // @public
    MINIMUM_HOME_SCREEN_ICON_SIZE: MINIMUM_HOME_SCREEN_ICON_SIZE,
    HOME_SCREEN_ICON_ASPECT_RATIO: HOME_SCREEN_ICON_ASPECT_RATIO,

    // @public
    MINIMUM_NAVBAR_ICON_SIZE: MINIMUM_NAVBAR_ICON_SIZE
  } );
} );<commit_msg>Revise documentation for Screen.phetioDocumentation, see https://github.com/phetsims/gravity-and-orbits/issues/293<commit_after>// Copyright 2013-2019, University of Colorado Boulder

/**
 * A Screen is the largest chunk of a simulation. (Java sims used the term Module, but that term
 * is too overloaded to use with JavaScript and Git.)
 *
 * When creating a Sim, Screens are supplied as the arguments. They can be specified as object literals or through
 * instances of this class. This class may centralize default behavior or state for Screens in the future, but right
 * now it only allows you to create Sims without using named parameter object literals.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const BooleanProperty = require( 'AXON/BooleanProperty' );
  const Dimension2 = require( 'DOT/Dimension2' );
  const inherit = require( 'PHET_CORE/inherit' );
  const joist = require( 'JOIST/joist' );
  const JoistA11yStrings = require( 'JOIST/JoistA11yStrings' );
  const merge = require( 'PHET_CORE/merge' );
  const Path = require( 'SCENERY/nodes/Path' );
  const Property = require( 'AXON/Property' );
  const Rectangle = require( 'SCENERY/nodes/Rectangle' );
  const Shape = require( 'KITE/Shape' );
  const StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  const Tandem = require( 'TANDEM/Tandem' );

  // a11y strings
  const screenNamePatternString = JoistA11yStrings.screenNamePattern.value;
  const screenSimPatternString = JoistA11yStrings.screenSimPattern.value;
  const simScreenString = JoistA11yStrings.simScreen.value;

  // constants
  const MINIMUM_HOME_SCREEN_ICON_SIZE = new Dimension2( 548, 373 );
  const MINIMUM_NAVBAR_ICON_SIZE = new Dimension2( 147, 100 );
  const NAVBAR_ICON_ASPECT_RATIO = MINIMUM_NAVBAR_ICON_SIZE.width / MINIMUM_NAVBAR_ICON_SIZE.height;
  const HOME_SCREEN_ICON_ASPECT_RATIO = MINIMUM_HOME_SCREEN_ICON_SIZE.width / MINIMUM_HOME_SCREEN_ICON_SIZE.height;
  const ICON_ASPECT_RATIO_TOLERANCE = 5E-3; // how close to the ideal aspect ratio an icon must be

  // Home screen and navigation bar icons must have the same aspect ratio, see https://github.com/phetsims/joist/issues/76
  assert && assert( Math.abs( HOME_SCREEN_ICON_ASPECT_RATIO - HOME_SCREEN_ICON_ASPECT_RATIO ) < ICON_ASPECT_RATIO_TOLERANCE,
    'MINIMUM_HOME_SCREEN_ICON_SIZE and MINIMUM_NAVBAR_ICON_SIZE must have the same aspect ratio' );

  /**
   * @param {function} createModel
   * @param {function:Object } createView - function( model )
   * @param {Object} [options]
   * @constructor
   */
  function Screen( createModel, createView, options ) {

    options = merge( {

      // {string|null} name of the sim, as displayed to the user.
      // For single-screen sims, there is no home screen or navigation bar, and null is OK.
      // For multi-screen sims, this must be provided.
      name: null,

      // {Property.<Color|string>} background color of the Screen
      backgroundColorProperty: new Property( 'white' ),

      // {Node|null} icon shown on the home screen. If null, then a default is created.
      // For single-screen sims, there is no home screen and the default is OK.
      homeScreenIcon: null,

      // {boolean} whether to draw a frame around the small icons on home screen
      showUnselectedHomeScreenIconFrame: false,

      // {Node|null} icon shown in the navigation bar. If null, then the home screen icon will be used, scaled to fit.
      navigationBarIcon: null,

      // {string|null} show a frame around the screen icon when the navbar's background fill is this color
      // 'black', 'white', or null (no frame)
      showScreenIconFrameForNavigationBarFill: null,

      // dt cap in seconds, see https://github.com/phetsims/joist/issues/130
      maxDT: 0.5,

      tandem: Tandem.REQUIRED,

      // a11y - The description that is used when interacting with screen icons/buttons in joist.
      // This is often a full but short sentence with a period at the end of it.
      descriptionContent: null
    }, options );

    assert && assert( _.includes( [ 'black', 'white', null ], options.showScreenIconFrameForNavigationBarFill ),
      'invalid showScreenIconFrameForNavigationBarFill: ' + options.showScreenIconFrameForNavigationBarFill );

    // Create a default homeScreenIcon, using the Screen's background color
    if ( !options.homeScreenIcon ) {
      options.homeScreenIcon = new Rectangle( 0, 0, MINIMUM_HOME_SCREEN_ICON_SIZE.width, MINIMUM_HOME_SCREEN_ICON_SIZE.height, {
        fill: options.backgroundColorProperty.value,
        stroke: 'black'
      } );
    }

    // navigationBarIcon defaults to homeScreenIcon, and will be scaled down
    if ( !options.navigationBarIcon ) {
      options.navigationBarIcon = options.homeScreenIcon;
    }

    // Validate icon sizes
    validateIconSize( options.homeScreenIcon, MINIMUM_HOME_SCREEN_ICON_SIZE, HOME_SCREEN_ICON_ASPECT_RATIO, 'homeScreenIcon' );
    validateIconSize( options.navigationBarIcon, MINIMUM_NAVBAR_ICON_SIZE, NAVBAR_ICON_ASPECT_RATIO, 'navigationBarIcon' );

    if ( assert && options.tandem.supplied ) {
      assert && assert( _.endsWith( options.tandem.phetioID, 'Screen' ), 'Screen tandems should end with Screen suffix' );
    }

    // @public (read-only, joist)
    this.screenTandem = options.tandem;

    assert && assert( !options.backgroundColor, 'Please provide backgroundColorProperty instead' );

    // @public
    this.backgroundColorProperty = options.backgroundColorProperty;

    // @public (read-only)
    this.name = options.name;
    this.homeScreenIcon = options.homeScreenIcon;
    this.navigationBarIcon = options.navigationBarIcon;
    this.showUnselectedHomeScreenIconFrame = options.showUnselectedHomeScreenIconFrame;
    this.showScreenIconFrameForNavigationBarFill = options.showScreenIconFrameForNavigationBarFill;

    // @public (read-only, joist)
    this.maxDT = options.maxDT;

    // @private
    this.createModel = createModel;
    this.createView = createView;

    // Construction of the model and view are delayed and controlled to enable features like
    // a) faster loading when only loading certain screens
    // b) showing a loading progress bar <not implemented>
    this._model = null; // @private
    this._view = null;  // @private

    // @public {Property.<boolean>} indicates whether the Screen is active. Clients can read this, joist sets it.
    // To prevent potential visual glitches, the value should change only while the screen's view is invisible.
    // That is: transitions from false to true before a Screen becomes visible, and from true to false after a Screen becomes invisible.
    this.activeProperty = new BooleanProperty( true, {
      tandem: options.tandem.createTandem( 'activeProperty' ),
      phetioReadOnly: true,
      phetioDocumentation: 'Indicates whether the screen is currently displayed in the simulation.  For single-screen ' +
                           'simulations, there is only one screen and it is always active.'
    } );

    // @public (a11y) - used to set the ScreenView's descriptionContent. This is a bit of a misnomer because Screen is
    // not a Node subtype, so this is a value property rather than a setter.
    this.descriptionContent = '';
    if ( options.descriptionContent ) {
      this.descriptionContent = options.descriptionContent;
    }
    else if ( this.name ) {
      this.descriptionContent = StringUtils.fillIn( screenNamePatternString, {
        name: this.name
      } );
    }
    else {
      this.descriptionContent = simScreenString; // fall back on generic name
    }

    const self = this;
    assert && this.activeProperty.lazyLink( function() {
      assert( self._view, 'isActive should not change before the Screen view has been initialized' );

      // In phet-io mode, the state of a sim can be set without a deterministic order. The activeProperty could be
      // changed before the view's visibility is set.
      if ( !phet.phetio ) {
        assert( !self._view.isVisible(), 'isActive should not change while the Screen view is visible' );
      }
    } );
  }

  joist.register( 'Screen', Screen );

  /**
   * Convenience function to validate the sizes for the home screen icon and navigation bar icon.
   * @param {Node} icon - the icon to validate
   * @param {Dimension2} minimumSize - the minimum allowed size for the icon
   * @param {number} aspectRatio - the required aspect ratio
   * @param {string} name - the name of the icon type (for assert messages)
   */
  var validateIconSize = function( icon, minimumSize, aspectRatio, name ) {
    assert && assert( icon.width >= minimumSize.width, `${name} width is too small: ${icon.width} < ${minimumSize.width}` );
    assert && assert( icon.height >= minimumSize.height, `${name} height is too small: ${icon.height} < ${minimumSize.height}` );

    // Validate home screen aspect ratio
    const actualAspectRatio = icon.width / icon.height;
    assert && assert(
      Math.abs( aspectRatio - actualAspectRatio ) < ICON_ASPECT_RATIO_TOLERANCE,
      name + ' has invalid aspect ratio: ' + actualAspectRatio
    );
  };

  /**
   * For showing ScreenView layoutBounds with 'dev' query parameter.
   * @param {Bounds2} layoutBounds
   * @returns {Node}
   */
  const devCreateLayoutBoundsNode = function( layoutBounds ) {
    return new Path( Shape.bounds( layoutBounds ), {
      stroke: 'red',
      lineWidth: 3,
      pickable: false
    } );
  };

  /**
   * For showing ScreenView visible bounds with 'showVisibleBounds' query parameter.
   * @param {ScreenView} screenView
   * @returns {Node}
   */
  const devCreateVisibleBoundsNode = function( screenView ) {
    const path = new Path( Shape.bounds( screenView.visibleBoundsProperty.value ), {
      stroke: 'blue',
      lineWidth: 6,
      pickable: false
    } );
    screenView.visibleBoundsProperty.link( function( visibleBounds ) {
      path.shape = Shape.bounds( visibleBounds );
    } );
    return path;
  };

  return inherit( Object, Screen, {

    // @public
    reset: function() {
      // Background color not reset, as it's a responsibility of the code that changes the property
    },

    // @public - Returns the model (if it has been constructed)
    get model() {
      assert && assert( this._model, 'Model has not yet been constructed' );
      return this._model;
    },

    // @public - Returns the view (if it has been constructed)
    get view() {
      assert && assert( this._view, 'View has not yet been constructed' );
      return this._view;
    },

    /**
     * Initialize the model.  Clients should use either this or initializeModelAndView
     * Clients may want to use this method to gain more control over the creation process
     * @public (joist-internal)
     */
    initializeModel: function() {
      assert && assert( this._model === null, 'there was already a model' );
      this._model = this.createModel();
    },

    /**
     * Initialize the view.  Clients should use either this or initializeModelAndView
     * Clients may want to use this method to gain more control over the creation process
     * @public (joist-internal)
     * @param {string} [simName] - The display name of the sim, used for a11y. Not provided for the home screen.
     * @param {number} [numberOfScreens] - the number of screens in the whole sim.
     */
    initializeView: function( simName, numberOfScreens ) {
      assert && assert( this._view === null, 'there was already a view' );
      this._view = this.createView( this.model );
      this._view.setVisible( false ); // a Screen is invisible until selected

      // Show the home screen's layoutBounds
      if ( phet.chipper.queryParameters.dev ) {
        this._view.addChild( devCreateLayoutBoundsNode( this._view.layoutBounds ) );
      }

      // For debugging, make it possible to see the visibleBounds.  This is not included with ?dev since
      // it should just be equal to what you see.
      if ( phet.chipper.queryParameters.showVisibleBounds ) {
        this._view.addChild( devCreateVisibleBoundsNode( this._view ) );
      }

      // Set the accessible label for the screen. If simName is not provided, then we are creating the home screen.
      if ( simName ) {

        // Single screen sims don't need screen names, instead just show the title of the sim.
        // Using total screens for sim breaks modularity a bit, but it also is needed as that parameter changes the
        // labelling of this screen, see https://github.com/phetsims/joist/issues/496
        if ( numberOfScreens === 1 ) {
          this._view.labelContent = simName;
        }
        else {

          // Like "My Awesome Screen" because "My Awesome" is the name of the screen.
          const screenNameWithScreen = StringUtils.fillIn( screenNamePatternString, {
            name: this.name
          } );

          // initialize proper PDOM labelling for ScreenView
          this._view.labelContent = StringUtils.fillIn( screenSimPatternString, {
            screenName: screenNameWithScreen,
            simName: simName
          } );
        }

        // if there is a screenSummaryNode, then set its intro string now
        this._view.setScreenSummaryIntroString( simName, numberOfScreens );
      }
      assert && this._view.accessibleAudit();
    },

    // Initialize both the model and view
    initializeModelAndView: function() {
      this.initializeModel();
      this.initializeView();
    }
  }, {

    // @public
    MINIMUM_HOME_SCREEN_ICON_SIZE: MINIMUM_HOME_SCREEN_ICON_SIZE,
    HOME_SCREEN_ICON_ASPECT_RATIO: HOME_SCREEN_ICON_ASPECT_RATIO,

    // @public
    MINIMUM_NAVBAR_ICON_SIZE: MINIMUM_NAVBAR_ICON_SIZE
  } );
} );<|endoftext|>


<commit_before>// Copyright 2018, University of Colorado Boulder

/**
 * Slider, with support for horizontal and vertical orientations. By default, the slider is constructed in the
 * horizontal orientation, then adjusted if the vertical orientation was specified.
 *
 * Note: This type was originally named HSlider, renamed in https://github.com/phetsims/sun/issues/380.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var AccessibleSlider = require( 'SUN/accessibility/AccessibleSlider' );
  var InstanceRegistry = require( 'PHET_CORE/documentation/InstanceRegistry' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var FocusHighlightFromNode = require( 'SCENERY/accessibility/FocusHighlightFromNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Property = require( 'AXON/Property' );
  var PropertyIO = require( 'AXON/PropertyIO' );
  var RangeIO = require( 'DOT/RangeIO' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var SliderIO = require( 'SUN/SliderIO' );
  var SliderThumb = require( 'SUN/SliderThumb' );
  var SliderTrack = require( 'SUN/SliderTrack' );
  var sun = require( 'SUN/sun' );
  var Tandem = require( 'TANDEM/Tandem' );
  var Util = require( 'DOT/Util' );

  // ifphetio
  var BooleanIO = require( 'ifphetio!PHET_IO/types/BooleanIO' );

  // constants
  var VERTICAL_ROTATION = -Math.PI / 2;

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

    var self = this;

    options = _.extend( {

      orientation: 'horizontal', // 'horizontal'|'vertical'

      // track
      trackSize: new Dimension2( 100, 5 ),
      trackFillEnabled: 'white',
      trackFillDisabled: 'gray',
      trackStroke: 'black',
      trackLineWidth: 1,
      trackCornerRadius: 0,

      // {Node} optional thumb, replaces the default.
      // Client is responsible for highlighting, disabling and pointer areas.
      // The thumb is positioned based on its center and hence can have its origin anywhere
      thumbNode: null,

      // Options for the default thumb, ignored if thumbNode is set
      thumbSize: new Dimension2( 22, 45 ),
      thumbFillEnabled: 'rgb(50,145,184)',
      thumbFillHighlighted: 'rgb(71,207,255)',
      thumbFillDisabled: '#F0F0F0',
      thumbStroke: 'black',
      thumbLineWidth: 1,
      thumbYOffset: 0, // center of the thumb is vertically offset by this amount from the center of the track
      thumbCenterLineStroke: 'white',
      thumbTouchAreaXDilation: 11,
      thumbTouchAreaYDilation: 11,
      thumbMouseAreaXDilation: 0,
      thumbMouseAreaYDilation: 0,

      // ticks
      tickLabelSpacing: 6,
      majorTickLength: 25,
      majorTickStroke: 'black',
      majorTickLineWidth: 1,
      minorTickLength: 10,
      minorTickStroke: 'black',
      minorTickLineWidth: 1,

      // other
      cursor: 'pointer',
      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

      enabledProperty: null, // see below
      enabledRangeProperty: null, // see below

      // phet-io
      tandem: Tandem.required,
      phetioType: SliderIO
    }, options );

    assert && assert( options.orientation === 'horizontal' || options.orientation === 'vertical',
      'invalid orientation: ' + options.orientation );
    this.orientation = options.orientation; // @private

    assert && assert( options.rotation === undefined,
      'Slider specified rotation based on orientation' );
    if ( options.orientation === 'vertical' ) {
      options.rotation = VERTICAL_ROTATION;
    }

    Node.call( this );

    var ownsEnabledProperty = !options.enabledProperty;
    var ownsEnabledRangeProperty = !options.enabledRangeProperty;

    // phet-io, Assign default options that need tandems.
    options.enabledProperty = options.enabledProperty || new Property( true, {
      tandem: options.tandem.createTandem( 'enabledProperty' ),
      phetioType: PropertyIO( BooleanIO )
    } );

    // controls the portion of the slider that is enabled
    options.enabledRangeProperty = options.enabledRangeProperty || new Property( range, {
      tandem: options.tandem.createTandem( 'enabledRangeProperty' ),
      phetioType: PropertyIO( RangeIO )
    } );

    // @public
    this.enabledProperty = options.enabledProperty;
    this.enabledRangeProperty = options.enabledRangeProperty;

    // @private options needed by prototype functions that add ticks
    this.tickOptions = _.pick( options, 'tickLabelSpacing',
      'majorTickLength', 'majorTickStroke', 'majorTickLineWidth',
      'minorTickLength', 'minorTickStroke', 'minorTickLineWidth' );

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

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

    // @private track
    this.track = new SliderTrack( valueProperty, this.valueToPosition, {

      // propagate options that are specific to SliderTrack
      size: options.trackSize,
      fillEnabled: options.trackFillEnabled,
      fillDisabled: options.trackFillDisabled,
      stroke: options.trackStroke,
      lineWidth: options.trackLineWidth,
      cornerRadius: options.trackCornerRadius,
      enabledProperty: options.enabledProperty,
      startDrag: options.startDrag,
      endDrag: options.endDrag,
      constrainValue: options.constrainValue,

      // phet-io
      tandem: options.tandem.createTandem( 'track' )
    } );
    this.track.centerX = this.valueToPosition( ( range.max + range.min ) / 2 );

    // The thumb of the slider
    var thumb = options.thumbNode || new SliderThumb( this.enabledProperty, {

      // propagate options that are specific to SliderThumb
      size: options.thumbSize,
      fillEnabled: options.thumbFillEnabled,
      fillHighlighted: options.thumbFillHighlighted,
      fillDisabled: options.thumbFillDisabled,
      stroke: options.thumbStroke,
      lineWidth: options.thumbLineWidth,
      centerLineStroke: options.thumbCenterLineStroke,
      tandem: options.tandem.createTandem( 'thumb' )
    } );

    // Dilate the local bounds horizontally so that it extends beyond where the thumb can reach.  This prevents layout
    // asymmetry when the slider thumb is off the edges of the track.  See https://github.com/phetsims/sun/issues/282
    this.track.localBounds = this.track.localBounds.dilatedX( thumb.width / 2 );

    // Add the track
    this.addChild( this.track );

    // do this outside of options hash, so that it applied to both default and custom thumbs
    thumb.centerY = this.track.centerY + options.thumbYOffset;
    this.addChild( thumb );

    // touchArea for the default thumb. If a custom thumb is provided, the client is responsible for its touchArea.
    if ( !options.thumbNode && ( options.thumbTouchAreaXDilation || options.thumbTouchAreaYDilation ) ) {
      thumb.touchArea = thumb.localBounds.dilatedXY( options.thumbTouchAreaXDilation, options.thumbTouchAreaYDilation );
    }

    // mouseArea for the default thumb. If a custom thumb is provided, the client is responsible for its mouseArea.
    if ( !options.thumbNode && ( options.thumbMouseAreaXDilation || options.thumbMouseAreaYDilation ) ) {
      thumb.mouseArea = thumb.localBounds.dilatedXY( options.thumbMouseAreaXDilation, options.thumbMouseAreaYDilation );
    }

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

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

      allowTouchSnag: true,

      start: function( event, trail ) {
        if ( self.enabledProperty.get() ) {
          options.startDrag();
          var transform = trail.subtrailTo( self ).getTransform();

          // Determine the offset relative to the center of the thumb
          clickXOffset = transform.inversePosition2( event.pointer.point ).x - thumb.centerX;
        }
      },

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

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

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

    // @public (read-only) {Boolean} - flag that indicates whether the thumb is currently being dragged
    this.thumbDragging = false;
    var thumbDraggingListener = thumbInputListener.isDraggingProperty.linkAttribute( this, 'thumbDragging' );

    // enable/disable
    var enabledObserver = function( enabled ) {
      self.cursor = self.enabledProperty.get() ? options.cursor : 'default';
      if ( !enabled ) {
        if ( thumbInputListener.dragging ) { thumbInputListener.endDrag(); }
      }
      self.pickable = enabled;
    };
    this.enabledProperty.link( enabledObserver ); // must be unlinked in disposeSlider

    // a11y - custom focus highlight that surrounds and moves with the thumb
    this.focusHighlight = new FocusHighlightFromNode( thumb );

    // update thumb location when value changes
    var valueObserver = function( value ) {
      thumb.centerX = self.valueToPosition( value );
      self.focusHighlight.centerX = thumb.centerX;
    };
    valueProperty.link( valueObserver ); // must be unlinked in disposeSlider

    // when the enabled range changes, the value to position linear function must change as well
    var enabledRangeObserver = function( enabledRange ) {

      assert && assert( enabledRange.min >= range.min && enabledRange.max <= range.max, 'enabledRange is out of range' );

      var initialValueToPosition = new LinearFunction( range.min, range.max, 0, options.trackSize.width, true /* clamp */ );
      var min = initialValueToPosition( enabledRange.min );
      var max = initialValueToPosition( enabledRange.max );

      // update the geometry of the enabled track
      self.track.updateEnabledTrackWidth( min, max );

      // update the function that maps value to position for the track and the slider
      self.valueToPosition = new LinearFunction( enabledRange.min, enabledRange.max, min, max, true /* clamp */ );
      self.track.valueToPosition = self.valueToPosition;

      // clamp the value to the enabled range if it changes
      valueProperty.set( Util.clamp( valueProperty.value, enabledRange.min, enabledRange.max ) );
    };
    this.enabledRangeProperty.link( enabledRangeObserver ); // needs to be unlinked in dispose function

    this.mutate( options );

    // @private Called by dispose
    this.disposeSlider = function() {
      thumb.dispose && thumb.dispose(); // in case a custom thumb is provided via options.thumbNode that doesn't implement dispose
      self.track.dispose();
      self.disposeAccessibleSlider(); // dispose accessibility
      valueProperty.unlink( valueObserver );
      ownsEnabledRangeProperty && self.enabledRangeProperty.dispose();
      ownsEnabledProperty && self.enabledProperty.dispose();
      thumbInputListener.isDraggingProperty.unlinkAttribute( thumbDraggingListener );
      thumbInputListener.dispose();
    };

    // mix accessible slider functionality into Slider
    this.initializeAccessibleSlider( valueProperty, this.enabledRangeProperty, this.enabledProperty,
      _.extend( {}, options, {
        ariaOrientation: options.orientation
      } ) );

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

  sun.register( 'Slider', Slider );

  inherit( Node, Slider, {

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

    /**
     * Adds a major tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     * @public
     */
    addMajorTick: function( value, label ) {
      this.addTick( this.majorTicksParent, value, label,
        this.tickOptions.majorTickLength, this.tickOptions.majorTickStroke, this.tickOptions.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.tickOptions.minorTickLength, this.tickOptions.minorTickStroke, this.tickOptions.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 ) {

        // For a vertical slider, rotate labels opposite the rotation of the slider, so that they appear as expected.
        if ( this.orientation === 'vertical' ) {
          label.rotation = -VERTICAL_ROTATION;
        }
        parent.addChild( label );
        label.centerX = tick.centerX;
        label.bottom = tick.top - this.tickOptions.tickLabelSpacing;
        label.pickable = false;
      }
    },

    // @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
    setEnabledRange: function( enabledRange ) { this.enabledRangeProperty.value = enabledRange; },
    set enabledRange( range ) { this.setEnabledRange( range ); },

    // @public
    getEnabledRange: function() { return this.enabledRangeProperty.value; },
    get enabledRange() { return this.getEnabledRange(); },

    // @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(); }
  } );

  // mix accessibility into Slider
  AccessibleSlider.mixInto( Slider );

  return Slider;
} );
<commit_msg>fix assertion message, https://github.com/phetsims/sun/issues/380<commit_after>// Copyright 2018, University of Colorado Boulder

/**
 * Slider, with support for horizontal and vertical orientations. By default, the slider is constructed in the
 * horizontal orientation, then adjusted if the vertical orientation was specified.
 *
 * Note: This type was originally named HSlider, renamed in https://github.com/phetsims/sun/issues/380.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var AccessibleSlider = require( 'SUN/accessibility/AccessibleSlider' );
  var InstanceRegistry = require( 'PHET_CORE/documentation/InstanceRegistry' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var FocusHighlightFromNode = require( 'SCENERY/accessibility/FocusHighlightFromNode' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Property = require( 'AXON/Property' );
  var PropertyIO = require( 'AXON/PropertyIO' );
  var RangeIO = require( 'DOT/RangeIO' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var SliderIO = require( 'SUN/SliderIO' );
  var SliderThumb = require( 'SUN/SliderThumb' );
  var SliderTrack = require( 'SUN/SliderTrack' );
  var sun = require( 'SUN/sun' );
  var Tandem = require( 'TANDEM/Tandem' );
  var Util = require( 'DOT/Util' );

  // ifphetio
  var BooleanIO = require( 'ifphetio!PHET_IO/types/BooleanIO' );

  // constants
  var VERTICAL_ROTATION = -Math.PI / 2;

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

    var self = this;

    options = _.extend( {

      orientation: 'horizontal', // 'horizontal'|'vertical'

      // track
      trackSize: new Dimension2( 100, 5 ),
      trackFillEnabled: 'white',
      trackFillDisabled: 'gray',
      trackStroke: 'black',
      trackLineWidth: 1,
      trackCornerRadius: 0,

      // {Node} optional thumb, replaces the default.
      // Client is responsible for highlighting, disabling and pointer areas.
      // The thumb is positioned based on its center and hence can have its origin anywhere
      thumbNode: null,

      // Options for the default thumb, ignored if thumbNode is set
      thumbSize: new Dimension2( 22, 45 ),
      thumbFillEnabled: 'rgb(50,145,184)',
      thumbFillHighlighted: 'rgb(71,207,255)',
      thumbFillDisabled: '#F0F0F0',
      thumbStroke: 'black',
      thumbLineWidth: 1,
      thumbYOffset: 0, // center of the thumb is vertically offset by this amount from the center of the track
      thumbCenterLineStroke: 'white',
      thumbTouchAreaXDilation: 11,
      thumbTouchAreaYDilation: 11,
      thumbMouseAreaXDilation: 0,
      thumbMouseAreaYDilation: 0,

      // ticks
      tickLabelSpacing: 6,
      majorTickLength: 25,
      majorTickStroke: 'black',
      majorTickLineWidth: 1,
      minorTickLength: 10,
      minorTickStroke: 'black',
      minorTickLineWidth: 1,

      // other
      cursor: 'pointer',
      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

      enabledProperty: null, // see below
      enabledRangeProperty: null, // see below

      // phet-io
      tandem: Tandem.required,
      phetioType: SliderIO
    }, options );

    assert && assert( options.orientation === 'horizontal' || options.orientation === 'vertical',
      'invalid orientation: ' + options.orientation );
    this.orientation = options.orientation; // @private

    assert && assert( options.rotation === undefined,
      'Slider specifies rotation based on orientation' );
    if ( options.orientation === 'vertical' ) {
      options.rotation = VERTICAL_ROTATION;
    }

    Node.call( this );

    var ownsEnabledProperty = !options.enabledProperty;
    var ownsEnabledRangeProperty = !options.enabledRangeProperty;

    // phet-io, Assign default options that need tandems.
    options.enabledProperty = options.enabledProperty || new Property( true, {
      tandem: options.tandem.createTandem( 'enabledProperty' ),
      phetioType: PropertyIO( BooleanIO )
    } );

    // controls the portion of the slider that is enabled
    options.enabledRangeProperty = options.enabledRangeProperty || new Property( range, {
      tandem: options.tandem.createTandem( 'enabledRangeProperty' ),
      phetioType: PropertyIO( RangeIO )
    } );

    // @public
    this.enabledProperty = options.enabledProperty;
    this.enabledRangeProperty = options.enabledRangeProperty;

    // @private options needed by prototype functions that add ticks
    this.tickOptions = _.pick( options, 'tickLabelSpacing',
      'majorTickLength', 'majorTickStroke', 'majorTickLineWidth',
      'minorTickLength', 'minorTickStroke', 'minorTickLineWidth' );

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

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

    // @private track
    this.track = new SliderTrack( valueProperty, this.valueToPosition, {

      // propagate options that are specific to SliderTrack
      size: options.trackSize,
      fillEnabled: options.trackFillEnabled,
      fillDisabled: options.trackFillDisabled,
      stroke: options.trackStroke,
      lineWidth: options.trackLineWidth,
      cornerRadius: options.trackCornerRadius,
      enabledProperty: options.enabledProperty,
      startDrag: options.startDrag,
      endDrag: options.endDrag,
      constrainValue: options.constrainValue,

      // phet-io
      tandem: options.tandem.createTandem( 'track' )
    } );
    this.track.centerX = this.valueToPosition( ( range.max + range.min ) / 2 );

    // The thumb of the slider
    var thumb = options.thumbNode || new SliderThumb( this.enabledProperty, {

      // propagate options that are specific to SliderThumb
      size: options.thumbSize,
      fillEnabled: options.thumbFillEnabled,
      fillHighlighted: options.thumbFillHighlighted,
      fillDisabled: options.thumbFillDisabled,
      stroke: options.thumbStroke,
      lineWidth: options.thumbLineWidth,
      centerLineStroke: options.thumbCenterLineStroke,
      tandem: options.tandem.createTandem( 'thumb' )
    } );

    // Dilate the local bounds horizontally so that it extends beyond where the thumb can reach.  This prevents layout
    // asymmetry when the slider thumb is off the edges of the track.  See https://github.com/phetsims/sun/issues/282
    this.track.localBounds = this.track.localBounds.dilatedX( thumb.width / 2 );

    // Add the track
    this.addChild( this.track );

    // do this outside of options hash, so that it applied to both default and custom thumbs
    thumb.centerY = this.track.centerY + options.thumbYOffset;
    this.addChild( thumb );

    // touchArea for the default thumb. If a custom thumb is provided, the client is responsible for its touchArea.
    if ( !options.thumbNode && ( options.thumbTouchAreaXDilation || options.thumbTouchAreaYDilation ) ) {
      thumb.touchArea = thumb.localBounds.dilatedXY( options.thumbTouchAreaXDilation, options.thumbTouchAreaYDilation );
    }

    // mouseArea for the default thumb. If a custom thumb is provided, the client is responsible for its mouseArea.
    if ( !options.thumbNode && ( options.thumbMouseAreaXDilation || options.thumbMouseAreaYDilation ) ) {
      thumb.mouseArea = thumb.localBounds.dilatedXY( options.thumbMouseAreaXDilation, options.thumbMouseAreaYDilation );
    }

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

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

      allowTouchSnag: true,

      start: function( event, trail ) {
        if ( self.enabledProperty.get() ) {
          options.startDrag();
          var transform = trail.subtrailTo( self ).getTransform();

          // Determine the offset relative to the center of the thumb
          clickXOffset = transform.inversePosition2( event.pointer.point ).x - thumb.centerX;
        }
      },

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

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

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

    // @public (read-only) {Boolean} - flag that indicates whether the thumb is currently being dragged
    this.thumbDragging = false;
    var thumbDraggingListener = thumbInputListener.isDraggingProperty.linkAttribute( this, 'thumbDragging' );

    // enable/disable
    var enabledObserver = function( enabled ) {
      self.cursor = self.enabledProperty.get() ? options.cursor : 'default';
      if ( !enabled ) {
        if ( thumbInputListener.dragging ) { thumbInputListener.endDrag(); }
      }
      self.pickable = enabled;
    };
    this.enabledProperty.link( enabledObserver ); // must be unlinked in disposeSlider

    // a11y - custom focus highlight that surrounds and moves with the thumb
    this.focusHighlight = new FocusHighlightFromNode( thumb );

    // update thumb location when value changes
    var valueObserver = function( value ) {
      thumb.centerX = self.valueToPosition( value );
      self.focusHighlight.centerX = thumb.centerX;
    };
    valueProperty.link( valueObserver ); // must be unlinked in disposeSlider

    // when the enabled range changes, the value to position linear function must change as well
    var enabledRangeObserver = function( enabledRange ) {

      assert && assert( enabledRange.min >= range.min && enabledRange.max <= range.max, 'enabledRange is out of range' );

      var initialValueToPosition = new LinearFunction( range.min, range.max, 0, options.trackSize.width, true /* clamp */ );
      var min = initialValueToPosition( enabledRange.min );
      var max = initialValueToPosition( enabledRange.max );

      // update the geometry of the enabled track
      self.track.updateEnabledTrackWidth( min, max );

      // update the function that maps value to position for the track and the slider
      self.valueToPosition = new LinearFunction( enabledRange.min, enabledRange.max, min, max, true /* clamp */ );
      self.track.valueToPosition = self.valueToPosition;

      // clamp the value to the enabled range if it changes
      valueProperty.set( Util.clamp( valueProperty.value, enabledRange.min, enabledRange.max ) );
    };
    this.enabledRangeProperty.link( enabledRangeObserver ); // needs to be unlinked in dispose function

    this.mutate( options );

    // @private Called by dispose
    this.disposeSlider = function() {
      thumb.dispose && thumb.dispose(); // in case a custom thumb is provided via options.thumbNode that doesn't implement dispose
      self.track.dispose();
      self.disposeAccessibleSlider(); // dispose accessibility
      valueProperty.unlink( valueObserver );
      ownsEnabledRangeProperty && self.enabledRangeProperty.dispose();
      ownsEnabledProperty && self.enabledProperty.dispose();
      thumbInputListener.isDraggingProperty.unlinkAttribute( thumbDraggingListener );
      thumbInputListener.dispose();
    };

    // mix accessible slider functionality into Slider
    this.initializeAccessibleSlider( valueProperty, this.enabledRangeProperty, this.enabledProperty,
      _.extend( {}, options, {
        ariaOrientation: options.orientation
      } ) );

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

  sun.register( 'Slider', Slider );

  inherit( Node, Slider, {

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

    /**
     * Adds a major tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     * @public
     */
    addMajorTick: function( value, label ) {
      this.addTick( this.majorTicksParent, value, label,
        this.tickOptions.majorTickLength, this.tickOptions.majorTickStroke, this.tickOptions.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.tickOptions.minorTickLength, this.tickOptions.minorTickStroke, this.tickOptions.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 ) {

        // For a vertical slider, rotate labels opposite the rotation of the slider, so that they appear as expected.
        if ( this.orientation === 'vertical' ) {
          label.rotation = -VERTICAL_ROTATION;
        }
        parent.addChild( label );
        label.centerX = tick.centerX;
        label.bottom = tick.top - this.tickOptions.tickLabelSpacing;
        label.pickable = false;
      }
    },

    // @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
    setEnabledRange: function( enabledRange ) { this.enabledRangeProperty.value = enabledRange; },
    set enabledRange( range ) { this.setEnabledRange( range ); },

    // @public
    getEnabledRange: function() { return this.enabledRangeProperty.value; },
    get enabledRange() { return this.getEnabledRange(); },

    // @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(); }
  } );

  // mix accessibility into Slider
  AccessibleSlider.mixInto( Slider );

  return Slider;
} );
<|endoftext|>


<commit_before>// Copyright (c) Microsoft Corporation. All rights reserved.

/**
 * @param {string} url
 * @param {Array} options
 * @param {function} success
 * @param {function} fail
 * @constructor
 */
function InAppBrowser(url, options, success, fail) {
    this._url = url;
    this._options = {};
    this._callbacks = {
        success: success,
        fail: fail
    };

    options = (options.trim() === '') ? [] : options.split(';');

    // prepare options object
    options.forEach(function (option) {
        var prop = option.split('='); // "theOption=value"
        this._options[prop[0]] = prop[1];
    }.bind(this));

    if (this._options.hidden !== 'yes') {
        this.show();
    }
}

/**
 * @enum
 */
InAppBrowser.Events = {
    LOAD_START: 'loadstart',
    LOAD_STOP: 'loadstop',
    LOAD_ERROR: 'loaderror',
    EXIT: 'exit'
};

InAppBrowser.prototype.show = function () {};

InAppBrowser.prototype.close = function () {};

InAppBrowser.prototype.injectScriptCode = function (callback, args) {
    console.error('InAppBrowser "injectScriptCode" simulation not supported');
};

InAppBrowser.prototype.injectScriptFile = function (callback, args) {
    console.error('InAppBrowser "injectScriptFile" simulation not supported');
};

InAppBrowser.prototype.injectStyleCode = function (callback, args) {
    console.error('InAppBrowser "injectStyleCode" simulation not supported');
};

InAppBrowser.prototype.injectStyleFile = function (callback, args) {
    console.error('InAppBrowser "injectScriptFile" simulation not supported');
};

/**
 * @param {string} eventName
 * @private
 */
InAppBrowser.prototype._success = function (eventName) {
    this._callbacks.success({
        type: eventName
    });
};

/**
 * @param {string} url
 * @param {Array} options
 * @param {function} success
 * @param {function} fail
 * @constructor
 */
function SystemBrowser(url, options, success, fail) {
    InAppBrowser.call(this, url, options, success, fail);

    this._window = null;
}

SystemBrowser.prototype = Object.create(InAppBrowser.prototype);
SystemBrowser.prototype.constructor = SystemBrowser;
SystemBrowser.prototype.parentClass = InAppBrowser.prototype;

SystemBrowser.prototype.show = function () {
    if (!this._window) {
        var modulemapper = cordova.require('cordova/modulemapper'),
            windowOpen = modulemapper.getOriginalSymbol(window, 'window.open');

        this._success(InAppBrowser.Events.LOAD_START);

        this._window = windowOpen.call(window, this._url, '_blank');
    }
};

SystemBrowser.prototype.close = function () {
    if (this._window) {
        this._window.close();
        this._window = null;

        this._success(InAppBrowser.Events.EXIT);
    }
};

/**
 * @param {string} url
 * @param {Array} options
 * @param {function} success
 * @param {function} fail
 * @constructor
 */
function IframeBrowser(url, options, success, fail) {
    this._container = null;
    this._frame = null;

    InAppBrowser.call(this, url, options, success, fail);
}

IframeBrowser.prototype = Object.create(InAppBrowser.prototype);
IframeBrowser.prototype.constructor = IframeBrowser;
IframeBrowser.prototype.parentClass = InAppBrowser.prototype;

IframeBrowser.prototype.show = function () {
    this._createFrame();

    this._frame.src = this._url;
    this._container.style.display = 'block';
};

IframeBrowser.prototype.close = function () {
    if (this._container) {
        this._container.parentNode.removeChild(this._container);
        this._container = null;
        this._frame = null;

        this._success(InAppBrowser.Events.EXIT);
    }
};

IframeBrowser.prototype.hide = function () {
    if (this._container) {
        this._container.style.display = 'none';
    }
};

IframeBrowser.prototype.injectScriptCode = function (callback, args) {
    if (this._container && this._frame) {
        var code = args[0].replace(/\r?\n|\r/g, ''),
            hasCallback = args[1];

        try {
            var result = this._injectCode(code);
            if (hasCallback) {
                callback(result);
            }
        } catch (error) {
            console.error('Error occured while trying to inject script', error);
        }
    }
};

IframeBrowser.prototype.injectScriptFile = function (callback, args) {
    var file = args[0];
    var code = '(function () {' +
        'var head = document.getElementsByTagName("head")[0];' +
        'var script = document.createElement("script");' +
        'script.type = "text/javascript";' +
        'script.id = "inappbrowser-inject-script";' +
        'script.src = "' + file + '";' +
        'head.appendChild(script);' +
        '}());';

    args[0] = code;

    this.injectScriptCode(callback, args);
};

IframeBrowser.prototype.injectStyleCode = function (callback, args) {
    if (this._container && this._frame) {
        var css = args[0].replace(/\r?\n|\r/g, ''),
            hasCallback = args[1];

        var code = '(function () {' +
            'var head = document.getElementsByTagName("head")[0];' +
            'var style = document.createElement("style");' +
            'style.type = "text/css";' +
            'if (style.styleSheet) {' +
            'style.styleSheet.cssText = "' + css + '";' +
            '} else {' +
            'style.appendChild(document.createTextNode("' + css + '"));}' +
            'head.appendChild(style);' +
            '}());';

        try {
            var result = this._injectCode(code);
            if (hasCallback) {
                callback(result);
            }
        } catch (error) {
            console.error('Error occured while trying to inject style', error);
        }
    }
};

IframeBrowser.prototype.injectStyleFile = function (callback, args) {
    if (this._container && this._frame) {
        var file = args[0],
            hasCallback = args[1];

        var code = '(function () {' +
            'var head = document.getElementsByTagName("head")[0];' +
            'var link = document.createElement("link");' +
            'link.type = "text/css";' +
            'link.rel = "stylesheet";' +
            'link.href = "' + file + '";' +
            'head.appendChild(link);' +
            '}());';

        try {
            var result = this._injectCode(code);
            if (hasCallback) {
                callback(result);
            }
        } catch (error) {
            console.error('Error occured while trying to inject a style file', error);
        }
    }
};

/**
 * @private
 */
IframeBrowser.prototype._createFrame = function () {
    if (!this._container) {
        this._container = document.createElement('div');
        this._frame = document.createElement('iframe');

        // container style
        var style = this._container.style;
        style.position = 'absolute';
        style.border = '0';
        style.backgroundColor = '#ffffff';
        style.width = '100%';
        style.height = '100%';
        style.zIndex = '10000';

        // iframe style
        style = this._frame.style;
        style.border = '0';
        style.width = '100%';

        if (!this._options.location || this._options.location === 'yes') {
            style.height = 'calc(100% - 35px)';

            this._container.appendChild(this._createNavigationBar());
        } else {
            style.height = '100%';
        }

        this._container.appendChild(this._frame);

        document.body.appendChild(this._container);

        this._frame.addEventListener('pageshow', this._success.bind(this, InAppBrowser.Events.LOAD_START));
        this._frame.addEventListener('load', this._success.bind(this, InAppBrowser.Events.LOAD_STOP));
        this._frame.addEventListener('error', this._success.bind(this, InAppBrowser.Events.LOAD_ERROR));
        this._frame.addEventListener('abort', this._success.bind(this, InAppBrowser.Events.LOAD_ERROR));
    }
};

/**
 * @private
 */
IframeBrowser.prototype._createNavigationBar = function () {
    var navigationDiv = document.createElement('div'),
        wrapper = document.createElement('div'),
        closeButton = document.createElement('button');

    navigationDiv.style.height = '30px';
    navigationDiv.style.padding = '2px';
    navigationDiv.style.backgroundColor = '#404040';

    closeButton.innerHTML = '✖';
    closeButton.style.width = '30px';
    closeButton.style.height = '25px';

    closeButton.addEventListener('click', function (e) {
        setTimeout(function () {
            this.close();
        }.bind(this), 0);
    }.bind(this));

    wrapper.appendChild(closeButton);
    navigationDiv.appendChild(wrapper);

    return navigationDiv;
};

/**
 * @private
 */
IframeBrowser.prototype._injectCode = function (code) {
    var result = this._frame.contentWindow.eval(code);

    return result || [];
};

/**
 * Create an instance of InAppBrowser based on the target type. When the type is '_system',
 * an instance of the system browser is created, when the type is '_blank', an instance of
 * a browser based on iframe is created. For any other target type, including '_self',
 * the current window will navigate to the given URL.
 * @param {function} success
 * @param {function} fail
 * @param {Array} args
 * @return {object}
 */
function create(success, fail, args) {
    // args[0]: url, args[1]: target, args[2]: options
    var Constructor;

    switch (args[1]) {
        case '_system':
            // open in a new browser tab
            Constructor = SystemBrowser;
            break;
        case '_blank':
            Constructor = IframeBrowser;
            break;
        default:
            // "_self" and any other option, use the same tab
            window.location = args[0];
            return;
    }

    return new Constructor(args[0], args[2], success, fail);
}

module.exports.create = create;
<commit_msg>Adding documentation and clean up.<commit_after>// Copyright (c) Microsoft Corporation. All rights reserved.

// Events taken from cordova-plugin-inappbrowser implementation, and iframe approach
// based from https://github.com/apache/cordova-plugin-inappbrowser/blob/master/src/browser/InAppBrowserProxy.js

/**
 * @param {string} url
 * @param {Array} options
 * @param {function} success
 * @param {function} fail
 * @constructor
 */
function InAppBrowser(url, options, success, fail) {
    this._url = url;
    this._options = {};
    this._callbacks = {
        success: success,
        fail: fail
    };

    options = (options.trim() === '') ? [] : options.split(';');

    // prepare options object
    options.forEach(function (option) {
        var prop = option.split('='); // "theOption=value"
        this._options[prop[0]] = prop[1];
    }.bind(this));

    if (this._options.hidden !== 'yes') {
        this.show();
    }
}

/**
 * @enum
 */
InAppBrowser.Events = {
    LOAD_START: 'loadstart',
    LOAD_STOP: 'loadstop',
    LOAD_ERROR: 'loaderror',
    EXIT: 'exit'
};

/**
 * JavaScript implementation of the Native call to show. The simulator browser should be
 * displayed.
 * Subclasses must implement this.
 */
InAppBrowser.prototype.show = function () {};

/**
 * Hide the simulator browser.
 * Subclasses must implement this.
 */
InAppBrowser.prototype.close = function () {};

InAppBrowser.prototype.injectScriptCode = function (callback, args) {
    console.error('InAppBrowser "injectScriptCode" simulation not supported');
};

InAppBrowser.prototype.injectScriptFile = function (callback, args) {
    console.error('InAppBrowser "injectScriptFile" simulation not supported');
};

InAppBrowser.prototype.injectStyleCode = function (callback, args) {
    console.error('InAppBrowser "injectStyleCode" simulation not supported');
};

InAppBrowser.prototype.injectStyleFile = function (callback, args) {
    console.error('InAppBrowser "injectScriptFile" simulation not supported');
};

/**
 * Execute the success callback specified at the native "open" call.
 * @param {string} eventName
 * @private
 */
InAppBrowser.prototype._success = function (eventName) {
    this._callbacks.success({
        type: eventName
    });
};

/**
 * Implementation of the System browser simulation. This uses a new browser window
 * as the system browser.
 * @param {string} url
 * @param {Array} options
 * @param {function} success
 * @param {function} fail
 * @constructor
 */
function SystemBrowser(url, options, success, fail) {
    InAppBrowser.call(this, url, options, success, fail);

    this._window = null;
}

SystemBrowser.prototype = Object.create(InAppBrowser.prototype);
SystemBrowser.prototype.constructor = SystemBrowser;
SystemBrowser.prototype.parentClass = InAppBrowser.prototype;

SystemBrowser.prototype.show = function () {
    if (!this._window) {
        var modulemapper = cordova.require('cordova/modulemapper'),
            windowOpen = modulemapper.getOriginalSymbol(window, 'window.open');

        this._success(InAppBrowser.Events.LOAD_START);

        this._window = windowOpen.call(window, this._url, '_blank');
    }
};

SystemBrowser.prototype.close = function () {
    if (this._window) {
        this._window.close();
        this._window = null;

        this._success(InAppBrowser.Events.EXIT);
    }
};

/**
 * Implementation of the WebView type of InAppBrowser plugin. This creates an iframe
 * that simulates the embedded WebView.
 * @param {string} url
 * @param {Array} options
 * @param {function} success
 * @param {function} fail
 * @constructor
 */
function IframeBrowser(url, options, success, fail) {
    this._container = null;
    this._frame = null;
    this._resizeCallback = this._resizeContainer.bind(this);
    this._isVisible = false;

    InAppBrowser.call(this, url, options, success, fail);
}

IframeBrowser.prototype = Object.create(InAppBrowser.prototype);
IframeBrowser.prototype.constructor = IframeBrowser;
IframeBrowser.prototype.parentClass = InAppBrowser.prototype;

IframeBrowser.prototype.show = function () {
    if (!this._container) {
        this._createFrame();
    }

    this._isVisible = true;

    this._frame.src = this._url;
    this._container.style.display = 'block';

    this._resizeContainer();
};

IframeBrowser.prototype.close = function () {
    if (this._container) {
        this._container.parentNode.removeChild(this._container);
        this._container = null;
        this._frame = null;
        this._isVisible = false;

        this._success(InAppBrowser.Events.EXIT);

        window.removeEventListener('resize', this._resizeCallback);
    }
};

IframeBrowser.prototype.hide = function () {
    if (this._container && this._isVisible) {
        this._container.style.display = 'none';
        this._isVisible = false;
    }
};

IframeBrowser.prototype.injectScriptCode = function (callback, args) {
    if (this._container && this._frame) {
        var code = args[0].replace(/\r?\n|\r/g, ''),
            hasCallback = args[1];

        try {
            var result = this._injectCode(code);
            if (hasCallback) {
                callback(result);
            }
        } catch (error) {
            console.error('Error occured while trying to inject script', error);
        }
    }
};

IframeBrowser.prototype.injectScriptFile = function (callback, args) {
    var file = args[0];
    var code = '(function () {' +
        'var head = document.getElementsByTagName("head")[0];' +
        'var script = document.createElement("script");' +
        'script.type = "text/javascript";' +
        'script.id = "inappbrowser-inject-script";' +
        'script.src = "' + file + '";' +
        'head.appendChild(script);' +
        '}());';

    args[0] = code;

    this.injectScriptCode(callback, args);
};

IframeBrowser.prototype.injectStyleCode = function (callback, args) {
    if (this._container && this._frame) {
        var css = args[0].replace(/\r?\n|\r/g, ''),
            hasCallback = args[1];

        var code = '(function () {' +
            'var head = document.getElementsByTagName("head")[0];' +
            'var style = document.createElement("style");' +
            'style.type = "text/css";' +
            'if (style.styleSheet) {' +
            'style.styleSheet.cssText = "' + css + '";' +
            '} else {' +
            'style.appendChild(document.createTextNode("' + css + '"));}' +
            'head.appendChild(style);' +
            '}());';

        try {
            var result = this._injectCode(code);
            if (hasCallback) {
                callback(result);
            }
        } catch (error) {
            console.error('Error occured while trying to inject style', error);
        }
    }
};

IframeBrowser.prototype.injectStyleFile = function (callback, args) {
    if (this._container && this._frame) {
        var file = args[0],
            hasCallback = args[1];

        var code = '(function () {' +
            'var head = document.getElementsByTagName("head")[0];' +
            'var link = document.createElement("link");' +
            'link.type = "text/css";' +
            'link.rel = "stylesheet";' +
            'link.href = "' + file + '";' +
            'head.appendChild(link);' +
            '}());';

        try {
            var result = this._injectCode(code);
            if (hasCallback) {
                callback(result);
            }
        } catch (error) {
            console.error('Error occured while trying to inject a style file', error);
        }
    }
};

/**
 * @private
 */
IframeBrowser.prototype._createFrame = function () {
    this._container = document.createElement('div');
    this._frame = document.createElement('iframe');

    // container style
    var style = this._container.style;
    style.position = 'absolute';
    style.border = '0';
    style.backgroundColor = '#ffffff';
    style.zIndex = '10000';
    style.width = document.body.clientWidth + 'px';
    style.height = document.body.clientHeight + 'px';

    // iframe style
    style = this._frame.style;
    style.border = '0';
    style.width = '100%';

    if (!this._options.location || this._options.location === 'yes') {
        style.height = 'calc(100% - 35px)';

        this._container.appendChild(this._createNavigationBar());
    } else {
        style.height = '100%';
    }

    this._container.appendChild(this._frame);

    document.body.appendChild(this._container);

    this._frame.addEventListener('pageshow', this._success.bind(this, InAppBrowser.Events.LOAD_START));
    this._frame.addEventListener('load', this._success.bind(this, InAppBrowser.Events.LOAD_STOP));
    this._frame.addEventListener('error', this._success.bind(this, InAppBrowser.Events.LOAD_ERROR));
    this._frame.addEventListener('abort', this._success.bind(this, InAppBrowser.Events.LOAD_ERROR));

    window.addEventListener('resize', this._resizeCallback);
};

/**
 * Creates a fake browser's navigation bar, that only contains a button to remove the iframe.
 * @private
 */
IframeBrowser.prototype._createNavigationBar = function () {
    var navigationDiv = document.createElement('div'),
        closeSpan = document.createElement('span');

    navigationDiv.style.height = '30px';
    navigationDiv.style.padding = '2px';
    navigationDiv.style.backgroundColor = '#404040';

    closeSpan.innerHTML = '✖';
    closeSpan.style.width = '30px';
    closeSpan.style.height = '25px';
    closeSpan.style.color = '#ffffff';
    closeSpan.style.fontSize = '22px';
    closeSpan.style.marginLeft = '5px';
    closeSpan.style.cursor = 'pointer';

    closeSpan.addEventListener('click', function (e) {
        setTimeout(function () {
            this.close();
        }.bind(this), 0);
    }.bind(this));

    navigationDiv.appendChild(closeSpan);

    return navigationDiv;
};

/**
 * Resize the main container with the document.body size, only when it is visible.
 * @private
 */
IframeBrowser.prototype._resizeContainer = function () {
    if (this._container && this._isVisible) {
        var body = document.body;

        this._container.style.width = body.clientWidth + 'px';
        this._container.style.height = body.clientHeight + 'px';
    }
};

/**
 * Execute the given code in the iframe context whenever it's possible. This may
 * throw an error when the URL loaded in the iframe is not in the same domain than
 * the application.
 * @throws It throws an error when the eval call from the iframe fails.
 * @private
 */
IframeBrowser.prototype._injectCode = function (code) {
    var result = this._frame.contentWindow.eval(code);

    return result ? [result] : [];
};

/**
 * Create an instance of InAppBrowser based on the target type. When the type is '_system',
 * an instance of the system browser is created, when the type is '_blank', an instance of
 * a browser based on iframe is created. For any other target type, including '_self',
 * the current window will navigate to the given URL.
 * @param {function} success
 * @param {function} fail
 * @param {Array} args
 * @return {object}
 */
function create(success, fail, args) {
    // args[0]: url, args[1]: target, args[2]: options
    var Constructor;

    switch (args[1]) {
    case '_system':
        // open in a new browser tab
        Constructor = SystemBrowser;
        break;
    case '_blank':
        Constructor = IframeBrowser;
        break;
    default:
        // "_self" and any other option, use the same tab
        window.location = args[0];
        return;
    }

    return new Constructor(args[0], args[2], success, fail);
}

module.exports.create = create;
<|endoftext|>