make sure the README will appear on github
[extjs.git] / examples / multiselect / DDView.js
1 /*\r
2  * Ext JS Library 2.2.1\r
3  * Copyright(c) 2006-2009, Ext JS, LLC.\r
4  * licensing@extjs.com\r
5  * \r
6  * http://extjs.com/license\r
7  */\r
8 \r
9 /*\r
10  * Software License Agreement (BSD License)\r
11  * Copyright (c) 2008, Nige "Animal" White\r
12  * All rights reserved.\r
13  *\r
14  * Redistribution and use in source and binary forms, with or without modification,\r
15  * are permitted provided that the following conditions are met:\r
16  *\r
17  *     * Redistributions of source code must retain the above copyright notice,\r
18  *       this list of conditions and the following disclaimer.\r
19  *     * Redistributions in binary form must reproduce the above copyright notice,\r
20  *       this list of conditions and the following disclaimer in the documentation\r
21  *       and/or other materials provided with the distribution.\r
22  *     * Neither the name of the original author nor the names of its contributors\r
23  *       may be used to endorse or promote products derived from this software\r
24  *       without specific prior written permission.\r
25  *\r
26  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND\r
27  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\r
28  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\r
29  * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,\r
30  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\r
31  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\r
32  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\r
33  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\r
34  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
35  * POSSIBILITY OF SUCH DAMAGE.\r
36  */\r
37 /**\r
38  * @class Ext.ux.DDView\r
39  * <p>A DnD-enabled version of {@link Ext.DataView}. Drag/drop is implemented by adding\r
40  * {@link Ext.data.Record}s to the target DDView. If copying is not being performed,\r
41  * the original {@link Ext.data.Record} is removed from the source DDView.</p>\r
42  * @constructor\r
43  * Create a new DDView\r
44  * @param {Object} config The configuration properties.\r
45  */\r
46 Ext.ux.DDView = function(config) {\r
47     if (!config.itemSelector) {\r
48         var tpl = config.tpl;\r
49         if (this.classRe.test(tpl)) {\r
50             config.tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1');\r
51         }\r
52         else {\r
53             config.tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item" $2');\r
54         }\r
55         config.itemSelector = ".x-combo-list-item";\r
56     }\r
57     Ext.ux.DDView.superclass.constructor.call(this, Ext.apply(config, {\r
58         border: false\r
59     }));\r
60 };\r
61 \r
62 Ext.extend(Ext.ux.DDView, Ext.DataView, {\r
63     /**\r
64      * @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone (defaults to undefined).\r
65      */\r
66     /**\r
67      * @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone (defaults to undefined).\r
68      */\r
69     /**\r
70      * @cfg {Boolean} copy Causes drag operations to copy nodes rather than move (defaults to false).\r
71      */\r
72     /**\r
73      * @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move (defaults to false).\r
74      */\r
75     /**\r
76      * @cfg {String} sortDir Sort direction for the view, 'ASC' or 'DESC' (defaults to 'ASC').\r
77      */\r
78     sortDir: 'ASC',\r
79 \r
80     // private\r
81     isFormField: true,\r
82     classRe: /class=(['"])(.*)\1/,\r
83     tagRe: /(<\w*)(.*?>)/,\r
84     reset: Ext.emptyFn,\r
85     clearInvalid: Ext.form.Field.prototype.clearInvalid,\r
86 \r
87     // private\r
88     afterRender: function() {\r
89         Ext.ux.DDView.superclass.afterRender.call(this);\r
90         if (this.dragGroup) {\r
91             this.setDraggable(this.dragGroup.split(","));\r
92         }\r
93         if (this.dropGroup) {\r
94             this.setDroppable(this.dropGroup.split(","));\r
95         }\r
96         if (this.deletable) {\r
97             this.setDeletable();\r
98         }\r
99         this.isDirtyFlag = false;\r
100         this.addEvents(\r
101             "drop"\r
102         );\r
103     },\r
104 \r
105     // private\r
106     validate: function() {\r
107         return true;\r
108     },\r
109 \r
110     // private\r
111     destroy: function() {\r
112         this.purgeListeners();\r
113         this.getEl().removeAllListeners();\r
114         this.getEl().remove();\r
115         if (this.dragZone) {\r
116             if (this.dragZone.destroy) {\r
117                 this.dragZone.destroy();\r
118             }\r
119         }\r
120         if (this.dropZone) {\r
121             if (this.dropZone.destroy) {\r
122                 this.dropZone.destroy();\r
123             }\r
124         }\r
125     },\r
126 \r
127         /**\r
128          * Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}.\r
129          */\r
130     getName: function() {\r
131         return this.name;\r
132     },\r
133 \r
134         /**\r
135          * Loads the View from a JSON string representing the Records to put into the Store.\r
136      * @param {String} value The JSON string\r
137          */\r
138     setValue: function(v) {\r
139         if (!this.store) {\r
140             throw "DDView.setValue(). DDView must be constructed with a valid Store";\r
141         }\r
142         var data = {};\r
143         data[this.store.reader.meta.root] = v ? [].concat(v) : [];\r
144         this.store.proxy = new Ext.data.MemoryProxy(data);\r
145         this.store.load();\r
146     },\r
147 \r
148         /**\r
149          * Returns the view's data value as a list of ids.\r
150      * @return {String} A parenthesised list of the ids of the Records in the View, e.g. (1,3,8).\r
151          */\r
152     getValue: function() {\r
153         var result = '(';\r
154         this.store.each(function(rec) {\r
155             result += rec.id + ',';\r
156         });\r
157         return result.substr(0, result.length - 1) + ')';\r
158     },\r
159 \r
160     getIds: function() {\r
161         var i = 0, result = new Array(this.store.getCount());\r
162         this.store.each(function(rec) {\r
163             result[i++] = rec.id;\r
164         });\r
165         return result;\r
166     },\r
167 \r
168     /**\r
169      * Returns true if the view's data has changed, else false.\r
170      * @return {Boolean}\r
171      */\r
172     isDirty: function() {\r
173         return this.isDirtyFlag;\r
174     },\r
175 \r
176         /**\r
177          * Part of the Ext.dd.DropZone interface. If no target node is found, the\r
178          * whole Element becomes the target, and this causes the drop gesture to append.\r
179          */\r
180     getTargetFromEvent : function(e) {\r
181         var target = e.getTarget();\r
182         while ((target !== null) && (target.parentNode != this.el.dom)) {\r
183             target = target.parentNode;\r
184         }\r
185         if (!target) {\r
186             target = this.el.dom.lastChild || this.el.dom;\r
187         }\r
188         return target;\r
189     },\r
190 \r
191         /**\r
192          * Create the drag data which consists of an object which has the property "ddel" as\r
193          * the drag proxy element.\r
194          */\r
195     getDragData : function(e) {\r
196         var target = this.findItemFromChild(e.getTarget());\r
197         if(target) {\r
198             if (!this.isSelected(target)) {\r
199                 delete this.ignoreNextClick;\r
200                 this.onItemClick(target, this.indexOf(target), e);\r
201                 this.ignoreNextClick = true;\r
202             }\r
203             var dragData = {\r
204                 sourceView: this,\r
205                 viewNodes: [],\r
206                 records: [],\r
207                 copy: this.copy || (this.allowCopy && e.ctrlKey)\r
208             };\r
209             if (this.getSelectionCount() == 1) {\r
210                 var i = this.getSelectedIndexes()[0];\r
211                 var n = this.getNode(i);\r
212                 dragData.viewNodes.push(dragData.ddel = n);\r
213                 dragData.records.push(this.store.getAt(i));\r
214                 dragData.repairXY = Ext.fly(n).getXY();\r
215             } else {\r
216                 dragData.ddel = document.createElement('div');\r
217                 dragData.ddel.className = 'multi-proxy';\r
218                 this.collectSelection(dragData);\r
219             }\r
220             return dragData;\r
221         }\r
222         return false;\r
223     },\r
224 \r
225     // override the default repairXY.\r
226     getRepairXY : function(e){\r
227         return this.dragData.repairXY;\r
228     },\r
229 \r
230         // private\r
231     collectSelection: function(data) {\r
232         data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY();\r
233         if (this.preserveSelectionOrder === true) {\r
234             Ext.each(this.getSelectedIndexes(), function(i) {\r
235                 var n = this.getNode(i);\r
236                 var dragNode = n.cloneNode(true);\r
237                 dragNode.id = Ext.id();\r
238                 data.ddel.appendChild(dragNode);\r
239                 data.records.push(this.store.getAt(i));\r
240                 data.viewNodes.push(n);\r
241             }, this);\r
242         } else {\r
243             var i = 0;\r
244             this.store.each(function(rec){\r
245                 if (this.isSelected(i)) {\r
246                     var n = this.getNode(i);\r
247                     var dragNode = n.cloneNode(true);\r
248                     dragNode.id = Ext.id();\r
249                     data.ddel.appendChild(dragNode);\r
250                     data.records.push(this.store.getAt(i));\r
251                     data.viewNodes.push(n);\r
252                 }\r
253                 i++;\r
254             }, this);\r
255         }\r
256     },\r
257 \r
258         /**\r
259          * Specify to which ddGroup items in this DDView may be dragged.\r
260      * @param {String} ddGroup The DD group name to assign this view to.\r
261          */\r
262     setDraggable: function(ddGroup) {\r
263         if (ddGroup instanceof Array) {\r
264             Ext.each(ddGroup, this.setDraggable, this);\r
265             return;\r
266         }\r
267         if (this.dragZone) {\r
268             this.dragZone.addToGroup(ddGroup);\r
269         } else {\r
270             this.dragZone = new Ext.dd.DragZone(this.getEl(), {\r
271                 containerScroll: true,\r
272                 ddGroup: ddGroup\r
273             });\r
274             // Draggability implies selection. DragZone's mousedown selects the element.\r
275             if (!this.multiSelect) { this.singleSelect = true; }\r
276 \r
277             // Wire the DragZone's handlers up to methods in *this*\r
278             this.dragZone.getDragData = this.getDragData.createDelegate(this);\r
279             this.dragZone.getRepairXY = this.getRepairXY;\r
280             this.dragZone.onEndDrag = this.onEndDrag;\r
281         }\r
282     },\r
283 \r
284         /**\r
285          * Specify from which ddGroup this DDView accepts drops.\r
286      * @param {String} ddGroup The DD group name from which to accept drops.\r
287          */\r
288     setDroppable: function(ddGroup) {\r
289         if (ddGroup instanceof Array) {\r
290             Ext.each(ddGroup, this.setDroppable, this);\r
291             return;\r
292         }\r
293         if (this.dropZone) {\r
294             this.dropZone.addToGroup(ddGroup);\r
295         } else {\r
296             this.dropZone = new Ext.dd.DropZone(this.getEl(), {\r
297                 owningView: this,\r
298                 containerScroll: true,\r
299                 ddGroup: ddGroup\r
300             });\r
301 \r
302             // Wire the DropZone's handlers up to methods in *this*\r
303             this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);\r
304             this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);\r
305             this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);\r
306             this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);\r
307             this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);\r
308         }\r
309     },\r
310 \r
311         // private\r
312     getDropPoint : function(e, n, dd){\r
313         if (n == this.el.dom) { return "above"; }\r
314         var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight;\r
315         var c = t + (b - t) / 2;\r
316         var y = Ext.lib.Event.getPageY(e);\r
317         if(y <= c) {\r
318             return "above";\r
319         }else{\r
320             return "below";\r
321         }\r
322     },\r
323 \r
324     // private\r
325     isValidDropPoint: function(pt, n, data) {\r
326         if (!data.viewNodes || (data.viewNodes.length != 1)) {\r
327             return true;\r
328         }\r
329         var d = data.viewNodes[0];\r
330         if (d == n) {\r
331             return false;\r
332         }\r
333         if ((pt == "below") && (n.nextSibling == d)) {\r
334             return false;\r
335         }\r
336         if ((pt == "above") && (n.previousSibling == d)) {\r
337             return false;\r
338         }\r
339         return true;\r
340     },\r
341 \r
342     // private\r
343     onNodeEnter : function(n, dd, e, data){\r
344         if (this.highlightColor && (data.sourceView != this)) {\r
345             this.el.highlight(this.highlightColor);\r
346         }\r
347         return false;\r
348     },\r
349 \r
350     // private\r
351     onNodeOver : function(n, dd, e, data){\r
352         var dragElClass = this.dropNotAllowed;\r
353         var pt = this.getDropPoint(e, n, dd);\r
354         if (this.isValidDropPoint(pt, n, data)) {\r
355             if (this.appendOnly || this.sortField) {\r
356                 return "x-tree-drop-ok-below";\r
357             }\r
358 \r
359             // set the insert point style on the target node\r
360             if (pt) {\r
361                 var targetElClass;\r
362                 if (pt == "above"){\r
363                     dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";\r
364                     targetElClass = "x-view-drag-insert-above";\r
365                 } else {\r
366                     dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";\r
367                     targetElClass = "x-view-drag-insert-below";\r
368                 }\r
369                 if (this.lastInsertClass != targetElClass){\r
370                     Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass);\r
371                     this.lastInsertClass = targetElClass;\r
372                 }\r
373             }\r
374         }\r
375         return dragElClass;\r
376     },\r
377 \r
378     // private\r
379     onNodeOut : function(n, dd, e, data){\r
380         this.removeDropIndicators(n);\r
381     },\r
382 \r
383     // private\r
384     onNodeDrop : function(n, dd, e, data){\r
385         if (this.fireEvent("drop", this, n, dd, e, data) === false) {\r
386             return false;\r
387         }\r
388         var pt = this.getDropPoint(e, n, dd);\r
389         var insertAt = (this.appendOnly || (n == this.el.dom)) ? this.store.getCount() : n.viewIndex;\r
390         if (pt == "below") {\r
391             insertAt++;\r
392         }\r
393 \r
394         // Validate if dragging within a DDView\r
395         if (data.sourceView == this) {\r
396             // If the first element to be inserted below is the target node, remove it\r
397             if (pt == "below") {\r
398                 if (data.viewNodes[0] == n) {\r
399                     data.viewNodes.shift();\r
400                 }\r
401             } else {  // If the last element to be inserted above is the target node, remove it\r
402                 if (data.viewNodes[data.viewNodes.length - 1] == n) {\r
403                     data.viewNodes.pop();\r
404                 }\r
405             }\r
406 \r
407             // Nothing to drop...\r
408             if (!data.viewNodes.length) {\r
409                 return false;\r
410             }\r
411 \r
412             // If we are moving DOWN, then because a store.remove() takes place first,\r
413             // the insertAt must be decremented.\r
414             if (insertAt > this.store.indexOf(data.records[0])) {\r
415                 insertAt--;\r
416             }\r
417         }\r
418 \r
419         // Dragging from a Tree. Use the Tree's recordFromNode function.\r
420         if (data.node instanceof Ext.tree.TreeNode) {\r
421             var r = data.node.getOwnerTree().recordFromNode(data.node);\r
422             if (r) {\r
423                 data.records = [ r ];\r
424             }\r
425         }\r
426 \r
427         if (!data.records) {\r
428             alert("Programming problem. Drag data contained no Records");\r
429             return false;\r
430         }\r
431 \r
432         for (var i = 0; i < data.records.length; i++) {\r
433             var r = data.records[i];\r
434             var dup = this.store.getById(r.id);\r
435             if (dup && (dd != this.dragZone)) {\r
436                 if(!this.allowDup && !this.allowTrash){\r
437                     Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);\r
438                     return true\r
439                 }\r
440                 var x=new Ext.data.Record();\r
441                 r.id=x.id;\r
442                 delete x;\r
443             }\r
444             if (data.copy) {\r
445                 this.store.insert(insertAt++, r.copy());\r
446             } else {\r
447                 if (data.sourceView) {\r
448                     data.sourceView.isDirtyFlag = true;\r
449                     data.sourceView.store.remove(r);\r
450                 }\r
451                 if(!this.allowTrash)this.store.insert(insertAt++, r);\r
452             }\r
453             if(this.sortField){\r
454                 this.store.sort(this.sortField, this.sortDir);\r
455             }\r
456             this.isDirtyFlag = true;\r
457         }\r
458         this.dragZone.cachedTarget = null;\r
459         return true;\r
460     },\r
461 \r
462     // private\r
463     onEndDrag: function(data, e) {\r
464         var d = Ext.get(this.dragData.ddel);\r
465         if (d && d.hasClass("multi-proxy")) {\r
466             d.remove();\r
467             //delete this.dragData.ddel;\r
468         }\r
469     },\r
470 \r
471     // private\r
472     removeDropIndicators : function(n){\r
473         if(n){\r
474             Ext.fly(n).removeClass([\r
475                 "x-view-drag-insert-above",\r
476                 "x-view-drag-insert-left",\r
477                 "x-view-drag-insert-right",\r
478                 "x-view-drag-insert-below"]);\r
479             this.lastInsertClass = "_noclass";\r
480         }\r
481     },\r
482 \r
483         /**\r
484          * Add a delete option to the DDView's context menu.\r
485          * @param {String} imageUrl The URL of the "delete" icon image.\r
486          */\r
487     setDeletable: function(imageUrl) {\r
488         if (!this.singleSelect && !this.multiSelect) {\r
489             this.singleSelect = true;\r
490         }\r
491         var c = this.getContextMenu();\r
492         this.contextMenu.on("itemclick", function(item) {\r
493             switch (item.id) {\r
494                 case "delete":\r
495                     this.remove(this.getSelectedIndexes());\r
496                     break;\r
497             }\r
498         }, this);\r
499         this.contextMenu.add({\r
500             icon: imageUrl || AU.resolveUrl("/images/delete.gif"),\r
501             id: "delete",\r
502             text: AU.getMessage("deleteItem")\r
503         });\r
504     },\r
505 \r
506         /**\r
507          * Return the context menu for this DDView.\r
508      * @return {Ext.menu.Menu} The context menu\r
509          */\r
510     getContextMenu: function() {\r
511         if (!this.contextMenu) {\r
512             // Create the View's context menu\r
513             this.contextMenu = new Ext.menu.Menu({\r
514                 id: this.id + "-contextmenu"\r
515             });\r
516             this.el.on("contextmenu", this.showContextMenu, this);\r
517         }\r
518         return this.contextMenu;\r
519     },\r
520 \r
521     /**\r
522      * Disables the view's context menu.\r
523      */\r
524     disableContextMenu: function() {\r
525         if (this.contextMenu) {\r
526             this.el.un("contextmenu", this.showContextMenu, this);\r
527         }\r
528     },\r
529 \r
530     // private\r
531     showContextMenu: function(e, item) {\r
532         item = this.findItemFromChild(e.getTarget());\r
533         if (item) {\r
534             e.stopEvent();\r
535             this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);\r
536             this.contextMenu.showAt(e.getXY());\r
537         }\r
538     },\r
539 \r
540         /**\r
541          * Remove {@link Ext.data.Record}s at the specified indices.\r
542          * @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.\r
543          */\r
544     remove: function(selectedIndices) {\r
545         selectedIndices = [].concat(selectedIndices);\r
546         for (var i = 0; i < selectedIndices.length; i++) {\r
547             var rec = this.store.getAt(selectedIndices[i]);\r
548             this.store.remove(rec);\r
549         }\r
550     },\r
551 \r
552         /**\r
553          * Double click fires the {@link #dblclick} event. Additionally, if this DDView is draggable, and there is only one other\r
554          * related DropZone that is in another DDView, it drops the selected node on that DDView.\r
555          */\r
556     onDblClick : function(e){\r
557         var item = this.findItemFromChild(e.getTarget());\r
558         if(item){\r
559             if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {\r
560                 return false;\r
561             }\r
562             if (this.dragGroup) {\r
563                 var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true);\r
564 \r
565                 // Remove instances of this View's DropZone\r
566                 while (targets.indexOf(this.dropZone) !== -1) {\r
567                     targets.remove(this.dropZone);\r
568                 }\r
569 \r
570                 // If there's only one other DropZone, and it is owned by a DDView, then drop it in\r
571                 if ((targets.length == 1) && (targets[0].owningView)) {\r
572                     this.dragZone.cachedTarget = null;\r
573                     var el = Ext.get(targets[0].getEl());\r
574                     var box = el.getBox(true);\r
575                     targets[0].onNodeDrop(el.dom, {\r
576                         target: el.dom,\r
577                         xy: [box.x, box.y + box.height - 1]\r
578                     }, null, this.getDragData(e));\r
579                 }\r
580             }\r
581         }\r
582     },\r
583 \r
584     // private\r
585     onItemClick : function(item, index, e){\r
586         // The DragZone's mousedown->getDragData already handled selection\r
587         if (this.ignoreNextClick) {\r
588             delete this.ignoreNextClick;\r
589             return;\r
590         }\r
591 \r
592         if(this.fireEvent("beforeclick", this, index, item, e) === false){\r
593             return false;\r
594         }\r
595         if(this.multiSelect || this.singleSelect){\r
596             if(this.multiSelect && e.shiftKey && this.lastSelection){\r
597                 this.select(this.getNodes(this.indexOf(this.lastSelection), index), false);\r
598             } else if (this.isSelected(item) && e.ctrlKey) {\r
599                 this.deselect(item);\r
600             }else{\r
601                 this.deselect(item);\r
602                 this.select(item, this.multiSelect && e.ctrlKey);\r
603                 this.lastSelection = item;\r
604             }\r
605             e.preventDefault();\r
606         }\r
607         return true;\r
608     }\r
609 });\r