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

766 lines
24 KiB
JavaScript

/**
* dragdrop.js - A minimalist library to handle drag/drop actions.
* Requires prototype.js 1.6.0.2+
*
* Adapted from SkyByte.js/SkyByteDD.js v1.0-beta, May 17 2007
* (c) 2007 Aleksandras Ilarionovas (Alex)
* http://www.skybyte.net/scripts/
*
* Scrolling and ghosting code adapted from script.aculo.us dragdrop.js v1.8.0
* (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
* (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
*
* The original scripts were freely distributable under the terms of an
* MIT-style license.
*
* Usage:
* ------
* new Drag(element, {
* caption: '', // Either string or function to set caption
* // on mouse move.
* classname: '', // Class name of the drag element.
* // DEFAULT: 'drag'
* constraint: '', // Constrain movement to 'horizontal' or
* // 'vertical'.
* ghosting: false, // Show ghost outline when dragging.
* nodrop: false, // Don't do drop checking. Optimizes
* // movement speed.
* offset: { x:0, y:0 }, // An offset to apply to ghosted elements.
* parentElement: function(), // Function returns the parent element.
* scroll: element, // Scroll this element when above/below (
* // only for vertical elements).
* snap: null, // If ghosting, snap allows to specify
* // coords at which the ghosted image will
* // "snap" into place.
* snapToParent: false // Keep image snapped inside the parent
* // element.
* threshold: 0 // Move threshold.
* });
*
* Events fired for Drags:
* -----------------------
* Custom events are triggered on the drag element. The 'memo' property of
* the Event object contains the original event object.
*
* 'DragDrop2:drag'
* Fired on mousemove.
*
* 'DragDrop2:end'
* Fired when dragging ends.
*
* 'DragDrop2:mousedown'
* Fired on mousedown.
*
* 'DragDrop2:mouseup'
* Fored on mouseup *if* the element was not dragged.
*
* 'DragDrop2:start'
* Fired when first moved more than 'threshold'.
*
*
* new Drop(element, {
* accept: [], // Accept filter by tag name(s) or leave empty to
* // accept all tags.
* caption: '', // Either string or function to set caption on
* // mouseover.
* hoverclass: '', // Change the drag element to this class when hovering
* // over an element.
* // DEFAULT: 'dragdrop'
* keypress: false // If true, will re-render caption if a keypress is
* // detected while a drop is active (useful for
* // CTRL/SHIFT actions).
* });
*
* Events fired for Drops:
* -----------------------
* Custom events are triggered on the drop element. The 'memo' property of
* the Event object contains the Drag object. The dragged element is available
* in 'memo.element'. The browser event that triggered the custom event is
* available in 'memo.dragevent'.
*
* 'DragDrop2:drop'
* Fired when mouse button released (a/k/a a drop event).
*
* 'DragDrop2:out'
* Fired when mouse leaves the drop zone.
*
* 'DragDrop2:over'
* Fired when mouse over drop zone.
*
*
* 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.
*
* Copyright 2008-2017 Horde LLC (http://www.horde.org/)
*
* @author Michael Slusarz <slusarz@horde.org>
* @package Horde
*/
var DragDrop = {
Drags: {
drags: $H(),
register: function(obj)
{
if (!this.div) {
this.div = new Element('DIV', { className: obj.options.classname }).setStyle({ position: 'absolute' }).hide();
$(document.body).insert(this.div);
document.observe('mousedown', this._mouseHandler.bindAsEventListener(this));
}
this.drags.set(obj.element.identify(), obj);
obj.element.addClassName('DragElt');
},
unregister: function(obj)
{
if (this.drag == obj.element) {
this.drag.deactivate();
}
this.drags.unset(obj.element.identify());
obj.element.removeClassName('DragElt');
},
getDrag: function(el)
{
return this.drags.get(Object.isElement(el) ? $(el).identify() : el);
},
activate: function(drag)
{
if (this.drag) {
this.deactivate();
}
this.drag = drag;
this.mousemoveE = drag._mouseMove.bindAsEventListener(drag);
this.mouseupE = drag._mouseUp.bindAsEventListener(drag);
this.keypressE = drag._keyPress.bindAsEventListener(drag);
document.observe('mousemove', this.mousemoveE);
document.observe('mouseup', this.mouseupE);
document.observe('keydown', this.keypressE);
document.observe('keyup', this.keypressE);
},
deactivate: function()
{
if (this.drag) {
this.drag = DragDrop.Drops.drop = null;
document.stopObserving('mousemove', this.mousemoveE);
document.stopObserving('mouseup', this.mouseupE);
document.stopObserving('keydown', this.keypressE);
document.stopObserving('keyup', this.keypressE);
}
},
_mouseHandler: function(e)
{
var elt = e.findElement('.DragElt');
if (this.drags.size() && elt) {
this.getDrag(elt).mouseDown(e);
}
}
},
Drops: {
drops: $H(),
register: function(obj)
{
this.drops.set(obj.element.identify(), obj);
obj.element.addClassName('DropElt');
},
unregister: function(obj)
{
if (this.drop == obj.element) {
this.drop = null;
}
this.drops.unset(obj.element.identify());
obj.element.removeClassName('DropElt');
},
getDrop: function(el)
{
return this.drops.get(Object.isElement(el) ? $(el).identify() : el);
}
},
validDrop: function(el)
{
var d = DragDrop.Drops.drop;
return (d &&
el &&
el != d.element &&
(!d.options.accept.size() ||
d.options.accept.include(el.tagName)));
}
};
Drag = Class.create({
initialize: function(el)
{
this.dragevent = null;
this.element = $(el);
this.ghostOffset = [ 0, 0 ];
this.options = Object.extend({
caption: '',
classname: 'drag',
constraint: null,
ghosting: false,
nodrop: false,
parentElement: null,
scroll: null,
snap: null,
snapToParent: false,
threshold: 0
}, arguments[1] || {});
if (this.options.scroll) {
this.options.scroll = $(this.options.scroll);
}
DragDrop.Drags.register(this);
// Disable text selection.
// See: http://ajaxcookbook.org/disable-text-selection/
// Stopping the event on mousedown works on all browsers, but avoid
// that if possible because it will prevent any event handlers further
// up the DOM tree from firing.
if (Prototype.Browser.IE) {
this.element.observe('selectstart', Event.stop);
} else if (Prototype.Browser.Gecko) {
this.element.setStyle({ MozUserSelect: 'none' });
}
},
destroy: function()
{
DragDrop.Drags.unregister(this);
},
mouseDown: function(e)
{
DragDrop.Drags.activate(this);
this.move = 0;
this.wasDragged = false;
this.wasMoved = false;
this.lastcaption = null;
this.clickEvent = e;
this.element.fire('DragDrop2:mousedown', Object.clone(e));
if (this.options.ghosting || this.options.caption) {
if (!DragDrop.Drags.cover) {
DragDrop.Drags.cover = new Element('DIV', { id: 'dragdrop2Cover' });
$(document.body).insert(DragDrop.Drags.cover);
DragDrop.Drags.cover.insert(new Element('DIV').setStyle({ position: 'absolute' }).hide());
}
$$('IFRAME').each(function(i) {
var z;
if (i.visible()) {
z = parseInt(i.getStyle('zIndex'), 10);
if (isNaN(z)) {
z = 2;
}
DragDrop.Drags.cover.insert(DragDrop.Drags.cover.down().clone(false).setStyle({ zIndex: z }).clonePosition(i).show());
}
}, this);
}
if (this.options.snapToParent) {
this.snap = this.options.parentElement
? this.options.parentElement().getDimensions()
: this.element.parentNode.getDimensions();
}
// Stop event to prevent text selection. IE and Gecko are handled in
// initialize().
if (!Prototype.Browser.IE && !Prototype.Browser.Gecko) {
e.stop();
}
},
_mouseMove: function(e)
{
var go, eo, po, xy, p, delta;
if (++this.move <= this.options.threshold) {
return;
} else if (!this.wasMoved) {
this.element.fire('DragDrop2:start', Object.clone(this.clickEvent));
this.wasMoved = true;
}
this.lastCoord = xy = [ e.pointerX(), e.pointerY() ];
if (!this.options.caption) {
if (!this.ghost) {
// Use the position of the original click event as the start
// coordinate.
xy = [ this.clickEvent.pointerX(), this.clickEvent.pointerY() ];
// Create the "ghost", i.e. the moving element, a clone of the
// original element, if it doesn't exist yet.
var layout = this.element.getLayout();
this.ghost = $(this.element.clone(true))
.writeAttribute('id', null)
.addClassName(this.options.classname)
.setStyle({ position: 'absolute', height: layout.get('height') + 'px', width: layout.get('width') + 'px' });
p = this.element.viewportOffset();
delta = document.body.viewportOffset();
delta[0] -= document.body.offsetLeft;
delta[1] -= document.body.offsetTop;
this.ghost.style.left = (p[0] - delta[0]) + 'px';
this.ghost.style.top = (p[1] - delta[1]) + 'px';
// eo is the offset of the original element to the body.
eo = this.element.cumulativeOffset();
// Save external dimensions, i.e. height and width including
// padding and margins, for later usage.
this.dim = {
width: layout.get('margin-box-width'),
height: layout.get('margin-box-height')
};
if (this.options.ghosting) {
var z = parseInt(this.element.getStyle('zIndex'), 10);
if (isNaN(z)) {
z = 1;
}
this.ghost.setOpacity(0.7).setStyle({ zIndex: z + 1 });
} else {
this.element.setStyle({ visibility: 'hidden' });
}
// Insert ghost into the parent, either specified by a
// function result, or using the original element's parent.
if (this.options.parentElement) {
this.options.parentElement().insert(this.ghost);
} else {
this.element.insert({ before: this.ghost });
}
// go is the offset of the ghost to the body. This might be
// different from the original element's offset because we
// used that element's position when cloning the ghost, but
// they might have different parents now.
go = this.ghost.cumulativeOffset();
// Calculate the difference between the ghost's offset and the
// orginal element's offset.
this.ghostOffset = [ go[0] - eo[0], go[1] - eo[1] ];
// Add the event coordinates to the offset, because we use the
// coordinates during later mousemove events as a basis for
// the new ghost position. But we don't want to position the
// ghost relative to the mouse pointer, but relative to where
// the mouse pointer clicked when the ghost was created.
// @todo: why do we subtract eo?
if (this.options.offset) {
this.mouseOffset = this.ghostOffset;
} else {
this.mouseOffset = [ this.ghostOffset[0] + xy[0] - eo[0],
this.ghostOffset[1] + xy[1] - eo[1] ];
}
if (!this.options.caption && this.options.constraint) {
// Because we later only set the left or top coordinates
// when using constraints, we have to set the correct
// "opposite" coordinates here.
po = this.ghost.getOffsetParent().cumulativeOffset();
switch (this.options.constraint) {
case 'horizontal':
this.ghost.setStyle({ top: (eo[1] - po[1]) + 'px' });
break;
case 'vertical':
this.ghost.setStyle({ left: (eo[0] - po[0]) + 'px' });
break;
}
}
}
// Subtract the ghost's offset to the original mouse position and
// add any scrolling.
xy[0] -= this.mouseOffset[0];
xy[1] -= this.mouseOffset[1];
this._setContents(this.ghost, xy[0], xy[1]);
}
if (!this.options.nodrop) {
this._onMoveDrag(xy, e);
}
this.wasDragged = true;
this.element.fire('DragDrop2:drag', Object.clone(e));
if (this.options.scroll) {
this._onMoveScroll();
}
},
_mouseUp: function(e)
{
var d = DragDrop.Drops.drop, tmp;
this._stopScrolling();
if (this.ghost) {
if (!this.options.ghosting) {
this.element.setStyle({ visibility: 'visible' });
}
try {
this.ghost.remove();
} catch (ex) {}
this.ghost = null;
}
DragDrop.Drags.div.hide();
if (DragDrop.validDrop(this.element)) {
this.dragevent = e;
d.element.fire('DragDrop2:drop', this);
}
DragDrop.Drags.deactivate();
if ((this.options.ghosting || this.options.caption) &&
DragDrop.Drags.cover) {
DragDrop.Drags.cover.down().siblings().invoke('remove');
}
if (!this.element.parentNode) {
tmp = new Element('DIV').insert(this.element);
}
this.element.fire(this.wasMoved ? 'DragDrop2:end' : 'DragDrop2:mouseup', Object.clone(e));
tmp = null;
},
_onMoveDrag: function(xy, e)
{
var d = DragDrop.Drops.drop,
div = DragDrop.Drags.div,
d_update = true,
elt = this._findElement(e);
/* elt will be null if we drag off the browser window. */
if (!Object.isElement(elt)) {
return;
}
if (this.lastelt == elt) {
this._setCaption(div, xy);
return;
}
this.lastelt = elt;
/* Do mouseover/mouseout-like detection here. Saves on observe calls
* and handles case where mouse moves over scrollbars. */
if (DragDrop.Drops.drops.size()) {
if (!elt.hasClassName('DropElt')) {
elt = elt.up('.DropElt');
}
if (elt) {
/* Ignore if mouse is over an offset ghosted element. */
if (elt == this.ghost) {
return;
}
elt = DragDrop.Drops.getDrop(elt);
if (elt == d) {
d_update = false;
} else {
elt.mouseOver(e);
d = elt;
}
} else if (d) {
d.mouseOut(e);
d = null;
}
}
if (d_update) {
this._updateCaption(d, div, e);
}
this._setCaption(div, xy);
},
_updateCaption: function(d, div, e)
{
var caption, cname, c_opt;
if (d && DragDrop.validDrop(this.element)) {
d_cap = d.options.caption;
if (!d_cap) {
return;
}
caption = Object.isFunction(d_cap) ? d_cap(d.element, this.element, e) : d_cap;
if (caption && d.options.hoverclass) {
cname = d.options.hoverclass;
}
}
if (!caption) {
c_opt = this.options.caption;
caption = Object.isFunction(c_opt) ? c_opt(this.element) : c_opt;
}
if (caption != this.lastcaption) {
this.lastcaption = caption;
div.update(caption).writeAttribute({ className: cname || this.options.classname });
if (caption.empty()) {
div.hide();
}
}
},
_findElement: function(e)
{
var drop, x, y;
if (this.options.caption ||
(this.options.offset &&
(this.options.offset.x > 0 || this.options.offset.y > 0))) {
return e.element();
}
if (!DragDrop.Drops.drops.size()) {
return;
}
Position.prepare();
x = e.pointerX();
y = e.pointerY();
drop = DragDrop.Drops.drops.find(function(drop) {
return Position.within(drop.value.element, x, y);
});
if (drop) {
return drop.value.element;
}
},
_keyPress: function(e)
{
if (DragDrop.Drops.drop &&
DragDrop.Drops.drop.options.keypress) {
this._updateCaption(DragDrop.Drops.drop, DragDrop.Drags.div, e);
}
},
_setCaption: function(div, xy)
{
if (this.lastcaption) {
this._setContents(div, xy[0] + 15, xy[1] + (this.ghost ? (this.ghost.getHeight() + 5) : 5));
}
},
_onMoveScroll: function()
{
this._stopScrolling();
var delta, p, speed, vp,
s = this.options.scroll,
dim = s.getDimensions();
// No need to scroll if element is not current scrolling.
if (s.scrollHeight == dim.height) {
return;
}
delta = document.viewport.getScrollOffsets();
p = s.viewportOffset();
speed = [ 0, 0 ];
vp = document.viewport.getDimensions();
p[0] += s.scrollLeft + delta.left;
p[2] = p[0] + dim.width;
// Only scroll if directly above/below element
if (this.lastCoord[0] > p[2] ||
this.lastCoord[0] < p[0]) {
return;
}
p[1] = vp.height - dim.height;
p[3] = vp.height - 10;
// Left scroll
//if (this.lastCoord[0] < p[0]) {
// speed[0] = this.lastCoord[0] - p[0];
//}
// Top scroll
if (this.lastCoord[1] < p[1]) {
speed[1] = this.lastCoord[1] - p[1];
}
// Scroll right
//if (this.lastCoord[0] > p[2]) {
// speed[0] = this.lastCoord[0] - p[2];
//}
// Scroll left
if (this.lastCoord[1] > p[3]) {
speed[1] = this.lastCoord[1] - p[3];
}
if (speed[0] || speed[1]) {
this.lastScrolled = new Date();
this.scrollInterval = setInterval(this._scroll.bind(this, speed[0] * 15, speed[1] * 15), 10);
}
},
_stopScrolling: function()
{
if (this.scrollInterval) {
clearInterval(this.scrollInterval);
this.scrollInterval = null;
}
},
_scroll: function(x, y)
{
var current = new Date(),
delta = current - this.lastScrolled,
s = this.options.scroll;
this.lastScrolled = current;
//s.scrollLeft += x * delta / 1000;
s.scrollTop += y * delta / 1000;
},
_setContents: function(elt, x, y)
{
var e_pos, vp, so, xy, style;
if (this.options.offset) {
x += this.options.offset.x;
y += this.options.offset.y;
}
if (this.options.snapToParent) {
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (x + this.dim.width > this.snap.width) {
x = this.snap.width - this.dim.width;
}
if (y + this.dim.height > this.snap.height) {
y = this.snap.height - this.dim.height;
}
} else if (this.options.snap) {
xy = this.options.snap(x, y, this.element);
x = xy[0];
y = xy[1];
} else {
e_pos = elt.getDimensions();
vp = document.viewport.getDimensions();
so = document.viewport.getScrollOffsets();
vp.width += so[0];
vp.height += so[1];
if (x + this.ghostOffset[0] < 0) {
x = -this.ghostOffset[0];
} else if (x + e_pos.width + this.ghostOffset[0] > vp.width) {
x = vp.width - e_pos.width - this.ghostOffset[0];
}
if (y + this.ghostOffset[1] < 0) {
y = -this.ghostOffset[1];
} else if (y + e_pos.height + this.ghostOffset[1] > vp.height) {
y = vp.height - e_pos.height - this.ghostOffset[1];
}
}
if (!this.options.caption) {
switch (this.options.constraint) {
case 'horizontal':
style = { left: x + 'px' };
break;
case 'vertical':
style = { top: y + 'px' };
break;
default:
style = { left: x + 'px', top: y + 'px' };
break;
}
} else {
style = { left: x + 'px', top: y + 'px' };
}
elt.setStyle(style).show();
}
}),
Drop = Class.create({
initialize: function(el)
{
this.element = $(el);
this.options = Object.extend({
accept: [],
caption: '',
hoverclass: 'dragdrop',
keypress: false
}, arguments[1] || {});
DragDrop.Drops.register(this);
},
destroy: function()
{
DragDrop.Drops.unregister(this);
},
mouseOver: function(e)
{
DragDrop.Drops.drop = this;
DragDrop.Drags.drag.dragevent = e;
this.element.fire('DragDrop2:over', DragDrop.Drags.drag);
},
mouseOut: function(e)
{
this.element.fire('DragDrop2:out', DragDrop.Drags.drag);
DragDrop.Drags.drag.dragevent = e;
DragDrop.Drops.drop = null;
}
});