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