mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 13:56:44 +00:00
Merge "CodeMirror: fix implementation of jQuery.textSelection encapsulate"
This commit is contained in:
commit
c63e81539a
2
resources/dist/main.js
vendored
2
resources/dist/main.js
vendored
File diff suppressed because one or more lines are too long
2
resources/dist/main.js.map.json
vendored
2
resources/dist/main.js.map.json
vendored
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,6 @@
|
|||
import { EditorSelection, EditorState, Extension } from '@codemirror/state';
|
||||
import { EditorState, Extension } from '@codemirror/state';
|
||||
import { EditorView, lineNumbers, highlightSpecialChars } from '@codemirror/view';
|
||||
import CodemirrorTextSelection from './codemirror.textSelection';
|
||||
import bidiIsolationExtension from './codemirror.bidiIsolation';
|
||||
|
||||
/**
|
||||
|
@ -8,6 +9,7 @@ import bidiIsolationExtension from './codemirror.bidiIsolation';
|
|||
* @property {EditorView} view
|
||||
* @property {EditorState} state
|
||||
* @property {boolean} readOnly
|
||||
* @property {CodemirrorTextSelection} textSelection
|
||||
*/
|
||||
export default class CodeMirror {
|
||||
/**
|
||||
|
@ -19,6 +21,7 @@ export default class CodeMirror {
|
|||
this.view = null;
|
||||
this.state = null;
|
||||
this.readOnly = this.$textarea.prop( 'readonly' );
|
||||
this.textSelection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -270,77 +273,18 @@ export default class CodeMirror {
|
|||
* @return {Object}
|
||||
*/
|
||||
get cmTextSelection() {
|
||||
const $cmDom = $( this.view.dom );
|
||||
if ( !this.textSelection ) {
|
||||
this.textSelection = new CodemirrorTextSelection( this.view );
|
||||
}
|
||||
return {
|
||||
getContents: () => this.view.state.doc.toString(),
|
||||
setContents: ( content ) => {
|
||||
this.view.dispatch( {
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.view.state.doc.length,
|
||||
insert: content
|
||||
}
|
||||
} );
|
||||
return $cmDom;
|
||||
},
|
||||
getSelection: () => {
|
||||
return this.view.state.sliceDoc(
|
||||
this.view.state.selection.main.from,
|
||||
this.view.state.selection.main.to
|
||||
);
|
||||
},
|
||||
setSelection: ( options = { start: 0, end: 0 } ) => {
|
||||
this.view.dispatch( {
|
||||
selection: { anchor: options.start, head: ( options.end || options.start ) }
|
||||
} );
|
||||
this.view.focus();
|
||||
return $cmDom;
|
||||
},
|
||||
encapsulateSelection: ( options ) => {
|
||||
// First set the selection, if applicable.
|
||||
if ( options.selectionStart || options.selectionEnd ) {
|
||||
this.view.dispatch( {
|
||||
selection: {
|
||||
anchor: options.selectionStart,
|
||||
head: ( options.selectionEnd || options.selectionStart )
|
||||
}
|
||||
} );
|
||||
}
|
||||
// Do the actual replacements.
|
||||
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.from,
|
||||
range.to + options.pre.length + options.post.length
|
||||
)
|
||||
} ) ) );
|
||||
this.view.focus();
|
||||
return $cmDom;
|
||||
},
|
||||
replaceSelection: ( value ) => {
|
||||
this.view.dispatch(
|
||||
this.view.state.replaceSelection( value )
|
||||
);
|
||||
return $cmDom;
|
||||
},
|
||||
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
|
||||
];
|
||||
},
|
||||
scrollToCaretPosition: () => {
|
||||
this.view.dispatch( {
|
||||
effects: EditorView.scrollIntoView( this.view.state.selection.main.head )
|
||||
} );
|
||||
return $cmDom;
|
||||
}
|
||||
getContents: () => this.textSelection.getContents(),
|
||||
setContents: ( content ) => this.textSelection.setContents( content ),
|
||||
getCaretPosition: ( options ) => this.textSelection.getCaretPosition( options ),
|
||||
scrollToCaretPosition: () => this.textSelection.scrollToCaretPosition(),
|
||||
getSelection: () => this.textSelection.getSelection(),
|
||||
setSelection: ( options ) => this.textSelection.setSelection( options ),
|
||||
replaceSelection: ( value ) => this.textSelection.replaceSelection( value ),
|
||||
encapsulateSelection: ( options ) => this.textSelection.encapsulateSelection( options )
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
202
src/codemirror.textSelection.js
Normal file
202
src/codemirror.textSelection.js
Normal file
|
@ -0,0 +1,202 @@
|
|||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
|
||||
/**
|
||||
* jQuery.textSelection implementation for CodeMirror.
|
||||
*
|
||||
* @see jQuery.fn.textSelection
|
||||
* @class CodemirrorTextSelection
|
||||
* @property {EditorView} view
|
||||
* @property {jQuery} $cmDom
|
||||
*/
|
||||
export default class CodemirrorTextSelection {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {EditorView} view
|
||||
*/
|
||||
constructor( view ) {
|
||||
this.view = view;
|
||||
this.$cmDom = $( view.dom );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of the editor.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
getContents() {
|
||||
return this.view.state.doc.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of the editor.
|
||||
*
|
||||
* @param {string} content
|
||||
* @return {jQuery}
|
||||
*/
|
||||
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
|
||||
* @return {number[]|number}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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
|
||||
* @return {jQuery}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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 when there are multiple selections.
|
||||
*
|
||||
* @see jQuery.fn.textSelection.encapsulateSelection
|
||||
* @todo Add support for 'ownline', 'selectPeri' and 'splitlines' options.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {jQuery}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -83,7 +83,24 @@ describe( 'CodeMirror textSelection for the wikitext 2010 editor', () => {
|
|||
);
|
||||
} );
|
||||
|
||||
// Content is now "<div>foobaz</div>"
|
||||
it( "correctly inserts the 'peri' option when using encapsulateSelection", async function () {
|
||||
await browser.execute( () => {
|
||||
$( '.cm-editor' ).textSelection( 'setContents', 'foobaz' )
|
||||
.textSelection( 'encapsulateSelection', {
|
||||
selectionStart: 0,
|
||||
selectionEnd: 6,
|
||||
pre: '<div>',
|
||||
post: '</div>',
|
||||
peri: 'Soundgarden',
|
||||
replace: true
|
||||
} );
|
||||
} );
|
||||
assert.strictEqual(
|
||||
await browser.execute( () => $( '.cm-editor' ).textSelection( 'getContents' ) ),
|
||||
'<div>Soundgarden</div>'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'scrolls to the correct place when using scrollToCaretPosition', async () => {
|
||||
await browser.execute( () => {
|
||||
const $cmEditor = $( '.cm-editor' );
|
||||
|
|
|
@ -89,8 +89,8 @@ module.exports = ( env, argv ) => ( {
|
|||
// Minified uncompressed size limits for chunks / assets and entrypoints. Keep these numbers
|
||||
// up-to-date and rounded to the nearest 10th of a kibibyte so that code sizing costs are
|
||||
// well understood. Related to bundlesize minified, gzipped compressed file size tests.
|
||||
maxAssetSize: 350.0 * 1024,
|
||||
maxEntrypointSize: 350.0 * 1024,
|
||||
maxAssetSize: 351.0 * 1024,
|
||||
maxEntrypointSize: 351.0 * 1024,
|
||||
|
||||
// The default filter excludes map files, but we rename ours.
|
||||
assetFilter: ( filename ) => !filename.endsWith( srcMapExt )
|
||||
|
|
Loading…
Reference in a new issue