( function () { '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 || $( '
' ).append( Util.parseMsg( 'apisandbox-intro' ) ) ) .append( $( '
][\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 ( Object.prototype.hasOwnProperty.call( pages, 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 ( Object.prototype.hasOwnProperty.call( pages, 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.itemsFieldset = null; this.deprecatedItemsFieldset = null; this.templatedItemsCache = {}; 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.setTitle( this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain() ); }; function widgetLabelOnClick() { var f = this.getField(); if ( typeof f.setDisabled === 'function' ) { f.setDisabled( false ); } if ( typeof f.focus === 'function' ) { f.focus(); } } /** * Create a widget and the FieldLayouts it needs * @private * @param {Object} ppi API paraminfo data for the parameter * @param {string} name API parameter name * @return {Object} * @return {OO.ui.Widget} return.widget * @return {OO.ui.FieldLayout} return.widgetField * @return {OO.ui.FieldLayout} return.helpField */ ApiSandbox.PageLayout.prototype.makeWidgetFieldLayouts = function ( ppi, name ) { var j, l, widget, descriptionContainer, tmp, flag, count, button, widgetField, helpField, layoutConfig; widget = Util.createWidgetForParameter( ppi ); if ( ppi.tokentype ) { this.tokenWidget = widget; } if ( this.paramInfo.templatedparameters.length ) { widget.on( 'change', this.updateTemplatedParameters, [ null ], this ); } descriptionContainer = $( '' ); tmp = Util.parseHTML( ppi.description ); tmp.filter( 'dl' ).makeCollapsible( { collapsed: true } ).children( '.mw-collapsible-toggle' ).each( function () { var $this = $( this ); $this.parent().prev( 'p' ).append( $this ); } ); descriptionContainer.append( $( '' ).addClass( 'description' ).append( tmp ) ); if ( ppi.info && ppi.info.length ) { for ( j = 0; j < ppi.info.length; j++ ) { descriptionContainer.append( $( '' ) .addClass( 'info' ) .append( Util.parseHTML( ppi.info[ j ] ) ) ); } } flag = true; count = Infinity; switch ( ppi.type ) { case 'namespace': flag = false; count = mw.config.get( 'wgFormattedNamespaces' ).length; break; case 'limit': if ( ppi.highmax !== undefined ) { descriptionContainer.append( $( '' ) .addClass( 'info' ) .append( Util.parseMsg( 'api-help-param-limit2', ppi.max, ppi.highmax ), ' ', Util.parseMsg( 'apisandbox-param-limit' ) ) ); } else { descriptionContainer.append( $( '' ) .addClass( 'info' ) .append( Util.parseMsg( 'api-help-param-limit', ppi.max ), ' ', Util.parseMsg( 'apisandbox-param-limit' ) ) ); } break; case 'integer': tmp = ''; if ( ppi.min !== undefined ) { tmp += 'min'; } if ( ppi.max !== undefined ) { tmp += 'max'; } if ( tmp !== '' ) { descriptionContainer.append( $( '' ) .addClass( 'info' ) .append( Util.parseMsg( 'api-help-param-integer-' + tmp, Util.apiBool( ppi.multi ) ? 2 : 1, ppi.min, ppi.max ) ) ); } break; default: if ( Array.isArray( ppi.type ) ) { flag = false; count = ppi.type.length; } break; } if ( Util.apiBool( ppi.multi ) ) { tmp = []; if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) && !( widget instanceof OptionalWidget && widget.widget instanceof OO.ui.TagMultiselectWidget ) ) { tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() ); } if ( count > ppi.lowlimit ) { tmp.push( mw.message( 'api-help-param-multi-max', ppi.lowlimit, ppi.highlimit ).parse() ); } if ( tmp.length ) { descriptionContainer.append( $( '' ) .addClass( 'info' ) .append( Util.parseHTML( tmp.join( ' ' ) ) ) ); } } if ( 'maxbytes' in ppi ) { descriptionContainer.append( $( '' ) .addClass( 'info' ) .append( Util.parseMsg( 'api-help-param-maxbytes', ppi.maxbytes ) ) ); } if ( 'maxchars' in ppi ) { descriptionContainer.append( $( '' ) .addClass( 'info' ) .append( Util.parseMsg( 'api-help-param-maxchars', ppi.maxchars ) ) ); } if ( ppi.usedTemplateVars && ppi.usedTemplateVars.length ) { tmp = $(); for ( j = 0, l = ppi.usedTemplateVars.length; j < l; j++ ) { tmp = tmp.add( $( '' ).text( ppi.usedTemplateVars[ j ] ) ); if ( j === l - 2 ) { tmp = tmp.add( mw.message( 'and' ).parseDom() ); tmp = tmp.add( mw.message( 'word-separator' ).parseDom() ); } else if ( j !== l - 1 ) { tmp = tmp.add( mw.message( 'comma-separator' ).parseDom() ); } } descriptionContainer.append( $( '' ) .addClass( 'info' ) .append( Util.parseMsg( 'apisandbox-templated-parameter-reason', ppi.usedTemplateVars.length, tmp ) ) ); } helpField = new OO.ui.FieldLayout( new OO.ui.Widget( { $content: '\xa0', classes: [ 'mw-apisandbox-spacer' ] } ), { align: 'inline', classes: [ 'mw-apisandbox-help-field' ], label: descriptionContainer } ); layoutConfig = { align: 'left', classes: [ 'mw-apisandbox-widget-field' ], label: name }; if ( ppi.tokentype ) { button = new OO.ui.ButtonWidget( { label: mw.message( 'apisandbox-fetch-token' ).text() } ); button.on( 'click', widget.fetchToken, [], widget ); widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig ); } else { widgetField = new OO.ui.FieldLayout( widget, layoutConfig ); } // We need our own click handler on the widget label to // turn off the disablement. widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) ); // Don't grey out the label when the field is disabled, // it makes it too hard to read and our "disabled" // isn't really disabled. widgetField.onFieldDisable( false ); widgetField.onFieldDisable = function () {}; widgetField.apiParamIndex = ppi.index; return { widget: widget, widgetField: widgetField, helpField: helpField }; }; /** * Update templated parameters in the page * @private * @param {Object} [params] Query parameters for initializing the widgets */ ApiSandbox.PageLayout.prototype.updateTemplatedParameters = function ( params ) { var p, toProcess, doProcess, tmp, toRemove, that = this, pi = this.paramInfo, prefix = that.prefix + pi.prefix; if ( !pi || !pi.templatedparameters.length ) { return; } if ( !$.isPlainObject( params ) ) { params = null; } toRemove = {}; // eslint-disable-next-line no-jquery/no-each-util $.each( this.templatedItemsCache, function ( k, el ) { if ( el.widget.isElementAttached() ) { toRemove[ k ] = el; } } ); // This bit duplicates the PHP logic in ApiBase::extractRequestParams(). // If you update this, see if that needs updating too. toProcess = pi.templatedparameters.map( function ( p ) { return { name: prefix + p.name, info: p, vars: $.extend( {}, p.templatevars ), usedVars: [] }; } ); doProcess = function ( placeholder, target ) { var values, container, index, usedVars, done, items, i; target = prefix + target; if ( !that.widgets[ target ] ) { // The target wasn't processed yet, try the next one. // If all hit this case, the parameter has no expansions. return true; } if ( !that.widgets[ target ].getApiValueForTemplates ) { // Not a multi-valued widget, so it can't have expansions. return false; } values = that.widgets[ target ].getApiValueForTemplates(); if ( !Array.isArray( values ) || !values.length ) { // The target was processed but has no (valid) values. // That means it has no expansions. return false; } // Expand this target in the name and all other targets, // then requeue if there are more targets left or create the widget // and add it to the form if all are done. delete p.vars[ placeholder ]; usedVars = p.usedVars.concat( [ target ] ); placeholder = '{' + placeholder + '}'; done = $.isEmptyObject( p.vars ); if ( done ) { container = Util.apiBool( p.info.deprecated ) ? that.deprecatedItemsFieldset : that.itemsFieldset; items = container.getItems(); index = undefined; for ( i = 0; i < items.length; i++ ) { if ( items[ i ].apiParamIndex !== undefined && items[ i ].apiParamIndex > p.info.index ) { index = i; break; } } } values.forEach( function ( value ) { var name, newVars; if ( !/^[^{}]*$/.exec( value ) ) { // Skip values that make invalid parameter names return; } name = p.name.replace( placeholder, value ); if ( done ) { if ( that.templatedItemsCache[ name ] ) { tmp = that.templatedItemsCache[ name ]; } else { tmp = that.makeWidgetFieldLayouts( $.extend( {}, p.info, { usedTemplateVars: usedVars } ), name ); that.templatedItemsCache[ name ] = tmp; } delete toRemove[ name ]; if ( !tmp.widget.isElementAttached() ) { that.widgets[ name ] = tmp.widget; container.addItems( [ tmp.widgetField, tmp.helpField ], index ); if ( index !== undefined ) { index += 2; } } if ( params ) { tmp.widget.setApiValue( Object.prototype.hasOwnProperty.call( params, name ) ? params[ name ] : undefined ); } } else { newVars = {}; // eslint-disable-next-line no-jquery/no-each-util $.each( p.vars, function ( k, v ) { newVars[ k ] = v.replace( placeholder, value ); } ); toProcess.push( { name: name, info: p.info, vars: newVars, usedVars: usedVars } ); } } ); return false; }; while ( toProcess.length ) { p = toProcess.shift(); // eslint-disable-next-line no-jquery/no-each-util $.each( p.vars, doProcess ); } // eslint-disable-next-line no-jquery/no-map-util toRemove = $.map( toRemove, function ( el, name ) { delete that.widgets[ name ]; return [ el.widgetField, el.helpField ]; } ); if ( toRemove.length ) { this.itemsFieldset.removeItems( toRemove ); this.deprecatedItemsFieldset.removeItems( toRemove ); } }; /** * 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, tmp, items = [], deprecatedItems = [], buttons = [], filterFmModules = function ( v ) { return v.substr( -2 ) !== 'fm' || !Object.prototype.hasOwnProperty.call( availableFormats, v.substr( 0, v.length - 2 ) ); }; // 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: $( '' ).append( pi.helpurls.map( function ( link ) { return $( '
- ' ).append( $( '' ) .attr( { href: link, target: '_blank' } ) .text( link ) ); } ) ) } } ) ); } if ( pi.examples.length ) { buttons.push( new OO.ui.PopupButtonWidget( { $overlay: true, label: mw.message( 'apisandbox-examples' ).text(), icon: 'code', popup: { width: 'auto', padded: true, $content: $( '