1 /**
  2  * @author Gillis Haasnoot <gillis.haasnoot@gmail.com>
  3  * @package Banana.Controls
  4  * @summary DataGridDataTreeListRender  
  5  */
  6 
  7 goog.require('Banana.Controls.DataGridBaseListRender');
  8 
  9 goog.require('Banana.Controls.DataGridTableHeaderItemRender');
 10 goog.require('Banana.Controls.DataGridTableContentItemRender');
 11 goog.require('Banana.Controls.DataGridTableFooterItemRender');
 12 
 13 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridControlColumn');
 14 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridCharLimitColumn');
 15 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridImageButtonColumn');
 16 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridLinkColumn');
 17 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridImageColumn');
 18 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridStatusColumn');
 19 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridHeaderColumn');
 20 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridButtonColumn');
 21 
 22 goog.provide('Banana.Controls.DataGridTableListRender');
 23 
 24 /** @namespace Banana.Controls.DataGridTableListRender */
 25 namespace('Banana.Controls').DataGridTableListRender = Banana.Controls.DataGridBaseListRender.extend(
 26 /** @lends Banana.Controls.DataGridTableListRender.prototype */
 27 {
 28 	/**
 29 	 * Creates a datagrid table list render. This is the most standard grid.
 30 	 * Columns can be inserted 
 31 	 * 
 32 	 * bindable events
 33 	 * 
 34 	 *  - onRowCreated 
 35 	 *  - onInitColumn //triggered during creation of the column
 36 	 *  - datagridmouseover
 37 	 *  - dataSourceChanged
 38 	 *  - onItemSelect
 39 	 *  - itemRenderChanged
 40 	 * 
 41 	 * Example:
 42 	 * 
 43 	 * 
 44 		//specify the datasource we put inside the grid
 45 		var datasource = [
 46 		                  {'name':'a name','description':'a description'},
 47 		                  {'name':'a name','description':'a description'},
 48 		                  {'name':'a name','description':'a description'}
 49 		                  ];
 50 		
 51 		//specify the columns
 52 		var columns = [
 53 		               new Banana.Controls.DataGridColumn().setHeaderText('name').setDataField('name'),
 54 		               new Banana.Controls.DataGridColumn().setHeaderText('description').setDataField('description')
 55 		               ]
 56 		
 57 		var datagrid = new Banana.Controls.DataGrid();
 58 		datagrid.setDataSource(datasource);
 59 		
 60 		//by default we have a Banana.Controls.DataGridTableListRender inside the datagrid.
 61 		var listRender = datagrid.getListRender();
 62 		listRender.setColumns(columns);
 63 			
 64 		this.addControl(datagrid);	
 65 	 * 
 66 	 * By default the table list render contains a headerItemRender, itemRender and footerItemRender
 67 	 * 
 68 	 * @constructs
 69 	 * @extends Banana.Controls.DataGridBaseListRender
 70 	 */
 71 	init : function()
 72 	{
 73 		this._super();
 74 		
 75 		this.addCssClass("BDataGridTableListRender")
 76 		
 77 		this.columns = [];
 78 		this.indexRowMap = [];
 79 		this.dataItemRenderMap = new Banana.Util.ArrayBiCollection(); //mapping of item renders to data
 80 		
 81 		this.defaultHeaderItemRender = Banana.Controls.DataGridTableHeaderItemRender;
 82 		this.defaultContentItemRender = Banana.Controls.DataGridTableContentItemRender;
 83 		this.defaultFooterItemRender = Banana.Controls.DataGridTableFooterItemRender;
 84 		
 85 		this.headerItemRender = null;
 86 		this.contentItemRender = null;
 87 		this.headerItemRender = null;
 88 	},
 89 	
 90 	/**
 91 	 * @param {mixed} data
 92 	 * @param {Banana.Controls.DataGridTableItemRender} render
 93 	 * 
 94 	 * @return {int} position where item is inserted
 95 	 */
 96 	addItem : function(data,render)
 97 	{	
 98 		var il = this._super(data);
 99 		
100 		if (render)
101 		{
102 			this.indexItemRenderFactory[il] = render; 
103 		}
104 
105 		this.createRowByIndex(il,true);
106 		this.createItemRenderByIndex(il,true);
107 		
108 		return il;
109 	},
110 	
111 	/**
112 	 * removes item from list.
113 	 * @param {mixed} data
114 	 */
115 	removeItem : function(data)
116 	{
117 		var index = this._super(data);
118 		
119 		if (index >=0)
120 		{
121 			this.removeRowByIndex(index);
122 		}
123 	},
124 	
125 	/**
126 	 * clears complete list
127 	 */
128 	removeAllItems : function()
129 	{
130 		this._super();
131 		
132 		this.indexRowMap = [];
133 		this.indexItemRenderFactory = [];
134 		this.tableBody.clear();		
135 	},
136 	
137 	/**
138 	 * @return Banana.Controls.DataGridTableContentItemRender
139 	 */
140 	getDefaultItemRender : function()
141 	{
142 		return this.defaultContentItemRender;
143 	},
144 	
145 	/**
146 	 * @param {Banana.Controls.DataGridTableHeaderItemRender} r 
147 	 */
148 	setHeaderItemRender : function(r)
149 	{
150 		this.headerItemRender = r;
151 	},
152 	
153 	/**
154 	 * @return {Banana.Controls.DataGridTableHeaderItemRender}
155 	 */
156 	getHeaderItemRender : function(r)
157 	{
158 		return this.headerItemRender || this.defaultHeaderItemRender;
159 	},		
160 	
161 	/**
162 	 * sets item render on index
163 	 * 
164 	 * @param {int} index
165 	 * @param {String} render
166 	 * @param {boolean} dontCreate when true we do NOT instantly create the itemrender. use this when list is in prerender phase
167 	 * @param {boolean} ignoreDataItemRenderMap 
168 	 */
169 	setItemRenderByIndex : function(index,render,dontCreate,ignoreDataItemRenderMap)
170 	{
171 		this.indexItemRenderFactory[index] = render;
172 		
173 		//also save the item render data map relation
174 		//could be handy when we set an item render and later change the datasource
175 		//the item location could be changed then. itemrender - index relation is 
176 		//in that situation not enough
177 		if (!ignoreDataItemRenderMap && this.datasource[index])
178 		{
179 			//TODO this is the only place a mapping between data and render is set.
180 			//this should also be possible at a later moment.
181 			if (this.datasource[index][this.indexKey])
182 			{
183 				this.dataItemRenderMap.addItem(this.datasource[index][this.indexKey],render);
184 			}
185 		}
186 
187 		if (!dontCreate)
188 		{
189 			this.clearRowByIndex(index);
190 			this.createItemRenderByIndex(index,true);
191 			this.triggerEvent('itemRenderChanged');
192 		}		
193 	},
194 	
195 	/**
196 	 * sets an itemrender on given indices. 
197 	 * 
198 	 * @param {Array} indices
199 	 * @param {method} renderFactory
200 	 */
201 	setItemRenderByIndices : function(indices,renderFactory)
202 	{
203 		if (!indices)
204 		{
205 			return;
206 		}
207 		
208 		if (!indices instanceof Array)
209 		{
210 			log.error("Calling setItemRenderByIndices without indices specified in listrender "+this.id);
211 			return;
212 		}
213 		
214 		//if we set more than 4 item renders we rerender whole list once. otherwise per item
215 		//TODO could be smarter. like somekind of percentage of total list count
216 		var dontRenderPerItem = (indices.length > 4);
217 		
218 		for (var i = 0; i < indices.length; i++)
219 		{
220 			this.setItemRenderByIndex(indices[i],renderFactory,dontRenderPerItem);
221 		}
222 	
223 		if (dontRenderPerItem)
224 		{
225 			this.triggerEvent('itemRenderChanged');
226 			this.rerender();
227 		}
228 	},	
229 	
230 	/**
231 	 * sets an itemrender. Render will use for all indices the given itemrender
232 	 * 
233 	 * @param {Banana.Controls.DataGridTableItemRender} render
234 	 * @return {this}
235 	 */
236 	setItemRender : function(render)
237 	{
238 		//grid is not yet initialized. set itemrender as default
239 		if (!this.isInitialized)
240 		{
241 			this.defaultContentItemRender = render;
242 			return this;
243 		}
244 		
245 		this.dataItemRenderMap.clear(); 
246 		
247 		for (var j =0, clen = this.indexRenderedItemRenderMap.length; j < clen; j++)
248 		{	
249 			this.setItemRenderByIndex(j,render,true)
250 		}
251 			
252 		this.tableBody.clear();
253 		this.createTableParts();
254 
255 		if (this.isRendered)
256 		{
257 			this.tableBody.invalidateDisplay();
258 		}
259 		this.triggerEvent('itemRenderChanged');
260 		
261 		return this;
262 	},	
263 	
264 	/**
265 	 * @return {boolean} true when given index and itemrender exists
266 	 */
267 	hasItemRenderAt : function(index,itemRender)
268 	{
269 		if (this.indexRenderedItemRenderMap[index] instanceof itemRender)
270 		{
271 			return true;
272 		}
273 		
274 		return false;
275 	},
276 	
277 	/**
278 	 * @param {Banana.Controls.DataGridTableFooterItemRender} r 
279 	 */
280 	setFooterItemRender : function(r)
281 	{
282 		this.footerItemRender = r;
283 	},	
284 	
285 	/**
286 	 * @return {Banana.Controls.DataGridTableFooterItemRender}
287 	 */
288 	getFooterItemRender : function()
289 	{
290 		return this.footerItemRender;
291 	},
292 	
293 	/**
294 	 * @param {Banana.Controls.ItemRender}
295 	 * @return {Banana.UiControl}
296 	 */
297 	getRowByItemRender : function(ir)
298 	{
299 		return this.getRowByIndex([this.indexRenderedItemRenderMap.indexOf(ir)]);
300 	},
301 	
302 	/**
303 	 * @param {int} index
304 	 * @return {Banana.Control.Panel}
305 	 */
306 	getRowByIndex : function(index)
307 	{
308 		return this.indexRowMap[index];
309 	},
310 
311 	/**
312 	 * Removes complete row. New indices will be calculated for remaining rows
313 	 * 
314 	 * @param {int} index 
315 	 */
316 	removeRowByIndex : function(index)
317 	{
318 		this.indexRowMap[index].remove();
319 		
320 		this.selectedIndices.remove(i);
321 		this.indexRowMap.splice(index,1);
322 		this.indexItemRenderFactory.splice(index,1);
323 		this.indexRenderedItemRenderMap.splice(index,1);
324 	},
325 
326 	/**
327 	 * @param {int} index of row to be cleared. all gui data inside will be removed
328 	 */
329 	clearRowByIndex : function(index)
330 	{
331 		this.indexRowMap[index].clear();
332 	},
333 	
334 	/**
335 	 * @return {int} index in list by given row
336 	 */
337 	getIndexByRow : function(row)
338 	{
339 		return this.indexRowMap.indexOf(row);
340 	},
341 	
342 	/**
343 	 * @return {int} index in list by given itemrender
344 	 */
345 	getIndexByItemRender : function(ir)
346 	{
347 		return this.indexRowMap.indexOf(ir.parent);
348 	},
349 	
350 	/**
351 	 * TODO:
352 	 * @ignore
353 	 */
354 	getItemRenderByData : function(data)
355 	{
356 		// TODO
357 	},
358 	
359 	/**
360 	 * Use this method to supply columns
361 	 * @param {Array} of new Banana.Controls.DataGridColumn
362 	 */
363 	setColumns : function(cols)
364 	{
365 		this.columns = cols;
366 	},
367 	
368 	/**
369 	 * start creation of controls here
370 	 * we also give the user possibility to to something with the columns by looping over it
371 	 * and trigger 'onInitColumn' even
372 	 * Previous selected items will be reselected
373 	 * @ignore
374 	 */
375 	createControls : function()
376 	{
377 		if (!this.columns || !this.columns.length)
378 		{
379 			log.warning("Unable to render grid "+this.id+". No columns specified");
380 		}
381 		
382 		for (var i=0; i<this.columns.length;i++)
383 		{
384 			this.triggerEvent('onInitColumn',this.columns[i]);
385 		}
386 			
387 		this.previousSelectedItems = this.getSelectedItems();
388 
389 		this.selectedIndices.clear();
390 		this._super();
391 		
392 		this.table  = new Banana.Controls.Table();
393 		this.table.setAttribute('cellspacing','0');
394 		
395 		this.tableHead = new Banana.Controls.TableHead();
396 		this.tableHead.addCssClass("BDataGridTableListHead");
397 		this.table.addControl(this.tableHead);
398 		
399 		this.tableBody = new Banana.Controls.TableBody();
400 		this.tableBody.addCssClass("BDataGridTableListBody");
401 		this.table.addControl(this.tableBody);
402 		
403 		this.table.bind('mousedown',this.getProxy(function(e){
404 			// TODO: This is catching every event in the scope of the table
405 			// This should first check if user is clicking inside a input box
406 			// otherwise multiselects etc don't work
407 			if (e.originalEvent.shiftKey || e.originalEvent.ctrlKey)
408 			{
409 				e.preventDefault();
410 			}
411 		}))
412 		
413 		for (var j =0, clen = this.columns.length; j < clen; j++)
414 		{
415 			this.columns[j].setListRender(this);
416 		}
417 		
418 		this.createTableParts();
419 		
420 		this.addControl(this.table);	
421 		
422 		this.setSelectedItems(this.previousSelectedItems);
423 	},
424 	
425 	/**
426 	 * create the parts of the table here
427 	 * @ignore
428 	 */
429 	createTableParts : function()
430 	{	
431 		if (this.datasource.length)
432 		{
433 			this.createHeader();
434 			this.createRows();
435 			this.createFooter();
436 		}	
437 	},
438 	
439 	/**
440 	 * create header
441 	 * @ignore
442 	 */
443 	createHeader : function()
444 	{
445 		var row = new Banana.Controls.TableRow();
446 
447 		var itemRender = this.getObject(this.getHeaderItemRender());
448 		itemRender.setListRender(this);
449 		
450 		row.addControl(itemRender);
451 		
452 		this.tableHead.addControl(row);		
453 	},
454 	
455 	/**
456 	 * create rows
457 	 * @ignore
458 	 */
459 	createRows : function()
460 	{
461 		//loop through the datasource to create the rows
462 		for (var i =0, len = this.datasource.length; i < len; i++)
463 		{
464 			this.createRowByIndex(i);
465 			this.createItemRenderByIndex(i)
466 		}		
467 	},
468 	
469 	/**
470 	 * create footer
471 	 * @ignore
472 	 */
473 	createFooter : function()
474 	{
475 		if (!this.getFooterItemRender()) return;
476 		
477 		var row = new Banana.Controls.TableRow();
478 		
479 		var itemRender = this.getObject(this.getFooterItemRender());
480 		itemRender.setListRender(this);	
481 		row.addControl(itemRender);
482 		
483 		this.tableBody.addControl(row);
484 	},
485 	
486 	/**
487 	 * 
488 	 * @param {int} index
489 	 * @param {boolean} instantRender when true we instantly render the row
490 	 * 
491 	 * @ignore
492 	 */
493 	createRowByIndex : function(index,instantRender)
494 	{
495 		var row = new Banana.Controls.TableRow();
496 		row.addCssClass((index % 2) ? 'BDataGridTableListRenderRow' : 'BDataGridTableListRenderRowAlt');			
497 		
498 		row.bind('mouseover',this.getProxy(function(e){this.onRowMouseOver(e)}),row);
499 		row.bind('mouseout',this.getProxy(function(e){this.onRowMouseOut(e)}),row);
500 		row.bind('click',this.getProxy(function(e){this.onRowMouseClick(e)}),row);
501 			
502 		this.tableBody.addControl(row,instantRender);
503 		
504 		if (instantRender)
505 		{
506 			this.tableBody.invalidateDisplay();
507 		}	
508 		
509 		this.indexRowMap[index] = row;
510 
511 		this.triggerEvent('onRowCreated', {'index':index,'row': row, 'data': this.datasource[index]});
512 	},
513 	
514 	/**
515 	 * creates item render by index
516 	 * 
517 	 * Item render will be either from 
518 	 *  - data to item render map
519 	 *  - index to item render map
520 	 *  - or default one
521 	 *  
522 	 *  used internally. don't call this manually.
523 	 *  
524 	 *  @param {int} index
525 	 *  @param {boolean} instantRerender when true we instantly rerender the item 
526 	 *  
527 	 *  @ignore
528 	 */
529 	createItemRenderByIndex : function(index,instantRerender)
530 	{
531 		// Optionally can be accessed by e.g. invalidateDisplay to peek
532 		// at which index it is created
533 		this.currentIndexCreation = index;
534 		var itemRenderFactory = null;
535 		
536 		if (this.datasource[index][this.indexKey] && this.dataItemRenderMap.getItem(this.datasource[index][this.indexKey]))
537 		{
538 			itemRenderFactory = this.dataItemRenderMap.getItem(this.datasource[index][this.indexKey]); 
539 		}
540 		else if (this.indexItemRenderFactory[index])
541 		{
542 			itemRenderFactory = this.indexItemRenderFactory[index];
543 		}
544 		else
545 		{
546 			itemRenderFactory = this.defaultContentItemRender;
547 		}
548 
549 		var itemRender = this.getObject(itemRenderFactory);
550 		
551 		itemRender.setData(this.datasource[index]);
552 
553 		itemRender.bind('dataChanged',this.getProxy(function(e,f){
554 			
555 			this.getDataSource()[parseInt(e.data)] = e.currentTarget.getData();
556 			this.triggerEvent('dataSourceChanged');
557 			
558 		}),index.toString())
559 		
560 		//save mapping between itemRender and data
561 		this.dataItemRenderMap.addItem(this.datasource[index][this.indexKey],itemRenderFactory);
562 		
563 		this.indexRenderedItemRenderMap[index] = itemRender;
564 		itemRender.setListRender(this);
565 		var row = this.indexRowMap[index];
566 		row.addControl(itemRender);
567 		
568 		if (instantRerender)
569 		{
570 			row.invalidateDisplay();
571 		}
572 		
573 		this.currentIndexCreation = undefined;
574 	},
575 	
576 	/**
577 	 * triggered when use moves over. we hightlight a row here
578 	 * 
579 	 * @param {event} e
580 	 * @ignore
581 	 */
582 	onRowMouseOver : function(e)
583 	{
584 		var index = this.indexRowMap.indexOf(e.data);
585 
586 		var itemRender = this.indexRenderedItemRenderMap[index];
587 		
588 		if (!itemRender.getIsSelectable()) return;
589 		
590 		itemRender.mouseOver();
591 	},
592 	
593 	/**
594 	 * triggered when use moves outside
595 	 * 
596 	 * @param {event} e
597 	 * @ignore
598 	 */
599 	onRowMouseOut : function(e)
600 	{
601 		var index = this.indexRowMap.indexOf(e.data);
602 		
603 		var itemRender = this.indexRenderedItemRenderMap[index];
604 		
605 		if (!itemRender.getIsSelectable()) return;
606 		
607 		itemRender.mouseOut();
608 	},
609 	
610 	/**
611 	 * triggered when use moves outside
612 	 * if row is unselected we trigger event 'onItemSelect'
613 	 * 
614 	 * @param {event} e
615 	 * @ignore
616 	 */
617 	onRowMouseClick : function(e)
618 	{
619 		var index = this.indexRowMap.indexOf(e.data);
620 
621 		var itemRender = this.indexRenderedItemRenderMap[index];
622 		
623 		if (!itemRender.getIsSelectable()) return;
624 
625 		if (index<0) {return}
626 
627 		var ctrlKey = e.ctrlKey;
628 		var shiftKey = e.shiftKey;
629 
630 		if (this.getIndexIsSelected(index))
631 		{
632 			if (!ctrlKey && !shiftKey)
633 			{
634 				this.clearSelectedIndices();
635 			}
636 			else
637 			{
638 				this.clearSelectedIndex(index);
639 			}
640 		}
641 		else
642 		{
643 			if (this.selectionType == 'single')
644 			{
645 				this.clearSelectedIndices();
646 				this.addSelectedIndex(index);
647 			}
648 			else
649 			{
650 				if (!ctrlKey && !shiftKey)
651 				{
652 					this.clearSelectedIndices();
653 					this.addSelectedIndex(index);
654 				}
655 				if (ctrlKey)
656 				{
657 					this.addSelectedIndex(index);
658 				}
659 				if (shiftKey)
660 				{
661 
662 					var firstItem = parseInt(this.selectedIndices.getKeyByIndex(0));
663 
664 					if (!firstItem) firstItem = 0;
665 
666 					var start;
667 					var end;
668 					if (index > firstItem)
669 					{
670 						start = firstItem;
671 						end = index;
672 					}
673 					else
674 					{
675 						start = index;
676 						end = parseInt(this.selectedIndices.getKeyByIndex([this.selectedIndices.getLength()-1]));
677 					}
678 
679 					this.clearSelectedIndices();
680 
681 					for (var i = parseInt(start); i <= parseInt(end); i++)
682 					{
683 						this.addSelectedIndex(i);
684 					}
685 				}
686 
687 			}
688 		}	
689 		
690 		this.triggerEvent('onItemSelect');
691 	},
692 	
693 	/**
694 	 * Selects the index. also adds BDataGridRowSelected css class to the item render 
695 	 * 
696 	 * @param {int} index
697 	 */
698 	selectIndex : function(index)
699 	{
700 		var ir = this.getRenderedItemRenderByIndex(index);
701 
702 		if (ir && typeof(ir.select) == 'function' && ir.getIsSelectable())
703 		{
704 			ir.select();
705 		}
706 	},
707 
708 	/**
709 	 * Selects the index. also removes BDataGridRowSelected css class from the item render
710 	 * 
711 	 * @param {int} index
712 	 */
713 	deSelectIndex : function(index)
714 	{
715 		var ir = this.getRenderedItemRenderByIndex(index);
716 
717 		if (ir && typeof(ir.deselect) == 'function')
718 		{
719 			ir.deselect();
720 		}
721 	}
722 });
723