Added release notes for 'ContentHandler::runLegacyHooks' removal
[mediawiki.git] / resources / src / mediawiki.widgets.datetime / ProlepticGregorianDateTimeFormatter.js
blob877edbaed888004d3a4c40a68f97312e809e7cf4
1 ( function ( $, mw ) {
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();
31                 config = $.extend( {
32                         weekStartsOn: 0,
33                         hour12Periods: this.constructor.static.hour12Periods
34                 }, config );
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 );
68                 // Parent constructor
69                 mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'super' ].call( this, config );
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         };
81         /* Setup */
83         OO.inheritClass( mw.widgets.datetime.ProlepticGregorianDateTimeFormatter, mw.widgets.datetime.DateTimeFormatter );
85         /* Static */
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         };
97         /**
98          * Default full month names.
99          *
100          * @static
101          * @inheritable
102          * @property {Object}
103          */
104         mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.fullMonthNames = null;
106         /**
107          * Default abbreviated month names.
108          *
109          * @static
110          * @inheritable
111          * @property {Object}
112          */
113         mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.shortMonthNames = null;
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;
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;
133         /**
134          * Default day letters.
135          *
136          * @static
137          * @inheritable
138          * @property {string[]}
139          */
140         mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.dayLetters = null;
142         /**
143          * Default AM/PM indicators
144          *
145          * @static
146          * @inheritable
147          * @property {string[]}
148          */
149         mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.hour12Periods = null;
151         mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.static.setupDefaults = function () {
152                 mw.widgets.datetime.DateTimeFormatter.static.setupDefaults.call( this );
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                 }
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                 }
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                 }
241                 if ( !this.hour12Periods ) {
242                         this.hour12Periods = [
243                                 mw.msg( 'period-am' ),
244                                 mw.msg( 'period-pm' )
245                         ];
246                 }
247         };
249         /* Methods */
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;
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;
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;
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;
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;
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;
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;
352                         case 'hour|period':
353                                 spec = {
354                                         component: 'hour12period',
355                                         calendarComponent: false,
356                                         type: 'boolean',
357                                         values: this.hour12Periods
358                                 };
359                                 break;
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;
372                         default:
373                                 return mw.widgets.datetime.ProlepticGregorianDateTimeFormatter[ 'super' ].prototype.getFieldForTag.call( this, tag, params );
374                 }
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                 }
389                 return spec;
390         };
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;
414                 if ( !( date instanceof Date ) ) {
415                         date = this.defaultDate;
416                 }
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                 }
444                 ret.hour12period = ret.hour >= 12 ? 1 : 0;
445                 ret.hour12 = ret.hour % 12;
446                 if ( ret.hour12 === 0 ) {
447                         ret.hour12 = 12;
448                 }
450                 return ret;
451         };
453         /**
454          * @inheritdoc
455          */
456         mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getDateFromComponents = function ( components ) {
457                 var date = new Date();
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 );
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                 }
475                 return date;
476         };
478         /**
479          * @inheritdoc
480          */
481         mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.adjustComponent = function ( date, component, delta, mode ) {
482                 var min, max, range, components;
484                 if ( !( date instanceof Date ) ) {
485                         date = this.defaultDate;
486                 }
487                 components = this.getComponentsFromDate( date );
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                 }
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                 }
568                 return this.getDateFromComponents( components );
569         };
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         };
598         /**
599          * @inheritdoc
600          */
601         mw.widgets.datetime.ProlepticGregorianDateTimeFormatter.prototype.getCalendarHeadings = function () {
602                 var a = this.dayLetters;
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         };
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         };
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                         };
634                 if ( !( date instanceof Date ) ) {
635                         date = this.defaultDate;
636                 }
638                 dt = new Date( date.getTime() );
639                 dt[ setDate ]( 1 );
640                 t = dt.getTime();
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                 }
652                 if ( this.weekStartsOn ) {
653                         d = ( d + 7 - this.weekStartsOn ) % 7;
654                 }
655                 d = 1 - d;
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                 }
672                 return ret;
673         };
675 }( jQuery, mediaWiki ) );