ApiSandbox: Visual separation of fields
[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 mw.special.ApiSandbox = 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( '+', '%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,
795 progress, $progressText, progressLoading,
796 deferreds = [],
797 params = {},
798 displayParams = {},
799 checkPages = [ pages.main ];
800
801 suppressErrors = false;
802
803 while ( checkPages.length ) {
804 page = checkPages.shift();
805 deferreds.push( page.apiCheckValid() );
806 page.getQueryParams( params, displayParams );
807 subpages = page.getSubpages();
808 for ( i = 0; i < subpages.length; i++ ) {
809 if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
810 checkPages.push( pages[ subpages[ i ].key ] );
811 }
812 }
813 }
814
815 $.when.apply( $, deferreds ).done( function () {
816 if ( $.inArray( false, arguments ) !== -1 ) {
817 windowManager.openWindow( 'errorAlert', {
818 title: mw.message( 'apisandbox-submit-invalid-fields-title' ).parse(),
819 message: mw.message( 'apisandbox-submit-invalid-fields-message' ).parse(),
820 actions: [
821 {
822 action: 'accept',
823 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
824 flags: 'primary'
825 }
826 ]
827 } );
828 return;
829 }
830
831 query = $.param( displayParams );
832
833 // Force a 'fm' format with wrappedhtml=1, if available
834 if ( params.format !== undefined ) {
835 if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) {
836 params.format = params.format + 'fm';
837 }
838 if ( params.format.substr( -2 ) === 'fm' ) {
839 params.wrappedhtml = 1;
840 }
841 }
842
843 progressLoading = false;
844 $progressText = $( '<span>' ).text( mw.message( 'apisandbox-sending-request' ).text() );
845 progress = new OO.ui.ProgressBarWidget( {
846 progress: false,
847 $content: $progressText
848 } );
849
850 $result = $( '<div>' )
851 .append( progress.$element );
852
853 resultPage = page = new OO.ui.PageLayout( '|results|' );
854 page.setupOutlineItem = function () {
855 this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() );
856 };
857 page.$element.empty()
858 .append(
859 new OO.ui.FieldLayout(
860 new OO.ui.TextInputWidget( {
861 readOnly: true,
862 value: mw.util.wikiScript( 'api' ) + '?' + query
863 } ), {
864 label: mw.message( 'apisandbox-request-url-label' ).parse()
865 }
866 ).$element,
867 $result
868 );
869 ApiSandbox.updateUI();
870 booklet.setPage( '|results|' );
871
872 location.href = oldhash = '#' + query;
873
874 api.post( params, {
875 contentType: 'multipart/form-data',
876 dataType: 'text',
877 xhr: function () {
878 var xhr = new window.XMLHttpRequest();
879 xhr.upload.addEventListener( 'progress', function ( e ) {
880 if ( !progressLoading ) {
881 if ( e.lengthComputable ) {
882 progress.setProgress( e.loaded * 100 / e.total );
883 } else {
884 progress.setProgress( false );
885 }
886 }
887 } );
888 xhr.addEventListener( 'progress', function ( e ) {
889 if ( !progressLoading ) {
890 progressLoading = true;
891 $progressText.text( mw.message( 'apisandbox-loading-results' ).text() );
892 }
893 if ( e.lengthComputable ) {
894 progress.setProgress( e.loaded * 100 / e.total );
895 } else {
896 progress.setProgress( false );
897 }
898 } );
899 return xhr;
900 }
901 } )
902 .fail( function ( code, data ) {
903 var details = 'HTTP error: ' + data.exception;
904 $result.empty()
905 .append(
906 new OO.ui.LabelWidget( {
907 label: mw.message( 'apisandbox-results-error', details ).text(),
908 classes: [ 'error' ]
909 } ).$element
910 );
911 } )
912 .done( function ( data, jqXHR ) {
913 var m, loadTime, button,
914 ct = jqXHR.getResponseHeader( 'Content-Type' );
915
916 $result.empty();
917 if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
918 data = $.parseJSON( data );
919 if ( data.modules.length ) {
920 mw.loader.load( data.modules );
921 }
922 $result.append( Util.parseHTML( data.html ) );
923 loadTime = data.time;
924 } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
925 $result.append( Util.parseHTML( m[ 0 ] ) );
926 if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
927 loadTime = parseInt( m[ 1 ], 10 );
928 }
929 } else {
930 $( '<pre>' )
931 .addClass( 'api-pretty-content' )
932 .text( data )
933 .appendTo( $result );
934 }
935 if ( typeof loadTime === 'number' ) {
936 $result.append(
937 $( '<div>' ).append(
938 new OO.ui.LabelWidget( {
939 label: mw.message( 'apisandbox-request-time', loadTime ).text()
940 } ).$element
941 )
942 );
943 }
944
945 if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
946 // Flush all saved tokens in case one of them is the bad one.
947 Util.markTokensBad();
948 button = new OO.ui.ButtonWidget( {
949 label: mw.message( 'apisandbox-results-fixtoken' ).text()
950 } );
951 button.on( 'click', ApiSandbox.fixTokenAndResend )
952 .on( 'click', button.setDisabled, [ true ], button )
953 .$element.appendTo( $result );
954 }
955 } );
956 } );
957 },
958
959 /**
960 * Handler for the "Correct token and resubmit" button
961 *
962 * Used on a 'badtoken' error, it re-fetches token parameters for all
963 * pages and then re-submits the query.
964 */
965 fixTokenAndResend: function () {
966 var page, subpages, i, k,
967 ok = true,
968 tokenWait = { dummy: true },
969 checkPages = [ pages.main ],
970 success = function ( k ) {
971 delete tokenWait[ k ];
972 if ( ok && $.isEmptyObject( tokenWait ) ) {
973 ApiSandbox.sendRequest();
974 }
975 },
976 failure = function ( k ) {
977 delete tokenWait[ k ];
978 ok = false;
979 };
980
981 while ( checkPages.length ) {
982 page = checkPages.shift();
983
984 if ( page.tokenWidget ) {
985 k = page.apiModule + page.tokenWidget.paramInfo.name;
986 tokenWait[ k ] = page.tokenWidget.fetchToken()
987 .done( success.bind( page.tokenWidget, k ) )
988 .fail( failure.bind( page.tokenWidget, k ) );
989 }
990
991 subpages = page.getSubpages();
992 for ( i = 0; i < subpages.length; i++ ) {
993 if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
994 checkPages.push( pages[ subpages[ i ].key ] );
995 }
996 }
997 }
998
999 success( 'dummy', '' );
1000 },
1001
1002 /**
1003 * Reset validity indicators for all widgets
1004 */
1005 updateValidityIndicators: function () {
1006 var page, subpages, i,
1007 checkPages = [ pages.main ];
1008
1009 while ( checkPages.length ) {
1010 page = checkPages.shift();
1011 page.apiCheckValid();
1012 subpages = page.getSubpages();
1013 for ( i = 0; i < subpages.length; i++ ) {
1014 if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
1015 checkPages.push( pages[ subpages[ i ].key ] );
1016 }
1017 }
1018 }
1019 }
1020 };
1021
1022 /**
1023 * PageLayout for API modules
1024 *
1025 * @class
1026 * @private
1027 * @extends OO.ui.PageLayout
1028 * @constructor
1029 * @param {Object} [config] Configuration options
1030 */
1031 ApiSandbox.PageLayout = function ( config ) {
1032 config = $.extend( { prefix: '' }, config );
1033 this.displayText = config.key;
1034 this.apiModule = config.path;
1035 this.prefix = config.prefix;
1036 this.paramInfo = null;
1037 this.apiIsValid = true;
1038 this.loadFromQueryParams = null;
1039 this.widgets = {};
1040 this.tokenWidget = null;
1041 this.indentLevel = config.indentLevel ? config.indentLevel : 0;
1042 ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
1043 this.loadParamInfo();
1044 };
1045 OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
1046 ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
1047 this.outlineItem.setLevel( this.indentLevel );
1048 this.outlineItem.setLabel( this.displayText );
1049 this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
1050 this.outlineItem.setIconTitle(
1051 this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
1052 );
1053 };
1054
1055 /**
1056 * Fetch module information for this page's module, then create UI
1057 */
1058 ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
1059 var dynamicFieldset, dynamicParamNameWidget,
1060 that = this,
1061 removeDynamicParamWidget = function ( name, layout ) {
1062 dynamicFieldset.removeItems( [ layout ] );
1063 delete that.widgets[ name ];
1064 },
1065 addDynamicParamWidget = function () {
1066 var name, layout, widget, button;
1067
1068 // Check name is filled in
1069 name = dynamicParamNameWidget.getValue().trim();
1070 if ( name === '' ) {
1071 dynamicParamNameWidget.focus();
1072 return;
1073 }
1074
1075 if ( that.widgets[ name ] !== undefined ) {
1076 windowManager.openWindow( 'errorAlert', {
1077 title: mw.message(
1078 'apisandbox-dynamic-error-exists', name
1079 ).parse(),
1080 actions: [
1081 {
1082 action: 'accept',
1083 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
1084 flags: 'primary'
1085 }
1086 ]
1087 } );
1088 return;
1089 }
1090
1091 widget = Util.createWidgetForParameter( {
1092 name: name,
1093 type: 'string',
1094 'default': ''
1095 }, {
1096 nooptional: true
1097 } );
1098 button = new OO.ui.ButtonWidget( {
1099 icon: 'remove',
1100 flags: 'destructive'
1101 } );
1102 layout = new OO.ui.ActionFieldLayout(
1103 widget,
1104 button,
1105 {
1106 label: name,
1107 align: 'left'
1108 }
1109 );
1110 button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
1111 that.widgets[ name ] = widget;
1112 dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
1113 widget.focus();
1114
1115 dynamicParamNameWidget.setValue( '' );
1116 };
1117
1118 this.$element.empty()
1119 .append( new OO.ui.ProgressBarWidget( {
1120 progress: false,
1121 text: mw.message( 'apisandbox-loading', this.displayText ).text()
1122 } ).$element );
1123
1124 Util.fetchModuleInfo( this.apiModule )
1125 .done( function ( pi ) {
1126 var prefix, i, j, dl, widget, $widgetLabel, widgetField, helpField, tmp, flag, count,
1127 items = [],
1128 deprecatedItems = [],
1129 buttons = [],
1130 filterFmModules = function ( v ) {
1131 return v.substr( -2 ) !== 'fm' ||
1132 !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
1133 },
1134 widgetLabelOnClick = function () {
1135 var f = this.getField();
1136 if ( $.isFunction( f.setDisabled ) ) {
1137 f.setDisabled( false );
1138 }
1139 if ( $.isFunction( f.focus ) ) {
1140 f.focus();
1141 }
1142 },
1143 doNothing = function () {};
1144
1145 // This is something of a hack. We always want the 'format' and
1146 // 'action' parameters from the main module to be specified,
1147 // and for 'format' we also want to simplify the dropdown since
1148 // we always send the 'fm' variant.
1149 if ( that.apiModule === 'main' ) {
1150 for ( i = 0; i < pi.parameters.length; i++ ) {
1151 if ( pi.parameters[ i ].name === 'action' ) {
1152 pi.parameters[ i ].required = true;
1153 delete pi.parameters[ i ][ 'default' ];
1154 }
1155 if ( pi.parameters[ i ].name === 'format' ) {
1156 tmp = pi.parameters[ i ].type;
1157 for ( j = 0; j < tmp.length; j++ ) {
1158 availableFormats[ tmp[ j ] ] = true;
1159 }
1160 pi.parameters[ i ].type = $.grep( tmp, filterFmModules );
1161 pi.parameters[ i ][ 'default' ] = 'json';
1162 pi.parameters[ i ].required = true;
1163 }
1164 }
1165 }
1166
1167 // Hide the 'wrappedhtml' parameter on format modules
1168 if ( pi.group === 'format' ) {
1169 pi.parameters = $.grep( pi.parameters, function ( p ) {
1170 return p.name !== 'wrappedhtml';
1171 } );
1172 }
1173
1174 that.paramInfo = pi;
1175
1176 items.push( new OO.ui.FieldLayout(
1177 new OO.ui.Widget( {} ).toggle( false ), {
1178 align: 'top',
1179 label: Util.parseHTML( pi.description )
1180 }
1181 ) );
1182
1183 if ( pi.helpurls.length ) {
1184 buttons.push( new OO.ui.PopupButtonWidget( {
1185 label: mw.message( 'apisandbox-helpurls' ).text(),
1186 icon: 'help',
1187 popup: {
1188 $content: $( '<ul>' ).append( $.map( pi.helpurls, function ( link ) {
1189 return $( '<li>' ).append( $( '<a>', {
1190 href: link,
1191 target: '_blank',
1192 text: link
1193 } ) );
1194 } ) )
1195 }
1196 } ) );
1197 }
1198
1199 if ( pi.examples.length ) {
1200 buttons.push( new OO.ui.PopupButtonWidget( {
1201 label: mw.message( 'apisandbox-examples' ).text(),
1202 icon: 'code',
1203 popup: {
1204 $content: $( '<ul>' ).append( $.map( pi.examples, function ( example ) {
1205 var a = $( '<a>', {
1206 href: '#' + example.query,
1207 html: example.description
1208 } );
1209 a.find( 'a' ).contents().unwrap(); // Can't nest links
1210 return $( '<li>' ).append( a );
1211 } ) )
1212 }
1213 } ) );
1214 }
1215
1216 if ( buttons.length ) {
1217 items.push( new OO.ui.FieldLayout(
1218 new OO.ui.ButtonGroupWidget( {
1219 items: buttons
1220 } ), { align: 'top' }
1221 ) );
1222 }
1223
1224 if ( pi.parameters.length ) {
1225 prefix = that.prefix + pi.prefix;
1226 for ( i = 0; i < pi.parameters.length; i++ ) {
1227 widget = Util.createWidgetForParameter( pi.parameters[ i ] );
1228 that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
1229 if ( pi.parameters[ i ].tokentype ) {
1230 that.tokenWidget = widget;
1231 }
1232
1233 dl = $( '<dl>' );
1234 dl.append( $( '<dd>', {
1235 addClass: 'description',
1236 append: Util.parseHTML( pi.parameters[ i ].description )
1237 } ) );
1238 if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
1239 for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
1240 dl.append( $( '<dd>', {
1241 addClass: 'info',
1242 append: Util.parseHTML( pi.parameters[ i ].info[ j ] )
1243 } ) );
1244 }
1245 }
1246 flag = true;
1247 count = 1e100;
1248 switch ( pi.parameters[ i ].type ) {
1249 case 'namespace':
1250 flag = false;
1251 count = mw.config.get( 'wgFormattedNamespaces' ).length;
1252 break;
1253
1254 case 'limit':
1255 if ( pi.parameters[ i ].highmax !== undefined ) {
1256 dl.append( $( '<dd>', {
1257 addClass: 'info',
1258 append: Util.parseHTML( mw.message(
1259 'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
1260 ).parse() )
1261 } ) );
1262 } else {
1263 dl.append( $( '<dd>', {
1264 addClass: 'info',
1265 append: Util.parseHTML( mw.message(
1266 'api-help-param-limit', pi.parameters[ i ].max
1267 ).parse() )
1268 } ) );
1269 }
1270 break;
1271
1272 case 'integer':
1273 tmp = '';
1274 if ( pi.parameters[ i ].min !== undefined ) {
1275 tmp += 'min';
1276 }
1277 if ( pi.parameters[ i ].max !== undefined ) {
1278 tmp += 'max';
1279 }
1280 if ( tmp !== '' ) {
1281 dl.append( $( '<dd>', {
1282 addClass: 'info',
1283 append: Util.parseHTML( mw.message(
1284 'api-help-param-integer-' + tmp,
1285 Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
1286 pi.parameters[ i ].min, pi.parameters[ i ].max
1287 ).parse() )
1288 } ) );
1289 }
1290 break;
1291
1292 default:
1293 if ( $.isArray( pi.parameters[ i ].type ) ) {
1294 flag = false;
1295 count = pi.parameters[ i ].type.length;
1296 }
1297 break;
1298 }
1299 if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
1300 tmp = [];
1301 if ( flag && !( widget instanceof OO.ui.CapsuleMultiSelectWidget ) &&
1302 !(
1303 widget instanceof OptionalWidget &&
1304 widget.widget instanceof OO.ui.CapsuleMultiSelectWidget
1305 )
1306 ) {
1307 tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
1308 }
1309 if ( count > pi.parameters[ i ].lowlimit ) {
1310 tmp.push(
1311 mw.message( 'api-help-param-multi-max',
1312 pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
1313 ).parse()
1314 );
1315 }
1316 if ( tmp.length ) {
1317 dl.append( $( '<dd>', {
1318 addClass: 'info',
1319 append: Util.parseHTML( tmp.join( ' ' ) )
1320 } ) );
1321 }
1322 }
1323 helpField = new OO.ui.FieldLayout(
1324 new OO.ui.Widget( {
1325 $content: '\xa0',
1326 classes: [ 'mw-apisandbox-spacer' ]
1327 } ), {
1328 align: 'inline',
1329 classes: [ 'mw-apisandbox-help-field' ],
1330 label: dl
1331 }
1332 );
1333
1334 $widgetLabel = $( '<span>' );
1335 widgetField = new OO.ui.FieldLayout(
1336 widget,
1337 {
1338 align: 'left',
1339 classes: [ 'mw-apisandbox-widget-field' ],
1340 label: prefix + pi.parameters[ i ].name,
1341 $label: $widgetLabel
1342 }
1343 );
1344
1345 // FieldLayout only does click for InputElement
1346 // widgets. So supply our own click handler.
1347 $widgetLabel.on( 'click', widgetLabelOnClick.bind( widgetField ) );
1348
1349 // Don't grey out the label when the field is disabled,
1350 // it makes it too hard to read and our "disabled"
1351 // isn't really disabled.
1352 widgetField.onFieldDisable = doNothing;
1353
1354 if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
1355 deprecatedItems.push( widgetField, helpField );
1356 } else {
1357 items.push( widgetField, helpField );
1358 }
1359 }
1360 }
1361
1362 if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
1363 items.push( new OO.ui.FieldLayout(
1364 new OO.ui.Widget( {} ).toggle( false ), {
1365 align: 'top',
1366 label: Util.parseHTML( mw.message( 'apisandbox-no-parameters' ).parse() )
1367 }
1368 ) );
1369 }
1370
1371 that.$element.empty();
1372
1373 new OO.ui.FieldsetLayout( {
1374 label: that.displayText
1375 } ).addItems( items )
1376 .$element.appendTo( that.$element );
1377
1378 if ( Util.apiBool( pi.dynamicparameters ) ) {
1379 dynamicFieldset = new OO.ui.FieldsetLayout();
1380 dynamicParamNameWidget = new OO.ui.TextInputWidget( {
1381 placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
1382 } ).on( 'enter', addDynamicParamWidget );
1383 dynamicFieldset.addItems( [
1384 new OO.ui.FieldLayout(
1385 new OO.ui.Widget( {} ).toggle( false ), {
1386 align: 'top',
1387 label: Util.parseHTML( pi.dynamicparameters )
1388 }
1389 ),
1390 new OO.ui.ActionFieldLayout(
1391 dynamicParamNameWidget,
1392 new OO.ui.ButtonWidget( {
1393 icon: 'add',
1394 flags: 'constructive'
1395 } ).on( 'click', addDynamicParamWidget ),
1396 {
1397 label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
1398 align: 'left'
1399 }
1400 )
1401 ] );
1402 $( '<fieldset>' )
1403 .append(
1404 $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
1405 dynamicFieldset.$element
1406 )
1407 .appendTo( that.$element );
1408 }
1409
1410 if ( deprecatedItems.length ) {
1411 tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
1412 $( '<fieldset>' )
1413 .append(
1414 $( '<legend>' ).append(
1415 new OO.ui.ToggleButtonWidget( {
1416 label: mw.message( 'apisandbox-deprecated-parameters' ).text()
1417 } ).on( 'change', tmp.toggle, [], tmp ).$element
1418 ),
1419 tmp.$element
1420 )
1421 .appendTo( that.$element );
1422 }
1423
1424 // Load stored params, if any, then update the booklet if we
1425 // have subpages (or else just update our valid-indicator).
1426 tmp = that.loadFromQueryParams;
1427 that.loadFromQueryParams = null;
1428 if ( $.isPlainObject( tmp ) ) {
1429 that.loadQueryParams( tmp );
1430 }
1431 if ( that.getSubpages().length > 0 ) {
1432 ApiSandbox.updateUI( tmp );
1433 } else {
1434 that.apiCheckValid();
1435 }
1436 } ).fail( function ( code, detail ) {
1437 that.$element.empty()
1438 .append(
1439 new OO.ui.LabelWidget( {
1440 label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
1441 classes: [ 'error' ]
1442 } ).$element,
1443 new OO.ui.ButtonWidget( {
1444 label: mw.message( 'apisandbox-retry' ).text()
1445 } ).on( 'click', that.loadParamInfo, [], that ).$element
1446 );
1447 } );
1448 };
1449
1450 /**
1451 * Check that all widgets on the page are in a valid state.
1452 *
1453 * @return {boolean}
1454 */
1455 ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
1456 var that = this;
1457
1458 if ( this.paramInfo === null ) {
1459 return $.Deferred().resolve( false ).promise();
1460 } else {
1461 return $.when.apply( $, $.map( this.widgets, function ( widget ) {
1462 return widget.apiCheckValid();
1463 } ) ).then( function () {
1464 that.apiIsValid = $.inArray( false, arguments ) === -1;
1465 if ( that.getOutlineItem() ) {
1466 that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
1467 that.getOutlineItem().setIconTitle(
1468 that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
1469 );
1470 }
1471 return $.Deferred().resolve( that.apiIsValid ).promise();
1472 } );
1473 }
1474 };
1475
1476 /**
1477 * Load form fields from query parameters
1478 *
1479 * @param {Object} params
1480 */
1481 ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
1482 if ( this.paramInfo === null ) {
1483 this.loadFromQueryParams = params;
1484 } else {
1485 $.each( this.widgets, function ( name, widget ) {
1486 var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
1487 widget.setApiValue( v );
1488 } );
1489 }
1490 };
1491
1492 /**
1493 * Load query params from form fields
1494 *
1495 * @param {Object} params Write query parameters into this object
1496 * @param {Object} displayParams Write query parameters for display into this object
1497 */
1498 ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
1499 $.each( this.widgets, function ( name, widget ) {
1500 var value = widget.getApiValue();
1501 if ( value !== undefined ) {
1502 params[ name ] = value;
1503 if ( $.isFunction( widget.getApiValueForDisplay ) ) {
1504 value = widget.getApiValueForDisplay();
1505 }
1506 displayParams[ name ] = value;
1507 }
1508 } );
1509 };
1510
1511 /**
1512 * Fetch a list of subpage names loaded by this page
1513 *
1514 * @return {Array}
1515 */
1516 ApiSandbox.PageLayout.prototype.getSubpages = function () {
1517 var ret = [];
1518 $.each( this.widgets, function ( name, widget ) {
1519 var submodules, i;
1520 if ( $.isFunction( widget.getSubmodules ) ) {
1521 submodules = widget.getSubmodules();
1522 for ( i = 0; i < submodules.length; i++ ) {
1523 ret.push( {
1524 key: name + '=' + submodules[ i ].value,
1525 path: submodules[ i ].path,
1526 prefix: widget.paramInfo.submoduleparamprefix || ''
1527 } );
1528 }
1529 }
1530 } );
1531 return ret;
1532 };
1533
1534 /**
1535 * A text input with a clickable indicator
1536 *
1537 * @class
1538 * @private
1539 * @constructor
1540 * @param {Object} [config] Configuration options
1541 */
1542 function TextInputWithIndicatorWidget( config ) {
1543 var k;
1544
1545 config = config || {};
1546 TextInputWithIndicatorWidget[ 'super' ].call( this, config );
1547
1548 this.$indicator = $( '<span>' ).addClass( 'mw-apisandbox-clickable-indicator' );
1549 OO.ui.mixin.TabIndexedElement.call(
1550 this, $.extend( {}, config, { $tabIndexed: this.$indicator } )
1551 );
1552
1553 this.input = new OO.ui.TextInputWidget( $.extend( {
1554 $indicator: this.$indicator,
1555 disabled: this.isDisabled()
1556 }, config.input ) );
1557
1558 // Forward most methods for convenience
1559 for ( k in this.input ) {
1560 if ( $.isFunction( this.input[ k ] ) && !this[ k ] ) {
1561 this[ k ] = this.input[ k ].bind( this.input );
1562 }
1563 }
1564
1565 this.$indicator.on( {
1566 click: this.onIndicatorClick.bind( this ),
1567 keypress: this.onIndicatorKeyPress.bind( this )
1568 } );
1569
1570 this.$element.append( this.input.$element );
1571 }
1572 OO.inheritClass( TextInputWithIndicatorWidget, OO.ui.Widget );
1573 OO.mixinClass( TextInputWithIndicatorWidget, OO.ui.mixin.TabIndexedElement );
1574 TextInputWithIndicatorWidget.prototype.onIndicatorClick = function ( e ) {
1575 if ( !this.isDisabled() && e.which === 1 ) {
1576 this.emit( 'indicator' );
1577 }
1578 return false;
1579 };
1580 TextInputWithIndicatorWidget.prototype.onIndicatorKeyPress = function ( e ) {
1581 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
1582 this.emit( 'indicator' );
1583 return false;
1584 }
1585 };
1586 TextInputWithIndicatorWidget.prototype.setDisabled = function ( disabled ) {
1587 TextInputWithIndicatorWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
1588 if ( this.input ) {
1589 this.input.setDisabled( this.isDisabled() );
1590 }
1591 return this;
1592 };
1593
1594 /**
1595 * A wrapper for a widget that provides an enable/disable button
1596 *
1597 * @class
1598 * @private
1599 * @constructor
1600 * @param {OO.ui.Widget} widget
1601 * @param {Object} [config] Configuration options
1602 */
1603 function OptionalWidget( widget, config ) {
1604 var k;
1605
1606 config = config || {};
1607
1608 this.widget = widget;
1609 this.$overlay = config.$overlay ||
1610 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-overlay' );
1611 this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
1612 .on( 'change', this.onCheckboxChange, [], this );
1613
1614 OptionalWidget[ 'super' ].call( this, config );
1615
1616 // Forward most methods for convenience
1617 for ( k in this.widget ) {
1618 if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
1619 this[ k ] = this.widget[ k ].bind( this.widget );
1620 }
1621 }
1622
1623 this.$overlay.on( 'click', this.onOverlayClick.bind( this ) );
1624
1625 this.$element
1626 .addClass( 'mw-apisandbox-optionalWidget' )
1627 .append(
1628 this.$overlay,
1629 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
1630 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
1631 widget.$element
1632 ),
1633 $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
1634 this.checkbox.$element
1635 )
1636 )
1637 );
1638
1639 this.setDisabled( widget.isDisabled() );
1640 }
1641 OO.inheritClass( OptionalWidget, OO.ui.Widget );
1642 OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
1643 this.setDisabled( !checked );
1644 };
1645 OptionalWidget.prototype.onOverlayClick = function () {
1646 this.setDisabled( false );
1647 if ( $.isFunction( this.widget.focus ) ) {
1648 this.widget.focus();
1649 }
1650 };
1651 OptionalWidget.prototype.setDisabled = function ( disabled ) {
1652 OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
1653 this.widget.setDisabled( this.isDisabled() );
1654 this.checkbox.setSelected( !this.isDisabled() );
1655 this.$overlay.toggle( this.isDisabled() );
1656 return this;
1657 };
1658
1659 $( ApiSandbox.init );
1660
1661 }( jQuery, mediaWiki, OO ) );