Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5954486
DateTimeFormatter.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
18 KB
Referenced Files
None
Subscribers
None
DateTimeFormatter.js
View Options
(
function
()
{
/**
* @class
* @classdesc Provides various methods needed for formatting dates and times.
* @abstract
* @mixes OO.EventEmitter
*
* @constructor
* @description Create an instance of `mw.widgets.datetime.DateTimeFormatter`.
* @param {Object} [config] Configuration options
* @param {string} [config.format='@default'] May be a key from the
* {@link mw.widgets.datetime.DateTimeFormatter.formats}, or a format
* specification as defined by {@link mw.widgets.datetime.DateTimeFormatter#parseFieldSpec}
* and {@link mw.widgets.datetime.DateTimeFormatter#getFieldForTag}.
* @param {boolean} [config.local=false] Whether dates are local time or UTC
* @param {string[]} [config.fullZones] Time zone indicators. Array of 2 strings, for
* UTC and local time.
* @param {string[]} [config.shortZones] Abbreviated time zone indicators. Array of 2
* strings, for UTC and local time.
* @param {Date} [config.defaultDate] Default date, for filling unspecified components.
* Defaults to the current date and time (with 0 milliseconds).
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
=
function
MwWidgetsDatetimeDateTimeFormatter
(
config
)
{
this
.
constructor
.
static
.
setupDefaults
();
config
=
Object
.
assign
(
{
format
:
'@default'
,
local
:
false
,
fullZones
:
this
.
constructor
.
static
.
fullZones
,
shortZones
:
this
.
constructor
.
static
.
shortZones
},
config
);
// Mixin constructors
OO
.
EventEmitter
.
call
(
this
);
// Properties
if
(
this
.
constructor
.
static
.
formats
[
config
.
format
]
)
{
this
.
format
=
this
.
constructor
.
static
.
formats
[
config
.
format
];
}
else
{
this
.
format
=
config
.
format
;
}
this
.
local
=
!!
config
.
local
;
this
.
fullZones
=
config
.
fullZones
;
this
.
shortZones
=
config
.
shortZones
;
if
(
config
.
defaultDate
instanceof
Date
)
{
this
.
defaultDate
=
config
.
defaultDate
;
}
else
{
this
.
defaultDate
=
new
Date
();
if
(
this
.
local
)
{
this
.
defaultDate
.
setMilliseconds
(
0
);
}
else
{
this
.
defaultDate
.
setUTCMilliseconds
(
0
);
}
}
};
/* Setup */
OO
.
initClass
(
mw
.
widgets
.
datetime
.
DateTimeFormatter
);
OO
.
mixinClass
(
mw
.
widgets
.
datetime
.
DateTimeFormatter
,
OO
.
EventEmitter
);
/* Static */
/**
* Default format specifications. See the {@link #format format} parameter.
*
* @static
* @inheritable
* @type {Object}
* @name mw.widgets.datetime.DateTimeFormatter.formats
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
static
.
formats
=
{};
/**
* Default time zone indicators.
*
* @static
* @inheritable
* @type {string[]}
* @name mw.widgets.datetime.DateTimeFormatter.fullZones
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
static
.
fullZones
=
null
;
/**
* Default abbreviated time zone indicators.
*
* @static
* @inheritable
* @type {string[]}
* @name mw.widgets.datetime.DateTimeFormatter.shortZones
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
static
.
shortZones
=
null
;
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
static
.
setupDefaults
=
function
()
{
if
(
!
this
.
fullZones
)
{
this
.
fullZones
=
[
mw
.
msg
(
'timezone-utc'
),
mw
.
msg
(
'timezone-local'
)
];
}
if
(
!
this
.
shortZones
)
{
this
.
shortZones
=
[
'Z'
,
this
.
fullZones
[
1
].
slice
(
0
,
1
).
toUpperCase
()
];
if
(
this
.
shortZones
[
1
]
===
'Z'
)
{
this
.
shortZones
[
1
]
=
'L'
;
}
}
};
/* Events */
/**
* A `local` event is emitted when the 'local' flag is changed.
*
* @event mw.widgets.datetime.DateTimeFormatter.local
* @param {boolean} local Whether dates are local time
*/
/* Methods */
/**
* Whether dates are in local time or UTC.
*
* @return {boolean} True if local time
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
getLocal
=
function
()
{
return
this
.
local
;
};
/**
* Toggle whether dates are in local time or UTC.
*
* @param {boolean} [flag] Set the flag instead of toggling it
* @fires mw.widgets.datetime.DateTimeFormatter.local
* @chainable
* @return {mw.widgets.datetime.DateTimeFormatter}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
toggleLocal
=
function
(
flag
)
{
if
(
flag
===
undefined
)
{
flag
=
!
this
.
local
;
}
else
{
flag
=
!!
flag
;
}
if
(
this
.
local
!==
flag
)
{
this
.
local
=
flag
;
this
.
emit
(
'local'
,
this
.
local
);
}
return
this
;
};
/**
* Get the default date.
*
* @return {Date}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
getDefaultDate
=
function
()
{
return
new
Date
(
this
.
defaultDate
.
getTime
()
);
};
/**
* Fetch the field specification array for this object.
*
* See {@link #parseFieldSpec parseFieldSpec} for details on the return value structure.
*
* @return {Array}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
getFieldSpec
=
function
()
{
return
this
.
parseFieldSpec
(
this
.
format
);
};
/**
* Parse a format string into a field specification.
*
* The input is a string containing tags formatted as ${tag|param|param...}
* (for editable fields) and $!{tag|param|param...} (for non-editable fields).
* Most tags are defined by {@link #getFieldForTag getFieldForTag}, but a few
* are defined here:
* - ${intercalary|X|text}: Text that is only displayed when the 'intercalary'
* component is X.
* - ${not-intercalary|X|text}: Text that is displayed unless the 'intercalary'
* component is X.
*
* Elements of the returned array are strings or objects. Strings are meant to
* be displayed as-is. Objects are as returned by {@link #getFieldForTag getFieldForTag}.
*
* @protected
* @param {string} format
* @return {Array}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
parseFieldSpec
=
function
(
format
)
{
let
m
,
last
,
tag
,
params
,
spec
;
const
ret
=
[],
re
=
/(.*?)(\$(!?)\{([^}]+)\})/g
;
last
=
0
;
while
(
(
m
=
re
.
exec
(
format
)
)
!==
null
)
{
last
=
re
.
lastIndex
;
if
(
m
[
1
]
!==
''
)
{
ret
.
push
(
m
[
1
]
);
}
params
=
m
[
4
].
split
(
'|'
);
tag
=
params
.
shift
();
spec
=
this
.
getFieldForTag
(
tag
,
params
);
if
(
spec
)
{
if
(
m
[
3
]
===
'!'
)
{
spec
.
editable
=
false
;
}
ret
.
push
(
spec
);
}
else
{
ret
.
push
(
m
[
2
]
);
}
}
if
(
last
<
format
.
length
)
{
ret
.
push
(
format
.
slice
(
last
)
);
}
return
ret
;
};
/**
* @typedef {Object} mw.widgets.datetime.DateTimeFormatter~FieldSpecificationObject
* @property {string|null} component Date component corresponding to this field, if any.
* @property {boolean} editable Whether this field is editable.
* @property {string} type What kind of field this is:
* - 'static': The field is a static string; component will be null.
* - 'number': The field is generally numeric.
* - 'string': The field is generally textual.
* - 'boolean': The field is a boolean.
* - 'toggleLocal': The field represents {@link #getLocal this.getLocal()}.
* Editing should directly call {@link #toggleLocal this.toggleLocal()}.
* @property {boolean} calendarComponent Whether this field is part of a calendar, e.g.
* part of the date instead of the time.
* @property {number} size Maximum number of characters in the field (when
* the 'intercalary' component is falsey). If 0, the field should be hidden entirely.
* @property {Object.<string,number>} intercalarySize Map from
* 'intercalary' component values to overridden sizes.
* @property {string} value For type='static', the string to display.
* @property {function(Mixed): string} formatValue A function to format a
* component value as a display string.
* @property {function(string): Mixed} parseValue A function to parse a
* display string into a component value. If parsing fails, returns undefined.
*/
/**
* Turn a tag into a field specification object.
*
* Fields implemented here are:
* - ${intercalary|X|text}: Text that is only displayed when the 'intercalary'
* component is X.
* - ${not-intercalary|X|text}: Text that is displayed unless the 'intercalary'
* component is X.
* - ${zone|#}: Timezone offset, "+0000" format.
* - ${zone|:}: Timezone offset, "+00:00" format.
* - ${zone|short}: Timezone from 'shortZones' configuration setting.
* - ${zone|full}: Timezone from 'fullZones' configuration setting.
*
* @protected
* @abstract
* @param {string} tag
* @param {string[]} params
* @return {FieldSpecificationObject} Field specification object, or null if the tag+params are unrecognized.
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
getFieldForTag
=
function
(
tag
,
params
)
{
let
c
,
spec
=
null
;
switch
(
tag
)
{
case
'intercalary'
:
case
'not-intercalary'
:
if
(
params
.
length
<
2
||
!
params
[
0
]
)
{
return
null
;
}
spec
=
{
component
:
null
,
calendarComponent
:
false
,
editable
:
false
,
type
:
'static'
,
value
:
params
.
slice
(
1
).
join
(
'|'
),
size
:
0
,
intercalarySize
:
{}
};
if
(
tag
===
'intercalary'
)
{
spec
.
intercalarySize
[
params
[
0
]
]
=
spec
.
value
.
length
;
}
else
{
spec
.
size
=
spec
.
value
.
length
;
spec
.
intercalarySize
[
params
[
0
]
]
=
0
;
}
return
spec
;
case
'zone'
:
switch
(
params
[
0
]
)
{
case
'#'
:
case
':'
:
c
=
params
[
0
]
===
'#'
?
''
:
':'
;
return
{
component
:
'zone'
,
calendarComponent
:
false
,
editable
:
true
,
type
:
'toggleLocal'
,
size
:
5
+
c
.
length
,
formatValue
:
function
(
v
)
{
let
o
,
r
;
if
(
v
)
{
o
=
new
Date
().
getTimezoneOffset
();
r
=
String
(
Math
.
abs
(
o
)
%
60
);
while
(
r
.
length
<
2
)
{
r
=
'0'
+
r
;
}
r
=
String
(
Math
.
floor
(
Math
.
abs
(
o
)
/
60
)
)
+
c
+
r
;
while
(
r
.
length
<
4
+
c
.
length
)
{
r
=
'0'
+
r
;
}
return
(
o
<=
0
?
'+'
:
'−'
)
+
r
;
}
else
{
return
'+00'
+
c
+
'00'
;
}
},
parseValue
:
function
(
v
)
{
let
m
;
v
=
String
(
v
).
trim
();
if
(
(
m
=
/^([+-−])([0-9]{1,2}):?([0-9]{2})$/
.
test
(
v
)
)
)
{
return
(
m
[
2
]
*
60
+
m
[
3
]
)
*
(
m
[
1
]
===
'+'
?
-
1
:
1
);
}
else
{
return
undefined
;
}
}
};
case
'short'
:
case
'full'
:
spec
=
{
component
:
'zone'
,
calendarComponent
:
false
,
editable
:
true
,
type
:
'toggleLocal'
,
values
:
params
[
0
]
===
'short'
?
this
.
shortZones
:
this
.
fullZones
,
formatValue
:
this
.
formatSpecValue
,
parseValue
:
this
.
parseSpecValue
};
spec
.
size
=
Math
.
max
.
apply
(
// eslint-disable-next-line no-jquery/no-map-util
null
,
$
.
map
(
spec
.
values
,
(
v
)
=>
v
.
length
)
);
return
spec
;
}
return
null
;
default
:
return
null
;
}
};
/**
* Format a value for a field specification.
*
* 'this' must be the field specification object. The intention is that you
* could just assign this function as the 'formatValue' for each field spec.
*
* Besides the publicly-documented fields, uses the following:
* - values: Enumerated values for the field
* - zeropad: Whether to pad the number with zeros.
*
* @protected
* @param {any} v
* @return {string}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
formatSpecValue
=
function
(
v
)
{
if
(
v
===
undefined
||
v
===
null
)
{
return
''
;
}
if
(
typeof
v
===
'boolean'
||
this
.
type
===
'toggleLocal'
)
{
v
=
v
?
1
:
0
;
}
if
(
this
.
values
)
{
return
this
.
values
[
v
];
}
v
=
String
(
v
);
if
(
this
.
zeropad
)
{
while
(
v
.
length
<
this
.
size
)
{
v
=
'0'
+
v
;
}
}
return
v
;
};
/**
* Parse a value for a field specification.
*
* 'this' must be the field specification object. The intention is that you
* could just assign this function as the 'parseValue' for each field spec.
*
* Besides the publicly-documented fields, uses the following:
* - values: Enumerated values for the field
*
* @protected
* @param {string} v
* @return {number|string|null}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
parseSpecValue
=
function
(
v
)
{
let
k
;
if
(
v
===
''
)
{
return
null
;
}
if
(
!
this
.
values
)
{
v
=
+
v
;
if
(
this
.
type
===
'boolean'
||
this
.
type
===
'toggleLocal'
)
{
return
isNaN
(
v
)
?
undefined
:
!!
v
;
}
else
{
return
isNaN
(
v
)
?
undefined
:
v
;
}
}
if
(
v
.
normalize
)
{
v
=
v
.
normalize
();
}
const
re
=
new
RegExp
(
'^\\s*'
+
mw
.
util
.
escapeRegExp
(
v
),
'i'
);
for
(
k
in
this
.
values
)
{
k
=
+
k
;
if
(
!
isNaN
(
k
)
&&
re
.
test
(
this
.
values
[
k
]
)
)
{
if
(
this
.
type
===
'boolean'
||
this
.
type
===
'toggleLocal'
)
{
return
!!
k
;
}
else
{
return
k
;
}
}
}
return
undefined
;
};
/**
* Get components from a Date object.
*
* Most specific components are defined by the subclass. "Global" components
* are:
* - intercalary: {string} Non-falsey values are used to indicate intercalary days.
* - zone: {number} Timezone offset in minutes.
*
* @abstract
* @param {Date|null} date
* @return {Object} Components
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
getComponentsFromDate
=
function
(
date
)
{
// Should be overridden by subclass
return
{
zone
:
this
.
local
?
date
.
getTimezoneOffset
()
:
0
};
};
/**
* Get a Date object from components.
*
* @param {Object} components Date components
* @return {Date}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
getDateFromComponents
=
function
(
/* components */
)
{
// Should be overridden by subclass
return
new
Date
();
};
/**
* Adjust a date.
*
* @param {Date|null} date To be adjusted
* @param {string} component To adjust
* @param {number} delta Adjustment amount
* @param {string} mode Adjustment mode:
* - 'overflow': "Jan 32" => "Feb 1", "Jan 33" => "Feb 2", "Feb 0" => "Jan 31", etc.
* - 'wrap': "Jan 32" => "Jan 1", "Jan 33" => "Jan 2", "Jan 0" => "Jan 31", etc.
* - 'clip': "Jan 32" => "Jan 31", "Feb 32" => "Feb 28" (or 29), "Feb 0" => "Feb 1", etc.
* @return {Date} Adjusted date
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
adjustComponent
=
function
(
date
/* , component, delta, mode */
)
{
// Should be overridden by subclass
return
date
;
};
/**
* Get the column headings (weekday abbreviations) for a calendar grid.
*
* Null-valued columns are hidden if getCalendarData() returns no "day" object
* for all days in that column.
*
* @abstract
* @return {Array} string or null
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
getCalendarHeadings
=
function
()
{
// Should be overridden by subclass
return
[];
};
/**
* Test whether two dates are in the same calendar grid.
*
* @abstract
* @param {Date} date1
* @param {Date} date2
* @return {boolean}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
sameCalendarGrid
=
function
(
date1
,
date2
)
{
// Should be overridden by subclass
return
date1
.
getTime
()
===
date2
.
getTime
();
};
/**
* Test whether the date parts of two Dates are equal.
*
* @param {Date} date1
* @param {Date} date2
* @return {boolean}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
datePartIsEqual
=
function
(
date1
,
date2
)
{
if
(
this
.
local
)
{
return
(
date1
.
getFullYear
()
===
date2
.
getFullYear
()
&&
date1
.
getMonth
()
===
date2
.
getMonth
()
&&
date1
.
getDate
()
===
date2
.
getDate
()
);
}
else
{
return
(
date1
.
getUTCFullYear
()
===
date2
.
getUTCFullYear
()
&&
date1
.
getUTCMonth
()
===
date2
.
getUTCMonth
()
&&
date1
.
getUTCDate
()
===
date2
.
getUTCDate
()
);
}
};
/**
* Test whether the time parts of two Dates are equal.
*
* @param {Date} date1
* @param {Date} date2
* @return {boolean}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
timePartIsEqual
=
function
(
date1
,
date2
)
{
if
(
this
.
local
)
{
return
(
date1
.
getHours
()
===
date2
.
getHours
()
&&
date1
.
getMinutes
()
===
date2
.
getMinutes
()
&&
date1
.
getSeconds
()
===
date2
.
getSeconds
()
&&
date1
.
getMilliseconds
()
===
date2
.
getMilliseconds
()
);
}
else
{
return
(
date1
.
getUTCHours
()
===
date2
.
getUTCHours
()
&&
date1
.
getUTCMinutes
()
===
date2
.
getUTCMinutes
()
&&
date1
.
getUTCSeconds
()
===
date2
.
getUTCSeconds
()
&&
date1
.
getUTCMilliseconds
()
===
date2
.
getUTCMilliseconds
()
);
}
};
/**
* Test whether toggleLocal() changes the date part.
*
* @param {Date} date
* @return {boolean}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
localChangesDatePart
=
function
(
date
)
{
return
(
date
.
getUTCFullYear
()
!==
date
.
getFullYear
()
||
date
.
getUTCMonth
()
!==
date
.
getMonth
()
||
date
.
getUTCDate
()
!==
date
.
getDate
()
);
};
/**
* Create a new Date by merging the date part from one with the time part from
* another.
*
* @param {Date} datepart
* @param {Date} timepart
* @return {Date}
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
mergeDateAndTime
=
function
(
datepart
,
timepart
)
{
const
ret
=
new
Date
(
datepart
.
getTime
()
);
if
(
this
.
local
)
{
ret
.
setHours
(
timepart
.
getHours
(),
timepart
.
getMinutes
(),
timepart
.
getSeconds
(),
timepart
.
getMilliseconds
()
);
}
else
{
ret
.
setUTCHours
(
timepart
.
getUTCHours
(),
timepart
.
getUTCMinutes
(),
timepart
.
getUTCSeconds
(),
timepart
.
getUTCMilliseconds
()
);
}
return
ret
;
};
/**
* @typedef {Object} mw.widgets.datetime.DateTimeFormatter~CalendarGridData
* @property {string} header String to display as the calendar header
* @property {string} monthComponent Component to adjust by ±1 to change months.
* @property {string} dayComponent Component to adjust by ±1 to change days.
* @property {string} [weekComponent] Component to adjust by ±1 to change
* weeks. If omitted, the dayComponent should be adjusted by ±the number of
* non-nullable columns returned by this.getCalendarHeadings() to change weeks.
* @property {Array} rows Array of arrays of "day" objects or null/undefined.
*/
/**
* Get data for a calendar grid.
*
* A "day" object is:
* - display: {string} Display text for the day.
* - date: {Date} Date to use when the day is selected.
* - extra: {string|null} 'prev' or 'next' on days used to fill out the weeks
* at the start and end of the month.
*
* In any one result object, 'extra' + 'display' will always be unique.
*
* @abstract
* @param {Date|null} current Current date
* @return {CalendarGridData} Data
*/
mw
.
widgets
.
datetime
.
DateTimeFormatter
.
prototype
.
getCalendarData
=
function
(
/* components */
)
{
// Should be overridden by subclass
return
{
header
:
''
,
monthComponent
:
'month'
,
dayComponent
:
'day'
,
rows
:
[]
};
};
}()
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Oct 3, 03:01 (3 w, 6 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
56/55/d8cde8e340d0b613fba5f4882ff4
Default Alt Text
DateTimeFormatter.js (18 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment