2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.
\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license
\r
8 function guardDomWalkerNonEmptyTextNode( node )
\r
10 return ( node.type == CKEDITOR.NODE_TEXT && node.getLength() > 0 );
\r
14 * Elements which break characters been considered as sequence.
\r
16 function checkCharactersBoundary ( node )
\r
18 var dtd = CKEDITOR.dtd;
\r
19 return node.isBlockBoundary(
\r
20 CKEDITOR.tools.extend( {}, dtd.$empty, dtd.$nonEditable ) );
\r
24 * Get the cursor object which represent both current character and it's dom
\r
27 var cursorStep = function()
\r
30 textNode : this.textNode,
\r
31 offset : this.offset,
\r
32 character : this.textNode ?
\r
33 this.textNode.getText().charAt( this.offset ) : null,
\r
34 hitMatchBoundary : this._.matchBoundary
\r
38 var pages = [ 'find', 'replace' ],
\r
40 [ 'txtFindFind', 'txtFindReplace' ],
\r
41 [ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
\r
42 [ 'txtFindWordChk', 'txtReplaceWordChk' ],
\r
43 [ 'txtFindCyclic', 'txtReplaceCyclic' ] ];
\r
46 * Synchronize corresponding filed values between 'replace' and 'find' pages.
\r
47 * @param {String} currentPageId The page id which receive values.
\r
49 function syncFieldsBetweenTabs( currentPageId )
\r
51 var sourceIndex, targetIndex,
\r
52 sourceField, targetField;
\r
54 sourceIndex = currentPageId === 'find' ? 1 : 0;
\r
55 targetIndex = 1 - sourceIndex;
\r
56 var i, l = fieldsMapping.length;
\r
57 for ( i = 0 ; i < l ; i++ )
\r
59 sourceField = this.getContentElement( pages[ sourceIndex ],
\r
60 fieldsMapping[ i ][ sourceIndex ] );
\r
61 targetField = this.getContentElement( pages[ targetIndex ],
\r
62 fieldsMapping[ i ][ targetIndex ] );
\r
64 targetField.setValue( sourceField.getValue() );
\r
68 var findDialog = function( editor, startupPage )
\r
70 // Style object for highlights.
\r
71 var highlightStyle = new CKEDITOR.style( editor.config.find_highlight );
\r
74 * Iterator which walk through the specified range char by char. By
\r
75 * default the walking will not stop at the character boundaries, until
\r
76 * the end of the range is encountered.
\r
77 * @param { CKEDITOR.dom.range } range
\r
78 * @param {Boolean} matchWord Whether the walking will stop at character boundary.
\r
80 var characterWalker = function( range , matchWord )
\r
83 new CKEDITOR.dom.walker( range );
\r
84 walker[ matchWord ? 'guard' : 'evaluator' ] =
\r
85 guardDomWalkerNonEmptyTextNode;
\r
86 walker.breakOnFalse = true;
\r
89 matchWord : matchWord,
\r
91 matchBoundary : false
\r
95 characterWalker.prototype = {
\r
103 return this.move( true );
\r
106 move : function( rtl )
\r
108 var currentTextNode = this.textNode;
\r
109 // Already at the end of document, no more character available.
\r
110 if( currentTextNode === null )
\r
111 return cursorStep.call( this );
\r
113 this._.matchBoundary = false;
\r
115 // There are more characters in the text node, step forward.
\r
116 if( currentTextNode
\r
118 && this.offset > 0 )
\r
121 return cursorStep.call( this );
\r
123 else if( currentTextNode
\r
124 && this.offset < currentTextNode.getLength() - 1 )
\r
127 return cursorStep.call( this );
\r
131 currentTextNode = null;
\r
132 // At the end of the text node, walking foward for the next.
\r
133 while ( !currentTextNode )
\r
136 this._.walker[ rtl ? 'previous' : 'next' ].call( this._.walker );
\r
138 // Stop searching if we're need full word match OR
\r
139 // already reach document end.
\r
140 if ( this._.matchWord && !currentTextNode
\r
141 ||this._.walker._.end )
\r
144 // Marking as match character boundaries.
\r
145 if( !currentTextNode
\r
146 && checkCharactersBoundary( this._.walker.current ) )
\r
147 this._.matchBoundary = true;
\r
150 // Found a fresh text node.
\r
151 this.textNode = currentTextNode;
\r
152 if ( currentTextNode )
\r
153 this.offset = rtl ? currentTextNode.getLength() - 1 : 0;
\r
158 return cursorStep.call( this );
\r
164 * A range of cursors which represent a trunk of characters which try to
\r
165 * match, it has the same length as the pattern string.
\r
167 var characterRange = function( characterWalker, rangeLength )
\r
170 walker : characterWalker,
\r
172 rangeLength : rangeLength,
\r
173 highlightRange : null,
\r
178 characterRange.prototype = {
\r
180 * Translate this range to {@link CKEDITOR.dom.range}
\r
182 toDomRange : function()
\r
184 var cursors = this._.cursors;
\r
185 if ( cursors.length < 1 )
\r
188 var first = cursors[0],
\r
189 last = cursors[ cursors.length - 1 ],
\r
190 range = new CKEDITOR.dom.range( editor.document );
\r
192 range.setStart( first.textNode, first.offset );
\r
193 range.setEnd( last.textNode, last.offset + 1 );
\r
197 * Reflect the latest changes from dom range.
\r
199 updateFromDomRange : function( domRange )
\r
202 walker = new characterWalker( domRange );
\r
203 this._.cursors = [];
\r
206 cursor = walker.next();
\r
207 if ( cursor.character )
\r
208 this._.cursors.push( cursor );
\r
210 while ( cursor.character );
\r
211 this._.rangeLength = this._.cursors.length;
\r
214 setMatched : function()
\r
216 this._.isMatched = true;
\r
219 clearMatched : function()
\r
221 this._.isMatched = false;
\r
224 isMatched : function()
\r
226 return this._.isMatched;
\r
230 * Hightlight the current matched chunk of text.
\r
232 highlight : function()
\r
234 // Do not apply if nothing is found.
\r
235 if ( this._.cursors.length < 1 )
\r
238 // Remove the previous highlight if there's one.
\r
239 if ( this._.highlightRange )
\r
240 this.removeHighlight();
\r
242 // Apply the highlight.
\r
243 var range = this.toDomRange();
\r
244 highlightStyle.applyToRange( range );
\r
245 this._.highlightRange = range;
\r
247 // Scroll the editor to the highlighted area.
\r
248 var element = range.startContainer;
\r
249 if ( element.type != CKEDITOR.NODE_ELEMENT )
\r
250 element = element.getParent();
\r
251 element.scrollIntoView();
\r
253 // Update the character cursors.
\r
254 this.updateFromDomRange( range );
\r
258 * Remove highlighted find result.
\r
260 removeHighlight : function()
\r
262 if ( !this._.highlightRange )
\r
265 highlightStyle.removeFromRange( this._.highlightRange );
\r
266 this.updateFromDomRange( this._.highlightRange );
\r
267 this._.highlightRange = null;
\r
270 moveBack : function()
\r
272 var retval = this._.walker.back(),
\r
273 cursors = this._.cursors;
\r
275 if ( retval.hitMatchBoundary )
\r
276 this._.cursors = cursors = [];
\r
278 cursors.unshift( retval );
\r
279 if ( cursors.length > this._.rangeLength )
\r
285 moveNext : function()
\r
287 var retval = this._.walker.next(),
\r
288 cursors = this._.cursors;
\r
290 // Clear the cursors queue if we've crossed a match boundary.
\r
291 if ( retval.hitMatchBoundary )
\r
292 this._.cursors = cursors = [];
\r
294 cursors.push( retval );
\r
295 if ( cursors.length > this._.rangeLength )
\r
301 getEndCharacter : function()
\r
303 var cursors = this._.cursors;
\r
304 if ( cursors.length < 1 )
\r
307 return cursors[ cursors.length - 1 ].character;
\r
310 getNextCharacterRange : function( maxLength )
\r
313 cursors = this._.cursors;
\r
314 if ( !( lastCursor = cursors[ cursors.length - 1 ] ) )
\r
316 return new characterRange(
\r
317 new characterWalker(
\r
318 getRangeAfterCursor( lastCursor ) ),
\r
322 getCursors : function()
\r
324 return this._.cursors;
\r
329 // The remaining document range after the character cursor.
\r
330 function getRangeAfterCursor( cursor , inclusive )
\r
332 var range = new CKEDITOR.dom.range();
\r
333 range.setStart( cursor.textNode,
\r
334 ( inclusive ? cursor.offset : cursor.offset + 1 ) );
\r
335 range.setEndAt( editor.document.getBody(),
\r
336 CKEDITOR.POSITION_BEFORE_END );
\r
340 // The document range before the character cursor.
\r
341 function getRangeBeforeCursor( cursor )
\r
343 var range = new CKEDITOR.dom.range();
\r
344 range.setStartAt( editor.document.getBody(),
\r
345 CKEDITOR.POSITION_AFTER_START );
\r
346 range.setEnd( cursor.textNode, cursor.offset );
\r
350 var KMP_NOMATCH = 0,
\r
354 * Examination the occurrence of a word which implement KMP algorithm.
\r
356 var kmpMatcher = function( pattern, ignoreCase )
\r
358 var overlap = [ -1 ];
\r
360 pattern = pattern.toLowerCase();
\r
361 for ( var i = 0 ; i < pattern.length ; i++ )
\r
363 overlap.push( overlap[i] + 1 );
\r
364 while ( overlap[ i + 1 ] > 0
\r
365 && pattern.charAt( i ) != pattern
\r
366 .charAt( overlap[ i + 1 ] - 1 ) )
\r
367 overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1;
\r
373 ignoreCase : !!ignoreCase,
\r
378 kmpMatcher.prototype =
\r
380 feedCharacter : function( c )
\r
382 if ( this._.ignoreCase )
\r
383 c = c.toLowerCase();
\r
387 if ( c == this._.pattern.charAt( this._.state ) )
\r
390 if ( this._.state == this._.pattern.length )
\r
393 return KMP_MATCHED;
\r
395 return KMP_ADVANCED;
\r
397 else if ( !this._.state )
\r
398 return KMP_NOMATCH;
\r
400 this._.state = this._.overlap[ this._.state ];
\r
412 var wordSeparatorRegex =
\r
413 /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;
\r
415 var isWordSeparator = function( c )
\r
419 var code = c.charCodeAt( 0 );
\r
420 return ( code >= 9 && code <= 0xd )
\r
421 || ( code >= 0x2000 && code <= 0x200a )
\r
422 || wordSeparatorRegex.test( c );
\r
426 searchRange : null,
\r
428 find : function( pattern, matchCase, matchWord, matchCyclic, highlightMatched )
\r
430 if( !this.matchRange )
\r
432 new characterRange(
\r
433 new characterWalker( this.searchRange ),
\r
437 this.matchRange.removeHighlight();
\r
438 this.matchRange = this.matchRange.getNextCharacterRange( pattern.length );
\r
441 var matcher = new kmpMatcher( pattern, !matchCase ),
\r
442 matchState = KMP_NOMATCH,
\r
445 while ( character !== null )
\r
447 this.matchRange.moveNext();
\r
448 while ( ( character = this.matchRange.getEndCharacter() ) )
\r
450 matchState = matcher.feedCharacter( character );
\r
451 if ( matchState == KMP_MATCHED )
\r
453 if ( this.matchRange.moveNext().hitMatchBoundary )
\r
457 if ( matchState == KMP_MATCHED )
\r
461 var cursors = this.matchRange.getCursors(),
\r
462 tail = cursors[ cursors.length - 1 ],
\r
463 head = cursors[ 0 ];
\r
465 var headWalker = new characterWalker( getRangeBeforeCursor( head ), true ),
\r
466 tailWalker = new characterWalker( getRangeAfterCursor( tail ), true );
\r
468 if ( ! ( isWordSeparator( headWalker.back().character )
\r
469 && isWordSeparator( tailWalker.next().character ) ) )
\r
472 this.matchRange.setMatched();
\r
473 if ( highlightMatched !== false )
\r
474 this.matchRange.highlight();
\r
479 this.matchRange.clearMatched();
\r
480 this.matchRange.removeHighlight();
\r
481 // Clear current session and restart with the default search
\r
485 this.searchRange = getSearchRange( true );
\r
486 this.matchRange = null;
\r
493 * Record how much replacement occurred toward one replacing.
\r
495 replaceCounter : 0,
\r
497 replace : function( dialog, pattern, newString, matchCase, matchWord,
\r
498 matchCyclic , isReplaceAll )
\r
500 // Successiveness of current replace/find.
\r
501 var result = false;
\r
503 // 1. Perform the replace when there's already a match here.
\r
504 // 2. Otherwise perform the find but don't replace it immediately.
\r
505 if ( this.matchRange && this.matchRange.isMatched()
\r
506 && !this.matchRange._.isReplaced )
\r
508 // Turn off highlight for a while when saving snapshots.
\r
509 this.matchRange.removeHighlight();
\r
510 var domRange = this.matchRange.toDomRange();
\r
511 var text = editor.document.createText( newString );
\r
512 if ( !isReplaceAll )
\r
514 // Save undo snaps before and after the replacement.
\r
515 var selection = editor.getSelection();
\r
516 selection.selectRanges( [ domRange ] );
\r
517 editor.fire( 'saveSnapshot' );
\r
519 domRange.deleteContents();
\r
520 domRange.insertNode( text );
\r
521 if ( !isReplaceAll )
\r
523 selection.selectRanges( [ domRange ] );
\r
524 editor.fire( 'saveSnapshot' );
\r
526 this.matchRange.updateFromDomRange( domRange );
\r
527 if ( !isReplaceAll )
\r
528 this.matchRange.highlight();
\r
529 this.matchRange._.isReplaced = true;
\r
530 this.replaceCounter++;
\r
534 result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );
\r
541 * The range in which find/replace happened, receive from user
\r
544 function getSearchRange( isDefault )
\r
547 sel = editor.getSelection(),
\r
548 body = editor.document.getBody();
\r
549 if ( sel && !isDefault )
\r
551 searchRange = sel.getRanges()[ 0 ].clone();
\r
552 searchRange.collapse( true );
\r
556 searchRange = new CKEDITOR.dom.range();
\r
557 searchRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
\r
559 searchRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
\r
560 return searchRange;
\r
564 title : editor.lang.findAndReplace.title,
\r
565 resizable : CKEDITOR.DIALOG_RESIZE_NONE,
\r
568 buttons : [ CKEDITOR.dialog.cancelButton ], //Cancel button only.
\r
572 label : editor.lang.findAndReplace.find,
\r
573 title : editor.lang.findAndReplace.find,
\r
578 widths : [ '230px', '90px' ],
\r
583 id : 'txtFindFind',
\r
584 label : editor.lang.findAndReplace.findWhat,
\r
586 labelLayout : 'horizontal',
\r
592 style : 'width:100%',
\r
593 label : editor.lang.findAndReplace.find,
\r
594 onClick : function()
\r
596 var dialog = this.getDialog();
\r
597 if ( !finder.find( dialog.getValueOf( 'find', 'txtFindFind' ),
\r
598 dialog.getValueOf( 'find', 'txtFindCaseChk' ),
\r
599 dialog.getValueOf( 'find', 'txtFindWordChk' ),
\r
600 dialog.getValueOf( 'find', 'txtFindCyclic' ) ) )
\r
601 alert( editor.lang.findAndReplace
\r
614 id : 'txtFindCaseChk',
\r
616 style : 'margin-top:28px',
\r
617 label : editor.lang.findAndReplace.matchCase
\r
621 id : 'txtFindWordChk',
\r
623 label : editor.lang.findAndReplace.matchWord
\r
627 id : 'txtFindCyclic',
\r
630 label : editor.lang.findAndReplace.matchCyclic
\r
638 label : editor.lang.findAndReplace.replace,
\r
643 widths : [ '230px', '90px' ],
\r
648 id : 'txtFindReplace',
\r
649 label : editor.lang.findAndReplace.findWhat,
\r
651 labelLayout : 'horizontal',
\r
657 style : 'width:100%',
\r
658 label : editor.lang.findAndReplace.replace,
\r
659 onClick : function()
\r
661 var dialog = this.getDialog();
\r
662 if ( !finder.replace( dialog,
\r
663 dialog.getValueOf( 'replace', 'txtFindReplace' ),
\r
664 dialog.getValueOf( 'replace', 'txtReplace' ),
\r
665 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
\r
666 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
\r
667 dialog.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
\r
668 alert( editor.lang.findAndReplace
\r
676 widths : [ '230px', '90px' ],
\r
682 label : editor.lang.findAndReplace.replaceWith,
\r
684 labelLayout : 'horizontal',
\r
690 style : 'width:100%',
\r
691 label : editor.lang.findAndReplace.replaceAll,
\r
693 onClick : function()
\r
695 var dialog = this.getDialog();
\r
698 finder.replaceCounter = 0;
\r
700 // Scope to full document.
\r
701 finder.searchRange = getSearchRange( true );
\r
702 if ( finder.matchRange )
\r
704 finder.matchRange.removeHighlight();
\r
705 finder.matchRange = null;
\r
707 editor.fire( 'saveSnapshot' );
\r
708 while( finder.replace( dialog,
\r
709 dialog.getValueOf( 'replace', 'txtFindReplace' ),
\r
710 dialog.getValueOf( 'replace', 'txtReplace' ),
\r
711 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
\r
712 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
\r
716 if ( finder.replaceCounter )
\r
718 alert( editor.lang.findAndReplace.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) );
\r
719 editor.fire( 'saveSnapshot' );
\r
722 alert( editor.lang.findAndReplace.notFoundMsg );
\r
734 id : 'txtReplaceCaseChk',
\r
736 label : editor.lang.findAndReplace
\r
741 id : 'txtReplaceWordChk',
\r
743 label : editor.lang.findAndReplace
\r
748 id : 'txtReplaceCyclic',
\r
751 label : editor.lang.findAndReplace
\r
759 onLoad : function()
\r
763 //keep track of the current pattern field in use.
\r
764 var patternField, wholeWordChkField;
\r
766 //Ignore initial page select on dialog show
\r
767 var isUserSelect = false;
\r
768 this.on('hide', function()
\r
770 isUserSelect = false;
\r
772 this.on('show', function()
\r
774 isUserSelect = true;
\r
777 this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc )
\r
779 return function( pageId )
\r
781 originalFunc.call( dialog, pageId );
\r
783 var currPage = dialog._.tabs[ pageId ];
\r
784 var patternFieldInput, patternFieldId, wholeWordChkFieldId;
\r
785 patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace';
\r
786 wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
\r
788 patternField = dialog.getContentElement( pageId,
\r
790 wholeWordChkField = dialog.getContentElement( pageId,
\r
791 wholeWordChkFieldId );
\r
793 // prepare for check pattern text filed 'keyup' event
\r
794 if ( !currPage.initialized )
\r
796 patternFieldInput = CKEDITOR.document
\r
797 .getById( patternField._.inputId );
\r
798 currPage.initialized = true;
\r
802 // synchronize fields on tab switch.
\r
803 syncFieldsBetweenTabs.call( this, pageId );
\r
808 onShow : function()
\r
810 // Establish initial searching start position.
\r
811 finder.searchRange = getSearchRange();
\r
813 if ( startupPage == 'replace' )
\r
814 this.getContentElement( 'replace', 'txtFindReplace' ).focus();
\r
816 this.getContentElement( 'find', 'txtFindFind' ).focus();
\r
818 onHide : function()
\r
820 if ( finder.matchRange && finder.matchRange.isMatched() )
\r
822 finder.matchRange.removeHighlight();
\r
824 editor.getSelection().selectRanges(
\r
825 [ finder.matchRange.toDomRange() ] );
\r
828 // Clear current session before dialog close
\r
829 delete finder.matchRange;
\r
834 CKEDITOR.dialog.add( 'find', function( editor )
\r
836 return findDialog( editor, 'find' );
\r
839 CKEDITOR.dialog.add( 'replace', function( editor )
\r
841 return findDialog( editor, 'replace' );
\r