<commit_before>function Hammer(pixiStage,board)
{
    var self = this;
    this.board = board;

    this.pixiHammer  = new PIXI.Sprite.fromImage(Config.textures.hammer);
    this.pixiTarget = new PIXI.Sprite.fromImage(Config.textures.target);

    var ratio = this.pixiHammer.height / this.pixiHammer.width;
    this.hit = false;

    this.pixiHammer.alpha = 0.5;
    this.pixiHammer.position ={
        x: 370,
        y: 490
    };
    this.pixiHammer.anchor = {
        x: 0.3,
        y: 0.75
    }

    this.pixiTarget.position = { x : 0.5 , y:0.5};
    this.pixiTarget.anchor = { x : 0.5 , y:0.5};

    this.position ={
       x : 0.5,
       y : 0.5
    };

    this.scoreSinceLastHit = 0;
    this.speed = this.originalSpeed = 0.2;

    pixiStage.addChild(this.pixiHammer);
    pixiStage.addChild(this.pixiTarget);

    setInterval(function(){self.activate()}, 4000);
}

Hammer.prototype.update = function(time, dt, score) {
    var mole = this.getTargetedMole()
    if( mole ) {
      if (distance(this.position,mole.molePositions) < 0.03) {
        setTimeout(this.activate.bind(this), 50);
      }
      this.moveToClosestMole(dt, mole)
    }

    var projection = projectVirtualPosition(this.position, Math.sin(time * 2) * 0.04 + 0.04)
    var targetProj = projectVirtualPosition(this.position, 0);

    this.pixiHammer.position = projection.position;
    this.pixiHammer.scale = projection.scale;

    this.pixiTarget.position = targetProj.position;
    this.pixiTarget.scale    = targetProj.scale;

    this.scoreSinceLastHit += score;
    this.speed += this.scoreSinceLastHit / 100000 / 3;
};

function distance(a,b){
    var dx = b.x - a.x
    var dy = b.y - a.y
    return  Math.sqrt(dx * dx + dy * dy);
}

function vec2subtract(a, b)
{
    return  {x: a.x - b.x, y: a.y - b.y}
}

Hammer.prototype.getTargetedMole = function(){
    var self = this;
    var closestMole;
    var hittableMoles = this.board.moles.filter(function(m){
        return m.isHittable();
    });
    if(hittableMoles.length === 0 ) return null;

    initialMole = hittableMoles.pop();

    closestMole = hittableMoles.reduce(function(m,c){
        var dist = distance(self.position,c.molePositions);
        if (m[1]< dist){
           return m;
        }
        else{
            return [c,dist];
        }
    },[initialMole,distance(this.position,initialMole.molePositions)])[0];

    return closestMole;
};

Hammer.prototype.moveToClosestMole = function(dt,nextMole){
    var molePosition = {
      x: nextMole.molePositions.x,
      y: nextMole.molePositions.yRisenPosition,
    }
    var direction = vec2subtract(molePosition, this.position)
    var length = distance(molePosition, this.position)

    var normalizedDirection = {
        x: direction.x / length || 0,
        y: direction.y / length || 0
    }

    var modspeed = function(v){
        return v*Math.random()*3
    };

    this.position.x += normalizedDirection.x * dt * (this.speed + modspeed(this.speed));
    this.position.y += normalizedDirection.y * dt * (this.speed + modspeed(this.speed));
}

Hammer.prototype.activate = function(){
    var self = this;
    this.hit = true;
    this.pixiHammer.alpha = 1;
    var hasHit = this.board.hit( this.position );
    if (hasHit) {
      this.scoreSinceLastHit = 0;
      this.speed = this.originalSpeed;
    }

    setTimeout(
        function(){
          self.hit = false;
          self.pixiHammer.alpha = 0.5;
        },200);
};
<commit_msg>Prevents jumps over the target<commit_after>function Hammer(pixiStage,board)
{
    var self = this;
    this.board = board;

    this.pixiHammer  = new PIXI.Sprite.fromImage(Config.textures.hammer);
    this.pixiTarget = new PIXI.Sprite.fromImage(Config.textures.target);

    var ratio = this.pixiHammer.height / this.pixiHammer.width;
    this.hit = false;

    this.pixiHammer.alpha = 0.5;
    this.pixiHammer.position ={
        x: 370,
        y: 490
    };
    this.pixiHammer.anchor = {
        x: 0.3,
        y: 0.75
    }

    this.pixiTarget.position = { x : 0.5 , y:0.5};
    this.pixiTarget.anchor = { x : 0.5 , y:0.5};

    this.position ={
       x : 0.5,
       y : 0.5
    };

    this.scoreSinceLastHit = 0;
    this.speed = this.originalSpeed = 0.2;

    pixiStage.addChild(this.pixiHammer);
    pixiStage.addChild(this.pixiTarget);

    setInterval(function(){self.activate()}, 4000);
}

Hammer.prototype.update = function(time, dt, score) {
    var mole = this.getTargetedMole()
    if( mole ) {
      if (distance(this.position,mole.molePositions) < 0.03) {
        setTimeout(this.activate.bind(this), 50);
      }
      this.moveToClosestMole(dt, mole)
    }

    var projection = projectVirtualPosition(this.position, Math.sin(time * 2) * 0.04 + 0.04)
    var targetProj = projectVirtualPosition(this.position, 0);

    this.pixiHammer.position = projection.position;
    this.pixiHammer.scale = projection.scale;

    this.pixiTarget.position = targetProj.position;
    this.pixiTarget.scale    = targetProj.scale;

    this.scoreSinceLastHit += score;
    this.speed += this.scoreSinceLastHit / 100000 / 3;
};

function distance(a,b){
    var dx = b.x - a.x
    var dy = b.y - a.y
    return  Math.sqrt(dx * dx + dy * dy);
}

function vec2subtract(a, b)
{
    return  {x: a.x - b.x, y: a.y - b.y}
}

Hammer.prototype.getTargetedMole = function(){
    var self = this;
    var closestMole;
    var hittableMoles = this.board.moles.filter(function(m){
        return m.isHittable();
    });
    if(hittableMoles.length === 0 ) return null;

    initialMole = hittableMoles.pop();

    closestMole = hittableMoles.reduce(function(m,c){
        var dist = distance(self.position,c.molePositions);
        if (m[1]< dist){
           return m;
        }
        else{
            return [c,dist];
        }
    },[initialMole,distance(this.position,initialMole.molePositions)])[0];

    return closestMole;
};

Hammer.prototype.moveToClosestMole = function(dt,nextMole){
    var molePosition = {
      x: nextMole.molePositions.x,
      y: nextMole.molePositions.yRisenPosition,
    }
    var direction = vec2subtract(molePosition, this.position)
    var length = distance(molePosition, this.position)
    length = length > 0.005 ? length : 1; // Prevent jumps over the target
    var normalizedDirection = {
        x: direction.x / length || 0,
        y: direction.y / length || 0
    }

    var modspeed = function(v){
        return v*Math.random()*3
    };

    this.position.x += normalizedDirection.x * dt * (this.speed + modspeed(this.speed));
    this.position.y += normalizedDirection.y * dt * (this.speed + modspeed(this.speed));
}

Hammer.prototype.activate = function(){
    var self = this;
    this.hit = true;
    this.pixiHammer.alpha = 1;
    var hasHit = this.board.hit( this.position );
    if (hasHit) {
      this.scoreSinceLastHit = 0;
      this.speed = this.originalSpeed;
    }

    setTimeout(
        function(){
          self.hit = false;
          self.pixiHammer.alpha = 0.5;
        },200);
};
<|endoftext|>


<commit_before>// Copyright 2013-2017, 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( function( require ) {
  'use strict';

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

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

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

  // constants
  var MINIMUM_HOME_SCREEN_ICON_SIZE = new Dimension2( 548, 373 );
  var MINIMUM_NAVBAR_ICON_SIZE = new Dimension2( 147, 100 );
  var NAVBAR_ICON_ASPECT_RATIO = MINIMUM_NAVBAR_ICON_SIZE.width / MINIMUM_NAVBAR_ICON_SIZE.height;
  var HOME_SCREEN_ICON_ASPECT_RATIO = MINIMUM_HOME_SCREEN_ICON_SIZE.width / MINIMUM_HOME_SCREEN_ICON_SIZE.height;
  var 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 = _.extend( {

      // {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,

      // {Node|null} icon shown in the navigation bar. If null, then the home screen icon will be used, scaled to fit.
      navigationBarIcon: 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 );

    // 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' );

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

    // @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 Property( true, {
      tandem: options.tandem.createTandem( 'activeProperty' ),
      phetioType: PropertyIO( BooleanIO ),
      phetioReadOnly: true,
      phetioInstanceDocumentation: '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
    }

    var self = this;
    assert && this.activeProperty.lazyLink( function( isActive ) {
      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 + ' is too small: ' + icon.width );
    assert && assert( icon.height >= minimumSize.height, name + ' is too small: ' + icon.height );

    // Validate home screen aspect ratio
    var 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}
   */
  var devCreateLayoutBoundsNode = function( layoutBounds ) {
    return new Path( Shape.bounds( layoutBounds ), {
      stroke: 'red',
      lineWidth: 3,
      pickable: false
    } );
  };

  /**
   * For showing ScreenView layoutBounds with 'showVisibleBounds' query parameter.
   * @param {ScreenView} screenView
   * @returns {Node}
   */
  var devCreateVisibleBoundsNode = function( screenView ) {
    var 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.
     */
    initializeView: function( simName ) {
      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 ) );
      }

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

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

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

    // 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>fix single screen headings in PDOM, https://github.com/phetsims/area-model-common/issues/149<commit_after>// Copyright 2013-2017, 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( function( require ) {
  'use strict';

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

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

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

  // constants
  var MINIMUM_HOME_SCREEN_ICON_SIZE = new Dimension2( 548, 373 );
  var MINIMUM_NAVBAR_ICON_SIZE = new Dimension2( 147, 100 );
  var NAVBAR_ICON_ASPECT_RATIO = MINIMUM_NAVBAR_ICON_SIZE.width / MINIMUM_NAVBAR_ICON_SIZE.height;
  var HOME_SCREEN_ICON_ASPECT_RATIO = MINIMUM_HOME_SCREEN_ICON_SIZE.width / MINIMUM_HOME_SCREEN_ICON_SIZE.height;
  var 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 = _.extend( {

      // {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,

      // {Node|null} icon shown in the navigation bar. If null, then the home screen icon will be used, scaled to fit.
      navigationBarIcon: 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 );

    // 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' );

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

    // @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 Property( true, {
      tandem: options.tandem.createTandem( 'activeProperty' ),
      phetioType: PropertyIO( BooleanIO ),
      phetioReadOnly: true,
      phetioInstanceDocumentation: '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
    }

    var self = this;
    assert && this.activeProperty.lazyLink( function( isActive ) {
      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 + ' is too small: ' + icon.width );
    assert && assert( icon.height >= minimumSize.height, name + ' is too small: ' + icon.height );

    // Validate home screen aspect ratio
    var 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}
   */
  var devCreateLayoutBoundsNode = function( layoutBounds ) {
    return new Path( Shape.bounds( layoutBounds ), {
      stroke: 'red',
      lineWidth: 3,
      pickable: false
    } );
  };

  /**
   * For showing ScreenView layoutBounds with 'showVisibleBounds' query parameter.
   * @param {ScreenView} screenView
   * @returns {Node}
   */
  var devCreateVisibleBoundsNode = function( screenView ) {
    var 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.
     */
    initializeView: function( simName ) {
      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 ) );
      }

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

        // Single screen sims don't have screen names.
        if ( !this.name ) {
          this._view.labelContent = simName;
        }
        else {

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

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

    // 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 2015-2019, University of Colorado Boulder

/**
 * Tandem defines a set of trees that are used to assign unique identifiers to PhetioObjects in PhET simulations and
 * notify listeners when the associated PhetioObjects have been added/removed. It is used to support PhET-iO.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const arrayRemove = require( 'PHET_CORE/arrayRemove' );
  const tandemNamespace = require( 'TANDEM/tandemNamespace' );
  const toCamelCase = require( 'PHET_CORE/toCamelCase' );

  // text
  const packageString = require( 'text!REPOSITORY/package.json' );

  // constants
  const packageJSON = JSON.parse( packageString ); // Tandem can't depend on joist, so cannot use packageJSON module
  const PHET_IO_ENABLED = _.hasIn( window, 'phet.phetio' );
  const PRINT_MISSING_TANDEMS = PHET_IO_ENABLED && phet.phetio.queryParameters.phetioPrintMissingTandems;
  const VALIDATE_TANDEMS = PHET_IO_ENABLED && phet.phetio.queryParameters.phetioValidateTandems;

  // used to keep track of missing tandems.  Each element has type {{phetioID:{string}, stack:{string}}
  const missingTandems = {
    required: [],
    optional: []
  };

  // Listeners that will be notified when items are registered/deregistered. See doc in addPhetioObjectListener
  const phetioObjectListeners = [];

  // variables
  // Before listeners are wired up, tandems are buffered.  When listeners are wired up, Tandem.launch() is called and
  // buffered tandems are flushed, then subsequent tandems are delivered to listeners directly
  let launched = false;

  // {PhetioObject[]} - PhetioObjects that have been added before listeners are ready.
  const bufferedPhetioObjects = [];

  class Tandem {

    /**
     * Typically, sims will create tandems using `tandem.createTandem`.  This constructor is used internally or when
     * a tandem must be created from scratch.
     *
     * @param {Tandem|null} parentTandem - parent for a child tandem, or null for a root tandem
     * @param {string} name - component name for this level, like 'resetAllButton'
     * @param {Object} [options]
     */
    constructor( parentTandem, name, options ) {
      assert && assert( parentTandem === null || parentTandem instanceof Tandem, 'parentTandem should be null or Tandem' );
      assert && assert( typeof name === 'string', 'name must be defined' );
      assert && assert( this.getTermRegex().test( name ), `name should match the regex pattern: ${name}` );

      // @public (read-only) {Tandem|null}
      this.parentTandem = parentTandem;

      // @public (read-only) - the last part of the tandem (after the last .), used e.g., in Joist for creating button
      // names dynamically based on screen names
      this.name = name;

      // @public (read-only)
      this.phetioID = this.parentTandem ? phetio.PhetioIDUtils.append( this.parentTandem.phetioID, this.name )
                                        : this.name;

      // options (even subtype options) must be stored so they can be passed through to children
      // Note: Make sure that added options here are also added to options for inheritance and/or for composition
      // (createTandem/parentTandem/getExtendedOptions) as appropriate.
      options = _.extend( {

        // required === false means it is an optional tandem
        required: true,

        // if the tandem is required but not supplied, an error will be thrown.
        supplied: true
      }, options );

      // @private
      this.required = options.required;

      // @public (read-only)
      this.supplied = options.supplied;
    }

    /**
     * Returns the regular expression which can be used to test each term. The term must consist only of alpha-numeric
     * characters or tildes.
     * @returns {RegExp}
     * @protected
     */
    getTermRegex() {
      return /^[a-zA-Z0-9~]+$/; // TODO: eliminate ~ once GroupTandem has been deleted, see https://github.com/phetsims/tandem/issues/87
    }

    /**
     * Adds a PhetioObject.  For example, it could be an axon Property, SCENERY/Node or SUN/RoundPushButton.
     * phetioEngine listens for when PhetioObjects are added and removed to keep track of them for PhET-iO.
     * @param {PhetioObject} phetioObject
     * @public
     */
    addPhetioObject( phetioObject ) {
      assert && assert( arguments.length === 1, 'Tandem.addPhetioObject takes one argument' );

      // Cannot use typical require statement for PhetioObject because it creates a module loading loop
      assert && assert( phetioObject instanceof tandemNamespace.PhetioObject, 'argument should be of type PhetioObject' );

      if ( PHET_IO_ENABLED ) {

        // Throw an error if the tandem is required but not supplied
        if ( Tandem.errorOnFailedValidation() ) {
          assert && assert( !( this.required && !this.supplied ), 'Tandem was required but not supplied' );
        }

        // When the query parameter phetioPrintMissingTandems is true, report tandems that are required but not supplied
        if ( PRINT_MISSING_TANDEMS && ( this.required && !this.supplied ) ) {
          missingTandems.required.push( { phetioID: this.phetioID, stack: new Error().stack } );
          return;
        }

        // If tandem is optional and not supplied, then ignore it.
        if ( !this.required && !this.supplied ) {
          if ( PRINT_MISSING_TANDEMS ) {
            const stackTrace = new Error().stack;

            // When the query parameter phetioPrintMissingTandems is true, report tandems that are optional but not
            // supplied, but not for Fonts because they are too numerous.
            if ( stackTrace.indexOf( 'Font' ) === -1 ) {
              missingTandems.optional.push( { phetioID: this.phetioID, stack: stackTrace } );
              return;
            }
          }

          // Optionally instrumented types without tandems are not added.
          return;
        }

        if ( !launched ) {
          bufferedPhetioObjects.push( phetioObject );
        }
        else {
          for ( let i = 0; i < phetioObjectListeners.length; i++ ) {
            phetioObjectListeners[ i ].addPhetioObject( phetioObject );
          }
        }
      }
    }

    /**
     * Removes a PhetioObject and signifies to listeners that it has been removed.
     * @param {PhetioObject} phetioObject
     * @public
     */
    removePhetioObject( phetioObject ) {

      if ( !this.required && !this.supplied ) {
        return;
      }

      // Only active when running as phet-io
      if ( PHET_IO_ENABLED ) {
        if ( !launched ) {
          assert && assert( bufferedPhetioObjects.indexOf( phetioObject ) >= 0, 'should contain item' );
          arrayRemove( bufferedPhetioObjects, phetioObject );
        }
        else {
          for ( let i = 0; i < phetioObjectListeners.length; i++ ) {
            phetioObjectListeners[ i ].removePhetioObject( phetioObject );
          }
        }
      }
    }

    /**
     * Used for creating new tandems, extends this Tandem's options with the passed-in options.
     * @param {Object} [options]
     * @returns {Object} -extended options
     * @public
     */
    getExtendedOptions( options ) {

      // Any child of something should be passed all inherited options. Make sure that this extend call includes all
      // that make sense from the constructor's extend call.
      return _.extend( {
        supplied: this.supplied,
        required: this.required
      }, options );
    }

    /**
     * Create a new Tandem by appending the given id
     * @param {string} name
     * @param {Object} [options]
     * @returns {Tandem}
     * @public
     */
    createTandem( name, options ) {
      return new Tandem( this, name, this.getExtendedOptions( options ) );
    }

    /**
     * For API validation, each PhetioObject has a corresponding concrete PhetioObject for comparison. Non-dynamic
     * PhetioObjects have the trivial case where its concrete phetioID is the same as its phetioID.
     *
     * @returns {string}
     * @public
     */
    getConcretePhetioID() {

      // Dynamic elements always have a parent container, hence since this does not have a parent, it must already be concrete
      return this.parentTandem ? phetio.PhetioIDUtils.append( this.parentTandem.getConcretePhetioID(), this.name ) : this.phetioID;
    }

    /**
     * Creates a group tandem for creating multiple indexed child tandems, such as:
     * sim.screen.model.electrons~0
     * sim.screen.model.electrons~1
     *
     * In this case, 'sim.screen.model.electron' is the string passed to createGroupTandem.
     *
     * Used for arrays, observable arrays, or when many elements of the same type are created and they do not otherwise
     * have unique identifiers.
     * @param {string} name
     * @returns {GroupTandem}
     * @deprecated - use GroupMemberTandem instead
     * @public
     */
    createGroupTandem( name ) {
      return new GroupTandem( this, name );
    }

    /**
     * Adds a listener that will be notified when items are registered/deregistered
     * Listeners have the form
     * {
     *   addPhetioObject(id,phetioObject),
     *   removePhetioObject(id,phetioObject)
     * }
     * where id is of type {string} and phetioObject is of type {PhetioObject}
     *
     * @param {Object} phetioObjectListener - described above
     * @public
     * @static
     */
    static addPhetioObjectListener( phetioObjectListener ) {
      phetioObjectListeners.push( phetioObjectListener );
    }

    /**
     * After all listeners have been added, then Tandem can be launched.  This registers all of the buffered PhetioObjects
     * and subsequent PhetioObjects will be registered directly.
     * @public
     * @static
     */
    static launch() {
      assert && assert( !launched, 'Tandem cannot be launched twice' );
      launched = true;
      while ( bufferedPhetioObjects.length > 0 ) {
        const phetioObject = bufferedPhetioObjects.shift();
        phetioObject.tandem.addPhetioObject( phetioObject );
      }
      assert && assert( bufferedPhetioObjects.length === 0, 'bufferedPhetioObjects should be empty' );
    }

    /**
     * Determine whether or not tandem validation failures should throw errors. If we are printing the missing tandems,
     * then no error should be thrown so that all problems are printed.
     * @returns {boolean} If tandems are being validated or not.
     * @public
     * @static
     */
    static errorOnFailedValidation() {
      return VALIDATE_TANDEMS && !PRINT_MISSING_TANDEMS;
    }

    /**
     * Given a phetioID, recursively create the Tandem structure needed to return a {Tandem} with the given phetioID.
     * This method is mostly to support a deprecated way of handling groups and state, please see @zepumph or @samreid
     * before using.
     * @deprecated
     * @param {string} phetioID
     * @returns {Tandem}
     * @public
     */
    static createFromPhetioID( phetioID ) {
      return phetioID.split( '.' ).reduce( ( tandem, nextComponent ) => {

        // first call case where tandem starts as the first string in the list
        if ( typeof tandem === 'string' ) {
          tandem = new Tandem( null, tandem );
        }
        return tandem.createTandem( nextComponent );
      } );
    }
  }

  // The next few statics are created outside the static block because they instantiate Tandem instances.

  /**
   * The root tandem for a simulation
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.rootTandem = new Tandem( null, toCamelCase( packageJSON.name ) );

  /**
   * Many simulation elements are nested under "general". This tandem is for elements that exists in all sims. For a
   * place to put simulation specific globals, see `Tandem.globalTandem`
   *
   * Hard coded "or" string to support backwards compatibility with the sonification wrapper, see https://github.com/phetsims/phet-io-wrapper-sonification/issues/84
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.generalTandem = Tandem.rootTandem.createTandem( phetio.PhetioIDUtils.GENERAL_COMPONENT_NAME || 'general' );

  /**
   * Simulation elements that don't belong in screens should be nested under "global". Note that this tandem should only
   * have simulation specific elements in them. Instrument items used by all sims under `Tandem.generalTandem`.
   *
   * Hard coded "or" string to support backwards compatibility with the sonification wrapper, see https://github.com/phetsims/phet-io-wrapper-sonification/issues/84
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.globalTandem = Tandem.rootTandem.createTandem( phetio.PhetioIDUtils.GLOBAL_COMPONENT_NAME || 'global' );

  /**
   * Used to indicate a common code component that supports tandem, but doesn't not require it.  If a tandem is not
   * passed in, then it will not be instrumented.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.optional = Tandem.rootTandem.createTandem( 'optionalTandem', {
    required: false,
    supplied: false
  } );

  /**
   * To be used exclusively to opt out of situations where a tandem is required.
   * See https://github.com/phetsims/tandem/issues/97.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.optOut = Tandem.optional;

  /**
   * Some common code (such as Checkbox or RadioButton) must always be instrumented.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.required = Tandem.rootTandem.createTandem( 'requiredTandem', {

    // let phetioPrintMissingTandems bypass this
    required: VALIDATE_TANDEMS || PRINT_MISSING_TANDEMS,
    supplied: false
  } );

  /**
   * Expose collected missing tandems only populated from specific query parameter, see phetioPrintMissingTandems
   * @public (phet-io internal)
   * @type {Object}
   */
  Tandem.missingTandems = missingTandems;

  /**
   * If PhET-iO is enabled in this runtime.
   * @public
   * @type {boolean}
   */
  Tandem.PHET_IO_ENABLED = PHET_IO_ENABLED;

  /**
   * Group Tandem -- Declared in the same file to avoid circular reference errors in module loading.
   * TODO: Replace GroupTandem usages with GroupMemberTandem, see https://github.com/phetsims/tandem/issues/87 and https://github.com/phetsims/phet-io/issues/1409
   */
  class GroupTandem extends Tandem {

    /**
     * @param {Tandem} parentTandem
     * @param {string} name
     * @constructor
     * @deprecated - see Group.js for the way of the future
     * @private create with Tandem.createGroupTandem
     */
    constructor( parentTandem, name ) {
      super( parentTandem, name );

      // @private for generating indices from a pool
      this.groupElementIndex = 0;
    }

    /**
     * Creates the next tandem in the group.
     * @returns {Tandem}
     * @public
     */
    createNextTandem() {
      return Tandem.createFromPhetioID( this.phetioID + '~' + ( this.groupElementIndex++ ) );
    }
  }

  return tandemNamespace.register( 'Tandem', Tandem );
} );<commit_msg>update docs, https://github.com/phetsims/phet-io/issues/1546<commit_after>// Copyright 2015-2019, University of Colorado Boulder

/**
 * Tandem defines a set of trees that are used to assign unique identifiers to PhetioObjects in PhET simulations and
 * notify listeners when the associated PhetioObjects have been added/removed. It is used to support PhET-iO.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const arrayRemove = require( 'PHET_CORE/arrayRemove' );
  const tandemNamespace = require( 'TANDEM/tandemNamespace' );
  const toCamelCase = require( 'PHET_CORE/toCamelCase' );

  // text
  const packageString = require( 'text!REPOSITORY/package.json' );

  // constants
  const packageJSON = JSON.parse( packageString ); // Tandem can't depend on joist, so cannot use packageJSON module
  const PHET_IO_ENABLED = _.hasIn( window, 'phet.phetio' );
  const PRINT_MISSING_TANDEMS = PHET_IO_ENABLED && phet.phetio.queryParameters.phetioPrintMissingTandems;
  const VALIDATE_TANDEMS = PHET_IO_ENABLED && phet.phetio.queryParameters.phetioValidateTandems;

  // used to keep track of missing tandems.  Each element has type {{phetioID:{string}, stack:{string}}
  const missingTandems = {
    required: [],
    optional: []
  };

  // Listeners that will be notified when items are registered/deregistered. See doc in addPhetioObjectListener
  const phetioObjectListeners = [];

  // variables
  // Before listeners are wired up, tandems are buffered.  When listeners are wired up, Tandem.launch() is called and
  // buffered tandems are flushed, then subsequent tandems are delivered to listeners directly
  let launched = false;

  // {PhetioObject[]} - PhetioObjects that have been added before listeners are ready.
  const bufferedPhetioObjects = [];

  class Tandem {

    /**
     * Typically, sims will create tandems using `tandem.createTandem`.  This constructor is used internally or when
     * a tandem must be created from scratch.
     *
     * @param {Tandem|null} parentTandem - parent for a child tandem, or null for a root tandem
     * @param {string} name - component name for this level, like 'resetAllButton'
     * @param {Object} [options]
     */
    constructor( parentTandem, name, options ) {
      assert && assert( parentTandem === null || parentTandem instanceof Tandem, 'parentTandem should be null or Tandem' );
      assert && assert( typeof name === 'string', 'name must be defined' );
      assert && assert( this.getTermRegex().test( name ), `name should match the regex pattern: ${name}` );

      // @public (read-only) {Tandem|null}
      this.parentTandem = parentTandem;

      // @public (read-only) - the last part of the tandem (after the last .), used e.g., in Joist for creating button
      // names dynamically based on screen names
      this.name = name;

      // @public (read-only)
      this.phetioID = this.parentTandem ? phetio.PhetioIDUtils.append( this.parentTandem.phetioID, this.name )
                                        : this.name;

      // options (even subtype options) must be stored so they can be passed through to children
      // Note: Make sure that added options here are also added to options for inheritance and/or for composition
      // (createTandem/parentTandem/getExtendedOptions) as appropriate.
      options = _.extend( {

        // required === false means it is an optional tandem
        required: true,

        // if the tandem is required but not supplied, an error will be thrown.
        supplied: true
      }, options );

      // @private
      this.required = options.required;

      // @public (read-only)
      this.supplied = options.supplied;
    }

    /**
     * Returns the regular expression which can be used to test each term. The term must consist only of alpha-numeric
     * characters or tildes.
     * @returns {RegExp}
     * @protected
     */
    getTermRegex() {
      return /^[a-zA-Z0-9~]+$/; // TODO: eliminate ~ once GroupTandem has been deleted, see https://github.com/phetsims/tandem/issues/87
    }

    /**
     * Adds a PhetioObject.  For example, it could be an axon Property, SCENERY/Node or SUN/RoundPushButton.
     * phetioEngine listens for when PhetioObjects are added and removed to keep track of them for PhET-iO.
     * @param {PhetioObject} phetioObject
     * @public
     */
    addPhetioObject( phetioObject ) {
      assert && assert( arguments.length === 1, 'Tandem.addPhetioObject takes one argument' );

      // Cannot use typical require statement for PhetioObject because it creates a module loading loop
      assert && assert( phetioObject instanceof tandemNamespace.PhetioObject, 'argument should be of type PhetioObject' );

      if ( PHET_IO_ENABLED ) {

        // Throw an error if the tandem is required but not supplied
        if ( Tandem.errorOnFailedValidation() ) {
          assert && assert( !( this.required && !this.supplied ), 'Tandem was required but not supplied' );
        }

        // When the query parameter phetioPrintMissingTandems is true, report tandems that are required but not supplied
        if ( PRINT_MISSING_TANDEMS && ( this.required && !this.supplied ) ) {
          missingTandems.required.push( { phetioID: this.phetioID, stack: new Error().stack } );
          return;
        }

        // If tandem is optional and not supplied, then ignore it.
        if ( !this.required && !this.supplied ) {
          if ( PRINT_MISSING_TANDEMS ) {
            const stackTrace = new Error().stack;

            // When the query parameter phetioPrintMissingTandems is true, report tandems that are optional but not
            // supplied, but not for Fonts because they are too numerous.
            if ( stackTrace.indexOf( 'Font' ) === -1 ) {
              missingTandems.optional.push( { phetioID: this.phetioID, stack: stackTrace } );
              return;
            }
          }

          // Optionally instrumented types without tandems are not added.
          return;
        }

        if ( !launched ) {
          bufferedPhetioObjects.push( phetioObject );
        }
        else {
          for ( let i = 0; i < phetioObjectListeners.length; i++ ) {
            phetioObjectListeners[ i ].addPhetioObject( phetioObject );
          }
        }
      }
    }

    /**
     * Removes a PhetioObject and signifies to listeners that it has been removed.
     * @param {PhetioObject} phetioObject
     * @public
     */
    removePhetioObject( phetioObject ) {

      if ( !this.required && !this.supplied ) {
        return;
      }

      // Only active when running as phet-io
      if ( PHET_IO_ENABLED ) {
        if ( !launched ) {
          assert && assert( bufferedPhetioObjects.indexOf( phetioObject ) >= 0, 'should contain item' );
          arrayRemove( bufferedPhetioObjects, phetioObject );
        }
        else {
          for ( let i = 0; i < phetioObjectListeners.length; i++ ) {
            phetioObjectListeners[ i ].removePhetioObject( phetioObject );
          }
        }
      }
    }

    /**
     * Used for creating new tandems, extends this Tandem's options with the passed-in options.
     * @param {Object} [options]
     * @returns {Object} -extended options
     * @public
     */
    getExtendedOptions( options ) {

      // Any child of something should be passed all inherited options. Make sure that this extend call includes all
      // that make sense from the constructor's extend call.
      return _.extend( {
        supplied: this.supplied,
        required: this.required
      }, options );
    }

    /**
     * Create a new Tandem by appending the given id
     * @param {string} name
     * @param {Object} [options]
     * @returns {Tandem}
     * @public
     */
    createTandem( name, options ) {
      return new Tandem( this, name, this.getExtendedOptions( options ) );
    }

    /**
     * For API validation, each PhetioObject has a corresponding concrete PhetioObject for comparison. Non-dynamic
     * PhetioObjects have the trivial case where its concrete phetioID is the same as its phetioID.
     *
     * @returns {string}
     * @public
     */
    getConcretePhetioID() {

      // Dynamic elements always have a parent container, hence since this does not have a parent, it must already be concrete
      return this.parentTandem ? phetio.PhetioIDUtils.append( this.parentTandem.getConcretePhetioID(), this.name ) : this.phetioID;
    }

    /**
     * Creates a group tandem for creating multiple indexed child tandems, such as:
     * sim.screen.model.electrons~0
     * sim.screen.model.electrons~1
     *
     * In this case, 'sim.screen.model.electron' is the string passed to createGroupTandem.
     *
     * Used for arrays, observable arrays, or when many elements of the same type are created and they do not otherwise
     * have unique identifiers.
     * @param {string} name
     * @returns {GroupTandem}
     * @deprecated - use Group instead
     * @public
     */
    createGroupTandem( name ) {
      return new GroupTandem( this, name );
    }

    /**
     * Adds a listener that will be notified when items are registered/deregistered
     * Listeners have the form
     * {
     *   addPhetioObject(id,phetioObject),
     *   removePhetioObject(id,phetioObject)
     * }
     * where id is of type {string} and phetioObject is of type {PhetioObject}
     *
     * @param {Object} phetioObjectListener - described above
     * @public
     * @static
     */
    static addPhetioObjectListener( phetioObjectListener ) {
      phetioObjectListeners.push( phetioObjectListener );
    }

    /**
     * After all listeners have been added, then Tandem can be launched.  This registers all of the buffered PhetioObjects
     * and subsequent PhetioObjects will be registered directly.
     * @public
     * @static
     */
    static launch() {
      assert && assert( !launched, 'Tandem cannot be launched twice' );
      launched = true;
      while ( bufferedPhetioObjects.length > 0 ) {
        const phetioObject = bufferedPhetioObjects.shift();
        phetioObject.tandem.addPhetioObject( phetioObject );
      }
      assert && assert( bufferedPhetioObjects.length === 0, 'bufferedPhetioObjects should be empty' );
    }

    /**
     * Determine whether or not tandem validation failures should throw errors. If we are printing the missing tandems,
     * then no error should be thrown so that all problems are printed.
     * @returns {boolean} If tandems are being validated or not.
     * @public
     * @static
     */
    static errorOnFailedValidation() {
      return VALIDATE_TANDEMS && !PRINT_MISSING_TANDEMS;
    }

    /**
     * Given a phetioID, recursively create the Tandem structure needed to return a {Tandem} with the given phetioID.
     * This method is mostly to support a deprecated way of handling groups and state, please see @zepumph or @samreid
     * before using.
     * @deprecated
     * @param {string} phetioID
     * @returns {Tandem}
     * @public
     */
    static createFromPhetioID( phetioID ) {
      return phetioID.split( '.' ).reduce( ( tandem, nextComponent ) => {

        // first call case where tandem starts as the first string in the list
        if ( typeof tandem === 'string' ) {
          tandem = new Tandem( null, tandem );
        }
        return tandem.createTandem( nextComponent );
      } );
    }
  }

  // The next few statics are created outside the static block because they instantiate Tandem instances.

  /**
   * The root tandem for a simulation
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.rootTandem = new Tandem( null, toCamelCase( packageJSON.name ) );

  /**
   * Many simulation elements are nested under "general". This tandem is for elements that exists in all sims. For a
   * place to put simulation specific globals, see `Tandem.globalTandem`
   *
   * Hard coded "or" string to support backwards compatibility with the sonification wrapper, see https://github.com/phetsims/phet-io-wrapper-sonification/issues/84
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.generalTandem = Tandem.rootTandem.createTandem( phetio.PhetioIDUtils.GENERAL_COMPONENT_NAME || 'general' );

  /**
   * Simulation elements that don't belong in screens should be nested under "global". Note that this tandem should only
   * have simulation specific elements in them. Instrument items used by all sims under `Tandem.generalTandem`.
   *
   * Hard coded "or" string to support backwards compatibility with the sonification wrapper, see https://github.com/phetsims/phet-io-wrapper-sonification/issues/84
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.globalTandem = Tandem.rootTandem.createTandem( phetio.PhetioIDUtils.GLOBAL_COMPONENT_NAME || 'global' );

  /**
   * Used to indicate a common code component that supports tandem, but doesn't not require it.  If a tandem is not
   * passed in, then it will not be instrumented.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.optional = Tandem.rootTandem.createTandem( 'optionalTandem', {
    required: false,
    supplied: false
  } );

  /**
   * To be used exclusively to opt out of situations where a tandem is required.
   * See https://github.com/phetsims/tandem/issues/97.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.optOut = Tandem.optional;

  /**
   * Some common code (such as Checkbox or RadioButton) must always be instrumented.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.required = Tandem.rootTandem.createTandem( 'requiredTandem', {

    // let phetioPrintMissingTandems bypass this
    required: VALIDATE_TANDEMS || PRINT_MISSING_TANDEMS,
    supplied: false
  } );

  /**
   * Expose collected missing tandems only populated from specific query parameter, see phetioPrintMissingTandems
   * @public (phet-io internal)
   * @type {Object}
   */
  Tandem.missingTandems = missingTandems;

  /**
   * If PhET-iO is enabled in this runtime.
   * @public
   * @type {boolean}
   */
  Tandem.PHET_IO_ENABLED = PHET_IO_ENABLED;

  /**
   * Group Tandem -- Declared in the same file to avoid circular reference errors in module loading.
   * TODO: Replace GroupTandem usages with GroupMemberTandem, see https://github.com/phetsims/tandem/issues/87 and https://github.com/phetsims/phet-io/issues/1409
   */
  class GroupTandem extends Tandem {

    /**
     * @param {Tandem} parentTandem
     * @param {string} name
     * @constructor
     * @deprecated - see Group.js for the way of the future
     * @private create with Tandem.createGroupTandem
     */
    constructor( parentTandem, name ) {
      super( parentTandem, name );

      // @private for generating indices from a pool
      this.groupElementIndex = 0;
    }

    /**
     * Creates the next tandem in the group.
     * @returns {Tandem}
     * @public
     */
    createNextTandem() {
      return Tandem.createFromPhetioID( this.phetioID + '~' + ( this.groupElementIndex++ ) );
    }
  }

  return tandemNamespace.register( 'Tandem', Tandem );
} );<|endoftext|>


<commit_before>(function () {
    function getParentByTagName(el, tagName) {
        var p = el.parentElement;

        if (p.tagName === tagName.toUpperCase()) {
            return p;
        } else {
            return getParentByTagName(p, tagName);
        }
    }

    var uploads = 0;
    function disableSubmit(el, status) {
        if (status) uploads++;
        else uploads--;

        var form = getParentByTagName(el, 'form');
        var submitBtn = form.querySelector('[type="submit"');

        submitBtn.disabled = (uploads > 0);
    }

    function error(el, msg) {
        el.querySelector('.file-url').value = '';
        el.querySelector('.file-input').value = '';

        if (errs = el.querySelector('.errors')) { errs.remove(); }

        var errorList = document.createElement('ul');
        errorList.setAttribute('class', 'errors');
        var errorMessage = document.createElement('li');
        errorMessage.appendChild(document.createTextNode(msg))
        errorList.appendChild(errorMessage);

        el.insertBefore(errorList, el.firstChild);
    }

    function update(el, fileUrl) {
        var link = el.querySelector('.file-link'),
            url = el.querySelector('.file-url');

        if (errs = el.querySelector('.errors')) { errs.remove(); }

        url.value = fileUrl;
        link.href = fileUrl;
        link.innerHTML = fileUrl.split('/').pop();

        el.classList.add('uploaded');
    }

    function getCookie(name) {
        var value = '; ' + document.cookie,
            parts = value.split('; ' + name + '=')
        if (parts.length == 2) return parts.pop().split(';').shift()
    }

    function request(method, url, data, headers, el, callback) {
        var req = new XMLHttpRequest();
        req.open(method, url, true);

        Object.keys(headers).forEach(function(key){
            req.setRequestHeader(key, headers[key])
        });

        req.onload = function() {
            disableSubmit(el, false)
            callback(req.status, req.responseText);
        }

        req.onerror = req.onabort = function() {
            disableSubmit(el, false)
            error(el, 'Not able to upload file');
        }

        req.send(data)
    }

    function uploadFile(e, data) {
        var el = e.target.parentElement,
            file = el.querySelector('.file-input').files[0],
            formData = new FormData(),
            headers  = {'X-CSRFToken': getCookie('csrftoken')};

        var url = data.url;
        Object.keys(data.fields).forEach(function(key){
            formData.append(key, data.fields[key])
        })
        formData.append('file', file);

        request('POST', url, formData, headers, el, function(status, xml) {
            if (status !== 204) {
                error(el, 'Not able to upload file')
            } else {
                var fileUrl = data.url + '/' + el.getAttribute('data-upload-to') + '/' + file.name;
                update(el, fileUrl);
            }
        });
    }

    function getSignedUrl(e) {
        var el = e.target.parentElement,
            file = el.querySelector('.file-input').files[0],
            form = new FormData(),
            headers  = {'X-CSRFToken': getCookie('csrftoken')},
            url = '/s3/signed-url/';

        disableSubmit(el, true);

        form.append('key', el.getAttribute('data-upload-to') + '/' + file.name);

        request('POST', url, form, headers, el, function(status, response) {
            if (status !== 200) {
                error(el, 'Not able to upload file')
            } else {
                uploadFile(e, JSON.parse(response));
            }
        });
    }

    function removeFile(e) {
        e.preventDefault();

        var el = e.target.parentElement.parentElement;
        el.querySelector('.file-url').value = '';
        el.querySelector('.file-input').value = '';
        el.classList.remove('uploaded');
    }

    function addEventHandlers(el) {
        var input = el.querySelector('.file-input'),
            remove = el.querySelector('.file-remove');

        input.addEventListener('change', getSignedUrl, false);
        remove.addEventListener('click', removeFile, false);
    }

    document.addEventListener('DOMContentLoaded', function(e) {
        ;[].forEach.call(document.querySelectorAll('.s3-buckets'), addEventHandlers)
    })

    document.addEventListener('DOMNodeInserted', function(e){
        if(e.target.tagName) {
            var el = e.target.querySelector('.s3-buckets')
            if(el) addEventHandlers(el)
        }
    })
}());
<commit_msg>Fix file key when no upload_to is given<commit_after>(function () {
    function getParentByTagName(el, tagName) {
        var p = el.parentElement;

        if (p.tagName === tagName.toUpperCase()) {
            return p;
        } else {
            return getParentByTagName(p, tagName);
        }
    }

    var uploads = 0;
    function disableSubmit(el, status) {
        if (status) uploads++;
        else uploads--;

        var form = getParentByTagName(el, 'form');
        var submitBtn = form.querySelector('[type="submit"');

        submitBtn.disabled = (uploads > 0);
    }

    function error(el, msg) {
        el.querySelector('.file-url').value = '';
        el.querySelector('.file-input').value = '';

        if (errs = el.querySelector('.errors')) { errs.remove(); }

        var errorList = document.createElement('ul');
        errorList.setAttribute('class', 'errors');
        var errorMessage = document.createElement('li');
        errorMessage.appendChild(document.createTextNode(msg))
        errorList.appendChild(errorMessage);

        el.insertBefore(errorList, el.firstChild);
    }

    function update(el, fileUrl) {
        var link = el.querySelector('.file-link'),
            url = el.querySelector('.file-url');

        if (errs = el.querySelector('.errors')) { errs.remove(); }

        url.value = fileUrl;
        link.href = fileUrl;
        link.innerHTML = fileUrl.split('/').pop();

        el.classList.add('uploaded');
    }

    function getCookie(name) {
        var value = '; ' + document.cookie,
            parts = value.split('; ' + name + '=')
        if (parts.length == 2) return parts.pop().split(';').shift()
    }

    function request(method, url, data, headers, el, callback) {
        var req = new XMLHttpRequest();
        req.open(method, url, true);

        Object.keys(headers).forEach(function(key){
            req.setRequestHeader(key, headers[key])
        });

        req.onload = function() {
            disableSubmit(el, false)
            callback(req.status, req.responseText);
        }

        req.onerror = req.onabort = function() {
            disableSubmit(el, false)
            error(el, 'Not able to upload file');
        }

        req.send(data)
    }

    function uploadFile(e, data) {
        var el = e.target.parentElement,
            file = el.querySelector('.file-input').files[0],
            formData = new FormData(),
            headers  = {'X-CSRFToken': getCookie('csrftoken')};

        var url = data.url;
        Object.keys(data.fields).forEach(function(key){
            formData.append(key, data.fields[key])
        })
        formData.append('file', file);

        request('POST', url, formData, headers, el, function(status, xml) {
            if (status !== 204) {
                error(el, 'Not able to upload file')
            } else {
                var fileUrl = data.url + '/';
                if (el.getAttribute('data-upload-to')) {
                    fileUrl += el.getAttribute('data-upload-to') + '/';
                }
                fileUrl += file.name;

                update(el, fileUrl);
            }
        });
    }

    function getSignedUrl(e) {
        var el = e.target.parentElement,
            file = el.querySelector('.file-input').files[0],
            form = new FormData(),
            headers  = {'X-CSRFToken': getCookie('csrftoken')},
            url = '/s3/signed-url/';

        disableSubmit(el, true);

        var key = file.name;
        if (el.getAttribute('data-upload-to')) {
            key = el.getAttribute('data-upload-to') + '/' + key;
        }

        form.append('key', key);

        request('POST', url, form, headers, el, function(status, response) {
            if (status !== 200) {
                error(el, 'Not able to upload file')
            } else {
                uploadFile(e, JSON.parse(response));
            }
        });
    }

    function removeFile(e) {
        e.preventDefault();

        var el = e.target.parentElement.parentElement;
        el.querySelector('.file-url').value = '';
        el.querySelector('.file-input').value = '';
        el.classList.remove('uploaded');
    }

    function addEventHandlers(el) {
        var input = el.querySelector('.file-input'),
            remove = el.querySelector('.file-remove');

        input.addEventListener('change', getSignedUrl, false);
        remove.addEventListener('click', removeFile, false);
    }

    document.addEventListener('DOMContentLoaded', function(e) {
        ;[].forEach.call(document.querySelectorAll('.s3-buckets'), addEventHandlers)
    })

    document.addEventListener('DOMNodeInserted', function(e){
        if(e.target.tagName) {
            var el = e.target.querySelector('.s3-buckets')
            if(el) addEventHandlers(el)
        }
    })
}());
<|endoftext|>


<commit_before>import flow from 'lodash/flow';
import * as challengeTypes from '../../utils/challengeTypes';
import protect from '../../utils/empty-protector';
import { decodeScriptTags } from '../../../utils/encode-decode';

// determine the component to view for each challenge
export const viewTypes = {
  [ challengeTypes.html ]: 'classic',
  [ challengeTypes.js ]: 'classic',
  [ challengeTypes.bonfire ]: 'classic',
  [ challengeTypes.frontEndProject ]: 'project',
  [ challengeTypes.backEndProject ]: 'project',
  // might not be used anymore
  [ challengeTypes.simpleProject ]: 'project',
  // formally hikes
  [ challengeTypes.video ]: 'video',
  [ challengeTypes.step ]: 'step',
  backend: 'backend'
};

// determine the type of submit function to use for the challenge on completion
export const submitTypes = {
  [ challengeTypes.html ]: 'tests',
  [ challengeTypes.js ]: 'tests',
  [ challengeTypes.bonfire ]: 'tests',
  // requires just a button press
  [ challengeTypes.simpleProject ]: 'project.simple',
  // requires just a single url
  // like codepen.com/my-project
  [ challengeTypes.frontEndProject ]: 'project.frontEnd',
  // requires two urls
  // a hosted URL where the app is running live
  // project code url like GitHub
  [ challengeTypes.backEndProject ]: 'project.backEnd',
  // formally hikes
  [ challengeTypes.video ]: 'video',
  [ challengeTypes.step ]: 'step',
  backend: 'backend'
};

// determines if a line in a challenge description
// has html that should be rendered
export const descriptionRegex = /\<blockquote|\<ol|\<h4|\<table/;

export function arrayToString(seedData = ['']) {
  seedData = Array.isArray(seedData) ? seedData : [seedData];
  return seedData.reduce((seed, line) => '' + seed + line + '\n', '\n');
}

export function buildSeed({ challengeSeed = [] } = {}) {
  return flow(
    arrayToString,
    decodeScriptTags
  )(challengeSeed);
}

const pathsMap = {
  [ challengeTypes.html ]: 'html',
  [ challengeTypes.js ]: 'js',
  [ challengeTypes.bonfire ]: 'js'
};

export function getPreFile({ challengeType }) {
  return {
    name: 'index',
    ext: pathsMap[challengeType] || 'html',
    key: getFileKey({ challengeType })
  };
}

export function getFileKey({ challengeType }) {
  return 'index' + (pathsMap[challengeType] || 'html');
}

export function createTests({ tests = [] }) {
  return tests
    .map(test => {
      if (typeof test === 'string') {
        return {
          text: ('' + test).split('message: ').pop().replace(/\'\);/g, ''),
          testString: test
        };
      }
      return test;
    });
}

function logReplacer(value) {
  if (Array.isArray(value)) {
    const replaced = value.map(logReplacer);
    return '[' + replaced.join(', ') + ']';
  }
  if (typeof value === 'string' && !value.startsWith('//')) {
    return '"' + value + '"';
  }
  if (typeof value === 'number' && isNaN(value)) {
    return value.toString();
  }
  if (typeof value === 'undefined') {
    return 'undefined';
  }
  if (value === null) {
    return 'null';
  }
  if (typeof value === 'function') {
    return value.name;
  }
  if (typeof value === 'object') {
    return JSON.stringify(value, null, 2);
  }

  return value;
}

export function loggerToStr(args) {
  args = Array.isArray(args) ? args : [args];
  return args
    .map(logReplacer)
    .reduce((str, arg) => str + arg + '\n', '');
}

export function getNextChallenge(
  current,
  entities,
  {
    isDev = false,
    skip = 0
  } = {}
) {
  const { challenge: challengeMap, block: blockMap } = entities;
  // find current challenge
  // find current block
  // find next challenge in block
  const currentChallenge = challengeMap[current];
  if (!currentChallenge) {
    return null;
  }
  const block = blockMap[currentChallenge.block];
  const index = block.challenges.indexOf(currentChallenge.dashedName);
  // use next challenge name to find challenge in challenge map
  const nextChallenge = challengeMap[
    // grab next challenge name in current block
    // skip is used to skip isComingSoon challenges
    block.challenges[ index + 1 + skip ]
  ];
  if (
    !isDev &&
    nextChallenge &&
    (nextChallenge.isComingSoon || nextChallenge.isBeta)
  ) {
    // if we find a next challenge and it is a coming soon
    // recur with plus one to skip this challenge
    return getNextChallenge(current, entities, { isDev, skip: skip + 1 });
  }
  return nextChallenge;
}

export function getFirstChallengeOfNextBlock(
  current,
  entities,
  {
    isDev = false,
    skip = 0
  } = {}
) {
  const {
    challenge: challengeMap,
    block: blockMap,
    superBlock: SuperBlockMap
  } = entities;
  const currentChallenge = challengeMap[current];
  if (!currentChallenge) {
    return null;
  }
  const block = blockMap[currentChallenge.block];
  if (!block) {
    return null;
  }
  const superBlock = SuperBlockMap[block.superBlock];
  if (!superBlock) {
    return null;
  }
  // find index of current block
  const index = superBlock.blocks.indexOf(block.dashedName);

  // find next block name
  // and pull block object from block map
  const newBlock = blockMap[
    superBlock.blocks[ index + 1 + skip ]
  ];
  if (!newBlock) {
    return null;
  }
  // grab first challenge from next block
  const nextChallenge = challengeMap[newBlock.challenges[0]];
  if (isDev || !nextChallenge || !nextChallenge.isComingSoon) {
    return nextChallenge;
  }
  // if first challenge is coming soon, find next challenge here
  const nextChallenge2 = getNextChallenge(
    nextChallenge.dashedName,
    entities,
    { isDev }
  );
  if (nextChallenge2) {
    return nextChallenge2;
  }
  // whole block is coming soon
  // skip this block
  return getFirstChallengeOfNextBlock(
    current,
    entities,
    { isDev, skip: skip + 1 }
  );
}

export function getFirstChallengeOfNextSuperBlock(
  current,
  entities,
  superBlocks,
  {
    isDev = false,
    skip = 0
  } = {}
) {
  const {
    challenge: challengeMap,
    block: blockMap,
    superBlock: SuperBlockMap
  } = entities;
  const currentChallenge = challengeMap[current];
  if (!currentChallenge) {
    return null;
  }
  const block = blockMap[currentChallenge.block];
  if (!block) {
    return null;
  }
  const superBlock = SuperBlockMap[block.superBlock];
  if (!superBlock) {
    return null;
  }
  const index = superBlocks.indexOf(superBlock.dashedName);
  const newSuperBlock = SuperBlockMap[superBlocks[ index + 1 + skip]];
  if (!newSuperBlock) {
    return null;
  }
  const newBlock = blockMap[
    newSuperBlock.blocks[ 0 ]
  ];
  if (!newBlock) {
    return null;
  }
  const nextChallenge = challengeMap[newBlock.challenges[0]];
  if (isDev || !nextChallenge || !nextChallenge.isComingSoon) {
    return nextChallenge;
  }
  // coming soon challenge, grab next
  // non coming soon challenge in same block instead
  const nextChallengeInBlock = getNextChallenge(
    nextChallenge.dashedName,
    entities,
    { isDev }
  );
  if (nextChallengeInBlock) {
    return nextChallengeInBlock;
  }
  // whole block is coming soon
  // grab first challenge in next block in newSuperBlock instead
  const challengeInNextBlock = getFirstChallengeOfNextBlock(
    nextChallenge.dashedName,
    entities,
    { isDev }
  );

  if (challengeInNextBlock) {
    return challengeInNextBlock;
  }
  // whole super block is coming soon
  // skip this super block
  return getFirstChallengeOfNextSuperBlock(
    current,
    entities,
    superBlocks,
    { isDev, skip: skip + 1 }
  );
}

export function getCurrentBlockName(current, entities) {
  const { challenge: challengeMap } = entities;
  const challenge = challengeMap[current];
  return challenge.block;
}

export function getCurrentSuperBlockName(current, entities) {
  const { challenge: challengeMap, block: blockMap } = entities;
  const challenge = challengeMap[current];
  const block = blockMap[challenge.block];
  return block.superBlock;
}

// gets new mouse position
// getMouse(
//   e: MouseEvent|TouchEvent,
//   [ dx: Number, dy: Number ]
// ) => [ Number, Number ]
export function getMouse(e, [dx, dy]) {
  let { pageX, pageY, touches, changedTouches } = e;

  // touches can be empty on touchend
  if (touches || changedTouches) {
    e.preventDefault();
    // these re-assigns the values of pageX, pageY from touches
    ({ pageX, pageY } = touches[0] || changedTouches[0]);
  }

  return [pageX - dx, pageY - dy];
}

export function filterComingSoonBetaChallenge(
  isDev = false,
  { isComingSoon, isBeta }
) {
  return !(isComingSoon || isBeta) ||
    isDev;
}

export function filterComingSoonBetaFromEntities(
  { challenge: challengeMap, ...rest },
  isDev = false
) {
  const filter = filterComingSoonBetaChallenge.bind(null, isDev);
  return {
    ...rest,
    challenge: Object.keys(challengeMap)
      .map(dashedName => challengeMap[dashedName])
      .filter(filter)
      .reduce((challengeMap, challenge) => {
        challengeMap[challenge.dashedName] = challenge;
        return challengeMap;
      }, {})
  };
}

export function searchableChallengeTitles({ challenge: challengeMap } = {}) {
  return Object.keys(challengeMap)
    .map(dashedName => challengeMap[dashedName])
    .reduce((accu, current) => {
        accu[current.dashedName] = current.title;
        return accu;
      }
    , {});
}

// interface Node {
//   isHidden: Boolean,
//   children: Void|[ ...Node ],
//   isOpen?: Boolean
// }
//
// interface MapUi
// {
//   children: [...{
//     name: (superBlock: String),
//     isOpen: Boolean,
//     isHidden: Boolean,
//     children: [...{
//       name: (blockName: String),
//       isOpen: Boolean,
//       isHidden: Boolean,
//       children: [...{
//         name: (challengeName: String),
//         isHidden: Boolean
//       }]
//     }]
//   }]
// }
export function createMapUi(
  { superBlock: superBlockMap, block: blockMap } = {},
  superBlocks,
  searchNameMap
) {
  if (!superBlocks || !superBlockMap || !blockMap) {
    return {};
  }
  return {
    children: superBlocks.map(superBlock => {
      return {
        name: superBlock,
        isOpen: true,
        isHidden: false,
        children: protect(superBlockMap[superBlock]).blocks.map(block => {
          return {
            name: block,
            isOpen: true,
            isHidden: false,
            children: protect(blockMap[block]).challenges.map(challenge => {
              return {
                name: challenge,
                title: searchNameMap[challenge],
                isHidden: false,
                children: null
              };
            })
          };
        })
      };
    })
  };
}

// synchronise
// traverseMapUi(
//   tree: MapUi|Node,
//   update: ((MapUi|Node) => MapUi|Node)
// ) => MapUi|Node
export function traverseMapUi(tree, update) {
  let childrenChanged;
  if (!Array.isArray(tree.children)) {
    return update(tree);
  }
  const newChildren = tree.children.map(node => {
    const newNode = traverseMapUi(node, update);
    if (!childrenChanged && newNode !== node) {
      childrenChanged = true;
    }
    return newNode;
  });
  if (childrenChanged) {
    tree = {
      ...tree,
      children: newChildren
    };
  }
  return update(tree);
}

// synchronise
// getNode(tree: MapUi, name: String) => MapUi
export function getNode(tree, name) {
  let node;
  traverseMapUi(tree, thisNode => {
    if (thisNode.name === name) {
      node = thisNode;
    }
    return thisNode;
  });
  return node;
}

// synchronise
// updateSingelNode(
//   tree: MapUi,
//   name: String,
//   update(MapUi|Node) => MapUi|Node
// ) => MapUi
export function updateSingleNode(tree, name, update) {
  return traverseMapUi(tree, node => {
    if (name !== node.name) {
      return node;
    }
    return update(node);
  });
}

// synchronise
// toggleThisPanel(tree: MapUi, name: String) => MapUi
export function toggleThisPanel(tree, name) {
  return updateSingleNode(tree, name, node => {
    return {
      ...node,
      isOpen: !node.isOpen
    };
  });
}

// toggleAllPanels(tree: MapUi, isOpen: Boolean = false ) => MapUi
export function toggleAllPanels(tree, isOpen = false) {
  return traverseMapUi(tree, node => {
    if (!Array.isArray(node.children) || node.isOpen === isOpen) {
      return node;
    }
    return {
      ...node,
      isOpen
    };
  });
}

// collapseAllPanels(tree: MapUi) => MapUi
export function collapseAllPanels(tree) {
  return toggleAllPanels(tree);
}

// expandAllPanels(tree: MapUi) => MapUi
export function expandAllPanels(tree) {
  return toggleAllPanels(tree, true);
}

// applyFilterToMap(tree: MapUi, filterRegex: RegExp) => MapUi
export function applyFilterToMap(tree, filterRegex) {
  return traverseMapUi(
    tree,
    node => {
      // no children indicates a challenge node
      // if leaf (challenge) then test if regex is a match
      if (!Array.isArray(node.children)) {
        // does challenge name meet filter criteria?
        if (filterRegex.test(node.title)) {
          // is challenge currently hidden?
          if (node.isHidden) {
            // unhide challenge, it matches
            return {
              ...node,
              isHidden: false
            };
          }
        } else if (!node.isHidden) {
          return {
            ...node,
            isHidden: true
          };
        }
        return node;
      }
      // if not leaf node (challenge) then
      // test to see if all its children are hidden
      if (node.children.every(node => node.isHidden)) {
        if (node.isHidden) {
          return node;
        }
        return {
          ...node,
          isHidden: true
        };
      } else if (node.isHidden) {
        return {
          ...node,
          isHidden: false
        };
      }
      // nothing has changed
      return node;
    }
  );
}

// unfilterMapUi(tree: MapUi) => MapUi
export function unfilterMapUi(tree) {
  return traverseMapUi(
    tree,
    node => {
      if (!node.isHidden) {
        return node;
      }
      return {
        ...node,
        isHidden: false
      };
    }
  );
}
<commit_msg>feat(Map): Map now is closed by default (#13980)<commit_after>import flow from 'lodash/flow';
import * as challengeTypes from '../../utils/challengeTypes';
import protect from '../../utils/empty-protector';
import { decodeScriptTags } from '../../../utils/encode-decode';

// determine the component to view for each challenge
export const viewTypes = {
  [ challengeTypes.html ]: 'classic',
  [ challengeTypes.js ]: 'classic',
  [ challengeTypes.bonfire ]: 'classic',
  [ challengeTypes.frontEndProject ]: 'project',
  [ challengeTypes.backEndProject ]: 'project',
  // might not be used anymore
  [ challengeTypes.simpleProject ]: 'project',
  // formally hikes
  [ challengeTypes.video ]: 'video',
  [ challengeTypes.step ]: 'step',
  backend: 'backend'
};

// determine the type of submit function to use for the challenge on completion
export const submitTypes = {
  [ challengeTypes.html ]: 'tests',
  [ challengeTypes.js ]: 'tests',
  [ challengeTypes.bonfire ]: 'tests',
  // requires just a button press
  [ challengeTypes.simpleProject ]: 'project.simple',
  // requires just a single url
  // like codepen.com/my-project
  [ challengeTypes.frontEndProject ]: 'project.frontEnd',
  // requires two urls
  // a hosted URL where the app is running live
  // project code url like GitHub
  [ challengeTypes.backEndProject ]: 'project.backEnd',
  // formally hikes
  [ challengeTypes.video ]: 'video',
  [ challengeTypes.step ]: 'step',
  backend: 'backend'
};

// determines if a line in a challenge description
// has html that should be rendered
export const descriptionRegex = /\<blockquote|\<ol|\<h4|\<table/;

export function arrayToString(seedData = ['']) {
  seedData = Array.isArray(seedData) ? seedData : [seedData];
  return seedData.reduce((seed, line) => '' + seed + line + '\n', '\n');
}

export function buildSeed({ challengeSeed = [] } = {}) {
  return flow(
    arrayToString,
    decodeScriptTags
  )(challengeSeed);
}

const pathsMap = {
  [ challengeTypes.html ]: 'html',
  [ challengeTypes.js ]: 'js',
  [ challengeTypes.bonfire ]: 'js'
};

export function getPreFile({ challengeType }) {
  return {
    name: 'index',
    ext: pathsMap[challengeType] || 'html',
    key: getFileKey({ challengeType })
  };
}

export function getFileKey({ challengeType }) {
  return 'index' + (pathsMap[challengeType] || 'html');
}

export function createTests({ tests = [] }) {
  return tests
    .map(test => {
      if (typeof test === 'string') {
        return {
          text: ('' + test).split('message: ').pop().replace(/\'\);/g, ''),
          testString: test
        };
      }
      return test;
    });
}

function logReplacer(value) {
  if (Array.isArray(value)) {
    const replaced = value.map(logReplacer);
    return '[' + replaced.join(', ') + ']';
  }
  if (typeof value === 'string' && !value.startsWith('//')) {
    return '"' + value + '"';
  }
  if (typeof value === 'number' && isNaN(value)) {
    return value.toString();
  }
  if (typeof value === 'undefined') {
    return 'undefined';
  }
  if (value === null) {
    return 'null';
  }
  if (typeof value === 'function') {
    return value.name;
  }
  if (typeof value === 'object') {
    return JSON.stringify(value, null, 2);
  }

  return value;
}

export function loggerToStr(args) {
  args = Array.isArray(args) ? args : [args];
  return args
    .map(logReplacer)
    .reduce((str, arg) => str + arg + '\n', '');
}

export function getNextChallenge(
  current,
  entities,
  {
    isDev = false,
    skip = 0
  } = {}
) {
  const { challenge: challengeMap, block: blockMap } = entities;
  // find current challenge
  // find current block
  // find next challenge in block
  const currentChallenge = challengeMap[current];
  if (!currentChallenge) {
    return null;
  }
  const block = blockMap[currentChallenge.block];
  const index = block.challenges.indexOf(currentChallenge.dashedName);
  // use next challenge name to find challenge in challenge map
  const nextChallenge = challengeMap[
    // grab next challenge name in current block
    // skip is used to skip isComingSoon challenges
    block.challenges[ index + 1 + skip ]
  ];
  if (
    !isDev &&
    nextChallenge &&
    (nextChallenge.isComingSoon || nextChallenge.isBeta)
  ) {
    // if we find a next challenge and it is a coming soon
    // recur with plus one to skip this challenge
    return getNextChallenge(current, entities, { isDev, skip: skip + 1 });
  }
  return nextChallenge;
}

export function getFirstChallengeOfNextBlock(
  current,
  entities,
  {
    isDev = false,
    skip = 0
  } = {}
) {
  const {
    challenge: challengeMap,
    block: blockMap,
    superBlock: SuperBlockMap
  } = entities;
  const currentChallenge = challengeMap[current];
  if (!currentChallenge) {
    return null;
  }
  const block = blockMap[currentChallenge.block];
  if (!block) {
    return null;
  }
  const superBlock = SuperBlockMap[block.superBlock];
  if (!superBlock) {
    return null;
  }
  // find index of current block
  const index = superBlock.blocks.indexOf(block.dashedName);

  // find next block name
  // and pull block object from block map
  const newBlock = blockMap[
    superBlock.blocks[ index + 1 + skip ]
  ];
  if (!newBlock) {
    return null;
  }
  // grab first challenge from next block
  const nextChallenge = challengeMap[newBlock.challenges[0]];
  if (isDev || !nextChallenge || !nextChallenge.isComingSoon) {
    return nextChallenge;
  }
  // if first challenge is coming soon, find next challenge here
  const nextChallenge2 = getNextChallenge(
    nextChallenge.dashedName,
    entities,
    { isDev }
  );
  if (nextChallenge2) {
    return nextChallenge2;
  }
  // whole block is coming soon
  // skip this block
  return getFirstChallengeOfNextBlock(
    current,
    entities,
    { isDev, skip: skip + 1 }
  );
}

export function getFirstChallengeOfNextSuperBlock(
  current,
  entities,
  superBlocks,
  {
    isDev = false,
    skip = 0
  } = {}
) {
  const {
    challenge: challengeMap,
    block: blockMap,
    superBlock: SuperBlockMap
  } = entities;
  const currentChallenge = challengeMap[current];
  if (!currentChallenge) {
    return null;
  }
  const block = blockMap[currentChallenge.block];
  if (!block) {
    return null;
  }
  const superBlock = SuperBlockMap[block.superBlock];
  if (!superBlock) {
    return null;
  }
  const index = superBlocks.indexOf(superBlock.dashedName);
  const newSuperBlock = SuperBlockMap[superBlocks[ index + 1 + skip]];
  if (!newSuperBlock) {
    return null;
  }
  const newBlock = blockMap[
    newSuperBlock.blocks[ 0 ]
  ];
  if (!newBlock) {
    return null;
  }
  const nextChallenge = challengeMap[newBlock.challenges[0]];
  if (isDev || !nextChallenge || !nextChallenge.isComingSoon) {
    return nextChallenge;
  }
  // coming soon challenge, grab next
  // non coming soon challenge in same block instead
  const nextChallengeInBlock = getNextChallenge(
    nextChallenge.dashedName,
    entities,
    { isDev }
  );
  if (nextChallengeInBlock) {
    return nextChallengeInBlock;
  }
  // whole block is coming soon
  // grab first challenge in next block in newSuperBlock instead
  const challengeInNextBlock = getFirstChallengeOfNextBlock(
    nextChallenge.dashedName,
    entities,
    { isDev }
  );

  if (challengeInNextBlock) {
    return challengeInNextBlock;
  }
  // whole super block is coming soon
  // skip this super block
  return getFirstChallengeOfNextSuperBlock(
    current,
    entities,
    superBlocks,
    { isDev, skip: skip + 1 }
  );
}

export function getCurrentBlockName(current, entities) {
  const { challenge: challengeMap } = entities;
  const challenge = challengeMap[current];
  return challenge.block;
}

export function getCurrentSuperBlockName(current, entities) {
  const { challenge: challengeMap, block: blockMap } = entities;
  const challenge = challengeMap[current];
  const block = blockMap[challenge.block];
  return block.superBlock;
}

// gets new mouse position
// getMouse(
//   e: MouseEvent|TouchEvent,
//   [ dx: Number, dy: Number ]
// ) => [ Number, Number ]
export function getMouse(e, [dx, dy]) {
  let { pageX, pageY, touches, changedTouches } = e;

  // touches can be empty on touchend
  if (touches || changedTouches) {
    e.preventDefault();
    // these re-assigns the values of pageX, pageY from touches
    ({ pageX, pageY } = touches[0] || changedTouches[0]);
  }

  return [pageX - dx, pageY - dy];
}

export function filterComingSoonBetaChallenge(
  isDev = false,
  { isComingSoon, isBeta }
) {
  return !(isComingSoon || isBeta) ||
    isDev;
}

export function filterComingSoonBetaFromEntities(
  { challenge: challengeMap, ...rest },
  isDev = false
) {
  const filter = filterComingSoonBetaChallenge.bind(null, isDev);
  return {
    ...rest,
    challenge: Object.keys(challengeMap)
      .map(dashedName => challengeMap[dashedName])
      .filter(filter)
      .reduce((challengeMap, challenge) => {
        challengeMap[challenge.dashedName] = challenge;
        return challengeMap;
      }, {})
  };
}

export function searchableChallengeTitles({ challenge: challengeMap } = {}) {
  return Object.keys(challengeMap)
    .map(dashedName => challengeMap[dashedName])
    .reduce((accu, current) => {
        accu[current.dashedName] = current.title;
        return accu;
      }
    , {});
}

// interface Node {
//   isHidden: Boolean,
//   children: Void|[ ...Node ],
//   isOpen?: Boolean
// }
//
// interface MapUi
// {
//   children: [...{
//     name: (superBlock: String),
//     isOpen: Boolean,
//     isHidden: Boolean,
//     children: [...{
//       name: (blockName: String),
//       isOpen: Boolean,
//       isHidden: Boolean,
//       children: [...{
//         name: (challengeName: String),
//         isHidden: Boolean
//       }]
//     }]
//   }]
// }
export function createMapUi(
  { superBlock: superBlockMap, block: blockMap } = {},
  superBlocks,
  searchNameMap
) {
  if (!superBlocks || !superBlockMap || !blockMap) {
    return {};
  }
  return {
    children: superBlocks.map(superBlock => {
      return {
        name: superBlock,
        isOpen: false,
        isHidden: false,
        children: protect(superBlockMap[superBlock]).blocks.map(block => {
          return {
            name: block,
            isOpen: false,
            isHidden: false,
            children: protect(blockMap[block]).challenges.map(challenge => {
              return {
                name: challenge,
                title: searchNameMap[challenge],
                isHidden: false,
                children: null
              };
            })
          };
        })
      };
    })
  };
}

// synchronise
// traverseMapUi(
//   tree: MapUi|Node,
//   update: ((MapUi|Node) => MapUi|Node)
// ) => MapUi|Node
export function traverseMapUi(tree, update) {
  let childrenChanged;
  if (!Array.isArray(tree.children)) {
    return update(tree);
  }
  const newChildren = tree.children.map(node => {
    const newNode = traverseMapUi(node, update);
    if (!childrenChanged && newNode !== node) {
      childrenChanged = true;
    }
    return newNode;
  });
  if (childrenChanged) {
    tree = {
      ...tree,
      children: newChildren
    };
  }
  return update(tree);
}

// synchronise
// getNode(tree: MapUi, name: String) => MapUi
export function getNode(tree, name) {
  let node;
  traverseMapUi(tree, thisNode => {
    if (thisNode.name === name) {
      node = thisNode;
    }
    return thisNode;
  });
  return node;
}

// synchronise
// updateSingelNode(
//   tree: MapUi,
//   name: String,
//   update(MapUi|Node) => MapUi|Node
// ) => MapUi
export function updateSingleNode(tree, name, update) {
  return traverseMapUi(tree, node => {
    if (name !== node.name) {
      return node;
    }
    return update(node);
  });
}

// synchronise
// toggleThisPanel(tree: MapUi, name: String) => MapUi
export function toggleThisPanel(tree, name) {
  return updateSingleNode(tree, name, node => {
    return {
      ...node,
      isOpen: !node.isOpen
    };
  });
}

// toggleAllPanels(tree: MapUi, isOpen: Boolean = false ) => MapUi
export function toggleAllPanels(tree, isOpen = false) {
  return traverseMapUi(tree, node => {
    if (!Array.isArray(node.children) || node.isOpen === isOpen) {
      return node;
    }
    return {
      ...node,
      isOpen
    };
  });
}

// collapseAllPanels(tree: MapUi) => MapUi
export function collapseAllPanels(tree) {
  return toggleAllPanels(tree);
}

// expandAllPanels(tree: MapUi) => MapUi
export function expandAllPanels(tree) {
  return toggleAllPanels(tree, true);
}

// applyFilterToMap(tree: MapUi, filterRegex: RegExp) => MapUi
export function applyFilterToMap(tree, filterRegex) {
  return traverseMapUi(
    tree,
    node => {
      // no children indicates a challenge node
      // if leaf (challenge) then test if regex is a match
      if (!Array.isArray(node.children)) {
        // does challenge name meet filter criteria?
        if (filterRegex.test(node.title)) {
          // is challenge currently hidden?
          if (node.isHidden) {
            // unhide challenge, it matches
            return {
              ...node,
              isHidden: false
            };
          }
        } else if (!node.isHidden) {
          return {
            ...node,
            isHidden: true
          };
        }
        return node;
      }
      // if not leaf node (challenge) then
      // test to see if all its children are hidden
      if (node.children.every(node => node.isHidden)) {
        if (node.isHidden) {
          return node;
        }
        return {
          ...node,
          isHidden: true
        };
      } else if (node.isHidden) {
        return {
          ...node,
          isHidden: false
        };
      }
      // nothing has changed
      return node;
    }
  );
}

// unfilterMapUi(tree: MapUi) => MapUi
export function unfilterMapUi(tree) {
  return traverseMapUi(
    tree,
    node => {
      if (!node.isHidden) {
        return node;
      }
      return {
        ...node,
        isHidden: false
      };
    }
  );
}
<|endoftext|>