blob: 82777a2bd6865e5000c7a7bad34bec7e6d58256d [file] [log] [blame]
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxPopupMenu
*
* Basic popup menu. To add a vertical scrollbar to a given submenu, the
* following code can be used.
*
* (code)
* var mxPopupMenuShowMenu = mxPopupMenu.prototype.showMenu;
* mxPopupMenu.prototype.showMenu = function()
* {
* mxPopupMenuShowMenu.apply(this, arguments);
*
* this.div.style.overflowY = 'auto';
* this.div.style.overflowX = 'hidden';
* this.div.style.maxHeight = '160px';
* };
* (end)
*
* Constructor: mxPopupMenu
*
* Constructs a popupmenu.
*
* Event: mxEvent.SHOW
*
* Fires after the menu has been shown in <popup>.
*/
function mxPopupMenu(factoryMethod)
{
this.factoryMethod = factoryMethod;
if (factoryMethod != null)
{
this.init();
}
};
/**
* Extends mxEventSource.
*/
mxPopupMenu.prototype = new mxEventSource();
mxPopupMenu.prototype.constructor = mxPopupMenu;
/**
* Variable: submenuImage
*
* URL of the image to be used for the submenu icon.
*/
mxPopupMenu.prototype.submenuImage = mxClient.imageBasePath + '/submenu.gif';
/**
* Variable: zIndex
*
* Specifies the zIndex for the popupmenu and its shadow. Default is 1006.
*/
mxPopupMenu.prototype.zIndex = 10006;
/**
* Variable: factoryMethod
*
* Function that is used to create the popup menu. The function takes the
* current panning handler, the <mxCell> under the mouse and the mouse
* event that triggered the call as arguments.
*/
mxPopupMenu.prototype.factoryMethod = null;
/**
* Variable: useLeftButtonForPopup
*
* Specifies if popupmenus should be activated by clicking the left mouse
* button. Default is false.
*/
mxPopupMenu.prototype.useLeftButtonForPopup = false;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxPopupMenu.prototype.enabled = true;
/**
* Variable: itemCount
*
* Contains the number of times <addItem> has been called for a new menu.
*/
mxPopupMenu.prototype.itemCount = 0;
/**
* Variable: autoExpand
*
* Specifies if submenus should be expanded on mouseover. Default is false.
*/
mxPopupMenu.prototype.autoExpand = false;
/**
* Variable: smartSeparators
*
* Specifies if separators should only be added if a menu item follows them.
* Default is false.
*/
mxPopupMenu.prototype.smartSeparators = false;
/**
* Variable: labels
*
* Specifies if any labels should be visible. Default is true.
*/
mxPopupMenu.prototype.labels = true;
/**
* Function: init
*
* Initializes the shapes required for this vertex handler.
*/
mxPopupMenu.prototype.init = function()
{
// Adds the inner table
this.table = document.createElement('table');
this.table.className = 'mxPopupMenu';
this.tbody = document.createElement('tbody');
this.table.appendChild(this.tbody);
// Adds the outer div
this.div = document.createElement('div');
this.div.className = 'mxPopupMenu';
this.div.style.display = 'inline';
this.div.style.zIndex = this.zIndex;
this.div.appendChild(this.table);
// Disables the context menu on the outer div
mxEvent.disableContextMenu(this.div);
};
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxPopupMenu.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*/
mxPopupMenu.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: isPopupTrigger
*
* Returns true if the given event is a popupmenu trigger for the optional
* given cell.
*
* Parameters:
*
* me - <mxMouseEvent> that represents the mouse event.
*/
mxPopupMenu.prototype.isPopupTrigger = function(me)
{
return me.isPopupTrigger() || (this.useLeftButtonForPopup && mxEvent.isLeftMouseButton(me.getEvent()));
};
/**
* Function: addItem
*
* Adds the given item to the given parent item. If no parent item is specified
* then the item is added to the top-level menu. The return value may be used
* as the parent argument, ie. as a submenu item. The return value is the table
* row that represents the item.
*
* Paramters:
*
* title - String that represents the title of the menu item.
* image - Optional URL for the image icon.
* funct - Function associated that takes a mouseup or touchend event.
* parent - Optional item returned by <addItem>.
* iconCls - Optional string that represents the CSS class for the image icon.
* IconsCls is ignored if image is given.
* enabled - Optional boolean indicating if the item is enabled. Default is true.
* active - Optional boolean indicating if the menu should implement any event handling.
* Default is true.
*/
mxPopupMenu.prototype.addItem = function(title, image, funct, parent, iconCls, enabled, active)
{
parent = parent || this;
this.itemCount++;
// Smart separators only added if element contains items
if (parent.willAddSeparator)
{
if (parent.containsItems)
{
this.addSeparator(parent, true);
}
parent.willAddSeparator = false;
}
parent.containsItems = true;
var tr = document.createElement('tr');
tr.className = 'mxPopupMenuItem';
var col1 = document.createElement('td');
col1.className = 'mxPopupMenuIcon';
// Adds the given image into the first column
if (image != null)
{
var img = document.createElement('img');
img.src = image;
col1.appendChild(img);
}
else if (iconCls != null)
{
var div = document.createElement('div');
div.className = iconCls;
col1.appendChild(div);
}
tr.appendChild(col1);
if (this.labels)
{
var col2 = document.createElement('td');
col2.className = 'mxPopupMenuItem' +
((enabled != null && !enabled) ? ' mxDisabled' : '');
mxUtils.write(col2, title);
col2.align = 'left';
tr.appendChild(col2);
var col3 = document.createElement('td');
col3.className = 'mxPopupMenuItem' +
((enabled != null && !enabled) ? ' mxDisabled' : '');
col3.style.paddingRight = '6px';
col3.style.textAlign = 'right';
tr.appendChild(col3);
if (parent.div == null)
{
this.createSubmenu(parent);
}
}
parent.tbody.appendChild(tr);
if (active != false && enabled != false)
{
var currentSelection = null;
mxEvent.addGestureListeners(tr,
mxUtils.bind(this, function(evt)
{
this.eventReceiver = tr;
if (parent.activeRow != tr && parent.activeRow != parent)
{
if (parent.activeRow != null && parent.activeRow.div.parentNode != null)
{
this.hideSubmenu(parent);
}
if (tr.div != null)
{
this.showSubmenu(parent, tr);
parent.activeRow = tr;
}
}
// Workaround for lost current selection in page because of focus in IE
if (mxClient.IS_QUIRKS || document.documentMode == 8)
{
currentSelection = document.selection.createRange();
}
mxEvent.consume(evt);
}),
mxUtils.bind(this, function(evt)
{
if (parent.activeRow != tr && parent.activeRow != parent)
{
if (parent.activeRow != null && parent.activeRow.div.parentNode != null)
{
this.hideSubmenu(parent);
}
if (this.autoExpand && tr.div != null)
{
this.showSubmenu(parent, tr);
parent.activeRow = tr;
}
}
// Sets hover style because TR in IE doesn't have hover
tr.className = 'mxPopupMenuItemHover';
}),
mxUtils.bind(this, function(evt)
{
// EventReceiver avoids clicks on a submenu item
// which has just been shown in the mousedown
if (this.eventReceiver == tr)
{
if (parent.activeRow != tr)
{
this.hideMenu();
}
// Workaround for lost current selection in page because of focus in IE
if (currentSelection != null)
{
// Workaround for "unspecified error" in IE8 standards
try
{
currentSelection.select();
}
catch (e)
{
// ignore
}
currentSelection = null;
}
if (funct != null)
{
funct(evt);
}
}
this.eventReceiver = null;
mxEvent.consume(evt);
})
);
// Resets hover style because TR in IE doesn't have hover
mxEvent.addListener(tr, 'mouseout',
mxUtils.bind(this, function(evt)
{
tr.className = 'mxPopupMenuItem';
})
);
}
return tr;
};
/**
* Adds a checkmark to the given menuitem.
*/
mxPopupMenu.prototype.addCheckmark = function(item, img)
{
var td = item.firstChild.nextSibling;
td.style.backgroundImage = 'url(\'' + img + '\')';
td.style.backgroundRepeat = 'no-repeat';
td.style.backgroundPosition = '2px 50%';
};
/**
* Function: createSubmenu
*
* Creates the nodes required to add submenu items inside the given parent
* item. This is called in <addItem> if a parent item is used for the first
* time. This adds various DOM nodes and a <submenuImage> to the parent.
*
* Parameters:
*
* parent - An item returned by <addItem>.
*/
mxPopupMenu.prototype.createSubmenu = function(parent)
{
parent.table = document.createElement('table');
parent.table.className = 'mxPopupMenu';
parent.tbody = document.createElement('tbody');
parent.table.appendChild(parent.tbody);
parent.div = document.createElement('div');
parent.div.className = 'mxPopupMenu';
parent.div.style.position = 'absolute';
parent.div.style.display = 'inline';
parent.div.style.zIndex = this.zIndex;
parent.div.appendChild(parent.table);
var img = document.createElement('img');
img.setAttribute('src', this.submenuImage);
// Last column of the submenu item in the parent menu
td = parent.firstChild.nextSibling.nextSibling;
td.appendChild(img);
};
/**
* Function: showSubmenu
*
* Shows the submenu inside the given parent row.
*/
mxPopupMenu.prototype.showSubmenu = function(parent, row)
{
if (row.div != null)
{
row.div.style.left = (parent.div.offsetLeft +
row.offsetLeft+row.offsetWidth - 1) + 'px';
row.div.style.top = (parent.div.offsetTop+row.offsetTop) + 'px';
document.body.appendChild(row.div);
// Moves the submenu to the left side if there is no space
var left = parseInt(row.div.offsetLeft);
var width = parseInt(row.div.offsetWidth);
var offset = mxUtils.getDocumentScrollOrigin(document);
var b = document.body;
var d = document.documentElement;
var right = offset.x + (b.clientWidth || d.clientWidth);
if (left + width > right)
{
row.div.style.left = Math.max(0, (parent.div.offsetLeft - width + ((mxClient.IS_IE) ? 6 : -6))) + 'px';
}
mxUtils.fit(row.div);
}
};
/**
* Function: addSeparator
*
* Adds a horizontal separator in the given parent item or the top-level menu
* if no parent is specified.
*
* Parameters:
*
* parent - Optional item returned by <addItem>.
* force - Optional boolean to ignore <smartSeparators>. Default is false.
*/
mxPopupMenu.prototype.addSeparator = function(parent, force)
{
parent = parent || this;
if (this.smartSeparators && !force)
{
parent.willAddSeparator = true;
}
else if (parent.tbody != null)
{
parent.willAddSeparator = false;
var tr = document.createElement('tr');
var col1 = document.createElement('td');
col1.className = 'mxPopupMenuIcon';
col1.style.padding = '0 0 0 0px';
tr.appendChild(col1);
var col2 = document.createElement('td');
col2.style.padding = '0 0 0 0px';
col2.setAttribute('colSpan', '2');
var hr = document.createElement('hr');
hr.setAttribute('size', '1');
col2.appendChild(hr);
tr.appendChild(col2);
parent.tbody.appendChild(tr);
}
};
/**
* Function: popup
*
* Shows the popup menu for the given event and cell.
*
* Example:
*
* (code)
* graph.panningHandler.popup = function(x, y, cell, evt)
* {
* mxUtils.alert('Hello, World!');
* }
* (end)
*/
mxPopupMenu.prototype.popup = function(x, y, cell, evt)
{
if (this.div != null && this.tbody != null && this.factoryMethod != null)
{
this.div.style.left = x + 'px';
this.div.style.top = y + 'px';
// Removes all child nodes from the existing menu
while (this.tbody.firstChild != null)
{
mxEvent.release(this.tbody.firstChild);
this.tbody.removeChild(this.tbody.firstChild);
}
this.itemCount = 0;
this.factoryMethod(this, cell, evt);
if (this.itemCount > 0)
{
this.showMenu();
this.fireEvent(new mxEventObject(mxEvent.SHOW));
}
}
};
/**
* Function: isMenuShowing
*
* Returns true if the menu is showing.
*/
mxPopupMenu.prototype.isMenuShowing = function()
{
return this.div != null && this.div.parentNode == document.body;
};
/**
* Function: showMenu
*
* Shows the menu.
*/
mxPopupMenu.prototype.showMenu = function()
{
// Disables filter-based shadow in IE9 standards mode
if (document.documentMode >= 9)
{
this.div.style.filter = 'none';
}
// Fits the div inside the viewport
document.body.appendChild(this.div);
mxUtils.fit(this.div);
};
/**
* Function: hideMenu
*
* Removes the menu and all submenus.
*/
mxPopupMenu.prototype.hideMenu = function()
{
if (this.div != null)
{
if (this.div.parentNode != null)
{
this.div.parentNode.removeChild(this.div);
}
this.hideSubmenu(this);
this.containsItems = false;
this.fireEvent(new mxEventObject(mxEvent.HIDE));
}
};
/**
* Function: hideSubmenu
*
* Removes all submenus inside the given parent.
*
* Parameters:
*
* parent - An item returned by <addItem>.
*/
mxPopupMenu.prototype.hideSubmenu = function(parent)
{
if (parent.activeRow != null)
{
this.hideSubmenu(parent.activeRow);
if (parent.activeRow.div.parentNode != null)
{
parent.activeRow.div.parentNode.removeChild(parent.activeRow.div);
}
parent.activeRow = null;
}
};
/**
* Function: destroy
*
* Destroys the handler and all its resources and DOM nodes.
*/
mxPopupMenu.prototype.destroy = function()
{
if (this.div != null)
{
mxEvent.release(this.div);
if (this.div.parentNode != null)
{
this.div.parentNode.removeChild(this.div);
}
this.div = null;
}
};