564 řádky
18 KiB
JavaScript
564 řádky
18 KiB
JavaScript
define("dijit/tree/dndSource", [
|
|
"dojo/_base/array", // array.forEach array.indexOf array.map
|
|
"dojo/_base/connect", // isCopyKey
|
|
"dojo/_base/declare", // declare
|
|
"dojo/dom-class", // domClass.add
|
|
"dojo/dom-geometry", // domGeometry.position
|
|
"dojo/_base/lang", // lang.mixin lang.hitch
|
|
"dojo/on", // subscribe
|
|
"dojo/touch",
|
|
"dojo/topic",
|
|
"dojo/dnd/Manager", // DNDManager.manager
|
|
"./_dndSelector"
|
|
], function(array, connect, declare, domClass, domGeometry, lang, on, touch, topic, DNDManager, _dndSelector){
|
|
|
|
// module:
|
|
// dijit/tree/dndSource
|
|
// summary:
|
|
// Handles drag and drop operations (as a source or a target) for `dijit.Tree`
|
|
|
|
/*=====
|
|
var __Item = {
|
|
// summary:
|
|
// New item to be added to the Tree, like:
|
|
// id: Anything
|
|
id: "",
|
|
// name: String
|
|
name: ""
|
|
};
|
|
=====*/
|
|
|
|
var dndSource = declare("dijit.tree.dndSource", _dndSelector, {
|
|
// summary:
|
|
// Handles drag and drop operations (as a source or a target) for `dijit.Tree`
|
|
|
|
// isSource: Boolean
|
|
// Can be used as a DnD source.
|
|
isSource: true,
|
|
|
|
// accept: String[]
|
|
// List of accepted types (text strings) for the Tree; defaults to
|
|
// ["text"]
|
|
accept: ["text", "treeNode"],
|
|
|
|
// copyOnly: [private] Boolean
|
|
// Copy items, if true, use a state of Ctrl key otherwise
|
|
copyOnly: false,
|
|
|
|
// dragThreshold: Number
|
|
// The move delay in pixels before detecting a drag; 5 by default
|
|
dragThreshold: 5,
|
|
|
|
// betweenThreshold: Integer
|
|
// Distance from upper/lower edge of node to allow drop to reorder nodes
|
|
betweenThreshold: 0,
|
|
|
|
// Flag used by Avatar.js to signal to generate text node when dragging
|
|
generateText: true,
|
|
|
|
constructor: function(/*dijit/Tree*/ tree, /*dijit/tree/dndSource*/ params){
|
|
// summary:
|
|
// a constructor of the Tree DnD Source
|
|
// tags:
|
|
// private
|
|
if(!params){ params = {}; }
|
|
lang.mixin(this, params);
|
|
var type = params.accept instanceof Array ? params.accept : ["text", "treeNode"];
|
|
this.accept = null;
|
|
if(type.length){
|
|
this.accept = {};
|
|
for(var i = 0; i < type.length; ++i){
|
|
this.accept[type[i]] = 1;
|
|
}
|
|
}
|
|
|
|
// class-specific variables
|
|
this.isDragging = false;
|
|
this.mouseDown = false;
|
|
this.targetAnchor = null; // DOMNode corresponding to the currently moused over TreeNode
|
|
this.targetBox = null; // coordinates of this.targetAnchor
|
|
this.dropPosition = ""; // whether mouse is over/after/before this.targetAnchor
|
|
this._lastX = 0;
|
|
this._lastY = 0;
|
|
|
|
// states
|
|
this.sourceState = "";
|
|
if(this.isSource){
|
|
domClass.add(this.node, "dojoDndSource");
|
|
}
|
|
this.targetState = "";
|
|
if(this.accept){
|
|
domClass.add(this.node, "dojoDndTarget");
|
|
}
|
|
|
|
// set up events
|
|
this.topics = [
|
|
topic.subscribe("/dnd/source/over", lang.hitch(this, "onDndSourceOver")),
|
|
topic.subscribe("/dnd/start", lang.hitch(this, "onDndStart")),
|
|
topic.subscribe("/dnd/drop", lang.hitch(this, "onDndDrop")),
|
|
topic.subscribe("/dnd/cancel", lang.hitch(this, "onDndCancel"))
|
|
];
|
|
},
|
|
|
|
// methods
|
|
checkAcceptance: function(/*===== source, nodes =====*/){
|
|
// summary:
|
|
// Checks if the target can accept nodes from this source
|
|
// source: dijit/tree/dndSource
|
|
// The source which provides items
|
|
// nodes: DOMNode[]
|
|
// Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if
|
|
// source is a dijit/Tree.
|
|
// tags:
|
|
// extension
|
|
return true; // Boolean
|
|
},
|
|
|
|
copyState: function(keyPressed){
|
|
// summary:
|
|
// Returns true, if we need to copy items, false to move.
|
|
// It is separated to be overwritten dynamically, if needed.
|
|
// keyPressed: Boolean
|
|
// The "copy" control key was pressed
|
|
// tags:
|
|
// protected
|
|
return this.copyOnly || keyPressed; // Boolean
|
|
},
|
|
destroy: function(){
|
|
// summary:
|
|
// Prepares the object to be garbage-collected.
|
|
this.inherited(arguments);
|
|
var h;
|
|
while(h = this.topics.pop()){ h.remove(); }
|
|
this.targetAnchor = null;
|
|
},
|
|
|
|
_onDragMouse: function(e, firstTime){
|
|
// summary:
|
|
// Helper method for processing onmousemove/onmouseover events while drag is in progress.
|
|
// Keeps track of current drop target.
|
|
// e: Event
|
|
// The mousemove event.
|
|
// firstTime: Boolean?
|
|
// If this flag is set, this is the first mouse move event of the drag, so call m.canDrop() etc.
|
|
// even if newTarget == null because the user quickly dragged a node in the Tree to a position
|
|
// over Tree.containerNode but not over any TreeNode (#7971)
|
|
|
|
var m = DNDManager.manager(),
|
|
oldTarget = this.targetAnchor, // the TreeNode corresponding to TreeNode mouse was previously over
|
|
newTarget = this.current, // TreeNode corresponding to TreeNode mouse is currently over
|
|
oldDropPosition = this.dropPosition; // the previous drop position (over/before/after)
|
|
|
|
// calculate if user is indicating to drop the dragged node before, after, or over
|
|
// (i.e., to become a child of) the target node
|
|
var newDropPosition = "Over";
|
|
if(newTarget && this.betweenThreshold > 0){
|
|
// If mouse is over a new TreeNode, then get new TreeNode's position and size
|
|
if(!this.targetBox || oldTarget != newTarget){
|
|
this.targetBox = domGeometry.position(newTarget.rowNode, true);
|
|
}
|
|
if((e.pageY - this.targetBox.y) <= this.betweenThreshold){
|
|
newDropPosition = "Before";
|
|
}else if((e.pageY - this.targetBox.y) >= (this.targetBox.h - this.betweenThreshold)){
|
|
newDropPosition = "After";
|
|
}
|
|
}
|
|
|
|
if(firstTime || newTarget != oldTarget || newDropPosition != oldDropPosition){
|
|
if(oldTarget){
|
|
this._removeItemClass(oldTarget.rowNode, oldDropPosition);
|
|
}
|
|
if(newTarget){
|
|
this._addItemClass(newTarget.rowNode, newDropPosition);
|
|
}
|
|
|
|
// Check if it's ok to drop the dragged node on/before/after the target node.
|
|
if(!newTarget){
|
|
m.canDrop(false);
|
|
}else if(newTarget == this.tree.rootNode && newDropPosition != "Over"){
|
|
// Can't drop before or after tree's root node; the dropped node would just disappear (at least visually)
|
|
m.canDrop(false);
|
|
}else{
|
|
// Guard against dropping onto yourself (TODO: guard against dropping onto your descendant, #7140)
|
|
var sameId = false;
|
|
if(m.source == this){
|
|
for(var dragId in this.selection){
|
|
var dragNode = this.selection[dragId];
|
|
if(dragNode.item === newTarget.item){
|
|
sameId = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(sameId){
|
|
m.canDrop(false);
|
|
}else if(this.checkItemAcceptance(newTarget.rowNode, m.source, newDropPosition.toLowerCase())
|
|
&& !this._isParentChildDrop(m.source, newTarget.rowNode)){
|
|
m.canDrop(true);
|
|
}else{
|
|
m.canDrop(false);
|
|
}
|
|
}
|
|
|
|
this.targetAnchor = newTarget;
|
|
this.dropPosition = newDropPosition;
|
|
}
|
|
},
|
|
|
|
onMouseMove: function(e){
|
|
// summary:
|
|
// Called for any onmousemove/ontouchmove events over the Tree
|
|
// e: Event
|
|
// onmousemouse/ontouchmove event
|
|
// tags:
|
|
// private
|
|
if(this.isDragging && this.targetState == "Disabled"){ return; }
|
|
this.inherited(arguments);
|
|
var m = DNDManager.manager();
|
|
if(this.isDragging){
|
|
this._onDragMouse(e);
|
|
}else{
|
|
if(this.mouseDown && this.isSource &&
|
|
(Math.abs(e.pageX-this._lastX)>=this.dragThreshold || Math.abs(e.pageY-this._lastY)>=this.dragThreshold)){
|
|
var nodes = this.getSelectedTreeNodes();
|
|
if(nodes.length){
|
|
if(nodes.length > 1){
|
|
//filter out all selected items which has one of their ancestor selected as well
|
|
var seen = this.selection, i = 0, r = [], n, p;
|
|
nextitem: while((n = nodes[i++])){
|
|
for(p = n.getParent(); p && p !== this.tree; p = p.getParent()){
|
|
if(seen[p.id]){ //parent is already selected, skip this node
|
|
continue nextitem;
|
|
}
|
|
}
|
|
//this node does not have any ancestors selected, add it
|
|
r.push(n);
|
|
}
|
|
nodes = r;
|
|
}
|
|
nodes = array.map(nodes, function(n){return n.domNode});
|
|
m.startDrag(this, nodes, this.copyState(connect.isCopyKey(e)));
|
|
this._onDragMouse(e, true); // because this may be the only mousemove event we get before the drop
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
onMouseDown: function(e){
|
|
// summary:
|
|
// Event processor for onmousedown/ontouchstart
|
|
// e: Event
|
|
// onmousedown/ontouchend event
|
|
// tags:
|
|
// private
|
|
this.mouseDown = true;
|
|
this.mouseButton = e.button;
|
|
this._lastX = e.pageX;
|
|
this._lastY = e.pageY;
|
|
this.inherited(arguments);
|
|
},
|
|
|
|
onMouseUp: function(e){
|
|
// summary:
|
|
// Event processor for onmouseup/ontouchend
|
|
// e: Event
|
|
// onmouseup/ontouchend event
|
|
// tags:
|
|
// private
|
|
if(this.mouseDown){
|
|
this.mouseDown = false;
|
|
this.inherited(arguments);
|
|
}
|
|
},
|
|
|
|
onMouseOut: function(){
|
|
// summary:
|
|
// Event processor for when mouse is moved away from a TreeNode
|
|
// tags:
|
|
// private
|
|
this.inherited(arguments);
|
|
this._unmarkTargetAnchor();
|
|
},
|
|
|
|
checkItemAcceptance: function(/*===== target, source, position =====*/){
|
|
// summary:
|
|
// Stub function to be overridden if one wants to check for the ability to drop at the node/item level
|
|
// description:
|
|
// In the base case, this is called to check if target can become a child of source.
|
|
// When betweenThreshold is set, position="before" or "after" means that we
|
|
// are asking if the source node can be dropped before/after the target node.
|
|
// target: DOMNode
|
|
// The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to
|
|
// Use dijit.getEnclosingWidget(target) to get the TreeNode.
|
|
// source: dijit/tree/dndSource
|
|
// The (set of) nodes we are dropping
|
|
// position: String
|
|
// "over", "before", or "after"
|
|
// tags:
|
|
// extension
|
|
return true;
|
|
},
|
|
|
|
// topic event processors
|
|
onDndSourceOver: function(source){
|
|
// summary:
|
|
// Topic event processor for /dnd/source/over, called when detected a current source.
|
|
// source: Object
|
|
// The dijit/tree/dndSource / dojo/dnd/Source which has the mouse over it
|
|
// tags:
|
|
// private
|
|
if(this != source){
|
|
this.mouseDown = false;
|
|
this._unmarkTargetAnchor();
|
|
}else if(this.isDragging){
|
|
var m = DNDManager.manager();
|
|
m.canDrop(false);
|
|
}
|
|
},
|
|
onDndStart: function(source, nodes, copy){
|
|
// summary:
|
|
// Topic event processor for /dnd/start, called to initiate the DnD operation
|
|
// source: Object
|
|
// The dijit/tree/dndSource / dojo/dnd/Source which is providing the items
|
|
// nodes: DomNode[]
|
|
// The list of transferred items, dndTreeNode nodes if dragging from a Tree
|
|
// copy: Boolean
|
|
// Copy items, if true, move items otherwise
|
|
// tags:
|
|
// private
|
|
|
|
if(this.isSource){
|
|
this._changeState("Source", this == source ? (copy ? "Copied" : "Moved") : "");
|
|
}
|
|
var accepted = this.checkAcceptance(source, nodes);
|
|
|
|
this._changeState("Target", accepted ? "" : "Disabled");
|
|
|
|
if(this == source){
|
|
DNDManager.manager().overSource(this);
|
|
}
|
|
|
|
this.isDragging = true;
|
|
},
|
|
|
|
itemCreator: function(nodes /*===== , target, source =====*/){
|
|
// summary:
|
|
// Returns objects passed to `Tree.model.newItem()` based on DnD nodes
|
|
// dropped onto the tree. Developer must override this method to enable
|
|
// dropping from external sources onto this Tree, unless the Tree.model's items
|
|
// happen to look like {id: 123, name: "Apple" } with no other attributes.
|
|
// description:
|
|
// For each node in nodes[], which came from source, create a hash of name/value
|
|
// pairs to be passed to Tree.model.newItem(). Returns array of those hashes.
|
|
// nodes: DomNode[]
|
|
// target: DomNode
|
|
// source: dojo/dnd/Source
|
|
// returns: __Item[]
|
|
// Array of name/value hashes for each new item to be added to the Tree
|
|
// tags:
|
|
// extension
|
|
|
|
// TODO: for 2.0 refactor so itemCreator() is called once per drag node, and
|
|
// make signature itemCreator(sourceItem, node, target) (or similar).
|
|
|
|
return array.map(nodes, function(node){
|
|
return {
|
|
"id": node.id,
|
|
"name": node.textContent || node.innerText || ""
|
|
};
|
|
}); // Object[]
|
|
},
|
|
|
|
onDndDrop: function(source, nodes, copy){
|
|
// summary:
|
|
// Topic event processor for /dnd/drop, called to finish the DnD operation.
|
|
// description:
|
|
// Updates data store items according to where node was dragged from and dropped
|
|
// to. The tree will then respond to those data store updates and redraw itself.
|
|
// source: Object
|
|
// The dijit/tree/dndSource / dojo/dnd/Source which is providing the items
|
|
// nodes: DomNode[]
|
|
// The list of transferred items, dndTreeNode nodes if dragging from a Tree
|
|
// copy: Boolean
|
|
// Copy items, if true, move items otherwise
|
|
// tags:
|
|
// protected
|
|
if(this.containerState == "Over"){
|
|
var tree = this.tree,
|
|
model = tree.model,
|
|
target = this.targetAnchor;
|
|
|
|
this.isDragging = false;
|
|
|
|
// Compute the new parent item
|
|
var newParentItem;
|
|
var insertIndex;
|
|
var before; // drop source before (aka previous sibling) of target
|
|
newParentItem = (target && target.item) || tree.item;
|
|
if(this.dropPosition == "Before" || this.dropPosition == "After"){
|
|
// TODO: if there is no parent item then disallow the drop.
|
|
// Actually this should be checked during onMouseMove too, to make the drag icon red.
|
|
newParentItem = (target.getParent() && target.getParent().item) || tree.item;
|
|
// Compute the insert index for reordering
|
|
insertIndex = target.getIndexInParent();
|
|
if(this.dropPosition == "After"){
|
|
insertIndex = target.getIndexInParent() + 1;
|
|
before = target.getNextSibling() && target.getNextSibling().item;
|
|
}else{
|
|
before = target.item;
|
|
}
|
|
}else{
|
|
newParentItem = (target && target.item) || tree.item;
|
|
}
|
|
|
|
// If necessary, use this variable to hold array of hashes to pass to model.newItem()
|
|
// (one entry in the array for each dragged node).
|
|
var newItemsParams;
|
|
|
|
array.forEach(nodes, function(node, idx){
|
|
// dojo/dnd/Item representing the thing being dropped.
|
|
// Don't confuse the use of item here (meaning a DnD item) with the
|
|
// uses below where item means dojo.data item.
|
|
var sourceItem = source.getItem(node.id);
|
|
|
|
// Information that's available if the source is another Tree
|
|
// (possibly but not necessarily this tree, possibly but not
|
|
// necessarily the same model as this Tree)
|
|
if(array.indexOf(sourceItem.type, "treeNode") != -1){
|
|
var childTreeNode = sourceItem.data,
|
|
childItem = childTreeNode.item,
|
|
oldParentItem = childTreeNode.getParent().item;
|
|
}
|
|
|
|
if(source == this){
|
|
// This is a node from my own tree, and we are moving it, not copying.
|
|
// Remove item from old parent's children attribute.
|
|
// TODO: dijit/tree/dndSelector should implement deleteSelectedNodes()
|
|
// and this code should go there.
|
|
|
|
if(typeof insertIndex == "number"){
|
|
if(newParentItem == oldParentItem && childTreeNode.getIndexInParent() < insertIndex){
|
|
insertIndex -= 1;
|
|
}
|
|
}
|
|
model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex, before);
|
|
}else if(model.isItem(childItem)){
|
|
// Item from same model
|
|
// (maybe we should only do this branch if the source is a tree?)
|
|
model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex, before);
|
|
}else{
|
|
// Get the hash to pass to model.newItem(). A single call to
|
|
// itemCreator() returns an array of hashes, one for each drag source node.
|
|
if(!newItemsParams){
|
|
newItemsParams = this.itemCreator(nodes, target.rowNode, source);
|
|
}
|
|
|
|
// Create new item in the tree, based on the drag source.
|
|
model.newItem(newItemsParams[idx], newParentItem, insertIndex, before);
|
|
}
|
|
}, this);
|
|
|
|
// Expand the target node (if it's currently collapsed) so the user can see
|
|
// where their node was dropped. In particular since that node is still selected.
|
|
this.tree._expandNode(target);
|
|
}
|
|
this.onDndCancel();
|
|
},
|
|
|
|
onDndCancel: function(){
|
|
// summary:
|
|
// Topic event processor for /dnd/cancel, called to cancel the DnD operation
|
|
// tags:
|
|
// private
|
|
this._unmarkTargetAnchor();
|
|
this.isDragging = false;
|
|
this.mouseDown = false;
|
|
delete this.mouseButton;
|
|
this._changeState("Source", "");
|
|
this._changeState("Target", "");
|
|
},
|
|
|
|
// When focus moves in/out of the entire Tree
|
|
onOverEvent: function(){
|
|
// summary:
|
|
// This method is called when mouse is moved over our container (like onmouseenter)
|
|
// tags:
|
|
// private
|
|
this.inherited(arguments);
|
|
DNDManager.manager().overSource(this);
|
|
},
|
|
onOutEvent: function(){
|
|
// summary:
|
|
// This method is called when mouse is moved out of our container (like onmouseleave)
|
|
// tags:
|
|
// private
|
|
this._unmarkTargetAnchor();
|
|
var m = DNDManager.manager();
|
|
if(this.isDragging){
|
|
m.canDrop(false);
|
|
}
|
|
m.outSource(this);
|
|
|
|
this.inherited(arguments);
|
|
},
|
|
|
|
_isParentChildDrop: function(source, targetRow){
|
|
// summary:
|
|
// Checks whether the dragged items are parent rows in the tree which are being
|
|
// dragged into their own children.
|
|
//
|
|
// source:
|
|
// The DragSource object.
|
|
//
|
|
// targetRow:
|
|
// The tree row onto which the dragged nodes are being dropped.
|
|
//
|
|
// tags:
|
|
// private
|
|
|
|
// If the dragged object is not coming from the tree this widget belongs to,
|
|
// it cannot be invalid.
|
|
if(!source.tree || source.tree != this.tree){
|
|
return false;
|
|
}
|
|
|
|
|
|
var root = source.tree.domNode;
|
|
var ids = source.selection;
|
|
|
|
var node = targetRow.parentNode;
|
|
|
|
// Iterate up the DOM hierarchy from the target drop row,
|
|
// checking of any of the dragged nodes have the same ID.
|
|
while(node != root && !ids[node.id]){
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return node.id && ids[node.id];
|
|
},
|
|
|
|
_unmarkTargetAnchor: function(){
|
|
// summary:
|
|
// Removes hover class of the current target anchor
|
|
// tags:
|
|
// private
|
|
if(!this.targetAnchor){ return; }
|
|
this._removeItemClass(this.targetAnchor.rowNode, this.dropPosition);
|
|
this.targetAnchor = null;
|
|
this.targetBox = null;
|
|
this.dropPosition = null;
|
|
},
|
|
|
|
_markDndStatus: function(copy){
|
|
// summary:
|
|
// Changes source's state based on "copy" status
|
|
this._changeState("Source", copy ? "Copied" : "Moved");
|
|
}
|
|
});
|
|
|
|
/*=====
|
|
dndSource.__Item = __Item;
|
|
=====*/
|
|
|
|
return dndSource;
|
|
});
|