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