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