mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 13:56:44 +00:00
CodeMirrorSearch: add num results and current selection; improve tabbing
Just like the 2017 editor, we show the number of results and which one is currently highlighted. This patch also brings the Tab behaviour closer to the 2017 editor. Hitting Tab from the search input focuses the replace input, followed by the replacement buttons, then the find buttons, then the content editable. Shift+Tab largely does the reverse, except Shift+Tab from the editor doesn't bring you to the search panel. Doing this would require a lot of work for minor benefit, as we'd need to determine which panel to focus to. Add basic unit test Bug: T371436 Change-Id: I968f91320ecb6ab9e9da0994052d33c76f85974b
This commit is contained in:
parent
87daa30df0
commit
3c3050447b
|
@ -221,6 +221,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