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