1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('views/button'); 9 10 /** @class 11 12 SC.PopupButtonView displays a pop-up menu when clicked, from which the user 13 can select an item. 14 15 To use, create the SC.PopupButtonView as you would a standard SC.ButtonView, 16 then set the menu property to an instance of SC.MenuPane. For example: 17 18 SC.PopupButtonView.design({ 19 layout: { width: 200, height: 18 }, 20 menuBinding: 'MyApp.menuController.menuPane' 21 }); 22 23 You would then have your MyApp.menuController return an instance of the menu 24 to display. 25 26 @extends SC.ButtonView 27 @author Santosh Shanbhogue 28 @author Tom Dale 29 @copyright 2008-2011, Strobe Inc. and contributors. 30 @version 1.0 31 */ 32 SC.PopupButtonView = SC.ButtonView.extend( 33 /** @scope SC.PopupButtonView.prototype */ { 34 35 /** 36 @type Array 37 @default ['sc-popup-button'] 38 @see SC.View#classNames 39 */ 40 classNames: ['sc-popup-button'], 41 42 /** 43 @type String 44 @default 'popupButtonRenderDelegate' 45 */ 46 renderDelegateName: 'popupButtonRenderDelegate', 47 48 49 // .......................................................... 50 // PROPERTIES 51 // 52 53 /** 54 The prefer matrix to use when displaying the menu. 55 56 @property 57 */ 58 preferMatrix: null, 59 60 /** 61 The SC.MenuPane that should be displayed when the button is clicked. 62 63 @type {SC.MenuPane} 64 @default null 65 */ 66 menu: null, 67 68 /** 69 If YES and the menu is a class, this will cause a task that will instantiate the menu 70 to be added to SC.backgroundTaskQueue. 71 72 @type Boolean 73 @default NO 74 */ 75 shouldLoadInBackground: NO, 76 77 // .......................................................... 78 // INTERNAL SUPPORT 79 // 80 81 /** @private 82 If necessary, adds the loading of the menu to the background task queue. 83 */ 84 init: function() { 85 sc_super(); 86 this._setupMenu(); 87 if (this.get('shouldLoadInBackground')) { 88 SC.backgroundTaskQueue.push(SC.PopupButtonMenuLoader.create({ popupButton: this })); 89 } 90 }, 91 92 /** @private 93 Sets up binding on the menu, removing any old ones if necessary. 94 */ 95 _setupMenu: function() { 96 var menu = this.get('instantiatedMenu'); 97 98 // clear existing bindings 99 if (this.isActiveBinding) this.isActiveBinding.disconnect(); 100 this.isActiveBinding = null; 101 102 // if there is a menu 103 if (menu && !menu.isClass) { 104 this.isActiveBinding = this.bind('isActive', menu, 'isVisibleInWindow'); 105 } 106 }, 107 108 /** @private 109 Setup the bindings for menu... 110 */ 111 _popup_menuDidChange: function() { 112 this._setupMenu(); 113 }.observes('menu'), 114 115 /** @private 116 isActive is NO, but when the menu is instantiated, it is bound to the menu's isVisibleInWindow property. 117 */ 118 isActive: NO, 119 120 /** @private 121 Instantiates the menu if it is not already instantiated. 122 */ 123 _instantiateMenu: function() { 124 // get menu 125 var menu = this.get('menu'); 126 127 // if it is already instantiated or does not exist, we cannot do anything 128 if (!menu || !menu.isClass) return; 129 130 // create 131 this.menu = menu.create(); 132 133 // setup 134 this._setupMenu(); 135 }, 136 137 /** @private 138 The guaranteed-instantiated menu. 139 */ 140 instantiatedMenu: function() { 141 // get the menu 142 var menu = this.get('menu'); 143 144 // if it is a class, we need to instantiate it 145 if (menu && menu.isClass) { 146 // do so 147 this._instantiateMenu(); 148 149 // get the new version of the local 150 menu = this.get('menu'); 151 } 152 153 // return 154 return menu; 155 }.property('menu').cacheable(), 156 157 /** @private 158 Displays the menu. 159 160 @param {SC.Event} evt 161 */ 162 action: function(evt) { 163 var menu = this.get('instantiatedMenu') ; 164 165 if (!menu) { 166 // @if (debug) 167 SC.Logger.warn("SC.PopupButton - Unable to show menu because the menu property is set to %@.".fmt(menu)); 168 // @endif 169 return NO ; 170 } 171 172 menu.popup(this, this.get('preferMatrix')) ; 173 return YES; 174 }, 175 176 /** @private 177 On mouse down, we set the state of the button, save some state for further 178 processing, then call the button's action method. 179 180 @param {SC.Event} evt 181 @returns {Boolean} 182 */ 183 mouseDown: function(evt) { 184 // Fast path, reject secondary clicks. 185 if (evt.which && evt.which !== 1) return false; 186 187 // If disabled, handle mouse down but ignore it. 188 if (!this.get('isEnabledInPane')) return YES ; 189 190 this._isMouseDown = YES; 191 192 this._action() ; 193 194 // Store the current timestamp. We register the timestamp after a setTimeout 195 // so that the menu has been rendered, in case that operation 196 // takes more than a few hundred milliseconds. 197 198 // One mouseUp, we'll use this value to determine how long the mouse was 199 // pressed. 200 201 // we need to keep track that we opened it just now in case we get the 202 // mouseUp before render finishes. If it is 0, then we know we have not 203 // waited long enough. 204 this._menuRenderedTimestamp = 0; 205 206 var self = this; 207 208 // setTimeout guarantees that all rendering is done. The browser will even 209 // have rendered by this point. 210 setTimeout(function() { 211 SC.run(function(){ 212 // a run loop might be overkill here but what if Date.now fails? 213 self._menuRenderedTimestamp = Date.now(); 214 }); 215 }, 1); 216 217 this.becomeFirstResponder(); 218 219 return YES ; 220 }, 221 222 /** @private 223 Because we responded YES to the mouseDown event, we have responsibility 224 for handling the corresponding mouseUp event. 225 226 However, the user may click on this button, then drag the mouse down to a 227 menu item, and release the mouse over the menu item. We therefore need to 228 delegate any mouseUp events to the menu's menu item, if one is selected. 229 230 We also need to differentiate between a single click and a click and hold. 231 If the user clicks and holds, we want to close the menu when they release. 232 Otherwise, we should wait until they click on the menu's modal pane before 233 removing our active state. 234 235 @param {SC.Event} evt 236 @returns {Boolean} 237 */ 238 mouseUp: function(evt) { 239 var timestamp = new Date().getTime(), 240 previousTimestamp = this._menuRenderedTimestamp, 241 menu = this.get('instantiatedMenu'), 242 touch = SC.platform.touch, 243 targetMenuItem; 244 245 // normalize the previousTimestamp: if it is 0, it might as well be now. 246 // 0 means that we have not even triggered the nearly-immediate saving of timestamp. 247 if (previousTimestamp === 0) previousTimestamp = Date.now(); 248 249 if (menu) { 250 // Get the menu item the user is currently hovering their mouse over 251 targetMenuItem = menu.getPath('rootMenu.targetMenuItem'); 252 253 if (targetMenuItem) { 254 // Have the menu item perform its action. 255 // If the menu returns NO, it had no action to 256 // perform, so we should close the menu immediately. 257 if (!targetMenuItem.performAction()) menu.remove(); 258 } else { 259 // If the user waits more than certain amount of time between 260 // mouseDown and mouseUp, we can assume that they are clicking and 261 // dragging to the menu item, and we should close the menu if they 262 //mouseup anywhere not inside the menu. 263 if (!touch && (timestamp - previousTimestamp > SC.ButtonView.CLICK_AND_HOLD_DELAY)) { 264 menu.remove(); 265 } 266 } 267 } 268 269 // Reset state. 270 this._isMouseDown = NO; 271 sc_super(); 272 return YES; 273 }, 274 275 /** @private 276 Overrides ButtonView's mouseExited method to remove the behavior where the 277 active state is removed on mouse exit. We want the button to remain active 278 as long as the menu is visible. 279 280 @param {SC.Event} evt 281 @returns {Boolean} 282 */ 283 mouseExited: function(evt) { 284 return YES; 285 }, 286 287 /** @private 288 Overrides performKeyEquivalent method to pass any keyboard shortcuts to 289 the menu. 290 291 @param {String} charCode string corresponding to shortcut pressed (e.g., 292 alt_shift_z) 293 @param {SC.Event} evt 294 */ 295 performKeyEquivalent: function(charCode, evt) { 296 if (!this.get('isEnabledInPane')) return NO ; 297 var menu = this.get('instantiatedMenu') ; 298 299 return (!!menu && menu.performKeyEquivalent(charCode, evt, YES)) ; 300 }, 301 302 /** @private */ 303 touchStart: function(evt) { 304 return this.mouseDown(evt); 305 }, 306 307 /** @private */ 308 touchEnd: function(evt) { 309 return this.mouseUp(evt); 310 } 311 312 }); 313 314 /** 315 @private 316 Handles lazy instantiation of popup button menu. 317 */ 318 SC.PopupButtonMenuLoader = SC.Task.extend({ 319 popupButton: null, 320 run: function() { 321 if (this.popupButton) this.popupButton._instantiateMenu(); 322 } 323 }); 324