@qooxdoo/framework
Version:
The JS Framework for Coders
988 lines (823 loc) • 29.1 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* Event handler, which supports drag events on DOM elements.
*
* NOTE: Instances of this class must be disposed of after use
*
* @require(qx.event.handler.Gesture)
* @require(qx.event.handler.Keyboard)
* @require(qx.event.handler.Capture)
*/
qx.Class.define("qx.event.handler.DragDrop",
{
extend : qx.core.Object,
implement : [ qx.event.IEventHandler, qx.core.IDisposable ],
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param manager {qx.event.Manager} Event manager for the window to use
*/
construct : function(manager)
{
this.base(arguments);
// Define shorthands
this.__manager = manager;
this.__root = manager.getWindow().document.documentElement;
// Initialize listener
this.__manager.addListener(this.__root, "longtap", this._onLongtap, this);
this.__manager.addListener(this.__root, "pointerdown", this._onPointerdown, this, true);
qx.event.Registration.addListener(window, "blur", this._onWindowBlur, this);
// Initialize data structures
this.__rebuildStructures();
},
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics :
{
/** @type {Integer} Priority of this handler */
PRIORITY : qx.event.Registration.PRIORITY_NORMAL,
/** @type {Map} Supported event types */
SUPPORTED_TYPES :
{
dragstart : 1,
dragend : 1,
dragover : 1,
dragleave : 1,
drop : 1,
drag : 1,
dragchange : 1,
droprequest : 1
},
/** @type {Integer} Whether the method "canHandleEvent" must be called */
IGNORE_CAN_HANDLE : true,
/**
* Array of strings holding the names of the allowed mouse buttons
* for Drag & Drop. The default is "left" but could be extended with
* "middle" or "right"
*/
ALLOWED_BUTTONS: ["left"],
/**
* The distance needed to change the mouse position before a drag session start.
*/
MIN_DRAG_DISTANCE : 5
},
properties : {
/**
* Widget instance of the drag & drop cursor. If non is given, the default
* {@link qx.ui.core.DragDropCursor} will be used.
*/
cursor : {
check : "qx.ui.core.Widget",
nullable : true,
init : null
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__manager : null,
__root : null,
__dropTarget : null,
__dragTarget : null,
__types : null,
__actions : null,
__keys : null,
__cache : null,
__currentType : null,
__currentAction : null,
__sessionActive : false,
__validDrop : false,
__validAction : false,
__dragTargetWidget : null,
__startConfig : null,
/*
---------------------------------------------------------------------------
EVENT HANDLER INTERFACE
---------------------------------------------------------------------------
*/
// interface implementation
canHandleEvent : function(target, type) {},
// interface implementation
registerEvent : function(target, type, capture) {
// Nothing needs to be done here
},
// interface implementation
unregisterEvent : function(target, type, capture) {
// Nothing needs to be done here
},
/*
---------------------------------------------------------------------------
PUBLIC METHODS
---------------------------------------------------------------------------
*/
/**
* Registers a supported type
*
* @param type {String} The type to add
*/
addType : function(type) {
this.__types[type] = true;
},
/**
* Registers a supported action. One of <code>move</code>,
* <code>copy</code> or <code>alias</code>.
*
* @param action {String} The action to add
*/
addAction : function(action) {
this.__actions[action] = true;
},
/**
* Whether the current drag target supports the given type
*
* @param type {String} Any type
* @return {Boolean} Whether the type is supported
*/
supportsType : function(type) {
return !!this.__types[type];
},
/**
* Whether the current drag target supports the given action
*
* @param type {String} Any type
* @return {Boolean} Whether the action is supported
*/
supportsAction : function(type) {
return !!this.__actions[type];
},
/**
* Whether the current drop target allows the current drag target.
*
* @param isAllowed {Boolean} False if a drop should be disallowed
*/
setDropAllowed : function(isAllowed) {
this.__validDrop = isAllowed;
this.__detectAction();
},
/**
* Returns the data of the given type during the <code>drop</code> event
* on the drop target. This method fires a <code>droprequest</code> at
* the drag target which should be answered by calls to {@link #addData}.
*
* Note that this is a synchronous method and if any of the drag and drop
* events handlers are implemented using Promises, this may fail; @see
* `getDataAsync`.
*
* @param type {String} Any supported type
* @return {var} The result data in a promise
*/
getData : function(type)
{
if (!this.__validDrop || !this.__dropTarget) {
throw new Error("This method must not be used outside the drop event listener!");
}
if (!this.__types[type]) {
throw new Error("Unsupported data type: " + type + "!");
}
if (!this.__cache[type])
{
this.__currentType = type;
this.__fireEvent("droprequest", this.__dragTarget, this.__dropTarget, false, false);
}
if (!this.__cache[type]) {
throw new Error("Please use a droprequest listener to the drag source to fill the manager with data!");
}
return this.__cache[type] || null;
},
/**
* Returns the data of the given type during the <code>drop</code> event
* on the drop target. This method fires a <code>droprequest</code> at
* the drag target which should be answered by calls to {@link #addData}.
*
* @param type {String} Any supported type
* @return {qx.Promise} The result data in a promise
*/
getDataAsync : function(type)
{
if (!this.__validDrop || !this.__dropTarget) {
throw new Error("This method must not be used outside the drop event listener!");
}
if (!this.__types[type]) {
throw new Error("Unsupported data type: " + type + "!");
}
var tracker = {};
var self = this;
if (!this.__cache[type]) {
qx.event.Utils.then(tracker, function () {
self.__currentType = type;
return self.__fireEvent("droprequest", self.__dragTarget, self.__dropTarget, false);
});
}
return qx.event.Utils.then(tracker, function () {
if (!this.__cache[type]) {
throw new Error("Please use a droprequest listener to the drag source to fill the manager with data!");
}
return this.__cache[type] || null;
});
},
/**
* Returns the currently selected action (by user keyboard modifiers)
*
* @return {String} One of <code>move</code>, <code>copy</code> or
* <code>alias</code>
*/
getCurrentAction : function() {
this.__detectAction();
return this.__currentAction;
},
/**
* Returns the currently selected action (by user keyboard modifiers)
*
* @return {qx.Promise|String} One of <code>move</code>, <code>copy</code> or
* <code>alias</code>
*/
getCurrentActionAsync : function() {
var self = this;
return qx.Promise.resolve(self.__detectAction())
.then(function() {
return self.__currentAction;
});
},
/**
* Returns the widget which has been the target of the drag start.
* @return {qx.ui.core.Widget} The widget on which the drag started.
*/
getDragTarget : function() {
return this.__dragTargetWidget;
},
/**
* Adds data of the given type to the internal storage. The data
* is available until the <code>dragend</code> event is fired.
*
* @param type {String} Any valid type
* @param data {var} Any data to store
*/
addData : function(type, data) {
this.__cache[type] = data;
},
/**
* Returns the type which was requested last.
*
* @return {String} The last requested data type
*/
getCurrentType : function() {
return this.__currentType;
},
/**
* Returns if a drag session is currently active
*
* @return {Boolean} active drag session
*/
isSessionActive : function() {
return this.__sessionActive;
},
/*
---------------------------------------------------------------------------
INTERNAL UTILS
---------------------------------------------------------------------------
*/
/**
* Rebuilds the internal data storage used during a drag&drop session
*/
__rebuildStructures : function()
{
this.__types = {};
this.__actions = {};
this.__keys = {};
this.__cache = {};
},
/**
* Detects the current action and stores it under the private
* field <code>__currentAction</code>. Also fires the event
* <code>dragchange</code> on every modification.
*
* @return {qx.Promise|null}
*/
__detectAction : function()
{
if (this.__dragTarget == null) {
if (qx.core.Environment.get("qx.promise")) {
return qx.Promise.reject();
} else {
return null;
}
}
var actions = this.__actions;
var keys = this.__keys;
var current = null;
if (this.__validDrop)
{
if (keys.Shift && keys.Control && actions.alias) {
current = "alias";
} else if (keys.Shift && keys.Alt && actions.copy) {
current = "copy";
} else if (keys.Shift && actions.move) {
current = "move";
} else if (keys.Alt && actions.alias) {
current = "alias";
} else if (keys.Control && actions.copy) {
current = "copy";
} else if (actions.move) {
current = "move";
} else if (actions.copy) {
current = "copy";
} else if (actions.alias) {
current = "alias";
}
}
var self = this;
var tracker = {};
var old = this.__currentAction;
if (current != old) {
if (this.__dropTarget) {
qx.event.Utils.catch(function () {
self.__validAction = false;
current = null;
});
qx.event.Utils.then(tracker, function () {
self.__currentAction = current;
return self.__fireEvent("dragchange", self.__dropTarget, self.__dragTarget, true);
});
qx.event.Utils.then(tracker, function (validAction) {
self.__validAction = validAction;
if (!validAction) {
current = null;
}
});
}
}
return qx.event.Utils.then(tracker, function() {
if (current != old) {
self.__currentAction = current;
return self.__fireEvent("dragchange", self.__dragTarget, self.__dropTarget, false);
}
});
},
/**
* Wrapper for {@link qx.event.Registration#fireEvent} for drag&drop events
* needed in this class.
*
* @param type {String} Event type
* @param target {Object} Target to fire on
* @param relatedTarget {Object} Related target, i.e. drag or drop target
* depending on the drag event
* @param cancelable {Boolean} Whether the event is cancelable
* @param original {qx.event.type.Pointer} Original pointer event
* @return {qx.Promise|Boolean} <code>true</code> if the event's default behavior was
* not prevented
*/
__fireEvent : function(type, target, relatedTarget, cancelable, original, async)
{
var Registration = qx.event.Registration;
var dragEvent = Registration.createEvent(type, qx.event.type.Drag, [ cancelable, original ]);
if (target !== relatedTarget) {
dragEvent.setRelatedTarget(relatedTarget);
}
var result = Registration.dispatchEvent(target, dragEvent);
if (qx.core.Environment.get("qx.promise")) {
if (async === undefined || async) {
return qx.Promise.resolve(result)
.then(function() {
return !dragEvent.getDefaultPrevented();
});
} else {
if (qx.core.Environment.get("qx.debug")) {
if (result instanceof qx.Promise) {
this.error("DragDrop event \"" + type + "\" returned a promise but a synchronous event was required, drag and drop may not work as expected (consider using getDataAsync)");
}
}
return result;
}
} else {
return result;
}
},
/**
* Finds next draggable parent of the given element. Maybe the element itself as well.
*
* Looks for the attribute <code>qxDraggable</code> with the value <code>on</code>.
*
* @param elem {Element} The element to query
* @return {Element} The next parent element which is draggable. May also be <code>null</code>
*/
__findDraggable : function(elem)
{
while (elem && elem.nodeType == 1)
{
if (elem.getAttribute("qxDraggable") == "on") {
return elem;
}
elem = elem.parentNode;
}
return null;
},
/**
* Finds next droppable parent of the given element. Maybe the element itself as well.
*
* Looks for the attribute <code>qxDroppable</code> with the value <code>on</code>.
*
* @param elem {Element} The element to query
* @return {Element} The next parent element which is droppable. May also be <code>null</code>
*/
__findDroppable : function(elem)
{
while (elem && elem.nodeType == 1)
{
if (elem.getAttribute("qxDroppable") == "on") {
return elem;
}
elem = elem.parentNode;
}
return null;
},
/**
* Cleans up a drag&drop session when <code>dragstart</code> was fired before.
*
* @return {qx.Promise?} promise, if one was created by event handlers
*/
clearSession : function()
{
//this.debug("clearSession");
// Deregister from root events
this.__manager.removeListener(this.__root, "pointermove", this._onPointermove, this);
this.__manager.removeListener(this.__root, "pointerup", this._onPointerup, this, true);
this.__manager.removeListener(this.__root, "keydown", this._onKeyDown, this, true);
this.__manager.removeListener(this.__root, "keyup", this._onKeyUp, this, true);
this.__manager.removeListener(this.__root, "keypress", this._onKeyPress, this, true);
this.__manager.removeListener(this.__root, "roll", this._onRoll, this, true);
var tracker = {};
var self = this;
// Fire dragend event
if (this.__dragTarget) {
qx.event.Utils.then(tracker, function() {
return self.__fireEvent("dragend", self.__dragTarget, self.__dropTarget, false);
});
}
return qx.event.Utils.then(tracker, function() {
// Cleanup
self.__validDrop = false;
self.__dropTarget = null;
if (self.__dragTargetWidget) {
self.__dragTargetWidget.removeState("drag");
self.__dragTargetWidget = null;
}
// Clear init
//self.debug("Clearing drag target");
self.__dragTarget = null;
self.__sessionActive = false;
self.__startConfig = null;
self.__rebuildStructures();
});
},
/*
---------------------------------------------------------------------------
EVENT HANDLERS
---------------------------------------------------------------------------
*/
/**
* Handler for long tap which takes care of starting the drag & drop session for
* touch interactions.
* @param e {qx.event.type.Tap} The longtap event.
*/
_onLongtap : function(e) {
// only for touch
if (e.getPointerType() != "touch") {
return;
}
// prevent scrolling
this.__manager.addListener(this.__root, "roll", this._onRoll, this, true);
return this._start(e);
},
/**
* Helper to start the drag & drop session. It is responsible for firing the
* dragstart event and attaching the key listener.
* @param e {qx.event.type.Pointer} Either a longtap or pointermove event.
*
* @return {Boolean} Returns <code>false</code> if drag session should be
* canceled.
*/
_start : function(e) {
// only for primary pointer and allowed buttons
var isButtonOk = qx.event.handler.DragDrop.ALLOWED_BUTTONS.indexOf(e.getButton()) !== -1;
if (!e.isPrimary() || !isButtonOk) {
return false;
}
// start target can be none as the drag & drop handler might
// be created after the first start event
var target = this.__startConfig ? this.__startConfig.target : e.getTarget();
var draggable = this.__findDraggable(target);
if (draggable) {
// This is the source target
//this.debug("Setting dragtarget = " + draggable);
this.__dragTarget = draggable;
var widgetOriginalTarget = qx.ui.core.Widget.getWidgetByElement(this.__startConfig.original);
while (widgetOriginalTarget && widgetOriginalTarget.isAnonymous()) {
widgetOriginalTarget = widgetOriginalTarget.getLayoutParent();
}
if (widgetOriginalTarget) {
this.__dragTargetWidget = widgetOriginalTarget;
widgetOriginalTarget.addState("drag");
}
// fire cancelable dragstart
var self = this;
var tracker = {};
qx.event.Utils.catch(function() {
//self.debug("dragstart FAILED, setting __sessionActive=false");
self.__sessionActive = false;
});
qx.event.Utils.then(tracker, function() {
return self.__fireEvent("dragstart", self.__dragTarget, self.__dropTarget, true, e);
});
return qx.event.Utils.then(tracker, function(validAction) {
if (!validAction) {
return;
}
//self.debug("dragstart ok, setting __sessionActive=true")
self.__manager.addListener(self.__root, "keydown", self._onKeyDown, self, true);
self.__manager.addListener(self.__root, "keyup", self._onKeyUp, self, true);
self.__manager.addListener(self.__root, "keypress", self._onKeyPress, self, true);
self.__sessionActive = true;
});
}
},
/**
* Event handler for the pointerdown event which stores the initial targets and the coordinates.
* @param e {qx.event.type.Pointer} The pointerdown event.
*/
_onPointerdown : function(e) {
if (e.isPrimary()) {
this.__startConfig = {
target: e.getTarget(),
original: e.getOriginalTarget(),
left : e.getDocumentLeft(),
top : e.getDocumentTop()
};
this.__manager.addListener(this.__root, "pointermove", this._onPointermove, this);
this.__manager.addListener(this.__root, "pointerup", this._onPointerup, this, true);
}
},
/**
* Event handler for the pointermove event which starts the drag session and
* is responsible for firing the drag, dragover and dragleave event.
* @param e {qx.event.type.Pointer} The pointermove event.
*/
_onPointermove : function(e) {
// only allow drag & drop for primary pointer
if (!e.isPrimary()) {
return;
}
//this.debug("_onPointermove: start");
var self = this;
var tracker = {};
qx.event.Utils.catch(function() {
return self.clearSession();
});
// start the drag session for mouse
if (!self.__sessionActive && e.getPointerType() == "mouse") {
var delta = self._getDelta(e);
// if the mouse moved a bit in any direction
var distance = qx.event.handler.DragDrop.MIN_DRAG_DISTANCE;
if (delta && (Math.abs(delta.x) > distance || Math.abs(delta.y) > distance)) {
//self.debug("_onPointermove: outside min drag distance");
qx.event.Utils.then(tracker, function() {
return self._start(e);
});
}
}
return qx.event.Utils.then(tracker, function() {
// check if the session has been activated
if (!self.__sessionActive) {
//self.debug("not active");
return;
}
var tracker = {};
qx.event.Utils.then(tracker, function() {
//self.debug("active, firing drag");
return self.__fireEvent("drag", self.__dragTarget, self.__dropTarget, true, e);
});
qx.event.Utils.then(tracker, function(validAction) {
if (!validAction) {
this.clearSession();
}
//self.debug("drag");
// find current hovered droppable
var el = e.getTarget();
if (self.__startConfig.target === el) {
// on touch devices the native events return wrong elements as target (its always the element where the dragging started)
el = e.getNativeEvent().view.document.elementFromPoint(e.getDocumentLeft(), e.getDocumentTop());
}
var cursor = self.getCursor();
if (!cursor) {
cursor = qx.ui.core.DragDropCursor.getInstance();
}
var cursorEl = cursor.getContentElement().getDomElement();
if (cursorEl && (el === cursorEl || cursorEl.contains(el))) {
var display = qx.bom.element.Style.get(cursorEl, "display");
// get the cursor out of the way
qx.bom.element.Style.set(cursorEl, "display", "none");
el = e.getNativeEvent().view.document.elementFromPoint(e.getDocumentLeft(), e.getDocumentTop());
qx.bom.element.Style.set(cursorEl, "display", display);
}
if (el !== cursorEl) {
var droppable = self.__findDroppable(el);
// new drop target detected
if (droppable && droppable != self.__dropTarget) {
var dropLeaveTarget = self.__dropTarget;
self.__validDrop = true; // initial value should be true
self.__dropTarget = droppable;
var innerTracker = {};
qx.event.Utils.catch(innerTracker, function () {
self.__dropTarget = null;
self.__validDrop = false;
});
// fire dragleave for previous drop target
if (dropLeaveTarget) {
qx.event.Utils.then(innerTracker, function () {
return self.__fireEvent("dragleave", dropLeaveTarget, self.__dragTarget, false, e);
});
}
qx.event.Utils.then(innerTracker, function () {
return self.__fireEvent("dragover", droppable, self.__dragTarget, true, e);
});
return qx.event.Utils.then(innerTracker, function (validDrop) {
self.__validDrop = validDrop;
});
}
// only previous drop target
else if (!droppable && self.__dropTarget) {
var innerTracker = {};
qx.event.Utils.then(innerTracker, function () {
return self.__fireEvent("dragleave", self.__dropTarget, self.__dragTarget, false, e);
});
return qx.event.Utils.then(innerTracker, function () {
self.__dropTarget = null;
self.__validDrop = false;
return self.__detectAction();
});
}
}
});
return qx.event.Utils.then(tracker, function() {
// Reevaluate current action
var keys = self.__keys;
keys.Control = e.isCtrlPressed();
keys.Shift = e.isShiftPressed();
keys.Alt = e.isAltPressed();
return self.__detectAction();
});
});
},
/**
* Helper function to compute the delta between current cursor position from given event
* and the stored coordinates at {@link #_onPointerdown}.
*
* @param e {qx.event.type.Pointer} The pointer event
*
* @return {Map} containing the deltaX as x, and deltaY as y.
*/
_getDelta : function(e)
{
if (!this.__startConfig) {
return null;
}
var deltaX = e.getDocumentLeft() - this.__startConfig.left;
var deltaY = e.getDocumentTop() - this.__startConfig.top;
return {
"x": deltaX,
"y": deltaY
};
},
/**
* Handler for the pointerup event which is responsible fore firing the drop event.
* @param e {qx.event.type.Pointer} The pointerup event
*/
_onPointerup : function(e) {
if (!e.isPrimary()) {
return;
}
var tracker = {};
var self = this;
// Fire drop event in success case
if (this.__validDrop && this.__validAction) {
qx.event.Utils.then(tracker, function() {
return self.__fireEvent("drop", self.__dropTarget, self.__dragTarget, false, e);
});
}
return qx.event.Utils.then(tracker, function() {
// Stop event
if (e.getTarget() == self.__dragTarget) {
e.stopPropagation();
}
// Clean up
return self.clearSession();
});
},
/**
* Roll listener to stop scrolling on touch devices.
* @param e {qx.event.type.Roll} The roll event.
*/
_onRoll : function(e) {
e.stop();
},
/**
* Event listener for window's <code>blur</code> event
*
* @param e {qx.event.type.Event} Event object
*/
_onWindowBlur : function(e) {
return this.clearSession();
},
/**
* Event listener for root's <code>keydown</code> event
*
* @param e {qx.event.type.KeySequence} Event object
*/
_onKeyDown : function(e) {
var iden = e.getKeyIdentifier();
switch(iden)
{
case "Alt":
case "Control":
case "Shift":
if (!this.__keys[iden])
{
this.__keys[iden] = true;
return this.__detectAction();
}
}
},
/**
* Event listener for root's <code>keyup</code> event
*
* @param e {qx.event.type.KeySequence} Event object
*/
_onKeyUp : function(e) {
var iden = e.getKeyIdentifier();
switch(iden)
{
case "Alt":
case "Control":
case "Shift":
if (this.__keys[iden])
{
this.__keys[iden] = false;
return this.__detectAction();
}
}
},
/**
* Event listener for root's <code>keypress</code> event
*
* @param e {qx.event.type.KeySequence} Event object
*/
_onKeyPress : function(e) {
var iden = e.getKeyIdentifier();
switch(iden)
{
case "Escape":
return this.clearSession();
}
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function()
{
qx.event.Registration.removeListener(window, "blur", this._onWindowBlur, this);
// Clear fields
this.__dragTarget = this.__dropTarget = this.__manager = this.__root =
this.__types = this.__actions = this.__keys = this.__cache = null;
},
/*
*****************************************************************************
DEFER
*****************************************************************************
*/
defer : function(statics) {
qx.event.Registration.addHandler(statics);
}
});