1 /* eslint-disable no-restricted-properties */
5 * Provides various methods needed for formatting dates and times.
9 * @mixins OO.EventEmitter
12 * @param {Object} [config] Configuration options
13 * @cfg {string} [format='@default'] May be a key from the {@link #static-formats static formats},
14 * or a format specification as defined by {@link #method-parseFieldSpec parseFieldSpec}
15 * and {@link #method-getFieldForTag getFieldForTag}.
16 * @cfg {boolean} [local=false] Whether dates are local time or UTC
17 * @cfg {string[]} [fullZones] Time zone indicators. Array of 2 strings, for
19 * @cfg {string[]} [shortZones] Abbreviated time zone indicators. Array of 2
20 * strings, for UTC and local time.
21 * @cfg {Date} [defaultDate] Default date, for filling unspecified components.
22 * Defaults to the current date and time (with 0 milliseconds).
24 mw
.widgets
.datetime
.DateTimeFormatter
= function MwWidgetsDatetimeDateTimeFormatter( config
) {
25 this.constructor.static.setupDefaults();
30 fullZones
: this.constructor.static.fullZones
,
31 shortZones
: this.constructor.static.shortZones
35 OO
.EventEmitter
.call( this );
38 if ( this.constructor.static.formats
[ config
.format
] ) {
39 this.format
= this.constructor.static.formats
[ config
.format
];
41 this.format
= config
.format
;
43 this.local
= !!config
.local
;
44 this.fullZones
= config
.fullZones
;
45 this.shortZones
= config
.shortZones
;
46 if ( config
.defaultDate
instanceof Date
) {
47 this.defaultDate
= config
.defaultDate
;
49 this.defaultDate
= new Date();
51 this.defaultDate
.setMilliseconds( 0 );
53 this.defaultDate
.setUTCMilliseconds( 0 );
60 OO
.initClass( mw
.widgets
.datetime
.DateTimeFormatter
);
61 OO
.mixinClass( mw
.widgets
.datetime
.DateTimeFormatter
, OO
.EventEmitter
);
66 * Default format specifications. See the {@link #format format} parameter.
72 mw
.widgets
.datetime
.DateTimeFormatter
.static.formats
= {};
75 * Default time zone indicators
79 * @property {string[]}
81 mw
.widgets
.datetime
.DateTimeFormatter
.static.fullZones
= null;
84 * Default abbreviated time zone indicators
88 * @property {string[]}
90 mw
.widgets
.datetime
.DateTimeFormatter
.static.shortZones
= null;
92 mw
.widgets
.datetime
.DateTimeFormatter
.static.setupDefaults = function () {
93 if ( !this.fullZones
) {
95 mw
.msg( 'timezone-utc' ),
96 mw
.msg( 'timezone-local' )
99 if ( !this.shortZones
) {
102 this.fullZones
[ 1 ].substr( 0, 1 ).toUpperCase()
104 if ( this.shortZones
[ 1 ] === 'Z' ) {
105 this.shortZones
[ 1 ] = 'L';
113 * A `local` event is emitted when the 'local' flag is changed.
121 * Whether dates are in local time or UTC
123 * @return {boolean} True if local time
125 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.getLocal = function () {
129 // eslint-disable-next-line valid-jsdoc
131 * Toggle whether dates are in local time or UTC
133 * @param {boolean} [flag] Set the flag instead of toggling it
137 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.toggleLocal = function ( flag
) {
138 if ( flag
=== undefined ) {
143 if ( this.local
!== flag
) {
145 this.emit( 'local', this.local
);
151 * Get the default date
155 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.getDefaultDate = function () {
156 return new Date( this.defaultDate
.getTime() );
160 * Fetch the field specification array for this object.
162 * See {@link #parseFieldSpec parseFieldSpec} for details on the return value structure.
166 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.getFieldSpec = function () {
167 return this.parseFieldSpec( this.format
);
171 * Parse a format string into a field specification
173 * The input is a string containing tags formatted as ${tag|param|param...}
174 * (for editable fields) and $!{tag|param|param...} (for non-editable fields).
175 * Most tags are defined by {@link #getFieldForTag getFieldForTag}, but a few
177 * - ${intercalary|X|text}: Text that is only displayed when the 'intercalary'
179 * - ${not-intercalary|X|text}: Text that is displayed unless the 'intercalary'
182 * Elements of the returned array are strings or objects. Strings are meant to
183 * be displayed as-is. Objects are as returned by {@link #getFieldForTag getFieldForTag}.
186 * @param {string} format
189 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.parseFieldSpec = function ( format
) {
190 var m
, last
, tag
, params
, spec
,
192 re
= /(.*?)(\$(!?)\{([^}]+)\})/g;
195 while ( ( m
= re
.exec( format
) ) !== null ) {
198 if ( m
[ 1 ] !== '' ) {
202 params
= m
[ 4 ].split( '|' );
203 tag
= params
.shift();
204 spec
= this.getFieldForTag( tag
, params
);
206 if ( m
[ 3 ] === '!' ) {
207 spec
.editable
= false;
214 if ( last
< format
.length
) {
215 ret
.push( format
.substr( last
) );
222 * Turn a tag into a field specification object
224 * Fields implemented here are:
225 * - ${intercalary|X|text}: Text that is only displayed when the 'intercalary'
227 * - ${not-intercalary|X|text}: Text that is displayed unless the 'intercalary'
229 * - ${zone|#}: Timezone offset, "+0000" format.
230 * - ${zone|:}: Timezone offset, "+00:00" format.
231 * - ${zone|short}: Timezone from 'shortZones' configuration setting.
232 * - ${zone|full}: Timezone from 'fullZones' configuration setting.
236 * @param {string} tag
237 * @param {string[]} params
238 * @return {Object|null} Field specification object, or null if the tag+params are unrecognized.
239 * @return {string|null} return.component Date component corresponding to this field, if any.
240 * @return {boolean} return.editable Whether this field is editable.
241 * @return {string} return.type What kind of field this is:
242 * - 'static': The field is a static string; component will be null.
243 * - 'number': The field is generally numeric.
244 * - 'string': The field is generally textual.
245 * - 'boolean': The field is a boolean.
246 * - 'toggleLocal': The field represents {@link #getLocal this.getLocal()}.
247 * Editing should directly call {@link #toggleLocal this.toggleLocal()}.
248 * @return {boolean} return.calendarComponent Whether this field is part of a calendar, e.g.
249 * part of the date instead of the time.
250 * @return {number} return.size Maximum number of characters in the field (when
251 * the 'intercalary' component is falsey). If 0, the field should be hidden entirely.
252 * @return {Object.<string,number>} return.intercalarySize Map from
253 * 'intercalary' component values to overridden sizes.
254 * @return {string} return.value For type='static', the string to display.
255 * @return {function(Mixed): string} return.formatValue A function to format a
256 * component value as a display string.
257 * @return {function(string): Mixed} return.parseValue A function to parse a
258 * display string into a component value. If parsing fails, returns undefined.
260 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.getFieldForTag = function ( tag
, params
) {
265 case 'not-intercalary':
266 if ( params
.length
< 2 || !params
[ 0 ] ) {
271 calendarComponent
: false,
274 value
: params
.slice( 1 ).join( '|' ),
278 if ( tag
=== 'intercalary' ) {
279 spec
.intercalarySize
[ params
[ 0 ] ] = spec
.value
.length
;
281 spec
.size
= spec
.value
.length
;
282 spec
.intercalarySize
[ params
[ 0 ] ] = 0;
287 switch ( params
[ 0 ] ) {
290 c
= params
[ 0 ] === '#' ? '' : ':';
293 calendarComponent
: false,
297 formatValue: function ( v
) {
300 o
= new Date().getTimezoneOffset();
301 r
= String( Math
.abs( o
) % 60 );
302 while ( r
.length
< 2 ) {
305 r
= String( Math
.floor( Math
.abs( o
) / 60 ) ) + c
+ r
;
306 while ( r
.length
< 4 + c
.length
) {
309 return ( o
<= 0 ? '+' : '−' ) + r
;
311 return '+00' + c
+ '00';
314 parseValue: function ( v
) {
316 v
= String( v
).trim();
317 if ( ( m
= /^([+-−])([0-9]{1,2}):?([0-9]{2})$/.test( v
) ) ) {
318 return ( m
[ 2 ] * 60 + m
[ 3 ] ) * ( m
[ 1 ] === '+' ? -1 : 1 );
329 calendarComponent
: false,
332 values
: params
[ 0 ] === 'short' ? this.shortZones
: this.fullZones
,
333 formatValue
: this.formatSpecValue
,
334 parseValue
: this.parseSpecValue
336 spec
.size
= Math
.max
.apply(
337 // eslint-disable-next-line jquery/no-map-util
338 null, $.map( spec
.values
, function ( v
) { return v
.length
; } )
350 * Format a value for a field specification
352 * 'this' must be the field specification object. The intention is that you
353 * could just assign this function as the 'formatValue' for each field spec.
355 * Besides the publicly-documented fields, uses the following:
356 * - values: Enumerated values for the field
357 * - zeropad: Whether to pad the number with zeros.
363 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.formatSpecValue = function ( v
) {
364 if ( v
=== undefined || v
=== null ) {
368 if ( typeof v
=== 'boolean' || this.type
=== 'toggleLocal' ) {
373 return this.values
[ v
];
377 if ( this.zeropad
) {
378 while ( v
.length
< this.size
) {
386 * Parse a value for a field specification
388 * 'this' must be the field specification object. The intention is that you
389 * could just assign this function as the 'parseValue' for each field spec.
391 * Besides the publicly-documented fields, uses the following:
392 * - values: Enumerated values for the field
396 * @return {number|string|null}
398 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.parseSpecValue = function ( v
) {
405 if ( !this.values
) {
407 if ( this.type
=== 'boolean' || this.type
=== 'toggleLocal' ) {
408 return isNaN( v
) ? undefined : !!v
;
410 return isNaN( v
) ? undefined : v
;
414 // eslint-disable-next-line jquery/no-each-util
416 // eslint-disable-next-line jquery/no-each-util
419 re
= new RegExp( '^\\s*' + v
.replace( /([\\{}()|.?*+\-^$\[\]])/g, '\\$1' ), 'i' ); // eslint-disable-line no-useless-escape
420 for ( k
in this.values
) {
422 if ( !isNaN( k
) && re
.test( this.values
[ k
] ) ) {
423 if ( this.type
=== 'boolean' || this.type
=== 'toggleLocal' ) {
434 * Get components from a Date object
436 * Most specific components are defined by the subclass. "Global" components
438 * - intercalary: {string} Non-falsey values are used to indicate intercalary days.
439 * - zone: {number} Timezone offset in minutes.
442 * @param {Date|null} date
443 * @return {Object} Components
445 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.getComponentsFromDate = function ( date
) {
446 // Should be overridden by subclass
448 zone
: this.local
? date
.getTimezoneOffset() : 0
453 * Get a Date object from components
455 * @param {Object} components Date components
458 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.getDateFromComponents = function ( /* components */ ) {
459 // Should be overridden by subclass
466 * @param {Date|null} date To be adjusted
467 * @param {string} component To adjust
468 * @param {number} delta Adjustment amount
469 * @param {string} mode Adjustment mode:
470 * - 'overflow': "Jan 32" => "Feb 1", "Jan 33" => "Feb 2", "Feb 0" => "Jan 31", etc.
471 * - 'wrap': "Jan 32" => "Jan 1", "Jan 33" => "Jan 2", "Jan 0" => "Jan 31", etc.
472 * - 'clip': "Jan 32" => "Jan 31", "Feb 32" => "Feb 28" (or 29), "Feb 0" => "Feb 1", etc.
473 * @return {Date} Adjusted date
475 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.adjustComponent = function ( date
/* , component, delta, mode */ ) {
476 // Should be overridden by subclass
481 * Get the column headings (weekday abbreviations) for a calendar grid
483 * Null-valued columns are hidden if getCalendarData() returns no "day" object
484 * for all days in that column.
487 * @return {Array} string or null
489 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.getCalendarHeadings = function () {
490 // Should be overridden by subclass
495 * Test whether two dates are in the same calendar grid
498 * @param {Date} date1
499 * @param {Date} date2
502 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.sameCalendarGrid = function ( date1
, date2
) {
503 // Should be overridden by subclass
504 return date1
.getTime() === date2
.getTime();
508 * Test whether the date parts of two Dates are equal
510 * @param {Date} date1
511 * @param {Date} date2
514 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.datePartIsEqual = function ( date1
, date2
) {
517 date1
.getFullYear() === date2
.getFullYear() &&
518 date1
.getMonth() === date2
.getMonth() &&
519 date1
.getDate() === date2
.getDate()
523 date1
.getUTCFullYear() === date2
.getUTCFullYear() &&
524 date1
.getUTCMonth() === date2
.getUTCMonth() &&
525 date1
.getUTCDate() === date2
.getUTCDate()
531 * Test whether the time parts of two Dates are equal
533 * @param {Date} date1
534 * @param {Date} date2
537 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.timePartIsEqual = function ( date1
, date2
) {
540 date1
.getHours() === date2
.getHours() &&
541 date1
.getMinutes() === date2
.getMinutes() &&
542 date1
.getSeconds() === date2
.getSeconds() &&
543 date1
.getMilliseconds() === date2
.getMilliseconds()
547 date1
.getUTCHours() === date2
.getUTCHours() &&
548 date1
.getUTCMinutes() === date2
.getUTCMinutes() &&
549 date1
.getUTCSeconds() === date2
.getUTCSeconds() &&
550 date1
.getUTCMilliseconds() === date2
.getUTCMilliseconds()
556 * Test whether toggleLocal() changes the date part
561 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.localChangesDatePart = function ( date
) {
563 date
.getUTCFullYear() !== date
.getFullYear() ||
564 date
.getUTCMonth() !== date
.getMonth() ||
565 date
.getUTCDate() !== date
.getDate()
570 * Create a new Date by merging the date part from one with the time part from
573 * @param {Date} datepart
574 * @param {Date} timepart
577 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.mergeDateAndTime = function ( datepart
, timepart
) {
578 var ret
= new Date( datepart
.getTime() );
583 timepart
.getMinutes(),
584 timepart
.getSeconds(),
585 timepart
.getMilliseconds()
589 timepart
.getUTCHours(),
590 timepart
.getUTCMinutes(),
591 timepart
.getUTCSeconds(),
592 timepart
.getUTCMilliseconds()
600 * Get data for a calendar grid
603 * - display: {string} Display text for the day.
604 * - date: {Date} Date to use when the day is selected.
605 * - extra: {string|null} 'prev' or 'next' on days used to fill out the weeks
606 * at the start and end of the month.
608 * In any one result object, 'extra' + 'display' will always be unique.
611 * @param {Date|null} current Current date
612 * @return {Object} Data
613 * @return {string} return.header String to display as the calendar header
614 * @return {string} return.monthComponent Component to adjust by ±1 to change months.
615 * @return {string} return.dayComponent Component to adjust by ±1 to change days.
616 * @return {string} [return.weekComponent] Component to adjust by ±1 to change
617 * weeks. If omitted, the dayComponent should be adjusted by ±the number of
618 * non-nullable columns returned by this.getCalendarHeadings() to change weeks.
619 * @return {Array} return.rows Array of arrays of "day" objects or null/undefined.
621 mw
.widgets
.datetime
.DateTimeFormatter
.prototype.getCalendarData = function ( /* components */ ) {
622 // Should be overridden by subclass
625 monthComponent
: 'month',