mediawiki-extensions-CodeMi.../resources/legacy/modules/ve-cm/ve.ui.CodeMirrorAction.js
MusikAnimal 7d3482f89e Isolate build step to CM6 library and restructure files to work with RL
CodeMirror 6 requires the use of NPM, but we can still bundle all CM
packages into one file, and then everything else (i.e. our code) is
managed by ResourceLoader as per usual. This makes contribution
considerably easier as we no longer need a build step for each change.

CM5 files are now under resources/legacy, and the CM6 files are moved to
the root of the resources/ directory. Only one file,
codemirror.bundle.js, is managed by Rollup, while everything else is RL.
The Rollup output for now is put under resources/lib/ alongside the CM5
upstream files.

This patch is *mostly* renames of files, along with changing ECMAScript
Module (ESM) syntax into the CommonJS style that ResourceLoader prefers.
We also remove more modern JS syntax (i.e. private class methods) that
we were able to use before because we had a build step with Babel.

This patch should effectively make no user-facing changes, or to the
ResourceLoader modules we offer in Extension:CodeMirror.

Finally, bump version in extension.json to 6, to match the upstream lib,
and add Bhsd as an author :-)

Bug: T368053
Change-Id: Ie258e49f5df8db23a7344ac3c4c9300aaa991042
2024-07-31 22:45:48 -04:00

288 lines
8.7 KiB
JavaScript

/*!
* VisualEditor UserInterface CodeMirrorAction class.
*/
/**
* CodeMirror action
*
* @class
* @extends ve.ui.Action
* @constructor
* @param {ve.ui.Surface} surface Surface to act on
*/
ve.ui.CodeMirrorAction = function VeUiCodeMirrorAction() {
// Parent constructor
ve.ui.CodeMirrorAction.super.apply( this, arguments );
};
/* Inheritance */
OO.inheritClass( ve.ui.CodeMirrorAction, ve.ui.Action );
/* Static Properties */
ve.ui.CodeMirrorAction.static.name = 'codeMirror';
/**
* @inheritdoc
*/
ve.ui.CodeMirrorAction.static.methods = [ 'toggle' ];
/* Methods */
/**
* @return {boolean}
*/
ve.ui.CodeMirrorAction.static.isLineNumbering = function () {
// T285660: Backspace related bug on Android browsers as of 2021
if ( /Android\b/.test( navigator.userAgent ) ) {
return false;
}
const namespaces = mw.config.get( 'wgCodeMirrorLineNumberingNamespaces' );
// Set to [] to disable everywhere, or null to enable everywhere
return !namespaces ||
namespaces.indexOf( mw.config.get( 'wgNamespaceNumber' ) ) !== -1;
};
/**
* @method
* @param {boolean} [enable] State to force toggle to, inverts current state if undefined
* @return {boolean} Action was executed
*/
ve.ui.CodeMirrorAction.prototype.toggle = function ( enable ) {
const surface = this.surface,
surfaceView = surface.getView(),
doc = surface.getModel().getDocument();
if ( !surface.mirror && enable !== false ) {
surface.mirror = true;
mw.loader.using( [
'ext.CodeMirror.lib',
'ext.CodeMirror.mode.mediawiki',
'jquery.client'
] ).then( () => {
const config = mw.config.get( 'extCodeMirrorConfig' );
if ( !surface.mirror ) {
// Action was toggled to false since promise started
return;
}
mw.loader.using( config.pluginModules, () => {
if ( !surface.mirror ) {
// Action was toggled to false since promise started
return;
}
const tabSizeValue = surfaceView.documentView.documentNode.$element.css( 'tab-size' );
const cmOptions = {
value: surface.getDom(),
mwConfig: config,
readOnly: 'nocursor',
lineWrapping: true,
scrollbarStyle: 'null',
specialChars: /^$/,
viewportMargin: 5,
tabSize: tabSizeValue ? +tabSizeValue : 8,
// select mediawiki as text input mode
mode: 'text/mediawiki',
extraKeys: {
Tab: false,
'Shift-Tab': false
}
};
cmOptions.matchBrackets = {
highlightNonMatching: false,
maxHighlightLineLength: 10000
};
if ( ve.ui.CodeMirrorAction.static.isLineNumbering() ) {
Object.assign( cmOptions, {
// Set up a special "padding" gutter to create space between the line numbers
// and page content. The first column name is a magic constant which causes
// the built-in line number gutter to appear in the desired, leftmost position.
gutters: [
'CodeMirror-linenumbers',
'CodeMirror-linenumber-padding'
],
lineNumbers: true
} );
}
surface.mirror = CodeMirror( surfaceView.$element[ 0 ], cmOptions );
// The VE/CM overlay technique only works with monospace fonts (as we use width-changing bold as a highlight)
// so revert any editfont user preference
surfaceView.$element.removeClass( 'mw-editfont-sans-serif mw-editfont-serif' ).addClass( 'mw-editfont-monospace' );
if ( mw.user.options.get( 'usecodemirror-colorblind' ) ) {
surfaceView.$element.addClass( 'cm-mw-colorblind-colors' );
}
const profile = $.client.profile();
const supportsTransparentText = 'WebkitTextFillColor' in document.body.style &&
// Disable on Firefox+OSX (T175223)
!( profile.layout === 'gecko' && profile.platform === 'mac' );
surfaceView.$documentNode.addClass(
supportsTransparentText ?
've-ce-documentNode-codeEditor-webkit-hide' :
've-ce-documentNode-codeEditor-hide'
);
if ( cmOptions.lineNumbers ) {
// Transfer gutter width to VE overlay.
const updateGutter = ( cmDisplay ) => {
surfaceView.$documentNode.css( 'margin-left', cmDisplay.gutters.offsetWidth );
};
CodeMirror.on( surface.mirror.display, 'updateGutter', updateGutter );
updateGutter( surface.mirror.display );
}
/* Events */
// As the action is regenerated each time, we need to store bound listeners
// in the mirror for later disconnection.
surface.mirror.veTransactionListener = this.onDocumentPrecommit.bind( this );
surface.mirror.veLangChangeListener = this.onLangChange.bind( this );
surface.mirror.veSelectListener = this.onSelect.bind( this );
doc.on( 'precommit', surface.mirror.veTransactionListener );
surfaceView.getDocument().on( 'langChange', surface.mirror.veLangChangeListener );
surface.getModel().on( 'select', surface.mirror.veSelectListener );
this.onLangChange();
ve.init.target.once( 'surfaceReady', () => {
if ( surface.mirror ) {
surface.mirror.refresh();
}
} );
} );
} );
} else if ( surface.mirror && enable !== true ) {
if ( surface.mirror !== true ) {
doc.off( 'precommit', surface.mirror.veTransactionListener );
surfaceView.getDocument().off( 'langChange', surface.mirror.veLangChangeListener );
surface.getModel().off( 'select', surface.mirror.veSelectListener );
// Restore edit-font
// eslint-disable-next-line mediawiki/class-doc
surfaceView.$element.removeClass( 'mw-editfont-monospace' ).addClass( 'mw-editfont-' + mw.user.options.get( 'editfont' ) );
surfaceView.$documentNode.removeClass(
've-ce-documentNode-codeEditor-webkit-hide ve-ce-documentNode-codeEditor-hide'
);
// Reset gutter.
surfaceView.$documentNode.css( 'margin-left', '' );
const mirrorElement = surface.mirror.getWrapperElement();
mirrorElement.parentNode.removeChild( mirrorElement );
}
surface.mirror = null;
}
return true;
};
/**
* Handle select events from the surface model
*
* @param {ve.dm.Selection} selection
*/
ve.ui.CodeMirrorAction.prototype.onSelect = function ( selection ) {
const range = selection.getCoveringRange();
// Do not re-trigger bracket matching as long as something is selected
if ( !range || !range.isCollapsed() ) {
return;
}
this.surface.mirror.setSelection( this.getPosFromOffset( range.from ) );
};
/**
* Handle langChange events from the document view
*/
ve.ui.CodeMirrorAction.prototype.onLangChange = function () {
const surface = this.surface,
doc = surface.getView().getDocument(),
dir = doc.getDir(), lang = doc.getLang();
surface.mirror.setOption( 'direction', dir );
// Set the wrapper to the appropriate language (T341342)
surface.mirror.getWrapperElement().setAttribute( 'lang', lang );
};
/**
* Handle precommit events from the document.
*
* The document is still in it's 'old' state before the transaction
* has been applied at this point.
*
* @param {ve.dm.Transaction} tx
*/
ve.ui.CodeMirrorAction.prototype.onDocumentPrecommit = function ( tx ) {
const replacements = [],
store = this.surface.getModel().getDocument().getStore(),
mirror = this.surface.mirror;
let offset = 0;
tx.operations.forEach( ( op ) => {
if ( op.type === 'retain' ) {
offset += op.length;
} else if ( op.type === 'replace' ) {
replacements.push( {
start: this.getPosFromOffset( offset ),
// Don't bother recalculating end offset if not a removal, replaceRange works with just one arg
end: op.remove.length ? this.getPosFromOffset( offset + op.remove.length ) : undefined,
data: new ve.dm.ElementLinearData( store, op.insert ).getSourceText()
} );
offset += op.remove.length;
}
} );
// Apply replacements in reverse to avoid having to shift offsets
for ( let i = replacements.length - 1; i >= 0; i-- ) {
mirror.replaceRange(
replacements[ i ].data,
replacements[ i ].start,
replacements[ i ].end
);
}
// HACK: The absolutely positioned CodeMirror doesn't calculate the viewport
// correctly when expanding from less than the viewport height. (T185184)
if ( mirror.display.sizer.style.minHeight !== this.lastHeight ) {
mirror.refresh();
this.lastHeight = mirror.display.sizer.style.minHeight;
}
};
/**
* Convert a VE offset to a 2D CodeMirror position
*
* @param {number} veOffset VE linear model offset
* @return {Object} Code mirror position, containing 'line' and 'ch' numbers
*/
ve.ui.CodeMirrorAction.prototype.getPosFromOffset = function ( veOffset ) {
return this.surface.mirror.posFromIndex(
this.surface.getModel().getSourceOffsetFromOffset( veOffset )
);
};
/* Registration */
{
// eslint-disable-next-line no-jquery/no-global-selector
const contentDir = $( '.mw-body-content .mw-parser-output' ).attr( 'dir' ) ||
// New pages will use wgPageContentLanguage which is set on the html element.
document.documentElement.dir;
if ( contentDir === 'ltr' ) {
ve.ui.actionFactory.register( ve.ui.CodeMirrorAction );
}
}