Merge "rdbms: make transaction rounds apply DBO_TRX to DB_REPLICA connections"
[mediawiki.git] / resources / src / mediawiki.special.apisandbox / Util.js
blob61b3e72d832504e3486cf286bc3bfb6f6161c191
1 let Util = null;
3 const api = new mw.Api(),
4         moduleInfoCache = {},
5         ApiSandbox = require( './ApiSandbox.js' ),
6         OptionalParamWidget = require( './OptionalParamWidget.js' ),
7         BooleanToggleSwitchParamWidget = require( './BooleanToggleSwitchParamWidget.js' ),
8         DateTimeParamWidget = require( './DateTimeParamWidget.js' ),
9         LimitParamWidget = require( './LimitParamWidget.js' ),
10         PasswordParamWidget = require( './PasswordParamWidget.js' ),
11         UploadSelectFileParamWidget = require( './UploadSelectFileParamWidget.js' );
13 const WidgetMethods = {
14         textInputWidget: {
15                 getApiValue: function () {
16                         return this.getValue();
17                 },
18                 setApiValue: function ( v ) {
19                         if ( v === undefined ) {
20                                 v = this.paramInfo.default;
21                         }
22                         this.setValue( v );
23                 },
24                 apiCheckValid: function ( shouldSuppressErrors ) {
25                         return this.getValidity().then(
26                                 () => $.Deferred().resolve( true ).promise(),
27                                 () => $.Deferred().resolve( false ).promise()
28                         ).done( ( ok ) => {
29                                 ok = ok || shouldSuppressErrors;
30                                 this.setIcon( ok ? null : 'alert' );
31                                 this.setTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
32                         } );
33                 }
34         },
36         tokenWidget: {
37                 alertTokenError: function ( code, error ) {
38                         ApiSandbox.windowManager.openWindow( 'errorAlert', {
39                                 title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
40                                 message: error,
41                                 actions: [
42                                         {
43                                                 action: 'accept',
44                                                 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
45                                                 flags: 'primary'
46                                         }
47                                 ]
48                         } );
49                 },
50                 fetchToken: function () {
51                         this.pushPending();
52                         return api.getToken( this.paramInfo.tokentype )
53                                 .done( this.setApiValue.bind( this ) )
54                                 .fail( this.alertTokenError.bind( this ) )
55                                 .always( this.popPending.bind( this ) );
56                 },
57                 setApiValue: function ( v ) {
58                         if ( v === undefined ) {
59                                 v = this.paramInfo.default;
60                         }
61                         this.setValue( v );
62                         if ( v === '123ABC' ) {
63                                 this.fetchToken();
64                         }
65                 }
66         },
68         dropdownWidget: {
69                 getApiValue: function () {
70                         const selected = this.getMenu().findFirstSelectedItem();
71                         return selected ? selected.getData() : undefined;
72                 },
73                 setApiValue: function ( v ) {
74                         if ( v === undefined ) {
75                                 v = this.paramInfo.default;
76                         }
77                         const menu = this.getMenu();
78                         if ( v === undefined ) {
79                                 menu.selectItem();
80                         } else {
81                                 menu.selectItemByData( String( v ) );
82                         }
83                 },
84                 apiCheckValid: function ( shouldSuppressErrors ) {
85                         const ok = this.getApiValue() !== undefined || shouldSuppressErrors;
86                         this.setIcon( ok ? null : 'alert' );
87                         this.setTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
88                         return $.Deferred().resolve( ok ).promise();
89                 }
90         },
92         tagWidget: {
93                 parseApiValue: function ( v ) {
94                         if ( v === undefined || v === '' || v === '\x1f' ) {
95                                 return [];
96                         } else {
97                                 v = String( v );
98                                 if ( v[ 0 ] !== '\x1f' ) {
99                                         return v.split( '|' );
100                                 } else {
101                                         return v.slice( 1 ).split( '\x1f' );
102                                 }
103                         }
104                 },
105                 getApiValueForTemplates: function () {
106                         return this.isDisabled() ? this.parseApiValue( this.paramInfo.default ) : this.getValue();
107                 },
108                 getApiValue: function () {
109                         const items = this.getValue();
110                         if ( items.join( '' ).indexOf( '|' ) === -1 ) {
111                                 return items.join( '|' );
112                         } else {
113                                 return '\x1f' + items.join( '\x1f' );
114                         }
115                 },
116                 setApiValue: function ( v ) {
117                         if ( v === undefined ) {
118                                 v = this.paramInfo.default;
119                         }
120                         this.setValue( this.parseApiValue( v ) );
121                 },
122                 apiCheckValid: function ( shouldSuppressErrors ) {
123                         let ok = true;
124                         if ( !shouldSuppressErrors ) {
125                                 const pi = this.paramInfo;
126                                 ok = this.getApiValue() !== undefined && !(
127                                         pi.allspecifier !== undefined &&
128                                         this.getValue().length > 1 &&
129                                         this.getValue().indexOf( pi.allspecifier ) !== -1
130                                 );
131                         }
133                         this.setIcon( ok ? null : 'alert' );
134                         this.setTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
135                         return $.Deferred().resolve( ok ).promise();
136                 },
137                 createTagItemWidget: function ( data, label ) {
138                         const item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label );
139                         if ( this.paramInfo.deprecatedvalues &&
140                                 this.paramInfo.deprecatedvalues.indexOf( data ) >= 0
141                         ) {
142                                 item.$element.addClass( 'mw-apisandbox-deprecated-value' );
143                         }
144                         if ( this.paramInfo.internalvalues &&
145                                 this.paramInfo.internalvalues.indexOf( data ) >= 0
146                         ) {
147                                 item.$element.addClass( 'mw-apisandbox-internal-value' );
148                         }
149                         return item;
150                 }
151         },
153         submoduleWidget: {
154                 single: function () {
155                         const v = this.isDisabled() ? this.paramInfo.default : this.getApiValue();
156                         return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
157                 },
158                 multi: function () {
159                         const map = this.paramInfo.submodules,
160                                 v = this.isDisabled() ? this.paramInfo.default : this.getApiValue();
161                         return v === undefined || v === '' ?
162                                 [] :
163                                 String( v ).split( '|' ).map( ( val ) => ( { value: val, path: map[ val ] } ) );
164                 }
165         }
168 const Validators = {
169         generic: function () {
170                 return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
171         }
175  * @class mw.special.ApiSandbox.Util
176  * @private
177  */
178 Util = {
179         /**
180          * Fetch API module info
181          *
182          * @param {string} module Module to fetch data for
183          * @return {jQuery.Promise}
184          */
185         fetchModuleInfo: function ( module ) {
186                 const deferred = $.Deferred();
188                 if ( Object.prototype.hasOwnProperty.call( moduleInfoCache, module ) ) {
189                         return deferred
190                                 .resolve( moduleInfoCache[ module ] )
191                                 .promise( { abort: () => {} } );
192                 } else {
193                         const apiPromise = api.post( {
194                                 action: 'paraminfo',
195                                 modules: module,
196                                 helpformat: 'html',
197                                 uselang: mw.config.get( 'wgUserLanguage' )
198                         } ).done( ( data ) => {
199                                 if ( data.warnings && data.warnings.paraminfo ) {
200                                         deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
201                                         return;
202                                 }
204                                 const info = data.paraminfo.modules;
205                                 if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
206                                         deferred.reject( '???', 'No module data returned' );
207                                         return;
208                                 }
210                                 moduleInfoCache[ module ] = info[ 0 ];
211                                 deferred.resolve( info[ 0 ] );
212                         } ).fail( ( code, details ) => {
213                                 if ( code === 'http' ) {
214                                         details = 'HTTP error: ' + details.exception;
215                                 } else if ( details.error ) {
216                                         details = details.error.info;
217                                 }
218                                 deferred.reject( code, details );
219                         } );
220                         return deferred
221                                 .promise( { abort: apiPromise.abort } );
222                 }
223         },
225         /**
226          * Mark all currently-in-use tokens as bad
227          */
228         markTokensBad: function () {
229                 const checkPages = [ ApiSandbox.pages.main ];
231                 while ( checkPages.length ) {
232                         const page = checkPages.shift();
234                         if ( page.tokenWidget ) {
235                                 api.badToken( page.tokenWidget.paramInfo.tokentype );
236                         }
238                         const subpages = page.getSubpages();
239                         subpages.forEach( ( subpage ) => {
240                                 if ( Object.prototype.hasOwnProperty.call( ApiSandbox.pages, subpage.key ) ) {
241                                         checkPages.push( ApiSandbox.pages[ subpage.key ] );
242                                 }
243                         } );
244                 }
245         },
247         /**
248          * Test an API boolean
249          *
250          * @param {any} value
251          * @return {boolean}
252          */
253         apiBool: function ( value ) {
254                 return value !== undefined && value !== false;
255         },
257         /**
258          * Create a widget for a parameter.
259          *
260          * @param {Object} pi Parameter info from API
261          * @param {Object} opts Additional options
262          * @return {OO.ui.Widget}
263          */
264         createWidgetForParameter: function ( pi, opts ) {
265                 let multiModeButton = null,
266                         multiModeInput = null,
267                         multiModeAllowed = false;
269                 opts = opts || {};
271                 let widget, items;
272                 switch ( pi.type ) {
273                         case 'boolean':
274                                 widget = new BooleanToggleSwitchParamWidget();
275                                 widget.paramInfo = pi;
276                                 pi.required = true; // Avoid wrapping in the non-required widget
277                                 break;
279                         case 'string':
280                                 // ApiParamInfo only sets `tokentype` when the parameter
281                                 // name is `token` AND the module ::needsToken() returns
282                                 // a truthy value; ApiBase, when the module ::needsToken()
283                                 // returns a truthy value, sets the `token` param to PARAM_TYPE
284                                 // string always, so we only need to have handling for
285                                 // token widgets for `string`. The token never accepts multiple
286                                 // values, though that doesn't appear to be enforced anywhere...
287                                 // and the token widget methods all assume it only a single value
288                                 if ( pi.tokentype ) {
289                                         // We probably don't need to check if its required,
290                                         // it always is, but whats the harm
291                                         widget = new OO.ui.TextInputWidget( {
292                                                 required: Util.apiBool( pi.required )
293                                         } );
294                                         widget.paramInfo = pi;
295                                         Object.assign( widget, WidgetMethods.textInputWidget );
296                                         widget.setValidation( Validators.generic );
297                                         Object.assign( widget, WidgetMethods.tokenWidget );
298                                         break;
299                                 }
300                                 // intentional fall through
301                         case 'user':
302                         case 'expiry':
303                                 if ( Util.apiBool( pi.multi ) ) {
304                                         widget = new OO.ui.TagMultiselectWidget( {
305                                                 allowArbitrary: true,
306                                                 allowDuplicates: Util.apiBool( pi.allowsduplicates ),
307                                                 $overlay: true
308                                         } );
309                                         widget.paramInfo = pi;
310                                         Object.assign( widget, WidgetMethods.tagWidget );
311                                 } else {
312                                         widget = new OO.ui.TextInputWidget( {
313                                                 required: Util.apiBool( pi.required )
314                                         } );
315                                         widget.paramInfo = pi;
316                                         Object.assign( widget, WidgetMethods.textInputWidget );
317                                         widget.setValidation( Validators.generic );
318                                 }
319                                 break;
321                         case 'raw':
322                         case 'text':
323                                 widget = new OO.ui.MultilineTextInputWidget( {
324                                         required: Util.apiBool( pi.required )
325                                 } );
326                                 widget.paramInfo = pi;
327                                 Object.assign( widget, WidgetMethods.textInputWidget );
328                                 widget.setValidation( Validators.generic );
329                                 break;
331                         case 'password':
332                                 widget = new PasswordParamWidget( {
333                                         required: Util.apiBool( pi.required )
334                                 } );
335                                 widget.paramInfo = pi;
336                                 widget.setValidation( Validators.generic );
337                                 multiModeAllowed = true;
338                                 multiModeInput = widget;
339                                 break;
341                         case 'integer':
342                                 widget = new OO.ui.NumberInputWidget( {
343                                         step: 1,
344                                         min: pi.min || -Infinity,
345                                         max: pi.max || Infinity,
346                                         required: Util.apiBool( pi.required )
347                                 } );
348                                 widget.paramInfo = pi;
349                                 Object.assign( widget, WidgetMethods.textInputWidget );
350                                 multiModeAllowed = true;
351                                 multiModeInput = widget;
352                                 break;
354                         case 'limit':
355                                 widget = new LimitParamWidget( {
356                                         required: Util.apiBool( pi.required )
357                                 } );
358                                 pi.min = pi.min || 0;
359                                 pi.apiSandboxMax = ( mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max ) || pi.max;
360                                 widget.paramInfo = pi;
361                                 multiModeAllowed = true;
362                                 multiModeInput = widget;
363                                 break;
365                         case 'timestamp':
366                                 widget = new DateTimeParamWidget( {
367                                         required: Util.apiBool( pi.required )
368                                 } );
369                                 widget.paramInfo = pi;
370                                 multiModeAllowed = true;
371                                 break;
373                         case 'upload':
374                                 widget = new UploadSelectFileParamWidget();
375                                 widget.paramInfo = pi;
376                                 break;
378                         case 'namespace':
379                                 // eslint-disable-next-line no-jquery/no-map-util
380                                 items = $.map( mw.config.get( 'wgFormattedNamespaces' ), ( name, ns ) => {
381                                         if ( ns === '0' ) {
382                                                 name = mw.msg( 'blanknamespace' );
383                                         }
384                                         return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
385                                 } ).sort( ( a, b ) => a.data - b.data );
386                                 if ( Util.apiBool( pi.multi ) ) {
387                                         if ( pi.allspecifier !== undefined ) {
388                                                 items.unshift( new OO.ui.MenuOptionWidget( {
389                                                         data: pi.allspecifier,
390                                                         label: mw.msg( 'apisandbox-multivalue-all-namespaces', pi.allspecifier )
391                                                 } ) );
392                                         }
394                                         widget = new OO.ui.MenuTagMultiselectWidget( {
395                                                 menu: { items: items },
396                                                 $overlay: true
397                                         } );
398                                         widget.paramInfo = pi;
399                                         Object.assign( widget, WidgetMethods.tagWidget );
400                                 } else {
401                                         widget = new OO.ui.DropdownWidget( {
402                                                 menu: { items: items },
403                                                 $overlay: true
404                                         } );
405                                         widget.paramInfo = pi;
406                                         Object.assign( widget, WidgetMethods.dropdownWidget );
407                                 }
408                                 break;
410                         case 'title':
411                                 if ( Util.apiBool( pi.multi ) ) {
412                                         widget = new mw.widgets.TitlesMultiselectWidget( {
413                                                 required: Util.apiBool( pi.required ),
414                                                 validateTitle: true,
415                                                 suggestions: true,
416                                                 showMissing: !Util.apiBool( pi.mustExist ),
417                                                 addQueryInput: !Util.apiBool( pi.mustExist ),
418                                                 tagLimit: pi.limit || undefined
419                                         } );
420                                         widget.paramInfo = pi;
421                                         Object.assign( widget, WidgetMethods.tagWidget );
422                                 } else {
423                                         widget = new mw.widgets.TitleInputWidget( {
424                                                 required: Util.apiBool( pi.required ),
425                                                 validateTitle: true,
426                                                 suggestions: true,
427                                                 autocomplete: true,
428                                                 showMissing: !Util.apiBool( pi.mustExist ),
429                                                 addQueryInput: !Util.apiBool( pi.mustExist )
430                                         } );
431                                         widget.paramInfo = pi;
432                                         Object.assign( widget, WidgetMethods.textInputWidget );
433                                 }
434                                 break;
436                         default:
437                                 if ( !Array.isArray( pi.type ) ) {
438                                         throw new Error( 'Unknown parameter type ' + pi.type );
439                                 }
441                                 items = pi.type.map( ( v ) => {
442                                         const optionWidget = new OO.ui.MenuOptionWidget( {
443                                                 data: String( v ),
444                                                 label: String( v )
445                                         } );
446                                         if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) {
447                                                 optionWidget.$element.addClass( 'mw-apisandbox-deprecated-value' );
448                                                 optionWidget.$label.before(
449                                                         $( '<span>' ).addClass( 'mw-apisandbox-flag' ).text( mw.msg( 'api-help-param-deprecated-label' ) )
450                                                 );
451                                         }
452                                         if ( pi.internalvalues && pi.internalvalues.indexOf( v ) >= 0 ) {
453                                                 optionWidget.$element.addClass( 'mw-apisandbox-internal-value' );
454                                                 optionWidget.$label.before(
455                                                         $( '<span>' ).addClass( 'mw-apisandbox-flag' ).text( mw.msg( 'api-help-param-internal-label' ) )
456                                                 );
457                                         }
458                                         return optionWidget;
459                                 } ).sort( ( a, b ) => a.label < b.label ? -1 : ( a.label > b.label ? 1 : 0 ) );
460                                 if ( Util.apiBool( pi.multi ) ) {
461                                         if ( pi.allspecifier !== undefined ) {
462                                                 items.unshift( new OO.ui.MenuOptionWidget( {
463                                                         data: pi.allspecifier,
464                                                         label: mw.msg( 'apisandbox-multivalue-all-values', pi.allspecifier )
465                                                 } ) );
466                                         }
468                                         widget = new OO.ui.MenuTagMultiselectWidget( {
469                                                 menu: { items: items },
470                                                 $overlay: true
471                                         } );
472                                         widget.paramInfo = pi;
473                                         Object.assign( widget, WidgetMethods.tagWidget );
474                                         if ( Util.apiBool( pi.submodules ) ) {
475                                                 widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
476                                                 widget.on( 'change', ApiSandbox.updateUI );
477                                         }
478                                 } else {
479                                         widget = new OO.ui.DropdownWidget( {
480                                                 menu: { items: items },
481                                                 $overlay: true
482                                         } );
483                                         widget.paramInfo = pi;
484                                         Object.assign( widget, WidgetMethods.dropdownWidget );
485                                         if ( Util.apiBool( pi.submodules ) ) {
486                                                 widget.getSubmodules = WidgetMethods.submoduleWidget.single;
487                                                 widget.getMenu().on( 'select', ApiSandbox.updateUI );
488                                         }
489                                         if ( pi.deprecatedvalues ) {
490                                                 widget.getMenu().on( 'select', ( item ) => {
491                                                         widget.$element.toggleClass(
492                                                                 'mw-apisandbox-deprecated-value',
493                                                                 pi.deprecatedvalues.indexOf( item.data ) >= 0
494                                                         );
495                                                 } );
496                                         }
497                                         if ( pi.internalvalues ) {
498                                                 widget.getMenu().on( 'select', ( item ) => {
499                                                         widget.$element.toggleClass(
500                                                                 'mw-apisandbox-internal-value',
501                                                                 pi.internalvalues.indexOf( item.data ) >= 0
502                                                         );
503                                                 } );
504                                         }
505                                 }
507                                 break;
508                 }
510                 if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
511                         const innerWidget = widget;
513                         multiModeButton = new OO.ui.ButtonWidget( {
514                                 label: mw.msg( 'apisandbox-add-multi' )
515                         } );
516                         const $content = innerWidget.$element.add( multiModeButton.$element );
518                         widget = new OO.ui.PopupTagMultiselectWidget( {
519                                 allowArbitrary: true,
520                                 allowDuplicates: Util.apiBool( pi.allowsduplicates ),
521                                 $overlay: true,
522                                 popup: {
523                                         classes: [ 'mw-apisandbox-popup' ],
524                                         padded: true,
525                                         $content: $content
526                                 }
527                         } );
528                         widget.paramInfo = pi;
529                         Object.assign( widget, WidgetMethods.tagWidget );
531                         const func = () => {
532                                 if ( !innerWidget.isDisabled() ) {
533                                         innerWidget.apiCheckValid( ApiSandbox.suppressErrors ).done( ( ok ) => {
534                                                 if ( ok ) {
535                                                         widget.addTag( innerWidget.getApiValue() );
536                                                         innerWidget.setApiValue( undefined );
537                                                 }
538                                         } );
539                                         return false;
540                                 }
541                         };
543                         if ( multiModeInput ) {
544                                 multiModeInput.on( 'enter', func );
545                         }
546                         multiModeButton.on( 'click', func );
547                 }
549                 let finalWidget;
550                 if ( Util.apiBool( pi.required ) || opts.nooptional ) {
551                         finalWidget = widget;
552                 } else {
553                         finalWidget = new OptionalParamWidget( widget );
554                         finalWidget.paramInfo = pi;
555                         if ( widget.getSubmodules ) {
556                                 finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
557                                 finalWidget.on( 'disable', () => {
558                                         setTimeout( ApiSandbox.updateUI );
559                                 } );
560                         }
561                         if ( widget.getApiValueForTemplates ) {
562                                 finalWidget.getApiValueForTemplates = widget.getApiValueForTemplates.bind( widget );
563                         }
564                         finalWidget.setDisabled( true );
565                 }
567                 widget.setApiValue( pi.default );
569                 return finalWidget;
570         },
572         /**
573          * Parse an HTML string and call Util.fixupHTML()
574          *
575          * @param {string} html HTML to parse
576          * @return {jQuery}
577          */
578         parseHTML: function ( html ) {
579                 const $ret = $( $.parseHTML( html ) );
580                 return Util.fixupHTML( $ret );
581         },
583         /**
584          * Parse an i18n message and call Util.fixupHTML()
585          *
586          * @param {string} key Key of message to get
587          * @param {...Mixed} parameters Values for $N replacements
588          * @return {jQuery}
589          */
590         parseMsg: function ( key, ...parameters ) {
591                 // eslint-disable-next-line mediawiki/msg-doc
592                 const $ret = mw.message( key, ...parameters ).parseDom();
593                 return Util.fixupHTML( $ret );
594         },
596         /**
597          * Fix HTML for ApiSandbox display
598          *
599          * Fixes are:
600          * - Add target="_blank" to any links
601          *
602          * @param {jQuery} $html DOM to process
603          * @return {jQuery}
604          */
605         fixupHTML: function ( $html ) {
606                 $html.filter( 'a' ).add( $html.find( 'a' ) )
607                         .filter( '[href]:not([target])' )
608                         .attr( 'target', '_blank' );
609                 return $html;
610         },
612         /**
613          * Format a request and return a bunch of menu option widgets
614          *
615          * @param {Object} displayParams Query parameters, sanitized for display.
616          * @param {Object} rawParams Query parameters. You should probably use displayParams instead.
617          * @param {string} method HTTP method that must be used for this request: 'get' or 'post'
618          * @param {Object} ajaxOptions Extra options that must be used for this request, in the format
619          *   expected by jQuery#ajax.
620          * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout
621          */
622         formatRequest: function ( displayParams, rawParams, method, ajaxOptions ) {
623                 let jsonLayout, phpLayout;
624                 const apiUrl = new URL( mw.util.wikiScript( 'api' ), location.origin ).toString();
625                 const items = [
626                         new OO.ui.MenuOptionWidget( {
627                                 label: Util.parseMsg( 'apisandbox-request-format-url-label' ),
628                                 data: new mw.widgets.CopyTextLayout( {
629                                         label: Util.parseMsg( 'apisandbox-request-url-label' ),
630                                         copyText: apiUrl + '?' + $.param( displayParams )
631                                 } )
632                         } ),
633                         new OO.ui.MenuOptionWidget( {
634                                 label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
635                                 data: jsonLayout = new mw.widgets.CopyTextLayout( {
636                                         label: Util.parseMsg( 'apisandbox-request-json-label' ),
637                                         copyText: JSON.stringify( displayParams, null, '\t' ),
638                                         multiline: true,
639                                         textInput: {
640                                                 classes: [ 'mw-apisandbox-textInputCode' ],
641                                                 autosize: true,
642                                                 maxRows: 6
643                                         }
644                                 } ).on( 'toggle', ( visible ) => {
645                                         if ( visible ) {
646                                                 // Call updatePosition instead of adjustSize
647                                                 // because the latter has weird caching
648                                                 // behavior and the former bypasses it.
649                                                 jsonLayout.textInput.updatePosition();
650                                         }
651                                 } )
652                         } ),
653                         new OO.ui.MenuOptionWidget( {
654                                 label: Util.parseMsg( 'apisandbox-request-format-php-label' ),
655                                 data: phpLayout = new mw.widgets.CopyTextLayout( {
656                                         label: Util.parseMsg( 'apisandbox-request-php-label' ),
657                                         copyText: '[\n' +
658                                                 Object.keys( displayParams ).map(
659                                                         // displayParams is a dictionary of strings or numbers
660                                                         ( param ) => '\t' +
661                                                                 JSON.stringify( param ) +
662                                                                 ' => ' +
663                                                                 JSON.stringify( displayParams[ param ] ).replace( /\$/g, '\\$' )
664                                                 ).join( ',\n' ) +
665                                                 '\n]',
666                                         multiline: true,
667                                         textInput: {
668                                                 classes: [ 'mw-apisandbox-textInputCode' ],
669                                                 autosize: true,
670                                                 maxRows: 6
671                                         }
672                                 } ).on( 'toggle', ( visible ) => {
673                                         if ( visible ) {
674                                                 // Call updatePosition instead of adjustSize
675                                                 // because the latter has weird caching
676                                                 // behavior and the former bypasses it.
677                                                 phpLayout.textInput.updatePosition();
678                                         }
679                                 } )
680                         } )
681                 ];
683                 mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams, method, ajaxOptions );
685                 return items;
686         },
688         /**
689          * Event handler for when formatDropdown's selection changes
690          */
691         onFormatDropdownChange: function () {
692                 const menu = ApiSandbox.formatDropdown.getMenu(),
693                         selected = menu.findFirstSelectedItem(),
694                         selectedField = selected ? selected.getData() : null;
696                 menu.getItems().forEach( ( item ) => {
697                         item.getData().toggle( item.getData() === selectedField );
698                 } );
699         }
702 module.exports = Util;