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