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