/*!
- * OOjs UI v0.18.4
+ * OOjs UI v0.19.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-18T00:07:07Z
+ * Date: 2017-02-08T00:38:31Z
*/
( function ( OO ) {
*/
OO.ui.generateElementId = function () {
OO.ui.elementId++;
- return 'ooui-' + OO.ui.elementId;
+ return 'oojsui-' + OO.ui.elementId;
};
/**
return this.label;
};
-/**
- * Fit the label.
- *
- * @chainable
- * @deprecated since 0.16.0
- */
-OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
- OO.ui.warnDeprecation( 'LabelElement#fitLabel: This is a deprecated no-op.' );
- return this;
-};
-
/**
* Set the content of the label.
*
/* Static Properties */
/**
+ * @static
* @inheritdoc
*/
OO.ui.ButtonWidget.static.cancelButtonMouseDownEvents = false;
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.IconWidget.static.tagName = 'span';
/**
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.IndicatorWidget.static.tagName = 'span';
/**
// Properties
this.input = config.input;
- // Events
+ // Initialization
if ( this.input instanceof OO.ui.InputWidget ) {
- this.$element.on( 'click', this.onClick.bind( this ) );
+ this.$element.attr( 'for', this.input.getInputId() );
}
-
- // Initialization
this.$element.addClass( 'oo-ui-labelWidget' );
};
/* Static Properties */
-OO.ui.LabelWidget.static.tagName = 'span';
-
-/* Methods */
-
/**
- * Handles label mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
+ * @static
+ * @inheritdoc
*/
-OO.ui.LabelWidget.prototype.onClick = function () {
- this.input.simulateLabelClick();
- return false;
-};
+OO.ui.LabelWidget.static.tagName = 'label';
/**
* PendingElement is a mixin that is used to create elements that notify users that something is happening
* }
* OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
*
+ * MessageDialog.static.name = 'myMessageDialog';
* MessageDialog.static.actions = [
* { action: 'save', label: 'Done', flags: 'primary' },
* { label: 'Cancel', flags: 'safe' }
return this;
};
+/**
+ * Element that will stick under a specified container, even when it is inserted elsewhere in the
+ * document (for example, in a OO.ui.Window's $overlay).
+ *
+ * The elements's position is automatically calculated and maintained when window is resized or the
+ * page is scrolled. If you reposition the container manually, you have to call #position to make
+ * sure the element is still placed correctly.
+ *
+ * As positioning is only possible when both the element and the container are attached to the DOM
+ * and visible, it's only done after you call #togglePositioning. You might want to do this inside
+ * the #toggle method to display a floating popup, for example.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
+ * @cfg {jQuery} [$floatableContainer] Node to position below
+ */
+OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$floatable = null;
+ this.$floatableContainer = null;
+ this.$floatableWindow = null;
+ this.$floatableClosestScrollable = null;
+ this.onFloatableScrollHandler = this.position.bind( this );
+ this.onFloatableWindowResizeHandler = this.position.bind( this );
+
+ // Initialization
+ this.setFloatableContainer( config.$floatableContainer );
+ this.setFloatableElement( config.$floatable || this.$element );
+};
+
+/* Methods */
+
+/**
+ * Set floatable element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $floatable Element to make floatable
+ */
+OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
+ if ( this.$floatable ) {
+ this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
+ this.$floatable.css( { left: '', top: '' } );
+ }
+
+ this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
+ this.position();
+};
+
+/**
+ * Set floatable container.
+ *
+ * The element will be always positioned under the specified container.
+ *
+ * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
+ */
+OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
+ this.$floatableContainer = $floatableContainer;
+ if ( this.$floatable ) {
+ this.position();
+ }
+};
+
+/**
+ * Toggle positioning.
+ *
+ * Do not turn positioning on until after the element is attached to the DOM and visible.
+ *
+ * @param {boolean} [positioning] Enable positioning, omit to toggle
+ * @chainable
+ */
+OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
+ var closestScrollableOfContainer;
+
+ if ( !this.$floatable || !this.$floatableContainer ) {
+ return this;
+ }
+
+ positioning = positioning === undefined ? !this.positioning : !!positioning;
+
+ if ( this.positioning !== positioning ) {
+ this.positioning = positioning;
+
+ closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
+ this.needsCustomPosition = !OO.ui.contains( this.$floatableContainer[ 0 ], this.$floatable[ 0 ] );
+ // If the scrollable is the root, we have to listen to scroll events
+ // on the window because of browser inconsistencies.
+ if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
+ closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
+ }
+
+ if ( positioning ) {
+ this.$floatableWindow = $( this.getElementWindow() );
+ this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
+
+ this.$floatableClosestScrollable = $( closestScrollableOfContainer );
+ this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
+
+ // Initial position after visible
+ this.position();
+ } else {
+ if ( this.$floatableWindow ) {
+ this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
+ this.$floatableWindow = null;
+ }
+
+ if ( this.$floatableClosestScrollable ) {
+ this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
+ this.$floatableClosestScrollable = null;
+ }
+
+ this.$floatable.css( { left: '', top: '' } );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Check whether the bottom edge of the given element is within the viewport of the given container.
+ *
+ * @private
+ * @param {jQuery} $element
+ * @param {jQuery} $container
+ * @return {boolean}
+ */
+OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
+ var elemRect, contRect,
+ leftEdgeInBounds = false,
+ bottomEdgeInBounds = false,
+ rightEdgeInBounds = false;
+
+ elemRect = $element[ 0 ].getBoundingClientRect();
+ if ( $container[ 0 ] === window ) {
+ contRect = {
+ top: 0,
+ left: 0,
+ right: document.documentElement.clientWidth,
+ bottom: document.documentElement.clientHeight
+ };
+ } else {
+ contRect = $container[ 0 ].getBoundingClientRect();
+ }
+
+ // For completeness, if we still cared about topEdgeInBounds, that'd be:
+ // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
+ if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
+ leftEdgeInBounds = true;
+ }
+ if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
+ bottomEdgeInBounds = true;
+ }
+ if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
+ rightEdgeInBounds = true;
+ }
+
+ // We only care that any part of the bottom edge is visible
+ return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
+};
+
+/**
+ * Position the floatable below its container.
+ *
+ * This should only be done when both of them are attached to the DOM and visible.
+ *
+ * @chainable
+ */
+OO.ui.mixin.FloatableElement.prototype.position = function () {
+ var pos;
+
+ if ( !this.positioning ) {
+ return this;
+ }
+
+ if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
+ this.$floatable.addClass( 'oo-ui-element-hidden' );
+ return;
+ } else {
+ this.$floatable.removeClass( 'oo-ui-element-hidden' );
+ }
+
+ if ( !this.needsCustomPosition ) {
+ return;
+ }
+
+ pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
+
+ // Position under container
+ pos.top += this.$floatableContainer.height();
+ this.$floatable.css( pos );
+
+ // We updated the position, so re-evaluate the clipping state.
+ // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
+ // will not notice the need to update itself.)
+ // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
+ // it not listen to the right events in the right places?
+ if ( this.clip ) {
+ this.clip();
+ }
+
+ return this;
+};
+
/**
* Element that can be automatically clipped to visible boundaries.
*
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.LabelElement
* @mixins OO.ui.mixin.ClippableElement
+ * @mixins OO.ui.mixin.FloatableElement
*
* @constructor
* @param {Object} [config] Configuration options
$clippable: this.$body,
$clippableContainer: this.$popup
} ) );
+ OO.ui.mixin.FloatableElement.call( this, config );
// Properties
this.$anchor = $( '<div>' );
OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.FloatableElement );
/* Methods */
OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
if ( change ) {
+ this.togglePositioning( show && !!this.$floatableContainer );
+
if ( show ) {
if ( this.autoClose ) {
this.bindMouseDownListener();
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
+ * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
+ * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
*/
OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
// Parent constructor
OO.ui.PopupButtonWidget.parent.call( this, config );
// Mixin constructors
- OO.ui.mixin.PopupElement.call( this, config );
+ OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, {
+ popup: {
+ $floatableContainer: this.$element
+ }
+ } ) );
+
+ // Properties
+ this.$overlay = config.$overlay || this.$element;
// Events
this.connect( this, { click: 'onAction' } );
// Initialization
this.$element
.addClass( 'oo-ui-popupButtonWidget' )
- .attr( 'aria-haspopup', 'true' )
- .append( this.popup.$element );
+ .attr( 'aria-haspopup', 'true' );
+ this.popup.$element
+ .addClass( 'oo-ui-popupButtonWidget-popup' )
+ .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
+ .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
+ this.$overlay.append( this.popup.$element );
};
/* Setup */
/* Static Properties */
+/**
+ * Whether this option can be selected. See #setSelected.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
OO.ui.OptionWidget.static.selectable = true;
+/**
+ * Whether this option can be highlighted. See #setHighlighted.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
OO.ui.OptionWidget.static.highlightable = true;
+/**
+ * Whether this option can be pressed. See #setPressed.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
OO.ui.OptionWidget.static.pressable = true;
+/**
+ * Whether this option will be scrolled into view when it is selected.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
/* Methods */
return this;
};
+/**
+ * Get text to match search strings against.
+ *
+ * The default implementation returns the label text, but subclasses
+ * can override this to provide more complex behavior.
+ *
+ * @return {string|boolean} String to match search string against
+ */
+OO.ui.OptionWidget.prototype.getMatchText = function () {
+ var label = this.getLabel();
+ return typeof label === 'string' ? label : this.$label.text();
+};
+
/**
* A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
* select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
* @protected
* @param {string} s String to match against items
* @param {boolean} [exact=false] Only accept exact matches
- * @return {Function} function ( OO.ui.OptionItem ) => boolean
+ * @return {Function} function ( OO.ui.OptionWidget ) => boolean
*/
OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
var re;
}
re = new RegExp( re, 'i' );
return function ( item ) {
- var l = item.getLabel();
- if ( typeof l !== 'string' ) {
- l = item.$label.text();
- }
- if ( l.normalize ) {
- l = l.normalize();
+ var matchText = item.getMatchText();
+ if ( matchText.normalize ) {
+ matchText = matchText.normalize();
}
- return re.test( l );
+ return re.test( matchText );
};
};
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
/**
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.MenuSectionOptionWidget.static.selectable = false;
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.MenuSectionOptionWidget.static.highlightable = false;
/**
* @protected
*/
OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
- var i, item,
+ var i, item, visible,
+ anyVisible = false,
len = this.items.length,
showAll = !this.isVisible(),
filter = showAll ? null : this.getItemMatcher( this.$input.val() );
for ( i = 0; i < len; i++ ) {
item = this.items[ i ];
if ( item instanceof OO.ui.OptionWidget ) {
- item.toggle( showAll || filter( item ) );
+ visible = showAll || filter( item );
+ anyVisible = anyVisible || visible;
+ item.toggle( visible );
}
}
+ this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
+
// Reevaluate clipping
this.clip();
};
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioOptionWidget.static.highlightable = false;
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioOptionWidget.static.pressable = false;
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioOptionWidget.static.tagName = 'label';
/* Methods */
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.CheckboxMultioptionWidget.static.tagName = 'label';
/* Methods */
* } );
*
* var multiselect=new OO.ui.CheckboxMultiselectWidget( {
- * items: [ option1, option2 ]
- * } );
- *
- * $( 'body' ).append( multiselect.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
- *
- * @class
- * @extends OO.ui.MultiselectWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget( config ) {
- // Parent constructor
- OO.ui.CheckboxMultiselectWidget.parent.call( this, config );
-
- // Properties
- this.$lastClicked = null;
-
- // Events
- this.$group.on( 'click', this.onClick.bind( this ) );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-checkboxMultiselectWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
-
-/* Methods */
-
-/**
- * Get an option by its position relative to the specified item (or to the start of the option array,
- * if item is `null`). The direction in which to search through the option array is specified with a
- * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
- * `null` if there are no options in the array.
- *
- * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
- * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
- * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
- */
-OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function ( item, direction ) {
- var currentIndex, nextIndex, i,
- increase = direction > 0 ? 1 : -1,
- len = this.items.length;
-
- if ( item ) {
- currentIndex = this.items.indexOf( item );
- nextIndex = ( currentIndex + increase + len ) % len;
- } else {
- // If no item is selected and moving forward, start at the beginning.
- // If moving backward, start at the end.
- nextIndex = direction > 0 ? 0 : len - 1;
- }
-
- for ( i = 0; i < len; i++ ) {
- item = this.items[ nextIndex ];
- if ( item && !item.isDisabled() ) {
- return item;
- }
- nextIndex = ( nextIndex + increase + len ) % len;
- }
- return null;
-};
-
-/**
- * Handle click events on checkboxes.
- *
- * @param {jQuery.Event} e
- */
-OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
- var $options, lastClickedIndex, nowClickedIndex, i, direction, wasSelected, items,
- $lastClicked = this.$lastClicked,
- $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
- .not( '.oo-ui-widget-disabled' );
-
- // Allow selecting multiple options at once by Shift-clicking them
- if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
- $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
- lastClickedIndex = $options.index( $lastClicked );
- nowClickedIndex = $options.index( $nowClicked );
- // If it's the same item, either the user is being silly, or it's a fake event generated by the
- // browser. In either case we don't need custom handling.
- if ( nowClickedIndex !== lastClickedIndex ) {
- items = this.items;
- wasSelected = items[ nowClickedIndex ].isSelected();
- direction = nowClickedIndex > lastClickedIndex ? 1 : -1;
-
- // This depends on the DOM order of the items and the order of the .items array being the same.
- for ( i = lastClickedIndex; i !== nowClickedIndex; i += direction ) {
- if ( !items[ i ].isDisabled() ) {
- items[ i ].setSelected( !wasSelected );
- }
- }
- // For the now-clicked element, use immediate timeout to allow the browser to do its own
- // handling first, then set our value. The order in which events happen is different for
- // clicks on the <input> and on the <label> and there are additional fake clicks fired for
- // non-click actions that change the checkboxes.
- e.preventDefault();
- setTimeout( function () {
- if ( !items[ nowClickedIndex ].isDisabled() ) {
- items[ nowClickedIndex ].setSelected( !wasSelected );
- }
- } );
- }
- }
-
- if ( $nowClicked.length ) {
- this.$lastClicked = $nowClicked;
- }
-};
-
-/**
- * Element that will stick under a specified container, even when it is inserted elsewhere in the
- * document (for example, in a OO.ui.Window's $overlay).
- *
- * The elements's position is automatically calculated and maintained when window is resized or the
- * page is scrolled. If you reposition the container manually, you have to call #position to make
- * sure the element is still placed correctly.
- *
- * As positioning is only possible when both the element and the container are attached to the DOM
- * and visible, it's only done after you call #togglePositioning. You might want to do this inside
- * the #toggle method to display a floating popup, for example.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
- * @cfg {jQuery} [$floatableContainer] Node to position below
- */
-OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$floatable = null;
- this.$floatableContainer = null;
- this.$floatableWindow = null;
- this.$floatableClosestScrollable = null;
- this.onFloatableScrollHandler = this.position.bind( this );
- this.onFloatableWindowResizeHandler = this.position.bind( this );
-
- // Initialization
- this.setFloatableContainer( config.$floatableContainer );
- this.setFloatableElement( config.$floatable || this.$element );
-};
-
-/* Methods */
-
-/**
- * Set floatable element.
- *
- * If an element is already set, it will be cleaned up before setting up the new element.
- *
- * @param {jQuery} $floatable Element to make floatable
- */
-OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
- if ( this.$floatable ) {
- this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
- this.$floatable.css( { left: '', top: '' } );
- }
-
- this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
- this.position();
-};
-
-/**
- * Set floatable container.
+ * items: [ option1, option2 ]
+ * } );
*
- * The element will be always positioned under the specified container.
+ * $( 'body' ).append( multiselect.$element );
*
- * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
- */
-OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
- this.$floatableContainer = $floatableContainer;
- if ( this.$floatable ) {
- this.position();
- }
-};
-
-/**
- * Toggle positioning.
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
*
- * Do not turn positioning on until after the element is attached to the DOM and visible.
+ * @class
+ * @extends OO.ui.MultiselectWidget
*
- * @param {boolean} [positioning] Enable positioning, omit to toggle
- * @chainable
+ * @constructor
+ * @param {Object} [config] Configuration options
*/
-OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
- var closestScrollableOfContainer, closestScrollableOfFloatable;
-
- positioning = positioning === undefined ? !this.positioning : !!positioning;
-
- if ( this.positioning !== positioning ) {
- this.positioning = positioning;
-
- closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
- closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
- this.needsCustomPosition = closestScrollableOfContainer !== closestScrollableOfFloatable;
- // If the scrollable is the root, we have to listen to scroll events
- // on the window because of browser inconsistencies.
- if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
- closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
- }
+OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget( config ) {
+ // Parent constructor
+ OO.ui.CheckboxMultiselectWidget.parent.call( this, config );
- if ( positioning ) {
- this.$floatableWindow = $( this.getElementWindow() );
- this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
+ // Properties
+ this.$lastClicked = null;
- this.$floatableClosestScrollable = $( closestScrollableOfContainer );
- this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
+ // Events
+ this.$group.on( 'click', this.onClick.bind( this ) );
- // Initial position after visible
- this.position();
- } else {
- if ( this.$floatableWindow ) {
- this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
- this.$floatableWindow = null;
- }
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-checkboxMultiselectWidget' );
+};
- if ( this.$floatableClosestScrollable ) {
- this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
- this.$floatableClosestScrollable = null;
- }
+/* Setup */
- this.$floatable.css( { left: '', top: '' } );
- }
- }
+OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
- return this;
-};
+/* Methods */
/**
- * Check whether the bottom edge of the given element is within the viewport of the given container.
+ * Get an option by its position relative to the specified item (or to the start of the option array,
+ * if item is `null`). The direction in which to search through the option array is specified with a
+ * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
+ * `null` if there are no options in the array.
*
- * @private
- * @param {jQuery} $element
- * @param {jQuery} $container
- * @return {boolean}
+ * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
+ * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
+ * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
*/
-OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
- var elemRect, contRect,
- leftEdgeInBounds = false,
- bottomEdgeInBounds = false,
- rightEdgeInBounds = false;
+OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function ( item, direction ) {
+ var currentIndex, nextIndex, i,
+ increase = direction > 0 ? 1 : -1,
+ len = this.items.length;
- elemRect = $element[ 0 ].getBoundingClientRect();
- if ( $container[ 0 ] === window ) {
- contRect = {
- top: 0,
- left: 0,
- right: document.documentElement.clientWidth,
- bottom: document.documentElement.clientHeight
- };
+ if ( item ) {
+ currentIndex = this.items.indexOf( item );
+ nextIndex = ( currentIndex + increase + len ) % len;
} else {
- contRect = $container[ 0 ].getBoundingClientRect();
+ // If no item is selected and moving forward, start at the beginning.
+ // If moving backward, start at the end.
+ nextIndex = direction > 0 ? 0 : len - 1;
}
- // For completeness, if we still cared about topEdgeInBounds, that'd be:
- // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
- if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
- leftEdgeInBounds = true;
- }
- if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
- bottomEdgeInBounds = true;
- }
- if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
- rightEdgeInBounds = true;
+ for ( i = 0; i < len; i++ ) {
+ item = this.items[ nextIndex ];
+ if ( item && !item.isDisabled() ) {
+ return item;
+ }
+ nextIndex = ( nextIndex + increase + len ) % len;
}
-
- // We only care that any part of the bottom edge is visible
- return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
+ return null;
};
/**
- * Position the floatable below its container.
- *
- * This should only be done when both of them are attached to the DOM and visible.
+ * Handle click events on checkboxes.
*
- * @chainable
+ * @param {jQuery.Event} e
*/
-OO.ui.mixin.FloatableElement.prototype.position = function () {
- var pos;
-
- if ( !this.positioning ) {
- return this;
- }
+OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
+ var $options, lastClickedIndex, nowClickedIndex, i, direction, wasSelected, items,
+ $lastClicked = this.$lastClicked,
+ $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
+ .not( '.oo-ui-widget-disabled' );
- if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
- this.$floatable.addClass( 'oo-ui-element-hidden' );
- return;
- } else {
- this.$floatable.removeClass( 'oo-ui-element-hidden' );
- }
+ // Allow selecting multiple options at once by Shift-clicking them
+ if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
+ $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
+ lastClickedIndex = $options.index( $lastClicked );
+ nowClickedIndex = $options.index( $nowClicked );
+ // If it's the same item, either the user is being silly, or it's a fake event generated by the
+ // browser. In either case we don't need custom handling.
+ if ( nowClickedIndex !== lastClickedIndex ) {
+ items = this.items;
+ wasSelected = items[ nowClickedIndex ].isSelected();
+ direction = nowClickedIndex > lastClickedIndex ? 1 : -1;
- if ( !this.needsCustomPosition ) {
- return;
+ // This depends on the DOM order of the items and the order of the .items array being the same.
+ for ( i = lastClickedIndex; i !== nowClickedIndex; i += direction ) {
+ if ( !items[ i ].isDisabled() ) {
+ items[ i ].setSelected( !wasSelected );
+ }
+ }
+ // For the now-clicked element, use immediate timeout to allow the browser to do its own
+ // handling first, then set our value. The order in which events happen is different for
+ // clicks on the <input> and on the <label> and there are additional fake clicks fired for
+ // non-click actions that change the checkboxes.
+ e.preventDefault();
+ setTimeout( function () {
+ if ( !items[ nowClickedIndex ].isDisabled() ) {
+ items[ nowClickedIndex ].setSelected( !wasSelected );
+ }
+ } );
+ }
}
- pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
-
- // Position under container
- pos.top += this.$floatableContainer.height();
- this.$floatable.css( pos );
-
- // We updated the position, so re-evaluate the clipping state.
- // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
- // will not notice the need to update itself.)
- // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
- // it not listen to the right events in the right places?
- if ( this.clip ) {
- this.clip();
+ if ( $nowClicked.length ) {
+ this.$lastClicked = $nowClicked;
}
-
- return this;
};
/**
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.ProgressBarWidget.static.tagName = 'div';
/* Methods */
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.InputWidget.static.supportsSimpleLabel = true;
/* Static Methods */
return $( '<input>' );
};
+/**
+ * Get input element's ID.
+ *
+ * If the element already has an ID then that is returned, otherwise unique ID is
+ * generated, set on the element, and returned.
+ *
+ * @return {string} The ID of the element
+ */
+OO.ui.InputWidget.prototype.getInputId = function () {
+ var id = this.$input.attr( 'id' );
+
+ if ( id === undefined ) {
+ id = OO.ui.generateElementId();
+ this.$input.attr( 'id', id );
+ }
+
+ return id;
+};
+
/**
* Handle potentially value-changing events.
*
* called directly.
*/
OO.ui.InputWidget.prototype.simulateLabelClick = function () {
+ OO.ui.warnDeprecation( 'InputWidget: simulateLabelClick() is deprecated.' );
if ( !this.isDisabled() ) {
if ( this.$input.is( ':checkbox, :radio' ) ) {
this.$input.click();
/**
* Disable generating `<label>` elements for buttons. One would very rarely need additional label
* for a button, and it's already a big clickable target, and it causes unexpected rendering.
+ *
+ * @static
+ * @inheritdoc
*/
OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
/* Static Methods */
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
*/
OO.ui.CheckboxMultiselectInputWidget = function OoUiCheckboxMultiselectInputWidget( config ) {
// Configuration initialization
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.CheckboxMultiselectInputWidget.static.supportsSimpleLabel = false;
/* Static Methods */
/**
* Set the options available for this input.
*
- * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
* @chainable
*/
OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options ) {
this.checkboxMultiselectWidget
.clearItems()
.addItems( options.map( function ( opt ) {
- var optValue, item;
+ var optValue, item, optDisabled;
optValue =
OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( widget, opt.data );
+ optDisabled = opt.disabled !== undefined ? opt.disabled : false;
item = new OO.ui.CheckboxMultioptionWidget( {
data: optValue,
- label: opt.label !== undefined ? opt.label : optValue
+ label: opt.label !== undefined ? opt.label : optValue,
+ disabled: optDisabled
} );
// Set the 'name' and 'value' for form submission
item.checkbox.$input.attr( 'name', widget.inputName );
* @throws {Error} An error is thrown if no widget is specified
*/
OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- var hasInputWidget, $div;
-
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
config = fieldWidget;
throw new Error( 'Widget not found' );
}
- hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
-
// Configuration initialization
config = $.extend( { align: 'left' }, config );
OO.ui.FieldLayout.parent.call( this, config );
// Mixin constructors
- OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+ $label: $( '<label>' )
+ } ) );
OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
// Properties
this.$field = $( '<div>' );
this.$messages = $( '<ul>' );
this.$header = $( '<div>' );
- this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
+ this.$body = $( '<div>' );
this.align = null;
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ popup: {
+ padded: true
+ },
classes: [ 'oo-ui-fieldLayout-help' ],
framed: false,
icon: 'info'
} );
-
- $div = $( '<div>' );
if ( config.help instanceof OO.ui.HtmlSnippet ) {
- $div.html( config.help.toString() );
+ this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
} else {
- $div.text( config.help );
+ this.popupButtonWidget.getPopup().$body.text( config.help );
}
- this.popupButtonWidget.getPopup().$body.append(
- $div.addClass( 'oo-ui-fieldLayout-help-content' )
- );
this.$help = this.popupButtonWidget.$element;
} else {
this.$help = $( [] );
}
// Events
- if ( hasInputWidget ) {
- this.$label.on( 'click', this.onLabelClick.bind( this ) );
- }
this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
// Initialization
+ if ( fieldWidget.constructor.static.supportsSimpleLabel ) {
+ this.$label.attr( 'for', this.fieldWidget.getInputId() );
+ }
this.$element
.addClass( 'oo-ui-fieldLayout' )
.toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget.isDisabled() )
this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
};
-/**
- * Handle label mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.FieldLayout.prototype.onLabelClick = function () {
- this.fieldWidget.simulateLabelClick();
- return false;
-};
-
/**
* Get the widget contained by the field.
*
* For important messages, you are advised to use `notices`, as they are always shown.
*/
OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
- var $div;
-
// Configuration initialization
config = config || {};
this.$header = $( '<div>' );
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ popup: {
+ padded: true
+ },
classes: [ 'oo-ui-fieldsetLayout-help' ],
framed: false,
icon: 'info'
} );
-
- $div = $( '<div>' );
if ( config.help instanceof OO.ui.HtmlSnippet ) {
- $div.html( config.help.toString() );
+ this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
} else {
- $div.text( config.help );
+ this.popupButtonWidget.getPopup().$body.text( config.help );
}
- this.popupButtonWidget.getPopup().$body.append(
- $div.addClass( 'oo-ui-fieldsetLayout-help-content' )
- );
this.$help = this.popupButtonWidget.$element;
} else {
this.$help = $( [] );
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.FieldsetLayout.static.tagName = 'fieldset';
/**
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.FormLayout.static.tagName = 'form';
/* Methods */