mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 22:03:28 +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",
|
||||||
"codemirror-replace-all",
|
"codemirror-replace-all",
|
||||||
"codemirror-replace-placeholder",
|
"codemirror-replace-placeholder",
|
||||||
|
"codemirror-find-results",
|
||||||
"codemirror-special-char-backspace",
|
"codemirror-special-char-backspace",
|
||||||
"codemirror-special-char-bell",
|
"codemirror-special-char-bell",
|
||||||
"codemirror-special-char-carriage-return",
|
"codemirror-special-char-carriage-return",
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"codemirror-replace-placeholder": "Replace",
|
"codemirror-replace-placeholder": "Replace",
|
||||||
"codemirror-replace-all": "Replace all",
|
"codemirror-replace-all": "Replace all",
|
||||||
"codemirror-done": "Done",
|
"codemirror-done": "Done",
|
||||||
|
"codemirror-find-results": "$1 of $2",
|
||||||
"codemirror-goto-line": "Go to line",
|
"codemirror-goto-line": "Go to line",
|
||||||
"codemirror-goto-line-go": "Go",
|
"codemirror-goto-line-go": "Go",
|
||||||
"codemirror-control-character": "Control character $1",
|
"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-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-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-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": "Label for the 'Go to line' input field.",
|
||||||
"codemirror-goto-line-go": "Label for the 'Go to line' submit button.\n{{Identical|Go}}",
|
"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.",
|
"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 {
|
.cm-mw-icon--match-case {
|
||||||
background-color: @color-base;
|
background-color: @color-base;
|
||||||
.cdx-mixin-css-icon( @cdx-icon-search-case-sensitive, @color-base, @size-icon-medium, true );
|
.cdx-mixin-css-icon( @cdx-icon-search-case-sensitive, @color-base, @size-icon-medium, true );
|
||||||
|
|
|
@ -4,13 +4,16 @@ const {
|
||||||
closeSearchPanel,
|
closeSearchPanel,
|
||||||
findNext,
|
findNext,
|
||||||
findPrevious,
|
findPrevious,
|
||||||
|
getSearchQuery,
|
||||||
keymap,
|
keymap,
|
||||||
|
openSearchPanel,
|
||||||
replaceAll,
|
replaceAll,
|
||||||
replaceNext,
|
replaceNext,
|
||||||
runScopeHandlers,
|
runScopeHandlers,
|
||||||
search,
|
search,
|
||||||
searchKeymap,
|
|
||||||
selectMatches,
|
selectMatches,
|
||||||
|
selectNextOccurrence,
|
||||||
|
selectSelectionMatches,
|
||||||
setSearchQuery
|
setSearchQuery
|
||||||
} = require( 'ext.CodeMirror.v6.lib' );
|
} = require( 'ext.CodeMirror.v6.lib' );
|
||||||
const CodeMirrorPanel = require( './codemirror.panel.js' );
|
const CodeMirrorPanel = require( './codemirror.panel.js' );
|
||||||
|
@ -70,6 +73,14 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
||||||
* @type {HTMLButtonElement}
|
* @type {HTMLButtonElement}
|
||||||
*/
|
*/
|
||||||
this.replaceAllButton = undefined;
|
this.replaceAllButton = undefined;
|
||||||
|
/**
|
||||||
|
* @type {HTMLButtonElement}
|
||||||
|
*/
|
||||||
|
this.doneButton = undefined;
|
||||||
|
/**
|
||||||
|
* @type {HTMLSpanElement}
|
||||||
|
*/
|
||||||
|
this.findResultsText = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -83,7 +94,14 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
||||||
return this.panel;
|
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.
|
// Search input.
|
||||||
const [ searchInputWrapper, searchInput ] = this.getTextInput(
|
const [ searchInputWrapper, searchInput ] = this.getTextInput(
|
||||||
'search',
|
'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'
|
'codemirror-find'
|
||||||
);
|
);
|
||||||
this.searchInput = searchInput;
|
this.searchInput = searchInput;
|
||||||
this.searchInput.setAttribute( 'main-field', 'true' );
|
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 );
|
firstRow.appendChild( searchInputWrapper );
|
||||||
|
|
||||||
this.appendPrevAndNextButtons( firstRow );
|
this.appendPrevAndNextButtons( firstRow );
|
||||||
|
@ -146,7 +170,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
||||||
buttonGroup.appendChild( this.prevButton );
|
buttonGroup.appendChild( this.prevButton );
|
||||||
this.prevButton.addEventListener( 'click', ( e ) => {
|
this.prevButton.addEventListener( 'click', ( e ) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
findPrevious( this.view );
|
this.findPrevious();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// "Next" button.
|
// "Next" button.
|
||||||
|
@ -154,7 +178,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
||||||
buttonGroup.appendChild( this.nextButton );
|
buttonGroup.appendChild( this.nextButton );
|
||||||
this.nextButton.addEventListener( 'click', ( e ) => {
|
this.nextButton.addEventListener( 'click', ( e ) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
findNext( this.view );
|
this.findNext();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
firstRow.appendChild( buttonGroup );
|
firstRow.appendChild( buttonGroup );
|
||||||
|
@ -225,6 +249,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
||||||
this.replaceButton.addEventListener( 'click', ( e ) => {
|
this.replaceButton.addEventListener( 'click', ( e ) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
replaceNext( this.view );
|
replaceNext( this.view );
|
||||||
|
this.updateNumMatchesText();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// "Replace all" button.
|
// "Replace all" button.
|
||||||
|
@ -234,12 +259,13 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
||||||
this.replaceAllButton.addEventListener( 'click', ( e ) => {
|
this.replaceAllButton.addEventListener( 'click', ( e ) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
replaceAll( this.view );
|
replaceAll( this.view );
|
||||||
|
this.updateNumMatchesText();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// "Done" button.
|
// "Done" button.
|
||||||
const doneButton = this.getButton( 'codemirror-done' );
|
this.doneButton = this.getButton( 'codemirror-done' );
|
||||||
row.appendChild( doneButton );
|
row.appendChild( this.doneButton );
|
||||||
doneButton.addEventListener( 'click', ( e ) => {
|
this.doneButton.addEventListener( 'click', ( e ) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
closeSearchPanel( this.view );
|
closeSearchPanel( this.view );
|
||||||
this.view.focus();
|
this.view.focus();
|
||||||
|
@ -254,12 +280,46 @@ class CodeMirrorSearch extends CodeMirrorPanel {
|
||||||
onKeydown( event ) {
|
onKeydown( event ) {
|
||||||
if ( runScopeHandlers( this.view, event, 'search-panel' ) ) {
|
if ( runScopeHandlers( this.view, event, 'search-panel' ) ) {
|
||||||
event.preventDefault();
|
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.preventDefault();
|
||||||
( event.shiftKey ? findPrevious : findNext )( this.view );
|
( event.shiftKey ? this.findPrevious : this.findNext ).call( this );
|
||||||
} else if ( event.key === 'Enter' && event.target === this.replaceInput ) {
|
} else if ( event.key === 'Enter' && event.target === this.replaceInput ) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
replaceNext( this.view );
|
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 )
|
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