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

/**
 * A RenderState represents the state information for a Node needed to determine how descendants are rendered.
 * It extracts all the information necessary from ancestors in a compact form so we can create an effective tree of
 * these.
 *
 * API for RenderState:
 * {
 *   isBackbone: Boolean
 *   isTransformed: Boolean
 *   isInstanceCanvasCache: Boolean
 *   isSharedCanvasCachePlaceholder: Boolean
 *   isSharedCanvasCacheSelf: Boolean
 *   selfRenderer: Renderer
 *   groupRenderer: Renderer
 *   sharedCacheRenderer: Renderer
 *   getStateForDescendant: function( trail ) : RenderState
 * }
 *
 * NOTE: Trails for transforms are not provided. Instead, inspecting isTransformed and what type of cache should
 * uniquely determine the transformBaseTrail and transformTrail necessary for rendering (and in an efficient way).
 * Not included here for performance (state doesn't need them)
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

define( function( require ) {
  'use strict';

  var inherit = require( 'PHET_CORE/inherit' );
  var Poolable = require( 'PHET_CORE/Poolable' );
  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/display/Renderer' );
  require( 'SCENERY/util/Trail' );

  var emptyObject = {};

  /*
   * @param {Node} node                    The node whose instance will have this state (inspect the hints / properties
   *                                       on this node)
   * @param {number} preferredRenderers    Either 0 (no preference), or a Renderer order bitmask (see Renderer.js)
   * @param {number} svgRenderer           SVG renderer settings to use
   * @param {number} canvasRenderer        Canvas renderer settings to use
   * @param {boolean} isUnderCanvasCache   Whether we are under any sort of Canvas cache (not including if this node is
   *                                       canvas cached)
   * @param {boolean} isShared             Whether this is the shared instance tree for a single-cache, instead of a
   *                                       reference to it
   * @param {boolean} isDisplayRoot
   *
   * Potential ways the state can change:
   * - any input changes, specifically including:
   * - node's renderer summary (what types of renderers are allowed below)
   * - node.hints | node.opacity
   */
  var RenderState = scenery.RenderState = function RenderState( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot ) {
    this.initialize( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot );
    debugger;
  };
  inherit( Object, RenderState, {
    initialize: function( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot ) {
      // this should be accurate right now, the pass to update these should have been completed earlier
      var combinedBitmask = node._rendererSummary.bitmask;

      this.preferredRenderers = preferredRenderers;
      this.svgRenderer = svgRenderer;
      this.canvasRenderer = canvasRenderer;
      this.isUnderCanvasCache = isUnderCanvasCache;
      this.isDisplayRoot = isDisplayRoot;

      this.isBackbone = false;
      this.isTransformed = false;
      this.isInstanceCanvasCache = false;
      this.isSharedCanvasCachePlaceholder = false;
      this.isSharedCanvasCacheSelf = false;

      //OHTWO TODO PERFORMANCE: These probably should be 0 (e.g. no renderer), so they are falsy, but that way remain
      // fixednum when set to actual renderers
      this.selfRenderer = null;
      this.groupRenderer = null;
      this.sharedCacheRenderer = null;

      var hints = node._hints || emptyObject;

      //OHTWO TODO: force the same layer/block for these. temporarily we are creating a backbone, but we should be able
      //            to create a composite and have these handled inside layers
      var hasTransparency = node._opacity !== 1 || hints.usesOpacity;
      var hasClip = node._clipArea !== null;

      if ( hints.renderer ) {
        this.preferredRenderers = scenery.Renderer.pushOrderBitmask( this.preferredRenderers, hints.renderer );
      }

      // check if we need a backbone or cache
      // if we are under a canvas cache, we will NEVER have a backbone
      // splits are accomplished just by having a backbone
      if ( isDisplayRoot || ( !isUnderCanvasCache && ( hasTransparency || hasClip || hints.requireElement || hints.cssTransform || hints.split ) ) ) {
        this.isBackbone = true;
        this.isTransformed = isDisplayRoot || !!hints.cssTransform; // for now, only trigger CSS transform if we have the specific hint
        this.groupRenderer = scenery.Renderer.bitmaskDOM | ( hints.forceAcceleration ? scenery.Renderer.bitmaskForceAcceleration : 0 ); // probably won't be used
      }
      else if ( hasTransparency || hasClip || hints.canvasCache ) {
        // everything underneath needs to be renderable with Canvas, otherwise we cannot cache
        assert && assert( ( combinedBitmask & scenery.Renderer.bitmaskCanvas ) !== 0, 'hints.canvasCache provided, but not all node contents can be rendered with Canvas under ' + node.constructor.name );

        if ( hints.singleCache ) {
          // TODO: scale options - fixed size, match highest resolution (adaptive), or mipmapped
          if ( isShared ) {
            this.isUnderCanvasCache = true;
            this.isSharedCanvasCacheSelf = true;

            //OHTWO TODO: Also consider SVG output
            this.sharedCacheRenderer = scenery.Renderer.bitmaskCanvas;
          }
          else {
            // everything underneath needs to guarantee that its bounds are valid
            //OHTWO TODO: We'll probably remove this if we go with the "safe bounds" approach
            assert && assert( ( combinedBitmask & scenery.bitmaskBoundsValid ) !== 0, 'hints.singleCache provided, but not all node contents have valid bounds under ' + node.constructor.name );

            this.isSharedCanvasCachePlaceholder = true;
          }
        }
        else {
          this.isInstanceCanvasCache = true;
          this.isUnderCanvasCache = true;
          this.groupRenderer = scenery.Renderer.bitmaskCanvas; // disallowing SVG here, so we don't have to break up our SVG group structure
        }
      }

      if ( node.isPainted() ) {
        this.setSelfRenderer( node );
      }

      return this;
    },

    setSelfRenderer: function( node ) {
      if ( this.isUnderCanvasCache ) {
        this.selfRenderer = this.canvasRenderer;
      }
      else {
        var success = false;

        // try preferred order if specified
        if ( this.preferredRenderers ) {
          success = this.trySelfRenderer( node, scenery.Renderer.bitmaskOrderFirst( this.preferredRenderers ), 0 ) ||
                    this.trySelfRenderer( node, scenery.Renderer.bitmaskOrderSecond( this.preferredRenderers ), 0 ) ||
                    this.trySelfRenderer( node, scenery.Renderer.bitmaskOrderThird( this.preferredRenderers ), 0 ) ||
                    this.trySelfRenderer( node, scenery.Renderer.bitmaskOrderFourth( this.preferredRenderers ), 0 );
        }

        // fall back to a default order
        success = success ||
                  this.trySelfRenderer( node, scenery.Renderer.bitmaskSVG, this.svgRenderer ) ||
                  this.trySelfRenderer( node, scenery.Renderer.bitmaskCanvas, this.canvasRenderer ) ||
                  this.trySelfRenderer( node, scenery.Renderer.bitmaskDOM, 0 ) ||
                  this.trySelfRenderer( node, scenery.Renderer.bitmaskWebGL, 0 );

        assert && assert( success, 'setSelfRenderer failure?' );
      }
    },

    // returns success of setting this.selfRenderer. renderer is expected to be a 1-bit bitmask, with rendererSpecifics
    // either 0 (autodetect) or a full renderer bitmask
    trySelfRenderer: function( node, renderer, rendererSpecifics ) {
      // if provided renderer === 0, we won't be compatible
      var compatible = !!( node._rendererBitmask & renderer );
      if ( compatible ) {
        if ( !rendererSpecifics ) {
          if ( scenery.Renderer.isCanvas( renderer ) ) {
            rendererSpecifics = this.canvasRenderer;
          }
          else if ( scenery.Renderer.isSVG( renderer ) ) {
            rendererSpecifics = this.svgRenderer;
          }
          else if ( scenery.Renderer.isDOM( renderer ) ) {
            // TODO: decide if CSS transform is to be applied here!
            rendererSpecifics = scenery.Renderer.bitmaskDOM;
          }
          else if ( scenery.Renderer.isWebGL( renderer ) ) {
            // TODO: details?
            rendererSpecifics = scenery.Renderer.bitmaskWebGL;
          }
        }
        assert && assert( rendererSpecifics,
          'Should have renderer specifics by now, otherwise we did not recognize the renderer' );
        this.selfRenderer = rendererSpecifics;
      }
      return compatible;
    },

    getStateForDescendant: function( node ) {
      return RenderState.createFromPool(
        node,

        // default renderer when available
        this.preferredRenderers,

        // default SVG renderer settings
        this.svgRenderer,

        // default Canvas renderer settings
        this.canvasRenderer,

        // isUnderCanvasCache
        (this.isUnderCanvasCache || this.isInstanceCanvasCache || this.isSharedCanvasCacheSelf),

        // isShared. No direct descendant is shared, since we create those specially with a new state
        // from createSharedCacheState
        false,

        // isDisplayRoot
        false
      );
    },

    /*
     * Whether we can just update the state on an Instance when changing from this state => otherState.
     * This is generally not possible if there is a change in whether the instance should be a transform root
     * (e.g. backbone/single-cache), so we will have to recreate the instance and its subtree if that is the case.
     */
    isInstanceCompatibleWith: function( otherState ) {
      return this.isTransformed === otherState.isTransformed && //OHTWO TODO: allow mutating based on this change
             this.isSharedCanvasCachePlaceholder === otherState.isSharedCanvasCachePlaceholder; //OHTWO TODO: allow mutating based on this change
    },

    toString: function() {
      var result = 'S[ ' +
                   ( this.isDisplayRoot ? 'displayRoot ' : '' ) +
                   ( this.isBackbone ? 'backbone ' : '' ) +
                   ( this.isInstanceCanvasCache ? 'instanceCache ' : '' ) +
                   ( this.isSharedCanvasCachePlaceholder ? 'sharedCachePlaceholder ' : '' ) +
                   ( this.isSharedCanvasCacheSelf ? 'sharedCacheSelf ' : '' ) +
                   ( this.isTransformed ? 'TR ' : '' ) +
                   ( this.selfRenderer ? this.selfRenderer.toString( 16 ) : '-' ) + ',' +
                   ( this.groupRenderer ? this.groupRenderer.toString( 16 ) : '-' ) + ',' +
                   ( this.sharedCacheRenderer ? this.sharedCacheRenderer.toString( 16 ) : '-' ) + ' ';
      return result + ']';
    }
  } );

  RenderState.createRootState = function( node ) {
    var baseState = RenderState.createFromPool(
      node,                           // trail
      0,                              // no preferred renderers
      scenery.Renderer.bitmaskSVG,    // default SVG renderer settings
      scenery.Renderer.bitmaskCanvas, // default Canvas renderer settings
      false,                          // isUnderCanvasCache
      false,                          // isShared
      true                            // isDisplayRoot
    );
    return baseState;
  };

  RenderState.createSharedCacheState = function( node ) {
    var baseState = RenderState.createFromPool(
      node,                             // trail
      0,                                // no preferred renderers (not really necessary, Canvas will be forced anyways)
      null,                             // no SVG renderer settings needed
      scenery.Renderer.bitmaskCanvas,   // default Canvas renderer settings
      true,                             // isUnderCanvasCache
      true,                             // isShared (since we are creating the shared one, not the individual instances referencing it)
      false                             // isDisplayRoot
    );
    return baseState;
  };

  /* jshint -W064 */
  Poolable( RenderState, {
    constructorDuplicateFactory: function( pool ) {
      return function( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot ) {
        if ( pool.length ) {
          sceneryLog && sceneryLog.RenderState && sceneryLog.RenderState( 'new from pool' );
          return pool.pop().initialize( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot );
        }
        else {
          sceneryLog && sceneryLog.RenderState && sceneryLog.RenderState( 'new from constructor' );
          return new RenderState( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot );
        }
      };
    }
  } );

  return RenderState;
} );
<commit_msg>Removed debugger statement, see #295<commit_after>// Copyright 2002-2014, University of Colorado Boulder

/**
 * A RenderState represents the state information for a Node needed to determine how descendants are rendered.
 * It extracts all the information necessary from ancestors in a compact form so we can create an effective tree of
 * these.
 *
 * API for RenderState:
 * {
 *   isBackbone: Boolean
 *   isTransformed: Boolean
 *   isInstanceCanvasCache: Boolean
 *   isSharedCanvasCachePlaceholder: Boolean
 *   isSharedCanvasCacheSelf: Boolean
 *   selfRenderer: Renderer
 *   groupRenderer: Renderer
 *   sharedCacheRenderer: Renderer
 *   getStateForDescendant: function( trail ) : RenderState
 * }
 *
 * NOTE: Trails for transforms are not provided. Instead, inspecting isTransformed and what type of cache should
 * uniquely determine the transformBaseTrail and transformTrail necessary for rendering (and in an efficient way).
 * Not included here for performance (state doesn't need them)
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

define( function( require ) {
  'use strict';

  var inherit = require( 'PHET_CORE/inherit' );
  var Poolable = require( 'PHET_CORE/Poolable' );
  var scenery = require( 'SCENERY/scenery' );
  require( 'SCENERY/display/Renderer' );
  require( 'SCENERY/util/Trail' );

  var emptyObject = {};

  /*
   * @param {Node} node                    The node whose instance will have this state (inspect the hints / properties
   *                                       on this node)
   * @param {number} preferredRenderers    Either 0 (no preference), or a Renderer order bitmask (see Renderer.js)
   * @param {number} svgRenderer           SVG renderer settings to use
   * @param {number} canvasRenderer        Canvas renderer settings to use
   * @param {boolean} isUnderCanvasCache   Whether we are under any sort of Canvas cache (not including if this node is
   *                                       canvas cached)
   * @param {boolean} isShared             Whether this is the shared instance tree for a single-cache, instead of a
   *                                       reference to it
   * @param {boolean} isDisplayRoot
   *
   * Potential ways the state can change:
   * - any input changes, specifically including:
   * - node's renderer summary (what types of renderers are allowed below)
   * - node.hints | node.opacity
   */
  var RenderState = scenery.RenderState = function RenderState( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot ) {
    this.initialize( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot );
  };
  inherit( Object, RenderState, {
    initialize: function( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot ) {
      // this should be accurate right now, the pass to update these should have been completed earlier
      var combinedBitmask = node._rendererSummary.bitmask;

      this.preferredRenderers = preferredRenderers;
      this.svgRenderer = svgRenderer;
      this.canvasRenderer = canvasRenderer;
      this.isUnderCanvasCache = isUnderCanvasCache;
      this.isDisplayRoot = isDisplayRoot;

      this.isBackbone = false;
      this.isTransformed = false;
      this.isInstanceCanvasCache = false;
      this.isSharedCanvasCachePlaceholder = false;
      this.isSharedCanvasCacheSelf = false;

      //OHTWO TODO PERFORMANCE: These probably should be 0 (e.g. no renderer), so they are falsy, but that way remain
      // fixednum when set to actual renderers
      this.selfRenderer = null;
      this.groupRenderer = null;
      this.sharedCacheRenderer = null;

      var hints = node._hints || emptyObject;

      //OHTWO TODO: force the same layer/block for these. temporarily we are creating a backbone, but we should be able
      //            to create a composite and have these handled inside layers
      var hasTransparency = node._opacity !== 1 || hints.usesOpacity;
      var hasClip = node._clipArea !== null;

      if ( hints.renderer ) {
        this.preferredRenderers = scenery.Renderer.pushOrderBitmask( this.preferredRenderers, hints.renderer );
      }

      // check if we need a backbone or cache
      // if we are under a canvas cache, we will NEVER have a backbone
      // splits are accomplished just by having a backbone
      if ( isDisplayRoot || ( !isUnderCanvasCache && ( hasTransparency || hasClip || hints.requireElement || hints.cssTransform || hints.split ) ) ) {
        this.isBackbone = true;
        this.isTransformed = isDisplayRoot || !!hints.cssTransform; // for now, only trigger CSS transform if we have the specific hint
        this.groupRenderer = scenery.Renderer.bitmaskDOM | ( hints.forceAcceleration ? scenery.Renderer.bitmaskForceAcceleration : 0 ); // probably won't be used
      }
      else if ( hasTransparency || hasClip || hints.canvasCache ) {
        // everything underneath needs to be renderable with Canvas, otherwise we cannot cache
        assert && assert( ( combinedBitmask & scenery.Renderer.bitmaskCanvas ) !== 0, 'hints.canvasCache provided, but not all node contents can be rendered with Canvas under ' + node.constructor.name );

        if ( hints.singleCache ) {
          // TODO: scale options - fixed size, match highest resolution (adaptive), or mipmapped
          if ( isShared ) {
            this.isUnderCanvasCache = true;
            this.isSharedCanvasCacheSelf = true;

            //OHTWO TODO: Also consider SVG output
            this.sharedCacheRenderer = scenery.Renderer.bitmaskCanvas;
          }
          else {
            // everything underneath needs to guarantee that its bounds are valid
            //OHTWO TODO: We'll probably remove this if we go with the "safe bounds" approach
            assert && assert( ( combinedBitmask & scenery.bitmaskBoundsValid ) !== 0, 'hints.singleCache provided, but not all node contents have valid bounds under ' + node.constructor.name );

            this.isSharedCanvasCachePlaceholder = true;
          }
        }
        else {
          this.isInstanceCanvasCache = true;
          this.isUnderCanvasCache = true;
          this.groupRenderer = scenery.Renderer.bitmaskCanvas; // disallowing SVG here, so we don't have to break up our SVG group structure
        }
      }

      if ( node.isPainted() ) {
        this.setSelfRenderer( node );
      }

      return this;
    },

    setSelfRenderer: function( node ) {
      if ( this.isUnderCanvasCache ) {
        this.selfRenderer = this.canvasRenderer;
      }
      else {
        var success = false;

        // try preferred order if specified
        if ( this.preferredRenderers ) {
          success = this.trySelfRenderer( node, scenery.Renderer.bitmaskOrderFirst( this.preferredRenderers ), 0 ) ||
                    this.trySelfRenderer( node, scenery.Renderer.bitmaskOrderSecond( this.preferredRenderers ), 0 ) ||
                    this.trySelfRenderer( node, scenery.Renderer.bitmaskOrderThird( this.preferredRenderers ), 0 ) ||
                    this.trySelfRenderer( node, scenery.Renderer.bitmaskOrderFourth( this.preferredRenderers ), 0 );
        }

        // fall back to a default order
        success = success ||
                  this.trySelfRenderer( node, scenery.Renderer.bitmaskSVG, this.svgRenderer ) ||
                  this.trySelfRenderer( node, scenery.Renderer.bitmaskCanvas, this.canvasRenderer ) ||
                  this.trySelfRenderer( node, scenery.Renderer.bitmaskDOM, 0 ) ||
                  this.trySelfRenderer( node, scenery.Renderer.bitmaskWebGL, 0 );

        assert && assert( success, 'setSelfRenderer failure?' );
      }
    },

    // returns success of setting this.selfRenderer. renderer is expected to be a 1-bit bitmask, with rendererSpecifics
    // either 0 (autodetect) or a full renderer bitmask
    trySelfRenderer: function( node, renderer, rendererSpecifics ) {
      // if provided renderer === 0, we won't be compatible
      var compatible = !!( node._rendererBitmask & renderer );
      if ( compatible ) {
        if ( !rendererSpecifics ) {
          if ( scenery.Renderer.isCanvas( renderer ) ) {
            rendererSpecifics = this.canvasRenderer;
          }
          else if ( scenery.Renderer.isSVG( renderer ) ) {
            rendererSpecifics = this.svgRenderer;
          }
          else if ( scenery.Renderer.isDOM( renderer ) ) {
            // TODO: decide if CSS transform is to be applied here!
            rendererSpecifics = scenery.Renderer.bitmaskDOM;
          }
          else if ( scenery.Renderer.isWebGL( renderer ) ) {
            // TODO: details?
            rendererSpecifics = scenery.Renderer.bitmaskWebGL;
          }
        }
        assert && assert( rendererSpecifics,
          'Should have renderer specifics by now, otherwise we did not recognize the renderer' );
        this.selfRenderer = rendererSpecifics;
      }
      return compatible;
    },

    getStateForDescendant: function( node ) {
      return RenderState.createFromPool(
        node,

        // default renderer when available
        this.preferredRenderers,

        // default SVG renderer settings
        this.svgRenderer,

        // default Canvas renderer settings
        this.canvasRenderer,

        // isUnderCanvasCache
        (this.isUnderCanvasCache || this.isInstanceCanvasCache || this.isSharedCanvasCacheSelf),

        // isShared. No direct descendant is shared, since we create those specially with a new state
        // from createSharedCacheState
        false,

        // isDisplayRoot
        false
      );
    },

    /*
     * Whether we can just update the state on an Instance when changing from this state => otherState.
     * This is generally not possible if there is a change in whether the instance should be a transform root
     * (e.g. backbone/single-cache), so we will have to recreate the instance and its subtree if that is the case.
     */
    isInstanceCompatibleWith: function( otherState ) {
      return this.isTransformed === otherState.isTransformed && //OHTWO TODO: allow mutating based on this change
             this.isSharedCanvasCachePlaceholder === otherState.isSharedCanvasCachePlaceholder; //OHTWO TODO: allow mutating based on this change
    },

    toString: function() {
      var result = 'S[ ' +
                   ( this.isDisplayRoot ? 'displayRoot ' : '' ) +
                   ( this.isBackbone ? 'backbone ' : '' ) +
                   ( this.isInstanceCanvasCache ? 'instanceCache ' : '' ) +
                   ( this.isSharedCanvasCachePlaceholder ? 'sharedCachePlaceholder ' : '' ) +
                   ( this.isSharedCanvasCacheSelf ? 'sharedCacheSelf ' : '' ) +
                   ( this.isTransformed ? 'TR ' : '' ) +
                   ( this.selfRenderer ? this.selfRenderer.toString( 16 ) : '-' ) + ',' +
                   ( this.groupRenderer ? this.groupRenderer.toString( 16 ) : '-' ) + ',' +
                   ( this.sharedCacheRenderer ? this.sharedCacheRenderer.toString( 16 ) : '-' ) + ' ';
      return result + ']';
    }
  } );

  RenderState.createRootState = function( node ) {
    var baseState = RenderState.createFromPool(
      node,                           // trail
      0,                              // no preferred renderers
      scenery.Renderer.bitmaskSVG,    // default SVG renderer settings
      scenery.Renderer.bitmaskCanvas, // default Canvas renderer settings
      false,                          // isUnderCanvasCache
      false,                          // isShared
      true                            // isDisplayRoot
    );
    return baseState;
  };

  RenderState.createSharedCacheState = function( node ) {
    var baseState = RenderState.createFromPool(
      node,                             // trail
      0,                                // no preferred renderers (not really necessary, Canvas will be forced anyways)
      null,                             // no SVG renderer settings needed
      scenery.Renderer.bitmaskCanvas,   // default Canvas renderer settings
      true,                             // isUnderCanvasCache
      true,                             // isShared (since we are creating the shared one, not the individual instances referencing it)
      false                             // isDisplayRoot
    );
    return baseState;
  };

  /* jshint -W064 */
  Poolable( RenderState, {
    constructorDuplicateFactory: function( pool ) {
      return function( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot ) {
        if ( pool.length ) {
          sceneryLog && sceneryLog.RenderState && sceneryLog.RenderState( 'new from pool' );
          return pool.pop().initialize( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot );
        }
        else {
          sceneryLog && sceneryLog.RenderState && sceneryLog.RenderState( 'new from constructor' );
          return new RenderState( node, preferredRenderers, svgRenderer, canvasRenderer, isUnderCanvasCache, isShared, isDisplayRoot );
        }
      };
    }
  } );

  return RenderState;
} );
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.build',
  name: 'BuildApp',

  imports: [
    'log',
    'error'
  ],
  requires: [
    'foam.dao.NodeFileDAO as FileDAO',
    'foam.dao.File',
    'foam.core.dao.OrDAO',
    'node.dao.ModelFileDAO',
    'foam.build.WebApplication'
  ],
  properties: [
    {
      name: 'appDefinition'
    },
    {
      name: 'controller',
      help: 'Name of the main controller/model to create',
    },
    {
      model_: 'StringArrayProperty',
      name: 'coreFiles',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; }
    },
    {
      name: 'defaultView',
      help: "Default view of the controller to use.  If not set, the controller will be used as view (if it is one), or DetailView will be used"
    },
    {
      name: 'targetPath',
      help: "Directory to write output files to.  Will be created if it doesn't exist.",
      required: true
    },
    {
      model_: 'BooleanProperty',
      name: 'precompileTemplates',
      help: 'True to precompile templates of models loaded from the ModelDAO.',
      defaultValue: false
    },
    {
      model_: 'BooleanProperty',
      name: 'includeFoamCSS',
      defaultValue: false
    },
    {
      model_: 'StringProperty',
      name: 'icon'
    },
    {
      model_: 'StringProperty',
      name: 'version'
    },
    {
      model_: 'StringArrayProperty',
      name: 'resources'
    },
    {
      model_: 'BooleanProperty',
      name: 'appcacheManifest',
      defaultValue: false
    },
    {
      model_: 'StringArrayProperty',
      name: 'extraFiles',
      help: 'Extra files to both load during the build process, and include in the built image.',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; }
    },
    {
      model_: 'StringArrayProperty',
      name: 'extraModels',
      help: 'Extra models to include in the image regardless of if they were arequired or not.',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; },
      factory: function() { return ['foam.ui.FoamTagView']; }
    },
    {
      model_: 'StringArrayProperty',
      name: 'blacklistModels',
      help: 'Models to unconditionally exclude from the image, even if they are listed as required.',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; }
    },
    {
      model_: 'BooleanProperty',
      name: 'outputManifest',
      defaultValue: false,
      help: 'Set to true to write out a MANIFEST file listing all included models.'
    },
    {
      name: 'htmlFileName',
      help: 'Name of the main html file to produce.',
      defaultValue: 'main.html'
    },
    {
      model_: 'StringArrayProperty',
      name: 'htmlHeaders'
    },
    {
      name: 'formatter',
      factory: function() {
        return {
          __proto__: JSONUtil.compact,
          keys_: {},
          keyify: JSONUtil.prettyModel.keyify,
          outputObject_: function(out, obj, opt_defaultModel) {
            var first = true;

            out('{');
            if ( obj.model_.id !== opt_defaultModel ) {
              this.outputModel_(out, obj);
              first = false;
            }

            if ( Template.isInstance(obj) ) var isTemplate = true;

            var properties = obj.model_.getRuntimeProperties();
            for ( var key in properties ) {
              var prop = properties[key];

              if ( ! this.p(prop) && ( ! isTemplate || prop.name !== 'code' ) ) continue;

              if ( prop.name === 'documentation' ) continue;

              if ( prop.name in obj.instance_ ) {
                var val = obj[prop.name];
                if ( Array.isArray(val) && ! val.length ) continue;
                if ( ! first ) out(',');
                out(this.keyify(prop.name), ': ');

                if ( prop.name === 'methods' ) {
                  out('{');
                  var ff = true;
                  for ( var i = 0 ; i < val.length ; i++ ) {
                    if ( ! ff ) out(',');

                    out(this.keyify(val[i].name), ': ');
                    out(val[i].code);
                    ff = false;
                  }
                  out('}');
                } else {
                  this.output(out, val);
                }
                first = false;
              }
            }

            out('}');
          }
        };
      }
    },
    {
      name: 'path',
      factory: function() { return require('path'); }
    },
    {
      name: 'fileDAO',
      factory: function() { return this.FileDAO.create(); }
    },
    {
      model_: 'StringArrayProperty',
      name: 'extraClassPaths',
      help: 'List of extra .js hierarchies to load models from.  Paths will be checked in the order given, finally falling back to the main FOAM js/ hierarchy.',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; }
    },
    {
      model_: 'StringProperty',
      name: 'sourceLocale',
      defaultValue: 'en'
    },
    {
      model_: 'StringProperty',
      name: 'locale'
    },
    {
      model_: 'StringProperty',
      name: 'i18nSourcePath',
      defaultValue: 'en'
    },
    {
      model_: 'StringArrayProperty',
      name: 'i18nSources',
      adapt: function(_, s) {
        if (typeof s === 'string') return s.split(',');
        return s;
      }
    },
    {
      model_: 'StringProperty',
      name: 'jsFileName',
      getter: function() {
        return 'foam' + (this.locale ? '_' + this.locale : '') + '.js';
      }
    },
    {
      model_: 'StringProperty',
      name: 'manifestFileName',
      getter: function() {
        return 'app' + (this.locale ? '_' + this.locale : '') + '.manifest';
      }
    },
    {
      name: 'localizedHTMLFileName_',
      getter: function() {
        var match = this.htmlFileName.match(/[.][^.]*$/g);
        var ext = match ? match[0] : '';
        var baseName = this.htmlFileName.slice(0,
            this.htmlFileName.length - ext.length);
        return baseName + (this.locale ? '_' + this.locale : '') + ext;
      }
    },
    {
      model_: 'StringProperty',
      name: 'delegate'
    }
  ],
  methods: {
    execute: function() {
      for ( var i = 0; i < this.extraClassPaths.length ; i++ ) {
        this.X.ModelDAO = this.OrDAO.create({
          delegate: this.ModelFileDAO.create({
            classpath: this.extraClassPaths[i]
          }),
          primary: this.X.ModelDAO
        });
      }

      if ( this.appDefinition ) {
        this.X.ModelDAO.find(this.appDefinition, {
          put: function(d) {
            this.copyFrom(d);
            this.execute_();
          }.bind(this),
          error: function() {
            console.log("App definition failed");
            this.execute_();
          }.bind(this)
        });
      } else {
        this.execute_();
      }
    },
    execute_: function() {
      if ( this.delegate ) {
        arequire(this.delegate)(function(DelegateModel) {
          DelegateModel.create({ builder: this }).buildApp();
        }.bind(this));
      } else {
        this.buildApp();
      }
    },
    buildApp: function() {
      if ( ! this.targetPath ) {
        this.error("targetPath is required");
        process.exit(1);
      }
      if ( ! this.controller ) {
        this.error("controller is required");
        process.exit(1);
      }

      for ( var i = 0 ; i < this.extraFiles.length ; i++ ) {
        var path = this.getFilePath(this.extraFiles[i]);
        require(path);
      }

      var view = this.defaultView ? arequire(this.defaultView) : anop;

      var seq = [view];
      for ( var i = 0; i < this.extraModels.length ; i++ ) {
        seq.push(arequire(this.extraModels[i]));
      }

      aseq(
        aseq.apply(null, seq),
        arequire(this.controller))(this.buildModel.bind(this));
    },
    buildCoreJS_: function(ret) {
      var i = 0;
      var self = this;
      var corejs = '';
      var file;
      if ( this.coreFiles.length ) var myfiles = this.coreFiles;
      else myfiles = files;
      myfiles = myfiles.concat(this.extraFiles);
      awhile(
        function() { return i < myfiles.length; },
        aif(
          function() {
            file = myfiles[i++];
            if ( Array.isArray(file) ) {
              if ( file[1] == IN_NODEJS )
                return false;
              file = file[0];
            }
            return true;
          },
          aseq(
            function(ret) {
              var path = this.getFilePath(file);
              this.fileDAO.find(path, {
                put: ret,
                error: function() {
                  self.error.apply(["Error reading file: ", path].concat(arguments));
                }
              });
            }.bind(this),
            function(ret, file) {
              corejs += '\n';
              corejs += file.contents;
              ret();
            })))(function() { ret(corejs); });
    },
    buildAppJS_: function(ret) {
      var models = {};
      var visited = {};
      var error = this.error;
      var self = this;

      function add(require) {
        if ( visited[require] ) return;
        visited[require] = true;

        var model = X.lookup(require);
        if ( ! model ) {
          error("Could not load model: ", require);
        }
        if ( model.package &&
             self.blacklistModels.indexOf(model.id) == -1 ) {
          models[model.id] = model;
        }

        model.getAllRequires().forEach(add);
      };
      add(this.controller);
      if ( this.defaultView ) add(this.defaultView);

      for ( var i = 0; i < this.extraModels.length ; i++ ) {
        add(this.extraModels[i]);
      }

      var contents = '';

      var ids = Object.keys(models);
      if ( this.outputManifest ) {
        this.fileDAO.put(this.File.create({
          path: this.targetPath + this.path.sep + 'MANIFEST',
          contents: ids.join('\n')
        }));
      }

      for ( var i = 0; i < ids.length; i++ ) {
        var model = models[ids[i]];
        if ( this.precompileTemplates ) {
          for ( var j = 0 ; j < model.templates.length ; j++ ) {
            var t = model.templates[j];
            // It's safe to remove leading and trailing whitespace from CSS.
            if ( t.name === 'CSS' ) t.template = t.template.split('\n').map(function(s) { return s.trim(); }).join('\n');
            t.code = TemplateUtil.compile(t);
            t.clearProperty('template');
          }
        }
        contents += 'CLASS(';
        var formatter = this.precompileTemplates ? this.formatter : JSONUtil.compact;
        contents += formatter.where(NOT_TRANSIENT).stringifyObject(models[ids[i]], 'Model');
        contents += ')\n';
      }

      ret(contents);
    },
    buildModel: function(model) {
      if ( ! model ) {
        this.error('Could not find model: ', this.controller);
      }
      this.log('Building   ', model.id);
      this.log('Target is: ', this.targetPath);
      this.log(this.precompileTemplates ? '' : 'NOT ', 'pre-compiling templates.');

      var self = this;
      aseq(
        function(ret) {
          var file = this.File.create({
            path: this.targetPath + this.path.sep + this.localizedHTMLFileName_,
            contents: this.HTML()
          });

          console.log('Writing: ', file.path);
          this.fileDAO.put(file, {
            put: ret,
            error: function() {
              self.error('ERROR writing file: ', file.path);
              process.exit(1);
            }
          });
        }.bind(this),
        aif(this.appcacheManifest,
            function(ret) {
              var file = this.File.create({
                path: this.targetPath + this.path.sep + this.manifestFileName,
                contents: this.MANIFEST()
              });
              console.log('Writing: ', file.path);
              this.fileDAO.put(file, {
                put: ret,
                error: function() {
                  this.error("ERROR writing file: ", file.path);
                  process.exit(1);
                }
              });
            }.bind(this)),
        apar(
          function(ret) { this.buildCoreJS_(ret); }.bind(this),
          function(ret) { this.buildAppJS_(ret); }.bind(this)),
          function(ret, corejs, appjs) {
            var file = this.File.create({
              path: this.targetPath + this.path.sep + this.jsFileName,
              contents: corejs + appjs
            });
            console.log('Writing: ', file.path);
            this.fileDAO.put(file, {
              put: ret,
              error: function() {
                self.error('ERROR writing file: ', file.path);
                process.exit(1);
              }
            });
          }.bind(this)
          )(
        function(){
          process.exit(0);
        });
    },
    getFilePath: function(file) {
      var path = file;
      if ( path.slice(-3) !== '.js' ) path += '.js';
      if ( path.charAt(0) !== this.path.sep )
        path = FOAM_BOOT_DIR + this.path.sep + path;
      return path;
    }
  },
  templates: [
    function HTML() {/*<html<% if ( this.appcacheManifest ) { %> manifest="%%getManifestFileName"<% } %>><meta charset="utf-8"><%= this.htmlHeaders.join('') %><% if ( this.includeFoamCSS ) { %><link rel="stylesheet" type="text/css" href="foam.css"/><% } %><% if ( this.icon ) { %><link rel="icon" sizes="128x128" href="<%= this.icon %>"/><% } %><foam model="<%= this.controller %>"<% if ( this.defaultView ) { %> view="<%= this.defaultView %>"<% } %>></foam>*/},
    function MANIFEST() {/*CACHE MANIFEST
# version <%= this.version %>
<% if ( this.appDefinition ) { %># hash: <%= this.appDefinition.hashCode() %><% } %>

CACHE:
%%jsFileName
%%localizedHTMLFileName_
<% if ( this.includeFoamCSS ) { %>foam.css<% } %>
<% for ( var i = 0 ; i < this.resources.length ; i++ ) { %><%= this.resources[i] %>
<% } %>

NETWORK:
*
*/}
  ]
});
<commit_msg>foam.build.BuildApp: Add distinction between messages and translations (old property left in place for backwards compatability for now)<commit_after>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.build',
  name: 'BuildApp',

  imports: [
    'log',
    'error'
  ],
  requires: [
    'foam.dao.NodeFileDAO as FileDAO',
    'foam.dao.File',
    'foam.core.dao.OrDAO',
    'node.dao.ModelFileDAO',
    'foam.build.WebApplication'
  ],
  properties: [
    {
      name: 'appDefinition'
    },
    {
      name: 'controller',
      help: 'Name of the main controller/model to create',
    },
    {
      model_: 'StringArrayProperty',
      name: 'coreFiles',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; }
    },
    {
      name: 'defaultView',
      help: "Default view of the controller to use.  If not set, the controller will be used as view (if it is one), or DetailView will be used"
    },
    {
      name: 'targetPath',
      help: "Directory to write output files to.  Will be created if it doesn't exist.",
      required: true
    },
    {
      model_: 'BooleanProperty',
      name: 'precompileTemplates',
      help: 'True to precompile templates of models loaded from the ModelDAO.',
      defaultValue: false
    },
    {
      model_: 'BooleanProperty',
      name: 'includeFoamCSS',
      defaultValue: false
    },
    {
      model_: 'StringProperty',
      name: 'icon'
    },
    {
      model_: 'StringProperty',
      name: 'version'
    },
    {
      model_: 'StringArrayProperty',
      name: 'resources'
    },
    {
      model_: 'BooleanProperty',
      name: 'appcacheManifest',
      defaultValue: false
    },
    {
      model_: 'StringArrayProperty',
      name: 'extraFiles',
      help: 'Extra files to both load during the build process, and include in the built image.',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; }
    },
    {
      model_: 'StringArrayProperty',
      name: 'extraModels',
      help: 'Extra models to include in the image regardless of if they were arequired or not.',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; },
      factory: function() { return ['foam.ui.FoamTagView']; }
    },
    {
      model_: 'StringArrayProperty',
      name: 'blacklistModels',
      help: 'Models to unconditionally exclude from the image, even if they are listed as required.',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; }
    },
    {
      model_: 'BooleanProperty',
      name: 'outputManifest',
      defaultValue: false,
      help: 'Set to true to write out a MANIFEST file listing all included models.'
    },
    {
      name: 'htmlFileName',
      help: 'Name of the main html file to produce.',
      defaultValue: 'main.html'
    },
    {
      model_: 'StringArrayProperty',
      name: 'htmlHeaders'
    },
    {
      name: 'formatter',
      factory: function() {
        return {
          __proto__: JSONUtil.compact,
          keys_: {},
          keyify: JSONUtil.prettyModel.keyify,
          outputObject_: function(out, obj, opt_defaultModel) {
            var first = true;

            out('{');
            if ( obj.model_.id !== opt_defaultModel ) {
              this.outputModel_(out, obj);
              first = false;
            }

            if ( Template.isInstance(obj) ) var isTemplate = true;

            var properties = obj.model_.getRuntimeProperties();
            for ( var key in properties ) {
              var prop = properties[key];

              if ( ! this.p(prop) && ( ! isTemplate || prop.name !== 'code' ) ) continue;

              if ( prop.name === 'documentation' ) continue;

              if ( prop.name in obj.instance_ ) {
                var val = obj[prop.name];
                if ( Array.isArray(val) && ! val.length ) continue;
                if ( ! first ) out(',');
                out(this.keyify(prop.name), ': ');

                if ( prop.name === 'methods' ) {
                  out('{');
                  var ff = true;
                  for ( var i = 0 ; i < val.length ; i++ ) {
                    if ( ! ff ) out(',');

                    out(this.keyify(val[i].name), ': ');
                    out(val[i].code);
                    ff = false;
                  }
                  out('}');
                } else {
                  this.output(out, val);
                }
                first = false;
              }
            }

            out('}');
          }
        };
      }
    },
    {
      name: 'path',
      factory: function() { return require('path'); }
    },
    {
      name: 'fileDAO',
      factory: function() { return this.FileDAO.create(); }
    },
    {
      model_: 'StringArrayProperty',
      name: 'extraClassPaths',
      help: 'List of extra .js hierarchies to load models from.  Paths will be checked in the order given, finally falling back to the main FOAM js/ hierarchy.',
      adapt: function(_, s) { if ( typeof s === 'string' ) return s.split(','); return s; }
    },
    {
      model_: 'StringProperty',
      name: 'sourceLocale',
      defaultValue: 'en'
    },
    {
      model_: 'StringProperty',
      name: 'locale'
    },
    {
      model_: 'StringProperty',
      name: 'i18nSourcePath',
      defaultValue: 'en'
    },
    {
      model_: 'StringArrayProperty',
      name: 'i18nMessages',
      adapt: function(_, s) {
        if (typeof s === 'string') return s.split(',');
        return s;
      }
    },
    {
      model_: 'StringArrayProperty',
      name: 'i18nTranslations',
      adapt: function(_, s) {
        if (typeof s === 'string') return s.split(',');
        return s;
      }
    },
    // TODO(markdittmer): Remove "i18nSources" when all build processes
    // support distinction between i18nMessages and i18nTranslations.
    {
      model_: 'StringArrayProperty',
      name: 'i18nSources',
      adapt: function(_, s) {
        if (typeof s === 'string') return s.split(',');
        return s;
      }
    },
    {
      model_: 'StringProperty',
      name: 'jsFileName',
      getter: function() {
        return 'foam' + (this.locale ? '_' + this.locale : '') + '.js';
      }
    },
    {
      model_: 'StringProperty',
      name: 'manifestFileName',
      getter: function() {
        return 'app' + (this.locale ? '_' + this.locale : '') + '.manifest';
      }
    },
    {
      name: 'localizedHTMLFileName_',
      getter: function() {
        var match = this.htmlFileName.match(/[.][^.]*$/g);
        var ext = match ? match[0] : '';
        var baseName = this.htmlFileName.slice(0,
            this.htmlFileName.length - ext.length);
        return baseName + (this.locale ? '_' + this.locale : '') + ext;
      }
    },
    {
      model_: 'StringProperty',
      name: 'delegate'
    }
  ],
  methods: {
    execute: function() {
      for ( var i = 0; i < this.extraClassPaths.length ; i++ ) {
        this.X.ModelDAO = this.OrDAO.create({
          delegate: this.ModelFileDAO.create({
            classpath: this.extraClassPaths[i]
          }),
          primary: this.X.ModelDAO
        });
      }

      if ( this.appDefinition ) {
        this.X.ModelDAO.find(this.appDefinition, {
          put: function(d) {
            this.copyFrom(d);
            this.execute_();
          }.bind(this),
          error: function() {
            console.log("App definition failed");
            this.execute_();
          }.bind(this)
        });
      } else {
        this.execute_();
      }
    },
    execute_: function() {
      if ( this.delegate ) {
        arequire(this.delegate)(function(DelegateModel) {
          DelegateModel.create({ builder: this }).buildApp();
        }.bind(this));
      } else {
        this.buildApp();
      }
    },
    buildApp: function() {
      if ( ! this.targetPath ) {
        this.error("targetPath is required");
        process.exit(1);
      }
      if ( ! this.controller ) {
        this.error("controller is required");
        process.exit(1);
      }

      for ( var i = 0 ; i < this.extraFiles.length ; i++ ) {
        var path = this.getFilePath(this.extraFiles[i]);
        require(path);
      }

      var view = this.defaultView ? arequire(this.defaultView) : anop;

      var seq = [view];
      for ( var i = 0; i < this.extraModels.length ; i++ ) {
        seq.push(arequire(this.extraModels[i]));
      }

      aseq(
        aseq.apply(null, seq),
        arequire(this.controller))(this.buildModel.bind(this));
    },
    buildCoreJS_: function(ret) {
      var i = 0;
      var self = this;
      var corejs = '';
      var file;
      if ( this.coreFiles.length ) var myfiles = this.coreFiles;
      else myfiles = files;
      myfiles = myfiles.concat(this.extraFiles);
      awhile(
        function() { return i < myfiles.length; },
        aif(
          function() {
            file = myfiles[i++];
            if ( Array.isArray(file) ) {
              if ( file[1] == IN_NODEJS )
                return false;
              file = file[0];
            }
            return true;
          },
          aseq(
            function(ret) {
              var path = this.getFilePath(file);
              this.fileDAO.find(path, {
                put: ret,
                error: function() {
                  self.error.apply(["Error reading file: ", path].concat(arguments));
                }
              });
            }.bind(this),
            function(ret, file) {
              corejs += '\n';
              corejs += file.contents;
              ret();
            })))(function() { ret(corejs); });
    },
    buildAppJS_: function(ret) {
      var models = {};
      var visited = {};
      var error = this.error;
      var self = this;

      function add(require) {
        if ( visited[require] ) return;
        visited[require] = true;

        var model = X.lookup(require);
        if ( ! model ) {
          error("Could not load model: ", require);
        }
        if ( model.package &&
             self.blacklistModels.indexOf(model.id) == -1 ) {
          models[model.id] = model;
        }

        model.getAllRequires().forEach(add);
      };
      add(this.controller);
      if ( this.defaultView ) add(this.defaultView);

      for ( var i = 0; i < this.extraModels.length ; i++ ) {
        add(this.extraModels[i]);
      }

      var contents = '';

      var ids = Object.keys(models);
      if ( this.outputManifest ) {
        this.fileDAO.put(this.File.create({
          path: this.targetPath + this.path.sep + 'MANIFEST',
          contents: ids.join('\n')
        }));
      }

      for ( var i = 0; i < ids.length; i++ ) {
        var model = models[ids[i]];
        if ( this.precompileTemplates ) {
          for ( var j = 0 ; j < model.templates.length ; j++ ) {
            var t = model.templates[j];
            // It's safe to remove leading and trailing whitespace from CSS.
            if ( t.name === 'CSS' ) t.template = t.template.split('\n').map(function(s) { return s.trim(); }).join('\n');
            t.code = TemplateUtil.compile(t);
            t.clearProperty('template');
          }
        }
        contents += 'CLASS(';
        var formatter = this.precompileTemplates ? this.formatter : JSONUtil.compact;
        contents += formatter.where(NOT_TRANSIENT).stringifyObject(models[ids[i]], 'Model');
        contents += ')\n';
      }

      ret(contents);
    },
    buildModel: function(model) {
      if ( ! model ) {
        this.error('Could not find model: ', this.controller);
      }
      this.log('Building   ', model.id);
      this.log('Target is: ', this.targetPath);
      this.log(this.precompileTemplates ? '' : 'NOT ', 'pre-compiling templates.');

      var self = this;
      aseq(
        function(ret) {
          var file = this.File.create({
            path: this.targetPath + this.path.sep + this.localizedHTMLFileName_,
            contents: this.HTML()
          });

          console.log('Writing: ', file.path);
          this.fileDAO.put(file, {
            put: ret,
            error: function() {
              self.error('ERROR writing file: ', file.path);
              process.exit(1);
            }
          });
        }.bind(this),
        aif(this.appcacheManifest,
            function(ret) {
              var file = this.File.create({
                path: this.targetPath + this.path.sep + this.manifestFileName,
                contents: this.MANIFEST()
              });
              console.log('Writing: ', file.path);
              this.fileDAO.put(file, {
                put: ret,
                error: function() {
                  this.error("ERROR writing file: ", file.path);
                  process.exit(1);
                }
              });
            }.bind(this)),
        apar(
          function(ret) { this.buildCoreJS_(ret); }.bind(this),
          function(ret) { this.buildAppJS_(ret); }.bind(this)),
          function(ret, corejs, appjs) {
            var file = this.File.create({
              path: this.targetPath + this.path.sep + this.jsFileName,
              contents: corejs + appjs
            });
            console.log('Writing: ', file.path);
            this.fileDAO.put(file, {
              put: ret,
              error: function() {
                self.error('ERROR writing file: ', file.path);
                process.exit(1);
              }
            });
          }.bind(this)
          )(
        function(){
          process.exit(0);
        });
    },
    getFilePath: function(file) {
      var path = file;
      if ( path.slice(-3) !== '.js' ) path += '.js';
      if ( path.charAt(0) !== this.path.sep )
        path = FOAM_BOOT_DIR + this.path.sep + path;
      return path;
    }
  },
  templates: [
    function HTML() {/*<html<% if ( this.appcacheManifest ) { %> manifest="%%getManifestFileName"<% } %>><meta charset="utf-8"><%= this.htmlHeaders.join('') %><% if ( this.includeFoamCSS ) { %><link rel="stylesheet" type="text/css" href="foam.css"/><% } %><% if ( this.icon ) { %><link rel="icon" sizes="128x128" href="<%= this.icon %>"/><% } %><foam model="<%= this.controller %>"<% if ( this.defaultView ) { %> view="<%= this.defaultView %>"<% } %>></foam>*/},
    function MANIFEST() {/*CACHE MANIFEST
# version <%= this.version %>
<% if ( this.appDefinition ) { %># hash: <%= this.appDefinition.hashCode() %><% } %>

CACHE:
%%jsFileName
%%localizedHTMLFileName_
<% if ( this.includeFoamCSS ) { %>foam.css<% } %>
<% for ( var i = 0 ; i < this.resources.length ; i++ ) { %><%= this.resources[i] %>
<% } %>

NETWORK:
*
*/}
  ]
});
<|endoftext|>


<commit_before>CLASS({
  package: 'foam.ui',
  name: 'SlidePanel',
  extendsModel: 'View',

  requires: [
    'GestureTarget'
  ],
  imports: [
    'clearTimeout',
    'document',
    'gestureManager',
    'setTimeout'
  ],

  constants: {
    CLOSED: {
      name: 'CLOSED',
      layout: function() {
        return [ this.parentWidth - this.stripWidth, this.minPanelWidth, this.stripWidth ];
      },
      onResize: function() {
        if ( this.parentWidth > this.minWidth + this.minPanelWidth )
          this.nextState = this.EXPANDED;
      },
      toggle: function() { this.open(); },
      open: function() { this.nextState = this.OPEN; },
      shadow: true
    },
    EXPANDED: {
      name: 'EXPANDED',
      layout: function() {
        var extraWidth = this.parentWidth - this.minWidth - this.minPanelWidth;
        var panelWidth = this.minPanelWidth + extraWidth * this.panelRatio;
        return [ this.parentWidth - panelWidth, panelWidth, panelWidth ];
      },
      onResize: function() {
        if ( this.parentWidth < this.minWidth + this.minPanelWidth )
          this.nextState = this.CLOSED;
      }
    },
    OPEN: {
      name: 'OPEN',
      layout: function() {
        return [ this.parentWidth - this.stripWidth, this.minPanelWidth, this.panelWidth ];
      },
      onResize: function() {
        if ( this.parentWidth > this.minWidth + this.minPanelWidth )
          this.nextState = this.OPEN_EXPANDED;
      },
      close: function() { this.nextState = this.CLOSED; },
      toggle: function() { this.close(); },
      shadow: true
    },
    OPEN_EXPANDED: {
      name: 'OPEN_EXPANDED',
      layout: function() { return this.EXPANDED.layout.call(this); },
      onResize: function() {
        if ( this.parentWidth < this.minWidth + this.minPanelWidth )
          this.nextState = this.OPEN;
      }
    }
  },

  help: 'A controller that shows a main view with a small strip of the ' +
      'secondary view visible at the right edge. This "panel" can be dragged ' +
      'by a finger or mouse pointer to any position from its small strip to ' +
      'fully exposed. If the containing view is wide enough, both panels ' +
      'will always be visible.',

  properties: [
    {
      name: 'state'
    },
    {
      name: 'nextState',
      postSet: function(oldState, newState) {
//        if ( oldState === newState ) return;
        this.state = newState;

        this.desiredLayout = this.state.layout.call(this);
      }
    },
    {
      name: 'currentLayout',
      postSet: function(_, layout) {
        this.mainWidth  = layout[0];
        this.panelWidth = layout[1]+2;
        this.panelX     = this.parentWidth-layout[2];
      }
    },
    {
      name: 'desiredLayout',
      postSet: function(_, layout) {
        var startLayout = this.currentLayout;
        var start = Date.now();
        var end   = start + 200;
        var animate = function() {
          var now = Date.now();
          var p = (now-start) / (end-start);
          if ( p < 1 ) {
            this.currentLayout = [
              startLayout[0] * ( 1 - p ) + layout[0] * p,
              startLayout[1] * ( 1 - p ) + layout[1] * p,
              startLayout[2] * ( 1 - p ) + layout[2] * p
            ];
            this.X.requestAnimationFrame(animate);
          } else {
            this.currentLayout = layout;
          }
        }.bind(this);
        animate();
      }
    },
    { model_: 'ViewFactoryProperty', name: 'mainView' },
    { model_: 'ViewFactoryProperty', name: 'panelView' },
    {
      model_: 'IntProperty',
      name: 'minWidth',
      defaultValueFn: function() {
        var e = this.main$();
        return e ? toNum(this.X.window.getComputedStyle(e).width) : 300;
      }
    },
    {
      model_: 'IntProperty',
      name: 'mainWidth',
      model_: 'IntProperty',
      hidden: true,
      help: 'Set internally by the resize handler',
      postSet: function(_, x) {
        this.main$().style.width = x + 'px';
      }
    },
    {
      model_: 'IntProperty',
      name: 'panelWidth',
      model_: 'IntProperty',
      hidden: true,
      help: 'Set internally by the resize handler',
      postSet: function(_, x) {
        this.panel$().style.width = x + 'px';
      }
    },
    {
      model_: 'IntProperty',
      name: 'minPanelWidth',
      defaultValueFn: function() {
        if ( this.panelView && this.panelView.minWidth )
          return this.panelView.minWidth + (this.panelView.stripWidth || 0);

        var e = this.panel$();
        return e ? toNum(this.X.window.getComputedStyle(e).width) : 250;
      }
    },
    {
      model_: 'IntProperty',
      name: 'parentWidth',
      help: 'A pseudoproperty that returns the current width (CSS pixels) of the containing element',
      getter: function() { return toNum(this.X.window.getComputedStyle(this.$.parentNode).width); }
    },
    {
      model_: 'IntProperty',
      name: 'stripWidth',
      help: 'The width in (CSS) pixels of the minimal visible strip of panel',
      defaultValue: 30
    },
    {
      model_: 'FloatProperty',
      name: 'panelRatio',
      help: 'The ratio (0-1) of the total width occupied by the panel, when ' +
          'the containing element is wide enough for expanded view.',
      defaultValue: 0.5
    },
    {
      model_: 'IntProperty',
      name: 'panelX',
      //defaultValueFn: function() { this.width - this.stripWidth; },
      preSet: function(oldX, x) {
        if ( oldX !== x ) this.dir_ = oldX.compareTo(x);

        // Bound it between its left and right limits: full open and just the
        // strip.
        if ( x <= this.parentWidth - this.panelWidth )
          return this.parentWidth - this.panelWidth;

        if ( x >= this.parentWidth - this.stripWidth )
          return this.parentWidth - this.stripWidth;

        return x;
      },
      postSet: function(_, x) {
        this.panel$().style.webkitTransform = 'translate3d(' + x + 'px, 0,0)';
      }
    },
    {
      name: 'dragGesture',
      hidden: true,
      transient: true,
      lazyFactory: function() {
        return this.GestureTarget.create({
          containerID: this.id + '-panel',
          handler: this,
          gesture: 'drag'
        });
      }
    },
    {
      name: 'tapGesture',
      hidden: true,
      transient: true,
      lazyFactory: function() {
        return this.GestureTarget.create({
          containerID: this.id + '-panel',
          handler: this,
          gesture: 'tap'
        });
      }
    },
    {
      name: 'expanded',
      help: 'If the panel is wide enough to expand the panel permanently.',
      defaultValue: false
    }
  ],

  templates: [
    function CSS() {/*
      .SlidePanel {
        display: flex;
        position: relative;
      }
      .SlidePanel .shadow {
        background: linear-gradient(to left, rgba(0,0,0,0.15) 0%,
                                             rgba(0,0,0,0) 100%);
        height: 100%;
        left: -8px;
        position: absolute;
        width: 8px;
      }
      .SlidePanel .main {
      }
      .SlidePanel .panel {
        position: absolute;
        top: 0;
        left: -1;
      }
    */},
    function toHTML() {/*

          <%= this.mainView() %>

          <%= this.panelView() %>

    */}
  ],

  methods: {
    initHTML: function() {
      this.state = this.nextState = this.CLOSED;

      this.gestureManager.install(this.dragGesture);
      this.gestureManager.install(this.tapGesture);

      // Resize first, then init the outer view, and finally the panel view.
      this.X.window.addEventListener('resize', this.onResize);

      this.main$().addEventListener('click',       this.onMainFocus);
      this.main$().addEventListener('DOMFocusIn',  this.onMainFocus);
      this.panel$().addEventListener('DOMFocusIn', this.onPanelFocus);
      this.onResize();
      this.initChildren(); // We didn't call SUPER(), so we have to do this here.
    },
    interpolate: function(state1, state2) {
      var layout1 = state1.layout.call(this);
      var layout2 = state2.layout.call(this);
      return [
        layout1[0] * this.progress + layout2[0] * ( 1 - this.progress ),
        layout1[1] * this.progress + layout2[1] * ( 1 - this.progress ),
        layout1[2] * this.progress + layout2[2] * ( 1 - this.progress ),
      ];
    },
    main$:   function() { return this.X.$(this.id + '-main'); },
    panel$:  function() { return this.X.$(this.id + '-panel'); },
    shadow$: function() { return this.X.$(this.id + '-shadow'); },
    open:    function() { this.state.open && this.state.open.call(this); },
    close:   function() { this.state.close && this.state.close.call(this); },
    toggle:  function() { this.state.toggle && this.state.toggle.call(this); }
  },

  listeners: [
    {
      name: 'onPanelFocus',
      isMerged: 1,
      code: function(e) { this.open(); }
    },
    {
      name: 'onMainFocus',
      isMerged: 1,
      code: function(e) { this.close(); }
    },
    {
      name: 'onResize',
      isFramed: true,
      code: function(e) {
        if ( ! this.$ ) return;
        this.state.onResize.call(this);
        this.shadow$().style.display = this.nextState.shadow ? 'inline' : 'none';
        this.nextState = this.nextState;
      }
    },
    {
      name: 'tapClick',
      code: function() {
        console.log('tapclick', this.expanded, this.opened);
        this.toggle();
      }
    },
    {
      name: 'dragStart',
      code: function(point) {
        if ( this.expanded ) return;
        // Otherwise, bind panelX to the absolute X.
        var self = this;
        var originalX = this.panelX;
        Events.map(point.x$, this.panelX$, function(x) {
          return originalX + point.totalX;
        });
      }
    },
    {
      name: 'dragEnd',
      code: function(point) {
        console.log('dragEnd: ', this.dir_, this.state.name);
        if ( this.dir_ < 0 ) this.close(); else this.open();
        this.nextState = this.nextState;
      }
    }
  ]
});
<commit_msg>Improved animation.<commit_after>CLASS({
  package: 'foam.ui',
  name: 'SlidePanel',
  extendsModel: 'View',

  requires: [
    'GestureTarget'
  ],
  imports: [
    'clearTimeout',
    'document',
    'gestureManager',
    'setTimeout'
  ],

  constants: {
    CLOSED: {
      name: 'CLOSED',
      layout: function() {
        return [ this.parentWidth - this.stripWidth, this.minPanelWidth, this.stripWidth ];
      },
      onResize: function() {
        if ( this.parentWidth > this.minWidth + this.minPanelWidth )
          this.nextState = this.EXPANDED;
      },
      toggle: function() { this.open(); },
      open: function() { this.nextState = this.OPEN; },
      shadow: true
    },
    EXPANDED: {
      name: 'EXPANDED',
      layout: function() {
        var extraWidth = this.parentWidth - this.minWidth - this.minPanelWidth;
        var panelWidth = this.minPanelWidth + extraWidth * this.panelRatio;
        return [ this.parentWidth - panelWidth, panelWidth, panelWidth ];
      },
      onResize: function() {
        if ( this.parentWidth < this.minWidth + this.minPanelWidth )
          this.nextState = this.CLOSED;
      }
    },
    OPEN: {
      name: 'OPEN',
      layout: function() {
        return [ this.parentWidth - this.stripWidth, this.minPanelWidth, this.panelWidth ];
      },
      onResize: function() {
        if ( this.parentWidth > this.minWidth + this.minPanelWidth )
          this.nextState = this.OPEN_EXPANDED;
      },
      close: function() { this.nextState = this.CLOSED; },
      toggle: function() { this.close(); },
      shadow: true
    },
    OPEN_EXPANDED: {
      name: 'OPEN_EXPANDED',
      layout: function() { return this.EXPANDED.layout.call(this); },
      onResize: function() {
        if ( this.parentWidth < this.minWidth + this.minPanelWidth )
          this.nextState = this.OPEN;
      }
    }
  },

  help: 'A controller that shows a main view with a small strip of the ' +
      'secondary view visible at the right edge. This "panel" can be dragged ' +
      'by a finger or mouse pointer to any position from its small strip to ' +
      'fully exposed. If the containing view is wide enough, both panels ' +
      'will always be visible.',

  properties: [
    {
      name: 'state'
    },
    {
      name: 'nextState',
      postSet: function(oldState, newState) {
//        if ( oldState === newState ) return;
        this.state = newState;

        this.desiredLayout = this.state.layout.call(this);
      }
    },
    {
      name: 'currentLayout',
      postSet: function(_, layout) {
        this.mainWidth  = layout[0];
        this.panelWidth = layout[1]+2;
        this.panelX     = this.parentWidth-layout[2];
      }
    },
    {
      name: 'desiredLayout',
      postSet: function(_, layout) {
        if ( ! this.currentLayout ) {
          this.currentLayout = layout;
          return;
        }
        var startLayout = this.currentLayout;
        var start = Date.now();
        var end   = start + 1000;
        var animate = function() {
          var now = Date.now();
          var p = (now-start) / (end-start);
          if ( p < 1 ) {
            this.currentLayout = [
              startLayout[0] * ( 1 - p ) + layout[0] * p,
              startLayout[1] * ( 1 - p ) + layout[1] * p,
              startLayout[2] * ( 1 - p ) + layout[2] * p
            ];
            if ( this.af_ ) this.X.cancelAnimationFrame(this.af_);
            this.af_ = this.X.requestAnimationFrame(animate);
          } else {
            this.currentLayout = layout;
          }
        }.bind(this);
        animate();
      }
    },
    { model_: 'ViewFactoryProperty', name: 'mainView' },
    { model_: 'ViewFactoryProperty', name: 'panelView' },
    {
      model_: 'IntProperty',
      name: 'minWidth',
      defaultValueFn: function() {
        var e = this.main$();
        return e ? toNum(this.X.window.getComputedStyle(e).width) : 300;
      }
    },
    {
      model_: 'IntProperty',
      name: 'mainWidth',
      model_: 'IntProperty',
      hidden: true,
      help: 'Set internally by the resize handler',
      postSet: function(_, x) {
        this.main$().style.width = x + 'px';
      }
    },
    {
      model_: 'IntProperty',
      name: 'panelWidth',
      model_: 'IntProperty',
      hidden: true,
      help: 'Set internally by the resize handler',
      postSet: function(_, x) {
        this.panel$().style.width = x + 'px';
      }
    },
    {
      model_: 'IntProperty',
      name: 'minPanelWidth',
      defaultValueFn: function() {
        if ( this.panelView && this.panelView.minWidth )
          return this.panelView.minWidth + (this.panelView.stripWidth || 0);

        var e = this.panel$();
        return e ? toNum(this.X.window.getComputedStyle(e).width) : 250;
      }
    },
    {
      model_: 'IntProperty',
      name: 'parentWidth',
      help: 'A pseudoproperty that returns the current width (CSS pixels) of the containing element',
      getter: function() { return toNum(this.X.window.getComputedStyle(this.$.parentNode).width); }
    },
    {
      model_: 'IntProperty',
      name: 'stripWidth',
      help: 'The width in (CSS) pixels of the minimal visible strip of panel',
      defaultValue: 30
    },
    {
      model_: 'FloatProperty',
      name: 'panelRatio',
      help: 'The ratio (0-1) of the total width occupied by the panel, when ' +
          'the containing element is wide enough for expanded view.',
      defaultValue: 0.5
    },
    {
      model_: 'IntProperty',
      name: 'panelX',
      //defaultValueFn: function() { this.width - this.stripWidth; },
      preSet: function(oldX, x) {
        if ( oldX !== x ) this.dir_ = oldX.compareTo(x);

        // Bound it between its left and right limits: full open and just the
        // strip.
        if ( x <= this.parentWidth - this.panelWidth )
          return this.parentWidth - this.panelWidth;

        if ( x >= this.parentWidth - this.stripWidth )
          return this.parentWidth - this.stripWidth;

        return x;
      },
      postSet: function(_, x) {
        this.panel$().style.webkitTransform = 'translate3d(' + x + 'px, 0,0)';
      }
    },
    {
      name: 'dragGesture',
      hidden: true,
      transient: true,
      lazyFactory: function() {
        return this.GestureTarget.create({
          containerID: this.id + '-panel',
          handler: this,
          gesture: 'drag'
        });
      }
    },
    {
      name: 'tapGesture',
      hidden: true,
      transient: true,
      lazyFactory: function() {
        return this.GestureTarget.create({
          containerID: this.id + '-panel',
          handler: this,
          gesture: 'tap'
        });
      }
    },
    {
      name: 'expanded',
      help: 'If the panel is wide enough to expand the panel permanently.',
      defaultValue: false
    }
  ],

  templates: [
    function CSS() {/*
      .SlidePanel {
        display: flex;
        position: relative;
      }
      .SlidePanel .shadow {
        background: linear-gradient(to left, rgba(0,0,0,0.15) 0%,
                                             rgba(0,0,0,0) 100%);
        height: 100%;
        left: -8px;
        position: absolute;
        width: 8px;
      }
      .SlidePanel .main {
      }
      .SlidePanel .panel {
        position: absolute;
        top: 0;
        left: -1;
      }
    */},
    function toHTML() {/*

          <%= this.mainView() %>

          <%= this.panelView() %>

    */}
  ],

  methods: {
    initHTML: function() {
      this.state = this.nextState = this.CLOSED;

      this.gestureManager.install(this.dragGesture);
      this.gestureManager.install(this.tapGesture);

      // Resize first, then init the outer view, and finally the panel view.
      this.X.window.addEventListener('resize', this.onResize);

      this.main$().addEventListener('click',       this.onMainFocus);
      this.main$().addEventListener('DOMFocusIn',  this.onMainFocus);
      this.panel$().addEventListener('DOMFocusIn', this.onPanelFocus);
      this.onResize();
      this.initChildren(); // We didn't call SUPER(), so we have to do this here.
    },
    interpolate: function(state1, state2) {
      var layout1 = state1.layout.call(this);
      var layout2 = state2.layout.call(this);
      return [
        layout1[0] * this.progress + layout2[0] * ( 1 - this.progress ),
        layout1[1] * this.progress + layout2[1] * ( 1 - this.progress ),
        layout1[2] * this.progress + layout2[2] * ( 1 - this.progress ),
      ];
    },
    main$:   function() { return this.X.$(this.id + '-main'); },
    panel$:  function() { return this.X.$(this.id + '-panel'); },
    shadow$: function() { return this.X.$(this.id + '-shadow'); },
    open:    function() { this.state.open && this.state.open.call(this); },
    close:   function() { this.state.close && this.state.close.call(this); },
    toggle:  function() { this.state.toggle && this.state.toggle.call(this); }
  },

  listeners: [
    {
      name: 'onPanelFocus',
      isMerged: 1,
      code: function(e) { this.open(); }
    },
    {
      name: 'onMainFocus',
      isMerged: 1,
      code: function(e) { this.close(); }
    },
    {
      name: 'onResize',
      isFramed: true,
      code: function(e) {
        if ( ! this.$ ) return;
        this.state.onResize.call(this);
        this.shadow$().style.display = this.nextState.shadow ? 'inline' : 'none';
        this.nextState = this.nextState;
      }
    },
    {
      name: 'tapClick',
      code: function() {
        console.log('tapclick', this.expanded, this.opened);
        this.toggle();
      }
    },
    {
      name: 'dragStart',
      code: function(point) {
        if ( this.expanded ) return;
        // Otherwise, bind panelX to the absolute X.
        var self = this;
        var originalX = this.panelX;
        Events.map(point.x$, this.panelX$, function(x) {
          return originalX + point.totalX;
        });
      }
    },
    {
      name: 'dragEnd',
      code: function(point) {
        console.log('dragEnd: ', this.dir_, this.state.name);
        if ( this.dir_ < 0 ) this.close(); else this.open();
        this.nextState = this.nextState;
      }
    }
  ]
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.ui',
  name: 'SpinnerView',
  extendsModel: 'foam.ui.View',

  documentation: 'Renders a spinner in the Material Design style. Has a ' +
      '$$DOC{ref:".data"} property and acts like a $$DOC{ref:"BooleanView"}, ' +
      'that creates and destroys and the spinner when the value changes.',
  // TODO(braden): This spinner doesn't render on Firefox.
  properties: [
    {
      name: 'data',
      documentation: 'Defaults to true, so that the spinner will show itself ' +
          'by default, if data is not set.',
      defaultValue: true,
      postSet: function(old, nu) {
        if ( ! this.$ ) return;
        if ( ! nu ) this.$.innerHTML = '';
        else if ( ! old && nu ) {
          this.$.innerHTML = this.toInnerHTML();
          this.initInnerHTML();
        }
      }
    },
    {
      name: 'color',
      documentation: 'The color to use for the spinner.',
      defaultValue: '#4285F4'
    },
    {
      name: 'extraClassName',
      defaultValue: 'spinner-container'
    }
  ],

  constants: {
    DURATION: '1333'
  },

  methods: {
    initHTML: function() {
      this.SUPER();
      this.data = this.data;
    }
  },

  templates: [
    function CSS() {/*
      <% var prefixes = ['-webkit-', '-moz-', '']; %>
      <% var bezier = 'cubic-bezier(0.4, 0.0, 0.2, 1)'; %>
      .spinner-container {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 100%;
        width: 100%;
      }
      .spinner-fixed-box {
        position: relative;
        height: 64px;
        width: 64px;
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>transform: translate3d(0px, 0px, 0px);
        <% } %>
      }

      .spinner-turning-box {
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>animation: container-rotate 1568ms linear infinite;
        <% } %>
        width: 100%;
        height: 100%;
      }

      .spinner-layer {
        position: absolute;
        height: 100%;
        width: 100%;
        <% for (var j = 0; j < prefixes.length; j++) { %>
          <%= prefixes[j] %>animation: fill-unfill-rotate <%= 4*this.DURATION %>ms <%= bezier %> infinite both;
        <% } %>
      }

      .spinner-circle-clipper {
        overflow: hidden;
        border-color: inherit;
        display: inline-block;
        height: 100%;
        position: relative;
        width: 50%;
      }

      .spinner-circle-clipper.spinner-clipper-left .spinner-circle {
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>animation: left-spin <%= this.DURATION %>ms <%= bezier %> infinite;
          <%= prefixes[i] %>transform: rotate(129deg);
        <% } %>
        border-right-color: transparent !important;
      }

      .spinner-circle-clipper.spinner-clipper-right .spinner-circle {
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>animation: right-spin <%= this.DURATION %>ms <%= bezier %> infinite;
          <%= prefixes[i] %>transform: rotate(-129deg);
        <% } %>
        border-left-color: transparent !important;
        left: -100%
      }

      .spinner-circle-clipper .spinner-circle {
        width: 200%;
      }

      .spinner-circle {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        box-sizing: border-box;
        height: 100%;
        border-width: 4px;
        border-style: solid;
        border-color: inherit;
        border-bottom-color: transparent !important;
        border-radius: 50%;
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>animation: none;
        <% } %>
      }

      .spinner-gap-patch {
        position: absolute;
        box-sizing: border-box;
        top: 0;
        left: 45%;
        width: 10%;
        height: 100%;
        overflow: hidden;
        border-color: inherit;
      }

      .spinner-gap-patch .spinner-circle {
        width: 1000%;
        left: -450%;
      }

      <% for (var i = 0; i < prefixes.length; i++) { %>
        @<%= prefixes[i] %>keyframes fill-unfill-rotate {
          12.5% { <%= prefixes[i] %>transform: rotate(135deg); }
          25%   { <%= prefixes[i] %>transform: rotate(270deg); }
          37.5% { <%= prefixes[i] %>transform: rotate(405deg); }
          50%   { <%= prefixes[i] %>transform: rotate(540deg); }
          62.5% { <%= prefixes[i] %>transform: rotate(675deg); }
          75%   { <%= prefixes[i] %>transform: rotate(810deg); }
          87.5% { <%= prefixes[i] %>transform: rotate(945deg); }
          to    { <%= prefixes[i] %>transform: rotate(1080deg); }
        }

        @<%= prefixes[i] %>keyframes left-spin {
          from { <%= prefixes[i] %>transform: rotate(130deg); }
          50%  { <%= prefixes[i] %>transform: rotate(-5deg); }
          to   { <%= prefixes[i] %>transform: rotate(130deg); }
        }

        @<%= prefixes[i] %>keyframes right-spin {
          from { <%= prefixes[i] %>transform: rotate(-130deg); }
          50%  { <%= prefixes[i] %>transform: rotate(5deg); }
          to   { <%= prefixes[i] %>transform: rotate(-130deg); }
        }

        @<%= prefixes[i] %>keyframes container-rotate {
          to { <%= prefixes[i] %>transform: rotate(360deg); }
        }
      <% } %>
    */},
    function toInnerHTML() {/*

          <div class="spinner-layer" style="border-color: <%= this.color %>">

    */}
  ]
});
<commit_msg>SpinnerView: Add hideContainer option to make the spinner hide its outer box<commit_after>/**
 * @license
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.ui',
  name: 'SpinnerView',
  extendsModel: 'foam.ui.View',

  documentation: 'Renders a spinner in the Material Design style. Has a ' +
      '$$DOC{ref:".data"} property and acts like a $$DOC{ref:"BooleanView"}, ' +
      'that creates and destroys and the spinner when the value changes.',
  // TODO(braden): This spinner doesn't render on Firefox.
  properties: [
    {
      name: 'data',
      documentation: 'Defaults to true, so that the spinner will show itself ' +
          'by default, if data is not set.',
      defaultValue: true,
      postSet: function(old, nu) {
        if ( ! this.$ ) return;
        if ( ! nu ) {
          this.$.innerHTML = '';
          if ( this.hideContainer ) {
            this.$.style.display = 'none';
          }
        } else if ( ! old && nu ) {
          this.$.innerHTML = this.toInnerHTML();
          this.initInnerHTML();
          this.$.style.display = '';
        }
      }
    },
    {
      name: 'hideContainer',
      documentation: 'Set true to hide the outer container when data is false.',
      defaultValue: false
    },
    {
      name: 'color',
      documentation: 'The color to use for the spinner.',
      defaultValue: '#4285F4'
    },
    {
      name: 'extraClassName',
      defaultValue: 'spinner-container'
    }
  ],

  constants: {
    DURATION: '1333'
  },

  methods: {
    initHTML: function() {
      this.SUPER();
      this.data = this.data;
    }
  },

  templates: [
    function CSS() {/*
      <% var prefixes = ['-webkit-', '-moz-', '']; %>
      <% var bezier = 'cubic-bezier(0.4, 0.0, 0.2, 1)'; %>
      .spinner-container {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 100%;
        width: 100%;
      }
      .spinner-fixed-box {
        position: relative;
        height: 64px;
        width: 64px;
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>transform: translate3d(0px, 0px, 0px);
        <% } %>
      }

      .spinner-turning-box {
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>animation: container-rotate 1568ms linear infinite;
        <% } %>
        width: 100%;
        height: 100%;
      }

      .spinner-layer {
        position: absolute;
        height: 100%;
        width: 100%;
        <% for (var j = 0; j < prefixes.length; j++) { %>
          <%= prefixes[j] %>animation: fill-unfill-rotate <%= 4*this.DURATION %>ms <%= bezier %> infinite both;
        <% } %>
      }

      .spinner-circle-clipper {
        overflow: hidden;
        border-color: inherit;
        display: inline-block;
        height: 100%;
        position: relative;
        width: 50%;
      }

      .spinner-circle-clipper.spinner-clipper-left .spinner-circle {
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>animation: left-spin <%= this.DURATION %>ms <%= bezier %> infinite;
          <%= prefixes[i] %>transform: rotate(129deg);
        <% } %>
        border-right-color: transparent !important;
      }

      .spinner-circle-clipper.spinner-clipper-right .spinner-circle {
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>animation: right-spin <%= this.DURATION %>ms <%= bezier %> infinite;
          <%= prefixes[i] %>transform: rotate(-129deg);
        <% } %>
        border-left-color: transparent !important;
        left: -100%
      }

      .spinner-circle-clipper .spinner-circle {
        width: 200%;
      }

      .spinner-circle {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        box-sizing: border-box;
        height: 100%;
        border-width: 4px;
        border-style: solid;
        border-color: inherit;
        border-bottom-color: transparent !important;
        border-radius: 50%;
        <% for (var i = 0; i < prefixes.length; i++) { %>
          <%= prefixes[i] %>animation: none;
        <% } %>
      }

      .spinner-gap-patch {
        position: absolute;
        box-sizing: border-box;
        top: 0;
        left: 45%;
        width: 10%;
        height: 100%;
        overflow: hidden;
        border-color: inherit;
      }

      .spinner-gap-patch .spinner-circle {
        width: 1000%;
        left: -450%;
      }

      <% for (var i = 0; i < prefixes.length; i++) { %>
        @<%= prefixes[i] %>keyframes fill-unfill-rotate {
          12.5% { <%= prefixes[i] %>transform: rotate(135deg); }
          25%   { <%= prefixes[i] %>transform: rotate(270deg); }
          37.5% { <%= prefixes[i] %>transform: rotate(405deg); }
          50%   { <%= prefixes[i] %>transform: rotate(540deg); }
          62.5% { <%= prefixes[i] %>transform: rotate(675deg); }
          75%   { <%= prefixes[i] %>transform: rotate(810deg); }
          87.5% { <%= prefixes[i] %>transform: rotate(945deg); }
          to    { <%= prefixes[i] %>transform: rotate(1080deg); }
        }

        @<%= prefixes[i] %>keyframes left-spin {
          from { <%= prefixes[i] %>transform: rotate(130deg); }
          50%  { <%= prefixes[i] %>transform: rotate(-5deg); }
          to   { <%= prefixes[i] %>transform: rotate(130deg); }
        }

        @<%= prefixes[i] %>keyframes right-spin {
          from { <%= prefixes[i] %>transform: rotate(-130deg); }
          50%  { <%= prefixes[i] %>transform: rotate(5deg); }
          to   { <%= prefixes[i] %>transform: rotate(-130deg); }
        }

        @<%= prefixes[i] %>keyframes container-rotate {
          to { <%= prefixes[i] %>transform: rotate(360deg); }
        }
      <% } %>
    */},
    function toInnerHTML() {/*

          <div class="spinner-layer" style="border-color: <%= this.color %>">

    */}
  ]
});
<|endoftext|>