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