@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,827 @@
|
|
|
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/ui/findandreplaceformview
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
View,
|
|
12
|
+
ButtonView,
|
|
13
|
+
FormHeaderView,
|
|
14
|
+
LabeledFieldView,
|
|
15
|
+
|
|
16
|
+
Model,
|
|
17
|
+
FocusCycler,
|
|
18
|
+
createLabeledInputText,
|
|
19
|
+
submitHandler,
|
|
20
|
+
ViewCollection,
|
|
21
|
+
injectCssTransitionDisabler,
|
|
22
|
+
|
|
23
|
+
createDropdown,
|
|
24
|
+
addListToDropdown
|
|
25
|
+
} from 'ckeditor5/src/ui';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
FocusTracker,
|
|
29
|
+
KeystrokeHandler,
|
|
30
|
+
Collection,
|
|
31
|
+
Rect
|
|
32
|
+
} from 'ckeditor5/src/utils';
|
|
33
|
+
|
|
34
|
+
// See: #8833.
|
|
35
|
+
// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
|
|
36
|
+
import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
|
|
37
|
+
import '../../theme/findandreplaceform.css';
|
|
38
|
+
|
|
39
|
+
// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
|
|
40
|
+
import previousArrow from '@ckeditor/ckeditor5-ui/theme/icons/previous-arrow.svg';
|
|
41
|
+
import { icons } from 'ckeditor5/src/core';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The find and replace form view class.
|
|
45
|
+
*
|
|
46
|
+
* See {@link module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView}.
|
|
47
|
+
*
|
|
48
|
+
* @extends module:ui/view~View
|
|
49
|
+
*/
|
|
50
|
+
export default class FindAndReplaceFormView extends View {
|
|
51
|
+
/**
|
|
52
|
+
* Creates a view of find and replace form.
|
|
53
|
+
*
|
|
54
|
+
* @param {module:utils/locale~Locale} [locale] The localization services instance.
|
|
55
|
+
*/
|
|
56
|
+
constructor( locale ) {
|
|
57
|
+
super( locale );
|
|
58
|
+
|
|
59
|
+
const t = locale.t;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stores the number of matched search results.
|
|
63
|
+
*
|
|
64
|
+
* @readonly
|
|
65
|
+
* @observable
|
|
66
|
+
* @member {Number} #matchCount
|
|
67
|
+
*/
|
|
68
|
+
this.set( 'matchCount', 0 );
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The offset of currently highlighted search result in {@link #matchCount matched results}.
|
|
72
|
+
*
|
|
73
|
+
* @readonly
|
|
74
|
+
* @observable
|
|
75
|
+
* @member {Number|null} #highlightOffset
|
|
76
|
+
*/
|
|
77
|
+
this.set( 'highlightOffset', 0 );
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `true` when the search params (find text, options) has been changed by the user since
|
|
81
|
+
* the last time find was executed. `false` otherwise.
|
|
82
|
+
*
|
|
83
|
+
* @readonly
|
|
84
|
+
* @observable
|
|
85
|
+
* @member {Boolean} #isDirty
|
|
86
|
+
*/
|
|
87
|
+
this.set( 'isDirty', false );
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A live object with the aggregated `isEnabled` states of editor commands related to find and
|
|
91
|
+
* replace. For instance, it may look as follows:
|
|
92
|
+
*
|
|
93
|
+
* {
|
|
94
|
+
* findNext: true,
|
|
95
|
+
* findPrevious: true,
|
|
96
|
+
* replace: false,
|
|
97
|
+
* replaceAll: false
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* @protected
|
|
101
|
+
* @readonly
|
|
102
|
+
* @observable
|
|
103
|
+
* @member {Object} #_areCommandsEnabled
|
|
104
|
+
*/
|
|
105
|
+
this.set( '_areCommandsEnabled', {} );
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The content of the counter label displaying the index of the current highlighted match
|
|
109
|
+
* on top of the find input, for instance "3 of 50".
|
|
110
|
+
*
|
|
111
|
+
* @protected
|
|
112
|
+
* @readonly
|
|
113
|
+
* @observable
|
|
114
|
+
* @member {String} #_resultsCounterText
|
|
115
|
+
*/
|
|
116
|
+
this.set( '_resultsCounterText', '' );
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The flag reflecting the state of the "Match case" switch button in the search options
|
|
120
|
+
* dropdown.
|
|
121
|
+
*
|
|
122
|
+
* @protected
|
|
123
|
+
* @readonly
|
|
124
|
+
* @observable
|
|
125
|
+
* @member {Boolean} #_matchCase
|
|
126
|
+
*/
|
|
127
|
+
this.set( '_matchCase', false );
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The flag reflecting the state of the "Whole words only" switch button in the search options
|
|
131
|
+
* dropdown.
|
|
132
|
+
*
|
|
133
|
+
* @protected
|
|
134
|
+
* @readonly
|
|
135
|
+
* @observable
|
|
136
|
+
* @member {Boolean} #_wholeWordsOnly
|
|
137
|
+
*/
|
|
138
|
+
this.set( '_wholeWordsOnly', false );
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* This flag is set `true` when some matches were found and the user didn't change the search
|
|
142
|
+
* params (text to find, options) yet. This is only possible immediately after hitting the "Find" button.
|
|
143
|
+
* `false` when there were no matches (see {@link #matchCount}) or the user changed the params (see {@link #isDirty}).
|
|
144
|
+
*
|
|
145
|
+
* It is used to control the enabled state of the replace UI (input and buttons); replacing text is only possible
|
|
146
|
+
* if this flag is `true`.
|
|
147
|
+
*
|
|
148
|
+
* @protected
|
|
149
|
+
* @readonly
|
|
150
|
+
* @observable
|
|
151
|
+
* @member {Boolean} #_searchResultsFound
|
|
152
|
+
*/
|
|
153
|
+
this.bind( '_searchResultsFound' ).to(
|
|
154
|
+
this, 'matchCount',
|
|
155
|
+
this, 'isDirty',
|
|
156
|
+
( matchCount, isDirty ) => {
|
|
157
|
+
return matchCount > 0 && !isDirty;
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The find in text input view that stores the searched string.
|
|
163
|
+
*
|
|
164
|
+
* @protected
|
|
165
|
+
* @readonly
|
|
166
|
+
* @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
|
|
167
|
+
*/
|
|
168
|
+
this._findInputView = this._createInputField( t( 'Find in text…' ) );
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The replace input view.
|
|
172
|
+
*
|
|
173
|
+
* @protected
|
|
174
|
+
* @readonly
|
|
175
|
+
* @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
|
|
176
|
+
*/
|
|
177
|
+
this._replaceInputView = this._createInputField( t( 'Replace with…' ) );
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The find button view that initializes the search process.
|
|
181
|
+
*
|
|
182
|
+
* @protected
|
|
183
|
+
* @readonly
|
|
184
|
+
* @member {module:ui/button/buttonview~ButtonView}
|
|
185
|
+
*/
|
|
186
|
+
this._findButtonView = this._createButton( {
|
|
187
|
+
label: t( 'Find' ),
|
|
188
|
+
class: 'ck-button-find ck-button-action',
|
|
189
|
+
withText: true
|
|
190
|
+
} );
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The find previous button view.
|
|
194
|
+
*
|
|
195
|
+
* @protected
|
|
196
|
+
* @readonly
|
|
197
|
+
* @member {module:ui/button/buttonview~ButtonView}
|
|
198
|
+
*/
|
|
199
|
+
this._findPrevButtonView = this._createButton( {
|
|
200
|
+
label: t( 'Previous result' ),
|
|
201
|
+
class: 'ck-button-prev',
|
|
202
|
+
icon: previousArrow,
|
|
203
|
+
keystroke: 'Shift+F3',
|
|
204
|
+
tooltip: true
|
|
205
|
+
} );
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The find next button view.
|
|
209
|
+
*
|
|
210
|
+
* @protected
|
|
211
|
+
* @readonly
|
|
212
|
+
* @member {module:ui/button/buttonview~ButtonView}
|
|
213
|
+
*/
|
|
214
|
+
this._findNextButtonView = this._createButton( {
|
|
215
|
+
label: t( 'Next result' ),
|
|
216
|
+
class: 'ck-button-next',
|
|
217
|
+
icon: previousArrow,
|
|
218
|
+
keystroke: 'F3',
|
|
219
|
+
tooltip: true
|
|
220
|
+
} );
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The find options dropdown.
|
|
224
|
+
*
|
|
225
|
+
* @protected
|
|
226
|
+
* @readonly
|
|
227
|
+
* @member {module:ui/dropdown/dropdownview~DropdownView}
|
|
228
|
+
*/
|
|
229
|
+
this._optionsDropdown = this._createOptionsDropdown();
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The replace button view.
|
|
233
|
+
*
|
|
234
|
+
* @protected
|
|
235
|
+
* @readonly
|
|
236
|
+
* @member {module:ui/button/buttonview~ButtonView}
|
|
237
|
+
*/
|
|
238
|
+
this._replaceButtonView = this._createButton( {
|
|
239
|
+
label: t( 'Replace' ),
|
|
240
|
+
class: 'ck-button-replace',
|
|
241
|
+
withText: true
|
|
242
|
+
} );
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The replace all button view.
|
|
246
|
+
*
|
|
247
|
+
* @protected
|
|
248
|
+
* @readonly
|
|
249
|
+
* @member {module:ui/button/buttonview~ButtonView}
|
|
250
|
+
*/
|
|
251
|
+
this._replaceAllButtonView = this._createButton( {
|
|
252
|
+
label: t( 'Replace all' ),
|
|
253
|
+
class: 'ck-button-replaceall',
|
|
254
|
+
withText: true
|
|
255
|
+
} );
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The fieldset aggregating the find UI.
|
|
259
|
+
*
|
|
260
|
+
* @protected
|
|
261
|
+
* @readonly
|
|
262
|
+
* @member {module:ui/view/view~View}
|
|
263
|
+
*/
|
|
264
|
+
this._findFieldsetView = this._createFindFieldset();
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The fieldset aggregating the replace UI.
|
|
268
|
+
*
|
|
269
|
+
* @protected
|
|
270
|
+
* @readonly
|
|
271
|
+
* @member {module:ui/view/view~View}
|
|
272
|
+
*/
|
|
273
|
+
this._replaceFieldsetView = this._createReplaceFieldset();
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Tracks information about the DOM focus in the form.
|
|
277
|
+
*
|
|
278
|
+
* @readonly
|
|
279
|
+
* @protected
|
|
280
|
+
* @member {module:utils/focustracker~FocusTracker}
|
|
281
|
+
*/
|
|
282
|
+
this._focusTracker = new FocusTracker();
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
286
|
+
*
|
|
287
|
+
* @readonly
|
|
288
|
+
* @protected
|
|
289
|
+
* @member {module:utils/keystrokehandler~KeystrokeHandler}
|
|
290
|
+
*/
|
|
291
|
+
this._keystrokes = new KeystrokeHandler();
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* A collection of views that can be focused in the form.
|
|
295
|
+
*
|
|
296
|
+
* @readonly
|
|
297
|
+
* @protected
|
|
298
|
+
* @member {module:ui/viewcollection~ViewCollection}
|
|
299
|
+
*/
|
|
300
|
+
this._focusables = new ViewCollection();
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Helps cycling over {@link #_focusables} in the form.
|
|
304
|
+
*
|
|
305
|
+
* @readonly
|
|
306
|
+
* @protected
|
|
307
|
+
* @member {module:ui/focuscycler~FocusCycler}
|
|
308
|
+
*/
|
|
309
|
+
this._focusCycler = new FocusCycler( {
|
|
310
|
+
focusables: this._focusables,
|
|
311
|
+
focusTracker: this._focusTracker,
|
|
312
|
+
keystrokeHandler: this._keystrokes,
|
|
313
|
+
actions: {
|
|
314
|
+
// Navigate form fields backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
|
|
315
|
+
focusPrevious: 'shift + tab',
|
|
316
|
+
|
|
317
|
+
// Navigate form fields forwards using the <kbd>Tab</kbd> key.
|
|
318
|
+
focusNext: 'tab'
|
|
319
|
+
}
|
|
320
|
+
} );
|
|
321
|
+
|
|
322
|
+
this.setTemplate( {
|
|
323
|
+
tag: 'form',
|
|
324
|
+
attributes: {
|
|
325
|
+
class: [
|
|
326
|
+
'ck',
|
|
327
|
+
'ck-find-and-replace-form'
|
|
328
|
+
],
|
|
329
|
+
|
|
330
|
+
tabindex: '-1'
|
|
331
|
+
},
|
|
332
|
+
children: [
|
|
333
|
+
new FormHeaderView( locale, {
|
|
334
|
+
label: t( 'Find and replace' )
|
|
335
|
+
} ),
|
|
336
|
+
this._findFieldsetView,
|
|
337
|
+
this._replaceFieldsetView
|
|
338
|
+
]
|
|
339
|
+
} );
|
|
340
|
+
|
|
341
|
+
injectCssTransitionDisabler( this );
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @inheritDoc
|
|
346
|
+
*/
|
|
347
|
+
render() {
|
|
348
|
+
super.render();
|
|
349
|
+
|
|
350
|
+
submitHandler( { view: this } );
|
|
351
|
+
|
|
352
|
+
this._initFocusCycling();
|
|
353
|
+
this._initKeystrokeHandling();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Focuses the fist {@link #_focusables} in the form.
|
|
358
|
+
*/
|
|
359
|
+
focus() {
|
|
360
|
+
this._focusCycler.focusFirst();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Resets the form before re-appearing.
|
|
365
|
+
*
|
|
366
|
+
* It clears error messages, hides the match counter and disables the replace feature
|
|
367
|
+
* until the next hit of the "Find" button.
|
|
368
|
+
*
|
|
369
|
+
* **Note**: It does not reset inputs and options, though. This way the form works better in editors with
|
|
370
|
+
* disappearing toolbar (e.g. BalloonEditor): hiding the toolbar by accident (together with the find and replace UI)
|
|
371
|
+
* does not require filling the entire form again.
|
|
372
|
+
*/
|
|
373
|
+
reset() {
|
|
374
|
+
this._findInputView.errorText = null;
|
|
375
|
+
this.isDirty = true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Returns the value of the find input.
|
|
380
|
+
*
|
|
381
|
+
* @protected
|
|
382
|
+
* @returns {String}
|
|
383
|
+
*/
|
|
384
|
+
get _textToFind() {
|
|
385
|
+
return this._findInputView.fieldView.element.value;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Returns the value of the replace input.
|
|
390
|
+
*
|
|
391
|
+
* @protected
|
|
392
|
+
* @returns {String}
|
|
393
|
+
*/
|
|
394
|
+
get _textToReplace() {
|
|
395
|
+
return this._replaceInputView.fieldView.element.value;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Configures and returns the `<fieldset>` aggregating all find controls.
|
|
400
|
+
*
|
|
401
|
+
* @private
|
|
402
|
+
* @returns {module:ui/view~View}
|
|
403
|
+
*/
|
|
404
|
+
_createFindFieldset() {
|
|
405
|
+
const locale = this.locale;
|
|
406
|
+
const fieldsetView = new View( locale );
|
|
407
|
+
|
|
408
|
+
// Typing in the find field invalidates all previous results (the form is "dirty").
|
|
409
|
+
this._findInputView.fieldView.on( 'input', () => {
|
|
410
|
+
this.isDirty = true;
|
|
411
|
+
} );
|
|
412
|
+
|
|
413
|
+
this._findButtonView.on( 'execute', this._onFindButtonExecute.bind( this ) );
|
|
414
|
+
|
|
415
|
+
// Pressing prev/next buttons fires related event on the form.
|
|
416
|
+
this._findPrevButtonView.delegate( 'execute' ).to( this, 'findPrevious' );
|
|
417
|
+
this._findNextButtonView.delegate( 'execute' ).to( this, 'findNext' );
|
|
418
|
+
|
|
419
|
+
// Prev/next buttons will be disabled when related editor command gets disabled.
|
|
420
|
+
this._findPrevButtonView.bind( 'isEnabled' ).to( this, '_areCommandsEnabled', ( { findPrevious } ) => findPrevious );
|
|
421
|
+
this._findNextButtonView.bind( 'isEnabled' ).to( this, '_areCommandsEnabled', ( { findNext } ) => findNext );
|
|
422
|
+
|
|
423
|
+
this._injectFindResultsCounter();
|
|
424
|
+
|
|
425
|
+
fieldsetView.setTemplate( {
|
|
426
|
+
tag: 'fieldset',
|
|
427
|
+
attributes: {
|
|
428
|
+
class: [ 'ck', 'ck-find-and-replace-form__find' ]
|
|
429
|
+
},
|
|
430
|
+
children: [
|
|
431
|
+
this._findInputView,
|
|
432
|
+
this._findButtonView,
|
|
433
|
+
this._findPrevButtonView,
|
|
434
|
+
this._findNextButtonView
|
|
435
|
+
]
|
|
436
|
+
} );
|
|
437
|
+
|
|
438
|
+
return fieldsetView;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The action performed when the {@link #_findButtonView} is pressed.
|
|
443
|
+
*
|
|
444
|
+
* @private
|
|
445
|
+
*/
|
|
446
|
+
_onFindButtonExecute() {
|
|
447
|
+
// When hitting "Find" in an empty input, an error should be displayed.
|
|
448
|
+
// Also, if the form was "dirty", it should remain so.
|
|
449
|
+
if ( !this._textToFind ) {
|
|
450
|
+
const t = this.t;
|
|
451
|
+
|
|
452
|
+
this._findInputView.errorText = t( 'Text to find must not be empty.' );
|
|
453
|
+
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Hitting "Find" automatically clears the dirty state.
|
|
458
|
+
this.isDirty = false;
|
|
459
|
+
|
|
460
|
+
this.fire( 'findNext', {
|
|
461
|
+
searchText: this._textToFind,
|
|
462
|
+
matchCase: this._matchCase,
|
|
463
|
+
wholeWords: this._wholeWordsOnly
|
|
464
|
+
} );
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Configures an injects the find results counter displaying a "N of M" label of the {@link #_findInputView}.
|
|
469
|
+
*
|
|
470
|
+
* @private
|
|
471
|
+
*/
|
|
472
|
+
_injectFindResultsCounter() {
|
|
473
|
+
const locale = this.locale;
|
|
474
|
+
const t = locale.t;
|
|
475
|
+
const bind = this.bindTemplate;
|
|
476
|
+
const resultsCounterView = new View( locale );
|
|
477
|
+
|
|
478
|
+
this.bind( '_resultsCounterText' ).to( this, 'highlightOffset', this, 'matchCount',
|
|
479
|
+
( highlightOffset, matchCount ) => t( '%0 of %1', [ highlightOffset, matchCount ] )
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
resultsCounterView.setTemplate( {
|
|
483
|
+
tag: 'span',
|
|
484
|
+
attributes: {
|
|
485
|
+
class: [
|
|
486
|
+
'ck',
|
|
487
|
+
'ck-results-counter',
|
|
488
|
+
// The counter only makes sense when the field text corresponds to search results in the editing.
|
|
489
|
+
bind.if( 'isDirty', 'ck-hidden' )
|
|
490
|
+
]
|
|
491
|
+
},
|
|
492
|
+
children: [
|
|
493
|
+
{
|
|
494
|
+
text: bind.to( '_resultsCounterText' )
|
|
495
|
+
}
|
|
496
|
+
]
|
|
497
|
+
} );
|
|
498
|
+
|
|
499
|
+
// The whole idea is that when the text of the counter changes, its width also increases/decreases and
|
|
500
|
+
// it consumes more or less space over the input. The input, on the other hand, should adjust it's right
|
|
501
|
+
// padding so its *entire* text always remains visible and available to the user.
|
|
502
|
+
const updateFindInputPadding = () => {
|
|
503
|
+
const inputElement = this._findInputView.fieldView.element;
|
|
504
|
+
|
|
505
|
+
// Don't adjust the padding if the input (also: counter) were not rendered or not inserted into DOM yet.
|
|
506
|
+
if ( !inputElement || !inputElement.offsetParent ) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const counterWidth = new Rect( resultsCounterView.element ).width;
|
|
511
|
+
const paddingPropertyName = locale.uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
|
|
512
|
+
|
|
513
|
+
if ( !counterWidth ) {
|
|
514
|
+
inputElement.style[ paddingPropertyName ] = null;
|
|
515
|
+
} else {
|
|
516
|
+
inputElement.style[ paddingPropertyName ] = `calc( 2 * var(--ck-spacing-standard) + ${ counterWidth }px )`;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Adjust the input padding when the text of the counter changes, for instance "1 of 200" is narrower than "123 of 200".
|
|
521
|
+
// Using "low" priority to let the text be set by the template binding first.
|
|
522
|
+
this.on( 'change:_resultsCounterText', updateFindInputPadding, { priority: 'low' } );
|
|
523
|
+
|
|
524
|
+
// Adjust the input padding when the counter shows or hides. When hidden, there should be no padding. When it shows, the
|
|
525
|
+
// padding should be set according to the text of the counter.
|
|
526
|
+
// Using "low" priority to let the text be set by the template binding first.
|
|
527
|
+
this.on( 'change:isDirty', updateFindInputPadding, { priority: 'low' } );
|
|
528
|
+
|
|
529
|
+
// Put the counter element next to the <input> in the find field.
|
|
530
|
+
this._findInputView.template.children[ 0 ].children.push( resultsCounterView );
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Configures and returns the `<fieldset>` aggregating all replace controls.
|
|
535
|
+
*
|
|
536
|
+
* @private
|
|
537
|
+
* @returns {module:ui/view~View}
|
|
538
|
+
*/
|
|
539
|
+
_createReplaceFieldset() {
|
|
540
|
+
const locale = this.locale;
|
|
541
|
+
const t = locale.t;
|
|
542
|
+
const fieldsetView = new View( locale );
|
|
543
|
+
|
|
544
|
+
this._replaceButtonView.bind( 'isEnabled' ).to(
|
|
545
|
+
this, '_areCommandsEnabled',
|
|
546
|
+
this, '_searchResultsFound',
|
|
547
|
+
( { replace }, resultsFound ) => replace && resultsFound );
|
|
548
|
+
|
|
549
|
+
this._replaceAllButtonView.bind( 'isEnabled' ).to(
|
|
550
|
+
this, '_areCommandsEnabled',
|
|
551
|
+
this, '_searchResultsFound',
|
|
552
|
+
( { replaceAll }, resultsFound ) => replaceAll && resultsFound );
|
|
553
|
+
|
|
554
|
+
this._replaceInputView.bind( 'isEnabled' ).to(
|
|
555
|
+
this, '_areCommandsEnabled',
|
|
556
|
+
this, '_searchResultsFound',
|
|
557
|
+
( { replace }, resultsFound ) => replace && resultsFound );
|
|
558
|
+
|
|
559
|
+
this._replaceInputView.bind( 'infoText' ).to(
|
|
560
|
+
this._replaceInputView, 'isEnabled',
|
|
561
|
+
this._replaceInputView, 'isFocused',
|
|
562
|
+
( isEnabled, isFocused ) => {
|
|
563
|
+
if ( isEnabled || !isFocused ) {
|
|
564
|
+
return '';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return t( 'Tip: Find some text first in order to replace it.' );
|
|
568
|
+
} );
|
|
569
|
+
|
|
570
|
+
this._replaceButtonView.on( 'execute', () => {
|
|
571
|
+
this.fire( 'replace', {
|
|
572
|
+
searchText: this._textToFind,
|
|
573
|
+
replaceText: this._textToReplace
|
|
574
|
+
} );
|
|
575
|
+
} );
|
|
576
|
+
|
|
577
|
+
this._replaceAllButtonView.on( 'execute', () => {
|
|
578
|
+
this.fire( 'replaceAll', {
|
|
579
|
+
searchText: this._textToFind,
|
|
580
|
+
replaceText: this._textToReplace
|
|
581
|
+
} );
|
|
582
|
+
|
|
583
|
+
this.focus();
|
|
584
|
+
} );
|
|
585
|
+
|
|
586
|
+
fieldsetView.setTemplate( {
|
|
587
|
+
tag: 'fieldset',
|
|
588
|
+
attributes: {
|
|
589
|
+
class: [ 'ck', 'ck-find-and-replace-form__replace' ]
|
|
590
|
+
},
|
|
591
|
+
children: [
|
|
592
|
+
this._replaceInputView,
|
|
593
|
+
this._optionsDropdown,
|
|
594
|
+
this._replaceButtonView,
|
|
595
|
+
this._replaceAllButtonView
|
|
596
|
+
]
|
|
597
|
+
} );
|
|
598
|
+
|
|
599
|
+
return fieldsetView;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Creates, configures and returns and instance of a dropdown allowing users to narrow
|
|
604
|
+
* the search criteria down. The dropdown has a list with switch buttons for each option.
|
|
605
|
+
*
|
|
606
|
+
* @private
|
|
607
|
+
* @returns {module:ui/dropdown/dropdownview~DropdownView}
|
|
608
|
+
*/
|
|
609
|
+
_createOptionsDropdown() {
|
|
610
|
+
const locale = this.locale;
|
|
611
|
+
const t = locale.t;
|
|
612
|
+
const dropdownView = createDropdown( locale );
|
|
613
|
+
|
|
614
|
+
dropdownView.class = 'ck-options-dropdown';
|
|
615
|
+
|
|
616
|
+
dropdownView.buttonView.set( {
|
|
617
|
+
withText: false,
|
|
618
|
+
label: t( 'Show options' ),
|
|
619
|
+
icon: icons.cog,
|
|
620
|
+
tooltip: true
|
|
621
|
+
} );
|
|
622
|
+
|
|
623
|
+
const matchCaseModel = new Model( {
|
|
624
|
+
withText: true,
|
|
625
|
+
label: t( 'Match case' ),
|
|
626
|
+
|
|
627
|
+
// A dummy read-only prop to make it easy to tell which switch was toggled.
|
|
628
|
+
_isMatchCaseSwitch: true
|
|
629
|
+
} );
|
|
630
|
+
|
|
631
|
+
const wholeWordsOnlyModel = new Model( {
|
|
632
|
+
withText: true,
|
|
633
|
+
label: t( 'Whole words only' )
|
|
634
|
+
} );
|
|
635
|
+
|
|
636
|
+
// Let the switches be controlled by form's observable properties.
|
|
637
|
+
matchCaseModel.bind( 'isOn' ).to( this, '_matchCase' );
|
|
638
|
+
wholeWordsOnlyModel.bind( 'isOn' ).to( this, '_wholeWordsOnly' );
|
|
639
|
+
|
|
640
|
+
// Update the state of the form when a switch is toggled.
|
|
641
|
+
dropdownView.on( 'execute', evt => {
|
|
642
|
+
if ( evt.source._isMatchCaseSwitch ) {
|
|
643
|
+
this._matchCase = !this._matchCase;
|
|
644
|
+
} else {
|
|
645
|
+
this._wholeWordsOnly = !this._wholeWordsOnly;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Toggling a switch makes the form dirty because this changes search criteria
|
|
649
|
+
// just like typing text of the find input.
|
|
650
|
+
this.isDirty = true;
|
|
651
|
+
} );
|
|
652
|
+
|
|
653
|
+
addListToDropdown( dropdownView, new Collection( [
|
|
654
|
+
{ type: 'switchbutton', model: matchCaseModel },
|
|
655
|
+
{ type: 'switchbutton', model: wholeWordsOnlyModel }
|
|
656
|
+
] ) );
|
|
657
|
+
|
|
658
|
+
return dropdownView;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Initializes the {@link #_focusables} and {@link #_focusTracker} to allow navigation
|
|
663
|
+
* using <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> keystrokes in the right order.
|
|
664
|
+
*
|
|
665
|
+
* @private
|
|
666
|
+
*/
|
|
667
|
+
_initFocusCycling() {
|
|
668
|
+
const childViews = [
|
|
669
|
+
this._findInputView,
|
|
670
|
+
this._findButtonView,
|
|
671
|
+
this._findPrevButtonView,
|
|
672
|
+
this._findNextButtonView,
|
|
673
|
+
this._replaceInputView,
|
|
674
|
+
this._optionsDropdown,
|
|
675
|
+
this._replaceButtonView,
|
|
676
|
+
this._replaceAllButtonView
|
|
677
|
+
];
|
|
678
|
+
|
|
679
|
+
childViews.forEach( v => {
|
|
680
|
+
// Register the view as focusable.
|
|
681
|
+
this._focusables.add( v );
|
|
682
|
+
|
|
683
|
+
// Register the view in the focus tracker.
|
|
684
|
+
this._focusTracker.add( v.element );
|
|
685
|
+
} );
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Initializes the keystroke handling in the form.
|
|
690
|
+
*
|
|
691
|
+
* @private
|
|
692
|
+
*/
|
|
693
|
+
_initKeystrokeHandling() {
|
|
694
|
+
const stopPropagation = data => data.stopPropagation();
|
|
695
|
+
const stopPropagationAndPreventDefault = data => {
|
|
696
|
+
data.stopPropagation();
|
|
697
|
+
data.preventDefault();
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// Start listening for the keystrokes coming from #element.
|
|
701
|
+
this._keystrokes.listenTo( this.element );
|
|
702
|
+
|
|
703
|
+
// Find the next result upon F3.
|
|
704
|
+
this._keystrokes.set( 'f3', event => {
|
|
705
|
+
stopPropagationAndPreventDefault( event );
|
|
706
|
+
|
|
707
|
+
this._findNextButtonView.fire( 'execute' );
|
|
708
|
+
} );
|
|
709
|
+
|
|
710
|
+
// Find the previous result upon F3.
|
|
711
|
+
this._keystrokes.set( 'shift+f3', event => {
|
|
712
|
+
stopPropagationAndPreventDefault( event );
|
|
713
|
+
|
|
714
|
+
this._findPrevButtonView.fire( 'execute' );
|
|
715
|
+
} );
|
|
716
|
+
|
|
717
|
+
// Find or replace upon pressing Enter in the find and replace fields.
|
|
718
|
+
this._keystrokes.set( 'enter', event => {
|
|
719
|
+
const target = event.target;
|
|
720
|
+
|
|
721
|
+
if ( target === this._findInputView.fieldView.element ) {
|
|
722
|
+
if ( this._areCommandsEnabled.findNext ) {
|
|
723
|
+
this._findNextButtonView.fire( 'execute' );
|
|
724
|
+
} else {
|
|
725
|
+
this._findButtonView.fire( 'execute' );
|
|
726
|
+
}
|
|
727
|
+
stopPropagationAndPreventDefault( event );
|
|
728
|
+
} else if ( target === this._replaceInputView.fieldView.element ) {
|
|
729
|
+
this._replaceButtonView.fire( 'execute' );
|
|
730
|
+
stopPropagationAndPreventDefault( event );
|
|
731
|
+
}
|
|
732
|
+
} );
|
|
733
|
+
|
|
734
|
+
// Find previous upon pressing Shift+Enter in the find field.
|
|
735
|
+
this._keystrokes.set( 'shift+enter', event => {
|
|
736
|
+
const target = event.target;
|
|
737
|
+
|
|
738
|
+
if ( target !== this._findInputView.fieldView.element ) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if ( this._areCommandsEnabled.findPrevious ) {
|
|
743
|
+
this._findPrevButtonView.fire( 'execute' );
|
|
744
|
+
} else {
|
|
745
|
+
this._findButtonView.fire( 'execute' );
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
stopPropagationAndPreventDefault( event );
|
|
749
|
+
} );
|
|
750
|
+
|
|
751
|
+
// Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's
|
|
752
|
+
// keystroke handler would take over the key management in the URL input.
|
|
753
|
+
// We need to prevent this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible.
|
|
754
|
+
this._keystrokes.set( 'arrowright', stopPropagation );
|
|
755
|
+
this._keystrokes.set( 'arrowleft', stopPropagation );
|
|
756
|
+
this._keystrokes.set( 'arrowup', stopPropagation );
|
|
757
|
+
this._keystrokes.set( 'arrowdown', stopPropagation );
|
|
758
|
+
|
|
759
|
+
// Intercept the `selectstart` event, which is blocked by default because of the default behavior
|
|
760
|
+
// of the DropdownView#panelView. This blocking prevents the native select all on Ctrl+A.
|
|
761
|
+
this.listenTo( this._findInputView.element, 'selectstart', ( evt, domEvt ) => {
|
|
762
|
+
domEvt.stopPropagation();
|
|
763
|
+
}, { priority: 'high' } );
|
|
764
|
+
|
|
765
|
+
this.listenTo( this._replaceInputView.element, 'selectstart', ( evt, domEvt ) => {
|
|
766
|
+
domEvt.stopPropagation();
|
|
767
|
+
}, { priority: 'high' } );
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Creates a button view.
|
|
772
|
+
*
|
|
773
|
+
* @private
|
|
774
|
+
* @param {Object} options The properties of the `ButtonView`.
|
|
775
|
+
* @returns {module:ui/button/buttonview~ButtonView} The button view instance.
|
|
776
|
+
*/
|
|
777
|
+
_createButton( options ) {
|
|
778
|
+
const button = new ButtonView( this.locale );
|
|
779
|
+
|
|
780
|
+
button.set( options );
|
|
781
|
+
|
|
782
|
+
return button;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Creates a labeled input view.
|
|
787
|
+
*
|
|
788
|
+
* @private
|
|
789
|
+
* @param {String} label The input label.
|
|
790
|
+
* @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} The labeled input view instance.
|
|
791
|
+
*/
|
|
792
|
+
_createInputField( label ) {
|
|
793
|
+
const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
|
|
794
|
+
|
|
795
|
+
labeledInput.label = label;
|
|
796
|
+
|
|
797
|
+
return labeledInput;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Fired when the find next button is triggered.
|
|
803
|
+
*
|
|
804
|
+
* @event findNext
|
|
805
|
+
* @param {String} searchText Search text.
|
|
806
|
+
*/
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Fired when the find previous button is triggered.
|
|
810
|
+
*
|
|
811
|
+
* @event findPrevious
|
|
812
|
+
* @param {String} searchText Search text.
|
|
813
|
+
*/
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Fired when the replace button is triggered.
|
|
817
|
+
*
|
|
818
|
+
* @event replace
|
|
819
|
+
* @param {String} replaceText Replacement text.
|
|
820
|
+
*/
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Fired when the replaceAll button is triggered.
|
|
824
|
+
*
|
|
825
|
+
* @event replaceAll
|
|
826
|
+
* @param {String} replaceText Replacement text.
|
|
827
|
+
*/
|