// Copyright: ©2006-2010 Sprout Systems, Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== /** * @class * @extends SC.ButtonView * @version 1.6 * @author Alex Iskander */ SC.PopupButtonView = SC.ButtonView.extend({ /** @scope SC.PopupButtonView.prototype */ /** The render delegate to use to render and update the HTML for the PopupButton. @type String @default 'popupButtonRenderDelegate' */ renderDelegateName: 'popupButtonRenderDelegate', /** The menu that will pop up when this button is clicked. This can be a class or an instance. @type {SC.MenuPane} @default SC.MenuPane */ menu: SC.MenuPane, /** If YES, a menu instantiation task will be placed in SproutCore's `SC.backgroundTaskQueue` so the menu will be instantiated before the user taps the button, improving response time. @type Boolean @default NO @property */ shouldLoadInBackground: NO, /** * @private * If YES, the menu has been instantiated; if NO, the 'menu' property * still has a class instead of an instance. */ _menuIsLoaded: NO, /** @private isActive is NO, but when the menu is instantiated, it is bound to the menu's isVisibleInWindow property. */ isActive: NO, acceptsFirstResponder: YES, /** @private */ init: function() { sc_super(); // keep track of the current instantiated menu separately from // our property. This allows us to destroy it when the property // changes, and to track if the property change was initiated by // us (since we set `menu` to the instantiated menu). this._currentMenu = null; this.invokeOnce('scheduleMenuSetupIfNeeded'); }, /** Adds menu instantiation to the background task queue if the menu is not already instantiated and if shouldLoadInBackground is YES. @method @private */ scheduleMenuSetupIfNeeded: function() { var menu = this.get('menu'); if (menu && menu.isClass && this.get('shouldLoadInBackground')) { SC.backgroundTaskQueue.push(SC.PopupButtonView.InstantiateMenuTask.create({ popupButton: this })); } }, /** @private if the menu changes, it must be set up again. */ menuDidChange: function() { // first, check if we are the ones who changed the property // by setting it to the instantiated menu var menu = this.get('menu'); if (menu === this._currentMenu) { return; } this.invokeOnce('scheduleMenuSetupIfNeeded'); }.observes('menu'), /** Instantiates the menu if it exists and is not already instantiated. If another menu is already instantiated, it will be destroyed. */ setupMenu: function() { var menu = this.get('menu'); // handle our existing menu, if any if (menu === this._currentMenu) { return; } if (this._currentMenu) { this.isActiveBinding.disconnect(); this._currentMenu.destroy(); this._currentMenu = null; } // do not do anything if there is nothing to do. if (menu && menu.isClass) { menu = this.createMenu(menu); } this._currentMenu = menu; this.set('menu', menu); this.isActiveBinding = this.bind('isActive', menu, 'isVisibleInWindow'); }, /** Called to instantiate a menu. You can override this to set properties such as the menu's width or the currently selected item. @param {SC.MenuPane} menu The MenuPane class to instantiate. */ createMenu: function(menu) { return menu.create(); }, /** Shows the PopupButton's menu. You can call this to show it manually. NOTE: The menu will not be shown until the end of the Run Loop. */ showMenu: function() { // problem: menu's bindings may not flush this.setupMenu(); // solution: pop up the menu later. Ugly-ish, but not too bad: this.invokeLast('_showMenu'); }, /** Hides the PopupButton's menu if it is currently showing. */ hideMenu: function() { var menu = this.get('menu'); if (menu && !menu.isClass) { menu.remove(); } }, /** The prefer matrix (positioning information) to use to pop up the new menu. @property @type Array @default [0, 0, 0] */ menuPreferMatrix: [0, 0, 0], /** @private The actual showing of the menu is delayed because bindings may need to flush. */ _showMenu: function() { var menu = this.get('menu'); menu.popup(this, this.get('menuPreferMatrix')); }, /** @private */ mouseDown: function(evt) { // If disabled, handle mouse down but ignore it. if (!this.get('isEnabled')) return YES ; this.set('_mouseDown', YES); this.showMenu(); this._mouseDownTimestamp = null; // Some nutty stuff going on here. If the number of menu items is large, and // it takes over 400 ms to create, then invokeLater will not return control // to the browser, thereby causing the menu pane to dismiss itself // instantly. Using setTimeout will guarantee that control goes back to the // browser. var self = this; // there is a bit of a race condition: we could get mouse up immediately. // In that case, we will take note that the timestamp is 0 and treat it // as if it were Date.now() at the time of checking. self._mouseDownTimestamp = 0; setTimeout(function() { self._mouseDownTimestamp = Date.now(); }, 1); this.becomeFirstResponder(); return YES; }, /** @private */ mouseUp: function(evt) { var menu = this.get('menu'), targetMenuItem, success; if (menu && this.get('_mouseDown')) { targetMenuItem = menu.getPath('rootMenu.targetMenuItem'); // normalize the mouseDownTimestamp: it may not have been set yet. if (this._mouseDownTimestamp === 0) { this._mouseDownTimestamp = Date.now(); } // If the user waits more than 400ms between mouseDown and mouseUp, // we can assume that they are clicking and dragging to the menu item, // and we should close the menu if they mouseup anywhere not inside // the menu. if(evt.timeStamp - this._mouseDownTimestamp > 400) { if (targetMenuItem && menu.get('mouseHasEntered') && this._mouseDownTimestamp) { // Have the menu item perform its action. // If the menu returns NO, it had no action to // perform, so we should close the menu immediately. if (!targetMenuItem.performAction()) { menu.remove(); } } else { menu.remove(); } } } this._mouseDownTimestamp = undefined; return YES; }, /** @private Shows the menu when the user presses Enter. Otherwise, hands it off to button to decide what to do. */ keyDown: function(event) { if (event.which == 13) { this.showMenu(); return YES; } return sc_super(); } }); /** @class An SC.Task that handles instantiating a PopupButtonView's menu. It is used by SC.PopupButtonView to instantiate the menu in the backgroundTaskQueue. */ SC.PopupButtonView.InstantiateMenuTask = SC.Task.extend( /**@scope SC.PopupButtonView.InstantiateMenuTask.prototype */ { /** The popupButton whose menu should be instantiated. @property @type {SC.PopupButtonView} @default null */ popupButton: null, /** Instantiates the menu. */ run: function(queue) { this.popupButton.setupMenu(); } });