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 goog.require('Banana.Controls.DataGridTileItemRender');
  9 
 10 goog.provide('Banana.Controls.DataGridTileListRender');
 11 
 12 /** @namespace Banana.Controls.DataGridTileListRender */
 13 namespace('Banana.Controls').DataGridTileListRender = Banana.Controls.DataGridBaseListRender.extend(
 14 /** @lends Banana.Controls.DataGridTileListRender.prototype */
 15 {
 16 	
 17 	/**
 18 	 * 
 19 	 * Creates a datagrid tile list render. Each item in the datasource is a tile.
 20 	 * Use setPlaceHolderWidth to have multiple tiles after each other. You can also define this in css
 21 	 * By default each tile is rendered by a Banana.Controls.DataGridTileItemRender instance
 22 	 * implement your own instance to construct more advanced item renders
 23 	 *  
 24 	 * example:
 25 	 
 26 	  //define our custom datagrid table item render
 27         myCustomItemRender = Banana.Controls.DataGridTableContentItemRender.extend({
 28 
 29 			createComponents : function()
 30 			{
 31 				var label = new Banana.Controls.Panel();
 32 				label.setData(this.getData().id);
 33 				this.addControl(new Banana.Controls.Panel());
 34 			}
 35 		});
 36 	 	
 37 		grid = new Banana.Controls.DataGrid();
 38 		
 39 		var listRender = new Banana.Controls.DataGridTileListRender()
 40 		listRender.setHItemCount(4);
 41 		listRender.setTilePadding(6);
 42 		
 43 		//note that this method required you to pass a function providing the itemrender.
 44 		listRender.setItemRender(function(){return new myCustomItemRender()});
 45 		
 46 		grid.setListRender(listRender);
 47 		
 48 		var content = [{id:1},{id:2}];	
 49 		
 50 		grid.setDataSource(content);	
 51 
 52 		this.addControl(grid);	 
 53 	 * 
 54 	 * @constructs
 55 	 * @extends Banana.Controls.DataGridBaseListRender
 56 	 */
 57 	init : function()
 58 	{
 59 		this._super();
 60 		
 61 		this.addCssClass("BDataGridTileListRender")
 62 		
 63 		this.indexKey = null; //specifies the indexkey used to indentify items
 64 		
 65 		this.indexTilePlaceHolderMap = [];
 66 		
 67 		this.dataItemRenderMap = new Banana.Util.ArrayBiCollection(); //mapping of item renders to data
 68 		
 69 		this.defaultContentItemRender = Banana.Controls.DataGridTileItemRender;
 70 		
 71 		this.contentItemRender = null;
 72 	},
 73 	
 74 	/**
 75 	 * Adds item
 76 	 * 
 77 	 * @param {mixed} data
 78 	 * @param {Boolean} when true we instantly renders it
 79 	 */
 80 	addItem : function(data,render)
 81 	{
 82 		this.applyUid(data);
 83 		
 84 		var il = this._super(data);
 85 		
 86 		if (render)
 87 		{
 88 			this.indexItemRenderFactory[il] = render; 
 89 		}
 90 		
 91 		this.clearSelectedIndices();
 92 		
 93 		this.createDivPlaceHolder(this.datasource.length-1,true); // -1 cause index start with 0
 94 		this.createItemRenderByIndex(this.datasource.length-1,true);
 95 			
 96 		return il;
 97 	},
 98 	
 99 	/**
100 	 * Adds item to the datasource. Results in rerender of the list
101 	 * 
102 	 * TODO: now every add results into instant render. we could optimize this
103 	 * by rendering everything at once.
104 	 * 
105 	 * @param {Array} items
106 	 */
107 	addItems : function(items)
108 	{
109 		var i;
110 		for (i=0, len= items.length; i < len; i++)
111 		{
112 			this.addItem(items[i]);
113 		}
114 	},
115 	
116 	/**
117 	 * At item at specific index. Results in rerender of the list
118 	 * 
119 	 * @param {Object} item
120 	 * @param {int} index
121 	 * @param {Banana.Controls.DataGridTileItemRender} render
122 	 * @param {Boolean} preventRender when true we wont render. useful whne adding multiple items at once
123 	 */
124 	addItemAt : function(item,index,render,preventRender)
125 	{
126 		this.applyUid(item);
127 				
128 		this.clearSelectedIndices();
129 		
130 		this.datasource.splice(index,0,item);
131 		this.indexDataMap.splice(index,0,item);
132 		
133 		if (render)
134 		{
135 			this.indexItemRenderFactory.splice(index,0,item);
136 		}
137 		
138 		//modify index helpers
139 		this.indexTilePlaceHolderMap.splice(index,0,null);
140 		this.indexItemRenderFactory.splice(index,0,null);
141 		this.indexRenderedItemRenderMap.splice(index,0,null);
142 		
143 		if (!preventRender)
144 		{
145 			this.setDataSource(this.datasource);
146 			this.triggerEvent('dataSourceChanged');
147 		}
148 	},
149 	
150 	/**
151 	 * At items at specific index. Results in rerender of the list
152 	 * 
153 	 * @param {Object} item
154 	 * @param {int} index
155 	 * @param {Banana.Controls.DataGridTileItemRender} render
156 	 */
157 	addItemsAt : function(items,index,render)
158 	{
159 		var i;
160 		for (i=0, len= items.length; i < len; i++)
161 		{
162 			this.addItemAt(items[i],index,render,true);
163 		}
164 		
165 		this.setDataSource(this.datasource); //force rerender
166 	},
167 	
168 	/**
169 	 * removes item if found in datasource
170 	 * @param {mixed} data
171 	 */
172 	removeItem : function(data)
173 	{
174 		var index = this.datasource.indexOf(data);
175 		
176 		if (index != -1)
177 		{
178 			this.removeItemRenderPlaceHolderByIndex(index);
179 		}
180 	},
181 	
182 	/**
183 	 * removes item from datagrid based on itemrender instance.
184 	 * @param {Banana.Controls.DataGridTileItemRender} itemRender
185 	 */
186 	removeItemByItemRender : function(itemRender)
187 	{
188 		var index = this.indexRenderedItemRenderMap.indexOf(itemRender);
189 		
190 		this.removeItemRenderPlaceHolderByIndex(index);
191 	},
192 	
193 	/**
194 	 * FIXME
195 	 * @ignore
196 	 */
197 	removeAllItems : function()
198 	{
199 		this._super();
200 	},
201 	
202 	/**
203 	 * @return {Banana.Controls.DataGridTileItemRender}
204 	 */
205 	getDefaultItemRender : function()
206 	{
207 		return this.defaultContentItemRender;
208 	},
209 	
210 	/**
211 	 * Use this to change item render on a specific index.
212 	 * By default the list render will rerender the new item render
213 	 * 
214 	 * @param {int} index
215 	 * @param {String} render
216 	 * @param {Boolean} dontCreate
217 	 * @param {Boolean} ignoreDataItemRenderMap
218 	 */
219 	setItemRenderByIndex : function(index,render,dontCreate,ignoreDataItemRenderMap)
220 	{
221 		this.indexItemRenderFactory[index] = render;
222 		
223 		//also save the item render data map relation
224 		//could be handy when we set an item render and later change the datasource
225 		//the item location could be changed then. itemrender - index relation is 
226 		//in that situation not enough
227 		if (!ignoreDataItemRenderMap && this.datasource && this.datasource[index])
228 		{
229 			//TODO this is the only place a mapping between data and render is set.
230 			//this should also be possible at a later moment.
231 			if (this.datasource[index][this.indexKey])
232 			{
233 				this.dataItemRenderMap.addItem(this.datasource[index][this.indexKey],render);
234 			}
235 		}
236 		
237 		if (!dontCreate)
238 		{
239 			this.clearItemRenderPlaceHolderByIndex(index);
240 			this.createItemRenderByIndex(index,true);
241 			//TODO only trigger when changed
242 			this.triggerEvent('itemRenderChanged');
243 		}		
244 	},
245 	
246 	/**
247 	 * Sets item renders on multi indices at once.
248 	 * 
249 	 * @param {Array} indices
250 	 * @param {Function} renderFactory of Banana.Controls.DataGridTileItemRender
251 	 */
252 	setItemRenderByIndices : function(indices,renderFactory)
253 	{
254 		if (!indices.length)
255 		{
256 			return;
257 		}
258 		
259 		//if we set more than 4 item renders we rerender whole list once. otherwise per item
260 		//TODO could be smarter. like somekind of percentage of total list count
261 		var dontRenderPerItem = (indices.length > 4);
262 
263 		var i;
264 		for (i = 0; i < indices.length; i++)
265 		{
266 			this.setItemRenderByIndex(indices[i],renderFactory,dontRenderPerItem);
267 		}
268 	
269 		if (dontRenderPerItem)
270 		{
271 			this.triggerEvent('itemRenderChanged');
272 			this.rerender();
273 		}
274 	},	
275 	
276 	/**
277 	 * Set the global item render for this list render.
278 	 * @param {Banana.Controls.DataGridTileItemRender} itemRender
279 	 */
280 	setItemRender : function(render)
281 	{
282 		//we set in general the item render. so we need to get rid of 
283 		//the data item render map.
284 		this.dataItemRenderMap.clear(); 
285 
286 		var j;
287 		for (j =0, clen = this.indexRenderedItemRenderMap.length; j < clen; j++)
288 		{	
289 			this.setItemRenderByIndex(j,render,true);
290 		}
291 		
292 		if (this.mainContainer)
293 		{
294 			this.mainContainer.clear();
295 			this.createItems();
296 			
297 			if (this.isRendered)
298 			{
299 				this.mainContainer.invalidateDisplay();
300 			}
301 		}
302 		else
303 		{
304 			this.defaultContentItemRender = render;
305 		}
306 	
307 		this.triggerEvent('itemRenderChanged');
308 	},	
309 	
310 	/**
311 	 * Rerenders item render by index. 
312 	 * @param {int} index
313 	 */
314 	rerenderIndex : function(index)
315 	{
316 		this.previousSelectedItems = this.getSelectedItems();
317 		this.indexTilePlaceHolderMap[index].clear();
318 		this.createItemRenderByIndex(index,true);
319 		this.setSelectedItems(this.previousSelectedItems);
320 	},
321 	
322 	/**
323 	 * gets rendered item render by data
324 	 * @param {Object} data
325 	 * @return {Banana.Controls.ItemRender}
326 	 * 
327 	 * @ignore
328 	 */
329 	getRenderedItemRenderByData : function(data)
330 	{
331 		//TODO we have no effective index from data to index.
332 	},
333 	
334 	/**
335 	 * @param {int} int
336 	 * @param {Banana.Controls.ItemRender} itemRender
337 	 * @return {boolean} 
338 	 */
339 	hasItemRenderAt : function(index,itemRender)
340 	{
341 		if (this.indexRenderedItemRenderMap[index] instanceof itemRender)
342 		{
343 			return true;
344 		}
345 		
346 		return false;
347 	},
348 	
349 	/**
350 	 * removes a placeholder by index
351 	 * 
352 	 * @param {int} index
353 	 */
354 	removeItemRenderPlaceHolderByIndex : function(index)
355 	{
356 		this.datasource.splice(index,1);
357 		
358 		this.indexTilePlaceHolderMap[index].remove();
359 		
360 		this.selectedIndices.remove(i);
361 		this.indexTilePlaceHolderMap.splice(index,1);
362 		this.indexItemRenderFactory.splice(index,1);
363 		this.indexRenderedItemRenderMap.splice(index,1);
364 		
365 		this.triggerEvent('dataSourceChanged');
366 	},
367 
368 	/**
369 	 * Empties placeholder by index
370 	 */
371 	clearItemRenderPlaceHolderByIndex : function(index)
372 	{
373 		this.indexTilePlaceHolderMap[index].clear();
374 	},
375 	
376 	/**
377 	 * @return {int}
378 	 */
379 	getIndexByItemRenderPlaceHolder : function(row)
380 	{
381 		return this.indexTilePlaceHolderMap.indexOf(row);
382 	},
383 	
384 	/**
385 	 * @param {int}
386 	 * @return {Banana.UiControl}
387 	 */
388 	getItemRenderPlaceHolderByIndex : function(index)
389 	{
390 		return this.indexTilePlaceHolderMap[index];
391 	},
392 
393 	/**
394 	 * @param {Banana.Controls.ItemRender}
395 	 * @return {Banana.UiControl}
396 	 */
397 	getItemRenderPlaceHolderByItemRender : function(ir)
398 	{
399 		return this.indexTilePlaceHolderMap[this.indexRenderedItemRenderMap.indexOf(ir)];
400 	},
401 	
402 	/**
403 	 * @return {int}
404 	 */
405 	getIndexByItemRender : function(ir)
406 	{
407 		return this.indexTilePlaceHolderMap.indexOf(ir.parent);
408 	},
409 	
410 	/**
411 	 * Invoked after changing datasource or invalidating 
412 	 * @ignore
413 	 */
414 	createControls : function()
415 	{
416 		this.setupIndexing();
417 		
418 		this.previousSelectedItems = this.getSelectedItems();
419 		
420 		this.selectedIndices.clear();
421 		this._super();
422 		
423 		this.mainContainer = new Banana.Controls.Panel();
424 	
425 		this.mainContainer.bind('mousedown',this.getProxy(function(e){
426 			// TODO: This is catching every event in the scope of the table
427 			// This should first check if user is clicking inside a input box
428 			// otherwise multiselects etc don't work
429 			if (e.originalEvent.shiftKey || e.originalEvent.ctrlKey)
430 			{
431 				e.preventDefault();
432 			}
433 		}));
434 		
435 		//FIXME this is not needed anymore?
436 		
437 		//if we gave a horizontal item count. only render when already rendered
438 		this.createItemsLater = false;
439 		
440 		if (this.horizontalItemCount && this.isRendered)
441 		{
442 			this.createItems();
443 		}
444 		else if (this.horizontalItemCount && !this.isRendered)
445 		{
446 			this.createItemsLater = true;
447 		}
448 		else
449 		{
450 			this.createItems();
451 		}
452 		
453 		this.addControl(this.mainContainer);	
454 		
455 		this.setSelectedItems(this.previousSelectedItems);
456 		
457 	},
458 	
459 	/**
460 	 * @ignore
461 	 */
462 	updateDisplay : function()
463 	{
464 		if (this.createItemsLater && this.getDemensions().width)
465 		{
466 			this.createItemsLater = false;
467 			this.createItems();
468 			
469 			this.mainContainer.invalidateDisplay();
470 		}
471 	},
472 	
473 	/**
474 	 * @ignore
475 	 */
476 	createItems : function()
477 	{
478 		if (this.datasource.length)
479 		{
480 			var i;
481 			for (i =0, len = this.datasource.length; i < len; i++)
482 			{
483 				this.applyUid(this.datasource[i]);
484 				
485 				this.createDivPlaceHolder(i);
486 				this.triggerEvent('onPreCreateItemRender',{'index':i,'data':this.datasource[i]});
487 				this.createItemRenderByIndex(i);
488 			}
489 			
490 			this.mainContainer.addControl("<div style='clear:both'></div>");
491 			this.cachedDemensions = null; //clear cache
492 		}	
493 	},
494 	
495 	/**
496 	 * @depricated
497 	 */
498 	setHorizontalTileCount : function(count)
499 	{
500 		log.warning("set horizontalTileCount datagridTileListRender is depricated. use setPlaceHolderWidth instead");	
501 		return this;
502 	},
503 	
504 	/**
505 	 * @depricated
506 	 */
507 	setTilePadding : function(padding)
508 	{
509 		log.warning("set tile padding in datagridTileListRender is depricated. apply style manualy in css");
510 		return this;
511 	},
512 	
513 	/**
514 	 * Creates a placeholder where itemrenders are rendered in
515 	 * 
516 	 * @param {int} int
517 	 * @param {Boolean} instantRender
518 	 * @ignore
519 	 */
520 	createDivPlaceHolder : function(index,instantRender)
521 	{	
522 		var tileplaceholder = new Banana.Controls.Panel();
523 		tileplaceholder.addCssClass("BDataGridTilePlaceHolder");
524 		
525 		if (this.placeHolderWidth)
526 		{
527 			tileplaceholder.setCss({width:this.placeHolderWidth});
528 		}
529 
530 		tileplaceholder.bind('mouseenter',this.getProxy(function(e){this.onRowMouseOver(e); return true;}),tileplaceholder);
531 		tileplaceholder.bind('mouseleave',this.getProxy(function(e){this.onRowMouseOut(e);return true;}),tileplaceholder);
532 		tileplaceholder.bind('click',this.getProxy(function(e){this.onRowMouseClick(e);return true;}),tileplaceholder);
533 			
534 		this.mainContainer.addControl(tileplaceholder,instantRender);
535 		
536 		if (instantRender)
537 		{
538 			this.mainContainer.invalidateDisplay();
539 		}	
540 		this.indexTilePlaceHolderMap[index] = tileplaceholder;	
541 	},
542 
543 	/**
544 	 * sets width of the placeholder. example "25%" or "50px" 
545 	 * @param {String} width
546 	 */
547 	setPlaceHolderWidth : function(width)
548 	{
549 		this.placeHolderWidth = width;
550 	},
551 	
552 	/**
553 	 * creates item render by index
554 	 * 
555 	 * Item render will be either from 
556 	 *  - data to item render map
557 	 *  - index to item render map
558 	 *  - or default one
559 	 *  
560 	 *  dont directly use this method. The item render is rendered into a 
561 	 *  placeholder which doesnt gets cleared here. 
562 	 *  to recreate a itemrender use rerenderIndex()
563 	 *  
564 	 *  @param {int} index
565 	 *  @param {boolean} instantRerender when true we directly render it
566 	 *  @ignore
567 	 */
568 	createItemRenderByIndex : function(index,instantRerender)
569 	{
570 		// Optionally can be accessed by e.g. invalidateDisplay to peek
571 		// at which index it is created
572 		this.currentIndexCreation = index;
573 		
574 		var itemRenderFactory = null;
575 		
576 		var key = this.datasource[index][this.indexKey];
577 		
578 		if (key  && this.dataItemRenderMap.getItem(key))
579 		{
580 			itemRenderFactory = this.dataItemRenderMap.getItem(key); 
581 		}
582 		else if (this.indexItemRenderFactory[index])
583 		{
584 			itemRenderFactory = this.indexItemRenderFactory[index];
585 		}
586 		else
587 		{
588 			itemRenderFactory = this.defaultContentItemRender;
589 		}
590 		
591 		var itemRender = this.getObject(itemRenderFactory);
592 
593 		itemRender.setData(this.datasource[index]);
594 
595 		itemRender.bind('dataChanged',this.getProxy(function(e,f){
596 			
597 			this.getDataSource()[parseInt(e.data, 10)] = e.currentTarget.getData();
598 			this.triggerEvent('dataSourceChanged');
599 			
600 		}),index.toString());
601 		
602 		//save mapping between itemRender and data
603 		this.dataItemRenderMap.addItem(this.datasource[index][this.indexKey],itemRenderFactory);
604 		
605 		this.indexRenderedItemRenderMap[index] = itemRender;
606 		itemRender.setListRender(this);
607 		var tilePlaceHolder = this.indexTilePlaceHolderMap[index];
608 		tilePlaceHolder.addControl(itemRender);
609 		
610 		if (instantRerender)
611 		{
612 			tilePlaceHolder.invalidateDisplay();
613 		}
614 		
615 		this.currentIndexCreation = undefined;
616 	},
617 	
618 	/**
619 	 * invoked after hovering over a tile
620 	 * @ignore
621 	 */
622 	onRowMouseOver : function(e)
623 	{
624 		var index = this.indexTilePlaceHolderMap.indexOf(e.data);
625 
626 		var itemRender = this.indexRenderedItemRenderMap[index];
627 		
628 		itemRender.mouseOver();
629 	},
630 	
631 	/**
632 	 * invoked after mouse moves out of a tile
633 	 * @ignore
634 	 */
635 	onRowMouseOut : function(e)
636 	{
637 		var index = this.indexTilePlaceHolderMap.indexOf(e.data);
638 		
639 		var itemRender = this.indexRenderedItemRenderMap[index];
640 		
641 		itemRender.mouseOut();
642 	},
643 	
644 	/**
645 	 * invoked after clicking on a row/tile
646 	 * @ignore
647 	 */
648 	onRowMouseClick : function(e)
649 	{
650 		var index = this.indexTilePlaceHolderMap.indexOf(e.data);
651 
652 		var itemRender = this.indexRenderedItemRenderMap[index];
653 		
654 		if (!itemRender.getIsSelectable())
655 		{
656 			return;
657 		}
658 
659 		if (index<0)
660 		{
661 			return;
662 		}
663 
664 		var ctrlKey = e.ctrlKey;
665 		var shiftKey = e.shiftKey;
666 
667 		if (this.getIndexIsSelected(index))
668 		{
669 			if (!ctrlKey && !shiftKey)
670 			{
671 				this.clearSelectedIndices();
672 			}
673 			else
674 			{
675 				this.clearSelectedIndex(index);
676 			}
677 		}
678 		else
679 		{
680 			if (this.selectionType === 'single')
681 			{
682 				this.clearSelectedIndices();
683 				this.addSelectedIndex(index);
684 			}
685 			else
686 			{
687 				if (!ctrlKey && !shiftKey)
688 				{
689 					this.clearSelectedIndices();
690 					this.addSelectedIndex(index);
691 				}
692 				if (ctrlKey)
693 				{
694 					this.addSelectedIndex(index);
695 				}
696 				if (shiftKey)
697 				{
698 
699 					var firstItem = parseInt(this.selectedIndices.getKeyByIndex(0), 10);
700 
701 					if (!firstItem)
702 					{
703 						firstItem = 0;
704 					}
705 
706 					var start;
707 					var end;
708 					if (index > firstItem)
709 					{
710 						start = firstItem;
711 						end = index;
712 					}
713 					else
714 					{
715 						start = index;
716 						end = parseInt(this.selectedIndices.getKeyByIndex([this.selectedIndices.getLength()-1]), 10);
717 					}
718 
719 					this.clearSelectedIndices();
720 
721 					var i;
722 					for (i = parseInt(start, 10); i <= parseInt(end, 10); i++)
723 					{
724 						this.addSelectedIndex(i);
725 					}
726 				}
727 
728 			}
729 		}	
730 		
731 		this.triggerEvent('onItemSelect');
732 	},
733 	
734 	/**
735 	 * Selects the index.  
736 	 * 
737 	 * @param {int} index
738 	 */
739 	selectIndex : function(index)
740 	{
741 		var ir = this.getRenderedItemRenderByIndex(index);
742 
743 		if (ir && typeof(ir.select) == 'function' && ir.getIsSelectable())
744 		{
745 			ir.select();
746 		}
747 	},
748 
749 	/**
750 	 * Selects the index.
751 	 * 
752 	 * @param {int} index
753 	 */
754 	deSelectIndex : function(index)
755 	{
756 		var ir = this.getRenderedItemRenderByIndex(index);
757 
758 		if (ir && typeof(ir.deselect) == 'function')
759 		{
760 			ir.deselect();
761 		}
762 	}
763 	
764 });