mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-12-13 14:48:16 +00:00
13c9eae26e
This is toggled by pressing Mod-Shift-, (or Command-Shift-, on MacOS), which then puts focus on the preferences panel. It can be closed with the Escape key, just like other CM panels. The CodeMirror class comes with these extension which can be toggled in preferences: * Bracket matching * Line numbering * Line wrapping * Highlight the active line * Show special characters Only bracket matching, line numbering, and line wrapping are available in the 2017 editor. The bidi isolation and template folding extensions are registered in CodeMirrorModeMediaWiki as they are MW-specific. CodeMirrorPreferences' new registerExtension() method allows any consumer of CodeMirror to add any arbitrary extensions to the preferences panel. This is expected to be called *after* CodeMirror has finished initializing. The 'ext.CodeMirror.ready' hook now passes the CodeMirror instance to accommodate this. The preferences are stored as a single user option in the database, called 'codemirror-preferences'. The defaults can be configured with the $wgCodeMirrorDefaultPreferences configuration setting. The sysadmin-facing values are the familiar boolean, but since CodeMirror is widely used, we make extra efforts to reduce the storage footprint (see T54777). This includes only storing preferences that differ from the defaults, and using binary representation instead of boolean values, since the user option is stored as a string. For now, all preferences are ignored in the 2017 editor. In a future patch, we may add some as toggleable Tools in the VE toolbar. Other changes: * Refactor CSS to use a .darkmode() mixin * Add a method to create a CSS-only fieldset in CodeMirrorPanel * Fix Jest tests now that there are more calls to mw.user.options.get() * Adjust Selenium tests to always use CM6 * Adjust Selenium tests to delete test pages (useful for local dev) * Remove unused code Bug: T359498 Change-Id: I70dcf2f49418cea632c452c1266440effad634f3
330 lines
9.2 KiB
JavaScript
330 lines
9.2 KiB
JavaScript
const {
|
|
Compartment,
|
|
EditorView,
|
|
Extension,
|
|
StateEffect,
|
|
StateEffectType,
|
|
StateField,
|
|
keymap,
|
|
showPanel
|
|
} = require( 'ext.CodeMirror.v6.lib' );
|
|
const CodeMirrorPanel = require( './codemirror.panel.js' );
|
|
require( './ext.CodeMirror.data.js' );
|
|
|
|
/**
|
|
* CodeMirrorPreferences is a panel that allows users to configure CodeMirror preferences.
|
|
* It is toggled by pressing `Mod`-`Shift`-`,` (or `Command`+`Shift`+`,` on macOS).
|
|
*
|
|
* 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 {Object} extensionRegistry Key-value pairs of CodeMirror Extensions.
|
|
* @param {boolean} [isVisualEditor=false] Whether the VE 2017 editor is being used.
|
|
*/
|
|
constructor( extensionRegistry, isVisualEditor = false ) {
|
|
super();
|
|
|
|
/** @type {string} */
|
|
this.optionName = 'codemirror-preferences';
|
|
|
|
/** @type {boolean} */
|
|
this.isVisualEditor = isVisualEditor;
|
|
|
|
// VisualEditor only supports a subset of Extensions.
|
|
const veSupportedExtensions = [
|
|
'bracketMatching',
|
|
'lineWrapping',
|
|
'lineNumbering'
|
|
];
|
|
|
|
/**
|
|
* Registry of CodeMirror Extensions that are made available to CodeMirrorPreferences.
|
|
*
|
|
* @type {Object<Extension>}
|
|
*/
|
|
this.extensionRegistry = extensionRegistry;
|
|
|
|
/** @type {mw.Api} */
|
|
this.api = new mw.Api();
|
|
|
|
/**
|
|
* Registry of CodeMirror Compartments that are made available for
|
|
* reconfiguration in CodeMirrorPreferences.
|
|
*
|
|
* @type {Object<Compartment>}
|
|
*/
|
|
this.compartmentRegistry = {};
|
|
for ( const extName of Object.keys( extensionRegistry ) ) {
|
|
if ( isVisualEditor && !veSupportedExtensions.includes( extName ) ) {
|
|
delete this.extensionRegistry[ extName ];
|
|
continue;
|
|
}
|
|
this.compartmentRegistry[ extName ] = new Compartment();
|
|
}
|
|
|
|
/** @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}
|
|
*/
|
|
this.preferences = this.fetchPreferences();
|
|
}
|
|
|
|
/**
|
|
* The default CodeMirror preferences, as defined by `$wgCodeMirrorPreferences`.
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
get defaultPreferences() {
|
|
return mw.config.get( 'extCodeMirrorConfig' ).defaultPreferences;
|
|
}
|
|
|
|
/**
|
|
* Fetch the user's CodeMirror preferences from the user options API,
|
|
* or clientside storage for unnamed users.
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
fetchPreferences() {
|
|
let storageObj = this.defaultPreferences;
|
|
|
|
if ( mw.user.isNamed() ) {
|
|
try {
|
|
storageObj = JSON.parse( mw.user.options.get( this.optionName ) );
|
|
} catch ( e ) {
|
|
// Invalid JSON, or no preferences set.
|
|
}
|
|
} else {
|
|
storageObj = mw.storage.getObject( this.optionName ) || this.defaultPreferences;
|
|
}
|
|
|
|
storageObj = Object.assign( {}, this.defaultPreferences, storageObj );
|
|
|
|
// Convert binary representation to boolean.
|
|
const preferences = {};
|
|
for ( const prefName in storageObj ) {
|
|
preferences[ prefName ] = !!storageObj[ prefName ];
|
|
}
|
|
return preferences;
|
|
}
|
|
|
|
/**
|
|
* Set the given CodeMirror preference and update the user option in the database,
|
|
* or clientside storage for unnamed users.
|
|
*
|
|
* @param {string} key
|
|
* @param {Mixed} value
|
|
*/
|
|
setPreference( key, value ) {
|
|
this.preferences[ key ] = value;
|
|
|
|
// Only save the preferences that differ from the defaults,
|
|
// and use a binary representation for storage. This is to prevent
|
|
// bloat of the user_properties table (T54777).
|
|
const storageObj = {};
|
|
for ( const prefName in this.preferences ) {
|
|
if ( !!this.preferences[ prefName ] !== !!this.defaultPreferences[ prefName ] ) {
|
|
storageObj[ prefName ] = this.preferences[ prefName ] ? 1 : 0;
|
|
}
|
|
}
|
|
mw.user.options.set( this.optionName, JSON.stringify( storageObj ) );
|
|
|
|
// Save the preferences to the database or clientside storage.
|
|
if ( mw.user.isNamed() ) {
|
|
this.api.saveOption( this.optionName, JSON.stringify( storageObj ) );
|
|
} else {
|
|
mw.storage.setObject( this.optionName, storageObj );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the value of the given CodeMirror preference.
|
|
*
|
|
* @param {string} prefName
|
|
* @return {boolean}
|
|
*/
|
|
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.
|
|
|
|
// Some preferences can be set per-namespace through wiki configuration.
|
|
// Values are an array of namespace IDs, [] to disable everywhere,
|
|
// or null to enable everywhere.
|
|
const namespacePrefs = [ 'lineNumbering', 'templateFolding' ];
|
|
if ( namespacePrefs.includes( prefName ) ) {
|
|
const namespaces = mw.config.get( 'extCodeMirrorConfig' )[ prefName + 'Namespaces' ];
|
|
return !namespaces || namespaces.includes( mw.config.get( 'wgNamespaceNumber' ) );
|
|
}
|
|
|
|
// These preferences do not have configuration settings.
|
|
return this.defaultPreferences[ prefName ];
|
|
}
|
|
|
|
/**
|
|
* Register an {@link Extension} with CodeMirrorPreferences, along with a
|
|
* corresponding {@link Compartment} so that the Extension can be reconfigured.
|
|
*
|
|
* @param {string} name
|
|
* @param {Extension} extension
|
|
* @param {EditorView} view
|
|
* @internal
|
|
*/
|
|
registerExtension( name, extension, view ) {
|
|
this.extensionRegistry[ name ] = extension;
|
|
this.compartmentRegistry[ name ] = new Compartment();
|
|
view.dispatch( {
|
|
effects: StateEffect.appendConfig.of(
|
|
this.compartmentRegistry[ name ].of(
|
|
this.getPreference( name ) ? this.extensionRegistry[ name ] : []
|
|
)
|
|
)
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
get extension() {
|
|
return [
|
|
keymap.of( {
|
|
key: 'Mod-Shift-,',
|
|
run: ( view ) => {
|
|
this.view = view;
|
|
const effects = [ this.prefsToggleEffect.of( true ) ];
|
|
if ( !this.view.state.field( this.panelStateField, false ) ) {
|
|
effects.push( StateEffect.appendConfig.of( [ this.panelStateField ] ) );
|
|
}
|
|
this.view.dispatch( { effects } );
|
|
this.view.dom.querySelector(
|
|
'.cm-mw-preferences-panel input:first-child'
|
|
).focus();
|
|
return true;
|
|
}
|
|
} ),
|
|
// Compartmentalized extensions
|
|
Object.keys( this.extensionRegistry ).map(
|
|
( name ) => this.compartmentRegistry[ name ].of(
|
|
// Only apply the extension if the preference (or default pref) is enabled.
|
|
this.getPreference( name ) ? this.extensionRegistry[ name ] : []
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
get panel() {
|
|
const container = document.createElement( 'div' );
|
|
container.className = 'cm-mw-preferences-panel cm-mw-panel';
|
|
container.addEventListener( 'keydown', this.onKeydown.bind( this ) );
|
|
|
|
const wrappers = [];
|
|
for ( const prefName in this.extensionRegistry ) {
|
|
const [ wrapper ] = this.getCheckbox(
|
|
prefName,
|
|
`codemirror-prefs-${ prefName.toLowerCase() }`,
|
|
this.getPreference( prefName )
|
|
);
|
|
wrappers.push( wrapper );
|
|
}
|
|
const fieldset = this.getFieldset( mw.msg( 'codemirror-prefs-title' ), ...wrappers );
|
|
container.appendChild( fieldset );
|
|
|
|
const closeBtn = this.getButton( 'codemirror-close', 'close', true );
|
|
closeBtn.classList.add( 'cdx-button--weight-quiet', 'cm-mw-panel-close' );
|
|
container.appendChild( closeBtn );
|
|
closeBtn.addEventListener( 'click', () => {
|
|
this.toggle( this.view, false );
|
|
} );
|
|
|
|
return {
|
|
dom: container,
|
|
top: true
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Toggle display of the preferences panel.
|
|
*
|
|
* @param {EditorView} view
|
|
* @param {boolean} [force]
|
|
* @return {boolean}
|
|
*/
|
|
toggle( view, force ) {
|
|
this.view = view;
|
|
const bool = typeof force === 'boolean' ?
|
|
force :
|
|
!this.view.state.field( this.panelStateField );
|
|
this.view.dispatch( {
|
|
effects: this.prefsToggleEffect.of( bool )
|
|
} );
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handle keydown events on the preferences panel.
|
|
*
|
|
* @param {KeyboardEvent} event
|
|
*/
|
|
onKeydown( event ) {
|
|
if ( event.key === 'Escape' ) {
|
|
event.preventDefault();
|
|
this.toggle( this.view, false );
|
|
this.view.focus();
|
|
} else if ( event.key === 'Enter' ) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
getCheckbox( name, label, checked ) {
|
|
const compartment = this.compartmentRegistry[ name ];
|
|
const extension = this.extensionRegistry[ name ];
|
|
const [ wrapper, input ] = super.getCheckbox( name, label, checked );
|
|
input.addEventListener( 'change', () => {
|
|
this.view.dispatch( {
|
|
effects: compartment.reconfigure( input.checked ? extension : [] )
|
|
} );
|
|
this.setPreference( name, input.checked );
|
|
} );
|
|
return [ wrapper, input ];
|
|
}
|
|
}
|
|
|
|
module.exports = CodeMirrorPreferences;
|