mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 13:56:44 +00:00
Merge "CodeMirrorSearch: add num results and current selection; improve tabbing"
This commit is contained in:
commit
140ac0cdb0
|
@ -217,6 +217,7 @@
|
|||
"codemirror-replace",
|
||||
"codemirror-replace-all",
|
||||
"codemirror-replace-placeholder",
|
||||
"codemirror-find-results",
|
||||
"codemirror-special-char-backspace",
|
||||
"codemirror-special-char-bell",
|
||||
"codemirror-special-char-carriage-return",
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"codemirror-replace-placeholder": "Replace",
|
||||
"codemirror-replace-all": "Replace all",
|
||||
"codemirror-done": "Done",
|
||||
"codemirror-find-results": "$1 of $2",
|
||||
"codemirror-goto-line": "Go to line",
|
||||
"codemirror-goto-line-go": "Go",
|
||||
"codemirror-control-character": "Control character $1",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"codemirror-replace-placeholder": "Placeholder text for the 'Replace' input in the CodeMirror search panel.",
|
||||
"codemirror-replace-all": "Label for the 'Replace all' button in the CodeMirror search panel.",
|
||||
"codemirror-done": "Label for the 'Done' button in CodeMirror panels.\n{{Identical|Done}}",
|
||||
"codemirror-find-results": "Label for find results showing how many results were found ($2), and which one is currently highlighted ($1).\n{{Identical|Of}}",
|
||||
"codemirror-goto-line": "Label for the 'Go to line' input field.",
|
||||
"codemirror-goto-line-go": "Label for the 'Go to line' submit button.\n{{Identical|Go}}",
|
||||
"codemirror-control-character": "Tooltip text shown when hovering over special characters. $1 is the Unicode value of the special character.",
|
||||
|
|
|
@ -138,6 +138,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.cm-mw-find-results {
|
||||
color: @color-placeholder;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
top: 50%;
|
||||
transform: translateY( -50% );
|
||||
}
|
||||
|
||||
.cm-mw-icon--match-case {
|
||||
background-color: @color-base;
|
||||
.cdx-mixin-css-icon( @cdx-icon-search-case-sensitive, @color-base, @size-icon-medium, true );
|
||||
|
|
|
@ -4,13 +4,16 @@ const {
|
|||
closeSearchPanel,
|
||||
findNext,
|
||||
findPrevious,
|
||||
getSearchQuery,
|
||||
keymap,
|
||||
openSearchPanel,
|
||||
replaceAll,
|
||||
replaceNext,
|
||||
runScopeHandlers,
|
||||
search,
|
||||
searchKeymap,
|
||||
selectMatches,
|
||||
selectNextOccurrence,
|
||||
selectSelectionMatches,
|
||||
setSearchQuery
|
||||
} = require( 'ext.CodeMirror.v6.lib' );
|
||||
const CodeMirrorPanel = require( './codemirror.panel.js' );
|
||||
|
@ -70,6 +73,14 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
this.replaceAllButton = undefined;
|
||||
/**
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
this.doneButton = undefined;
|
||||
/**
|
||||
* @type {HTMLSpanElement}
|
||||
*/
|
||||
this.findResultsText = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +94,14 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
return this.panel;
|
||||
}
|
||||
} ),
|
||||
keymap.of( searchKeymap )
|
||||
keymap.of( [
|
||||
{ key: 'Mod-f', run: openSearchPanel, scope: 'editor search-panel' },
|
||||
{ key: 'F3', run: this.findNext.bind( this ), shift: this.findPrevious.bind( this ), scope: 'editor search-panel', preventDefault: true },
|
||||
{ key: 'Mod-g', run: this.findNext.bind( this ), shift: this.findPrevious.bind( this ), scope: 'editor search-panel', preventDefault: true },
|
||||
{ key: 'Escape', run: closeSearchPanel, scope: 'editor search-panel' },
|
||||
{ key: 'Mod-Shift-l', run: selectSelectionMatches },
|
||||
{ key: 'Mod-d', run: selectNextOccurrence, preventDefault: true }
|
||||
] )
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -102,11 +120,17 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
// Search input.
|
||||
const [ searchInputWrapper, searchInput ] = this.getTextInput(
|
||||
'search',
|
||||
this.searchQuery.search || '',
|
||||
this.searchQuery.search || this.view.state.sliceDoc(
|
||||
this.view.state.selection.main.from,
|
||||
this.view.state.selection.main.to
|
||||
),
|
||||
'codemirror-find'
|
||||
);
|
||||
this.searchInput = searchInput;
|
||||
this.searchInput.setAttribute( 'main-field', 'true' );
|
||||
this.findResultsText = document.createElement( 'span' );
|
||||
this.findResultsText.className = 'cm-mw-find-results';
|
||||
searchInputWrapper.appendChild( this.findResultsText );
|
||||
firstRow.appendChild( searchInputWrapper );
|
||||
|
||||
this.appendPrevAndNextButtons( firstRow );
|
||||
|
@ -146,7 +170,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
buttonGroup.appendChild( this.prevButton );
|
||||
this.prevButton.addEventListener( 'click', ( e ) => {
|
||||
e.preventDefault();
|
||||
findPrevious( this.view );
|
||||
this.findPrevious();
|
||||
} );
|
||||
|
||||
// "Next" button.
|
||||
|
@ -154,7 +178,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
buttonGroup.appendChild( this.nextButton );
|
||||
this.nextButton.addEventListener( 'click', ( e ) => {
|
||||
e.preventDefault();
|
||||
findNext( this.view );
|
||||
this.findNext();
|
||||
} );
|
||||
|
||||
firstRow.appendChild( buttonGroup );
|
||||
|
@ -225,6 +249,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
this.replaceButton.addEventListener( 'click', ( e ) => {
|
||||
e.preventDefault();
|
||||
replaceNext( this.view );
|
||||
this.updateNumMatchesText();
|
||||
} );
|
||||
|
||||
// "Replace all" button.
|
||||
|
@ -234,12 +259,13 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
this.replaceAllButton.addEventListener( 'click', ( e ) => {
|
||||
e.preventDefault();
|
||||
replaceAll( this.view );
|
||||
this.updateNumMatchesText();
|
||||
} );
|
||||
|
||||
// "Done" button.
|
||||
const doneButton = this.getButton( 'codemirror-done' );
|
||||
row.appendChild( doneButton );
|
||||
doneButton.addEventListener( 'click', ( e ) => {
|
||||
this.doneButton = this.getButton( 'codemirror-done' );
|
||||
row.appendChild( this.doneButton );
|
||||
this.doneButton.addEventListener( 'click', ( e ) => {
|
||||
e.preventDefault();
|
||||
closeSearchPanel( this.view );
|
||||
this.view.focus();
|
||||
|
@ -254,12 +280,46 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
onKeydown( event ) {
|
||||
if ( runScopeHandlers( this.view, event, 'search-panel' ) ) {
|
||||
event.preventDefault();
|
||||
} else if ( event.key === 'Enter' && event.target === this.searchInput ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( this.view.state.readOnly ) {
|
||||
// Use normal tab behaviour if the editor is read-only.
|
||||
return;
|
||||
}
|
||||
|
||||
if ( event.key === 'Enter' && event.target === this.searchInput ) {
|
||||
event.preventDefault();
|
||||
( event.shiftKey ? findPrevious : findNext )( this.view );
|
||||
( event.shiftKey ? this.findPrevious : this.findNext ).call( this );
|
||||
} else if ( event.key === 'Enter' && event.target === this.replaceInput ) {
|
||||
event.preventDefault();
|
||||
replaceNext( this.view );
|
||||
this.updateNumMatchesText();
|
||||
} else if ( event.key === 'Tab' ) {
|
||||
if ( !event.shiftKey && event.target === this.searchInput ) {
|
||||
// Tabbing from the search input should focus the replaceInput.
|
||||
event.preventDefault();
|
||||
this.replaceInput.focus();
|
||||
} else if ( event.shiftKey && event.target === this.replaceInput ) {
|
||||
// Shift+Tabbing from the replaceInput should focus the searchInput.
|
||||
event.preventDefault();
|
||||
this.searchInput.focus();
|
||||
} else if ( !event.shiftKey && event.target === this.doneButton ) {
|
||||
// Tabbing from the "Done" button should focus the prevButton.
|
||||
event.preventDefault();
|
||||
this.prevButton.focus();
|
||||
} else if ( !event.shiftKey && event.target === this.wholeWordButton ) {
|
||||
// Tabbing from the "Whole word" button should focus the editor,
|
||||
// or the next focusable panel if there is one.
|
||||
event.preventDefault();
|
||||
const el = this.view.dom.querySelector( '.cm-mw-panel--search-panel' );
|
||||
if ( el && el.nextElementSibling && el.nextElementSibling.classList.contains( 'cm-panel' ) ) {
|
||||
const input = el.nextElementSibling.querySelector( 'input' );
|
||||
( input || el.nextElementSibling ).focus();
|
||||
} else {
|
||||
this.view.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,6 +342,55 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
|||
effects: setSearchQuery.of( query )
|
||||
} );
|
||||
}
|
||||
this.updateNumMatchesText( query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the previous match.
|
||||
*
|
||||
* @return {boolean} Whether a match was found.
|
||||
*/
|
||||
findPrevious() {
|
||||
const ret = findPrevious( this.view );
|
||||
this.updateNumMatchesText();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next match.
|
||||
*
|
||||
* @return {boolean} Whether a match was found.
|
||||
*/
|
||||
findNext() {
|
||||
const ret = findNext( this.view );
|
||||
this.updateNumMatchesText();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the number of matches for the given {@link SearchQuery}
|
||||
* and the index of the current match in the find input.
|
||||
*
|
||||
* @param {SearchQuery} [query]
|
||||
*/
|
||||
updateNumMatchesText( query ) {
|
||||
const cursor = query ?
|
||||
query.getCursor( this.view.state ) :
|
||||
getSearchQuery( this.view.state ).getCursor( this.view.state );
|
||||
let count = 0,
|
||||
current = 1;
|
||||
const { from, to } = this.view.state.selection.main;
|
||||
let item = cursor.next();
|
||||
while ( !item.done ) {
|
||||
if ( item.value.from === from && item.value.to === to ) {
|
||||
current = count + 1;
|
||||
}
|
||||
item = cursor.next();
|
||||
count++;
|
||||
}
|
||||
this.findResultsText.textContent = count ?
|
||||
mw.msg( 'codemirror-find-results', current, count ) :
|
||||
'';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
30
tests/jest/codemirror.search.test.js
Normal file
30
tests/jest/codemirror.search.test.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* eslint-disable-next-line n/no-missing-require */
|
||||
const { EditorView, EditorState } = require( 'ext.CodeMirror.v6.lib' );
|
||||
const CodeMirrorSearch = require( '../../resources/codemirror.search.js' );
|
||||
|
||||
describe( 'CodeMirrorSearch', () => {
|
||||
it( 'should provide an Extension getter and a Panel getter', () => {
|
||||
const cmSearch = new CodeMirrorSearch();
|
||||
cmSearch.view = new EditorView();
|
||||
expect( cmSearch.extension ).toBeInstanceOf( Array );
|
||||
expect( cmSearch.extension[ 0 ][ 0 ].constructor.name ).toStrictEqual( 'FacetProvider' );
|
||||
expect( cmSearch.panel ).toHaveProperty( 'dom' );
|
||||
} );
|
||||
|
||||
it( 'should disable replacement fields if the textarea is read-only', () => {
|
||||
const cmSearch = new CodeMirrorSearch();
|
||||
cmSearch.view = new EditorView();
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
cmSearch.panel;
|
||||
|
||||
cmSearch.view = new EditorView( {
|
||||
state: EditorState.create( {
|
||||
doc: '',
|
||||
extensions: [ EditorState.readOnly.of( true ) ]
|
||||
} )
|
||||
} );
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
cmSearch.panel;
|
||||
expect( cmSearch.replaceInput.disabled ).toBe( true );
|
||||
} );
|
||||
} );
|
Loading…
Reference in a new issue