Merge "jquery.tablesorter: Silence an expected "sort-rowspan-error" warning"
[mediawiki.git] / resources / src / mediawiki.special.apisandbox / ApiSandboxLayout.js
blob9a21adf5a88ead2bfdf2eeb18cc997129f10ff1e
1 const ParamLabelWidget = require( './ParamLabelWidget.js' ),
2 OptionalParamWidget = require( './OptionalParamWidget.js' ),
3 ApiSandbox = require( './ApiSandbox.js' ),
4 Util = require( './Util.js' );
6 /**
7 * PageLayout for API modules
9 * @class
10 * @private
11 * @extends OO.ui.PageLayout
12 * @constructor
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;
23 this.widgets = {};
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 );
30 this.loadParamInfo();
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()
44 /**
45 * Create a widget and the FieldLayouts it needs
47 * @private
48 * @param {Object} ppi API paraminfo data for the parameter
49 * @param {string} name API parameter name
50 * @return {Object}
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 );
63 } );
66 const helpLabel = new ParamLabelWidget();
68 let $tmp = Util.parseHTML( ppi.description );
69 $tmp.filter( 'dl' ).makeCollapsible( {
70 collapsed: true
71 } ).children( '.mw-collapsible-toggle' ).each( ( i, el ) => {
72 const $el = $( el );
73 $el.parent().prev( 'p' ).append( $el );
74 } );
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 ) );
82 let flag = true;
83 let count = Infinity;
84 let tmp;
85 switch ( ppi.type ) {
86 case 'namespace':
87 flag = false;
88 count = mw.config.get( 'wgFormattedNamespaces' ).length;
89 break;
91 case 'limit':
92 tmp = [
93 mw.message(
94 'paramvalidator-help-type-number-minmax', 1,
95 widget.paramInfo.min, widget.paramInfo.apiSandboxMax
96 ).parse(),
97 mw.message( 'apisandbox-param-limit' ).parse()
99 helpLabel.addInfo( Util.parseHTML( tmp.join( mw.msg( 'word-separator' ) ) ) );
100 break;
102 case 'integer':
103 tmp = '';
104 if ( ppi.min !== undefined ) {
105 tmp += 'min';
107 if ( ppi.max !== undefined ) {
108 tmp += 'max';
110 if ( tmp !== '' ) {
111 helpLabel.addInfo(
112 Util.parseMsg(
113 'paramvalidator-help-type-number-' + tmp,
114 Util.apiBool( ppi.multi ) ? 2 : 1,
115 ppi.min, ppi.max
119 break;
121 default:
122 if ( Array.isArray( ppi.type ) ) {
123 flag = false;
124 count = ppi.type.length;
126 break;
128 if ( Util.apiBool( ppi.multi ) ) {
129 tmp = [];
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 ) {
139 tmp.push(
140 mw.message( 'paramvalidator-help-multi-max', ppi.lowlimit, ppi.highlimit ).parse()
143 if ( tmp.length ) {
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 ) {
154 $tmp = $();
155 for ( let j = 0, l = ppi.usedTemplateVars.length; j < l; j++ ) {
156 $tmp = $tmp.add( $( '<var>' ).text( ppi.usedTemplateVars[ j ] ) );
157 if ( j === l - 2 ) {
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() );
164 helpLabel.addInfo(
165 Util.parseMsg(
166 'apisandbox-templated-parameter-reason',
167 ppi.usedTemplateVars.length,
168 $tmp
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(
177 helpLabel,
179 align: 'top',
180 classes: [ 'mw-apisandbox-help-field' ]
184 const layoutConfig = {
185 align: 'left',
186 classes: [ 'mw-apisandbox-widget-field' ],
187 label: name
190 let widgetField;
191 if ( ppi.tokentype ) {
192 const button = new OO.ui.ButtonWidget( {
193 label: mw.msg( 'apisandbox-fetch-token' )
194 } );
195 button.on( 'click', () => {
196 widget.fetchToken();
197 } );
199 widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
200 } else {
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' ) {
211 widget.focus();
213 } );
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;
223 return {
224 widget: widget,
225 widgetField: widgetField,
226 helpField: helpField
231 * Update templated parameters in the page
233 * @private
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 ) {
241 return;
244 if ( !$.isPlainObject( params ) ) {
245 params = null;
248 let toRemove = {};
249 // eslint-disable-next-line no-jquery/no-each-util
250 $.each( this.templatedItemsCache, ( k, el ) => {
251 if ( el.widget.isElementAttached() ) {
252 toRemove[ k ] = el;
254 } );
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,
260 info: info,
261 vars: Object.assign( {}, info.templatevars ),
262 usedVars: []
263 } ) );
264 let p;
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.
271 return true;
274 if ( !this.widgets[ target ].getApiValueForTemplates ) {
275 // Not a multi-valued widget, so it can't have expansions.
276 return false;
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.
283 return false;
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;
294 if ( done ) {
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 ) {
299 index = i;
300 break;
304 values.forEach( ( value ) => {
305 if ( !/^[^{}]*$/.exec( value ) ) {
306 // Skip values that make invalid parameter names
307 return;
310 const name = p.name.replace( placeholder, value );
311 if ( done ) {
312 let tmp;
313 if ( this.templatedItemsCache[ name ] ) {
314 tmp = this.templatedItemsCache[ name ];
315 } else {
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 ) {
326 index += 2;
329 if ( params ) {
330 tmp.widget.setApiValue( Object.prototype.hasOwnProperty.call( params, name ) ? params[ name ] : undefined );
332 } else {
333 const newVars = {};
334 // eslint-disable-next-line no-jquery/no-each-util
335 $.each( p.vars, ( k, v ) => {
336 newVars[ k ] = v.replace( placeholder, value );
337 } );
338 toProcess.push( {
339 name: name,
340 info: p.info,
341 vars: newVars,
342 usedVars: usedVars
343 } );
345 } );
346 return false;
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 ];
358 } );
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();
377 if ( name === '' ) {
378 dynamicParamNameWidget.focus();
379 return;
382 if ( this.widgets[ name ] !== undefined ) {
383 ApiSandbox.windowManager.openWindow( 'errorAlert', {
384 title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
385 actions: [
387 action: 'accept',
388 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
389 flags: 'primary'
392 } );
393 return;
396 const widget = Util.createWidgetForParameter( {
397 name: name,
398 type: 'string',
399 default: ''
400 }, {
401 nooptional: true
402 } );
403 const button = new OO.ui.ButtonWidget( {
404 icon: 'trash',
405 flags: 'destructive'
406 } );
407 const actionFieldLayout = new OO.ui.ActionFieldLayout(
408 widget,
409 button,
411 label: name,
412 align: 'left'
415 button.on( 'click', () => {
416 removeDynamicParamWidget( name, actionFieldLayout );
417 } );
418 this.widgets[ name ] = widget;
419 dynamicFieldset.addItems( [ actionFieldLayout ], dynamicFieldset.getItemCount() - 1 );
420 widget.focus();
422 dynamicParamNameWidget.setValue( '' );
425 this.$element.empty()
426 .append(
427 document.createTextNode(
428 mw.msg( 'apisandbox-loading', this.displayText )
430 new OO.ui.ProgressBarWidget( { progress: false } ).$element
433 Util.fetchModuleInfo( this.apiModule )
434 .done( ( pi ) => {
435 const items = [],
436 deprecatedItems = [],
437 buttons = [],
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;
455 } );
456 parameter.type = types.filter( filterFmModules );
457 parameter.default = 'json';
458 parameter.required = true;
460 } );
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 );
471 p.required = true;
473 return p;
474 } );
477 this.paramInfo = pi;
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 ), {
490 align: 'top',
491 label: $desc
493 ) );
495 if ( pi.helpurls.length ) {
496 buttons.push( new OO.ui.PopupButtonWidget( {
497 $overlay: true,
498 label: mw.msg( 'apisandbox-helpurls' ),
499 icon: 'help',
500 popup: {
501 width: 'auto',
502 padded: true,
503 classes: [ 'mw-apisandbox-popup-help' ],
504 $content: $( '<ul>' ).append( pi.helpurls.map( ( link ) => $( '<li>' ).append( $( '<a>' )
505 .attr( { href: link, target: '_blank' } )
506 .text( link )
507 ) ) )
509 } ) );
512 if ( pi.examples.length ) {
513 buttons.push( new OO.ui.PopupButtonWidget( {
514 $overlay: true,
515 label: mw.msg( 'apisandbox-examples' ),
516 icon: 'code',
517 popup: {
518 width: 'auto',
519 padded: true,
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 );
527 } ) )
529 } ) );
532 if ( buttons.length ) {
533 items.push( new OO.ui.FieldLayout(
534 new OO.ui.ButtonGroupWidget( {
535 items: buttons
536 } ), { align: 'top' }
537 ) );
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 );
547 } else {
548 items.push( tmpLayout.widgetField, tmpLayout.helpField );
550 } );
553 if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
554 items.push( new OO.ui.FieldLayout(
555 new OO.ui.Widget( {} ).toggle( false ), {
556 align: 'top',
557 label: Util.parseMsg( 'apisandbox-no-parameters' )
559 ) );
562 this.$element.empty();
564 this.itemsFieldset = new OO.ui.FieldsetLayout( {
565 label: this.displayText
566 } );
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 ), {
578 align: 'top',
579 label: Util.parseHTML( pi.dynamicparameters )
582 new OO.ui.ActionFieldLayout(
583 dynamicParamNameWidget,
584 new OO.ui.ButtonWidget( {
585 icon: 'add',
586 flags: 'progressive'
587 } ).on( 'click', addDynamicParamWidget ),
589 label: mw.msg( 'apisandbox-dynamic-parameters-add-label' ),
590 align: 'left'
593 ] );
594 $( '<fieldset>' )
595 .append(
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() )
605 .append(
606 $( '<legend>' ).append(
607 new OO.ui.ToggleButtonWidget( {
608 label: mw.msg( 'apisandbox-deprecated-parameters' )
609 } ).on( 'change', () => {
610 this.deprecatedItemsFieldset.toggle();
611 } ).$element
613 this.deprecatedItemsFieldset.$element
615 .appendTo( this.$element );
616 this.deprecatedItemsFieldset.on( 'add', () => {
617 $tmp.toggle( !this.deprecatedItemsFieldset.isEmpty() );
618 } );
619 this.deprecatedItemsFieldset.on( 'remove', () => {
620 $tmp.toggle( !this.deprecatedItemsFieldset.isEmpty() );
621 } );
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 );
628 } else {
629 this.updateTemplatedParameters();
631 if ( this.getSubpages().length > 0 ) {
632 ApiSandbox.updateUI( tmp );
633 } else {
634 this.apiCheckValid();
636 } ).fail( ( code, detail ) => {
637 this.$element.empty()
638 .append(
639 new OO.ui.LabelWidget( {
640 label: mw.msg( 'apisandbox-load-error', this.apiModule, detail ),
641 classes: [ 'error' ]
642 } ).$element,
643 new OO.ui.ButtonWidget( {
644 label: mw.msg( 'apisandbox-retry' )
645 } ).on( 'click', () => {
646 this.loadParamInfo();
647 } ).$element
649 } );
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 ) {
659 return [];
660 } else {
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()
671 } );
672 return promises;
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;
684 } else {
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 );
689 } );
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';
716 } );
720 * Fetch a list of subpage names loaded by this page
722 * @return {Array}
724 ApiSandboxLayout.prototype.getSubpages = function () {
725 const ret = [];
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 ) => {
730 ret.push( {
731 key: name + '=' + submodule.value,
732 path: submodule.path,
733 prefix: widget.paramInfo.submoduleparamprefix || ''
734 } );
735 } );
737 } );
738 return ret;
741 module.exports = ApiSandboxLayout;