mediawiki-extensions-Visual.../modules/ve-mw/ui/widgets/ve.ui.MWTransclusionOutlineTemplateWidget.js
Thiemo Kreuz 8fd1b6d8b8 Defer creating template parameter widget for 0 parameters
From the user's perspective nothing changes. A template without
parameters doesn't show anything. (Technically there was an empty
<div>, but it doesn't do anything.) The moment the first parameter
is added the required sub-widget is created and available from there
on.

This saves loading time and memory, especially when a multi-part
template contains many templates without parameters.

Bug: T298259
Change-Id: Ib1bd2cd0be4fece4acc92e6e5f63133a7986cf81
2022-01-20 12:06:03 +00:00

346 lines
11 KiB
JavaScript

/*!
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Container for template, as rendered in the template dialog sidebar.
*
* @class
* @extends ve.ui.MWTransclusionOutlinePartWidget
*
* @constructor
* @param {ve.dm.MWTemplateModel} template
*/
ve.ui.MWTransclusionOutlineTemplateWidget = function VeUiMWTransclusionOutlineTemplateWidget( template ) {
var spec = template.getSpec();
// Parent constructor
ve.ui.MWTransclusionOutlineTemplateWidget.super.call( this, template, {
icon: 'puzzle',
label: spec.getLabel(),
ariaDescriptionUnselected: ve.msg( 'visualeditor-dialog-transclusion-template-widget-aria' ),
ariaDescriptionSelected: ve.msg( 'visualeditor-dialog-transclusion-template-widget-aria-selected' ),
ariaDescriptionSelectedSingle: ve.msg( 'visualeditor-dialog-transclusion-template-widget-aria-selected-single' )
} );
// Initialization
this.templateModel = template.connect( this, {
add: 'onParameterAddedToTemplateModel',
remove: 'onParameterRemovedFromTemplateModel'
} );
this.initializeParameterSelectWidget();
this.initializeStickyFilters();
};
/* Inheritance */
OO.inheritClass( ve.ui.MWTransclusionOutlineTemplateWidget, ve.ui.MWTransclusionOutlinePartWidget );
/* Events */
/**
* @event focusTemplateParameterById
* @param {string} pageName Unique id of the {@see OO.ui.BookletLayout} page, e.g. something like
* "part_1" or "part_1/param1".
*/
/**
* Triggered when the user uses the search widget at the top to filter the list of parameters.
*
* @event filterParametersById
* @param {Object.<string,boolean>} visibility Keyed by unique id of the parameter, e.g. something
* like "part_1/param1". Note this lists only parameters that are currently shown as a checkbox.
* The spec might contain more parameters (e.g. deprecated).
*/
/* Methods */
/**
* @private
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.initializeParameterSelectWidget = function () {
if ( this.parameters ) {
return;
}
var template = this.templateModel,
spec = template.getSpec();
var parameterNames = this.templateModel
.getAllParametersOrdered()
.filter( function ( paramName ) {
if ( spec.isParameterDeprecated( paramName ) && !template.hasParameter( paramName ) ) {
return false;
}
// Don't create a checkbox for ve.ui.MWParameterPlaceholderPage
return paramName;
} );
if ( !parameterNames.length ) {
return;
}
var $parametersAriaDescription = $( '<span>' )
.text( ve.msg( 'visualeditor-dialog-transclusion-param-selection-aria-description' ) )
.addClass( 've-ui-mwTransclusionOutline-ariaHidden' );
this.parameters = new ve.ui.MWTransclusionOutlineParameterSelectWidget( {
items: parameterNames.map( this.createCheckbox.bind( this ) ),
ariaLabel: ve.msg( 'visualeditor-dialog-transclusion-param-selection-aria-label', spec.getLabel() ),
$ariaDescribedBy: $parametersAriaDescription
} ).connect( this, {
choose: 'onTemplateParameterChoose',
templateParameterSelectionChanged: 'onTemplateParameterSelectionChanged',
change: 'onParameterWidgetListChanged'
} );
this.$element.append(
$parametersAriaDescription,
this.parameters.$element
);
};
/**
* @private
* @param {string} paramName Parameter name or alias as used in the model
* @return {OO.ui.OptionWidget}
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.createCheckbox = function ( paramName ) {
var spec = this.templateModel.getSpec();
return ve.ui.MWTransclusionOutlineParameterSelectWidget.static.createItem( {
required: spec.isParameterRequired( paramName ),
label: spec.getParameterLabel( paramName ),
data: paramName,
selected: this.templateModel.hasParameter( paramName )
} );
};
/**
* @private
* @param {string} paramName
* @return {number}
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.findCanonicalPosition = function ( paramName ) {
var insertAt = 0,
// Note this might include parameters that don't have a checkbox, e.g. deprecated
allParamNames = this.templateModel.getAllParametersOrdered();
for ( var i = 0; i < allParamNames.length; i++ ) {
if ( allParamNames[ i ] === paramName || !this.parameters.items[ insertAt ] ) {
break;
} else if ( this.parameters.items[ insertAt ].getData() === allParamNames[ i ] ) {
insertAt++;
}
}
return insertAt;
};
/**
* @param {string} [paramName] Parameter name to highlight, e.g. "param1". Omit for no highlight.
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.highlightParameter = function ( paramName ) {
if ( this.parameters ) {
this.parameters.highlightParameter( paramName );
}
};
/**
* @inheritDoc
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.setSelected = function ( state ) {
if ( !state && this.isSelected() && this.parameters ) {
this.parameters.highlightItem();
}
ve.ui.MWTransclusionOutlineTemplateWidget.super.prototype.setSelected.call( this, state );
};
/**
* @private
* @param {ve.dm.MWParameterModel} param
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.onParameterAddedToTemplateModel = function ( param ) {
var paramName = param.getName();
// The placeholder (currently) doesn't get a corresponding item in the sidebar
if ( !paramName ) {
return;
}
this.initializeParameterSelectWidget();
this.initializeStickyFilters();
// All parameters known via the spec already have a checkbox
var item = this.parameters.findItemFromData( paramName );
if ( !item ) {
item = this.createCheckbox( paramName );
this.parameters.addItems( [ item ], this.findCanonicalPosition( paramName ) );
// Make sure an active filter is applied to the new checkbox as well
var filter = this.searchWidget && this.searchWidget.getValue();
if ( filter ) {
this.filterParameters( filter );
}
}
item.setSelected( true, true );
// Reset filter, but only if it hides the relevant checkbox
if ( !item.isVisible() ) {
this.searchWidget.setValue( '' );
}
};
/**
* @private
* @param {ve.dm.MWParameterModel} param
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.onParameterRemovedFromTemplateModel = function ( param ) {
this.parameters.markParameterAsUnused( param.getName() );
};
/**
* @private
* @param {OO.ui.OptionWidget} item
* @param {boolean} selected
* @fires focusTemplateParameterById
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.onTemplateParameterChoose = function ( item, selected ) {
this.onTemplateParameterSelectionChanged( item, selected );
var param = this.templateModel.getParameter( item.getData() );
if ( param && selected ) {
this.emit( 'focusTemplateParameterById', param.getId() );
}
};
/**
* @private
* @param {OO.ui.OptionWidget} item
* @param {boolean} selected
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.onTemplateParameterSelectionChanged = function ( item, selected ) {
var paramName = item.getData(),
param = this.templateModel.getParameter( paramName );
if ( !selected ) {
this.templateModel.removeParameter( param );
} else if ( !param ) {
param = new ve.dm.MWParameterModel( this.templateModel, paramName );
this.templateModel.addParameter( param );
}
};
/**
* @private
* @param {OO.ui.Element[]} items
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.onParameterWidgetListChanged = function ( items ) {
this.toggleFilters( items.length );
};
/**
* @private
* @param {number} numberOfParameters
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.toggleFilters = function ( numberOfParameters ) {
this.searchWidget.toggle( numberOfParameters );
this.toggleUnusedWidget.toggle( numberOfParameters );
};
/**
* @private
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.initializeStickyFilters = function () {
if ( this.$stickyFilters || !this.parameters || !this.parameters.getItemCount() ) {
return;
}
this.searchWidget = new OO.ui.SearchInputWidget( {
title: ve.msg( 'visualeditor-dialog-transclusion-filter-title', this.templateModel.getSpec().getLabel() ),
placeholder: ve.msg( 'visualeditor-dialog-transclusion-filter-placeholder' ),
classes: [ 've-ui-mwTransclusionOutlineTemplateWidget-searchWidget' ]
} ).connect( this, {
change: 'filterParameters'
} );
this.searchWidget.$element.attr( 'role', 'search' );
this.toggleUnusedWidget = new ve.ui.MWTransclusionOutlineToggleUnusedWidget();
this.toggleUnusedWidget.connect( this, {
toggleUnusedFields: 'onToggleUnusedFields'
} );
this.infoWidget = new OO.ui.LabelWidget( {
label: ve.msg( 'visualeditor-dialog-transclusion-filter-no-match' ),
classes: [ 've-ui-mwTransclusionOutlineTemplateWidget-no-match' ]
} ).toggle( false );
this.$stickyFilters = $( '<div>' )
.addClass( 've-ui-mwTransclusionOutlineTemplateWidget-sticky' )
.append(
this.header.$element,
this.searchWidget.$element,
this.toggleUnusedWidget.$element
);
this.$element.prepend(
this.$stickyFilters,
this.infoWidget.$element
);
this.toggleFilters( this.parameters.getItemCount() );
};
/**
* Narrows the list of checkboxes down to parameters that match the user's input. The search
* algorithm is modelled after {@see ve.ui.MWParameterSearchWidget.buildIndex}. We search the
* parameter's primary name, aliases, label, and description. But not e.g. the example value.
*
* @private
* @param {string} query user input
* @fires filterParametersById
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.filterParameters = function ( query ) {
var self = this,
template = this.templateModel,
spec = this.templateModel.getSpec(),
visibility = {},
nothingFound = true;
query = query.trim().toLowerCase();
// Note: We can't really cache this because the list of know parameters can change any time
this.parameters.items.forEach( function ( item ) {
var paramName = item.getData(),
placesToSearch = [
spec.getPrimaryParameterName( paramName ),
spec.getParameterLabel( paramName ),
spec.getParameterDescription( paramName )
].concat( spec.getParameterAliases( paramName ) );
var foundSomeMatch = placesToSearch.some( function ( term ) {
// Aliases missed validation for a long time and aren't guaranteed to be strings
return term && typeof term === 'string' && term.toLowerCase().indexOf( query ) !== -1;
} );
item.toggle( foundSomeMatch );
nothingFound = nothingFound && !foundSomeMatch;
var param = template.getParameter( paramName );
if ( param ) {
visibility[ param.getId() ] = foundSomeMatch;
}
} );
this.infoWidget.toggle( nothingFound );
self.emit( 'filterParametersById', visibility );
this.toggleUnusedWidget.toggle( !query );
};
/**
* @private
* @param {boolean} visibility
*/
ve.ui.MWTransclusionOutlineTemplateWidget.prototype.onToggleUnusedFields = function ( visibility ) {
this.parameters.items.forEach( function ( item ) {
item.toggle( visibility || item.isSelected() );
} );
};