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