@@ -2,24 +2,40 @@ import {
22 SearchQuery ,
33 findNext ,
44 findPrev ,
5+ getMatchHighlights ,
56 getSearchState ,
67 replaceAll ,
78 replaceNext ,
89 setSearchState ,
910} from 'prosemirror-search' ;
1011
1112import { keydownHandler } from '#pm/keymap' ;
12- import { type Command , Plugin , PluginKey , type PluginView } from '#pm/state' ;
13+ import {
14+ type Command ,
15+ type EditorState ,
16+ Plugin ,
17+ PluginKey ,
18+ type PluginView ,
19+ type Transaction ,
20+ } from '#pm/state' ;
1321import type { EditorView } from '#pm/view' ;
1422import { debounce } from 'src/lodash' ;
15- import { type SearchPopupProps , renderSearchPopup } from 'src/search/SearchPopup ' ;
23+ import { type SearchCounter , type SearchPopupProps , renderSearchPopup } from 'src/search' ;
1624
1725import { getReactRendererFromState } from '../ReactRenderer' ;
26+ import { hideSelectionMenu } from '../SelectionContext' ;
27+
28+ import { FocusManager } from './FocusManager' ;
1829
19- import 'prosemirror-search/style/ search.css ' ;
30+ import './ search-plugin.scss ' ;
2031
2132const pluginKey = new PluginKey < SearchViewState > ( 'search-view' ) ;
2233
34+ const SearchClassName = {
35+ Match : 'ProseMirror-search-match' ,
36+ ActiveMatch : 'ProseMirror-active-search-match' ,
37+ } ;
38+
2339type TrMeta = { open : false } | { open : true ; search : string } ;
2440type SearchQueryConfig = ConstructorParameters < typeof SearchQuery > [ 0 ] ;
2541
@@ -28,7 +44,12 @@ const openSearch: Command = (state, dispatch) => {
2844 if ( ! pluginState ?. open && dispatch ) {
2945 const search = state . doc . textBetween ( state . selection . from , state . selection . to , ' ' ) ;
3046 const meta : TrMeta = { open : true , search} ;
31- dispatch ( setSearchState ( state . tr . setMeta ( pluginKey , meta ) , new SearchQuery ( { search} ) ) ) ;
47+ dispatch (
48+ setSearchState (
49+ hideSelectionMenu ( state . tr . setMeta ( pluginKey , meta ) ) ,
50+ new SearchQuery ( { search} ) ,
51+ ) ,
52+ ) ;
3253 }
3354 return true ;
3455} ;
@@ -84,7 +105,9 @@ export const searchViewPlugin = (params: SearchViewPluginParams) => {
84105class SeachPluginView implements PluginView {
85106 private readonly _view : EditorView ;
86107 private readonly _renderer ;
108+ private readonly _focusManager : FocusManager ;
87109
110+ private _counter : SearchCounter ;
88111 private _viewState : SearchViewState | undefined ;
89112 private _searchState : ReturnType < typeof getSearchState > ;
90113
@@ -95,18 +118,26 @@ class SeachPluginView implements PluginView {
95118 this . _view = view ;
96119 this . _viewState = pluginKey . getState ( view . state ) ;
97120 this . _searchState = getSearchState ( view . state ) ;
121+ this . _counter = getCounter ( view . state ) ;
98122
99123 this . _renderer = this . _createRenderer ( params ) ;
124+ this . _focusManager = new FocusManager ( view . dom . ownerDocument ) ;
100125
101126 this . _onSearchChangeDebounced = debounce ( this . _handleSearchChange . bind ( this ) ) ;
102127 this . _onReplacementChangeDebounced = debounce ( this . _handleReplacementChange . bind ( this ) ) ;
103128 }
104129
105130 update ( ) {
131+ const counter = getCounter ( this . _view . state ) ;
106132 const newViewState = pluginKey . getState ( this . _view . state ) ;
107133 console . log ( 'searchViewPlugin view update' , { newViewState} ) ;
108134 // const newSearchState = getSearchState(view.state);
109- if ( newViewState !== this . _viewState ) {
135+ if (
136+ newViewState !== this . _viewState ||
137+ counter . total !== this . _counter . total ||
138+ counter . current !== this . _counter . current
139+ ) {
140+ this . _counter = counter ;
110141 this . _viewState = newViewState ;
111142 this . _searchState = getSearchState ( this . _view . state ) ;
112143 console . log ( 'searchViewPlugin view update rerender' , {
@@ -139,6 +170,7 @@ class SeachPluginView implements PluginView {
139170 return renderSearchPopup ( {
140171 anchor,
141172 open : viewState . open ,
173+ counter : this . _counter ,
142174 initial : searchState . query ,
143175 onClose : this . _onClose ,
144176 onSearchPrev : this . _onSearchPrev ,
@@ -196,29 +228,25 @@ class SeachPluginView implements PluginView {
196228 } ;
197229
198230 private _onSearchPrev = ( ) => {
199- // this._view.focus();
200231 this . _onSearchChangeDebounced . flush ( ) ;
201- findPrev ( this . _view . state , this . _view . dispatch ) ;
202- // this._scrollToSelection();
232+ this . _preserveFocus ( wrapCommand ( findPrev , hideSelectionMenu ) ) ;
203233 } ;
204234
205235 private _onSearchNext = ( ) => {
206- // this._view.focus();
207236 this . _onSearchChangeDebounced . flush ( ) ;
208- findNext ( this . _view . state , this . _view . dispatch ) ;
209- // this._scrollToSelection();
237+ this . _preserveFocus ( wrapCommand ( findNext , hideSelectionMenu ) ) ;
210238 } ;
211239
212240 private _onReplaceNext = ( ) => {
213241 this . _onSearchChangeDebounced . flush ( ) ;
214242 this . _onReplacementChangeDebounced . flush ( ) ;
215- replaceNext ( this . _view . state , this . _view . dispatch ) ;
243+ this . _preserveFocus ( wrapCommand ( replaceNext , hideSelectionMenu ) ) ;
216244 } ;
217245
218246 private _onReplaceAll = ( ) => {
219247 this . _onSearchChangeDebounced . flush ( ) ;
220248 this . _onReplacementChangeDebounced . flush ( ) ;
221- replaceAll ( this . _view . state , this . _view . dispatch ) ;
249+ this . _preserveFocus ( wrapCommand ( replaceAll , hideSelectionMenu ) ) ;
222250 } ;
223251
224252 private _onConfigChange : SearchPopupProps [ 'onConfigChange' ] = ( config ) => {
@@ -229,7 +257,38 @@ class SeachPluginView implements PluginView {
229257 } ) ;
230258 } ;
231259
232- // private _scrollToSelection() {
233- // this._view.dispatch(this._view.state.tr.scrollIntoView());
234- // }
260+ private _preserveFocus ( command : Command ) {
261+ this . _focusManager . storeFocus ( ) ;
262+ this . _view . focus ( ) ;
263+ command ( this . _view . state , this . _view . dispatch , this . _view ) ;
264+ this . _focusManager . restoreFocus ( { preventScroll : true } ) ;
265+ }
266+ }
267+
268+ function wrapCommand ( command : Command , wrapper : ( tr : Transaction ) => void ) : Command {
269+ return ( state , dispatch , view ) => {
270+ return command (
271+ state ,
272+ dispatch
273+ ? ( tr ) => {
274+ wrapper ( tr ) ;
275+ return dispatch ( tr ) ;
276+ }
277+ : undefined ,
278+ view ,
279+ ) ;
280+ } ;
281+ }
282+
283+ function getCounter ( state : EditorState ) : SearchCounter {
284+ const decoSet = getMatchHighlights ( state ) ;
285+ const searchDecos = decoSet . find ( undefined , undefined , ( ) => true ) ;
286+ const activeIndex = searchDecos . findIndex ( ( deco ) => {
287+ const d = deco as { type ?: { attrs ?: Record < string , any > } } ;
288+ return d . type ?. attrs ?. class === SearchClassName . ActiveMatch ;
289+ } ) ;
290+ return {
291+ total : searchDecos . length ,
292+ current : activeIndex + 1 ,
293+ } ;
235294}
0 commit comments