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