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