1 /*
  2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
  3 For licensing, see LICENSE.html or http://ckeditor.com/license
  4 */
  5 
  6 (function()
  7 {
  8 	var isReplace;
  9 
 10 	function findEvaluator( node )
 11 	{
 12 		return node.type == CKEDITOR.NODE_TEXT && node.getLength() > 0 && ( !isReplace || !node.isReadOnly() );
 13 	}
 14 
 15 	/**
 16 	 * Elements which break characters been considered as sequence.
 17 	*/
 18 	function nonCharactersBoundary( node )
 19 	{
 20 		return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary(
 21 			CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$empty, CKEDITOR.dtd.$nonEditable ) ) );
 22 	}
 23 
 24 	/**
 25 	 * Get the cursor object which represent both current character and it's dom
 26 	 * position thing.
 27 	 */
 28 	var cursorStep = function()
 29 	{
 30 		return {
 31 			textNode : this.textNode,
 32 			offset : this.offset,
 33 			character : this.textNode ?
 34 				this.textNode.getText().charAt( this.offset ) : null,
 35 			hitMatchBoundary : this._.matchBoundary
 36 		};
 37 	};
 38 
 39 	var pages = [ 'find', 'replace' ],
 40 		fieldsMapping = [
 41 		[ 'txtFindFind', 'txtFindReplace' ],
 42 		[ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
 43 		[ 'txtFindWordChk', 'txtReplaceWordChk' ],
 44 		[ 'txtFindCyclic', 'txtReplaceCyclic' ] ];
 45 
 46 	/**
 47 	 * Synchronize corresponding filed values between 'replace' and 'find' pages.
 48 	 * @param {String} currentPageId	The page id which receive values.
 49 	 */
 50 	function syncFieldsBetweenTabs( currentPageId )
 51 	{
 52 		var sourceIndex, targetIndex,
 53 			sourceField, targetField;
 54 
 55 		sourceIndex = currentPageId === 'find' ? 1 : 0;
 56 		targetIndex = 1 - sourceIndex;
 57 		var i, l = fieldsMapping.length;
 58 		for ( i = 0 ; i < l ; i++ )
 59 		{
 60 			sourceField = this.getContentElement( pages[ sourceIndex ],
 61 					fieldsMapping[ i ][ sourceIndex ] );
 62 			targetField = this.getContentElement( pages[ targetIndex ],
 63 					fieldsMapping[ i ][ targetIndex ] );
 64 
 65 			targetField.setValue( sourceField.getValue() );
 66 		}
 67 	}
 68 
 69 	var findDialog = function( editor, startupPage )
 70 	{
 71 		// Style object for highlights: (#5018)
 72 		// 1. Defined as full match style to avoid compromising ordinary text color styles.
 73 		// 2. Must be apply onto inner-most text to avoid conflicting with ordinary text color styles visually.
 74 		var highlightStyle = new CKEDITOR.style(
 75 			CKEDITOR.tools.extend( { attributes : { 'data-cke-highlight': 1 }, fullMatch : 1, ignoreReadonly : 1, childRule : function(){ return 0; } },
 76 			editor.config.find_highlight, true ) );
 77 
 78 		/**
 79 		 * Iterator which walk through the specified range char by char. By
 80 		 * default the walking will not stop at the character boundaries, until
 81 		 * the end of the range is encountered.
 82 		 * @param { CKEDITOR.dom.range } range
 83 		 * @param {Boolean} matchWord Whether the walking will stop at character boundary.
 84 		 */
 85 		var characterWalker = function( range , matchWord )
 86 		{
 87 			var self = this;
 88 			var walker =
 89 				new CKEDITOR.dom.walker( range );
 90 			walker.guard = matchWord ? nonCharactersBoundary : function( node )
 91 			{
 92 				!nonCharactersBoundary( node ) && ( self._.matchBoundary = true );
 93 			};
 94 			walker[ 'evaluator' ] = findEvaluator;
 95 			walker.breakOnFalse = 1;
 96 
 97 			if ( range.startContainer.type == CKEDITOR.NODE_TEXT )
 98 			{
 99 				this.textNode = range.startContainer;
100 				this.offset = range.startOffset - 1;
101 			}
102 
103 			this._ = {
104 				matchWord : matchWord,
105 				walker : walker,
106 				matchBoundary : false
107 			};
108 		};
109 
110 		characterWalker.prototype = {
111 			next : function()
112 			{
113 				return this.move();
114 			},
115 
116 			back : function()
117 			{
118 				return this.move( true );
119 			},
120 
121 			move : function( rtl )
122 			{
123 				var currentTextNode = this.textNode;
124 				// Already at the end of document, no more character available.
125 				if ( currentTextNode === null )
126 					return cursorStep.call( this );
127 
128 				this._.matchBoundary = false;
129 
130 				// There are more characters in the text node, step forward.
131 				if ( currentTextNode
132 				    && rtl
133 					&& this.offset > 0 )
134 				{
135 					this.offset--;
136 					return cursorStep.call( this );
137 				}
138 				else if ( currentTextNode
139 					&& this.offset < currentTextNode.getLength() - 1 )
140 				{
141 					this.offset++;
142 					return cursorStep.call( this );
143 				}
144 				else
145 				{
146 					currentTextNode = null;
147 					// At the end of the text node, walking foward for the next.
148 					while ( !currentTextNode )
149 					{
150 						currentTextNode =
151 							this._.walker[ rtl ? 'previous' : 'next' ].call( this._.walker );
152 
153 						// Stop searching if we're need full word match OR
154 						// already reach document end.
155 						if ( this._.matchWord && !currentTextNode
156 							 || this._.walker._.end )
157 							break;
158 						}
159 					// Found a fresh text node.
160 					this.textNode = currentTextNode;
161 					if ( currentTextNode )
162 						this.offset = rtl ? currentTextNode.getLength() - 1 : 0;
163 					else
164 						this.offset = 0;
165 				}
166 
167 				return cursorStep.call( this );
168 			}
169 
170 		};
171 
172 		/**
173 		 * A range of cursors which represent a trunk of characters which try to
174 		 * match, it has the same length as the pattern  string.
175 		 */
176 		var characterRange = function( characterWalker, rangeLength )
177 		{
178 			this._ = {
179 				walker : characterWalker,
180 				cursors : [],
181 				rangeLength : rangeLength,
182 				highlightRange : null,
183 				isMatched : 0
184 			};
185 		};
186 
187 		characterRange.prototype = {
188 			/**
189 			 * Translate this range to {@link CKEDITOR.dom.range}
190 			 */
191 			toDomRange : function()
192 			{
193 				var range = new CKEDITOR.dom.range( editor.document );
194 				var cursors = this._.cursors;
195 				if ( cursors.length < 1 )
196 				{
197 					var textNode = this._.walker.textNode;
198 					if ( textNode )
199 							range.setStartAfter( textNode );
200 					else
201 						return null;
202 				}
203 				else
204 				{
205 					var first = cursors[0],
206 							last = cursors[ cursors.length - 1 ];
207 
208 					range.setStart( first.textNode, first.offset );
209 					range.setEnd( last.textNode, last.offset + 1 );
210 				}
211 
212 				return range;
213 			},
214 			/**
215 			 * Reflect the latest changes from dom range.
216 			 */
217 			updateFromDomRange : function( domRange )
218 			{
219 				var cursor,
220 						walker = new characterWalker( domRange );
221 				this._.cursors = [];
222 				do
223 				{
224 					cursor = walker.next();
225 					if ( cursor.character )
226 						this._.cursors.push( cursor );
227 				}
228 				while ( cursor.character );
229 				this._.rangeLength = this._.cursors.length;
230 			},
231 
232 			setMatched : function()
233 			{
234 				this._.isMatched = true;
235 			},
236 
237 			clearMatched : function()
238 			{
239 				this._.isMatched = false;
240 			},
241 
242 			isMatched : function()
243 			{
244 				return this._.isMatched;
245 			},
246 
247 			/**
248 			 * Hightlight the current matched chunk of text.
249 			 */
250 			highlight : function()
251 			{
252 				// Do not apply if nothing is found.
253 				if ( this._.cursors.length < 1 )
254 					return;
255 
256 				// Remove the previous highlight if there's one.
257 				if ( this._.highlightRange )
258 					this.removeHighlight();
259 
260 				// Apply the highlight.
261 				var range = this.toDomRange(),
262 					bookmark = range.createBookmark();
263 				highlightStyle.applyToRange( range );
264 				range.moveToBookmark( bookmark );
265 				this._.highlightRange = range;
266 
267 				// Scroll the editor to the highlighted area.
268 				var element = range.startContainer;
269 				if ( element.type != CKEDITOR.NODE_ELEMENT )
270 					element = element.getParent();
271 				element.scrollIntoView();
272 
273 				// Update the character cursors.
274 				this.updateFromDomRange( range );
275 			},
276 
277 			/**
278 			 * Remove highlighted find result.
279 			 */
280 			removeHighlight : function()
281 			{
282 				if ( !this._.highlightRange )
283 					return;
284 
285 				var bookmark = this._.highlightRange.createBookmark();
286 				highlightStyle.removeFromRange( this._.highlightRange );
287 				this._.highlightRange.moveToBookmark( bookmark );
288 				this.updateFromDomRange( this._.highlightRange );
289 				this._.highlightRange = null;
290 			},
291 
292 			isReadOnly : function()
293 			{
294 				if ( !this._.highlightRange )
295 					return 0;
296 
297 				return this._.highlightRange.startContainer.isReadOnly();
298 			},
299 
300 			moveBack : function()
301 			{
302 				var retval = this._.walker.back(),
303 					cursors = this._.cursors;
304 
305 				if ( retval.hitMatchBoundary )
306 					this._.cursors = cursors = [];
307 
308 				cursors.unshift( retval );
309 				if ( cursors.length > this._.rangeLength )
310 					cursors.pop();
311 
312 				return retval;
313 			},
314 
315 			moveNext : function()
316 			{
317 				var retval = this._.walker.next(),
318 					cursors = this._.cursors;
319 
320 				// Clear the cursors queue if we've crossed a match boundary.
321 				if ( retval.hitMatchBoundary )
322 					this._.cursors = cursors = [];
323 
324 				cursors.push( retval );
325 				if ( cursors.length > this._.rangeLength )
326 					cursors.shift();
327 
328 				return retval;
329 			},
330 
331 			getEndCharacter : function()
332 			{
333 				var cursors = this._.cursors;
334 				if ( cursors.length < 1 )
335 					return null;
336 
337 				return cursors[ cursors.length - 1 ].character;
338 			},
339 
340 			getNextCharacterRange : function( maxLength )
341 			{
342 				var lastCursor,
343 						nextRangeWalker,
344 						cursors = this._.cursors;
345 
346 				if ( ( lastCursor = cursors[ cursors.length - 1 ] ) && lastCursor.textNode )
347 					nextRangeWalker = new characterWalker( getRangeAfterCursor( lastCursor ) );
348 				// In case it's an empty range (no cursors), figure out next range from walker (#4951).
349 				else
350 					nextRangeWalker = this._.walker;
351 
352 				return new characterRange( nextRangeWalker, maxLength );
353 			},
354 
355 			getCursors : function()
356 			{
357 				return this._.cursors;
358 			}
359 		};
360 
361 
362 		// The remaining document range after the character cursor.
363 		function getRangeAfterCursor( cursor , inclusive )
364 		{
365 			var range = new CKEDITOR.dom.range();
366 			range.setStart( cursor.textNode,
367 						   ( inclusive ? cursor.offset : cursor.offset + 1 ) );
368 			range.setEndAt( editor.document.getBody(),
369 							CKEDITOR.POSITION_BEFORE_END );
370 			return range;
371 		}
372 
373 		// The document range before the character cursor.
374 		function getRangeBeforeCursor( cursor )
375 		{
376 			var range = new CKEDITOR.dom.range();
377 			range.setStartAt( editor.document.getBody(),
378 							CKEDITOR.POSITION_AFTER_START );
379 			range.setEnd( cursor.textNode, cursor.offset );
380 			return range;
381 		}
382 
383 		var KMP_NOMATCH = 0,
384 			KMP_ADVANCED = 1,
385 			KMP_MATCHED = 2;
386 		/**
387 		 * Examination the occurrence of a word which implement KMP algorithm.
388 		 */
389 		var kmpMatcher = function( pattern, ignoreCase )
390 		{
391 			var overlap = [ -1 ];
392 			if ( ignoreCase )
393 				pattern = pattern.toLowerCase();
394 			for ( var i = 0 ; i < pattern.length ; i++ )
395 			{
396 				overlap.push( overlap[i] + 1 );
397 				while ( overlap[ i + 1 ] > 0
398 					&& pattern.charAt( i ) != pattern
399 						.charAt( overlap[ i + 1 ] - 1 ) )
400 					overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1;
401 			}
402 
403 			this._ = {
404 				overlap : overlap,
405 				state : 0,
406 				ignoreCase : !!ignoreCase,
407 				pattern : pattern
408 			};
409 		};
410 
411 		kmpMatcher.prototype =
412 		{
413 			feedCharacter : function( c )
414 			{
415 				if ( this._.ignoreCase )
416 					c = c.toLowerCase();
417 
418 				while ( true )
419 				{
420 					if ( c == this._.pattern.charAt( this._.state ) )
421 					{
422 						this._.state++;
423 						if ( this._.state == this._.pattern.length )
424 						{
425 							this._.state = 0;
426 							return KMP_MATCHED;
427 						}
428 						return KMP_ADVANCED;
429 					}
430 					else if ( !this._.state )
431 						return KMP_NOMATCH;
432 					else
433 						this._.state = this._.overlap[ this._.state ];
434 				}
435 
436 				return null;
437 			},
438 
439 			reset : function()
440 			{
441 				this._.state = 0;
442 			}
443 		};
444 
445 		var wordSeparatorRegex =
446 		/[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;
447 
448 		var isWordSeparator = function( c )
449 		{
450 			if ( !c )
451 				return true;
452 			var code = c.charCodeAt( 0 );
453 			return ( code >= 9 && code <= 0xd )
454 				|| ( code >= 0x2000 && code <= 0x200a )
455 				|| wordSeparatorRegex.test( c );
456 		};
457 
458 		var finder = {
459 			searchRange : null,
460 			matchRange : null,
461 			find : function( pattern, matchCase, matchWord, matchCyclic, highlightMatched, cyclicRerun )
462 			{
463 				if ( !this.matchRange )
464 					this.matchRange =
465 						new characterRange(
466 							new characterWalker( this.searchRange ),
467 							pattern.length );
468 				else
469 				{
470 					this.matchRange.removeHighlight();
471 					this.matchRange = this.matchRange.getNextCharacterRange( pattern.length );
472 				}
473 
474 				var matcher = new kmpMatcher( pattern, !matchCase ),
475 					matchState = KMP_NOMATCH,
476 					character = '%';
477 
478 				while ( character !== null )
479 				{
480 					this.matchRange.moveNext();
481 					while ( ( character = this.matchRange.getEndCharacter() ) )
482 					{
483 						matchState = matcher.feedCharacter( character );
484 						if ( matchState == KMP_MATCHED )
485 							break;
486 						if ( this.matchRange.moveNext().hitMatchBoundary )
487 							matcher.reset();
488 					}
489 
490 					if ( matchState == KMP_MATCHED )
491 					{
492 						if ( matchWord )
493 						{
494 							var cursors = this.matchRange.getCursors(),
495 								tail = cursors[ cursors.length - 1 ],
496 								head = cursors[ 0 ];
497 
498 							var rangeBefore = getRangeBeforeCursor( head ),
499 								rangeAfter = getRangeAfterCursor( tail );
500 
501 							// The word boundary checks requires to trim the text nodes. (#9036)
502 							rangeBefore.trim();
503 							rangeAfter.trim();
504 
505 							var headWalker = new characterWalker( rangeBefore, true ),
506 								tailWalker = new characterWalker( rangeAfter, true );
507 
508 							if ( ! ( isWordSeparator( headWalker.back().character )
509 										&& isWordSeparator( tailWalker.next().character ) ) )
510 								continue;
511 						}
512 						this.matchRange.setMatched();
513 						if ( highlightMatched !== false )
514 							this.matchRange.highlight();
515 						return true;
516 					}
517 				}
518 
519 				this.matchRange.clearMatched();
520 				this.matchRange.removeHighlight();
521 				// Clear current session and restart with the default search
522 				// range.
523 				// Re-run the finding once for cyclic.(#3517)
524 				if ( matchCyclic && !cyclicRerun )
525 				{
526 					this.searchRange = getSearchRange( 1 );
527 					this.matchRange = null;
528 					return arguments.callee.apply( this,
529 						Array.prototype.slice.call( arguments ).concat( [ true ] ) );
530 				}
531 
532 				return false;
533 			},
534 
535 			/**
536 			 * Record how much replacement occurred toward one replacing.
537 			 */
538 			replaceCounter : 0,
539 
540 			replace : function( dialog, pattern, newString, matchCase, matchWord,
541 				matchCyclic , isReplaceAll )
542 			{
543 				isReplace = 1;
544 
545 				// Successiveness of current replace/find.
546 				var result = 0;
547 
548 				// 1. Perform the replace when there's already a match here.
549 				// 2. Otherwise perform the find but don't replace it immediately.
550 				if ( this.matchRange && this.matchRange.isMatched()
551 						&& !this.matchRange._.isReplaced && !this.matchRange.isReadOnly() )
552 				{
553 					// Turn off highlight for a while when saving snapshots.
554 					this.matchRange.removeHighlight();
555 					var domRange = this.matchRange.toDomRange();
556 					var text = editor.document.createText( newString );
557 					if ( !isReplaceAll )
558 					{
559 						// Save undo snaps before and after the replacement.
560 						var selection = editor.getSelection();
561 						selection.selectRanges( [ domRange ] );
562 						editor.fire( 'saveSnapshot' );
563 					}
564 					domRange.deleteContents();
565 					domRange.insertNode( text );
566 					if ( !isReplaceAll )
567 					{
568 						selection.selectRanges( [ domRange ] );
569 						editor.fire( 'saveSnapshot' );
570 					}
571 					this.matchRange.updateFromDomRange( domRange );
572 					if ( !isReplaceAll )
573 						this.matchRange.highlight();
574 					this.matchRange._.isReplaced = true;
575 					this.replaceCounter++;
576 					result = 1;
577 				}
578 				else
579 					result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );
580 
581 				isReplace = 0;
582 
583 				return result;
584 			}
585 		};
586 
587 		/**
588 		 * The range in which find/replace happened, receive from user
589 		 * selection prior.
590 		 */
591 		function getSearchRange( isDefault )
592 		{
593 			var searchRange,
594 				sel = editor.getSelection(),
595 				body = editor.document.getBody();
596 			if ( sel && !isDefault )
597 			{
598 				searchRange = sel.getRanges()[ 0 ].clone();
599 				searchRange.collapse( true );
600 			}
601 			else
602 			{
603 				searchRange = new CKEDITOR.dom.range();
604 				searchRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
605 			}
606 			searchRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
607 			return searchRange;
608 		}
609 
610 		var lang = editor.lang.findAndReplace;
611 		return {
612 			title : lang.title,
613 			resizable : CKEDITOR.DIALOG_RESIZE_NONE,
614 			minWidth : 350,
615 			minHeight : 170,
616 			buttons : [ CKEDITOR.dialog.cancelButton ],		// Cancel button only.
617 			contents : [
618 				{
619 					id : 'find',
620 					label : lang.find,
621 					title : lang.find,
622 					accessKey : '',
623 					elements : [
624 						{
625 							type : 'hbox',
626 							widths : [ '230px', '90px' ],
627 							children :
628 							[
629 								{
630 									type : 'text',
631 									id : 'txtFindFind',
632 									label : lang.findWhat,
633 									isChanged : false,
634 									labelLayout : 'horizontal',
635 									accessKey : 'F'
636 								},
637 								{
638 									type : 'button',
639 									id : 'btnFind',
640 									align : 'left',
641 									style : 'width:100%',
642 									label : lang.find,
643 									onClick : function()
644 									{
645 										var dialog = this.getDialog();
646 										if ( !finder.find( dialog.getValueOf( 'find', 'txtFindFind' ),
647 													dialog.getValueOf( 'find', 'txtFindCaseChk' ),
648 													dialog.getValueOf( 'find', 'txtFindWordChk' ),
649 													dialog.getValueOf( 'find', 'txtFindCyclic' ) ) )
650 											alert( lang
651 												.notFoundMsg );
652 									}
653 								}
654 							]
655 						},
656 						{
657 							type : 'fieldset',
658 							label : CKEDITOR.tools.htmlEncode( lang.findOptions ),
659 							style : 'margin-top:29px',
660 							children :
661 							[
662 								{
663 									type : 'vbox',
664 									padding : 0,
665 									children :
666 									[
667 										{
668 											type : 'checkbox',
669 											id : 'txtFindCaseChk',
670 											isChanged : false,
671 											label : lang.matchCase
672 										},
673 										{
674 											type : 'checkbox',
675 											id : 'txtFindWordChk',
676 											isChanged : false,
677 											label : lang.matchWord
678 										},
679 										{
680 											type : 'checkbox',
681 											id : 'txtFindCyclic',
682 											isChanged : false,
683 											'default' : true,
684 											label : lang.matchCyclic
685 										}
686 									]
687 								}
688 							]
689 						}
690 					]
691 				},
692 				{
693 					id : 'replace',
694 					label : lang.replace,
695 					accessKey : 'M',
696 					elements : [
697 						{
698 							type : 'hbox',
699 							widths : [ '230px', '90px' ],
700 							children :
701 							[
702 								{
703 									type : 'text',
704 									id : 'txtFindReplace',
705 									label : lang.findWhat,
706 									isChanged : false,
707 									labelLayout : 'horizontal',
708 									accessKey : 'F'
709 								},
710 								{
711 									type : 'button',
712 									id : 'btnFindReplace',
713 									align : 'left',
714 									style : 'width:100%',
715 									label : lang.replace,
716 									onClick : function()
717 									{
718 										var dialog = this.getDialog();
719 										if ( !finder.replace( dialog,
720 													dialog.getValueOf( 'replace', 'txtFindReplace' ),
721 													dialog.getValueOf( 'replace', 'txtReplace' ),
722 													dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
723 													dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
724 													dialog.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
725 											alert( lang
726 												.notFoundMsg );
727 									}
728 								}
729 							]
730 						},
731 						{
732 							type : 'hbox',
733 							widths : [ '230px', '90px' ],
734 							children :
735 							[
736 								{
737 									type : 'text',
738 									id : 'txtReplace',
739 									label : lang.replaceWith,
740 									isChanged : false,
741 									labelLayout : 'horizontal',
742 									accessKey : 'R'
743 								},
744 								{
745 									type : 'button',
746 									id : 'btnReplaceAll',
747 									align : 'left',
748 									style : 'width:100%',
749 									label : lang.replaceAll,
750 									isChanged : false,
751 									onClick : function()
752 									{
753 										var dialog = this.getDialog();
754 										var replaceNums;
755 
756 										finder.replaceCounter = 0;
757 
758 										// Scope to full document.
759 										finder.searchRange = getSearchRange( 1 );
760 										if ( finder.matchRange )
761 										{
762 											finder.matchRange.removeHighlight();
763 											finder.matchRange = null;
764 										}
765 										editor.fire( 'saveSnapshot' );
766 										while ( finder.replace( dialog,
767 											dialog.getValueOf( 'replace', 'txtFindReplace' ),
768 											dialog.getValueOf( 'replace', 'txtReplace' ),
769 											dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
770 											dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
771 											false, true ) )
772 										{ /*jsl:pass*/ }
773 
774 										if ( finder.replaceCounter )
775 										{
776 											alert( lang.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) );
777 											editor.fire( 'saveSnapshot' );
778 										}
779 										else
780 											alert( lang.notFoundMsg );
781 									}
782 								}
783 							]
784 						},
785 						{
786 							type : 'fieldset',
787 							label : CKEDITOR.tools.htmlEncode( lang.findOptions ),
788 							children :
789 							[
790 								{
791 									type : 'vbox',
792 									padding : 0,
793 									children :
794 									[
795 										{
796 											type : 'checkbox',
797 											id : 'txtReplaceCaseChk',
798 											isChanged : false,
799 											label : lang.matchCase
800 										},
801 										{
802 											type : 'checkbox',
803 											id : 'txtReplaceWordChk',
804 											isChanged : false,
805 											label : lang.matchWord
806 										},
807 										{
808 											type : 'checkbox',
809 											id : 'txtReplaceCyclic',
810 											isChanged : false,
811 											'default' : true,
812 											label : lang.matchCyclic
813 										}
814 									]
815 								}
816 							]
817 						}
818 					]
819 				}
820 			],
821 			onLoad : function()
822 			{
823 				var dialog = this;
824 
825 				// Keep track of the current pattern field in use.
826 				var patternField, wholeWordChkField;
827 
828 				// Ignore initial page select on dialog show
829 				var isUserSelect = 0;
830 				this.on( 'hide', function()
831 						{
832 							isUserSelect = 0;
833 						});
834 				this.on( 'show', function()
835 						{
836 							isUserSelect = 1;
837 						});
838 
839 				this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc )
840 					{
841 						return function( pageId )
842 						{
843 							originalFunc.call( dialog, pageId );
844 
845 							var currPage = dialog._.tabs[ pageId ];
846 							var patternFieldInput, patternFieldId, wholeWordChkFieldId;
847 							patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace';
848 							wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
849 
850 							patternField = dialog.getContentElement( pageId,
851 								patternFieldId );
852 							wholeWordChkField = dialog.getContentElement( pageId,
853 								wholeWordChkFieldId );
854 
855 							// Prepare for check pattern text filed 'keyup' event
856 							if ( !currPage.initialized )
857 							{
858 								patternFieldInput = CKEDITOR.document
859 									.getById( patternField._.inputId );
860 								currPage.initialized = true;
861 							}
862 
863 							// Synchronize fields on tab switch.
864 							if ( isUserSelect )
865 								syncFieldsBetweenTabs.call( this, pageId );
866 						};
867 					} );
868 
869 			},
870 			onShow : function()
871 			{
872 				// Establish initial searching start position.
873 				finder.searchRange = getSearchRange();
874 
875 				// Fill in the find field with selected text.
876 				var selectedText = this.getParentEditor().getSelection().getSelectedText(),
877 					patternFieldId = ( startupPage == 'find' ? 'txtFindFind' : 'txtFindReplace' );
878 
879 				var field = this.getContentElement( startupPage, patternFieldId );
880 				field.setValue( selectedText );
881 				field.select();
882 
883 				this.selectPage( startupPage );
884 
885 				this[ ( startupPage == 'find' && this._.editor.readOnly? 'hide' : 'show' ) + 'Page' ]( 'replace');
886 			},
887 			onHide : function()
888 			{
889 				var range;
890 				if ( finder.matchRange && finder.matchRange.isMatched() )
891 				{
892 					finder.matchRange.removeHighlight();
893 					editor.focus();
894 
895 					range = finder.matchRange.toDomRange();
896 					if ( range )
897 						editor.getSelection().selectRanges( [ range ] );
898 				}
899 
900 				// Clear current session before dialog close
901 				delete finder.matchRange;
902 			},
903 			onFocus : function()
904 			{
905 				if ( startupPage == 'replace' )
906 					return this.getContentElement( 'replace', 'txtFindReplace' );
907 				else
908 					return this.getContentElement( 'find', 'txtFindFind' );
909 			}
910 		};
911 	};
912 
913 	CKEDITOR.dialog.add( 'find', function( editor )
914 		{
915 			return findDialog( editor, 'find' );
916 		});
917 
918 	CKEDITOR.dialog.add( 'replace', function( editor )
919 		{
920 			return findDialog( editor, 'replace' );
921 		});
922 })();
923