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