1 const ParamLabelWidget
= require( './ParamLabelWidget.js' ),
2 OptionalParamWidget
= require( './OptionalParamWidget.js' ),
3 ApiSandbox
= require( './ApiSandbox.js' ),
4 Util
= require( './Util.js' );
7 * PageLayout for API modules
11 * @extends OO.ui.PageLayout
13 * @param {Object} [config] Configuration options
15 function ApiSandboxLayout( config
) {
16 config
= Object
.assign( { prefix
: '', expanded
: false }, config
);
17 this.displayText
= config
.key
;
18 this.apiModule
= config
.path
;
19 this.prefix
= config
.prefix
;
20 this.paramInfo
= null;
21 this.apiIsValid
= true;
22 this.loadFromQueryParams
= null;
24 this.itemsFieldset
= null;
25 this.deprecatedItemsFieldset
= null;
26 this.templatedItemsCache
= {};
27 this.tokenWidget
= null;
28 this.indentLevel
= config
.indentLevel
? config
.indentLevel
: 0;
29 ApiSandboxLayout
.super.call( this, config
.key
, config
);
33 OO
.inheritClass( ApiSandboxLayout
, OO
.ui
.PageLayout
);
35 ApiSandboxLayout
.prototype.setupOutlineItem = function () {
36 this.outlineItem
.setLevel( this.indentLevel
);
37 this.outlineItem
.setLabel( this.displayText
);
38 this.outlineItem
.setIcon( this.apiIsValid
|| ApiSandbox
.suppressErrors
? null : 'alert' );
39 this.outlineItem
.setTitle(
40 this.apiIsValid
|| ApiSandbox
.suppressErrors
? '' : mw
.message( 'apisandbox-alert-page' ).plain()
45 * Create a widget and the FieldLayouts it needs
48 * @param {Object} ppi API paraminfo data for the parameter
49 * @param {string} name API parameter name
51 * @return {OO.ui.Widget} return.widget
52 * @return {OO.ui.FieldLayout} return.widgetField
53 * @return {OO.ui.FieldLayout} return.helpField
55 ApiSandboxLayout
.prototype.makeWidgetFieldLayouts = function ( ppi
, name
) {
56 const widget
= Util
.createWidgetForParameter( ppi
);
57 if ( ppi
.tokentype
) {
58 this.tokenWidget
= widget
;
60 if ( this.paramInfo
.templatedparameters
.length
) {
61 widget
.on( 'change', () => {
62 this.updateTemplatedParameters( null );
66 const helpLabel
= new ParamLabelWidget();
68 let $tmp
= Util
.parseHTML( ppi
.description
);
69 $tmp
.filter( 'dl' ).makeCollapsible( {
71 } ).children( '.mw-collapsible-toggle' ).each( ( i
, el
) => {
73 $el
.parent().prev( 'p' ).append( $el
);
75 helpLabel
.addDescription( $tmp
);
77 if ( ppi
.info
&& ppi
.info
.length
) {
78 for ( let i
= 0; i
< ppi
.info
.length
; i
++ ) {
79 helpLabel
.addInfo( Util
.parseHTML( ppi
.info
[ i
].text
) );
88 count
= mw
.config
.get( 'wgFormattedNamespaces' ).length
;
94 'paramvalidator-help-type-number-minmax', 1,
95 widget
.paramInfo
.min
, widget
.paramInfo
.apiSandboxMax
97 mw
.message( 'apisandbox-param-limit' ).parse()
99 helpLabel
.addInfo( Util
.parseHTML( tmp
.join( mw
.msg( 'word-separator' ) ) ) );
104 if ( ppi
.min
!== undefined ) {
107 if ( ppi
.max
!== undefined ) {
113 'paramvalidator-help-type-number-' + tmp
,
114 Util
.apiBool( ppi
.multi
) ? 2 : 1,
122 if ( Array
.isArray( ppi
.type
) ) {
124 count
= ppi
.type
.length
;
128 if ( Util
.apiBool( ppi
.multi
) ) {
130 if ( flag
&& !( widget
instanceof OO
.ui
.TagMultiselectWidget
) &&
132 widget
instanceof OptionalParamWidget
&&
133 widget
.widget
instanceof OO
.ui
.TagMultiselectWidget
136 tmp
.push( mw
.message( 'api-help-param-multi-separate' ).parse() );
138 if ( count
> ppi
.lowlimit
) {
140 mw
.message( 'paramvalidator-help-multi-max', ppi
.lowlimit
, ppi
.highlimit
).parse()
144 helpLabel
.addInfo( Util
.parseHTML( tmp
.join( mw
.msg( 'word-separator' ) ) ) );
147 if ( 'maxbytes' in ppi
) {
148 helpLabel
.addInfo( Util
.parseMsg( 'paramvalidator-help-type-string-maxbytes', ppi
.maxbytes
) );
150 if ( 'maxchars' in ppi
) {
151 helpLabel
.addInfo( Util
.parseMsg( 'paramvalidator-help-type-string-maxchars', ppi
.maxchars
) );
153 if ( ppi
.usedTemplateVars
&& ppi
.usedTemplateVars
.length
) {
155 for ( let j
= 0, l
= ppi
.usedTemplateVars
.length
; j
< l
; j
++ ) {
156 $tmp
= $tmp
.add( $( '<var>' ).text( ppi
.usedTemplateVars
[ j
] ) );
158 $tmp
= $tmp
.add( mw
.message( 'and' ).parseDom() );
159 $tmp
= $tmp
.add( mw
.message( 'word-separator' ).parseDom() );
160 } else if ( j
!== l
- 1 ) {
161 $tmp
= $tmp
.add( mw
.message( 'comma-separator' ).parseDom() );
166 'apisandbox-templated-parameter-reason',
167 ppi
.usedTemplateVars
.length
,
173 // TODO: Consder adding more options for the position of helpInline
174 // so that this can become part of the widgetField, instead of
175 // having to use a separate field.
176 const helpField
= new OO
.ui
.FieldLayout(
180 classes
: [ 'mw-apisandbox-help-field' ]
184 const layoutConfig
= {
186 classes
: [ 'mw-apisandbox-widget-field' ],
191 if ( ppi
.tokentype
) {
192 const button
= new OO
.ui
.ButtonWidget( {
193 label
: mw
.msg( 'apisandbox-fetch-token' )
195 button
.on( 'click', () => {
199 widgetField
= new OO
.ui
.ActionFieldLayout( widget
, button
, layoutConfig
);
201 widgetField
= new OO
.ui
.FieldLayout( widget
, layoutConfig
);
204 // We need our own click handler on the widget label to
205 // turn off the disablement.
206 widgetField
.$label
.on( 'click', () => {
207 if ( typeof widget
.setDisabled
=== 'function' ) {
208 widget
.setDisabled( false );
210 if ( typeof widget
.focus
=== 'function' ) {
215 // Don't grey out the label when the field is disabled,
216 // it makes it too hard to read and our "disabled"
217 // isn't really disabled.
218 widgetField
.onFieldDisable( false );
219 widgetField
.onFieldDisable = function () {};
221 widgetField
.apiParamIndex
= ppi
.index
;
225 widgetField
: widgetField
,
231 * Update templated parameters in the page
234 * @param {Object} [params] Query parameters for initializing the widgets
236 ApiSandboxLayout
.prototype.updateTemplatedParameters = function ( params
) {
237 const pi
= this.paramInfo
,
238 prefix
= this.prefix
+ pi
.prefix
;
240 if ( !pi
|| !pi
.templatedparameters
.length
) {
244 if ( !$.isPlainObject( params
) ) {
249 // eslint-disable-next-line no-jquery/no-each-util
250 $.each( this.templatedItemsCache
, ( k
, el
) => {
251 if ( el
.widget
.isElementAttached() ) {
256 // This bit duplicates the PHP logic in ApiBase::extractRequestParams().
257 // If you update this, see if that needs updating too.
258 const toProcess
= pi
.templatedparameters
.map( ( info
) => ( {
259 name
: prefix
+ info
.name
,
261 vars
: Object
.assign( {}, info
.templatevars
),
265 const doProcess
= ( placeholder
, target
) => {
266 target
= prefix
+ target
;
268 if ( !this.widgets
[ target
] ) {
269 // The target wasn't processed yet, try the next one.
270 // If all hit this case, the parameter has no expansions.
274 if ( !this.widgets
[ target
].getApiValueForTemplates
) {
275 // Not a multi-valued widget, so it can't have expansions.
279 const values
= this.widgets
[ target
].getApiValueForTemplates();
280 if ( !Array
.isArray( values
) || !values
.length
) {
281 // The target was processed but has no (valid) values.
282 // That means it has no expansions.
286 // Expand this target in the name and all other targets,
287 // then requeue if there are more targets left or create the widget
288 // and add it to the form if all are done.
289 delete p
.vars
[ placeholder
];
290 const usedVars
= p
.usedVars
.concat( [ target
] );
291 placeholder
= '{' + placeholder
+ '}';
292 const done
= $.isEmptyObject( p
.vars
);
293 let index
, container
;
295 container
= Util
.apiBool( p
.info
.deprecated
) ? this.deprecatedItemsFieldset
: this.itemsFieldset
;
296 const items
= container
.getItems();
297 for ( let i
= 0; i
< items
.length
; i
++ ) {
298 if ( items
[ i
].apiParamIndex
!== undefined && items
[ i
].apiParamIndex
> p
.info
.index
) {
304 values
.forEach( ( value
) => {
305 if ( !/^[^{}]*$/.exec( value
) ) {
306 // Skip values that make invalid parameter names
310 const name
= p
.name
.replace( placeholder
, value
);
313 if ( this.templatedItemsCache
[ name
] ) {
314 tmp
= this.templatedItemsCache
[ name
];
316 tmp
= this.makeWidgetFieldLayouts(
317 Object
.assign( {}, p
.info
, { usedTemplateVars
: usedVars
} ), name
319 this.templatedItemsCache
[ name
] = tmp
;
321 delete toRemove
[ name
];
322 if ( !tmp
.widget
.isElementAttached() ) {
323 this.widgets
[ name
] = tmp
.widget
;
324 container
.addItems( [ tmp
.widgetField
, tmp
.helpField
], index
);
325 if ( index
!== undefined ) {
330 tmp
.widget
.setApiValue( Object
.prototype.hasOwnProperty
.call( params
, name
) ? params
[ name
] : undefined );
334 // eslint-disable-next-line no-jquery/no-each-util
335 $.each( p
.vars
, ( k
, v
) => {
336 newVars
[ k
] = v
.replace( placeholder
, value
);
348 while ( toProcess
.length
) {
349 p
= toProcess
.shift();
350 // eslint-disable-next-line no-jquery/no-each-util
351 $.each( p
.vars
, doProcess
);
354 // eslint-disable-next-line no-jquery/no-map-util
355 toRemove
= $.map( toRemove
, ( el
, name
) => {
356 delete this.widgets
[ name
];
357 return [ el
.widgetField
, el
.helpField
];
359 if ( toRemove
.length
) {
360 this.itemsFieldset
.removeItems( toRemove
);
361 this.deprecatedItemsFieldset
.removeItems( toRemove
);
366 * Fetch module information for this page's module, then create UI
368 ApiSandboxLayout
.prototype.loadParamInfo = function () {
369 let dynamicFieldset
, dynamicParamNameWidget
;
370 const removeDynamicParamWidget
= ( name
, item
) => {
371 dynamicFieldset
.removeItems( [ item
] );
372 delete this.widgets
[ name
];
374 addDynamicParamWidget
= () => {
375 // Check name is filled in
376 const name
= dynamicParamNameWidget
.getValue().trim();
378 dynamicParamNameWidget
.focus();
382 if ( this.widgets
[ name
] !== undefined ) {
383 ApiSandbox
.windowManager
.openWindow( 'errorAlert', {
384 title
: Util
.parseMsg( 'apisandbox-dynamic-error-exists', name
),
388 label
: OO
.ui
.msg( 'ooui-dialog-process-dismiss' ),
396 const widget
= Util
.createWidgetForParameter( {
403 const button
= new OO
.ui
.ButtonWidget( {
407 const actionFieldLayout
= new OO
.ui
.ActionFieldLayout(
415 button
.on( 'click', () => {
416 removeDynamicParamWidget( name
, actionFieldLayout
);
418 this.widgets
[ name
] = widget
;
419 dynamicFieldset
.addItems( [ actionFieldLayout
], dynamicFieldset
.getItemCount() - 1 );
422 dynamicParamNameWidget
.setValue( '' );
425 this.$element
.empty()
427 document
.createTextNode(
428 mw
.msg( 'apisandbox-loading', this.displayText
)
430 new OO
.ui
.ProgressBarWidget( { progress
: false } ).$element
433 Util
.fetchModuleInfo( this.apiModule
)
436 deprecatedItems
= [],
438 filterFmModules
= ( v
) => v
.slice( -2 ) !== 'fm' ||
439 !Object
.prototype.hasOwnProperty
.call( ApiSandbox
.availableFormats
, v
.slice( 0, -2 ) );
441 // This is something of a hack. We always want the 'format' and
442 // 'action' parameters from the main module to be specified,
443 // and for 'format' we also want to simplify the dropdown since
444 // we always send the 'fm' variant.
445 if ( this.apiModule
=== 'main' ) {
446 pi
.parameters
.forEach( ( parameter
) => {
447 if ( parameter
.name
=== 'action' ) {
448 parameter
.required
= true;
449 delete parameter
.default;
451 if ( parameter
.name
=== 'format' ) {
452 const types
= parameter
.type
;
453 types
.forEach( ( type
) => {
454 ApiSandbox
.availableFormats
[ type
] = true;
456 parameter
.type
= types
.filter( filterFmModules
);
457 parameter
.default = 'json';
458 parameter
.required
= true;
463 // Hide the 'wrappedhtml' parameter on format modules
464 // and make formatversion default to the latest version for humans
465 // (even though machines get a different default for b/c)
466 if ( pi
.group
=== 'format' ) {
467 pi
.parameters
= pi
.parameters
.filter( ( p
) => p
.name
!== 'wrappedhtml' ).map( ( p
) => {
468 if ( p
.name
=== 'formatversion' ) {
469 // Use the highest numeric value
470 p
.default = p
.type
.reduce( ( prev
, current
) => !isNaN( current
) ? Math
.max( prev
, current
) : prev
);
479 let $desc
= Util
.parseHTML( pi
.description
);
480 if ( pi
.deprecated
!== undefined ) {
481 $desc
= $( '<span>' ).addClass( 'apihelp-deprecated' ).text( mw
.msg( 'api-help-param-deprecated' ) )
482 .add( document
.createTextNode( mw
.msg( 'word-separator' ) ) ).add( $desc
);
484 if ( pi
.internal !== undefined ) {
485 $desc
= $( '<span>' ).addClass( 'apihelp-internal' ).text( mw
.msg( 'api-help-param-internal' ) )
486 .add( document
.createTextNode( mw
.msg( 'word-separator' ) ) ).add( $desc
);
488 items
.push( new OO
.ui
.FieldLayout(
489 new OO
.ui
.Widget( {} ).toggle( false ), {
495 if ( pi
.helpurls
.length
) {
496 buttons
.push( new OO
.ui
.PopupButtonWidget( {
498 label
: mw
.msg( 'apisandbox-helpurls' ),
503 classes
: [ 'mw-apisandbox-popup-help' ],
504 $content
: $( '<ul>' ).append( pi
.helpurls
.map( ( link
) => $( '<li>' ).append( $( '<a>' )
505 .attr( { href
: link
, target
: '_blank' } )
512 if ( pi
.examples
.length
) {
513 buttons
.push( new OO
.ui
.PopupButtonWidget( {
515 label
: mw
.msg( 'apisandbox-examples' ),
520 classes
: [ 'mw-apisandbox-popup-help' ],
521 $content
: $( '<ul>' ).append( pi
.examples
.map( ( example
) => {
522 const $a
= $( '<a>' )
523 .attr( 'href', '#' + example
.query
)
524 .html( example
.description
);
525 $a
.find( 'a' ).contents().unwrap(); // Can't nest links
526 return $( '<li>' ).append( $a
);
532 if ( buttons
.length
) {
533 items
.push( new OO
.ui
.FieldLayout(
534 new OO
.ui
.ButtonGroupWidget( {
536 } ), { align
: 'top' }
540 if ( pi
.parameters
.length
) {
541 const prefix
= this.prefix
+ pi
.prefix
;
542 pi
.parameters
.forEach( ( parameter
) => {
543 const tmpLayout
= this.makeWidgetFieldLayouts( parameter
, prefix
+ parameter
.name
);
544 this.widgets
[ prefix
+ parameter
.name
] = tmpLayout
.widget
;
545 if ( Util
.apiBool( parameter
.deprecated
) ) {
546 deprecatedItems
.push( tmpLayout
.widgetField
, tmpLayout
.helpField
);
548 items
.push( tmpLayout
.widgetField
, tmpLayout
.helpField
);
553 if ( !pi
.parameters
.length
&& !Util
.apiBool( pi
.dynamicparameters
) ) {
554 items
.push( new OO
.ui
.FieldLayout(
555 new OO
.ui
.Widget( {} ).toggle( false ), {
557 label
: Util
.parseMsg( 'apisandbox-no-parameters' )
562 this.$element
.empty();
564 this.itemsFieldset
= new OO
.ui
.FieldsetLayout( {
565 label
: this.displayText
567 this.itemsFieldset
.addItems( items
);
568 this.itemsFieldset
.$element
.appendTo( this.$element
);
570 if ( Util
.apiBool( pi
.dynamicparameters
) ) {
571 dynamicFieldset
= new OO
.ui
.FieldsetLayout();
572 dynamicParamNameWidget
= new OO
.ui
.TextInputWidget( {
573 placeholder
: mw
.msg( 'apisandbox-dynamic-parameters-add-placeholder' )
574 } ).on( 'enter', addDynamicParamWidget
);
575 dynamicFieldset
.addItems( [
576 new OO
.ui
.FieldLayout(
577 new OO
.ui
.Widget( {} ).toggle( false ), {
579 label
: Util
.parseHTML( pi
.dynamicparameters
)
582 new OO
.ui
.ActionFieldLayout(
583 dynamicParamNameWidget
,
584 new OO
.ui
.ButtonWidget( {
587 } ).on( 'click', addDynamicParamWidget
),
589 label
: mw
.msg( 'apisandbox-dynamic-parameters-add-label' ),
596 $( '<legend>' ).text( mw
.msg( 'apisandbox-dynamic-parameters' ) ),
597 dynamicFieldset
.$element
599 .appendTo( this.$element
);
602 this.deprecatedItemsFieldset
= new OO
.ui
.FieldsetLayout().addItems( deprecatedItems
).toggle( false );
603 const $tmp
= $( '<fieldset>' )
604 .toggle( !this.deprecatedItemsFieldset
.isEmpty() )
606 $( '<legend>' ).append(
607 new OO
.ui
.ToggleButtonWidget( {
608 label
: mw
.msg( 'apisandbox-deprecated-parameters' )
609 } ).on( 'change', () => {
610 this.deprecatedItemsFieldset
.toggle();
613 this.deprecatedItemsFieldset
.$element
615 .appendTo( this.$element
);
616 this.deprecatedItemsFieldset
.on( 'add', () => {
617 $tmp
.toggle( !this.deprecatedItemsFieldset
.isEmpty() );
619 this.deprecatedItemsFieldset
.on( 'remove', () => {
620 $tmp
.toggle( !this.deprecatedItemsFieldset
.isEmpty() );
622 // Load stored params, if any, then update the booklet if we
623 // have subpages (or else just update our valid-indicator).
624 const tmp
= this.loadFromQueryParams
;
625 this.loadFromQueryParams
= null;
626 if ( $.isPlainObject( tmp
) ) {
627 this.loadQueryParams( tmp
);
629 this.updateTemplatedParameters();
631 if ( this.getSubpages().length
> 0 ) {
632 ApiSandbox
.updateUI( tmp
);
634 this.apiCheckValid();
636 } ).fail( ( code
, detail
) => {
637 this.$element
.empty()
639 new OO
.ui
.LabelWidget( {
640 label
: mw
.msg( 'apisandbox-load-error', this.apiModule
, detail
),
643 new OO
.ui
.ButtonWidget( {
644 label
: mw
.msg( 'apisandbox-retry' )
645 } ).on( 'click', () => {
646 this.loadParamInfo();
653 * Check that all widgets on the page are in a valid state.
655 * @return {jQuery.Promise[]} One promise for each widget, resolved with `false` if invalid
657 ApiSandboxLayout
.prototype.apiCheckValid = function () {
658 if ( this.paramInfo
=== null ) {
661 // eslint-disable-next-line no-jquery/no-map-util
662 const promises
= $.map( this.widgets
, ( widget
) => widget
.apiCheckValid( ApiSandbox
.suppressErrors
) );
663 $.when( ...promises
).then( ( ...results
) => {
664 this.apiIsValid
= results
.indexOf( false ) === -1;
665 if ( this.getOutlineItem() ) {
666 this.getOutlineItem().setIcon( this.apiIsValid
|| ApiSandbox
.suppressErrors
? null : 'alert' );
667 this.getOutlineItem().setTitle(
668 this.apiIsValid
|| ApiSandbox
.suppressErrors
? '' : mw
.message( 'apisandbox-alert-page' ).plain()
677 * Load form fields from query parameters
679 * @param {Object} params
681 ApiSandboxLayout
.prototype.loadQueryParams = function ( params
) {
682 if ( this.paramInfo
=== null ) {
683 this.loadFromQueryParams
= params
;
685 // eslint-disable-next-line no-jquery/no-each-util
686 $.each( this.widgets
, ( name
, widget
) => {
687 const v
= Object
.prototype.hasOwnProperty
.call( params
, name
) ? params
[ name
] : undefined;
688 widget
.setApiValue( v
);
690 this.updateTemplatedParameters( params
);
695 * Load query params from form fields
697 * @param {Object} params Write query parameters into this object
698 * @param {Object} displayParams Write query parameters for display into this object
699 * @param {Object} ajaxOptions Write options for the API request into this object, in the format
700 * expected by jQuery#ajax.
702 ApiSandboxLayout
.prototype.getQueryParams = function ( params
, displayParams
, ajaxOptions
) {
703 // eslint-disable-next-line no-jquery/no-each-util
704 $.each( this.widgets
, ( name
, widget
) => {
705 let value
= widget
.getApiValue();
706 if ( value
!== undefined ) {
707 params
[ name
] = value
;
708 if ( typeof widget
.getApiValueForDisplay
=== 'function' ) {
709 value
= widget
.getApiValueForDisplay();
711 displayParams
[ name
] = value
;
712 if ( typeof widget
.requiresFormData
=== 'function' && widget
.requiresFormData() ) {
713 ajaxOptions
.contentType
= 'multipart/form-data';
720 * Fetch a list of subpage names loaded by this page
724 ApiSandboxLayout
.prototype.getSubpages = function () {
726 // eslint-disable-next-line no-jquery/no-each-util
727 $.each( this.widgets
, ( name
, widget
) => {
728 if ( typeof widget
.getSubmodules
=== 'function' ) {
729 widget
.getSubmodules().forEach( ( submodule
) => {
731 key
: name
+ '=' + submodule
.value
,
732 path
: submodule
.path
,
733 prefix
: widget
.paramInfo
.submoduleparamprefix
|| ''
741 module
.exports
= ApiSandboxLayout
;