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