1 /**
  2  * @author Gillis Haasnoot <gillis.haasnoot@gmail.com>
  3  * @package Banana.Controls
  4  * @summary Dataset
  5  */
  6 
  7 goog.provide('Banana.Data.DataSet');
  8 
  9 /**
 10 @class Banana.Data
 11 @name Banana.Data
 12 */
 13 
 14 /** @namespace Banana.Data.DataSet.Validator  */
 15 namespace('Banana.Data').DataSet = Banana.Control.extend(
 16 /** @lends Banana.Data.DataSet.prototype */
 17 {
 18 	/**
 19 	 * Datasets are local storages to maintain data in one central place and provide all or parts of the data to controls.
 20 	 * All DataControls within banana have bindDataSet and bindDataSourceSet methods available to bind on a specific dataset.
 21 	 * 
 22 	 * it looks like this
 23 	 * 
 24 	 * Control.bindDataSet("datasetName","optionalProperty");  
 25 	 * 
 26 	 * the optionalProperty is used when you supply a complex object as data in your dataset.
 27 	 * For example
 28 	 * 
 29 	 * var data = {"value1":"foo","value2":"foo2"}
 30 	 * Control.bindDataSet("datasetName",value1);
 31 	 * 
 32 	 * Data can be of any type. Strings, Arrays or Deep nested arrays/objects.
 33 	 * 
 34 	 * Changing data in controls propagates back to the dataset to maintain consistent data.
 35 	 * There are 3 ways of getting data from the dataset
 36 	 * 1. getData() retreives the original data. changes in controls will not affect this data, unless commitControlData is used
 37 	 * 2. getControlData() retreives only data which have registered controls on it
 38 	 * 3. getChangedDataSet() retreives all data with the latest by controls modified data
 39 	 * 
 40 	 * When new data is inserted in the dataset all controls are getting updated with new data.
 41 	 * Sometimes you are using complex data like 
 42 	 * var data = {"value1":{'id':12,'name':'foo'}}
 43 	 * When you update the dataset with new data like
 44 	 * var data = {"value1":{'id':12,'name':'foo foo foo foo foo foo'}}
 45 	 * You want to keep the references pointing to the same origin. Dataset handles this for you by suppling a key with setDataKey() on the dataset
 46 	 * 
 47 	 * If new data is inserted only data which is changed will affect controls. This increases performance.
 48 	 * 
 49 	 * use dataset.clientUpdate to make data visible into the controls
 50 	 * @constructs
 51 	 * @extends Banana.Control
 52 	 */
 53 	init : function()
 54 	{
 55 		this.dataKey = 'id';
 56 		this.isChanged = false;
 57 		this.controlRegisterModified = false
 58 	},
 59 	
 60 	/**
 61 	 * @param {String} id sets the datakey which is used to maintain key value pointer references
 62 	 */
 63 	setDataKey : function(id)
 64 	{
 65 		this.dataKey = id;
 66 	},
 67 	
 68 	/**
 69 	 * Clears the dataset by removing internal data and unregisters all controls
 70 	 */
 71 	clear : function()
 72 	{
 73 		this.unregisterEvents();
 74 		this.registeredDataSourceControls = undefined;
 75 		this.registeredDataControls = undefined;
 76 		this.data = undefined;
 77 		this.oldData = undefined;
 78 	},
 79 
 80 	/**
 81 	 * we overwrite the bind function to detect if a user listens to controldatachaged events
 82 	 * if so, we register on every control attached to this dataset a listener.
 83 	 */
 84 	bind : function(name,func)
 85 	{
 86 		if (name == 'controlDataChanged')
 87 		{
 88 			this.enableControlChangeEvent = true;
 89 		}
 90 
 91 		return this._super(name,func);
 92 	},
 93 
 94 	/**
 95 	 * clears the data in all controls
 96 	 */
 97 	clearControlData : function()
 98 	{
 99 		for (id in this.registeredDataSourceControls)
100 		{
101 			var c = this.registeredDataSourceControls[id];
102 
103 			if (c.bindedDataSource)
104 			{
105 				if (c.bindedDataSource[1])
106 				{
107 					c.setDataSource(null,true);
108 				}
109 			}
110 		}
111 		for (id in this.registeredDataControls)
112 		{
113 			var c = this.registeredDataControls[id];
114 
115 			if (c.bindedData)
116 			{
117 				if (c.bindedData[1])
118 				{
119 					c.setData(null,true);
120 				}
121 			}
122 		}
123 
124 		this.oldData = undefined;
125 		jQuery(this).trigger('dataChanged',this.data); //is this a good place????
126 	},
127 		
128 	/**
129 	 * sets data in dataset
130 	 * We check if previous data is changed compared to new data.
131 	 * if so we copy all new properties to the old data, to make sure our object references stay intact.
132 	 * this is specialy usefull to prevent changes between items
133 	 *
134 	 * so that only changed controls are getting updated
135 	 * @param {mixed} d data
136 	 * @param {boolean} ignoreEqualCheck when true we dont check if new data is equal to old.
137 	 * @return {this} 
138 	 */
139 	setData : function(d,ignoreEqualCheck)
140 	{
141 		this.allowControlUpdate = true;
142 
143 		//when new controls are added to this dataset we always update
144 		//if this.data and new data are the same, we just trigger change event
145 		if (this.data != d && this.data && !this.controlRegisterModified)
146 		{
147 			//if the datas are not the same we copy references, otherwise we do nothing
148 			if (ignoreEqualCheck || !Banana.Util.ObjectsAreEqual(d,this.data))
149 			{
150 				Banana.Util.CopyTo(d,this.data,this.dataKey);
151 				
152 				this.triggerEvent('dataChanged');
153 			}
154 			else
155 			{
156 				this.allowControlUpdate = false;
157 				this.triggerEvent('dataUnchanged');
158 			}
159 		}
160 		else
161 		{
162 			this.data = d;
163 			this.triggerEvent('dataChanged');
164 		}
165 		return this;
166 	},
167 
168 	/**
169 	 * resets control to original data
170 	 */
171 	resetControls : function()
172 	{
173 		if (!this.getData())
174 		{
175 			return;
176 		}
177 		
178 		this.allowControlUpdate =true;
179 		this.clientUpdate();
180 	},
181 	
182 	/**
183 	 * returns data in dataset
184 	 * always returns most up to date data
185 	 * 
186 	 * @return {mixed} data
187 	 */
188 	getData : function()
189 	{
190 		return this.data;
191 	},
192 	
193 	/**
194 	 * @return {boolean} true when data is changed. 
195 	 */
196 	getIsChanged : function()
197 	{
198 		return this.isChanged;
199 	},
200 	
201 	/**
202 	 * updates all registered controls with the current dataset data
203 	 */
204 	clientUpdate : function()
205 	{
206 		if (!this.allowControlUpdate)
207 		{
208 			return false;
209 		}
210 
211 		for (id in this.registeredDataSourceControls)
212 		{
213 			var c = this.registeredDataSourceControls[id];
214 			
215 			if (c.bindedDataSource)
216 			{
217 				this.setControlDataSource(c);
218 			}	
219 		}
220 
221 		for (id in this.registeredDataControls)
222 		{
223 			var c = this.registeredDataControls[id];
224 
225 			if (c.bindedData)
226 			{
227 				if (c.bindedData[1])
228 				{
229 					this.setControlData(c);
230 				}
231 			}
232 		}
233 		this.controlRegisterModified = false;
234 		this.isChanged = false;
235 	},
236 
237 	/**
238 	 * Retreives the data from all registered data controls.
239 	 * Besides the data we also have the original data and isDataChanged boolean
240 	 * @param {flattened} when true we just return a key value object 
241 	 * @return {object} 
242 	 */
243 	getControlData : function(flattened)
244 	{
245 		var d = {};
246 
247 		for (var id in this.registeredDataControls)
248 		{
249 			var c = this.registeredDataControls[id];
250 			var data = c.getData();
251 			if (!(c instanceof Banana.Controls.DataControl)) continue;
252 
253 			if (flattened)
254 			{
255 				if (data === undefined)
256 				{
257 					cd = null
258 				}
259 				else
260 				{
261 					cd = data;
262 				}
263 			}
264 			else
265 			{
266 				var cd = {};
267 
268 				cd['originalData'] = null;
269 				cd['isDataChanged'] = true;
270 
271 				if (this.getData())
272 				{
273 					 cd['originalData'] = this.getDataByPath(c.bindedData[1]) || null;
274 					 cd['isDataChanged']= this.getDataByPath(c.bindedData[1]) != data;
275 				}
276 
277 				if (data === undefined)
278 				{
279 					cd['data'] = null;
280 				}
281 				else
282 				{
283 					cd['data'] = data;
284 				}
285 			}
286 			//we want to ensure datastructures
287 			this.ensureObjectFromPath(d,c.bindedData[1],cd);
288 
289 		}
290 		return d;
291 	},
292 
293 	/**
294 	 * Get the dataset with changed values from input fields
295 	 *
296 	 * @return {mixed} The dataset
297 	 */
298 	getChangedDataSet: function()
299 	{
300 		var d = this.getData();
301 		if (!d)
302 			d = {};	
303 
304 		for (var id in this.registeredDataControls)
305 		{
306 			var c = this.registeredDataControls[id];
307 			if (!(c instanceof Banana.Controls.DataControl)) continue;
308 	
309 			//if bindedfield is like xxx.yyy.zzz we make sure that the data is in same path
310 			var field = c.bindedData[1];
311 			var sfield = field.split('.');
312 
313 			this.setDataByPath(c.getData(),field);
314 		}
315 		
316 		return d;
317 	},
318 
319 	/**
320 	 * Persist the data from the controls to the dataset.
321 	 * This function should be used after a successful save to
322 	 * reset the dataChanged flags.
323 	 */
324 	commitControlData : function()
325 	{
326 		var d = {};
327 		for (var id in this.registeredDataControls)
328 		{
329 			var c = this.registeredDataControls[id];
330 			if (!(c instanceof Banana.Controls.DataControl)) continue;
331 
332 			d = this.ensureObjectFromPath(d,c.bindedData[1],c.getData());
333 		}
334 
335 		this.setData(d);
336 		return this;
337 	},
338 
339 	/**
340 	 * ensures an object contains properties acording to dot limited path
341 	 * additionaly possible to insert data at last property
342 	 *
343 	 * @param {object} obj
344 	 * @param {string} path i.e test.sub.subsu
345 	 * @param {object} props to insert at last property
346 	 * @param {string} refpath used by function
347 	 * @param {object} refObj used by function
348 	 *
349 	 */
350 	ensureObjectFromPath : function(obj,path,props,refpath,refObj)
351 	{
352 		if (!path)
353 		{
354 			refObj[refpath] = props;
355 			return;
356 		}
357 
358 		var split = path.split('.');
359 
360 		refObj = obj;
361 
362 		if (!obj[split[0]])
363 		{
364 			obj[split[0]] = {};
365 		}
366 
367 		var s = split[0];
368 		split.splice(0,1)
369 		refpath = path;
370 		var path = split.join('.');
371 
372 		this.ensureObjectFromPath(obj[s],path,props,refpath,refObj)
373 
374 		return obj;
375 	},
376 
377 	/**
378 	 * sets data on control from dataset data
379 	 *
380 	 * @param {Banana.Controls.DataControl} c
381 	 * @param {mixed} d
382 	 */
383 	setControlData : function(c,d)
384 	{
385 		var data = (this.getData() === null) ? null : this.getDataByPath(c.bindedData[1]);
386 
387 		c.setData(data );
388 		c.triggerEvent('controlDataBinded',data);
389 	},
390 
391 	/**
392 	 * sets datasource data on control from dataset data
393 	 *
394 	 * @param {Banana.Controls.DataControl} c
395 	 */
396 	setControlDataSource : function(c)
397 	{
398 		if (c.bindedDataSource[1])
399 		{
400 			var d = this.getDataByPath(c.bindedDataSource[1]);
401 
402 			c.setDataSource(d);
403 		}
404 		else
405 		{
406 			c.setDataSource(this.getData());
407 		}
408 		c.triggerEvent('controlDataSourceBinded');
409 	},
410 
411 	/**
412 	 * @param {String} path
413 	 * @returns {mixed} 
414 	 */
415 	getDataByPath : function(path)
416 	{
417 		path = path.split('.');
418 
419 		var d = this.getData();
420 
421 		if (!d) return d;
422 		
423 		for(var i=0,len = path.length; i < len; i++)
424 		{
425 			d = d[path[i]];
426 			if (d == undefined)
427 			{
428 				return null;
429 			}
430 		}
431 
432 		return d;
433 	},
434 	
435 	/**
436 	 * sets data by path
437 	 * 
438 	 * @param {mixed} data
439 	 * @param {String} path
440 	 */
441 	setDataByPath : function(data,path)
442 	{
443 		path = path.split('.');
444 		var d = this.getData();	
445 		var refPath = null;
446 		
447 		if (path.length == 1)
448 		{
449 			refPath = path;
450 		}
451 		//we have a path like xxx.yyy.zzz with data assigned to zzz
452 		//we fetch the zzz part out of it and assign the data to it
453 		else
454 		{		
455 			var d = this.getData();	
456 			var refPath = null;
457 			
458 			for(var i=0,len = path.length; i < len-1; i++)
459 			{
460 				d = d[path[i]];
461 			}
462 			
463 			//our reference path is the last one in given path
464 			refPath = path[path.length-1];
465 		}
466 		
467 		d[refPath] = data;
468 
469 		return d;
470 	},
471 	
472 	/**
473 	 * binds data from dataset to datasource in control
474 	 *
475 	 * @param {Banana.Controls.DataControl} c
476 	 */
477 	bindControlToDataSource : function(c)
478 	{
479 		this.controlRegisterModified =true;
480 
481 		if (!this.registeredDataSourceControls)
482 		{
483 			this.registeredDataSourceControls = {};
484 		}
485 
486 		if (!this.registeredDataSourceControls[c.getId()])
487 		{
488 			if (this.getData())
489 			{
490 				this.setControlDataSource(c);
491 			}
492 
493 			this.registeredDataSourceControls[c.getId()] = c;
494 		}
495 
496 	},
497 
498 	/**
499 	 * binds data from dataset to data in control
500 	 *
501 	 * @param {Banana.Controls.DataControl} c
502 	 */
503 	bindControlToData : function(c)
504 	{
505 		this.controlRegisterModified =true;
506 
507 		if (!this.registeredDataControls)
508 		{
509 			this.registeredDataControls = {};
510 		}
511 
512 		if (!this.registeredDataControls[c.getId()])
513 		{
514 			if (this.getData())
515 			{
516 				this.setControlData(c);
517 			}
518 
519 			if (this.enableControlChangeEvent)
520 			{
521 
522 				c.bind('dataChanged',this.getProxy(this.controlChangedHandler))
523 
524 			}
525 
526 			this.registeredDataControls[c.getId()] = c;
527 		}
528 
529 	},
530 
531 	/**
532 	 * handler for change events.
533 	 * checks current changed control and store changed status in an array
534 	 * this is needed to track changes of controls.
535 	 * only when all checked controls are equal to original data we fire a restoreevent. unaffected controls are playing no
536 	 * role in the descision of firing the change or restore event
537 	 *
538 	 * TODO needs to be improved. Could slow down things a bit
539 	 *
540 	 * @param Event e
541 	 */
542 	controlChangedHandler : function(e)
543 	{
544 		if (!this.controlChecks) {this.controlChecks ={};}
545 
546 		var c = e.currentTarget;
547 		var orgData = (this.getData() === null) ? '' : this.getDataByPath(c.bindedData[1]);
548 		orgData = orgData || '';
549 
550 		if (orgData != e.currentTarget.data)
551 		{
552 			this.controlChecks[e.currentTarget.id] = false;
553 		}
554 		else
555 		{
556 			this.controlChecks[e.currentTarget.id] = true;
557 		}
558 
559 		for (index in this.controlChecks)
560 		{
561 			if (this.controlChecks[index] == false)
562 			{
563 				this.triggerEvent('controlDataChanged');
564 				return;
565 			}
566 		}
567 
568 		this.triggerEvent('controlDataRestored');
569 		return;
570 	},
571 
572 
573 	/**
574 	 * unbinds controls from this dataset
575 	 *
576 	 * @param {Banana.Controls.DataControl} c
577 	 */
578 	unBindControl : function(c)
579 	{
580 		this.controlRegisterModified =true;
581 
582 		delete this.registeredDataControls[c.getId()];
583 	},
584 
585 	/**
586 	 * unbinds control from this dataset
587 	 *
588 	 * @param {Banana.Controls.DataControl} c
589 	 */
590 	unBindDataSourceControl : function(c)
591 	{
592 		if (!this.registeredDataSourceControls)
593 		{
594 			return;
595 		}
596 
597 		this.controlRegisterModified =true;
598 		delete this.registeredDataSourceControls[c.getId()];
599 	}
600 });