1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 /*globals Shared */
  8 
  9 /** @class
 10 
 11   A Date field add behaviour to the Text Field to support date management,
 12   for example, disabling deletion, and special behaviour to tabs commands.
 13 
 14   This field view is tightly integrated with SC.DateTime
 15 
 16   By default the Date Field View show Date only, but if you need to show the Time do:
 17 
 18       dateAndTime: Shared.DateFieldView.design({
 19         showTime: YES,
 20         valueBinding: '...'
 21       }),
 22 
 23   and if you only need to show time:
 24 
 25       timeOnly: Shared.DateFieldView.design({
 26         showTime: YES,
 27         showDate: NO,
 28         valueBinding: '...'
 29       })
 30 
 31   Example usage with special format:
 32 
 33       specialDate: Shared.DateFieldView.design({
 34         formatDate: '%d %b of %Y',
 35         valueBinding: '...'
 36       }),
 37 
 38   You can override these format as you like, but has some limitations,
 39   actually only support these KEY from SC.DateTime:
 40 
 41   %a %b %d %H %I %j %m %M %p %S %U %W %y %Y
 42 
 43   These are keys that has FIXED length, so we can control the selection and tabing.
 44 
 45   @extends SC.TextFieldView
 46   @since SproutCore 1.0
 47   @author Juan Pablo Goldfinger
 48 */
 49 SC.DateFieldView = SC.TextFieldView.extend(
 50 /** @scope SC.DateFieldView.prototype */ {
 51 
 52   /**
 53     @type String
 54     @default null
 55   */
 56   value: null,
 57 
 58   /**
 59     @type Boolean
 60     @default YES
 61   */
 62   showDate: YES,
 63 
 64   /**
 65     @type Boolean
 66     @default NO
 67   */
 68   showTime: NO,
 69 
 70   /**
 71     Set this to NO to disallow the default keyboard handling, which attempts to convert
 72     numeric keystrokes into valid dates.
 73 
 74     @type Boolean
 75     @default YES
 76   */
 77   allowNumericInput: YES,
 78 
 79   /**
 80     @type String
 81     @default '%I:%M %p'
 82   */
 83   formatTime: '%I:%M %p',
 84 
 85   /**
 86     @type String
 87     @default '%d/%m/%Y'
 88   */
 89   formatDate: '%d/%m/%Y',
 90 
 91   /**
 92     @type String
 93     @default '%d/%m/%Y %I:%M %p'
 94   */
 95   formatDateTime: '%d/%m/%Y %I:%M %p',
 96 
 97   // DateTime constants (with fixed width, like numbers or abbs with fixed length)
 98   // original: '%a %A %b %B %c %d %h %H %i %I %j %m %M %p %S %U %W %x %X %y %Y %Z %%'.w(),
 99   // NOTE: I think that %a and %b aren't useful because is more adequate to represent day
100   // with 1..31 without zeros at start, but causes the length not to be fixed)
101 
102   /** @private*/
103   _dtConstants: ['%a', '%b', '%d', '%H', '%I', '%j', '%m', '%M', '%p', '%S', '%U', '%W', '%y', '%Y'],
104   // Width constants for each representation %@.
105 
106   /** @private */
107   _wtConstants: [3,3,2,2,2,3,2,2,2,2,2,2,2,4],
108 
109   /** @private */
110   activeSelection: 0,
111 
112   /*
113   FUTURE: DatePickerSupport.
114   createChildViews: function() {
115     sc_super();
116     if (SC.browser.isWebkit) {
117       // ON MOZILLA DON'T WORK
118       var view = Shared.DatePickerView.extend({
119         layout: { right: 0, centerY: 0, width: 18, height: 15 }
120       });
121       view.bind('value', [this, 'value']);
122       view.bind('isVisible', [this, 'isEnabled']);
123       this.set('rightAccessoryView', view);
124     }
125   },
126   */
127 
128   /**
129     The current format to apply for Validator and to show.
130 
131     @field
132     @type String
133     @observes showTime
134     @observes showDate
135   */
136   format: function() {
137     var st = this.get('showTime');
138     var sd = this.get('showDate');
139     if (st === YES && sd === YES) return this.get('formatDateTime');
140     if (st === YES) return this.get('formatTime');
141     return this.get('formatDate');
142   }.property('showTime', 'showDate', 'formatDateTime', 'formatDate', 'formatTime').cacheable(),
143 
144   /**
145     The current validator to format the Date to the input field and vice versa.
146 
147     @field
148     @type SC.Validator.DateTime
149     @observes format
150   */
151   validator: function() {
152     return SC.Validator.DateTime.extend({ format: this.get('format') });
153   }.property('format').cacheable(),
154 
155   /**
156     Array of Key/TextSelection found for the current format.
157 
158     @field
159     @type SC.Array
160   */
161   tabsSelections: function() {
162     var arr = [];
163     var ft = this.get('format');
164     var _dt = this.get('_dtConstants');
165     var _wt = this.get('_wtConstants');
166 
167     // Parse the string format to retrieve and build
168     // a TextSelection array ordered to support tabs behaviour
169     if (SC.empty(ft)) {
170       throw new Error('The format string is empty, and must be a valid string.');
171     }
172 
173     var pPos, key, keyPos, startAt = 0, nPos = 0, oPos = 0;
174     while(startAt < ft.length && ft.indexOf('%', startAt) !== -1) {
175       pPos = ft.indexOf('%', startAt);
176       key = ft.substring(pPos, pPos + 2);
177       startAt = pPos + 2;
178 
179       keyPos = _dt.indexOf(key);
180       if (keyPos === -1) {
181         throw new Error("SC.DateFieldView: The format's key '%@' is not supported.".fmt(key));
182       }
183       nPos = nPos + pPos - oPos;
184       arr.push(SC.Object.create({
185         key: key,
186         textSelection: SC.TextSelection.create({ start: nPos, end: nPos + _wt[keyPos] })
187       }));
188       nPos = nPos + _wt[keyPos];
189       oPos = startAt;
190     }
191     pPos = key = keyPos = null;
192 
193     return arr;
194   }.property('format').cacheable(),
195 
196   /** @private
197     If the activeSelection changes or the value changes, update the "TextSelection" to show accordingly.
198   */
199   updateTextSelectionObserver: function() {
200     var as = this.get('activeSelection');
201     var ts = this.get('tabsSelections');
202     if (this.get('isEditing')) {
203       this.selection(null, ts[as].get('textSelection'));
204     }
205   }.observes('activeSelection', 'value'),
206 
207   /** @private
208     Updates the value according the key.
209   */
210   updateValue: function(key, upOrDown) {
211     // 0 is DOWN - 1 is UP
212     var newValue = (upOrDown === 0) ? -1 : 1;
213     var value = this.get('value'), hour;
214     switch(key) {
215       case '%a': case '%d': case '%j': this.set('value', value.advance({ day: newValue })); break;
216       case '%b': case '%m': this.set('value', value.advance({ month: newValue })); break;
217       case '%H': case '%I': this.set('value', value.advance({ hour: newValue })); break;
218       case '%M': this.set('value', value.advance({ minute: newValue })); break;
219       case '%p': {
220         hour = value.get('hour') >= 12 ? -12 : 12;
221         this.set('value', value.advance({ hour: hour }));
222         break;
223       }
224       case '%S': this.set('value', value.advance({ second: newValue })); break;
225       case '%U': this.set('value', value.advance({ week1: newValue })); break;
226       case '%W': this.set('value', value.advance({ week0: newValue })); break;
227       case '%y': case '%Y': this.set('value', value.advance({ year: newValue })); break;
228     }
229   },
230 
231 
232   /** @private */
233   _lastValue: null,
234 
235   /** @private */
236   _lastKey: null,
237 
238   _selectRootElement: function() {
239     // TODO: This is a solution while I don't found how we
240     // receive the last key from the last input.
241     // (to see if is entering with Tab or backTab)
242     /*if (this.get('activeSelection') === -1) {
243     }*/
244   },
245 
246 
247   // ..........................................................
248   // Key Event Support
249   //
250 
251   /** @private */
252   keyDown: function(evt) {
253     if (this.interpretKeyEvents(evt)) {
254       evt.stop();
255       return YES;
256     }
257     return sc_super();
258   },
259 
260   /** @private */
261   ctrl_a: function() {
262     return YES;
263   },
264 
265   /** @private */
266   moveUp: function(evt) {
267     var as = this.get('activeSelection');
268     var ts = this.get('tabsSelections');
269     this.updateValue(ts[as].get('key'), 1);
270     return YES;
271   },
272 
273   /** @private */
274   moveDown: function(evt) {
275     var as = this.get('activeSelection');
276     var ts = this.get('tabsSelections');
277     this.updateValue(ts[as].get('key'), 0);
278     return YES;
279   },
280 
281   /** @private */
282   moveRight: function(evt) {
283     var ts = this.get('tabsSelections');
284     var ns = this.get('activeSelection') + 1;
285     if (ns === ts.length) {
286       ns = 0;
287     }
288     this.set('activeSelection', ns);
289     return YES;
290   },
291 
292   /** @private */
293   moveLeft: function(evt) {
294     var ts = this.get('tabsSelections');
295     var ns = this.get('activeSelection') - 1;
296     if (ns === -1) {
297       ns = ts.length - 1;
298     }
299     this.set('activeSelection', ns);
300     return YES;
301   },
302 
303   /** @private */
304   insertTab: function(evt) {
305     var ts = this.get('tabsSelections');
306     var ns = this.get('activeSelection') + 1;
307     if (ns < ts.length) {
308       this.set('activeSelection', ns);
309       return YES;
310     }
311     return NO;
312   },
313 
314   /** @private */
315   insertBacktab: function(evt) {
316     var ns = this.get('activeSelection') - 1;
317     if (ns !== -1) {
318       this.set('activeSelection', ns);
319       return YES;
320     }
321     return NO;
322   },
323 
324   /** @private */
325   mouseUp: function(evt) {
326     var ret = sc_super();
327     var cs = this.get('selection');
328     if (SC.none(cs)) {
329       this.set('activeSelection', 0);
330     } else {
331       var caret = cs.get('start');
332       var ts = this.get('tabsSelections');
333       var _tsLen = ts.length, cts;
334       for(var i=0; i<_tsLen; i++) {
335         cts = ts[i].get('textSelection');
336         if (caret >= cts.get('start') && caret <= cts.get('end')) {
337           this.set('activeSelection', i);
338         }
339       }
340     }
341     return ret;
342   },
343 
344   /** @private */
345   deleteBackward: function(evt) {
346     return YES;
347   },
348 
349   /** @private */
350   deleteForward: function(evt) {
351     return YES;
352   },
353 
354   /** @private */
355   _numericCharacters: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
356   /** @private */
357   _nonnumericCharacters: [' ', '-', '/', ':'],
358   /** @private Validates and applies supported keystrokes. */
359   insertText: function(chr, evt) {
360     // If it's a nonnumeric "advance" character, advance.
361     // TODO: instead of having a list of possible delimiter characters, we should actually look
362     // at what the next separator character is and only advance on that one.
363     if (this._nonnumericCharacters.contains(chr)) this.moveRight();
364 
365     // If it's a numeric character (and we're doing those), validate and apply it.
366     if (this.get('allowNumericInput') && this._numericCharacters.contains(chr)) {
367       var as = this.get('activeSelection'),
368           ts = this.get('tabsSelections'),
369           key = ts[as].get('key');
370 
371       var value = this.get('value'),
372           lastValue = this._lastValue,
373           length = 2,
374           min = 0,
375           max, key, newValue;
376 
377       switch(key) {
378         case '%Y':
379           key = 'year';
380           min = 1000;
381           max = 9999;
382           length = 4;
383         break;
384         case '%y':
385           key = 'year';
386           max = 99;
387         break;
388         case '%m':
389           key = 'month';
390           min = 1;
391           max = 12;
392         break;
393         case '%d':
394           key = 'day';
395           min = 1;
396           max = value.advance({ month: 1 }).adjust({ day: 0 }).get('day');
397         break;
398         case '%H':
399           key = 'hour';
400           max = 23;
401         break;
402         case '%I':
403           key = 'hour';
404           max = 11;
405         break;
406         case '%M':
407           key = 'minute';
408           max = 59;
409         break;
410         case '%S':
411           key = 'second';
412           max = 59;
413         break;
414       }
415 
416       if (SC.none(lastValue) || this._lastKey !== key) {
417         lastValue = value.get(key);
418         lastValue = (lastValue < 10 ? '0' : '') + lastValue;
419       }
420 
421       if (lastValue.length > length) lastValue = lastValue.substr(-length);
422 
423       // Removes the first character and adds the new one at the end of the string
424       lastValue = lastValue.slice(1) + chr;
425       newValue = parseInt(lastValue, 10);
426 
427       // If the value is allow, updates the value
428       if (newValue <= max && newValue >= min) {
429         var hash = {};
430         hash[key] = newValue;
431 
432         this.set('value', value.adjust(hash, NO));
433       }
434 
435       this._lastValue = lastValue;
436       this._lastKey = key;
437     }
438 
439     // Regardless, we handled the event.
440     return YES;
441   }
442 });
443