425 lines
12 KiB
JavaScript
425 lines
12 KiB
JavaScript
/**
|
|
* ContextSensitive: a library for generating context-sensitive content on
|
|
* HTML elements. It will take over the click/oncontextmenu functions for the
|
|
* document, and works only where these are possible to override. It allows
|
|
* contextmenus to be created via both a left and right mouse click.
|
|
*
|
|
* On Opera, the context menu is triggered by a left click + SHIFT + CTRL
|
|
* combination.
|
|
*
|
|
* Requires prototypejs 1.6+ and scriptaculous 1.8+ (effects.js only).
|
|
*
|
|
*
|
|
* Usage:
|
|
* ------
|
|
* cs = new ContextSensitive();
|
|
*
|
|
* Custom Events:
|
|
* --------------
|
|
* Custom events are triggered on the base element. The parameters given
|
|
* below are available through the 'memo' property of the Event object.
|
|
*
|
|
* ContextSensitive:click
|
|
* Fired when a contextmenu element is clicked on.
|
|
* params: (object) elt - (Element) The menu element clicked on.
|
|
* trigger - (string) The parent menu.
|
|
*
|
|
* ContextSensitive:show
|
|
* Fired before a contextmenu is displayed.
|
|
* params: (string) The DOM ID of the context menu.
|
|
*
|
|
* ContextSensitive:trigger
|
|
* Fired when a context menu is triggered and the element does not exist on
|
|
* the page.
|
|
* params: (string) The DOM ID of the context menu element.
|
|
*
|
|
*
|
|
* Original code by Havard Eide (http://eide.org/) released under the MIT
|
|
* license.
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
* DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @copyright 2006-2015 Horde LLC
|
|
*/
|
|
|
|
var ContextSensitive = Class.create({
|
|
|
|
initialize: function()
|
|
{
|
|
this.baseelt = null;
|
|
this.current = [];
|
|
this.elements = $H();
|
|
this.submenus = $H();
|
|
this.submenus_cache = [];
|
|
this.triggers = [];
|
|
|
|
if (!Prototype.Browser.Opera) {
|
|
document.observe('contextmenu', this._rightClickHandler.bindAsEventListener(this));
|
|
}
|
|
document.observe('click', this._leftClickHandler.bindAsEventListener(this));
|
|
document.observe('mouseover', this._mouseoverHandler.bindAsEventListener(this));
|
|
},
|
|
|
|
/**
|
|
* Elements are of type ContextSensitive.Element.
|
|
*/
|
|
addElement: function(id, target, opts)
|
|
{
|
|
// !!opts.left == Boolean(left)
|
|
var left = !!opts.left;
|
|
if (id && !this.validElement(id, left)) {
|
|
// ~~left == Number(left)
|
|
this.elements.set(id + ~~left, new ContextSensitive.Element(id, target, opts));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove a registered element.
|
|
*/
|
|
removeElement: function(id)
|
|
{
|
|
this.elements.unset(id + '0');
|
|
this.elements.unset(id + '1');
|
|
},
|
|
|
|
/**
|
|
* Hide the currently displayed element(s).
|
|
*/
|
|
close: function()
|
|
{
|
|
this._closeMenu(0, true);
|
|
},
|
|
|
|
/**
|
|
* Close all menus below a specified level.
|
|
*/
|
|
_closeMenu: function(idx, immediate)
|
|
{
|
|
if (this.current.size()) {
|
|
this.current.splice(idx, this.current.size() - idx).each(function(s) {
|
|
// Fade-out on final display.
|
|
if (!immediate && idx === 0) {
|
|
s.fade({ duration: 0.15 });
|
|
} else {
|
|
$(s).hide();
|
|
}
|
|
});
|
|
|
|
this.triggers.splice(idx, this.triggers.size() - idx).each(function(s) {
|
|
$(s).removeClassName('contextHover');
|
|
});
|
|
|
|
if (idx === 0) {
|
|
this.baseelt = null;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the current displayed menu element ID, if any. If more than one
|
|
* submenu is open, returns the last ID opened.
|
|
*/
|
|
currentmenu: function()
|
|
{
|
|
return this.current.last();
|
|
},
|
|
|
|
/**
|
|
* Get a valid element (the ones that can be right-clicked) based
|
|
* on a element ID.
|
|
*/
|
|
validElement: function(id, left)
|
|
{
|
|
return this.elements.get(id + ~~(!!left));
|
|
},
|
|
|
|
/**
|
|
* Set the disabled flag of an event.
|
|
*/
|
|
disable: function(id, left, disable)
|
|
{
|
|
var e = this.validElement(id, left);
|
|
if (e) {
|
|
e.disable = disable;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when a left click event occurs. Will return before the
|
|
* element is closed if we click on an element inside of it.
|
|
*/
|
|
_leftClickHandler: function(e)
|
|
{
|
|
var base, elt, elt_up, trigger;
|
|
|
|
if (this.operaCheck(e)) {
|
|
this._rightClickHandler(e, false);
|
|
e.stop();
|
|
return;
|
|
}
|
|
|
|
// Check for a right click. FF on Linux triggers an onclick event even
|
|
// w/a right click, so disregard.
|
|
if (e.isRightClick()) {
|
|
return;
|
|
}
|
|
|
|
// Check for click in open contextmenu.
|
|
if (this.current.size()) {
|
|
elt = e.element();
|
|
if (!elt.match('A')) {
|
|
elt = elt.up('A');
|
|
if (!elt) {
|
|
this._rightClickHandler(e, true);
|
|
return;
|
|
}
|
|
}
|
|
elt_up = elt.up('.contextMenu');
|
|
|
|
if (elt_up) {
|
|
e.stop();
|
|
|
|
if (elt.hasClassName('contextSubmenu') &&
|
|
elt_up.identify() != this.currentmenu()) {
|
|
this._closeMenu(this.current.indexOf(elt.identify()));
|
|
}
|
|
|
|
base = this.baseelt;
|
|
trigger = this.triggers.last();
|
|
this.close();
|
|
base.fire('ContextSensitive:click', { elt: elt, trigger: trigger });
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if the mouseclick is registered to an element now.
|
|
this._rightClickHandler(e, true);
|
|
},
|
|
|
|
/**
|
|
* Checks if the Opera right-click emulation is present.
|
|
*/
|
|
operaCheck: function(e)
|
|
{
|
|
return Prototype.Browser.Opera && e.shiftKey && e.ctrlKey;
|
|
},
|
|
|
|
/**
|
|
* Called when a right click event occurs.
|
|
*/
|
|
_rightClickHandler: function(e, left)
|
|
{
|
|
if (this.trigger(e.element(), left, e.pointerX(), e.pointerY())) {
|
|
e.stop();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Display context menu if valid element has been activated.
|
|
*/
|
|
trigger: function(target, leftclick, x, y)
|
|
{
|
|
var ctx, def_ctx, offset, offsets, tmp, voffsets;
|
|
|
|
if (!Object.isElement(target)) {
|
|
return false;
|
|
}
|
|
|
|
[ target ].concat(target.ancestors()).find(function(n) {
|
|
ctx = this.validElement(n.id, leftclick);
|
|
return ctx;
|
|
}, this);
|
|
|
|
// Try to retrieve the context-sensitive element we want to
|
|
// display. If we can't find it we just return.
|
|
if (!ctx ||
|
|
ctx.disable ||
|
|
(leftclick && target == this.baseelt)) {
|
|
tmp = target.up('.contextMenu');
|
|
def_ctx = tmp && this.current.include(tmp.readAttribute('id'));
|
|
this.close();
|
|
return def_ctx;
|
|
}
|
|
|
|
this.close();
|
|
|
|
// Register the element that was clicked on.
|
|
this.baseelt = target;
|
|
|
|
offset = ctx.opts.offset;
|
|
if (!offset && (Object.isUndefined(x) || Object.isUndefined(y))) {
|
|
offset = target.identify();
|
|
}
|
|
offset = $(offset);
|
|
|
|
if (offset) {
|
|
offsets = offset.viewportOffset();
|
|
voffsets = document.viewport.getScrollOffsets();
|
|
x = offsets[0] + voffsets.left;
|
|
y = offsets[1] + offset.getHeight() + voffsets.top;
|
|
}
|
|
|
|
if (!this._displayMenu(ctx.ctx, x, y)) {
|
|
return false;
|
|
}
|
|
|
|
this.triggers.push(ctx.ctx);
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Display the [sub]menu on the screen.
|
|
*/
|
|
_displayMenu: function(elt_id, x, y)
|
|
{
|
|
var eltL, h, id, v, w,
|
|
elt = $(elt_id);
|
|
|
|
if (!elt) {
|
|
document.fire('ContextSensitive:trigger', elt_id);
|
|
elt = $(elt_id);
|
|
if (!elt) {
|
|
return false;
|
|
}
|
|
|
|
elt.addClassName('contextMenu');
|
|
|
|
// Attempt to attach submenus now.
|
|
this.submenus_cache = this.submenus_cache.reject(function(s) {
|
|
return $(s)
|
|
? $(s).addClassName('contextSubmenu')
|
|
: false;
|
|
});
|
|
}
|
|
|
|
id = elt.identify();
|
|
|
|
this.baseelt.fire('ContextSensitive:show', id);
|
|
|
|
// Get window/element dimensions
|
|
elt.setStyle({ visibility: 'hidden' }).show();
|
|
eltL = elt.getLayout();
|
|
h = eltL.get('border-box-height');
|
|
w = eltL.get('border-box-width');
|
|
elt.hide().setStyle({
|
|
height: 'auto',
|
|
visibility: 'visible'
|
|
});
|
|
v = document.viewport.getDimensions();
|
|
|
|
// Make sure context window is entirely on screen
|
|
if ((y + h + 5) > v.height) {
|
|
y = Math.max(5, v.height - h - 5);
|
|
|
|
if (h - 10 > v.height) {
|
|
elt.setStyle({ height: (v.height - 10) + 'px' });
|
|
}
|
|
}
|
|
|
|
if ((x + w) > v.width) {
|
|
x = this.current.size()
|
|
? ($(this.current.last()).viewportOffset()[0] - w)
|
|
: (v.width - w - 2);
|
|
}
|
|
|
|
elt.setStyle({ left: x + 'px', top: y + 'px' });
|
|
|
|
if (this.current.size()) {
|
|
elt.show();
|
|
} else {
|
|
// Fade-in on initial display.
|
|
elt.appear({ duration: 0.15 });
|
|
}
|
|
|
|
this.current.push(id);
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Add a submenu to an existing menu.
|
|
*/
|
|
addSubMenu: function(id, submenu)
|
|
{
|
|
if (!this.submenus.get(id)) {
|
|
this.submenus.set(id, submenu);
|
|
this.submenus_cache.push(id);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Mouseover DOM Event handler.
|
|
*/
|
|
_mouseoverHandler: function(e)
|
|
{
|
|
if (!this.current.size()) {
|
|
return;
|
|
}
|
|
|
|
var cm = this.currentmenu(),
|
|
elt = e.element(),
|
|
elt_up = elt.up('.contextMenu'),
|
|
id = elt.identify(),
|
|
id_div, offsets, sub, voffsets, x, y;
|
|
|
|
if (!elt_up) {
|
|
return;
|
|
}
|
|
|
|
id_div = elt_up.identify();
|
|
|
|
if (elt.hasClassName('contextSubmenu')) {
|
|
sub = this.submenus.get(id);
|
|
if (sub != cm || this.currentmenu() != id) {
|
|
if (id_div != cm) {
|
|
this._closeMenu(this.current.indexOf(id_div) + 1);
|
|
}
|
|
|
|
offsets = elt.viewportOffset();
|
|
voffsets = document.viewport.getScrollOffsets();
|
|
x = offsets[0] + voffsets.left + elt.getWidth();
|
|
y = offsets[1] + voffsets.top;
|
|
if (this._displayMenu(sub, x, y)) {
|
|
this.triggers.push(id);
|
|
elt.addClassName('contextHover');
|
|
}
|
|
}
|
|
} else if ((this.current.size() > 1) &&
|
|
id_div != cm) {
|
|
this._closeMenu(this.current.indexOf(id));
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
ContextSensitive.Element = Class.create({
|
|
|
|
// opts: 'left' -> monitor left click; 'offset' -> id of element used to
|
|
// determine offset placement
|
|
initialize: function(id, target, opts)
|
|
{
|
|
this.id = id;
|
|
this.ctx = target;
|
|
this.opts = opts;
|
|
this.opts.left = !!opts.left;
|
|
this.disable = opts.disable;
|
|
}
|
|
|
|
});
|