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