mediawiki-extensions-CodeMi.../src/codemirror.textSelection.js
MusikAnimal d652f3d2a2 CM6: Add jsdoc build step, fix JSDoc annotations, and add @stable tags
There is a known bug with JSDoc and using `export default`. These must
be separate statements for JSDoc to parse properly.
See https://github.com/jsdoc/jsdoc/issues/1132

Update README; change log now lives on the wiki.

Bug: T359986
Depends-On: I58a0766e35eddaf7bebe2c080757bb09963d8555
Change-Id: Ibc2212ef9eab512511b13a99ecc2ccbda8c52ece
2024-03-26 13:35:47 -04:00

232 lines
5.8 KiB
JavaScript

import { EditorView } from '@codemirror/view';
import { EditorSelection } from '@codemirror/state';
/**
* [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;
}
}
export default CodeMirrorTextSelection;