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