1 // ==========================================================================
  2 // Project:   The M-Project - Mobile HTML5 Application Framework
  3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
  4 // Creator:   Dominik
  5 // Date:      26.10.2010
  6 // License:   Dual licensed under the MIT or GPL Version 2 licenses.
  7 //            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
  8 //            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
  9 // ==========================================================================
 10 
 11 m_require('core/foundation/model.js');
 12 
 13 /**
 14  * @class
 15  *
 16  * M.View defines the prototype for any view within The M-Project. It implements lots of basic
 17  * properties and methods that are used in many derived views. M.View specifies a default
 18  * behaviour for functionalities like rendering, theming, delegating updates etc.
 19  *
 20  * @extends M.Object
 21  */
 22 M.View = M.Object.extend(
 23 /** @scope M.View.prototype */ {
 24 
 25     /**
 26      * The type of this object.
 27      *
 28      * @type String
 29      */
 30     type: 'M.View',
 31 
 32     /**
 33      * A boolean value to definitely recognize a view as a view, independent on its
 34      * concrete type, e.g. M.ButtonView or M.LabelView.
 35      *
 36      * @type Boolean
 37      */
 38     isView: YES,
 39 
 40     /**
 41      * The value property is a generic property for all values. Even if not all views
 42      * really use it, e.g. the wrapper views like M.ButtonGroupView, most of it do.
 43      *
 44      * @property {String}
 45      */
 46     value: null,
 47 
 48     /**
 49      * The path to a content that is bind to the view's value.
 50      *
 51      * @property {String}
 52      */
 53     contentBinding: null,
 54 
 55     /**
 56      * An array specifying the view's children.
 57      *
 58      * @type Array
 59      */
 60     childViews: null,
 61 
 62     /**
 63      * Indicates whether this view currently has the focus or not.
 64      *
 65      * @type Boolean
 66      */
 67     hasFocus: NO,
 68 
 69     /**
 70      * The id of the view used for the html attribute id. Every view gets its own unique
 71      * id during the rendering process.
 72      */
 73     id: null,
 74 
 75     /**
 76      * Indicates whether the view should be displayed inline or not. This property isn't
 77      * supported by all views, but e.g. by M.LabelView or M.ButtonView.
 78      */
 79     isInline: NO,
 80 
 81     /*
 82      * Indicates whether the view is currently enabled or disabled.
 83      */
 84     isEnabled: YES,
 85 
 86     /**
 87      * This property can be used to save a reference to the view's parent view.
 88      *
 89      * @param {Object}
 90      */
 91     parentView: null,
 92 
 93     /**
 94      * If a view represents a model, e.g. within a list view, this property is used to save
 95      * the model's id. So the view can be used to get to the record.
 96      *
 97      * @param {Object}
 98      */
 99     modelId: null,
100 
101     /**
102      * This property can be used to assign a css class to the view to get a custom styling.
103      *
104      * @type String
105      */
106     cssClass: null,
107 
108     /**
109      * This property can be used to assign a css style to the view. This allows you to
110      * create your custom styles inline.
111      *
112      * @type String
113      */
114     cssStyle: null,
115 
116     /**
117      * This property can be used to assign a css class to the view if an error occurs. The
118      * applying of this class is automatically triggered if the validation of the view
119      * goes wrong. This property is mainly used by input views, e.g. M.TextFieldView.
120      *
121      * @type String
122      */
123     cssClassOnError: null,
124 
125     /**
126      * This property can be used to assign a css class to the view on its initialization. This
127      * property is mainly used for input ui elements like text fields, that might have a initial
128      * value that should be rendered in a different style than the later value entered by the
129      * user. This property is mainly used by input views, e.g. M.TextFieldView.
130      *
131      * @type String
132      */
133     cssClassOnInit: null,
134 
135     /**
136      * This property is used internally to recursively build the pages html representation.
137      * It is once set within the render method and then eventually updated within the
138      * renderUpdate method.
139      *
140      * @type String
141      */
142     html: '',
143 
144     /**
145      * Determines whether an onChange event will trigger a defined action or not.
146      * This property is basically interesting for input ui elements, e.g. for
147      * text fields.
148      *
149      * @type Boolean
150      */
151     triggerActionOnChange: NO,
152 
153     /**
154      * Determines whether an onKeyUp event will trigger a defined action or not.
155      * This property is basically interesting for input ui elements, e.g. for
156      * text fields.
157      *
158      * @type Boolean
159      */
160     triggerActionOnKeyUp: NO,
161 
162     /**
163      * Determines whether an onKeyUp event with the enter button will trigger a defined
164      * action or not. This property is basically interesting for input ui elements, e.g.
165      * for text fields.
166      *
167      * @type Boolean
168      */
169     triggerActionOnEnter: NO,
170 
171     /**
172      * This method encapsulates the 'extend' method of M.Object for better reading of code syntax.
173      * It triggers the content binding for this view,
174      * gets an ID from and registers itself at the ViewManager.
175      *
176      * @param {Object} obj The mixed in object for the extend call.
177      */
178     design: function(obj) {
179         var view = this.extend(obj);
180         if(view.contentBinding) {
181             view.attachToObservable(view.contentBinding);
182         } else if(view.computedValue && view.computedValue.contentBinding) {
183             view.attachToObservable(view.computedValue.contentBinding);
184         }
185         view.id = M.Application.viewManager.getNextId();
186         M.Application.viewManager.register(view);
187         return view;
188     },
189 
190      /**
191      * This is the basic render method for any views. It does not specific rendering, it just calls
192      * renderChildViews method. Most views overwrite this method with a custom render behaviour.
193      * 
194      * @private
195      * @returns {String} The list item view's html representation.
196      */
197     render: function() {
198         this.renderChildViews();
199         return this.html;
200     },
201 
202     /**
203      * @interface
204      *
205      * This method defines an interface method for updating an already rendered html representation
206      * of a view. This should be implemented with a specific behaviour for any view.
207      */
208     renderUpdate: function() {
209 
210     },
211 
212     /**
213      * Triggers render() on all children. This method defines a basic behaviour for rendering a view's
214      * child views. If a custom behaviour for a view is desired, the view has to overwrite this method.
215      *
216      * @private
217      */
218     renderChildViews: function() {
219         if(this.childViews) {
220             var childViews = $.trim(this.childViews).split(' ');
221             for(var i in childViews) {
222                 if(this.type === 'M.PageView' && this[childViews[i]].type === 'M.TabBarView') {
223                     this.hasTabBarView = YES;
224                     this.tabBarView = this[childViews[i]];
225                 }
226                 this.html += this[childViews[i]].render();
227             }
228             return this.html;
229         }
230     },
231 
232     /**
233      * Clears the html property of a view and triggers the same method on all of its
234      * child views.
235      */
236     clearHtml: function() {
237         this.html = '';
238         if(this.childViews) {
239             var childViews = $.trim(this.childViews).split(' ');
240             for(var i in childViews) {
241                 this[childViews[i]].clearHtml();
242             }
243         }
244     },
245 
246     /**
247      * If the view's computedValue property is set, compute the value. This allows you to
248      * apply a method to a dynamically set value. E.g. you can provide your value with an
249      * toUpperCase().
250      */
251     computeValue: function() {
252         if(this.computedValue) {
253             this.value = this.computedValue.operation(this.computedValue.valuePattern ? this.value : this.computedValue.value, this);
254         }
255     },
256 
257     /**
258      * This method is a basic implementation for theming a view. It simply calls the
259      * themeChildViews method. Most views overwrite this method with a custom theming
260      * behaviour.
261      */
262     theme: function() {
263         this.themeChildViews();
264     },
265 
266     /**
267      * This method triggers the theme method on all children.
268      */
269     themeChildViews: function() {
270         if(this.childViews) {
271             var childViews = $.trim(this.childViews).split(' ');
272             for(var i in childViews) {
273                 this[childViews[i]].theme();
274             }
275         }
276     },
277 
278     /**
279      * The contentDidChange method is automatically called by the observable when the
280      * observable's state did change. It then updates the view's value property based
281      * on the specified content binding.
282      */
283     contentDidChange: function(){
284         var bindingPath = this.contentBinding ? this.contentBinding.split('.') : this.computedValue.contentBinding.split('.');
285         if(bindingPath && bindingPath.length === 3 && !(this.hasFocus && this.type === 'M.TextFieldView')) {
286             if(this.contentBinding) {
287                 this.value = eval(bindingPath[0])[bindingPath[1]][bindingPath[2]];
288             } else {
289                 this.computedValue.value = eval(bindingPath[0])[bindingPath[1]][bindingPath[2]];
290             }
291             this.renderUpdate();
292             this.delegateValueUpdate();
293         } else if(bindingPath && bindingPath.length === 3) {
294             return;
295         } else {
296             M.Logger.log('bindingPath not valid', M.WARN);
297         }
298     },
299 
300     /**
301      * This method attaches the view to an observable to be later notified once the observable's
302      * state did change.
303      *
304      * @param {String} contentBinding The path to the observable property.
305      */
306     attachToObservable: function(contentBinding) {
307         var bindingPath = contentBinding.split('.');
308         if(bindingPath && bindingPath.length === 3 && eval(bindingPath[0]) && eval(bindingPath[0])[bindingPath[1]]) {
309             eval(bindingPath[0])[bindingPath[1]].observable.attach(this, bindingPath[2]);
310             this.isObserver = YES;
311         } else {
312             M.Logger.log('bindingPath not valid', M.WARN);
313         }
314     },
315 
316     /**
317      * @interface
318      * 
319      * This method defines an interface method for setting the view's value from its DOM
320      * representation. This should be implemented with a specific behaviour for any view.
321      */
322     setValueFromDOM: function() {
323 
324     },
325 
326     /**
327      * This method delegates any value changes to a controller, if the 'contentBindingReverse'
328      * property is specified.
329      */
330     delegateValueUpdate: function() {
331         /* delegate value updates to a binded controller,
332            but only if the view currently is the master */
333         if(this.contentBindingReverse && this.hasFocus) {
334             var params = this.contentBindingReverse.split('.');
335             var controller = eval(params[0]);
336             controller[params[1]].set(params[2], this.value);
337         }
338     },
339 
340     /**
341      * @interface
342      *
343      * This method defines an interface method for styling the view. This should be
344      * implemented with a specific behaviour for any view.
345      */
346     style: function() {
347 
348     },
349 
350     /**
351      * This method is called whenever the view got the focus and basically only sets
352      * the view's hasFocus property to YES. If a more complex behaviour is desired,
353      * a view has to overwrite this method.
354      */
355     gotFocus: function() {
356         this.hasFocus = YES;
357     },
358 
359     /**
360      * This method is called whenever the view lost the focus and basically only sets
361      * the view's hasFocus property to NO. If a more complex behaviour is desired,
362      * a view has to overwrite this method.
363      */
364     lostFocus: function() {
365         this.hasFocus = NO;
366     },
367 
368     /**
369      * This method secure the passed string. It is mainly used for securing input elements
370      * like M.TextFieldView but since it is part of M.View it can be used and called out
371      * of any view.
372      *
373      * So far we only replace '<' and '>' with their corresponding html entity. The functionality
374      * of this method will be extended in the future. If a more complex behaviour is desired,
375      * any view using this method has to overwrite it.
376      *
377      * @param {String} str The string to be secured.
378      * @returns {String} The secured string.
379      */
380     secure: function(str) {
381         return str.replace(/</g, "<").replace(/>/g, ">");
382     },
383 
384     /**
385      * This method parses a given string, replaces any new line, '\n', with a line break, '<br/>',
386      * and returns the modified string. This can be useful especially for input views, e.g. it is
387      * used in context with the M.TextFieldView.
388      *
389      * @param {String} str The string to be modified.
390      * @returns {String} The modified string.
391      */
392     nl2br: function(str) {
393         if(str) {
394             if(typeof(str) !== 'string') {
395                 str = String(str);
396             }
397             return str.replace(/\n/g, '<br />\n');
398         }
399         return str;
400     },
401 
402     /**
403      * Adds a css class to the view's DOM representation.
404      *
405      * @param {String} cssClass The css class to be added.
406      */
407     addCssClass: function(cssClass) {
408         $('#' + this.id).addClass(cssClass);
409     },
410 
411     /**
412      * Removes a css class to the view's DOM representation.
413      *
414      * @param {String} cssClass The css class to be added.
415      */
416     removeCssClass: function(cssClass) {
417         $('#' + this.id).removeClass(cssClass);
418     },
419 
420     /**
421      * Adds or updates a css property to the view's DOM representation.
422      *
423      * @param {String} key The property's name.
424      * @param {String} value The property's value.
425      */
426     setCssProperty: function(key, value) {
427         $('#' + this.id).css(key, value);
428     },
429 
430     /**
431      * Removes a css property from the view's DOM representation.
432      *
433      * @param {String} key The property's name.
434      */
435     removeCssProperty: function(key) {
436         this.setCssProperty(key, '');
437     }
438 
439 });