X-Git-Url: https://git.cyclocoop.org/?a=blobdiff_plain;f=resources%2Flib%2Foojs-ui%2Foojs-ui.js;h=0ad88fe19f95aa2c5699f8ec820d41ac981499d3;hb=d50f6c0e22b980703dc96a4c2937419ec9b02436;hp=5f758950fa7cfde8f69f253bf88199f47e1fde99;hpb=0d8e9eacae1f47c60518f871dbb73f4539413d39;p=lhc%2Fweb%2Fwiklou.git diff --git a/resources/lib/oojs-ui/oojs-ui.js b/resources/lib/oojs-ui/oojs-ui.js index 5f758950fa..0ad88fe19f 100644 --- a/resources/lib/oojs-ui/oojs-ui.js +++ b/resources/lib/oojs-ui/oojs-ui.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.6.0 + * OOjs UI v0.8.0 * https://www.mediawiki.org/wiki/OOjs_UI * - * Copyright 2011â2014 OOjs Team and other contributors. + * Copyright 2011â2015 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2014-12-16T21:00:55Z + * Date: 2015-02-19T01:33:11Z */ ( function ( OO ) { @@ -70,24 +70,24 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { var i, len, langs; // Requested language - if ( obj[lang] ) { - return obj[lang]; + if ( obj[ lang ] ) { + return obj[ lang ]; } // Known user language langs = OO.ui.getUserLanguages(); for ( i = 0, len = langs.length; i < len; i++ ) { - lang = langs[i]; - if ( obj[lang] ) { - return obj[lang]; + lang = langs[ i ]; + if ( obj[ lang ] ) { + return obj[ lang ]; } } // Fallback language - if ( obj[fallback] ) { - return obj[fallback]; + if ( obj[ fallback ] ) { + return obj[ fallback ]; } // First existing language for ( lang in obj ) { - return obj[lang]; + return obj[ lang ]; } return undefined; @@ -110,7 +110,7 @@ OO.ui.contains = function ( containers, contained, matchContainers ) { containers = [ containers ]; } for ( i = containers.length - 1; i >= 0; i-- ) { - if ( ( matchContainers && contained === containers[i] ) || $.contains( containers[i], contained ) ) { + if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) { return true; } } @@ -171,12 +171,13 @@ OO.ui.contains = function ( containers, contained, matchContainers ) { * @return {string} Translated message with parameters substituted */ OO.ui.msg = function ( key ) { - var message = messages[key], params = Array.prototype.slice.call( arguments, 1 ); + var message = messages[ key ], + params = Array.prototype.slice.call( arguments, 1 ); if ( typeof message === 'string' ) { // Perform $1 substitution message = message.replace( /\$(\d+)/g, function ( unused, n ) { var i = parseInt( n, 10 ); - return params[i - 1] !== undefined ? params[i - 1] : '$' + n; + return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n; } ); } else { // Return placeholder if message not found @@ -304,7 +305,68 @@ OO.ui.PendingElement.prototype.popPending = function () { }; /** - * List of actions. + * ActionSets manage the behavior of the {@link OO.ui.ActionWidget Action widgets} that comprise them. + * Actions can be made available for specific contexts (modes) and circumstances + * (abilities). Please see the [OOjs UI documentation on MediaWiki][1] for more information. + * + * @example + * // Example: An action set used in a process dialog + * function ProcessDialog( config ) { + * ProcessDialog.super.call( this, config ); + * } + * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog ); + * ProcessDialog.static.title = 'An action set in a process dialog'; + * // An action set that uses modes ('edit' and 'help' mode, in this example). + * ProcessDialog.static.actions = [ + * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] }, + * { action: 'help', modes: 'edit', label: 'Help' }, + * { modes: 'edit', label: 'Cancel', flags: 'safe' }, + * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' } + * ]; + * + * ProcessDialog.prototype.initialize = function () { + * ProcessDialog.super.prototype.initialize.apply( this, arguments ); + * this.panel1 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } ); + * this.panel1.$element.append( '
This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.
' ); + * this.panel2 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } ); + * this.panel2.$element.append( 'This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode
' ); + * this.stackLayout= new OO.ui.StackLayout( { + * items: [ this.panel1, this.panel2 ] + * }); + * this.$body.append( this.stackLayout.$element ); + * }; + * ProcessDialog.prototype.getSetupProcess = function ( data ) { + * return ProcessDialog.super.prototype.getSetupProcess.call( this, data ) + * .next( function () { + * this.actions.setMode('edit'); + * }, this ); + * }; + * ProcessDialog.prototype.getActionProcess = function ( action ) { + * if ( action === 'help' ) { + * this.actions.setMode( 'help' ); + * this.stackLayout.setItem( this.panel2 ); + * } else if ( action === 'back' ) { + * this.actions.setMode( 'edit' ); + * this.stackLayout.setItem( this.panel1 ); + * } else if ( action === 'continue' ) { + * var dialog = this; + * return new OO.ui.Process( function () { + * dialog.close(); + * } ); + * } + * return ProcessDialog.super.prototype.getActionProcess.call( this, action ); + * }; + * ProcessDialog.prototype.getBodyHeight = function () { + * return this.panel1.$element.outerHeight( true ); + * }; + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * var processDialog = new ProcessDialog({ + * size: 'medium'}); + * windowManager.addWindows( [ processDialog ] ); + * windowManager.openWindow( processDialog ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets * * @abstract * @class @@ -342,7 +404,11 @@ OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter ); /* Static Properties */ /** - * Symbolic name of dialog. + * Symbolic name of the flags used to identify special actions. Special actions are displayed in the + * header of a {@link OO.ui.ProcessDialog process dialog}. + * See the [OOjs UI documentation on MediaWiki][2] for more information and examples. + * + * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs * * @abstract * @static @@ -382,6 +448,7 @@ OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ]; /** * Handle action change events. * + * @private * @fires change */ OO.ui.ActionSet.prototype.onActionChange = function () { @@ -403,7 +470,7 @@ OO.ui.ActionSet.prototype.isSpecial = function ( action ) { var flag; for ( flag in this.special ) { - if ( action === this.special[flag] ) { + if ( action === this.special[ flag ] ) { return true; } } @@ -431,13 +498,13 @@ OO.ui.ActionSet.prototype.get = function ( filters ) { // Collect category candidates matches = []; for ( category in this.categorized ) { - list = filters[category]; + list = filters[ category ]; if ( list ) { if ( !Array.isArray( list ) ) { list = [ list ]; } for ( i = 0, len = list.length; i < len; i++ ) { - actions = this.categorized[category][list[i]]; + actions = this.categorized[ category ][ list[ i ] ]; if ( Array.isArray( actions ) ) { matches.push.apply( matches, actions ); } @@ -446,7 +513,7 @@ OO.ui.ActionSet.prototype.get = function ( filters ) { } // Remove by boolean filters for ( i = 0, len = matches.length; i < len; i++ ) { - match = matches[i]; + match = matches[ i ]; if ( ( filters.visible !== undefined && match.isVisible() !== filters.visible ) || ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled ) @@ -458,7 +525,7 @@ OO.ui.ActionSet.prototype.get = function ( filters ) { } // Remove duplicates for ( i = 0, len = matches.length; i < len; i++ ) { - match = matches[i]; + match = matches[ i ]; index = matches.lastIndexOf( match ); while ( index !== i ) { matches.splice( index, 1 ); @@ -512,7 +579,7 @@ OO.ui.ActionSet.prototype.setMode = function ( mode ) { this.changing = true; for ( i = 0, len = this.list.length; i < len; i++ ) { - action = this.list[i]; + action = this.list[ i ]; action.toggle( action.hasMode( mode ) ); } @@ -536,10 +603,10 @@ OO.ui.ActionSet.prototype.setAbilities = function ( actions ) { var i, len, action, item; for ( i = 0, len = this.list.length; i < len; i++ ) { - item = this.list[i]; + item = this.list[ i ]; action = item.getAction(); - if ( actions[action] !== undefined ) { - item.setDisabled( !actions[action] ); + if ( actions[ action ] !== undefined ) { + item.setDisabled( !actions[ action ] ); } } @@ -582,7 +649,7 @@ OO.ui.ActionSet.prototype.add = function ( actions ) { this.changing = true; for ( i = 0, len = actions.length; i < len; i++ ) { - action = actions[i]; + action = actions[ i ]; action.connect( this, { click: [ 'emit', 'click', action ], resize: [ 'emit', 'resize', action ], @@ -611,7 +678,7 @@ OO.ui.ActionSet.prototype.remove = function ( actions ) { this.changing = true; for ( i = 0, len = actions.length; i < len; i++ ) { - action = actions[i]; + action = actions[ i ]; index = this.list.indexOf( action ); if ( index !== -1 ) { action.disconnect( this ); @@ -639,7 +706,7 @@ OO.ui.ActionSet.prototype.clear = function () { this.changing = true; for ( i = 0, len = this.list.length; i < len; i++ ) { - action = this.list[i]; + action = this.list[ i ]; action.disconnect( this ); } @@ -671,31 +738,31 @@ OO.ui.ActionSet.prototype.organize = function () { this.special = {}; this.others = []; for ( i = 0, iLen = this.list.length; i < iLen; i++ ) { - action = this.list[i]; + action = this.list[ i ]; if ( action.isVisible() ) { // Populate categories for ( category in this.categories ) { - if ( !this.categorized[category] ) { - this.categorized[category] = {}; + if ( !this.categorized[ category ] ) { + this.categorized[ category ] = {}; } - list = action[this.categories[category]](); + list = action[ this.categories[ category ] ](); if ( !Array.isArray( list ) ) { list = [ list ]; } for ( j = 0, jLen = list.length; j < jLen; j++ ) { - item = list[j]; - if ( !this.categorized[category][item] ) { - this.categorized[category][item] = []; + item = list[ j ]; + if ( !this.categorized[ category ][ item ] ) { + this.categorized[ category ][ item ] = []; } - this.categorized[category][item].push( action ); + this.categorized[ category ][ item ].push( action ); } } // Populate special/others special = false; for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) { - flag = specialFlags[j]; - if ( !this.special[flag] && action.hasFlag( flag ) ) { - this.special[flag] = action; + flag = specialFlags[ j ]; + if ( !this.special[ flag ] && action.hasFlag( flag ) ) { + this.special[ flag ] = action; special = true; break; } @@ -712,15 +779,17 @@ OO.ui.ActionSet.prototype.organize = function () { }; /** - * DOM element abstraction. + * Each Element represents a rendering in the DOMâa button or an icon, for example, or anything + * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events + * connected to them and can't be interacted with. * * @abstract * @class * * @constructor * @param {Object} [config] Configuration options - * @cfg {Function} [$] jQuery for the frame the widget is in * @cfg {string[]} [classes] CSS class names to add + * @cfg {string} [id] HTML id attribute * @cfg {string} [text] Text to insert * @cfg {jQuery} [$content] Content elements to append (after text) * @cfg {Mixed} [data] Element data @@ -730,17 +799,20 @@ OO.ui.Element = function OoUiElement( config ) { config = config || {}; // Properties - this.$ = config.$ || OO.ui.Element.static.getJQuery( document ); + this.$ = $; this.data = config.data; - this.$element = this.$( this.$.context.createElement( this.getTagName() ) ); + this.$element = $( document.createElement( this.getTagName() ) ); this.elementGroup = null; this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this ); this.updateThemeClassesPending = false; // Initialization - if ( $.isArray( config.classes ) ) { + if ( Array.isArray( config.classes ) ) { this.$element.addClass( config.classes.join( ' ' ) ); } + if ( config.id ) { + this.$element.attr( 'id', config.id ); + } if ( config.text ) { this.$element.text( config.text ); } @@ -800,7 +872,7 @@ OO.ui.Element.static.getJQuery = function ( context, $iframe ) { */ OO.ui.Element.static.getDocument = function ( obj ) { // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable - return ( obj[0] && obj[0].ownerDocument ) || + return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) || // Empty jQuery selections might have a context obj.context || // HTMLElement @@ -835,7 +907,7 @@ OO.ui.Element.static.getDir = function ( obj ) { var isDoc, isWin; if ( obj instanceof jQuery ) { - obj = obj[0]; + obj = obj[ 0 ]; } isDoc = obj.nodeType === 9; isWin = obj.document !== undefined; @@ -875,8 +947,8 @@ OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) { // Get iframe element frames = from.parent.document.getElementsByTagName( 'iframe' ); for ( i = 0, len = frames.length; i < len; i++ ) { - if ( frames[i].contentWindow === from ) { - frame = frames[i]; + if ( frames[ i ].contentWindow === from ) { + frame = frames[ i ]; break; } } @@ -947,10 +1019,10 @@ OO.ui.Element.static.getBorders = function ( el ) { right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0; return { - top: Math.round( top ), - left: Math.round( left ), - bottom: Math.round( bottom ), - right: Math.round( right ) + top: top, + left: left, + bottom: bottom, + right: right }; }; @@ -1052,14 +1124,14 @@ OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) } while ( $parent.length ) { - if ( $parent[0] === this.getRootScrollableElement( el ) ) { - return $parent[0]; + if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) { + return $parent[ 0 ]; } i = props.length; while ( i-- ) { - val = $parent.css( props[i] ); + val = $parent.css( props[ i ] ); if ( val === 'auto' || val === 'scroll' ) { - return $parent[0]; + return $parent[ 0 ]; } } $parent = $parent.parent(); @@ -1138,6 +1210,33 @@ OO.ui.Element.static.scrollIntoView = function ( el, config ) { } }; +/** + * Force the browser to reconsider whether it really needs to render scrollbars inside the element + * and reserve space for them, because it probably doesn't. + * + * Workaround primarily forA simple dialog window. Press \'Esc\' to close.
' ); + * this.$body.append( this.content.$element ); + * }; + * MyDialog.prototype.getBodyHeight = function () { + * return this.content.$element.outerHeight( true ); + * }; + * var myDialog = new MyDialog( { + * size: 'medium' + * } ); + * // Create and append a window manager, which opens and closes the window. + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * windowManager.addWindows( [ myDialog ] ); + * // Open the window! + * windowManager.openWindow( myDialog ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs * * @abstract * @class @@ -2467,7 +2357,7 @@ OO.ui.Dialog.prototype.getSetupProcess = function ( data ) { ); for ( i = 0, len = actions.length; i < len; i++ ) { items.push( - new OO.ui.ActionWidget( $.extend( { $: this.$ }, actions[i] ) ) + new OO.ui.ActionWidget( actions[ i ] ) ); } this.actions.add( items ); @@ -2502,7 +2392,7 @@ OO.ui.Dialog.prototype.initialize = function () { OO.ui.Dialog.super.prototype.initialize.call( this ); // Properties - this.title = new OO.ui.LabelWidget( { $: this.$ } ); + this.title = new OO.ui.LabelWidget(); // Initialization this.$content.addClass( 'oo-ui-dialog-content' ); @@ -2527,7 +2417,7 @@ OO.ui.Dialog.prototype.detachActions = function () { // Detach all actions that may have been previously attached for ( i = 0, len = this.attachedActions.length; i < len; i++ ) { - this.attachedActions[i].$element.detach(); + this.attachedActions[ i ].$element.detach(); } this.attachedActions = []; }; @@ -2545,46 +2435,56 @@ OO.ui.Dialog.prototype.executeAction = function ( action ) { }; /** - * Collection of windows. + * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation. + * Managed windows are mutually exclusive. If a new window is opened while a current window is opening + * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows + * themselves are persistent andârather than being torn down when closedâcan be repopulated with the + * pertinent data and reused. + * + * Over the lifecycle of a window, the window manager makes available three promises: `opening`, + * `opened`, and `closing`, which represent the primary stages of the cycle: + * + * **Opening**: the opening stage begins when the window managerâs #openWindow or a windowâs + * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window. + * + * - an `opening` event is emitted with an `opening` promise + * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before + * the windowâs {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the + * window and its result executed + * - a `setup` progress notification is emitted from the `opening` promise + * - the #getReadyDelay method is called the returned value is used to time a pause in execution before + * the windowâs {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the + * window and its result executed + * - a `ready` progress notification is emitted from the `opening` promise + * - the `opening` promise is resolved with an `opened` promise + * + * **Opened**: the window is now open. + * + * **Closing**: the closing stage begins when the window manager's #closeWindow or the + * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins + * to close the window. + * + * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted + * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before + * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the + * window and its result executed + * - a `hold` progress notification is emitted from the `closing` promise + * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before + * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the + * window and its result executed + * - a `teardown` progress notification is emitted from the `closing` promise + * - the `closing` promise is resolved. The window is now closed + * + * See the [OOjs UI documentation on MediaWiki][1] for more information. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers * * @class * @extends OO.ui.Element * @mixins OO.EventEmitter * - * Managed windows are mutually exclusive. If a window is opened while there is a current window - * already opening or opened, the current window will be closed without data. Empty closing data - * should always result in the window being closed without causing constructive or destructive - * action. - * - * As a window is opened and closed, it passes through several stages and the manager emits several - * corresponding events. - * - * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening - * - {@link #event-opening} is emitted with `opening` promise - * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution - * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed - * - `setup` progress notification is emitted from opening promise - * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution - * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed - * - `ready` progress notification is emitted from opening promise - * - `opening` promise is resolved with `opened` promise - * - Window is now open - * - * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing - * - `opened` promise is resolved with `closing` promise - * - {@link #event-closing} is emitted with `closing` promise - * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution - * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed - * - `hold` progress notification is emitted from opening promise - * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution - * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed - * - `teardown` progress notification is emitted from opening promise - * - Closing promise is resolved - * - Window is now closed - * * @constructor * @param {Object} [config] Configuration options - * @cfg {boolean} [isolate] Configure managed windows to isolate their content using inline frames * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation * @cfg {boolean} [modal=true] Prevent interaction outside the dialog */ @@ -2601,22 +2501,17 @@ OO.ui.WindowManager = function OoUiWindowManager( config ) { // Properties this.factory = config.factory; this.modal = config.modal === undefined || !!config.modal; - this.isolate = !!config.isolate; this.windows = {}; this.opening = null; this.opened = null; this.closing = null; this.preparingToOpen = null; this.preparingToClose = null; - this.size = null; this.currentWindow = null; this.$ariaHidden = null; - this.requestedSize = null; this.onWindowResizeTimeout = null; this.onWindowResizeHandler = this.onWindowResize.bind( this ); this.afterWindowResizeHandler = this.afterWindowResize.bind( this ); - this.onWindowMouseWheelHandler = this.onWindowMouseWheel.bind( this ); - this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this ); // Initialization this.$element @@ -2684,6 +2579,9 @@ OO.ui.WindowManager.static.sizes = { large: { width: 700 }, + larger: { + width: 900 + }, full: { // These can be non-numeric because they are never used in calculations width: '100%', @@ -2725,36 +2623,6 @@ OO.ui.WindowManager.prototype.afterWindowResize = function () { } }; -/** - * Handle window mouse wheel events. - * - * @param {jQuery.Event} e Mouse wheel event - */ -OO.ui.WindowManager.prototype.onWindowMouseWheel = function () { - // Kill all events in the parent window if the child window is isolated - return !this.shouldIsolate(); -}; - -/** - * Handle document key down events. - * - * @param {jQuery.Event} e Key down event - */ -OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) { - switch ( e.which ) { - case OO.ui.Keys.PAGEUP: - case OO.ui.Keys.PAGEDOWN: - case OO.ui.Keys.END: - case OO.ui.Keys.HOME: - case OO.ui.Keys.LEFT: - case OO.ui.Keys.UP: - case OO.ui.Keys.RIGHT: - case OO.ui.Keys.DOWN: - // Kill all events in the parent window if the child window is isolated - return !this.shouldIsolate(); - } -}; - /** * Check if window is opening. * @@ -2782,17 +2650,6 @@ OO.ui.WindowManager.prototype.isOpened = function ( win ) { return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending'; }; -/** - * Check if window contents should be isolated. - * - * Window content isolation is done using inline frames. - * - * @return {boolean} Window contents should be isolated - */ -OO.ui.WindowManager.prototype.shouldIsolate = function () { - return this.isolate; -}; - /** * Check if a window is being managed. * @@ -2803,7 +2660,7 @@ OO.ui.WindowManager.prototype.hasWindow = function ( win ) { var name; for ( name in this.windows ) { - if ( this.windows[name] === win ) { + if ( this.windows[ name ] === win ) { return true; } } @@ -2867,7 +2724,7 @@ OO.ui.WindowManager.prototype.getTeardownDelay = function () { */ OO.ui.WindowManager.prototype.getWindow = function ( name ) { var deferred = $.Deferred(), - win = this.windows[name]; + win = this.windows[ name ]; if ( !( win instanceof OO.ui.Window ) ) { if ( this.factory ) { @@ -2876,7 +2733,7 @@ OO.ui.WindowManager.prototype.getWindow = function ( name ) { 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory' ) ); } else { - win = this.factory.create( name, this, { $: this.$ } ); + win = this.factory.create( name, this ); this.addWindows( [ win ] ); deferred.resolve( win ); } @@ -2912,7 +2769,6 @@ OO.ui.WindowManager.prototype.getCurrentWindow = function () { */ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) { var manager = this, - preparing = [], opening = $.Deferred(); // Argument handling @@ -2935,17 +2791,8 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) { // Window opening if ( opening.state() !== 'rejected' ) { - if ( !win.getManager() ) { - win.setManager( this ); - } - preparing.push( win.load() ); - - if ( this.closing ) { - // If a window is currently closing, wait for it to complete - preparing.push( this.closing ); - } - - this.preparingToOpen = $.when.apply( $, preparing ); + // If a window is currently closing, wait for it to complete + this.preparingToOpen = $.when( this.closing ); // Ensure handlers get called after preparingToOpen is set this.preparingToOpen.done( function () { if ( manager.modal ) { @@ -2988,13 +2835,12 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) { */ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { var manager = this, - preparing = [], closing = $.Deferred(), opened; // Argument handling if ( typeof win === 'string' ) { - win = this.windows[win]; + win = this.windows[ win ]; } else if ( !this.hasWindow( win ) ) { win = null; } @@ -3016,12 +2862,8 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { // Window closing if ( closing.state() !== 'rejected' ) { - if ( this.opening ) { - // If the window is currently opening, close it when it's done - preparing.push( this.opening ); - } - - this.preparingToClose = $.when.apply( $, preparing ); + // If the window is currently opening, close it when it's done + this.preparingToClose = $.when( this.opening ); // Ensure handlers get called after preparingToClose is set this.preparingToClose.done( function () { manager.closing = closing; @@ -3063,15 +2905,15 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { OO.ui.WindowManager.prototype.addWindows = function ( windows ) { var i, len, win, name, list; - if ( $.isArray( windows ) ) { + if ( Array.isArray( windows ) ) { // Convert to map of windows by looking up symbolic names from static configuration list = {}; for ( i = 0, len = windows.length; i < len; i++ ) { - name = windows[i].constructor.static.name; + name = windows[ i ].constructor.static.name; if ( typeof name !== 'string' ) { throw new Error( 'Cannot add window' ); } - list[name] = windows[i]; + list[ name ] = windows[ i ]; } } else if ( $.isPlainObject( windows ) ) { list = windows; @@ -3079,9 +2921,10 @@ OO.ui.WindowManager.prototype.addWindows = function ( windows ) { // Add windows for ( name in list ) { - win = list[name]; - this.windows[name] = win; + win = list[ name ]; + this.windows[ name ] = win.toggle( false ); this.$element.append( win.$element ); + win.setManager( this ); } }; @@ -3090,7 +2933,7 @@ OO.ui.WindowManager.prototype.addWindows = function ( windows ) { * * Windows will be closed before they are removed. * - * @param {string} name Symbolic name of window to remove + * @param {string[]} names Symbolic names of windows to remove * @return {jQuery.Promise} Promise resolved when window is closed and removed * @throws {Error} If windows being removed are not being managed */ @@ -3099,13 +2942,13 @@ OO.ui.WindowManager.prototype.removeWindows = function ( names ) { manager = this, promises = [], cleanup = function ( name, win ) { - delete manager.windows[name]; + delete manager.windows[ name ]; win.$element.detach(); }; for ( i = 0, len = names.length; i < len; i++ ) { - name = names[i]; - win = this.windows[name]; + name = names[ i ]; + win = this.windows[ name ]; if ( !win ) { throw new Error( 'Cannot remove window' ); } @@ -3144,16 +2987,16 @@ OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) { sizes = this.constructor.static.sizes, size = win.getSize(); - if ( !sizes[size] ) { + if ( !sizes[ size ] ) { size = this.constructor.static.defaultSize; } - if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) { + if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) { size = 'full'; } this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' ); this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' ); - win.setDimensions( sizes[size] ); + win.setDimensions( sizes[ size ] ); this.emit( 'resize', win ); @@ -3171,37 +3014,19 @@ OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) { if ( on ) { if ( !this.globalEvents ) { - this.$( this.getElementDocument() ).on( { - // Prevent scrolling by keys in top-level window - keydown: this.onDocumentKeyDownHandler - } ); - this.$( this.getElementWindow() ).on( { - // Prevent scrolling by wheel in top-level window - mousewheel: this.onWindowMouseWheelHandler, + $( this.getElementWindow() ).on( { // Start listening for top-level window dimension changes 'orientationchange resize': this.onWindowResizeHandler } ); - // Disable window scrolling in isolated windows - if ( !this.shouldIsolate() ) { - $( this.getElementDocument().body ).css( 'overflow', 'hidden' ); - } + $( this.getElementDocument().body ).css( 'overflow', 'hidden' ); this.globalEvents = true; } } else if ( this.globalEvents ) { - // Unbind global events - this.$( this.getElementDocument() ).off( { - // Allow scrolling by keys in top-level window - keydown: this.onDocumentKeyDownHandler - } ); - this.$( this.getElementWindow() ).off( { - // Allow scrolling by wheel in top-level window - mousewheel: this.onWindowMouseWheelHandler, + $( this.getElementWindow() ).off( { // Stop listening for top-level window dimension changes 'orientationchange resize': this.onWindowResizeHandler } ); - if ( !this.shouldIsolate() ) { - $( this.getElementDocument().body ).css( 'overflow', '' ); - } + $( this.getElementDocument().body ).css( 'overflow', '' ); this.globalEvents = false; } @@ -3236,17 +3061,15 @@ OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) { /** * Destroy window manager. - * - * Windows will not be closed, only removed from the DOM. */ OO.ui.WindowManager.prototype.destroy = function () { this.toggleGlobalEvents( false ); this.toggleAriaIsolation( false ); + this.clearWindows(); this.$element.remove(); }; /** - * @abstract * @class * * @constructor @@ -3381,7 +3204,7 @@ OO.ui.Process.prototype.execute = function () { // Use rejected promise for error return $.Deferred().reject( [ result ] ).promise(); } - if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) { + if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) { // Use rejected promise for list of errors return $.Deferred().reject( result ).promise(); } @@ -3397,9 +3220,9 @@ OO.ui.Process.prototype.execute = function () { if ( this.steps.length ) { // Generate a chain reaction of promises - promise = proceed( this.steps[0] )(); + promise = proceed( this.steps[ 0 ] )(); for ( i = 1, len = this.steps.length; i < len; i++ ) { - promise = promise.then( proceed( this.steps[i] ) ); + promise = promise.then( proceed( this.steps[ i ] ) ); } } else { promise = $.Deferred().resolve().promise(); @@ -3509,8 +3332,8 @@ OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, dem // Auto for ( i = 0, len = included.length; i < len; i++ ) { - if ( !used[included[i]] ) { - auto.push( included[i] ); + if ( !used[ included[ i ] ] ) { + auto.push( included[ i ] ); } } @@ -3538,22 +3361,22 @@ OO.ui.ToolFactory.prototype.extract = function ( collection, used ) { if ( collection === '*' ) { for ( name in this.registry ) { - tool = this.registry[name]; + tool = this.registry[ name ]; if ( // Only add tools by group name when auto-add is enabled tool.static.autoAddToCatchall && // Exclude already used tools - ( !used || !used[name] ) + ( !used || !used[ name ] ) ) { names.push( name ); if ( used ) { - used[name] = true; + used[ name ] = true; } } } - } else if ( $.isArray( collection ) ) { + } else if ( Array.isArray( collection ) ) { for ( i = 0, len = collection.length; i < len; i++ ) { - item = collection[i]; + item = collection[ i ]; // Allow plain strings as shorthand for named tools if ( typeof item === 'string' ) { item = { name: item }; @@ -3561,26 +3384,26 @@ OO.ui.ToolFactory.prototype.extract = function ( collection, used ) { if ( OO.isPlainObject( item ) ) { if ( item.group ) { for ( name in this.registry ) { - tool = this.registry[name]; + tool = this.registry[ name ]; if ( // Include tools with matching group tool.static.group === item.group && // Only add tools by group name when auto-add is enabled tool.static.autoAddToGroup && // Exclude already used tools - ( !used || !used[name] ) + ( !used || !used[ name ] ) ) { names.push( name ); if ( used ) { - used[name] = true; + used[ name ] = true; } } } // Include tools with matching name and exclude already used tools - } else if ( item.name && ( !used || !used[item.name] ) ) { + } else if ( item.name && ( !used || !used[ item.name ] ) ) { names.push( item.name ); if ( used ) { - used[item.name] = true; + used[ item.name ] = true; } } } @@ -3605,7 +3428,7 @@ OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() { // Register default toolgroups for ( i = 0, l = defaultClasses.length; i < l; i++ ) { - this.register( defaultClasses[i] ); + this.register( defaultClasses[ i ] ); } }; @@ -3678,95 +3501,219 @@ OO.ui.Theme.prototype.updateElementClasses = function ( element ) { }; /** - * Element with a button. - * - * Buttons are used for controls which can be clicked. They can be configured to use tab indexing - * and access keys for accessibility purposes. + * Element supporting "sequential focus navigation" using the 'tabindex' attribute. * * @abstract * @class * * @constructor * @param {Object} [config] Configuration options - * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `` - * @cfg {boolean} [framed=true] Render button with a frame - * @cfg {number} [tabIndex=0] Button's tab index. Use 0 to use default ordering, use -1 to prevent - * tab focusing. - * @cfg {string} [accessKey] Button's access key + * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element + * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to + * prevent tab focusing, use null to suppress the `tabindex` attribute. */ -OO.ui.ButtonElement = function OoUiButtonElement( config ) { +OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) { // Configuration initialization - config = config || {}; + config = $.extend( { tabIndex: 0 }, config ); // Properties - this.$button = null; - this.framed = null; + this.$tabIndexed = null; this.tabIndex = null; - this.accessKey = null; - this.active = false; - this.onMouseUpHandler = this.onMouseUp.bind( this ); - this.onMouseDownHandler = this.onMouseDown.bind( this ); + + // Events + this.connect( this, { disable: 'onDisable' } ); // Initialization - this.$element.addClass( 'oo-ui-buttonElement' ); - this.toggleFramed( config.framed === undefined || config.framed ); - this.setTabIndex( config.tabIndex || 0 ); - this.setAccessKey( config.accessKey ); - this.setButtonElement( config.$button || this.$( '' ) ); + this.setTabIndex( config.tabIndex ); + this.setTabIndexedElement( config.$tabIndexed || this.$element ); }; /* Setup */ -OO.initClass( OO.ui.ButtonElement ); +OO.initClass( OO.ui.TabIndexedElement ); -/* Static Properties */ +/* Methods */ /** - * Cancel mouse down events. + * Set the element with `tabindex` attribute. * - * @static - * @inheritable - * @property {boolean} + * If an element is already set, it will be cleaned up before setting up the new element. + * + * @param {jQuery} $tabIndexed Element to set tab index on + * @chainable */ -OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true; - -/* Methods */ +OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) { + var tabIndex = this.tabIndex; + // Remove attributes from old $tabIndexed + this.setTabIndex( null ); + // Force update of new $tabIndexed + this.$tabIndexed = $tabIndexed; + this.tabIndex = tabIndex; + return this.updateTabIndex(); +}; /** - * Set the button element. - * - * If an element is already set, it will be cleaned up before setting up the new element. + * Set tab index value. * - * @param {jQuery} $button Element to use as button + * @param {number|null} tabIndex Tab index value or null for no tab index + * @chainable */ -OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) { - if ( this.$button ) { - this.$button - .removeClass( 'oo-ui-buttonElement-button' ) - .removeAttr( 'role accesskey tabindex' ) - .off( 'mousedown', this.onMouseDownHandler ); +OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) { + tabIndex = typeof tabIndex === 'number' ? tabIndex : null; + + if ( this.tabIndex !== tabIndex ) { + this.tabIndex = tabIndex; + this.updateTabIndex(); } - this.$button = $button - .addClass( 'oo-ui-buttonElement-button' ) - .attr( { role: 'button', accesskey: this.accessKey, tabindex: this.tabIndex } ) - .on( 'mousedown', this.onMouseDownHandler ); + return this; }; /** - * Handles mouse down events. + * Update the `tabindex` attribute, in case of changes to tab index or + * disabled state. * - * @param {jQuery.Event} e Mouse down event + * @chainable */ -OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) { - if ( this.isDisabled() || e.which !== 1 ) { - return false; +OO.ui.TabIndexedElement.prototype.updateTabIndex = function () { + if ( this.$tabIndexed ) { + if ( this.tabIndex !== null ) { + // Do not index over disabled elements + this.$tabIndexed.attr( { + tabindex: this.isDisabled() ? -1 : this.tabIndex, + // ChromeVox and NVDA do not seem to inherit this from parent elements + 'aria-disabled': this.isDisabled().toString() + } ); + } else { + this.$tabIndexed.removeAttr( 'tabindex aria-disabled' ); + } } - // Remove the tab-index while the button is down to prevent the button from stealing focus - this.$button.removeAttr( 'tabindex' ); - this.$element.addClass( 'oo-ui-buttonElement-pressed' ); - // Run the mouseup handler no matter where the mouse is when the button is let go, so we can - // reliably reapply the tabindex and remove the pressed class + return this; +}; + +/** + * Handle disable events. + * + * @param {boolean} disabled Element is disabled + */ +OO.ui.TabIndexedElement.prototype.onDisable = function () { + this.updateTabIndex(); +}; + +/** + * Get tab index value. + * + * @return {number|null} Tab index value + */ +OO.ui.TabIndexedElement.prototype.getTabIndex = function () { + return this.tabIndex; +}; + +/** + * ButtonElement is often mixed into other classes to generate a button, which is a clickable + * interface element that can be configured with access keys for accessibility. + * See the [OOjs UI documentation on MediaWiki] [1] for examples. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons + * @abstract + * @class + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `` + * @cfg {boolean} [framed=true] Render button with a frame + * @cfg {string} [accessKey] Button's access key + */ +OO.ui.ButtonElement = function OoUiButtonElement( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.$button = config.$button || $( '' ); + this.framed = null; + this.accessKey = null; + this.active = false; + this.onMouseUpHandler = this.onMouseUp.bind( this ); + this.onMouseDownHandler = this.onMouseDown.bind( this ); + this.onKeyDownHandler = this.onKeyDown.bind( this ); + this.onKeyUpHandler = this.onKeyUp.bind( this ); + this.onClickHandler = this.onClick.bind( this ); + this.onKeyPressHandler = this.onKeyPress.bind( this ); + + // Initialization + this.$element.addClass( 'oo-ui-buttonElement' ); + this.toggleFramed( config.framed === undefined || config.framed ); + this.setAccessKey( config.accessKey ); + this.setButtonElement( this.$button ); +}; + +/* Setup */ + +OO.initClass( OO.ui.ButtonElement ); + +/* Static Properties */ + +/** + * Cancel mouse down events. + * + * @static + * @inheritable + * @property {boolean} + */ +OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true; + +/* Events */ + +/** + * @event click + */ + +/* Methods */ + +/** + * Set the button element. + * + * If an element is already set, it will be cleaned up before setting up the new element. + * + * @param {jQuery} $button Element to use as button + */ +OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) { + if ( this.$button ) { + this.$button + .removeClass( 'oo-ui-buttonElement-button' ) + .removeAttr( 'role accesskey' ) + .off( { + mousedown: this.onMouseDownHandler, + keydown: this.onKeyDownHandler, + click: this.onClickHandler, + keypress: this.onKeyPressHandler + } ); + } + + this.$button = $button + .addClass( 'oo-ui-buttonElement-button' ) + .attr( { role: 'button', accesskey: this.accessKey } ) + .on( { + mousedown: this.onMouseDownHandler, + keydown: this.onKeyDownHandler, + click: this.onClickHandler, + keypress: this.onKeyPressHandler + } ); +}; + +/** + * Handles mouse down events. + * + * @protected + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) { + if ( this.isDisabled() || e.which !== 1 ) { + return; + } + this.$element.addClass( 'oo-ui-buttonElement-pressed' ); + // Run the mouseup handler no matter where the mouse is when the button is let go, so we can + // reliably remove the pressed class this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true ); // Prevent change of focus unless specifically configured otherwise if ( this.constructor.static.cancelButtonMouseDownEvents ) { @@ -3777,19 +3724,77 @@ OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) { /** * Handles mouse up events. * + * @protected * @param {jQuery.Event} e Mouse up event */ OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) { if ( this.isDisabled() || e.which !== 1 ) { - return false; + return; } - // Restore the tab-index after the button is up to restore the button's accessibility - this.$button.attr( 'tabindex', this.tabIndex ); this.$element.removeClass( 'oo-ui-buttonElement-pressed' ); // Stop listening for mouseup, since we only needed this once this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true ); }; +/** + * Handles mouse click events. + * + * @protected + * @param {jQuery.Event} e Mouse click event + * @fires click + */ +OO.ui.ButtonElement.prototype.onClick = function ( e ) { + if ( !this.isDisabled() && e.which === 1 ) { + this.emit( 'click' ); + } + return false; +}; + +/** + * Handles key down events. + * + * @protected + * @param {jQuery.Event} e Key down event + */ +OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) { + if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) { + return; + } + this.$element.addClass( 'oo-ui-buttonElement-pressed' ); + // Run the keyup handler no matter where the key is when the button is let go, so we can + // reliably remove the pressed class + this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true ); +}; + +/** + * Handles key up events. + * + * @protected + * @param {jQuery.Event} e Key up event + */ +OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) { + if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) { + return; + } + this.$element.removeClass( 'oo-ui-buttonElement-pressed' ); + // Stop listening for keyup, since we only needed this once + this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true ); +}; + +/** + * Handles key press events. + * + * @protected + * @param {jQuery.Event} e Key press event + * @fires click + */ +OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) { + if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) { + this.emit( 'click' ); + } + return false; +}; + /** * Check if button has a frame. * @@ -3818,29 +3823,6 @@ OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) { return this; }; -/** - * Set tab index. - * - * @param {number|null} tabIndex Button's tab index, use null to remove - * @chainable - */ -OO.ui.ButtonElement.prototype.setTabIndex = function ( tabIndex ) { - tabIndex = typeof tabIndex === 'number' && tabIndex >= 0 ? tabIndex : null; - - if ( this.tabIndex !== tabIndex ) { - if ( this.$button ) { - if ( tabIndex !== null ) { - this.$button.attr( 'tabindex', tabIndex ); - } else { - this.$button.removeAttr( 'tabindex' ); - } - } - this.tabIndex = tabIndex; - } - - return this; -}; - /** * Set access key. * @@ -3876,7 +3858,12 @@ OO.ui.ButtonElement.prototype.setActive = function ( value ) { }; /** - * Element containing a sequence of child elements. + * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or + * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing + * items from the group is done through the interface the class provides. + * For more information, please see the [OOjs UI documentation on MediaWiki] [1]. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups * * @abstract * @class @@ -3895,7 +3882,7 @@ OO.ui.GroupElement = function OoUiGroupElement( config ) { this.aggregateItemEvents = {}; // Initialization - this.setGroupElement( config.$group || this.$( 'List of categories...
' ), + * 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 @@ -9005,7 +9636,7 @@ OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) { // Initialization this.$element.addClass( 'oo-ui-buttonGroupWidget' ); - if ( $.isArray( config.items ) ) { + if ( Array.isArray( config.items ) ) { this.addItems( config.items ); } }; @@ -9016,7 +9647,23 @@ OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget ); OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement ); /** - * Generic widget for buttons. + * 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 @@ -9026,15 +9673,18 @@ OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement ); * @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 clicked - * @cfg {string} [target] Target to open hyperlink in + * @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 || {}; + // FIXME: The `nofollow` alias is deprecated and will be removed (T89767) + config = $.extend( { noFollow: config && config.nofollow }, config ); // Parent constructor OO.ui.ButtonWidget.super.call( this, config ); @@ -9046,18 +9696,14 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( 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; this.isHyperlink = false; - // Events - this.$button.on( { - click: this.onClick.bind( this ), - keypress: this.onKeyPress.bind( this ) - } ); - // Initialization this.$button.append( this.$icon, this.$label, this.$indicator ); this.$element @@ -9065,6 +9711,7 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) { .append( this.$button ); this.setHref( config.href ); this.setTarget( config.target ); + this.setNoFollow( config.noFollow ); }; /* Setup */ @@ -9076,45 +9723,54 @@ 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 ); -/* Events */ +/* Methods */ /** - * @event click + * @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' ); + } -/* Methods */ + return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e ); +}; /** - * Handles mouse click events. - * - * @param {jQuery.Event} e Mouse click event - * @fires click + * @inheritdoc */ -OO.ui.ButtonWidget.prototype.onClick = function () { +OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) { if ( !this.isDisabled() ) { - this.emit( 'click' ); - if ( this.isHyperlink ) { - return true; - } + // Restore the tab-index after the button is up to restore the button's accessibility + this.$button.attr( 'tabindex', this.tabIndex ); } - return false; + + return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e ); }; /** - * Handles keypress events. - * - * @param {jQuery.Event} e Keypress event - * @fires click + * @inheritdoc + */ +OO.ui.ButtonWidget.prototype.onClick = function ( e ) { + var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e ); + if ( this.isHyperlink ) { + return true; + } + return ret; +}; + +/** + * @inheritdoc */ OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) { - if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) { - this.emit( 'click' ); - if ( this.isHyperlink ) { - return true; - } + var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e ); + if ( this.isHyperlink ) { + return true; } - return false; + return ret; }; /** @@ -9135,6 +9791,15 @@ 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. * @@ -9178,7 +9843,32 @@ OO.ui.ButtonWidget.prototype.setTarget = function ( target ) { }; /** - * Button widget that executes an action and is managed by an OO.ui.ActionSet. + * 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; +}; + +/** + * 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. 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 @@ -9348,9 +10038,13 @@ OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( 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 ); }; @@ -9362,22 +10056,10 @@ OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement ); /* Methods */ /** - * Handles mouse click events. - * - * @param {jQuery.Event} e Mouse click event + * Handle the button action being triggered. */ -OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) { - // Skip clicks within the popup - if ( $.contains( this.popup.$element[0], e.target ) ) { - return; - } - - if ( !this.isDisabled() ) { - this.popup.toggle(); - // Parent method - OO.ui.PopupButtonWidget.super.prototype.onClick.call( this ); - } - return false; +OO.ui.PopupButtonWidget.prototype.onAction = function () { + this.popup.toggle(); }; /** @@ -9401,6 +10083,9 @@ OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) { // Mixin constructors OO.ui.ToggleWidget.call( this, config ); + // Events + this.connect( this, { click: 'onAction' } ); + // Initialization this.$element.addClass( 'oo-ui-toggleButtonWidget' ); }; @@ -9413,15 +10098,10 @@ OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget ); /* Methods */ /** - * @inheritdoc + * Handle the button action being triggered. */ -OO.ui.ToggleButtonWidget.prototype.onClick = function () { - if ( !this.isDisabled() ) { - this.setValue( !this.value ); - } - - // Parent method - return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this ); +OO.ui.ToggleButtonWidget.prototype.onAction = function () { + this.setValue( !this.value ); }; /** @@ -9430,6 +10110,7 @@ OO.ui.ToggleButtonWidget.prototype.onClick = function () { OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) { value = !!value; if ( value !== this.value ) { + this.$button.attr( 'aria-pressed', value.toString() ); this.setActive( value ); } @@ -9440,12 +10121,37 @@ OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) { }; /** - * Dropdown menu of options. + * 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' + * } ) + * ] + * } + * } ); * - * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which - * can be accessed using the #getMenu method. + * $('body').append(dropDown.$element); * - * Use with OO.ui.MenuOptionWidget. + * 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 @@ -9453,6 +10159,7 @@ OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) { * @mixins OO.ui.IndicatorElement * @mixins OO.ui.LabelElement * @mixins OO.ui.TitledElement + * @mixins OO.ui.TabIndexedElement * * @constructor * @param {Object} [config] Configuration options @@ -9465,18 +10172,24 @@ OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) { // Parent constructor OO.ui.DropdownWidget.super.call( this, config ); + // Properties (must be set before TabIndexedElement constructor call) + this.$handle = this.$( '' ); + // 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( { $: this.$, widget: this }, config.menu ) ); - this.$handle = this.$( '' ); + this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) ); // Events - this.$element.on( { click: this.onClick.bind( this ) } ); + this.$handle.on( { + click: this.onClick.bind( this ), + keypress: this.onKeyPress.bind( this ) + } ); this.menu.connect( this, { select: 'onMenuSelect' } ); // Initialization @@ -9495,6 +10208,7 @@ 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 */ @@ -9510,6 +10224,7 @@ OO.ui.DropdownWidget.prototype.getMenu = function () { /** * Handles menu select events. * + * @private * @param {OO.ui.MenuOptionWidget} item Selected menu item */ OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) { @@ -9530,30 +10245,49 @@ OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) { }; /** - * Handles mouse click events. + * Handle mouse click events. * + * @private * @param {jQuery.Event} e Mouse click event */ OO.ui.DropdownWidget.prototype.onClick = function ( e ) { - // Skip clicks within the menu - if ( $.contains( this.menu.$element[0], e.target ) ) { - return; + if ( !this.isDisabled() && e.which === 1 ) { + this.menu.toggle(); } + return false; +}; - if ( !this.isDisabled() ) { - if ( this.menu.isVisible() ) { - this.menu.toggle( false ); - } else { - this.menu.toggle( true ); - } +/** + * 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; }; /** - * Icon widget. + * 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); * - * See OO.ui.IconElement for more information. + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons * * @class * @extends OO.ui.Widget @@ -9627,12 +10361,18 @@ OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement ); OO.ui.IndicatorWidget.static.tagName = 'span'; /** - * Base class for input widgets. + * 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 @@ -9647,14 +10387,15 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) { // Parent constructor OO.ui.InputWidget.super.call( this, config ); - // Mixin constructors - OO.ui.FlaggedElement.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 ) ); @@ -9670,6 +10411,7 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) { 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 */ @@ -9683,12 +10425,15 @@ OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement ); /** * 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 + * @param {Object} config Configuration options * @return {jQuery} Input element */ OO.ui.InputWidget.prototype.getInputElement = function () { - return this.$( '' ); + return $( '' ); }; /** @@ -9712,6 +10457,12 @@ OO.ui.InputWidget.prototype.onEdit = function () { * @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; }; @@ -9721,13 +10472,7 @@ OO.ui.InputWidget.prototype.getValue = function () { * @param {boolean} isRTL */ OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) { - if ( isRTL ) { - this.$input.removeClass( 'oo-ui-ltr' ); - this.$input.addClass( 'oo-ui-rtl' ); - } else { - this.$input.removeClass( 'oo-ui-rtl' ); - this.$input.addClass( 'oo-ui-ltr' ); - } + this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' ); }; /** @@ -9778,7 +10523,7 @@ OO.ui.InputWidget.prototype.simulateLabelClick = function () { if ( this.$input.is( ':checkbox,:radio' ) ) { this.$input.click(); } else if ( this.$input.is( ':input' ) ) { - this.$input[0].focus(); + this.$input[ 0 ].focus(); } } }; @@ -9800,7 +10545,7 @@ OO.ui.InputWidget.prototype.setDisabled = function ( state ) { * @chainable */ OO.ui.InputWidget.prototype.focus = function () { - this.$input[0].focus(); + this.$input[ 0 ].focus(); return this; }; @@ -9810,12 +10555,27 @@ OO.ui.InputWidget.prototype.focus = function () { * @chainable */ OO.ui.InputWidget.prototype.blur = function () { - this.$input[0].blur(); + this.$input[ 0 ].blur(); return this; }; /** - * A button that is an input widget. Intended to be used within a OO.ui.FormLayout. + * 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 `` (the default) or an HTML `` 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 @@ -9852,12 +10612,6 @@ OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) { OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) ); OO.ui.FlaggedElement.call( this, config ); - // Events - this.$input.on( { - click: this.onClick.bind( this ), - keypress: this.onKeyPress.bind( this ) - } ); - // Initialization if ( !config.useInputTag ) { this.$input.append( this.$icon, this.$label, this.$indicator ); @@ -9875,28 +10629,15 @@ OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement ); OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement ); OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement ); -/* Events */ - -/** - * @event click - */ - /* Methods */ /** - * Get input element. - * + * @inheritdoc * @private - * @param {Object} [config] Configuration options - * @return {jQuery} Input element */ OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) { - // Configuration initialization - config = config || {}; - var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">'; - - return this.$( html ); + return $( html ); }; /** @@ -9942,32 +10683,6 @@ OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) { return this; }; -/** - * Handles mouse click events. - * - * @param {jQuery.Event} e Mouse click event - * @fires click - */ -OO.ui.ButtonInputWidget.prototype.onClick = function () { - if ( !this.isDisabled() ) { - this.emit( 'click' ); - } - return false; -}; - -/** - * Handles keypress events. - * - * @param {jQuery.Event} e Keypress event - * @fires click - */ -OO.ui.ButtonInputWidget.prototype.onKeyPress = function ( e ) { - if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) { - this.emit( 'click' ); - } - return false; -}; - /** * Checkbox input widget. * @@ -9979,6 +10694,9 @@ OO.ui.ButtonInputWidget.prototype.onKeyPress = function ( e ) { * @cfg {boolean} [selected=false] Whether the checkbox is initially selected */ OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) { + // Configuration initialization + config = config || {}; + // Parent constructor OO.ui.CheckboxInputWidget.super.call( this, config ); @@ -9994,13 +10712,11 @@ OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget ); /* Methods */ /** - * Get input element. - * + * @inheritdoc * @private - * @return {jQuery} Input element */ OO.ui.CheckboxInputWidget.prototype.getInputElement = function () { - return this.$( '' ); + return $( '' ); }; /** @@ -10038,9 +10754,139 @@ OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) { * @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; }; +/** + * A OO.ui.DropdownWidget synchronized with a `` for form submission. Intended to + * be used within a OO.ui.FormLayout. + * + * @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 $( '' ); +}; + +/** + * Handles menu select events. + * + * @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 ) { + var item = this.dropdownWidget.getMenu().getItemFromData( value ); + if ( item ) { + this.dropdownWidget.getMenu().selectItem( item ); + } + 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; +}; + /** * Radio input widget. * @@ -10055,6 +10901,9 @@ OO.ui.CheckboxInputWidget.prototype.isSelected = function () { * @cfg {boolean} [selected=false] Whether the radio button is initially selected */ OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) { + // Configuration initialization + config = config || {}; + // Parent constructor OO.ui.RadioInputWidget.super.call( this, config ); @@ -10070,13 +10919,11 @@ OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget ); /* Methods */ /** - * Get input element. - * + * @inheritdoc * @private - * @return {jQuery} Input element */ OO.ui.RadioInputWidget.prototype.getInputElement = function () { - return this.$( '' ); + return $( '' ); }; /** @@ -10115,21 +10962,31 @@ OO.ui.RadioInputWidget.prototype.isSelected = function () { * @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'] HTML tag `type` attribute * @cfg {string} [placeholder] Placeholder text + * @cfg {boolean} [autofocus=false] Ask the browser to focus this widget, using the 'autofocus' HTML + * attribute * @cfg {boolean} [readOnly=false] Prevent changes + * @cfg {number} [maxLength] Maximum allowed number of characters to input * @cfg {boolean} [multiline=false] Allow multiple lines of text * @cfg {boolean} [autosize=false] Automatically resize to fit content * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing - * @cfg {RegExp|string} [validate] Regular expression (or symbolic name referencing + * @cfg {string} [labelPosition='after'] Label position, 'before' or 'after' + * @cfg {boolean} [required=false] Mark the field as required + * @cfg {RegExp|string} [validate] Regular expression to validate against (or symbolic name referencing * one, see #static-validationPatterns) */ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) { // Configuration initialization - config = $.extend( { readOnly: false }, config ); + config = $.extend( { + type: 'text', + labelPosition: 'after', + maxRows: 10 + }, config ); // Parent constructor OO.ui.TextInputWidget.super.call( this, config ); @@ -10138,12 +10995,13 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) { 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 !== undefined ? config.maxRows : 10; + this.maxRows = config.maxRows; this.validate = null; // Clone for resizing @@ -10151,10 +11009,12 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) { this.$clone = this.$input .clone() .insertAfter( this.$input ) - .hide(); + .attr( 'aria-hidden', 'true' ) + .addClass( 'oo-ui-element-hidden' ); } this.setValidation( config.validate ); + this.setPosition( config.labelPosition ); // Events this.$input.on( { @@ -10164,16 +11024,25 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) { this.$element.on( 'DOMNodeInsertedIntoDocument', 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 ) ); // Initialization this.$element .addClass( 'oo-ui-textInputWidget' ) .append( this.$icon, this.$indicator ); - this.setReadOnly( config.readOnly ); + this.setReadOnly( !!config.readOnly ); if ( config.placeholder ) { this.$input.attr( 'placeholder', config.placeholder ); } - this.$element.attr( 'role', 'textbox' ); + 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', 'true' ); + } }; /* Setup */ @@ -10182,6 +11051,7 @@ 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 */ @@ -10203,12 +11073,16 @@ OO.ui.TextInputWidget.static.validationPatterns = { /** * User clicks the icon. * + * @deprecated Fundamentally not accessible. Make the icon focusable, associate a label or tooltip, + * and handle click/keypress events on it manually. * @event icon */ /** * User clicks the indicator. * + * @deprecated Fundamentally not accessible. Make the indicator focusable, associate a label or + * tooltip, and handle click/keypress events on it manually. * @event indicator */ @@ -10222,7 +11096,7 @@ OO.ui.TextInputWidget.static.validationPatterns = { */ OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) { if ( e.which === 1 ) { - this.$input[0].focus(); + this.$input[ 0 ].focus(); this.emit( 'icon' ); return false; } @@ -10236,7 +11110,7 @@ OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) { */ OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) { if ( e.which === 1 ) { - this.$input[0].focus(); + this.$input[ 0 ].focus(); this.emit( 'indicator' ); return false; } @@ -10260,7 +11134,10 @@ OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) { * @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(); }; /** @@ -10325,11 +11202,11 @@ OO.ui.TextInputWidget.prototype.adjustSize = function () { // Set inline height property to 0 to measure scroll height .css( 'height', 0 ); - this.$clone[0].style.display = 'block'; + this.$clone.removeClass( 'oo-ui-element-hidden' ); this.valCache = this.$input.val(); - scrollHeight = this.$clone[0].scrollHeight; + scrollHeight = this.$clone[ 0 ].scrollHeight; // Remove inline height property to measure natural heights this.$clone.css( 'height', '' ); @@ -10345,10 +11222,10 @@ OO.ui.TextInputWidget.prototype.adjustSize = function () { // 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; + measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight; idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError ); - this.$clone[0].style.display = 'none'; + this.$clone.addClass( 'oo-ui-element-hidden' ); // Only apply inline height when expansion beyond natural height is needed if ( idealHeight > innerHeight ) { @@ -10362,19 +11239,11 @@ OO.ui.TextInputWidget.prototype.adjustSize = function () { }; /** - * Get input element. - * + * @inheritdoc * @private - * @param {Object} [config] Configuration options - * @return {jQuery} Input element */ OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) { - // Configuration initialization - config = config || {}; - - var type = config.type || 'text'; - - return config.multiline ? this.$( '