mediawiki-extensions-CodeMi.../resources/modules/ve-cm/ve.ui.CodeMirrorAction.js
MusikAnimal 46b7208d13 Convert applicable ResourceLoader modules to use packageFiles
Move WikiEditor-specific code to ext.CodeMirror.WikiEditor, leaivng only
CodeMirror-specific things in ext.CodeMirror, including the logUsage
method which was duplicated in the VE plugin and now refactored.

Add .env to .gitignore so that selenium tests can be ran more easily

This patch leaves the other non-mediawiki modes still using the
'scripts' system instead of 'packageFiles'. These are not used in
MediaWiki directly but by some extensions (i.e. PhpTags) and using
packageFiles will break that integration.

Bug: T272035
Change-Id: I3bafef196c1f713443d7b8e9cb7dc2891b379f5d
2023-08-24 11:55:13 -04:00

283 lines
8.5 KiB
JavaScript

/*!
* VisualEditor UserInterface CodeMirrorAction class.
*
* @copyright 2011-2017 VisualEditor Team and others; see http://ve.mit-license.org
*/
/**
* 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 );