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