modules/ve-mw/ui/pages/ve.ui.MWSettingsPage.js
/*!
* VisualEditor user interface MWSettingsPage class.
*
* @copyright See AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* MediaWiki meta dialog settings page.
*
* @class
* @extends OO.ui.PageLayout
*
* @constructor
* @param {string} name Unique symbolic name of page
* @param {Object} [config] Configuration options
* @param {jQuery} [config.$overlay] Overlay to render dropdowns in
*/
ve.ui.MWSettingsPage = function VeUiMWSettingsPage( name, config ) {
// Parent constructor
ve.ui.MWSettingsPage.super.apply( this, arguments );
// Properties
this.fragment = null;
this.tocOptionTouched = false;
this.redirectOptionsTouched = false;
this.tableOfContentsTouched = false;
this.label = ve.msg( 'visualeditor-dialog-meta-settings-section' );
this.settingsFieldset = new OO.ui.FieldsetLayout( {
label: ve.msg( 'visualeditor-dialog-meta-settings-label' ),
icon: 'pageSettings'
} );
// Initialization
// Table of Contents items
this.tableOfContents = new OO.ui.FieldLayout(
new OO.ui.ButtonSelectWidget( {
classes: [ 've-test-page-settings-table-of-contents' ]
} )
.addItems( [
new OO.ui.ButtonOptionWidget( {
data: 'mw:PageProp/forcetoc',
label: ve.msg( 'visualeditor-dialog-meta-settings-toc-force' )
} ),
new OO.ui.ButtonOptionWidget( {
data: 'default',
label: ve.msg( 'visualeditor-dialog-meta-settings-toc-default' )
} ),
new OO.ui.ButtonOptionWidget( {
data: 'mw:PageProp/notoc',
label: ve.msg( 'visualeditor-dialog-meta-settings-toc-disable' )
} )
] )
.connect( this, { select: 'onTableOfContentsFieldChange' } ),
{
$overlay: config.$overlay,
align: 'top',
label: ve.msg( 'visualeditor-dialog-meta-settings-toc-label' ),
help: ve.msg( 'visualeditor-dialog-meta-settings-toc-help' )
}
);
// Redirect items
this.enableRedirectInput = new OO.ui.CheckboxInputWidget();
this.enableRedirectField = new OO.ui.FieldLayout(
this.enableRedirectInput,
{
$overlay: config.$overlay,
classes: [ 've-test-page-settings-enable-redirect' ],
align: 'inline',
label: ve.msg( 'visualeditor-dialog-meta-settings-redirect-label' ),
help: ve.msg( 'visualeditor-dialog-meta-settings-redirect-help' )
}
);
this.redirectTargetInput = new mw.widgets.TitleInputWidget( {
placeholder: ve.msg( 'visualeditor-dialog-meta-settings-redirect-placeholder' ),
$overlay: config.$overlay,
api: ve.init.target.getContentApi()
} );
this.redirectTargetInput.$input.attr( 'aria-label', ve.msg( 'visualeditor-dialog-meta-settings-redirect-placeholder' ) );
this.redirectTargetField = new OO.ui.FieldLayout(
this.redirectTargetInput,
{ align: 'top' }
);
this.enableStaticRedirectInput = new OO.ui.CheckboxInputWidget();
this.enableStaticRedirectField = new OO.ui.FieldLayout(
this.enableStaticRedirectInput,
{
$overlay: config.$overlay,
classes: [ 've-test-page-settings-prevent-redirect' ],
align: 'inline',
label: ve.msg( 'visualeditor-dialog-meta-settings-redirect-staticlabel' ),
help: ve.msg( 'visualeditor-dialog-meta-settings-redirect-statichelp' )
}
);
this.enableRedirectInput.connect( this, { change: 'onEnableRedirectChange' } );
this.redirectTargetInput.connect( this, { change: 'onRedirectTargetChange' } );
this.enableStaticRedirectInput.connect( this, { change: 'onEnableStaticRedirectChange' } );
this.metaItemCheckboxes = [
{
metaName: 'mwNoEditSection',
label: ve.msg( 'visualeditor-dialog-meta-settings-noeditsection-label' ),
help: ve.msg( 'visualeditor-dialog-meta-settings-noeditsection-help' ),
classes: [ 've-test-page-settings-noeditsection' ]
},
...ve.ui.MWSettingsPage.static.extraMetaCheckboxes
];
if ( mw.config.get( 'wgNamespaceNumber' ) === mw.config.get( 'wgNamespaceIds' ).category ) {
this.metaItemCheckboxes.push(
{
metaName: 'mwHiddenCategory',
label: ve.msg( 'visualeditor-dialog-meta-settings-hiddencat-label' ),
help: ve.msg( 'visualeditor-dialog-meta-settings-hiddencat-help' )
},
{
metaName: 'mwNoGallery',
label: ve.msg( 'visualeditor-dialog-meta-settings-nogallery-label' ),
help: ve.msg( 'visualeditor-dialog-meta-settings-nogallery-help' )
}
);
}
this.settingsFieldset.addItems( [
this.enableRedirectField,
this.redirectTargetField,
this.enableStaticRedirectField,
this.tableOfContents
] );
this.metaItemCheckboxes.forEach( ( metaItemCheckbox ) => {
metaItemCheckbox.fieldLayout = new OO.ui.FieldLayout(
new OO.ui.CheckboxInputWidget(),
// See above for classes
// eslint-disable-next-line mediawiki/class-doc
{
$overlay: config.$overlay,
classes: metaItemCheckbox.classes,
align: 'inline',
label: metaItemCheckbox.label,
help: metaItemCheckbox.help || ''
}
);
this.settingsFieldset.addItems( [ metaItemCheckbox.fieldLayout ] );
} );
this.$element.append( this.settingsFieldset.$element );
};
/* Inheritance */
OO.inheritClass( ve.ui.MWSettingsPage, OO.ui.PageLayout );
/* Allow extra meta item checkboxes to be added by extensions etc. */
ve.ui.MWSettingsPage.static.extraMetaCheckboxes = [];
/**
* Add a checkbox to the list of changeable page settings
*
* @param {string} metaName The name of the DM meta item
* @param {string} label The label to show next to the checkbox
*/
ve.ui.MWSettingsPage.static.addMetaCheckbox = function ( metaName, label ) {
this.extraMetaCheckboxes.push( { metaName: metaName, label: label } );
};
/* Methods */
/* Table of Contents methods */
/**
* @inheritdoc
*/
ve.ui.MWSettingsPage.prototype.setupOutlineItem = function () {
this.outlineItem
.setIcon( 'pageSettings' )
.setLabel( ve.msg( 'visualeditor-dialog-meta-settings-section' ) );
};
/**
* Handle Table Of Contents display change events.
*/
ve.ui.MWSettingsPage.prototype.onTableOfContentsFieldChange = function () {
this.tableOfContentsTouched = true;
};
/* Redirect methods */
/**
* Handle redirect state change events.
*
* @param {boolean} value Whether a redirect is to be set for this page
*/
ve.ui.MWSettingsPage.prototype.onEnableRedirectChange = function ( value ) {
this.redirectTargetInput.setDisabled( !value );
this.enableStaticRedirectInput.setDisabled( !value );
if ( value ) {
/*
* HACK: When editing a page which has a redirect, the meta dialog
* automatically opens with the settings page's redirect field focused.
* When this happens, we don't want the lookup dropdown to appear until
* the user actually does something.
* Using setTimeout because we need to defer this until after the
* dialog has opened - otherwise its internal lookupDisabled logic will
* fail to have any effect during the actual focusing and calling of
* OO.ui.LookupElement#onLookupInputFocus/OO.ui.LookupElement#populateLookupMenu.
* https://phabricator.wikimedia.org/T137309
*/
setTimeout( () => {
this.redirectTargetInput.focus();
} );
} else {
this.redirectTargetInput.setValue( '' );
this.enableStaticRedirectInput.setSelected( false );
}
this.redirectOptionsTouched = true;
};
/**
* @return {boolean} Whether redirect link is valid.
*/
ve.ui.MWSettingsPage.prototype.checkValidRedirect = function () {
if ( this.enableRedirectInput.isSelected() ) {
const title = this.redirectTargetInput.getValue();
if ( !mw.Title.newFromText( title ) ) {
/*
* TODO more precise error message. Modify the Title.newFromText method in Title.js
* my idea is to in the parse method instead of a boolean return a string with an error message (not an error code since the error string can have parameters),
* then in Title.newFromText instead of returning null, return the error string. Use that string there in setErrors.
* Problem: some methods might depend on it returning null.
* Solution: either make it a new method (Title.newFromTextThrow), or add a an optional parameter to return the error message.
*/
this.redirectTargetField.setErrors( [ mw.msg( 'visualeditor-title-error' ) ] );
return false;
} else {
this.redirectTargetField.setErrors( [] );
}
} else {
this.redirectTargetField.setErrors( [] );
}
return true;
};
/**
* Handle redirect target change events.
*/
ve.ui.MWSettingsPage.prototype.onRedirectTargetChange = function () {
this.redirectOptionsTouched = true;
};
/**
* Handle static redirect state change events.
*/
ve.ui.MWSettingsPage.prototype.onEnableStaticRedirectChange = function () {
this.redirectOptionsTouched = true;
};
/**
* Get the first meta item of a given name
*
* @param {string} name Name of the meta item
* @return {Object|null} Meta item, if any
*/
ve.ui.MWSettingsPage.prototype.getMetaItem = function ( name ) {
return this.fragment.getDocument().getMetaList().getItemsInGroup( name )[ 0 ] || null;
};
/**
* Setup settings page.
*
* @param {ve.dm.SurfaceFragment} fragment Surface fragment
* @param {Object} config
* @param {Object} [config.data] Dialog setup data
* @param {boolean} [config.isReadOnly=false] Dialog is in read-only mode
* @return {jQuery.Promise}
*/
ve.ui.MWSettingsPage.prototype.setup = function ( fragment, config ) {
this.fragment = fragment;
// Table of Contents items
const tableOfContentsField = this.tableOfContents.getField();
const tableOfContentsMetaItem = this.getMetaItem( 'mwTOC' );
const tableOfContentsMode = tableOfContentsMetaItem && tableOfContentsMetaItem.getAttribute( 'property' ) || 'default';
tableOfContentsField
.selectItemByData( tableOfContentsMode )
.setDisabled( config.isReadOnly );
this.tableOfContentsTouched = false;
// Redirect items (disabled states set by change event)
const redirectTargetItem = this.getMetaItem( 'mwRedirect' );
const redirectTarget = redirectTargetItem && redirectTargetItem.getAttribute( 'title' ) || '';
const redirectStatic = this.getMetaItem( 'mwStaticRedirect' );
this.enableRedirectInput
.setSelected( !!redirectTargetItem )
.setDisabled( config.isReadOnly );
this.redirectTargetInput
.setValue( redirectTarget )
.setDisabled( !redirectTargetItem )
.setReadOnly( config.isReadOnly );
this.enableStaticRedirectInput
.setSelected( !!redirectStatic )
.setDisabled( !redirectTargetItem || config.isReadOnly );
this.redirectOptionsTouched = false;
// Simple checkbox items
this.metaItemCheckboxes.forEach( ( metaItemCheckbox ) => {
const isSelected = !!this.getMetaItem( metaItemCheckbox.metaName );
metaItemCheckbox.fieldLayout.getField()
.setSelected( isSelected )
.setDisabled( config.isReadOnly );
} );
return ve.createDeferred().resolve().promise();
};
/**
* Tear down settings page.
*
* @param {Object} [data] Dialog tear down data
*/
ve.ui.MWSettingsPage.prototype.teardown = function ( data ) {
// Data initialisation
data = data || {};
if ( data.action !== 'done' ) {
return;
}
// Table of Contents items
const currentTableOfContents = this.getMetaItem( 'mwTOC' );
const newTableOfContentsData = this.tableOfContents.getField().findSelectedItem();
// Redirect items
const currentRedirectTargetItem = this.getMetaItem( 'mwRedirect' );
const newRedirectData = this.redirectTargetInput.getValue();
const newRedirectItemData = { type: 'mwRedirect', attributes: { title: newRedirectData } };
const currentStaticRedirectItem = this.getMetaItem( 'mwStaticRedirect' );
const newStaticRedirectState = this.enableStaticRedirectInput.isSelected();
// Alter the TOC option flag iff it's been touched & is actually different
if ( this.tableOfContentsTouched ) {
if ( newTableOfContentsData.data === 'default' ) {
if ( currentTableOfContents ) {
this.fragment.removeMeta( currentTableOfContents );
}
} else {
const newTableOfContentsItem = { type: 'mwTOC', attributes: { property: newTableOfContentsData.data } };
if ( !currentTableOfContents ) {
this.fragment.insertMeta( newTableOfContentsItem );
} else if ( currentTableOfContents.getAttribute( 'property' ) !== newTableOfContentsData.data ) {
this.fragment.replaceMeta(
currentTableOfContents,
ve.extendObject( true, {}, currentTableOfContents.getElement(), newTableOfContentsItem )
);
}
}
}
// Alter the redirect options iff they've been touched & are different
if ( this.redirectOptionsTouched ) {
if ( currentRedirectTargetItem ) {
if ( newRedirectData ) {
if ( currentRedirectTargetItem.getAttribute( 'title' ) !== newRedirectData ) {
// There was a redirect and is a new one, but they differ, so replace
this.fragment.replaceMeta(
currentRedirectTargetItem,
ve.extendObject( true, {},
currentRedirectTargetItem.getElement(),
newRedirectItemData
)
);
}
} else {
// There was a redirect and is no new one, so remove
this.fragment.removeMeta( currentRedirectTargetItem );
}
} else {
if ( newRedirectData ) {
// There's no existing redirect but there is a new one, so create
// HACK: Putting this at position 0 so that it works – T63862
this.fragment.insertMeta( newRedirectItemData, 0 );
}
}
if ( currentStaticRedirectItem && ( !newStaticRedirectState || !newRedirectData ) ) {
this.fragment.removeMeta( currentStaticRedirectItem );
}
if ( !currentStaticRedirectItem && newStaticRedirectState && newRedirectData ) {
this.fragment.insertMeta( { type: 'mwStaticRedirect' } );
}
}
this.metaItemCheckboxes.forEach( ( metaItemCheckbox ) => {
const currentItem = this.getMetaItem( metaItemCheckbox.metaName ),
isSelected = metaItemCheckbox.fieldLayout.getField().isSelected();
if ( currentItem && !isSelected ) {
this.fragment.removeMeta( currentItem );
} else if ( !currentItem && isSelected ) {
this.fragment.insertMeta( { type: metaItemCheckbox.metaName } );
}
} );
this.fragment = null;
};
ve.ui.MWSettingsPage.prototype.getFieldsets = function () {
return [
this.settingsFieldset
];
};