Added release notes for 'ContentHandler::runLegacyHooks' removal
[mediawiki.git] / resources / src / mediawiki.special / mediawiki.special.apisandbox.js
blobfdb54cbc4a6d064028991ef782c4a5e12a4612f7
1 /* eslint-disable no-use-before-define */
2 ( function ( $, mw, OO ) {
3         'use strict';
4         var ApiSandbox, Util, WidgetMethods, Validators,
5                 $content, panel, booklet, oldhash, windowManager, fullscreenButton,
6                 api = new mw.Api(),
7                 bookletPages = [],
8                 availableFormats = {},
9                 resultPage = null,
10                 suppressErrors = true,
11                 updatingBooklet = false,
12                 pages = {},
13                 moduleInfoCache = {},
14                 baseRequestParams;
16         WidgetMethods = {
17                 textInputWidget: {
18                         getApiValue: function () {
19                                 return this.getValue();
20                         },
21                         setApiValue: function ( v ) {
22                                 if ( v === undefined ) {
23                                         v = this.paramInfo[ 'default' ];
24                                 }
25                                 this.setValue( v );
26                         },
27                         apiCheckValid: function () {
28                                 var that = this;
29                                 return this.getValidity().then( function () {
30                                         return $.Deferred().resolve( true ).promise();
31                                 }, function () {
32                                         return $.Deferred().resolve( false ).promise();
33                                 } ).done( function ( ok ) {
34                                         ok = ok || suppressErrors;
35                                         that.setIcon( ok ? null : 'alert' );
36                                         that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
37                                 } );
38                         }
39                 },
41                 dateTimeInputWidget: {
42                         getValidity: function () {
43                                 if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) {
44                                         return $.Deferred().resolve().promise();
45                                 } else {
46                                         return $.Deferred().reject().promise();
47                                 }
48                         }
49                 },
51                 tokenWidget: {
52                         alertTokenError: function ( code, error ) {
53                                 windowManager.openWindow( 'errorAlert', {
54                                         title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
55                                         message: error,
56                                         actions: [
57                                                 {
58                                                         action: 'accept',
59                                                         label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
60                                                         flags: 'primary'
61                                                 }
62                                         ]
63                                 } );
64                         },
65                         fetchToken: function () {
66                                 this.pushPending();
67                                 return api.getToken( this.paramInfo.tokentype )
68                                         .done( this.setApiValue.bind( this ) )
69                                         .fail( this.alertTokenError.bind( this ) )
70                                         .always( this.popPending.bind( this ) );
71                         },
72                         setApiValue: function ( v ) {
73                                 WidgetMethods.textInputWidget.setApiValue.call( this, v );
74                                 if ( v === '123ABC' ) {
75                                         this.fetchToken();
76                                 }
77                         }
78                 },
80                 passwordWidget: {
81                         getApiValueForDisplay: function () {
82                                 return '';
83                         }
84                 },
86                 toggleSwitchWidget: {
87                         getApiValue: function () {
88                                 return this.getValue() ? 1 : undefined;
89                         },
90                         setApiValue: function ( v ) {
91                                 this.setValue( Util.apiBool( v ) );
92                         },
93                         apiCheckValid: function () {
94                                 return $.Deferred().resolve( true ).promise();
95                         }
96                 },
98                 dropdownWidget: {
99                         getApiValue: function () {
100                                 var item = this.getMenu().getSelectedItem();
101                                 return item === null ? undefined : item.getData();
102                         },
103                         setApiValue: function ( v ) {
104                                 var menu = this.getMenu();
106                                 if ( v === undefined ) {
107                                         v = this.paramInfo[ 'default' ];
108                                 }
109                                 if ( v === undefined ) {
110                                         menu.selectItem();
111                                 } else {
112                                         menu.selectItemByData( String( v ) );
113                                 }
114                         },
115                         apiCheckValid: function () {
116                                 var ok = this.getApiValue() !== undefined || suppressErrors;
117                                 this.setIcon( ok ? null : 'alert' );
118                                 this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
119                                 return $.Deferred().resolve( ok ).promise();
120                         }
121                 },
123                 capsuleWidget: {
124                         getApiValue: function () {
125                                 var items = this.getItemsData();
126                                 if ( items.join( '' ).indexOf( '|' ) === -1 ) {
127                                         return items.join( '|' );
128                                 } else {
129                                         return '\x1f' + items.join( '\x1f' );
130                                 }
131                         },
132                         setApiValue: function ( v ) {
133                                 if ( v === undefined || v === '' || v === '\x1f' ) {
134                                         this.setItemsFromData( [] );
135                                 } else {
136                                         v = String( v );
137                                         if ( v.indexOf( '\x1f' ) !== 0 ) {
138                                                 this.setItemsFromData( v.split( '|' ) );
139                                         } else {
140                                                 this.setItemsFromData( v.substr( 1 ).split( '\x1f' ) );
141                                         }
142                                 }
143                         },
144                         apiCheckValid: function () {
145                                 var ok = true,
146                                         pi = this.paramInfo;
148                                 if ( !suppressErrors ) {
149                                         ok = this.getApiValue() !== undefined && !(
150                                                 pi.allspecifier !== undefined &&
151                                                 this.getItemsData().length > 1 &&
152                                                 this.getItemsData().indexOf( pi.allspecifier ) !== -1
153                                         );
154                                 }
156                                 this.setIcon( ok ? null : 'alert' );
157                                 this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
158                                 return $.Deferred().resolve( ok ).promise();
159                         }
160                 },
162                 optionalWidget: {
163                         getApiValue: function () {
164                                 return this.isDisabled() ? undefined : this.widget.getApiValue();
165                         },
166                         setApiValue: function ( v ) {
167                                 this.setDisabled( v === undefined );
168                                 this.widget.setApiValue( v );
169                         },
170                         apiCheckValid: function () {
171                                 if ( this.isDisabled() ) {
172                                         return $.Deferred().resolve( true ).promise();
173                                 } else {
174                                         return this.widget.apiCheckValid();
175                                 }
176                         }
177                 },
179                 submoduleWidget: {
180                         single: function () {
181                                 var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
182                                 return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
183                         },
184                         multi: function () {
185                                 var map = this.paramInfo.submodules,
186                                         v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
187                                 return v === undefined || v === '' ? [] : $.map( String( v ).split( '|' ), function ( v ) {
188                                         return { value: v, path: map[ v ] };
189                                 } );
190                         }
191                 },
193                 uploadWidget: {
194                         getApiValueForDisplay: function () {
195                                 return '...';
196                         },
197                         getApiValue: function () {
198                                 return this.getValue();
199                         },
200                         setApiValue: function () {
201                                 // Can't, sorry.
202                         },
203                         apiCheckValid: function () {
204                                 var ok = this.getValue() !== null || suppressErrors;
205                                 this.setIcon( ok ? null : 'alert' );
206                                 this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
207                                 return $.Deferred().resolve( ok ).promise();
208                         }
209                 }
210         };
212         Validators = {
213                 generic: function () {
214                         return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
215                 }
216         };
218         /**
219          * @class mw.special.ApiSandbox.Util
220          * @private
221          */
222         Util = {
223                 /**
224                  * Fetch API module info
225                  *
226                  * @param {string} module Module to fetch data for
227                  * @return {jQuery.Promise}
228                  */
229                 fetchModuleInfo: function ( module ) {
230                         var apiPromise,
231                                 deferred = $.Deferred();
233                         if ( moduleInfoCache.hasOwnProperty( module ) ) {
234                                 return deferred
235                                         .resolve( moduleInfoCache[ module ] )
236                                         .promise( { abort: function () {} } );
237                         } else {
238                                 apiPromise = api.post( {
239                                         action: 'paraminfo',
240                                         modules: module,
241                                         helpformat: 'html',
242                                         uselang: mw.config.get( 'wgUserLanguage' )
243                                 } ).done( function ( data ) {
244                                         var info;
246                                         if ( data.warnings && data.warnings.paraminfo ) {
247                                                 deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
248                                                 return;
249                                         }
251                                         info = data.paraminfo.modules;
252                                         if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
253                                                 deferred.reject( '???', 'No module data returned' );
254                                                 return;
255                                         }
257                                         moduleInfoCache[ module ] = info[ 0 ];
258                                         deferred.resolve( info[ 0 ] );
259                                 } ).fail( function ( code, details ) {
260                                         if ( code === 'http' ) {
261                                                 details = 'HTTP error: ' + details.exception;
262                                         } else if ( details.error ) {
263                                                 details = details.error.info;
264                                         }
265                                         deferred.reject( code, details );
266                                 } );
267                                 return deferred
268                                         .promise( { abort: apiPromise.abort } );
269                         }
270                 },
272                 /**
273                  * Mark all currently-in-use tokens as bad
274                  */
275                 markTokensBad: function () {
276                         var page, subpages, i,
277                                 checkPages = [ pages.main ];
279                         while ( checkPages.length ) {
280                                 page = checkPages.shift();
282                                 if ( page.tokenWidget ) {
283                                         api.badToken( page.tokenWidget.paramInfo.tokentype );
284                                 }
286                                 subpages = page.getSubpages();
287                                 for ( i = 0; i < subpages.length; i++ ) {
288                                         if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
289                                                 checkPages.push( pages[ subpages[ i ].key ] );
290                                         }
291                                 }
292                         }
293                 },
295                 /**
296                  * Test an API boolean
297                  *
298                  * @param {Mixed} value
299                  * @return {boolean}
300                  */
301                 apiBool: function ( value ) {
302                         return value !== undefined && value !== false;
303                 },
305                 /**
306                  * Create a widget for a parameter.
307                  *
308                  * @param {Object} pi Parameter info from API
309                  * @param {Object} opts Additional options
310                  * @return {OO.ui.Widget}
311                  */
312                 createWidgetForParameter: function ( pi, opts ) {
313                         var widget, innerWidget, finalWidget, items, $button, $content, func,
314                                 multiMode = 'none';
316                         opts = opts || {};
318                         switch ( pi.type ) {
319                                 case 'boolean':
320                                         widget = new OO.ui.ToggleSwitchWidget();
321                                         widget.paramInfo = pi;
322                                         $.extend( widget, WidgetMethods.toggleSwitchWidget );
323                                         pi.required = true; // Avoid wrapping in the non-required widget
324                                         break;
326                                 case 'string':
327                                 case 'user':
328                                         if ( pi.tokentype ) {
329                                                 widget = new TextInputWithIndicatorWidget( {
330                                                         input: {
331                                                                 indicator: 'previous',
332                                                                 indicatorTitle: mw.message( 'apisandbox-fetch-token' ).text(),
333                                                                 required: Util.apiBool( pi.required )
334                                                         }
335                                                 } );
336                                         } else if ( Util.apiBool( pi.multi ) ) {
337                                                 widget = new OO.ui.CapsuleMultiselectWidget( {
338                                                         allowArbitrary: true,
339                                                         allowDuplicates: Util.apiBool( pi.allowsduplicates )
340                                                 } );
341                                                 widget.paramInfo = pi;
342                                                 $.extend( widget, WidgetMethods.capsuleWidget );
343                                         } else {
344                                                 widget = new OO.ui.TextInputWidget( {
345                                                         required: Util.apiBool( pi.required )
346                                                 } );
347                                         }
348                                         if ( !Util.apiBool( pi.multi ) ) {
349                                                 widget.paramInfo = pi;
350                                                 $.extend( widget, WidgetMethods.textInputWidget );
351                                                 widget.setValidation( Validators.generic );
352                                         }
353                                         if ( pi.tokentype ) {
354                                                 $.extend( widget, WidgetMethods.tokenWidget );
355                                                 widget.input.paramInfo = pi;
356                                                 $.extend( widget.input, WidgetMethods.textInputWidget );
357                                                 $.extend( widget.input, WidgetMethods.tokenWidget );
358                                                 widget.on( 'indicator', widget.fetchToken, [], widget );
359                                         }
360                                         break;
362                                 case 'text':
363                                         widget = new OO.ui.TextInputWidget( {
364                                                 multiline: true,
365                                                 required: Util.apiBool( pi.required )
366                                         } );
367                                         widget.paramInfo = pi;
368                                         $.extend( widget, WidgetMethods.textInputWidget );
369                                         widget.setValidation( Validators.generic );
370                                         break;
372                                 case 'password':
373                                         widget = new OO.ui.TextInputWidget( {
374                                                 type: 'password',
375                                                 required: Util.apiBool( pi.required )
376                                         } );
377                                         widget.paramInfo = pi;
378                                         $.extend( widget, WidgetMethods.textInputWidget );
379                                         $.extend( widget, WidgetMethods.passwordWidget );
380                                         widget.setValidation( Validators.generic );
381                                         multiMode = 'enter';
382                                         break;
384                                 case 'integer':
385                                         widget = new OO.ui.NumberInputWidget( {
386                                                 required: Util.apiBool( pi.required ),
387                                                 isInteger: true
388                                         } );
389                                         widget.setIcon = widget.input.setIcon.bind( widget.input );
390                                         widget.setIconTitle = widget.input.setIconTitle.bind( widget.input );
391                                         widget.getValidity = widget.input.getValidity.bind( widget.input );
392                                         widget.paramInfo = pi;
393                                         $.extend( widget, WidgetMethods.textInputWidget );
394                                         if ( Util.apiBool( pi.enforcerange ) ) {
395                                                 widget.setRange( pi.min || -Infinity, pi.max || Infinity );
396                                         }
397                                         multiMode = 'enter';
398                                         break;
400                                 case 'limit':
401                                         widget = new OO.ui.TextInputWidget( {
402                                                 required: Util.apiBool( pi.required )
403                                         } );
404                                         widget.setValidation( function ( value ) {
405                                                 var n, pi = this.paramInfo;
407                                                 if ( value === 'max' ) {
408                                                         return true;
409                                                 } else {
410                                                         n = +value;
411                                                         return !isNaN( n ) && isFinite( n ) &&
412                                                                 Math.floor( n ) === n &&
413                                                                 n >= pi.min && n <= pi.apiSandboxMax;
414                                                 }
415                                         } );
416                                         pi.min = pi.min || 0;
417                                         pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max;
418                                         widget.paramInfo = pi;
419                                         $.extend( widget, WidgetMethods.textInputWidget );
420                                         multiMode = 'enter';
421                                         break;
423                                 case 'timestamp':
424                                         widget = new mw.widgets.datetime.DateTimeInputWidget( {
425                                                 formatter: {
426                                                         format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}'
427                                                 },
428                                                 required: Util.apiBool( pi.required ),
429                                                 clearable: false
430                                         } );
431                                         widget.paramInfo = pi;
432                                         $.extend( widget, WidgetMethods.textInputWidget );
433                                         $.extend( widget, WidgetMethods.dateTimeInputWidget );
434                                         multiMode = 'indicator';
435                                         break;
437                                 case 'upload':
438                                         widget = new OO.ui.SelectFileWidget();
439                                         widget.paramInfo = pi;
440                                         $.extend( widget, WidgetMethods.uploadWidget );
441                                         break;
443                                 case 'namespace':
444                                         items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) {
445                                                 if ( ns === '0' ) {
446                                                         name = mw.message( 'blanknamespace' ).text();
447                                                 }
448                                                 return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
449                                         } ).sort( function ( a, b ) {
450                                                 return a.data - b.data;
451                                         } );
452                                         if ( Util.apiBool( pi.multi ) ) {
453                                                 if ( pi.allspecifier !== undefined ) {
454                                                         items.unshift( new OO.ui.MenuOptionWidget( {
455                                                                 data: pi.allspecifier,
456                                                                 label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text()
457                                                         } ) );
458                                                 }
460                                                 widget = new OO.ui.CapsuleMultiselectWidget( {
461                                                         menu: { items: items }
462                                                 } );
463                                                 widget.paramInfo = pi;
464                                                 $.extend( widget, WidgetMethods.capsuleWidget );
465                                         } else {
466                                                 widget = new OO.ui.DropdownWidget( {
467                                                         menu: { items: items }
468                                                 } );
469                                                 widget.paramInfo = pi;
470                                                 $.extend( widget, WidgetMethods.dropdownWidget );
471                                         }
472                                         break;
474                                 default:
475                                         if ( !$.isArray( pi.type ) ) {
476                                                 throw new Error( 'Unknown parameter type ' + pi.type );
477                                         }
479                                         items = $.map( pi.type, function ( v ) {
480                                                 return new OO.ui.MenuOptionWidget( { data: String( v ), label: String( v ) } );
481                                         } );
482                                         if ( Util.apiBool( pi.multi ) ) {
483                                                 if ( pi.allspecifier !== undefined ) {
484                                                         items.unshift( new OO.ui.MenuOptionWidget( {
485                                                                 data: pi.allspecifier,
486                                                                 label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text()
487                                                         } ) );
488                                                 }
490                                                 widget = new OO.ui.CapsuleMultiselectWidget( {
491                                                         menu: { items: items }
492                                                 } );
493                                                 widget.paramInfo = pi;
494                                                 $.extend( widget, WidgetMethods.capsuleWidget );
495                                                 if ( Util.apiBool( pi.submodules ) ) {
496                                                         widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
497                                                         widget.on( 'change', ApiSandbox.updateUI );
498                                                 }
499                                         } else {
500                                                 widget = new OO.ui.DropdownWidget( {
501                                                         menu: { items: items }
502                                                 } );
503                                                 widget.paramInfo = pi;
504                                                 $.extend( widget, WidgetMethods.dropdownWidget );
505                                                 if ( Util.apiBool( pi.submodules ) ) {
506                                                         widget.getSubmodules = WidgetMethods.submoduleWidget.single;
507                                                         widget.getMenu().on( 'choose', ApiSandbox.updateUI );
508                                                 }
509                                         }
511                                         break;
512                         }
514                         if ( Util.apiBool( pi.multi ) && multiMode !== 'none' ) {
515                                 innerWidget = widget;
516                                 switch ( multiMode ) {
517                                         case 'enter':
518                                                 $content = innerWidget.$element;
519                                                 break;
521                                         case 'indicator':
522                                                 $button = innerWidget.$indicator;
523                                                 $button.css( 'cursor', 'pointer' );
524                                                 $button.attr( 'tabindex', 0 );
525                                                 $button.parent().append( $button );
526                                                 innerWidget.setIndicator( 'next' );
527                                                 $content = innerWidget.$element;
528                                                 break;
530                                         default:
531                                                 throw new Error( 'Unknown multiMode "' + multiMode + '"' );
532                                 }
534                                 widget = new OO.ui.CapsuleMultiselectWidget( {
535                                         allowArbitrary: true,
536                                         allowDuplicates: Util.apiBool( pi.allowsduplicates ),
537                                         popup: {
538                                                 classes: [ 'mw-apisandbox-popup' ],
539                                                 $content: $content
540                                         }
541                                 } );
542                                 widget.paramInfo = pi;
543                                 $.extend( widget, WidgetMethods.capsuleWidget );
545                                 func = function () {
546                                         if ( !innerWidget.isDisabled() ) {
547                                                 innerWidget.apiCheckValid().done( function ( ok ) {
548                                                         if ( ok ) {
549                                                                 widget.addItemsFromData( [ innerWidget.getApiValue() ] );
550                                                                 innerWidget.setApiValue( undefined );
551                                                         }
552                                                 } );
553                                                 return false;
554                                         }
555                                 };
556                                 switch ( multiMode ) {
557                                         case 'enter':
558                                                 innerWidget.connect( null, { enter: func } );
559                                                 break;
561                                         case 'indicator':
562                                                 $button.on( {
563                                                         click: func,
564                                                         keypress: function ( e ) {
565                                                                 if ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) {
566                                                                         func();
567                                                                 }
568                                                         }
569                                                 } );
570                                                 break;
571                                 }
572                         }
574                         if ( Util.apiBool( pi.required ) || opts.nooptional ) {
575                                 finalWidget = widget;
576                         } else {
577                                 finalWidget = new OptionalWidget( widget );
578                                 finalWidget.paramInfo = pi;
579                                 $.extend( finalWidget, WidgetMethods.optionalWidget );
580                                 if ( widget.getSubmodules ) {
581                                         finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
582                                         finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } );
583                                 }
584                                 finalWidget.setDisabled( true );
585                         }
587                         widget.setApiValue( pi[ 'default' ] );
589                         return finalWidget;
590                 },
592                 /**
593                  * Parse an HTML string and call Util.fixupHTML()
594                  *
595                  * @param {string} html HTML to parse
596                  * @return {jQuery}
597                  */
598                 parseHTML: function ( html ) {
599                         var $ret = $( $.parseHTML( html ) );
600                         return Util.fixupHTML( $ret );
601                 },
603                 /**
604                  * Parse an i18n message and call Util.fixupHTML()
605                  *
606                  * @param {string} key Key of message to get
607                  * @param {...Mixed} parameters Values for $N replacements
608                  * @return {jQuery}
609                  */
610                 parseMsg: function () {
611                         var $ret = mw.message.apply( mw.message, arguments ).parseDom();
612                         return Util.fixupHTML( $ret );
613                 },
615                 /**
616                  * Fix HTML for ApiSandbox display
617                  *
618                  * Fixes are:
619                  * - Add target="_blank" to any links
620                  *
621                  * @param {jQuery} $html DOM to process
622                  * @return {jQuery}
623                  */
624                 fixupHTML: function ( $html ) {
625                         $html.filter( 'a' ).add( $html.find( 'a' ) )
626                                 .filter( '[href]:not([target])' )
627                                 .attr( 'target', '_blank' );
628                         return $html;
629                 }
630         };
632         /**
633         * Interface to ApiSandbox UI
634         *
635         * @class mw.special.ApiSandbox
636         */
637         ApiSandbox = {
638                 /**
639                  * Initialize the UI
640                  *
641                  * Automatically called on $.ready()
642                  */
643                 init: function () {
644                         var $toolbar;
646                         ApiSandbox.isFullscreen = false;
648                         $content = $( '#mw-apisandbox' );
650                         windowManager = new OO.ui.WindowManager();
651                         $( 'body' ).append( windowManager.$element );
652                         windowManager.addWindows( {
653                                 errorAlert: new OO.ui.MessageDialog()
654                         } );
656                         fullscreenButton = new OO.ui.ButtonWidget( {
657                                 label: mw.message( 'apisandbox-fullscreen' ).text(),
658                                 title: mw.message( 'apisandbox-fullscreen-tooltip' ).text()
659                         } ).on( 'click', ApiSandbox.toggleFullscreen );
661                         $toolbar = $( '<div>' )
662                                 .addClass( 'mw-apisandbox-toolbar' )
663                                 .append(
664                                         fullscreenButton.$element,
665                                         new OO.ui.ButtonWidget( {
666                                                 label: mw.message( 'apisandbox-submit' ).text(),
667                                                 flags: [ 'primary', 'progressive' ]
668                                         } ).on( 'click', ApiSandbox.sendRequest ).$element,
669                                         new OO.ui.ButtonWidget( {
670                                                 label: mw.message( 'apisandbox-reset' ).text(),
671                                                 flags: 'destructive'
672                                         } ).on( 'click', ApiSandbox.resetUI ).$element
673                                 );
675                         booklet = new OO.ui.BookletLayout( {
676                                 outlined: true,
677                                 autoFocus: false
678                         } );
680                         panel = new OO.ui.PanelLayout( {
681                                 classes: [ 'mw-apisandbox-container' ],
682                                 content: [ booklet ],
683                                 expanded: false,
684                                 framed: true
685                         } );
687                         pages.main = new ApiSandbox.PageLayout( { key: 'main', path: 'main' } );
689                         // Parse the current hash string
690                         if ( !ApiSandbox.loadFromHash() ) {
691                                 ApiSandbox.updateUI();
692                         }
694                         // If the hashchange event exists, use it. Otherwise, fake it.
695                         // And, of course, IE has to be dumb.
696                         if ( 'onhashchange' in window &&
697                                 ( document.documentMode === undefined || document.documentMode >= 8 )
698                         ) {
699                                 $( window ).on( 'hashchange', ApiSandbox.loadFromHash );
700                         } else {
701                                 setInterval( function () {
702                                         if ( oldhash !== location.hash ) {
703                                                 ApiSandbox.loadFromHash();
704                                         }
705                                 }, 1000 );
706                         }
708                         $content
709                                 .empty()
710                                 .append( $( '<p>' ).append( Util.parseMsg( 'apisandbox-intro' ) ) )
711                                 .append(
712                                         $( '<div>', { id: 'mw-apisandbox-ui' } )
713                                                 .append( $toolbar )
714                                                 .append( panel.$element )
715                                 );
717                         $( window ).on( 'resize', ApiSandbox.resizePanel );
719                         ApiSandbox.resizePanel();
720                 },
722                 /**
723                  * Toggle "fullscreen" mode
724                  */
725                 toggleFullscreen: function () {
726                         var $body = $( document.body ),
727                                 $ui = $( '#mw-apisandbox-ui' );
729                         ApiSandbox.isFullscreen = !ApiSandbox.isFullscreen;
731                         $body.toggleClass( 'mw-apisandbox-fullscreen', ApiSandbox.isFullscreen );
732                         $ui.toggleClass( 'mw-body-content', ApiSandbox.isFullscreen );
733                         if ( ApiSandbox.isFullscreen ) {
734                                 fullscreenButton.setLabel( mw.message( 'apisandbox-unfullscreen' ).text() );
735                                 fullscreenButton.setTitle( mw.message( 'apisandbox-unfullscreen-tooltip' ).text() );
736                                 $body.append( $ui );
737                         } else {
738                                 fullscreenButton.setLabel( mw.message( 'apisandbox-fullscreen' ).text() );
739                                 fullscreenButton.setTitle( mw.message( 'apisandbox-fullscreen-tooltip' ).text() );
740                                 $content.append( $ui );
741                         }
742                         ApiSandbox.resizePanel();
743                 },
745                 /**
746                  * Set the height of the panel based on the current viewport.
747                  */
748                 resizePanel: function () {
749                         var height = $( window ).height(),
750                                 contentTop = $content.offset().top;
752                         if ( ApiSandbox.isFullscreen ) {
753                                 height -= panel.$element.offset().top - $( '#mw-apisandbox-ui' ).offset().top;
754                                 panel.$element.height( height - 1 );
755                         } else {
756                                 // Subtract the height of the intro text
757                                 height -= panel.$element.offset().top - contentTop;
759                                 panel.$element.height( height - 10 );
760                                 $( window ).scrollTop( contentTop - 5 );
761                         }
762                 },
764                 /**
765                  * Update the current query when the page hash changes
766                  *
767                  * @return {boolean} Successful
768                  */
769                 loadFromHash: function () {
770                         var params, m, re,
771                                 hash = location.hash;
773                         if ( oldhash === hash ) {
774                                 return false;
775                         }
776                         oldhash = hash;
777                         if ( hash === '' ) {
778                                 return false;
779                         }
781                         // I'm surprised this doesn't seem to exist in jQuery or mw.util.
782                         params = {};
783                         hash = hash.replace( /\+/g, '%20' );
784                         re = /([^&=#]+)=?([^&#]*)/g;
785                         while ( ( m = re.exec( hash ) ) ) {
786                                 params[ decodeURIComponent( m[ 1 ] ) ] = decodeURIComponent( m[ 2 ] );
787                         }
789                         ApiSandbox.updateUI( params );
790                         return true;
791                 },
793                 /**
794                  * Update the pages in the booklet
795                  *
796                  * @param {Object} [params] Optional query parameters to load
797                  */
798                 updateUI: function ( params ) {
799                         var i, page, subpages, j, removePages,
800                                 addPages = [];
802                         if ( !$.isPlainObject( params ) ) {
803                                 params = undefined;
804                         }
806                         if ( updatingBooklet ) {
807                                 return;
808                         }
809                         updatingBooklet = true;
810                         try {
811                                 if ( params !== undefined ) {
812                                         pages.main.loadQueryParams( params );
813                                 }
814                                 addPages.push( pages.main );
815                                 if ( resultPage !== null ) {
816                                         addPages.push( resultPage );
817                                 }
818                                 pages.main.apiCheckValid();
820                                 i = 0;
821                                 while ( addPages.length ) {
822                                         page = addPages.shift();
823                                         if ( bookletPages[ i ] !== page ) {
824                                                 for ( j = i; j < bookletPages.length; j++ ) {
825                                                         if ( bookletPages[ j ].getName() === page.getName() ) {
826                                                                 bookletPages.splice( j, 1 );
827                                                         }
828                                                 }
829                                                 bookletPages.splice( i, 0, page );
830                                                 booklet.addPages( [ page ], i );
831                                         }
832                                         i++;
834                                         if ( page.getSubpages ) {
835                                                 subpages = page.getSubpages();
836                                                 for ( j = 0; j < subpages.length; j++ ) {
837                                                         if ( !pages.hasOwnProperty( subpages[ j ].key ) ) {
838                                                                 subpages[ j ].indentLevel = page.indentLevel + 1;
839                                                                 pages[ subpages[ j ].key ] = new ApiSandbox.PageLayout( subpages[ j ] );
840                                                         }
841                                                         if ( params !== undefined ) {
842                                                                 pages[ subpages[ j ].key ].loadQueryParams( params );
843                                                         }
844                                                         addPages.splice( j, 0, pages[ subpages[ j ].key ] );
845                                                         pages[ subpages[ j ].key ].apiCheckValid();
846                                                 }
847                                         }
848                                 }
850                                 if ( bookletPages.length > i ) {
851                                         removePages = bookletPages.splice( i, bookletPages.length - i );
852                                         booklet.removePages( removePages );
853                                 }
855                                 if ( !booklet.getCurrentPageName() ) {
856                                         booklet.selectFirstSelectablePage();
857                                 }
858                         } finally {
859                                 updatingBooklet = false;
860                         }
861                 },
863                 /**
864                  * Reset button handler
865                  */
866                 resetUI: function () {
867                         suppressErrors = true;
868                         pages = {
869                                 main: new ApiSandbox.PageLayout( { key: 'main', path: 'main' } )
870                         };
871                         resultPage = null;
872                         ApiSandbox.updateUI();
873                 },
875                 /**
876                  * Submit button handler
877                  *
878                  * @param {Object} [params] Use this set of params instead of those in the form fields.
879                  *   The form fields will be updated to match.
880                  */
881                 sendRequest: function ( params ) {
882                         var page, subpages, i, query, $result, $focus,
883                                 progress, $progressText, progressLoading,
884                                 deferreds = [],
885                                 paramsAreForced = !!params,
886                                 displayParams = {},
887                                 checkPages = [ pages.main ];
889                         // Blur any focused widget before submit, because
890                         // OO.ui.ButtonWidget doesn't take focus itself (T128054)
891                         $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement );
892                         if ( $focus.length ) {
893                                 $focus[ 0 ].blur();
894                         }
896                         suppressErrors = false;
898                         // save widget state in params (or load from it if we are forced)
899                         if ( paramsAreForced ) {
900                                 ApiSandbox.updateUI( params );
901                         }
902                         params = {};
903                         while ( checkPages.length ) {
904                                 page = checkPages.shift();
905                                 deferreds.push( page.apiCheckValid() );
906                                 page.getQueryParams( params, displayParams );
907                                 subpages = page.getSubpages();
908                                 for ( i = 0; i < subpages.length; i++ ) {
909                                         if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
910                                                 checkPages.push( pages[ subpages[ i ].key ] );
911                                         }
912                                 }
913                         }
915                         if ( !paramsAreForced ) {
916                                 // forced params means we are continuing a query; the base query should be preserved
917                                 baseRequestParams = $.extend( {}, params );
918                         }
920                         $.when.apply( $, deferreds ).done( function () {
921                                 var jsonInput;
923                                 if ( $.inArray( false, arguments ) !== -1 ) {
924                                         windowManager.openWindow( 'errorAlert', {
925                                                 title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ),
926                                                 message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ),
927                                                 actions: [
928                                                         {
929                                                                 action: 'accept',
930                                                                 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
931                                                                 flags: 'primary'
932                                                         }
933                                                 ]
934                                         } );
935                                         return;
936                                 }
938                                 query = $.param( displayParams );
940                                 // Force a 'fm' format with wrappedhtml=1, if available
941                                 if ( params.format !== undefined ) {
942                                         if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) {
943                                                 params.format = params.format + 'fm';
944                                         }
945                                         if ( params.format.substr( -2 ) === 'fm' ) {
946                                                 params.wrappedhtml = 1;
947                                         }
948                                 }
950                                 progressLoading = false;
951                                 $progressText = $( '<span>' ).text( mw.message( 'apisandbox-sending-request' ).text() );
952                                 progress = new OO.ui.ProgressBarWidget( {
953                                         progress: false,
954                                         $content: $progressText
955                                 } );
957                                 $result = $( '<div>' )
958                                         .append( progress.$element );
960                                 resultPage = page = new OO.ui.PageLayout( '|results|' );
961                                 page.setupOutlineItem = function () {
962                                         this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() );
963                                 };
964                                 page.$element.empty()
965                                         .append(
966                                                 new OO.ui.FieldLayout(
967                                                         new OO.ui.TextInputWidget( {
968                                                                 readOnly: true,
969                                                                 value: mw.util.wikiScript( 'api' ) + '?' + query
970                                                         } ), {
971                                                                 label: Util.parseMsg( 'apisandbox-request-url-label' )
972                                                         }
973                                                 ).$element,
974                                                 new OO.ui.FieldLayout(
975                                                         jsonInput = new OO.ui.TextInputWidget( {
976                                                                 classes: [ 'mw-apisandbox-textInputCode' ],
977                                                                 readOnly: true,
978                                                                 multiline: true,
979                                                                 autosize: true,
980                                                                 maxRows: 6,
981                                                                 value: JSON.stringify( displayParams, null, '\t' )
982                                                         } ), {
983                                                                 label: Util.parseMsg( 'apisandbox-request-params-json' )
984                                                         }
985                                                 ).$element,
986                                                 $result
987                                         );
988                                 ApiSandbox.updateUI();
989                                 booklet.setPage( '|results|' );
991                                 // Resize the multiline input once visible
992                                 jsonInput.adjustSize();
994                                 location.href = oldhash = '#' + query;
996                                 api.post( params, {
997                                         contentType: 'multipart/form-data',
998                                         dataType: 'text',
999                                         xhr: function () {
1000                                                 var xhr = new window.XMLHttpRequest();
1001                                                 xhr.upload.addEventListener( 'progress', function ( e ) {
1002                                                         if ( !progressLoading ) {
1003                                                                 if ( e.lengthComputable ) {
1004                                                                         progress.setProgress( e.loaded * 100 / e.total );
1005                                                                 } else {
1006                                                                         progress.setProgress( false );
1007                                                                 }
1008                                                         }
1009                                                 } );
1010                                                 xhr.addEventListener( 'progress', function ( e ) {
1011                                                         if ( !progressLoading ) {
1012                                                                 progressLoading = true;
1013                                                                 $progressText.text( mw.message( 'apisandbox-loading-results' ).text() );
1014                                                         }
1015                                                         if ( e.lengthComputable ) {
1016                                                                 progress.setProgress( e.loaded * 100 / e.total );
1017                                                         } else {
1018                                                                 progress.setProgress( false );
1019                                                         }
1020                                                 } );
1021                                                 return xhr;
1022                                         }
1023                                 } )
1024                                         .then( null, function ( code, data, result, jqXHR ) {
1025                                                 if ( code !== 'http' ) {
1026                                                         // Not really an error, work around mw.Api thinking it is.
1027                                                         return $.Deferred()
1028                                                                 .resolve( result, jqXHR )
1029                                                                 .promise();
1030                                                 }
1031                                                 return this;
1032                                         } )
1033                                         .fail( function ( code, data ) {
1034                                                 var details = 'HTTP error: ' + data.exception;
1035                                                 $result.empty()
1036                                                         .append(
1037                                                                 new OO.ui.LabelWidget( {
1038                                                                         label: mw.message( 'apisandbox-results-error', details ).text(),
1039                                                                         classes: [ 'error' ]
1040                                                                 } ).$element
1041                                                         );
1042                                         } )
1043                                         .done( function ( data, jqXHR ) {
1044                                                 var m, loadTime, button, clear,
1045                                                         ct = jqXHR.getResponseHeader( 'Content-Type' );
1047                                                 $result.empty();
1048                                                 if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
1049                                                         data = JSON.parse( data );
1050                                                         if ( data.modules.length ) {
1051                                                                 mw.loader.load( data.modules );
1052                                                         }
1053                                                         if ( data.status && data.status !== 200 ) {
1054                                                                 $( '<div>' )
1055                                                                         .addClass( 'api-pretty-header api-pretty-status' )
1056                                                                         .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) )
1057                                                                         .appendTo( $result );
1058                                                         }
1059                                                         $result.append( Util.parseHTML( data.html ) );
1060                                                         loadTime = data.time;
1061                                                 } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
1062                                                         $result.append( Util.parseHTML( m[ 0 ] ) );
1063                                                         if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
1064                                                                 loadTime = parseInt( m[ 1 ], 10 );
1065                                                         }
1066                                                 } else {
1067                                                         $( '<pre>' )
1068                                                                 .addClass( 'api-pretty-content' )
1069                                                                 .text( data )
1070                                                                 .appendTo( $result );
1071                                                 }
1072                                                 if ( paramsAreForced || data[ 'continue' ] ) {
1073                                                         $result.append(
1074                                                                 $( '<div>' ).append(
1075                                                                         new OO.ui.ButtonWidget( {
1076                                                                                 label: mw.message( 'apisandbox-continue' ).text()
1077                                                                         } ).on( 'click', function () {
1078                                                                                 ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
1079                                                                         } ).setDisabled( !data[ 'continue' ] ).$element,
1080                                                                         ( clear = new OO.ui.ButtonWidget( {
1081                                                                                 label: mw.message( 'apisandbox-continue-clear' ).text()
1082                                                                         } ).on( 'click', function () {
1083                                                                                 ApiSandbox.updateUI( baseRequestParams );
1084                                                                                 clear.setDisabled( true );
1085                                                                                 booklet.setPage( '|results|' );
1086                                                                         } ).setDisabled( !paramsAreForced ) ).$element,
1087                                                                         new OO.ui.PopupButtonWidget( {
1088                                                                                 framed: false,
1089                                                                                 icon: 'info',
1090                                                                                 popup: {
1091                                                                                         $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
1092                                                                                         padded: true
1093                                                                                 }
1094                                                                         } ).$element
1095                                                                 )
1096                                                         );
1097                                                 }
1098                                                 if ( typeof loadTime === 'number' ) {
1099                                                         $result.append(
1100                                                                 $( '<div>' ).append(
1101                                                                         new OO.ui.LabelWidget( {
1102                                                                                 label: mw.message( 'apisandbox-request-time', loadTime ).text()
1103                                                                         } ).$element
1104                                                                 )
1105                                                         );
1106                                                 }
1108                                                 if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
1109                                                         // Flush all saved tokens in case one of them is the bad one.
1110                                                         Util.markTokensBad();
1111                                                         button = new OO.ui.ButtonWidget( {
1112                                                                 label: mw.message( 'apisandbox-results-fixtoken' ).text()
1113                                                         } );
1114                                                         button.on( 'click', ApiSandbox.fixTokenAndResend )
1115                                                                 .on( 'click', button.setDisabled, [ true ], button )
1116                                                                 .$element.appendTo( $result );
1117                                                 }
1118                                         } );
1119                         } );
1120                 },
1122                 /**
1123                  * Handler for the "Correct token and resubmit" button
1124                  *
1125                  * Used on a 'badtoken' error, it re-fetches token parameters for all
1126                  * pages and then re-submits the query.
1127                  */
1128                 fixTokenAndResend: function () {
1129                         var page, subpages, i, k,
1130                                 ok = true,
1131                                 tokenWait = { dummy: true },
1132                                 checkPages = [ pages.main ],
1133                                 success = function ( k ) {
1134                                         delete tokenWait[ k ];
1135                                         if ( ok && $.isEmptyObject( tokenWait ) ) {
1136                                                 ApiSandbox.sendRequest();
1137                                         }
1138                                 },
1139                                 failure = function ( k ) {
1140                                         delete tokenWait[ k ];
1141                                         ok = false;
1142                                 };
1144                         while ( checkPages.length ) {
1145                                 page = checkPages.shift();
1147                                 if ( page.tokenWidget ) {
1148                                         k = page.apiModule + page.tokenWidget.paramInfo.name;
1149                                         tokenWait[ k ] = page.tokenWidget.fetchToken()
1150                                                 .done( success.bind( page.tokenWidget, k ) )
1151                                                 .fail( failure.bind( page.tokenWidget, k ) );
1152                                 }
1154                                 subpages = page.getSubpages();
1155                                 for ( i = 0; i < subpages.length; i++ ) {
1156                                         if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
1157                                                 checkPages.push( pages[ subpages[ i ].key ] );
1158                                         }
1159                                 }
1160                         }
1162                         success( 'dummy', '' );
1163                 },
1165                 /**
1166                  * Reset validity indicators for all widgets
1167                  */
1168                 updateValidityIndicators: function () {
1169                         var page, subpages, i,
1170                                 checkPages = [ pages.main ];
1172                         while ( checkPages.length ) {
1173                                 page = checkPages.shift();
1174                                 page.apiCheckValid();
1175                                 subpages = page.getSubpages();
1176                                 for ( i = 0; i < subpages.length; i++ ) {
1177                                         if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
1178                                                 checkPages.push( pages[ subpages[ i ].key ] );
1179                                         }
1180                                 }
1181                         }
1182                 }
1183         };
1185         /**
1186          * PageLayout for API modules
1187          *
1188          * @class
1189          * @private
1190          * @extends OO.ui.PageLayout
1191          * @constructor
1192          * @param {Object} [config] Configuration options
1193          */
1194         ApiSandbox.PageLayout = function ( config ) {
1195                 config = $.extend( { prefix: '' }, config );
1196                 this.displayText = config.key;
1197                 this.apiModule = config.path;
1198                 this.prefix = config.prefix;
1199                 this.paramInfo = null;
1200                 this.apiIsValid = true;
1201                 this.loadFromQueryParams = null;
1202                 this.widgets = {};
1203                 this.tokenWidget = null;
1204                 this.indentLevel = config.indentLevel ? config.indentLevel : 0;
1205                 ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
1206                 this.loadParamInfo();
1207         };
1208         OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
1209         ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
1210                 this.outlineItem.setLevel( this.indentLevel );
1211                 this.outlineItem.setLabel( this.displayText );
1212                 this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
1213                 this.outlineItem.setIconTitle(
1214                         this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
1215                 );
1216         };
1218         /**
1219          * Fetch module information for this page's module, then create UI
1220          */
1221         ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
1222                 var dynamicFieldset, dynamicParamNameWidget,
1223                         that = this,
1224                         removeDynamicParamWidget = function ( name, layout ) {
1225                                 dynamicFieldset.removeItems( [ layout ] );
1226                                 delete that.widgets[ name ];
1227                         },
1228                         addDynamicParamWidget = function () {
1229                                 var name, layout, widget, button;
1231                                 // Check name is filled in
1232                                 name = dynamicParamNameWidget.getValue().trim();
1233                                 if ( name === '' ) {
1234                                         dynamicParamNameWidget.focus();
1235                                         return;
1236                                 }
1238                                 if ( that.widgets[ name ] !== undefined ) {
1239                                         windowManager.openWindow( 'errorAlert', {
1240                                                 title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
1241                                                 actions: [
1242                                                         {
1243                                                                 action: 'accept',
1244                                                                 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
1245                                                                 flags: 'primary'
1246                                                         }
1247                                                 ]
1248                                         } );
1249                                         return;
1250                                 }
1252                                 widget = Util.createWidgetForParameter( {
1253                                         name: name,
1254                                         type: 'string',
1255                                         'default': ''
1256                                 }, {
1257                                         nooptional: true
1258                                 } );
1259                                 button = new OO.ui.ButtonWidget( {
1260                                         icon: 'remove',
1261                                         flags: 'destructive'
1262                                 } );
1263                                 layout = new OO.ui.ActionFieldLayout(
1264                                         widget,
1265                                         button,
1266                                         {
1267                                                 label: name,
1268                                                 align: 'left'
1269                                         }
1270                                 );
1271                                 button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
1272                                 that.widgets[ name ] = widget;
1273                                 dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
1274                                 widget.focus();
1276                                 dynamicParamNameWidget.setValue( '' );
1277                         };
1279                 this.$element.empty()
1280                         .append( new OO.ui.ProgressBarWidget( {
1281                                 progress: false,
1282                                 text: mw.message( 'apisandbox-loading', this.displayText ).text()
1283                         } ).$element );
1285                 Util.fetchModuleInfo( this.apiModule )
1286                         .done( function ( pi ) {
1287                                 var prefix, i, j, dl, widget, $widgetLabel, widgetField, helpField, tmp, flag, count,
1288                                         items = [],
1289                                         deprecatedItems = [],
1290                                         buttons = [],
1291                                         filterFmModules = function ( v ) {
1292                                                 return v.substr( -2 ) !== 'fm' ||
1293                                                         !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
1294                                         },
1295                                         widgetLabelOnClick = function () {
1296                                                 var f = this.getField();
1297                                                 if ( $.isFunction( f.setDisabled ) ) {
1298                                                         f.setDisabled( false );
1299                                                 }
1300                                                 if ( $.isFunction( f.focus ) ) {
1301                                                         f.focus();
1302                                                 }
1303                                         },
1304                                         doNothing = function () {};
1306                                 // This is something of a hack. We always want the 'format' and
1307                                 // 'action' parameters from the main module to be specified,
1308                                 // and for 'format' we also want to simplify the dropdown since
1309                                 // we always send the 'fm' variant.
1310                                 if ( that.apiModule === 'main' ) {
1311                                         for ( i = 0; i < pi.parameters.length; i++ ) {
1312                                                 if ( pi.parameters[ i ].name === 'action' ) {
1313                                                         pi.parameters[ i ].required = true;
1314                                                         delete pi.parameters[ i ][ 'default' ];
1315                                                 }
1316                                                 if ( pi.parameters[ i ].name === 'format' ) {
1317                                                         tmp = pi.parameters[ i ].type;
1318                                                         for ( j = 0; j < tmp.length; j++ ) {
1319                                                                 availableFormats[ tmp[ j ] ] = true;
1320                                                         }
1321                                                         pi.parameters[ i ].type = $.grep( tmp, filterFmModules );
1322                                                         pi.parameters[ i ][ 'default' ] = 'json';
1323                                                         pi.parameters[ i ].required = true;
1324                                                 }
1325                                         }
1326                                 }
1328                                 // Hide the 'wrappedhtml' parameter on format modules
1329                                 if ( pi.group === 'format' ) {
1330                                         pi.parameters = $.grep( pi.parameters, function ( p ) {
1331                                                 return p.name !== 'wrappedhtml';
1332                                         } );
1333                                 }
1335                                 that.paramInfo = pi;
1337                                 items.push( new OO.ui.FieldLayout(
1338                                         new OO.ui.Widget( {} ).toggle( false ), {
1339                                                 align: 'top',
1340                                                 label: Util.parseHTML( pi.description )
1341                                         }
1342                                 ) );
1344                                 if ( pi.helpurls.length ) {
1345                                         buttons.push( new OO.ui.PopupButtonWidget( {
1346                                                 label: mw.message( 'apisandbox-helpurls' ).text(),
1347                                                 icon: 'help',
1348                                                 popup: {
1349                                                         $content: $( '<ul>' ).append( $.map( pi.helpurls, function ( link ) {
1350                                                                 return $( '<li>' ).append( $( '<a>', {
1351                                                                         href: link,
1352                                                                         target: '_blank',
1353                                                                         text: link
1354                                                                 } ) );
1355                                                         } ) )
1356                                                 }
1357                                         } ) );
1358                                 }
1360                                 if ( pi.examples.length ) {
1361                                         buttons.push( new OO.ui.PopupButtonWidget( {
1362                                                 label: mw.message( 'apisandbox-examples' ).text(),
1363                                                 icon: 'code',
1364                                                 popup: {
1365                                                         $content: $( '<ul>' ).append( $.map( pi.examples, function ( example ) {
1366                                                                 var a = $( '<a>', {
1367                                                                         href: '#' + example.query,
1368                                                                         html: example.description
1369                                                                 } );
1370                                                                 a.find( 'a' ).contents().unwrap(); // Can't nest links
1371                                                                 return $( '<li>' ).append( a );
1372                                                         } ) )
1373                                                 }
1374                                         } ) );
1375                                 }
1377                                 if ( buttons.length ) {
1378                                         items.push( new OO.ui.FieldLayout(
1379                                                 new OO.ui.ButtonGroupWidget( {
1380                                                         items: buttons
1381                                                 } ), { align: 'top' }
1382                                         ) );
1383                                 }
1385                                 if ( pi.parameters.length ) {
1386                                         prefix = that.prefix + pi.prefix;
1387                                         for ( i = 0; i < pi.parameters.length; i++ ) {
1388                                                 widget = Util.createWidgetForParameter( pi.parameters[ i ] );
1389                                                 that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
1390                                                 if ( pi.parameters[ i ].tokentype ) {
1391                                                         that.tokenWidget = widget;
1392                                                 }
1394                                                 dl = $( '<dl>' );
1395                                                 dl.append( $( '<dd>', {
1396                                                         addClass: 'description',
1397                                                         append: Util.parseHTML( pi.parameters[ i ].description )
1398                                                 } ) );
1399                                                 if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
1400                                                         for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
1401                                                                 dl.append( $( '<dd>', {
1402                                                                         addClass: 'info',
1403                                                                         append: Util.parseHTML( pi.parameters[ i ].info[ j ] )
1404                                                                 } ) );
1405                                                         }
1406                                                 }
1407                                                 flag = true;
1408                                                 count = 1e100;
1409                                                 switch ( pi.parameters[ i ].type ) {
1410                                                         case 'namespace':
1411                                                                 flag = false;
1412                                                                 count = mw.config.get( 'wgFormattedNamespaces' ).length;
1413                                                                 break;
1415                                                         case 'limit':
1416                                                                 if ( pi.parameters[ i ].highmax !== undefined ) {
1417                                                                         dl.append( $( '<dd>', {
1418                                                                                 addClass: 'info',
1419                                                                                 append: [
1420                                                                                         Util.parseMsg(
1421                                                                                                 'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
1422                                                                                         ),
1423                                                                                         ' ',
1424                                                                                         Util.parseMsg( 'apisandbox-param-limit' )
1425                                                                                 ]
1426                                                                         } ) );
1427                                                                 } else {
1428                                                                         dl.append( $( '<dd>', {
1429                                                                                 addClass: 'info',
1430                                                                                 append: [
1431                                                                                         Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
1432                                                                                         ' ',
1433                                                                                         Util.parseMsg( 'apisandbox-param-limit' )
1434                                                                                 ]
1435                                                                         } ) );
1436                                                                 }
1437                                                                 break;
1439                                                         case 'integer':
1440                                                                 tmp = '';
1441                                                                 if ( pi.parameters[ i ].min !== undefined ) {
1442                                                                         tmp += 'min';
1443                                                                 }
1444                                                                 if ( pi.parameters[ i ].max !== undefined ) {
1445                                                                         tmp += 'max';
1446                                                                 }
1447                                                                 if ( tmp !== '' ) {
1448                                                                         dl.append( $( '<dd>', {
1449                                                                                 addClass: 'info',
1450                                                                                 append: Util.parseMsg(
1451                                                                                         'api-help-param-integer-' + tmp,
1452                                                                                         Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
1453                                                                                         pi.parameters[ i ].min, pi.parameters[ i ].max
1454                                                                                 )
1455                                                                         } ) );
1456                                                                 }
1457                                                                 break;
1459                                                         default:
1460                                                                 if ( $.isArray( pi.parameters[ i ].type ) ) {
1461                                                                         flag = false;
1462                                                                         count = pi.parameters[ i ].type.length;
1463                                                                 }
1464                                                                 break;
1465                                                 }
1466                                                 if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
1467                                                         tmp = [];
1468                                                         if ( flag && !( widget instanceof OO.ui.CapsuleMultiselectWidget ) &&
1469                                                                 !(
1470                                                                         widget instanceof OptionalWidget &&
1471                                                                         widget.widget instanceof OO.ui.CapsuleMultiselectWidget
1472                                                                 )
1473                                                         ) {
1474                                                                 tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
1475                                                         }
1476                                                         if ( count > pi.parameters[ i ].lowlimit ) {
1477                                                                 tmp.push(
1478                                                                         mw.message( 'api-help-param-multi-max',
1479                                                                                 pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
1480                                                                         ).parse()
1481                                                                 );
1482                                                         }
1483                                                         if ( tmp.length ) {
1484                                                                 dl.append( $( '<dd>', {
1485                                                                         addClass: 'info',
1486                                                                         append: Util.parseHTML( tmp.join( ' ' ) )
1487                                                                 } ) );
1488                                                         }
1489                                                 }
1490                                                 helpField = new OO.ui.FieldLayout(
1491                                                         new OO.ui.Widget( {
1492                                                                 $content: '\xa0',
1493                                                                 classes: [ 'mw-apisandbox-spacer' ]
1494                                                         } ), {
1495                                                                 align: 'inline',
1496                                                                 classes: [ 'mw-apisandbox-help-field' ],
1497                                                                 label: dl
1498                                                         }
1499                                                 );
1501                                                 $widgetLabel = $( '<span>' );
1502                                                 widgetField = new OO.ui.FieldLayout(
1503                                                         widget,
1504                                                         {
1505                                                                 align: 'left',
1506                                                                 classes: [ 'mw-apisandbox-widget-field' ],
1507                                                                 label: prefix + pi.parameters[ i ].name,
1508                                                                 $label: $widgetLabel
1509                                                         }
1510                                                 );
1512                                                 // FieldLayout only does click for InputElement
1513                                                 // widgets. So supply our own click handler.
1514                                                 $widgetLabel.on( 'click', widgetLabelOnClick.bind( widgetField ) );
1516                                                 // Don't grey out the label when the field is disabled,
1517                                                 // it makes it too hard to read and our "disabled"
1518                                                 // isn't really disabled.
1519                                                 widgetField.onFieldDisable( false );
1520                                                 widgetField.onFieldDisable = doNothing;
1522                                                 if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
1523                                                         deprecatedItems.push( widgetField, helpField );
1524                                                 } else {
1525                                                         items.push( widgetField, helpField );
1526                                                 }
1527                                         }
1528                                 }
1530                                 if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
1531                                         items.push( new OO.ui.FieldLayout(
1532                                                 new OO.ui.Widget( {} ).toggle( false ), {
1533                                                         align: 'top',
1534                                                         label: Util.parseMsg( 'apisandbox-no-parameters' )
1535                                                 }
1536                                         ) );
1537                                 }
1539                                 that.$element.empty();
1541                                 new OO.ui.FieldsetLayout( {
1542                                         label: that.displayText
1543                                 } ).addItems( items )
1544                                         .$element.appendTo( that.$element );
1546                                 if ( Util.apiBool( pi.dynamicparameters ) ) {
1547                                         dynamicFieldset = new OO.ui.FieldsetLayout();
1548                                         dynamicParamNameWidget = new OO.ui.TextInputWidget( {
1549                                                 placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
1550                                         } ).on( 'enter', addDynamicParamWidget );
1551                                         dynamicFieldset.addItems( [
1552                                                 new OO.ui.FieldLayout(
1553                                                         new OO.ui.Widget( {} ).toggle( false ), {
1554                                                                 align: 'top',
1555                                                                 label: Util.parseHTML( pi.dynamicparameters )
1556                                                         }
1557                                                 ),
1558                                                 new OO.ui.ActionFieldLayout(
1559                                                         dynamicParamNameWidget,
1560                                                         new OO.ui.ButtonWidget( {
1561                                                                 icon: 'add',
1562                                                                 flags: 'progressive'
1563                                                         } ).on( 'click', addDynamicParamWidget ),
1564                                                         {
1565                                                                 label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
1566                                                                 align: 'left'
1567                                                         }
1568                                                 )
1569                                         ] );
1570                                         $( '<fieldset>' )
1571                                                 .append(
1572                                                         $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
1573                                                         dynamicFieldset.$element
1574                                                 )
1575                                                 .appendTo( that.$element );
1576                                 }
1578                                 if ( deprecatedItems.length ) {
1579                                         tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
1580                                         $( '<fieldset>' )
1581                                                 .append(
1582                                                         $( '<legend>' ).append(
1583                                                                 new OO.ui.ToggleButtonWidget( {
1584                                                                         label: mw.message( 'apisandbox-deprecated-parameters' ).text()
1585                                                                 } ).on( 'change', tmp.toggle, [], tmp ).$element
1586                                                         ),
1587                                                         tmp.$element
1588                                                 )
1589                                                 .appendTo( that.$element );
1590                                 }
1592                                 // Load stored params, if any, then update the booklet if we
1593                                 // have subpages (or else just update our valid-indicator).
1594                                 tmp = that.loadFromQueryParams;
1595                                 that.loadFromQueryParams = null;
1596                                 if ( $.isPlainObject( tmp ) ) {
1597                                         that.loadQueryParams( tmp );
1598                                 }
1599                                 if ( that.getSubpages().length > 0 ) {
1600                                         ApiSandbox.updateUI( tmp );
1601                                 } else {
1602                                         that.apiCheckValid();
1603                                 }
1604                         } ).fail( function ( code, detail ) {
1605                                 that.$element.empty()
1606                                         .append(
1607                                                 new OO.ui.LabelWidget( {
1608                                                         label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
1609                                                         classes: [ 'error' ]
1610                                                 } ).$element,
1611                                                 new OO.ui.ButtonWidget( {
1612                                                         label: mw.message( 'apisandbox-retry' ).text()
1613                                                 } ).on( 'click', that.loadParamInfo, [], that ).$element
1614                                         );
1615                         } );
1616         };
1618         /**
1619          * Check that all widgets on the page are in a valid state.
1620          *
1621          * @return {boolean}
1622          */
1623         ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
1624                 var that = this;
1626                 if ( this.paramInfo === null ) {
1627                         return $.Deferred().resolve( false ).promise();
1628                 } else {
1629                         return $.when.apply( $, $.map( this.widgets, function ( widget ) {
1630                                 return widget.apiCheckValid();
1631                         } ) ).then( function () {
1632                                 that.apiIsValid = $.inArray( false, arguments ) === -1;
1633                                 if ( that.getOutlineItem() ) {
1634                                         that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
1635                                         that.getOutlineItem().setIconTitle(
1636                                                 that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
1637                                         );
1638                                 }
1639                                 return $.Deferred().resolve( that.apiIsValid ).promise();
1640                         } );
1641                 }
1642         };
1644         /**
1645          * Load form fields from query parameters
1646          *
1647          * @param {Object} params
1648          */
1649         ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
1650                 if ( this.paramInfo === null ) {
1651                         this.loadFromQueryParams = params;
1652                 } else {
1653                         $.each( this.widgets, function ( name, widget ) {
1654                                 var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
1655                                 widget.setApiValue( v );
1656                         } );
1657                 }
1658         };
1660         /**
1661          * Load query params from form fields
1662          *
1663          * @param {Object} params Write query parameters into this object
1664          * @param {Object} displayParams Write query parameters for display into this object
1665          */
1666         ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
1667                 $.each( this.widgets, function ( name, widget ) {
1668                         var value = widget.getApiValue();
1669                         if ( value !== undefined ) {
1670                                 params[ name ] = value;
1671                                 if ( $.isFunction( widget.getApiValueForDisplay ) ) {
1672                                         value = widget.getApiValueForDisplay();
1673                                 }
1674                                 displayParams[ name ] = value;
1675                         }
1676                 } );
1677         };
1679         /**
1680          * Fetch a list of subpage names loaded by this page
1681          *
1682          * @return {Array}
1683          */
1684         ApiSandbox.PageLayout.prototype.getSubpages = function () {
1685                 var ret = [];
1686                 $.each( this.widgets, function ( name, widget ) {
1687                         var submodules, i;
1688                         if ( $.isFunction( widget.getSubmodules ) ) {
1689                                 submodules = widget.getSubmodules();
1690                                 for ( i = 0; i < submodules.length; i++ ) {
1691                                         ret.push( {
1692                                                 key: name + '=' + submodules[ i ].value,
1693                                                 path: submodules[ i ].path,
1694                                                 prefix: widget.paramInfo.submoduleparamprefix || ''
1695                                         } );
1696                                 }
1697                         }
1698                 } );
1699                 return ret;
1700         };
1702         /**
1703          * A text input with a clickable indicator
1704          *
1705          * @class
1706          * @private
1707          * @constructor
1708          * @param {Object} [config] Configuration options
1709          */
1710         function TextInputWithIndicatorWidget( config ) {
1711                 var k;
1713                 config = config || {};
1714                 TextInputWithIndicatorWidget[ 'super' ].call( this, config );
1716                 this.$indicator = $( '<span>' ).addClass( 'mw-apisandbox-clickable-indicator' );
1717                 OO.ui.mixin.TabIndexedElement.call(
1718                         this, $.extend( {}, config, { $tabIndexed: this.$indicator } )
1719                 );
1721                 this.input = new OO.ui.TextInputWidget( $.extend( {
1722                         $indicator: this.$indicator,
1723                         disabled: this.isDisabled()
1724                 }, config.input ) );
1726                 // Forward most methods for convenience
1727                 for ( k in this.input ) {
1728                         if ( $.isFunction( this.input[ k ] ) && !this[ k ] ) {
1729                                 this[ k ] = this.input[ k ].bind( this.input );
1730                         }
1731                 }
1733                 this.$indicator.on( {
1734                         click: this.onIndicatorClick.bind( this ),
1735                         keypress: this.onIndicatorKeyPress.bind( this )
1736                 } );
1738                 this.$element.append( this.input.$element );
1739         }
1740         OO.inheritClass( TextInputWithIndicatorWidget, OO.ui.Widget );
1741         OO.mixinClass( TextInputWithIndicatorWidget, OO.ui.mixin.TabIndexedElement );
1742         TextInputWithIndicatorWidget.prototype.onIndicatorClick = function ( e ) {
1743                 if ( !this.isDisabled() && e.which === 1 ) {
1744                         this.emit( 'indicator' );
1745                 }
1746                 return false;
1747         };
1748         TextInputWithIndicatorWidget.prototype.onIndicatorKeyPress = function ( e ) {
1749                 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
1750                         this.emit( 'indicator' );
1751                         return false;
1752                 }
1753         };
1754         TextInputWithIndicatorWidget.prototype.setDisabled = function ( disabled ) {
1755                 TextInputWithIndicatorWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
1756                 if ( this.input ) {
1757                         this.input.setDisabled( this.isDisabled() );
1758                 }
1759                 return this;
1760         };
1762         /**
1763          * A wrapper for a widget that provides an enable/disable button
1764          *
1765          * @class
1766          * @private
1767          * @constructor
1768          * @param {OO.ui.Widget} widget
1769          * @param {Object} [config] Configuration options
1770          */
1771         function OptionalWidget( widget, config ) {
1772                 var k;
1774                 config = config || {};
1776                 this.widget = widget;
1777                 this.$overlay = config.$overlay ||
1778                         $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-overlay' );
1779                 this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
1780                         .on( 'change', this.onCheckboxChange, [], this );
1782                 OptionalWidget[ 'super' ].call( this, config );
1784                 // Forward most methods for convenience
1785                 for ( k in this.widget ) {
1786                         if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
1787                                 this[ k ] = this.widget[ k ].bind( this.widget );
1788                         }
1789                 }
1791                 this.$overlay.on( 'click', this.onOverlayClick.bind( this ) );
1793                 this.$element
1794                         .addClass( 'mw-apisandbox-optionalWidget' )
1795                         .append(
1796                                 this.$overlay,
1797                                 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
1798                                         $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
1799                                                 widget.$element
1800                                         ),
1801                                         $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
1802                                                 this.checkbox.$element
1803                                         )
1804                                 )
1805                         );
1807                 this.setDisabled( widget.isDisabled() );
1808         }
1809         OO.inheritClass( OptionalWidget, OO.ui.Widget );
1810         OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
1811                 this.setDisabled( !checked );
1812         };
1813         OptionalWidget.prototype.onOverlayClick = function () {
1814                 this.setDisabled( false );
1815                 if ( $.isFunction( this.widget.focus ) ) {
1816                         this.widget.focus();
1817                 }
1818         };
1819         OptionalWidget.prototype.setDisabled = function ( disabled ) {
1820                 OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
1821                 this.widget.setDisabled( this.isDisabled() );
1822                 this.checkbox.setSelected( !this.isDisabled() );
1823                 this.$overlay.toggle( this.isDisabled() );
1824                 return this;
1825         };
1827         $( ApiSandbox.init );
1829         module.exports = ApiSandbox;
1831 }( jQuery, mediaWiki, OO ) );