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