1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 
  9 /** @class
 10 
 11   A container view will display its "content" view as its only child.  You can
 12   use a container view to easily swap out views on your page.  In addition to
 13   displaying the actual view in the content property, you can also set the
 14   nowShowing property to the property path of a view in your page and the
 15   view will be found and swapped in for you.
 16 
 17   # Animated Transitions
 18 
 19   To animate the transition between views, you can provide a transitionSwap
 20   plugin to SC.ContainerView.  There are several common transitions pre-built
 21   and if you want to create your own, the SC.ViewTransitionProtocol defines the
 22   methods to implement.
 23 
 24   The transitions included with SC.ContainerView are:
 25 
 26     - SC.ContainerView.DISSOLVE - fades between the two views
 27     - SC.ContainerView.FADE_COLOR - fades out to a color and then in to the new view
 28     - SC.ContainerView.MOVE_IN - moves the new view in over top of the old view
 29     - SC.ContainerView.PUSH - pushes the old view out with the new view
 30     - SC.ContainerView.REVEAL - moves the old view out revealing the new view underneath
 31 
 32   To use a transitionSwap plugin, simply set it as the value of the container view's
 33   `transitionSwap` property.
 34 
 35   For example,
 36 
 37       container = SC.ContainerView.create({
 38         transitionSwap: SC.ContainerView.PUSH
 39       });
 40 
 41   Since each transitionSwap plugin predefines a unique animation, SC.ContainerView
 42   provides the transitionSwapOptions property to allow for modifications to the
 43   animation.
 44 
 45   For example,
 46 
 47       container = SC.ContainerView.create({
 48         transitionSwap: SC.ContainerView.PUSH,
 49         transitionSwapOptions: {
 50           duration: 1.25,    // Use a longer duration then default
 51           direction: 'up'    // Push the old content up
 52         }
 53       });
 54 
 55   All the predefined transitionSwap plugins take options to modify the default
 56   duration and timing of the animation and to see what other options are
 57   available, refer to the documentation of the plugin.
 58 
 59   @extends SC.View
 60   @since SproutCore 1.0
 61 */
 62 SC.ContainerView = SC.View.extend(
 63   /** @scope SC.ContainerView.prototype */ {
 64 
 65   // ------------------------------------------------------------------------
 66   // Properties
 67   //
 68 
 69   /**
 70     @type Array
 71     @default ['sc-container-view']
 72     @see SC.View#classNames
 73     @see SC.Object#concatenatedProperties
 74   */
 75   classNames: ['sc-container-view'],
 76 
 77   /**
 78     The content view to display.  This will become the only child view of
 79     the view.  Note that if you set the nowShowing property to any value other
 80     than 'null', the container view will automatically change the contentView
 81     to reflect view indicated by the value.
 82 
 83     @type SC.View
 84     @default null
 85   */
 86   contentView: null,
 87 
 88   /** @private */
 89   contentViewBindingDefault: SC.Binding.single(),
 90 
 91   /**
 92     Whether the container view is in the process of transitioning or not.
 93 
 94     You should observe this property in order to delay any updates to the new
 95     content until the transition is complete.
 96 
 97     @type Boolean
 98     @default false
 99     @since Version 1.10
100   */
101   isTransitioning: NO,
102 
103   /**
104     Optional path name for the content view.  Set this to a property path
105     pointing to the view you want to display.  This will automatically change
106     the content view for you. If you pass a relative property path or a single
107     property name, then the container view will look for it first on its page
108     object then relative to itself. If you pass a full property name
109     (e.g. "MyApp.anotherPage.anotherView"), then the path will be followed
110     from the top-level.
111 
112     @type String|SC.View
113     @default null
114   */
115   nowShowing: null,
116 
117   /** @private */
118   renderDelegateName: 'containerRenderDelegate',
119 
120   /**
121     The transitionSwap plugin to use when swapping views.
122 
123     SC.ContainerView uses a pluggable transition architecture where the
124     transition setup, animation and cleanup can be handled by a specified
125     transitionSwap plugin.
126 
127     There are a number of pre-built plugins available:
128 
129       SC.ContainerView.DISSOLVE
130       SC.ContainerView.FADE_COLOR
131       SC.ContainerView.MOVE_IN
132       SC.ContainerView.PUSH
133       SC.ContainerView.REVEAL
134 
135     You can even provide your own custom transitionSwap plugins.  Just create an
136     object that conforms to the SC.SwapTransitionProtocol protocol.
137 
138     @type Object (SC.SwapTransitionProtocol)
139     @default null
140     @since Version 1.10
141   */
142   transitionSwap: null,
143 
144   /**
145     The options for the given transitionSwap plugin.
146 
147     These options are specific to the current transitionSwap plugin used and are
148     used to modify the transition animation.  To determine what options
149     may be used for a given transition and to see what the default options are,
150     see the documentation for the transition plugin being used.
151 
152     Most transitions will accept a duration and timing option, but may
153     also use other options.  For example, SC.ContainerView.PUSH accepts options
154     like:
155 
156         transitionSwapOptions: {
157           direction: 'left',
158           duration: 0.25,
159           timing: 'linear'
160         }
161 
162     @type Object
163     @default null
164     @since Version 1.10
165   */
166   transitionSwapOptions: null,
167 
168   // ------------------------------------------------------------------------
169   // Methods
170   //
171 
172   /** @private */
173   init: function () {
174     var view;
175 
176     sc_super();
177 
178     if (this.get('nowShowing')) {
179       // If nowShowing is directly set, invoke the instantiation of
180       // it as well.
181       this.nowShowingDidChange();
182     } else {
183       // If contentView is directly set, then swap it into nowShowing so that it
184       // is properly instantiated and ready for swapping.
185       // Fixes: https://github.com/sproutcore/sproutcore/issues/1069
186       view = this.get('contentView');
187 
188       if (view) {
189         this.set('nowShowing', view);
190       }
191     }
192 
193     // Observe for changes to the content view and initialize once.
194     this.addObserver('contentView', this, this._sc_contentViewDidChange);
195     this._sc_contentViewDidChange();
196   },
197 
198   /** @private Cancels the active transition. */
199   _sc_cancelTransitions: function () {
200     var contentStatecharts = this._contentStatecharts;
201 
202     // Exit all the statecharts immediately. This mutates the array!
203     if (contentStatecharts) {
204       for (var i = contentStatecharts.length - 1; i >= 0; i--) {
205         contentStatecharts[i].doExit(true);
206       }
207     }
208   },
209 
210   /** @private
211     Overridden to prevent clipping of child views while animating.
212 
213     In particular, collection views have trouble being animated in a certain
214     manner if they think their clipping frame hides themself.  For example,
215     the PUSH transition returns a double width/height frame with an adjusted
216     left/top while the transition is in process so neither view thinks it
217     is clipped.
218    */
219   clippingFrame: function () {
220     var contentStatecharts = this._contentStatecharts,
221       frame = this.get('frame'),
222       ret = sc_super();
223 
224     // Allow for a modified clippingFrame while transitioning.
225     if (this.get('isTransitioning')) {
226       // Each transition may adjust the clippingFrame to accommodate itself.
227       for (var i = contentStatecharts.length - 1; i >= 0; i--) {
228         ret = contentStatecharts[i].transitionClippingFrame(ret);
229       }
230     } else {
231       ret.width = frame.width;
232     }
233 
234     return ret;
235   }.property('parentView', 'frame').cacheable(),
236 
237   /** @private
238     Invoked whenever the content property changes.  This method will simply
239     call replaceContent.  Override replaceContent to change how the view is
240     swapped out.
241   */
242   _sc_contentViewDidChange: function () {
243     var contentView = this.get('contentView');
244 
245     // If it's an uninstantiated view, then attempt to instantiate it.
246     if (contentView && contentView.kindOf(SC.CoreView)) {
247       contentView = this.createChildView(contentView);
248     }
249 
250     this.replaceContent(contentView);
251   },
252 
253   /** @private */
254   destroy: function () {
255     // Clean up observers.
256     this.removeObserver('contentView', this, this._sc_contentViewDidChange);
257 
258     // Cancel any active transitions.
259     // Note: this will also destroy any content view that the container created.
260     this._sc_cancelTransitions();
261 
262     // Remove our internal reference to the statecharts.
263     this._contentStatecharts = this._currentStatechart = null;
264 
265     return sc_super();
266   },
267 
268   /** @private
269     Invoked whenever the nowShowing property changes.  This will try to find
270     the new content if possible and set it.  If you set nowShowing to an
271     empty string or null, then the current content will be cleared.
272   */
273   nowShowingDidChange: function () {
274     // This code turns this.nowShowing into a view object by any means necessary.
275     var content = this.get('nowShowing');
276 
277     // If it's a string, try to turn it into the object it references...
278     if (SC.typeOf(content) === SC.T_STRING && content.length > 0) {
279       var dotspot = content.indexOf('.');
280       // No dot means a local property, either to this view or this view's page.
281       if (dotspot === -1) {
282         var tempContent = this.get(content);
283         content = SC.kindOf(tempContent, SC.CoreView) ? tempContent : SC.objectForPropertyPath(content, this.get('page'));
284       }
285       // Dot at beginning means local property path.
286       else if (dotspot === 0) {
287         content = this.getPath(content.slice(1));
288       }
289       // Dot after the beginning
290       else {
291         content = SC.objectForPropertyPath(content);
292       }
293     }
294 
295     // If it's an uninstantiated view, then attempt to instantiate it.
296     if (content && content.kindOf(SC.CoreView)) {
297       content = this.createChildView(content);
298     }
299 
300     //@if(debug)
301     // Prevent developers from assigning non-view content to a container.
302     if (content && !SC.kindOf(content, SC.CoreView)) {
303       SC.error("Developer Error: You should not assign non-View content to an SC.ContainerView.");
304       content = null;
305     }
306     //@endif
307 
308     // Sets the content.
309     this.set('contentView', content);
310   }.observes('nowShowing'),
311 
312   /** @private Called by new content statechart to indicate that it is ready. */
313   statechartReady: function () {
314     var contentStatecharts = this._contentStatecharts;
315 
316     // Exit all other remaining statecharts immediately.  This mutates the array!
317     // This allows transitions where the previous content is left in place to
318     // clean up all previous content once the new content transitions in.
319     for (var i = contentStatecharts.length - 2; i >= 0; i--) {
320       contentStatecharts[i].doExit(true);
321     }
322 
323     this.set('isTransitioning', NO);
324   },
325 
326   /** @private Called by content statecharts to indicate that they have exited. */
327   statechartEnded: function (statechart) {
328     var contentStatecharts = this._contentStatecharts;
329 
330     // Remove the statechart.
331     contentStatecharts.removeObject(statechart);
332 
333     // Once all the other statecharts have exited. Indicate that the current
334     // statechart is entered. This allows transitions where the new
335     // content is left in place to update state once all previous statecharts
336     // have exited.
337     if (contentStatecharts.length === 1) {
338       contentStatecharts[0].entered();
339     }
340   },
341 
342   /** @private
343     Replaces any child views with the passed new content.
344 
345     This method is automatically called whenever your contentView property
346     changes.  You can override it if you want to provide some behavior other
347     than the default.
348 
349     @param {SC.View} newContent the new content view or null.
350   */
351   replaceContent: function (newContent) {
352     var contentStatecharts,
353       currentStatechart = this._currentStatechart,
354       newStatechart;
355 
356     // Track that we are transitioning.
357     this.set('isTransitioning', YES);
358 
359     // Create a statechart for the new content.
360     contentStatecharts = this._contentStatecharts;
361     if (!contentStatecharts) { contentStatecharts = this._contentStatecharts = []; }
362 
363     // Call doExit on all current content statecharts.  Any statecharts in the
364     // process of exiting may accelerate their exits.
365     for (var i = contentStatecharts.length - 1; i >= 0; i--) {
366       var found = contentStatecharts[i].doExit(false, newContent);
367 
368       // If the content already belongs to a content statechart reuse that statechart.
369       if (found) {
370         newStatechart = contentStatecharts[i];
371         newStatechart.set('previousStatechart', currentStatechart);
372         newStatechart.gotoEnteringState();
373       }
374     }
375 
376     // Add the new content statechart, which will enter automatically.
377     if (!newStatechart) {
378       newStatechart = SC.ContainerContentStatechart.create({
379         container: this,
380         content: newContent,
381         previousStatechart: currentStatechart
382       });
383 
384       contentStatecharts.pushObject(newStatechart);
385     }
386 
387     // Track the current statechart.
388     this._currentStatechart = newStatechart;
389   },
390 
391   /** @private SC.Observable.prototype */
392   set: function (key, value) {
393 
394     // Changing the transitionSwap in the middle of a transition must cancel the transitions.
395     if (key === 'transitionSwap' && this.get('isTransitioning')) {
396       //@if(debug)
397       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));
398       //@endif
399 
400       // Cancel the active transitions.
401       this._sc_cancelTransitions();
402     }
403 
404     return sc_super();
405   }
406 
407 });
408 
409 
410 // When in debug mode, core developers can log the container content states.
411 //@if(debug)
412 SC.LOG_CONTAINER_CONTENT_STATES = false;
413 //@endif
414 
415 /** @private
416   In order to support transitioning views in and out of the container view,
417   each content view needs its own simple statechart.  This is required, because
418   while only one view will ever be transitioning in, several views may be in
419   the process of transitioning out.  See the 'SC.ContainerView Statechart.graffle'
420   file in the repository.
421 */
422 SC.ContainerContentStatechart = SC.Object.extend({
423 
424   // ------------------------------------------------------------------------
425   // Properties
426   //
427 
428   container: null,
429 
430   content: null,
431 
432   previousStatechart: null,
433 
434   state: 'none',
435 
436   // ------------------------------------------------------------------------
437   // Methods
438   //
439 
440   init: function () {
441     sc_super();
442 
443     // Default entry state.
444     this.gotoEnteringState();
445   },
446 
447   transitionClippingFrame: function (clippingFrame) {
448     var container = this.get('container'),
449       options = container.get('transitionSwapOptions') || {},
450       transitionSwap = container.get('transitionSwap');
451 
452     if (transitionSwap && transitionSwap.transitionClippingFrame) {
453       return transitionSwap.transitionClippingFrame(container, clippingFrame, options);
454     } else {
455       return clippingFrame;
456     }
457   },
458 
459   // ------------------------------------------------------------------------
460   // Actions & Events
461   //
462 
463   entered: function () {
464     //@if(debug)
465     if (SC.LOG_CONTAINER_CONTENT_STATES) {
466       var container = this.get('container'),
467         content = this.get('content');
468 
469       SC.Logger.log('%@ (%@)(%@, %@) — entered callback'.fmt(this, this.state, container, content));
470     }
471     //@endif
472 
473     if (this.state === 'entering') {
474       this.gotoReadyState();
475     }
476   },
477 
478   doExit: function (immediately, newContent) {
479     if (this.state !== 'exited') {
480       this.gotoExitingState(immediately, newContent);
481     //@if(debug)
482     } else {
483       throw new Error('Developer Error: SC.ContainerView should not receive an internal doExit event while in exited state.');
484     //@endif
485     }
486 
487     // If the new content matches our own content, indicate this to the container.
488     if (this.get('content') === newContent) {
489       return true;
490     } else {
491       return false;
492     }
493   },
494 
495   exited: function () {
496     //@if(debug)
497     if (SC.LOG_CONTAINER_CONTENT_STATES) {
498       var container = this.get('container'),
499         content = this.get('content');
500 
501       SC.Logger.log('%@ (%@)(%@, %@) — exited callback'.fmt(this, this.state, container, content));
502     }
503     //@endif
504 
505     if (this.state === 'exiting') {
506       this.gotoExitedState();
507     }
508   },
509 
510   // ------------------------------------------------------------------------
511   // States
512   //
513 
514   // Entering
515   gotoEnteringState: function () {
516     var container = this.get('container'),
517       content = this.get('content'),
518       previousStatechart = this.get('previousStatechart'),
519       options = container.get('transitionSwapOptions') || {},
520       transitionSwap = container.get('transitionSwap');
521 
522     //@if(debug)
523     if (SC.LOG_CONTAINER_CONTENT_STATES) {
524       SC.Logger.log('%@ (%@)(%@, %@) — Entering (Previous: %@)'.fmt(this, this.state, container, content, previousStatechart));
525     }
526     //@endif
527 
528     // If currently in the exiting state, reverse to entering.
529     if (this.state === 'exiting' && transitionSwap.reverseBuildOut) {
530       transitionSwap.reverseBuildOut(this, container, content, options);
531 
532       // Assign the state.
533       this.set('state', 'entering');
534 
535       // Fast path!!
536       return;
537     } else if (content) {
538       container.appendChild(content);
539     }
540 
541     // Assign the state.
542     this.set('state', 'entering');
543 
544     // Don't transition unless there is a previous statechart.
545     if (previousStatechart && content && transitionSwap) {
546       if (transitionSwap.willBuildInToView) {
547         transitionSwap.willBuildInToView(container, content, previousStatechart, options);
548       }
549 
550       if (transitionSwap.buildInToView) {
551         transitionSwap.buildInToView(this, container, content, previousStatechart, options);
552       } else {
553         this.entered();
554       }
555     } else {
556       this.entered();
557     }
558   },
559 
560   // Exiting
561   gotoExitingState: function (immediately) {
562     var container = this.get('container'),
563       content = this.get('content'),
564       exitCount = this._exitCount,
565       options = container.get('transitionSwapOptions') || {},
566       transitionSwap = container.get('transitionSwap');
567 
568     //@if(debug)
569     if (SC.LOG_CONTAINER_CONTENT_STATES) {
570       if (!exitCount) { exitCount = this._exitCount = 1; }
571       SC.Logger.log('%@ (%@)(%@, %@) — Exiting (x%@)'.fmt(this, this.state, container, content, this._exitCount));
572     }
573     //@endif
574 
575     // If currently in the entering state, reverse to exiting.
576     if (this.state === 'entering' && transitionSwap.reverseBuildIn) {
577       transitionSwap.reverseBuildIn(this, container, content, options);
578 
579       // Assign the state.
580       this.set('state', 'exiting');
581 
582       // Fast path!!
583       return;
584     }
585 
586     // Assign the state.
587     this.set('state', 'exiting');
588 
589     if (!immediately && content && transitionSwap) {
590       // Re-entering the exiting state may need to accelerate the transition, pass the count to the plugin.
591       if (!exitCount) { exitCount = this._exitCount = 1; }
592 
593       if (transitionSwap.willBuildOutFromView) {
594         transitionSwap.willBuildOutFromView(container, content, options, exitCount);
595       }
596 
597       if (transitionSwap.buildOutFromView) {
598         transitionSwap.buildOutFromView(this, container, content, options, exitCount);
599       } else {
600         // this.exited();
601       }
602 
603       // Increment the exit count each time doExit is called.
604       this._exitCount += 1;
605     } else {
606       this.exited();
607     }
608   },
609 
610   // Exited
611   gotoExitedState: function () {
612     var container = this.get('container'),
613       content = this.get('content'),
614       options = container.get('transitionSwapOptions') || {},
615       transitionSwap = container.get('transitionSwap');
616 
617     //@if(debug)
618     if (SC.LOG_CONTAINER_CONTENT_STATES) {
619       SC.Logger.log('%@ (%@)(%@, %@) — Exited'.fmt(this, this.state, container, content));
620     }
621     //@endif
622 
623     if (content) {
624       if (transitionSwap && transitionSwap.didBuildOutFromView) {
625         transitionSwap.didBuildOutFromView(container, content, options);
626       }
627 
628       if (content.createdByParent) {
629         container.removeChildAndDestroy(content);
630       } else {
631         container.removeChild(content);
632       }
633     }
634 
635     // Send ended event to container view statechart.
636     container.statechartEnded(this);
637 
638     // Reset the exiting count.
639     this._exitCount = 0;
640 
641     // Assign the state.
642     this.set('state', 'exited');
643   },
644 
645   // Ready
646   gotoReadyState: function () {
647     var container = this.get('container'),
648       content = this.get('content'),
649       options = container.get('transitionSwapOptions') || {},
650       transitionSwap = container.get('transitionSwap');
651 
652     //@if(debug)
653     if (SC.LOG_CONTAINER_CONTENT_STATES) {
654       SC.Logger.log('%@ (%@)(%@, %@) — Entered'.fmt(this, this.state, container, content));
655     }
656     //@endif
657 
658     if (content && transitionSwap && transitionSwap.didBuildInToView) {
659       transitionSwap.didBuildInToView(container, content, options);
660     }
661 
662     // Send ready event to container view statechart.
663     container.statechartReady();
664 
665     // Assign the state.
666     this.set('state', 'ready');
667   }
668 
669 });
670