@qooxdoo/framework
Version:
The JS Framework for Coders
926 lines (768 loc) • 24.1 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
************************************************************************ */
/**
* This singleton manages visible menu instances and supports some
* core features to schedule menu open/close with timeout support.
*
* It also manages the whole keyboard support for the currently
* registered widgets.
*
* The zIndex order is also managed by this class.
*/
qx.Class.define("qx.ui.menu.Manager",
{
type : "singleton",
extend : qx.core.Object,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
construct : function()
{
this.base(arguments);
// Create data structure
this.__objects = [];
var el = document.body;
var Registration = qx.event.Registration;
// React on pointer/mouse events, but on native, to support inline applications
Registration.addListener(window.document.documentElement, "pointerdown", this._onPointerDown, this, true);
Registration.addListener(el, "roll", this._onRoll, this, true);
// React on keypress events
Registration.addListener(el, "keydown", this._onKeyUpDown, this, true);
Registration.addListener(el, "keyup", this._onKeyUpDown, this, true);
Registration.addListener(el, "keypress", this._onKeyPress, this, true);
// only use the blur event to hide windows on non touch devices [BUG #4033]
// When the menu is located on top of an iFrame, the select will fail
if (!qx.core.Environment.get("event.touch")) {
// Hide all when the window is blurred
qx.bom.Element.addListener(window, "blur", this.hideAll, this);
}
// Create open timer
this.__openTimer = new qx.event.Timer();
this.__openTimer.addListener("interval", this._onOpenInterval, this);
// Create close timer
this.__closeTimer = new qx.event.Timer();
this.__closeTimer.addListener("interval", this._onCloseInterval, this);
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__scheduleOpen : null,
__scheduleClose : null,
__openTimer : null,
__closeTimer : null,
__objects : null,
/*
---------------------------------------------------------------------------
HELPER METHODS
---------------------------------------------------------------------------
*/
/**
* Query engine for menu children.
*
* @param menu {qx.ui.menu.Menu} Any menu instance
* @param start {Integer} Child index to start with
* @param iter {Integer} Iteration count, normally <code>+1</code> or <code>-1</code>
* @param loop {Boolean?false} Whether to wrap when reaching the begin/end of the list
* @return {qx.ui.menu.Button} Any menu button or <code>null</code>
*/
_getChild : function(menu, start, iter, loop)
{
var children = menu.getChildren();
var length = children.length;
var child;
for (var i=start; i<length && i>=0; i+=iter)
{
child = children[i];
if (child.isEnabled() && !child.isAnonymous() && child.isVisible()) {
return child;
}
}
if (loop)
{
i = i == length ? 0 : length-1;
for (; i!=start; i+=iter)
{
child = children[i];
if (child.isEnabled() && !child.isAnonymous() && child.isVisible()) {
return child;
}
}
}
return null;
},
/**
* Whether the given widget is inside any Menu instance.
*
* @param widget {qx.ui.core.Widget} Any widget
* @return {Boolean} <code>true</code> when the widget is part of any menu
*/
_isInMenu : function(widget)
{
while(widget)
{
if (widget instanceof qx.ui.menu.Menu) {
return true;
}
widget = widget.getLayoutParent();
}
return false;
},
/**
* Whether the given widget is one of the menu openers.
*
* @param widget {qx.ui.core.Widget} Any widget
* @return {Boolean} <code>true</code> if the widget is a menu opener
*/
_isMenuOpener : function(widget)
{
var menus = this.__objects;
for (var i = 0; i < menus.length; i++) {
if (menus[i].getOpener() === widget) {
return true;
}
}
return false;
},
/**
* Returns an instance of a menu button if the given widget is a child
*
* @param widget {qx.ui.core.Widget} any widget
* @return {qx.ui.menu.Button} Any menu button instance or <code>null</code>
*/
_getMenuButton : function(widget)
{
while(widget)
{
if (widget instanceof qx.ui.menu.AbstractButton) {
return widget;
}
widget = widget.getLayoutParent();
}
return null;
},
/*
---------------------------------------------------------------------------
PUBLIC METHODS
---------------------------------------------------------------------------
*/
/**
* Adds a menu to the list of visible menus.
*
* @param obj {qx.ui.menu.Menu} Any menu instance.
*/
add : function(obj)
{
if (qx.core.Environment.get("qx.debug"))
{
if (!(obj instanceof qx.ui.menu.Menu)) {
throw new Error("Object is no menu: " + obj);
}
}
var reg = this.__objects;
reg.push(obj);
obj.setZIndex(1e6+reg.length);
},
/**
* Remove a menu from the list of visible menus.
*
* @param obj {qx.ui.menu.Menu} Any menu instance.
*/
remove : function(obj)
{
if (qx.core.Environment.get("qx.debug"))
{
if (!(obj instanceof qx.ui.menu.Menu)) {
throw new Error("Object is no menu: " + obj);
}
}
var reg = this.__objects;
if (reg) {
qx.lang.Array.remove(reg, obj);
}
},
/**
* Hides all currently opened menus.
*/
hideAll : function()
{
var reg = this.__objects;
if (reg)
{
for (var i=reg.length-1; i>=0; i--) {
reg[i].exclude();
}
}
},
/**
* Returns the menu which was opened at last (which
* is the active one this way)
*
* @return {qx.ui.menu.Menu} The current active menu or <code>null</code>
*/
getActiveMenu : function()
{
var reg = this.__objects;
return reg.length > 0 ? reg[reg.length-1] : null;
},
/*
---------------------------------------------------------------------------
SCHEDULED OPEN/CLOSE SUPPORT
---------------------------------------------------------------------------
*/
/**
* Schedules the given menu to be opened after the
* {@link qx.ui.menu.Menu#openInterval} configured by the
* menu instance itself.
*
* @param menu {qx.ui.menu.Menu} The menu to schedule for open
*/
scheduleOpen : function(menu)
{
// Cancel close of given menu first
this.cancelClose(menu);
// When the menu is already visible
if (menu.isVisible())
{
// Cancel all other open requests
if (this.__scheduleOpen) {
this.cancelOpen(this.__scheduleOpen);
}
}
// When the menu is not visible and not scheduled already
// then schedule it for opening
else if (this.__scheduleOpen != menu)
{
// menu.debug("Schedule open");
this.__scheduleOpen = menu;
this.__openTimer.restartWith(menu.getOpenInterval());
}
},
/**
* Schedules the given menu to be closed after the
* {@link qx.ui.menu.Menu#closeInterval} configured by the
* menu instance itself.
*
* @param menu {qx.ui.menu.Menu} The menu to schedule for close
*/
scheduleClose : function(menu)
{
// Cancel open of the menu first
this.cancelOpen(menu);
// When the menu is already invisible
if (!menu.isVisible())
{
// Cancel all other close requests
if (this.__scheduleClose) {
this.cancelClose(this.__scheduleClose);
}
}
// When the menu is visible and not scheduled already
// then schedule it for closing
else if (this.__scheduleClose != menu)
{
// menu.debug("Schedule close");
this.__scheduleClose = menu;
this.__closeTimer.restartWith(menu.getCloseInterval());
}
},
/**
* When the given menu is scheduled for open this pending
* request is canceled.
*
* @param menu {qx.ui.menu.Menu} The menu to cancel for open
*/
cancelOpen : function(menu)
{
if (this.__scheduleOpen == menu)
{
// menu.debug("Cancel open");
this.__openTimer.stop();
this.__scheduleOpen = null;
}
},
/**
* When the given menu is scheduled for close this pending
* request is canceled.
*
* @param menu {qx.ui.menu.Menu} The menu to cancel for close
*/
cancelClose : function(menu)
{
if (this.__scheduleClose == menu)
{
// menu.debug("Cancel close");
this.__closeTimer.stop();
this.__scheduleClose = null;
}
},
/*
---------------------------------------------------------------------------
TIMER EVENT HANDLERS
---------------------------------------------------------------------------
*/
/**
* Event listener for a pending open request. Configured to the interval
* of the current menu to open.
*
* @param e {qx.event.type.Event} Interval event
*/
_onOpenInterval : function(e)
{
// Stop timer
this.__openTimer.stop();
// Open menu and reset flag
this.__scheduleOpen.open();
this.__scheduleOpen = null;
},
/**
* Event listener for a pending close request. Configured to the interval
* of the current menu to close.
*
* @param e {qx.event.type.Event} Interval event
*/
_onCloseInterval : function(e)
{
// Stop timer, reset scheduling flag
this.__closeTimer.stop();
// Close menu and reset flag
this.__scheduleClose.exclude();
this.__scheduleClose = null;
},
/*
---------------------------------------------------------------------------
CONTEXTMENU EVENT HANDLING
---------------------------------------------------------------------------
*/
/**
* Internal function registers a handler to stop next
* <code>contextmenu</code> event.
* This function will be called by {@link qx.ui.menu.Button#_onTap}, if
* right click was pressed.
*
* @internal
*/
preventContextMenuOnce : function()
{
qx.event.Registration.addListener(document.body, "contextmenu", this.__onPreventContextMenu, this, true);
},
/**
* Internal event handler to stop <code>contextmenu</code> event bubbling,
* if target is inside the opened menu.
*
* @param e {qx.event.type.Mouse} contextmenu event
*
* @internal
*/
__onPreventContextMenu : function(e)
{
var target = e.getTarget();
target = qx.ui.core.Widget.getWidgetByElement(target, true);
if (this._isInMenu(target)) {
e.stopPropagation();
e.preventDefault();
}
// stop only once
qx.event.Registration.removeListener(document.body, "contextmenu", this.__onPreventContextMenu, this, true);
},
/*
---------------------------------------------------------------------------
POINTER EVENT HANDLERS
---------------------------------------------------------------------------
*/
/**
* Event handler for pointerdown events
*
* @param e {qx.event.type.Pointer} pointerdown event
*/
_onPointerDown : function(e)
{
var target = e.getTarget();
target = qx.ui.core.Widget.getWidgetByElement(target, true);
// If the target is 'null' the tap appears on a DOM element witch is not
// a widget. This happens normally with an inline application, when the user
// taps not in the inline application. In this case all all currently
// open menus should be closed.
if (target == null) {
this.hideAll();
return;
}
// If the target is the one which has opened the current menu
// we ignore the pointerdown to let the button process the event
// further with toggling or ignoring the tap.
if (target.getMenu && target.getMenu() && target.getMenu().isVisible()) {
return;
}
// All taps not inside a menu will hide all currently open menus
if (this.__objects.length > 0 && !this._isInMenu(target)) {
this.hideAll();
}
},
/*
---------------------------------------------------------------------------
KEY EVENT HANDLING
---------------------------------------------------------------------------
*/
/**
* @type {Map} Map of all keys working on an active menu selection
* @lint ignoreReferenceField(__selectionKeys)
*/
__selectionKeys :
{
"Enter" : 1,
"Space" : 1
},
/**
* @type {Map} Map of all keys working without a selection
* @lint ignoreReferenceField(__navigationKeys)
*/
__navigationKeys :
{
"Escape" : 1,
"Up" : 1,
"Down" : 1,
"Left" : 1,
"Right" : 1
},
/**
* Event handler for all keyup/keydown events. Stops all events
* when any menu is opened.
*
* @param e {qx.event.type.KeySequence} Keyboard event
*/
_onKeyUpDown : function(e)
{
var menu = this.getActiveMenu();
if (!menu) {
return;
}
// Stop for all supported key combos
var iden = e.getKeyIdentifier();
if (this.__navigationKeys[iden] || (this.__selectionKeys[iden] && menu.getSelectedButton())) {
e.stopPropagation();
}
},
/**
* Event handler for all keypress events. Delegates the event to the more
* specific methods defined in this class.
*
* Currently processes the keys: <code>Up</code>, <code>Down</code>,
* <code>Left</code>, <code>Right</code> and <code>Enter</code>.
*
* @param e {qx.event.type.KeySequence} Keyboard event
*/
_onKeyPress : function(e)
{
var menu = this.getActiveMenu();
if (!menu) {
return;
}
var iden = e.getKeyIdentifier();
var navigation = this.__navigationKeys[iden];
var selection = this.__selectionKeys[iden];
if (navigation)
{
switch(iden)
{
case "Up":
this._onKeyPressUp(menu);
break;
case "Down":
this._onKeyPressDown(menu);
break;
case "Left":
this._onKeyPressLeft(menu);
break;
case "Right":
this._onKeyPressRight(menu);
break;
case "Escape":
this.hideAll();
break;
}
e.stopPropagation();
e.preventDefault();
}
else if (selection)
{
// Do not process these events when no item is hovered
var button = menu.getSelectedButton();
if (button)
{
switch(iden)
{
case "Enter":
this._onKeyPressEnter(menu, button, e);
break;
case "Space":
this._onKeyPressSpace(menu, button, e);
break;
}
e.stopPropagation();
e.preventDefault();
}
}
},
/**
* Event handler for <code>Up</code> key
*
* @param menu {qx.ui.menu.Menu} The active menu
*/
_onKeyPressUp : function(menu)
{
// Query for previous child
var selectedButton = menu.getSelectedButton();
var children = menu.getChildren();
var start = selectedButton ? menu.indexOf(selectedButton)-1 : children.length-1;
var nextItem = this._getChild(menu, start, -1, true);
// Reconfigure property
if (nextItem) {
menu.setSelectedButton(nextItem);
} else {
menu.resetSelectedButton();
}
},
/**
* Event handler for <code>Down</code> key
*
* @param menu {qx.ui.menu.Menu} The active menu
*/
_onKeyPressDown : function(menu)
{
// Query for next child
var selectedButton = menu.getSelectedButton();
var start = selectedButton ? menu.indexOf(selectedButton)+1 : 0;
var nextItem = this._getChild(menu, start, 1, true);
// Reconfigure property
if (nextItem) {
menu.setSelectedButton(nextItem);
} else {
menu.resetSelectedButton();
}
},
/**
* Event handler for <code>Left</code> key
*
* @param menu {qx.ui.menu.Menu} The active menu
*/
_onKeyPressLeft : function(menu)
{
var menuOpener = menu.getOpener();
if (!menuOpener) {
return;
}
// Back to the "parent" menu
if (menuOpener instanceof qx.ui.menu.AbstractButton)
{
var parentMenu = menuOpener.getLayoutParent();
parentMenu.resetOpenedButton();
parentMenu.setSelectedButton(menuOpener);
}
// Goto the previous toolbar button
else if (menuOpener instanceof qx.ui.menubar.Button)
{
var buttons = menuOpener.getMenuBar().getMenuButtons();
var index = buttons.indexOf(menuOpener);
// This should not happen, definitely!
if (index === -1) {
return;
}
// Get previous button, fallback to end if first arrived
var prevButton = null;
var length = buttons.length;
for (var i = 1; i <= length; i++)
{
var button = buttons[(index - i + length) % length];
if(button.isEnabled() && button.isVisible()) {
prevButton = button;
break;
}
}
if (prevButton && prevButton != menuOpener) {
prevButton.open(true);
}
}
},
/**
* Event handler for <code>Right</code> key
*
* @param menu {qx.ui.menu.Menu} The active menu
*/
_onKeyPressRight : function(menu)
{
var selectedButton = menu.getSelectedButton();
// Open sub-menu of hovered item and select first child
if (selectedButton)
{
var subMenu = selectedButton.getMenu();
if (subMenu)
{
// Open previously hovered item
menu.setOpenedButton(selectedButton);
// Hover first item in new submenu
var first = this._getChild(subMenu, 0, 1);
if (first) {
subMenu.setSelectedButton(first);
}
return;
}
}
// No hover and no open item
// When first button has a menu, open it, otherwise only hover it
else if (!menu.getOpenedButton())
{
var first = this._getChild(menu, 0, 1);
if (first)
{
menu.setSelectedButton(first);
if (first.getMenu()) {
menu.setOpenedButton(first);
}
return;
}
}
// Jump to the next toolbar button
var menuOpener = menu.getOpener();
// Look up opener hierarchy for menu button
if (menuOpener instanceof qx.ui.menu.Button && selectedButton)
{
// From one inner selected button try to find the top level
// menu button which has opened the whole menu chain.
while (menuOpener)
{
menuOpener = menuOpener.getLayoutParent();
if (menuOpener instanceof qx.ui.menu.Menu)
{
menuOpener = menuOpener.getOpener();
if (menuOpener instanceof qx.ui.menubar.Button) {
break;
}
}
else
{
break;
}
}
if (!menuOpener) {
return;
}
}
// Ask the toolbar for the next menu button
if (menuOpener instanceof qx.ui.menubar.Button)
{
var buttons = menuOpener.getMenuBar().getMenuButtons();
var index = buttons.indexOf(menuOpener);
// This should not happen, definitely!
if (index === -1) {
return;
}
// Get next button, fallback to first if end arrived
var nextButton = null;
var length = buttons.length;
for (var i = 1; i <= length; i++)
{
var button = buttons[(index + i) % length];
if(button.isEnabled() && button.isVisible()) {
nextButton = button;
break;
}
}
if (nextButton && nextButton != menuOpener) {
nextButton.open(true);
}
}
},
/**
* Event handler for <code>Enter</code> key
*
* @param menu {qx.ui.menu.Menu} The active menu
* @param button {qx.ui.menu.AbstractButton} The selected button
* @param e {qx.event.type.KeySequence} The keypress event
*/
_onKeyPressEnter : function(menu, button, e)
{
// Route keypress event to the selected button
if (button.hasListener("keypress"))
{
// Clone and reconfigure event
var clone = e.clone();
clone.setBubbles(false);
clone.setTarget(button);
// Finally dispatch the clone
button.dispatchEvent(clone);
}
// Hide all open menus
this.hideAll();
},
/**
* Event handler for <code>Space</code> key
*
* @param menu {qx.ui.menu.Menu} The active menu
* @param button {qx.ui.menu.AbstractButton} The selected button
* @param e {qx.event.type.KeySequence} The keypress event
*/
_onKeyPressSpace : function(menu, button, e)
{
// Route keypress event to the selected button
if (button.hasListener("keypress"))
{
// Clone and reconfigure event
var clone = e.clone();
clone.setBubbles(false);
clone.setTarget(button);
// Finally dispatch the clone
button.dispatchEvent(clone);
}
},
/**
* Event handler for roll which hides all windows on scroll.
*
* @param e {qx.event.type.Roll} The roll event.
*/
_onRoll : function(e) {
var target = e.getTarget();
target = qx.ui.core.Widget.getWidgetByElement(target, true);
if (
this.__objects.length > 0
&& !this._isInMenu(target)
&& !this._isMenuOpener(target)
&& !e.getMomentum()
) {
this.hideAll();
}
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function()
{
var Registration = qx.event.Registration;
var el = document.body;
// React on pointerdown events
Registration.removeListener(window.document.documentElement, "pointerdown", this._onPointerDown, this, true);
// React on keypress events
Registration.removeListener(el, "keydown", this._onKeyUpDown, this, true);
Registration.removeListener(el, "keyup", this._onKeyUpDown, this, true);
Registration.removeListener(el, "keypress", this._onKeyPress, this, true);
this._disposeObjects("__openTimer", "__closeTimer");
this._disposeArray("__objects");
}
});