@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.
Files changed (42) hide show
  1. package/LICENSE.md +17 -0
  2. package/README.md +20 -0
  3. package/build/find-and-replace.js +5 -0
  4. package/build/translations/de.js +1 -0
  5. package/build/translations/gl.js +1 -0
  6. package/build/translations/hu.js +1 -0
  7. package/build/translations/it.js +1 -0
  8. package/build/translations/nl.js +1 -0
  9. package/build/translations/no.js +1 -0
  10. package/build/translations/ru.js +1 -0
  11. package/build/translations/sr-latn.js +1 -0
  12. package/build/translations/sr.js +1 -0
  13. package/build/translations/zh-cn.js +1 -0
  14. package/ckeditor5-metadata.json +18 -0
  15. package/lang/contexts.json +15 -0
  16. package/lang/translations/de.po +69 -0
  17. package/lang/translations/en.po +69 -0
  18. package/lang/translations/gl.po +69 -0
  19. package/lang/translations/hu.po +69 -0
  20. package/lang/translations/it.po +69 -0
  21. package/lang/translations/nl.po +69 -0
  22. package/lang/translations/no.po +69 -0
  23. package/lang/translations/ru.po +69 -0
  24. package/lang/translations/sr-latn.po +69 -0
  25. package/lang/translations/sr.po +69 -0
  26. package/lang/translations/zh-cn.po +69 -0
  27. package/package.json +62 -0
  28. package/src/findandreplace.js +97 -0
  29. package/src/findandreplaceediting.js +254 -0
  30. package/src/findandreplacestate.js +131 -0
  31. package/src/findandreplaceui.js +202 -0
  32. package/src/findcommand.js +95 -0
  33. package/src/findnextcommand.js +67 -0
  34. package/src/findpreviouscommand.js +31 -0
  35. package/src/index.js +10 -0
  36. package/src/replaceallcommand.js +61 -0
  37. package/src/replacecommand.js +69 -0
  38. package/src/ui/findandreplaceformview.js +827 -0
  39. package/src/utils.js +166 -0
  40. package/theme/findandreplace.css +13 -0
  41. package/theme/findandreplaceform.css +17 -0
  42. 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
+ }