API: Display message-per-value style help for submodule parameters
[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
1143 $result.empty();
1144 if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
1145 data = JSON.parse( data );
1146 if ( data.modules.length ) {
1147 mw.loader.load( data.modules );
1148 }
1149 if ( data.status && data.status !== 200 ) {
1150 $( '<div>' )
1151 .addClass( 'api-pretty-header api-pretty-status' )
1152 .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) )
1153 .appendTo( $result );
1154 }
1155 $result.append( Util.parseHTML( data.html ) );
1156 loadTime = data.time;
1157 } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
1158 $result.append( Util.parseHTML( m[ 0 ] ) );
1159 if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
1160 loadTime = parseInt( m[ 1 ], 10 );
1161 }
1162 } else {
1163 $( '<pre>' )
1164 .addClass( 'api-pretty-content' )
1165 .text( data )
1166 .appendTo( $result );
1167 }
1168 if ( paramsAreForced || data[ 'continue' ] ) {
1169 $result.append(
1170 $( '<div>' ).append(
1171 new OO.ui.ButtonWidget( {
1172 label: mw.message( 'apisandbox-continue' ).text()
1173 } ).on( 'click', function () {
1174 ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
1175 } ).setDisabled( !data[ 'continue' ] ).$element,
1176 ( clear = new OO.ui.ButtonWidget( {
1177 label: mw.message( 'apisandbox-continue-clear' ).text()
1178 } ).on( 'click', function () {
1179 ApiSandbox.updateUI( baseRequestParams );
1180 clear.setDisabled( true );
1181 booklet.setPage( '|results|' );
1182 } ).setDisabled( !paramsAreForced ) ).$element,
1183 new OO.ui.PopupButtonWidget( {
1184 $overlay: $( '#mw-apisandbox-ui' ),
1185 framed: false,
1186 icon: 'info',
1187 popup: {
1188 $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
1189 padded: true,
1190 width: 'auto'
1191 }
1192 } ).$element
1193 )
1194 );
1195 }
1196 if ( typeof loadTime === 'number' ) {
1197 $result.append(
1198 $( '<div>' ).append(
1199 new OO.ui.LabelWidget( {
1200 label: mw.message( 'apisandbox-request-time', loadTime ).text()
1201 } ).$element
1202 )
1203 );
1204 }
1205
1206 if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
1207 // Flush all saved tokens in case one of them is the bad one.
1208 Util.markTokensBad();
1209 button = new OO.ui.ButtonWidget( {
1210 label: mw.message( 'apisandbox-results-fixtoken' ).text()
1211 } );
1212 button.on( 'click', ApiSandbox.fixTokenAndResend )
1213 .on( 'click', button.setDisabled, [ true ], button )
1214 .$element.appendTo( $result );
1215 }
1216 }, function ( code, data ) {
1217 var details = 'HTTP error: ' + data.exception;
1218 $result.empty()
1219 .append(
1220 new OO.ui.LabelWidget( {
1221 label: mw.message( 'apisandbox-results-error', details ).text(),
1222 classes: [ 'error' ]
1223 } ).$element
1224 );
1225 } );
1226 } );
1227 },
1228
1229 /**
1230 * Handler for the "Correct token and resubmit" button
1231 *
1232 * Used on a 'badtoken' error, it re-fetches token parameters for all
1233 * pages and then re-submits the query.
1234 */
1235 fixTokenAndResend: function () {
1236 var page, subpages, i, k,
1237 ok = true,
1238 tokenWait = { dummy: true },
1239 checkPages = [ pages.main ],
1240 success = function ( k ) {
1241 delete tokenWait[ k ];
1242 if ( ok && $.isEmptyObject( tokenWait ) ) {
1243 ApiSandbox.sendRequest();
1244 }
1245 },
1246 failure = function ( k ) {
1247 delete tokenWait[ k ];
1248 ok = false;
1249 };
1250
1251 while ( checkPages.length ) {
1252 page = checkPages.shift();
1253
1254 if ( page.tokenWidget ) {
1255 k = page.apiModule + page.tokenWidget.paramInfo.name;
1256 tokenWait[ k ] = page.tokenWidget.fetchToken();
1257 tokenWait[ k ]
1258 .done( success.bind( page.tokenWidget, k ) )
1259 .fail( failure.bind( page.tokenWidget, k ) );
1260 }
1261
1262 subpages = page.getSubpages();
1263 for ( i = 0; i < subpages.length; i++ ) {
1264 if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
1265 checkPages.push( pages[ subpages[ i ].key ] );
1266 }
1267 }
1268 }
1269
1270 success( 'dummy', '' );
1271 },
1272
1273 /**
1274 * Reset validity indicators for all widgets
1275 */
1276 updateValidityIndicators: function () {
1277 var page, subpages, i,
1278 checkPages = [ pages.main ];
1279
1280 while ( checkPages.length ) {
1281 page = checkPages.shift();
1282 page.apiCheckValid();
1283 subpages = page.getSubpages();
1284 for ( i = 0; i < subpages.length; i++ ) {
1285 if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
1286 checkPages.push( pages[ subpages[ i ].key ] );
1287 }
1288 }
1289 }
1290 }
1291 };
1292
1293 /**
1294 * PageLayout for API modules
1295 *
1296 * @class
1297 * @private
1298 * @extends OO.ui.PageLayout
1299 * @constructor
1300 * @param {Object} [config] Configuration options
1301 */
1302 ApiSandbox.PageLayout = function ( config ) {
1303 config = $.extend( { prefix: '' }, config );
1304 this.displayText = config.key;
1305 this.apiModule = config.path;
1306 this.prefix = config.prefix;
1307 this.paramInfo = null;
1308 this.apiIsValid = true;
1309 this.loadFromQueryParams = null;
1310 this.widgets = {};
1311 this.tokenWidget = null;
1312 this.indentLevel = config.indentLevel ? config.indentLevel : 0;
1313 ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
1314 this.loadParamInfo();
1315 };
1316 OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
1317 ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
1318 this.outlineItem.setLevel( this.indentLevel );
1319 this.outlineItem.setLabel( this.displayText );
1320 this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
1321 this.outlineItem.setIconTitle(
1322 this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
1323 );
1324 };
1325
1326 /**
1327 * Fetch module information for this page's module, then create UI
1328 */
1329 ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
1330 var dynamicFieldset, dynamicParamNameWidget,
1331 that = this,
1332 removeDynamicParamWidget = function ( name, layout ) {
1333 dynamicFieldset.removeItems( [ layout ] );
1334 delete that.widgets[ name ];
1335 },
1336 addDynamicParamWidget = function () {
1337 var name, layout, widget, button;
1338
1339 // Check name is filled in
1340 name = dynamicParamNameWidget.getValue().trim();
1341 if ( name === '' ) {
1342 dynamicParamNameWidget.focus();
1343 return;
1344 }
1345
1346 if ( that.widgets[ name ] !== undefined ) {
1347 windowManager.openWindow( 'errorAlert', {
1348 title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
1349 actions: [
1350 {
1351 action: 'accept',
1352 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
1353 flags: 'primary'
1354 }
1355 ]
1356 } );
1357 return;
1358 }
1359
1360 widget = Util.createWidgetForParameter( {
1361 name: name,
1362 type: 'string',
1363 'default': ''
1364 }, {
1365 nooptional: true
1366 } );
1367 button = new OO.ui.ButtonWidget( {
1368 icon: 'trash',
1369 flags: 'destructive'
1370 } );
1371 layout = new OO.ui.ActionFieldLayout(
1372 widget,
1373 button,
1374 {
1375 label: name,
1376 align: 'left'
1377 }
1378 );
1379 button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
1380 that.widgets[ name ] = widget;
1381 dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
1382 widget.focus();
1383
1384 dynamicParamNameWidget.setValue( '' );
1385 };
1386
1387 this.$element.empty()
1388 .append( new OO.ui.ProgressBarWidget( {
1389 progress: false,
1390 text: mw.message( 'apisandbox-loading', this.displayText ).text()
1391 } ).$element );
1392
1393 Util.fetchModuleInfo( this.apiModule )
1394 .done( function ( pi ) {
1395 var prefix, i, j, descriptionContainer, widget, widgetField, helpField, tmp, flag, count,
1396 items = [],
1397 deprecatedItems = [],
1398 buttons = [],
1399 filterFmModules = function ( v ) {
1400 return v.substr( -2 ) !== 'fm' ||
1401 !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
1402 },
1403 widgetLabelOnClick = function () {
1404 var f = this.getField();
1405 if ( $.isFunction( f.setDisabled ) ) {
1406 f.setDisabled( false );
1407 }
1408 if ( $.isFunction( f.focus ) ) {
1409 f.focus();
1410 }
1411 },
1412 doNothing = function () {};
1413
1414 // This is something of a hack. We always want the 'format' and
1415 // 'action' parameters from the main module to be specified,
1416 // and for 'format' we also want to simplify the dropdown since
1417 // we always send the 'fm' variant.
1418 if ( that.apiModule === 'main' ) {
1419 for ( i = 0; i < pi.parameters.length; i++ ) {
1420 if ( pi.parameters[ i ].name === 'action' ) {
1421 pi.parameters[ i ].required = true;
1422 delete pi.parameters[ i ][ 'default' ];
1423 }
1424 if ( pi.parameters[ i ].name === 'format' ) {
1425 tmp = pi.parameters[ i ].type;
1426 for ( j = 0; j < tmp.length; j++ ) {
1427 availableFormats[ tmp[ j ] ] = true;
1428 }
1429 pi.parameters[ i ].type = $.grep( tmp, filterFmModules );
1430 pi.parameters[ i ][ 'default' ] = 'json';
1431 pi.parameters[ i ].required = true;
1432 }
1433 }
1434 }
1435
1436 // Hide the 'wrappedhtml' parameter on format modules
1437 if ( pi.group === 'format' ) {
1438 pi.parameters = $.grep( pi.parameters, function ( p ) {
1439 return p.name !== 'wrappedhtml';
1440 } );
1441 }
1442
1443 that.paramInfo = pi;
1444
1445 items.push( new OO.ui.FieldLayout(
1446 new OO.ui.Widget( {} ).toggle( false ), {
1447 align: 'top',
1448 label: Util.parseHTML( pi.description )
1449 }
1450 ) );
1451
1452 if ( pi.helpurls.length ) {
1453 buttons.push( new OO.ui.PopupButtonWidget( {
1454 $overlay: $( '#mw-apisandbox-ui' ),
1455 label: mw.message( 'apisandbox-helpurls' ).text(),
1456 icon: 'help',
1457 popup: {
1458 width: 'auto',
1459 padded: true,
1460 $content: $( '<ul>' ).append( $.map( pi.helpurls, function ( link ) {
1461 return $( '<li>' ).append( $( '<a>', {
1462 href: link,
1463 target: '_blank',
1464 text: link
1465 } ) );
1466 } ) )
1467 }
1468 } ) );
1469 }
1470
1471 if ( pi.examples.length ) {
1472 buttons.push( new OO.ui.PopupButtonWidget( {
1473 $overlay: $( '#mw-apisandbox-ui' ),
1474 label: mw.message( 'apisandbox-examples' ).text(),
1475 icon: 'code',
1476 popup: {
1477 width: 'auto',
1478 padded: true,
1479 $content: $( '<ul>' ).append( $.map( pi.examples, function ( example ) {
1480 var a = $( '<a>', {
1481 href: '#' + example.query,
1482 html: example.description
1483 } );
1484 a.find( 'a' ).contents().unwrap(); // Can't nest links
1485 return $( '<li>' ).append( a );
1486 } ) )
1487 }
1488 } ) );
1489 }
1490
1491 if ( buttons.length ) {
1492 items.push( new OO.ui.FieldLayout(
1493 new OO.ui.ButtonGroupWidget( {
1494 items: buttons
1495 } ), { align: 'top' }
1496 ) );
1497 }
1498
1499 if ( pi.parameters.length ) {
1500 prefix = that.prefix + pi.prefix;
1501 for ( i = 0; i < pi.parameters.length; i++ ) {
1502 widget = Util.createWidgetForParameter( pi.parameters[ i ] );
1503 that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
1504 if ( pi.parameters[ i ].tokentype ) {
1505 that.tokenWidget = widget;
1506 }
1507
1508 descriptionContainer = $( '<div>' );
1509
1510 tmp = Util.parseHTML( pi.parameters[ i ].description );
1511 tmp.filter( 'dl' ).makeCollapsible( {
1512 collapsed: true
1513 } ).children( '.mw-collapsible-toggle' ).each( function () {
1514 var $this = $( this );
1515 $this.parent().prev( 'p' ).append( $this );
1516 } );
1517 descriptionContainer.append( $( '<div>', { addClass: 'description', append: tmp } ) );
1518
1519 if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
1520 for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
1521 descriptionContainer.append( $( '<div>', {
1522 addClass: 'info',
1523 append: Util.parseHTML( pi.parameters[ i ].info[ j ] )
1524 } ) );
1525 }
1526 }
1527 flag = true;
1528 count = 1e100;
1529 switch ( pi.parameters[ i ].type ) {
1530 case 'namespace':
1531 flag = false;
1532 count = mw.config.get( 'wgFormattedNamespaces' ).length;
1533 break;
1534
1535 case 'limit':
1536 if ( pi.parameters[ i ].highmax !== undefined ) {
1537 descriptionContainer.append( $( '<div>', {
1538 addClass: 'info',
1539 append: [
1540 Util.parseMsg(
1541 'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
1542 ),
1543 ' ',
1544 Util.parseMsg( 'apisandbox-param-limit' )
1545 ]
1546 } ) );
1547 } else {
1548 descriptionContainer.append( $( '<div>', {
1549 addClass: 'info',
1550 append: [
1551 Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
1552 ' ',
1553 Util.parseMsg( 'apisandbox-param-limit' )
1554 ]
1555 } ) );
1556 }
1557 break;
1558
1559 case 'integer':
1560 tmp = '';
1561 if ( pi.parameters[ i ].min !== undefined ) {
1562 tmp += 'min';
1563 }
1564 if ( pi.parameters[ i ].max !== undefined ) {
1565 tmp += 'max';
1566 }
1567 if ( tmp !== '' ) {
1568 descriptionContainer.append( $( '<div>', {
1569 addClass: 'info',
1570 append: Util.parseMsg(
1571 'api-help-param-integer-' + tmp,
1572 Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
1573 pi.parameters[ i ].min, pi.parameters[ i ].max
1574 )
1575 } ) );
1576 }
1577 break;
1578
1579 default:
1580 if ( Array.isArray( pi.parameters[ i ].type ) ) {
1581 flag = false;
1582 count = pi.parameters[ i ].type.length;
1583 }
1584 break;
1585 }
1586 if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
1587 tmp = [];
1588 if ( flag && !( widget instanceof OO.ui.CapsuleMultiselectWidget ) &&
1589 !(
1590 widget instanceof OptionalWidget &&
1591 widget.widget instanceof OO.ui.CapsuleMultiselectWidget
1592 )
1593 ) {
1594 tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
1595 }
1596 if ( count > pi.parameters[ i ].lowlimit ) {
1597 tmp.push(
1598 mw.message( 'api-help-param-multi-max',
1599 pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
1600 ).parse()
1601 );
1602 }
1603 if ( tmp.length ) {
1604 descriptionContainer.append( $( '<div>', {
1605 addClass: 'info',
1606 append: Util.parseHTML( tmp.join( ' ' ) )
1607 } ) );
1608 }
1609 }
1610 helpField = new OO.ui.FieldLayout(
1611 new OO.ui.Widget( {
1612 $content: '\xa0',
1613 classes: [ 'mw-apisandbox-spacer' ]
1614 } ), {
1615 align: 'inline',
1616 classes: [ 'mw-apisandbox-help-field' ],
1617 label: descriptionContainer
1618 }
1619 );
1620
1621 widgetField = new OO.ui.FieldLayout(
1622 widget,
1623 {
1624 align: 'left',
1625 classes: [ 'mw-apisandbox-widget-field' ],
1626 label: prefix + pi.parameters[ i ].name
1627 }
1628 );
1629
1630 // We need our own click handler on the widget label to
1631 // turn off the disablement.
1632 widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
1633
1634 // Don't grey out the label when the field is disabled,
1635 // it makes it too hard to read and our "disabled"
1636 // isn't really disabled.
1637 widgetField.onFieldDisable( false );
1638 widgetField.onFieldDisable = doNothing;
1639
1640 if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
1641 deprecatedItems.push( widgetField, helpField );
1642 } else {
1643 items.push( widgetField, helpField );
1644 }
1645 }
1646 }
1647
1648 if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
1649 items.push( new OO.ui.FieldLayout(
1650 new OO.ui.Widget( {} ).toggle( false ), {
1651 align: 'top',
1652 label: Util.parseMsg( 'apisandbox-no-parameters' )
1653 }
1654 ) );
1655 }
1656
1657 that.$element.empty();
1658
1659 new OO.ui.FieldsetLayout( {
1660 label: that.displayText
1661 } ).addItems( items )
1662 .$element.appendTo( that.$element );
1663
1664 if ( Util.apiBool( pi.dynamicparameters ) ) {
1665 dynamicFieldset = new OO.ui.FieldsetLayout();
1666 dynamicParamNameWidget = new OO.ui.TextInputWidget( {
1667 placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
1668 } ).on( 'enter', addDynamicParamWidget );
1669 dynamicFieldset.addItems( [
1670 new OO.ui.FieldLayout(
1671 new OO.ui.Widget( {} ).toggle( false ), {
1672 align: 'top',
1673 label: Util.parseHTML( pi.dynamicparameters )
1674 }
1675 ),
1676 new OO.ui.ActionFieldLayout(
1677 dynamicParamNameWidget,
1678 new OO.ui.ButtonWidget( {
1679 icon: 'add',
1680 flags: 'progressive'
1681 } ).on( 'click', addDynamicParamWidget ),
1682 {
1683 label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
1684 align: 'left'
1685 }
1686 )
1687 ] );
1688 $( '<fieldset>' )
1689 .append(
1690 $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
1691 dynamicFieldset.$element
1692 )
1693 .appendTo( that.$element );
1694 }
1695
1696 if ( deprecatedItems.length ) {
1697 tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
1698 $( '<fieldset>' )
1699 .append(
1700 $( '<legend>' ).append(
1701 new OO.ui.ToggleButtonWidget( {
1702 label: mw.message( 'apisandbox-deprecated-parameters' ).text()
1703 } ).on( 'change', tmp.toggle, [], tmp ).$element
1704 ),
1705 tmp.$element
1706 )
1707 .appendTo( that.$element );
1708 }
1709
1710 // Load stored params, if any, then update the booklet if we
1711 // have subpages (or else just update our valid-indicator).
1712 tmp = that.loadFromQueryParams;
1713 that.loadFromQueryParams = null;
1714 if ( $.isPlainObject( tmp ) ) {
1715 that.loadQueryParams( tmp );
1716 }
1717 if ( that.getSubpages().length > 0 ) {
1718 ApiSandbox.updateUI( tmp );
1719 } else {
1720 that.apiCheckValid();
1721 }
1722 } ).fail( function ( code, detail ) {
1723 that.$element.empty()
1724 .append(
1725 new OO.ui.LabelWidget( {
1726 label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
1727 classes: [ 'error' ]
1728 } ).$element,
1729 new OO.ui.ButtonWidget( {
1730 label: mw.message( 'apisandbox-retry' ).text()
1731 } ).on( 'click', that.loadParamInfo, [], that ).$element
1732 );
1733 } );
1734 };
1735
1736 /**
1737 * Check that all widgets on the page are in a valid state.
1738 *
1739 * @return {boolean}
1740 */
1741 ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
1742 var that = this;
1743
1744 if ( this.paramInfo === null ) {
1745 return $.Deferred().resolve( false ).promise();
1746 } else {
1747 return $.when.apply( $, $.map( this.widgets, function ( widget ) {
1748 return widget.apiCheckValid();
1749 } ) ).then( function () {
1750 that.apiIsValid = $.inArray( false, arguments ) === -1;
1751 if ( that.getOutlineItem() ) {
1752 that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
1753 that.getOutlineItem().setIconTitle(
1754 that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
1755 );
1756 }
1757 return $.Deferred().resolve( that.apiIsValid ).promise();
1758 } );
1759 }
1760 };
1761
1762 /**
1763 * Load form fields from query parameters
1764 *
1765 * @param {Object} params
1766 */
1767 ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
1768 if ( this.paramInfo === null ) {
1769 this.loadFromQueryParams = params;
1770 } else {
1771 $.each( this.widgets, function ( name, widget ) {
1772 var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
1773 widget.setApiValue( v );
1774 } );
1775 }
1776 };
1777
1778 /**
1779 * Load query params from form fields
1780 *
1781 * @param {Object} params Write query parameters into this object
1782 * @param {Object} displayParams Write query parameters for display into this object
1783 */
1784 ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
1785 $.each( this.widgets, function ( name, widget ) {
1786 var value = widget.getApiValue();
1787 if ( value !== undefined ) {
1788 params[ name ] = value;
1789 if ( $.isFunction( widget.getApiValueForDisplay ) ) {
1790 value = widget.getApiValueForDisplay();
1791 }
1792 displayParams[ name ] = value;
1793 }
1794 } );
1795 };
1796
1797 /**
1798 * Fetch a list of subpage names loaded by this page
1799 *
1800 * @return {Array}
1801 */
1802 ApiSandbox.PageLayout.prototype.getSubpages = function () {
1803 var ret = [];
1804 $.each( this.widgets, function ( name, widget ) {
1805 var submodules, i;
1806 if ( $.isFunction( widget.getSubmodules ) ) {
1807 submodules = widget.getSubmodules();
1808 for ( i = 0; i < submodules.length; i++ ) {
1809 ret.push( {
1810 key: name + '=' + submodules[ i ].value,
1811 path: submodules[ i ].path,
1812 prefix: widget.paramInfo.submoduleparamprefix || ''
1813 } );
1814 }
1815 }
1816 } );
1817 return ret;
1818 };
1819
1820 /**
1821 * A text input with a clickable indicator
1822 *
1823 * @class
1824 * @private
1825 * @constructor
1826 * @param {Object} [config] Configuration options
1827 */
1828 function TextInputWithIndicatorWidget( config ) {
1829 var k;
1830
1831 config = config || {};
1832 TextInputWithIndicatorWidget[ 'super' ].call( this, config );
1833
1834 this.$indicator = $( '<span>' ).addClass( 'mw-apisandbox-clickable-indicator' );
1835 OO.ui.mixin.TabIndexedElement.call(
1836 this, $.extend( {}, config, { $tabIndexed: this.$indicator } )
1837 );
1838
1839 this.input = new OO.ui.TextInputWidget( $.extend( {
1840 $indicator: this.$indicator,
1841 disabled: this.isDisabled()
1842 }, config.input ) );
1843
1844 // Forward most methods for convenience
1845 for ( k in this.input ) {
1846 if ( $.isFunction( this.input[ k ] ) && !this[ k ] ) {
1847 this[ k ] = this.input[ k ].bind( this.input );
1848 }
1849 }
1850
1851 this.$indicator.on( {
1852 click: this.onIndicatorClick.bind( this ),
1853 keypress: this.onIndicatorKeyPress.bind( this )
1854 } );
1855
1856 this.$element.append( this.input.$element );
1857 }
1858 OO.inheritClass( TextInputWithIndicatorWidget, OO.ui.Widget );
1859 OO.mixinClass( TextInputWithIndicatorWidget, OO.ui.mixin.TabIndexedElement );
1860 TextInputWithIndicatorWidget.prototype.onIndicatorClick = function ( e ) {
1861 if ( !this.isDisabled() && e.which === 1 ) {
1862 this.emit( 'indicator' );
1863 }
1864 return false;
1865 };
1866 TextInputWithIndicatorWidget.prototype.onIndicatorKeyPress = function ( e ) {
1867 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
1868 this.emit( 'indicator' );
1869 return false;
1870 }
1871 };
1872 TextInputWithIndicatorWidget.prototype.setDisabled = function ( disabled ) {
1873 TextInputWithIndicatorWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
1874 if ( this.input ) {
1875 this.input.setDisabled( this.isDisabled() );
1876 }
1877 return this;
1878 };
1879
1880 /**
1881 * A wrapper for a widget that provides an enable/disable button
1882 *
1883 * @class
1884 * @private
1885 * @constructor
1886 * @param {OO.ui.Widget} widget
1887 * @param {Object} [config] Configuration options
1888 */
1889 function OptionalWidget( widget, config ) {
1890 var k;
1891
1892 config = config || {};
1893
1894 this.widget = widget;
1895 this.$overlay = config.$overlay ||
1896 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-overlay' );
1897 this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
1898 .on( 'change', this.onCheckboxChange, [], this );
1899
1900 OptionalWidget[ 'super' ].call( this, config );
1901
1902 // Forward most methods for convenience
1903 for ( k in this.widget ) {
1904 if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
1905 this[ k ] = this.widget[ k ].bind( this.widget );
1906 }
1907 }
1908
1909 this.$overlay.on( 'click', this.onOverlayClick.bind( this ) );
1910
1911 this.$element
1912 .addClass( 'mw-apisandbox-optionalWidget' )
1913 .append(
1914 this.$overlay,
1915 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
1916 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
1917 widget.$element
1918 ),
1919 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
1920 this.checkbox.$element
1921 )
1922 )
1923 );
1924
1925 this.setDisabled( widget.isDisabled() );
1926 }
1927 OO.inheritClass( OptionalWidget, OO.ui.Widget );
1928 OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
1929 this.setDisabled( !checked );
1930 };
1931 OptionalWidget.prototype.onOverlayClick = function () {
1932 this.setDisabled( false );
1933 if ( $.isFunction( this.widget.focus ) ) {
1934 this.widget.focus();
1935 }
1936 };
1937 OptionalWidget.prototype.setDisabled = function ( disabled ) {
1938 OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
1939 this.widget.setDisabled( this.isDisabled() );
1940 this.checkbox.setSelected( !this.isDisabled() );
1941 this.$overlay.toggle( this.isDisabled() );
1942 return this;
1943 };
1944
1945 $( ApiSandbox.init );
1946
1947 module.exports = ApiSandbox;
1948
1949 }( jQuery, mediaWiki, OO ) );