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 });