1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2010 Sprout Systems, 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/picker'); 9 sc_require('views/menu_item'); 10 11 /** 12 @class 13 14 `SC.MenuPane` allows you to display a standard menu. Menus appear over other 15 panes, and block input to other views until a selection is made or the pane 16 is dismissed by clicking outside of its bounds. 17 18 You can create a menu pane and manage it yourself, or you can use the 19 `SC.SelectButtonView` and `SC.PopupButtonView` controls to manage the menu for 20 you. 21 22 ## Specifying Menu Items 23 24 The menu pane examines the `items` property to determine what menu items 25 should be presented to the user. 26 27 In its most simple form, you can provide an array of strings. Every item 28 will be converted into a menu item whose title is the string. 29 30 If you need more control over the menu items, such as specifying a keyboard 31 shortcut, enabled state, custom height, or submenu, you can provide an array 32 of content objects. 33 34 Out of the box, the menu pane has some default keys it uses to get 35 information from the objects. For example, to find out the title of the menu 36 item, the menu pane will ask your object for its `title` property. If you 37 need to change this key, you can set the `itemTitleKey` property on the pane 38 itself. 39 40 var menuItems = [ 41 { title: 'Menu Item', keyEquivalent: 'ctrl_shift_n' }, 42 { title: 'Checked Menu Item', checkbox: YES, keyEquivalent: 'ctrl_a' }, 43 { title: 'Selected Menu Item', keyEquivalent: ['backspace', 'delete'] }, 44 { isSeparator: YES }, 45 { title: 'Menu Item with Icon', icon: 'inbox', keyEquivalent: 'ctrl_m' }, 46 { title: 'Menu Item with Icon', icon: 'folder', keyEquivalent: 'ctrl_p' } 47 ]; 48 49 var menu = SC.MenuPane.create({ 50 items: menuItems 51 }); 52 53 ## Observing User Selections 54 55 To determine when a user clicks on a menu item, you can observe the 56 `selectedItem` property for changes. 57 58 @extends SC.PickerPane 59 @since SproutCore 1.0 60 */ 61 SC.MenuPane = SC.PickerPane.extend( 62 /** @scope SC.MenuPane.prototype */ { 63 64 /** @private Cache of the items array, used for clean up of observers. */ 65 _sc_menu_items: null, 66 67 /** 68 @type Array 69 @default ['sc-menu'] 70 @see SC.View#classNames 71 */ 72 classNames: ['sc-menu'], 73 74 /** 75 The WAI-ARIA role for menu pane. 76 77 @type String 78 @default 'menu' 79 @constant 80 */ 81 ariaRole: 'menu', 82 83 84 // .......................................................... 85 // PROPERTIES 86 // 87 88 /** 89 The array of items to display. This can be a simple array of strings, 90 objects or hashes. If you pass objects or hashes, you can also set the 91 various itemKey properties to tell the menu how to extract the information 92 it needs. 93 94 @type Array 95 @default [] 96 */ 97 items: null, 98 99 /** 100 The size of the menu. This will set a CSS style on the menu that can be 101 used by the current theme to style the appearance of the control. This 102 value will also determine the default `itemHeight`, `itemSeparatorHeight`, 103 `menuHeightPadding`, and `submenuOffsetX` if you don't explicitly set these 104 properties. 105 106 Your theme can override the default values for each control size by specifying 107 them in the `menuRenderDelegate`. For example: 108 109 MyTheme.menuRenderDelegate = SC.BaseTheme.menuRenderDelegate.create({ 110 'sc-tiny-size': { 111 itemHeight: 20, 112 itemSeparatorHeight: 9, 113 menuHeightPadding: 6, 114 submenuOffsetX: 2 115 } 116 }); 117 118 Changing the controlSize once the menu is instantiated has no effect. 119 120 @type String 121 @default SC.REGULAR_CONTROL_SIZE 122 */ 123 controlSize: SC.REGULAR_CONTROL_SIZE, 124 125 /** 126 The height of each menu item, in pixels. 127 128 You can override this on a per-item basis by setting the (by default) 129 `height` property on your object. 130 131 If you don't specify a value, the item height will be inferred from 132 `controlSize`. 133 134 @type Number 135 @default itemHeight from theme if present, or 20. 136 */ 137 itemHeight: SC.propertyFromRenderDelegate('itemHeight', 20), 138 139 /** 140 The height of separator menu items. 141 142 You can override this on a per-item basis by setting the (by default) 143 `height` property on your object. 144 145 If you don't specify a value, the height of the separator menu items will 146 be inferred from `controlSize`. 147 148 @type Number 149 @default itemSeparatorHeight from theme, or 9. 150 */ 151 itemSeparatorHeight: SC.propertyFromRenderDelegate('itemSeparatorHeight', 9), 152 153 /** 154 The height of the menu pane. This is updated every time menuItemViews 155 is recalculated. 156 157 @type Number 158 @default 0 159 @isReadOnly 160 */ 161 menuHeight: 0, 162 163 /** 164 The amount of padding to add to the height of the pane. 165 166 The first menu item is offset by half this amount, and the other half is 167 added to the height of the menu, such that a space between the top and the 168 bottom is created. 169 170 If you don't specify a value, the padding will be inferred from the 171 controlSize. 172 173 @type Number 174 @default menuHeightPadding from theme, or 6 175 */ 176 menuHeightPadding: SC.propertyFromRenderDelegate('menuHeightPadding', 6), 177 178 /** 179 The amount of offset x while positioning submenu. 180 181 If you don't specify a value, the padding will be inferred from the 182 controlSize. 183 184 @type Number 185 @default submenuOffsetX from theme, or 2 186 */ 187 submenuOffsetX: SC.propertyFromRenderDelegate('submenuOffsetX', 2), 188 189 /** 190 The last menu item to be selected by the user. 191 192 You can place an observer on this property to be notified when the user 193 makes a selection. 194 195 @type SC.Object 196 @default null 197 @isReadOnly 198 */ 199 selectedItem: null, 200 201 /** 202 The view class to use when creating new menu item views. 203 204 The menu pane will automatically create an instance of the view class you 205 set here for each item in the `items` array. You may provide your own 206 subclass for this property to display the customized content. 207 208 @type SC.View 209 @default SC.MenuItemView 210 */ 211 exampleView: SC.MenuItemView, 212 213 /** 214 The view or element to which the menu will anchor itself. 215 216 When the menu pane is shown, it will remain anchored to the view you 217 specify, even if the window is resized. You should specify the anchor as a 218 parameter when calling `popup()`, rather than setting it directly. 219 220 @type SC.View 221 @isReadOnly 222 */ 223 anchor: null, 224 225 /** 226 `YES` if this menu pane was generated by a parent `SC.MenuPane`. 227 228 @type Boolean 229 @default NO 230 @isReadOnly 231 */ 232 isSubMenu: NO, 233 234 /** 235 If true, title of menu items will be escaped to avoid scripting attacks. 236 237 @type Boolean 238 @default YES 239 */ 240 escapeHTML: YES, 241 242 /** 243 Whether the title of menu items should be localized before display. 244 245 @type Boolean 246 @default YES 247 */ 248 localize: YES, 249 250 /** 251 Whether or not this menu pane should accept the “current menu pane” 252 designation when visible, which is the highest-priority pane when routing 253 events. Generally you want this set to `YES` so that your menu pane can 254 intercept keyboard events. 255 256 @type Boolean 257 @default YES 258 */ 259 acceptsMenuPane: YES, 260 261 /** 262 Disable context menu. 263 264 @type Boolean 265 @default NO 266 */ 267 isContextMenuEnabled: NO, 268 269 270 // .......................................................... 271 // METHODS 272 // 273 274 /** 275 Makes the menu visible and adds it to the HTML document. 276 277 If you provide a view or element as the first parameter, the menu will 278 anchor itself to the view, and intelligently reposition itself if the 279 contents of the menu exceed the available space. 280 281 @param {SC.View} anchorViewOrElement the view or element to which the menu 282 should anchor. 283 @param {Array} (preferMatrix) The prefer matrix used to position the pane. 284 */ 285 popup: function (anchorViewOrElement, preferMatrix) { 286 this.beginPropertyChanges(); 287 if (anchorViewOrElement) { 288 if (anchorViewOrElement.isView) { 289 this._anchorView = anchorViewOrElement; 290 this._setupScrollObservers(anchorViewOrElement); 291 } else { 292 this._anchorHTMLElement = anchorViewOrElement; 293 } 294 } 295 // this.set('anchor',anchorViewOrElement); 296 if (preferMatrix) this.set('preferMatrix', preferMatrix); 297 298 // Resize the pane's initial height to fit the height of the menu. 299 // Note: SC.PickerPane's positioning code may adjust the height to fit within the window. 300 this.adjust('height', this.get('menuHeight')); 301 this.positionPane(); 302 303 // Because panes themselves do not receive key events, we need to set the 304 // pane's defaultResponder to itself. This way key events can be 305 // interpreted in keyUp. 306 this.set('defaultResponder', this); 307 this.endPropertyChanges(); 308 309 // Prevent body overflow (we don't want to overflow because of shadows). 310 SC.bodyOverflowArbitrator.requestHidden(this, true); 311 312 //@if(debug) 313 // A debug-mode only flag to indicate that the popup method was called (see override of append in SC.PickerPane). 314 this._sc_didUsePopup = true; 315 //@endif 316 317 this.append(); 318 }, 319 320 // .......................................................... 321 // ITEM KEYS 322 // 323 324 /** 325 The name of the property that contains the title for each item. 326 327 @type String 328 @default "title" 329 @commonTask Menu Item Properties 330 */ 331 itemTitleKey: 'title', 332 333 /** 334 The name of the property that contains the value for each item. 335 336 @type String 337 @default "value" 338 @commonTask Menu Item Properties 339 */ 340 itemValueKey: 'value', 341 342 /** 343 The name of the property that contains the tooltip for each item. 344 345 @type String 346 @default "toolTip" 347 @commonTask Menu Item Properties 348 */ 349 itemToolTipKey: 'toolTip', 350 351 /** 352 The name of the property that determines whether the item is enabled. 353 354 @type String 355 @default "isEnabled" 356 @commonTask Menu Item Properties 357 */ 358 itemIsEnabledKey: 'isEnabled', 359 360 /** 361 The name of the property that contains the icon for each item. 362 363 @type String 364 @default "icon" 365 @commonTask Menu Item Properties 366 */ 367 itemIconKey: 'icon', 368 369 /** 370 The name of the property that contains the height for each item. 371 372 @readOnly 373 @type String 374 @default "height" 375 @commonTask Menu Item Properties 376 */ 377 itemHeightKey: 'height', 378 379 /** 380 The name of the property that contains an optional submenu for each item. 381 382 @type String 383 @default "subMenu" 384 @commonTask Menu Item Properties 385 */ 386 itemSubMenuKey: 'subMenu', 387 388 /** 389 The name of the property that determines whether the item is a menu 390 separator. 391 392 @type String 393 @default "isSeparator" 394 @commonTask Menu Item Properties 395 */ 396 itemSeparatorKey: 'isSeparator', 397 398 /** 399 The name of the property that contains the target for the action that is 400 triggered when the user clicks the menu item. 401 402 Note that this property is ignored if the menu item has a submenu. 403 404 @type String 405 @default "target" 406 @commonTask Menu Item Properties 407 */ 408 itemTargetKey: 'target', 409 410 /** 411 The name of the property that contains the action that is triggered when 412 the user clicks the menu item. 413 414 Note that this property is ignored if the menu item has a submenu. 415 416 @type String 417 @default "action" 418 @commonTask Menu Item Properties 419 */ 420 itemActionKey: 'action', 421 422 /** 423 The name of the property that determines whether the menu item should 424 display a checkbox. 425 426 @type String 427 @default "checkbox" 428 @commonTask Menu Item Properties 429 */ 430 itemCheckboxKey: 'checkbox', 431 432 /** 433 The name of the property that contains the shortcut to be displayed. 434 435 The shortcut should communicate the keyboard equivalent to the user. 436 437 @type String 438 @default "shortcut" 439 @commonTask Menu Item Properties 440 */ 441 itemShortCutKey: 'shortcut', 442 443 /** 444 The name of the property that contains the key equivalent of the menu 445 item. 446 447 The action of the menu item will be fired, and the menu pane's 448 `selectedItem` property set to the menu item, if the user presses this 449 key combination on the keyboard. 450 451 @type String 452 @default "keyEquivalent" 453 @commonTask Menu Item Properties 454 */ 455 itemKeyEquivalentKey: 'keyEquivalent', 456 457 /** 458 The name of the property that determines whether menu flash should be 459 disabled. 460 461 When you click on a menu item, it will flash several times to indicate 462 selection to the user. Some browsers block windows from opening outside of 463 a mouse event, so you may wish to disable menu flashing if the action of 464 the menu item should open a new window. 465 466 @type String 467 @default "disableMenuFlash" 468 @commonTask Menu Item Properties 469 */ 470 itemDisableMenuFlashKey: 'disableMenuFlash', 471 472 /** 473 The name of the property that determines whether layerID should be applied to the item . 474 475 @type String 476 @default "layerId" 477 @commonTask Menu Item Properties 478 */ 479 itemLayerIdKey: 'layerId', 480 481 /** 482 The name of the property that determines whether a unique exampleView should be created for the item . 483 484 @type String 485 @default "exampleView" 486 @commonTask Menu Item Properties 487 */ 488 itemExampleViewKey: 'exampleView', 489 490 /** 491 The array of keys used by SC.MenuItemView when inspecting your menu items 492 for display properties. 493 494 @private 495 @isReadOnly 496 @type Array 497 */ 498 menuItemKeys: ['itemTitleKey', 'itemValueKey', 'itemToolTipKey', 'itemIsEnabledKey', 'itemIconKey', 'itemSeparatorKey', 'itemActionKey', 'itemCheckboxKey', 'itemShortCutKey', 'itemHeightKey', 'itemSubMenuKey', 'itemKeyEquivalentKey', 'itemTargetKey', 'itemLayerIdKey'], 499 500 // .......................................................... 501 // DEFAULT PROPERTIES 502 // 503 504 /* 505 If an item doesn't specify a target, this is used. (Only used if an action is found and is not a function.) 506 507 @type String 508 @default null 509 */ 510 target: null, 511 512 /* 513 If an item doesn't specify an action, this is used. 514 515 @type String 516 @default null 517 */ 518 action: null, 519 520 // .......................................................... 521 // INTERNAL PROPERTIES 522 // 523 524 /** @private */ 525 preferType: SC.PICKER_MENU, 526 527 // .......................................................... 528 // INTERNAL METHODS 529 // 530 531 /** @private */ 532 init: function () { 533 // Initialize the items array. 534 if (!this.items) { this.items = []; } 535 536 // An associative array of the shortcut keys. The key is the shortcut in the 537 // form 'ctrl_z', and the value is the menu item of the action to trigger. 538 this._keyEquivalents = {}; 539 540 // Continue initializing now that default values exist. 541 sc_super(); 542 543 // Initialize the observer function once. 544 this._sc_menu_itemsDidChange(); 545 }, 546 547 displayProperties: ['controlSize'], 548 549 renderDelegateName: 'menuRenderDelegate', 550 551 /** 552 Creates the child scroll view, and sets its `contentView` to a new 553 view. This new view is saved and managed by the `SC.MenuPane`, 554 and contains the visible menu items. 555 556 @private 557 @returns {SC.View} receiver 558 */ 559 createChildViews: function () { 560 var scroll, menuView; 561 562 // Create the menu items collection view. 563 // TODO: Should this not be an SC.ListView? 564 menuView = this._menuView = SC.View.create({ 565 layout: { height: 0 } 566 }); 567 568 scroll = this._menuScrollView = this.createChildView(SC.MenuScrollView, { 569 controlSize: this.get('controlSize'), 570 contentView: menuView 571 }); 572 573 this.childViews = [scroll]; 574 575 return this; 576 }, 577 578 /** 579 Called when the pane is attached. Takes on menu pane status. 580 581 We don't call `sc_super()` here because `PanelPane` sets the current pane to 582 be the key pane when attached. 583 */ 584 didAppendToDocument: function () { 585 this.becomeMenuPane(); 586 }, 587 588 /** 589 Called when the pane is detached. Closes all submenus and resigns menu pane 590 status. 591 592 We don't call `sc_super()` here because `PanelPane` resigns key pane when 593 detached. 594 */ 595 willRemoveFromDocument: function () { 596 var parentMenu = this.get('parentMenu'); 597 598 this.set('currentMenuItem', null); 599 this.closeOpenMenus(); 600 this.resignMenuPane(); 601 602 if (parentMenu) { 603 parentMenu.becomeMenuPane(); 604 } 605 }, 606 607 /** 608 Make the pane the menu pane. When you call this, all key events will 609 temporarily be routed to this pane. Make sure that you call 610 resignMenuPane; otherwise all key events will be blocked to other panes. 611 612 @returns {SC.Pane} receiver 613 */ 614 becomeMenuPane: function () { 615 if (this.rootResponder) this.rootResponder.makeMenuPane(this); 616 617 return this; 618 }, 619 620 /** 621 Remove the menu pane status from the pane. This will simply set the 622 `menuPane` on the `rootResponder` to `null. 623 624 @returns {SC.Pane} receiver 625 */ 626 resignMenuPane: function () { 627 if (this.rootResponder) this.rootResponder.makeMenuPane(null); 628 629 return this; 630 }, 631 632 /** 633 The array of child menu item views that compose the menu. 634 635 This computed property parses `displayItems` and constructs an 636 `SC.MenuItemView` (or whatever class you have set as the `exampleView`) for every item. 637 638 This calls createMenuItemViews. If you want to override this property, override 639 that method. 640 641 @type 642 @type Array 643 @readOnly 644 */ 645 menuItemViews: function () { 646 return this.createMenuItemViews(); 647 }.property('displayItems').cacheable(), 648 649 /** 650 Processes the displayItems and creates menu item views for each item. 651 652 Override this method to change how menuItemViews is calculated. 653 654 @return Array 655 */ 656 createMenuItemViews: function () { 657 var views = [], items = this.get('displayItems'), 658 exampleView = this.get('exampleView'), item, itemView, view, 659 exampleViewKey, itemExampleView, 660 height, heightKey, separatorKey, defaultHeight, separatorHeight, 661 menuHeight, menuHeightPadding, keyEquivalentKey, keyEquivalent, 662 keyArray, idx, layerIdKey, propertiesHash, escapeHTML, 663 len; 664 665 if (!items) return views; // return an empty array 666 667 heightKey = this.get('itemHeightKey'); 668 separatorKey = this.get('itemSeparatorKey'); 669 exampleViewKey = this.get('itemExampleViewKey'); 670 defaultHeight = this.get('itemHeight'); 671 keyEquivalentKey = this.get('itemKeyEquivalentKey'); 672 separatorHeight = this.get('itemSeparatorHeight'); 673 layerIdKey = this.get('itemLayerIdKey'); 674 escapeHTML = this.get('escapeHTML'); 675 menuHeightPadding = Math.floor(this.get('menuHeightPadding') / 2); 676 menuHeight = menuHeightPadding; 677 678 keyArray = this.menuItemKeys.map(SC._menu_fetchKeys, this); 679 680 len = items.get('length'); 681 for (idx = 0; idx < len; idx++) { 682 item = items[idx]; 683 height = item.get(heightKey); 684 if (!height) { 685 height = item.get(separatorKey) ? separatorHeight : defaultHeight; 686 } 687 688 propertiesHash = { 689 layout: { height: height, top: menuHeight }, 690 contentDisplayProperties: keyArray, 691 content: item, 692 parentMenu: this, 693 escapeHTML: escapeHTML 694 }; 695 696 if (item.get(layerIdKey)) { 697 propertiesHash.layerId = item.get(layerIdKey); 698 } 699 700 // Item has its own exampleView so use it 701 itemExampleView = item.get(exampleViewKey); 702 if (itemExampleView) { 703 itemView = itemExampleView; 704 } else { 705 itemView = exampleView; 706 } 707 708 view = this._menuView.createChildView(itemView, propertiesHash); 709 views[idx] = view; 710 menuHeight += height; 711 keyEquivalent = item.get(keyEquivalentKey); 712 if (keyEquivalent) { 713 // if array, apply each one for this view 714 if (SC.typeOf(keyEquivalent) === SC.T_ARRAY) { 715 keyEquivalent.forEach(function (keyEq) { 716 this._keyEquivalents[keyEq] = view; 717 }, this); 718 } else { 719 this._keyEquivalents[keyEquivalent] = view; 720 } 721 } 722 } 723 724 this.set('menuHeight', menuHeight + menuHeightPadding); 725 return views; 726 }, 727 728 /** 729 Returns the menu item view for the content object at the specified index. 730 731 @param Number idx the item index 732 @returns {SC.MenuItemView} instantiated view 733 */ 734 menuItemViewForContentIndex: function (idx) { 735 var menuItemViews = this.get('menuItemViews'); 736 737 if (!menuItemViews) return undefined; 738 return menuItemViews.objectAt(idx); 739 }, 740 741 /** 742 If this is a submenu, this property corresponds to the 743 top-most parent menu. If this is the root menu, it returns 744 itself. 745 746 @type SC.MenuPane 747 @isReadOnly 748 @type 749 */ 750 rootMenu: function () { 751 if (this.get('isSubMenu')) return this.getPath('parentMenu.rootMenu'); 752 return this; 753 }.property('isSubMenu').cacheable(), 754 755 /** @private @see SC.Object */ 756 destroy: function () { 757 var ret = sc_super(); 758 759 // Clean up previous enumerable observer. 760 if (this._sc_menu_items) { 761 this._sc_menu_items.removeObserver('[]', this, '_sc_menu_itemPropertiesDidChange'); 762 } 763 764 // Destroy the menu view we created. The scroll view's container will NOT 765 // destroy this because it receives it already instantiated. 766 this._menuView.destroy(); 767 768 // Clean up caches. 769 this._sc_menu_items = null; 770 this._menuView = null; 771 772 return ret; 773 }, 774 775 /** 776 Close the menu if the user resizes the window. 777 778 @private 779 */ 780 windowSizeDidChange: function () { 781 this.remove(); 782 return sc_super(); 783 }, 784 785 /** 786 Returns an array of normalized display items. 787 788 Because the items property can be provided as either an array of strings, 789 or an object with key-value pairs, or an exotic mish-mash of both, we need 790 to normalize it for our display logic. 791 792 If an `items` member is an object, we can assume it is formatted properly 793 and leave it as-is. 794 795 If an `items` member is a string, we create a hash with the title value 796 set to that string, and some sensible defaults for the other properties. 797 798 A side effect of running this computed property is that the menuHeight 799 property is updated. 800 801 `displayItems` should never be set directly; instead, set `items` and 802 `displayItems` will update automatically. 803 804 @type 805 @type Array 806 @isReadOnly 807 */ 808 displayItems: function () { 809 var items = this.get('items'), 810 len, 811 ret = [], idx, item, itemType; 812 813 if (!items) return null; 814 815 len = items.get('length'); 816 817 // Loop through the items property and transmute as needed, then 818 // copy the new objects into the ret array. 819 for (idx = 0; idx < len; idx++) { 820 item = items.objectAt(idx); 821 822 // fast track out if we can't do anything with this item 823 if (!item || (!ret.length && item[this.get('itemSeparatorKey')])) continue; 824 825 itemType = SC.typeOf(item); 826 if (itemType === SC.T_STRING) { 827 item = SC.Object.create({ title: item, 828 value: item, 829 isEnabled: YES 830 }); 831 } else if (itemType === SC.T_HASH) { 832 item = SC.Object.create(item); 833 } 834 item.contentIndex = idx; 835 836 ret.push(item); 837 } 838 839 return ret; 840 }.property('items').cacheable(), 841 842 /** @private */ 843 _sc_menu_itemsDidChange: function () { 844 var items = this.get('items'); 845 846 // Clean up previous enumerable observer. 847 if (this._sc_menu_items) { 848 this._sc_menu_items.removeObserver('[]', this, '_sc_menu_itemPropertiesDidChange'); 849 } 850 851 // Add new enumerable observer 852 if (items) { 853 items.addObserver('[]', this, '_sc_menu_itemPropertiesDidChange'); 854 } 855 856 // Cache the last items. 857 this._sc_menu_items = items; 858 859 var itemViews; 860 itemViews = this.get('menuItemViews'); 861 this._menuView.replaceAllChildren(itemViews); 862 this._menuView.adjust('height', this.get('menuHeight')); 863 }.observes('items'), 864 865 /** @private */ 866 _sc_menu_itemPropertiesDidChange: function () { 867 // Indicate that the displayItems changed. 868 this.notifyPropertyChange('displayItems'); 869 870 var itemViews; 871 itemViews = this.get('menuItemViews'); 872 this._menuView.replaceAllChildren(itemViews); 873 this._menuView.adjust('height', this.get('menuHeight')); 874 }, 875 876 currentMenuItem: function (key, value) { 877 if (value !== undefined) { 878 if (this._currentMenuItem !== null) { 879 this.set('previousMenuItem', this._currentMenuItem); 880 } 881 this._currentMenuItem = value; 882 this.setPath('rootMenu.targetMenuItem', value); 883 884 return value; 885 } 886 887 return this._currentMenuItem; 888 }.property().cacheable(), 889 890 /** @private */ 891 _sc_menu_currentMenuItemDidChange: function () { 892 var currentMenuItem = this.get('currentMenuItem'), 893 previousMenuItem = this.get('previousMenuItem'); 894 895 if (previousMenuItem) { 896 if (previousMenuItem.get('hasSubMenu') && currentMenuItem === null) { 897 898 } else { 899 previousMenuItem.resignFirstResponder(); 900 this.closeOpenMenusFor(previousMenuItem); 901 } 902 } 903 904 // Scroll to the selected menu item if it's not visible on screen. 905 // This is useful for keyboard navigation and programmatically selecting 906 // the selected menu item, as in `SelectButtonView`. 907 if (currentMenuItem && currentMenuItem.get('isEnabled')) { 908 currentMenuItem.scrollToVisible(); 909 } 910 }.observes('currentMenuItem'), 911 912 closeOpenMenusFor: function (menuItem) { 913 if (!menuItem) return; 914 915 var menu = menuItem.get('parentMenu'); 916 917 // Close any open menus if a root menu changes 918 while (menu && menuItem) { 919 menu = menuItem.get('subMenu'); 920 if (menu) { 921 menu.remove(); 922 menuItem.resignFirstResponder(); 923 menuItem = menu.get('previousMenuItem'); 924 } 925 } 926 }, 927 928 closeOpenMenus: function () { 929 this.closeOpenMenusFor(this.get('previousMenuItem')); 930 }, 931 932 //Mouse and Key Events 933 934 /** @private */ 935 mouseDown: function (evt) { 936 this.modalPaneDidClick(evt); 937 return YES; 938 }, 939 940 /** @private 941 Note when the mouse has entered, so that if this is a submenu, 942 the menu item to which it belongs knows whether to maintain its highlight 943 or not. 944 945 @param {Event} evt 946 */ 947 mouseEntered: function () { 948 this.set('mouseHasEntered', YES); 949 }, 950 951 keyUp: function (evt) { 952 var ret = this.interpretKeyEvents(evt); 953 return !ret ? NO : ret; 954 }, 955 956 /** 957 Selects the next enabled menu item above the currently 958 selected menu item when the up-arrow key is pressed. 959 960 @private 961 */ 962 moveUp: function () { 963 var currentMenuItem = this.get('currentMenuItem'), 964 items = this.get('menuItemViews'), 965 currentIndex, idx; 966 967 if (!currentMenuItem) { 968 idx = items.get('length') - 1; 969 } else { 970 currentIndex = currentMenuItem.getPath('content.contentIndex'); 971 if (currentIndex === 0) return YES; 972 idx = currentIndex - 1; 973 } 974 975 while (idx >= 0) { 976 if (items[idx].get('isEnabled')) { 977 this.set('currentMenuItem', items[idx]); 978 items[idx].becomeFirstResponder(); 979 break; 980 } 981 idx--; 982 } 983 984 return YES; 985 }, 986 987 /** 988 Selects the next enabled menu item below the currently 989 selected menu item when the down-arrow key is pressed. 990 991 @private 992 */ 993 moveDown: function () { 994 var currentMenuItem = this.get('currentMenuItem'), 995 items = this.get('menuItemViews'), 996 len = items.get('length'), 997 currentIndex, idx; 998 999 if (!currentMenuItem) { 1000 idx = 0; 1001 } else { 1002 currentIndex = currentMenuItem.getPath('content.contentIndex'); 1003 if (currentIndex === len) return YES; 1004 idx = currentIndex + 1; 1005 } 1006 1007 while (idx < len) { 1008 if (items[idx].get('isEnabled')) { 1009 this.set('currentMenuItem', items[idx]); 1010 items[idx].becomeFirstResponder(); 1011 break; 1012 } 1013 idx++; 1014 } 1015 1016 return YES; 1017 }, 1018 1019 /** 1020 Selects the first enabled menu item when the home key is pressed. 1021 1022 @private 1023 */ 1024 moveToBeginningOfDocument: function () { 1025 var items = this.get('menuItemViews'), 1026 len = items.get('length'), 1027 idx = 0; 1028 1029 while (idx < len) { 1030 if (items[idx].get('isEnabled')) { 1031 this.set('currentMenuItem', items[idx]); 1032 items[idx].becomeFirstResponder(); 1033 break; 1034 } 1035 ++idx; 1036 } 1037 1038 return YES; 1039 }, 1040 1041 /** 1042 Selects the last enabled menu item when the end key is pressed. 1043 1044 @private 1045 */ 1046 moveToEndOfDocument: function () { 1047 var items = this.get('menuItemViews'), 1048 len = items.get('length'), 1049 idx = len - 1; 1050 1051 while (idx >= 0) { 1052 if (items[idx].get('isEnabled')) { 1053 this.set('currentMenuItem', items[idx]); 1054 items[idx].becomeFirstResponder(); 1055 break; 1056 } 1057 --idx; 1058 } 1059 1060 return YES; 1061 }, 1062 1063 /** 1064 Selects the next item one page down. If that is not enabled, 1065 continues to move down until it finds an enabled item. 1066 1067 @private 1068 */ 1069 pageDown: function () { 1070 var currentMenuItem = this.get('currentMenuItem'), 1071 item, items = this.get('menuItemViews'), 1072 len = items.get('length'), 1073 idx = 0, 1074 foundItemIdx, 1075 verticalOffset = 0, 1076 verticalPageScroll; 1077 1078 if (this._menuScrollView && this._menuScrollView.bottomScrollerView) { 1079 1080 if (currentMenuItem) { 1081 idx = currentMenuItem.getPath('content.contentIndex'); 1082 } 1083 1084 foundItemIdx = idx; 1085 verticalPageScroll = this._menuScrollView.get('verticalPageScroll'); 1086 for (idx; idx < len; idx++) { 1087 item = items[idx]; 1088 verticalOffset += item.get('layout').height; 1089 1090 if (verticalOffset > verticalPageScroll) { 1091 // We've gone further than an entire page scroll, so stop. 1092 break; 1093 } else { 1094 // Only accept enabled items (which also excludes separators). 1095 if (item.get('isEnabled')) { foundItemIdx = idx; } 1096 } 1097 } 1098 1099 item = items[foundItemIdx]; 1100 this.set('currentMenuItem', item); 1101 item.becomeFirstResponder(); 1102 } else { 1103 this.moveToEndOfDocument(); 1104 } 1105 1106 return YES; 1107 }, 1108 1109 /** 1110 Selects the previous item one page up. If that is not enabled, 1111 continues to move up until it finds an enabled item. 1112 1113 @private 1114 */ 1115 pageUp: function () { 1116 var currentMenuItem = this.get('currentMenuItem'), 1117 item, items = this.get('menuItemViews'), 1118 len = items.get('length'), 1119 idx = len - 1, 1120 foundItemIdx, 1121 verticalOffset = 0, 1122 verticalPageScroll; 1123 1124 if (this._menuScrollView && this._menuScrollView.topScrollerView) { 1125 1126 if (currentMenuItem) { 1127 idx = currentMenuItem.getPath('content.contentIndex'); 1128 } 1129 1130 foundItemIdx = idx; 1131 verticalPageScroll = this._menuScrollView.get('verticalPageScroll'); 1132 for (idx; idx >= 0; idx--) { 1133 item = items[idx]; 1134 verticalOffset += item.get('layout').height; 1135 1136 if (verticalOffset > verticalPageScroll) { 1137 // We've gone further than an entire page scroll, so stop. 1138 break; 1139 } else { 1140 // Only accept enabled items (which also excludes separators). 1141 if (item.get('isEnabled')) { foundItemIdx = idx; } 1142 } 1143 } 1144 1145 item = items[foundItemIdx]; 1146 this.set('currentMenuItem', item); 1147 item.becomeFirstResponder(); 1148 } else { 1149 this.moveToBeginningOfDocument(); 1150 } 1151 1152 return YES; 1153 }, 1154 1155 insertText: function (chr) { 1156 var timer = this._timer, keyBuffer = this._keyBuffer; 1157 1158 if (timer) { 1159 timer.invalidate(); 1160 } 1161 timer = this._timer = SC.Timer.schedule({ 1162 target: this, 1163 action: 'clearKeyBuffer', 1164 interval: 500, 1165 isPooled: NO 1166 }); 1167 1168 keyBuffer = keyBuffer || ''; 1169 keyBuffer += chr.toUpperCase(); 1170 1171 this.selectMenuItemForString(keyBuffer); 1172 this._keyBuffer = keyBuffer; 1173 1174 return YES; 1175 }, 1176 1177 /** @private 1178 Called by the view hierarchy when the menu should respond to a shortcut 1179 key being pressed. 1180 1181 Normally, the menu will only respond to key presses when it is visible. 1182 However, when the menu is part of another control, such as an 1183 SC.PopupButtonView, the menu should still respond if it is hidden but its 1184 parent control is visible. In those cases, the parameter 1185 fromVisibleControl will be set to `YES`. 1186 1187 @param keyEquivalent {String} the shortcut key sequence that was pressed 1188 @param fromVisibleControl Boolean if the shortcut key press was proxied 1189 to this menu by a visible parent control 1190 @returns Boolean 1191 */ 1192 performKeyEquivalent: function (keyEquivalent, evt, fromVisibleControl) { 1193 //If menu is not visible 1194 if (!fromVisibleControl && !this.get('isVisibleInWindow')) return NO; 1195 1196 // Look for menu item that has this key equivalent 1197 var menuItem = this._keyEquivalents[keyEquivalent]; 1198 1199 // If found, have it perform its action 1200 if (menuItem) { 1201 menuItem.performAction(YES); 1202 return YES; 1203 } 1204 1205 // If escape key or the enter key was pressed and no menu item handled it, 1206 // close the menu pane and return YES that the event was handled 1207 if (keyEquivalent === 'escape' || keyEquivalent === 'return') { 1208 this.remove(); 1209 return YES; 1210 } 1211 1212 return NO; 1213 1214 }, 1215 1216 selectMenuItemForString: function (buffer) { 1217 var items = this.get('menuItemViews'), item, title, idx, len, bufferLength; 1218 if (!items) return; 1219 1220 bufferLength = buffer.length; 1221 len = items.get('length'); 1222 for (idx = 0; idx < len; idx++) { 1223 item = items.objectAt(idx); 1224 title = item.get('title'); 1225 1226 if (!title) continue; 1227 1228 title = title.replace(/ /g, '').substr(0, bufferLength).toUpperCase(); 1229 if (title === buffer) { 1230 this.set('currentMenuItem', item); 1231 item.becomeFirstResponder(); 1232 break; 1233 } 1234 } 1235 }, 1236 1237 /** 1238 Clear the key buffer if the user does not enter any text after a certain 1239 amount of time. 1240 1241 This is called by the timer created in the `insertText` method. 1242 1243 @private 1244 */ 1245 clearKeyBuffer: function () { 1246 this._keyBuffer = ''; 1247 }, 1248 1249 /** 1250 Close the menu and any open submenus if the user clicks outside the menu. 1251 1252 Because only the root-most menu has a modal pane, this will only ever get 1253 called once. 1254 1255 @returns Boolean 1256 @private 1257 */ 1258 modalPaneDidClick: function () { 1259 this.remove(); 1260 1261 return YES; 1262 } 1263 }); 1264 1265 1266 /** @private */ 1267 SC._menu_fetchKeys = function (k) { 1268 return this.get(k); 1269 }; 1270 1271 /** @private */ 1272 SC._menu_fetchItem = function (k) { 1273 if (!k) return null; 1274 return this.get ? this.get(k) : this[k]; 1275 }; 1276 1277 1278 /** 1279 Default metrics for the different control sizes. 1280 */ 1281 SC.MenuPane.TINY_MENU_ITEM_HEIGHT = 10; 1282 // SC.MenuPane.TINY_MENU_ITEM_SEPARATOR_HEIGHT = 2; 1283 // SC.MenuPane.TINY_MENU_HEIGHT_PADDING = 2; 1284 // SC.MenuPane.TINY_SUBMENU_OFFSET_X = 0; 1285 1286 SC.MenuPane.SMALL_MENU_ITEM_HEIGHT = 16; 1287 // SC.MenuPane.SMALL_MENU_ITEM_SEPARATOR_HEIGHT = 7; 1288 // SC.MenuPane.SMALL_MENU_HEIGHT_PADDING = 4; 1289 // SC.MenuPane.SMALL_SUBMENU_OFFSET_X = 2; 1290 1291 SC.MenuPane.REGULAR_MENU_ITEM_HEIGHT = 22; 1292 // SC.MenuPane.REGULAR_MENU_ITEM_SEPARATOR_HEIGHT = 9; 1293 // SC.MenuPane.REGULAR_MENU_HEIGHT_PADDING = 6; 1294 // SC.MenuPane.REGULAR_SUBMENU_OFFSET_X = 2; 1295 1296 SC.MenuPane.LARGE_MENU_ITEM_HEIGHT = 31; 1297 // SC.MenuPane.LARGE_MENU_ITEM_SEPARATOR_HEIGHT = 20; 1298 // SC.MenuPane.LARGE_MENU_HEIGHT_PADDING = 0; 1299 // SC.MenuPane.LARGE_SUBMENU_OFFSET_X = 4; 1300 1301 SC.MenuPane.HUGE_MENU_ITEM_HEIGHT = 20; 1302 // SC.MenuPane.HUGE_MENU_ITEM_SEPARATOR_HEIGHT = 9; 1303 // SC.MenuPane.HUGE_MENU_HEIGHT_PADDING = 0; 1304 // SC.MenuPane.HUGE_SUBMENU_OFFSET_X = 0; 1305