@ckeditor/ckeditor5-find-and-replace 30.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +17 -0
- package/README.md +20 -0
- package/build/find-and-replace.js +5 -0
- package/build/translations/de.js +1 -0
- package/build/translations/gl.js +1 -0
- package/build/translations/hu.js +1 -0
- package/build/translations/it.js +1 -0
- package/build/translations/nl.js +1 -0
- package/build/translations/no.js +1 -0
- package/build/translations/ru.js +1 -0
- package/build/translations/sr-latn.js +1 -0
- package/build/translations/sr.js +1 -0
- package/build/translations/zh-cn.js +1 -0
- package/ckeditor5-metadata.json +18 -0
- package/lang/contexts.json +15 -0
- package/lang/translations/de.po +69 -0
- package/lang/translations/en.po +69 -0
- package/lang/translations/gl.po +69 -0
- package/lang/translations/hu.po +69 -0
- package/lang/translations/it.po +69 -0
- package/lang/translations/nl.po +69 -0
- package/lang/translations/no.po +69 -0
- package/lang/translations/ru.po +69 -0
- package/lang/translations/sr-latn.po +69 -0
- package/lang/translations/sr.po +69 -0
- package/lang/translations/zh-cn.po +69 -0
- package/package.json +62 -0
- package/src/findandreplace.js +97 -0
- package/src/findandreplaceediting.js +254 -0
- package/src/findandreplacestate.js +131 -0
- package/src/findandreplaceui.js +202 -0
- package/src/findcommand.js +95 -0
- package/src/findnextcommand.js +67 -0
- package/src/findpreviouscommand.js +31 -0
- package/src/index.js +10 -0
- package/src/replaceallcommand.js +61 -0
- package/src/replacecommand.js +69 -0
- package/src/ui/findandreplaceformview.js +827 -0
- package/src/utils.js +166 -0
- package/theme/findandreplace.css +13 -0
- package/theme/findandreplaceform.css +17 -0
- package/theme/icons/find-replace.svg +1 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module find-and-replace/findandreplaceediting
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Plugin } from 'ckeditor5/src/core';
|
|
11
|
+
import { updateFindResultFromRange } from './utils';
|
|
12
|
+
import FindCommand from './findcommand';
|
|
13
|
+
import ReplaceCommand from './replacecommand';
|
|
14
|
+
import ReplaceAllCommand from './replaceallcommand';
|
|
15
|
+
import FindNextCommand from './findnextcommand';
|
|
16
|
+
import FindPreviousCommand from './findpreviouscommand';
|
|
17
|
+
import FindAndReplaceState from './findandreplacestate';
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
|
|
20
|
+
import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
|
|
21
|
+
|
|
22
|
+
import { debounce } from 'lodash-es';
|
|
23
|
+
|
|
24
|
+
import '../theme/findandreplace.css';
|
|
25
|
+
|
|
26
|
+
const HIGHLIGHT_CLASS = 'ck-find-result_selected';
|
|
27
|
+
|
|
28
|
+
// Reacts to document changes in order to update search list.
|
|
29
|
+
function onDocumentChange( results, model, searchCallback ) {
|
|
30
|
+
const changedNodes = new Set();
|
|
31
|
+
const removedMarkers = new Set();
|
|
32
|
+
|
|
33
|
+
const changes = model.document.differ.getChanges();
|
|
34
|
+
|
|
35
|
+
// Get nodes in which changes happened to re-run a search callback on them.
|
|
36
|
+
changes.forEach( change => {
|
|
37
|
+
if ( change.name === '$text' || model.schema.isInline( change.position.nodeAfter ) ) {
|
|
38
|
+
changedNodes.add( change.position.parent );
|
|
39
|
+
|
|
40
|
+
[ ...model.markers.getMarkersAtPosition( change.position ) ].forEach( markerAtChange => {
|
|
41
|
+
removedMarkers.add( markerAtChange.name );
|
|
42
|
+
} );
|
|
43
|
+
} else if ( change.type === 'insert' ) {
|
|
44
|
+
changedNodes.add( change.position.nodeAfter );
|
|
45
|
+
}
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
// Get markers from removed nodes also.
|
|
49
|
+
model.document.differ.getChangedMarkers().forEach( ( { name, data: { newRange } } ) => {
|
|
50
|
+
if ( newRange && newRange.start.root.rootName === '$graveyard' ) {
|
|
51
|
+
removedMarkers.add( name );
|
|
52
|
+
}
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
// Get markers from the updated nodes and remove all (search will be re-run on these nodes).
|
|
56
|
+
changedNodes.forEach( node => {
|
|
57
|
+
const markersInNode = [ ...model.markers.getMarkersIntersectingRange( model.createRangeIn( node ) ) ];
|
|
58
|
+
|
|
59
|
+
markersInNode.forEach( marker => removedMarkers.add( marker.name ) );
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
// Remove results & markers from the changed part of content.
|
|
63
|
+
model.change( writer => {
|
|
64
|
+
removedMarkers.forEach( markerName => {
|
|
65
|
+
// Remove the result first - in order to prevent rendering a removed marker.
|
|
66
|
+
if ( results.has( markerName ) ) {
|
|
67
|
+
results.remove( markerName );
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
writer.removeMarker( markerName );
|
|
71
|
+
} );
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
// Run search callback again on updated nodes.
|
|
75
|
+
changedNodes.forEach( nodeToCheck => {
|
|
76
|
+
updateFindResultFromRange( model.createRangeOn( nodeToCheck ), model, searchCallback, results );
|
|
77
|
+
} );
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Implements the editing part for find and replace plugin. For example conversion, commands etc.
|
|
82
|
+
*
|
|
83
|
+
* @extends module:core/plugin~Plugin
|
|
84
|
+
*/
|
|
85
|
+
export default class FindAndReplaceEditing extends Plugin {
|
|
86
|
+
/**
|
|
87
|
+
* @inheritDoc
|
|
88
|
+
*/
|
|
89
|
+
static get pluginName() {
|
|
90
|
+
return 'FindAndReplaceEditing';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @inheritDoc
|
|
95
|
+
*/
|
|
96
|
+
init() {
|
|
97
|
+
/**
|
|
98
|
+
* The collection of currently highlighted search results.
|
|
99
|
+
*
|
|
100
|
+
* @private
|
|
101
|
+
* @member {module:utils/collection~Collection} #_activeResults
|
|
102
|
+
*/
|
|
103
|
+
this._activeResults = null;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* An object storing the find and replace state within a given editor instance.
|
|
107
|
+
*
|
|
108
|
+
* @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #state
|
|
109
|
+
*/
|
|
110
|
+
this.state = new FindAndReplaceState( this.editor.model );
|
|
111
|
+
|
|
112
|
+
this._defineConverters();
|
|
113
|
+
this._defineCommands();
|
|
114
|
+
|
|
115
|
+
this.listenTo( this.state, 'change:highlightedResult', ( eventInfo, name, newValue, oldValue ) => {
|
|
116
|
+
const { model } = this.editor;
|
|
117
|
+
|
|
118
|
+
model.change( writer => {
|
|
119
|
+
if ( oldValue ) {
|
|
120
|
+
const oldMatchId = oldValue.marker.name.split( ':' )[ 1 ];
|
|
121
|
+
const oldMarker = model.markers.get( `findResultHighlighted:${ oldMatchId }` );
|
|
122
|
+
|
|
123
|
+
if ( oldMarker ) {
|
|
124
|
+
writer.removeMarker( oldMarker );
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if ( newValue ) {
|
|
129
|
+
const newMatchId = newValue.marker.name.split( ':' )[ 1 ];
|
|
130
|
+
writer.addMarker( `findResultHighlighted:${ newMatchId }`, {
|
|
131
|
+
usingOperation: false,
|
|
132
|
+
affectsData: false,
|
|
133
|
+
range: newValue.marker.getRange()
|
|
134
|
+
} );
|
|
135
|
+
}
|
|
136
|
+
} );
|
|
137
|
+
} );
|
|
138
|
+
|
|
139
|
+
const debouncedScrollListener = debounce( scrollToHighlightedResult.bind( this ), 32 );
|
|
140
|
+
// Debounce scroll as highlight might be changed very frequently, e.g. when there's a replace all command.
|
|
141
|
+
this.listenTo( this.state, 'change:highlightedResult', debouncedScrollListener, { priority: 'low' } );
|
|
142
|
+
|
|
143
|
+
// It's possible that the editor will get destroyed before debounced call kicks in.
|
|
144
|
+
// This would result with accessing a view three that is no longer in DOM.
|
|
145
|
+
this.listenTo( this.editor, 'destroy', debouncedScrollListener.cancel );
|
|
146
|
+
|
|
147
|
+
/* istanbul ignore next */
|
|
148
|
+
function scrollToHighlightedResult( eventInfo, name, newValue ) {
|
|
149
|
+
if ( newValue ) {
|
|
150
|
+
const domConverter = this.editor.editing.view.domConverter;
|
|
151
|
+
const viewRange = this.editor.editing.mapper.toViewRange( newValue.marker.getRange() );
|
|
152
|
+
|
|
153
|
+
scrollViewportToShowTarget( {
|
|
154
|
+
target: domConverter.viewRangeToDom( viewRange ),
|
|
155
|
+
viewportOffset: 40
|
|
156
|
+
} );
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Initiate a search.
|
|
163
|
+
*
|
|
164
|
+
* @param {Function|String} callbackOrText
|
|
165
|
+
* @returns {module:utils/collection~Collection}
|
|
166
|
+
*/
|
|
167
|
+
find( callbackOrText ) {
|
|
168
|
+
const { editor } = this;
|
|
169
|
+
const { model } = editor;
|
|
170
|
+
|
|
171
|
+
const { findCallback, results } = editor.execute( 'find', callbackOrText );
|
|
172
|
+
|
|
173
|
+
this._activeResults = results;
|
|
174
|
+
|
|
175
|
+
// @todo: handle this listener, another copy is in findcommand.js file.
|
|
176
|
+
this.listenTo( model.document, 'change:data', () => onDocumentChange( this._activeResults, model, findCallback ) );
|
|
177
|
+
|
|
178
|
+
return this._activeResults;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Stops active results from updating, and clears out the results.
|
|
183
|
+
*/
|
|
184
|
+
stop() {
|
|
185
|
+
if ( !this._activeResults ) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.stopListening( this.editor.model.document );
|
|
190
|
+
|
|
191
|
+
this.state.clear( this.editor.model );
|
|
192
|
+
|
|
193
|
+
this._activeResults = null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Sets up the commands.
|
|
198
|
+
*
|
|
199
|
+
* @private
|
|
200
|
+
*/
|
|
201
|
+
_defineCommands() {
|
|
202
|
+
this.editor.commands.add( 'find', new FindCommand( this.editor, this.state ) );
|
|
203
|
+
this.editor.commands.add( 'findNext', new FindNextCommand( this.editor, this.state ) );
|
|
204
|
+
this.editor.commands.add( 'findPrevious', new FindPreviousCommand( this.editor, this.state ) );
|
|
205
|
+
this.editor.commands.add( 'replace', new ReplaceCommand( this.editor, this.state ) );
|
|
206
|
+
this.editor.commands.add( 'replaceAll', new ReplaceAllCommand( this.editor, this.state ) );
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Sets up the marker downcast converters for search results highlighting.
|
|
211
|
+
*
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
_defineConverters() {
|
|
215
|
+
const { editor } = this;
|
|
216
|
+
|
|
217
|
+
// Setup the marker highlighting conversion.
|
|
218
|
+
editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
|
|
219
|
+
model: 'findResult',
|
|
220
|
+
view: ( { markerName } ) => {
|
|
221
|
+
const [ , id ] = markerName.split( ':' );
|
|
222
|
+
|
|
223
|
+
// Marker removal from the view has a bug: https://github.com/ckeditor/ckeditor5/issues/7499
|
|
224
|
+
// A minimal option is to return a new object for each converted marker...
|
|
225
|
+
return {
|
|
226
|
+
name: 'span',
|
|
227
|
+
classes: [ 'ck-find-result' ],
|
|
228
|
+
attributes: {
|
|
229
|
+
// ...however, adding a unique attribute should be future-proof..
|
|
230
|
+
'data-find-result': id
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
} );
|
|
235
|
+
|
|
236
|
+
editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
|
|
237
|
+
model: 'findResultHighlighted',
|
|
238
|
+
view: ( { markerName } ) => {
|
|
239
|
+
const [ , id ] = markerName.split( ':' );
|
|
240
|
+
|
|
241
|
+
// Marker removal from the view has a bug: https://github.com/ckeditor/ckeditor5/issues/7499
|
|
242
|
+
// A minimal option is to return a new object for each converted marker...
|
|
243
|
+
return {
|
|
244
|
+
name: 'span',
|
|
245
|
+
classes: [ HIGHLIGHT_CLASS ],
|
|
246
|
+
attributes: {
|
|
247
|
+
// ...however, adding a unique attribute should be future-proof..
|
|
248
|
+
'data-find-result': id
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
} );
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module find-and-replace/findandreplacestate
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ObservableMixin, mix, Collection } from 'ckeditor5/src/utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The object storing find and replace plugin state for a given editor instance.
|
|
14
|
+
*
|
|
15
|
+
* @mixes module:utils/observablemixin~ObservableMixin
|
|
16
|
+
*/
|
|
17
|
+
export default class FindAndReplaceState {
|
|
18
|
+
/**
|
|
19
|
+
* Creates an instance of the state.
|
|
20
|
+
*
|
|
21
|
+
* @param {module:engine/model/model~Model} model
|
|
22
|
+
*/
|
|
23
|
+
constructor( model ) {
|
|
24
|
+
/**
|
|
25
|
+
* A collection of find matches.
|
|
26
|
+
*
|
|
27
|
+
* @protected
|
|
28
|
+
* @observable
|
|
29
|
+
* @member {module:utils/collection~Collection} #results
|
|
30
|
+
*/
|
|
31
|
+
this.set( 'results', new Collection() );
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Currently highlighted search result in {@link #results matched results}.
|
|
35
|
+
*
|
|
36
|
+
* @readonly
|
|
37
|
+
* @observable
|
|
38
|
+
* @member {Object|null} #highlightedResult
|
|
39
|
+
*/
|
|
40
|
+
this.set( 'highlightedResult', null );
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Searched text value.
|
|
44
|
+
*
|
|
45
|
+
* @readonly
|
|
46
|
+
* @observable
|
|
47
|
+
* @member {String} #searchText
|
|
48
|
+
*/
|
|
49
|
+
this.set( 'searchText', '' );
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Replace text value.
|
|
53
|
+
*
|
|
54
|
+
* @readonly
|
|
55
|
+
* @observable
|
|
56
|
+
* @member {String} #replaceText
|
|
57
|
+
*/
|
|
58
|
+
this.set( 'replaceText', '' );
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Indicates whether the matchCase checkbox has been checked.
|
|
62
|
+
*
|
|
63
|
+
* @readonly
|
|
64
|
+
* @observable
|
|
65
|
+
* @member {Boolean} #matchCase
|
|
66
|
+
*/
|
|
67
|
+
this.set( 'matchCase', false );
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Indicates whether the matchWholeWords checkbox has been checked.
|
|
71
|
+
*
|
|
72
|
+
* @readonly
|
|
73
|
+
* @observable
|
|
74
|
+
* @member {Boolean} #matchWholeWords
|
|
75
|
+
*/
|
|
76
|
+
this.set( 'matchWholeWords', false );
|
|
77
|
+
|
|
78
|
+
this.results.on( 'change', ( eventInfo, { removed, index } ) => {
|
|
79
|
+
removed = Array.from( removed );
|
|
80
|
+
|
|
81
|
+
if ( removed.length ) {
|
|
82
|
+
let highlightedResultRemoved = false;
|
|
83
|
+
|
|
84
|
+
model.change( writer => {
|
|
85
|
+
for ( const removedResult of removed ) {
|
|
86
|
+
if ( this.highlightedResult === removedResult ) {
|
|
87
|
+
highlightedResultRemoved = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ( model.markers.has( removedResult.marker.name ) ) {
|
|
91
|
+
writer.removeMarker( removedResult.marker );
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} );
|
|
95
|
+
|
|
96
|
+
if ( highlightedResultRemoved ) {
|
|
97
|
+
const nextHighlightedIndex = index >= this.results.length ? 0 : index;
|
|
98
|
+
this.highlightedResult = this.results.get( nextHighlightedIndex );
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} );
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Cleans the state up and removes markers from the model.
|
|
106
|
+
*
|
|
107
|
+
* @param {module:engine/model/model~Model} model
|
|
108
|
+
*/
|
|
109
|
+
clear( model ) {
|
|
110
|
+
this.searchText = '';
|
|
111
|
+
|
|
112
|
+
model.change( writer => {
|
|
113
|
+
if ( this.highlightedResult ) {
|
|
114
|
+
const oldMatchId = this.highlightedResult.marker.name.split( ':' )[ 1 ];
|
|
115
|
+
const oldMarker = model.markers.get( `findResultHighlighted:${ oldMatchId }` );
|
|
116
|
+
|
|
117
|
+
if ( oldMarker ) {
|
|
118
|
+
writer.removeMarker( oldMarker );
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[ ...this.results ].forEach( ( { marker } ) => {
|
|
123
|
+
writer.removeMarker( marker );
|
|
124
|
+
} );
|
|
125
|
+
} );
|
|
126
|
+
|
|
127
|
+
this.results.clear();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
mix( FindAndReplaceState, ObservableMixin );
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module find-and-replace/findandreplaceui
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Plugin } from 'ckeditor5/src/core';
|
|
11
|
+
import { createDropdown } from 'ckeditor5/src/ui';
|
|
12
|
+
import FindAndReplaceFormView from './ui/findandreplaceformview';
|
|
13
|
+
|
|
14
|
+
import loupeIcon from '../theme/icons/find-replace.svg';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The default find and replace UI.
|
|
18
|
+
*
|
|
19
|
+
* It registers the `'findAndReplace'` UI button in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}.
|
|
20
|
+
* that uses the {@link module:find-and-replace/findandreplace~FindAndReplace FindAndReplace} plugin API.
|
|
21
|
+
*
|
|
22
|
+
* @extends module:core/plugin~Plugin
|
|
23
|
+
*/
|
|
24
|
+
export default class FindAndReplaceUI extends Plugin {
|
|
25
|
+
/**
|
|
26
|
+
* @inheritDoc
|
|
27
|
+
*/
|
|
28
|
+
static get pluginName() {
|
|
29
|
+
return 'FindAndReplaceUI';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @inheritDoc
|
|
34
|
+
*/
|
|
35
|
+
constructor( editor ) {
|
|
36
|
+
super( editor );
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A reference to the find and replace form view.
|
|
40
|
+
*
|
|
41
|
+
* @member {module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView} #formView
|
|
42
|
+
*/
|
|
43
|
+
this.formView = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @inheritDoc
|
|
48
|
+
*/
|
|
49
|
+
init() {
|
|
50
|
+
const editor = this.editor;
|
|
51
|
+
|
|
52
|
+
// Register the toolbar dropdown component.
|
|
53
|
+
editor.ui.componentFactory.add( 'findAndReplace', locale => {
|
|
54
|
+
const dropdown = createDropdown( locale );
|
|
55
|
+
const formView = this.formView = new FindAndReplaceFormView( editor.locale );
|
|
56
|
+
|
|
57
|
+
// Dropdown should be disabled when in source editing mode. See #10001.
|
|
58
|
+
dropdown.bind( 'isEnabled' ).to( editor.commands.get( 'find' ) );
|
|
59
|
+
dropdown.panelView.children.add( formView );
|
|
60
|
+
|
|
61
|
+
// Every time a dropdown is opened, the search text field should get focused and selected for better UX.
|
|
62
|
+
// Note: Using the low priority here to make sure the following listener starts working after
|
|
63
|
+
// the default action of the drop-down is executed (i.e. the panel showed up). Otherwise,
|
|
64
|
+
// the invisible form/input cannot be focused/selected.
|
|
65
|
+
//
|
|
66
|
+
// Each time a dropdown is closed, move the focus back to the find and replace toolbar button
|
|
67
|
+
// and let the find and replace editing feature know that all search results can be invalidated
|
|
68
|
+
// and no longer should be marked in the content.
|
|
69
|
+
dropdown.on( 'change:isOpen', ( event, name, isOpen ) => {
|
|
70
|
+
if ( isOpen ) {
|
|
71
|
+
formView.disableCssTransitions();
|
|
72
|
+
|
|
73
|
+
formView.reset();
|
|
74
|
+
formView._findInputView.fieldView.select();
|
|
75
|
+
formView.focus();
|
|
76
|
+
|
|
77
|
+
formView.enableCssTransitions();
|
|
78
|
+
} else {
|
|
79
|
+
formView.focus();
|
|
80
|
+
|
|
81
|
+
this.fire( 'searchReseted' );
|
|
82
|
+
}
|
|
83
|
+
}, { priority: 'low' } );
|
|
84
|
+
|
|
85
|
+
this._setupDropdownButton( dropdown );
|
|
86
|
+
this._setupFormView( formView );
|
|
87
|
+
|
|
88
|
+
return dropdown;
|
|
89
|
+
} );
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sets up the find and replace button.
|
|
94
|
+
*
|
|
95
|
+
* @private
|
|
96
|
+
* @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
|
|
97
|
+
*/
|
|
98
|
+
_setupDropdownButton( dropdown ) {
|
|
99
|
+
const editor = this.editor;
|
|
100
|
+
const t = editor.locale.t;
|
|
101
|
+
|
|
102
|
+
dropdown.buttonView.set( {
|
|
103
|
+
icon: loupeIcon,
|
|
104
|
+
label: t( 'Find and replace' ),
|
|
105
|
+
keystroke: 'CTRL+F',
|
|
106
|
+
tooltip: true
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
editor.keystrokes.set( 'Ctrl+F', ( data, cancelEvent ) => {
|
|
110
|
+
dropdown.isOpen = true;
|
|
111
|
+
cancelEvent();
|
|
112
|
+
} );
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sets up the form view for the find and replace.
|
|
117
|
+
*
|
|
118
|
+
* @private
|
|
119
|
+
* @param {module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView} formView A related form view.
|
|
120
|
+
*/
|
|
121
|
+
_setupFormView( formView ) {
|
|
122
|
+
const editor = this.editor;
|
|
123
|
+
const commands = editor.commands;
|
|
124
|
+
const findAndReplaceEditing = this.editor.plugins.get( 'FindAndReplaceEditing' );
|
|
125
|
+
const editingState = findAndReplaceEditing.state;
|
|
126
|
+
const sortMapping = { before: -1, same: 0, after: 1 };
|
|
127
|
+
|
|
128
|
+
// Let the form know which result is being highlighted.
|
|
129
|
+
formView.bind( 'highlightOffset' ).to( editingState, 'highlightedResult', highlightedResult => {
|
|
130
|
+
if ( !highlightedResult ) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return Array.from( editingState.results )
|
|
135
|
+
.sort( ( a, b ) => sortMapping[ a.marker.getStart().compareWith( b.marker.getStart() ) ] )
|
|
136
|
+
.indexOf( highlightedResult ) + 1;
|
|
137
|
+
} );
|
|
138
|
+
|
|
139
|
+
// Let the form know how many results were found in total.
|
|
140
|
+
formView.listenTo( editingState.results, 'change', () => {
|
|
141
|
+
formView.matchCount = editingState.results.length;
|
|
142
|
+
} );
|
|
143
|
+
|
|
144
|
+
// Command states are used to enable/disable individual form controls.
|
|
145
|
+
// To keep things simple, instead of binding 4 individual observables, there's only one that combines every
|
|
146
|
+
// commands' isEnabled state. Yes, it will change more often but this simplifies the structure of the form.
|
|
147
|
+
formView.bind( '_areCommandsEnabled' ).to(
|
|
148
|
+
commands.get( 'findNext' ), 'isEnabled',
|
|
149
|
+
commands.get( 'findPrevious' ), 'isEnabled',
|
|
150
|
+
commands.get( 'replace' ), 'isEnabled',
|
|
151
|
+
commands.get( 'replaceAll' ), 'isEnabled',
|
|
152
|
+
( findNext, findPrevious, replace, replaceAll ) => ( { findNext, findPrevious, replace, replaceAll } )
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// The UI plugin works as an interface between the form and the editing part of the feature.
|
|
156
|
+
formView.delegate( 'findNext', 'findPrevious', 'replace', 'replaceAll' ).to( this );
|
|
157
|
+
|
|
158
|
+
// Let the feature know that search results are no longer relevant because the user changed the searched phrase
|
|
159
|
+
// (or options) but didn't hit the "Find" button yet (e.g. still typing).
|
|
160
|
+
formView.on( 'change:isDirty', ( evt, data, isDirty ) => {
|
|
161
|
+
if ( isDirty ) {
|
|
162
|
+
this.fire( 'searchReseted' );
|
|
163
|
+
}
|
|
164
|
+
} );
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Fired when the find next button is triggered.
|
|
170
|
+
*
|
|
171
|
+
* @event findNext
|
|
172
|
+
* @param {String} searchText Search text.
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Fired when the find previous button is triggered.
|
|
177
|
+
*
|
|
178
|
+
* @event findPrevious
|
|
179
|
+
* @param {String} searchText Search text.
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fired when the replace button is triggered.
|
|
184
|
+
*
|
|
185
|
+
* @event replace
|
|
186
|
+
* @param {String} replaceText Replacement text.
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Fired when the replaceAll button is triggered.
|
|
191
|
+
*
|
|
192
|
+
* @event replaceAll
|
|
193
|
+
* @param {String} replaceText Replacement text.
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Fired when the UI was reset and the search results marked in the editing root should be invalidated,
|
|
198
|
+
* for instance, because the user changed the searched phrase (or options) but didn't hit
|
|
199
|
+
* the "Find" button yet.
|
|
200
|
+
*
|
|
201
|
+
* @event searchReseted
|
|
202
|
+
*/
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module find-and-replace/findcommand
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from 'ckeditor5/src/core';
|
|
11
|
+
import { updateFindResultFromRange, findByTextCallback } from './utils';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The find command. It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
|
|
15
|
+
*
|
|
16
|
+
* @extends module:core/command~Command
|
|
17
|
+
*/
|
|
18
|
+
export default class FindCommand extends Command {
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new `FindCommand` instance.
|
|
21
|
+
*
|
|
22
|
+
* @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
|
|
23
|
+
* @param {module:find-and-replace/findandreplacestate~FindAndReplaceState} state An object to hold plugin state.
|
|
24
|
+
*/
|
|
25
|
+
constructor( editor, state ) {
|
|
26
|
+
super( editor );
|
|
27
|
+
|
|
28
|
+
// The find command is always enabled.
|
|
29
|
+
this.isEnabled = true;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The find and replace state object used for command operations.
|
|
33
|
+
*
|
|
34
|
+
* @private
|
|
35
|
+
* @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #_state
|
|
36
|
+
*/
|
|
37
|
+
this._state = state;
|
|
38
|
+
|
|
39
|
+
// Do not block the command if the editor goes into the read-only mode as it does not impact the data. See #9975.
|
|
40
|
+
this.listenTo( editor, 'change:isReadOnly', () => {
|
|
41
|
+
this.clearForceDisabled( 'readOnlyMode' );
|
|
42
|
+
} );
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Executes the command.
|
|
47
|
+
*
|
|
48
|
+
* @param {Function|String} callbackOrText
|
|
49
|
+
* @param {Object} [options]
|
|
50
|
+
* @param {Boolean} [options.matchCase=false] If set to `true`, the letter case will be matched.
|
|
51
|
+
* @param {Boolean} [options.wholeWords=false] If set to `true`, only whole words that match `callbackOrText` will be matched.
|
|
52
|
+
*
|
|
53
|
+
* @fires execute
|
|
54
|
+
*/
|
|
55
|
+
execute( callbackOrText, { matchCase, wholeWords } = {} ) {
|
|
56
|
+
const { editor } = this;
|
|
57
|
+
const { model } = editor;
|
|
58
|
+
|
|
59
|
+
let findCallback;
|
|
60
|
+
|
|
61
|
+
// Allow to execute `find()` on a plugin with a keyword only.
|
|
62
|
+
if ( typeof callbackOrText === 'string' ) {
|
|
63
|
+
findCallback = findByTextCallback( callbackOrText, { matchCase, wholeWords } );
|
|
64
|
+
|
|
65
|
+
this._state.searchText = callbackOrText;
|
|
66
|
+
} else {
|
|
67
|
+
findCallback = callbackOrText;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Initial search is done on all nodes in all roots inside the content.
|
|
71
|
+
const results = model.document.getRootNames()
|
|
72
|
+
.reduce( ( ( currentResults, rootName ) => updateFindResultFromRange(
|
|
73
|
+
model.createRangeIn( model.document.getRoot( rootName ) ),
|
|
74
|
+
model,
|
|
75
|
+
findCallback,
|
|
76
|
+
currentResults
|
|
77
|
+
) ), null );
|
|
78
|
+
|
|
79
|
+
this._state.clear( model );
|
|
80
|
+
this._state.results.addMany( Array.from( results ) );
|
|
81
|
+
this._state.highlightedResult = results.get( 0 );
|
|
82
|
+
|
|
83
|
+
if ( typeof callbackOrText === 'string' ) {
|
|
84
|
+
this._state.searchText = callbackOrText;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this._state.matchCase = !!matchCase;
|
|
88
|
+
this._state.matchWholeWords = !!wholeWords;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
results,
|
|
92
|
+
findCallback
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|