1 // Copyright: ©2006-2010 Sprout Systems, Inc. and contributors. 2 // Portions ©2008-2011 Apple Inc. All rights reserved. 3 // License: Licensed under MIT license (see license.js) 4 // ========================================================================== 5 6 /** 7 * @class 8 * @extends SC.ButtonView 9 * @version 1.6 10 * @author Alex Iskander 11 */ 12 SC.PopupButtonView = SC.ButtonView.extend({ 13 /** @scope SC.PopupButtonView.prototype */ 14 15 16 /** 17 The render delegate to use to render and update the HTML for the PopupButton. 18 19 @type String 20 @default 'popupButtonRenderDelegate' 21 */ 22 renderDelegateName: 'popupButtonRenderDelegate', 23 24 /** 25 The menu that will pop up when this button is clicked. This can be a class or 26 an instance. 27 28 @type {SC.MenuPane} 29 @default SC.MenuPane 30 */ 31 menu: SC.MenuPane, 32 33 /** 34 If YES, a menu instantiation task will be placed in SproutCore's 35 `SC.backgroundTaskQueue` so the menu will be instantiated before 36 the user taps the button, improving response time. 37 38 @type Boolean 39 @default NO 40 @property 41 */ 42 shouldLoadInBackground: NO, 43 44 /** 45 * @private 46 * If YES, the menu has been instantiated; if NO, the 'menu' property 47 * still has a class instead of an instance. 48 */ 49 _menuIsLoaded: NO, 50 51 /** @private 52 isActive is NO, but when the menu is instantiated, it is bound to the menu's isVisibleInWindow property. 53 */ 54 isActive: NO, 55 56 acceptsFirstResponder: YES, 57 58 59 /** 60 @private 61 */ 62 init: function() { 63 sc_super(); 64 65 // keep track of the current instantiated menu separately from 66 // our property. This allows us to destroy it when the property 67 // changes, and to track if the property change was initiated by 68 // us (since we set `menu` to the instantiated menu). 69 this._currentMenu = null; 70 this.invokeOnce('scheduleMenuSetupIfNeeded'); 71 }, 72 73 /** 74 Adds menu instantiation to the background task queue if the menu 75 is not already instantiated and if shouldLoadInBackground is YES. 76 77 @method 78 @private 79 */ 80 scheduleMenuSetupIfNeeded: function() { 81 var menu = this.get('menu'); 82 83 if (menu && menu.isClass && this.get('shouldLoadInBackground')) { 84 SC.backgroundTaskQueue.push(SC.PopupButtonView.InstantiateMenuTask.create({ popupButton: this })); 85 } 86 }, 87 88 /** @private if the menu changes, it must be set up again. */ 89 menuDidChange: function() { 90 // first, check if we are the ones who changed the property 91 // by setting it to the instantiated menu 92 var menu = this.get('menu'); 93 if (menu === this._currentMenu) { 94 return; 95 } 96 97 this.invokeOnce('scheduleMenuSetupIfNeeded'); 98 }.observes('menu'), 99 100 /** 101 Instantiates the menu if it exists and is not already instantiated. 102 If another menu is already instantiated, it will be destroyed. 103 */ 104 setupMenu: function() { 105 var menu = this.get('menu'); 106 107 // handle our existing menu, if any 108 if (menu === this._currentMenu) { return; } 109 if (this._currentMenu) { 110 this.isActiveBinding.disconnect(); 111 112 this._currentMenu.destroy(); 113 this._currentMenu = null; 114 } 115 116 // do not do anything if there is nothing to do. 117 if (menu && menu.isClass) { 118 menu = this.createMenu(menu); 119 } 120 121 this._currentMenu = menu; 122 this.set('menu', menu); 123 124 this.isActiveBinding = this.bind('isActive', menu, 'isVisibleInWindow'); 125 }, 126 127 /** 128 Called to instantiate a menu. You can override this to set properties 129 such as the menu's width or the currently selected item. 130 131 @param {SC.MenuPane} menu The MenuPane class to instantiate. 132 */ 133 createMenu: function(menu) { 134 return menu.create(); 135 }, 136 137 138 /** 139 Shows the PopupButton's menu. You can call this to show it manually. 140 141 NOTE: The menu will not be shown until the end of the Run Loop. 142 */ 143 showMenu: function() { 144 // problem: menu's bindings may not flush 145 this.setupMenu(); 146 147 // solution: pop up the menu later. Ugly-ish, but not too bad: 148 this.invokeLast('_showMenu'); 149 }, 150 151 /** 152 Hides the PopupButton's menu if it is currently showing. 153 */ 154 hideMenu: function() { 155 var menu = this.get('menu'); 156 if (menu && !menu.isClass) { 157 menu.remove(); 158 } 159 }, 160 161 /** 162 The prefer matrix (positioning information) to use to pop up the new menu. 163 164 @property 165 @type Array 166 @default [0, 0, 0] 167 */ 168 menuPreferMatrix: [0, 0, 0], 169 170 /** 171 @private 172 The actual showing of the menu is delayed because bindings may need 173 to flush. 174 */ 175 _showMenu: function() { 176 var menu = this.get('menu'); 177 178 menu.popup(this, this.get('menuPreferMatrix')); 179 }, 180 181 /** @private */ 182 mouseDown: function(evt) { 183 // If disabled, handle mouse down but ignore it. 184 if (!this.get('isEnabled')) return YES ; 185 186 this.set('_mouseDown', YES); 187 188 this.showMenu(); 189 190 this._mouseDownTimestamp = null; 191 192 // Some nutty stuff going on here. If the number of menu items is large, and 193 // it takes over 400 ms to create, then invokeLater will not return control 194 // to the browser, thereby causing the menu pane to dismiss itself 195 // instantly. Using setTimeout will guarantee that control goes back to the 196 // browser. 197 var self = this; 198 199 // there is a bit of a race condition: we could get mouse up immediately. 200 // In that case, we will take note that the timestamp is 0 and treat it 201 // as if it were Date.now() at the time of checking. 202 self._mouseDownTimestamp = 0; 203 204 setTimeout(function() { 205 self._mouseDownTimestamp = Date.now(); 206 }, 1); 207 208 this.becomeFirstResponder(); 209 210 return YES; 211 }, 212 213 /** @private */ 214 mouseUp: function(evt) { 215 var menu = this.get('menu'), targetMenuItem, success; 216 217 if (menu && this.get('_mouseDown')) { 218 targetMenuItem = menu.getPath('rootMenu.targetMenuItem'); 219 220 // normalize the mouseDownTimestamp: it may not have been set yet. 221 if (this._mouseDownTimestamp === 0) { 222 this._mouseDownTimestamp = Date.now(); 223 } 224 225 // If the user waits more than 400ms between mouseDown and mouseUp, 226 // we can assume that they are clicking and dragging to the menu item, 227 // and we should close the menu if they mouseup anywhere not inside 228 // the menu. 229 if(evt.timeStamp - this._mouseDownTimestamp > 400) { 230 if (targetMenuItem && menu.get('mouseHasEntered') && this._mouseDownTimestamp) { 231 // Have the menu item perform its action. 232 // If the menu returns NO, it had no action to 233 // perform, so we should close the menu immediately. 234 if (!targetMenuItem.performAction()) { 235 menu.remove(); 236 } 237 } 238 239 else { 240 menu.remove(); 241 } 242 } 243 } 244 245 this._mouseDownTimestamp = undefined; 246 return YES; 247 }, 248 249 /** 250 @private 251 252 Shows the menu when the user presses Enter. Otherwise, hands it off to button 253 to decide what to do. 254 */ 255 keyDown: function(event) { 256 if (event.which == 13) { 257 this.showMenu(); 258 return YES; 259 } 260 261 return sc_super(); 262 }, 263 264 /** @private */ 265 touchStart: function(evt) { 266 return this.mouseDown(evt); 267 }, 268 269 /** @private */ 270 touchEnd: function(evt) { 271 return this.mouseUp(evt); 272 } 273 }); 274 275 /** 276 @class 277 278 An SC.Task that handles instantiating a PopupButtonView's menu. It is used 279 by SC.PopupButtonView to instantiate the menu in the backgroundTaskQueue. 280 */ 281 SC.PopupButtonView.InstantiateMenuTask = SC.Task.extend( 282 /**@scope SC.PopupButtonView.InstantiateMenuTask.prototype */ { 283 284 /** 285 The popupButton whose menu should be instantiated. 286 287 @property 288 @type {SC.PopupButtonView} 289 @default null 290 */ 291 popupButton: null, 292 293 /** Instantiates the menu. */ 294 run: function(queue) { 295 this.popupButton.setupMenu(); 296 } 297 }); 298 299