/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of "Scrivo" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* $Id: PopupMenu.js 616 2013-04-22 23:48:38Z geert $
*/
"use strict";
SUI.PopupMenu = SUI.defineClass(
/** @lends SUI.PopupMenu.prototype */{
/** @ignore */ baseClass: SUI.Box,
/**
* @class
* SUI.PopupMenu is a popupmenu as typically can be seen as context menu,
* or as drop down menu on a main menu bar. The items in the menu are
* linked to action handlers or to another (sub)menu.
*
* @augments SUI.Box
*
* @description
* Construct a popup menu. A large number of variables can be set
* to customize the listview to your specific needs.
*
* @constructs
* @param see base class
* @param {SUI.ActionList} arg.actionlist (optional) an action list with
* the definition of the actions
* @param {object[]} arg.cols The menu item definition: an array with
* objects which can contain the following fields:
* @param {boolean} arg.cols[].separator Is the item a separator
* @param {String} arg.cols[].icon The location of the icon for the menu
* item
* @param {boolean} arg.cols[].disabled The enabled state of the menu item
* @param {String} arg.cols[].title The menu item text
* @param {String} arg.cols[].actionId An action identifier if an action
* defined in an action list
* @param {Function} arg.cols[].handler A callback to execute on menu
* item selection
* @param {object[]} arg.cols[].arg.items The item of a submenu (follows
* same definition)
*/
initializer: function(arg) {
SUI.PopupMenu.initializeBase(this, arg);
// are we creating a sub menu
this._main = arg.sub !== true;
// is this menu associated with and action list
this._actionList = arg.actionlist || null;
// start with an empty items array
this.items = [];
this._buildControl(arg.items);
},
/**
* Width of the outer border of the menu
*/
BORDER_WIDTH: 1,
/**
* Size (width and height) of the menu's icons
*/
ICON_SIZE: 16,
/**
* Width of the area where the icons are displayed
*/
ICONBAR_WIDTH: 23,
/**
* Border width of a menu item
*/
ITEM_BORDER_WIDTH: 1,
/**
* The height (inclusive border) of a menu item
*/
ITEM_HEIGHT: 22,
/**
* Padding of the menu items
*/
ITEM_PADDING: 2,
/**
* Padding of the menu (4 sides and not the item padding)
*/
MENU_PADDING: 1,
/**
* The height of the menu separator line
*/
SEPARATOR_LINE_HEIGHT: 1,
/**
* The height of a menu separator
*/
SEPARATOR_HEIGHT: 5,
/**
* Display the popup menu. Set the CSS size and position of the menu
* and all the items.
*/
display: function() {
// set the CSS dimensions of the menu
this.setDim();
this.iconbar.setDim();
// set the CSS dimensions of the menu items
for (var i=0; i<this.items.length; i++) {
this.items[i].bar.setDim();
if (!this.items[i].separator) {
this.items[i].titlediv.setDim();
}
}
},
/**
* Lay out the popup menu. Calculate the size and position of the
* menu and its items.
*/
layOut: function() {
// calculate height
this.height(this.border().height + this.padding().height);
for (var i=0; i<this.items.length; i++) {
if (this.items[i].separator) {
this.height(this.height() + this.SEPARATOR_HEIGHT);
} else {
this.height(this.height() + this.ITEM_HEIGHT);
}
}
// if the menu will drop off the screen correct the top and or left
// of the menu.
if (this.top() + this.height() > SUI.browser.viewportHeight) {
this.top(SUI.browser.viewportHeight - this.height());
}
if (this.left() + this.width() > SUI.browser.viewportWidth) {
this.left(SUI.browser.viewportWidth - this.width());
}
// get the size and position of the icon bar
this.iconbar.setRect(0, 0, this.ICONBAR_WIDTH,
this.clientHeight() + this.padding().height);
var l = 0;
var t = 0;
// get the size an positions of the items
for (var i=0; i<this.items.length; i++) {
if (this.items[i].separator) {
// get separator size and position
this.items[i].bar.setRect(t,
l + this.ICONBAR_WIDTH
+ this.SEPARATOR_HEIGHT,
this.clientWidth() - this.ICONBAR_WIDTH -
- 2 * this.SEPARATOR_HEIGHT,
(this.SEPARATOR_HEIGHT / 2 | 0)
+ this.SEPARATOR_LINE_HEIGHT);
t += this.SEPARATOR_HEIGHT;
} else {
// get menu item size and position
this.items[i].bar.setRect(t, l,
this.clientWidth(), this.ITEM_HEIGHT);
this.items[i].titlediv.setRect(0, this.ICONBAR_WIDTH,
this.clientWidth() - this.ICONBAR_WIDTH, this.ITEM_HEIGHT);
t += this.ITEM_HEIGHT;
// It the menu was displayed before the item can still have
// the selected CSS class, so clear it
this.items[i].bar.removeClass("sui-popup-item-selected");
// It the menu was disabled before the item can still have
// the disabled CSS class, so clear it
this.items[i].bar.removeClass("sui-popup-item-disabled");
if (SUI.browser.isIE) {
this.items[i].titlediv.removeClass(
"sui-popup-item-disabled");
}
// if the item is not enabled set the disabled CSS style
if (!this._isEnabled(this.items[i])) {
this.items[i].bar.addClass("sui-popup-item-disabled");
if (SUI.browser.isIE) {
this.items[i].titlediv.addClass(
"sui-popup-item-disabled");
}
}
}
}
},
/**
* Remove the currently active menu from the screen. Note that this
* function normally will be called from the framework and that there is
* no need to call it yourself. Note also that it removes the currently
* active menu, so not necessarily the one that you have created.
* @param {int} top Top position of the menu
* @param {int} left Left position of the menu
*/
removeMenu: function() {
// if there is an active menu
if (SUI.PopupMenu.activeMenu) {
// remove it and ...
SUI.PopupMenu.activeMenu._removeMenu();
// ... call the onRemoveMenu changed event handler
SUI.PopupMenu.activeMenu.callListener("onRemoveMenu");
SUI.PopupMenu.activeMenu = null;
}
},
/**
* Show the popup menu at the specified location.
* @param {int} top Top position of the menu
* @param {int} left Left position of the menu
*/
showMenu: function(top, left) {
// if we're drawing the main(top) menu we'll need to take care of
// some extra stuff to. We'll assign the menu to a static variable
// because there only can be one menu active at the same time. So
// if the static is already set, we'll remove the menu first.
if (this._main) {
// there can be only one popupmenu, remove the old one first
this.removeMenu();
// add a event listener on the document to catch the clicks next
// to the menu and remove it
var that = this;
SUI.browser.addEventListener(document, "mousedown",
function (e) {
if (!that.removeMenu()) {
SUI.browser.noPropagation(e);
}
}
);
// make this menu the active submenu
SUI.PopupMenu.activeMenu = this;
}
// Add the menu to the document body ...
// TODO: look for a better attachment point
this.parent({el: function() { return document.body; }});
// ... and draw the popup at the required position
this.left(left);
this.top(top);
this.draw();
},
/**
* onRemoveMenu event handler: is executed when the the menu is removed.
*/
onRemoveMenu: function() {
},
_activeSubmenuItem: null,
/* Set event handlers of a menu item:
* mousedown => _selectItem;
* mouseover => _highlightItem;
* mouseout => _restoreItem
*/
_addEventHandlers: function(item) {
// 'that' and 'item' are two closure variables
var that = this;
// Do _selectItem on the mousedown event of a menu item.
SUI.browser.addEventListener(item.bar.el(), "mousedown",
function(e) {
if (!that._selectItem(item)) {
SUI.browser.noPropagation(e);
}
}
);
// Do _highlightItem on the mouseover event of a menu item.
SUI.browser.addEventListener(item.bar.el(), "mouseover",
function(e) {
if (!that._highlightItem(item)) {
SUI.browser.noPropagation(e);
}
}
);
// Do _restoreItem on the mouseout event of a menu item.
SUI.browser.addEventListener(item.bar.el(), "mouseout",
function(e) {
if (!that._restoreItem(item)) {
SUI.browser.noPropagation(e);
}
}
);
},
/* Make all required boxes for the popup menu, set event handlers and
* target actions
*/
_buildControl: function(items) {
// set the style for the control
this.border(new SUI.Border(this.BORDER_WIDTH));
this.padding(new SUI.Padding(this.MENU_PADDING));
this.addClass("sui-popup");
// create and area for the icons
this.iconbar = new SUI.Box({parent: this});
this.iconbar.addClass("sui-popup-iconbar");
var hasSub = false;
// loop through all the items
for (var i=0; i<items.length; i++) {
// create a default item profile
var item = {
separator: items[i].separator || false,
icon: items[i].icon || null,
enabled: !items[i].disabled || false,
title: items[i].title || "Item "+i,
sub: false
};
// is this item associated with an action in an action list ...
if (items[i].actionId !== undefined && this._actionList) {
// ... yes, get the data for the menu item from the action list
var a = this._actionList.get(items[i].actionId);
item.actionId = items[i].actionId;
item.title = a.title;
item.icon = a.icon;
item.handler = a.handler;
} else if (items[i].handler !== undefined) {
// ... else if a handler was given use that one
item.handler = items[i].handler;
} else if (items[i].items !== undefined) {
// ... else create a submenu if submenu data
items[i].sub = true;
item.submenu = new SUI.PopupMenu(items[i]);
hasSub = true;
}
// add the item to the menu's item array
this.items.push(this._createItem(item));
}
// set the width of the menu to the current width (width of the
// longest text) plus additional elements.
this.width(this.width() + this.ICONBAR_WIDTH
+ 2 * (this.ITEM_PADDING + this.MENU_PADDING
+ this.ITEM_BORDER_WIDTH + this.BORDER_WIDTH)
+ (hasSub ? this.ICON_SIZE : 0));
},
/* Create the boxes for the popup menu item.
*/
_createItem: function(item) {
if (item.separator) {
// create a box for a separator line
item.bar = new SUI.Box({parent: this});
item.bar.border(new SUI.Border(0, 0, this.SEPARATOR_LINE_HEIGHT));
item.bar.addClass("sui-popup-separator");
} else {
// create a box for the popup menu item
item.bar = new SUI.Box({parent: this});
item.bar.border(new SUI.Border(this.ITEM_BORDER_WIDTH));
item.bar.addClass("sui-popup-item");
// and a inner box for the menu item text
item.titlediv = new SUI.Box({parent: item.bar});
item.titlediv.padding(
new SUI.Padding(this.ITEM_PADDING));
item.titlediv.el().innerHTML = item.title;
// if the items as a sub-menu set the sub-menu marker
if (item.submenu) {
item.titlediv.el().style.backgroundImage =
"url("+SUI.imgDir+"/"+SUI.resource.pmSub+")";
}
// if the item has an icon add that to the menu
if (item.icon) {
var img = document.createElement("IMG");
img.src = SUI.imgDir + "/" + item.icon;
img.style.padding = this.ITEM_PADDING + "px";
item.bar.el().appendChild(img);
}
// set the event handlers
this._addEventHandlers(item);
}
// get the text width of the item text ...
var iw = SUI.style.textLength(item.title);
// ... and if it is wider that the current text ...
if (iw > this.width()) {
// ... use that width
this.width(iw);
}
return item;
},
/* Highlight a menu item.
*/
_highlightItem: function(item) {
// highlighting is only possible if an item is enabled
if (this._isEnabled(item)) {
// if there is a submenu shown and we want to highlight another
// menu item ...
if (this._activeSubmenuItem && this._activeSubmenuItem !== item) {
// ... then remove it ...
this._activeSubmenuItem.submenu._removeMenu();
/// ... and clear the highlight
this._activeSubmenuItem.bar.removeClass(
"sui-popup-item-selected");
this._activeSubmenuItem = null;
}
// add the highlight to the requested item
item.bar.addClass("sui-popup-item-selected");
// if the item as a submenu them show it
if (item.submenu && !this._activeSubmenuItem) {
var top = this.top() + item.bar.top() - this.BORDER_WIDTH;
var left = this.left() + this.clientWidth();
item.submenu.showMenu(top, left);
this._activeSubmenuItem = item;
}
}
},
/* Is a menu item eabled.
*/
_isEnabled: function(item) {
// if a action list was used then get the enabled state from the
// action list, else use its local setting
return item.actionId
? this._actionList.get(item.actionId).enabled : item.enabled;
},
/* Remove the menu and it currently shown submenus.
*/
_removeMenu: function() {
// if a submenu was shown ...
if (this._activeSubmenuItem) {
// ... recurse into the submenu
this._activeSubmenuItem.submenu._removeMenu();
this._activeSubmenuItem = null;
}
// remove the menu from the document tree
this.removeBox();
},
/* Remove the highlight from the menu item.
*/
_restoreItem: function(item) {
// remove the highlight only if we move to another menu item and the
// currently highlighted one is not a submenu (in that case we'll
// remove the highlight not until we highlighting the new menu item)
if (this._activeSubmenuItem !== item) {
item.bar.removeClass("sui-popup-item-selected");
}
},
/* Select an item, execute the handler if the item is not a submenu
* and remove the popup.
*/
_selectItem: function(item) {
// if the item is enabled and not a submenu ...
if (this._isEnabled(item) && !item.submenu) {
// ... execute the handler and remove the popup.
this.removeMenu();
item.handler(this);
}
}
});