Files
server/usr/share/psa-horde/js/contextsensitive.js
2026-01-07 20:52:11 +01:00

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;
}
});