]> git.wh0rd.org - 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 });