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