1 // ========================================================================== 2 // Project: The M-Project - Mobile HTML5 Application Framework 3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved. 4 // (c) 2011 panacoda GmbH. All rights reserved. 5 // Creator: Dominik 6 // Date: 26.10.2010 7 // License: Dual licensed under the MIT or GPL Version 2 licenses. 8 // http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE 9 // http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE 10 // ========================================================================== 11 12 m_require('core/foundation/model.js'); 13 14 /** 15 * @class 16 * 17 * M.View defines the prototype for any view within The M-Project. It implements lots of basic 18 * properties and methods that are used in many derived views. M.View specifies a default 19 * behaviour for functionalities like rendering, theming, delegating updates etc. 20 * 21 * @extends M.Object 22 */ 23 M.View = M.Object.extend( 24 /** @scope M.View.prototype */ { 25 26 /** 27 * The type of this object. 28 * 29 * @type String 30 */ 31 type: 'M.View', 32 33 /** 34 * A boolean value to definitely recognize a view as a view, independent on its 35 * concrete type, e.g. M.ButtonView or M.LabelView. 36 * 37 * @type Boolean 38 */ 39 isView: YES, 40 41 /** 42 * The value property is a generic property for all values. Even if not all views 43 * really use it, e.g. the wrapper views like M.ButtonGroupView, most of it do. 44 * 45 * @property {String} 46 */ 47 value: null, 48 49 /** 50 * This property contains the relevant information about the view's computed value. In 51 * particular it is used to specify the pre-value, the content binding and the just- 52 * in-time performed operation, that computes the view's value. 53 * 54 * @property {Object} 55 */ 56 computedValue: null, 57 58 /** 59 * The path to a content that is bound to the view's value. If this content 60 * changes, the view will automatically be updated. 61 * 62 * @property {String} 63 */ 64 contentBinding: null, 65 66 /** 67 * The path to a content that is bound to the view's value (reverse). If this 68 * the view's value changes, the bound content will automatically be updated. 69 * 70 * @property {String} 71 */ 72 contentBindingReverse: null, 73 74 /** 75 * An array specifying the view's children. 76 * 77 * @type Array 78 */ 79 childViews: null, 80 81 /** 82 * Indicates whether this view currently has the focus or not. 83 * 84 * @type Boolean 85 */ 86 hasFocus: NO, 87 88 /** 89 * The id of the view used for the html attribute id. Every view gets its own unique 90 * id during the rendering process. 91 */ 92 id: null, 93 94 /** 95 * Indicates whether the view should be displayed inline or not. This property isn't 96 * supported by all views, but e.g. by M.LabelView or M.ButtonView. 97 */ 98 isInline: NO, 99 100 /* 101 * Indicates whether the view is currently enabled or disabled. 102 */ 103 isEnabled: YES, 104 105 /** 106 * This property can be used to save a reference to the view's parent view. 107 * 108 * @param {Object} 109 */ 110 parentView: null, 111 112 /** 113 * If a view represents a model, e.g. within a list view, this property is used to save 114 * the model's id. So the view can be used to get to the record. 115 * 116 * @param {Object} 117 */ 118 modelId: null, 119 120 /** 121 * This property can be used to assign a css class to the view to get a custom styling. 122 * 123 * @type String 124 */ 125 cssClass: null, 126 127 /** 128 * This property can be used to assign a css style to the view. This allows you to 129 * create your custom styles inline. 130 * 131 * @type String 132 */ 133 cssStyle: null, 134 135 /** 136 * This property can be used to assign a css class to the view if an error occurs. The 137 * applying of this class is automatically triggered if the validation of the view 138 * goes wrong. This property is mainly used by input views, e.g. M.TextFieldView. 139 * 140 * @type String 141 */ 142 cssClassOnError: null, 143 144 /** 145 * This property can be used to assign a css class to the view on its initialization. This 146 * property is mainly used for input ui elements like text fields, that might have a initial 147 * value that should be rendered in a different style than the later value entered by the 148 * user. This property is mainly used by input views, e.g. M.TextFieldView. 149 * 150 * @type String 151 */ 152 cssClassOnInit: null, 153 154 /** 155 * This property is used internally to recursively build the pages html representation. 156 * It is once set within the render method and then eventually updated within the 157 * renderUpdate method. 158 * 159 * @type String 160 */ 161 html: '', 162 163 /** 164 * Determines whether an onChange event will trigger a defined action or not. 165 * This property is basically interesting for input ui elements, e.g. for 166 * text fields. 167 * 168 * @type Boolean 169 */ 170 triggerActionOnChange: NO, 171 172 /** 173 * Determines whether an onKeyUp event will trigger a defined action or not. 174 * This property is basically interesting for input ui elements, e.g. for 175 * text fields. 176 * 177 * @type Boolean 178 */ 179 triggerActionOnKeyUp: NO, 180 181 /** 182 * Determines whether an onKeyUp event with the enter button will trigger a defined 183 * action or not. This property is basically interesting for input ui elements, e.g. 184 * for text fields. 185 * 186 * @type Boolean 187 */ 188 triggerActionOnEnter: NO, 189 190 /** 191 * This property is used to specify a view's events and their corresponding actions. 192 * 193 * @type Object 194 */ 195 events: null, 196 197 /** 198 * This property is used to specify a view's internal events and their corresponding actions. 199 * 200 * @private 201 * @type Object 202 */ 203 internalEvents: null, 204 205 /** 206 * This property specifies the recommended events for this type of view. 207 * 208 * @type Array 209 */ 210 recommendedEvents: null, 211 212 /** 213 * This method encapsulates the 'extend' method of M.Object for better reading of code syntax. 214 * It triggers the content binding for this view, 215 * gets an ID from and registers itself at the ViewManager. 216 * 217 * @param {Object} obj The mixed in object for the extend call. 218 */ 219 design: function(obj) { 220 var view = this.extend(obj); 221 view.id = M.ViewManager.getNextId(); 222 M.ViewManager.register(view); 223 224 view.attachToObservable(); 225 226 return view; 227 }, 228 229 /** 230 * This is the basic render method for any views. It does not specific rendering, it just calls 231 * renderChildViews method. Most views overwrite this method with a custom render behaviour. 232 * 233 * @private 234 * @returns {String} The list item view's html representation. 235 */ 236 render: function() { 237 this.renderChildViews(); 238 return this.html; 239 }, 240 241 /** 242 * @interface 243 * 244 * This method defines an interface method for updating an already rendered html representation 245 * of a view. This should be implemented with a specific behaviour for any view. 246 */ 247 renderUpdate: function() { 248 249 }, 250 251 /** 252 * Triggers render() on all children. This method defines a basic behaviour for rendering a view's 253 * child views. If a custom behaviour for a view is desired, the view has to overwrite this method. 254 * 255 * @private 256 */ 257 renderChildViews: function() { 258 if(this.childViews) { 259 var childViews = this.getChildViewsAsArray(); 260 for(var i in childViews) { 261 if(this[childViews[i]]) { 262 this[childViews[i]]._name = childViews[i]; 263 this.html += this[childViews[i]].render(); 264 } else { 265 this.childViews = this.childViews.replace(childViews[i], ' '); 266 M.Logger.log('There is no child view \'' + childViews[i] + '\' available for ' + this.type + ' (' + (this._name ? this._name + ', ' : '') + '#' + this.id + ')! It will be excluded from the child views and won\'t be rendered.', M.WARN); 267 } 268 269 if(this.type === 'M.PageView' && this[childViews[i]].type === 'M.TabBarView') { 270 this.hasTabBarView = YES; 271 this.tabBarView = this[childViews[i]]; 272 } 273 } 274 return this.html; 275 } 276 }, 277 278 /** 279 * This method is used internally for removing a view's child views both from DOM and the 280 * view manager. 281 * 282 * @private 283 */ 284 removeChildViews: function() { 285 var childViews = this.getChildViewsAsArray(); 286 for(var i in childViews) { 287 if(this[childViews[i]].childViews) { 288 this[childViews[i]].removeChildViews(); 289 } 290 this[childViews[i]].destroy(); 291 M.ViewManager.unregister(this[childViews[i]]); 292 } 293 $('#' + this.id).empty(); 294 }, 295 296 /** 297 * This method transforms the child views property (string) into an array. 298 * 299 * @returns {Array} The child views as an array. 300 */ 301 getChildViewsAsArray: function() { 302 return $.trim(this.childViews.replace(/\s+/g, ' ')).split(' '); 303 }, 304 305 /** 306 * This method creates and returns an associative array of all child views and 307 * their values. 308 * 309 * The key of an array item is the name of the view specified in the view 310 * definition. The value of an array item is the value of the corresponding 311 * view. 312 * 313 * @returns {Array} The child view's values as an array. 314 */ 315 getValues: function() { 316 var values = {}; 317 if(this.childViews) { 318 var childViews = this.getChildViewsAsArray(); 319 for(var i in childViews) { 320 if(Object.getPrototypeOf(this[childViews[i]]).hasOwnProperty('getValue')) { 321 values[childViews[i]] = this[childViews[i]].getValue(); 322 } 323 if(this[childViews[i]].childViews) { 324 var newValues = this[childViews[i]].getValues(); 325 for(var value in newValues) { 326 values[value] = newValues[value]; 327 } 328 } 329 } 330 } 331 return values; 332 }, 333 334 /** 335 * @interface 336 * 337 * This method defines an interface method for getting the view's value. This should 338 * be implemented for any view that offers a value and can be used within a form view. 339 */ 340 getValue: function() { 341 342 }, 343 344 /** 345 * This method creates and returns an associative array of all child views and 346 * their ids. 347 * 348 * The key of an array item is the name of the view specified in the view 349 * definition. The value of an array item is the id of the corresponding 350 * view. 351 * 352 * @returns {Array} The child view's ids as an array. 353 */ 354 getIds: function() { 355 var ids = {}; 356 if(this.childViews) { 357 var childViews = this.getChildViewsAsArray(); 358 for(var i in childViews) { 359 if(this[childViews[i]].id) { 360 ids[childViews[i]] = this[childViews[i]].id; 361 } 362 if(this[childViews[i]].childViews) { 363 var newIds = this[childViews[i]].getIds(); 364 for(var id in newIds) { 365 ids[id] = newIds[id]; 366 } 367 } 368 } 369 } 370 return ids; 371 }, 372 373 374 /** 375 * Clears the html property of a view and triggers the same method on all of its 376 * child views. 377 */ 378 clearHtml: function() { 379 this.html = ''; 380 if(this.childViews) { 381 var childViews = this.getChildViewsAsArray(); 382 for(var i in childViews) { 383 this[childViews[i]].clearHtml(); 384 } 385 } 386 }, 387 388 /** 389 * If the view's computedValue property is set, compute the value. This allows you to 390 * apply a method to a dynamically set value. E.g. you can provide your value with an 391 * toUpperCase(). 392 */ 393 computeValue: function() { 394 if(this.computedValue) { 395 this.value = this.computedValue.operation(this.computedValue.valuePattern ? this.value : this.computedValue.value, this); 396 } 397 }, 398 399 /** 400 * This method is a basic implementation for theming a view. It simply calls the 401 * themeChildViews method. Most views overwrite this method with a custom theming 402 * behaviour. 403 */ 404 theme: function() { 405 this.themeChildViews(); 406 }, 407 408 /** 409 * This method is responsible for registering events for view elements and its child views. It 410 * basically passes the view's event-property to M.EventDispatcher to bind the appropriate 411 * events. 412 */ 413 registerEvents: function() { 414 var externalEvents = {}; 415 for(var event in this.events) { 416 externalEvents[event] = this.events[event]; 417 } 418 419 if(this.internalEvents && externalEvents) { 420 for(var event in externalEvents) { 421 if(this.internalEvents[event]) { 422 this.internalEvents[event].nextEvent = externalEvents[event]; 423 delete externalEvents[event]; 424 } 425 } 426 M.EventDispatcher.registerEvents(this.id, this.internalEvents, this.recommendedEvents, this.type); 427 } else if(this.internalEvents) { 428 M.EventDispatcher.registerEvents(this.id, this.internalEvents, this.recommendedEvents, this.type); 429 } 430 431 if(externalEvents) { 432 M.EventDispatcher.registerEvents(this.id, externalEvents, this.recommendedEvents, this.type); 433 } 434 435 if(this.childViews) { 436 var childViews = this.getChildViewsAsArray(); 437 for(var i in childViews) { 438 this[childViews[i]].registerEvents(); 439 } 440 } 441 }, 442 443 /** 444 * This method triggers the theme method on all children. 445 */ 446 themeChildViews: function() { 447 if(this.childViews) { 448 var childViews = this.getChildViewsAsArray(); 449 for(var i in childViews) { 450 this[childViews[i]].theme(); 451 } 452 } 453 }, 454 455 /** 456 * The contentDidChange method is automatically called by the observable when the 457 * observable's state did change. It then updates the view's value property based 458 * on the specified content binding. 459 */ 460 contentDidChange: function(){ 461 var contentBinding = this.contentBinding ? this.contentBinding : (this.computedValue) ? this.computedValue.contentBinding : null; 462 463 if(!contentBinding) { 464 return; 465 } 466 467 var value = contentBinding.target; 468 var propertyChain = contentBinding.property.split('.'); 469 _.each(propertyChain, function(level) { 470 if(value) { 471 value = value[level]; 472 } 473 }); 474 475 if(value === undefined || value === null) { 476 M.Logger.log('The value assigned by content binding (property: \'' + contentBinding.property + '\') for ' + this.type + ' (' + (this._name ? this._name + ', ' : '') + '#' + this.id + ') is invalid!', M.WARN); 477 return; 478 } 479 480 if(this.contentBinding) { 481 this.value = value; 482 } else if(this.computedValue.contentBinding) { 483 this.computedValue.value = value; 484 } 485 486 this.renderUpdate(); 487 this.delegateValueUpdate(); 488 }, 489 490 /** 491 * This method attaches the view to an observable to be later notified once the observable's 492 * state did change. 493 */ 494 attachToObservable: function() { 495 var contentBinding = this.contentBinding ? this.contentBinding : (this.computedValue) ? this.computedValue.contentBinding : null; 496 497 if(!contentBinding) { 498 return; 499 } 500 501 if(typeof(contentBinding) === 'object') { 502 if(contentBinding.target && typeof(contentBinding.target) === 'object') { 503 if(contentBinding.property && typeof(contentBinding.property) === 'string') { 504 var propertyChain = contentBinding.property.split('.'); 505 if(contentBinding.target[propertyChain[0]] !== undefined) { 506 if(!contentBinding.target.observable) { 507 contentBinding.target.observable = M.Observable.extend({}); 508 } 509 contentBinding.target.observable.attach(this, contentBinding.property); 510 this.isObserver = YES; 511 } else { 512 M.Logger.log('The specified target for contentBinding for \'' + this.type + '\' (' + (this._name ? this._name + ', ' : '') + '#' + this.id + ')\' has no property \'' + contentBinding.property + '!', M.WARN); 513 } 514 } else { 515 M.Logger.log('The type of the value of \'action\' in contentBinding for \'' + this.type + '\' (' + (this._name ? this._name + ', ' : '') + '#' + this.id + ')\' is \'' + typeof(contentBinding.property) + ' but it must be of type \'string\'!', M.WARN); 516 } 517 } else { 518 M.Logger.log('No valid target specified in content binding \'' + this.type + '\' (' + (this._name ? this._name + ', ' : '') + '#' + this.id + ')!', M.WARN); 519 } 520 } else { 521 M.Logger.log('No valid content binding specified for \'' + this.type + '\' (' + (this._name ? this._name + ', ' : '') + '#' + this.id + ')!', M.WARN); 522 } 523 }, 524 525 /** 526 * @interface 527 * 528 * This method defines an interface method for setting the view's value from its DOM 529 * representation. This should be implemented with a specific behaviour for any view. 530 */ 531 setValueFromDOM: function() { 532 533 }, 534 535 /** 536 * This method delegates any value changes to a controller, if the 'contentBindingReverse' 537 * property is specified. 538 */ 539 delegateValueUpdate: function() { 540 /** 541 * delegate value updates to a bound controller, but only if the view currently is 542 * the master 543 */ 544 if(this.contentBindingReverse && this.hasFocus) { 545 this.contentBindingReverse.target.set(this.contentBindingReverse.property, this.value); 546 } 547 }, 548 549 /** 550 * @interface 551 * 552 * This method defines an interface method for styling the view. This should be 553 * implemented with a specific behaviour for any view. 554 */ 555 style: function() { 556 557 }, 558 559 /** 560 * This method is called whenever the view got the focus and basically only sets 561 * the view's hasFocus property to YES. If a more complex behaviour is desired, 562 * a view has to overwrite this method. 563 */ 564 gotFocus: function() { 565 this.hasFocus = YES; 566 }, 567 568 /** 569 * This method is called whenever the view lost the focus and basically only sets 570 * the view's hasFocus property to NO. If a more complex behaviour is desired, 571 * a view has to overwrite this method. 572 */ 573 lostFocus: function() { 574 this.hasFocus = NO; 575 }, 576 577 /** 578 * This method secure the passed string. It is mainly used for securing input elements 579 * like M.TextFieldView but since it is part of M.View it can be used and called out 580 * of any view. 581 * 582 * So far we only replace '<' and '>' with their corresponding html entity. The functionality 583 * of this method will be extended in the future. If a more complex behaviour is desired, 584 * any view using this method has to overwrite it. 585 * 586 * @param {String} str The string to be secured. 587 * @returns {String} The secured string. 588 */ 589 secure: function(str) { 590 return str.replace(/</g, "<").replace(/>/g, ">"); 591 }, 592 593 /** 594 * This method parses a given string, replaces any new line, '\n', with a line break, '<br/>', 595 * and returns the modified string. This can be useful especially for input views, e.g. it is 596 * used in context with the M.TextFieldView. 597 * 598 * @param {String} str The string to be modified. 599 * @returns {String} The modified string. 600 */ 601 nl2br: function(str) { 602 if(str) { 603 if(typeof(str) !== 'string') { 604 str = String(str); 605 } 606 return str.replace(/\n/g, '<br />'); 607 } 608 return str; 609 }, 610 611 /** 612 * This method parses a given string, replaces any tabulator, '\t', with four spaces, ' ', 613 * and returns the modified string. This can be useful especially for input views, e.g. it is 614 * used in context with the M.TextFieldView. 615 * 616 * @param {String} str The string to be modified. 617 * @returns {String} The modified string. 618 */ 619 tab2space: function(str) { 620 if(str) { 621 if(typeof(str) !== 'string') { 622 str = String(str); 623 } 624 return str.replace(/\t/g, ' '); 625 } 626 return str; 627 }, 628 629 /** 630 * @interface 631 * 632 * This method defines an interface method for clearing a view's value. This should be 633 * implemented with a specific behaviour for any input view. This method defines a basic 634 * functionality for clearing a view's value. This should be overwritten with a specific 635 * behaviour for most input view. What we do here is nothing but to call the cleaValue 636 * method for any child view. 637 */ 638 clearValue: function() { 639 640 }, 641 642 /** 643 * This method defines a basic functionality for clearing a view's value. This should be 644 * overwritten with a specific behaviour for most input view. What we do here is nothing 645 * but to call the cleaValue method for any child view. 646 */ 647 clearValues: function() { 648 if(this.childViews) { 649 var childViews = this.getChildViewsAsArray(); 650 for(var i in childViews) { 651 if(this[childViews[i]].childViews) { 652 this[childViews[i]].clearValues(); 653 } 654 if(typeof(this[childViews[i]].clearValue) === 'function'){ 655 this[childViews[i]].clearValue(); 656 } 657 } 658 } 659 this.clearValue(); 660 }, 661 662 /** 663 * Adds a css class to the view's DOM representation. 664 * 665 * @param {String} cssClass The css class to be added. 666 */ 667 addCssClass: function(cssClass) { 668 $('#' + this.id).addClass(cssClass); 669 }, 670 671 /** 672 * Removes a css class to the view's DOM representation. 673 * 674 * @param {String} cssClass The css class to be added. 675 */ 676 removeCssClass: function(cssClass) { 677 $('#' + this.id).removeClass(cssClass); 678 }, 679 680 /** 681 * Adds or updates a css property to the view's DOM representation. 682 * 683 * @param {String} key The property's name. 684 * @param {String} value The property's value. 685 */ 686 setCssProperty: function(key, value) { 687 $('#' + this.id).css(key, value); 688 }, 689 690 /** 691 * Removes a css property from the view's DOM representation. 692 * 693 * @param {String} key The property's name. 694 */ 695 removeCssProperty: function(key) { 696 this.setCssProperty(key, ''); 697 } 698 699 });