mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-27 15:40:00 +00:00
d7d6cd9410
Change-Id: I43c38a5f21d14341a54f5d18ea535d1c12a81c09
281 lines
8.5 KiB
JavaScript
281 lines
8.5 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;
|
|
}
|
|
|
|
var 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 ) {
|
|
var action = this,
|
|
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( function () {
|
|
var config = mw.config.get( 'extCodeMirrorConfig' );
|
|
|
|
if ( !surface.mirror ) {
|
|
// Action was toggled to false since promise started
|
|
return;
|
|
}
|
|
mw.loader.using( config.pluginModules, function () {
|
|
if ( !surface.mirror ) {
|
|
// Action was toggled to false since promise started
|
|
return;
|
|
}
|
|
var tabSizeValue = surfaceView.documentView.documentNode.$element.css( 'tab-size' );
|
|
var 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() ) {
|
|
$.extend( 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' );
|
|
}
|
|
|
|
var profile = $.client.profile();
|
|
var 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.
|
|
var updateGutter = function ( 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 = action.onDocumentPrecommit.bind( action );
|
|
surface.mirror.veLangChangeListener = action.onLangChange.bind( action );
|
|
surface.mirror.veSelectListener = action.onSelect.bind( action );
|
|
|
|
doc.on( 'precommit', surface.mirror.veTransactionListener );
|
|
surfaceView.getDocument().on( 'langChange', surface.mirror.veLangChangeListener );
|
|
surface.getModel().on( 'select', surface.mirror.veSelectListener );
|
|
|
|
action.onLangChange();
|
|
|
|
ve.init.target.once( 'surfaceReady', function () {
|
|
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', '' );
|
|
|
|
var 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 ) {
|
|
var 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 () {
|
|
var 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 ) {
|
|
var offset = 0,
|
|
replacements = [],
|
|
action = this,
|
|
store = this.surface.getModel().getDocument().getStore(),
|
|
mirror = this.surface.mirror;
|
|
|
|
tx.operations.forEach( function ( op ) {
|
|
if ( op.type === 'retain' ) {
|
|
offset += op.length;
|
|
} else if ( op.type === 'replace' ) {
|
|
replacements.push( {
|
|
start: action.getPosFromOffset( offset ),
|
|
// Don't bother recalculating end offset if not a removal, replaceRange works with just one arg
|
|
end: op.remove.length ? action.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 ( var 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 */
|
|
|
|
ve.ui.actionFactory.register( ve.ui.CodeMirrorAction );
|