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