@ckeditor/ckeditor5-find-and-replace 36.0.0 → 37.0.0-alpha.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.
@@ -2,130 +2,59 @@
2
2
  * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
- /**
7
- * @module find-and-replace/findandreplacestate
8
- */
9
-
10
- import { ObservableMixin, mix, Collection } from 'ckeditor5/src/utils';
11
-
5
+ import { ObservableMixin, Collection } from 'ckeditor5/src/utils';
12
6
  /**
13
7
  * The object storing find and replace plugin state for a given editor instance.
14
- *
15
- * @mixes module:utils/observablemixin~ObservableMixin
16
8
  */
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
- }
9
+ export default class FindAndReplaceState extends ObservableMixin() {
10
+ /**
11
+ * Creates an instance of the state.
12
+ */
13
+ constructor(model) {
14
+ super();
15
+ this.set('results', new Collection());
16
+ this.set('highlightedResult', null);
17
+ this.set('searchText', '');
18
+ this.set('replaceText', '');
19
+ this.set('matchCase', false);
20
+ this.set('matchWholeWords', false);
21
+ this.results.on('change', (eventInfo, { removed, index }) => {
22
+ if (Array.from(removed).length) {
23
+ let highlightedResultRemoved = false;
24
+ model.change(writer => {
25
+ for (const removedResult of removed) {
26
+ if (this.highlightedResult === removedResult) {
27
+ highlightedResultRemoved = true;
28
+ }
29
+ if (model.markers.has(removedResult.marker.name)) {
30
+ writer.removeMarker(removedResult.marker);
31
+ }
32
+ }
33
+ });
34
+ if (highlightedResultRemoved) {
35
+ const nextHighlightedIndex = index >= this.results.length ? 0 : index;
36
+ this.highlightedResult = this.results.get(nextHighlightedIndex);
37
+ }
38
+ }
39
+ });
40
+ }
41
+ /**
42
+ * Cleans the state up and removes markers from the model.
43
+ */
44
+ clear(model) {
45
+ this.searchText = '';
46
+ model.change(writer => {
47
+ if (this.highlightedResult) {
48
+ const oldMatchId = this.highlightedResult.marker.name.split(':')[1];
49
+ const oldMarker = model.markers.get(`findResultHighlighted:${oldMatchId}`);
50
+ if (oldMarker) {
51
+ writer.removeMarker(oldMarker);
52
+ }
53
+ }
54
+ [...this.results].forEach(({ marker }) => {
55
+ writer.removeMarker(marker);
56
+ });
57
+ });
58
+ this.results.clear();
59
+ }
129
60
  }
130
-
131
- mix( FindAndReplaceState, ObservableMixin );
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module find-and-replace/findandreplaceui
7
+ */
8
+ import { type Editor, Plugin } from 'ckeditor5/src/core';
9
+ import { type ViewWithCssTransitionDisabler } from 'ckeditor5/src/ui';
10
+ import FindAndReplaceFormView from './ui/findandreplaceformview';
11
+ /**
12
+ * The default find and replace UI.
13
+ *
14
+ * It registers the `'findAndReplace'` UI button in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}.
15
+ * that uses the {@link module:find-and-replace/findandreplace~FindAndReplace FindAndReplace} plugin API.
16
+ */
17
+ export default class FindAndReplaceUI extends Plugin {
18
+ /**
19
+ * @inheritDoc
20
+ */
21
+ static get pluginName(): 'FindAndReplaceUI';
22
+ /**
23
+ * A reference to the find and replace form view.
24
+ */
25
+ formView: FindAndReplaceFormView & ViewWithCssTransitionDisabler | null;
26
+ /**
27
+ * @inheritDoc
28
+ */
29
+ constructor(editor: Editor);
30
+ /**
31
+ * @inheritDoc
32
+ */
33
+ init(): void;
34
+ /**
35
+ * Sets up the find and replace button.
36
+ */
37
+ private _setupDropdownButton;
38
+ /**
39
+ * Sets up the form view for the find and replace.
40
+ *
41
+ * @param formView A related form view.
42
+ */
43
+ private _setupFormView;
44
+ }
45
+ /**
46
+ * Fired when the UI was reset and the search results marked in the editing root should be invalidated,
47
+ * for instance, because the user changed the searched phrase (or options) but didn't hit
48
+ * the "Find" button yet.
49
+ *
50
+ * @eventName searchReseted
51
+ */
52
+ export type SearchResetedEvent = {
53
+ name: 'searchReseted';
54
+ args: [];
55
+ };
56
+ declare module '@ckeditor/ckeditor5-core' {
57
+ interface PluginsMap {
58
+ [FindAndReplaceUI.pluginName]: FindAndReplaceUI;
59
+ }
60
+ }
@@ -2,205 +2,131 @@
2
2
  * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module find-and-replace/findandreplaceui
8
7
  */
9
-
10
8
  import { Plugin } from 'ckeditor5/src/core';
11
- import { createDropdown } from 'ckeditor5/src/ui';
9
+ import { createDropdown, CssTransitionDisablerMixin } from 'ckeditor5/src/ui';
12
10
  import FindAndReplaceFormView from './ui/findandreplaceformview';
13
-
14
11
  import loupeIcon from '../theme/icons/find-replace.svg';
15
-
16
12
  /**
17
13
  * The default find and replace UI.
18
14
  *
19
15
  * It registers the `'findAndReplace'` UI button in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}.
20
16
  * that uses the {@link module:find-and-replace/findandreplace~FindAndReplace FindAndReplace} plugin API.
21
- *
22
- * @extends module:core/plugin~Plugin
23
17
  */
24
18
  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
-
56
- // Dropdown should be disabled when in source editing mode. See #10001.
57
- dropdown.bind( 'isEnabled' ).to( editor.commands.get( 'find' ) );
58
-
59
- dropdown.once( 'change:isOpen', () => {
60
- this.formView = new FindAndReplaceFormView( editor.locale );
61
-
62
- dropdown.panelView.children.add( this.formView );
63
-
64
- this._setupFormView( this.formView );
65
- } );
66
-
67
- // Every time a dropdown is opened, the search text field should get focused and selected for better UX.
68
- // Note: Using the low priority here to make sure the following listener starts working after
69
- // the default action of the drop-down is executed (i.e. the panel showed up). Otherwise,
70
- // the invisible form/input cannot be focused/selected.
71
- //
72
- // Each time a dropdown is closed, move the focus back to the find and replace toolbar button
73
- // and let the find and replace editing feature know that all search results can be invalidated
74
- // and no longer should be marked in the content.
75
- dropdown.on( 'change:isOpen', ( event, name, isOpen ) => {
76
- if ( isOpen ) {
77
- this.formView.disableCssTransitions();
78
-
79
- this.formView.reset();
80
- this.formView._findInputView.fieldView.select();
81
-
82
- this.formView.enableCssTransitions();
83
- } else {
84
- this.fire( 'searchReseted' );
85
- }
86
- }, { priority: 'low' } );
87
-
88
- this._setupDropdownButton( dropdown );
89
-
90
- return dropdown;
91
- } );
92
- }
93
-
94
- /**
95
- * Sets up the find and replace button.
96
- *
97
- * @private
98
- * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
99
- */
100
- _setupDropdownButton( dropdown ) {
101
- const editor = this.editor;
102
- const t = editor.locale.t;
103
-
104
- dropdown.buttonView.set( {
105
- icon: loupeIcon,
106
- label: t( 'Find and replace' ),
107
- keystroke: 'CTRL+F',
108
- tooltip: true
109
- } );
110
-
111
- editor.keystrokes.set( 'Ctrl+F', ( data, cancelEvent ) => {
112
- if ( dropdown.isEnabled ) {
113
- dropdown.isOpen = true;
114
- cancelEvent();
115
- }
116
- } );
117
- }
118
-
119
- /**
120
- * Sets up the form view for the find and replace.
121
- *
122
- * @private
123
- * @param {module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView} formView A related form view.
124
- */
125
- _setupFormView( formView ) {
126
- const editor = this.editor;
127
- const commands = editor.commands;
128
- const findAndReplaceEditing = this.editor.plugins.get( 'FindAndReplaceEditing' );
129
- const editingState = findAndReplaceEditing.state;
130
- const sortMapping = { before: -1, same: 0, after: 1 };
131
-
132
- // Let the form know which result is being highlighted.
133
- formView.bind( 'highlightOffset' ).to( editingState, 'highlightedResult', highlightedResult => {
134
- if ( !highlightedResult ) {
135
- return 0;
136
- }
137
-
138
- return Array.from( editingState.results )
139
- .sort( ( a, b ) => sortMapping[ a.marker.getStart().compareWith( b.marker.getStart() ) ] )
140
- .indexOf( highlightedResult ) + 1;
141
- } );
142
-
143
- // Let the form know how many results were found in total.
144
- formView.listenTo( editingState.results, 'change', () => {
145
- formView.matchCount = editingState.results.length;
146
- } );
147
-
148
- // Command states are used to enable/disable individual form controls.
149
- // To keep things simple, instead of binding 4 individual observables, there's only one that combines every
150
- // commands' isEnabled state. Yes, it will change more often but this simplifies the structure of the form.
151
- formView.bind( '_areCommandsEnabled' ).to(
152
- commands.get( 'findNext' ), 'isEnabled',
153
- commands.get( 'findPrevious' ), 'isEnabled',
154
- commands.get( 'replace' ), 'isEnabled',
155
- commands.get( 'replaceAll' ), 'isEnabled',
156
- ( findNext, findPrevious, replace, replaceAll ) => ( { findNext, findPrevious, replace, replaceAll } )
157
- );
158
-
159
- // The UI plugin works as an interface between the form and the editing part of the feature.
160
- formView.delegate( 'findNext', 'findPrevious', 'replace', 'replaceAll' ).to( this );
161
-
162
- // Let the feature know that search results are no longer relevant because the user changed the searched phrase
163
- // (or options) but didn't hit the "Find" button yet (e.g. still typing).
164
- formView.on( 'change:isDirty', ( evt, data, isDirty ) => {
165
- if ( isDirty ) {
166
- this.fire( 'searchReseted' );
167
- }
168
- } );
169
- }
19
+ /**
20
+ * @inheritDoc
21
+ */
22
+ static get pluginName() {
23
+ return 'FindAndReplaceUI';
24
+ }
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ constructor(editor) {
29
+ super(editor);
30
+ this.formView = null;
31
+ }
32
+ /**
33
+ * @inheritDoc
34
+ */
35
+ init() {
36
+ const editor = this.editor;
37
+ // Register the toolbar dropdown component.
38
+ editor.ui.componentFactory.add('findAndReplace', locale => {
39
+ const dropdown = createDropdown(locale);
40
+ // Dropdown should be disabled when in source editing mode. See #10001.
41
+ const findCommand = editor.commands.get('find');
42
+ dropdown.bind('isEnabled').to(findCommand);
43
+ dropdown.once('change:isOpen', () => {
44
+ this.formView = new (CssTransitionDisablerMixin(FindAndReplaceFormView))(editor.locale);
45
+ dropdown.panelView.children.add(this.formView);
46
+ this._setupFormView(this.formView);
47
+ });
48
+ // Every time a dropdown is opened, the search text field should get focused and selected for better UX.
49
+ // Note: Using the low priority here to make sure the following listener starts working after
50
+ // the default action of the drop-down is executed (i.e. the panel showed up). Otherwise,
51
+ // the invisible form/input cannot be focused/selected.
52
+ //
53
+ // Each time a dropdown is closed, move the focus back to the find and replace toolbar button
54
+ // and let the find and replace editing feature know that all search results can be invalidated
55
+ // and no longer should be marked in the content.
56
+ dropdown.on('change:isOpen', (event, name, isOpen) => {
57
+ if (isOpen) {
58
+ this.formView.disableCssTransitions();
59
+ this.formView.reset();
60
+ this.formView._findInputView.fieldView.select();
61
+ this.formView.enableCssTransitions();
62
+ }
63
+ else {
64
+ this.fire('searchReseted');
65
+ }
66
+ }, { priority: 'low' });
67
+ this._setupDropdownButton(dropdown);
68
+ return dropdown;
69
+ });
70
+ }
71
+ /**
72
+ * Sets up the find and replace button.
73
+ */
74
+ _setupDropdownButton(dropdown) {
75
+ const editor = this.editor;
76
+ const t = editor.locale.t;
77
+ dropdown.buttonView.set({
78
+ icon: loupeIcon,
79
+ label: t('Find and replace'),
80
+ keystroke: 'CTRL+F',
81
+ tooltip: true
82
+ });
83
+ editor.keystrokes.set('Ctrl+F', (data, cancelEvent) => {
84
+ if (dropdown.isEnabled) {
85
+ dropdown.isOpen = true;
86
+ cancelEvent();
87
+ }
88
+ });
89
+ }
90
+ /**
91
+ * Sets up the form view for the find and replace.
92
+ *
93
+ * @param formView A related form view.
94
+ */
95
+ _setupFormView(formView) {
96
+ const editor = this.editor;
97
+ const commands = editor.commands;
98
+ const findAndReplaceEditing = this.editor.plugins.get('FindAndReplaceEditing');
99
+ const editingState = findAndReplaceEditing.state;
100
+ const sortMapping = { before: -1, same: 0, after: 1, different: 1 };
101
+ // Let the form know which result is being highlighted.
102
+ formView.bind('highlightOffset').to(editingState, 'highlightedResult', highlightedResult => {
103
+ if (!highlightedResult) {
104
+ return 0;
105
+ }
106
+ return Array.from(editingState.results)
107
+ .sort((a, b) => sortMapping[a.marker.getStart().compareWith(b.marker.getStart())])
108
+ .indexOf(highlightedResult) + 1;
109
+ });
110
+ // Let the form know how many results were found in total.
111
+ formView.listenTo(editingState.results, 'change', () => {
112
+ formView.matchCount = editingState.results.length;
113
+ });
114
+ // Command states are used to enable/disable individual form controls.
115
+ // To keep things simple, instead of binding 4 individual observables, there's only one that combines every
116
+ // commands' isEnabled state. Yes, it will change more often but this simplifies the structure of the form.
117
+ const findNextCommand = commands.get('findNext');
118
+ const findPreviousCommand = commands.get('findPrevious');
119
+ const replaceCommand = commands.get('replace');
120
+ const replaceAllCommand = commands.get('replaceAll');
121
+ formView.bind('_areCommandsEnabled').to(findNextCommand, 'isEnabled', findPreviousCommand, 'isEnabled', replaceCommand, 'isEnabled', replaceAllCommand, 'isEnabled', (findNext, findPrevious, replace, replaceAll) => ({ findNext, findPrevious, replace, replaceAll }));
122
+ // The UI plugin works as an interface between the form and the editing part of the feature.
123
+ formView.delegate('findNext', 'findPrevious', 'replace', 'replaceAll').to(this);
124
+ // Let the feature know that search results are no longer relevant because the user changed the searched phrase
125
+ // (or options) but didn't hit the "Find" button yet (e.g. still typing).
126
+ formView.on('change:isDirty', (evt, data, isDirty) => {
127
+ if (isDirty) {
128
+ this.fire('searchReseted');
129
+ }
130
+ });
131
+ }
170
132
  }
171
-
172
- /**
173
- * Fired when the find next button is triggered.
174
- *
175
- * @event findNext
176
- * @param {String} searchText Search text.
177
- */
178
-
179
- /**
180
- * Fired when the find previous button is triggered.
181
- *
182
- * @event findPrevious
183
- * @param {String} searchText Search text.
184
- */
185
-
186
- /**
187
- * Fired when the replace button is triggered.
188
- *
189
- * @event replace
190
- * @param {String} replaceText Replacement text.
191
- */
192
-
193
- /**
194
- * Fired when the replaceAll button is triggered.
195
- *
196
- * @event replaceAll
197
- * @param {String} replaceText Replacement text.
198
- */
199
-
200
- /**
201
- * Fired when the UI was reset and the search results marked in the editing root should be invalidated,
202
- * for instance, because the user changed the searched phrase (or options) but didn't hit
203
- * the "Find" button yet.
204
- *
205
- * @event searchReseted
206
- */
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module find-and-replace/findandreplaceutils
7
+ */
8
+ import type { Item, Model, Range } from 'ckeditor5/src/engine';
9
+ import { Plugin } from 'ckeditor5/src/core';
10
+ import { Collection } from 'ckeditor5/src/utils';
11
+ import type { ResultType } from './findandreplace';
12
+ /**
13
+ * A set of helpers related to find and replace.
14
+ */
15
+ export default class FindAndReplaceUtils extends Plugin {
16
+ /**
17
+ * @inheritDoc
18
+ */
19
+ static get pluginName(): 'FindAndReplaceUtils';
20
+ /**
21
+ * Executes findCallback and updates search results list.
22
+ *
23
+ * @param range The model range to scan for matches.
24
+ * @param model The model.
25
+ * @param findCallback The callback that should return `true` if provided text matches the search term.
26
+ * @param startResults An optional collection of find matches that the function should
27
+ * start with. This would be a collection returned by a previous `updateFindResultFromRange()` call.
28
+ * @returns A collection of objects describing find match.
29
+ *
30
+ * An example structure:
31
+ *
32
+ * ```js
33
+ * {
34
+ * id: resultId,
35
+ * label: foundItem.label,
36
+ * marker
37
+ * }
38
+ * ```
39
+ */
40
+ updateFindResultFromRange(range: Range, model: Model, findCallback: ({ item, text }: {
41
+ item: Item;
42
+ text: string;
43
+ }) => Array<ResultType>, startResults: Collection<ResultType> | null): Collection<ResultType>;
44
+ /**
45
+ * Returns text representation of a range. The returned text length should be the same as range length.
46
+ * In order to achieve this, this function will replace inline elements (text-line) as new line character ("\n").
47
+ *
48
+ * @param range The model range.
49
+ * @returns The text content of the provided range.
50
+ */
51
+ rangeToText(range: Range): string;
52
+ /**
53
+ * Creates a text matching callback for a specified search term and matching options.
54
+ *
55
+ * @param searchTerm The search term.
56
+ * @param options Matching options.
57
+ * - options.matchCase=false If set to `true` letter casing will be ignored.
58
+ * - options.wholeWords=false If set to `true` only whole words that match `callbackOrText` will be matched.
59
+ */
60
+ findByTextCallback(searchTerm: string, options: {
61
+ matchCase?: boolean;
62
+ wholeWords?: boolean;
63
+ }): ({ item, text }: {
64
+ item: Item;
65
+ text: string;
66
+ }) => Array<ResultType>;
67
+ }
68
+ declare module '@ckeditor/ckeditor5-core' {
69
+ interface PluginsMap {
70
+ [FindAndReplaceUtils.pluginName]: FindAndReplaceUtils;
71
+ }
72
+ }