@qooxdoo/framework
Version:
The JS Framework for Coders
814 lines (664 loc) • 19.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)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* The menu is a popup like control which supports buttons. It comes
* with full keyboard navigation and an improved timeout based pointer
* control behavior.
*
* This class is the container for all derived instances of
* {@link qx.ui.menu.AbstractButton}.
*
* @childControl slidebar {qx.ui.menu.MenuSlideBar} shows a slidebar to easily navigate inside the menu (if too little space is left)
*/
qx.Class.define("qx.ui.menu.Menu",
{
extend : qx.ui.core.Widget,
include : [
qx.ui.core.MPlacement,
qx.ui.core.MRemoteChildrenHandling
],
construct : function()
{
this.base(arguments);
// Use hard coded layout
this._setLayout(new qx.ui.menu.Layout);
// Automatically add to application's root
var root = this.getApplicationRoot();
root.add(this);
// Register pointer listeners
this.addListener("pointerover", this._onPointerOver);
this.addListener("pointerout", this._onPointerOut);
// add resize listener
this.addListener("resize", this._onResize, this);
root.addListener("resize", this._onResize, this);
this._blocker = new qx.ui.core.Blocker(root);
// Initialize properties
this.initVisibility();
this.initKeepFocus();
this.initKeepActive();
},
properties :
{
/*
---------------------------------------------------------------------------
WIDGET PROPERTIES
---------------------------------------------------------------------------
*/
// overridden
appearance :
{
refine : true,
init : "menu"
},
// overridden
allowGrowX :
{
refine : true,
init: false
},
// overridden
allowGrowY :
{
refine : true,
init: false
},
// overridden
visibility :
{
refine : true,
init : "excluded"
},
// overridden
keepFocus :
{
refine : true,
init : true
},
// overridden
keepActive :
{
refine : true,
init : true
},
/*
---------------------------------------------------------------------------
STYLE OPTIONS
---------------------------------------------------------------------------
*/
/** The spacing between each cell of the menu buttons */
spacingX :
{
check : "Integer",
apply : "_applySpacingX",
init : 0,
themeable : true
},
/** The spacing between each menu button */
spacingY :
{
check : "Integer",
apply : "_applySpacingY",
init : 0,
themeable : true
},
/**
* Default icon column width if no icons are rendered.
* This property is ignored as soon as an icon is present.
*/
iconColumnWidth :
{
check : "Integer",
init : 0,
themeable : true,
apply : "_applyIconColumnWidth"
},
/** Default arrow column width if no sub menus are rendered */
arrowColumnWidth :
{
check : "Integer",
init : 0,
themeable : true,
apply : "_applyArrowColumnWidth"
},
/**
* Color of the blocker
*/
blockerColor :
{
check : "Color",
init : null,
nullable: true,
apply : "_applyBlockerColor",
themeable: true
},
/**
* Opacity of the blocker
*/
blockerOpacity :
{
check : "Number",
init : 1,
apply : "_applyBlockerOpacity",
themeable: true
},
/*
---------------------------------------------------------------------------
FUNCTIONALITY PROPERTIES
---------------------------------------------------------------------------
*/
/** The currently selected button */
selectedButton :
{
check : "qx.ui.core.Widget",
nullable : true,
apply : "_applySelectedButton"
},
/** The currently opened button (sub menu is visible) */
openedButton :
{
check : "qx.ui.core.Widget",
nullable : true,
apply : "_applyOpenedButton"
},
/** Widget that opened the menu */
opener :
{
check : "qx.ui.core.Widget",
nullable : true
},
/*
---------------------------------------------------------------------------
BEHAVIOR PROPERTIES
---------------------------------------------------------------------------
*/
/** Interval in ms after which sub menus should be opened */
openInterval :
{
check : "Integer",
themeable : true,
init : 250,
apply : "_applyOpenInterval"
},
/** Interval in ms after which sub menus should be closed */
closeInterval :
{
check : "Integer",
themeable : true,
init : 250,
apply : "_applyCloseInterval"
},
/** Blocks the background if value is <code>true<code> */
blockBackground :
{
check : "Boolean",
themeable : true,
init : false
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__scheduledOpen : null,
__onAfterSlideBarAdd : null,
/** @type {qx.ui.core.Blocker} blocker for background blocking */
_blocker : null,
/*
---------------------------------------------------------------------------
PUBLIC API
---------------------------------------------------------------------------
*/
/**
* Opens the menu and configures the opener
*/
open : function()
{
if (this.getOpener() != null)
{
var isPlaced = this.placeToWidget(this.getOpener(), true);
if(isPlaced) {
this.__updateSlideBar();
this.show();
this._placementTarget = this.getOpener();
} else {
this.warn("Could not open menu instance because 'opener' widget is not visible");
}
} else {
this.warn("The menu instance needs a configured 'opener' widget!");
}
},
/**
* Opens the menu at the pointer position
*
* @param e {qx.event.type.Pointer} Pointer event to align to
*/
openAtPointer : function(e)
{
this.placeToPointer(e);
this.__updateSlideBar();
this.show();
this._placementTarget = {
left: e.getDocumentLeft(),
top: e.getDocumentTop()
};
},
/**
* Opens the menu in relation to the given point
*
* @param point {Map} Coordinate of any point with the keys <code>left</code>
* and <code>top</code>.
*/
openAtPoint : function(point)
{
this.placeToPoint(point);
this.__updateSlideBar();
this.show();
this._placementTarget = point;
},
/**
* Convenience method to add a separator to the menu
*/
addSeparator : function() {
this.add(new qx.ui.menu.Separator);
},
/**
* Returns the column sizes detected during the pre-layout phase
*
* @return {Array} List of all column widths
*/
getColumnSizes : function() {
return this._getMenuLayout().getColumnSizes();
},
/**
* Return all selectable menu items.
*
* @return {qx.ui.core.Widget[]} selectable widgets
*/
getSelectables : function() {
var result = [];
var children = this.getChildren();
for (var i = 0; i < children.length; i++)
{
if (children[i].isEnabled()) {
result.push(children[i]);
}
}
return result;
},
/*
---------------------------------------------------------------------------
PROPERTY APPLY ROUTINES
---------------------------------------------------------------------------
*/
// property apply
_applyIconColumnWidth : function(value, old) {
this._getMenuLayout().setIconColumnWidth(value);
},
// property apply
_applyArrowColumnWidth : function(value, old) {
this._getMenuLayout().setArrowColumnWidth(value);
},
// property apply
_applySpacingX : function(value, old) {
this._getMenuLayout().setColumnSpacing(value);
},
// property apply
_applySpacingY : function(value, old) {
this._getMenuLayout().setSpacing(value);
},
// overridden
_applyVisibility : function(value, old)
{
this.base(arguments, value, old);
var mgr = qx.ui.menu.Manager.getInstance();
if (value === "visible")
{
// Register to manager (zIndex handling etc.)
mgr.add(this);
// Mark opened in parent menu
var parentMenu = this.getParentMenu();
if (parentMenu) {
parentMenu.setOpenedButton(this.getOpener());
}
}
else if (old === "visible")
{
// Deregister from manager (zIndex handling etc.)
mgr.remove(this);
// Unmark opened in parent menu
var parentMenu = this.getParentMenu();
if (parentMenu && parentMenu.getOpenedButton() == this.getOpener()) {
parentMenu.resetOpenedButton();
}
// Clear properties
this.resetOpenedButton();
this.resetSelectedButton();
}
this.__updateBlockerVisibility();
},
/**
* Updates the blocker's visibility
*/
__updateBlockerVisibility : function()
{
if (this.isVisible())
{
if (this.getBlockBackground()) {
var zIndex = this.getZIndex();
this._blocker.blockContent(zIndex - 1);
}
}
else
{
if (this._blocker.isBlocked()) {
this._blocker.unblock();
}
}
},
/**
* Get the parent menu. Returns <code>null</code> if the menu doesn't have a
* parent menu.
*
* @return {qx.ui.core.Widget|null} The parent menu.
*/
getParentMenu : function()
{
var widget = this.getOpener();
if (!widget || !(widget instanceof qx.ui.menu.AbstractButton)) {
return null;
}
if (widget && widget.getContextMenu() === this) {
return null;
}
while (widget && !(widget instanceof qx.ui.menu.Menu)) {
widget = widget.getLayoutParent();
}
return widget;
},
// property apply
_applySelectedButton : function(value, old)
{
if (old) {
old.removeState("selected");
}
if (value) {
value.addState("selected");
}
},
// property apply
_applyOpenedButton : function(value, old)
{
if (old && old.getMenu()) {
old.getMenu().exclude();
}
if (value) {
value.getMenu().open();
}
},
// property apply
_applyBlockerColor : function(value, old) {
this._blocker.setColor(value);
},
// property apply
_applyBlockerOpacity : function(value, old) {
this._blocker.setOpacity(value);
},
/*
---------------------------------------------------------------------------
SCROLLING SUPPORT
---------------------------------------------------------------------------
*/
// overridden
getChildrenContainer : function() {
return this.getChildControl("slidebar", true) || this;
},
// overridden
_createChildControlImpl : function(id, hash)
{
var control;
switch(id)
{
case "slidebar":
var control = new qx.ui.menu.MenuSlideBar();
var layout = this._getLayout();
this._setLayout(new qx.ui.layout.Grow());
var slidebarLayout = control.getLayout();
control.setLayout(layout);
slidebarLayout.dispose();
var children = qx.lang.Array.clone(this.getChildren());
for (var i=0; i<children.length; i++) {
control.add(children[i]);
}
this.removeListener("resize", this._onResize, this);
control.getChildrenContainer().addListener("resize", this._onResize, this);
this._add(control);
break;
}
return control || this.base(arguments, id);
},
/**
* Get the menu layout manager
*
* @return {qx.ui.layout.Abstract} The menu layout manager
*/
_getMenuLayout : function()
{
if (this.hasChildControl("slidebar")) {
return this.getChildControl("slidebar").getChildrenContainer().getLayout();
} else {
return this._getLayout();
}
},
/**
* Get the menu bounds
*
* @return {Map} The menu bounds
*/
_getMenuBounds : function()
{
if (this.hasChildControl("slidebar")) {
return this.getChildControl("slidebar").getChildrenContainer().getBounds();
} else {
return this.getBounds();
}
},
/**
* Computes the size of the menu. This method is used by the
* {@link qx.ui.core.MPlacement} mixin.
* @return {Map} The menu bounds
*/
_computePlacementSize : function() {
return this._getMenuBounds();
},
/**
* Updates the visibility of the slidebar based on the menu's current size
* and position.
*/
__updateSlideBar : function()
{
var menuBounds = this._getMenuBounds();
if (!menuBounds)
{
this.addListenerOnce("resize", this.__updateSlideBar, this);
return;
}
var rootHeight = this.getLayoutParent().getBounds().height;
var top = this.getLayoutProperties().top;
var left = this.getLayoutProperties().left;
// Adding the slidebar must be deferred because this call can happen
// during the layout flush, which make it impossible to move existing
// layout to the slidebar
if (top < 0)
{
this._assertSlideBar(function() {
this.setHeight(menuBounds.height + top);
this.moveTo(left, 0);
});
}
else if (top + menuBounds.height > rootHeight)
{
this._assertSlideBar(function() {
this.setHeight(rootHeight - top);
});
}
else
{
this.setHeight(null);
}
},
/**
* Schedules the addition of the slidebar and calls the given callback
* after the slidebar has been added.
*
* @param callback {Function} the callback to call
* @return {var|undefined} The return value of the callback if the slidebar
* already exists, or <code>undefined</code> if it doesn't
*/
_assertSlideBar : function(callback)
{
if (this.hasChildControl("slidebar")) {
return callback.call(this);
}
this.__onAfterSlideBarAdd = callback;
qx.ui.core.queue.Widget.add(this);
},
// overridden
syncWidget : function(jobs)
{
this.getChildControl("slidebar");
if (this.__onAfterSlideBarAdd)
{
this.__onAfterSlideBarAdd.call(this);
delete this.__onAfterSlideBarAdd;
}
},
/*
---------------------------------------------------------------------------
EVENT HANDLING
---------------------------------------------------------------------------
*/
/**
* Update position if the menu or the root is resized
*/
_onResize : function()
{
if (this.isVisible())
{
var target = this._placementTarget;
if (!target) {
return;
} else if (target instanceof qx.ui.core.Widget) {
this.placeToWidget(target, true);
} else if (target.top !== undefined) {
this.placeToPoint(target);
} else {
throw new Error("Unknown target: " + target);
}
this.__updateSlideBar();
}
},
/**
* Event listener for pointerover event.
*
* @param e {qx.event.type.Pointer} pointerover event
*/
_onPointerOver : function(e)
{
// Cache manager
var mgr = qx.ui.menu.Manager.getInstance();
// Be sure this menu is kept
mgr.cancelClose(this);
// Change selection
var target = e.getTarget();
if (target.isEnabled() && target instanceof qx.ui.menu.AbstractButton)
{
// Select button directly
this.setSelectedButton(target);
var subMenu = target.getMenu && target.getMenu();
if (subMenu)
{
subMenu.setOpener(target);
// Finally schedule for opening
mgr.scheduleOpen(subMenu);
// Remember scheduled menu for opening
this.__scheduledOpen = subMenu;
}
else
{
var opened = this.getOpenedButton();
if (opened) {
mgr.scheduleClose(opened.getMenu());
}
if (this.__scheduledOpen)
{
mgr.cancelOpen(this.__scheduledOpen);
this.__scheduledOpen = null;
}
}
}
else if (!this.getOpenedButton())
{
// When no button is opened reset the selection
// Otherwise keep it
this.resetSelectedButton();
}
},
/**
* Event listener for pointerout event.
*
* @param e {qx.event.type.Pointer} pointerout event
*/
_onPointerOut : function(e)
{
// Cache manager
var mgr = qx.ui.menu.Manager.getInstance();
// Detect whether the related target is out of the menu
if (!qx.ui.core.Widget.contains(this, e.getRelatedTarget()))
{
// Update selected property
// Force it to the open sub menu in cases where that is opened
// Otherwise reset it. Menus which are left by the cursor should
// not show any selection.
var opened = this.getOpenedButton();
opened ? this.setSelectedButton(opened) : this.resetSelectedButton();
// Cancel a pending close request for the currently
// opened sub menu
if (opened) {
mgr.cancelClose(opened.getMenu());
}
// When leaving this menu to the outside, stop
// all pending requests to open any other sub menu
if (this.__scheduledOpen) {
mgr.cancelOpen(this.__scheduledOpen);
}
}
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function()
{
if (!qx.core.ObjectRegistry.inShutDown) {
qx.ui.menu.Manager.getInstance().remove(this);
}
this.getApplicationRoot().removeListener("resize", this._onResize, this);
this._placementTarget = null;
this._disposeObjects("_blocker");
}
});