summaryrefslogtreecommitdiff
path: root/www/wiki/resources/src/mediawiki.widgets.datetime/CalendarWidget.js
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/resources/src/mediawiki.widgets.datetime/CalendarWidget.js')
-rw-r--r--www/wiki/resources/src/mediawiki.widgets.datetime/CalendarWidget.js596
1 files changed, 596 insertions, 0 deletions
diff --git a/www/wiki/resources/src/mediawiki.widgets.datetime/CalendarWidget.js b/www/wiki/resources/src/mediawiki.widgets.datetime/CalendarWidget.js
new file mode 100644
index 00000000..fa059914
--- /dev/null
+++ b/www/wiki/resources/src/mediawiki.widgets.datetime/CalendarWidget.js
@@ -0,0 +1,596 @@
+( function ( $, mw ) {
+
+ /**
+ * CalendarWidget displays a calendar that can be used to select a date. It
+ * uses {@link mw.widgets.datetime.DateTimeFormatter DateTimeFormatter} to get the details of
+ * the calendar.
+ *
+ * This widget is mainly intended to be used as a popup from a
+ * {@link mw.widgets.datetime.DateTimeInputWidget DateTimeInputWidget}, but may also be used
+ * standalone.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object|mw.widgets.datetime.DateTimeFormatter} [formatter={}] Configuration options for
+ * mw.widgets.datetime.ProlepticGregorianDateTimeFormatter, or an mw.widgets.datetime.DateTimeFormatter
+ * instance to use.
+ * @cfg {OO.ui.Widget|null} [widget=null] Widget associated with the calendar.
+ * Specifying this configures the calendar to be used as a popup from the
+ * specified widget (e.g. absolute positioning, automatic hiding when clicked
+ * outside).
+ * @cfg {Date|null} [min=null] Minimum allowed date
+ * @cfg {Date|null} [max=null] Maximum allowed date
+ * @cfg {Date} [focusedDate] Initially focused date.
+ * @cfg {Date|Date[]|null} [selected=null] Selected date(s).
+ */
+ mw.widgets.datetime.CalendarWidget = function MwWidgetsDatetimeCalendarWidget( config ) {
+ var $colgroup, $headTR, headings, i;
+
+ // Configuration initialization
+ config = $.extend( {
+ min: null,
+ max: null,
+ focusedDate: new Date(),
+ selected: null,
+ formatter: {}
+ }, config );
+
+ // Parent constructor
+ mw.widgets.datetime.CalendarWidget[ 'super' ].call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$element } ) );
+
+ // Properties
+ if ( config.min instanceof Date && config.min.getTime() >= -62167219200000 ) {
+ this.min = config.min;
+ } else {
+ this.min = new Date( -62167219200000 ); // 0000-01-01T00:00:00.000Z
+ }
+ if ( config.max instanceof Date && config.max.getTime() <= 253402300799999 ) {
+ this.max = config.max;
+ } else {
+ this.max = new Date( 253402300799999 ); // 9999-12-31T12:59:59.999Z
+ }
+
+ if ( config.focusedDate instanceof Date ) {
+ this.focusedDate = config.focusedDate;
+ } else {
+ this.focusedDate = new Date();
+ }
+
+ this.selected = [];
+
+ if ( config.formatter instanceof mw.widgets.datetime.DateTimeFormatter ) {
+ this.formatter = config.formatter;
+ } else if ( $.isPlainObject( config.formatter ) ) {
+ this.formatter = new mw.widgets.datetime.ProlepticGregorianDateTimeFormatter( config.formatter );
+ } else {
+ throw new Error( '"formatter" must be an mw.widgets.datetime.DateTimeFormatter or a plain object' );
+ }
+
+ this.calendarData = null;
+
+ this.widget = config.widget;
+ this.$widget = config.widget ? config.widget.$element : null;
+ this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
+
+ this.$head = $( '<div>' );
+ this.$header = $( '<span>' );
+ this.$table = $( '<table>' );
+ this.cols = [];
+ this.colNullable = [];
+ this.headings = [];
+ this.$tableBody = $( '<tbody>' );
+ this.rows = [];
+ this.buttons = {};
+ this.minWidth = 1;
+ this.daysPerWeek = 0;
+
+ // Events
+ this.$element.on( {
+ keydown: this.onKeyDown.bind( this )
+ } );
+ this.formatter.connect( this, {
+ local: 'onLocalChange'
+ } );
+ if ( this.$widget ) {
+ this.checkFocusHandler = this.checkFocus.bind( this );
+ this.$element.on( {
+ focusout: this.onFocusOut.bind( this )
+ } );
+ this.$widget.on( {
+ focusout: this.onFocusOut.bind( this )
+ } );
+ }
+
+ // Initialization
+ this.$head
+ .addClass( 'mw-widgets-datetime-calendarWidget-heading' )
+ .append(
+ new OO.ui.ButtonWidget( {
+ icon: 'previous',
+ framed: false,
+ classes: [ 'mw-widgets-datetime-calendarWidget-previous' ],
+ tabIndex: -1
+ } ).connect( this, { click: 'onPrevClick' } ).$element,
+ new OO.ui.ButtonWidget( {
+ icon: 'next',
+ framed: false,
+ classes: [ 'mw-widgets-datetime-calendarWidget-next' ],
+ tabIndex: -1
+ } ).connect( this, { click: 'onNextClick' } ).$element,
+ this.$header
+ );
+ $colgroup = $( '<colgroup>' );
+ $headTR = $( '<tr>' );
+ this.$table
+ .addClass( 'mw-widgets-datetime-calendarWidget-grid' )
+ .append( $colgroup )
+ .append( $( '<thead>' ).append( $headTR ) )
+ .append( this.$tableBody );
+
+ headings = this.formatter.getCalendarHeadings();
+ for ( i = 0; i < headings.length; i++ ) {
+ this.cols[ i ] = $( '<col>' );
+ this.headings[ i ] = $( '<th>' );
+ this.colNullable[ i ] = headings[ i ] === null;
+ if ( headings[ i ] !== null ) {
+ this.headings[ i ].text( headings[ i ] );
+ this.minWidth = Math.max( this.minWidth, headings[ i ].length );
+ this.daysPerWeek++;
+ }
+ $colgroup.append( this.cols[ i ] );
+ $headTR.append( this.headings[ i ] );
+ }
+
+ this.setSelected( config.selected );
+ this.$element
+ .addClass( 'mw-widgets-datetime-calendarWidget' )
+ .append( this.$head, this.$table );
+
+ if ( this.widget ) {
+ this.$element.addClass( 'mw-widgets-datetime-calendarWidget-dependent' );
+
+ // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+ // that reference properties not initialized at that time of parent class construction
+ // TODO: Find a better way to handle post-constructor setup
+ this.visible = false;
+ this.$element.addClass( 'oo-ui-element-hidden' );
+ } else {
+ this.updateUI();
+ }
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.datetime.CalendarWidget, OO.ui.Widget );
+ OO.mixinClass( mw.widgets.datetime.CalendarWidget, OO.ui.mixin.TabIndexedElement );
+
+ /* Events */
+
+ /**
+ * A `change` event is emitted when the selected dates change
+ *
+ * @event change
+ */
+
+ /**
+ * A `focusChange` event is emitted when the focused date changes
+ *
+ * @event focusChange
+ */
+
+ /**
+ * A `page` event is emitted when the current "month" changes
+ *
+ * @event page
+ */
+
+ /* Methods */
+
+ /**
+ * Return the current selected dates
+ *
+ * @return {Date[]}
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.getSelected = function () {
+ return this.selected;
+ };
+
+ // eslint-disable-next-line valid-jsdoc
+ /**
+ * Set the selected dates
+ *
+ * @param {Date|Date[]|null} dates
+ * @fires change
+ * @chainable
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.setSelected = function ( dates ) {
+ var i, changed = false;
+
+ if ( dates instanceof Date ) {
+ dates = [ dates ];
+ } else if ( Array.isArray( dates ) ) {
+ dates = dates.filter( function ( dt ) { return dt instanceof Date; } );
+ dates.sort();
+ } else {
+ dates = [];
+ }
+
+ if ( this.selected.length !== dates.length ) {
+ changed = true;
+ } else {
+ for ( i = 0; i < dates.length; i++ ) {
+ if ( dates[ i ].getTime() !== this.selected[ i ].getTime() ) {
+ changed = true;
+ break;
+ }
+ }
+ }
+
+ if ( changed ) {
+ this.selected = dates;
+ this.emit( 'change', dates );
+ this.updateUI();
+ }
+
+ return this;
+ };
+
+ /**
+ * Return the currently-focused date
+ *
+ * @return {Date}
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.getFocusedDate = function () {
+ return this.focusedDate;
+ };
+
+ // eslint-disable-next-line valid-jsdoc
+ /**
+ * Set the currently-focused date
+ *
+ * @param {Date} date
+ * @fires page
+ * @chainable
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.setFocusedDate = function ( date ) {
+ var changePage = false,
+ updateUI = false;
+
+ if ( this.focusedDate.getTime() === date.getTime() ) {
+ return this;
+ }
+
+ if ( !this.formatter.sameCalendarGrid( this.focusedDate, date ) ) {
+ changePage = true;
+ updateUI = true;
+ } else if (
+ !this.formatter.timePartIsEqual( this.focusedDate, date ) ||
+ !this.formatter.datePartIsEqual( this.focusedDate, date )
+ ) {
+ updateUI = true;
+ }
+
+ this.focusedDate = date;
+ this.emit( 'focusChanged', this.focusedDate );
+ if ( changePage ) {
+ this.emit( 'page', date );
+ }
+ if ( updateUI ) {
+ this.updateUI();
+ }
+
+ return this;
+ };
+
+ /**
+ * Adjust a date
+ *
+ * @protected
+ * @param {Date} date Date to adjust
+ * @param {string} component Component: 'month', 'week', or 'day'
+ * @param {number} delta Integer, usually -1 or 1
+ * @param {boolean} [enforceRange=true] Whether to enforce this.min and this.max
+ * @return {Date}
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.adjustDate = function ( date, component, delta ) {
+ var newDate,
+ data = this.calendarData;
+
+ if ( !data ) {
+ return date;
+ }
+
+ switch ( component ) {
+ case 'month':
+ newDate = this.formatter.adjustComponent( date, data.monthComponent, delta, 'overflow' );
+ break;
+
+ case 'week':
+ if ( data.weekComponent === undefined ) {
+ newDate = this.formatter.adjustComponent(
+ date, data.dayComponent, delta * this.daysPerWeek, 'overflow' );
+ } else {
+ newDate = this.formatter.adjustComponent( date, data.weekComponent, delta, 'overflow' );
+ }
+ break;
+
+ case 'day':
+ newDate = this.formatter.adjustComponent( date, data.dayComponent, delta, 'overflow' );
+ break;
+
+ default:
+ throw new Error( 'Unknown component' );
+ }
+
+ while ( newDate < this.min ) {
+ newDate = this.formatter.adjustComponent( newDate, data.dayComponent, 1, 'overflow' );
+ }
+ while ( newDate > this.max ) {
+ newDate = this.formatter.adjustComponent( newDate, data.dayComponent, -1, 'overflow' );
+ }
+
+ return newDate;
+ };
+
+ /**
+ * Update the user interface
+ *
+ * @protected
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.updateUI = function () {
+ var r, c, row, day, k, $cell,
+ width = this.minWidth,
+ nullCols = [],
+ focusedDate = this.getFocusedDate(),
+ selected = this.getSelected(),
+ datePartIsEqual = this.formatter.datePartIsEqual.bind( this.formatter ),
+ isSelected = function ( dt ) {
+ return datePartIsEqual( this, dt );
+ };
+
+ this.calendarData = this.formatter.getCalendarData( focusedDate );
+
+ this.$header.text( this.calendarData.header );
+
+ for ( c = 0; c < this.colNullable.length; c++ ) {
+ nullCols[ c ] = this.colNullable[ c ];
+ if ( nullCols[ c ] ) {
+ for ( r = 0; r < this.calendarData.rows.length; r++ ) {
+ if ( this.calendarData.rows[ r ][ c ] ) {
+ nullCols[ c ] = false;
+ break;
+ }
+ }
+ }
+ }
+
+ this.$tableBody.children().detach();
+ for ( r = 0; r < this.calendarData.rows.length; r++ ) {
+ if ( !this.rows[ r ] ) {
+ this.rows[ r ] = $( '<tr>' );
+ } else {
+ this.rows[ r ].children().detach();
+ }
+ this.$tableBody.append( this.rows[ r ] );
+ row = this.calendarData.rows[ r ];
+ for ( c = 0; c < row.length; c++ ) {
+ day = row[ c ];
+ if ( day === null ) {
+ k = 'empty-' + r + '-' + c;
+ if ( !this.buttons[ k ] ) {
+ this.buttons[ k ] = $( '<td>' );
+ }
+ $cell = this.buttons[ k ];
+ $cell.toggleClass( 'oo-ui-element-hidden', nullCols[ c ] );
+ } else {
+ k = ( day.extra ? day.extra : '' ) + day.display;
+ width = Math.max( width, day.display.length );
+ if ( !this.buttons[ k ] ) {
+ this.buttons[ k ] = new OO.ui.ButtonWidget( {
+ $element: $( '<td>' ),
+ classes: [
+ 'mw-widgets-datetime-calendarWidget-cell',
+ day.extra ? 'mw-widgets-datetime-calendarWidget-extra' : ''
+ ],
+ framed: true,
+ label: day.display,
+ tabIndex: -1
+ } );
+ this.buttons[ k ].connect( this, { click: [ 'onDayClick', this.buttons[ k ] ] } );
+ }
+ this.buttons[ k ]
+ .setData( day.date )
+ .setDisabled( day.date < this.min || day.date > this.max );
+ $cell = this.buttons[ k ].$element;
+ $cell.toggleClass( 'mw-widgets-datetime-calendarWidget-focused',
+ this.formatter.datePartIsEqual( focusedDate, day.date ) );
+ $cell.toggleClass( 'mw-widgets-datetime-calendarWidget-selected',
+ selected.some( isSelected, day.date ) );
+ }
+ this.rows[ r ].append( $cell );
+ }
+ }
+
+ for ( c = 0; c < this.cols.length; c++ ) {
+ if ( nullCols[ c ] ) {
+ this.cols[ c ].width( 0 );
+ } else {
+ this.cols[ c ].width( width + 'em' );
+ }
+ this.cols[ c ].toggleClass( 'oo-ui-element-hidden', nullCols[ c ] );
+ this.headings[ c ].toggleClass( 'oo-ui-element-hidden', nullCols[ c ] );
+ }
+ };
+
+ /**
+ * Handles formatter 'local' flag changing
+ *
+ * @protected
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.onLocalChange = function () {
+ if ( this.formatter.localChangesDatePart( this.getFocusedDate() ) ) {
+ this.emit( 'page', this.getFocusedDate() );
+ }
+
+ this.updateUI();
+ };
+
+ /**
+ * Handles previous button click
+ *
+ * @protected
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.onPrevClick = function () {
+ this.setFocusedDate( this.adjustDate( this.getFocusedDate(), 'month', -1 ) );
+ if ( !this.$widget || OO.ui.contains( this.$element[ 0 ], document.activeElement, true ) ) {
+ this.$element.focus();
+ }
+ };
+
+ /**
+ * Handles next button click
+ *
+ * @protected
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.onNextClick = function () {
+ this.setFocusedDate( this.adjustDate( this.getFocusedDate(), 'month', 1 ) );
+ if ( !this.$widget || OO.ui.contains( this.$element[ 0 ], document.activeElement, true ) ) {
+ this.$element.focus();
+ }
+ };
+
+ /**
+ * Handles day button click
+ *
+ * @protected
+ * @param {OO.ui.ButtonWidget} $button
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.onDayClick = function ( $button ) {
+ this.setFocusedDate( $button.getData() );
+ this.setSelected( [ $button.getData() ] );
+ if ( !this.$widget || OO.ui.contains( this.$element[ 0 ], document.activeElement, true ) ) {
+ this.$element.focus();
+ }
+ };
+
+ /**
+ * Handles document mouse down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse down event
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.onDocumentMouseDown = function ( e ) {
+ if ( this.$widget &&
+ !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
+ !OO.ui.contains( this.$widget[ 0 ], e.target, true )
+ ) {
+ this.toggle( false );
+ }
+ };
+
+ /**
+ * Handles key presses.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key down event
+ * @return {boolean} False to cancel the default event
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.onKeyDown = function ( e ) {
+ var focusedDate = this.getFocusedDate();
+
+ if ( !this.isDisabled() ) {
+ switch ( e.which ) {
+ case OO.ui.Keys.ENTER:
+ case OO.ui.Keys.SPACE:
+ this.setSelected( [ focusedDate ] );
+ return false;
+
+ case OO.ui.Keys.LEFT:
+ this.setFocusedDate( this.adjustDate( focusedDate, 'day', -1 ) );
+ return false;
+
+ case OO.ui.Keys.RIGHT:
+ this.setFocusedDate( this.adjustDate( focusedDate, 'day', 1 ) );
+ return false;
+
+ case OO.ui.Keys.UP:
+ this.setFocusedDate( this.adjustDate( focusedDate, 'week', -1 ) );
+ return false;
+
+ case OO.ui.Keys.DOWN:
+ this.setFocusedDate( this.adjustDate( focusedDate, 'week', 1 ) );
+ return false;
+
+ case OO.ui.Keys.PAGEUP:
+ this.setFocusedDate( this.adjustDate( focusedDate, 'month', -1 ) );
+ return false;
+
+ case OO.ui.Keys.PAGEDOWN:
+ this.setFocusedDate( this.adjustDate( focusedDate, 'month', 1 ) );
+ return false;
+ }
+ }
+ };
+
+ /**
+ * Handles focusout events in dependent mode
+ *
+ * @private
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.onFocusOut = function () {
+ setTimeout( this.checkFocusHandler );
+ };
+
+ /**
+ * When we or our widget lost focus, check if the calendar should be hidden.
+ *
+ * @private
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.checkFocus = function () {
+ var containers = [ this.$element[ 0 ], this.$widget[ 0 ] ],
+ activeElement = document.activeElement;
+
+ if ( !activeElement || !OO.ui.contains( containers, activeElement, true ) ) {
+ this.toggle( false );
+ }
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.widgets.datetime.CalendarWidget.prototype.toggle = function ( visible ) {
+ var change;
+
+ visible = ( visible === undefined ? !this.visible : !!visible );
+ change = visible !== this.isVisible();
+
+ // Parent method
+ mw.widgets.datetime.CalendarWidget[ 'super' ].prototype.toggle.call( this, visible );
+
+ if ( change ) {
+ if ( visible ) {
+ // Auto-hide
+ if ( this.$widget ) {
+ this.getElementDocument().addEventListener(
+ 'mousedown', this.onDocumentMouseDownHandler, true
+ );
+ }
+ this.updateUI();
+ } else {
+ this.getElementDocument().removeEventListener(
+ 'mousedown', this.onDocumentMouseDownHandler, true
+ );
+ }
+ }
+
+ return this;
+ };
+
+}( jQuery, mediaWiki ) );