From: Timo Tijhof Date: Thu, 10 May 2018 17:38:34 +0000 (+0100) Subject: resources: Give mediawiki.special.* files their own place in src/ X-Git-Tag: 1.34.0-rc.0~5444 X-Git-Url: https://git.cyclocoop.org/%27.%24link.%27?a=commitdiff_plain;h=48a4deeada19aae8ee86d55551b49b2ace688073;p=lhc%2Fweb%2Fwiklou.git resources: Give mediawiki.special.* files their own place in src/ Bug: T193826 Change-Id: Id25cd18079f48308f6ab42207445bbbd74ed5fda --- diff --git a/resources/Resources.php b/resources/Resources.php index ea4e5eafe7..d0bc1ba623 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -2001,11 +2001,11 @@ return [ ], 'mediawiki.special.apisandbox.styles' => [ 'targets' => [ 'desktop', 'mobile' ], - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css', + 'styles' => 'resources/src/mediawiki.special.apisandbox.styles.css', ], 'mediawiki.special.apisandbox' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.css', - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.js', + 'styles' => 'resources/src/mediawiki.special.apisandbox/apisandbox.css', + 'scripts' => 'resources/src/mediawiki.special.apisandbox/apisandbox.js', 'targets' => [ 'desktop', 'mobile' ], 'dependencies' => [ 'mediawiki.api', @@ -2073,7 +2073,7 @@ return [ ], ], 'mediawiki.special.block' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.block.js', + 'scripts' => 'resources/src/mediawiki.special.block.js', 'dependencies' => [ 'oojs-ui-core', 'oojs-ui.styles.icons-editing-core', @@ -2086,7 +2086,7 @@ return [ ], ], 'mediawiki.special.changecredentials.js' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changecredentials.js', + 'scripts' => 'resources/src/mediawiki.special.changecredentials.js', 'dependencies' => [ 'mediawiki.api', 'mediawiki.htmlform.ooui' @@ -2094,18 +2094,18 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.changeslist' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.css', + 'styles' => 'resources/src/mediawiki.special.changeslist.css', 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.changeslist.enhanced' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css', + 'styles' => 'resources/src/mediawiki.special.changeslist.enhanced.css', ], 'mediawiki.special.changeslist.legend' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css', + 'styles' => 'resources/src/mediawiki.special.changeslist.legend.css', 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.changeslist.legend.js' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js', + 'scripts' => 'resources/src/mediawiki.special.changeslist.legend.js', 'dependencies' => [ 'jquery.makeCollapsible', 'mediawiki.cookie', @@ -2113,20 +2113,20 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.changeslist.visitedstatus' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js', + 'scripts' => 'resources/src/mediawiki.special.changeslist.visitedstatus.js', ], 'mediawiki.special.comparepages.styles' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less', + 'styles' => 'resources/src/mediawiki.special.comparepages.styles.less', ], 'mediawiki.special.contributions' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.contributions.js', + 'scripts' => 'resources/src/mediawiki.special.contributions.js', 'dependencies' => [ 'mediawiki.widgets.DateInputWidget', 'mediawiki.jqueryMsg', ] ], 'mediawiki.special.edittags' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.edittags.js', + 'scripts' => 'resources/src/mediawiki.special.edittags.js', 'dependencies' => [ 'jquery.chosen', 'jquery.lengthLimit', @@ -2137,38 +2137,38 @@ return [ ], ], 'mediawiki.special.edittags.styles' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.edittags.css', + 'styles' => 'resources/src/mediawiki.special.edittags.styles.css', ], 'mediawiki.special.import' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.import.js', + 'scripts' => 'resources/src/mediawiki.special.import.js', ], 'mediawiki.special.movePage' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.movePage.js', + 'scripts' => 'resources/src/mediawiki.special.movePage.js', 'dependencies' => [ 'mediawiki.widgets.visibleLengthLimit', 'mediawiki.widgets', ], ], 'mediawiki.special.movePage.styles' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.movePage.css', + 'styles' => 'resources/src/mediawiki.special.movePage.css', ], 'mediawiki.special.pageLanguage' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.pageLanguage.js', + 'scripts' => 'resources/src/mediawiki.special.pageLanguage.js', 'dependencies' => [ 'oojs-ui-core', ], ], 'mediawiki.special.pagesWithProp' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css', + 'styles' => 'resources/src/mediawiki.special.pagesWithProp.css', ], 'mediawiki.special.preferences' => [ 'targets' => [ 'desktop', 'mobile' ], 'scripts' => [ - 'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js', + 'resources/src/mediawiki.special.preferences/confirmClose.js', + 'resources/src/mediawiki.special.preferences/convertmessagebox.js', + 'resources/src/mediawiki.special.preferences/tabs.legacy.js', + 'resources/src/mediawiki.special.preferences/timezone.js', + 'resources/src/mediawiki.special.preferences/personalEmail.js', ], 'messages' => [ 'prefs-tabs-navigation-hint', @@ -2184,17 +2184,19 @@ return [ ], 'mediawiki.special.preferences.styles' => [ 'targets' => [ 'desktop', 'mobile' ], - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css', + // legacy + 'styles' => 'resources/src/mediawiki.special.preferences.styles.css', ], 'mediawiki.special.preferences.ooui' => [ 'targets' => [ 'desktop', 'mobile' ], 'scripts' => [ - 'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js', - 'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js', + // FIXME: This uses files already belonging to another module + 'resources/src/mediawiki.special.preferences/confirmClose.js', + 'resources/src/mediawiki.special.preferences/convertmessagebox.js', + 'resources/src/mediawiki.special.preferences.ooui/editfont.js', + 'resources/src/mediawiki.special.preferences.ooui/tabs.js', + 'resources/src/mediawiki.special.preferences/timezone.js', + 'resources/src/mediawiki.special.preferences/personalEmail.js', ], 'messages' => [ 'prefs-tabs-navigation-hint', @@ -2213,14 +2215,14 @@ return [ ], 'mediawiki.special.preferences.styles.ooui' => [ 'targets' => [ 'desktop', 'mobile' ], - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css', + 'styles' => 'resources/src/mediawiki.special.preferences.styles.ooui.css', ], 'mediawiki.special.recentchanges' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.recentchanges.js', + 'scripts' => 'resources/src/mediawiki.special.recentchanges.js', 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.revisionDelete' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.revisionDelete.js', + 'scripts' => 'resources/src/mediawiki.special.revisionDelete.js', 'messages' => [ // @todo Load this message in content language 'colon-separator', @@ -2231,8 +2233,8 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.search' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.search.js', - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.search.css', + 'scripts' => 'resources/src/mediawiki.special.search/search.js', + 'styles' => 'resources/src/mediawiki.special.search/search.css', 'dependencies' => 'mediawiki.widgets.SearchInputWidget', 'messages' => [ 'powersearch-togglelabel', @@ -2241,7 +2243,7 @@ return [ ], ], 'mediawiki.special.search.commonsInterwikiWidget' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js', + 'scripts' => 'resources/src/mediawiki.special.search.commonsInterwikiWidget.js', 'dependencies' => [ 'mediawiki.api', 'mediawiki.Uri', @@ -2254,24 +2256,23 @@ return [ ], ], 'mediawiki.special.search.interwikiwidget.styles' => [ - 'styles' => 'resources/src/mediawiki.special/' - . 'mediawiki.special.search.interwikiwidget.styles.less', + 'styles' => 'resources/src/mediawiki.special.search.interwikiwidget.styles.less', 'targets' => [ 'desktop', 'mobile' ] ], 'mediawiki.special.search.styles' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.search.styles.css', + 'styles' => 'resources/src/mediawiki.special.search.styles.css', 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.undelete' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.undelete.js', + 'scripts' => 'resources/src/mediawiki.special.undelete.js', 'dependencies' => [ 'mediawiki.widgets.visibleLengthLimit', 'mediawiki.widgets', ], ], 'mediawiki.special.unwatchedPages' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js', - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css', + 'scripts' => 'resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js', + 'styles' => 'resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css', 'messages' => [ 'addedwatchtext-short', 'removedwatchtext-short', @@ -2291,9 +2292,9 @@ return [ ], 'mediawiki.special.upload' => [ 'templates' => [ - 'thumbnail.html' => 'resources/src/mediawiki.special/templates/thumbnail.html', + 'thumbnail.html' => 'resources/src/mediawiki.special.upload/templates/thumbnail.html', ], - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.upload.js', + 'scripts' => 'resources/src/mediawiki.special.upload/upload.js', 'messages' => [ 'widthheight', 'size-bytes', @@ -2319,21 +2320,21 @@ return [ ], ], 'mediawiki.special.upload.styles' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.upload.styles.css', + 'styles' => 'resources/src/mediawiki.special.upload.styles.css', ], 'mediawiki.special.userlogin.common.styles' => [ 'targets' => [ 'desktop', 'mobile' ], 'skinStyles' => [ - 'default' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.common.css', + 'default' => 'resources/src/mediawiki.special.userlogin.common.styles/userlogin.css', ], ], 'mediawiki.special.userlogin.login.styles' => [ 'styles' => [ - 'resources/src/mediawiki.special/mediawiki.special.userlogin.login.css', + 'resources/src/mediawiki.special.userlogin.login.styles/login.css', ], ], 'mediawiki.special.userlogin.signup.js' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js', + 'scripts' => 'resources/src/mediawiki.special.userlogin.signup.js', 'messages' => [ 'createacct-emailrequired', 'noname', @@ -2348,18 +2349,18 @@ return [ ], 'mediawiki.special.userlogin.signup.styles' => [ 'styles' => [ - 'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css', + 'resources/src/mediawiki.special.userlogin.signup.styles/signup.css', ], ], 'mediawiki.special.userrights' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userrights.js', + 'scripts' => 'resources/src/mediawiki.special.userrights.js', 'dependencies' => [ 'mediawiki.notification.convertmessagebox', 'jquery.lengthLimit', ], ], 'mediawiki.special.watchlist' => [ - 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js', + 'scripts' => 'resources/src/mediawiki.special.watchlist.js', 'messages' => [ 'addedwatchtext', 'addedwatchtext-talk', @@ -2380,10 +2381,10 @@ return [ ], ], 'mediawiki.special.watchlist.styles' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.css', + 'styles' => 'resources/src/mediawiki.special.watchlist.styles.css', ], 'mediawiki.special.version' => [ - 'styles' => 'resources/src/mediawiki.special/mediawiki.special.version.css', + 'styles' => 'resources/src/mediawiki.special.version.css', ], /* MediaWiki Installer */ diff --git a/resources/src/mediawiki.special.apisandbox.styles.css b/resources/src/mediawiki.special.apisandbox.styles.css new file mode 100644 index 0000000000..4dc4c27ab0 --- /dev/null +++ b/resources/src/mediawiki.special.apisandbox.styles.css @@ -0,0 +1,3 @@ +.client-js .mw-apisandbox-nojs { + display: none; +} diff --git a/resources/src/mediawiki.special.apisandbox/apisandbox.css b/resources/src/mediawiki.special.apisandbox/apisandbox.css new file mode 100644 index 0000000000..fe5ac416bf --- /dev/null +++ b/resources/src/mediawiki.special.apisandbox/apisandbox.css @@ -0,0 +1,110 @@ +.mw-apisandbox-toolbar { + background: #fff; + -webkit-position: sticky; + position: sticky; + top: 0; + margin-bottom: -1px; + padding: 0.5em 0; + border-bottom: 1px solid #a2a9b1; + text-align: right; + z-index: 1; +} + +#mw-apisandbox-ui .mw-apisandbox-link { + display: none; +} + +.mw-apisandbox-popup .oo-ui-popupWidget-body > .oo-ui-widget { + vertical-align: middle; +} + +/* So DateTimeInputWidget's calendar popup works... */ +.mw-apisandbox-popup .oo-ui-popupWidget-popup, +.mw-apisandbox-popup .oo-ui-popupWidget-body { + overflow: visible; +} + +/* Display contents of the popup on a single line */ +.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body { + display: table; +} + +.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * { + display: table-cell; +} + +.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > .oo-ui-buttonWidget { + padding-left: 0.5em; + width: 1%; +} + +.mw-apisandbox-spacer { + display: inline-block; + height: 1px; + width: 5em; +} + +.mw-apisandbox-help-field { + border-bottom: 1px solid rgba( 0, 0, 0, 0.1 ); +} + +.mw-apisandbox-help-field:last-child { + border-bottom: 0; +} + +.mw-apisandbox-optionalWidget { + width: 100%; +} + +.mw-apisandbox-optionalWidget.oo-ui-widget-disabled { + position: relative; + z-index: 0; /* New stacking context to prevent the cover from leaking out */ +} + +.mw-apisandbox-optionalWidget-cover { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 2; + cursor: pointer; +} + +.mw-apisandbox-optionalWidget-fields { + display: table; + width: 100%; +} + +.mw-apisandbox-optionalWidget-widget, +.mw-apisandbox-optionalWidget-checkbox { + display: table-cell; + vertical-align: middle; +} + +.mw-apisandbox-optionalWidget-checkbox { + width: 1%; /* Will be expanded by content */ + white-space: nowrap; + padding-left: 0.5em; +} + +.mw-apisandbox-textInputCode .oo-ui-inputWidget-input { + font-family: monospace, monospace; + font-size: 0.8125em; + -moz-tab-size: 4; + tab-size: 4; +} + +.mw-apisandbox-widget-field .oo-ui-textInputWidget { + /* Leave at least enough space for icon, indicator, and a sliver of text */ + min-width: 6em; +} + +.apihelp-deprecated { + font-weight: bold; + color: #d33; +} + +.apihelp-deprecated-value .oo-ui-labelElement-label { + text-decoration: line-through; +} diff --git a/resources/src/mediawiki.special.apisandbox/apisandbox.js b/resources/src/mediawiki.special.apisandbox/apisandbox.js new file mode 100644 index 0000000000..523a62e75b --- /dev/null +++ b/resources/src/mediawiki.special.apisandbox/apisandbox.js @@ -0,0 +1,1864 @@ +( function ( $, mw, OO ) { + 'use strict'; + var ApiSandbox, Util, WidgetMethods, Validators, + $content, panel, booklet, oldhash, windowManager, + formatDropdown, + api = new mw.Api(), + bookletPages = [], + availableFormats = {}, + resultPage = null, + suppressErrors = true, + updatingBooklet = false, + pages = {}, + moduleInfoCache = {}, + baseRequestParams; + + /** + * A wrapper for a widget that provides an enable/disable button + * + * @class + * @private + * @constructor + * @param {OO.ui.Widget} widget + * @param {Object} [config] Configuration options + */ + function OptionalWidget( widget, config ) { + var k; + + config = config || {}; + + this.widget = widget; + this.$cover = config.$cover || + $( '
' ).addClass( 'mw-apisandbox-optionalWidget-cover' ); + this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox ) + .on( 'change', this.onCheckboxChange, [], this ); + + OptionalWidget[ 'super' ].call( this, config ); + + // Forward most methods for convenience + for ( k in this.widget ) { + if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) { + this[ k ] = this.widget[ k ].bind( this.widget ); + } + } + + this.$cover.on( 'click', this.onOverlayClick.bind( this ) ); + + this.$element + .addClass( 'mw-apisandbox-optionalWidget' ) + .append( + this.$cover, + $( '
' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append( + $( '
' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append( + widget.$element + ), + $( '
' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append( + this.checkbox.$element + ) + ) + ); + + this.setDisabled( widget.isDisabled() ); + } + OO.inheritClass( OptionalWidget, OO.ui.Widget ); + OptionalWidget.prototype.onCheckboxChange = function ( checked ) { + this.setDisabled( !checked ); + }; + OptionalWidget.prototype.onOverlayClick = function () { + this.setDisabled( false ); + if ( $.isFunction( this.widget.focus ) ) { + this.widget.focus(); + } + }; + OptionalWidget.prototype.setDisabled = function ( disabled ) { + OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled ); + this.widget.setDisabled( this.isDisabled() ); + this.checkbox.setSelected( !this.isDisabled() ); + this.$cover.toggle( this.isDisabled() ); + return this; + }; + + WidgetMethods = { + textInputWidget: { + getApiValue: function () { + return this.getValue(); + }, + setApiValue: function ( v ) { + if ( v === undefined ) { + v = this.paramInfo[ 'default' ]; + } + this.setValue( v ); + }, + apiCheckValid: function () { + var that = this; + return this.getValidity().then( function () { + return $.Deferred().resolve( true ).promise(); + }, function () { + return $.Deferred().resolve( false ).promise(); + } ).done( function ( ok ) { + ok = ok || suppressErrors; + that.setIcon( ok ? null : 'alert' ); + that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); + } ); + } + }, + + dateTimeInputWidget: { + getValidity: function () { + if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) { + return $.Deferred().resolve().promise(); + } else { + return $.Deferred().reject().promise(); + } + } + }, + + tokenWidget: { + alertTokenError: function ( code, error ) { + windowManager.openWindow( 'errorAlert', { + title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ), + message: error, + actions: [ + { + action: 'accept', + label: OO.ui.msg( 'ooui-dialog-process-dismiss' ), + flags: 'primary' + } + ] + } ); + }, + fetchToken: function () { + this.pushPending(); + return api.getToken( this.paramInfo.tokentype ) + .done( this.setApiValue.bind( this ) ) + .fail( this.alertTokenError.bind( this ) ) + .always( this.popPending.bind( this ) ); + }, + setApiValue: function ( v ) { + WidgetMethods.textInputWidget.setApiValue.call( this, v ); + if ( v === '123ABC' ) { + this.fetchToken(); + } + } + }, + + passwordWidget: { + getApiValueForDisplay: function () { + return ''; + } + }, + + toggleSwitchWidget: { + getApiValue: function () { + return this.getValue() ? 1 : undefined; + }, + setApiValue: function ( v ) { + this.setValue( Util.apiBool( v ) ); + }, + apiCheckValid: function () { + return $.Deferred().resolve( true ).promise(); + } + }, + + dropdownWidget: { + getApiValue: function () { + var item = this.getMenu().findSelectedItem(); + return item === null ? undefined : item.getData(); + }, + setApiValue: function ( v ) { + var menu = this.getMenu(); + + if ( v === undefined ) { + v = this.paramInfo[ 'default' ]; + } + if ( v === undefined ) { + menu.selectItem(); + } else { + menu.selectItemByData( String( v ) ); + } + }, + apiCheckValid: function () { + var ok = this.getApiValue() !== undefined || suppressErrors; + this.setIcon( ok ? null : 'alert' ); + this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); + return $.Deferred().resolve( ok ).promise(); + } + }, + + tagWidget: { + getApiValue: function () { + var items = this.getValue(); + if ( items.join( '' ).indexOf( '|' ) === -1 ) { + return items.join( '|' ); + } else { + return '\x1f' + items.join( '\x1f' ); + } + }, + setApiValue: function ( v ) { + if ( v === undefined || v === '' || v === '\x1f' ) { + this.setValue( [] ); + } else { + v = String( v ); + if ( v.indexOf( '\x1f' ) !== 0 ) { + this.setValue( v.split( '|' ) ); + } else { + this.setValue( v.substr( 1 ).split( '\x1f' ) ); + } + } + }, + apiCheckValid: function () { + var ok = true, + pi = this.paramInfo; + + if ( !suppressErrors ) { + ok = this.getApiValue() !== undefined && !( + pi.allspecifier !== undefined && + this.getValue().length > 1 && + this.getValue().indexOf( pi.allspecifier ) !== -1 + ); + } + + this.setIcon( ok ? null : 'alert' ); + this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); + return $.Deferred().resolve( ok ).promise(); + }, + createTagItemWidget: function ( data, label ) { + var item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label ); + if ( this.paramInfo.deprecatedvalues && + this.paramInfo.deprecatedvalues.indexOf( data ) >= 0 + ) { + item.$element.addClass( 'apihelp-deprecated-value' ); + } + return item; + } + }, + + optionalWidget: { + getApiValue: function () { + return this.isDisabled() ? undefined : this.widget.getApiValue(); + }, + setApiValue: function ( v ) { + this.setDisabled( v === undefined ); + this.widget.setApiValue( v ); + }, + apiCheckValid: function () { + if ( this.isDisabled() ) { + return $.Deferred().resolve( true ).promise(); + } else { + return this.widget.apiCheckValid(); + } + } + }, + + submoduleWidget: { + single: function () { + var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue(); + return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ]; + }, + multi: function () { + var map = this.paramInfo.submodules, + v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue(); + return v === undefined || v === '' ? [] : String( v ).split( '|' ).map( function ( v ) { + return { value: v, path: map[ v ] }; + } ); + } + }, + + uploadWidget: { + getApiValueForDisplay: function () { + return '...'; + }, + getApiValue: function () { + return this.getValue(); + }, + setApiValue: function () { + // Can't, sorry. + }, + apiCheckValid: function () { + var ok = this.getValue() !== null || suppressErrors; + this.setIcon( ok ? null : 'alert' ); + this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); + return $.Deferred().resolve( ok ).promise(); + } + } + }; + + Validators = { + generic: function () { + return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== ''; + } + }; + + /** + * @class mw.special.ApiSandbox.Util + * @private + */ + Util = { + /** + * Fetch API module info + * + * @param {string} module Module to fetch data for + * @return {jQuery.Promise} + */ + fetchModuleInfo: function ( module ) { + var apiPromise, + deferred = $.Deferred(); + + if ( moduleInfoCache.hasOwnProperty( module ) ) { + return deferred + .resolve( moduleInfoCache[ module ] ) + .promise( { abort: function () {} } ); + } else { + apiPromise = api.post( { + action: 'paraminfo', + modules: module, + helpformat: 'html', + uselang: mw.config.get( 'wgUserLanguage' ) + } ).done( function ( data ) { + var info; + + if ( data.warnings && data.warnings.paraminfo ) { + deferred.reject( '???', data.warnings.paraminfo[ '*' ] ); + return; + } + + info = data.paraminfo.modules; + if ( !info || info.length !== 1 || info[ 0 ].path !== module ) { + deferred.reject( '???', 'No module data returned' ); + return; + } + + moduleInfoCache[ module ] = info[ 0 ]; + deferred.resolve( info[ 0 ] ); + } ).fail( function ( code, details ) { + if ( code === 'http' ) { + details = 'HTTP error: ' + details.exception; + } else if ( details.error ) { + details = details.error.info; + } + deferred.reject( code, details ); + } ); + return deferred + .promise( { abort: apiPromise.abort } ); + } + }, + + /** + * Mark all currently-in-use tokens as bad + */ + markTokensBad: function () { + var page, subpages, i, + checkPages = [ pages.main ]; + + while ( checkPages.length ) { + page = checkPages.shift(); + + if ( page.tokenWidget ) { + api.badToken( page.tokenWidget.paramInfo.tokentype ); + } + + subpages = page.getSubpages(); + for ( i = 0; i < subpages.length; i++ ) { + if ( pages.hasOwnProperty( subpages[ i ].key ) ) { + checkPages.push( pages[ subpages[ i ].key ] ); + } + } + } + }, + + /** + * Test an API boolean + * + * @param {Mixed} value + * @return {boolean} + */ + apiBool: function ( value ) { + return value !== undefined && value !== false; + }, + + /** + * Create a widget for a parameter. + * + * @param {Object} pi Parameter info from API + * @param {Object} opts Additional options + * @return {OO.ui.Widget} + */ + createWidgetForParameter: function ( pi, opts ) { + var widget, innerWidget, finalWidget, items, $content, func, + multiModeButton = null, + multiModeInput = null, + multiModeAllowed = false; + + opts = opts || {}; + + switch ( pi.type ) { + case 'boolean': + widget = new OO.ui.ToggleSwitchWidget(); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.toggleSwitchWidget ); + pi.required = true; // Avoid wrapping in the non-required widget + break; + + case 'string': + case 'user': + if ( Util.apiBool( pi.multi ) ) { + widget = new OO.ui.TagMultiselectWidget( { + allowArbitrary: true, + allowDuplicates: Util.apiBool( pi.allowsduplicates ), + $overlay: true + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.tagWidget ); + } else { + widget = new OO.ui.TextInputWidget( { + required: Util.apiBool( pi.required ) + } ); + } + if ( !Util.apiBool( pi.multi ) ) { + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + widget.setValidation( Validators.generic ); + } + if ( pi.tokentype ) { + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + $.extend( widget, WidgetMethods.tokenWidget ); + } + break; + + case 'text': + widget = new OO.ui.MultilineTextInputWidget( { + required: Util.apiBool( pi.required ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + widget.setValidation( Validators.generic ); + break; + + case 'password': + widget = new OO.ui.TextInputWidget( { + type: 'password', + required: Util.apiBool( pi.required ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + $.extend( widget, WidgetMethods.passwordWidget ); + widget.setValidation( Validators.generic ); + multiModeAllowed = true; + multiModeInput = widget; + break; + + case 'integer': + widget = new OO.ui.NumberInputWidget( { + required: Util.apiBool( pi.required ), + isInteger: true + } ); + widget.setIcon = widget.input.setIcon.bind( widget.input ); + widget.setIconTitle = widget.input.setIconTitle.bind( widget.input ); + widget.getValidity = widget.input.getValidity.bind( widget.input ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + if ( Util.apiBool( pi.enforcerange ) ) { + widget.setRange( pi.min || -Infinity, pi.max || Infinity ); + } + multiModeAllowed = true; + multiModeInput = widget; + break; + + case 'limit': + widget = new OO.ui.TextInputWidget( { + required: Util.apiBool( pi.required ) + } ); + widget.setValidation( function ( value ) { + var n, pi = this.paramInfo; + + if ( value === 'max' ) { + return true; + } else { + n = +value; + return !isNaN( n ) && isFinite( n ) && + Math.floor( n ) === n && + n >= pi.min && n <= pi.apiSandboxMax; + } + } ); + pi.min = pi.min || 0; + pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max; + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + multiModeAllowed = true; + multiModeInput = widget; + break; + + case 'timestamp': + widget = new mw.widgets.datetime.DateTimeInputWidget( { + formatter: { + format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}' + }, + required: Util.apiBool( pi.required ), + clearable: false + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + $.extend( widget, WidgetMethods.dateTimeInputWidget ); + multiModeAllowed = true; + break; + + case 'upload': + widget = new OO.ui.SelectFileWidget(); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.uploadWidget ); + break; + + case 'namespace': + items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) { + if ( ns === '0' ) { + name = mw.message( 'blanknamespace' ).text(); + } + return new OO.ui.MenuOptionWidget( { data: ns, label: name } ); + } ).sort( function ( a, b ) { + return a.data - b.data; + } ); + if ( Util.apiBool( pi.multi ) ) { + if ( pi.allspecifier !== undefined ) { + items.unshift( new OO.ui.MenuOptionWidget( { + data: pi.allspecifier, + label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text() + } ) ); + } + + widget = new OO.ui.MenuTagMultiselectWidget( { + menu: { items: items }, + $overlay: true + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.tagWidget ); + } else { + widget = new OO.ui.DropdownWidget( { + menu: { items: items }, + $overlay: true + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.dropdownWidget ); + } + break; + + default: + if ( !Array.isArray( pi.type ) ) { + throw new Error( 'Unknown parameter type ' + pi.type ); + } + + items = pi.type.map( function ( v ) { + var config = { + data: String( v ), + label: String( v ), + classes: [] + }; + if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) { + config.classes.push( 'apihelp-deprecated-value' ); + } + return new OO.ui.MenuOptionWidget( config ); + } ); + if ( Util.apiBool( pi.multi ) ) { + if ( pi.allspecifier !== undefined ) { + items.unshift( new OO.ui.MenuOptionWidget( { + data: pi.allspecifier, + label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text() + } ) ); + } + + widget = new OO.ui.MenuTagMultiselectWidget( { + menu: { items: items }, + $overlay: true + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.tagWidget ); + if ( Util.apiBool( pi.submodules ) ) { + widget.getSubmodules = WidgetMethods.submoduleWidget.multi; + widget.on( 'change', ApiSandbox.updateUI ); + } + } else { + widget = new OO.ui.DropdownWidget( { + menu: { items: items }, + $overlay: true + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.dropdownWidget ); + if ( Util.apiBool( pi.submodules ) ) { + widget.getSubmodules = WidgetMethods.submoduleWidget.single; + widget.getMenu().on( 'select', ApiSandbox.updateUI ); + } + if ( pi.deprecatedvalues ) { + widget.getMenu().on( 'select', function ( item ) { + this.$element.toggleClass( + 'apihelp-deprecated-value', + pi.deprecatedvalues.indexOf( item.data ) >= 0 + ); + }, [], widget ); + } + } + + break; + } + + if ( Util.apiBool( pi.multi ) && multiModeAllowed ) { + innerWidget = widget; + + multiModeButton = new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-add-multi' ).text() + } ); + $content = innerWidget.$element.add( multiModeButton.$element ); + + widget = new OO.ui.PopupTagMultiselectWidget( { + allowArbitrary: true, + allowDuplicates: Util.apiBool( pi.allowsduplicates ), + $overlay: true, + popup: { + classes: [ 'mw-apisandbox-popup' ], + padded: true, + $content: $content + } + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.tagWidget ); + + func = function () { + if ( !innerWidget.isDisabled() ) { + innerWidget.apiCheckValid().done( function ( ok ) { + if ( ok ) { + widget.addTag( innerWidget.getApiValue() ); + innerWidget.setApiValue( undefined ); + } + } ); + return false; + } + }; + + if ( multiModeInput ) { + multiModeInput.on( 'enter', func ); + } + multiModeButton.on( 'click', func ); + } + + if ( Util.apiBool( pi.required ) || opts.nooptional ) { + finalWidget = widget; + } else { + finalWidget = new OptionalWidget( widget ); + finalWidget.paramInfo = pi; + $.extend( finalWidget, WidgetMethods.optionalWidget ); + if ( widget.getSubmodules ) { + finalWidget.getSubmodules = widget.getSubmodules.bind( widget ); + finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } ); + } + finalWidget.setDisabled( true ); + } + + widget.setApiValue( pi[ 'default' ] ); + + return finalWidget; + }, + + /** + * Parse an HTML string and call Util.fixupHTML() + * + * @param {string} html HTML to parse + * @return {jQuery} + */ + parseHTML: function ( html ) { + var $ret = $( $.parseHTML( html ) ); + return Util.fixupHTML( $ret ); + }, + + /** + * Parse an i18n message and call Util.fixupHTML() + * + * @param {string} key Key of message to get + * @param {...Mixed} parameters Values for $N replacements + * @return {jQuery} + */ + parseMsg: function () { + var $ret = mw.message.apply( mw.message, arguments ).parseDom(); + return Util.fixupHTML( $ret ); + }, + + /** + * Fix HTML for ApiSandbox display + * + * Fixes are: + * - Add target="_blank" to any links + * + * @param {jQuery} $html DOM to process + * @return {jQuery} + */ + fixupHTML: function ( $html ) { + $html.filter( 'a' ).add( $html.find( 'a' ) ) + .filter( '[href]:not([target])' ) + .attr( 'target', '_blank' ); + return $html; + }, + + /** + * Format a request and return a bunch of menu option widgets + * + * @param {Object} displayParams Query parameters, sanitized for display. + * @param {Object} rawParams Query parameters. You should probably use displayParams instead. + * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout + */ + formatRequest: function ( displayParams, rawParams ) { + var jsonInput, + items = [ + new OO.ui.MenuOptionWidget( { + label: Util.parseMsg( 'apisandbox-request-format-url-label' ), + data: new OO.ui.FieldLayout( + new OO.ui.TextInputWidget( { + readOnly: true, + value: mw.util.wikiScript( 'api' ) + '?' + $.param( displayParams ) + } ), { + label: Util.parseMsg( 'apisandbox-request-url-label' ) + } + ) + } ), + new OO.ui.MenuOptionWidget( { + label: Util.parseMsg( 'apisandbox-request-format-json-label' ), + data: new OO.ui.FieldLayout( + jsonInput = new OO.ui.MultilineTextInputWidget( { + classes: [ 'mw-apisandbox-textInputCode' ], + readOnly: true, + autosize: true, + maxRows: 6, + value: JSON.stringify( displayParams, null, '\t' ) + } ), { + label: Util.parseMsg( 'apisandbox-request-json-label' ) + } + ).on( 'toggle', function ( visible ) { + if ( visible ) { + // Call updatePosition instead of adjustSize + // because the latter has weird caching + // behavior and the former bypasses it. + jsonInput.updatePosition(); + } + } ) + } ) + ]; + + mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams ); + + return items; + }, + + /** + * Event handler for when formatDropdown's selection changes + */ + onFormatDropdownChange: function () { + var i, + menu = formatDropdown.getMenu(), + items = menu.getItems(), + selectedField = menu.findSelectedItem() ? menu.findSelectedItem().getData() : null; + + for ( i = 0; i < items.length; i++ ) { + items[ i ].getData().toggle( items[ i ].getData() === selectedField ); + } + } + }; + + /** + * Interface to ApiSandbox UI + * + * @class mw.special.ApiSandbox + */ + ApiSandbox = { + /** + * Initialize the UI + * + * Automatically called on $.ready() + */ + init: function () { + var $toolbar; + + $content = $( '#mw-apisandbox' ); + + windowManager = new OO.ui.WindowManager(); + $( 'body' ).append( windowManager.$element ); + windowManager.addWindows( { + errorAlert: new OO.ui.MessageDialog() + } ); + + $toolbar = $( '
' ) + .addClass( 'mw-apisandbox-toolbar' ) + .append( + new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-submit' ).text(), + flags: [ 'primary', 'progressive' ] + } ).on( 'click', ApiSandbox.sendRequest ).$element, + new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-reset' ).text(), + flags: 'destructive' + } ).on( 'click', ApiSandbox.resetUI ).$element + ); + + booklet = new OO.ui.BookletLayout( { + expanded: false, + outlined: true, + autoFocus: false + } ); + + panel = new OO.ui.PanelLayout( { + classes: [ 'mw-apisandbox-container' ], + content: [ booklet ], + expanded: false, + framed: true + } ); + + pages.main = new ApiSandbox.PageLayout( { key: 'main', path: 'main' } ); + + // Parse the current hash string + if ( !ApiSandbox.loadFromHash() ) { + ApiSandbox.updateUI(); + } + + $( window ).on( 'hashchange', ApiSandbox.loadFromHash ); + + $content + .empty() + .append( $( '

' ).append( Util.parseMsg( 'apisandbox-intro' ) ) ) + .append( + $( '

' ).attr( 'id', 'mw-apisandbox-ui' ) + .append( $toolbar ) + .append( panel.$element ) + ); + }, + + /** + * Update the current query when the page hash changes + * + * @return {boolean} Successful + */ + loadFromHash: function () { + var params, m, re, + hash = location.hash; + + if ( oldhash === hash ) { + return false; + } + oldhash = hash; + if ( hash === '' ) { + return false; + } + + // I'm surprised this doesn't seem to exist in jQuery or mw.util. + params = {}; + hash = hash.replace( /\+/g, '%20' ); + re = /([^&=#]+)=?([^&#]*)/g; + while ( ( m = re.exec( hash ) ) ) { + params[ decodeURIComponent( m[ 1 ] ) ] = decodeURIComponent( m[ 2 ] ); + } + + ApiSandbox.updateUI( params ); + return true; + }, + + /** + * Update the pages in the booklet + * + * @param {Object} [params] Optional query parameters to load + */ + updateUI: function ( params ) { + var i, page, subpages, j, removePages, + addPages = []; + + if ( !$.isPlainObject( params ) ) { + params = undefined; + } + + if ( updatingBooklet ) { + return; + } + updatingBooklet = true; + try { + if ( params !== undefined ) { + pages.main.loadQueryParams( params ); + } + addPages.push( pages.main ); + if ( resultPage !== null ) { + addPages.push( resultPage ); + } + pages.main.apiCheckValid(); + + i = 0; + while ( addPages.length ) { + page = addPages.shift(); + if ( bookletPages[ i ] !== page ) { + for ( j = i; j < bookletPages.length; j++ ) { + if ( bookletPages[ j ].getName() === page.getName() ) { + bookletPages.splice( j, 1 ); + } + } + bookletPages.splice( i, 0, page ); + booklet.addPages( [ page ], i ); + } + i++; + + if ( page.getSubpages ) { + subpages = page.getSubpages(); + for ( j = 0; j < subpages.length; j++ ) { + if ( !pages.hasOwnProperty( subpages[ j ].key ) ) { + subpages[ j ].indentLevel = page.indentLevel + 1; + pages[ subpages[ j ].key ] = new ApiSandbox.PageLayout( subpages[ j ] ); + } + if ( params !== undefined ) { + pages[ subpages[ j ].key ].loadQueryParams( params ); + } + addPages.splice( j, 0, pages[ subpages[ j ].key ] ); + pages[ subpages[ j ].key ].apiCheckValid(); + } + } + } + + if ( bookletPages.length > i ) { + removePages = bookletPages.splice( i, bookletPages.length - i ); + booklet.removePages( removePages ); + } + + if ( !booklet.getCurrentPageName() ) { + booklet.selectFirstSelectablePage(); + } + } finally { + updatingBooklet = false; + } + }, + + /** + * Reset button handler + */ + resetUI: function () { + suppressErrors = true; + pages = { + main: new ApiSandbox.PageLayout( { key: 'main', path: 'main' } ) + }; + resultPage = null; + ApiSandbox.updateUI(); + }, + + /** + * Submit button handler + * + * @param {Object} [params] Use this set of params instead of those in the form fields. + * The form fields will be updated to match. + */ + sendRequest: function ( params ) { + var page, subpages, i, query, $result, $focus, + progress, $progressText, progressLoading, + deferreds = [], + paramsAreForced = !!params, + displayParams = {}, + tokenWidgets = [], + checkPages = [ pages.main ]; + + // Blur any focused widget before submit, because + // OO.ui.ButtonWidget doesn't take focus itself (T128054) + $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement ); + if ( $focus.length ) { + $focus[ 0 ].blur(); + } + + suppressErrors = false; + + // save widget state in params (or load from it if we are forced) + if ( paramsAreForced ) { + ApiSandbox.updateUI( params ); + } + params = {}; + while ( checkPages.length ) { + page = checkPages.shift(); + if ( page.tokenWidget ) { + tokenWidgets.push( page.tokenWidget ); + } + deferreds = deferreds.concat( page.apiCheckValid() ); + page.getQueryParams( params, displayParams ); + subpages = page.getSubpages(); + for ( i = 0; i < subpages.length; i++ ) { + if ( pages.hasOwnProperty( subpages[ i ].key ) ) { + checkPages.push( pages[ subpages[ i ].key ] ); + } + } + } + + if ( !paramsAreForced ) { + // forced params means we are continuing a query; the base query should be preserved + baseRequestParams = $.extend( {}, params ); + } + + $.when.apply( $, deferreds ).done( function () { + var formatItems, menu, selectedLabel, deferred, actions, errorCount; + + // Count how many times `value` occurs in `array`. + function countValues( value, array ) { + var count, i; + count = 0; + for ( i = 0; i < array.length; i++ ) { + if ( array[ i ] === value ) { + count++; + } + } + return count; + } + + errorCount = countValues( false, arguments ); + if ( errorCount > 0 ) { + actions = [ + { + action: 'accept', + label: OO.ui.msg( 'ooui-dialog-process-dismiss' ), + flags: 'primary' + } + ]; + if ( tokenWidgets.length ) { + // Check all token widgets' validity separately + deferred = $.when.apply( $, tokenWidgets.map( function ( w ) { + return w.apiCheckValid(); + } ) ); + + deferred.done( function () { + // If only the tokens are invalid, offer to fix them + var tokenErrorCount = countValues( false, arguments ); + if ( tokenErrorCount === errorCount ) { + delete actions[ 0 ].flags; + actions.push( { + action: 'fix', + label: mw.message( 'apisandbox-results-fixtoken' ).text(), + flags: 'primary' + } ); + } + } ); + } else { + deferred = $.Deferred().resolve(); + } + deferred.always( function () { + windowManager.openWindow( 'errorAlert', { + title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ), + message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ), + actions: actions + } ).closed.then( function ( data ) { + if ( data && data.action === 'fix' ) { + ApiSandbox.fixTokenAndResend(); + } + } ); + } ); + return; + } + + query = $.param( displayParams ); + + formatItems = Util.formatRequest( displayParams, params ); + + // Force a 'fm' format with wrappedhtml=1, if available + if ( params.format !== undefined ) { + if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) { + params.format = params.format + 'fm'; + } + if ( params.format.substr( -2 ) === 'fm' ) { + params.wrappedhtml = 1; + } + } + + progressLoading = false; + $progressText = $( '' ).text( mw.message( 'apisandbox-sending-request' ).text() ); + progress = new OO.ui.ProgressBarWidget( { + progress: false, + $content: $progressText + } ); + + $result = $( '
' ) + .append( progress.$element ); + + resultPage = page = new OO.ui.PageLayout( '|results|', { expanded: false } ); + page.setupOutlineItem = function () { + this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() ); + }; + + if ( !formatDropdown ) { + formatDropdown = new OO.ui.DropdownWidget( { + menu: { items: [] }, + $overlay: true + } ); + formatDropdown.getMenu().on( 'select', Util.onFormatDropdownChange ); + } + + menu = formatDropdown.getMenu(); + selectedLabel = menu.findSelectedItem() ? menu.findSelectedItem().getLabel() : ''; + if ( typeof selectedLabel !== 'string' ) { + selectedLabel = selectedLabel.text(); + } + menu.clearItems().addItems( formatItems ); + menu.chooseItem( menu.getItemFromLabel( selectedLabel ) || menu.findFirstSelectableItem() ); + + // Fire the event to update field visibilities + Util.onFormatDropdownChange(); + + page.$element.empty() + .append( + new OO.ui.FieldLayout( + formatDropdown, { + label: Util.parseMsg( 'apisandbox-request-selectformat-label' ) + } + ).$element, + formatItems.map( function ( item ) { + return item.getData().$element; + } ), + $result + ); + ApiSandbox.updateUI(); + booklet.setPage( '|results|' ); + + location.href = oldhash = '#' + query; + + api.post( params, { + contentType: 'multipart/form-data', + dataType: 'text', + xhr: function () { + var xhr = new window.XMLHttpRequest(); + xhr.upload.addEventListener( 'progress', function ( e ) { + if ( !progressLoading ) { + if ( e.lengthComputable ) { + progress.setProgress( e.loaded * 100 / e.total ); + } else { + progress.setProgress( false ); + } + } + } ); + xhr.addEventListener( 'progress', function ( e ) { + if ( !progressLoading ) { + progressLoading = true; + $progressText.text( mw.message( 'apisandbox-loading-results' ).text() ); + } + if ( e.lengthComputable ) { + progress.setProgress( e.loaded * 100 / e.total ); + } else { + progress.setProgress( false ); + } + } ); + return xhr; + } + } ) + .catch( function ( code, data, result, jqXHR ) { + var deferred = $.Deferred(); + + if ( code !== 'http' ) { + // Not really an error, work around mw.Api thinking it is. + deferred.resolve( result, jqXHR ); + } else { + // Just forward it. + deferred.reject.apply( deferred, arguments ); + } + return deferred.promise(); + } ) + .then( function ( data, jqXHR ) { + var m, loadTime, button, clear, + ct = jqXHR.getResponseHeader( 'Content-Type' ), + loginSuppressed = jqXHR.getResponseHeader( 'MediaWiki-Login-Suppressed' ) || 'false'; + + $result.empty(); + if ( loginSuppressed !== 'false' ) { + $( '
' ) + .addClass( 'warning' ) + .append( Util.parseMsg( 'apisandbox-results-login-suppressed' ) ) + .appendTo( $result ); + } + if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) { + data = JSON.parse( data ); + if ( data.modules.length ) { + mw.loader.load( data.modules ); + } + if ( data.status && data.status !== 200 ) { + $( '
' ) + .addClass( 'api-pretty-header api-pretty-status' ) + .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) ) + .appendTo( $result ); + } + $result.append( Util.parseHTML( data.html ) ); + loadTime = data.time; + } else if ( ( m = data.match( /][\s\S]*<\/pre>/ ) ) ) { + $result.append( Util.parseHTML( m[ 0 ] ) ); + if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) { + loadTime = parseInt( m[ 1 ], 10 ); + } + } else { + $( '
' )
+								.addClass( 'api-pretty-content' )
+								.text( data )
+								.appendTo( $result );
+						}
+						if ( paramsAreForced || data[ 'continue' ] ) {
+							$result.append(
+								$( '
' ).append( + new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-continue' ).text() + } ).on( 'click', function () { + ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) ); + } ).setDisabled( !data[ 'continue' ] ).$element, + ( clear = new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-continue-clear' ).text() + } ).on( 'click', function () { + ApiSandbox.updateUI( baseRequestParams ); + clear.setDisabled( true ); + booklet.setPage( '|results|' ); + } ).setDisabled( !paramsAreForced ) ).$element, + new OO.ui.PopupButtonWidget( { + $overlay: true, + framed: false, + icon: 'info', + popup: { + $content: $( '
' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ), + padded: true, + width: 'auto' + } + } ).$element + ) + ); + } + if ( typeof loadTime === 'number' ) { + $result.append( + $( '
' ).append( + new OO.ui.LabelWidget( { + label: mw.message( 'apisandbox-request-time', loadTime ).text() + } ).$element + ) + ); + } + + if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) { + // Flush all saved tokens in case one of them is the bad one. + Util.markTokensBad(); + button = new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-results-fixtoken' ).text() + } ); + button.on( 'click', ApiSandbox.fixTokenAndResend ) + .on( 'click', button.setDisabled, [ true ], button ) + .$element.appendTo( $result ); + } + }, function ( code, data ) { + var details = 'HTTP error: ' + data.exception; + $result.empty() + .append( + new OO.ui.LabelWidget( { + label: mw.message( 'apisandbox-results-error', details ).text(), + classes: [ 'error' ] + } ).$element + ); + } ); + } ); + }, + + /** + * Handler for the "Correct token and resubmit" button + * + * Used on a 'badtoken' error, it re-fetches token parameters for all + * pages and then re-submits the query. + */ + fixTokenAndResend: function () { + var page, subpages, i, k, + ok = true, + tokenWait = { dummy: true }, + checkPages = [ pages.main ], + success = function ( k ) { + delete tokenWait[ k ]; + if ( ok && $.isEmptyObject( tokenWait ) ) { + ApiSandbox.sendRequest(); + } + }, + failure = function ( k ) { + delete tokenWait[ k ]; + ok = false; + }; + + while ( checkPages.length ) { + page = checkPages.shift(); + + if ( page.tokenWidget ) { + k = page.apiModule + page.tokenWidget.paramInfo.name; + tokenWait[ k ] = page.tokenWidget.fetchToken(); + tokenWait[ k ] + .done( success.bind( page.tokenWidget, k ) ) + .fail( failure.bind( page.tokenWidget, k ) ); + } + + subpages = page.getSubpages(); + for ( i = 0; i < subpages.length; i++ ) { + if ( pages.hasOwnProperty( subpages[ i ].key ) ) { + checkPages.push( pages[ subpages[ i ].key ] ); + } + } + } + + success( 'dummy', '' ); + }, + + /** + * Reset validity indicators for all widgets + */ + updateValidityIndicators: function () { + var page, subpages, i, + checkPages = [ pages.main ]; + + while ( checkPages.length ) { + page = checkPages.shift(); + page.apiCheckValid(); + subpages = page.getSubpages(); + for ( i = 0; i < subpages.length; i++ ) { + if ( pages.hasOwnProperty( subpages[ i ].key ) ) { + checkPages.push( pages[ subpages[ i ].key ] ); + } + } + } + } + }; + + /** + * PageLayout for API modules + * + * @class + * @private + * @extends OO.ui.PageLayout + * @constructor + * @param {Object} [config] Configuration options + */ + ApiSandbox.PageLayout = function ( config ) { + config = $.extend( { prefix: '', expanded: false }, config ); + this.displayText = config.key; + this.apiModule = config.path; + this.prefix = config.prefix; + this.paramInfo = null; + this.apiIsValid = true; + this.loadFromQueryParams = null; + this.widgets = {}; + this.tokenWidget = null; + this.indentLevel = config.indentLevel ? config.indentLevel : 0; + ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config ); + this.loadParamInfo(); + }; + OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout ); + ApiSandbox.PageLayout.prototype.setupOutlineItem = function () { + this.outlineItem.setLevel( this.indentLevel ); + this.outlineItem.setLabel( this.displayText ); + this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' ); + this.outlineItem.setIconTitle( + this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain() + ); + }; + + /** + * Fetch module information for this page's module, then create UI + */ + ApiSandbox.PageLayout.prototype.loadParamInfo = function () { + var dynamicFieldset, dynamicParamNameWidget, + that = this, + removeDynamicParamWidget = function ( name, layout ) { + dynamicFieldset.removeItems( [ layout ] ); + delete that.widgets[ name ]; + }, + addDynamicParamWidget = function () { + var name, layout, widget, button; + + // Check name is filled in + name = dynamicParamNameWidget.getValue().trim(); + if ( name === '' ) { + dynamicParamNameWidget.focus(); + return; + } + + if ( that.widgets[ name ] !== undefined ) { + windowManager.openWindow( 'errorAlert', { + title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ), + actions: [ + { + action: 'accept', + label: OO.ui.msg( 'ooui-dialog-process-dismiss' ), + flags: 'primary' + } + ] + } ); + return; + } + + widget = Util.createWidgetForParameter( { + name: name, + type: 'string', + 'default': '' + }, { + nooptional: true + } ); + button = new OO.ui.ButtonWidget( { + icon: 'trash', + flags: 'destructive' + } ); + layout = new OO.ui.ActionFieldLayout( + widget, + button, + { + label: name, + align: 'left' + } + ); + button.on( 'click', removeDynamicParamWidget, [ name, layout ] ); + that.widgets[ name ] = widget; + dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 ); + widget.focus(); + + dynamicParamNameWidget.setValue( '' ); + }; + + this.$element.empty() + .append( new OO.ui.ProgressBarWidget( { + progress: false, + text: mw.message( 'apisandbox-loading', this.displayText ).text() + } ).$element ); + + Util.fetchModuleInfo( this.apiModule ) + .done( function ( pi ) { + var prefix, i, j, descriptionContainer, widget, layoutConfig, button, widgetField, helpField, tmp, flag, count, + items = [], + deprecatedItems = [], + buttons = [], + filterFmModules = function ( v ) { + return v.substr( -2 ) !== 'fm' || + !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) ); + }, + widgetLabelOnClick = function () { + var f = this.getField(); + if ( $.isFunction( f.setDisabled ) ) { + f.setDisabled( false ); + } + if ( $.isFunction( f.focus ) ) { + f.focus(); + } + }; + + // This is something of a hack. We always want the 'format' and + // 'action' parameters from the main module to be specified, + // and for 'format' we also want to simplify the dropdown since + // we always send the 'fm' variant. + if ( that.apiModule === 'main' ) { + for ( i = 0; i < pi.parameters.length; i++ ) { + if ( pi.parameters[ i ].name === 'action' ) { + pi.parameters[ i ].required = true; + delete pi.parameters[ i ][ 'default' ]; + } + if ( pi.parameters[ i ].name === 'format' ) { + tmp = pi.parameters[ i ].type; + for ( j = 0; j < tmp.length; j++ ) { + availableFormats[ tmp[ j ] ] = true; + } + pi.parameters[ i ].type = tmp.filter( filterFmModules ); + pi.parameters[ i ][ 'default' ] = 'json'; + pi.parameters[ i ].required = true; + } + } + } + + // Hide the 'wrappedhtml' parameter on format modules + if ( pi.group === 'format' ) { + pi.parameters = pi.parameters.filter( function ( p ) { + return p.name !== 'wrappedhtml'; + } ); + } + + that.paramInfo = pi; + + items.push( new OO.ui.FieldLayout( + new OO.ui.Widget( {} ).toggle( false ), { + align: 'top', + label: Util.parseHTML( pi.description ) + } + ) ); + + if ( pi.helpurls.length ) { + buttons.push( new OO.ui.PopupButtonWidget( { + $overlay: true, + label: mw.message( 'apisandbox-helpurls' ).text(), + icon: 'help', + popup: { + width: 'auto', + padded: true, + $content: $( '