diff options
Diffstat (limited to 'vendor/oojs/oojs-ui/src/widgets')
39 files changed, 5234 insertions, 0 deletions
diff --git a/vendor/oojs/oojs-ui/src/widgets/ActionWidget.js b/vendor/oojs/oojs-ui/src/widgets/ActionWidget.js new file mode 100644 index 00000000..789f04f1 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ActionWidget.js @@ -0,0 +1,170 @@ +/** + * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action. + * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability + * of the actions. + * + * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}. + * Please see the [OOjs UI documentation on MediaWiki] [1] for more information + * and examples. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets + * + * @class + * @extends OO.ui.ButtonWidget + * @mixins OO.ui.PendingElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’). + * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action + * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method + * for more information about setting modes. + * @cfg {boolean} [framed=false] Render the action button with a frame + */ +OO.ui.ActionWidget = function OoUiActionWidget( config ) { + // Configuration initialization + config = $.extend( { framed: false }, config ); + + // Parent constructor + OO.ui.ActionWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.PendingElement.call( this, config ); + + // Properties + this.action = config.action || ''; + this.modes = config.modes || []; + this.width = 0; + this.height = 0; + + // Initialization + this.$element.addClass( 'oo-ui-actionWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget ); +OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement ); + +/* Events */ + +/** + * A resize event is emitted when the size of the widget changes. + * + * @event resize + */ + +/* Methods */ + +/** + * Check if the action is configured to be available in the specified `mode`. + * + * @param {string} mode Name of mode + * @return {boolean} The action is configured with the mode + */ +OO.ui.ActionWidget.prototype.hasMode = function ( mode ) { + return this.modes.indexOf( mode ) !== -1; +}; + +/** + * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’). + * + * @return {string} + */ +OO.ui.ActionWidget.prototype.getAction = function () { + return this.action; +}; + +/** + * Get the symbolic name of the mode or modes for which the action is configured to be available. + * + * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method. + * Only actions that are configured to be avaiable in the current mode will be visible. All other actions + * are hidden. + * + * @return {string[]} + */ +OO.ui.ActionWidget.prototype.getModes = function () { + return this.modes.slice(); +}; + +/** + * Emit a resize event if the size has changed. + * + * @private + * @chainable + */ +OO.ui.ActionWidget.prototype.propagateResize = function () { + var width, height; + + if ( this.isElementAttached() ) { + width = this.$element.width(); + height = this.$element.height(); + + if ( width !== this.width || height !== this.height ) { + this.width = width; + this.height = height; + this.emit( 'resize' ); + } + } + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.ActionWidget.prototype.setIcon = function () { + // Mixin method + OO.ui.IconElement.prototype.setIcon.apply( this, arguments ); + this.propagateResize(); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.ActionWidget.prototype.setLabel = function () { + // Mixin method + OO.ui.LabelElement.prototype.setLabel.apply( this, arguments ); + this.propagateResize(); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.ActionWidget.prototype.setFlags = function () { + // Mixin method + OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments ); + this.propagateResize(); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.ActionWidget.prototype.clearFlags = function () { + // Mixin method + OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments ); + this.propagateResize(); + + return this; +}; + +/** + * Toggle the visibility of the action button. + * + * @param {boolean} [show] Show button, omit to toggle visibility + * @chainable + */ +OO.ui.ActionWidget.prototype.toggle = function () { + // Parent method + OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments ); + this.propagateResize(); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ButtonGroupWidget.js b/vendor/oojs/oojs-ui/src/widgets/ButtonGroupWidget.js new file mode 100644 index 00000000..f1388ab8 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ButtonGroupWidget.js @@ -0,0 +1,53 @@ +/** + * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and + * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added, + * removed, and cleared from the group. + * + * @example + * // Example: A ButtonGroupWidget with two buttons + * var button1 = new OO.ui.PopupButtonWidget( { + * label: 'Select a category', + * icon: 'menu', + * popup: { + * $content: $( '<p>List of categories...</p>' ), + * padded: true, + * align: 'left' + * } + * } ); + * var button2 = new OO.ui.ButtonWidget( { + * label: 'Add item' + * }); + * var buttonGroup = new OO.ui.ButtonGroupWidget( { + * items: [button1, button2] + * } ); + * $( 'body' ).append( buttonGroup.$element ); + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.GroupElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add + */ +OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.ButtonGroupWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) ); + + // Initialization + this.$element.addClass( 'oo-ui-buttonGroupWidget' ); + if ( Array.isArray( config.items ) ) { + this.addItems( config.items ); + } +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement ); diff --git a/vendor/oojs/oojs-ui/src/widgets/ButtonInputWidget.js b/vendor/oojs/oojs-ui/src/widgets/ButtonInputWidget.js new file mode 100644 index 00000000..1d4d97fe --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ButtonInputWidget.js @@ -0,0 +1,121 @@ +/** + * ButtonInputWidget is used to submit HTML forms and is intended to be used within + * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably + * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an + * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the + * [OOjs UI documentation on MediaWiki] [1] for more information. + * + * @example + * // A ButtonInputWidget rendered as an HTML button, the default. + * var button = new OO.ui.ButtonInputWidget( { + * label: 'Input button', + * icon: 'check', + * value: 'check' + * } ); + * $( 'body' ).append( button.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs + * + * @class + * @extends OO.ui.InputWidget + * @mixins OO.ui.ButtonElement + * @mixins OO.ui.IconElement + * @mixins OO.ui.IndicatorElement + * @mixins OO.ui.LabelElement + * @mixins OO.ui.TitledElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'. + * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default. + * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators}, + * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only + * be set to `true` when there’s need to support IE6 in a form with multiple buttons. + */ +OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) { + // Configuration initialization + config = $.extend( { type: 'button', useInputTag: false }, config ); + + // Properties (must be set before parent constructor, which calls #setValue) + this.useInputTag = config.useInputTag; + + // Parent constructor + OO.ui.ButtonInputWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) ); + OO.ui.IconElement.call( this, config ); + OO.ui.IndicatorElement.call( this, config ); + OO.ui.LabelElement.call( this, config ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) ); + + // Initialization + if ( !config.useInputTag ) { + this.$input.append( this.$icon, this.$label, this.$indicator ); + } + this.$element.addClass( 'oo-ui-buttonInputWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget ); +OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement ); +OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement ); +OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement ); +OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement ); + +/* Methods */ + +/** + * @inheritdoc + * @private + */ +OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) { + var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">'; + return $( html ); +}; + +/** + * Set label value. + * + * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag. + * + * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or + * text, or `null` for no label + * @chainable + */ +OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) { + OO.ui.LabelElement.prototype.setLabel.call( this, label ); + + if ( this.useInputTag ) { + if ( typeof label === 'function' ) { + label = OO.ui.resolveMsg( label ); + } + if ( label instanceof jQuery ) { + label = label.text(); + } + if ( !label ) { + label = ''; + } + this.$input.val( label ); + } + + return this; +}; + +/** + * Set the value of the input. + * + * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as + * they do not support {@link #value values}. + * + * @param {string} value New value + * @chainable + */ +OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) { + if ( !this.useInputTag ) { + OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value ); + } + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ButtonOptionWidget.js b/vendor/oojs/oojs-ui/src/widgets/ButtonOptionWidget.js new file mode 100644 index 00000000..7758c949 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ButtonOptionWidget.js @@ -0,0 +1,60 @@ +/** + * ButtonOptionWidget is a special type of {@link OO.ui.ButtonElement button element} that + * can be selected and configured with data. The class is + * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the + * [OOjs UI documentation on MediaWiki] [1] for more information. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options + * + * @class + * @extends OO.ui.DecoratedOptionWidget + * @mixins OO.ui.ButtonElement + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) { + // Configuration initialization + config = $.extend( { tabIndex: -1 }, config ); + + // Parent constructor + OO.ui.ButtonOptionWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.ButtonElement.call( this, config ); + OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) ); + + // Initialization + this.$element.addClass( 'oo-ui-buttonOptionWidget' ); + this.$button.append( this.$element.contents() ); + this.$element.append( this.$button ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget ); +OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement ); +OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement ); + +/* Static Properties */ + +// Allow button mouse down events to pass through so they can be handled by the parent select widget +OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false; + +OO.ui.ButtonOptionWidget.static.highlightable = false; + +/* Methods */ + +/** + * @inheritdoc + */ +OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) { + OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state ); + + if ( this.constructor.static.selectable ) { + this.setActive( state ); + } + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ButtonSelectWidget.js b/vendor/oojs/oojs-ui/src/widgets/ButtonSelectWidget.js new file mode 100644 index 00000000..18177761 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ButtonSelectWidget.js @@ -0,0 +1,62 @@ +/** + * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains + * button options and is used together with + * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for + * highlighting, choosing, and selecting mutually exclusive options. Please see + * the [OOjs UI documentation on MediaWiki] [1] for more information. + * + * @example + * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets + * var option1 = new OO.ui.ButtonOptionWidget( { + * data: 1, + * label: 'Option 1', + * title: 'Button option 1' + * } ); + * + * var option2 = new OO.ui.ButtonOptionWidget( { + * data: 2, + * label: 'Option 2', + * title: 'Button option 2' + * } ); + * + * var option3 = new OO.ui.ButtonOptionWidget( { + * data: 3, + * label: 'Option 3', + * title: 'Button option 3' + * } ); + * + * var buttonSelect=new OO.ui.ButtonSelectWidget( { + * items: [ option1, option2, option3 ] + * } ); + * $( 'body' ).append( buttonSelect.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options + * + * @class + * @extends OO.ui.SelectWidget + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) { + // Parent constructor + OO.ui.ButtonSelectWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.TabIndexedElement.call( this, config ); + + // Events + this.$element.on( { + focus: this.bindKeyDownListener.bind( this ), + blur: this.unbindKeyDownListener.bind( this ) + } ); + + // Initialization + this.$element.addClass( 'oo-ui-buttonSelectWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget ); +OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.TabIndexedElement ); diff --git a/vendor/oojs/oojs-ui/src/widgets/ButtonWidget.js b/vendor/oojs/oojs-ui/src/widgets/ButtonWidget.js new file mode 100644 index 00000000..474fafed --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ButtonWidget.js @@ -0,0 +1,215 @@ +/** + * ButtonWidget is a generic widget for buttons. A wide variety of looks, + * feels, and functionality can be customized via the class’s configuration options + * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information + * and examples. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches + * + * @example + * // A button widget + * var button = new OO.ui.ButtonWidget( { + * label: 'Button with Icon', + * icon: 'remove', + * iconTitle: 'Remove' + * } ); + * $( 'body' ).append( button.$element ); + * + * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class. + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.ButtonElement + * @mixins OO.ui.IconElement + * @mixins OO.ui.IndicatorElement + * @mixins OO.ui.LabelElement + * @mixins OO.ui.TitledElement + * @mixins OO.ui.FlaggedElement + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [href] Hyperlink to visit when the button is clicked. + * @cfg {string} [target] The frame or window in which to open the hyperlink. + * @cfg {boolean} [noFollow] Search engine traversal hint (default: true) + */ +OO.ui.ButtonWidget = function OoUiButtonWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.ButtonWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.ButtonElement.call( this, config ); + OO.ui.IconElement.call( this, config ); + OO.ui.IndicatorElement.call( this, config ); + OO.ui.LabelElement.call( this, config ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) ); + OO.ui.FlaggedElement.call( this, config ); + OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) ); + + // Properties + this.href = null; + this.target = null; + this.noFollow = false; + + // Events + this.connect( this, { disable: 'onDisable' } ); + + // Initialization + this.$button.append( this.$icon, this.$label, this.$indicator ); + this.$element + .addClass( 'oo-ui-buttonWidget' ) + .append( this.$button ); + this.setHref( config.href ); + this.setTarget( config.target ); + this.setNoFollow( config.noFollow ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement ); +OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement ); + +/* Methods */ + +/** + * @inheritdoc + */ +OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) { + if ( !this.isDisabled() ) { + // Remove the tab-index while the button is down to prevent the button from stealing focus + this.$button.removeAttr( 'tabindex' ); + } + + return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e ); +}; + +/** + * @inheritdoc + */ +OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) { + if ( !this.isDisabled() ) { + // Restore the tab-index after the button is up to restore the button's accessibility + this.$button.attr( 'tabindex', this.tabIndex ); + } + + return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e ); +}; + +/** + * Get hyperlink location. + * + * @return {string} Hyperlink location + */ +OO.ui.ButtonWidget.prototype.getHref = function () { + return this.href; +}; + +/** + * Get hyperlink target. + * + * @return {string} Hyperlink target + */ +OO.ui.ButtonWidget.prototype.getTarget = function () { + return this.target; +}; + +/** + * Get search engine traversal hint. + * + * @return {boolean} Whether search engines should avoid traversing this hyperlink + */ +OO.ui.ButtonWidget.prototype.getNoFollow = function () { + return this.noFollow; +}; + +/** + * Set hyperlink location. + * + * @param {string|null} href Hyperlink location, null to remove + */ +OO.ui.ButtonWidget.prototype.setHref = function ( href ) { + href = typeof href === 'string' ? href : null; + + if ( href !== this.href ) { + this.href = href; + this.updateHref(); + } + + return this; +}; + +/** + * Update the `href` attribute, in case of changes to href or + * disabled state. + * + * @private + * @chainable + */ +OO.ui.ButtonWidget.prototype.updateHref = function () { + if ( this.href !== null && !this.isDisabled() ) { + this.$button.attr( 'href', this.href ); + } else { + this.$button.removeAttr( 'href' ); + } + + return this; +}; + +/** + * Handle disable events. + * + * @private + * @param {boolean} disabled Element is disabled + */ +OO.ui.ButtonWidget.prototype.onDisable = function () { + this.updateHref(); +}; + +/** + * Set hyperlink target. + * + * @param {string|null} target Hyperlink target, null to remove + */ +OO.ui.ButtonWidget.prototype.setTarget = function ( target ) { + target = typeof target === 'string' ? target : null; + + if ( target !== this.target ) { + this.target = target; + if ( target !== null ) { + this.$button.attr( 'target', target ); + } else { + this.$button.removeAttr( 'target' ); + } + } + + return this; +}; + +/** + * Set search engine traversal hint. + * + * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink + */ +OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) { + noFollow = typeof noFollow === 'boolean' ? noFollow : true; + + if ( noFollow !== this.noFollow ) { + this.noFollow = noFollow; + if ( noFollow ) { + this.$button.attr( 'rel', 'nofollow' ); + } else { + this.$button.removeAttr( 'rel' ); + } + } + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/CheckboxInputWidget.js b/vendor/oojs/oojs-ui/src/widgets/CheckboxInputWidget.js new file mode 100644 index 00000000..1ac93bc5 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/CheckboxInputWidget.js @@ -0,0 +1,110 @@ +/** + * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value. + * Note that these {@link OO.ui.InputWidget input widgets} are best laid out + * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline} + * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1]. + * + * This widget can be used inside a HTML form, such as a OO.ui.FormLayout. + * + * @example + * // An example of selected, unselected, and disabled checkbox inputs + * var checkbox1=new OO.ui.CheckboxInputWidget( { + * value: 'a', + * selected: true + * } ); + * var checkbox2=new OO.ui.CheckboxInputWidget( { + * value: 'b' + * } ); + * var checkbox3=new OO.ui.CheckboxInputWidget( { + * value:'c', + * disabled: true + * } ); + * // Create a fieldset layout with fields for each checkbox. + * var fieldset = new OO.ui.FieldsetLayout( { + * label: 'Checkboxes' + * } ); + * fieldset.addItems( [ + * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ), + * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ), + * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ), + * ] ); + * $( 'body' ).append( fieldset.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs + * + * @class + * @extends OO.ui.InputWidget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected. + */ +OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.CheckboxInputWidget.super.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-checkboxInputWidget' ); + this.setSelected( config.selected !== undefined ? config.selected : false ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget ); + +/* Methods */ + +/** + * @inheritdoc + * @private + */ +OO.ui.CheckboxInputWidget.prototype.getInputElement = function () { + return $( '<input type="checkbox" />' ); +}; + +/** + * @inheritdoc + */ +OO.ui.CheckboxInputWidget.prototype.onEdit = function () { + var widget = this; + if ( !this.isDisabled() ) { + // Allow the stack to clear so the value will be updated + setTimeout( function () { + widget.setSelected( widget.$input.prop( 'checked' ) ); + } ); + } +}; + +/** + * Set selection state of this checkbox. + * + * @param {boolean} state `true` for selected + * @chainable + */ +OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) { + state = !!state; + if ( this.selected !== state ) { + this.selected = state; + this.$input.prop( 'checked', this.selected ); + this.emit( 'change', this.selected ); + } + return this; +}; + +/** + * Check if this checkbox is selected. + * + * @return {boolean} Checkbox is selected + */ +OO.ui.CheckboxInputWidget.prototype.isSelected = function () { + // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify + // it, and we won't know unless they're kind enough to trigger a 'change' event. + var selected = this.$input.prop( 'checked' ); + if ( this.selected !== selected ) { + this.setSelected( selected ); + } + return this.selected; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ComboBoxWidget.js b/vendor/oojs/oojs-ui/src/widgets/ComboBoxWidget.js new file mode 100644 index 00000000..346d17ee --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ComboBoxWidget.js @@ -0,0 +1,230 @@ +/** + * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value + * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which + * a value can be chosen instead). Users can choose options from the combo box in one of two ways: + * + * - by typing a value in the text input field. If the value exactly matches the value of a menu + * option, that option will appear to be selected. + * - by choosing a value from the menu. The value of the chosen option will then appear in the text + * input field. + * + * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1]. + * + * @example + * // Example: A ComboBoxWidget. + * var comboBox = new OO.ui.ComboBoxWidget( { + * label: 'ComboBoxWidget', + * input: { value: 'Option One' }, + * menu: { + * items: [ + * new OO.ui.MenuOptionWidget( { + * data: 'Option 1', + * label: 'Option One' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'Option 2', + * label: 'Option Two' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'Option 3', + * label: 'Option Three' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'Option 4', + * label: 'Option Four' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'Option 5', + * label: 'Option Five' + * } ) + * ] + * } + * } ); + * $( 'body' ).append( comboBox.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}. + * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}. + * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where + * the expanded menu 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 menu uses relative positioning. + */ +OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.ComboBoxWidget.super.call( this, config ); + + // Properties (must be set before TabIndexedElement constructor call) + this.$indicator = this.$( '<span>' ); + + // Mixin constructors + OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) ); + + // Properties + this.$overlay = config.$overlay || this.$element; + this.input = new OO.ui.TextInputWidget( $.extend( + { + indicator: 'down', + $indicator: this.$indicator, + disabled: this.isDisabled() + }, + config.input + ) ); + this.input.$input.eq( 0 ).attr( { + role: 'combobox', + 'aria-autocomplete': 'list' + } ); + this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend( + { + widget: this, + input: this.input, + disabled: this.isDisabled() + }, + config.menu + ) ); + + // Events + this.$indicator.on( { + click: this.onClick.bind( this ), + keypress: this.onKeyPress.bind( this ) + } ); + this.input.connect( this, { + change: 'onInputChange', + enter: 'onInputEnter' + } ); + this.menu.connect( this, { + choose: 'onMenuChoose', + add: 'onMenuItemsChange', + remove: 'onMenuItemsChange' + } ); + + // Initialization + this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element ); + this.$overlay.append( this.menu.$element ); + this.onMenuItemsChange(); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement ); + +/* Methods */ + +/** + * Get the combobox's menu. + * @return {OO.ui.TextInputMenuSelectWidget} Menu widget + */ +OO.ui.ComboBoxWidget.prototype.getMenu = function () { + return this.menu; +}; + +/** + * Handle input change events. + * + * @private + * @param {string} value New value + */ +OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) { + var match = this.menu.getItemFromData( value ); + + this.menu.selectItem( match ); + if ( this.menu.getHighlightedItem() ) { + this.menu.highlightItem( match ); + } + + if ( !this.isDisabled() ) { + this.menu.toggle( true ); + } +}; + +/** + * Handle mouse click events. + * + * + * @private + * @param {jQuery.Event} e Mouse click event + */ +OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) { + if ( !this.isDisabled() && e.which === 1 ) { + this.menu.toggle(); + this.input.$input[ 0 ].focus(); + } + return false; +}; + +/** + * Handle key press events. + * + * + * @private + * @param {jQuery.Event} e Key press event + */ +OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) { + if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) { + this.menu.toggle(); + this.input.$input[ 0 ].focus(); + return false; + } +}; + +/** + * Handle input enter events. + * + * @private + */ +OO.ui.ComboBoxWidget.prototype.onInputEnter = function () { + if ( !this.isDisabled() ) { + this.menu.toggle( false ); + } +}; + +/** + * Handle menu choose events. + * + * @private + * @param {OO.ui.OptionWidget} item Chosen item + */ +OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) { + this.input.setValue( item.getData() ); +}; + +/** + * Handle menu item change events. + * + * @private + */ +OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () { + var match = this.menu.getItemFromData( this.input.getValue() ); + this.menu.selectItem( match ); + if ( this.menu.getHighlightedItem() ) { + this.menu.highlightItem( match ); + } + this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() ); +}; + +/** + * @inheritdoc + */ +OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) { + // Parent method + OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled ); + + if ( this.input ) { + this.input.setDisabled( this.isDisabled() ); + } + if ( this.menu ) { + this.menu.setDisabled( this.isDisabled() ); + } + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/DecoratedOptionWidget.js b/vendor/oojs/oojs-ui/src/widgets/DecoratedOptionWidget.js new file mode 100644 index 00000000..e39ebcad --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/DecoratedOptionWidget.js @@ -0,0 +1,55 @@ +/** + * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured + * with an {@link OO.ui.IconElement icon} and/or {@link OO.ui.IndicatorElement indicator}. + * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive + * options. For more information about options and selects, please see the + * [OOjs UI documentation on MediaWiki][1]. + * + * @example + * // Decorated options in a select widget + * var select = new OO.ui.SelectWidget( { + * items: [ + * new OO.ui.DecoratedOptionWidget( { + * data: 'a', + * label: 'Option with icon', + * icon: 'help' + * } ), + * new OO.ui.DecoratedOptionWidget( { + * data: 'b', + * label: 'Option with indicator', + * indicator: 'next' + * } ) + * ] + * } ); + * $( 'body' ).append( select.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options + * + * @class + * @extends OO.ui.OptionWidget + * @mixins OO.ui.IconElement + * @mixins OO.ui.IndicatorElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) { + // Parent constructor + OO.ui.DecoratedOptionWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.IconElement.call( this, config ); + OO.ui.IndicatorElement.call( this, config ); + + // Initialization + this.$element + .addClass( 'oo-ui-decoratedOptionWidget' ) + .prepend( this.$icon ) + .append( this.$indicator ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget ); +OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement ); diff --git a/vendor/oojs/oojs-ui/src/widgets/DropdownInputWidget.js b/vendor/oojs/oojs-ui/src/widgets/DropdownInputWidget.js new file mode 100644 index 00000000..d4a5ce2a --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/DropdownInputWidget.js @@ -0,0 +1,137 @@ +/** + * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used + * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value + * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for + * more information about input widgets. + * + * @example + * // Example: A DropdownInputWidget with three options + * var dropDown = new OO.ui.DropdownInputWidget( { + * label: 'Dropdown menu: Select a menu option', + * options: [ + * { data: 'a', label: 'First' } , + * { data: 'b', label: 'Second'} , + * { data: 'c', label: 'Third' } + * ] + * } ); + * $( 'body' ).append( dropDown.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs + * + * @class + * @extends OO.ui.InputWidget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }` + */ +OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) { + // Configuration initialization + config = config || {}; + + // Properties (must be done before parent constructor which calls #setDisabled) + this.dropdownWidget = new OO.ui.DropdownWidget(); + + // Parent constructor + OO.ui.DropdownInputWidget.super.call( this, config ); + + // Events + this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } ); + + // Initialization + this.setOptions( config.options || [] ); + this.$element + .addClass( 'oo-ui-dropdownInputWidget' ) + .append( this.dropdownWidget.$element ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget ); + +/* Methods */ + +/** + * @inheritdoc + * @private + */ +OO.ui.DropdownInputWidget.prototype.getInputElement = function () { + return $( '<input type="hidden">' ); +}; + +/** + * Handles menu select events. + * + * @private + * @param {OO.ui.MenuOptionWidget} item Selected menu item + */ +OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) { + this.setValue( item.getData() ); +}; + +/** + * @inheritdoc + */ +OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) { + this.dropdownWidget.getMenu().selectItemByData( value ); + OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value ); + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) { + this.dropdownWidget.setDisabled( state ); + OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state ); + return this; +}; + +/** + * Set the options available for this input. + * + * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }` + * @chainable + */ +OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) { + var value = this.getValue(); + + // Rebuild the dropdown menu + this.dropdownWidget.getMenu() + .clearItems() + .addItems( options.map( function ( opt ) { + return new OO.ui.MenuOptionWidget( { + data: opt.data, + label: opt.label !== undefined ? opt.label : opt.data + } ); + } ) ); + + // Restore the previous value, or reset to something sensible + if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) { + // Previous value is still available, ensure consistency with the dropdown + this.setValue( value ); + } else { + // No longer valid, reset + if ( options.length ) { + this.setValue( options[ 0 ].data ); + } + } + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.DropdownInputWidget.prototype.focus = function () { + this.dropdownWidget.getMenu().toggle( true ); + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.DropdownInputWidget.prototype.blur = function () { + this.dropdownWidget.getMenu().toggle( false ); + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/DropdownWidget.js b/vendor/oojs/oojs-ui/src/widgets/DropdownWidget.js new file mode 100644 index 00000000..f6c592e8 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/DropdownWidget.js @@ -0,0 +1,149 @@ +/** + * DropdownWidgets are not menus themselves, rather they contain a menu of options created with + * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that + * users can interact with it. + * + * @example + * // Example: A DropdownWidget with a menu that contains three options + * var dropDown = new OO.ui.DropdownWidget( { + * label: 'Dropdown menu: Select a menu option', + * menu: { + * items: [ + * new OO.ui.MenuOptionWidget( { + * data: 'a', + * label: 'First' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'b', + * label: 'Second' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'c', + * label: 'Third' + * } ) + * ] + * } + * } ); + * + * $( 'body' ).append( dropDown.$element ); + * + * For more information, please see the [OOjs UI documentation on MediaWiki] [1]. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.IconElement + * @mixins OO.ui.IndicatorElement + * @mixins OO.ui.LabelElement + * @mixins OO.ui.TitledElement + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {Object} [menu] Configuration options to pass to menu widget + */ +OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) { + // Configuration initialization + config = $.extend( { indicator: 'down' }, config ); + + // Parent constructor + OO.ui.DropdownWidget.super.call( this, config ); + + // Properties (must be set before TabIndexedElement constructor call) + this.$handle = this.$( '<span>' ); + + // Mixin constructors + OO.ui.IconElement.call( this, config ); + OO.ui.IndicatorElement.call( this, config ); + OO.ui.LabelElement.call( this, config ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) ); + OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) ); + + // Properties + this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) ); + + // Events + this.$handle.on( { + click: this.onClick.bind( this ), + keypress: this.onKeyPress.bind( this ) + } ); + this.menu.connect( this, { select: 'onMenuSelect' } ); + + // Initialization + this.$handle + .addClass( 'oo-ui-dropdownWidget-handle' ) + .append( this.$icon, this.$label, this.$indicator ); + this.$element + .addClass( 'oo-ui-dropdownWidget' ) + .append( this.$handle, this.menu.$element ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement ); + +/* Methods */ + +/** + * Get the menu. + * + * @return {OO.ui.MenuSelectWidget} Menu of widget + */ +OO.ui.DropdownWidget.prototype.getMenu = function () { + return this.menu; +}; + +/** + * Handles menu select events. + * + * @private + * @param {OO.ui.MenuOptionWidget} item Selected menu item + */ +OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) { + var selectedLabel; + + if ( !item ) { + return; + } + + selectedLabel = item.getLabel(); + + // If the label is a DOM element, clone it, because setLabel will append() it + if ( selectedLabel instanceof jQuery ) { + selectedLabel = selectedLabel.clone(); + } + + this.setLabel( selectedLabel ); +}; + +/** + * Handle mouse click events. + * + * @private + * @param {jQuery.Event} e Mouse click event + */ +OO.ui.DropdownWidget.prototype.onClick = function ( e ) { + if ( !this.isDisabled() && e.which === 1 ) { + this.menu.toggle(); + } + return false; +}; + +/** + * Handle key press events. + * + * @private + * @param {jQuery.Event} e Key press event + */ +OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) { + if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) { + this.menu.toggle(); + return false; + } +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/GroupWidget.js b/vendor/oojs/oojs-ui/src/widgets/GroupWidget.js new file mode 100644 index 00000000..7d9be905 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/GroupWidget.js @@ -0,0 +1,48 @@ +/** + * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement. + * + * Use together with OO.ui.ItemWidget to make disabled state inheritable. + * + * @private + * @abstract + * @class + * @extends OO.ui.GroupElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.GroupWidget = function OoUiGroupWidget( config ) { + // Parent constructor + OO.ui.GroupWidget.super.call( this, config ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement ); + +/* Methods */ + +/** + * Set the disabled state of the widget. + * + * This will also update the disabled state of child widgets. + * + * @param {boolean} disabled Disable widget + * @chainable + */ +OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) { + var i, len; + + // Parent method + // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget + OO.ui.Widget.prototype.setDisabled.call( this, disabled ); + + // During construction, #setDisabled is called before the OO.ui.GroupElement constructor + if ( this.items ) { + for ( i = 0, len = this.items.length; i < len; i++ ) { + this.items[ i ].updateDisabled(); + } + } + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/IconWidget.js b/vendor/oojs/oojs-ui/src/widgets/IconWidget.js new file mode 100644 index 00000000..66ea3871 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/IconWidget.js @@ -0,0 +1,54 @@ +/** + * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget, + * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1] + * for a list of icons included in the library. + * + * @example + * // An icon widget with a label + * var myIcon = new OO.ui.IconWidget( { + * icon: 'help', + * iconTitle: 'Help' + * } ); + * // Create a label. + * var iconLabel = new OO.ui.LabelWidget( { + * label: 'Help' + * } ); + * $( 'body' ).append( myIcon.$element, iconLabel.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.IconElement + * @mixins OO.ui.TitledElement + * @mixins OO.ui.FlaggedElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.IconWidget = function OoUiIconWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.IconWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) ); + OO.ui.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) ); + + // Initialization + this.$element.addClass( 'oo-ui-iconWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement ); +OO.mixinClass( OO.ui.IconWidget, OO.ui.FlaggedElement ); + +/* Static Properties */ + +OO.ui.IconWidget.static.tagName = 'span'; diff --git a/vendor/oojs/oojs-ui/src/widgets/IndicatorWidget.js b/vendor/oojs/oojs-ui/src/widgets/IndicatorWidget.js new file mode 100644 index 00000000..e6ecddf0 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/IndicatorWidget.js @@ -0,0 +1,52 @@ +/** + * IndicatorWidgets create indicators, which are small graphics that are generally used to draw + * attention to the status of an item or to clarify the function of a control. For a list of + * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1]. + * + * @example + * // Example of an indicator widget + * var indicator1 = new OO.ui.IndicatorWidget( { + * indicator: 'alert' + * } ); + * + * // Create a fieldset layout to add a label + * var fieldset = new OO.ui.FieldsetLayout(); + * fieldset.addItems( [ + * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } ) + * ] ); + * $( 'body' ).append( fieldset.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.IndicatorElement + * @mixins OO.ui.TitledElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.IndicatorWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) ); + + // Initialization + this.$element.addClass( 'oo-ui-indicatorWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement ); +OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement ); + +/* Static Properties */ + +OO.ui.IndicatorWidget.static.tagName = 'span'; diff --git a/vendor/oojs/oojs-ui/src/widgets/InputWidget.js b/vendor/oojs/oojs-ui/src/widgets/InputWidget.js new file mode 100644 index 00000000..de790bac --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/InputWidget.js @@ -0,0 +1,207 @@ +/** + * InputWidget is the base class for all input widgets, which + * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs}, + * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}. + * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs + * + * @abstract + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.FlaggedElement + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [name=''] The value of the input’s HTML `name` attribute. + * @cfg {string} [value=''] The value of the input. + * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input + * before it is accepted. + */ +OO.ui.InputWidget = function OoUiInputWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.InputWidget.super.call( this, config ); + + // Properties + this.$input = this.getInputElement( config ); + this.value = ''; + this.inputFilter = config.inputFilter; + + // Mixin constructors + OO.ui.FlaggedElement.call( this, config ); + OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) ); + + // Events + this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) ); + + // Initialization + this.$input + .attr( 'name', config.name ) + .prop( 'disabled', this.isDisabled() ); + this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) ); + this.setValue( config.value ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement ); +OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement ); + +/* Events */ + +/** + * @event change + * + * A change event is emitted when the value of the input changes. + * + * @param {string} value + */ + +/* Methods */ + +/** + * Get input element. + * + * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in + * different circumstances. The element must have a `value` property (like form elements). + * + * @private + * @param {Object} config Configuration options + * @return {jQuery} Input element + */ +OO.ui.InputWidget.prototype.getInputElement = function () { + return $( '<input>' ); +}; + +/** + * Handle potentially value-changing events. + * + * @private + * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event + */ +OO.ui.InputWidget.prototype.onEdit = function () { + var widget = this; + if ( !this.isDisabled() ) { + // Allow the stack to clear so the value will be updated + setTimeout( function () { + widget.setValue( widget.$input.val() ); + } ); + } +}; + +/** + * Get the value of the input. + * + * @return {string} Input value + */ +OO.ui.InputWidget.prototype.getValue = function () { + // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify + // it, and we won't know unless they're kind enough to trigger a 'change' event. + var value = this.$input.val(); + if ( this.value !== value ) { + this.setValue( value ); + } + return this.value; +}; + +/** + * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right). + * + * @param {boolean} isRTL + * Direction is right-to-left + */ +OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) { + this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' ); +}; + +/** + * Set the value of the input. + * + * @param {string} value New value + * @fires change + * @chainable + */ +OO.ui.InputWidget.prototype.setValue = function ( value ) { + value = this.cleanUpValue( value ); + // Update the DOM if it has changed. Note that with cleanUpValue, it + // is possible for the DOM value to change without this.value changing. + if ( this.$input.val() !== value ) { + this.$input.val( value ); + } + if ( this.value !== value ) { + this.value = value; + this.emit( 'change', this.value ); + } + return this; +}; + +/** + * Clean up incoming value. + * + * Ensures value is a string, and converts undefined and null to empty string. + * + * @private + * @param {string} value Original value + * @return {string} Cleaned up value + */ +OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) { + if ( value === undefined || value === null ) { + return ''; + } else if ( this.inputFilter ) { + return this.inputFilter( String( value ) ); + } else { + return String( value ); + } +}; + +/** + * Simulate the behavior of clicking on a label bound to this input. This method is only called by + * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be + * called directly. + */ +OO.ui.InputWidget.prototype.simulateLabelClick = function () { + if ( !this.isDisabled() ) { + if ( this.$input.is( ':checkbox, :radio' ) ) { + this.$input.click(); + } + if ( this.$input.is( ':input' ) ) { + this.$input[ 0 ].focus(); + } + } +}; + +/** + * @inheritdoc + */ +OO.ui.InputWidget.prototype.setDisabled = function ( state ) { + OO.ui.InputWidget.super.prototype.setDisabled.call( this, state ); + if ( this.$input ) { + this.$input.prop( 'disabled', this.isDisabled() ); + } + return this; +}; + +/** + * Focus the input. + * + * @chainable + */ +OO.ui.InputWidget.prototype.focus = function () { + this.$input[ 0 ].focus(); + return this; +}; + +/** + * Blur the input. + * + * @chainable + */ +OO.ui.InputWidget.prototype.blur = function () { + this.$input[ 0 ].blur(); + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ItemWidget.js b/vendor/oojs/oojs-ui/src/widgets/ItemWidget.js new file mode 100644 index 00000000..292514f4 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ItemWidget.js @@ -0,0 +1,48 @@ +/** + * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget. + * + * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This + * allows bidirectional communication. + * + * Use together with OO.ui.GroupWidget to make disabled state inheritable. + * + * @private + * @abstract + * @class + * + * @constructor + */ +OO.ui.ItemWidget = function OoUiItemWidget() { + // +}; + +/* Methods */ + +/** + * Check if widget is disabled. + * + * Checks parent if present, making disabled state inheritable. + * + * @return {boolean} Widget is disabled + */ +OO.ui.ItemWidget.prototype.isDisabled = function () { + return this.disabled || + ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() ); +}; + +/** + * Set group element is in. + * + * @param {OO.ui.GroupElement|null} group Group element, null if none + * @chainable + */ +OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) { + // Parent method + // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element + OO.ui.Element.prototype.setElementGroup.call( this, group ); + + // Initialize item disabled states + this.updateDisabled(); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/LabelWidget.js b/vendor/oojs/oojs-ui/src/widgets/LabelWidget.js new file mode 100644 index 00000000..e00990bb --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/LabelWidget.js @@ -0,0 +1,84 @@ +/** + * LabelWidgets help identify the function of interface elements. Each LabelWidget can + * be configured with a `label` option that is set to a string, a label node, or a function: + * + * - String: a plaintext string + * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a + * label that includes a link or special styling, such as a gray color or additional graphical elements. + * - Function: a function that will produce a string in the future. Functions are used + * in cases where the value of the label is not currently defined. + * + * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which + * will come into focus when the label is clicked. + * + * @example + * // Examples of LabelWidgets + * var label1 = new OO.ui.LabelWidget( { + * label: 'plaintext label' + * } ); + * var label2 = new OO.ui.LabelWidget( { + * label: $( '<a href="default.html">jQuery label</a>' ) + * } ); + * // Create a fieldset layout with fields for each example + * var fieldset = new OO.ui.FieldsetLayout(); + * fieldset.addItems( [ + * new OO.ui.FieldLayout( label1 ), + * new OO.ui.FieldLayout( label2 ) + * ] ); + * $( 'body' ).append( fieldset.$element ); + * + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.LabelElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label. + * Clicking the label will focus the specified input field. + */ +OO.ui.LabelWidget = function OoUiLabelWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.LabelWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) ); + OO.ui.TitledElement.call( this, config ); + + // Properties + this.input = config.input; + + // Events + if ( this.input instanceof OO.ui.InputWidget ) { + this.$element.on( 'click', this.onClick.bind( this ) ); + } + + // Initialization + this.$element.addClass( 'oo-ui-labelWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement ); +OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement ); + +/* Static Properties */ + +OO.ui.LabelWidget.static.tagName = 'span'; + +/* Methods */ + +/** + * Handles label mouse click events. + * + * @private + * @param {jQuery.Event} e Mouse click event + */ +OO.ui.LabelWidget.prototype.onClick = function () { + this.input.simulateLabelClick(); + return false; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/MenuOptionWidget.js b/vendor/oojs/oojs-ui/src/widgets/MenuOptionWidget.js new file mode 100644 index 00000000..cc620d97 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/MenuOptionWidget.js @@ -0,0 +1,33 @@ +/** + * MenuOptionWidget is an option widget that looks like a menu item. The class is used with + * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see + * the [OOjs UI documentation on MediaWiki] [1] for more information. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options + * + * @class + * @extends OO.ui.DecoratedOptionWidget + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) { + // Configuration initialization + config = $.extend( { icon: 'check' }, config ); + + // Parent constructor + OO.ui.MenuOptionWidget.super.call( this, config ); + + // Initialization + this.$element + .attr( 'role', 'menuitem' ) + .addClass( 'oo-ui-menuOptionWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget ); + +/* Static Properties */ + +OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true; diff --git a/vendor/oojs/oojs-ui/src/widgets/MenuSectionOptionWidget.js b/vendor/oojs/oojs-ui/src/widgets/MenuSectionOptionWidget.js new file mode 100644 index 00000000..054df7fc --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/MenuSectionOptionWidget.js @@ -0,0 +1,55 @@ +/** + * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related + * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected. + * + * @example + * var myDropdown = new OO.ui.DropdownWidget( { + * menu: { + * items: [ + * new OO.ui.MenuSectionOptionWidget( { + * label: 'Dogs' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'corgi', + * label: 'Welsh Corgi' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'poodle', + * label: 'Standard Poodle' + * } ), + * new OO.ui.MenuSectionOptionWidget( { + * label: 'Cats' + * } ), + * new OO.ui.MenuOptionWidget( { + * data: 'lion', + * label: 'Lion' + * } ) + * ] + * } + * } ); + * $( 'body' ).append( myDropdown.$element ); + * + * + * @class + * @extends OO.ui.DecoratedOptionWidget + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) { + // Parent constructor + OO.ui.MenuSectionOptionWidget.super.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-menuSectionOptionWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget ); + +/* Static Properties */ + +OO.ui.MenuSectionOptionWidget.static.selectable = false; + +OO.ui.MenuSectionOptionWidget.static.highlightable = false; diff --git a/vendor/oojs/oojs-ui/src/widgets/MenuSelectWidget.js b/vendor/oojs/oojs-ui/src/widgets/MenuSelectWidget.js new file mode 100644 index 00000000..a1755edd --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/MenuSelectWidget.js @@ -0,0 +1,254 @@ +/** + * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and + * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget. + * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget}, + * and {@link OO.ui.LookupElement LookupElement} for examples of widgets that contain menus. + * MenuSelectWidgets themselves are not instantiated directly, rather subclassed + * and customized to be opened, closed, and displayed as needed. + * + * By default, menus are clipped to the visible viewport and are not visible when a user presses the + * mouse outside the menu. + * + * Menus also have support for keyboard interaction: + * + * - Enter/Return key: choose and select a menu option + * - Up-arrow key: highlight the previous menu option + * - Down-arrow key: highlight the next menu option + * - Esc key: hide the menu + * + * Please see the [OOjs UI documentation on MediaWiki][1] for more information. + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options + * + * @class + * @extends OO.ui.SelectWidget + * @mixins OO.ui.ClippableElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match + * the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget} + * and {@link OO.ui.LookupElement LookupElement} + * @cfg {OO.ui.Widget} [widget] Widget associated with the menu’s active state. If the user clicks the mouse + * anywhere on the page outside of this widget, the menu is hidden. + * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu. + */ +OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.MenuSelectWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) ); + + // Properties + this.newItems = null; + this.autoHide = config.autoHide === undefined || !!config.autoHide; + this.$input = config.input ? config.input.$input : null; + this.$widget = config.widget ? config.widget.$element : null; + this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this ); + + // Initialization + this.$element + .addClass( 'oo-ui-menuSelectWidget' ) + .attr( 'role', 'menu' ); + + // 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' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget ); +OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement ); + +/* Methods */ + +/** + * Handles document mouse down events. + * + * @protected + * @param {jQuery.Event} e Key down event + */ +OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) { + if ( + !OO.ui.contains( this.$element[ 0 ], e.target, true ) && + ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) ) + ) { + this.toggle( false ); + } +}; + +/** + * @inheritdoc + */ +OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) { + var currentItem = this.getHighlightedItem() || this.getSelectedItem(); + + if ( !this.isDisabled() && this.isVisible() ) { + switch ( e.keyCode ) { + case OO.ui.Keys.LEFT: + case OO.ui.Keys.RIGHT: + // Do nothing if a text field is associated, arrow keys will be handled natively + if ( !this.$input ) { + OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e ); + } + break; + case OO.ui.Keys.ESCAPE: + case OO.ui.Keys.TAB: + if ( currentItem ) { + currentItem.setHighlighted( false ); + } + this.toggle( false ); + // Don't prevent tabbing away, prevent defocusing + if ( e.keyCode === OO.ui.Keys.ESCAPE ) { + e.preventDefault(); + e.stopPropagation(); + } + break; + default: + OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e ); + return; + } + } +}; + +/** + * @inheritdoc + */ +OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () { + if ( this.$input ) { + this.$input.on( 'keydown', this.onKeyDownHandler ); + } else { + OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this ); + } +}; + +/** + * @inheritdoc + */ +OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () { + if ( this.$input ) { + this.$input.off( 'keydown', this.onKeyDownHandler ); + } else { + OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this ); + } +}; + +/** + * Choose an item. + * + * When a user chooses an item, the menu is closed. + * + * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard + * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method. + * @param {OO.ui.OptionWidget} item Item to choose + * @chainable + */ +OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) { + OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item ); + this.toggle( false ); + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) { + var i, len, item; + + // Parent method + OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index ); + + // Auto-initialize + if ( !this.newItems ) { + this.newItems = []; + } + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + if ( this.isVisible() ) { + // Defer fitting label until item has been attached + item.fitLabel(); + } else { + this.newItems.push( item ); + } + } + + // Reevaluate clipping + this.clip(); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) { + // Parent method + OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items ); + + // Reevaluate clipping + this.clip(); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.MenuSelectWidget.prototype.clearItems = function () { + // Parent method + OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this ); + + // Reevaluate clipping + this.clip(); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) { + visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length; + + var i, len, + change = visible !== this.isVisible(); + + // Parent method + OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible ); + + if ( change ) { + if ( visible ) { + this.bindKeyDownListener(); + + if ( this.newItems && this.newItems.length ) { + for ( i = 0, len = this.newItems.length; i < len; i++ ) { + this.newItems[ i ].fitLabel(); + } + this.newItems = null; + } + this.toggleClipping( true ); + + // Auto-hide + if ( this.autoHide ) { + this.getElementDocument().addEventListener( + 'mousedown', this.onDocumentMouseDownHandler, true + ); + } + } else { + this.unbindKeyDownListener(); + this.getElementDocument().removeEventListener( + 'mousedown', this.onDocumentMouseDownHandler, true + ); + this.toggleClipping( false ); + } + } + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/OptionWidget.js b/vendor/oojs/oojs-ui/src/widgets/OptionWidget.js new file mode 100644 index 00000000..0bc2486e --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/OptionWidget.js @@ -0,0 +1,177 @@ +/** + * OptionWidgets are special elements that can be selected and configured with data. The + * data is often unique for each option, but it does not have to be. OptionWidgets are used + * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information + * and examples, please see the [OOjs UI documentation on MediaWiki][1]. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.LabelElement + * @mixins OO.ui.FlaggedElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.OptionWidget = function OoUiOptionWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.OptionWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.ItemWidget.call( this ); + OO.ui.LabelElement.call( this, config ); + OO.ui.FlaggedElement.call( this, config ); + + // Properties + this.selected = false; + this.highlighted = false; + this.pressed = false; + + // Initialization + this.$element + .data( 'oo-ui-optionWidget', this ) + .attr( 'role', 'option' ) + .addClass( 'oo-ui-optionWidget' ) + .append( this.$label ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget ); +OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement ); +OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement ); + +/* Static Properties */ + +OO.ui.OptionWidget.static.selectable = true; + +OO.ui.OptionWidget.static.highlightable = true; + +OO.ui.OptionWidget.static.pressable = true; + +OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false; + +/* Methods */ + +/** + * Check if the option can be selected. + * + * @return {boolean} Item is selectable + */ +OO.ui.OptionWidget.prototype.isSelectable = function () { + return this.constructor.static.selectable && !this.isDisabled(); +}; + +/** + * Check if the option can be highlighted. A highlight indicates that the option + * may be selected when a user presses enter or clicks. Disabled items cannot + * be highlighted. + * + * @return {boolean} Item is highlightable + */ +OO.ui.OptionWidget.prototype.isHighlightable = function () { + return this.constructor.static.highlightable && !this.isDisabled(); +}; + +/** + * Check if the option can be pressed. The pressed state occurs when a user mouses + * down on an item, but has not yet let go of the mouse. + * + * @return {boolean} Item is pressable + */ +OO.ui.OptionWidget.prototype.isPressable = function () { + return this.constructor.static.pressable && !this.isDisabled(); +}; + +/** + * Check if the option is selected. + * + * @return {boolean} Item is selected + */ +OO.ui.OptionWidget.prototype.isSelected = function () { + return this.selected; +}; + +/** + * Check if the option is highlighted. A highlight indicates that the + * item may be selected when a user presses enter or clicks. + * + * @return {boolean} Item is highlighted + */ +OO.ui.OptionWidget.prototype.isHighlighted = function () { + return this.highlighted; +}; + +/** + * Check if the option is pressed. The pressed state occurs when a user mouses + * down on an item, but has not yet let go of the mouse. The item may appear + * selected, but it will not be selected until the user releases the mouse. + * + * @return {boolean} Item is pressed + */ +OO.ui.OptionWidget.prototype.isPressed = function () { + return this.pressed; +}; + +/** + * Set the option’s selected state. In general, all modifications to the selection + * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} + * method instead of this method. + * + * @param {boolean} [state=false] Select option + * @chainable + */ +OO.ui.OptionWidget.prototype.setSelected = function ( state ) { + if ( this.constructor.static.selectable ) { + this.selected = !!state; + this.$element + .toggleClass( 'oo-ui-optionWidget-selected', state ) + .attr( 'aria-selected', state.toString() ); + if ( state && this.constructor.static.scrollIntoViewOnSelect ) { + this.scrollElementIntoView(); + } + this.updateThemeClasses(); + } + return this; +}; + +/** + * Set the option’s highlighted state. In general, all programmatic + * modifications to the highlight should be handled by the + * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )} + * method instead of this method. + * + * @param {boolean} [state=false] Highlight option + * @chainable + */ +OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) { + if ( this.constructor.static.highlightable ) { + this.highlighted = !!state; + this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state ); + this.updateThemeClasses(); + } + return this; +}; + +/** + * Set the option’s pressed state. In general, all + * programmatic modifications to the pressed state should be handled by the + * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )} + * method instead of this method. + * + * @param {boolean} [state=false] Press option + * @chainable + */ +OO.ui.OptionWidget.prototype.setPressed = function ( state ) { + if ( this.constructor.static.pressable ) { + this.pressed = !!state; + this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state ); + this.updateThemeClasses(); + } + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/OutlineControlsWidget.js b/vendor/oojs/oojs-ui/src/widgets/OutlineControlsWidget.js new file mode 100644 index 00000000..0219a44d --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/OutlineControlsWidget.js @@ -0,0 +1,145 @@ +/** + * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}. + * Controls include moving items up and down, removing items, and adding different kinds of items. + * ####Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.#### + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.GroupElement + * @mixins OO.ui.IconElement + * + * @constructor + * @param {OO.ui.OutlineSelectWidget} outline Outline to control + * @param {Object} [config] Configuration options + * @cfg {Object} [abilities] List of abilties + * @cfg {boolean} [abilities.move=true] Allow moving movable items + * @cfg {boolean} [abilities.remove=true] Allow removing removable items + */ +OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) { + // Allow passing positional parameters inside the config object + if ( OO.isPlainObject( outline ) && config === undefined ) { + config = outline; + outline = config.outline; + } + + // Configuration initialization + config = $.extend( { icon: 'add' }, config ); + + // Parent constructor + OO.ui.OutlineControlsWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.GroupElement.call( this, config ); + OO.ui.IconElement.call( this, config ); + + // Properties + this.outline = outline; + this.$movers = $( '<div>' ); + this.upButton = new OO.ui.ButtonWidget( { + framed: false, + icon: 'collapse', + title: OO.ui.msg( 'ooui-outline-control-move-up' ) + } ); + this.downButton = new OO.ui.ButtonWidget( { + framed: false, + icon: 'expand', + title: OO.ui.msg( 'ooui-outline-control-move-down' ) + } ); + this.removeButton = new OO.ui.ButtonWidget( { + framed: false, + icon: 'remove', + title: OO.ui.msg( 'ooui-outline-control-remove' ) + } ); + this.abilities = { move: true, remove: true }; + + // Events + outline.connect( this, { + select: 'onOutlineChange', + add: 'onOutlineChange', + remove: 'onOutlineChange' + } ); + this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } ); + this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } ); + this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } ); + + // Initialization + this.$element.addClass( 'oo-ui-outlineControlsWidget' ); + this.$group.addClass( 'oo-ui-outlineControlsWidget-items' ); + this.$movers + .addClass( 'oo-ui-outlineControlsWidget-movers' ) + .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element ); + this.$element.append( this.$icon, this.$group, this.$movers ); + this.setAbilities( config.abilities || {} ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement ); +OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement ); + +/* Events */ + +/** + * @event move + * @param {number} places Number of places to move + */ + +/** + * @event remove + */ + +/* Methods */ + +/** + * Set abilities. + * + * @param {Object} abilities List of abilties + * @param {boolean} [abilities.move] Allow moving movable items + * @param {boolean} [abilities.remove] Allow removing removable items + */ +OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) { + var ability; + + for ( ability in this.abilities ) { + if ( abilities[ability] !== undefined ) { + this.abilities[ability] = !!abilities[ability]; + } + } + + this.onOutlineChange(); +}; + +/** + * + * @private + * Handle outline change events. + */ +OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () { + var i, len, firstMovable, lastMovable, + items = this.outline.getItems(), + selectedItem = this.outline.getSelectedItem(), + movable = this.abilities.move && selectedItem && selectedItem.isMovable(), + removable = this.abilities.remove && selectedItem && selectedItem.isRemovable(); + + if ( movable ) { + i = -1; + len = items.length; + while ( ++i < len ) { + if ( items[ i ].isMovable() ) { + firstMovable = items[ i ]; + break; + } + } + i = len; + while ( i-- ) { + if ( items[ i ].isMovable() ) { + lastMovable = items[ i ]; + break; + } + } + } + this.upButton.setDisabled( !movable || selectedItem === firstMovable ); + this.downButton.setDisabled( !movable || selectedItem === lastMovable ); + this.removeButton.setDisabled( !removable ); +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/OutlineOptionWidget.js b/vendor/oojs/oojs-ui/src/widgets/OutlineOptionWidget.js new file mode 100644 index 00000000..d792fee6 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/OutlineOptionWidget.js @@ -0,0 +1,130 @@ +/** + * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}. + * + * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain + * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout} + * for an example. + * + * @class + * @extends OO.ui.DecoratedOptionWidget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number} [level] Indentation level + * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}. + */ +OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.OutlineOptionWidget.super.call( this, config ); + + // Properties + this.level = 0; + this.movable = !!config.movable; + this.removable = !!config.removable; + + // Initialization + this.$element.addClass( 'oo-ui-outlineOptionWidget' ); + this.setLevel( config.level ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget ); + +/* Static Properties */ + +OO.ui.OutlineOptionWidget.static.highlightable = false; + +OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true; + +OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-'; + +OO.ui.OutlineOptionWidget.static.levels = 3; + +/* Methods */ + +/** + * Check if item is movable. + * + * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}. + * + * @return {boolean} Item is movable + */ +OO.ui.OutlineOptionWidget.prototype.isMovable = function () { + return this.movable; +}; + +/** + * Check if item is removable. + * + * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}. + * + * @return {boolean} Item is removable + */ +OO.ui.OutlineOptionWidget.prototype.isRemovable = function () { + return this.removable; +}; + +/** + * Get indentation level. + * + * @return {number} Indentation level + */ +OO.ui.OutlineOptionWidget.prototype.getLevel = function () { + return this.level; +}; + +/** + * Set movability. + * + * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}. + * + * @param {boolean} movable Item is movable + * @chainable + */ +OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) { + this.movable = !!movable; + this.updateThemeClasses(); + return this; +}; + +/** + * Set removability. + * + * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}. + * + * @param {boolean} movable Item is removable + * @chainable + */ +OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) { + this.removable = !!removable; + this.updateThemeClasses(); + return this; +}; + +/** + * Set indentation level. + * + * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel] + * @chainable + */ +OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) { + var levels = this.constructor.static.levels, + levelClass = this.constructor.static.levelClass, + i = levels; + + this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0; + while ( i-- ) { + if ( this.level === i ) { + this.$element.addClass( levelClass + i ); + } else { + this.$element.removeClass( levelClass + i ); + } + } + this.updateThemeClasses(); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/OutlineSelectWidget.js b/vendor/oojs/oojs-ui/src/widgets/OutlineSelectWidget.js new file mode 100644 index 00000000..cd7db546 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/OutlineSelectWidget.js @@ -0,0 +1,34 @@ +/** + * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options} + * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget. + * + * ####Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.#### + * + * @class + * @extends OO.ui.SelectWidget + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) { + // Parent constructor + OO.ui.OutlineSelectWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.TabIndexedElement.call( this, config ); + + // Events + this.$element.on( { + focus: this.bindKeyDownListener.bind( this ), + blur: this.unbindKeyDownListener.bind( this ) + } ); + + // Initialization + this.$element.addClass( 'oo-ui-outlineSelectWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget ); +OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.TabIndexedElement ); diff --git a/vendor/oojs/oojs-ui/src/widgets/PopupButtonWidget.js b/vendor/oojs/oojs-ui/src/widgets/PopupButtonWidget.js new file mode 100644 index 00000000..5643b77c --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/PopupButtonWidget.js @@ -0,0 +1,57 @@ +/** + * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget}, + * which is used to display additional information or options. + * + * @example + * // Example of a popup button. + * var popupButton = new OO.ui.PopupButtonWidget( { + * label: 'Popup button with options', + * icon: 'menu', + * popup: { + * $content: $( '<p>Additional options here.</p>' ), + * padded: true, + * align: 'force-left' + * } + * } ); + * // Append the button to the DOM. + * $( 'body' ).append( popupButton.$element ); + * + * @class + * @extends OO.ui.ButtonWidget + * @mixins OO.ui.PopupElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) { + // Parent constructor + OO.ui.PopupButtonWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.PopupElement.call( this, config ); + + // Events + this.connect( this, { click: 'onAction' } ); + + // Initialization + this.$element + .addClass( 'oo-ui-popupButtonWidget' ) + .attr( 'aria-haspopup', 'true' ) + .append( this.popup.$element ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget ); +OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement ); + +/* Methods */ + +/** + * Handle the button action being triggered. + * + * @private + */ +OO.ui.PopupButtonWidget.prototype.onAction = function () { + this.popup.toggle(); +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/PopupWidget.js b/vendor/oojs/oojs-ui/src/widgets/PopupWidget.js new file mode 100644 index 00000000..0b1f4ca6 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/PopupWidget.js @@ -0,0 +1,395 @@ +/** + * PopupWidget is a container for content. The popup is overlaid and positioned absolutely. + * By default, each popup has an anchor that points toward its origin. + * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples. + * + * @example + * // A popup widget. + * var popup = new OO.ui.PopupWidget( { + * $content: $( '<p>Hi there!</p>' ), + * padded: true, + * width: 300 + * } ); + * + * $( 'body' ).append( popup.$element ); + * // To display the popup, toggle the visibility to 'true'. + * popup.toggle( true ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.LabelElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number} [width=320] Width of popup in pixels + * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height. + * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup + * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`. + * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the + * popup is leaning towards the right of the screen. + * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence + * in the given language, which means it will flip to the correct positioning in right-to-left languages. + * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the + * sentence in the given language. + * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container. + * See the [OOjs UI docs on MediaWiki][3] for an example. + * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample + * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels. + * @cfg {jQuery} [$content] Content to append to the popup's body + * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus. + * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked. + * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2] + * for an example. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample + * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close + * button. + * @cfg {boolean} [padded] Add padding to the popup's body + */ +OO.ui.PopupWidget = function OoUiPopupWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.PopupWidget.super.call( this, config ); + + // Properties (must be set before ClippableElement constructor call) + this.$body = $( '<div>' ); + + // Mixin constructors + OO.ui.LabelElement.call( this, config ); + OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) ); + + // Properties + this.$popup = $( '<div>' ); + this.$head = $( '<div>' ); + this.$anchor = $( '<div>' ); + // If undefined, will be computed lazily in updateDimensions() + this.$container = config.$container; + this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10; + this.autoClose = !!config.autoClose; + this.$autoCloseIgnore = config.$autoCloseIgnore; + this.transitionTimeout = null; + this.anchor = null; + this.width = config.width !== undefined ? config.width : 320; + this.height = config.height !== undefined ? config.height : null; + this.setAlignment( config.align ); + this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } ); + this.onMouseDownHandler = this.onMouseDown.bind( this ); + this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this ); + + // Events + this.closeButton.connect( this, { click: 'onCloseButtonClick' } ); + + // Initialization + this.toggleAnchor( config.anchor === undefined || config.anchor ); + this.$body.addClass( 'oo-ui-popupWidget-body' ); + this.$anchor.addClass( 'oo-ui-popupWidget-anchor' ); + this.$head + .addClass( 'oo-ui-popupWidget-head' ) + .append( this.$label, this.closeButton.$element ); + if ( !config.head ) { + this.$head.addClass( 'oo-ui-element-hidden' ); + } + this.$popup + .addClass( 'oo-ui-popupWidget-popup' ) + .append( this.$head, this.$body ); + this.$element + .addClass( 'oo-ui-popupWidget' ) + .append( this.$popup, this.$anchor ); + // Move content, which was added to #$element by OO.ui.Widget, to the body + if ( config.$content instanceof jQuery ) { + this.$body.append( config.$content ); + } + if ( config.padded ) { + this.$body.addClass( 'oo-ui-popupWidget-body-padded' ); + } + + // 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' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement ); +OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement ); + +/* Methods */ + +/** + * Handles mouse down events. + * + * @private + * @param {MouseEvent} e Mouse down event + */ +OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) { + if ( + this.isVisible() && + !$.contains( this.$element[ 0 ], e.target ) && + ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length ) + ) { + this.toggle( false ); + } +}; + +/** + * Bind mouse down listener. + * + * @private + */ +OO.ui.PopupWidget.prototype.bindMouseDownListener = function () { + // Capture clicks outside popup + this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true ); +}; + +/** + * Handles close button click events. + * + * @private + */ +OO.ui.PopupWidget.prototype.onCloseButtonClick = function () { + if ( this.isVisible() ) { + this.toggle( false ); + } +}; + +/** + * Unbind mouse down listener. + * + * @private + */ +OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () { + this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true ); +}; + +/** + * Handles key down events. + * + * @private + * @param {KeyboardEvent} e Key down event + */ +OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) { + if ( + e.which === OO.ui.Keys.ESCAPE && + this.isVisible() + ) { + this.toggle( false ); + e.preventDefault(); + e.stopPropagation(); + } +}; + +/** + * Bind key down listener. + * + * @private + */ +OO.ui.PopupWidget.prototype.bindKeyDownListener = function () { + this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true ); +}; + +/** + * Unbind key down listener. + * + * @private + */ +OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () { + this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true ); +}; + +/** + * Show, hide, or toggle the visibility of the anchor. + * + * @param {boolean} [show] Show anchor, omit to toggle + */ +OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) { + show = show === undefined ? !this.anchored : !!show; + + if ( this.anchored !== show ) { + if ( show ) { + this.$element.addClass( 'oo-ui-popupWidget-anchored' ); + } else { + this.$element.removeClass( 'oo-ui-popupWidget-anchored' ); + } + this.anchored = show; + } +}; + +/** + * Check if the anchor is visible. + * + * @return {boolean} Anchor is visible + */ +OO.ui.PopupWidget.prototype.hasAnchor = function () { + return this.anchor; +}; + +/** + * @inheritdoc + */ +OO.ui.PopupWidget.prototype.toggle = function ( show ) { + show = show === undefined ? !this.isVisible() : !!show; + + var change = show !== this.isVisible(); + + // Parent method + OO.ui.PopupWidget.super.prototype.toggle.call( this, show ); + + if ( change ) { + if ( show ) { + if ( this.autoClose ) { + this.bindMouseDownListener(); + this.bindKeyDownListener(); + } + this.updateDimensions(); + this.toggleClipping( true ); + } else { + this.toggleClipping( false ); + if ( this.autoClose ) { + this.unbindMouseDownListener(); + this.unbindKeyDownListener(); + } + } + } + + return this; +}; + +/** + * Set the size of the popup. + * + * Changing the size may also change the popup's position depending on the alignment. + * + * @param {number} width Width in pixels + * @param {number} height Height in pixels + * @param {boolean} [transition=false] Use a smooth transition + * @chainable + */ +OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) { + this.width = width; + this.height = height !== undefined ? height : null; + if ( this.isVisible() ) { + this.updateDimensions( transition ); + } +}; + +/** + * Update the size and position. + * + * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will + * be called automatically. + * + * @param {boolean} [transition=false] Use a smooth transition + * @chainable + */ +OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) { + var popupOffset, originOffset, containerLeft, containerWidth, containerRight, + popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth, + align = this.align, + widget = this; + + if ( !this.$container ) { + // Lazy-initialize $container if not specified in constructor + this.$container = $( this.getClosestScrollableElementContainer() ); + } + + // Set height and width before measuring things, since it might cause our measurements + // to change (e.g. due to scrollbars appearing or disappearing) + this.$popup.css( { + width: this.width, + height: this.height !== null ? this.height : 'auto' + } ); + + // If we are in RTL, we need to flip the alignment, unless it is center + if ( align === 'forwards' || align === 'backwards' ) { + if ( this.$container.css( 'direction' ) === 'rtl' ) { + align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ]; + } else { + align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ]; + } + + } + + // Compute initial popupOffset based on alignment + popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ]; + + // Figure out if this will cause the popup to go beyond the edge of the container + originOffset = this.$element.offset().left; + containerLeft = this.$container.offset().left; + containerWidth = this.$container.innerWidth(); + containerRight = containerLeft + containerWidth; + popupLeft = popupOffset - this.containerPadding; + popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding; + overlapLeft = ( originOffset + popupLeft ) - containerLeft; + overlapRight = containerRight - ( originOffset + popupRight ); + + // Adjust offset to make the popup not go beyond the edge, if needed + if ( overlapRight < 0 ) { + popupOffset += overlapRight; + } else if ( overlapLeft < 0 ) { + popupOffset -= overlapLeft; + } + + // Adjust offset to avoid anchor being rendered too close to the edge + // $anchor.width() doesn't work with the pure CSS anchor (returns 0) + // TODO: Find a measurement that works for CSS anchors and image anchors + anchorWidth = this.$anchor[ 0 ].scrollWidth * 2; + if ( popupOffset + this.width < anchorWidth ) { + popupOffset = anchorWidth - this.width; + } else if ( -popupOffset < anchorWidth ) { + popupOffset = -anchorWidth; + } + + // Prevent transition from being interrupted + clearTimeout( this.transitionTimeout ); + if ( transition ) { + // Enable transition + this.$element.addClass( 'oo-ui-popupWidget-transitioning' ); + } + + // Position body relative to anchor + this.$popup.css( 'margin-left', popupOffset ); + + if ( transition ) { + // Prevent transitioning after transition is complete + this.transitionTimeout = setTimeout( function () { + widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); + }, 200 ); + } else { + // Prevent transitioning immediately + this.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); + } + + // Reevaluate clipping state since we've relocated and resized the popup + this.clip(); + + return this; +}; + +/** + * Set popup alignment + * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`, + * `backwards` or `forwards`. + */ +OO.ui.PopupWidget.prototype.setAlignment = function ( align ) { + // Validate alignment and transform deprecated values + if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) { + this.align = { left: 'force-right', right: 'force-left' }[ align ] || align; + } else { + this.align = 'center'; + } +}; + +/** + * Get popup alignment + * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`, + * `backwards` or `forwards`. + */ +OO.ui.PopupWidget.prototype.getAlignment = function () { + return this.align; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ProgressBarWidget.js b/vendor/oojs/oojs-ui/src/widgets/ProgressBarWidget.js new file mode 100644 index 00000000..a4676282 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ProgressBarWidget.js @@ -0,0 +1,96 @@ +/** + * Progress bars visually display the status of an operation, such as a download, + * and can be either determinate or indeterminate: + * + * - **determinate** process bars show the percent of an operation that is complete. + * + * - **indeterminate** process bars use a visual display of motion to indicate that an operation + * is taking place. Because the extent of an indeterminate operation is unknown, the bar does + * not use percentages. + * + * The value of the `progress` configuration determines whether the bar is determinate or indeterminate. + * + * @example + * // Examples of determinate and indeterminate progress bars. + * var progressBar1 = new OO.ui.ProgressBarWidget( { + * progress: 33 + * } ); + * var progressBar2 = new OO.ui.ProgressBarWidget(); + * + * // Create a FieldsetLayout to layout progress bars + * var fieldset = new OO.ui.FieldsetLayout; + * fieldset.addItems( [ + * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}), + * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'}) + * ] ); + * $( 'body' ).append( fieldset.$element ); + * + * @class + * @extends OO.ui.Widget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate). + * To create a determinate progress bar, specify a number that reflects the initial percent complete. + * By default, the progress bar is indeterminate. + */ +OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.ProgressBarWidget.super.call( this, config ); + + // Properties + this.$bar = $( '<div>' ); + this.progress = null; + + // Initialization + this.setProgress( config.progress !== undefined ? config.progress : false ); + this.$bar.addClass( 'oo-ui-progressBarWidget-bar' ); + this.$element + .attr( { + role: 'progressbar', + 'aria-valuemin': 0, + 'aria-valuemax': 100 + } ) + .addClass( 'oo-ui-progressBarWidget' ) + .append( this.$bar ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget ); + +/* Static Properties */ + +OO.ui.ProgressBarWidget.static.tagName = 'div'; + +/* Methods */ + +/** + * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`. + * + * @return {number|boolean} Progress percent + */ +OO.ui.ProgressBarWidget.prototype.getProgress = function () { + return this.progress; +}; + +/** + * Set the percent of the process completed or `false` for an indeterminate process. + * + * @param {number|boolean} progress Progress percent or `false` for indeterminate + */ +OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) { + this.progress = progress; + + if ( progress !== false ) { + this.$bar.css( 'width', this.progress + '%' ); + this.$element.attr( 'aria-valuenow', this.progress ); + } else { + this.$bar.css( 'width', '' ); + this.$element.removeAttr( 'aria-valuenow' ); + } + this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress ); +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/RadioInputWidget.js b/vendor/oojs/oojs-ui/src/widgets/RadioInputWidget.js new file mode 100644 index 00000000..47ac20bf --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/RadioInputWidget.js @@ -0,0 +1,94 @@ +/** + * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set, + * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select} + * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information, + * please see the [OOjs UI documentation on MediaWiki][1]. + * + * This widget can be used inside a HTML form, such as a OO.ui.FormLayout. + * + * @example + * // An example of selected, unselected, and disabled radio inputs + * var radio1 = new OO.ui.RadioInputWidget( { + * value: 'a', + * selected: true + * } ); + * var radio2 = new OO.ui.RadioInputWidget( { + * value: 'b' + * } ); + * var radio3 = new OO.ui.RadioInputWidget( { + * value: 'c', + * disabled: true + * } ); + * // Create a fieldset layout with fields for each radio button. + * var fieldset = new OO.ui.FieldsetLayout( { + * label: 'Radio inputs' + * } ); + * fieldset.addItems( [ + * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ), + * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ), + * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ), + * ] ); + * $( 'body' ).append( fieldset.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs + * + * @class + * @extends OO.ui.InputWidget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected. + */ +OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.RadioInputWidget.super.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-radioInputWidget' ); + this.setSelected( config.selected !== undefined ? config.selected : false ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget ); + +/* Methods */ + +/** + * @inheritdoc + * @private + */ +OO.ui.RadioInputWidget.prototype.getInputElement = function () { + return $( '<input type="radio" />' ); +}; + +/** + * @inheritdoc + */ +OO.ui.RadioInputWidget.prototype.onEdit = function () { + // RadioInputWidget doesn't track its state. +}; + +/** + * Set selection state of this radio button. + * + * @param {boolean} state `true` for selected + * @chainable + */ +OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) { + // RadioInputWidget doesn't track its state. + this.$input.prop( 'checked', state ); + return this; +}; + +/** + * Check if this radio button is selected. + * + * @return {boolean} Radio is selected + */ +OO.ui.RadioInputWidget.prototype.isSelected = function () { + return this.$input.prop( 'checked' ); +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/RadioOptionWidget.js b/vendor/oojs/oojs-ui/src/widgets/RadioOptionWidget.js new file mode 100644 index 00000000..97187c0a --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/RadioOptionWidget.js @@ -0,0 +1,66 @@ +/** + * RadioOptionWidget is an option widget that looks like a radio button. + * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options. + * Please see the [OOjs UI documentation on MediaWiki] [1] for more information. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option + * + * @class + * @extends OO.ui.OptionWidget + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) { + // Configuration initialization + config = config || {}; + + // Properties (must be done before parent constructor which calls #setDisabled) + this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } ); + + // Parent constructor + OO.ui.RadioOptionWidget.super.call( this, config ); + + // Initialization + this.$element + .addClass( 'oo-ui-radioOptionWidget' ) + .prepend( this.radio.$element ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget ); + +/* Static Properties */ + +OO.ui.RadioOptionWidget.static.highlightable = false; + +OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true; + +OO.ui.RadioOptionWidget.static.pressable = false; + +OO.ui.RadioOptionWidget.static.tagName = 'label'; + +/* Methods */ + +/** + * @inheritdoc + */ +OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) { + OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state ); + + this.radio.setSelected( state ); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) { + OO.ui.RadioOptionWidget.super.prototype.setDisabled.call( this, disabled ); + + this.radio.setDisabled( this.isDisabled() ); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/RadioSelectWidget.js b/vendor/oojs/oojs-ui/src/widgets/RadioSelectWidget.js new file mode 100644 index 00000000..3859205f --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/RadioSelectWidget.js @@ -0,0 +1,58 @@ +/** + * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio + * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides + * an interface for adding, removing and selecting options. + * Please see the [OOjs UI documentation on MediaWiki][1] for more information. + * + * @example + * // A RadioSelectWidget with RadioOptions. + * var option1 = new OO.ui.RadioOptionWidget( { + * data: 'a', + * label: 'Selected radio option' + * } ); + * + * var option2 = new OO.ui.RadioOptionWidget( { + * data: 'b', + * label: 'Unselected radio option' + * } ); + * + * var radioSelect=new OO.ui.RadioSelectWidget( { + * items: [ option1, option2 ] + * } ); + * + * // Select 'option 1' using the RadioSelectWidget's selectItem() method. + * radioSelect.selectItem( option1 ); + * + * $( 'body' ).append( radioSelect.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options + + * + * @class + * @extends OO.ui.SelectWidget + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) { + // Parent constructor + OO.ui.RadioSelectWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.TabIndexedElement.call( this, config ); + + // Events + this.$element.on( { + focus: this.bindKeyDownListener.bind( this ), + blur: this.unbindKeyDownListener.bind( this ) + } ); + + // Initialization + this.$element.addClass( 'oo-ui-radioSelectWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget ); +OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.TabIndexedElement ); diff --git a/vendor/oojs/oojs-ui/src/widgets/SearchWidget.js b/vendor/oojs/oojs-ui/src/widgets/SearchWidget.js new file mode 100644 index 00000000..4909d9e1 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/SearchWidget.js @@ -0,0 +1,176 @@ +/** + * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query, + * and a {@link OO.ui.TextInputMenuSelectWidget menu} of search results, which is displayed beneath the query + * field. Unlike {@link OO.ui.LookupElement lookup menus}, search result menus are always visible to the user. + * Users can choose an item from the menu or type a query into the text field to search for a matching result item. + * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window. + * + * Each time the query is changed, the search result menu is cleared and repopulated. Please see + * the [OOjs UI demos][1] for an example. + * + * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr + * + * @class + * @extends OO.ui.Widget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string|jQuery} [placeholder] Placeholder text for query input + * @cfg {string} [value] Initial query value + */ +OO.ui.SearchWidget = function OoUiSearchWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.SearchWidget.super.call( this, config ); + + // Properties + this.query = new OO.ui.TextInputWidget( { + icon: 'search', + placeholder: config.placeholder, + value: config.value + } ); + this.results = new OO.ui.SelectWidget(); + this.$query = $( '<div>' ); + this.$results = $( '<div>' ); + + // Events + this.query.connect( this, { + change: 'onQueryChange', + enter: 'onQueryEnter' + } ); + this.results.connect( this, { + highlight: 'onResultsHighlight', + select: 'onResultsSelect' + } ); + this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) ); + + // Initialization + this.$query + .addClass( 'oo-ui-searchWidget-query' ) + .append( this.query.$element ); + this.$results + .addClass( 'oo-ui-searchWidget-results' ) + .append( this.results.$element ); + this.$element + .addClass( 'oo-ui-searchWidget' ) + .append( this.$results, this.$query ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget ); + +/* Events */ + +/** + * A 'highlight' event is emitted when an item is highlighted. The highlight indicates which + * item will be selected. When a user mouses over a menu item, it is highlighted. If a search + * string is typed into the query field instead, the first menu item that matches the query + * will be highlighted. + + * @event highlight + * @deprecated Connect straight to getResults() events instead + * @param {Object|null} item Item data or null if no item is highlighted + */ + +/** + * A 'select' event is emitted when an item is selected. A menu item is selected when it is clicked, + * or when a user types a search query, a menu result is highlighted, and the user presses enter. + * + * @event select + * @deprecated Connect straight to getResults() events instead + * @param {Object|null} item Item data or null if no item is selected + */ + +/* Methods */ + +/** + * Handle query key down events. + * + * @private + * @param {jQuery.Event} e Key down event + */ +OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) { + var highlightedItem, nextItem, + dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 ); + + if ( dir ) { + highlightedItem = this.results.getHighlightedItem(); + if ( !highlightedItem ) { + highlightedItem = this.results.getSelectedItem(); + } + nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir ); + this.results.highlightItem( nextItem ); + nextItem.scrollElementIntoView(); + } +}; + +/** + * Handle select widget select events. + * + * Clears existing results. Subclasses should repopulate items according to new query. + * + * @private + * @param {string} value New value + */ +OO.ui.SearchWidget.prototype.onQueryChange = function () { + // Reset + this.results.clearItems(); +}; + +/** + * Handle select widget enter key events. + * + * Selects highlighted item. + * + * @private + * @param {string} value New value + */ +OO.ui.SearchWidget.prototype.onQueryEnter = function () { + // Reset + this.results.selectItem( this.results.getHighlightedItem() ); +}; + +/** + * Handle select widget highlight events. + * + * @private + * @deprecated Connect straight to getResults() events instead + * @param {OO.ui.OptionWidget} item Highlighted item + * @fires highlight + */ +OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) { + this.emit( 'highlight', item ? item.getData() : null ); +}; + +/** + * Handle select widget select events. + * + * @private + * @deprecated Connect straight to getResults() events instead + * @param {OO.ui.OptionWidget} item Selected item + * @fires select + */ +OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) { + this.emit( 'select', item ? item.getData() : null ); +}; + +/** + * Get the query input. + * + * @return {OO.ui.TextInputWidget} Query input + */ +OO.ui.SearchWidget.prototype.getQuery = function () { + return this.query; +}; + +/** + * Get the search results menu. + * + * @return {OO.ui.SelectWidget} Menu of search results + */ +OO.ui.SearchWidget.prototype.getResults = function () { + return this.results; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/SelectWidget.js b/vendor/oojs/oojs-ui/src/widgets/SelectWidget.js new file mode 100644 index 00000000..91172af0 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/SelectWidget.js @@ -0,0 +1,630 @@ +/** + * 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}, + * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget + * menu selects}. + * + * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more + * information, please see the [OOjs UI documentation on MediaWiki][1]. + * + * @example + * // Example of a select widget with three options + * var select = new OO.ui.SelectWidget( { + * items: [ + * new OO.ui.OptionWidget( { + * data: 'a', + * label: 'Option One', + * } ), + * new OO.ui.OptionWidget( { + * data: 'b', + * label: 'Option Two', + * } ), + * new OO.ui.OptionWidget( { + * data: 'c', + * label: 'Option Three', + * } ) + * ] + * } ); + * $( 'body' ).append( select.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options + * + * @abstract + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.GroupElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select. + * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See + * the [OOjs UI documentation on MediaWiki] [2] for examples. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options + */ +OO.ui.SelectWidget = function OoUiSelectWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.SelectWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) ); + + // Properties + this.pressed = false; + this.selecting = null; + this.onMouseUpHandler = this.onMouseUp.bind( this ); + this.onMouseMoveHandler = this.onMouseMove.bind( this ); + this.onKeyDownHandler = this.onKeyDown.bind( this ); + + // Events + this.$element.on( { + mousedown: this.onMouseDown.bind( this ), + mouseover: this.onMouseOver.bind( this ), + mouseleave: this.onMouseLeave.bind( this ) + } ); + + // Initialization + this.$element + .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' ) + .attr( 'role', 'listbox' ); + if ( Array.isArray( config.items ) ) { + this.addItems( config.items ); + } +}; + +/* Setup */ + +OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget ); + +// Need to mixin base class as well +OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement ); +OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget ); + +/* Events */ + +/** + * @event highlight + * + * A `highlight` event is emitted when the highlight is changed with the #highlightItem method. + * + * @param {OO.ui.OptionWidget|null} item Highlighted item + */ + +/** + * @event press + * + * A `press` event is emitted when the #pressItem method is used to programmatically modify the + * pressed state of an option. + * + * @param {OO.ui.OptionWidget|null} item Pressed item + */ + +/** + * @event select + * + * A `select` event is emitted when the selection is modified programmatically with the #selectItem method. + * + * @param {OO.ui.OptionWidget|null} item Selected item + */ + +/** + * @event choose + * A `choose` event is emitted when an item is chosen with the #chooseItem method. + * @param {OO.ui.OptionWidget} item Chosen item + */ + +/** + * @event add + * + * An `add` event is emitted when options are added to the select with the #addItems method. + * + * @param {OO.ui.OptionWidget[]} items Added items + * @param {number} index Index of insertion point + */ + +/** + * @event remove + * + * A `remove` event is emitted when options are removed from the select with the #clearItems + * or #removeItems methods. + * + * @param {OO.ui.OptionWidget[]} items Removed items + */ + +/* Methods */ + +/** + * Handle mouse down events. + * + * @private + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) { + var item; + + if ( !this.isDisabled() && e.which === 1 ) { + this.togglePressed( true ); + item = this.getTargetItem( e ); + if ( item && item.isSelectable() ) { + this.pressItem( item ); + this.selecting = item; + this.getElementDocument().addEventListener( + 'mouseup', + this.onMouseUpHandler, + true + ); + this.getElementDocument().addEventListener( + 'mousemove', + this.onMouseMoveHandler, + true + ); + } + } + return false; +}; + +/** + * Handle mouse up events. + * + * @private + * @param {jQuery.Event} e Mouse up event + */ +OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) { + var item; + + this.togglePressed( false ); + if ( !this.selecting ) { + item = this.getTargetItem( e ); + if ( item && item.isSelectable() ) { + this.selecting = item; + } + } + if ( !this.isDisabled() && e.which === 1 && this.selecting ) { + this.pressItem( null ); + this.chooseItem( this.selecting ); + this.selecting = null; + } + + this.getElementDocument().removeEventListener( + 'mouseup', + this.onMouseUpHandler, + true + ); + this.getElementDocument().removeEventListener( + 'mousemove', + this.onMouseMoveHandler, + true + ); + + return false; +}; + +/** + * Handle mouse move events. + * + * @private + * @param {jQuery.Event} e Mouse move event + */ +OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) { + var item; + + if ( !this.isDisabled() && this.pressed ) { + item = this.getTargetItem( e ); + if ( item && item !== this.selecting && item.isSelectable() ) { + this.pressItem( item ); + this.selecting = item; + } + } + return false; +}; + +/** + * Handle mouse over events. + * + * @private + * @param {jQuery.Event} e Mouse over event + */ +OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) { + var item; + + if ( !this.isDisabled() ) { + item = this.getTargetItem( e ); + this.highlightItem( item && item.isHighlightable() ? item : null ); + } + return false; +}; + +/** + * Handle mouse leave events. + * + * @private + * @param {jQuery.Event} e Mouse over event + */ +OO.ui.SelectWidget.prototype.onMouseLeave = function () { + if ( !this.isDisabled() ) { + this.highlightItem( null ); + } + return false; +}; + +/** + * Handle key down events. + * + * @protected + * @param {jQuery.Event} e Key down event + */ +OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) { + var nextItem, + handled = false, + currentItem = this.getHighlightedItem() || this.getSelectedItem(); + + if ( !this.isDisabled() && this.isVisible() ) { + switch ( e.keyCode ) { + case OO.ui.Keys.ENTER: + if ( currentItem && currentItem.constructor.static.highlightable ) { + // Was only highlighted, now let's select it. No-op if already selected. + this.chooseItem( currentItem ); + handled = true; + } + break; + case OO.ui.Keys.UP: + case OO.ui.Keys.LEFT: + nextItem = this.getRelativeSelectableItem( currentItem, -1 ); + handled = true; + break; + case OO.ui.Keys.DOWN: + case OO.ui.Keys.RIGHT: + nextItem = this.getRelativeSelectableItem( currentItem, 1 ); + handled = true; + break; + case OO.ui.Keys.ESCAPE: + case OO.ui.Keys.TAB: + if ( currentItem && currentItem.constructor.static.highlightable ) { + currentItem.setHighlighted( false ); + } + this.unbindKeyDownListener(); + // Don't prevent tabbing away / defocusing + handled = false; + break; + } + + if ( nextItem ) { + if ( nextItem.constructor.static.highlightable ) { + this.highlightItem( nextItem ); + } else { + this.chooseItem( nextItem ); + } + nextItem.scrollElementIntoView(); + } + + if ( handled ) { + // Can't just return false, because e is not always a jQuery event + e.preventDefault(); + e.stopPropagation(); + } + } +}; + +/** + * Bind key down listener. + * + * @protected + */ +OO.ui.SelectWidget.prototype.bindKeyDownListener = function () { + this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true ); +}; + +/** + * Unbind key down listener. + * + * @protected + */ +OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () { + this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true ); +}; + +/** + * Get the closest item to a jQuery.Event. + * + * @private + * @param {jQuery.Event} e + * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found + */ +OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) { + return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null; +}; + +/** + * Get selected item. + * + * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected + */ +OO.ui.SelectWidget.prototype.getSelectedItem = function () { + var i, len; + + for ( i = 0, len = this.items.length; i < len; i++ ) { + if ( this.items[ i ].isSelected() ) { + return this.items[ i ]; + } + } + return null; +}; + +/** + * Get highlighted item. + * + * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted + */ +OO.ui.SelectWidget.prototype.getHighlightedItem = function () { + var i, len; + + for ( i = 0, len = this.items.length; i < len; i++ ) { + if ( this.items[ i ].isHighlighted() ) { + return this.items[ i ]; + } + } + return null; +}; + +/** + * Toggle pressed state. + * + * Press is a state that occurs when a user mouses down on an item, but + * has not yet let go of the mouse. The item may appear selected, but it will not be selected + * until the user releases the mouse. + * + * @param {boolean} pressed An option is being pressed + */ +OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) { + if ( pressed === undefined ) { + pressed = !this.pressed; + } + if ( pressed !== this.pressed ) { + this.$element + .toggleClass( 'oo-ui-selectWidget-pressed', pressed ) + .toggleClass( 'oo-ui-selectWidget-depressed', !pressed ); + this.pressed = pressed; + } +}; + +/** + * Highlight an option. If the `item` param is omitted, no options will be highlighted + * and any existing highlight will be removed. The highlight is mutually exclusive. + * + * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight + * @fires highlight + * @chainable + */ +OO.ui.SelectWidget.prototype.highlightItem = function ( item ) { + var i, len, highlighted, + changed = false; + + for ( i = 0, len = this.items.length; i < len; i++ ) { + highlighted = this.items[ i ] === item; + if ( this.items[ i ].isHighlighted() !== highlighted ) { + this.items[ i ].setHighlighted( highlighted ); + changed = true; + } + } + if ( changed ) { + this.emit( 'highlight', item ); + } + + return this; +}; + +/** + * Programmatically select an option by its data. If the `data` parameter is omitted, + * or if the item does not exist, all options will be deselected. + * + * @param {Object|string} [data] Value of the item to select, omit to deselect all + * @fires select + * @chainable + */ +OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) { + var itemFromData = this.getItemFromData( data ); + if ( data === undefined || !itemFromData ) { + return this.selectItem(); + } + return this.selectItem( itemFromData ); +}; + +/** + * Programmatically select an option by its reference. If the `item` parameter is omitted, + * all options will be deselected. + * + * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all + * @fires select + * @chainable + */ +OO.ui.SelectWidget.prototype.selectItem = function ( item ) { + var i, len, selected, + changed = false; + + for ( i = 0, len = this.items.length; i < len; i++ ) { + selected = this.items[ i ] === item; + if ( this.items[ i ].isSelected() !== selected ) { + this.items[ i ].setSelected( selected ); + changed = true; + } + } + if ( changed ) { + this.emit( 'select', item ); + } + + return this; +}; + +/** + * Press an item. + * + * Press is a state that occurs when a user mouses down on an item, but has not + * yet let go of the mouse. The item may appear selected, but it will not be selected until the user + * releases the mouse. + * + * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all + * @fires press + * @chainable + */ +OO.ui.SelectWidget.prototype.pressItem = function ( item ) { + var i, len, pressed, + changed = false; + + for ( i = 0, len = this.items.length; i < len; i++ ) { + pressed = this.items[ i ] === item; + if ( this.items[ i ].isPressed() !== pressed ) { + this.items[ i ].setPressed( pressed ); + changed = true; + } + } + if ( changed ) { + this.emit( 'press', item ); + } + + return this; +}; + +/** + * Choose an item. + * + * Note that ‘choose’ should never be modified programmatically. A user can choose + * an option with the keyboard or mouse and it becomes selected. To select an item programmatically, + * use the #selectItem method. + * + * This method is identical to #selectItem, but may vary in subclasses that take additional action + * when users choose an item with the keyboard or mouse. + * + * @param {OO.ui.OptionWidget} item Item to choose + * @fires choose + * @chainable + */ +OO.ui.SelectWidget.prototype.chooseItem = function ( item ) { + this.selectItem( item ); + this.emit( 'choose', item ); + + return this; +}; + +/** + * 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.OptionWidget|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.OptionWidget|null} Item at position, `null` if there are no items in the select + */ +OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) { + var currentIndex, nextIndex, i, + increase = direction > 0 ? 1 : -1, + len = this.items.length; + + if ( item instanceof OO.ui.OptionWidget ) { + currentIndex = $.inArray( item, this.items ); + 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 instanceof OO.ui.OptionWidget && item.isSelectable() ) { + return item; + } + nextIndex = ( nextIndex + increase + len ) % len; + } + return null; +}; + +/** + * Get the next selectable item or `null` if there are no selectable items. + * Disabled options and menu-section markers and breaks are not selectable. + * + * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items + */ +OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () { + var i, len, item; + + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[ i ]; + if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) { + return item; + } + } + + return null; +}; + +/** + * Add an array of options to the select. Optionally, an index number can be used to + * specify an insertion point. + * + * @param {OO.ui.OptionWidget[]} items Items to add + * @param {number} [index] Index to insert items after + * @fires add + * @chainable + */ +OO.ui.SelectWidget.prototype.addItems = function ( items, index ) { + // Mixin method + OO.ui.GroupWidget.prototype.addItems.call( this, items, index ); + + // Always provide an index, even if it was omitted + this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index ); + + return this; +}; + +/** + * Remove the specified array of options from the select. Options will be detached + * from the DOM, not removed, so they can be reused later. To remove all options from + * the select, you may wish to use the #clearItems method instead. + * + * @param {OO.ui.OptionWidget[]} items Items to remove + * @fires remove + * @chainable + */ +OO.ui.SelectWidget.prototype.removeItems = function ( items ) { + var i, len, item; + + // Deselect items being removed + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + if ( item.isSelected() ) { + this.selectItem( null ); + } + } + + // Mixin method + OO.ui.GroupWidget.prototype.removeItems.call( this, items ); + + this.emit( 'remove', items ); + + return this; +}; + +/** + * Clear all options from the select. Options will be detached from the DOM, not removed, + * so that they can be reused later. To remove a subset of options from the select, use + * the #removeItems method. + * + * @fires remove + * @chainable + */ +OO.ui.SelectWidget.prototype.clearItems = function () { + var items = this.items.slice(); + + // Mixin method + OO.ui.GroupWidget.prototype.clearItems.call( this ); + + // Clear selection + this.selectItem( null ); + + this.emit( 'remove', items ); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/TabOptionWidget.js b/vendor/oojs/oojs-ui/src/widgets/TabOptionWidget.js new file mode 100644 index 00000000..08f5b30d --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/TabOptionWidget.js @@ -0,0 +1,31 @@ +/** + * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}. + * + * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain + * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout} + * for an example. + * + * @class + * @extends OO.ui.OptionWidget + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.TabOptionWidget.super.call( this, config ); + + // Initialization + this.$element.addClass( 'oo-ui-tabOptionWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget ); + +/* Static Properties */ + +OO.ui.TabOptionWidget.static.highlightable = false; diff --git a/vendor/oojs/oojs-ui/src/widgets/TabSelectWidget.js b/vendor/oojs/oojs-ui/src/widgets/TabSelectWidget.js new file mode 100644 index 00000000..ced3218b --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/TabSelectWidget.js @@ -0,0 +1,33 @@ +/** + * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options} + * + * ####Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.#### + * + * @class + * @extends OO.ui.SelectWidget + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) { + // Parent constructor + OO.ui.TabSelectWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.TabIndexedElement.call( this, config ); + + // Events + this.$element.on( { + focus: this.bindKeyDownListener.bind( this ), + blur: this.unbindKeyDownListener.bind( this ) + } ); + + // Initialization + this.$element.addClass( 'oo-ui-tabSelectWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget ); +OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.TabIndexedElement ); diff --git a/vendor/oojs/oojs-ui/src/widgets/TextInputMenuSelectWidget.js b/vendor/oojs/oojs-ui/src/widgets/TextInputMenuSelectWidget.js new file mode 100644 index 00000000..6b971e81 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/TextInputMenuSelectWidget.js @@ -0,0 +1,103 @@ +/** + * TextInputMenuSelectWidget is a menu that is specially designed to be positioned beneath + * a {@link OO.ui.TextInputWidget text input} field. The menu's position is automatically + * calculated and maintained when the menu is toggled or the window is resized. + * See OO.ui.ComboBoxWidget for an example of a widget that uses this class. + * + * @class + * @extends OO.ui.MenuSelectWidget + * + * @constructor + * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$container=input.$element] Element to render menu under + */ +OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( inputWidget, config ) { + // Allow passing positional parameters inside the config object + if ( OO.isPlainObject( inputWidget ) && config === undefined ) { + config = inputWidget; + inputWidget = config.inputWidget; + } + + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.TextInputMenuSelectWidget.super.call( this, config ); + + // Properties + this.inputWidget = inputWidget; + this.$container = config.$container || this.inputWidget.$element; + this.onWindowResizeHandler = this.onWindowResize.bind( this ); + + // Initialization + this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget ); + +/* Methods */ + +/** + * Handle window resize event. + * + * @private + * @param {jQuery.Event} e Window resize event + */ +OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () { + this.position(); +}; + +/** + * @inheritdoc + */ +OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) { + visible = visible === undefined ? !this.isVisible() : !!visible; + + var change = visible !== this.isVisible(); + + if ( change && visible ) { + // Make sure the width is set before the parent method runs. + // After this we have to call this.position(); again to actually + // position ourselves correctly. + this.position(); + } + + // Parent method + OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible ); + + if ( change ) { + if ( this.isVisible() ) { + this.position(); + $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler ); + } else { + $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler ); + } + } + + return this; +}; + +/** + * Position the menu. + * + * @private + * @chainable + */ +OO.ui.TextInputMenuSelectWidget.prototype.position = function () { + var $container = this.$container, + pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() ); + + // Position under input + pos.top += $container.height(); + this.$element.css( pos ); + + // Set width + this.setIdealSize( $container.width() ); + // We updated the position, so re-evaluate the clipping state + this.clip(); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/TextInputWidget.js b/vendor/oojs/oojs-ui/src/widgets/TextInputWidget.js new file mode 100644 index 00000000..1ba26641 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/TextInputWidget.js @@ -0,0 +1,535 @@ +/** + * TextInputWidgets, like HTML text inputs, can be configured with options that customize the + * size of the field as well as its presentation. In addition, these widgets can be configured + * with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators}, an optional + * validation-pattern (used to determine if an input value is valid or not) and an input filter, + * which modifies incoming values rather than validating them. + * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples. + * + * This widget can be used inside a HTML form, such as a OO.ui.FormLayout. + * + * @example + * // Example of a text input widget + * var textInput = new OO.ui.TextInputWidget( { + * value: 'Text input' + * } ) + * $( 'body' ).append( textInput.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs + * + * @class + * @extends OO.ui.InputWidget + * @mixins OO.ui.IconElement + * @mixins OO.ui.IndicatorElement + * @mixins OO.ui.PendingElement + * @mixins OO.ui.LabelElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [type='text'] The value of the HTML `type` attribute + * @cfg {string} [placeholder] Placeholder text + * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to + * instruct the browser to focus this widget. + * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input. + * @cfg {number} [maxLength] Maximum number of characters allowed in the input. + * @cfg {boolean} [multiline=false] Allow multiple lines of text + * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content. + * Use the #maxRows config to specify a maximum number of displayed rows. + * @cfg {boolean} [maxRows=10] Maximum number of rows to display when #autosize is set to true. + * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of + * the value or placeholder text: `'before'` or `'after'` + * @cfg {boolean} [required=false] Mark the field as required + * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a + * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' + * (the value must contain only numbers); when RegExp, a regular expression that must match the + * value for it to be considered valid; when Function, a function receiving the value as parameter + * that must return true, or promise resolving to true, for it to be considered valid. + */ +OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) { + // Configuration initialization + config = $.extend( { + type: 'text', + labelPosition: 'after', + maxRows: 10 + }, config ); + + // Parent constructor + OO.ui.TextInputWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.IconElement.call( this, config ); + OO.ui.IndicatorElement.call( this, config ); + OO.ui.PendingElement.call( this, config ); + OO.ui.LabelElement.call( this, config ); + + // Properties + this.readOnly = false; + this.multiline = !!config.multiline; + this.autosize = !!config.autosize; + this.maxRows = config.maxRows; + this.validate = null; + + // Clone for resizing + if ( this.autosize ) { + this.$clone = this.$input + .clone() + .insertAfter( this.$input ) + .attr( 'aria-hidden', 'true' ) + .addClass( 'oo-ui-element-hidden' ); + } + + this.setValidation( config.validate ); + this.setLabelPosition( config.labelPosition ); + + // Events + this.$input.on( { + keypress: this.onKeyPress.bind( this ), + blur: this.onBlur.bind( this ) + } ); + this.$input.one( { + focus: this.onElementAttach.bind( this ) + } ); + this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) ); + this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) ); + this.on( 'labelChange', this.updatePosition.bind( this ) ); + this.connect( this, { change: 'onChange' } ); + + // Initialization + this.$element + .addClass( 'oo-ui-textInputWidget' ) + .append( this.$icon, this.$indicator ); + this.setReadOnly( !!config.readOnly ); + if ( config.placeholder ) { + this.$input.attr( 'placeholder', config.placeholder ); + } + if ( config.maxLength !== undefined ) { + this.$input.attr( 'maxlength', config.maxLength ); + } + if ( config.autofocus ) { + this.$input.attr( 'autofocus', 'autofocus' ); + } + if ( config.required ) { + this.$input.attr( 'required', 'required' ); + this.$input.attr( 'aria-required', 'true' ); + } + if ( this.label || config.autosize ) { + this.installParentChangeDetector(); + } +}; + +/* Setup */ + +OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget ); +OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement ); +OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement ); +OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement ); + +/* Static properties */ + +OO.ui.TextInputWidget.static.validationPatterns = { + 'non-empty': /.+/, + integer: /^\d+$/ +}; + +/* Events */ + +/** + * An `enter` event is emitted when the user presses 'enter' inside the text box. + * + * Not emitted if the input is multiline. + * + * @event enter + */ + +/* Methods */ + +/** + * Handle icon mouse down events. + * + * @private + * @param {jQuery.Event} e Mouse down event + * @fires icon + */ +OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) { + if ( e.which === 1 ) { + this.$input[ 0 ].focus(); + return false; + } +}; + +/** + * Handle indicator mouse down events. + * + * @private + * @param {jQuery.Event} e Mouse down event + * @fires indicator + */ +OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) { + if ( e.which === 1 ) { + this.$input[ 0 ].focus(); + return false; + } +}; + +/** + * Handle key press events. + * + * @private + * @param {jQuery.Event} e Key press event + * @fires enter If enter key is pressed and input is not multiline + */ +OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) { + if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) { + this.emit( 'enter', e ); + } +}; + +/** + * Handle blur events. + * + * @private + * @param {jQuery.Event} e Blur event + */ +OO.ui.TextInputWidget.prototype.onBlur = function () { + this.setValidityFlag(); +}; + +/** + * Handle element attach events. + * + * @private + * @param {jQuery.Event} e Element attach event + */ +OO.ui.TextInputWidget.prototype.onElementAttach = function () { + // Any previously calculated size is now probably invalid if we reattached elsewhere + this.valCache = null; + this.adjustSize(); + this.positionLabel(); +}; + +/** + * Handle change events. + * + * @param {string} value + * @private + */ +OO.ui.TextInputWidget.prototype.onChange = function () { + this.setValidityFlag(); + this.adjustSize(); +}; + +/** + * Check if the input is {@link #readOnly read-only}. + * + * @return {boolean} + */ +OO.ui.TextInputWidget.prototype.isReadOnly = function () { + return this.readOnly; +}; + +/** + * Set the {@link #readOnly read-only} state of the input. + * + * @param {boolean} state Make input read-only + * @chainable + */ +OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) { + this.readOnly = !!state; + this.$input.prop( 'readOnly', this.readOnly ); + return this; +}; + +/** + * Support function for making #onElementAttach work across browsers. + * + * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument + * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback. + * + * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the + * first time that the element gets attached to the documented. + */ +OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () { + var mutationObserver, onRemove, topmostNode, fakeParentNode, + MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, + widget = this; + + if ( MutationObserver ) { + // The new way. If only it wasn't so ugly. + + if ( this.$element.closest( 'html' ).length ) { + // Widget is attached already, do nothing. This breaks the functionality of this function when + // the widget is detached and reattached. Alas, doing this correctly with MutationObserver + // would require observation of the whole document, which would hurt performance of other, + // more important code. + return; + } + + // Find topmost node in the tree + topmostNode = this.$element[0]; + while ( topmostNode.parentNode ) { + topmostNode = topmostNode.parentNode; + } + + // We have no way to detect the $element being attached somewhere without observing the entire + // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the + // parent node of $element, and instead detect when $element is removed from it (and thus + // probably attached somewhere else). If there is no parent, we create a "fake" one. If it + // doesn't get attached, we end up back here and create the parent. + + mutationObserver = new MutationObserver( function ( mutations ) { + var i, j, removedNodes; + for ( i = 0; i < mutations.length; i++ ) { + removedNodes = mutations[ i ].removedNodes; + for ( j = 0; j < removedNodes.length; j++ ) { + if ( removedNodes[ j ] === topmostNode ) { + setTimeout( onRemove, 0 ); + return; + } + } + } + } ); + + onRemove = function () { + // If the node was attached somewhere else, report it + if ( widget.$element.closest( 'html' ).length ) { + widget.onElementAttach(); + } + mutationObserver.disconnect(); + widget.installParentChangeDetector(); + }; + + // Create a fake parent and observe it + fakeParentNode = $( '<div>' ).append( this.$element )[0]; + mutationObserver.observe( fakeParentNode, { childList: true } ); + } else { + // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for + // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated. + this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) ); + } +}; + +/** + * Automatically adjust the size of the text input. + * + * This only affects #multiline inputs that are {@link #autosize autosized}. + * + * @chainable + */ +OO.ui.TextInputWidget.prototype.adjustSize = function () { + var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight; + + if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) { + this.$clone + .val( this.$input.val() ) + .attr( 'rows', '' ) + // Set inline height property to 0 to measure scroll height + .css( 'height', 0 ); + + this.$clone.removeClass( 'oo-ui-element-hidden' ); + + this.valCache = this.$input.val(); + + scrollHeight = this.$clone[ 0 ].scrollHeight; + + // Remove inline height property to measure natural heights + this.$clone.css( 'height', '' ); + innerHeight = this.$clone.innerHeight(); + outerHeight = this.$clone.outerHeight(); + + // Measure max rows height + this.$clone + .attr( 'rows', this.maxRows ) + .css( 'height', 'auto' ) + .val( '' ); + maxInnerHeight = this.$clone.innerHeight(); + + // Difference between reported innerHeight and scrollHeight with no scrollbars present + // Equals 1 on Blink-based browsers and 0 everywhere else + measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight; + idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError ); + + this.$clone.addClass( 'oo-ui-element-hidden' ); + + // Only apply inline height when expansion beyond natural height is needed + if ( idealHeight > innerHeight ) { + // Use the difference between the inner and outer height as a buffer + this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) ); + } else { + this.$input.css( 'height', '' ); + } + } + return this; +}; + +/** + * @inheritdoc + * @private + */ +OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) { + return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' ); +}; + +/** + * Check if the input supports multiple lines. + * + * @return {boolean} + */ +OO.ui.TextInputWidget.prototype.isMultiline = function () { + return !!this.multiline; +}; + +/** + * Check if the input automatically adjusts its size. + * + * @return {boolean} + */ +OO.ui.TextInputWidget.prototype.isAutosizing = function () { + return !!this.autosize; +}; + +/** + * Select the entire text of the input. + * + * @chainable + */ +OO.ui.TextInputWidget.prototype.select = function () { + this.$input.select(); + return this; +}; + +/** + * Set the validation pattern. + * + * The validation pattern is either a regular expression, a function, or the symbolic name of a + * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the + * value must contain only numbers). + * + * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name + * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class. + */ +OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) { + if ( validate instanceof RegExp || validate instanceof Function ) { + this.validate = validate; + } else { + this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/; + } +}; + +/** + * Sets the 'invalid' flag appropriately. + * + * @param {boolean} [isValid] Optionally override validation result + */ +OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) { + var widget = this, + setFlag = function ( valid ) { + if ( !valid ) { + widget.$input.attr( 'aria-invalid', 'true' ); + } else { + widget.$input.removeAttr( 'aria-invalid' ); + } + widget.setFlags( { invalid: !valid } ); + }; + + if ( isValid !== undefined ) { + setFlag( isValid ); + } else { + this.isValid().done( setFlag ); + } +}; + +/** + * Check if a value is valid. + * + * This method returns a promise that resolves with a boolean `true` if the current value is + * considered valid according to the supplied {@link #validate validation pattern}. + * + * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid. + */ +OO.ui.TextInputWidget.prototype.isValid = function () { + if ( this.validate instanceof Function ) { + var result = this.validate( this.getValue() ); + if ( $.isFunction( result.promise ) ) { + return result.promise(); + } else { + return $.Deferred().resolve( !!result ).promise(); + } + } else { + return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise(); + } +}; + +/** + * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`. + * + * @param {string} labelPosition Label position, 'before' or 'after' + * @chainable + */ +OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) { + this.labelPosition = labelPosition; + this.updatePosition(); + return this; +}; + +/** + * Deprecated alias of #setLabelPosition + * + * @deprecated Use setLabelPosition instead. + */ +OO.ui.TextInputWidget.prototype.setPosition = + OO.ui.TextInputWidget.prototype.setLabelPosition; + +/** + * Update the position of the inline label. + * + * This method is called by #setLabelPosition, and can also be called on its own if + * something causes the label to be mispositioned. + * + * + * @chainable + */ +OO.ui.TextInputWidget.prototype.updatePosition = function () { + var after = this.labelPosition === 'after'; + + this.$element + .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after ) + .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after ); + + if ( this.label ) { + this.positionLabel(); + } + + return this; +}; + +/** + * Position the label by setting the correct padding on the input. + * + * @private + * @chainable + */ +OO.ui.TextInputWidget.prototype.positionLabel = function () { + // Clear old values + this.$input + // Clear old values if present + .css( { + 'padding-right': '', + 'padding-left': '' + } ); + + if ( this.label ) { + this.$element.append( this.$label ); + } else { + this.$label.detach(); + return; + } + + var after = this.labelPosition === 'after', + rtl = this.$element.css( 'direction' ) === 'rtl', + property = after === rtl ? 'padding-left' : 'padding-right'; + + this.$input.css( property, this.$label.outerWidth( true ) ); + + return this; +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ToggleButtonWidget.js b/vendor/oojs/oojs-ui/src/widgets/ToggleButtonWidget.js new file mode 100644 index 00000000..bb36f083 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ToggleButtonWidget.js @@ -0,0 +1,114 @@ +/** + * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a + * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be + * configured with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators}, + * {@link OO.ui.TitledElement titles}, {@link OO.ui.FlaggedElement styling flags}, + * and {@link OO.ui.LabelElement labels}. Please see + * the [OOjs UI documentation][1] on MediaWiki for more information. + * + * @example + * // Toggle buttons in the 'off' and 'on' state. + * var toggleButton1 = new OO.ui.ToggleButtonWidget( { + * label: 'Toggle Button off' + * } ); + * var toggleButton2 = new OO.ui.ToggleButtonWidget( { + * label: 'Toggle Button on', + * value: true + * } ); + * // Append the buttons to the DOM. + * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons + * + * @class + * @extends OO.ui.ToggleWidget + * @mixins OO.ui.ButtonElement + * @mixins OO.ui.IconElement + * @mixins OO.ui.IndicatorElement + * @mixins OO.ui.LabelElement + * @mixins OO.ui.TitledElement + * @mixins OO.ui.FlaggedElement + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [value=false] The toggle button’s initial on/off + * state. By default, the button is in the 'off' state. + */ +OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.ToggleButtonWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.ButtonElement.call( this, config ); + OO.ui.IconElement.call( this, config ); + OO.ui.IndicatorElement.call( this, config ); + OO.ui.LabelElement.call( this, config ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) ); + OO.ui.FlaggedElement.call( this, config ); + OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) ); + + // Events + this.connect( this, { click: 'onAction' } ); + + // Initialization + this.$button.append( this.$icon, this.$label, this.$indicator ); + this.$element + .addClass( 'oo-ui-toggleButtonWidget' ) + .append( this.$button ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget ); +OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonElement ); +OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.IndicatorElement ); +OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.LabelElement ); +OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.TitledElement ); +OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.FlaggedElement ); +OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.TabIndexedElement ); + +/* Methods */ + +/** + * Handle the button action being triggered. + * + * @private + */ +OO.ui.ToggleButtonWidget.prototype.onAction = function () { + this.setValue( !this.value ); +}; + +/** + * @inheritdoc + */ +OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) { + value = !!value; + if ( value !== this.value ) { + // Might be called from parent constructor before ButtonElement constructor + if ( this.$button ) { + this.$button.attr( 'aria-pressed', value.toString() ); + } + this.setActive( value ); + } + + // Parent method + OO.ui.ToggleButtonWidget.super.prototype.setValue.call( this, value ); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) { + if ( this.$button ) { + this.$button.removeAttr( 'aria-pressed' ); + } + OO.ui.ButtonElement.prototype.setButtonElement.call( this, $button ); + this.$button.attr( 'aria-pressed', this.value.toString() ); +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ToggleSwitchWidget.js b/vendor/oojs/oojs-ui/src/widgets/ToggleSwitchWidget.js new file mode 100644 index 00000000..94f0f996 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ToggleSwitchWidget.js @@ -0,0 +1,92 @@ +/** + * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean + * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented + * visually by a slider in the leftmost position. + * + * @example + * // Toggle switches in the 'off' and 'on' position. + * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget(); + * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( { + * value: true + * } ); + * + * // Create a FieldsetLayout to layout and label switches + * var fieldset = new OO.ui.FieldsetLayout( { + * label: 'Toggle switches' + * } ); + * fieldset.addItems( [ + * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ), + * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } ) + * ] ); + * $( 'body' ).append( fieldset.$element ); + * + * @class + * @extends OO.ui.ToggleWidget + * @mixins OO.ui.TabIndexedElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [value=false] The toggle switch’s initial on/off state. + * By default, the toggle switch is in the 'off' position. + */ +OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) { + // Parent constructor + OO.ui.ToggleSwitchWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.TabIndexedElement.call( this, config ); + + // Properties + this.dragging = false; + this.dragStart = null; + this.sliding = false; + this.$glow = $( '<span>' ); + this.$grip = $( '<span>' ); + + // Events + this.$element.on( { + click: this.onClick.bind( this ), + keypress: this.onKeyPress.bind( this ) + } ); + + // Initialization + this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' ); + this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' ); + this.$element + .addClass( 'oo-ui-toggleSwitchWidget' ) + .attr( 'role', 'checkbox' ) + .append( this.$glow, this.$grip ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget ); +OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement ); + +/* Methods */ + +/** + * Handle mouse click events. + * + * @private + * @param {jQuery.Event} e Mouse click event + */ +OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) { + if ( !this.isDisabled() && e.which === 1 ) { + this.setValue( !this.value ); + } + return false; +}; + +/** + * Handle key press events. + * + * @private + * @param {jQuery.Event} e Key press event + */ +OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) { + if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) { + this.setValue( !this.value ); + return false; + } +}; diff --git a/vendor/oojs/oojs-ui/src/widgets/ToggleWidget.js b/vendor/oojs/oojs-ui/src/widgets/ToggleWidget.js new file mode 100644 index 00000000..16d6ba50 --- /dev/null +++ b/vendor/oojs/oojs-ui/src/widgets/ToggleWidget.js @@ -0,0 +1,71 @@ +/** + * ToggleWidget implements basic behavior of widgets with an on/off state. + * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples. + * + * @abstract + * @class + * @extends OO.ui.Widget + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {boolean} [value=false] The toggle’s initial on/off state. + * By default, the toggle is in the 'off' state. + */ +OO.ui.ToggleWidget = function OoUiToggleWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.ToggleWidget.super.call( this, config ); + + // Properties + this.value = null; + + // Initialization + this.$element.addClass( 'oo-ui-toggleWidget' ); + this.setValue( !!config.value ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget ); + +/* Events */ + +/** + * @event change + * + * A change event is emitted when the on/off state of the toggle changes. + * + * @param {boolean} value Value representing the new state of the toggle + */ + +/* Methods */ + +/** + * Get the value representing the toggle’s state. + * + * @return {boolean} The on/off state of the toggle + */ +OO.ui.ToggleWidget.prototype.getValue = function () { + return this.value; +}; + +/** + * Set the state of the toggle: `true` for 'on', `false' for 'off'. + * + * @param {boolean} value The state of the toggle + * @fires change + * @chainable + */ +OO.ui.ToggleWidget.prototype.setValue = function ( value ) { + value = !!value; + if ( this.value !== value ) { + this.value = value; + this.emit( 'change', value ); + this.$element.toggleClass( 'oo-ui-toggleWidget-on', value ); + this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value ); + this.$element.attr( 'aria-checked', value.toString() ); + } + return this; +}; |