roojs-core.js
[roojs1] / Roo / DDView.js
1 //<script type="text/javasscript">
2  
3
4 /**
5  * @class Roo.DDView
6  * A DnD enabled version of Roo.View.
7  * @param {Element/String} container The Element in which to create the View.
8  * @param {String} tpl The template string used to create the markup for each element of the View
9  * @param {Object} config The configuration properties. These include all the config options of
10  * {@link Roo.View} plus some specific to this class.<br>
11  * <p>
12  * Drag/drop is implemented by adding {@link Roo.data.Record}s to the target DDView. If copying is
13  * not being performed, the original {@link Roo.data.Record} is removed from the source DDView.<br>
14  * <p>
15  * The following extra CSS rules are needed to provide insertion point highlighting:<pre><code>
16 .x-view-drag-insert-above {
17         border-top:1px dotted #3366cc;
18 }
19 .x-view-drag-insert-below {
20         border-bottom:1px dotted #3366cc;
21 }
22 </code></pre>
23  * 
24  */
25  
26 Roo.DDView = function(container, tpl, config) {
27     Roo.DDView.superclass.constructor.apply(this, arguments);
28     this.getEl().setStyle("outline", "0px none");
29     this.getEl().unselectable();
30     if (this.dragGroup) {
31         this.setDraggable(this.dragGroup.split(","));
32     }
33     if (this.dropGroup) {
34         this.setDroppable(this.dropGroup.split(","));
35     }
36     if (this.deletable) {
37         this.setDeletable();
38     }
39     this.isDirtyFlag = false;
40         this.addEvents({
41                 "drop" : true
42         });
43 };
44
45 Roo.extend(Roo.DDView, Roo.View, {
46 /**     @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone. */
47 /**     @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone. */
48 /**     @cfg {Boolean} copy Causes drag operations to copy nodes rather than move. */
49 /**     @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move. */
50
51         isFormField: true,
52
53         reset: Roo.emptyFn,
54         
55         clearInvalid: Roo.form.Field.prototype.clearInvalid,
56
57         validate: function() {
58                 return true;
59         },
60         
61         destroy: function() {
62                 this.purgeListeners();
63                 this.getEl.removeAllListeners();
64                 this.getEl().remove();
65                 if (this.dragZone) {
66                         if (this.dragZone.destroy) {
67                                 this.dragZone.destroy();
68                         }
69                 }
70                 if (this.dropZone) {
71                         if (this.dropZone.destroy) {
72                                 this.dropZone.destroy();
73                         }
74                 }
75         },
76
77 /**     Allows this class to be an Roo.form.Field so it can be found using {@link Roo.form.BasicForm#findField}. */
78         getName: function() {
79                 return this.name;
80         },
81
82 /**     Loads the View from a JSON string representing the Records to put into the Store. */
83         setValue: function(v) {
84                 if (!this.store) {
85                         throw "DDView.setValue(). DDView must be constructed with a valid Store";
86                 }
87                 var data = {};
88                 data[this.store.reader.meta.root] = v ? [].concat(v) : [];
89                 this.store.proxy = new Roo.data.MemoryProxy(data);
90                 this.store.load();
91         },
92
93 /**     @return {String} a parenthesised list of the ids of the Records in the View. */
94         getValue: function() {
95                 var result = '(';
96                 this.store.each(function(rec) {
97                         result += rec.id + ',';
98                 });
99                 return result.substr(0, result.length - 1) + ')';
100         },
101         
102         getIds: function() {
103                 var i = 0, result = new Array(this.store.getCount());
104                 this.store.each(function(rec) {
105                         result[i++] = rec.id;
106                 });
107                 return result;
108         },
109         
110         isDirty: function() {
111                 return this.isDirtyFlag;
112         },
113
114 /**
115  *      Part of the Roo.dd.DropZone interface. If no target node is found, the
116  *      whole Element becomes the target, and this causes the drop gesture to append.
117  */
118     getTargetFromEvent : function(e) {
119                 var target = e.getTarget();
120                 while ((target !== null) && (target.parentNode != this.el.dom)) {
121                 target = target.parentNode;
122                 }
123                 if (!target) {
124                         target = this.el.dom.lastChild || this.el.dom;
125                 }
126                 return target;
127     },
128
129 /**
130  *      Create the drag data which consists of an object which has the property "ddel" as
131  *      the drag proxy element. 
132  */
133     getDragData : function(e) {
134         var target = this.findItemFromChild(e.getTarget());
135                 if(target) {
136                         this.handleSelection(e);
137                         var selNodes = this.getSelectedNodes();
138             var dragData = {
139                 source: this,
140                 copy: this.copy || (this.allowCopy && e.ctrlKey),
141                 nodes: selNodes,
142                 records: []
143                         };
144                         var selectedIndices = this.getSelectedIndexes();
145                         for (var i = 0; i < selectedIndices.length; i++) {
146                                 dragData.records.push(this.store.getAt(selectedIndices[i]));
147                         }
148                         if (selNodes.length == 1) {
149                                 dragData.ddel = target.cloneNode(true); // the div element
150                         } else {
151                                 var div = document.createElement('div'); // create the multi element drag "ghost"
152                                 div.className = 'multi-proxy';
153                                 for (var i = 0, len = selNodes.length; i < len; i++) {
154                                         div.appendChild(selNodes[i].cloneNode(true));
155                                 }
156                                 dragData.ddel = div;
157                         }
158             //console.log(dragData)
159             //console.log(dragData.ddel.innerHTML)
160                         return dragData;
161                 }
162         //console.log('nodragData')
163                 return false;
164     },
165     
166 /**     Specify to which ddGroup items in this DDView may be dragged. */
167     setDraggable: function(ddGroup) {
168         if (ddGroup instanceof Array) {
169                 Roo.each(ddGroup, this.setDraggable, this);
170                 return;
171         }
172         if (this.dragZone) {
173                 this.dragZone.addToGroup(ddGroup);
174         } else {
175                         this.dragZone = new Roo.dd.DragZone(this.getEl(), {
176                                 containerScroll: true,
177                                 ddGroup: ddGroup 
178
179                         });
180 //                      Draggability implies selection. DragZone's mousedown selects the element.
181                         if (!this.multiSelect) { this.singleSelect = true; }
182
183 //                      Wire the DragZone's handlers up to methods in *this*
184                         this.dragZone.getDragData = this.getDragData.createDelegate(this);
185                 }
186     },
187
188 /**     Specify from which ddGroup this DDView accepts drops. */
189     setDroppable: function(ddGroup) {
190         if (ddGroup instanceof Array) {
191                 Roo.each(ddGroup, this.setDroppable, this);
192                 return;
193         }
194         if (this.dropZone) {
195                 this.dropZone.addToGroup(ddGroup);
196         } else {
197                         this.dropZone = new Roo.dd.DropZone(this.getEl(), {
198                                 containerScroll: true,
199                                 ddGroup: ddGroup
200                         });
201
202 //                      Wire the DropZone's handlers up to methods in *this*
203                         this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this);
204                         this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this);
205                         this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this);
206                         this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this);
207                         this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this);
208                 }
209     },
210
211 /**     Decide whether to drop above or below a View node. */
212     getDropPoint : function(e, n, dd){
213         if (n == this.el.dom) { return "above"; }
214                 var t = Roo.lib.Dom.getY(n), b = t + n.offsetHeight;
215                 var c = t + (b - t) / 2;
216                 var y = Roo.lib.Event.getPageY(e);
217                 if(y <= c) {
218                         return "above";
219                 }else{
220                         return "below";
221                 }
222     },
223
224     onNodeEnter : function(n, dd, e, data){
225                 return false;
226     },
227     
228     onNodeOver : function(n, dd, e, data){
229                 var pt = this.getDropPoint(e, n, dd);
230                 // set the insert point style on the target node
231                 var dragElClass = this.dropNotAllowed;
232                 if (pt) {
233                         var targetElClass;
234                         if (pt == "above"){
235                                 dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above";
236                                 targetElClass = "x-view-drag-insert-above";
237                         } else {
238                                 dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below";
239                                 targetElClass = "x-view-drag-insert-below";
240                         }
241                         if (this.lastInsertClass != targetElClass){
242                                 Roo.fly(n).replaceClass(this.lastInsertClass, targetElClass);
243                                 this.lastInsertClass = targetElClass;
244                         }
245                 }
246                 return dragElClass;
247         },
248
249     onNodeOut : function(n, dd, e, data){
250                 this.removeDropIndicators(n);
251     },
252
253     onNodeDrop : function(n, dd, e, data){
254         if (this.fireEvent("drop", this, n, dd, e, data) === false) {
255                 return false;
256         }
257         var pt = this.getDropPoint(e, n, dd);
258                 var insertAt = (n == this.el.dom) ? this.nodes.length : n.nodeIndex;
259                 if (pt == "below") { insertAt++; }
260                 for (var i = 0; i < data.records.length; i++) {
261                         var r = data.records[i];
262                         var dup = this.store.getById(r.id);
263                         if (dup && (dd != this.dragZone)) {
264                                 Roo.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1);
265                         } else {
266                                 if (data.copy) {
267                                         this.store.insert(insertAt++, r.copy());
268                                 } else {
269                                         data.source.isDirtyFlag = true;
270                                         r.store.remove(r);
271                                         this.store.insert(insertAt++, r);
272                                 }
273                                 this.isDirtyFlag = true;
274                         }
275                 }
276                 this.dragZone.cachedTarget = null;
277                 return true;
278     },
279
280     removeDropIndicators : function(n){
281                 if(n){
282                         Roo.fly(n).removeClass([
283                                 "x-view-drag-insert-above",
284                                 "x-view-drag-insert-below"]);
285                         this.lastInsertClass = "_noclass";
286                 }
287     },
288
289 /**
290  *      Utility method. Add a delete option to the DDView's context menu.
291  *      @param {String} imageUrl The URL of the "delete" icon image.
292  */
293         setDeletable: function(imageUrl) {
294                 if (!this.singleSelect && !this.multiSelect) {
295                         this.singleSelect = true;
296                 }
297                 var c = this.getContextMenu();
298                 this.contextMenu.on("itemclick", function(item) {
299                         switch (item.id) {
300                                 case "delete":
301                                         this.remove(this.getSelectedIndexes());
302                                         break;
303                         }
304                 }, this);
305                 this.contextMenu.add({
306                         icon: imageUrl,
307                         id: "delete",
308                         text: 'Delete'
309                 });
310         },
311         
312 /**     Return the context menu for this DDView. */
313         getContextMenu: function() {
314                 if (!this.contextMenu) {
315 //                      Create the View's context menu
316                         this.contextMenu = new Roo.menu.Menu({
317                                 id: this.id + "-contextmenu"
318                         });
319                         this.el.on("contextmenu", this.showContextMenu, this);
320                 }
321                 return this.contextMenu;
322         },
323         
324         disableContextMenu: function() {
325                 if (this.contextMenu) {
326                         this.el.un("contextmenu", this.showContextMenu, this);
327                 }
328         },
329
330         showContextMenu: function(e, item) {
331         item = this.findItemFromChild(e.getTarget());
332                 if (item) {
333                         e.stopEvent();
334                         this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true);
335                         this.contextMenu.showAt(e.getXY());
336             }
337     },
338
339 /**
340  *      Remove {@link Roo.data.Record}s at the specified indices.
341  *      @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove.
342  */
343     remove: function(selectedIndices) {
344                 selectedIndices = [].concat(selectedIndices);
345                 for (var i = 0; i < selectedIndices.length; i++) {
346                         var rec = this.store.getAt(selectedIndices[i]);
347                         this.store.remove(rec);
348                 }
349     },
350
351 /**
352  *      Double click fires the event, but also, if this is draggable, and there is only one other
353  *      related DropZone, it transfers the selected node.
354  */
355     onDblClick : function(e){
356         var item = this.findItemFromChild(e.getTarget());
357         if(item){
358             if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) {
359                 return false;
360             }
361             if (this.dragGroup) {
362                     var targets = Roo.dd.DragDropMgr.getRelated(this.dragZone, true);
363                     while (targets.indexOf(this.dropZone) > -1) {
364                             targets.remove(this.dropZone);
365                                 }
366                     if (targets.length == 1) {
367                                         this.dragZone.cachedTarget = null;
368                         var el = Roo.get(targets[0].getEl());
369                         var box = el.getBox(true);
370                         targets[0].onNodeDrop(el.dom, {
371                                 target: el.dom,
372                                 xy: [box.x, box.y + box.height - 1]
373                         }, null, this.getDragData(e));
374                     }
375                 }
376         }
377     },
378     
379     handleSelection: function(e) {
380                 this.dragZone.cachedTarget = null;
381         var item = this.findItemFromChild(e.getTarget());
382         if (!item) {
383                 this.clearSelections(true);
384                 return;
385         }
386                 if (item && (this.multiSelect || this.singleSelect)){
387                         if(this.multiSelect && e.shiftKey && (!e.ctrlKey) && this.lastSelection){
388                                 this.select(this.getNodes(this.indexOf(this.lastSelection), item.nodeIndex), false);
389                         }else if (this.isSelected(this.getNode(item)) && e.ctrlKey){
390                                 this.unselect(item);
391                         } else {
392                                 this.select(item, this.multiSelect && e.ctrlKey);
393                                 this.lastSelection = item;
394                         }
395                 }
396     },
397
398     onItemClick : function(item, index, e){
399                 if(this.fireEvent("beforeclick", this, index, item, e) === false){
400                         return false;
401                 }
402                 return true;
403     },
404
405     unselect : function(nodeInfo, suppressEvent){
406                 var node = this.getNode(nodeInfo);
407                 if(node && this.isSelected(node)){
408                         if(this.fireEvent("beforeselect", this, node, this.selections) !== false){
409                                 Roo.fly(node).removeClass(this.selectedClass);
410                                 this.selections.remove(node);
411                                 if(!suppressEvent){
412                                         this.fireEvent("selectionchange", this, this.selections);
413                                 }
414                         }
415                 }
416     }
417 });