const {
EditorView,
Extension,
StateEffect,
StateEffectType,
StateField,
keymap,
showPanel
} = require( 'ext.CodeMirror.lib' );
const CodeMirrorPanel = require( './codemirror.panel.js' );
require( './ext.CodeMirror.data.js' );
/**
* @typedef {boolean|string} CodeMirrorPreferences~PrefValue
*/
/**
* CodeMirrorPreferences is a panel that allows users to configure CodeMirror preferences.
* It is toggled by pressing `Ctrl`-`Shift`-`,` (or `Command`-`Shift`-`,` on macOS).
* Only the commonly used "primary" preferences with a visual effect are shown in the panel,
* in order to reduce in-editor clutter. A "Full preferences" link is provided to open a dialog
* with all available preferences. This can also be opened by pressing `Alt`-`Shift`-`,`.
*
* Note that this code, like MediaWiki Core, refers to the user's preferences as "options".
* In this class, "preferences" refer to the user's preferences for CodeMirror, which
* are stored as a single user 'option' in the database.
*/
class CodeMirrorPreferences extends CodeMirrorPanel {
/**
* @param {CodeMirrorExtensionRegistry} extensionRegistry
* @param {string} mode The CodeMirror mode being used, e.g. 'mediawiki', 'javascript', etc.
* @param {CodeMirrorKeymap} cmKeymap Reference to the keymap instance.
* @param {boolean} [isVisualEditor=false] Whether the VE 2017 editor is being used.
* @fires CodeMirror~'ext.CodeMirror.preferences.ready'
*/
constructor( extensionRegistry, mode, cmKeymap, isVisualEditor = false ) {
super();
/** @type {CodeMirrorExtensionRegistry} */
this.extensionRegistry = extensionRegistry;
/** @type {string} */
this.mode = mode;
/** @type {CodeMirrorKeymap} */
this.keymap = cmKeymap;
/** @type {boolean} */
this.isVisualEditor = isVisualEditor;
/** @type {mw.Api} */
this.api = new mw.Api();
/** @type {StateEffectType} */
this.prefsToggleEffect = StateEffect.define();
/** @type {StateField} */
this.panelStateField = StateField.define( {
create: () => true,
update: ( value, transaction ) => {
for ( const e of transaction.effects ) {
if ( e.is( this.prefsToggleEffect ) ) {
value = e.value;
}
}
return value;
},
// eslint-disable-next-line arrow-body-style
provide: ( stateField ) => {
// eslint-disable-next-line arrow-body-style
return showPanel.from( stateField, ( on ) => {
return on ? () => this.panel : null;
} );
}
} );
/**
* The user's CodeMirror preferences.
*
* @type {Object<string, PrefValue>}
*/
this.preferences = this.fetchPreferences();
/**
* Preferences that are disabled from being changed in the panel or dialog
* when {@link CodeMirrorPreferences#lockPreference lockPreference()} is called.
*
* @type {Set<string>}
*/
this.disabledPreferences = new Set();
/**
* Preferences registered with
* {@link CodeMirrorPreferences#registerCallback registerCallback()}.
* These do not have an associated {@link Extension} and instead execute a callback function
* when the preference is changed.
*
* The Map object is keyed by preference name, with values being the callback function.
*
* @type {Map}
*/
this.callbackPreferences = new Map();
/**
* Preferences that are considered "slow" and indicated as such in the preferences dialog.
*
* @type {Set<string>}
*/
this.slowPreferences = new Set();
/**
* If any feature calls for a form element that's not just a checkbox,
* it needs to be listed here. Key is the preference name, and the value
* is a {@link FormSpecifier form specifier}.
*
* @type {Map<string, FormSpecifier>}
*/
this.formSpecification = new Map();
/**
* Fired just before {@link CodeMirrorPreferences} has been instantiated.
*
* @event CodeMirror~'ext.CodeMirror.preferences.ready'
* @param {CodeMirrorPreferences} preferences
*/
mw.hook( 'ext.CodeMirror.preferences.ready' ).fire( this );
/**
* Preferences that are shown in the preferences panel, as defined by
* `$wgCodeMirrorPrimaryPreferences`. These "primary" preferences should:
* - Be commonly used,
* - Be easy to understand,
* - Have an immediate visual effect, and
* - Limited to a small subset to avoid consuming too much in-editor space.
*
* @type {string[]}
*/
this.primaryPreferences = Object.keys( this.mwConfigPrimary )
.filter( ( prefName ) => !!this.mwConfigPrimary[ prefName ] );
/**
* Configuration for the full preferences dialog.
*
* Each key is a section name having an i18n message key
* of the form `codemirror-prefs-section-<section>`.
*
* Values are arrays of preference names that belong to that section.
* Any preference not listed here will be shown in the "Other" section.
*
* @type {Object}
*/
this.dialogConfig = {
lines: [
'lineNumbering',
'lineWrapping',
'activeLine'
],
characters: [
'specialChars',
'whitespace',
'trailingWhitespace'
],
'code-assistance': [
'autocomplete',
'codeFolding',
'bracketMatching',
'closeBrackets',
'closeTags',
'lint'
]
};
}
/**
* @return {Object<string, PrefValue|Array>}
* @private
*/
getMwConfigDefaults() {
if ( this.mode === 'mediawiki' ) {
return mw.config.get( 'extCodeMirrorConfig' ).defaultPreferences;
}
return mw.config.get( 'extCodeMirrorConfig' ).defaultPreferencesCode;
}
/**
* @type {Object<string, boolean>}
* @private
*/
get mwConfigPrimary() {
return mw.config.get( 'extCodeMirrorConfig' ).primaryPreferences;
}
/**
* Get the name of the user option where CodeMirror preferences are stored for the current mode.
*
* @return {string}
* @private
*/
getOptionName() {
return this.mode === 'mediawiki' ? 'codemirror-preferences' : 'codemirror-preferences-code';
}
/**
* The default CodeMirror preferences in boolean format, derived from
* `$wgCodeMirrorDefaultPreferences` or `$wgCodeMirrorDefaultPreferencesCode`
* depending on the current mode.
*
* @return {Object<string, PrefValue>}
*/
getDefaultPreferences() {
if ( this.defaultPreferences ) {
return this.defaultPreferences;
}
const nsId = mw.config.get( 'wgNamespaceNumber' );
const newDefaults = {};
for ( const prefName in this.getMwConfigDefaults() ) {
const defaultValue = this.getMwConfigDefaults()[ prefName ];
const prefValue = defaultValue === undefined ? false : defaultValue;
if ( typeof prefValue === 'boolean' || typeof prefValue === 'string' ) {
newDefaults[ prefName ] = prefValue;
continue;
}
// Assume an array of namespace IDs (integers) and CM modes (strings).
const supportedNamespace = prefValue.includes( nsId );
const supportedMode = prefValue.includes( this.mode );
newDefaults[ prefName ] = supportedNamespace || supportedMode;
}
/**
* @type {Object<string, PrefValue>}
* @private
*/
this.defaultPreferences = newDefaults;
return this.defaultPreferences;
}
/**
* Fetch the user's CodeMirror preferences from the user options API,
* or clientside storage for unnamed users.
*
* @return {Object<string, PrefValue>}
* @internal
*/
fetchPreferences() {
const storageObj = Object.assign(
{},
this.getDefaultPreferences(),
this.fetchPreferencesInternal()
);
// Convert binary representation to boolean.
const preferences = {};
for ( const prefName in storageObj ) {
// B/c with older "mode ID" system; 2 = mediawiki-only, 3 = all other modes.
let prefValue = storageObj[ prefName ];
if ( prefValue === 2 ) {
prefValue = this.mode === 'mediawiki';
} else if ( prefValue === 3 ) {
prefValue = this.mode !== 'mediawiki';
}
preferences[ prefName ] = typeof prefValue === 'string' ? prefValue : !!prefValue;
}
return preferences;
}
/**
* @return {Object<string, number|string>}
* @internal
* @private
*/
fetchPreferencesInternal() {
if ( mw.user.isNamed() ) {
try {
return JSON.parse( mw.user.options.get( this.getOptionName() ) ) || {};
} catch ( e ) {
// Invalid JSON, or no preferences set.
return {};
}
} else {
return mw.storage.getObject( this.getOptionName() ) || {};
}
}
/**
* Set the given CodeMirror preference and update the user option in the database,
* or clientside storage for unnamed users. Preferences remain "sticky" only for
* the mediawiki (wikitext) mode, or to all non-mediawiki modes.
*
* The `value` is either a boolean (enabled or disabled), or a string. The string can
* be of any form, such serialized JSON. Each individual feature is responsible for
* decoding or normalizing the value, if necessary.
*
* @param {string} key
* @param {PrefValue} value A string value indicates enabled but with the given value.
* @internal
*/
setPreference( key, value ) {
if ( this.getPreference( key ) === value ) {
// No change or pref is disabled, so do nothing.
return;
}
this.preferences[ key ] = value;
if ( this.disabledPreferences.has( key ) ) {
// Preference is locked, so do not update storage or fire hooks.
return;
}
/**
* Run any registered functional callbacks for this preference.
*
* @see CodeMirrorPreferences#callbackPreferences
*/
if ( this.callbackPreferences.has( key ) ) {
this.callbackPreferences.get( key )( value );
}
// Only save the preferences that differ from the defaults,
// and use a binary representation for booleans.
let storageObj = {};
for ( const prefName in this.preferences ) {
if ( this.preferences[ prefName ] !== this.getDefaultPreferences()[ prefName ] ) {
storageObj[ prefName ] = typeof this.preferences[ prefName ] === 'string' ?
this.preferences[ prefName ] :
Number( !!this.preferences[ prefName ] );
}
}
// If preferences wholly match the defaults, delete the user option.
if ( Object.keys( storageObj ).length === 0 ) {
storageObj = null;
}
this.setPreferencesInternal( storageObj );
this.firePreferencesApplyHook( key, value );
}
/**
* @param {Object} storageObj
* @internal
* @private
*/
setPreferencesInternal( storageObj ) {
const stringified = storageObj === null ? null : JSON.stringify( storageObj );
if ( mw.user.isNamed() ) {
this.saveUserOptionInternal( this.getOptionName(), stringified );
} else {
mw.user.options.set( this.getOptionName(), stringified || null );
mw.storage.setObject( this.getOptionName(), storageObj );
}
}
/**
* Update the given user option, overriding the global user option if applicable.
* All CodeMirror user options should also be listed in "GlobalPreferencesAutoPrefs"
* in extension.json so that they are auto-globals (T428887).
*
* @param {string} optionname
* @param {string|number|null} optionvalue
* @internal
*/
saveUserOptionInternal( optionname, optionvalue ) {
this.api.saveOption( optionname, optionvalue, { global: 'update' } );
mw.user.options.set( optionname, optionvalue || null );
}
/**
* Lock a preference to the given value, disabling the option in
* the preferences panel and dialog. The user option in the database
* is **not** changed.
*
* This is useful for integrations that need to disable incompatible extensions.
*
* @param {string} prefName
* @param {EditorView} [view]
* @param {PrefValue} [force=false] Force the extension to be enabled or
* disabled (boolean), or enabled with a given value (string).
* @stable to call
*/
lockPreference( prefName, view, force = false ) {
if ( view ) {
this.extensionRegistry.toggle( prefName, view, force );
}
this.disabledPreferences.add( prefName );
this.setPreference( prefName, force );
this.firePreferencesApplyHook( prefName, force );
}
/**
* @param {string} prefName
* @param {PrefValue} [value]
* @fires CodeMirror~'ext.CodeMirror.preferences.apply'
* @internal
*/
firePreferencesApplyHook( prefName, value ) {
/**
* Fired when a CodeMirror preference is changed or initially applied in a session.
* The preference may not have been saved to the database yet.
*
* @event CodeMirror~'ext.CodeMirror.preferences.apply'
* @param {string} prefName
* @param {PrefValue} prefValue
*/
mw.hook( 'ext.CodeMirror.preferences.apply' ).fire(
prefName,
value === undefined ? this.getPreference( prefName ) : value
);
}
/**
* Get the value of the given CodeMirror preference.
*
* @param {string} prefName
* @return {PrefValue}
* @stable to call
*/
getPreference( prefName ) {
// First check the preference explicitly set by the user.
// For now, we don't allow CodeMirror preferences to override
// config settings in the 2017 editor, since there's no UI to set them.
if ( !this.isVisualEditor && this.preferences[ prefName ] !== undefined ) {
return this.preferences[ prefName ];
}
// Otherwise, go by the defaults.
return this.getDefaultPreferences()[ prefName ];
}
/**
* Check if the user has any preferences that differ from the defaults.
* This is used to determine whether EventLogging should happen.
*
* @return {boolean}
* @internal
*/
hasNonDefaultPreferences() {
for ( const prefName in this.preferences ) {
if ( this.preferences[ prefName ] !== this.getDefaultPreferences()[ prefName ] ) {
return true;
}
}
return false;
}
/**
* Register an {@link Extension} with {@link CodeMirrorExtensionRegistry}
* and enable it if the corresponding preference is set.
*
* @param {string} name
* @param {Extension} extension
* @param {EditorView} view
* @param {boolean} [slow=false] Setting to true will indicate that
* the feature is "potentially slow" in the preferences dialog.
* @internal
*/
registerExtension( name, extension, view, slow = false ) {
this.extensionRegistry.register( name, extension, view, !!this.getPreference( name ) );
if ( slow ) {
this.slowPreferences.add( name );
}
this.firePreferencesApplyHook( name );
}
/**
* Register and enable an {@link Extension} with {@link CodeMirrorExtensionRegistry}
* from a {@link CodeMirrorExtensionRegistry#reconfigValueMap reconfiguration value}
* retrieved from the preference value.
*
* This can be used for initially registering features that use non-boolean values.
*
* @param {string} name
* @param {EditorView} view
* @param {boolean} [slow=false] Setting to true will indicate that
* the feature is "potentially slow" in the preferences dialog.
* @internal
*/
registerExtensionFromValueMap( name, view, slow = false ) {
const prefValue = this.getPreference( name );
if ( typeof prefValue !== 'string' ) {
throw new Error(
`[CodeMirror] Registering "${ name }" from reconfig ` +
'value map with a non-string value'
);
}
this.extensionRegistry.registerFromValueMap( name, view, prefValue );
if ( slow ) {
this.slowPreferences.add( name );
}
this.firePreferencesApplyHook( name, prefValue );
}
/**
* Instead of an {@link Extension}, register a callback function that is executed
* when the preference value is changed. The callback is executed immediately if
* the preference is already set when registered.
*
* @param {string} name
* @param {Function} callback Function that takes the new preference value.
* @param {EditorView} view
* @param {boolean} [slow=false] Setting to true will indicate that
* the feature is "potentially slow" in the preferences dialog.
* @internal
*/
registerCallback( name, callback, view, slow = false ) {
// Register a dummy extension.
this.extensionRegistry.register( name, [], view, this.getPreference( name ) );
this.callbackPreferences.set( name, callback );
if ( this.getPreference( name ) ) {
callback( true );
}
if ( slow ) {
this.slowPreferences.add( name );
}
}
/**
* Toggle an {@link Extension} on or off with {@link CodeMirrorExtensionRegistry}
* and update the preference.
*
* @param {string} name
* @param {EditorView} view
* @internal
*/
toggleExtension( name, view ) {
const prefValue = this.getPreference( name );
if ( typeof prefValue !== 'boolean' ) {
throw new Error( `[CodeMirror] Toggling the non-boolean preference "${ prefValue }"` );
}
const toEnable = !this.getPreference( name );
this.extensionRegistry.toggle( name, view, toEnable );
this.setPreference( name, toEnable );
}
/**
* @inheritDoc
*/
get extension() {
return [
keymap.of( [
// Toggling the preferences panel.
{ key: 'Mod-Shift-,', run: ( view ) => this.toggle( view, true ) },
// Toggling the full preferences dialog.
{ key: 'Alt-Shift-,', run: ( view ) => this.showPreferencesDialog( view ) }
] ),
// At this point the registry contains only extensions managed by CodeMirrorPreferences.
this.extensionRegistry.names.map( ( name ) => {
// Only apply the Extension if the preference (or default pref) is set.
if ( this.getPreference( name ) ) {
this.firePreferencesApplyHook( name );
return this.extensionRegistry.get( name );
}
return this.extensionRegistry.getCompartment( name ).of( [] );
} )
];
}
/**
* @inheritDoc
*/
get panel() {
const container = document.createElement( 'div' );
container.className = 'cm-mw-preferences-panel cm-mw-panel';
container.addEventListener( 'keydown', this.onKeydownPanel.bind( this ) );
const heading = document.createElement( 'div' );
heading.textContent = mw.msg( 'codemirror-prefs-title' );
heading.appendChild( this.getHelpLinks() );
container.appendChild(
this.getFieldsetWithFields(
this.primaryPreferences,
heading
)
);
const closeBtn = this.getButton( 'codemirror-close', { icon: 'close', iconOnly: true } );
closeBtn.classList.add( 'cdx-button--weight-quiet', 'cm-mw-panel-close' );
container.appendChild( closeBtn );
closeBtn.addEventListener( 'click', () => {
this.toggle( this.view, false );
} );
/**
* Fired when the preferences panel is opened or closed.
*
* @event CodeMirror~'ext.CodeMirror.preferences.display'
* @param {HTMLDivElement|null} container The preferences panel container,
* or null if the panel is being closed.
* @internal
*/
mw.hook( 'ext.CodeMirror.preferences.display' ).fire( container );
return {
dom: container,
top: true
};
}
/**
* @return {HTMLSpanElement}
* @private
*/
getHelpLinks() {
const helpSpan = document.createElement( 'span' );
helpSpan.className = 'cm-mw-panel__help';
const helpLink = document.createElement( 'a' );
helpLink.href = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:CodeMirror';
helpLink.target = '_blank';
helpLink.textContent = mw.msg( 'codemirror-prefs-help' );
// Click listener added in CodeMirrorKeymap since we don't have a CodeMirror instance here.
const shortcutLink = document.createElement( 'a' );
shortcutLink.className = 'cm-mw-panel__kbd-help';
shortcutLink.href = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:CodeMirror#Keyboard_shortcuts';
shortcutLink.textContent = mw.msg( 'codemirror-prefs-keymap' );
shortcutLink.onclick = ( e ) => e.preventDefault();
shortcutLink.title = this.keymap.getTitleWithShortcut(
this.keymap.keymapHelpRegistry.other.help
);
const fullPrefsLink = document.createElement( 'a' );
fullPrefsLink.href = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:CodeMirror#Features';
fullPrefsLink.textContent = mw.msg( 'codemirror-prefs-panel-full' );
fullPrefsLink.onclick = ( e ) => {
e.preventDefault();
this.showPreferencesDialog( this.view );
};
fullPrefsLink.title = this.keymap.getTitleWithShortcut(
this.keymap.keymapHelpRegistry.other.fullPreferences
);
helpSpan.append(
' ',
mw.msg( 'parentheses-start' ),
helpLink,
mw.msg( 'pipe-separator' ),
shortcutLink,
mw.msg( 'pipe-separator' ),
fullPrefsLink,
mw.msg( 'parentheses-end' )
);
return helpSpan;
}
/**
* Get a fieldset containing form fields (namely checkboxes) for the given preferences.
*
* @param {string[]} prefNames Names of preferences to include.
* @param {string|HTMLElement} [title] Title of the fieldset.
* @return {HTMLFieldSetElement}
* @private
*/
getFieldsetWithFields(
prefNames,
title = mw.msg( 'codemirror-prefs-title' )
) {
// Only include registered extensions.
prefNames = prefNames.filter(
( name ) => this.extensionRegistry.names.includes( name ) &&
this.preferences[ name ] !== undefined
);
const wrappers = [];
for ( const prefName of prefNames ) {
let wrapper, input;
const formSpecifier = this.formSpecification.get( prefName );
if ( formSpecifier ) {
[ wrapper, input ] = this.getFormField( prefName, formSpecifier );
} else {
[ wrapper, input ] = this.getCheckbox(
prefName,
`codemirror-prefs-${ prefName.toLowerCase() }`,
this.getPreference( prefName )
);
}
if ( this.disabledPreferences.has( prefName ) ) {
input.disabled = true;
}
if ( this.slowPreferences.has( prefName ) ) {
const slowSpan = document.createElement( 'span' );
slowSpan.className = 'cm-mw-slow-feature';
slowSpan.textContent = mw.msg( 'parentheses', mw.msg( 'codemirror-potentially-slow' ) );
const label = wrapper.querySelector( 'label' );
label.append( ' ' );
label.appendChild( slowSpan );
}
wrappers.push( wrapper );
}
return this.getFieldset( title, ...wrappers );
}
/**
* Toggle display of the preferences panel.
*
* @param {EditorView} view
* @param {boolean} [force] Force the panel to open or close.
* @return {boolean}
*/
toggle( view, force ) {
this.view = view;
// If there is no primary CodeMirror instance (e.g. on Special:SecurePoll/translate).
if ( !view.dom.isConnected ) {
this.showPreferencesDialog( view );
return true;
}
const effects = [];
let bool;
// Add the panel state field to the state if it doesn't exist.
if ( !this.view.state.field( this.panelStateField, false ) ) {
effects.push( StateEffect.appendConfig.of( [ this.panelStateField ] ) );
bool = true;
} else {
bool = !this.view.state.field( this.panelStateField );
}
if ( typeof force === 'boolean' ) {
bool = force;
}
effects.push( this.prefsToggleEffect.of( bool ) );
this.view.dispatch( { effects } );
// If the panel is being opened, focus the first input.
if ( bool ) {
this.view.dom.querySelector(
'.cm-mw-preferences-panel input:first-child'
).focus();
} else {
mw.hook( 'ext.CodeMirror.preferences.display' ).fire( null );
}
return true;
}
/**
* Handle keydown events on the preferences panel.
*
* @param {KeyboardEvent} event
*/
onKeydownPanel( event ) {
if ( event.key === 'Escape' ) {
event.preventDefault();
this.toggle( this.view, false );
this.view.focus();
} else if ( event.key === 'Enter' ) {
event.preventDefault();
}
}
/**
* Show the dialog with all available preferences.
*
* @param {EditorView} view
* @return {boolean}
*/
showPreferencesDialog( view ) {
if ( this.dialog ) {
this.animateDialog( true );
return true;
}
this.view = view;
const fieldsets = [];
const sectionPrefs = [];
for ( const [ section, prefs ] of Object.entries( this.dialogConfig ) ) {
sectionPrefs.push( ...prefs );
const fieldset = this.getFieldsetWithFields(
prefs,
// Message here may include but are not limited to:
// * codemirror-prefs-section-appearance
// * codemirror-prefs-section-lines
// * codemirror-prefs-section-characters
// * codemirror-prefs-section-code-assistance
// * codemirror-prefs-section-other
mw.msg( `codemirror-prefs-section-${ section }` )
);
if ( fieldset.children.length > 1 ) {
fieldsets.push( fieldset );
}
}
// Add a fieldset for the remaining preferences.
const otherPrefs = Object.keys( this.preferences ).filter(
( name ) => !sectionPrefs.includes( name ) &&
this.extensionRegistry.isRegistered( name, view )
);
if ( otherPrefs.length > 0 ) {
fieldsets.push(
this.getFieldsetWithFields(
otherPrefs,
mw.msg( 'codemirror-prefs-section-other' )
)
);
}
const resetButton = this.getButton(
'codemirror-prefs-reset',
{ action: 'destructive', weight: 'quiet' }
);
resetButton.addEventListener( 'click', () => {
for ( const prefName in this.getDefaultPreferences() ) {
if ( !this.extensionRegistry.isRegistered( prefName, view ) ) {
continue;
}
const defaultPref = this.getDefaultPreferences()[ prefName ];
this.setPreference( prefName, defaultPref );
this.extensionRegistry.toggle( prefName, view, defaultPref );
}
} );
this.dialog = this.showDialog(
'codemirror-prefs-title',
'preferences',
fieldsets,
resetButton
);
return true;
}
/**
* @inheritDoc
*/
getCheckbox( name, label, checked ) {
const [ wrapper, input ] = super.getCheckbox( name, label, checked );
input.addEventListener( 'change', () => {
this.extensionRegistry.toggle( name, this.view, input.checked );
this.setPreference( name, input.checked );
} );
// Update the checked state when the preference is changed.
mw.hook( 'ext.CodeMirror.preferences.apply' ).add( ( pref, enabled ) => {
if ( pref === name ) {
input.checked = enabled;
}
} );
return [ wrapper, input ];
}
/**
* @inheritDoc
*/
getSelect( name, label, options, selected ) {
const [ wrapper, select ] = super.getSelect( name, label, options, selected );
select.addEventListener( 'change', () => {
this.extensionRegistry.reconfigureFromValueMap( name, this.view, select.value );
this.setPreference( name, select.value );
} );
// Update the selected value when the preference is changed.
mw.hook( 'ext.CodeMirror.preferences.apply' ).add( ( pref, value ) => {
if ( pref === name ) {
select.value = value;
}
} );
return [ wrapper, select ];
}
}
module.exports = CodeMirrorPreferences;