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