source: Dev/trunk/src/client/dijit/tree/dndSource.js @ 483

Last change on this file since 483 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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