mediawiki-extensions-CodeMi.../resources/codemirror.textSelection.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

233 lines
5.7 KiB
JavaScript

const { EditorSelection, EditorView } = require( 'ext.CodeMirror.v6.lib' );
/**
* [jQuery.textSelection]{@link jQuery.fn.textSelection} implementation for CodeMirror.
* This is registered to both the textarea and the `.cm-editor` element.
*
* @see jQuery.fn.textSelection
*/
class CodeMirrorTextSelection {
/**
* @constructor
* @param {EditorView} view
*/
constructor( view ) {
/**
* The CodeMirror view.
*
* @type {EditorView}
*/
this.view = view;
/**
* The CodeMirror DOM.
*
* @type {jQuery}
*/
this.$cmDom = $( view.dom );
}
/**
* Get the contents of the editor.
*
* @return {string}
* @stable to call
*/
getContents() {
return this.view.state.doc.toString();
}
/**
* Set the contents of the editor.
*
* @param {string} content
* @return {jQuery}
* @stable to call
*/
setContents( content ) {
this.view.dispatch( {
changes: {
from: 0,
to: this.view.state.doc.length,
insert: content
}
} );
return this.$cmDom;
}
/**
* Get the current caret position.
*
* @param {Object} [options]
* @param {boolean} [options.startAndEnd] Whether to return the start and end of the selection
* instead of the caret position.
* @return {number[]|number}
* @stable to call
*/
getCaretPosition( options ) {
if ( !options.startAndEnd ) {
return this.view.state.selection.main.head;
}
return [
this.view.state.selection.main.from,
this.view.state.selection.main.to
];
}
/**
* Scroll the editor to the current caret position.
*
* @return {jQuery}
* @stable to call
*/
scrollToCaretPosition() {
const scrollEffect = EditorView.scrollIntoView( this.view.state.selection.main.head );
scrollEffect.value.isSnapshot = true;
this.view.dispatch( {
effects: scrollEffect
} );
return this.$cmDom;
}
/**
* Get the selected text.
*
* @return {string}
* @stable to call
*/
getSelection() {
return this.view.state.sliceDoc(
this.view.state.selection.main.from,
this.view.state.selection.main.to
);
}
/**
* Set the selected text.
*
* @param {Object} options
* @param {number} options.start The start of the selection.
* @param {number} [options.end=options.start] The end of the selection.
* @return {jQuery}
* @stable to call
*/
setSelection( options ) {
this.view.dispatch( {
selection: { anchor: options.start, head: ( options.end || options.start ) }
} );
this.view.focus();
return this.$cmDom;
}
/**
* Replace the selected text with the given value.
*
* @param {string} value
* @return {jQuery}
* @stable to call
*/
replaceSelection( value ) {
this.view.dispatch(
this.view.state.replaceSelection( value )
);
return this.$cmDom;
}
/**
* Encapsulate the selected text with the given values.
*
* This is intentionally a near-identical implementation to jQuery.textSelection,
* except it uses CodeMirror's
* [EditorState.changeByRange](https://codemirror.net/docs/ref/#state.EditorState.changeByRange)
* when there are multiple selections.
*
* @todo Add support for 'ownline' and 'splitlines' options.
*
* @param {Object} options
* @param {string} [options.pre] The text to insert before the cursor/selection.
* @param {string} [options.post] The text to insert after the cursor/selection.
* @param {string} [options.peri] Text to insert between pre and post and select afterwards.
* @param {boolean} [options.replace=false] If there is a selection, replace it with peri
* instead of leaving it alone.
* @param {boolean} [options.selectPeri=true] Select the peri text if it was inserted.
* @param {number} [options.selectionStart] Position to start selection at.
* @param {number} [options.selectionEnd=options.selectionStart] Position to end selection at.
* @return {jQuery}
* @stable to call
*/
encapsulateSelection( options ) {
let selectedText,
isSample = false;
const checkSelectedText = () => {
if ( !selectedText ) {
selectedText = options.peri;
isSample = true;
} else if ( options.replace ) {
selectedText = options.peri;
} else {
while ( selectedText.charAt( selectedText.length - 1 ) === ' ' ) {
// Exclude ending space char
selectedText = selectedText.slice( 0, -1 );
options.post += ' ';
}
while ( selectedText.charAt( 0 ) === ' ' ) {
// Exclude prepending space char
selectedText = selectedText.slice( 1 );
options.pre = ' ' + options.pre;
}
}
};
this.view.focus();
// Set the selection, if applicable.
if ( options.selectionStart !== undefined ) {
this.setSelection( {
start: options.selectionStart,
end: options.selectionEnd || options.selectionStart
} );
}
selectedText = this.getSelection();
const [ startPos ] = this.getCaretPosition( { startAndEnd: true } );
checkSelectedText();
const insertText = options.pre + selectedText + options.post;
/**
* Use CodeMirror's API when there are multiple selections.
*
* @see https://codemirror.net/examples/change/
*/
if ( this.view.state.selection.ranges.length > 1 ) {
this.view.dispatch( this.view.state.changeByRange( ( range ) => ( {
changes: [
{ from: range.from, insert: options.pre },
{ from: range.to, insert: options.post }
],
range: EditorSelection.range(
range.to + options.pre.length + options.post.length,
range.to + options.pre.length + options.post.length
)
} ) ) );
return this.$cmDom;
}
this.replaceSelection( insertText );
if ( isSample && options.selectPeri ) {
this.setSelection( {
start: startPos + options.pre.length,
end: startPos + options.pre.length + selectedText.length
} );
} else {
this.setSelection( {
start: startPos + insertText.length
} );
}
return this.$cmDom;
}
}
module.exports = CodeMirrorTextSelection;