@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,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
+ */