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