2 Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.
\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license
\r
10 function findEvaluator( node )
\r
12 return node.type == CKEDITOR.NODE_TEXT && node.getLength() > 0 && ( !isReplace || !node.isReadOnly() );
\r
16 * Elements which break characters been considered as sequence.
\r
18 function nonCharactersBoundary( node )
\r
20 return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary(
\r
21 CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$empty, CKEDITOR.dtd.$nonEditable ) ) );
\r
25 * Get the cursor object which represent both current character and it's dom
\r
28 var cursorStep = function()
\r
31 textNode : this.textNode,
\r
32 offset : this.offset,
\r
33 character : this.textNode ?
\r
34 this.textNode.getText().charAt( this.offset ) : null,
\r
35 hitMatchBoundary : this._.matchBoundary
\r
39 var pages = [ 'find', 'replace' ],
\r
41 [ 'txtFindFind', 'txtFindReplace' ],
\r
42 [ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
\r
43 [ 'txtFindWordChk', 'txtReplaceWordChk' ],
\r
44 [ 'txtFindCyclic', 'txtReplaceCyclic' ] ];
\r
47 * Synchronize corresponding filed values between 'replace' and 'find' pages.
\r
48 * @param {String} currentPageId The page id which receive values.
\r
50 function syncFieldsBetweenTabs( currentPageId )
\r
52 var sourceIndex, targetIndex,
\r
53 sourceField, targetField;
\r
55 sourceIndex = currentPageId === 'find' ? 1 : 0;
\r
56 targetIndex = 1 - sourceIndex;
\r
57 var i, l = fieldsMapping.length;
\r
58 for ( i = 0 ; i < l ; i++ )
\r
60 sourceField = this.getContentElement( pages[ sourceIndex ],
\r
61 fieldsMapping[ i ][ sourceIndex ] );
\r
62 targetField = this.getContentElement( pages[ targetIndex ],
\r
63 fieldsMapping[ i ][ targetIndex ] );
\r
65 targetField.setValue( sourceField.getValue() );
\r
69 var findDialog = function( editor, startupPage )
\r
71 // Style object for highlights: (#5018)
\r
72 // 1. Defined as full match style to avoid compromising ordinary text color styles.
\r
73 // 2. Must be apply onto inner-most text to avoid conflicting with ordinary text color styles visually.
\r
74 var highlightStyle = new CKEDITOR.style( CKEDITOR.tools.extend( { fullMatch : true, childRule : function(){ return false; } },
\r
75 editor.config.find_highlight ) );
\r
78 * Iterator which walk through the specified range char by char. By
\r
79 * default the walking will not stop at the character boundaries, until
\r
80 * the end of the range is encountered.
\r
81 * @param { CKEDITOR.dom.range } range
\r
82 * @param {Boolean} matchWord Whether the walking will stop at character boundary.
\r
84 var characterWalker = function( range , matchWord )
\r
87 new CKEDITOR.dom.walker( range );
\r
88 walker.guard = matchWord ? nonCharactersBoundary : null;
\r
89 walker[ 'evaluator' ] = findEvaluator;
\r
90 walker.breakOnFalse = true;
\r
93 matchWord : matchWord,
\r
95 matchBoundary : false
\r
99 characterWalker.prototype = {
\r
102 return this.move();
\r
107 return this.move( true );
\r
110 move : function( rtl )
\r
112 var currentTextNode = this.textNode;
\r
113 // Already at the end of document, no more character available.
\r
114 if ( currentTextNode === null )
\r
115 return cursorStep.call( this );
\r
117 this._.matchBoundary = false;
\r
119 // There are more characters in the text node, step forward.
\r
120 if ( currentTextNode
\r
122 && this.offset > 0 )
\r
125 return cursorStep.call( this );
\r
127 else if ( currentTextNode
\r
128 && this.offset < currentTextNode.getLength() - 1 )
\r
131 return cursorStep.call( this );
\r
135 currentTextNode = null;
\r
136 // At the end of the text node, walking foward for the next.
\r
137 while ( !currentTextNode )
\r
140 this._.walker[ rtl ? 'previous' : 'next' ].call( this._.walker );
\r
142 // Stop searching if we're need full word match OR
\r
143 // already reach document end.
\r
144 if ( this._.matchWord && !currentTextNode
\r
145 ||this._.walker._.end )
\r
148 // Marking as match character boundaries.
\r
149 if ( !currentTextNode
\r
150 && !nonCharactersBoundary( this._.walker.current ) )
\r
151 this._.matchBoundary = true;
\r
154 // Found a fresh text node.
\r
155 this.textNode = currentTextNode;
\r
156 if ( currentTextNode )
\r
157 this.offset = rtl ? currentTextNode.getLength() - 1 : 0;
\r
162 return cursorStep.call( this );
\r
168 * A range of cursors which represent a trunk of characters which try to
\r
169 * match, it has the same length as the pattern string.
\r
171 var characterRange = function( characterWalker, rangeLength )
\r
174 walker : characterWalker,
\r
176 rangeLength : rangeLength,
\r
177 highlightRange : null,
\r
182 characterRange.prototype = {
\r
184 * Translate this range to {@link CKEDITOR.dom.range}
\r
186 toDomRange : function()
\r
188 var range = new CKEDITOR.dom.range( editor.document );
\r
189 var cursors = this._.cursors;
\r
190 if ( cursors.length < 1 )
\r
192 var textNode = this._.walker.textNode;
\r
194 range.setStartAfter( textNode );
\r
200 var first = cursors[0],
\r
201 last = cursors[ cursors.length - 1 ];
\r
203 range.setStart( first.textNode, first.offset );
\r
204 range.setEnd( last.textNode, last.offset + 1 );
\r
210 * Reflect the latest changes from dom range.
\r
212 updateFromDomRange : function( domRange )
\r
215 walker = new characterWalker( domRange );
\r
216 this._.cursors = [];
\r
219 cursor = walker.next();
\r
220 if ( cursor.character )
\r
221 this._.cursors.push( cursor );
\r
223 while ( cursor.character );
\r
224 this._.rangeLength = this._.cursors.length;
\r
227 setMatched : function()
\r
229 this._.isMatched = true;
\r
232 clearMatched : function()
\r
234 this._.isMatched = false;
\r
237 isMatched : function()
\r
239 return this._.isMatched;
\r
243 * Hightlight the current matched chunk of text.
\r
245 highlight : function()
\r
247 // Do not apply if nothing is found.
\r
248 if ( this._.cursors.length < 1 )
\r
251 // Remove the previous highlight if there's one.
\r
252 if ( this._.highlightRange )
\r
253 this.removeHighlight();
\r
255 // Apply the highlight.
\r
256 var range = this.toDomRange(),
\r
257 bookmark = range.createBookmark();
\r
258 highlightStyle.applyToRange( range );
\r
259 range.moveToBookmark( bookmark );
\r
260 this._.highlightRange = range;
\r
262 // Scroll the editor to the highlighted area.
\r
263 var element = range.startContainer;
\r
264 if ( element.type != CKEDITOR.NODE_ELEMENT )
\r
265 element = element.getParent();
\r
266 element.scrollIntoView();
\r
268 // Update the character cursors.
\r
269 this.updateFromDomRange( range );
\r
273 * Remove highlighted find result.
\r
275 removeHighlight : function()
\r
277 if ( !this._.highlightRange )
\r
280 var bookmark = this._.highlightRange.createBookmark();
\r
281 highlightStyle.removeFromRange( this._.highlightRange );
\r
282 this._.highlightRange.moveToBookmark( bookmark );
\r
283 this.updateFromDomRange( this._.highlightRange );
\r
284 this._.highlightRange = null;
\r
287 isReadOnly : function()
\r
289 if ( !this._.highlightRange )
\r
292 return this._.highlightRange.startContainer.isReadOnly();
\r
295 moveBack : function()
\r
297 var retval = this._.walker.back(),
\r
298 cursors = this._.cursors;
\r
300 if ( retval.hitMatchBoundary )
\r
301 this._.cursors = cursors = [];
\r
303 cursors.unshift( retval );
\r
304 if ( cursors.length > this._.rangeLength )
\r
310 moveNext : function()
\r
312 var retval = this._.walker.next(),
\r
313 cursors = this._.cursors;
\r
315 // Clear the cursors queue if we've crossed a match boundary.
\r
316 if ( retval.hitMatchBoundary )
\r
317 this._.cursors = cursors = [];
\r
319 cursors.push( retval );
\r
320 if ( cursors.length > this._.rangeLength )
\r
326 getEndCharacter : function()
\r
328 var cursors = this._.cursors;
\r
329 if ( cursors.length < 1 )
\r
332 return cursors[ cursors.length - 1 ].character;
\r
335 getNextCharacterRange : function( maxLength )
\r
339 cursors = this._.cursors;
\r
341 if ( ( lastCursor = cursors[ cursors.length - 1 ] ) && lastCursor.textNode )
\r
342 nextRangeWalker = new characterWalker( getRangeAfterCursor( lastCursor ) );
\r
343 // In case it's an empty range (no cursors), figure out next range from walker (#4951).
\r
345 nextRangeWalker = this._.walker;
\r
347 return new characterRange( nextRangeWalker, maxLength );
\r
350 getCursors : function()
\r
352 return this._.cursors;
\r
357 // The remaining document range after the character cursor.
\r
358 function getRangeAfterCursor( cursor , inclusive )
\r
360 var range = new CKEDITOR.dom.range();
\r
361 range.setStart( cursor.textNode,
\r
362 ( inclusive ? cursor.offset : cursor.offset + 1 ) );
\r
363 range.setEndAt( editor.document.getBody(),
\r
364 CKEDITOR.POSITION_BEFORE_END );
\r
368 // The document range before the character cursor.
\r
369 function getRangeBeforeCursor( cursor )
\r
371 var range = new CKEDITOR.dom.range();
\r
372 range.setStartAt( editor.document.getBody(),
\r
373 CKEDITOR.POSITION_AFTER_START );
\r
374 range.setEnd( cursor.textNode, cursor.offset );
\r
378 var KMP_NOMATCH = 0,
\r
382 * Examination the occurrence of a word which implement KMP algorithm.
\r
384 var kmpMatcher = function( pattern, ignoreCase )
\r
386 var overlap = [ -1 ];
\r
388 pattern = pattern.toLowerCase();
\r
389 for ( var i = 0 ; i < pattern.length ; i++ )
\r
391 overlap.push( overlap[i] + 1 );
\r
392 while ( overlap[ i + 1 ] > 0
\r
393 && pattern.charAt( i ) != pattern
\r
394 .charAt( overlap[ i + 1 ] - 1 ) )
\r
395 overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1;
\r
401 ignoreCase : !!ignoreCase,
\r
406 kmpMatcher.prototype =
\r
408 feedCharacter : function( c )
\r
410 if ( this._.ignoreCase )
\r
411 c = c.toLowerCase();
\r
415 if ( c == this._.pattern.charAt( this._.state ) )
\r
418 if ( this._.state == this._.pattern.length )
\r
421 return KMP_MATCHED;
\r
423 return KMP_ADVANCED;
\r
425 else if ( !this._.state )
\r
426 return KMP_NOMATCH;
\r
428 this._.state = this._.overlap[ this._.state ];
\r
440 var wordSeparatorRegex =
\r
441 /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;
\r
443 var isWordSeparator = function( c )
\r
447 var code = c.charCodeAt( 0 );
\r
448 return ( code >= 9 && code <= 0xd )
\r
449 || ( code >= 0x2000 && code <= 0x200a )
\r
450 || wordSeparatorRegex.test( c );
\r
454 searchRange : null,
\r
456 find : function( pattern, matchCase, matchWord, matchCyclic, highlightMatched, cyclicRerun )
\r
458 if ( !this.matchRange )
\r
460 new characterRange(
\r
461 new characterWalker( this.searchRange ),
\r
465 this.matchRange.removeHighlight();
\r
466 this.matchRange = this.matchRange.getNextCharacterRange( pattern.length );
\r
469 var matcher = new kmpMatcher( pattern, !matchCase ),
\r
470 matchState = KMP_NOMATCH,
\r
473 while ( character !== null )
\r
475 this.matchRange.moveNext();
\r
476 while ( ( character = this.matchRange.getEndCharacter() ) )
\r
478 matchState = matcher.feedCharacter( character );
\r
479 if ( matchState == KMP_MATCHED )
\r
481 if ( this.matchRange.moveNext().hitMatchBoundary )
\r
485 if ( matchState == KMP_MATCHED )
\r
489 var cursors = this.matchRange.getCursors(),
\r
490 tail = cursors[ cursors.length - 1 ],
\r
491 head = cursors[ 0 ];
\r
493 var headWalker = new characterWalker( getRangeBeforeCursor( head ), true ),
\r
494 tailWalker = new characterWalker( getRangeAfterCursor( tail ), true );
\r
496 if ( ! ( isWordSeparator( headWalker.back().character )
\r
497 && isWordSeparator( tailWalker.next().character ) ) )
\r
500 this.matchRange.setMatched();
\r
501 if ( highlightMatched !== false )
\r
502 this.matchRange.highlight();
\r
507 this.matchRange.clearMatched();
\r
508 this.matchRange.removeHighlight();
\r
509 // Clear current session and restart with the default search
\r
511 // Re-run the finding once for cyclic.(#3517)
\r
512 if ( matchCyclic && !cyclicRerun )
\r
514 this.searchRange = getSearchRange( true );
\r
515 this.matchRange = null;
\r
516 return arguments.callee.apply( this,
\r
517 Array.prototype.slice.call( arguments ).concat( [ true ] ) );
\r
524 * Record how much replacement occurred toward one replacing.
\r
526 replaceCounter : 0,
\r
528 replace : function( dialog, pattern, newString, matchCase, matchWord,
\r
529 matchCyclic , isReplaceAll )
\r
533 // Successiveness of current replace/find.
\r
534 var result = false;
\r
536 // 1. Perform the replace when there's already a match here.
\r
537 // 2. Otherwise perform the find but don't replace it immediately.
\r
538 if ( this.matchRange && this.matchRange.isMatched()
\r
539 && !this.matchRange._.isReplaced && !this.matchRange.isReadOnly() )
\r
541 // Turn off highlight for a while when saving snapshots.
\r
542 this.matchRange.removeHighlight();
\r
543 var domRange = this.matchRange.toDomRange();
\r
544 var text = editor.document.createText( newString );
\r
545 if ( !isReplaceAll )
\r
547 // Save undo snaps before and after the replacement.
\r
548 var selection = editor.getSelection();
\r
549 selection.selectRanges( [ domRange ] );
\r
550 editor.fire( 'saveSnapshot' );
\r
552 domRange.deleteContents();
\r
553 domRange.insertNode( text );
\r
554 if ( !isReplaceAll )
\r
556 selection.selectRanges( [ domRange ] );
\r
557 editor.fire( 'saveSnapshot' );
\r
559 this.matchRange.updateFromDomRange( domRange );
\r
560 if ( !isReplaceAll )
\r
561 this.matchRange.highlight();
\r
562 this.matchRange._.isReplaced = true;
\r
563 this.replaceCounter++;
\r
567 result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );
\r
576 * The range in which find/replace happened, receive from user
\r
579 function getSearchRange( isDefault )
\r
582 sel = editor.getSelection(),
\r
583 body = editor.document.getBody();
\r
584 if ( sel && !isDefault )
\r
586 searchRange = sel.getRanges()[ 0 ].clone();
\r
587 searchRange.collapse( true );
\r
591 searchRange = new CKEDITOR.dom.range();
\r
592 searchRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
\r
594 searchRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
\r
595 return searchRange;
\r
599 title : editor.lang.findAndReplace.title,
\r
600 resizable : CKEDITOR.DIALOG_RESIZE_NONE,
\r
603 buttons : [ CKEDITOR.dialog.cancelButton ], //Cancel button only.
\r
607 label : editor.lang.findAndReplace.find,
\r
608 title : editor.lang.findAndReplace.find,
\r
613 widths : [ '230px', '90px' ],
\r
618 id : 'txtFindFind',
\r
619 label : editor.lang.findAndReplace.findWhat,
\r
621 labelLayout : 'horizontal',
\r
627 style : 'width:100%',
\r
628 label : editor.lang.findAndReplace.find,
\r
629 onClick : function()
\r
631 var dialog = this.getDialog();
\r
632 if ( !finder.find( dialog.getValueOf( 'find', 'txtFindFind' ),
\r
633 dialog.getValueOf( 'find', 'txtFindCaseChk' ),
\r
634 dialog.getValueOf( 'find', 'txtFindWordChk' ),
\r
635 dialog.getValueOf( 'find', 'txtFindCyclic' ) ) )
\r
636 alert( editor.lang.findAndReplace
\r
649 id : 'txtFindCaseChk',
\r
651 style : 'margin-top:28px',
\r
652 label : editor.lang.findAndReplace.matchCase
\r
656 id : 'txtFindWordChk',
\r
658 label : editor.lang.findAndReplace.matchWord
\r
662 id : 'txtFindCyclic',
\r
665 label : editor.lang.findAndReplace.matchCyclic
\r
673 label : editor.lang.findAndReplace.replace,
\r
678 widths : [ '230px', '90px' ],
\r
683 id : 'txtFindReplace',
\r
684 label : editor.lang.findAndReplace.findWhat,
\r
686 labelLayout : 'horizontal',
\r
692 style : 'width:100%',
\r
693 label : editor.lang.findAndReplace.replace,
\r
694 onClick : function()
\r
696 var dialog = this.getDialog();
\r
697 if ( !finder.replace( dialog,
\r
698 dialog.getValueOf( 'replace', 'txtFindReplace' ),
\r
699 dialog.getValueOf( 'replace', 'txtReplace' ),
\r
700 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
\r
701 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
\r
702 dialog.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
\r
703 alert( editor.lang.findAndReplace
\r
711 widths : [ '230px', '90px' ],
\r
717 label : editor.lang.findAndReplace.replaceWith,
\r
719 labelLayout : 'horizontal',
\r
725 style : 'width:100%',
\r
726 label : editor.lang.findAndReplace.replaceAll,
\r
728 onClick : function()
\r
730 var dialog = this.getDialog();
\r
733 finder.replaceCounter = 0;
\r
735 // Scope to full document.
\r
736 finder.searchRange = getSearchRange( true );
\r
737 if ( finder.matchRange )
\r
739 finder.matchRange.removeHighlight();
\r
740 finder.matchRange = null;
\r
742 editor.fire( 'saveSnapshot' );
\r
743 while ( finder.replace( dialog,
\r
744 dialog.getValueOf( 'replace', 'txtFindReplace' ),
\r
745 dialog.getValueOf( 'replace', 'txtReplace' ),
\r
746 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
\r
747 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
\r
751 if ( finder.replaceCounter )
\r
753 alert( editor.lang.findAndReplace.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) );
\r
754 editor.fire( 'saveSnapshot' );
\r
757 alert( editor.lang.findAndReplace.notFoundMsg );
\r
769 id : 'txtReplaceCaseChk',
\r
771 label : editor.lang.findAndReplace
\r
776 id : 'txtReplaceWordChk',
\r
778 label : editor.lang.findAndReplace
\r
783 id : 'txtReplaceCyclic',
\r
786 label : editor.lang.findAndReplace
\r
794 onLoad : function()
\r
798 //keep track of the current pattern field in use.
\r
799 var patternField, wholeWordChkField;
\r
801 //Ignore initial page select on dialog show
\r
802 var isUserSelect = false;
\r
803 this.on('hide', function()
\r
805 isUserSelect = false;
\r
807 this.on('show', function()
\r
809 isUserSelect = true;
\r
812 this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc )
\r
814 return function( pageId )
\r
816 originalFunc.call( dialog, pageId );
\r
818 var currPage = dialog._.tabs[ pageId ];
\r
819 var patternFieldInput, patternFieldId, wholeWordChkFieldId;
\r
820 patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace';
\r
821 wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
\r
823 patternField = dialog.getContentElement( pageId,
\r
825 wholeWordChkField = dialog.getContentElement( pageId,
\r
826 wholeWordChkFieldId );
\r
828 // prepare for check pattern text filed 'keyup' event
\r
829 if ( !currPage.initialized )
\r
831 patternFieldInput = CKEDITOR.document
\r
832 .getById( patternField._.inputId );
\r
833 currPage.initialized = true;
\r
836 if ( isUserSelect )
\r
837 // synchronize fields on tab switch.
\r
838 syncFieldsBetweenTabs.call( this, pageId );
\r
843 onShow : function()
\r
845 // Establish initial searching start position.
\r
846 finder.searchRange = getSearchRange();
\r
848 this.selectPage( startupPage );
\r
850 onHide : function()
\r
853 if ( finder.matchRange && finder.matchRange.isMatched() )
\r
855 finder.matchRange.removeHighlight();
\r
858 range = finder.matchRange.toDomRange();
\r
860 editor.getSelection().selectRanges( [ range ] );
\r
863 // Clear current session before dialog close
\r
864 delete finder.matchRange;
\r
866 onFocus : function()
\r
868 if ( startupPage == 'replace' )
\r
869 return this.getContentElement( 'replace', 'txtFindReplace' );
\r
871 return this.getContentElement( 'find', 'txtFindFind' );
\r
876 CKEDITOR.dialog.add( 'find', function( editor )
\r
878 return findDialog( editor, 'find' );
\r
881 CKEDITOR.dialog.add( 'replace', function( editor )
\r
883 return findDialog( editor, 'replace' );
\r