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('panes/palette');
  9 
 10 /**
 11   Popular customized picker position rules:
 12   default: initiated just below the anchor.
 13            shift x, y to optimized picker visibility and make sure top-left corner is always visible.
 14   menu :   same as default rule +
 15            default(1, 4, 3) or custom offset below the anchor for default location to fine tuned visual alignment +
 16            enforce min left(7px)/right(8px) padding to the window
 17   fixed :  default(1, 4, 3) or custom offset below the anchor for default location to cope with specific anchor and skip fitPositionToScreen
 18   pointer :take default [0, 1, 2, 3, 2] or custom matrix to choose one of four perfect pointer positions.Ex:
 19            perfect right (0) > perfect left (1) > perfect top (2) > perfect bottom (3)
 20            fallback to perfect top (2)
 21   menu-pointer :take default [3, 0, 1, 2, 3] or custom matrix to choose one of four perfect pointer positions.Ex:
 22           perfect bottom (3) > perfect right (0) > perfect left (1) > perfect top (2)
 23           fallback to perfect bottom (3)
 24 */
 25 
 26 /**
 27   @type String
 28   @constant
 29   @static
 30 */
 31 SC.PICKER_MENU = 'menu';
 32 
 33 /**
 34   @type String
 35   @constant
 36   @static
 37 */
 38 SC.PICKER_FIXED = 'fixed';
 39 
 40 /**
 41   @type String
 42   @constant
 43   @static
 44 */
 45 SC.PICKER_POINTER = 'pointer';
 46 
 47 /**
 48   @type String
 49   @constant
 50   @static
 51 */
 52 SC.PICKER_MENU_POINTER = 'menu-pointer';
 53 
 54 /**
 55   @class
 56 
 57   Display a non-modal pane that automatically repositions around a view so as
 58   to remain visible.
 59 
 60   An `SC.PickerPane` repositions around the view to which it is anchored as the
 61   browser window is resized so as to ensure the pane's content remains visible.
 62   A picker pane is useful for displaying supplementary information and does not
 63   block the user's interaction with other UI elements. Picker panes typically
 64   provide a better user experience than modal panels.
 65 
 66   An `SC.PickerPane` repositions itself according to the optional `preferMatrix`
 67   argument passed in the `.popup()` method call. The `preferMatrix` either
 68   specifies an offset-based arrangement behavior or a position-based arrangement
 69   behavior depending on the `preferType` argument in the `.popup()` call.
 70 
 71   The simplest way to create and display a picker pane:
 72 
 73       SC.PickerPane.create({
 74         layout: { width: 400, height: 200 },
 75         contentView: SC.View.extend({})
 76       }).popup(someView);
 77 
 78   This displays the `SC.PickerPane` anchored to `someView`.
 79 
 80   ## Positioning
 81 
 82   Picker pane positioning can be classified into two broad categories:
 83   offset-based and position-based.
 84 
 85   ### Offset-based
 86 
 87   When `preferType` is unspecified, `SC.PICKER_MENU` or `SC.PICKER_FIXED`, then
 88   the `preferMatrix` array describes the offset that is used to position the
 89   pane below the anchor. The offset is described by an array of three values,
 90   defaulting to `[1, 4, SC.POSITION_BOTTOM]`. The first value controls the x offset and the second
 91   value the y offset. The third value can be `SC.POSITION_RIGHT` (0) or `SC.POSITION_BOTTOM` (3),
 92   controlling whether the origin of the pane is further offset by the width
 93   (in the case of SC.POSITION_RIGHT) or the height (in the case of SC.POSITION_BOTTOM) of the anchor.
 94 
 95   ### Position-based
 96 
 97   When `preferType` is `SC.PICKER_POINTER` or `SC.PICKER_MENU_POINTER`, then
 98   the `preferMatrix` specifies the sides in the order in which you want the
 99   `SC.PickerPane` to try to arrange itself around the view to which it is
100   anchored. The fifth element in the `preferMatrix` specifies which side the
101   `SC.PickerPane` should display on when there isn't enough space around any
102   of the preferred sides.
103 
104   The sides may be one of:
105 
106   * SC.POSITION_RIGHT (i.e. 0) - to the right of the anchor
107   * SC.POSITION_LEFT (i.e. 1)- to the left of the anchor
108   * SC.POSITION_TOP (i.e. 2) - above the anchor
109   * SC.POSITION_BOTTOM (i.e. 3) - below the anchor
110 
111   For example, the `preferMatrix` of,
112 
113       [SC.POSITION_BOTTOM, SC.POSITION_RIGHT, SC.POSITION_LEFT, SC.POSITION_TOP, SC.POSITION_TOP],
114 
115   indicates: Display below the anchor (SC.POSITION_BOTTOM); if there isn't enough
116   space then display to the right of the anchor (SC.POSITION_RIGHT).
117   If there isn't enough space either below or to the right of the anchor, then appear
118   to the left (SC.POSITION_LEFT), unless there is also no space on the left, in which case display
119   above the anchor (SC.POSITION_TOP).
120 
121   Note: The position constants are simply the integers 0 to 3, so a short form
122   of the example above would read,
123 
124       [3, 0, 1, 2, 2]
125 
126   ## Position Rules
127 
128   When invoking `.popup()` you can optionally specify a picker position rule with
129   the `preferType` argument.
130 
131   If no `preferType` is specified, the picker pane is displayed just below the anchor.
132   The pane will reposition automatically for optimal visibility, ensuring the top-left
133   corner is visible.
134 
135   These position rules have the following behaviors:
136 
137   ### `SC.PICKER_MENU`
138 
139   Positioning is offset-based, with `preferMatrix` defaulting to `[1, 4, SC.POSITION_BOTTOM]`.
140   Furthermore, a minimum left and right padding to window, of 7px and 8px, respectively,
141   is enforced.
142 
143 
144   ### `SC.PICKER_FIXED`
145 
146   Positioning is offset-based, with `preferMatrix` defaulting to `[1, 4, SC.POSITION_BOTTOM]` and
147   skipping `fitPositionToScreen`.
148 
149 
150   ### `SC.PICKER_POINTER`
151 
152   Positioning is position-based, with `preferMatrix` defaulting to `[SC.POSITION_RIGHT, SC.POSITION_LEFT, SC.POSITION_TOP, SC.POSITION_BOTTOM, SC.POSITION_TOP]` or `[0, 1, 2, 3, 2]` for short,
153   i.e. right > left > top > bottom; fallback to top.
154 
155 
156   ### `SC.PICKER_MENU_POINTER`
157 
158   Positioning is position-based, with `preferMatrix` defaulting to `[SC.POSITION_BOTTOM, SC.POSITION_RIGHT, SC.POSITION_LEFT, SC.POSITION_TOP, SC.POSITION_BOTTOM]` or `[3, 0, 1, 2, 3]` for short,
159   i.e. bottom, right, left, top; fallback to bottom.
160 
161 
162   ## Examples
163 
164   Examples for applying popular customized picker position rules:
165 
166   ### default:
167 
168       SC.PickerPane.create({
169         layout: { width: 400, height: 200 },
170         contentView: SC.View.extend({})
171       }).popup(anchor);
172 
173   ### menu below the anchor with default `preferMatrix` of `[1, 4, SC.POSITION_BOTTOM]`:
174 
175       SC.PickerPane.create({
176         layout: { width: 400, height: 200 },
177         contentView: SC.View.extend({})
178       }).popup(anchor, SC.PICKER_MENU);
179 
180   ### menu on the right side of anchor with custom `preferMatrix` of `[2, 6, SC.POSITION_RIGHT]`:
181 
182       SC.PickerPane.create({
183         layout: { width: 400, height: 200 },
184         contentView: SC.View.extend({})
185       }).popup(anchor, SC.PICKER_MENU, [2, 6, SC.POSITION_RIGHT]);
186 
187   ### fixed below the anchor with default `preferMatrix` of `[1, 4, SC.POSITION_BOTTOM]`:
188 
189       SC.PickerPane.create({
190         layout: { width: 400, height: 200 },
191         contentView: SC.View.extend({})
192       }).popup(anchor, SC.PICKER_FIXED);
193 
194   ### fixed on the right side of anchor with `preferMatrix` of `[-22,-17, SC.POSITION_RIGHT]`:
195 
196       SC.PickerPane.create({
197         layout: { width: 400, height: 200 },
198         contentView: SC.View.extend({})
199       }).popup(anchor, SC.PICKER_FIXED, [-22,-17, SC.POSITION_RIGHT]);
200 
201   ### pointer with default `preferMatrix` of `[SC.POSITION_RIGHT, SC.POSITION_LEFT, SC.POSITION_TOP, SC.POSITION_BOTTOM, SC.POSITION_TOP]` or `[0, 1, 2, 3, 2]`:
202 
203       SC.PickerPane.create({
204         layout: { width: 400, height: 200 },
205         contentView: SC.View.extend({})
206       }).popup(anchor, SC.PICKER_POINTER);
207 
208   Positioning: SC.POSITION_RIGHT (0) > SC.POSITION_LEFT (1) > SC.POSITION_TOP (2) > SC.POSITION_BOTTOM (3). Fallback to SC.POSITION_TOP (2).
209 
210   ### pointer with custom `preferMatrix` of `[SC.POSITION_BOTTOM, SC.POSITION_RIGHT, SC.POSITION_LEFT, SC.POSITION_TOP, SC.POSITION_TOP]` or `[3, 0, 1, 2, 2]`:
211 
212       SC.PickerPane.create({
213         layout: { width: 400, height: 200 },
214         contentView: SC.View.extend({})
215       }).popup(anchor, SC.PICKER_POINTER, [3, 0, 1, 2, 2]);
216 
217   Positioning: SC.POSITION_BOTTOM (3) > SC.POSITION_RIGHT (0) > SC.POSITION_LEFT (1) > SC.POSITION_TOP (2). Fallback to SC.POSITION_TOP (2).
218 
219   ### menu-pointer with default `preferMatrix` of `[SC.POSITION_BOTTOM, SC.POSITION_RIGHT, SC.POSITION_LEFT, SC.POSITION_TOP, SC.POSITION_BOTTOM]` or `[3, 0, 1, 2, 3]`:
220 
221       SC.PickerPane.create({
222         layout: { width: 400, height: 200 },
223         contentView: SC.View.extend({})
224       }).popup(anchor, SC.PICKER_MENU_POINTER);
225 
226   Positioning: SC.POSITION_BOTTOM (3) > SC.POSITION_RIGHT (0) > SC.POSITION_LEFT (1) > SC.POSITION_TOP (2). Fallback to SC.POSITION_BOTTOM (3).
227 
228   ### Transition-In Special Handling
229 
230   This view has special behavior when used with SC.View's `transitionIn` plugin support. If the
231   plugin defines `layoutProperties` of either `scale` or `rotate`, then the picker will adjust its
232   transform origin X & Y position to appear to scale or rotate out of the anchor. The result is a
233   very nice effect that picker panes appear to pop out of their anchors. To see it in effect,
234   simply set the `transitionIn` property of the pane to one of `SC.View.SCALE_IN` or `SC.View.POP_IN`.
235 
236   @extends SC.PalettePane
237   @since SproutCore 1.0
238 */
239 SC.PickerPane = SC.PalettePane.extend(
240 /** @scope SC.PickerPane.prototype */ {
241 
242   //@if(debug)
243   /** @private Debug-mode only flag for ensuring that the pane is appended via `popup`. */
244   _sc_didUsePopup: false,
245   //@endif
246 
247   /**
248     @type Array
249     @default ['sc-picker']
250     @see SC.View#classNames
251   */
252   classNames: ['sc-picker'],
253 
254   /**
255     @type Boolean
256     @default YES
257   */
258   isAnchored: YES,
259 
260   /**
261     @type Boolean
262     @default YES
263   */
264   isModal: YES,
265 
266   /**
267     @private
268     TODO: Remove SC.POINTER_LAYOUT backward compatibility.
269   */
270   _sc_pointerLayout: SC.POINTER_LAYOUT || ['perfectRight', 'perfectLeft', 'perfectTop', 'perfectBottom'],
271 
272   /** @private
273     @type String
274     @default 'perfectRight'
275   */
276   pointerPos: 'perfectRight',
277 
278   /** @private
279     @type Number
280     @default 0
281   */
282   pointerPosX: 0,
283 
284   /** @private
285     @type Number
286     @default 0
287   */
288   pointerPosY: 0,
289 
290   /** @private
291     When calling `popup`, you pass a view or element to anchor the pane. This
292     property returns the anchor element. (If you've anchored to a view, this
293     is its layer.) You can use this to properly position your view.
294 
295     @type HTMLElement
296     @default null
297   */
298   anchorElement: function (key, value) {
299     // Getter
300     if (value === undefined) {
301       if (this._anchorView) return this._anchorView.get('layer');
302       else return this._anchorHTMLElement;
303     }
304     // Setter
305     else {
306       // Strip jQuery objects. (We do this first in case an empty one is passed in.)
307       if (value && value.isCoreQuery) value = value[0];
308 
309       // Throw an error if a null or empty value is set. You're not allowed to go anchorless.
310       // (TODO: why can't we go anchorless? positionPane happily centers an unmoored pane.)
311       if (!value) {
312         SC.throw("You must set 'anchorElement' to either a view or a DOM element");
313       }
314 
315       // Clean up any previous anchor elements.
316       this._removeScrollObservers();
317 
318       if (value.isView) {
319         this._setupScrollObservers(value);
320         this._anchorView        = value;
321         this._anchorHTMLElement = null;
322         return value.get('layer');
323       }
324       else {
325         // TODO: We could setupScrollObservers on passed elements too, but it would
326         // be a bit more complicated.
327         this._anchorView        = null;
328         this._anchorHTMLElement = value;
329         return value;
330       }
331     }
332   }.property().cacheable(),
333 
334   /** @private
335     anchor rect calculated by computeAnchorRect from init popup
336 
337     @type Hash
338     @default null
339   */
340   anchorCached: null,
341 
342   /**
343     The type of picker pane.
344 
345     Picker panes can behave and appear in slightly differing ways
346     depending on the value of `preferType`. By default, with no `preferType`
347     specified, the pane will appear directly below the anchor element with
348     its left side aligned to the anchor's left side.
349 
350     However, if you wish to position the pane by a specified offset to the
351     right or below the anchor using the values of `preferMatrix` as an offset
352     configuration, you can set `preferType` to one of `SC.PICKER_MENU` or
353     `SC.PICKER_FIXED`. These two picker types both use the `preferMatrix` to
354     adjust the position of the pane below or to the right of the anchor.
355 
356     The difference is that `SC.PICKER_MENU` also uses the `windowPadding`
357     value to ensure that the pane doesn't go outside the bounds of the visible
358     window.
359 
360     If you wish to position the pane on whichever side it will best fit and include
361     a pointer, then you can use one of `SC.PICKER_POINTER` or `SC.PICKER_MENU_POINTER`
362     for `preferType`. With this setting the pane will use the values of
363     `preferMatrix` to indicate the preferred side of the anchor for the picker
364     to appear.
365 
366     The difference between these two is that `SC.PICKER_MENU_POINTER` prefers
367     to position below the anchor by default and `SC.PICKER_POINTER` prefers to
368     position to the right of the anchor by default. As well, the `SC.PICKER_MENU_POINTER`
369     type will resize itself if its height extends outside the visible window
370     (which is useful for long menus that can scroll).
371 
372     @type String
373     @default null
374   */
375   preferType: null,
376 
377   /**
378     The configuration value for the current type of picker pane.
379 
380     This dual-purpose property controls the positioning of the pane depending
381     on what the value of `preferType` is.
382 
383     ## Offset based use of `preferMatrix`
384 
385     For `preferType` of `SC.PICKER_MENU` or `SC.PICKER_FIXED`, `preferMatrix`
386     determines the x and y offset of the pane from either the right or bottom
387     side of the anchor. In this case, the `preferMatrix` should be an array of,
388 
389         [*x offset*, *y offset*, *offset position*]
390 
391     For example, to position the pane 10px directly below the anchor, we would
392     use,
393 
394         preferMatrix: [0, 10, SC.POSITION_BOTTOM]
395 
396     To position the pane 10px down and 5px right of the anchor's right side,
397     we would use,
398 
399         preferMatrix: [5, 10, SC.POSITION_RIGHT]
400 
401     ## Position based use of `preferMatrix`
402 
403     For `preferType` of `SC.PICKER_POINTER` or `SC.PICKER_MENU_POINTER`, `preferMatrix`
404     determines the side of the anchor to appear on in order of preference.
405     In this case, the `preferMatrix` should be an array of,
406 
407         [*preferred side*, *2nd preferred side*, *3rd preferred side*, *4th preferred side*, *fallback side if none fit*]
408 
409     Note that if the pane can't fit within the window bounds (including `windowPadding`)
410     on any of the sides, then the last side is used as a fallback.
411 
412     @type Array
413     @default preferType == SC.PICKER_MENU || preferType == SC.PICKER_FIXED ? [1, 4, SC.POSITION_BOTTOM] (i.e. [1, 4, 3])
414     @default preferType == SC.PICKER_POINTER ? [SC.POSITION_RIGHT, SC.POSITION_LEFT, SC.POSITION_TOP, SC.POSITION_BOTTOM, SC.POSITION_TOP] (i.e. [0, 1, 2, 3, 2])
415     @default preferType == SC.PICKER_MENU_POINTER ? [SC.POSITION_BOTTOM, SC.POSITION_RIGHT, SC.POSITION_LEFT, SC.POSITION_TOP, SC.POSITION_BOTTOM] (i.e. [3, 0, 1, 2, 3])
416     @default null
417   */
418   preferMatrix: null,
419 
420   /**
421     The offset of the pane from its target when positioned with `preferType` of
422     `SC.PICKER_POINTER` or `SC.PICKER_MENU_POINTER`.
423 
424     When using `SC.PICKER_POINTER` or `SC.PICKER_MENU_POINTER` as the `preferType`,
425     the pane will include a pointer element (ex. a small triangle on the side of
426     the pane). This also means that the pane will be offset by an additional
427     distance in order to make space for the pointer. The offset distance of each
428     side is specified by `pointerOffset`.
429 
430     Therefore, if you are using a custom picker pane style or you would just
431     like to change the default offsets, you should specify your own value like
432     so:
433 
434         pointerOffset: [*right offset*, *left offset*, *top offset*, *bottom offset*]
435 
436     For example,
437 
438         // If the pane is to the right of the target, offset 15px further right for a left-side pointer.
439         // If the pane is to the left of the target, offset -15px further left for a right-side pointer.
440         // If the pane is above the target, offset -30px up for a bottom pointer.
441         // If the pane is below the target, offset 20px down for a top pointer.
442         pointerOffset: [15, -15, -30, 20]
443 
444     @type Array
445     @default preferType == SC.PICKER_POINTER ? SC.PickerPane.PICKER_POINTER_OFFSET (i.e. [9, -9, -18, 18])
446     @default preferType == SC.PICKER_MENU_POINTER && controlSize == SC.TINY_CONTROL_SIZE ? SC.PickerPane.TINY_PICKER_MENU_POINTER_OFFSET (i.e [9, -9, -18, 18])
447     @default preferType == SC.PICKER_MENU_POINTER && controlSize == SC.SMALL_CONTROL_SIZE ? SC.PickerPane.SMALL_PICKER_MENU_POINTER_OFFSET (i.e [9, -9, -8, 8])
448     @default preferType == SC.PICKER_MENU_POINTER && controlSize == SC.REGULAR_CONTROL_SIZE ? SC.PickerPane.REGULAR_PICKER_MENU_POINTER_OFFSET (i.e [9, -9, -12, 12])
449     @default preferType == SC.PICKER_MENU_POINTER && controlSize == SC.LARGE_CONTROL_SIZE ? SC.PickerPane.LARGE_PICKER_MENU_POINTER_OFFSET (i.e [9, -9, -16, 16])
450     @default preferType == SC.PICKER_MENU_POINTER && controlSize == SC.HUGE_CONTROL_SIZE ? SC.PickerPane.HUGE_PICKER_MENU_POINTER_OFFSET (i.e [9, -9, -18, 18])
451     @default preferType == SC.PICKER_MENU_POINTER ? SC.PickerPane.REGULAR_PICKER_MENU_POINTER_OFFSET (i.e [9, -9, -12, 12])
452   */
453   pointerOffset: null,
454 
455   /** @deprecated Version 1.10.  Use windowPadding instead.
456     default offset of extra-right pointer for picker-pointer or pointer-menu
457 
458     @type Number
459     @default 0
460   */
461   extraRightOffset: function () {
462     //@if (debug)
463     SC.warn('SC.PickerPane#extraRightOffset is deprecated.  The pointer will position itself automatically.');
464     //@endif
465 
466     return this.get('windowPadding');
467   }.property('windowPadding').cacheable(),
468 
469   /**
470     The target object to invoke the remove action on when the user clicks off the
471     picker that is to be removed.
472 
473     If you set this target, the action will be called on the target object
474     directly when the user clicks off the picker. If you leave this property
475     set to null, then the button will search the responder chain for a view that
476     implements the action when the button is pressed instead.
477 
478     @type Object
479     @default null
480   */
481   removeTarget: null,
482 
483   /**
484     The name of the action you want triggered when the user clicks off the
485     picker pane that is to be removed.
486 
487     This property is used in conjunction with the removeTarget property to execute
488     a method when the user clicks off the picker pane.
489 
490     If you do not set a target, then clicking off the picker pane will cause the
491     responder chain to search for a view that implements the action you name
492     here, if one was provided.
493 
494     Note that this property is optional. If no explicit value is provided then the
495     picker pane will perform the default action which is to simply remove itself.
496 
497     @type String
498     @default null
499   */
500   removeAction: null,
501 
502 
503   /**
504     Disable repositioning as the window or size changes. It stays in the original
505     popup position.
506 
507     @type Boolean
508     @default NO
509   */
510   repositionOnWindowResize: YES,
511 
512 
513   /** @private
514     Default padding around the window's edge that the pane will not overlap.
515 
516     This value is set to the value of SC.PickerPane.WINDOW_PADDING, except when
517     using preferType of SC.PICKER_MENU_POINTER, where it will be set according
518     to the `controlSize` value of the pane to one of:
519 
520       SC.PickerPane.TINY_MENU_WINDOW_PADDING
521       SC.PickerPane.SMALL_MENU_WINDOW_PADDING
522       SC.PickerPane.REGULAR_MENU_WINDOW_PADDING
523       SC.PickerPane.LARGE_MENU_WINDOW_PADDING
524       SC.PickerPane.HUGE_MENU_WINDOW_PADDING
525 
526     @type Number
527     @default SC.PickerPane.WINDOW_PADDING
528   */
529   windowPadding: null,
530 
531   //@if(debug)
532   // Provide some developer support. People have occasionally been misled by calling `append`
533   // on PickerPanes, which fails to position the pane properly. Hopefully, we can give
534   // them a clue to speed up finding the problem.
535   /** @private SC.Pane */
536   append: function () {
537     if (!this._sc_didUsePopup) {
538       SC.warn("Developer Warning: You should not use .append() with SC.PickerPane. Instead use .popup() and pass in an anchor view or element.");
539     }
540 
541     this._sc_didUsePopup = false;
542 
543     return sc_super();
544   },
545   //@endif
546 
547   /* @private If the pane changes size, reposition as necessary. */
548   viewDidResize: function () {
549     // Don't forget to call the superclass method.
550     sc_super();
551 
552     // Re-position.
553     this.positionPane(true);
554   },
555 
556   /**
557     Displays a new picker pane.
558 
559     @param {SC.View|HTMLElement} anchorViewOrElement view or element to anchor to
560     @param {String} [preferType] apply picker position rule
561     @param {Array} [preferMatrix] apply custom offset or position pref matrix for specific preferType
562     @param {Number} [pointerOffset]
563     @returns {SC.PickerPane} receiver
564   */
565   popup: function (anchorViewOrElement, preferType, preferMatrix, pointerOffset) {
566     this.beginPropertyChanges();
567     this.setIfChanged('anchorElement', anchorViewOrElement);
568     if (preferType) { this.set('preferType', preferType); }
569     if (preferMatrix) { this.set('preferMatrix', preferMatrix); }
570     if (pointerOffset) { this.set('pointerOffset', pointerOffset); }
571     this.endPropertyChanges();
572     this.positionPane();
573     this._hideOverflow();
574 
575     //@if(debug)
576     // A debug-mode only flag to indicate that the popup method was called (see override of append).
577     this._sc_didUsePopup = true;
578     //@endif
579 
580     return this.append();
581   },
582 
583   /** @private
584     The ideal position for a picker pane is just below the anchor that
585     triggered it + offset of specific preferType. Find that ideal position,
586     then call fitPositionToScreen to get final position. If anchor is missing,
587     fallback to center.
588   */
589   positionPane: function (useAnchorCached) {
590     var frame = this.get('borderFrame'),
591       preferType = this.get('preferType'),
592       preferMatrix = this.get('preferMatrix'),
593       origin, adjustHash,
594       anchor, anchorCached, anchorElement;
595 
596     // usually an anchorElement will be passed.  The ideal position is just
597     // below the anchor + default or custom offset according to preferType.
598     // If that is not possible, fitPositionToScreen will take care of that for
599     // other alternative and fallback position.
600     anchorCached = this.get('anchorCached');
601     anchorElement = this.get('anchorElement');
602     if (useAnchorCached && anchorCached) {
603       anchor = anchorCached;
604     } else if (anchorElement) {
605       anchor = this.computeAnchorRect(anchorElement);
606       this.set('anchorCached', anchor);
607     } // else no anchor to use
608 
609     if (anchor) {
610       origin = SC.cloneRect(anchor);
611 
612       // Adjust the origin for offset based positioning.
613       switch (preferType) {
614       case SC.PICKER_MENU:
615       case SC.PICKER_FIXED:
616         if (!preferMatrix || preferMatrix.length !== 3) {
617           // default below the anchor with fine-tuned visual alignment
618           // for Menu to appear just below the anchorElement.
619           this.set('preferMatrix', [1, 4, 3]);
620           preferMatrix = this.get('preferMatrix');
621         }
622 
623         // fine-tuned visual alignment from preferMatrix
624         origin.x += ((preferMatrix[2] === 0) ? origin.width : 0) + preferMatrix[0];
625         origin.y += ((preferMatrix[2] === 3) ? origin.height : 0) + preferMatrix[1];
626         break;
627       default:
628         origin.y += origin.height;
629         break;
630       }
631 
632       // Since we repeatedly need to know the half-width and half-height of the
633       // frames, add those properties.
634       anchor.halfWidth = parseInt(anchor.width * 0.5, 0);
635       anchor.halfHeight = parseInt(anchor.height * 0.5, 0);
636 
637       // Don't pollute the borderFrame rect.
638       frame = SC.cloneRect(frame);
639       frame.halfWidth = parseInt(frame.width * 0.5, 0);
640       frame.halfHeight = parseInt(frame.height * 0.5, 0);
641 
642       frame = this.fitPositionToScreen(origin, frame, anchor);
643 
644       // Create an adjustment layout from the computed position.
645       adjustHash = {
646         left: frame.x,
647         top: frame.y
648       };
649 
650       // If the computed position also constrains width or height, add it to the adjustment.
651       /*jshint eqnull:true*/
652       if (frame.width != null) {
653         adjustHash.width = frame.width;
654       }
655 
656       if (frame.height != null) {
657         adjustHash.height = frame.height;
658       }
659 
660       /*
661         Special case behavior for transitions that include scale or rotate: notably SC.View.SCALE_IN and SC.View.POP_IN.
662 
663         We make an assumption that the picker should always scale out of the anchor, so we set the
664         transform origin accordingly.
665       */
666       var transitionIn = this.get('transitionIn');
667       if (transitionIn && (transitionIn.layoutProperties.indexOf('scale') >= 0 || transitionIn.layoutProperties.indexOf('rotate') >= 0)) {
668         var transformOriginX, transformOriginY;
669 
670         switch (preferType) {
671         // If the picker uses a pointer, set the origin to the pointer.
672         case SC.PICKER_POINTER:
673         case SC.PICKER_MENU_POINTER:
674           switch (this.get('pointerPos')) {
675           case 'perfectTop':
676             transformOriginX = (frame.halfWidth + this.get('pointerPosX')) / frame.width;
677             transformOriginY = 1;
678             break;
679           case 'perfectRight':
680             transformOriginX = 0;
681             transformOriginY = (frame.halfHeight + this.get('pointerPosY')) / frame.height;
682             break;
683           case 'perfectBottom':
684             transformOriginX = (frame.halfWidth + this.get('pointerPosX')) / frame.width;
685             transformOriginY = 0;
686             break;
687           case 'perfectLeft':
688             transformOriginX = 1;
689             transformOriginY = (frame.halfHeight + this.get('pointerPosY')) / frame.height;
690             break;
691           }
692           break;
693 
694         // If the picker doesn't use a pointer, set the origin to the correct corner.
695         case SC.PICKER_MENU:
696         case SC.PICKER_FIXED:
697           if (frame.x >= anchor.x) {
698             transformOriginX = 0;
699           } else {
700             transformOriginX = 1;
701           }
702           if (frame.y >= anchor.y) {
703             transformOriginY = 0;
704           } else {
705             transformOriginY = 1;
706           }
707 
708           break;
709         }
710 
711         adjustHash.transformOriginX = transformOriginX;
712         adjustHash.transformOriginY = transformOriginY;
713       }
714 
715       // Adjust.
716       this.adjust(adjustHash);
717 
718     // if no anchor view has been set for some reason, just center.
719     } else {
720       this.adjust({
721         centerX: 0,
722         centerY: 0
723       });
724     }
725 
726     return this;
727   },
728 
729   /** @private
730     This method will return ret (x, y, width, height) from a rectangular element
731     Notice: temp hack for calculating visible anchor height by counting height
732     up to window bottom only. We do have 'clippingFrame' supported from view.
733     But since our anchor can be element, we use this solution for now.
734   */
735   computeAnchorRect: function (anchor) {
736     var bounding, ret, cq,
737         wsize = SC.RootResponder.responder.computeWindowSize();
738     // Some browsers natively implement getBoundingClientRect, so if it's
739     // available we'll use it for speed.
740     if (anchor.getBoundingClientRect) {
741       // Webkit and Firefox 3.5 will get everything they need by
742       // calling getBoundingClientRect()
743       bounding = anchor.getBoundingClientRect();
744       ret = {
745         x:      bounding.left,
746         y:      bounding.top,
747         width:  bounding.width,
748         height: bounding.height
749       };
750       // If width and height are undefined this means we are in IE or FF < 3.5
751       // if we did not get the frame dimensions the do the calculations
752       // based on an element
753       if (ret.width === undefined || ret.height === undefined) {
754         cq = SC.$(anchor);
755         ret.width = cq.outerWidth();
756         ret.height = cq.outerHeight();
757       }
758     } else {
759       // Only really old versions will have to go through this code path.
760       ret   = SC.offset(anchor); // get x & y
761       cq    = SC.$(anchor);
762       ret.width = cq.outerWidth();
763       ret.height = cq.outerHeight();
764     }
765     ret.height = (wsize.height - ret.y) < ret.height ? (wsize.height - ret.y) : ret.height;
766 
767     if (!SC.browser.isIE && window.scrollX > 0 || window.scrollY > 0) {
768       ret.x += window.scrollX;
769       ret.y += window.scrollY;
770     } else if (SC.browser.isIE && (document.documentElement.scrollTop > 0 || document.documentElement.scrollLeft > 0)) {
771       ret.x += document.documentElement.scrollLeft;
772       ret.y += document.documentElement.scrollTop;
773     }
774     return ret;
775   },
776 
777   /** @private
778     This method will dispatch to the correct re-position rule according to preferType
779   */
780   fitPositionToScreen: function (preferredPosition, frame, anchorFrame) {
781     var windowSize = SC.RootResponder.responder.computeWindowSize(),
782         windowFrame = { x: 0, y: 0, width: windowSize.width, height: windowSize.height };
783 
784     // if window size is smaller than the minimum size of app, use minimum size.
785     var mainPane = SC.RootResponder.responder.mainPane;
786     if (mainPane) {
787       var minWidth = mainPane.layout.minWidth,
788           minHeight = mainPane.layout.minHeight;
789 
790       if (minWidth && windowFrame.width < minWidth) {
791         windowFrame.width = mainPane.layout.minWidth;
792       }
793 
794       if (minHeight && windowFrame.height < minHeight) {
795         windowFrame.height = mainPane.layout.minHeight;
796       }
797     }
798 
799     frame.x = preferredPosition.x;
800     frame.y = preferredPosition.y;
801 
802     var preferType = this.get('preferType');
803     if (preferType) {
804       switch (preferType) {
805       case SC.PICKER_MENU:
806         // apply menu re-position rule
807         frame = this.fitPositionToScreenMenu(windowFrame, frame, this.get('isSubMenu'));
808         break;
809       case SC.PICKER_MENU_POINTER:
810         this.setupPointer(anchorFrame);
811         frame = this.fitPositionToScreenMenuPointer(windowFrame, frame, anchorFrame);
812         break;
813       case SC.PICKER_POINTER:
814         // apply pointer re-position rule
815         this.setupPointer(anchorFrame);
816         frame = this.fitPositionToScreenPointer(windowFrame, frame, anchorFrame);
817         break;
818       case SC.PICKER_FIXED:
819         // skip fitPositionToScreen
820         break;
821       default:
822         break;
823       }
824     } else {
825       // apply default re-position rule
826       frame = this.fitPositionToScreenDefault(windowFrame, frame, anchorFrame);
827     }
828 
829     return frame;
830   },
831 
832   /** @private
833     re-position rule migrated from old SC.OverlayPaneView.
834     shift x, y to optimized picker visibility and make sure top-left corner is always visible.
835   */
836   fitPositionToScreenDefault: function (windowFrame, frame, anchorFrame) {
837     var maximum;
838 
839     // make sure the right edge fits on the screen.  If not, anchor to
840     // right edge of anchor or right edge of window, whichever is closer.
841     if (SC.maxX(frame) > windowFrame.width) {
842       maximum = Math.max(SC.maxX(anchorFrame), frame.width);
843       frame.x = Math.min(maximum, windowFrame.width) - frame.width;
844     }
845 
846     // if the left edge is off of the screen, try to position at left edge
847     // of anchor.  If that pushes right edge off screen, shift back until
848     // right is on screen or left = 0
849     if (SC.minX(frame) < 0) {
850       frame.x = SC.minX(Math.max(anchorFrame, 0));
851       if (SC.maxX(frame) > windowFrame.width) {
852         frame.x = Math.max(0, windowFrame.width - frame.width);
853       }
854     }
855 
856     // make sure bottom edge fits on screen.  If not, try to anchor to top
857     // of anchor or bottom edge of screen.
858     if (SC.maxY(frame) > windowFrame.height) {
859       maximum = Math.max((anchorFrame.y - frame.height), 0);
860       if (maximum > windowFrame.height) {
861         frame.y = Math.max(0, windowFrame.height - frame.height);
862       } else { frame.y = maximum; }
863     }
864 
865     // if top edge is off screen, try to anchor to bottom of anchor. If that
866     // pushes off bottom edge, shift up until it is back on screen or top =0
867     if (SC.minY(frame) < 0) {
868       maximum = Math.min(SC.maxY(anchorFrame), (windowFrame.height - anchorFrame.height));
869       frame.y = Math.max(maximum, 0);
870     }
871 
872     return frame;
873   },
874 
875   /** @private
876     Reposition the pane in a way that is optimized for menus.
877 
878     Specifically, we want to ensure that the pane is at least 7 pixels from
879     the left side of the screen, and 20 pixels from the right side.
880 
881     If the menu is a submenu, we also want to reposition the pane to the left
882     of the parent menu if it would otherwise exceed the width of the viewport.
883   */
884   fitPositionToScreenMenu: function (windowFrame, frame, subMenu) {
885     var windowPadding = this.get('windowPadding');
886 
887     // Set up init location for submenu
888     if (subMenu) {
889       frame.x -= this.get('submenuOffsetX');
890       frame.y -= Math.floor(this.get('menuHeightPadding') / 2);
891     }
892 
893     // Make sure we are at least the window padding from the left edge of the screen to start.
894     if (frame.x < windowPadding) {
895       frame.x = windowPadding;
896     }
897 
898     // If the right edge of the pane is within the window padding of the right edge
899     // of the window, we need to reposition it.
900     if ((frame.x + frame.width + windowPadding) > windowFrame.width) {
901       if (subMenu) {
902         // Submenus should be re-anchored to the left of the parent menu
903         frame.x = frame.x - (frame.width * 2);
904       } else {
905         // Otherwise, just shift the pane windowPadding pixels from the right edge
906         frame.x = windowFrame.width - frame.width - windowPadding;
907       }
908     }
909 
910     // Make sure we are at least the window padding from the top edge of the screen to start.
911     if (frame.y < windowPadding) {
912       frame.y = windowPadding;
913     }
914 
915     // If the height of the menu is bigger than the window height, shift it upward.
916     if (frame.y + frame.height + windowPadding > windowFrame.height) {
917       frame.y = Math.max(windowPadding, windowFrame.height - frame.height - windowPadding);
918     }
919 
920     // If the height of the menu is still bigger than the window height, resize it.
921     if (frame.y + frame.height + windowPadding > windowFrame.height) {
922       frame.height = windowFrame.height - (2 * windowPadding);
923     }
924 
925     return frame;
926   },
927 
928   /** @private
929     Reposition the pane in a way that is optimized for menus that have a
930     point element.
931 
932     This simply calls fitPositionToScreenPointer, then ensures that the menu
933     does not exceed the height of the viewport.
934 
935     @returns {Rect}
936   */
937   fitPositionToScreenMenuPointer: function (windowFrame, frame, anchorFrame) {
938     frame = this.fitPositionToScreenPointer(windowFrame, frame, anchorFrame);
939 
940     // If the height of the menu is bigger than the window height, resize it.
941     if (frame.height + frame.y + 35 >= windowFrame.height) {
942       frame.height = windowFrame.height - frame.y - (SC.MenuPane.VERTICAL_OFFSET * 2);
943     }
944 
945     return frame;
946   },
947 
948   /** @private
949     re-position rule for triangle pointer picker.
950   */
951   fitPositionToScreenPointer: function (windowFrame, frame, anchorFrame) {
952     var curType,
953         deltas,
954         matrix = this.get('preferMatrix'),
955         offset = this.get('pointerOffset'),
956         topLefts, botRights,
957         windowPadding = this.get('windowPadding');
958 
959     // Determine the top-left corner of each of the 4 perfectly positioned
960     // frames, while taking the pointer offset into account.
961     topLefts = [
962       // Top left [x, y] if positioned evenly to the right of the anchor
963       [anchorFrame.x + anchorFrame.width + offset[0], anchorFrame.y + anchorFrame.halfHeight - frame.halfHeight],
964 
965       // Top left [x, y] if positioned evenly to the left of the anchor
966       [anchorFrame.x - frame.width + offset[1], anchorFrame.y + anchorFrame.halfHeight - frame.halfHeight],
967 
968       // Top left [x, y] if positioned evenly above the anchor
969       [anchorFrame.x + anchorFrame.halfWidth - frame.halfWidth, anchorFrame.y - frame.height + offset[2]],
970 
971       // Top left [x, y] if positioned evenly below the anchor
972       [anchorFrame.x + anchorFrame.halfWidth - frame.halfWidth, anchorFrame.y + anchorFrame.height + offset[3]]
973     ];
974 
975     // Determine the bottom-right corner of each of the 4 perfectly positioned
976     // frames, while taking the pointer offset into account.
977     botRights = [
978       // Bottom right [x, y] if positioned evenly to the right of the anchor
979       [anchorFrame.x + anchorFrame.width + frame.width + offset[0], anchorFrame.y + anchorFrame.halfHeight + frame.halfHeight],
980 
981       // Bottom right [x, y] if positioned evenly to the left of the anchor
982       [anchorFrame.x + offset[1], anchorFrame.y + anchorFrame.halfHeight + frame.halfHeight],
983 
984       // Bottom right [x, y] if positioned evenly above the anchor
985       [anchorFrame.x + anchorFrame.halfWidth + frame.halfWidth, anchorFrame.y + offset[2]],
986 
987       // Bottom right [x, y] if positioned evenly below the anchor
988       [anchorFrame.x + anchorFrame.halfWidth + frame.halfWidth, anchorFrame.y + anchorFrame.height + frame.height + offset[3]]
989     ];
990 
991     // Loop through the preferred matrix, hopefully finding one that will fit
992     // perfectly.
993     for (var i = 0, pointerLen = this._sc_pointerLayout.length; i < pointerLen; i++) {
994       // The current preferred side.
995       curType = matrix[i];
996 
997       // Determine if any of the sides of the pane would go beyond the window's
998       // edge for each of the 4 perfectly positioned frames; taking the amount
999       // of windowPadding into account.  This is done by measuring the distance
1000       // from each side of the frame to the side of the window.  If the distance
1001       // is negative then the edge is overlapping.
1002       //
1003       // If a perfect position has no overlapping edges, then it is a viable
1004       // option for positioning.
1005       deltas = {
1006         top: topLefts[curType][1] - windowPadding,
1007         right: windowFrame.width - windowPadding - botRights[curType][0],
1008         bottom: windowFrame.height - windowPadding - botRights[curType][1],
1009         left: topLefts[curType][0] - windowPadding
1010       };
1011 
1012       // UNUSED.  It would be nice to get the picker as close as possible.
1013       // Cache the fallback deltas.
1014       // if (curType === matrix[4]) {
1015       //   fallbackDeltas = deltas;
1016       // }
1017 
1018       // If no edges overflow, then use this layout.
1019       if (deltas.top >= 0 &&
1020           deltas.right >= 0 &&
1021           deltas.bottom >= 0 &&
1022           deltas.left >= 0) {
1023 
1024         frame.x = topLefts[curType][0];
1025         frame.y = topLefts[curType][1];
1026 
1027         this.set('pointerPosX', 0);
1028         this.set('pointerPosY', 0);
1029         this.set('pointerPos', this._sc_pointerLayout[curType]);
1030 
1031         break;
1032 
1033       // If we prefer right or left and can fit right or left respectively, but
1034       // can't fit the top within the window top and padding, then check if by
1035       // adjusting the top of the pane down if it would still be beside the
1036       // anchor and still above the bottom of the window with padding.
1037       } else if (((curType === 0 && deltas.right >= 0) || // Right fits for preferred right
1038                  (curType === 1 &&  deltas.left >= 0)) && // or left fits for preferred left,
1039                  deltas.top < 0 && // but top doesn't fit,
1040                  deltas.top + frame.halfHeight >= 0) {  // yet it could.
1041 
1042         // Adjust the pane position by the amount of downward shifting.
1043         frame.x = topLefts[curType][0];
1044         frame.y = topLefts[curType][1] - deltas.top;
1045 
1046         // Offset the pointer position by the opposite amount of downward
1047         // shifting (minus half the height of the pointer).
1048         this.set('pointerPosX', 0);
1049         this.set('pointerPosY', deltas.top);
1050         this.set('pointerPos', this._sc_pointerLayout[curType]);
1051         break;
1052 
1053       // If we prefer right or left and can fit right or left respectively, but
1054       // can't fit the bottom within the window bottom and padding, then check
1055       // if by adjusting the top of the pane up if it would still be beside the
1056       // anchor and still below the top of the window with padding.
1057       } else if (((curType === 0 && deltas.right >= 0) || // Right fits for preferred right
1058                  (curType === 1 &&  deltas.left >= 0)) && // or left fits for preferred left,
1059                  deltas.bottom < 0 && // but bottom doesn't fit,
1060                  deltas.bottom + frame.halfHeight >= 0) {  // yet it could.
1061 
1062         // Adjust the pane position by the amount of upward shifting.
1063         frame.x = topLefts[curType][0];
1064         frame.y = topLefts[curType][1] + deltas.bottom;
1065 
1066         // Offset the pointer position by the opposite amount of upward
1067         // shifting (minus half the height of the pointer).
1068         this.set('pointerPosX', 0);
1069         this.set('pointerPosY', Math.abs(deltas.bottom));
1070         this.set('pointerPos', this._sc_pointerLayout[curType]);
1071         break;
1072 
1073       // If we prefer top or bottom and can fit top or bottom respectively, but
1074       // can't fit the right side within the window right side plus padding,
1075       // then check if by adjusting the pane leftwards to fit if it would still
1076       // be beside the anchor and still fit within the left side of the window
1077       // with padding.
1078       } else if (((curType === 2 && deltas.top >= 0) || // Top fits for preferred top
1079                  (curType === 3 &&  deltas.bottom >= 0)) && // or bottom fits for preferred bottom,
1080                  deltas.right < 0 && // but right doesn't fit,
1081                  deltas.right + frame.halfWidth >= 0) {  // yet it could.
1082 
1083         // Adjust the pane position by the amount of leftward shifting.
1084         frame.x = topLefts[curType][0] + deltas.right;
1085         frame.y = topLefts[curType][1];
1086 
1087         // Offset the pointer position by the opposite amount of leftward
1088         // shifting (minus half the width of the pointer).
1089         this.set('pointerPosX', Math.abs(deltas.right));
1090         this.set('pointerPosY', 0);
1091         this.set('pointerPos', this._sc_pointerLayout[curType]);
1092         break;
1093 
1094       // If we prefer top or bottom and can fit top or bottom respectively, but
1095       // can't fit the left side within the window left side plus padding,
1096       // then check if by adjusting the pane rightwards to fit if it would still
1097       // be beside the anchor and still fit within the right side of the window
1098       // with padding.
1099       } else if (((curType === 2 && deltas.top >= 0) || // Top fits for preferred top
1100                  (curType === 3 &&  deltas.bottom >= 0)) && // or bottom fits for preferred bottom,
1101                  deltas.left < 0 && // but left doesn't fit,
1102                  deltas.left + frame.halfWidth >= 0) {  // yet it could.
1103 
1104         // Adjust the pane position by the amount of leftward shifting.
1105         frame.x = topLefts[curType][0] - deltas.left;
1106         frame.y = topLefts[curType][1];
1107 
1108         // Offset the pointer position by the opposite amount of leftward
1109         // shifting (minus half the width of the pointer).
1110         this.set('pointerPosX', deltas.left);
1111         this.set('pointerPosY', 0);
1112         this.set('pointerPos', this._sc_pointerLayout[curType]);
1113         break;
1114       }
1115 
1116     }
1117 
1118     // If no arrangement was found to fit, then use the fall back preferred type.
1119     if (i === pointerLen) {
1120       if (matrix[4] === -1) {
1121         frame.x = anchorFrame.x + anchorFrame.halfWidth;
1122         frame.y = anchorFrame.y + anchorFrame.halfHeight - frame.halfHeight;
1123 
1124         this.set('pointerPos', this._sc_pointerLayout[0] + ' fallback');
1125         this.set('pointerPosY', frame.halfHeight - 40);
1126       } else {
1127         frame.x = topLefts[matrix[4]][0];
1128         frame.y = topLefts[matrix[4]][1];
1129 
1130         this.set('pointerPos', this._sc_pointerLayout[matrix[4]]);
1131         this.set('pointerPosY', 0);
1132       }
1133 
1134       this.set('pointerPosX', 0);
1135     }
1136 
1137     this.invokeLast(this._adjustPointerPosition);
1138 
1139     return frame;
1140   },
1141 
1142   /** @private Measure the pointer element and adjust it by the determined offset. */
1143   _adjustPointerPosition: function () {
1144     var pointer = this.$('.sc-pointer'),
1145       pointerPos = this.get('pointerPos'),
1146       marginLeft,
1147       marginTop;
1148 
1149     switch (pointerPos) {
1150     case 'perfectRight':
1151     case 'perfectLeft':
1152       marginTop = -Math.round(pointer.outerHeight() / 2);
1153       marginTop += this.get('pointerPosY');
1154       pointer.attr('style', "margin-top: " + marginTop + "px");
1155       break;
1156     case 'perfectTop':
1157     case 'perfectBottom':
1158       marginLeft = -Math.round(pointer.outerWidth() / 2);
1159       marginLeft += this.get('pointerPosX');
1160       pointer.attr('style', "margin-left: " + marginLeft + "px;");
1161       break;
1162     }
1163   },
1164 
1165   /** @private
1166     This method will set up pointerOffset and preferMatrix according to type
1167     and size if not provided explicitly.
1168   */
1169   setupPointer: function (a) {
1170     var pointerOffset = this.get('pointerOffset'),
1171         K = SC.PickerPane;
1172 
1173     // Set windowPadding and pointerOffset (SC.PICKER_MENU_POINTER only).
1174     if (!pointerOffset || pointerOffset.length !== 4) {
1175       if (this.get('preferType') === SC.PICKER_MENU || this.get('preferType') === SC.PICKER_MENU_POINTER) {
1176         switch (this.get('controlSize')) {
1177         case SC.TINY_CONTROL_SIZE:
1178           this.set('pointerOffset', K.TINY_PICKER_MENU_POINTER_OFFSET);
1179           this.set('windowPadding', K.TINY_MENU_WINDOW_PADDING);
1180           break;
1181         case SC.SMALL_CONTROL_SIZE:
1182           this.set('pointerOffset', K.SMALL_PICKER_MENU_POINTER_OFFSET);
1183           this.set('windowPadding', K.SMALL_MENU_WINDOW_PADDING);
1184           break;
1185         case SC.REGULAR_CONTROL_SIZE:
1186           this.set('pointerOffset', K.REGULAR_PICKER_MENU_POINTER_OFFSET);
1187           this.set('windowPadding', K.REGULAR_MENU_WINDOW_PADDING);
1188           break;
1189         case SC.LARGE_CONTROL_SIZE:
1190           this.set('pointerOffset', K.LARGE_PICKER_MENU_POINTER_OFFSET);
1191           this.set('windowPadding', K.LARGE_MENU_WINDOW_PADDING);
1192           break;
1193         case SC.HUGE_CONTROL_SIZE:
1194           this.set('pointerOffset', K.HUGE_PICKER_MENU_POINTER_OFFSET);
1195           this.set('windowPadding', K.HUGE_MENU_WINDOW_PADDING);
1196           break;
1197         default:
1198           this.set('pointerOffset', K.REGULAR_PICKER_MENU_POINTER_OFFSET);
1199           this.set('windowPadding', K.REGULAR_MENU_WINDOW_PADDING);
1200           //@if(debug)
1201           SC.warn('SC.PickerPane with preferType of SC.PICKER_MENU_POINTER should either define a controlSize or provide a pointerOffset. SC.PickerPane will fall back to default pointerOffset of SC.PickerPane.REGULAR_PICKER_MENU_POINTER_OFFSET and default windowPadding of SC.PickerPane.WINDOW_PADDING');
1202           //@endif
1203         }
1204       } else {
1205         var overlapTuningX = (a.width < 16)  ? ((a.width < 4)  ? 9 : 6) : 0,
1206             overlapTuningY = (a.height < 16) ? ((a.height < 4) ? 9 : 6) : 0,
1207             offsetKey      = K.PICKER_POINTER_OFFSET;
1208 
1209         var offset = [offsetKey[0] + overlapTuningX,
1210                       offsetKey[1] - overlapTuningX,
1211                       offsetKey[2] - overlapTuningY,
1212                       offsetKey[3] + overlapTuningY];
1213 
1214         this.set('pointerOffset', offset);
1215       }
1216     }
1217 
1218     // set up preferMatrix according to type if not provided explicitly:
1219     // take default [0, 1, 2, 3, 2] for picker, [3, 0, 1, 2, 3] for menu picker if
1220     // custom matrix not provided explicitly
1221     var preferMatrix = this.get('preferMatrix');
1222     if (!preferMatrix || preferMatrix.length !== 5) {
1223       // menu-picker default re-position rule :
1224       // perfect bottom (3) > perfect right (0) > perfect left (1) > perfect top (2)
1225       // fallback to perfect bottom (3)
1226       // picker default re-position rule :
1227       // perfect right (0) > perfect left (1) > perfect top (2) > perfect bottom (3)
1228       // fallback to perfect top (2)
1229       this.set('preferMatrix', this.get('preferType') === SC.PICKER_MENU_POINTER ? [3, 2, 1, 0, 3] : [0, 1, 2, 3, 2]);
1230     }
1231   },
1232 
1233   /**
1234     @type Array
1235     @default ['pointerPos']
1236     @see SC.View#displayProperties
1237   */
1238   displayProperties: ['pointerPos'],
1239 
1240   /**
1241     @type String
1242     @default 'pickerRenderDelegate'
1243   */
1244   renderDelegateName: 'pickerRenderDelegate',
1245 
1246   /** @private - click away picker. */
1247   modalPaneDidClick: function (evt) {
1248     var f = this.get('frame'),
1249         target = this.get('removeTarget') || null,
1250         action = this.get('removeAction'),
1251         rootResponder = this.get('rootResponder');
1252 
1253     if (!this.clickInside(f, evt)) {
1254       // We're not in the Pane so we must be in the modal
1255       if (action) {
1256         rootResponder.sendAction(action, target, this, this, null, this);
1257       } else {
1258         this.remove();
1259       }
1260 
1261       return YES;
1262     }
1263 
1264     return NO;
1265   },
1266 
1267   /** @private */
1268   mouseDown: function (evt) {
1269     return this.modalPaneDidClick(evt);
1270   },
1271 
1272   /** @private
1273     internal method to define the range for clicking inside so the picker
1274     won't be clicked away default is the range of contentView frame.
1275     Over-write for adjustments. ex: shadow
1276   */
1277   clickInside: function (frame, evt) {
1278     return SC.pointInRect({ x: evt.pageX, y: evt.pageY }, frame);
1279   },
1280 
1281   /**
1282     Invoked by the root responder. Re-position picker whenever the window resizes.
1283   */
1284   windowSizeDidChange: function (oldSize, newSize) {
1285     sc_super();
1286 
1287     if (this.repositionOnWindowResize) {
1288       // Do this in the next run loop. This ensures that positionPane is only called once even if scroll view
1289       // offsets are changing at the same time as the window is resizing (see _scrollOffsetDidChange below).
1290       this.invokeNext(this.positionPane);
1291     }
1292   },
1293 
1294   remove: function () {
1295     if (this.get('isVisibleInWindow')) {
1296       this._withdrawOverflowRequest();
1297     }
1298     this._removeScrollObservers();
1299 
1300     return sc_super();
1301   },
1302 
1303   /** @private
1304     Internal method to hide the overflow on the body to make sure we don't
1305     show scrollbars when the picker has shadows, as it's really annoying.
1306   */
1307   _hideOverflow: function () {
1308     var main = SC.$('.sc-main'),
1309         minWidth = parseInt(main.css('minWidth'), 0),
1310         minHeight = parseInt(main.css('minHeight'), 0),
1311         windowSize = SC.RootResponder.responder.get('currentWindowSize');
1312 
1313     if (windowSize.width >= minWidth && windowSize.height >= minHeight) {
1314       SC.bodyOverflowArbitrator.requestHidden(this);
1315     }
1316   },
1317 
1318   /** @private
1319     Internal method to show the overflow on the body to make sure we don't
1320     show scrollbars when the picker has shadows, as it's really annoying.
1321   */
1322   _withdrawOverflowRequest: function () {
1323     SC.bodyOverflowArbitrator.withdrawRequest(this);
1324   },
1325 
1326   /** @private
1327     Detect if view is inside a scroll view. Do this by traversing parent view
1328     hierarchy until you hit a scroll view or main pane.
1329   */
1330   _getScrollViewOfView: function (view) {
1331     var curLevel = view;
1332     while (curLevel) {
1333       if (curLevel.isScrollable) {
1334         break;
1335       }
1336 
1337       curLevel = curLevel.get('parentView');
1338     }
1339 
1340     return curLevel;
1341   },
1342 
1343   /** @private
1344     If anchor view is in a scroll view, setup observers on scroll offsets.
1345   */
1346   _setupScrollObservers: function (anchorView) {
1347     var scrollView = this._getScrollViewOfView(anchorView);
1348     if (scrollView) {
1349       scrollView.addObserver('canScrollHorizontal', this, this._scrollCanScrollHorizontalDidChange);
1350       scrollView.addObserver('canScrollVertical', this, this._scrollCanScrollVerticalDidChange);
1351 
1352       // Fire the observers once to initialize them.
1353       this._scrollCanScrollHorizontalDidChange(scrollView);
1354       this._scrollCanScrollVerticalDidChange(scrollView);
1355 
1356       this._scrollView = scrollView;
1357     }
1358   },
1359 
1360   /** @private Modify horizontalScrollOffset observer. */
1361   _scrollCanScrollHorizontalDidChange: function (scrollView) {
1362     if (scrollView.get('canScrollHorizontal')) {
1363       scrollView.addObserver('horizontalScrollOffset', this, this._scrollOffsetDidChange);
1364     } else {
1365       scrollView.removeObserver('horizontalScrollOffset', this, this._scrollOffsetDidChange);
1366     }
1367   },
1368 
1369   /** @private Modify verticalScrollOffset observer. */
1370   _scrollCanScrollVerticalDidChange: function (scrollView) {
1371     if (scrollView.get('canScrollVertical')) {
1372       scrollView.addObserver('verticalScrollOffset', this, this._scrollOffsetDidChange);
1373     } else {
1374       scrollView.removeObserver('verticalScrollOffset', this, this._scrollOffsetDidChange);
1375     }
1376   },
1377 
1378   /** @private Teardown observers setup in _setupScrollObservers. */
1379   _removeScrollObservers: function () {
1380     var scrollView = this._scrollView;
1381     if (scrollView) {
1382       scrollView.removeObserver('canScrollHorizontal', this, this._scrollCanScrollHorizontalDidChange);
1383       scrollView.removeObserver('canScrollVertical', this, this._scrollCanScrollVerticalDidChange);
1384       scrollView.removeObserver('horizontalScrollOffset', this, this._scrollOffsetDidChange);
1385       scrollView.removeObserver('verticalScrollOffset', this, this._scrollOffsetDidChange);
1386     }
1387   },
1388 
1389   /** @private Reposition pane whenever scroll offsets change. */
1390   _scrollOffsetDidChange: function () {
1391     // Filter the observer firing. We don't want to reposition multiple times if both horizontal and vertical
1392     // scroll offsets are updating.
1393     // Note: do this *after* the current run loop finishes. This allows the scroll view to scroll to
1394     // actually move so that the anchor's position is correct before we reposition.
1395     this.invokeNext(this.positionPane);
1396   },
1397 
1398   /** @private SC.Object */
1399   init: function () {
1400     sc_super();
1401 
1402     // Set defaults that can only be configured on initialization.
1403     if (!this.windowPadding) { this.windowPadding = SC.PickerPane.WINDOW_PADDING; }
1404   },
1405 
1406   /** @private SC.Object */
1407   destroy: function () {
1408     this._scrollView = null;
1409     this._anchorView = null;
1410     this._anchorHTMLElement = null;
1411     return sc_super();
1412   }
1413 
1414 });
1415 
1416 
1417 /** Class methods. */
1418 SC.PickerPane.mixin( /** @scope SC.PickerPane */ {
1419 
1420   //---------------------------------------------------------------------------
1421   // Constants
1422   //
1423 
1424   /** @static */
1425   WINDOW_PADDING: 20,
1426 
1427   /** @static */
1428   TINY_MENU_WINDOW_PADDING: 12,
1429 
1430   /** @static */
1431   SMALL_MENU_WINDOW_PADDING: 11,
1432 
1433   /** @static */
1434   REGULAR_MENU_WINDOW_PADDING: 12,
1435 
1436   /** @static */
1437   LARGE_MENU_WINDOW_PADDING: 17,
1438 
1439   /** @static */
1440   HUGE_MENU_WINDOW_PADDING: 12,
1441 
1442   /** @static */
1443   PICKER_POINTER_OFFSET: [9, -9, -18, 18],
1444 
1445   /** @static */
1446   TINY_PICKER_MENU_POINTER_OFFSET: [9, -9, -18, 18],
1447 
1448   /** @static */
1449   SMALL_PICKER_MENU_POINTER_OFFSET: [9, -9, -8, 8],
1450 
1451   /** @static */
1452   REGULAR_PICKER_MENU_POINTER_OFFSET: [9, -9, -12, 12],
1453 
1454   /** @static */
1455   LARGE_PICKER_MENU_POINTER_OFFSET: [9, -9, -16, 16],
1456 
1457   /** @static */
1458   HUGE_PICKER_MENU_POINTER_OFFSET: [9, -9, -18, 18],
1459 
1460   /** @deprecated Version 1.10.  Use SC.PickerPane.WINDOW_PADDING.
1461     @static
1462   */
1463   PICKER_EXTRA_RIGHT_OFFSET: 20,
1464 
1465   /** @deprecated Version 1.10.  Use SC.PickerPane.TINY_MENU_WINDOW_PADDING.
1466     @static
1467   */
1468   TINY_PICKER_MENU_EXTRA_RIGHT_OFFSET: 12,
1469 
1470   /** @deprecated Version 1.10.  Use SC.PickerPane.SMALL_MENU_WINDOW_PADDING.
1471     @static
1472   */
1473   SMALL_PICKER_MENU_EXTRA_RIGHT_OFFSET: 11,
1474 
1475   /** @deprecated Version 1.10.  Use SC.PickerPane.REGULAR_MENU_WINDOW_PADDING.
1476     @static
1477   */
1478   REGULAR_PICKER_MENU_EXTRA_RIGHT_OFFSET: 12,
1479 
1480   /** @deprecated Version 1.10.  Use SC.PickerPane.LARGE_MENU_WINDOW_PADDING.
1481     @static
1482   */
1483   LARGE_PICKER_MENU_EXTRA_RIGHT_OFFSET: 17,
1484 
1485   /** @deprecated Version 1.10.  Use SC.PickerPane.HUGE_MENU_WINDOW_PADDING.
1486     @static
1487   */
1488   HUGE_PICKER_MENU_EXTRA_RIGHT_OFFSET: 12
1489 
1490 });
1491