2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
3 For licensing, see LICENSE.html or http://ckeditor.com/license
10 function findEvaluator( node )
12 return node.type == CKEDITOR.NODE_TEXT && node.getLength() > 0 && ( !isReplace || !node.isReadOnly() );
16 * Elements which break characters been considered as sequence.
18 function nonCharactersBoundary( node )
20 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary(
21 CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$empty, CKEDITOR.dtd.$nonEditable ) ) );
25 * Get the cursor object which represent both current character and it's dom
28 var cursorStep = function()
31 textNode : this.textNode,
33 character : this.textNode ?
34 this.textNode.getText().charAt( this.offset ) : null,
35 hitMatchBoundary : this._.matchBoundary
39 var pages = [ 'find', 'replace' ],
41 [ 'txtFindFind', 'txtFindReplace' ],
42 [ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
43 [ 'txtFindWordChk', 'txtReplaceWordChk' ],
44 [ 'txtFindCyclic', 'txtReplaceCyclic' ] ];
47 * Synchronize corresponding filed values between 'replace' and 'find' pages.
48 * @param {String} currentPageId The page id which receive values.
50 function syncFieldsBetweenTabs( currentPageId )
52 var sourceIndex, targetIndex,
53 sourceField, targetField;
55 sourceIndex = currentPageId === 'find' ? 1 : 0;
56 targetIndex = 1 - sourceIndex;
57 var i, l = fieldsMapping.length;
58 for ( i = 0 ; i < l ; i++ )
60 sourceField = this.getContentElement( pages[ sourceIndex ],
61 fieldsMapping[ i ][ sourceIndex ] );
62 targetField = this.getContentElement( pages[ targetIndex ],
63 fieldsMapping[ i ][ targetIndex ] );
65 targetField.setValue( sourceField.getValue() );
69 var findDialog = function( editor, startupPage )
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( CKEDITOR.tools.extend( { fullMatch : true, childRule : function(){ return 0; } },
75 editor.config.find_highlight ) );
78 * Iterator which walk through the specified range char by char. By
79 * default the walking will not stop at the character boundaries, until
80 * the end of the range is encountered.
81 * @param { CKEDITOR.dom.range } range
82 * @param {Boolean} matchWord Whether the walking will stop at character boundary.
84 var characterWalker = function( range , matchWord )
88 new CKEDITOR.dom.walker( range );
89 walker.guard = matchWord ? nonCharactersBoundary : function( node )
91 !nonCharactersBoundary( node ) && ( self._.matchBoundary = true );
93 walker[ 'evaluator' ] = findEvaluator;
94 walker.breakOnFalse = 1;
96 if ( range.startContainer.type == CKEDITOR.NODE_TEXT )
98 this.textNode = range.startContainer;
99 this.offset = range.startOffset - 1;
103 matchWord : matchWord,
105 matchBoundary : false
109 characterWalker.prototype = {
117 return this.move( true );
120 move : function( rtl )
122 var currentTextNode = this.textNode;
123 // Already at the end of document, no more character available.
124 if ( currentTextNode === null )
125 return cursorStep.call( this );
127 this._.matchBoundary = false;
129 // There are more characters in the text node, step forward.
135 return cursorStep.call( this );
137 else if ( currentTextNode
138 && this.offset < currentTextNode.getLength() - 1 )
141 return cursorStep.call( this );
145 currentTextNode = null;
146 // At the end of the text node, walking foward for the next.
147 while ( !currentTextNode )
150 this._.walker[ rtl ? 'previous' : 'next' ].call( this._.walker );
152 // Stop searching if we're need full word match OR
153 // already reach document end.
154 if ( this._.matchWord && !currentTextNode
155 || this._.walker._.end )
158 // Found a fresh text node.
159 this.textNode = currentTextNode;
160 if ( currentTextNode )
161 this.offset = rtl ? currentTextNode.getLength() - 1 : 0;
166 return cursorStep.call( this );
172 * A range of cursors which represent a trunk of characters which try to
173 * match, it has the same length as the pattern string.
175 var characterRange = function( characterWalker, rangeLength )
178 walker : characterWalker,
180 rangeLength : rangeLength,
181 highlightRange : null,
186 characterRange.prototype = {
188 * Translate this range to {@link CKEDITOR.dom.range}
190 toDomRange : function()
192 var range = new CKEDITOR.dom.range( editor.document );
193 var cursors = this._.cursors;
194 if ( cursors.length < 1 )
196 var textNode = this._.walker.textNode;
198 range.setStartAfter( textNode );
204 var first = cursors[0],
205 last = cursors[ cursors.length - 1 ];
207 range.setStart( first.textNode, first.offset );
208 range.setEnd( last.textNode, last.offset + 1 );
214 * Reflect the latest changes from dom range.
216 updateFromDomRange : function( domRange )
219 walker = new characterWalker( domRange );
223 cursor = walker.next();
224 if ( cursor.character )
225 this._.cursors.push( cursor );
227 while ( cursor.character );
228 this._.rangeLength = this._.cursors.length;
231 setMatched : function()
233 this._.isMatched = true;
236 clearMatched : function()
238 this._.isMatched = false;
241 isMatched : function()
243 return this._.isMatched;
247 * Hightlight the current matched chunk of text.
249 highlight : function()
251 // Do not apply if nothing is found.
252 if ( this._.cursors.length < 1 )
255 // Remove the previous highlight if there's one.
256 if ( this._.highlightRange )
257 this.removeHighlight();
259 // Apply the highlight.
260 var range = this.toDomRange(),
261 bookmark = range.createBookmark();
262 highlightStyle.applyToRange( range );
263 range.moveToBookmark( bookmark );
264 this._.highlightRange = range;
266 // Scroll the editor to the highlighted area.
267 var element = range.startContainer;
268 if ( element.type != CKEDITOR.NODE_ELEMENT )
269 element = element.getParent();
270 element.scrollIntoView();
272 // Update the character cursors.
273 this.updateFromDomRange( range );
277 * Remove highlighted find result.
279 removeHighlight : function()
281 if ( !this._.highlightRange )
284 var bookmark = this._.highlightRange.createBookmark();
285 highlightStyle.removeFromRange( this._.highlightRange );
286 this._.highlightRange.moveToBookmark( bookmark );
287 this.updateFromDomRange( this._.highlightRange );
288 this._.highlightRange = null;
291 isReadOnly : function()
293 if ( !this._.highlightRange )
296 return this._.highlightRange.startContainer.isReadOnly();
299 moveBack : function()
301 var retval = this._.walker.back(),
302 cursors = this._.cursors;
304 if ( retval.hitMatchBoundary )
305 this._.cursors = cursors = [];
307 cursors.unshift( retval );
308 if ( cursors.length > this._.rangeLength )
314 moveNext : function()
316 var retval = this._.walker.next(),
317 cursors = this._.cursors;
319 // Clear the cursors queue if we've crossed a match boundary.
320 if ( retval.hitMatchBoundary )
321 this._.cursors = cursors = [];
323 cursors.push( retval );
324 if ( cursors.length > this._.rangeLength )
330 getEndCharacter : function()
332 var cursors = this._.cursors;
333 if ( cursors.length < 1 )
336 return cursors[ cursors.length - 1 ].character;
339 getNextCharacterRange : function( maxLength )
343 cursors = this._.cursors;
345 if ( ( lastCursor = cursors[ cursors.length - 1 ] ) && lastCursor.textNode )
346 nextRangeWalker = new characterWalker( getRangeAfterCursor( lastCursor ) );
347 // In case it's an empty range (no cursors), figure out next range from walker (#4951).
349 nextRangeWalker = this._.walker;
351 return new characterRange( nextRangeWalker, maxLength );
354 getCursors : function()
356 return this._.cursors;
361 // The remaining document range after the character cursor.
362 function getRangeAfterCursor( cursor , inclusive )
364 var range = new CKEDITOR.dom.range();
365 range.setStart( cursor.textNode,
366 ( inclusive ? cursor.offset : cursor.offset + 1 ) );
367 range.setEndAt( editor.document.getBody(),
368 CKEDITOR.POSITION_BEFORE_END );
372 // The document range before the character cursor.
373 function getRangeBeforeCursor( cursor )
375 var range = new CKEDITOR.dom.range();
376 range.setStartAt( editor.document.getBody(),
377 CKEDITOR.POSITION_AFTER_START );
378 range.setEnd( cursor.textNode, cursor.offset );
386 * Examination the occurrence of a word which implement KMP algorithm.
388 var kmpMatcher = function( pattern, ignoreCase )
390 var overlap = [ -1 ];
392 pattern = pattern.toLowerCase();
393 for ( var i = 0 ; i < pattern.length ; i++ )
395 overlap.push( overlap[i] + 1 );
396 while ( overlap[ i + 1 ] > 0
397 && pattern.charAt( i ) != pattern
398 .charAt( overlap[ i + 1 ] - 1 ) )
399 overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1;
405 ignoreCase : !!ignoreCase,
410 kmpMatcher.prototype =
412 feedCharacter : function( c )
414 if ( this._.ignoreCase )
419 if ( c == this._.pattern.charAt( this._.state ) )
422 if ( this._.state == this._.pattern.length )
429 else if ( !this._.state )
432 this._.state = this._.overlap[ this._.state ];
444 var wordSeparatorRegex =
445 /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;
447 var isWordSeparator = function( c )
451 var code = c.charCodeAt( 0 );
452 return ( code >= 9 && code <= 0xd )
453 || ( code >= 0x2000 && code <= 0x200a )
454 || wordSeparatorRegex.test( c );
460 find : function( pattern, matchCase, matchWord, matchCyclic, highlightMatched, cyclicRerun )
462 if ( !this.matchRange )
465 new characterWalker( this.searchRange ),
469 this.matchRange.removeHighlight();
470 this.matchRange = this.matchRange.getNextCharacterRange( pattern.length );
473 var matcher = new kmpMatcher( pattern, !matchCase ),
474 matchState = KMP_NOMATCH,
477 while ( character !== null )
479 this.matchRange.moveNext();
480 while ( ( character = this.matchRange.getEndCharacter() ) )
482 matchState = matcher.feedCharacter( character );
483 if ( matchState == KMP_MATCHED )
485 if ( this.matchRange.moveNext().hitMatchBoundary )
489 if ( matchState == KMP_MATCHED )
493 var cursors = this.matchRange.getCursors(),
494 tail = cursors[ cursors.length - 1 ],
497 var headWalker = new characterWalker( getRangeBeforeCursor( head ), true ),
498 tailWalker = new characterWalker( getRangeAfterCursor( tail ), true );
500 if ( ! ( isWordSeparator( headWalker.back().character )
501 && isWordSeparator( tailWalker.next().character ) ) )
504 this.matchRange.setMatched();
505 if ( highlightMatched !== false )
506 this.matchRange.highlight();
511 this.matchRange.clearMatched();
512 this.matchRange.removeHighlight();
513 // Clear current session and restart with the default search
515 // Re-run the finding once for cyclic.(#3517)
516 if ( matchCyclic && !cyclicRerun )
518 this.searchRange = getSearchRange( 1 );
519 this.matchRange = null;
520 return arguments.callee.apply( this,
521 Array.prototype.slice.call( arguments ).concat( [ true ] ) );
528 * Record how much replacement occurred toward one replacing.
532 replace : function( dialog, pattern, newString, matchCase, matchWord,
533 matchCyclic , isReplaceAll )
537 // Successiveness of current replace/find.
540 // 1. Perform the replace when there's already a match here.
541 // 2. Otherwise perform the find but don't replace it immediately.
542 if ( this.matchRange && this.matchRange.isMatched()
543 && !this.matchRange._.isReplaced && !this.matchRange.isReadOnly() )
545 // Turn off highlight for a while when saving snapshots.
546 this.matchRange.removeHighlight();
547 var domRange = this.matchRange.toDomRange();
548 var text = editor.document.createText( newString );
551 // Save undo snaps before and after the replacement.
552 var selection = editor.getSelection();
553 selection.selectRanges( [ domRange ] );
554 editor.fire( 'saveSnapshot' );
556 domRange.deleteContents();
557 domRange.insertNode( text );
560 selection.selectRanges( [ domRange ] );
561 editor.fire( 'saveSnapshot' );
563 this.matchRange.updateFromDomRange( domRange );
565 this.matchRange.highlight();
566 this.matchRange._.isReplaced = true;
567 this.replaceCounter++;
571 result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );
580 * The range in which find/replace happened, receive from user
583 function getSearchRange( isDefault )
586 sel = editor.getSelection(),
587 body = editor.document.getBody();
588 if ( sel && !isDefault )
590 searchRange = sel.getRanges()[ 0 ].clone();
591 searchRange.collapse( true );
595 searchRange = new CKEDITOR.dom.range();
596 searchRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
598 searchRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
602 var lang = editor.lang.findAndReplace;
605 resizable : CKEDITOR.DIALOG_RESIZE_NONE,
608 buttons : [ CKEDITOR.dialog.cancelButton ], // Cancel button only.
618 widths : [ '230px', '90px' ],
624 label : lang.findWhat,
626 labelLayout : 'horizontal',
632 style : 'width:100%',
636 var dialog = this.getDialog();
637 if ( !finder.find( dialog.getValueOf( 'find', 'txtFindFind' ),
638 dialog.getValueOf( 'find', 'txtFindCaseChk' ),
639 dialog.getValueOf( 'find', 'txtFindWordChk' ),
640 dialog.getValueOf( 'find', 'txtFindCyclic' ) ) )
654 id : 'txtFindCaseChk',
656 style : 'margin-top:28px',
657 label : lang.matchCase
661 id : 'txtFindWordChk',
663 label : lang.matchWord
667 id : 'txtFindCyclic',
670 label : lang.matchCyclic
678 label : lang.replace,
683 widths : [ '230px', '90px' ],
688 id : 'txtFindReplace',
689 label : lang.findWhat,
691 labelLayout : 'horizontal',
697 style : 'width:100%',
698 label : lang.replace,
701 var dialog = this.getDialog();
702 if ( !finder.replace( dialog,
703 dialog.getValueOf( 'replace', 'txtFindReplace' ),
704 dialog.getValueOf( 'replace', 'txtReplace' ),
705 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
706 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
707 dialog.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
716 widths : [ '230px', '90px' ],
722 label : lang.replaceWith,
724 labelLayout : 'horizontal',
730 style : 'width:100%',
731 label : lang.replaceAll,
735 var dialog = this.getDialog();
738 finder.replaceCounter = 0;
740 // Scope to full document.
741 finder.searchRange = getSearchRange( 1 );
742 if ( finder.matchRange )
744 finder.matchRange.removeHighlight();
745 finder.matchRange = null;
747 editor.fire( 'saveSnapshot' );
748 while ( finder.replace( dialog,
749 dialog.getValueOf( 'replace', 'txtFindReplace' ),
750 dialog.getValueOf( 'replace', 'txtReplace' ),
751 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
752 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
756 if ( finder.replaceCounter )
758 alert( lang.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) );
759 editor.fire( 'saveSnapshot' );
762 alert( lang.notFoundMsg );
774 id : 'txtReplaceCaseChk',
781 id : 'txtReplaceWordChk',
788 id : 'txtReplaceCyclic',
803 // Keep track of the current pattern field in use.
804 var patternField, wholeWordChkField;
806 // Ignore initial page select on dialog show
807 var isUserSelect = 0;
808 this.on( 'hide', function()
812 this.on( 'show', function()
817 this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc )
819 return function( pageId )
821 originalFunc.call( dialog, pageId );
823 var currPage = dialog._.tabs[ pageId ];
824 var patternFieldInput, patternFieldId, wholeWordChkFieldId;
825 patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace';
826 wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
828 patternField = dialog.getContentElement( pageId,
830 wholeWordChkField = dialog.getContentElement( pageId,
831 wholeWordChkFieldId );
833 // Prepare for check pattern text filed 'keyup' event
834 if ( !currPage.initialized )
836 patternFieldInput = CKEDITOR.document
837 .getById( patternField._.inputId );
838 currPage.initialized = true;
841 // Synchronize fields on tab switch.
843 syncFieldsBetweenTabs.call( this, pageId );
850 // Establish initial searching start position.
851 finder.searchRange = getSearchRange();
853 this.selectPage( startupPage );
858 if ( finder.matchRange && finder.matchRange.isMatched() )
860 finder.matchRange.removeHighlight();
863 range = finder.matchRange.toDomRange();
865 editor.getSelection().selectRanges( [ range ] );
868 // Clear current session before dialog close
869 delete finder.matchRange;
873 if ( startupPage == 'replace' )
874 return this.getContentElement( 'replace', 'txtFindReplace' );
876 return this.getContentElement( 'find', 'txtFindFind' );
881 CKEDITOR.dialog.add( 'find', function( editor )
883 return findDialog( editor, 'find' );
886 CKEDITOR.dialog.add( 'replace', function( editor )
888 return findDialog( editor, 'replace' );