source: Dev/branches/rest-dojo-ui/client/dijit/tree/dndSource.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 17.7 KB
Line 
1define([
2        "dojo/_base/array", // array.forEach array.indexOf array.map
3        "dojo/_base/connect", // isCopyKey
4        "dojo/_base/declare", // declare
5        "dojo/dom-class", // domClass.add
6        "dojo/dom-geometry", // domGeometry.position
7        "dojo/_base/lang", // lang.mixin lang.hitch
8        "dojo/on", // subscribe
9        "dojo/touch",
10        "dojo/topic",
11        "dojo/dnd/Manager", // DNDManager.manager
12        "./_dndSelector"
13], function(array, connect, declare, domClass, domGeometry, lang, on, touch, topic, DNDManager, _dndSelector){
14
15// module:
16//              dijit/tree/dndSource
17// summary:
18//              Handles drag and drop operations (as a source or a target) for `dijit.Tree`
19
20/*=====
21dijit.tree.__SourceArgs = function(){
22        // summary:
23        //              A dict of parameters for Tree source configuration.
24        // isSource: Boolean?
25        //              Can be used as a DnD source. Defaults to true.
26        // accept: String[]
27        //              List of accepted types (text strings) for a target; defaults to
28        //              ["text", "treeNode"]
29        // copyOnly: Boolean?
30        //              Copy items, if true, use a state of Ctrl key otherwise,
31        // dragThreshold: Number
32        //              The move delay in pixels before detecting a drag; 0 by default
33        // betweenThreshold: Integer
34        //              Distance from upper/lower edge of node to allow drop to reorder nodes
35        this.isSource = isSource;
36        this.accept = accept;
37        this.autoSync = autoSync;
38        this.copyOnly = copyOnly;
39        this.dragThreshold = dragThreshold;
40        this.betweenThreshold = betweenThreshold;
41}
42=====*/
43
44return declare("dijit.tree.dndSource", _dndSelector, {
45        // summary:
46        //              Handles drag and drop operations (as a source or a target) for `dijit.Tree`
47
48        // isSource: [private] Boolean
49        //              Can be used as a DnD source.
50        isSource: true,
51
52        // accept: String[]
53        //              List of accepted types (text strings) for the Tree; defaults to
54        //              ["text"]
55        accept: ["text", "treeNode"],
56
57        // copyOnly: [private] Boolean
58        //              Copy items, if true, use a state of Ctrl key otherwise
59        copyOnly: false,
60
61        // dragThreshold: Number
62        //              The move delay in pixels before detecting a drag; 5 by default
63        dragThreshold: 5,
64
65        // betweenThreshold: Integer
66        //              Distance from upper/lower edge of node to allow drop to reorder nodes
67        betweenThreshold: 0,
68
69        constructor: function(/*dijit.Tree*/ tree, /*dijit.tree.__SourceArgs*/ params){
70                // summary:
71                //              a constructor of the Tree DnD Source
72                // tags:
73                //              private
74                if(!params){ params = {}; }
75                lang.mixin(this, params);
76                this.isSource = typeof params.isSource == "undefined" ? true : params.isSource;
77                var type = params.accept instanceof Array ? params.accept : ["text", "treeNode"];
78                this.accept = null;
79                if(type.length){
80                        this.accept = {};
81                        for(var i = 0; i < type.length; ++i){
82                                this.accept[type[i]] = 1;
83                        }
84                }
85
86                // class-specific variables
87                this.isDragging = false;
88                this.mouseDown = false;
89                this.targetAnchor = null;       // DOMNode corresponding to the currently moused over TreeNode
90                this.targetBox = null;  // coordinates of this.targetAnchor
91                this.dropPosition = ""; // whether mouse is over/after/before this.targetAnchor
92                this._lastX = 0;
93                this._lastY = 0;
94
95                // states
96                this.sourceState = "";
97                if(this.isSource){
98                        domClass.add(this.node, "dojoDndSource");
99                }
100                this.targetState = "";
101                if(this.accept){
102                        domClass.add(this.node, "dojoDndTarget");
103                }
104
105                // set up events
106                this.topics = [
107                        topic.subscribe("/dnd/source/over", lang.hitch(this, "onDndSourceOver")),
108                        topic.subscribe("/dnd/start", lang.hitch(this, "onDndStart")),
109                        topic.subscribe("/dnd/drop", lang.hitch(this, "onDndDrop")),
110                        topic.subscribe("/dnd/cancel", lang.hitch(this, "onDndCancel"))
111                ];
112        },
113
114        // methods
115        checkAcceptance: function(/*===== source, nodes =====*/){
116                // summary:
117                //              Checks if the target can accept nodes from this source
118                // source: dijit.tree.dndSource
119                //              The source which provides items
120                // nodes: DOMNode[]
121                //              Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if
122                //              source is a dijit.Tree.
123                // tags:
124                //              extension
125                return true;    // Boolean
126        },
127
128        copyState: function(keyPressed){
129                // summary:
130                //              Returns true, if we need to copy items, false to move.
131                //              It is separated to be overwritten dynamically, if needed.
132                // keyPressed: Boolean
133                //              The "copy" control key was pressed
134                // tags:
135                //              protected
136                return this.copyOnly || keyPressed;     // Boolean
137        },
138        destroy: function(){
139                // summary:
140                //              Prepares the object to be garbage-collected.
141                this.inherited(arguments);
142                var h;
143                while(h = this.topics.pop()){ h.remove(); }
144                this.targetAnchor = null;
145        },
146
147        _onDragMouse: function(e){
148                // summary:
149                //              Helper method for processing onmousemove/onmouseover events while drag is in progress.
150                //              Keeps track of current drop target.
151
152                var m = DNDManager.manager(),
153                        oldTarget = this.targetAnchor,                  // the TreeNode corresponding to TreeNode mouse was previously over
154                        newTarget = this.current,                               // TreeNode corresponding to TreeNode mouse is currently over
155                        oldDropPosition = this.dropPosition;    // the previous drop position (over/before/after)
156
157                // calculate if user is indicating to drop the dragged node before, after, or over
158                // (i.e., to become a child of) the target node
159                var newDropPosition = "Over";
160                if(newTarget && this.betweenThreshold > 0){
161                        // If mouse is over a new TreeNode, then get new TreeNode's position and size
162                        if(!this.targetBox || oldTarget != newTarget){
163                                this.targetBox = domGeometry.position(newTarget.rowNode, true);
164                        }
165                        if((e.pageY - this.targetBox.y) <= this.betweenThreshold){
166                                newDropPosition = "Before";
167                        }else if((e.pageY - this.targetBox.y) >= (this.targetBox.h - this.betweenThreshold)){
168                                newDropPosition = "After";
169                        }
170                }
171
172                if(newTarget != oldTarget || newDropPosition != oldDropPosition){
173                        if(oldTarget){
174                                this._removeItemClass(oldTarget.rowNode, oldDropPosition);
175                        }
176                        if(newTarget){
177                                this._addItemClass(newTarget.rowNode, newDropPosition);
178                        }
179
180                        // Check if it's ok to drop the dragged node on/before/after the target node.
181                        if(!newTarget){
182                                m.canDrop(false);
183                        }else if(newTarget == this.tree.rootNode && newDropPosition != "Over"){
184                                // Can't drop before or after tree's root node; the dropped node would just disappear (at least visually)
185                                m.canDrop(false);
186                        }else{
187                                // Guard against dropping onto yourself (TODO: guard against dropping onto your descendant, #7140)
188                                var model = this.tree.model,
189                                        sameId = false;
190                                if(m.source == this){
191                                        for(var dragId in this.selection){
192                                                var dragNode = this.selection[dragId];
193                                                if(dragNode.item === newTarget.item){
194                                                        sameId = true;
195                                                        break;
196                                                }
197                                        }
198                                }
199                                if(sameId){
200                                        m.canDrop(false);
201                                }else if(this.checkItemAcceptance(newTarget.rowNode, m.source, newDropPosition.toLowerCase())
202                                                && !this._isParentChildDrop(m.source, newTarget.rowNode)){
203                                        m.canDrop(true);
204                                }else{
205                                        m.canDrop(false);
206                                }
207                        }
208
209                        this.targetAnchor = newTarget;
210                        this.dropPosition = newDropPosition;
211                }
212        },
213
214        onMouseMove: function(e){
215                // summary:
216                //              Called for any onmousemove/ontouchmove events over the Tree
217                // e: Event
218                //              onmousemouse/ontouchmove event
219                // tags:
220                //              private
221                if(this.isDragging && this.targetState == "Disabled"){ return; }
222                this.inherited(arguments);
223                var m = DNDManager.manager();
224                if(this.isDragging){
225                        this._onDragMouse(e);
226                }else{
227                        if(this.mouseDown && this.isSource &&
228                                 (Math.abs(e.pageX-this._lastX)>=this.dragThreshold || Math.abs(e.pageY-this._lastY)>=this.dragThreshold)){
229                                var nodes = this.getSelectedTreeNodes();
230                                if(nodes.length){
231                                        if(nodes.length > 1){
232                                                //filter out all selected items which has one of their ancestor selected as well
233                                                var seen = this.selection, i = 0, r = [], n, p;
234                                                nextitem: while((n = nodes[i++])){
235                                                        for(p = n.getParent(); p && p !== this.tree; p = p.getParent()){
236                                                                if(seen[p.id]){ //parent is already selected, skip this node
237                                                                        continue nextitem;
238                                                                }
239                                                        }
240                                                        //this node does not have any ancestors selected, add it
241                                                        r.push(n);
242                                                }
243                                                nodes = r;
244                                        }
245                                        nodes = array.map(nodes, function(n){return n.domNode});
246                                        m.startDrag(this, nodes, this.copyState(connect.isCopyKey(e)));
247                                }
248                        }
249                }
250        },
251
252        onMouseDown: function(e){
253                // summary:
254                //              Event processor for onmousedown/ontouchstart
255                // e: Event
256                //              onmousedown/ontouchend event
257                // tags:
258                //              private
259                this.mouseDown = true;
260                this.mouseButton = e.button;
261                this._lastX = e.pageX;
262                this._lastY = e.pageY;
263                this.inherited(arguments);
264        },
265
266        onMouseUp: function(e){
267                // summary:
268                //              Event processor for onmouseup/ontouchend
269                // e: Event
270                //              onmouseup/ontouchend event
271                // tags:
272                //              private
273                if(this.mouseDown){
274                        this.mouseDown = false;
275                        this.inherited(arguments);
276                }
277        },
278
279        onMouseOut: function(){
280                // summary:
281                //              Event processor for when mouse is moved away from a TreeNode
282                // tags:
283                //              private
284                this.inherited(arguments);
285                this._unmarkTargetAnchor();
286        },
287
288        checkItemAcceptance: function(/*===== target, source, position =====*/){
289                // summary:
290                //              Stub function to be overridden if one wants to check for the ability to drop at the node/item level
291                // description:
292                //              In the base case, this is called to check if target can become a child of source.
293                //              When betweenThreshold is set, position="before" or "after" means that we
294                //              are asking if the source node can be dropped before/after the target node.
295                // target: DOMNode
296                //              The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to
297                //              Use dijit.getEnclosingWidget(target) to get the TreeNode.
298                // source: dijit.tree.dndSource
299                //              The (set of) nodes we are dropping
300                // position: String
301                //              "over", "before", or "after"
302                // tags:
303                //              extension
304                return true;
305        },
306
307        // topic event processors
308        onDndSourceOver: function(source){
309                // summary:
310                //              Topic event processor for /dnd/source/over, called when detected a current source.
311                // source: Object
312                //              The dijit.tree.dndSource / dojo.dnd.Source which has the mouse over it
313                // tags:
314                //              private
315                if(this != source){
316                        this.mouseDown = false;
317                        this._unmarkTargetAnchor();
318                }else if(this.isDragging){
319                        var m = DNDManager.manager();
320                        m.canDrop(false);
321                }
322        },
323        onDndStart: function(source, nodes, copy){
324                // summary:
325                //              Topic event processor for /dnd/start, called to initiate the DnD operation
326                // source: Object
327                //              The dijit.tree.dndSource / dojo.dnd.Source which is providing the items
328                // nodes: DomNode[]
329                //              The list of transferred items, dndTreeNode nodes if dragging from a Tree
330                // copy: Boolean
331                //              Copy items, if true, move items otherwise
332                // tags:
333                //              private
334
335                if(this.isSource){
336                        this._changeState("Source", this == source ? (copy ? "Copied" : "Moved") : "");
337                }
338                var accepted = this.checkAcceptance(source, nodes);
339
340                this._changeState("Target", accepted ? "" : "Disabled");
341
342                if(this == source){
343                        DNDManager.manager().overSource(this);
344                }
345
346                this.isDragging = true;
347        },
348
349        itemCreator: function(nodes /*===== , target, source =====*/){
350                // summary:
351                //              Returns objects passed to `Tree.model.newItem()` based on DnD nodes
352                //              dropped onto the tree.   Developer must override this method to enable
353                //              dropping from external sources onto this Tree, unless the Tree.model's items
354                //              happen to look like {id: 123, name: "Apple" } with no other attributes.
355                // description:
356                //              For each node in nodes[], which came from source, create a hash of name/value
357                //              pairs to be passed to Tree.model.newItem().  Returns array of those hashes.
358                // nodes: DomNode[]
359                // target: DomNode
360                // source: dojo.dnd.Source
361                // returns: Object[]
362                //              Array of name/value hashes for each new item to be added to the Tree, like:
363                // |    [
364                // |            { id: 123, label: "apple", foo: "bar" },
365                // |            { id: 456, label: "pear", zaz: "bam" }
366                // |    ]
367                // tags:
368                //              extension
369
370                // TODO: for 2.0 refactor so itemCreator() is called once per drag node, and
371                // make signature itemCreator(sourceItem, node, target) (or similar).
372
373                return array.map(nodes, function(node){
374                        return {
375                                "id": node.id,
376                                "name": node.textContent || node.innerText || ""
377                        };
378                }); // Object[]
379        },
380
381        onDndDrop: function(source, nodes, copy){
382                // summary:
383                //              Topic event processor for /dnd/drop, called to finish the DnD operation.
384                // description:
385                //              Updates data store items according to where node was dragged from and dropped
386                //              to.   The tree will then respond to those data store updates and redraw itself.
387                // source: Object
388                //              The dijit.tree.dndSource / dojo.dnd.Source which is providing the items
389                // nodes: DomNode[]
390                //              The list of transferred items, dndTreeNode nodes if dragging from a Tree
391                // copy: Boolean
392                //              Copy items, if true, move items otherwise
393                // tags:
394                //              protected
395                if(this.containerState == "Over"){
396                        var tree = this.tree,
397                                model = tree.model,
398                                target = this.targetAnchor;
399
400                        this.isDragging = false;
401
402                        // Compute the new parent item
403                        var newParentItem;
404                        var insertIndex;
405                        newParentItem = (target && target.item) || tree.item;
406                        if(this.dropPosition == "Before" || this.dropPosition == "After"){
407                                // TODO: if there is no parent item then disallow the drop.
408                                // Actually this should be checked during onMouseMove too, to make the drag icon red.
409                                newParentItem = (target.getParent() && target.getParent().item) || tree.item;
410                                // Compute the insert index for reordering
411                                insertIndex = target.getIndexInParent();
412                                if(this.dropPosition == "After"){
413                                        insertIndex = target.getIndexInParent() + 1;
414                                }
415                        }else{
416                                newParentItem = (target && target.item) || tree.item;
417                        }
418
419                        // If necessary, use this variable to hold array of hashes to pass to model.newItem()
420                        // (one entry in the array for each dragged node).
421                        var newItemsParams;
422
423                        array.forEach(nodes, function(node, idx){
424                                // dojo.dnd.Item representing the thing being dropped.
425                                // Don't confuse the use of item here (meaning a DnD item) with the
426                                // uses below where item means dojo.data item.
427                                var sourceItem = source.getItem(node.id);
428
429                                // Information that's available if the source is another Tree
430                                // (possibly but not necessarily this tree, possibly but not
431                                // necessarily the same model as this Tree)
432                                if(array.indexOf(sourceItem.type, "treeNode") != -1){
433                                        var childTreeNode = sourceItem.data,
434                                                childItem = childTreeNode.item,
435                                                oldParentItem = childTreeNode.getParent().item;
436                                }
437
438                                if(source == this){
439                                        // This is a node from my own tree, and we are moving it, not copying.
440                                        // Remove item from old parent's children attribute.
441                                        // TODO: dijit.tree.dndSelector should implement deleteSelectedNodes()
442                                        // and this code should go there.
443
444                                        if(typeof insertIndex == "number"){
445                                                if(newParentItem == oldParentItem && childTreeNode.getIndexInParent() < insertIndex){
446                                                        insertIndex -= 1;
447                                                }
448                                        }
449                                        model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex);
450                                }else if(model.isItem(childItem)){
451                                        // Item from same model
452                                        // (maybe we should only do this branch if the source is a tree?)
453                                        model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex);
454                                }else{
455                                        // Get the hash to pass to model.newItem().  A single call to
456                                        // itemCreator() returns an array of hashes, one for each drag source node.
457                                        if(!newItemsParams){
458                                                newItemsParams = this.itemCreator(nodes, target.rowNode, source);
459                                        }
460
461                                        // Create new item in the tree, based on the drag source.
462                                        model.newItem(newItemsParams[idx], newParentItem, insertIndex);
463                                }
464                        }, this);
465
466                        // Expand the target node (if it's currently collapsed) so the user can see
467                        // where their node was dropped.   In particular since that node is still selected.
468                        this.tree._expandNode(target);
469                }
470                this.onDndCancel();
471        },
472
473        onDndCancel: function(){
474                // summary:
475                //              Topic event processor for /dnd/cancel, called to cancel the DnD operation
476                // tags:
477                //              private
478                this._unmarkTargetAnchor();
479                this.isDragging = false;
480                this.mouseDown = false;
481                delete this.mouseButton;
482                this._changeState("Source", "");
483                this._changeState("Target", "");
484        },
485
486        // When focus moves in/out of the entire Tree
487        onOverEvent: function(){
488                // summary:
489                //              This method is called when mouse is moved over our container (like onmouseenter)
490                // tags:
491                //              private
492                this.inherited(arguments);
493                DNDManager.manager().overSource(this);
494        },
495        onOutEvent: function(){
496                // summary:
497                //              This method is called when mouse is moved out of our container (like onmouseleave)
498                // tags:
499                //              private
500                this._unmarkTargetAnchor();
501                var m = DNDManager.manager();
502                if(this.isDragging){
503                        m.canDrop(false);
504                }
505                m.outSource(this);
506
507                this.inherited(arguments);
508        },
509
510        _isParentChildDrop: function(source, targetRow){
511                // summary:
512                //              Checks whether the dragged items are parent rows in the tree which are being
513                //              dragged into their own children.
514                //
515                // source:
516                //              The DragSource object.
517                //
518                // targetRow:
519                //              The tree row onto which the dragged nodes are being dropped.
520                //
521                // tags:
522                //              private
523
524                // If the dragged object is not coming from the tree this widget belongs to,
525                // it cannot be invalid.
526                if(!source.tree || source.tree != this.tree){
527                        return false;
528                }
529
530
531                var root = source.tree.domNode;
532                var ids = source.selection;
533
534                var node = targetRow.parentNode;
535
536                // Iterate up the DOM hierarchy from the target drop row,
537                // checking of any of the dragged nodes have the same ID.
538                while(node != root && !ids[node.id]){
539                        node = node.parentNode;
540                }
541
542                return node.id && ids[node.id];
543        },
544
545        _unmarkTargetAnchor: function(){
546                // summary:
547                //              Removes hover class of the current target anchor
548                // tags:
549                //              private
550                if(!this.targetAnchor){ return; }
551                this._removeItemClass(this.targetAnchor.rowNode, this.dropPosition);
552                this.targetAnchor = null;
553                this.targetBox = null;
554                this.dropPosition = null;
555        },
556
557        _markDndStatus: function(copy){
558                // summary:
559                //              Changes source's state based on "copy" status
560                this._changeState("Source", copy ? "Copied" : "Moved");
561        }
562});
563
564});
Note: See TracBrowser for help on using the repository browser.