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:   Sebastian
  6 // Date:      04.11.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 /**
 13  * A constant value for input type: text
 14  *
 15  * @type String
 16  */
 17 M.INPUT_TEXT = 'text';
 18 
 19 /**
 20  * A constant value for input type: password
 21  *
 22  * @type String
 23  */
 24 M.INPUT_PASSWORD = 'password';
 25 
 26 /**
 27  * A constant value for input type: number
 28  *
 29  * @type String
 30  */
 31 M.INPUT_NUMBER = 'number';
 32 
 33 /**
 34  * A constant value for input type: tel
 35  *
 36  * @type String
 37  */
 38 M.INPUT_TELEPHONE = 'tel';
 39 
 40 /**
 41  * A constant value for input type: url
 42  *
 43  * @type String
 44  */
 45 M.INPUT_URL = 'url';
 46 
 47 /**
 48  * A constant value for input type: email
 49  *
 50  * @type String
 51  */
 52 M.INPUT_EMAIL = 'email';
 53 
 54 /**
 55  * A constant value for input type: time
 56  *
 57  * @type String
 58  */
 59 M.INPUT_TIME = 'time';
 60 
 61 /**
 62  * A constant value for input type: date
 63  *
 64  * @type String
 65  */
 66 M.INPUT_DATE = 'date';
 67 
 68 /**
 69  * A constant value for input type: month
 70  *
 71  * @type String
 72  */
 73 M.INPUT_MONTH = 'month';
 74 
 75 /**
 76  * A constant value for input type: week
 77  *
 78  * @type String
 79  */
 80 M.INPUT_WEEK = 'week';
 81 
 82 /**
 83  * A constant value for input type: datetime
 84  *
 85  * @type String
 86  */
 87 M.INPUT_DATETIME = 'datetime';
 88 
 89 /**
 90  * A constant value for input type: datetime-local
 91  *
 92  * @type String
 93  */
 94 M.INPUT_DATETIME_LOCAL = 'datetime-local';
 95 
 96 /**
 97  * @class
 98  *
 99  * M.TextFieldView is the prototype of any text field input view. It can be rendered as both
100  * a single line text field and a multiple line text field. If it is styled as a multiple
101  * line text field, is has a built-in autogrow mechanism so the textfield is getting larger
102  * depending on the number of lines of text a user enters.
103  *
104  * @extends M.View
105  */
106 M.TextFieldView = M.View.extend(
107 /** @scope M.TextFieldView.prototype */ {
108 
109    /**
110      * The type of this object.
111      *
112      * @type String
113      */
114     type: 'M.TextFieldView',
115 
116    /**
117     * The name of the text field. During the rendering, this property gets assigned to the name
118     * property of the text field's html representation. This can be used to manually access the
119     * text field's DOM representation later on.
120     *
121     * @type String
122     */
123     name: null,
124 
125     /**
126      * The label proeprty defines a text that is shown above or next to the textfield as a 'title'
127      * for the textfield. e.g. "Name:". If no label is specified, no label will be displayed.
128      *
129      * @type String
130      */
131     label: null,
132 
133     /**
134      * The initial text shown inside the text field describing the input or making a suggestion for input
135      * e.g. "Please enter your Name."
136      *
137      * @type String
138      */
139     initialText: '',
140 
141     /**
142      * Determines whether to display the textfield grouped with the label specified with the label property.
143      * If set to YES, the textfield and its label are wrapped in a container and styled as a unit 'out of
144      * the box'. If set to NO, custom styling could be necessary.
145      *
146      * If there is no label specified, this property is ignored by default.
147      *
148      * @type Boolean
149      */
150     isGrouped: NO,
151 
152     /**
153      * Defines whether the text field has multiple lines respectively is a text area.
154      *
155      * @type Boolean
156      */
157     hasMultipleLines: NO,
158 
159     /**
160      * This property specifies the input type of this input field. Possible values are:
161      *
162      *   - M.INPUT_TEXT --> text input (default)
163      *   - M.INPUT_PASSWORD --> password
164      *   - M.INPUT_NUMBER --> number
165      *   - M.INPUT_TELEPHONE --> tel
166      *   - M.INPUT_URL --> url
167      *   - M.INPUT_EMAIL --> email
168      *
169      * Note, that these types are not yet supported by all browsers!
170      *
171      * @type String
172      */
173     inputType: M.INPUT_TEXT,
174 
175     /**
176      * This property is used internally to determine all the possible input types for a
177      * date textfield.
178      *
179      * @private
180      * @type Array
181      */
182     dateInputTypes: [M.INPUT_DATETIME, M.INPUT_DATE, M.INPUT_MONTH, M.INPUT_WEEK, M.INPUT_TIME, M.INPUT_DATETIME_LOCAL],
183 
184     /**
185      * This property can be used to specify the allowed number if chars for this text field
186      * view. If nothing is specified, the corresponding 'maxlength' HTML property will not
187      * be set.
188      *
189      * @type Number
190      */
191     numberOfChars: null,
192 
193     /**
194      * This property can be used to specify whether to use the native implementation
195      * of one of the HTML5 input types if it is available. If set to YES, e.g. iOS5
196      * will render its own date/time picker controls to the corresponding input
197      * type. If set to no, the native implementation will be disabled.
198      *
199      * @type Boolean
200      */
201     useNativeImplementationIfAvailable: YES,
202 
203     /**
204      * This property specifies the recommended events for this type of view.
205      *
206      * @type Array
207      */
208     recommendedEvents: ['focus', 'blur', 'enter', 'keyup', 'tap'],
209 
210     /**
211      * Renders a TextFieldView
212      * 
213      * @private
214      * @returns {String} The text field view's html representation.
215      */
216     render: function() {
217         this.computeValue();
218         this.html += '<div';
219 
220         if(this.label && this.isGrouped) {
221             this.html += ' data-role="fieldcontain"';
222         }
223 
224         if(this.cssClass) {
225             this.html += ' class="' + this.cssClass + '_container"';
226         }
227 
228         this.html += '>';
229 
230         if(this.label) {
231             this.html += '<label for="' + (this.name ? this.name : this.id) + '">' + this.label + '</label>';
232         }
233 
234         if(this.hasMultipleLines) {
235             this.html += '<textarea cols="40" rows="8" name="' + (this.name ? this.name : this.id) + '" id="' + this.id + '"' + this.style() + '>' + (this.value ? this.value : this.initialText) + '</textarea>';
236             
237         } else {
238             var type = this.inputType;
239             if(_.include(this.dateInputTypes, this.inputType) && !this.useNativeImplementationIfAvailable) {
240                 type = 'text';
241             }
242             
243             this.html += '<input ' + (this.numberOfChars ? 'maxlength="' + this.numberOfChars + '"' : '') + 'type="' + type + '" name="' + (this.name ? this.name : this.id) + '" id="' + this.id + '"' + this.style() + ' value="' + (this.value ? this.value : this.initialText) + '" />';
244         }
245 
246         this.html += '</div>';
247 
248         return this.html;
249     },
250 
251     /**
252      * This method is responsible for registering events for view elements and its child views. It
253      * basically passes the view's event-property to M.EventDispatcher to bind the appropriate
254      * events.
255      *
256      * It extend M.View's registerEvents method with some special stuff for text field views and
257      * their internal events.
258      */
259     registerEvents: function() {
260         this.internalEvents = {
261             focus: {
262                 target: this,
263                 action: 'gotFocus'
264             },
265             blur: {
266                 target: this,
267                 action: 'lostFocus'
268             },
269             keyup: {
270                 target: this,
271                 action: 'setValueFromDOM'
272             },
273             tap: {
274                 target: this,
275                 action: 'handleTap'
276             }
277         }
278         this.bindToCaller(this, M.View.registerEvents)();
279     },
280 
281     /**
282      * The contentDidChange method is automatically called by the observable when the
283      * observable's state did change. It then updates the view's value property based
284      * on the specified content binding.
285      *
286      * This is a special implementation for M.TextFieldView.
287      */
288     contentDidChange: function(){
289         /* if the text field has the focus, we do not apply the content binding */
290         if(this.hasFocus) {
291             return;
292         }
293 
294         /* let M.View do the real job */
295         this.bindToCaller(this, M.View.contentDidChange)();
296 
297         this.renderUpdate();
298         this.delegateValueUpdate();
299     },
300 
301     /**
302      * Updates a TextFieldView with DOM access by jQuery.
303      *
304      * @param {Boolean} preventValueComputing Determines whether to execute computeValue() or not.
305      * @private
306      */
307     renderUpdate: function(preventValueComputing) {
308         if(!preventValueComputing) {
309             this.computeValue();
310         }
311         $('#' + this.id).val(this.value);
312         this.styleUpdate();
313     },
314 
315     /**
316      * This method is called whenever the view is taped/clicked. Typically a text
317      * field view would not use a tap event. But since a tap is called before the
318      * focus event, we use this to do some input type depending stuff, e.g. show
319      * a date picker.
320      *
321      * @param {String} id The DOM id of the event target.
322      * @param {Object} event The DOM event.
323      * @param {Object} nextEvent The next event (external event), if specified.
324      */
325     handleTap: function(id, event, nextEvent) {
326         if(_.include(this.dateInputTypes, this.inputType) && (!M.Environment.supportsInputType(this.inputType) || !this.useNativeImplementationIfAvailable)) {
327             M.DatePickerView.show({
328                 source: this,
329                 useSourceDateAsInitialDate: YES,
330                 showDatePicker: (this.inputType !== M.INPUT_TIME),
331                 showTimePicker: (this.inputType === M.INPUT_TIME || this.inputType === M.INPUT_DATETIME || this.inputType === M.INPUT_DATETIME_LOCAL),
332                 dateOrder: (this.inputType === M.INPUT_MONTH ? M.DatePickerView.dateOrderMonthOnly : M.DatePickerView.dateOrder),
333                 dateFormat: (this.inputType === M.INPUT_MONTH ? M.DatePickerView.dateFormatMonthOnly : M.DatePickerView.dateFormat)
334             });
335         }
336 
337         if(nextEvent) {
338             M.EventDispatcher.callHandler(nextEvent, event, YES);
339         }
340     },
341 
342     /**
343      * This method is called whenever the view gets the focus.
344      * If there is a initial text specified and the value of this text field
345      * still equals this initial text, the value is emptied.
346      *
347      * @param {String} id The DOM id of the event target.
348      * @param {Object} event The DOM event.
349      * @param {Object} nextEvent The next event (external event), if specified.
350      */
351     gotFocus: function(id, event, nextEvent) {
352         if(this.initialText && (!this.value || this.initialText === this.value)) {
353             this.setValue('');
354             if(this.cssClassOnInit) {
355                 this.removeCssClass(this.cssClassOnInit);
356             }
357         }
358         this.hasFocus = YES;
359 
360         if(nextEvent) {
361             M.EventDispatcher.callHandler(nextEvent, event, YES);
362         }
363     },
364 
365     /**
366      * This method is called whenever the view lost the focus.
367      * If there is a initial text specified and the value of this text field
368      * is empty, the value is set to the initial text.
369      *
370      * @param {String} id The DOM id of the event target.
371      * @param {Object} event The DOM event.
372      * @param {Object} nextEvent The next event (external event), if specified.
373      */
374     lostFocus: function(id, event, nextEvent) {
375         if(this.initialText && !this.value) {
376             this.setValue(this.initialText, NO);
377             this.value = '';
378             if(this.cssClassOnInit) {
379                 this.addCssClass(this.cssClassOnInit);
380             }
381         }
382         this.hasFocus = NO;
383 
384         if(nextEvent) {
385             M.EventDispatcher.callHandler(nextEvent, event, YES);
386         }
387     },
388 
389     /**
390      * Method to append css styles inline to the rendered text field.
391      *
392      * @private
393      * @returns {String} The text field's styling as html representation.
394      */
395     style: function() {
396         var html = ' style="';
397         if(this.isInline) {
398             html += 'display:inline;';
399         }
400         html += '"';
401 
402         if(!this.isEnabled) {
403             html += ' disabled="disabled"';
404         }
405         
406         if(this.cssClass) {
407             html += ' class="' + this.cssClass + '"';
408         }
409 
410         return html;
411     },
412 
413     /**
414      * Triggers the rendering engine, jQuery mobile, to style the text field.
415      *
416      * @private
417      */
418     theme: function() {
419         if(this.initialText && !this.value && this.cssClassOnInit) {
420             this.addCssClass(this.cssClassOnInit);
421         }
422 
423         /* trigger keyup event to make the text field autogrow */
424         if(this.value) {
425             $('#'  + this.id).trigger('keyup');
426         }
427     },
428 
429     /**
430      * Method to append css styles inline to the rendered view on the fly.
431      *
432      * @private
433      */
434     styleUpdate: function() {
435         /* trigger keyup event to make the text field autogrow (enable fist, if necessary) */
436         if(this.value) {
437             $('#' + this.id).removeAttr('disabled');
438             $('#'  + this.id).trigger('keyup');
439         }
440 
441         if(this.isInline) {
442             $('#' + this.id).attr('display', 'inline');
443         } else {
444             $('#' + this.id).removeAttr('display');
445         }
446 
447         if(!this.isEnabled) {
448             $('#' + this.id).attr('disabled', 'disabled');
449         } else {
450             $('#' + this.id).removeAttr('disabled');
451         }
452     },
453 
454     /**
455      * This method sets its value to the value it has in its DOM representation
456      * and then delegates these changes to a controller property if the
457      * contentBindingReverse property is set.
458      *
459      * Additionally call target / action if set.
460      *
461      * @param {String} id The DOM id of the event target.
462      * @param {Object} event The DOM event.
463      * @param {Object} nextEvent The next event (external event), if specified.
464      */
465     setValueFromDOM: function(id, event, nextEvent) {
466         this.value = this.secure($('#' + this.id).val());
467         this.delegateValueUpdate();
468 
469         if(nextEvent) {
470             M.EventDispatcher.callHandler(nextEvent, event, YES);
471         }
472     },
473 
474     /**
475      * This method sets the text field's value, initiates its re-rendering
476      * and call the delegateValueUpdate().
477      *
478      * @param {String} value The value to be applied to the text field view.
479      * @param {Boolean} delegateUpdate Determines whether to delegate this value update to any observer or not.
480      * @param {Boolean} preventValueComputing Determines whether to execute computeValue() or not.
481      */
482     setValue: function(value, delegateUpdate, preventValueComputing) {
483         this.value = value;
484         this.renderUpdate(preventValueComputing);
485 
486         if(delegateUpdate) {
487             this.delegateValueUpdate();
488         }
489     },
490 
491     /**
492      * This method disables the text field by setting the disabled property of its
493      * html representation to true.
494      */
495     disable: function() {
496         this.isEnabled = NO;
497         this.renderUpdate();
498     },
499 
500     /**
501      * This method enables the text field by setting the disabled property of its
502      * html representation to false.
503      */
504     enable: function() {
505         this.isEnabled = YES;
506         this.renderUpdate();
507     },
508 
509     /**
510      * This method clears the text field's value, both in the DOM and within the JS object.
511      */
512     clearValue: function() {
513         this.setValue('');
514 
515         /* call lostFocus() to get the initial text displayed */
516         this.lostFocus();
517     },
518 
519     /**
520      * This method returns the text field view's value.
521      *
522      * @returns {String} The text field view's value.
523      */
524     getValue: function() {
525         return this.value;
526     }
527 
528 });