DateTimeInputWidget: Only show calendar when focusing date components, not time compo...
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets.datetime / ProlepticGregorianDateTimeFormatter.js
1 ( function ( $, mw ) {
2
3 /**
4 * Provides various methods needed for formatting dates and times. This
5 * implementation implements the proleptic Gregorian calendar over years
6 * 0000–9999.
7 *
8 * @class
9 * @extends mw.widgets.datetime.DateTimeFormatter
10 *
11 * @constructor
12 * @param {Object} [config] Configuration options
13 * @cfg {Object} [fullMonthNames] Mapping 1–12 to full month names.
14 * @cfg {Object} [shortMonthNames] Mapping 1–12 to abbreviated month names.
15 * If {@link #fullMonthNames fullMonthNames} is given and this is not,
16 * defaults to the first three characters from that setting.
17 * @cfg {Object} [fullDayNames] Mapping 0–6 to full day of week names. 0 is Sunday, 6 is Saturday.
18 * @cfg {Object} [shortDayNames] Mapping 0–6 to abbreviated day of week names. 0 is Sunday, 6 is Saturday.
19 * If {@link #fullDayNames fullDayNames} is given and this is not, defaults to
20 * the first three characters from that setting.
21 * @cfg {string[]} [dayLetters] Weekday column headers for a calendar. Array of 7 strings.
22 * If {@link #fullDayNames fullDayNames} or {@link #shortDayNames shortDayNames}
23 * are given and this is not, defaults to the first character from
24 * shortDayNames.
25 * @cfg {string[]} [hour12Periods] AM and PM texts. Array of 2 strings, AM and PM.
26 * @cfg {number} [weekStartsOn=0] What day the week starts on: 0 is Sunday, 1 is Monday, 6 is Saturday.
27 */
28 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter = function MwWidgetsDatetimeProlepticGregorianDateTimeFormatter( config ) {
29 var statick = this.constructor[ 'static' ];
30
31 statick.setupDefaults();
32
33 config = $.extend( {
34 weekStartsOn: 0,
35 hour12Periods: statick.hour12Periods
36 }, config );
37
38 if ( config.fullMonthNames && !config.shortMonthNames ) {
39 config.shortMonthNames = {};
40 $.each( config.fullMonthNames, function ( k, v ) {
41 config.shortMonthNames[ k ] = v.substr( 0, 3 );
42 }.bind( this ) );
43 }
44 if ( config.shortDayNames && !config.dayLetters ) {
45 config.dayLetters = [];
46 $.each( config.shortDayNames, function ( k, v ) {
47 config.dayLetters[ k ] = v.substr( 0, 1 );
48 }.bind( this ) );
49 }
50 if ( config.fullDayNames && !config.dayLetters ) {
51 config.dayLetters = [];
52 $.each( config.fullDayNames, function ( k, v ) {
53 config.dayLetters[ k ] = v.substr( 0, 1 );
54 }.bind( this ) );
55 }
56 if ( config.fullDayNames && !config.shortDayNames ) {
57 config.shortDayNames = {};
58 $.each( config.fullDayNames, function ( k, v ) {
59 config.shortDayNames[ k ] = v.substr( 0, 3 );
60 }.bind( this ) );
61 }
62 config = $.extend( {
63 fullMonthNames: statick.fullMonthNames,
64 shortMonthNames: statick.shortMonthNames,
65 fullDayNames: statick.fullDayNames,
66 shortDayNames: statick.shortDayNames,
67 dayLetters: statick.dayLetters
68 }, config );
69
70 // Parent constructor
71 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'super' ].call( this, config );
72
73 // Properties
74 this.weekStartsOn = config.weekStartsOn % 7;
75 this.fullMonthNames = config.fullMonthNames;
76 this.shortMonthNames = config.shortMonthNames;
77 this.fullDayNames = config.fullDayNames;
78 this.shortDayNames = config.shortDayNames;
79 this.dayLetters = config.dayLetters;
80 this.hour12Periods = config.hour12Periods;
81 };
82
83 /* Setup */
84
85 OO.inheritClass( mw.widgets.datetime.ProlepticGregorianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
86
87 /* Static */
88
89 /**
90 * @inheritdoc
91 */
92 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'static' ].formats = {
93 '@time': '${hour|0}:${minute|0}:${second|0}',
94 '@date': '$!{dow|short} ${day|#} ${month|short} ${year|#}',
95 '@datetime': '$!{dow|short} ${day|#} ${month|short} ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}',
96 '@default': '$!{dow|short} ${day|#} ${month|short} ${year|#} ${hour|0}:${minute|0}:${second|0} $!{zone|short}'
97 };
98
99 /**
100 * Default full month names.
101 *
102 * @static
103 * @inheritable
104 * @property {Object}
105 */
106 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'static' ].fullMonthNames = null;
107
108 /**
109 * Default abbreviated month names.
110 *
111 * @static
112 * @inheritable
113 * @property {Object}
114 */
115 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'static' ].shortMonthNames = null;
116
117 /**
118 * Default full day of week names.
119 *
120 * @static
121 * @inheritable
122 * @property {Object}
123 */
124 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'static' ].fullDayNames = null;
125
126 /**
127 * Default abbreviated day of week names.
128 *
129 * @static
130 * @inheritable
131 * @property {Object}
132 */
133 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'static' ].shortDayNames = null;
134
135 /**
136 * Default day letters.
137 *
138 * @static
139 * @inheritable
140 * @property {string[]}
141 */
142 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'static' ].dayLetters = null;
143
144 /**
145 * Default AM/PM indicators
146 *
147 * @static
148 * @inheritable
149 * @property {string[]}
150 */
151 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'static' ].hour12Periods = null;
152
153 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'static' ].setupDefaults = function () {
154 mw.widgets.datetime.DateTimeFormatter[ 'static' ].setupDefaults.call( this );
155
156 if ( this.fullMonthNames && !this.shortMonthNames ) {
157 this.shortMonthNames = {};
158 $.each( this.fullMonthNames, function ( k, v ) {
159 this.shortMonthNames[ k ] = v.substr( 0, 3 );
160 }.bind( this ) );
161 }
162 if ( this.shortDayNames && !this.dayLetters ) {
163 this.dayLetters = [];
164 $.each( this.shortDayNames, function ( k, v ) {
165 this.dayLetters[ k ] = v.substr( 0, 1 );
166 }.bind( this ) );
167 }
168 if ( this.fullDayNames && !this.dayLetters ) {
169 this.dayLetters = [];
170 $.each( this.fullDayNames, function ( k, v ) {
171 this.dayLetters[ k ] = v.substr( 0, 1 );
172 }.bind( this ) );
173 }
174 if ( this.fullDayNames && !this.shortDayNames ) {
175 this.shortDayNames = {};
176 $.each( this.fullDayNames, function ( k, v ) {
177 this.shortDayNames[ k ] = v.substr( 0, 3 );
178 }.bind( this ) );
179 }
180
181 if ( !this.fullMonthNames ) {
182 this.fullMonthNames = {
183 1: mw.msg( 'january' ),
184 2: mw.msg( 'february' ),
185 3: mw.msg( 'march' ),
186 4: mw.msg( 'april' ),
187 5: mw.msg( 'may_long' ),
188 6: mw.msg( 'june' ),
189 7: mw.msg( 'july' ),
190 8: mw.msg( 'august' ),
191 9: mw.msg( 'september' ),
192 10: mw.msg( 'october' ),
193 11: mw.msg( 'november' ),
194 12: mw.msg( 'december' )
195 };
196 }
197 if ( !this.shortMonthNames ) {
198 this.shortMonthNames = {
199 1: mw.msg( 'jan' ),
200 2: mw.msg( 'feb' ),
201 3: mw.msg( 'mar' ),
202 4: mw.msg( 'apr' ),
203 5: mw.msg( 'may' ),
204 6: mw.msg( 'jun' ),
205 7: mw.msg( 'jul' ),
206 8: mw.msg( 'aug' ),
207 9: mw.msg( 'sep' ),
208 10: mw.msg( 'oct' ),
209 11: mw.msg( 'nov' ),
210 12: mw.msg( 'dec' )
211 };
212 }
213
214 if ( !this.fullDayNames ) {
215 this.fullDayNames = {
216 0: mw.msg( 'sunday' ),
217 1: mw.msg( 'monday' ),
218 2: mw.msg( 'tuesday' ),
219 3: mw.msg( 'wednesday' ),
220 4: mw.msg( 'thursday' ),
221 5: mw.msg( 'friday' ),
222 6: mw.msg( 'saturday' )
223 };
224 }
225 if ( !this.shortDayNames ) {
226 this.shortDayNames = {
227 0: mw.msg( 'sun' ),
228 1: mw.msg( 'mon' ),
229 2: mw.msg( 'tue' ),
230 3: mw.msg( 'wed' ),
231 4: mw.msg( 'thu' ),
232 5: mw.msg( 'fri' ),
233 6: mw.msg( 'sat' )
234 };
235 }
236 if ( !this.dayLetters ) {
237 this.dayLetters = [];
238 $.each( this.shortDayNames, function ( k, v ) {
239 this.dayLetters[ k ] = v.substr( 0, 1 );
240 }.bind( this ) );
241 }
242
243 if ( !this.hour12Periods ) {
244 this.hour12Periods = [
245 mw.msg( 'period-am' ),
246 mw.msg( 'period-pm' )
247 ];
248 }
249 };
250
251 /* Methods */
252
253 /**
254 * @inheritdoc
255 *
256 * Additional fields implemented here are:
257 * - ${year|#}: Year as a number
258 * - ${year|0}: Year as a number, zero-padded to 4 digits
259 * - ${month|#}: Month as a number
260 * - ${month|0}: Month as a number with leading 0
261 * - ${month|short}: Month from 'shortMonthNames' configuration setting
262 * - ${month|full}: Month from 'fullMonthNames' configuration setting
263 * - ${day|#}: Day of the month as a number
264 * - ${day|0}: Day of the month as a number with leading 0
265 * - ${dow|short}: Day of the week from 'shortDayNames' configuration setting
266 * - ${dow|full}: Day of the week from 'fullDayNames' configuration setting
267 * - ${hour|#}: Hour as a number
268 * - ${hour|0}: Hour as a number with leading 0
269 * - ${hour|12}: Hour in a 12-hour clock as a number
270 * - ${hour|012}: Hour in a 12-hour clock as a number, with leading 0
271 * - ${hour|period}: Value from 'hour12Periods' configuration setting
272 * - ${minute|#}: Minute as a number
273 * - ${minute|0}: Minute as a number with leading 0
274 * - ${second|#}: Second as a number
275 * - ${second|0}: Second as a number with leading 0
276 * - ${millisecond|#}: Millisecond as a number
277 * - ${millisecond|0}: Millisecond as a number, zero-padded to 3 digits
278 */
279 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getFieldForTag = function ( tag, params ) {
280 var spec = null;
281
282 switch ( tag + '|' + params[ 0 ] ) {
283 case 'year|#':
284 case 'year|0':
285 spec = {
286 component: 'year',
287 calendarComponent: true,
288 type: 'number',
289 size: 4,
290 zeropad: params[ 0 ] === '0'
291 };
292 break;
293
294 case 'month|short':
295 case 'month|full':
296 spec = {
297 component: 'month',
298 calendarComponent: true,
299 type: 'string',
300 values: params[ 0 ] === 'short' ? this.shortMonthNames : this.fullMonthNames
301 };
302 break;
303
304 case 'dow|short':
305 case 'dow|full':
306 spec = {
307 component: 'dow',
308 calendarComponent: true,
309 editable: false,
310 type: 'string',
311 values: params[ 0 ] === 'short' ? this.shortDayNames : this.fullDayNames
312 };
313 break;
314
315 case 'month|#':
316 case 'month|0':
317 case 'day|#':
318 case 'day|0':
319 spec = {
320 component: tag,
321 calendarComponent: true,
322 type: 'number',
323 size: 2,
324 zeropad: params[ 0 ] === '0'
325 };
326 break;
327
328 case 'hour|#':
329 case 'hour|0':
330 case 'minute|#':
331 case 'minute|0':
332 case 'second|#':
333 case 'second|0':
334 spec = {
335 component: tag,
336 calendarComponent: false,
337 type: 'number',
338 size: 2,
339 zeropad: params[ 0 ] === '0'
340 };
341 break;
342
343 case 'hour|12':
344 case 'hour|012':
345 spec = {
346 component: 'hour12',
347 calendarComponent: false,
348 type: 'number',
349 size: 2,
350 zeropad: params[ 0 ] === '012'
351 };
352 break;
353
354 case 'hour|period':
355 spec = {
356 component: 'hour12period',
357 calendarComponent: false,
358 type: 'boolean',
359 values: this.hour12Periods
360 };
361 break;
362
363 case 'millisecond|#':
364 case 'millisecond|0':
365 spec = {
366 component: 'millisecond',
367 calendarComponent: false,
368 type: 'number',
369 size: 3,
370 zeropad: params[ 0 ] === '0'
371 };
372 break;
373
374 default:
375 return mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'super' ].prototype.getFieldForTag.call( this, tag, params );
376 }
377
378 if ( spec ) {
379 if ( spec.editable === undefined ) {
380 spec.editable = true;
381 }
382 spec.formatValue = this.formatSpecValue;
383 spec.parseValue = this.parseSpecValue;
384 if ( spec.values ) {
385 spec.size = Math.max.apply(
386 null, $.map( spec.values, function ( v ) { return v.length; } )
387 );
388 }
389 }
390
391 return spec;
392 };
393
394 /**
395 * Get components from a Date object
396 *
397 * Components are:
398 * - year {number}
399 * - month {number} (1-12)
400 * - day {number} (1-31)
401 * - dow {number} (0-6, 0 is Sunday)
402 * - hour {number} (0-23)
403 * - hour12 {number} (1-12)
404 * - hour12period {boolean}
405 * - minute {number} (0-59)
406 * - second {number} (0-59)
407 * - millisecond {number} (0-999)
408 * - zone {number}
409 *
410 * @param {Date|null} date
411 * @return {Object} Components
412 */
413 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getComponentsFromDate = function ( date ) {
414 var ret;
415
416 if ( !( date instanceof Date ) ) {
417 date = this.defaultDate;
418 }
419
420 if ( this.local ) {
421 ret = {
422 year: date.getFullYear(),
423 month: date.getMonth() + 1,
424 day: date.getDate(),
425 dow: date.getDay() % 7,
426 hour: date.getHours(),
427 minute: date.getMinutes(),
428 second: date.getSeconds(),
429 millisecond: date.getMilliseconds(),
430 zone: date.getTimezoneOffset()
431 };
432 } else {
433 ret = {
434 year: date.getUTCFullYear(),
435 month: date.getUTCMonth() + 1,
436 day: date.getUTCDate(),
437 dow: date.getUTCDay() % 7,
438 hour: date.getUTCHours(),
439 minute: date.getUTCMinutes(),
440 second: date.getUTCSeconds(),
441 millisecond: date.getUTCMilliseconds(),
442 zone: 0
443 };
444 }
445
446 ret.hour12period = ret.hour >= 12 ? 1 : 0;
447 ret.hour12 = ret.hour % 12;
448 if ( ret.hour12 === 0 ) {
449 ret.hour12 = 12;
450 }
451
452 return ret;
453 };
454
455 /**
456 * @inheritdoc
457 */
458 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
459 var date = new Date();
460
461 components = $.extend( {}, components );
462 if ( components.hour === undefined && components.hour12 !== undefined && components.hour12period !== undefined ) {
463 components.hour = ( components.hour12 % 12 ) + ( components.hour12period ? 12 : 0 );
464 }
465 components = $.extend( {}, this.getComponentsFromDate( null ), components );
466
467 if ( components.zone ) {
468 // Can't just use the constructor because that's stupid about ancient years.
469 date.setFullYear( components.year, components.month - 1, components.day );
470 date.setHours( components.hour, components.minute, components.second, components.millisecond );
471 } else {
472 // Date.UTC() is stupid about ancient years too.
473 date.setUTCFullYear( components.year, components.month - 1, components.day );
474 date.setUTCHours( components.hour, components.minute, components.second, components.millisecond );
475 }
476
477 return date;
478 };
479
480 /**
481 * @inheritdoc
482 */
483 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.adjustComponent = function ( date, component, delta, mode ) {
484 var min, max, range, components;
485
486 if ( !( date instanceof Date ) ) {
487 date = this.defaultDate;
488 }
489 components = this.getComponentsFromDate( date );
490
491 switch ( component ) {
492 case 'year':
493 min = 0;
494 max = 9999;
495 break;
496 case 'month':
497 min = 1;
498 max = 12;
499 break;
500 case 'day':
501 min = 1;
502 max = this.getDaysInMonth( components.month, components.year );
503 break;
504 case 'hour':
505 min = 0;
506 max = 23;
507 break;
508 case 'minute':
509 case 'second':
510 min = 0;
511 max = 59;
512 break;
513 case 'millisecond':
514 min = 0;
515 max = 999;
516 break;
517 case 'hour12period':
518 component = 'hour';
519 min = 0;
520 max = 23;
521 delta *= 12;
522 break;
523 case 'hour12':
524 component = 'hour';
525 min = components.hour12period ? 12 : 0;
526 max = components.hour12period ? 23 : 11;
527 break;
528 default:
529 return new Date( date.getTime() );
530 }
531
532 components[ component ] += delta;
533 range = max - min + 1;
534 switch ( mode ) {
535 case 'overflow':
536 // Date() will mostly handle it automatically. But months need
537 // manual handling to prevent e.g. Jan 31 => Mar 3.
538 if ( component === 'month' || component === 'year' ) {
539 while ( components.month < 1 ) {
540 components[ component ] += 12;
541 components.year--;
542 }
543 while ( components.month > 12 ) {
544 components[ component ] -= 12;
545 components.year++;
546 }
547 }
548 break;
549 case 'wrap':
550 while ( components[ component ] < min ) {
551 components[ component ] += range;
552 }
553 while ( components[ component ] > max ) {
554 components[ component ] -= range;
555 }
556 break;
557 case 'clip':
558 if ( components[ component ] < min ) {
559 components[ component ] = min;
560 }
561 if ( components[ component ] < max ) {
562 components[ component ] = max;
563 }
564 break;
565 }
566 if ( component === 'month' || component === 'year' ) {
567 components.day = Math.min( components.day, this.getDaysInMonth( components.month, components.year ) );
568 }
569
570 return this.getDateFromComponents( components );
571 };
572
573 /**
574 * Get the number of days in a month
575 *
576 * @protected
577 * @param {number} month
578 * @param {number} year
579 * @return {number}
580 */
581 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getDaysInMonth = function ( month, year ) {
582 switch ( month ) {
583 case 4:
584 case 6:
585 case 9:
586 case 11:
587 return 30;
588 case 2:
589 if ( year % 4 ) {
590 return 28;
591 } else if ( year % 100 ) {
592 return 29;
593 }
594 return ( year % 400 ) ? 28 : 29;
595 default:
596 return 31;
597 }
598 };
599
600 /**
601 * @inheritdoc
602 */
603 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getCalendarHeadings = function () {
604 var a = this.dayLetters;
605
606 if ( this.weekStartsOn ) {
607 return a.slice( this.weekStartsOn ).concat( a.slice( 0, this.weekStartsOn ) );
608 } else {
609 return a.slice( 0 ); // clone
610 }
611 };
612
613 /**
614 * @inheritdoc
615 */
616 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.sameCalendarGrid = function ( date1, date2 ) {
617 if ( this.local ) {
618 return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth();
619 } else {
620 return date1.getUTCFullYear() === date2.getUTCFullYear() && date1.getUTCMonth() === date2.getUTCMonth();
621 }
622 };
623
624 /**
625 * @inheritdoc
626 */
627 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getCalendarData = function ( date ) {
628 var dt, t, d, e, i, row,
629 getDate = this.local ? 'getDate' : 'getUTCDate',
630 setDate = this.local ? 'setDate' : 'setUTCDate',
631 ret = {
632 dayComponent: 'day',
633 monthComponent: 'month'
634 };
635
636 if ( !( date instanceof Date ) ) {
637 date = this.defaultDate;
638 }
639
640 dt = new Date( date.getTime() );
641 dt[ setDate ]( 1 );
642 t = dt.getTime();
643
644 if ( this.local ) {
645 ret.header = this.fullMonthNames[ dt.getMonth() + 1 ] + ' ' + dt.getFullYear();
646 d = dt.getDay() % 7;
647 e = this.getDaysInMonth( dt.getMonth() + 1, dt.getFullYear() );
648 } else {
649 ret.header = this.fullMonthNames[ dt.getUTCMonth() + 1 ] + ' ' + dt.getUTCFullYear();
650 d = dt.getUTCDay() % 7;
651 e = this.getDaysInMonth( dt.getUTCMonth() + 1, dt.getUTCFullYear() );
652 }
653
654 if ( this.weekStartsOn ) {
655 d = ( d + 7 - this.weekStartsOn ) % 7;
656 }
657 d = 1 - d;
658
659 ret.rows = [];
660 while ( d <= e ) {
661 row = [];
662 for ( i = 0; i < 7; i++, d++ ) {
663 dt = new Date( t );
664 dt[ setDate ]( d );
665 row[ i ] = {
666 display: String( dt[ getDate ]() ),
667 date: dt,
668 extra: d < 1 ? 'prev' : d > e ? 'next' : null
669 };
670 }
671 ret.rows.push( row );
672 }
673
674 return ret;
675 };
676
677 }( jQuery, mediaWiki ) );