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