2 * MediaWiki Widgets – CalendarWidget class.
4 * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
5 * @license The MIT License (MIT); see LICENSE.txt
12 * Creates an mw.widgets.CalendarWidget object.
15 * @extends OO.ui.Widget
16 * @mixins OO.ui.mixin.TabIndexedElement
19 * @param {Object} [config] Configuration options
20 * @cfg {string} [precision='day'] Date precision to use, 'day' or 'month'
21 * @cfg {string|null} [date=null] Day or month date (depending on `precision`), in the
22 * format 'YYYY-MM-DD' or 'YYYY-MM'. When null, defaults to current date.
24 mw
.widgets
.CalendarWidget
= function MWWCalendarWidget( config
) {
25 // Config initialization
26 config
= config
|| {};
29 mw
.widgets
.CalendarWidget
.parent
.call( this, config
);
32 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$element
} ) );
35 this.precision
= config
.precision
|| 'day';
36 // Currently selected date (day or month)
38 // Current UI state (date and precision we're displaying right now)
40 this.displayLayer
= this.getDisplayLayers()[ 0 ]; // 'month', 'year', 'duodecade'
42 this.$header
= $( '<div>' ).addClass( 'mw-widget-calendarWidget-header' );
43 this.$bodyOuterWrapper
= $( '<div>' ).addClass( 'mw-widget-calendarWidget-body-outer-wrapper' );
44 this.$bodyWrapper
= $( '<div>' ).addClass( 'mw-widget-calendarWidget-body-wrapper' );
45 this.$body
= $( '<div>' ).addClass( 'mw-widget-calendarWidget-body' );
46 this.labelButton
= new OO
.ui
.ButtonWidget( {
50 classes
: [ 'mw-widget-calendarWidget-labelButton' ]
52 this.upButton
= new OO
.ui
.ButtonWidget( {
56 classes
: [ 'mw-widget-calendarWidget-upButton' ]
58 this.prevButton
= new OO
.ui
.ButtonWidget( {
62 classes
: [ 'mw-widget-calendarWidget-prevButton' ]
64 this.nextButton
= new OO
.ui
.ButtonWidget( {
68 classes
: [ 'mw-widget-calendarWidget-nextButton' ]
72 this.labelButton
.connect( this, { click
: 'onUpButtonClick' } );
73 this.upButton
.connect( this, { click
: 'onUpButtonClick' } );
74 this.prevButton
.connect( this, { click
: 'onPrevButtonClick' } );
75 this.nextButton
.connect( this, { click
: 'onNextButtonClick' } );
77 focus
: this.onFocus
.bind( this ),
78 mousedown
: this.onClick
.bind( this ),
79 keydown
: this.onKeyDown
.bind( this )
84 .addClass( 'mw-widget-calendarWidget' )
85 .append( this.$header
, this.$bodyOuterWrapper
.append( this.$bodyWrapper
.append( this.$body
) ) );
87 this.prevButton
.$element
,
88 this.nextButton
.$element
,
89 this.upButton
.$element
,
90 this.labelButton
.$element
92 this.setDate( config
.date
!== undefined ? config
.date
: null );
97 OO
.inheritClass( mw
.widgets
.CalendarWidget
, OO
.ui
.Widget
);
98 OO
.mixinClass( mw
.widgets
.CalendarWidget
, OO
.ui
.mixin
.TabIndexedElement
);
105 * A change event is emitted when the chosen date changes.
107 * @param {string} date Day or month date, in the format 'YYYY-MM-DD' or 'YYYY-MM'
113 * Get the date format ('YYYY-MM-DD' or 'YYYY-MM', depending on precision), which is used
114 * internally and for dates accepted by #setDate and returned by #getDate.
117 * @returns {string} Format
119 mw
.widgets
.CalendarWidget
.prototype.getDateFormat = function () {
127 * Get the date precision this calendar uses, 'day' or 'month'.
130 * @returns {string} Precision, 'day' or 'month'
132 mw
.widgets
.CalendarWidget
.prototype.getPrecision = function () {
133 return this.precision
;
137 * Get list of possible display layers.
140 * @returns {string[]} Layers
142 mw
.widgets
.CalendarWidget
.prototype.getDisplayLayers = function () {
143 return [ 'month', 'year', 'duodecade' ].slice( this.precision
=== 'month' ? 1 : 0 );
147 * Update the calendar.
150 * @param {string|null} [fade=null] Direction in which to fade out current calendar contents, 'previous',
152 * @returns {string} Format
154 mw
.widgets
.CalendarWidget
.prototype.updateUI = function ( fade
) {
155 var items
, today
, selected
, currentMonth
, currentYear
, currentDay
, i
, needsFade
,
156 $bodyWrapper
= this.$bodyWrapper
;
159 this.displayLayer
=== this.previousDisplayLayer
&&
160 this.previousMoment
&&
161 this.previousMoment
.isSame( this.moment
, this.precision
=== 'month' ? 'month' : 'day' )
168 if ( this.$oldBody
) {
169 this.$oldBody
.remove();
171 this.$oldBody
= this.$body
.addClass( 'mw-widget-calendarWidget-old-body' );
172 // Clone without children
173 this.$body
= $( this.$body
[0].cloneNode( false ) )
174 .removeClass( 'mw-widget-calendarWidget-old-body' )
175 .toggleClass( 'mw-widget-calendarWidget-body-month', this.displayLayer
=== 'month' )
176 .toggleClass( 'mw-widget-calendarWidget-body-year', this.displayLayer
=== 'year' )
177 .toggleClass( 'mw-widget-calendarWidget-body-duodecade', this.displayLayer
=== 'duodecade' );
180 selected
= moment( this.getDate(), this.getDateFormat() );
182 switch ( this.displayLayer
) {
184 this.labelButton
.setLabel( this.moment
.format( 'MMMM YYYY' ) );
185 this.upButton
.toggle( true );
187 // First week displayed is the first week spanned by the month, unless it begins on Monday, in
188 // which case first week displayed is the previous week. This makes the calendar "balanced"
189 // and also neatly handles 28-day February sometimes spanning only 4 weeks.
190 currentDay
= moment( this.moment
).startOf( 'month' ).subtract( 1, 'day' ).startOf( 'week' );
192 // Day-of-week labels. Localisation-independent: works with weeks starting on Saturday, Sunday
194 for ( i
= 0; i
< 7; i
++ ) {
197 .addClass( 'mw-widget-calendarWidget-day-heading' )
198 .text( currentDay
.format( 'dd' ) )
200 currentDay
.add( 1, 'day' );
202 currentDay
.subtract( 7, 'days' );
204 // Actual calendar month. Always displays 6 weeks, for consistency (months can span 4 to 6
206 for ( i
= 0; i
< 42; i
++ ) {
209 .addClass( 'mw-widget-calendarWidget-item mw-widget-calendarWidget-day' )
210 .toggleClass( 'mw-widget-calendarWidget-day-additional', !currentDay
.isSame( this.moment
, 'month' ) )
211 .toggleClass( 'mw-widget-calendarWidget-day-today', currentDay
.isSame( today
, 'day' ) )
212 .toggleClass( 'mw-widget-calendarWidget-item-selected', currentDay
.isSame( selected
, 'day' ) )
213 .text( currentDay
.format( 'D' ) )
214 .data( 'date', currentDay
.date() )
215 .data( 'month', currentDay
.month() )
216 .data( 'year', currentDay
.year() )
218 currentDay
.add( 1, 'day' );
223 this.labelButton
.setLabel( this.moment
.format( 'YYYY' ) );
224 this.upButton
.toggle( true );
226 currentMonth
= moment( this.moment
).startOf( 'year' );
227 for ( i
= 0; i
< 12; i
++ ) {
230 .addClass( 'mw-widget-calendarWidget-item mw-widget-calendarWidget-month' )
231 .toggleClass( 'mw-widget-calendarWidget-item-selected', currentMonth
.isSame( selected
, 'month' ) )
232 .text( currentMonth
.format( 'MMMM' ) )
233 .data( 'month', currentMonth
.month() )
235 currentMonth
.add( 1, 'month' );
237 // Shuffle the array to display months in columns rather than rows.
239 items
[ 0 ], items
[ 6 ], // | January | July |
240 items
[ 1 ], items
[ 7 ], // | February | August |
241 items
[ 2 ], items
[ 8 ], // | March | September |
242 items
[ 3 ], items
[ 9 ], // | April | October |
243 items
[ 4 ], items
[ 10 ], // | May | November |
244 items
[ 5 ], items
[ 11 ] // | June | December |
249 this.labelButton
.setLabel( null );
250 this.upButton
.toggle( false );
252 currentYear
= moment( { year
: Math
.floor( this.moment
.year() / 20 ) * 20 } );
253 for ( i
= 0; i
< 20; i
++ ) {
256 .addClass( 'mw-widget-calendarWidget-item mw-widget-calendarWidget-year' )
257 .toggleClass( 'mw-widget-calendarWidget-item-selected', currentYear
.isSame( selected
, 'year' ) )
258 .text( currentYear
.format( 'YYYY' ) )
259 .data( 'year', currentYear
.year() )
261 currentYear
.add( 1, 'year' );
266 this.$body
.append
.apply( this.$body
, items
);
269 .removeClass( 'mw-widget-calendarWidget-body-wrapper-fade-up' )
270 .removeClass( 'mw-widget-calendarWidget-body-wrapper-fade-down' )
271 .removeClass( 'mw-widget-calendarWidget-body-wrapper-fade-previous' )
272 .removeClass( 'mw-widget-calendarWidget-body-wrapper-fade-next' );
274 needsFade
= this.previousDisplayLayer
!== this.displayLayer
;
275 if ( this.displayLayer
=== 'month' ) {
276 needsFade
= needsFade
|| !this.moment
.isSame( this.previousMoment
, 'month' );
277 } else if ( this.displayLayer
=== 'year' ) {
278 needsFade
= needsFade
|| !this.moment
.isSame( this.previousMoment
, 'year' );
279 } else if ( this.displayLayer
=== 'duodecade' ) {
280 needsFade
= needsFade
|| (
281 Math
.floor( this.moment
.year() / 20 ) * 20 !==
282 Math
.floor( this.previousMoment
.year() / 20 ) * 20
286 if ( fade
&& needsFade
) {
287 this.$oldBody
.find( '.mw-widget-calendarWidget-item-selected' )
288 .removeClass( 'mw-widget-calendarWidget-item-selected' );
289 if ( fade
=== 'previous' || fade
=== 'up' ) {
290 this.$body
.insertBefore( this.$oldBody
);
291 } else if ( fade
=== 'next' || fade
=== 'down' ) {
292 this.$body
.insertAfter( this.$oldBody
);
294 setTimeout( function () {
295 $bodyWrapper
.addClass( 'mw-widget-calendarWidget-body-wrapper-fade-' + fade
);
298 this.$oldBody
.replaceWith( this.$body
);
301 this.previousMoment
= moment( this.moment
);
302 this.previousDisplayLayer
= this.displayLayer
;
304 this.$body
.on( 'click', this.onBodyClick
.bind( this ) );
308 * Handle click events on the "up" button, switching to less precise view.
311 mw
.widgets
.CalendarWidget
.prototype.onUpButtonClick = function () {
313 layers
= this.getDisplayLayers(),
314 currentLayer
= layers
.indexOf( this.displayLayer
);
315 if ( currentLayer
!== layers
.length
- 1 ) {
317 this.displayLayer
= layers
[ currentLayer
+ 1 ];
318 this.updateUI( 'up' );
325 * Handle click events on the "previous" button, switching to previous pane.
328 mw
.widgets
.CalendarWidget
.prototype.onPrevButtonClick = function () {
329 switch ( this.displayLayer
) {
331 this.moment
.subtract( 1, 'month' );
334 this.moment
.subtract( 1, 'year' );
337 this.moment
.subtract( 20, 'years' );
340 this.updateUI( 'previous' );
344 * Handle click events on the "next" button, switching to next pane.
347 mw
.widgets
.CalendarWidget
.prototype.onNextButtonClick = function () {
348 switch ( this.displayLayer
) {
350 this.moment
.add( 1, 'month' );
353 this.moment
.add( 1, 'year' );
356 this.moment
.add( 20, 'years' );
359 this.updateUI( 'next' );
363 * Handle click events anywhere in the body of the widget, which contains the matrix of days,
364 * months or years to choose. Maybe change the pane or switch to more precise view, depending on
368 mw
.widgets
.CalendarWidget
.prototype.onBodyClick = function ( e
) {
370 previousMoment
= moment( this.moment
),
371 $target
= $( e
.target
),
372 layers
= this.getDisplayLayers(),
373 currentLayer
= layers
.indexOf( this.displayLayer
);
374 if ( $target
.data( 'year' ) !== undefined ) {
375 this.moment
.year( $target
.data( 'year' ) );
377 if ( $target
.data( 'month' ) !== undefined ) {
378 this.moment
.month( $target
.data( 'month' ) );
380 if ( $target
.data( 'date' ) !== undefined ) {
381 this.moment
.date( $target
.data( 'date' ) );
383 if ( currentLayer
=== 0 ) {
384 this.setDateFromMoment();
386 this.precision
=== 'day' && this.moment
.isBefore( previousMoment
, 'month' ) ? 'previous' :
387 this.precision
=== 'day' && this.moment
.isAfter( previousMoment
, 'month' ) ? 'next' : null
391 this.displayLayer
= layers
[ currentLayer
- 1 ];
392 this.updateUI( 'down' );
399 * @param {string|null} [date=null] Day or month date, in the format 'YYYY-MM-DD' or 'YYYY-MM'.
400 * When null, defaults to current date. When invalid, the date is not changed.
402 mw
.widgets
.CalendarWidget
.prototype.setDate = function ( date
) {
403 var mom
= date
!== null ? moment( date
, this.getDateFormat() ) : moment();
404 if ( mom
.isValid() ) {
406 this.setDateFromMoment();
407 this.displayLayer
= this.getDisplayLayers()[ 0 ];
413 * Reset the user interface of this widget to reflect selected date.
415 mw
.widgets
.CalendarWidget
.prototype.resetUI = function () {
416 this.moment
= moment( this.getDate(), this.getDateFormat() );
417 this.displayLayer
= this.getDisplayLayers()[ 0 ];
422 * Set the date from moment object.
426 mw
.widgets
.CalendarWidget
.prototype.setDateFromMoment = function () {
427 // Switch to English locale to avoid number formatting. We want the internal value to be
428 // '2015-07-24' and not '٢٠١٥-٠٧-٢٤' even if the UI language is Arabic.
429 var newDate
= moment( this.moment
).locale( 'en' ).format( this.getDateFormat() );
430 if ( this.date
!== newDate
) {
432 this.emit( 'change', this.date
);
437 * Get current date, in the format 'YYYY-MM-DD' or 'YYYY-MM', depending on precision. Digits will
440 * @returns {string} Date string
442 mw
.widgets
.CalendarWidget
.prototype.getDate = function () {
447 * Handle focus events.
451 mw
.widgets
.CalendarWidget
.prototype.onFocus = function () {
452 this.displayLayer
= this.getDisplayLayers()[ 0 ];
453 this.updateUI( 'down' );
457 * Handle mouse click events.
460 * @param {jQuery.Event} e Mouse click event
462 mw
.widgets
.CalendarWidget
.prototype.onClick = function ( e
) {
463 if ( !this.isDisabled() && e
.which
=== 1 ) {
464 // Prevent unintended focussing
470 * Handle key down events.
473 * @param {jQuery.Event} e Key down event
475 mw
.widgets
.CalendarWidget
.prototype.onKeyDown = function ( e
) {
477 dir
= OO
.ui
.Element
.static.getDir( this.$element
),
478 nextDirectionKey
= dir
=== 'ltr' ? OO
.ui
.Keys
.RIGHT
: OO
.ui
.Keys
.LEFT
,
479 prevDirectionKey
= dir
=== 'ltr' ? OO
.ui
.Keys
.LEFT
: OO
.ui
.Keys
.RIGHT
,
480 updateInDirection
= null;
482 if ( !this.isDisabled() ) {
484 case prevDirectionKey
:
485 this.moment
.subtract( 1, this.precision
=== 'month' ? 'month' : 'day' );
486 updateInDirection
= 'previous';
488 case nextDirectionKey
:
489 this.moment
.add( 1, this.precision
=== 'month' ? 'month' : 'day' );
490 updateInDirection
= 'next';
493 this.moment
.subtract( 1, this.precision
=== 'month' ? 'month' : 'week' );
494 updateInDirection
= 'previous';
496 case OO
.ui
.Keys
.DOWN
:
497 this.moment
.add( 1, this.precision
=== 'month' ? 'month' : 'week' );
498 updateInDirection
= 'next';
500 case OO
.ui
.Keys
.PAGEUP
:
501 this.moment
.subtract( 1, this.precision
=== 'month' ? 'year' : 'month' );
502 updateInDirection
= 'previous';
504 case OO
.ui
.Keys
.PAGEDOWN
:
505 this.moment
.add( 1, this.precision
=== 'month' ? 'year' : 'month' );
506 updateInDirection
= 'next';
510 if ( updateInDirection
) {
511 this.displayLayer
= this.getDisplayLayers()[ 0 ];
512 this.setDateFromMoment();
513 this.updateUI( updateInDirection
);
519 }( jQuery
, mediaWiki
) );