Roo/View.js
[roojs1] / Roo / View.js
1 /*
2  * Based on:
3  * Ext JS Library 1.1.1
4  * Copyright(c) 2006-2007, Ext JS, LLC.
5  *
6  * Originally Released Under LGPL - original licence link has changed is not relivant.
7  *
8  * Fork - LGPL
9  * <script type="text/javascript">
10  */
11
12 /**
13  * @class Roo.View
14  * @extends Roo.util.Observable
15  * Create a "View" for an element based on a data model or UpdateManager and the supplied DomHelper template. 
16  * This class also supports single and multi selection modes. <br>
17  * Create a data model bound view:
18  <pre><code>
19  var store = new Roo.data.Store(...);
20
21  var view = new Roo.View({
22     el : "my-element",
23     template : '&lt;div id="{0}"&gt;{2} - {1}&lt;/div&gt;', // auto create template
24  
25     singleSelect: true,
26     selectedClass: "ydataview-selected",
27     store: store
28  });
29
30  // listen for node click?
31  view.on("click", function(vw, index, node, e){
32  alert('Node "' + node.id + '" at index: ' + index + " was clicked.");
33  });
34
35  // load XML data
36  dataModel.load("foobar.xml");
37  </code></pre>
38  For an example of creating a JSON/UpdateManager view, see {@link Roo.JsonView}.
39  * <br><br>
40  * <b>Note: The root of your template must be a single node. Table/row implementations may work but are not supported due to
41  * IE"s limited insertion support with tables and Opera"s faulty event bubbling.</b>
42  * 
43  * Note: old style constructor is still suported (container, template, config)
44  * 
45  * @constructor
46  * Create a new View
47  * @param {Object} config The config object
48  * 
49  */
50 Roo.View = function(config, depreciated_tpl, depreciated_config){
51     
52     if (typeof(depreciated_tpl) == 'undefined') {
53         config = depreciated_container;
54         depreciated_tpl = config.template;
55     } else {
56         // old format..
57         this.el  = Roo.get(config);
58         this.tpl = depreciated_tpl;
59         Roo.apply(this, depreciated_config);
60     }
61      
62     
63     if(typeof(this.tpl) == "string"){
64         this.tpl = new Roo.Template(this.tpl);
65     } 
66     
67     
68     this.tpl.compile();
69     /**
70      * The template used by this View
71      * @type {Roo.DomHelper.Template}
72      */
73     //this.tpl = tpl;
74
75     Roo.apply(this, config);
76
77     /** @private */
78     this.addEvents({
79     /**
80      * @event beforeclick
81      * Fires before a click is processed. Returns false to cancel the default action.
82      * @param {Roo.View} this
83      * @param {Number} index The index of the target node
84      * @param {HTMLElement} node The target node
85      * @param {Roo.EventObject} e The raw event object
86      */
87         "beforeclick" : true,
88     /**
89      * @event click
90      * Fires when a template node is clicked.
91      * @param {Roo.View} this
92      * @param {Number} index The index of the target node
93      * @param {HTMLElement} node The target node
94      * @param {Roo.EventObject} e The raw event object
95      */
96         "click" : true,
97     /**
98      * @event dblclick
99      * Fires when a template node is double clicked.
100      * @param {Roo.View} this
101      * @param {Number} index The index of the target node
102      * @param {HTMLElement} node The target node
103      * @param {Roo.EventObject} e The raw event object
104      */
105         "dblclick" : true,
106     /**
107      * @event contextmenu
108      * Fires when a template node is right clicked.
109      * @param {Roo.View} this
110      * @param {Number} index The index of the target node
111      * @param {HTMLElement} node The target node
112      * @param {Roo.EventObject} e The raw event object
113      */
114         "contextmenu" : true,
115     /**
116      * @event selectionchange
117      * Fires when the selected nodes change.
118      * @param {Roo.View} this
119      * @param {Array} selections Array of the selected nodes
120      */
121         "selectionchange" : true,
122
123     /**
124      * @event beforeselect
125      * Fires before a selection is made. If any handlers return false, the selection is cancelled.
126      * @param {Roo.View} this
127      * @param {HTMLElement} node The node to be selected
128      * @param {Array} selections Array of currently selected nodes
129      */
130         "beforeselect" : true
131     });
132
133     this.el.on({
134         "click": this.onClick,
135         "dblclick": this.onDblClick,
136         "contextmenu": this.onContextMenu,
137         scope:this
138     });
139
140     this.selections = [];
141     this.nodes = [];
142     this.cmp = new Roo.CompositeElementLite([]);
143     if(this.store){
144         this.store = Roo.factory(this.store, Roo.data);
145         this.setStore(this.store, true);
146     }
147     Roo.View.superclass.constructor.call(this);
148 };
149
150 Roo.extend(Roo.View, Roo.util.Observable, {
151     
152     /**
153      * The container element.
154      * @cfg {String|Roo.Element}
155      */
156     container : '',
157     /**
158      * The template used by this View
159      * @cfg {String|Roo.DomHelper.Template}
160      */
161     template : '',
162     
163     /**
164      * The template used by this View (after contruction)
165      * @type {Roo.DomHelper.Template}
166      */
167     this.tpl : false,
168     
169     /**
170      * The css class to add to selected nodes
171      * @type {Roo.DomHelper.Template}
172      */
173     selectedClass : "x-view-selected",
174     
175     emptyText : "",
176     /**
177      * Returns the element this view is bound to.
178      * @return {Roo.Element}
179      */
180     getEl : function(){
181         return this.el;
182     },
183
184     /**
185      * Refreshes the view.
186      */
187     refresh : function(){
188         var t = this.tpl;
189         this.clearSelections();
190         this.el.update("");
191         var html = [];
192         var records = this.store.getRange();
193         if(records.length < 1){
194             this.el.update(this.emptyText);
195             return;
196         }
197         for(var i = 0, len = records.length; i < len; i++){
198             var data = this.prepareData(records[i].data, i, records[i]);
199             html[html.length] = t.apply(data);
200         }
201         this.el.update(html.join(""));
202         this.nodes = this.el.dom.childNodes;
203         this.updateIndexes(0);
204     },
205
206     /**
207      * Function to override to reformat the data that is sent to
208      * the template for each node.
209      * @param {Array/Object} data The raw data (array of colData for a data model bound view or
210      * a JSON object for an UpdateManager bound view).
211      */
212     prepareData : function(data){
213         return data;
214     },
215
216     onUpdate : function(ds, record){
217         this.clearSelections();
218         var index = this.store.indexOf(record);
219         var n = this.nodes[index];
220         this.tpl.insertBefore(n, this.prepareData(record.data));
221         n.parentNode.removeChild(n);
222         this.updateIndexes(index, index);
223     },
224
225     onAdd : function(ds, records, index){
226         this.clearSelections();
227         if(this.nodes.length == 0){
228             this.refresh();
229             return;
230         }
231         var n = this.nodes[index];
232         for(var i = 0, len = records.length; i < len; i++){
233             var d = this.prepareData(records[i].data);
234             if(n){
235                 this.tpl.insertBefore(n, d);
236             }else{
237                 this.tpl.append(this.el, d);
238             }
239         }
240         this.updateIndexes(index);
241     },
242
243     onRemove : function(ds, record, index){
244         this.clearSelections();
245         this.el.dom.removeChild(this.nodes[index]);
246         this.updateIndexes(index);
247     },
248
249     /**
250      * Refresh an individual node.
251      * @param {Number} index
252      */
253     refreshNode : function(index){
254         this.onUpdate(this.store, this.store.getAt(index));
255     },
256
257     updateIndexes : function(startIndex, endIndex){
258         var ns = this.nodes;
259         startIndex = startIndex || 0;
260         endIndex = endIndex || ns.length - 1;
261         for(var i = startIndex; i <= endIndex; i++){
262             ns[i].nodeIndex = i;
263         }
264     },
265
266     /**
267      * Changes the data store this view uses and refresh the view.
268      * @param {Store} store
269      */
270     setStore : function(store, initial){
271         if(!initial && this.store){
272             this.store.un("datachanged", this.refresh);
273             this.store.un("add", this.onAdd);
274             this.store.un("remove", this.onRemove);
275             this.store.un("update", this.onUpdate);
276             this.store.un("clear", this.refresh);
277         }
278         if(store){
279           
280             store.on("datachanged", this.refresh, this);
281             store.on("add", this.onAdd, this);
282             store.on("remove", this.onRemove, this);
283             store.on("update", this.onUpdate, this);
284             store.on("clear", this.refresh, this);
285         }
286         
287         if(store){
288             this.refresh();
289         }
290     },
291
292     /**
293      * Returns the template node the passed child belongs to or null if it doesn't belong to one.
294      * @param {HTMLElement} node
295      * @return {HTMLElement} The template node
296      */
297     findItemFromChild : function(node){
298         var el = this.el.dom;
299         if(!node || node.parentNode == el){
300                     return node;
301             }
302             var p = node.parentNode;
303             while(p && p != el){
304             if(p.parentNode == el){
305                 return p;
306             }
307             p = p.parentNode;
308         }
309             return null;
310     },
311
312     /** @ignore */
313     onClick : function(e){
314         var item = this.findItemFromChild(e.getTarget());
315         if(item){
316             var index = this.indexOf(item);
317             if(this.onItemClick(item, index, e) !== false){
318                 this.fireEvent("click", this, index, item, e);
319             }
320         }else{
321             this.clearSelections();
322         }
323     },
324
325     /** @ignore */
326     onContextMenu : function(e){
327         var item = this.findItemFromChild(e.getTarget());
328         if(item){
329             this.fireEvent("contextmenu", this, this.indexOf(item), item, e);
330         }
331     },
332
333     /** @ignore */
334     onDblClick : function(e){
335         var item = this.findItemFromChild(e.getTarget());
336         if(item){
337             this.fireEvent("dblclick", this, this.indexOf(item), item, e);
338         }
339     },
340
341     onItemClick : function(item, index, e){
342         if(this.fireEvent("beforeclick", this, index, item, e) === false){
343             return false;
344         }
345         if(this.multiSelect || this.singleSelect){
346             if(this.multiSelect && e.shiftKey && this.lastSelection){
347                 this.select(this.getNodes(this.indexOf(this.lastSelection), index), false);
348             }else{
349                 this.select(item, this.multiSelect && e.ctrlKey);
350                 this.lastSelection = item;
351             }
352             e.preventDefault();
353         }
354         return true;
355     },
356
357     /**
358      * Get the number of selected nodes.
359      * @return {Number}
360      */
361     getSelectionCount : function(){
362         return this.selections.length;
363     },
364
365     /**
366      * Get the currently selected nodes.
367      * @return {Array} An array of HTMLElements
368      */
369     getSelectedNodes : function(){
370         return this.selections;
371     },
372
373     /**
374      * Get the indexes of the selected nodes.
375      * @return {Array}
376      */
377     getSelectedIndexes : function(){
378         var indexes = [], s = this.selections;
379         for(var i = 0, len = s.length; i < len; i++){
380             indexes.push(s[i].nodeIndex);
381         }
382         return indexes;
383     },
384
385     /**
386      * Clear all selections
387      * @param {Boolean} suppressEvent (optional) true to skip firing of the selectionchange event
388      */
389     clearSelections : function(suppressEvent){
390         if(this.nodes && (this.multiSelect || this.singleSelect) && this.selections.length > 0){
391             this.cmp.elements = this.selections;
392             this.cmp.removeClass(this.selectedClass);
393             this.selections = [];
394             if(!suppressEvent){
395                 this.fireEvent("selectionchange", this, this.selections);
396             }
397         }
398     },
399
400     /**
401      * Returns true if the passed node is selected
402      * @param {HTMLElement/Number} node The node or node index
403      * @return {Boolean}
404      */
405     isSelected : function(node){
406         var s = this.selections;
407         if(s.length < 1){
408             return false;
409         }
410         node = this.getNode(node);
411         return s.indexOf(node) !== -1;
412     },
413
414     /**
415      * Selects nodes.
416      * @param {Array/HTMLElement/String/Number} nodeInfo An HTMLElement template node, index of a template node, id of a template node or an array of any of those to select
417      * @param {Boolean} keepExisting (optional) true to keep existing selections
418      * @param {Boolean} suppressEvent (optional) true to skip firing of the selectionchange vent
419      */
420     select : function(nodeInfo, keepExisting, suppressEvent){
421         if(nodeInfo instanceof Array){
422             if(!keepExisting){
423                 this.clearSelections(true);
424             }
425             for(var i = 0, len = nodeInfo.length; i < len; i++){
426                 this.select(nodeInfo[i], true, true);
427             }
428         } else{
429             var node = this.getNode(nodeInfo);
430             if(node && !this.isSelected(node)){
431                 if(!keepExisting){
432                     this.clearSelections(true);
433                 }
434                 if(this.fireEvent("beforeselect", this, node, this.selections) !== false){
435                     Roo.fly(node).addClass(this.selectedClass);
436                     this.selections.push(node);
437                     if(!suppressEvent){
438                         this.fireEvent("selectionchange", this, this.selections);
439                     }
440                 }
441             }
442         }
443     },
444
445     /**
446      * Gets a template node.
447      * @param {HTMLElement/String/Number} nodeInfo An HTMLElement template node, index of a template node or the id of a template node
448      * @return {HTMLElement} The node or null if it wasn't found
449      */
450     getNode : function(nodeInfo){
451         if(typeof nodeInfo == "string"){
452             return document.getElementById(nodeInfo);
453         }else if(typeof nodeInfo == "number"){
454             return this.nodes[nodeInfo];
455         }
456         return nodeInfo;
457     },
458
459     /**
460      * Gets a range template nodes.
461      * @param {Number} startIndex
462      * @param {Number} endIndex
463      * @return {Array} An array of nodes
464      */
465     getNodes : function(start, end){
466         var ns = this.nodes;
467         start = start || 0;
468         end = typeof end == "undefined" ? ns.length - 1 : end;
469         var nodes = [];
470         if(start <= end){
471             for(var i = start; i <= end; i++){
472                 nodes.push(ns[i]);
473             }
474         } else{
475             for(var i = start; i >= end; i--){
476                 nodes.push(ns[i]);
477             }
478         }
479         return nodes;
480     },
481
482     /**
483      * Finds the index of the passed node
484      * @param {HTMLElement/String/Number} nodeInfo An HTMLElement template node, index of a template node or the id of a template node
485      * @return {Number} The index of the node or -1
486      */
487     indexOf : function(node){
488         node = this.getNode(node);
489         if(typeof node.nodeIndex == "number"){
490             return node.nodeIndex;
491         }
492         var ns = this.nodes;
493         for(var i = 0, len = ns.length; i < len; i++){
494             if(ns[i] == node){
495                 return i;
496             }
497         }
498         return -1;
499     }
500 });