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