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 sc_require('views/view'); 9 sc_require('views/view/acceleration'); 10 sc_require('views/view/cursor'); 11 sc_require('views/view/enabled'); 12 sc_require('views/view/keyboard'); 13 sc_require('views/view/layout'); 14 sc_require('views/view/manipulation'); 15 sc_require('views/view/theming'); 16 sc_require('views/view/touch'); 17 sc_require('views/view/visibility'); 18 sc_require('mixins/responder_context'); 19 20 21 /** 22 Indicates a value has a mixed state of both on and off. 23 24 @type String 25 */ 26 SC.MIXED_STATE = '__MIXED__' ; 27 28 /** @class 29 A Pane is like a regular view except that it does not need to live within a 30 parent view. You usually use a Pane to form the root of a view hierarchy in 31 your application, such as your main application view or for floating 32 palettes, popups, menus, etc. 33 34 Usually you will not work directly with the SC.Pane class, but with one of 35 its subclasses such as SC.MainPane, SC.Panel, or SC.PopupPane. 36 37 ## Showing a Pane 38 39 To make a pane visible, you need to add it to your HTML document. The 40 simplest way to do this is to call the append() method: 41 42 myPane = SC.Pane.create(); 43 myPane.append(); // adds the pane to the document 44 45 This will insert your pane into the end of your HTML document body, causing 46 it to display on screen. It will also register your pane with the 47 SC.RootResponder for the document so you can start to receive keyboard, 48 mouse, and touch events. 49 50 If you need more specific control for where you pane appears in the 51 document, you can use several other insertion methods such as appendTo(), 52 prependTo(), before() and after(). These methods all take a an element to 53 indicate where in your HTML document you would like you pane to be inserted. 54 55 Once a pane is inserted into the document, it will be sized and positioned 56 according to the layout you have specified. It will then automatically 57 resize with the window if needed, relaying resize notifications to children 58 as well. 59 60 ## Hiding a Pane 61 62 When you are finished with a pane, you can hide the pane by calling the 63 remove() method. This method will actually remove the Pane from the 64 document body, as well as deregistering it from the RootResponder so that it 65 no longer receives events. 66 67 The isVisibleInWindow method will also change to NO for the Pane and all of 68 its childViews and the views will no longer have their updateDisplay methods 69 called. 70 71 You can readd a pane to the document again any time in the future by using 72 any of the insertion methods defined in the previous section. 73 74 ## Receiving Events 75 76 Your pane and its child views will automatically receive any mouse or touch 77 events as long as it is on the screen. To receive keyboard events, however, 78 you must focus the keyboard on your pane by calling makeKeyPane() on the 79 pane itself. This will cause the RootResponder to route keyboard events to 80 your pane. The pane, in turn, will route those events to its current 81 keyView, if there is any. 82 83 Note that all SC.Views (anything that implements SC.ClassicResponder, 84 really) will be notified when it is about or gain or lose keyboard focus. 85 These notifications are sent both when the view is made keyView of a 86 particular pane and when the pane is made keyPane for the entire 87 application. 88 89 You can prevent your Pane from becoming key by setting the acceptsKeyPane 90 to NO on the pane. This is useful when creating palettes and other popups 91 that should not steal keyboard control from another view. 92 93 @extends SC.View 94 @extends SC.ResponderContext 95 @since SproutCore 1.0 96 */ 97 SC.Pane = SC.View.extend(SC.ResponderContext, 98 /** @scope SC.Pane.prototype */ { 99 100 /** 101 Returns YES for easy detection of when you reached the pane. 102 @type Boolean 103 */ 104 isPane: YES, 105 106 /** 107 Set to the current page when the pane is instantiated from a page object. 108 @property {SC.Page} 109 */ 110 page: null, 111 112 // ....................................................... 113 // ROOT RESPONDER SUPPORT 114 // 115 116 /** 117 The rootResponder for this pane. Whenever you add a pane to a document, 118 this property will be set to the rootResponder that is now forwarding 119 events to the pane. 120 121 @property {SC.Responder} 122 */ 123 rootResponder: null, 124 125 /** 126 Attempts to send the specified event up the responder chain for this pane. This 127 method is used by the RootResponder to correctly delegate mouse, touch and keyboard 128 events. You can also use it to send your own events to the pane's responders, though 129 you will usually not do this. 130 131 A responder chain is a linked list of responders - mostly views - which are each 132 sequentially given an opportunity to handle the event. The responder chain begins with 133 the event's `target` view, and proceeds up the chain of parentViews (via the customizable 134 nextResponder property) until it reaches the pane and its defaultResponder. You can 135 specify the `target` responder; by default, it is the pane's current `firstResponder` 136 (see SC.View keyboard event documentation for more on the first responder). 137 138 Beginning with the target, each responder is given the chance to handle the named event. 139 In order to handle an event, a responder must implement a method with the name of the 140 event. For example, to handle the mouseDown event, expose a `mouseDown` method. If a 141 responder handles a method, then the event will stop bubbling up the responder chain. 142 (If your responder exposes a handler method but you do not always want to handle that 143 method, you can signal that the method should continue bubbling up the responder chain by 144 returning NO from your handler.) 145 146 In some rare cases, you may want to only alert part of the responder chain. For example, 147 SC.ScrollView uses this to capture a touch to give the user a moment to begin scrolling 148 on otherwise-tappable controls. To accomplish this, pass a view (or responder) as the 149 `untilResponder` argument. If the responder chain includes this view, it will break the 150 chain there and not proceed. (Note that the `untilResponder` object will not be given a 151 chance to respond to the event.) 152 153 @param {String} action The name of the event (i.e. method name) to invoke. 154 @param {SC.Event} evt The optional event object. 155 @param {SC.Responder} target The responder chain's first member. If not specified, will 156 use the pane's current firstResponder instead. 157 @param {SC.Responder} untilResponder If specified, the responder chain will break when 158 this object is reached, preventing it and subsequent responders from receiving 159 the event. 160 @returns {Object} object that handled the event 161 */ 162 sendEvent: function(action, evt, target, untilResponder) { 163 // Until there's time for a refactor of this method, note the early return for untilResponder, marked 164 // below with "FAST PATH". 165 166 // walk up the responder chain looking for a method to handle the event 167 if (!target) target = this.get('firstResponder') ; 168 while(target) { 169 if (action === 'touchStart') { 170 // first, we must check that the target is not already touch responder 171 // if it is, we don't want to have "found" it; that kind of recursion is sure to 172 // cause really severe, and even worse, really odd bugs. 173 if (evt.touchResponder === target) { 174 target = null; 175 break; 176 } 177 178 // now, only pass along if the target does not already have any touches, or is 179 // capable of accepting multitouch. 180 if (!target.get("hasTouch") || target.get("acceptsMultitouch")) { 181 if (target.tryToPerform("touchStart", evt)) break; 182 } 183 } else if (action === 'touchEnd' && !target.get("acceptsMultitouch")) { 184 if (!target.get("hasTouch")) { 185 if (target.tryToPerform("touchEnd", evt)) break; 186 } 187 } else { 188 if (target.tryToPerform(action, evt)) break; 189 } 190 191 // If we've reached the pane, we're at the end of the chain. 192 target = (target === this) ? null : target.get('nextResponder'); 193 // FAST PATH: If we've reached untilResponder, break the chain. (TODO: refactor out this early return. The 194 // point is to avoid pinging defaultResponder if we ran into the untilResponder.) 195 if (target === untilResponder) { 196 return (evt && evt.mouseHandler) || null; 197 } 198 } 199 200 // if no handler was found in the responder chain, try the default 201 if (!target && (target = this.get('defaultResponder'))) { 202 if (typeof target === SC.T_STRING) { 203 target = SC.objectForPropertyPath(target); 204 } 205 206 if (!target) target = null; 207 else target = target.tryToPerform(action, evt) ? target : null ; 208 } 209 210 // if we don't have a default responder or no responders in the responder 211 // chain handled the event, see if the pane itself implements the event 212 else if (!target && !(target = this.get('defaultResponder'))) { 213 target = this.tryToPerform(action, evt) ? this : null ; 214 } 215 216 return (evt && evt.mouseHandler) || target; 217 }, 218 219 // ....................................................... 220 // RESPONDER CONTEXT 221 // 222 223 /** 224 Pane's never have a next responder. 225 226 @property {SC.Responder} 227 @readOnly 228 */ 229 nextResponder: function() { 230 return null; 231 }.property().cacheable(), 232 233 /** 234 The first responder. This is the first view that should receive action 235 events. Whenever you click on a view, it will usually become 236 firstResponder. 237 238 @property {SC.Responder} 239 */ 240 firstResponder: null, 241 242 /** 243 If YES, this pane can become the key pane. You may want to set this to NO 244 for certain types of panes. For example, a palette may never want to 245 become key. The default value is YES. 246 247 @type Boolean 248 */ 249 acceptsKeyPane: YES, 250 251 /** 252 This is set to YES when your pane is currently the target of key events. 253 254 @type Boolean 255 */ 256 isKeyPane: NO, 257 258 /** 259 Make the pane receive key events. Until you call this method, the 260 keyView set for this pane will not receive key events. 261 262 @returns {SC.Pane} receiver 263 */ 264 becomeKeyPane: function() { 265 if (this.get('isKeyPane')) return this ; 266 if (this.rootResponder) this.rootResponder.makeKeyPane(this) ; 267 268 return this ; 269 }, 270 271 /** 272 Remove the pane view status from the pane. This will simply set the 273 keyPane on the rootResponder to null. 274 275 @returns {SC.Pane} receiver 276 */ 277 resignKeyPane: function() { 278 if (!this.get('isKeyPane')) return this ; 279 if (this.rootResponder) this.rootResponder.makeKeyPane(null); 280 281 return this ; 282 }, 283 284 /** 285 Makes the passed view (or any object that implements SC.Responder) into 286 the new firstResponder for this pane. This will cause the current first 287 responder to lose its responder status and possibly keyResponder status as 288 well. 289 290 @param {SC.View} view 291 @param {Event} evt that cause this to become first responder 292 @returns {SC.Pane} receiver 293 */ 294 makeFirstResponder: function(original, view, evt) { 295 // firstResponder should never be null 296 if(!view) view = this; 297 298 var current = this.get('firstResponder'), 299 isKeyPane = this.get('isKeyPane'); 300 301 if (current === view) return this ; // nothing to do 302 303 // if we are currently key pane, then notify key views of change also 304 if (isKeyPane) { 305 if (current) { current.tryToPerform('willLoseKeyResponderTo', view); } 306 if (view) { 307 view.tryToPerform('willBecomeKeyResponderFrom', current); 308 } 309 } 310 311 if (current) { 312 current.beginPropertyChanges(); 313 current.set('isKeyResponder', NO); 314 } 315 316 if (view) { 317 view.beginPropertyChanges(); 318 view.set('isKeyResponder', isKeyPane); 319 } 320 321 original(view, evt); 322 323 if(current) current.endPropertyChanges(); 324 if(view) view.endPropertyChanges(); 325 326 // and notify again if needed. 327 if (isKeyPane) { 328 if (view) { 329 view.tryToPerform('didBecomeKeyResponderFrom', current); 330 } 331 if (current) { 332 current.tryToPerform('didLoseKeyResponderTo', view); 333 } 334 } 335 336 return this ; 337 }.enhance(), 338 339 /** 340 Called just before the pane loses it's keyPane status. This will notify 341 the current keyView, if there is one, that it is about to lose focus, 342 giving it one last opportunity to save its state. 343 344 @param {SC.Pane} pane 345 @returns {SC.Pane} receiver 346 */ 347 willLoseKeyPaneTo: function(pane) { 348 this._forwardKeyChange(this.get('isKeyPane'), 'willLoseKeyResponderTo', pane, NO); 349 return this ; 350 }, 351 352 /** 353 Called just before the pane becomes keyPane. Notifies the current keyView 354 that it is about to gain focus. The keyView can use this opportunity to 355 prepare itself, possibly stealing any value it might need to steal from 356 the current key view. 357 358 @param {SC.Pane} pane 359 @returns {SC.Pane} receiver 360 */ 361 willBecomeKeyPaneFrom: function(pane) { 362 this._forwardKeyChange(!this.get('isKeyPane'), 'willBecomeKeyResponderFrom', pane, YES); 363 return this ; 364 }, 365 366 367 didBecomeKeyResponderFrom: function(responder) {}, 368 369 /** 370 Called just after the pane has lost its keyPane status. Notifies the 371 current keyView of the change. The keyView can use this method to do any 372 final cleanup and changes its own display value if needed. 373 374 @param {SC.Pane} pane 375 @returns {SC.Pane} receiver 376 */ 377 didLoseKeyPaneTo: function(pane) { 378 var isKeyPane = this.get('isKeyPane'); 379 this.set('isKeyPane', NO); 380 this._forwardKeyChange(isKeyPane, 'didLoseKeyResponderTo', pane); 381 return this ; 382 }, 383 384 /** 385 Called just after the keyPane focus has changed to the receiver. Notifies 386 the keyView of its new status. The keyView should use this method to 387 update its display and actually set focus on itself at the browser level 388 if needed. 389 390 @param {SC.Pane} pane 391 @returns {SC.Pane} receiver 392 393 */ 394 didBecomeKeyPaneFrom: function(pane) { 395 var isKeyPane = this.get('isKeyPane'); 396 this.set('isKeyPane', YES); 397 this._forwardKeyChange(!isKeyPane, 'didBecomeKeyResponderFrom', pane, YES); 398 return this ; 399 }, 400 401 // ....................................................... 402 // MAIN PANE SUPPORT 403 // 404 405 /** 406 Returns YES whenever the pane has been set as the main pane for the 407 application. 408 409 @type Boolean 410 */ 411 isMainPane: NO, 412 413 /** 414 Invoked when the pane is about to become the focused pane. Override to 415 implement your own custom handling. 416 417 @param {SC.Pane} pane the pane that currently have focus 418 @returns {void} 419 */ 420 focusFrom: function(pane) {}, 421 422 /** 423 Invoked when the the pane is about to lose its focused pane status. 424 Override to implement your own custom handling 425 426 @param {SC.Pane} pane the pane that will receive focus next 427 @returns {void} 428 */ 429 blurTo: function(pane) {}, 430 431 /** 432 Invoked when the view is about to lose its mainPane status. The default 433 implementation will also remove the pane from the document since you can't 434 have more than one mainPane in the document at a time. 435 436 @param {SC.Pane} pane 437 @returns {void} 438 */ 439 blurMainTo: function(pane) { 440 this.set('isMainPane', NO) ; 441 }, 442 443 /** 444 Invokes when the view is about to become the new mainPane. The default 445 implementation simply updates the isMainPane property. In your subclass, 446 you should make sure your pane has been added to the document before 447 trying to make it the mainPane. See SC.MainPane for more information. 448 449 @param {SC.Pane} pane 450 @returns {void} 451 */ 452 focusMainFrom: function(pane) { 453 this.set('isMainPane', YES); 454 }, 455 456 // ....................................................... 457 // ADDING/REMOVE PANES TO SCREEN 458 // 459 460 /** 461 Inserts the pane at the end of the document. This will also add the pane 462 to the rootResponder. 463 464 @param {SC.RootResponder} rootResponder 465 @returns {SC.Pane} receiver 466 */ 467 append: function() { 468 return this.appendTo(document.body) ; 469 }, 470 471 /** 472 Removes the pane from the document. 473 474 This will *not* destroy the pane's layer or destroy the pane itself. 475 476 @returns {SC.Pane} receiver 477 */ 478 remove: function() { 479 if (this.get('isAttached')) { 480 this._doDetach(); 481 } 482 483 return this ; 484 }, 485 486 /** 487 Inserts the current pane into the page. The actual DOM insertion is done 488 by a function passed into `insert`, which receives the layer as a 489 parameter. This function is responsible for making sure a layer exists, 490 is not already attached, and for calling `paneDidAttach` when done. 491 492 pane = SC.Pane.create(); 493 pane.insert(function(layer) { 494 jQuery(layer).insertBefore("#otherElement"); 495 }); 496 497 @param {Function} fn function which performs the actual DOM manipulation 498 necessary in order to insert the pane's layer into the DOM. 499 @returns {SC.Pane} receiver 500 */ 501 insert: function(fn) { 502 // Render the layer. 503 this.createLayer(); 504 505 // Pass the layer to the callback (TODO: why?) 506 var layer = this.get('layer'); 507 fn(layer); 508 509 return this; 510 }, 511 512 /** 513 Inserts the pane into the DOM. 514 515 @param {DOMElement|jQuery|String} elem the element to append the pane's layer to. 516 This is passed to `jQuery()`, so any value supported by `jQuery()` will work. 517 @returns {SC.Pane} receiver 518 */ 519 appendTo: function(elem) { 520 var self = this; 521 522 return this.insert(function () { 523 self._doAttach(jQuery(elem)[0]); 524 }); 525 }, 526 527 /** 528 This has been deprecated and may cause issues when used. Please use 529 didAppendToDocument instead, which is not defined by SC.Pane (i.e. you 530 don't need to call sc_super when implementing didAppendToDocument in direct 531 subclasses of SC.Pane). 532 533 @deprecated Version 1.10 534 */ 535 paneDidAttach: function() { 536 // Does nothing. Left here so that subclasses that implement the method 537 // and call sc_super() won't fail. 538 }, 539 540 /** 541 This method is called after the pane is attached and before child views 542 are notified that they were appended to the document. Override this 543 method to recompute properties that depend on the pane's existence 544 in the document but must be run prior to child view notification. 545 */ 546 recomputeDependentProperties: function () { 547 // Does nothing. Left here so that subclasses that implement the method 548 // and call sc_super() won't fail. 549 }, 550 551 /** @deprecated Version 1.11. Use `isAttached` instead. */ 552 isPaneAttached: function () { 553 554 //@if(debug) 555 SC.warn("Developer Warning: The `isPaneAttached` property of `SC.Pane` has been deprecated. Please use the `isAttached` property instead."); 556 //@endif 557 558 return this.get('isAttached'); 559 }.property('isAttached').cacheable(), 560 561 /** 562 If YES, a touch intercept pane will be added above this pane when on 563 touch platforms. 564 */ 565 wantsTouchIntercept: NO, 566 567 /** 568 Returns YES if wantsTouchIntercept and this is a touch platform. 569 */ 570 hasTouchIntercept: function(){ 571 return this.get('wantsTouchIntercept') && SC.platform.touch; 572 }.property('wantsTouchIntercept').cacheable(), 573 574 /** 575 The Z-Index of the pane. Currently, you have to match this in CSS. 576 TODO: ALLOW THIS TO AUTOMATICALLY SET THE Z-INDEX OF THE PANE (as an option). 577 ACTUAL TODO: Remove this because z-index is evil. 578 */ 579 zIndex: 0, 580 581 /** 582 The amount over the pane's z-index that the touch intercept should be. 583 */ 584 touchZ: 99, 585 586 /** @private */ 587 _addIntercept: function() { 588 if (this.get('hasTouchIntercept')) { 589 var div = document.createElement("div"); 590 var divStyle = div.style; 591 divStyle.position = "absolute"; 592 divStyle.left = "0px"; 593 divStyle.top = "0px"; 594 divStyle.right = "0px"; 595 divStyle.bottom = "0px"; 596 divStyle[SC.browser.experimentalStyleNameFor('transform')] = "translateZ(0px)"; 597 divStyle.zIndex = this.get("zIndex") + this.get("touchZ"); 598 div.className = "touch-intercept"; 599 div.id = "touch-intercept-" + SC.guidFor(this); 600 this._touchIntercept = div; 601 document.body.appendChild(div); 602 } 603 }, 604 605 /** @private */ 606 _removeIntercept: function() { 607 if (this._touchIntercept) { 608 document.body.removeChild(this._touchIntercept); 609 this._touchIntercept = null; 610 } 611 }, 612 613 /** @private */ 614 hideTouchIntercept: function() { 615 if (this._touchIntercept) this._touchIntercept.style.display = "none"; 616 }, 617 618 /** @private */ 619 showTouchIntercept: function() { 620 if (this._touchIntercept) this._touchIntercept.style.display = "block"; 621 }, 622 623 /** @private */ 624 // updateLayerLocation: function () { 625 // if(this.get('designer') && SC.suppressMain) return sc_super(); 626 // // note: the normal code here to update node location is removed 627 // // because we don't need it for panes. 628 // return this; 629 // }, 630 631 /** @private */ 632 init: function() { 633 // Backwards compatibility 634 //@if(debug) 635 // TODO: REMOVE THIS 636 if (this.hasTouchIntercept === YES) { 637 SC.error("Developer Error: Do not set `hasTouchIntercept` on a pane directly. Please use `wantsTouchIntercept` instead."); 638 } 639 //@endif 640 641 // if a layer was set manually then we will just attach to existing HTML. 642 var hasLayer = !!this.get('layer'); 643 644 sc_super(); 645 646 if (hasLayer) { 647 this._attached(); 648 } 649 }, 650 651 /** @private */ 652 classNames: ['sc-pane'] 653 654 }) ; 655