// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ==========================================================================

/** @class

A container view will display its "content" view as its only child.  You can
use a container view to easily swap out views on your page.  In addition to
displaying the actual view in the content property, you can also set the
nowShowing property to the property path of a view in your page and the
view will be found and swapped in for you.

# Animated Transitions

To animate the transition between views, you can provide a transitionSwap
plugin to SC.ContainerView.  There are several common transitions pre-built
and if you want to create your own, the SC.ViewTransitionProtocol defines the
methods to implement.

The transitions included with SC.ContainerView are:

  - SC.ContainerView.DISSOLVE - fades between the two views
  - SC.ContainerView.FADE_COLOR - fades out to a color and then in to the new view
  - SC.ContainerView.MOVE_IN - moves the new view in over top of the old view
  - SC.ContainerView.PUSH - pushes the old view out with the new view
  - SC.ContainerView.REVEAL - moves the old view out revealing the new view underneath

To use a transitionSwap plugin, simply set it as the value of the container view's
`transitionSwap` property.

For example,

    container = SC.ContainerView.create({
      transitionSwap: SC.ContainerView.PUSH
    });

Since each transitionSwap plugin predefines a unique animation, SC.ContainerView
provides the transitionSwapOptions property to allow for modifications to the
animation.

For example,

    container = SC.ContainerView.create({
      transitionSwap: SC.ContainerView.PUSH,
      transitionSwapOptions: {
        duration: 1.25,    // Use a longer duration then default
        direction: 'up'    // Push the old content up
      }
    });

All the predefined transitionSwap plugins take options to modify the default
duration and timing of the animation and to see what other options are
available, refer to the documentation of the plugin.

@extends SC.View
@since SproutCore 1.0

*/ SC.ContainerView = SC.View.extend(

/** @scope SC.ContainerView.prototype */ {

// ------------------------------------------------------------------------
// Properties
//

/**
  @type Array
  @default ['sc-container-view']
  @see SC.View#classNames
  @see SC.Object#concatenatedProperties
*/
classNames: ['sc-container-view'],

/**
  The content view to display.  This will become the only child view of
  the view.  Note that if you set the nowShowing property to any value other
  than 'null', the container view will automatically change the contentView
  to reflect view indicated by the value.

  @type SC.View
  @default null
*/
contentView: null,

/** @private */
contentViewBindingDefault: SC.Binding.single(),

/**
  Whether the container view is in the process of transitioning or not.

  You should observe this property in order to delay any updates to the new
  content until the transition is complete.

  @type Boolean
  @default false
  @since Version 1.10
*/
isTransitioning: NO,

/**
  Optional path name for the content view.  Set this to a property path
  pointing to the view you want to display.  This will automatically change
  the content view for you. If you pass a relative property path or a single
  property name, then the container view will look for it first on its page
  object then relative to itself. If you pass a full property name
  (e.g. "MyApp.anotherPage.anotherView"), then the path will be followed
  from the top-level.

  @type String|SC.View
  @default null
*/
nowShowing: null,

/** @private */
renderDelegateName: 'containerRenderDelegate',

/**
  The transitionSwap plugin to use when swapping views.

  SC.ContainerView uses a pluggable transition architecture where the
  transition setup, animation and cleanup can be handled by a specified
  transitionSwap plugin.

  There are a number of pre-built plugins available:

    SC.ContainerView.DISSOLVE
    SC.ContainerView.FADE_COLOR
    SC.ContainerView.MOVE_IN
    SC.ContainerView.PUSH
    SC.ContainerView.REVEAL

  You can even provide your own custom transitionSwap plugins.  Just create an
  object that conforms to the SC.SwapTransitionProtocol protocol.

  @type Object (SC.SwapTransitionProtocol)
  @default null
  @since Version 1.10
*/
transitionSwap: null,

/**
  The options for the given transitionSwap plugin.

  These options are specific to the current transitionSwap plugin used and are
  used to modify the transition animation.  To determine what options
  may be used for a given transition and to see what the default options are,
  see the documentation for the transition plugin being used.

  Most transitions will accept a duration and timing option, but may
  also use other options.  For example, SC.ContainerView.PUSH accepts options
  like:

      transitionSwapOptions: {
        direction: 'left',
        duration: 0.25,
        timing: 'linear'
      }

  @type Object
  @default null
  @since Version 1.10
*/
transitionSwapOptions: null,

// ------------------------------------------------------------------------
// Methods
//

/** @private */
init: function () {
  var view;

  sc_super();

  if (this.get('nowShowing')) {
    // If nowShowing is directly set, invoke the instantiation of
    // it as well.
    this.nowShowingDidChange();
  } else {
    // If contentView is directly set, then swap it into nowShowing so that it
    // is properly instantiated and ready for swapping.
    // Fixes: https://github.com/sproutcore/sproutcore/issues/1069
    view = this.get('contentView');

    if (view) {
      this.set('nowShowing', view);
    }
  }

  // Observe for changes to the content view and initialize once.
  this.addObserver('contentView', this, this._sc_contentViewDidChange);
  this._sc_contentViewDidChange();
},

/** @private Cancels the active transition. */
_sc_cancelTransitions: function () {
  var contentStatecharts = this._contentStatecharts;

  // Exit all the statecharts immediately. This mutates the array!
  if (contentStatecharts) {
    for (var i = contentStatecharts.length - 1; i >= 0; i--) {
      contentStatecharts[i].doExit(true);
    }
  }
},

/** @private
  Overridden to prevent clipping of child views while animating.

  In particular, collection views have trouble being animated in a certain
  manner if they think their clipping frame hides themself.  For example,
  the PUSH transition returns a double width/height frame with an adjusted
  left/top while the transition is in process so neither view thinks it
  is clipped.
 */
clippingFrame: function () {
  var contentStatecharts = this._contentStatecharts,
    frame = this.get('frame'),
    ret = sc_super();

  // Allow for a modified clippingFrame while transitioning.
  if (this.get('isTransitioning')) {
    // Each transition may adjust the clippingFrame to accommodate itself.
    for (var i = contentStatecharts.length - 1; i >= 0; i--) {
      ret = contentStatecharts[i].transitionClippingFrame(ret);
    }
  } else {
    ret.width = frame.width;
  }

  return ret;
}.property('parentView', 'frame').cacheable(),

/** @private
  Invoked whenever the content property changes.  This method will simply
  call replaceContent.  Override replaceContent to change how the view is
  swapped out.
*/
_sc_contentViewDidChange: function () {
  var contentView = this.get('contentView');

  // If it's an uninstantiated view, then attempt to instantiate it.
  if (contentView && contentView.kindOf(SC.CoreView)) {
    contentView = this.createChildView(contentView);
  }

  this.replaceContent(contentView);
},

/** @private */
destroy: function () {
  // Clean up observers.
  this.removeObserver('contentView', this, this._sc_contentViewDidChange);

  // Cancel any active transitions.
  // Note: this will also destroy any content view that the container created.
  this._sc_cancelTransitions();

  // Remove our internal reference to the statecharts.
  this._contentStatecharts = this._currentStatechart = null;

  return sc_super();
},

/** @private
  Invoked whenever the nowShowing property changes.  This will try to find
  the new content if possible and set it.  If you set nowShowing to an
  empty string or null, then the current content will be cleared.
*/
nowShowingDidChange: function () {
  // This code turns this.nowShowing into a view object by any means necessary.
  var content = this.get('nowShowing');

  // If it's a string, try to turn it into the object it references...
  if (SC.typeOf(content) === SC.T_STRING && content.length > 0) {
    var dotspot = content.indexOf('.');
    // No dot means a local property, either to this view or this view's page.
    if (dotspot === -1) {
      var tempContent = this.get(content);
      content = SC.kindOf(tempContent, SC.CoreView) ? tempContent : SC.objectForPropertyPath(content, this.get('page'));
    }
    // Dot at beginning means local property path.
    else if (dotspot === 0) {
      content = this.getPath(content.slice(1));
    }
    // Dot after the beginning
    else {
      content = SC.objectForPropertyPath(content);
    }
  }

  // If it's an uninstantiated view, then attempt to instantiate it.
  if (content && content.kindOf(SC.CoreView)) {
    content = this.createChildView(content);
  }

  //@if(debug)
  // Prevent developers from assigning non-view content to a container.
  if (content && !SC.kindOf(content, SC.CoreView)) {
    SC.error("Developer Error: You should not assign non-View content to an SC.ContainerView.");
    content = null;
  }
  //@endif

  // Sets the content.
  this.set('contentView', content);
}.observes('nowShowing'),

/** @private Called by new content statechart to indicate that it is ready. */
statechartReady: function () {
  var contentStatecharts = this._contentStatecharts;

  // Exit all other remaining statecharts immediately.  This mutates the array!
  // This allows transitions where the previous content is left in place to
  // clean up all previous content once the new content transitions in.
  for (var i = contentStatecharts.length - 2; i >= 0; i--) {
    contentStatecharts[i].doExit(true);
  }

  this.set('isTransitioning', NO);
},

/** @private Called by content statecharts to indicate that they have exited. */
statechartEnded: function (statechart) {
  var contentStatecharts = this._contentStatecharts;

  // Remove the statechart.
  contentStatecharts.removeObject(statechart);

  // Once all the other statecharts have exited. Indicate that the current
  // statechart is entered. This allows transitions where the new
  // content is left in place to update state once all previous statecharts
  // have exited.
  if (contentStatecharts.length === 1) {
    contentStatecharts[0].entered();
  }
},

/** @private
  Replaces any child views with the passed new content.

  This method is automatically called whenever your contentView property
  changes.  You can override it if you want to provide some behavior other
  than the default.

  @param {SC.View} newContent the new content view or null.
*/
replaceContent: function (newContent) {
  var contentStatecharts,
    currentStatechart = this._currentStatechart,
    newStatechart;

  // Track that we are transitioning.
  this.set('isTransitioning', YES);

  // Create a statechart for the new content.
  contentStatecharts = this._contentStatecharts;
  if (!contentStatecharts) { contentStatecharts = this._contentStatecharts = []; }

  // Call doExit on all current content statecharts.  Any statecharts in the
  // process of exiting may accelerate their exits.
  for (var i = contentStatecharts.length - 1; i >= 0; i--) {
    var found = contentStatecharts[i].doExit(false, newContent);

    // If the content already belongs to a content statechart reuse that statechart.
    if (found) {
      newStatechart = contentStatecharts[i];
      newStatechart.set('previousStatechart', currentStatechart);
      newStatechart.gotoEnteringState();
    }
  }

  // Add the new content statechart, which will enter automatically.
  if (!newStatechart) {
    newStatechart = SC.ContainerContentStatechart.create({
      container: this,
      content: newContent,
      previousStatechart: currentStatechart
    });

    contentStatecharts.pushObject(newStatechart);
  }

  // Track the current statechart.
  this._currentStatechart = newStatechart;
},

/** @private SC.Observable.prototype */
set: function (key, value) {

  // Changing the transitionSwap in the middle of a transition must cancel the transitions.
  if (key === 'transitionSwap' && this.get('isTransitioning')) {
    //@if(debug)
    SC.warn("Developer Warning: You should not change the value of transitionSwap on %@ while the container view is transitioning. The transition was cancelled.".fmt(this));
    //@endif

    // Cancel the active transitions.
    this._sc_cancelTransitions();
  }

  return sc_super();
}

});

// When in debug mode, core developers can log the container content states. //@if(debug) SC.LOG_CONTAINER_CONTENT_STATES = false; //@endif

/** @private

In order to support transitioning views in and out of the container view,
each content view needs its own simple statechart.  This is required, because
while only one view will ever be transitioning in, several views may be in
the process of transitioning out.  See the 'SC.ContainerView Statechart.graffle'
file in the repository.

*/ SC.ContainerContentStatechart = SC.Object.extend({

// ------------------------------------------------------------------------
// Properties
//

container: null,

content: null,

previousStatechart: null,

state: 'none',

// ------------------------------------------------------------------------
// Methods
//

init: function () {
  sc_super();

  // Default entry state.
  this.gotoEnteringState();
},

transitionClippingFrame: function (clippingFrame) {
  var container = this.get('container'),
    options = container.get('transitionSwapOptions') || {},
    transitionSwap = container.get('transitionSwap');

  if (transitionSwap && transitionSwap.transitionClippingFrame) {
    return transitionSwap.transitionClippingFrame(container, clippingFrame, options);
  } else {
    return clippingFrame;
  }
},

// ------------------------------------------------------------------------
// Actions & Events
//

entered: function () {
  //@if(debug)
  if (SC.LOG_CONTAINER_CONTENT_STATES) {
    var container = this.get('container'),
      content = this.get('content');

    SC.Logger.log('%@ (%@)(%@, %@) — entered callback'.fmt(this, this.state, container, content));
  }
  //@endif

  if (this.state === 'entering') {
    this.gotoReadyState();
  }
},

doExit: function (immediately, newContent) {
  if (this.state !== 'exited') {
    this.gotoExitingState(immediately, newContent);
  //@if(debug)
  } else {
    throw new Error('Developer Error: SC.ContainerView should not receive an internal doExit event while in exited state.');
  //@endif
  }

  // If the new content matches our own content, indicate this to the container.
  if (this.get('content') === newContent) {
    return true;
  } else {
    return false;
  }
},

exited: function () {
  //@if(debug)
  if (SC.LOG_CONTAINER_CONTENT_STATES) {
    var container = this.get('container'),
      content = this.get('content');

    SC.Logger.log('%@ (%@)(%@, %@) — exited callback'.fmt(this, this.state, container, content));
  }
  //@endif

  if (this.state === 'exiting') {
    this.gotoExitedState();
  }
},

// ------------------------------------------------------------------------
// States
//

// Entering
gotoEnteringState: function () {
  var container = this.get('container'),
    content = this.get('content'),
    previousStatechart = this.get('previousStatechart'),
    options = container.get('transitionSwapOptions') || {},
    transitionSwap = container.get('transitionSwap');

  //@if(debug)
  if (SC.LOG_CONTAINER_CONTENT_STATES) {
    SC.Logger.log('%@ (%@)(%@, %@) — Entering (Previous: %@)'.fmt(this, this.state, container, content, previousStatechart));
  }
  //@endif

  // If currently in the exiting state, reverse to entering.
  if (this.state === 'exiting' && transitionSwap.reverseBuildOut) {
    transitionSwap.reverseBuildOut(this, container, content, options);

    // Assign the state.
    this.set('state', 'entering');

    // Fast path!!
    return;
  } else if (content) {
    container.appendChild(content);
  }

  // Assign the state.
  this.set('state', 'entering');

  // Don't transition unless there is a previous statechart.
  if (previousStatechart && content && transitionSwap) {
    if (transitionSwap.willBuildInToView) {
      transitionSwap.willBuildInToView(container, content, previousStatechart, options);
    }

    if (transitionSwap.buildInToView) {
      transitionSwap.buildInToView(this, container, content, previousStatechart, options);
    } else {
      this.entered();
    }
  } else {
    this.entered();
  }
},

// Exiting
gotoExitingState: function (immediately) {
  var container = this.get('container'),
    content = this.get('content'),
    exitCount = this._exitCount,
    options = container.get('transitionSwapOptions') || {},
    transitionSwap = container.get('transitionSwap');

  //@if(debug)
  if (SC.LOG_CONTAINER_CONTENT_STATES) {
    if (!exitCount) { exitCount = this._exitCount = 1; }
    SC.Logger.log('%@ (%@)(%@, %@) — Exiting (x%@)'.fmt(this, this.state, container, content, this._exitCount));
  }
  //@endif

  // If currently in the entering state, reverse to exiting.
  if (this.state === 'entering' && transitionSwap.reverseBuildIn) {
    transitionSwap.reverseBuildIn(this, container, content, options);

    // Assign the state.
    this.set('state', 'exiting');

    // Fast path!!
    return;
  }

  // Assign the state.
  this.set('state', 'exiting');

  if (!immediately && content && transitionSwap) {
    // Re-entering the exiting state may need to accelerate the transition, pass the count to the plugin.
    if (!exitCount) { exitCount = this._exitCount = 1; }

    if (transitionSwap.willBuildOutFromView) {
      transitionSwap.willBuildOutFromView(container, content, options, exitCount);
    }

    if (transitionSwap.buildOutFromView) {
      transitionSwap.buildOutFromView(this, container, content, options, exitCount);
    } else {
      // this.exited();
    }

    // Increment the exit count each time doExit is called.
    this._exitCount += 1;
  } else {
    this.exited();
  }
},

// Exited
gotoExitedState: function () {
  var container = this.get('container'),
    content = this.get('content'),
    options = container.get('transitionSwapOptions') || {},
    transitionSwap = container.get('transitionSwap');

  //@if(debug)
  if (SC.LOG_CONTAINER_CONTENT_STATES) {
    SC.Logger.log('%@ (%@)(%@, %@) — Exited'.fmt(this, this.state, container, content));
  }
  //@endif

  if (content) {
    if (transitionSwap && transitionSwap.didBuildOutFromView) {
      transitionSwap.didBuildOutFromView(container, content, options);
    }

    if (content.createdByParent) {
      container.removeChildAndDestroy(content);
    } else {
      container.removeChild(content);
    }
  }

  // Send ended event to container view statechart.
  container.statechartEnded(this);

  // Reset the exiting count.
  this._exitCount = 0;

  // Assign the state.
  this.set('state', 'exited');
},

// Ready
gotoReadyState: function () {
  var container = this.get('container'),
    content = this.get('content'),
    options = container.get('transitionSwapOptions') || {},
    transitionSwap = container.get('transitionSwap');

  //@if(debug)
  if (SC.LOG_CONTAINER_CONTENT_STATES) {
    SC.Logger.log('%@ (%@)(%@, %@) — Entered'.fmt(this, this.state, container, content));
  }
  //@endif

  if (content && transitionSwap && transitionSwap.didBuildInToView) {
    transitionSwap.didBuildInToView(container, content, options);
  }

  // Send ready event to container view statechart.
  container.statechartReady();

  // Assign the state.
  this.set('state', 'ready');
}

});