blob: 8a0ac0c01134e2d7fd11d6cf3a3e6c30444758a3 [file] [log] [blame]
/*
Copyright (c) 2004-2006, The Dojo Foundation
All Rights Reserved.
Licensed under the Academic Free License version 2.1 or above OR the
modified BSD license. For more information on Dojo licensing, see:
http://dojotoolkit.org/community/licensing.shtml
*/
dojo.provide("dojo.dnd.HtmlDragManager");
dojo.require("dojo.dnd.DragAndDrop");
dojo.require("dojo.event.*");
dojo.require("dojo.lang.array");
dojo.require("dojo.html.common");
dojo.require("dojo.html.layout");
// NOTE: there will only ever be a single instance of HTMLDragManager, so it's
// safe to use prototype properties for book-keeping.
dojo.declare("dojo.dnd.HtmlDragManager", dojo.dnd.DragManager, {
/**
* There are several sets of actions that the DnD code cares about in the
* HTML context:
* 1.) mouse-down ->
* (draggable selection)
* (dragObject generation)
* mouse-move ->
* (draggable movement)
* (droppable detection)
* (inform droppable)
* (inform dragObject)
* mouse-up
* (inform/destroy dragObject)
* (inform draggable)
* (inform droppable)
* 2.) mouse-down -> mouse-down
* (click-hold context menu)
* 3.) mouse-click ->
* (draggable selection)
* shift-mouse-click ->
* (augment draggable selection)
* mouse-down ->
* (dragObject generation)
* mouse-move ->
* (draggable movement)
* (droppable detection)
* (inform droppable)
* (inform dragObject)
* mouse-up
* (inform draggable)
* (inform droppable)
* 4.) mouse-up
* (clobber draggable selection)
*/
disabled: false, // to kill all dragging!
nestedTargets: false,
mouseDownTimer: null, // used for click-hold operations
dsCounter: 0,
dsPrefix: "dojoDragSource",
// dimension calculation cache for use durring drag
dropTargetDimensions: [],
currentDropTarget: null,
// currentDropTargetPoints: null,
previousDropTarget: null,
_dragTriggered: false,
selectedSources: [],
dragObjects: [],
dragSources: [],
// mouse position properties
currentX: null,
currentY: null,
lastX: null,
lastY: null,
mouseDownX: null,
mouseDownY: null,
threshold: 7,
dropAcceptable: false,
cancelEvent: function(e){ e.stopPropagation(); e.preventDefault();},
// method over-rides
registerDragSource: function(ds){
//dojo.profile.start("register DragSource");
if(ds["domNode"]){
// FIXME: dragSource objects SHOULD have some sort of property that
// references their DOM node, we shouldn't just be passing nodes and
// expecting it to work.
//dojo.profile.start("register DragSource 1");
var dp = this.dsPrefix;
var dpIdx = dp+"Idx_"+(this.dsCounter++);
ds.dragSourceId = dpIdx;
this.dragSources[dpIdx] = ds;
ds.domNode.setAttribute(dp, dpIdx);
//dojo.profile.end("register DragSource 1");
//dojo.profile.start("register DragSource 2");
// so we can drag links
if(dojo.render.html.ie){
//dojo.profile.start("register DragSource IE");
dojo.event.browser.addListener(ds.domNode, "ondragstart", this.cancelEvent);
// terribly slow
//dojo.event.connect(ds.domNode, "ondragstart", this.cancelEvent);
//dojo.profile.end("register DragSource IE");
}
//dojo.profile.end("register DragSource 2");
}
//dojo.profile.end("register DragSource");
},
unregisterDragSource: function(ds){
if (ds["domNode"]){
var dp = this.dsPrefix;
var dpIdx = ds.dragSourceId;
delete ds.dragSourceId;
delete this.dragSources[dpIdx];
ds.domNode.setAttribute(dp, null);
if(dojo.render.html.ie){
dojo.event.browser.removeListener(ds.domNode, "ondragstart", this.cancelEvent);
}
}
},
registerDropTarget: function(dt){
this.dropTargets.push(dt);
},
unregisterDropTarget: function(dt){
var index = dojo.lang.find(this.dropTargets, dt, true);
if (index>=0) {
this.dropTargets.splice(index, 1);
}
},
/**
* Get the DOM element that is meant to drag.
* Loop through the parent nodes of the event target until
* the element is found that was created as a DragSource and
* return it.
*
* @param event object The event for which to get the drag source.
*/
getDragSource: function(e){
var tn = e.target;
if(tn === dojo.body()){ return; }
var ta = dojo.html.getAttribute(tn, this.dsPrefix);
while((!ta)&&(tn)){
tn = tn.parentNode;
if((!tn)||(tn === dojo.body())){ return; }
ta = dojo.html.getAttribute(tn, this.dsPrefix);
}
return this.dragSources[ta];
},
onKeyDown: function(e){
},
onMouseDown: function(e){
if(this.disabled) { return; }
// only begin on left click
if(dojo.render.html.ie) {
if(e.button != 1) { return; }
} else if(e.which != 1) {
return;
}
var target = e.target.nodeType == dojo.html.TEXT_NODE ?
e.target.parentNode : e.target;
// do not start drag involvement if the user is interacting with
// a form element.
if(dojo.html.isTag(target, "button", "textarea", "input", "select", "option")) {
return;
}
// find a selection object, if one is a parent of the source node
var ds = this.getDragSource(e);
// this line is important. if we aren't selecting anything then
// we need to return now, so preventDefault() isn't called, and thus
// the event is propogated to other handling code
if(!ds){ return; }
if(!dojo.lang.inArray(this.selectedSources, ds)){
this.selectedSources.push(ds);
ds.onSelected();
}
this.mouseDownX = e.pageX;
this.mouseDownY = e.pageY;
// Must stop the mouse down from being propogated, or otherwise can't
// drag links in firefox.
// WARNING: preventing the default action on all mousedown events
// prevents user interaction with the contents.
e.preventDefault();
dojo.event.connect(document, "onmousemove", this, "onMouseMove");
},
onMouseUp: function(e, cancel){
// if we aren't dragging then ignore the mouse-up
// (in particular, don't call preventDefault(), because other
// code may need to process this event)
if(this.selectedSources.length==0){
return;
}
this.mouseDownX = null;
this.mouseDownY = null;
this._dragTriggered = false;
// e.preventDefault();
e.dragSource = this.dragSource;
// let ctrl be used for multiselect or another action
// if I use same key to trigger treeV3 node selection and here,
// I have bugs with drag'n'drop. why ?? no idea..
if((!e.shiftKey)&&(!e.ctrlKey)){
//if(!e.shiftKey){
if(this.currentDropTarget) {
this.currentDropTarget.onDropStart();
}
dojo.lang.forEach(this.dragObjects, function(tempDragObj){
var ret = null;
if(!tempDragObj){ return; }
if(this.currentDropTarget) {
e.dragObject = tempDragObj;
// NOTE: we can't get anything but the current drop target
// here since the drag shadow blocks mouse-over events.
// This is probelematic for dropping "in" something
var ce = this.currentDropTarget.domNode.childNodes;
if(ce.length > 0){
e.dropTarget = ce[0];
while(e.dropTarget == tempDragObj.domNode){
e.dropTarget = e.dropTarget.nextSibling;
}
}else{
e.dropTarget = this.currentDropTarget.domNode;
}
if(this.dropAcceptable){
ret = this.currentDropTarget.onDrop(e);
}else{
this.currentDropTarget.onDragOut(e);
}
}
e.dragStatus = this.dropAcceptable && ret ? "dropSuccess" : "dropFailure";
// decouple the calls for onDragEnd, so they don't block the execution here
// ie. if the onDragEnd would call an alert, the execution here is blocked until the
// user has confirmed the alert box and then the rest of the dnd code is executed
// while the mouse doesnt "hold" the dragged object anymore ... and so on
dojo.lang.delayThese([
function() {
// in FF1.5 this throws an exception, see
// http://dojotoolkit.org/pipermail/dojo-interest/2006-April/006751.html
try{
tempDragObj.dragSource.onDragEnd(e)
} catch(err) {
// since the problem seems passing e, we just copy all
// properties and try the copy ...
var ecopy = {};
for (var i in e) {
if (i=="type") { // the type property contains the exception, no idea why...
ecopy.type = "mouseup";
continue;
}
ecopy[i] = e[i];
}
tempDragObj.dragSource.onDragEnd(ecopy);
}
}
, function() {tempDragObj.onDragEnd(e)}]);
}, this);
this.selectedSources = [];
this.dragObjects = [];
this.dragSource = null;
if(this.currentDropTarget) {
this.currentDropTarget.onDropEnd();
}
} else {
//dojo.debug("special click");
}
dojo.event.disconnect(document, "onmousemove", this, "onMouseMove");
this.currentDropTarget = null;
},
onScroll: function(){
//dojo.profile.start("DNDManager updateoffset");
for(var i = 0; i < this.dragObjects.length; i++) {
if(this.dragObjects[i].updateDragOffset) {
this.dragObjects[i].updateDragOffset();
}
}
//dojo.profile.end("DNDManager updateoffset");
// TODO: do not recalculate, only adjust coordinates
if (this.dragObjects.length) {
this.cacheTargetLocations();
}
},
_dragStartDistance: function(x, y){
if((!this.mouseDownX)||(!this.mouseDownX)){
return;
}
var dx = Math.abs(x-this.mouseDownX);
var dx2 = dx*dx;
var dy = Math.abs(y-this.mouseDownY);
var dy2 = dy*dy;
return parseInt(Math.sqrt(dx2+dy2), 10);
},
cacheTargetLocations: function(){
dojo.profile.start("cacheTargetLocations");
this.dropTargetDimensions = [];
dojo.lang.forEach(this.dropTargets, function(tempTarget){
var tn = tempTarget.domNode;
//only cache dropTarget which can accept current dragSource
if(!tn || !tempTarget.accepts([this.dragSource])){ return; }
var abs = dojo.html.getAbsolutePosition(tn, true);
var bb = dojo.html.getBorderBox(tn);
this.dropTargetDimensions.push([
[abs.x, abs.y], // upper-left
// lower-right
[ abs.x+bb.width, abs.y+bb.height ],
tempTarget
]);
//dojo.debug("Cached for "+tempTarget)
}, this);
dojo.profile.end("cacheTargetLocations");
//dojo.debug("Cache locations")
},
onMouseMove: function(e){
if((dojo.render.html.ie)&&(e.button != 1)){
// Oooops - mouse up occurred - e.g. when mouse was not over the
// window. I don't think we can detect this for FF - but at least
// we can be nice in IE.
this.currentDropTarget = null;
this.onMouseUp(e, true);
return;
}
// if we've got some sources, but no drag objects, we need to send
// onDragStart to all the right parties and get things lined up for
// drop target detection
if( (this.selectedSources.length)&&
(!this.dragObjects.length) ){
var dx;
var dy;
if(!this._dragTriggered){
this._dragTriggered = (this._dragStartDistance(e.pageX, e.pageY) > this.threshold);
if(!this._dragTriggered){ return; }
dx = e.pageX - this.mouseDownX;
dy = e.pageY - this.mouseDownY;
}
// the first element is always our dragSource, if there are multiple
// selectedSources (elements that move along) then the first one is the master
// and for it the events will be fired etc.
this.dragSource = this.selectedSources[0];
dojo.lang.forEach(this.selectedSources, function(tempSource){
if(!tempSource){ return; }
var tdo = tempSource.onDragStart(e);
if(tdo){
tdo.onDragStart(e);
// "bump" the drag object to account for the drag threshold
tdo.dragOffset.y += dy;
tdo.dragOffset.x += dx;
tdo.dragSource = tempSource;
this.dragObjects.push(tdo);
}
}, this);
/* clean previous drop target in dragStart */
this.previousDropTarget = null;
this.cacheTargetLocations();
}
// FIXME: we need to add dragSources and dragObjects to e
dojo.lang.forEach(this.dragObjects, function(dragObj){
if(dragObj){ dragObj.onDragMove(e); }
});
// if we have a current drop target, check to see if we're outside of
// it. If so, do all the actions that need doing.
if(this.currentDropTarget){
//dojo.debug(dojo.html.hasParent(this.currentDropTarget.domNode))
var c = dojo.html.toCoordinateObject(this.currentDropTarget.domNode, true);
// var dtp = this.currentDropTargetPoints;
var dtp = [
[c.x,c.y], [c.x+c.width, c.y+c.height]
];
}
if((!this.nestedTargets)&&(dtp)&&(this.isInsideBox(e, dtp))){
if(this.dropAcceptable){
this.currentDropTarget.onDragMove(e, this.dragObjects);
}
}else{
// FIXME: need to fix the event object!
// see if we can find a better drop target
var bestBox = this.findBestTarget(e);
if(bestBox.target === null){
if(this.currentDropTarget){
this.currentDropTarget.onDragOut(e);
this.previousDropTarget = this.currentDropTarget;
this.currentDropTarget = null;
// this.currentDropTargetPoints = null;
}
this.dropAcceptable = false;
return;
}
if(this.currentDropTarget !== bestBox.target){
if(this.currentDropTarget){
this.previousDropTarget = this.currentDropTarget;
this.currentDropTarget.onDragOut(e);
}
this.currentDropTarget = bestBox.target;
// this.currentDropTargetPoints = bestBox.points;
e.dragObjects = this.dragObjects;
this.dropAcceptable = this.currentDropTarget.onDragOver(e);
}else{
if(this.dropAcceptable){
this.currentDropTarget.onDragMove(e, this.dragObjects);
}
}
}
},
findBestTarget: function(e) {
var _this = this;
var bestBox = new Object();
bestBox.target = null;
bestBox.points = null;
dojo.lang.every(this.dropTargetDimensions, function(tmpDA) {
if(!_this.isInsideBox(e, tmpDA)){
return true;
}
bestBox.target = tmpDA[2];
bestBox.points = tmpDA;
// continue iterating only if _this.nestedTargets == true
return Boolean(_this.nestedTargets);
});
return bestBox;
},
isInsideBox: function(e, coords){
if( (e.pageX > coords[0][0])&&
(e.pageX < coords[1][0])&&
(e.pageY > coords[0][1])&&
(e.pageY < coords[1][1]) ){
return true;
}
return false;
},
onMouseOver: function(e){
},
onMouseOut: function(e){
}
});
dojo.dnd.dragManager = new dojo.dnd.HtmlDragManager();
// global namespace protection closure
(function(){
var d = document;
var dm = dojo.dnd.dragManager;
//TODO: when focus manager is ready, dragManager should be rewritten to use it
// set up event handlers on the document (or no?)
dojo.event.connect(d, "onkeydown", dm, "onKeyDown");
dojo.event.connect(d, "onmouseover", dm, "onMouseOver");
dojo.event.connect(d, "onmouseout", dm, "onMouseOut");
dojo.event.connect(d, "onmousedown", dm, "onMouseDown");
dojo.event.connect(d, "onmouseup", dm, "onMouseUp");
// TODO: process scrolling of elements, not only window (focus manager would
// probably come to rescue here as well)
dojo.event.connect(window, "onscroll", dm, "onScroll");
})();