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, cyclicRerun )
\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
483 // Re-run the finding once for cyclic.(#3517)
\r
484 if ( matchCyclic && !cyclicRerun )
\r
486 this.searchRange = getSearchRange( true );
\r
487 this.matchRange = null;
\r
488 return arguments.callee.apply( this,
\r
489 Array.prototype.slice.call( arguments ).concat( [ true ] ) );
\r
496 * Record how much replacement occurred toward one replacing.
\r
498 replaceCounter : 0,
\r
500 replace : function( dialog, pattern, newString, matchCase, matchWord,
\r
501 matchCyclic , isReplaceAll )
\r
503 // Successiveness of current replace/find.
\r
504 var result = false;
\r
506 // 1. Perform the replace when there's already a match here.
\r
507 // 2. Otherwise perform the find but don't replace it immediately.
\r
508 if ( this.matchRange && this.matchRange.isMatched()
\r
509 && !this.matchRange._.isReplaced )
\r
511 // Turn off highlight for a while when saving snapshots.
\r
512 this.matchRange.removeHighlight();
\r
513 var domRange = this.matchRange.toDomRange();
\r
514 var text = editor.document.createText( newString );
\r
515 if ( !isReplaceAll )
\r
517 // Save undo snaps before and after the replacement.
\r
518 var selection = editor.getSelection();
\r
519 selection.selectRanges( [ domRange ] );
\r
520 editor.fire( 'saveSnapshot' );
\r
522 domRange.deleteContents();
\r
523 domRange.insertNode( text );
\r
524 if ( !isReplaceAll )
\r
526 selection.selectRanges( [ domRange ] );
\r
527 editor.fire( 'saveSnapshot' );
\r
529 this.matchRange.updateFromDomRange( domRange );
\r
530 if ( !isReplaceAll )
\r
531 this.matchRange.highlight();
\r
532 this.matchRange._.isReplaced = true;
\r
533 this.replaceCounter++;
\r
537 result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );
\r
544 * The range in which find/replace happened, receive from user
\r
547 function getSearchRange( isDefault )
\r
550 sel = editor.getSelection(),
\r
551 body = editor.document.getBody();
\r
552 if ( sel && !isDefault )
\r
554 searchRange = sel.getRanges()[ 0 ].clone();
\r
555 searchRange.collapse( true );
\r
559 searchRange = new CKEDITOR.dom.range();
\r
560 searchRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
\r
562 searchRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
\r
563 return searchRange;
\r
567 title : editor.lang.findAndReplace.title,
\r
568 resizable : CKEDITOR.DIALOG_RESIZE_NONE,
\r
571 buttons : [ CKEDITOR.dialog.cancelButton ], //Cancel button only.
\r
575 label : editor.lang.findAndReplace.find,
\r
576 title : editor.lang.findAndReplace.find,
\r
581 widths : [ '230px', '90px' ],
\r
586 id : 'txtFindFind',
\r
587 label : editor.lang.findAndReplace.findWhat,
\r
589 labelLayout : 'horizontal',
\r
595 style : 'width:100%',
\r
596 label : editor.lang.findAndReplace.find,
\r
597 onClick : function()
\r
599 var dialog = this.getDialog();
\r
600 if ( !finder.find( dialog.getValueOf( 'find', 'txtFindFind' ),
\r
601 dialog.getValueOf( 'find', 'txtFindCaseChk' ),
\r
602 dialog.getValueOf( 'find', 'txtFindWordChk' ),
\r
603 dialog.getValueOf( 'find', 'txtFindCyclic' ) ) )
\r
604 alert( editor.lang.findAndReplace
\r
617 id : 'txtFindCaseChk',
\r
619 style : 'margin-top:28px',
\r
620 label : editor.lang.findAndReplace.matchCase
\r
624 id : 'txtFindWordChk',
\r
626 label : editor.lang.findAndReplace.matchWord
\r
630 id : 'txtFindCyclic',
\r
633 label : editor.lang.findAndReplace.matchCyclic
\r
641 label : editor.lang.findAndReplace.replace,
\r
646 widths : [ '230px', '90px' ],
\r
651 id : 'txtFindReplace',
\r
652 label : editor.lang.findAndReplace.findWhat,
\r
654 labelLayout : 'horizontal',
\r
660 style : 'width:100%',
\r
661 label : editor.lang.findAndReplace.replace,
\r
662 onClick : function()
\r
664 var dialog = this.getDialog();
\r
665 if ( !finder.replace( dialog,
\r
666 dialog.getValueOf( 'replace', 'txtFindReplace' ),
\r
667 dialog.getValueOf( 'replace', 'txtReplace' ),
\r
668 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
\r
669 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
\r
670 dialog.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
\r
671 alert( editor.lang.findAndReplace
\r
679 widths : [ '230px', '90px' ],
\r
685 label : editor.lang.findAndReplace.replaceWith,
\r
687 labelLayout : 'horizontal',
\r
693 style : 'width:100%',
\r
694 label : editor.lang.findAndReplace.replaceAll,
\r
696 onClick : function()
\r
698 var dialog = this.getDialog();
\r
701 finder.replaceCounter = 0;
\r
703 // Scope to full document.
\r
704 finder.searchRange = getSearchRange( true );
\r
705 if ( finder.matchRange )
\r
707 finder.matchRange.removeHighlight();
\r
708 finder.matchRange = null;
\r
710 editor.fire( 'saveSnapshot' );
\r
711 while( finder.replace( dialog,
\r
712 dialog.getValueOf( 'replace', 'txtFindReplace' ),
\r
713 dialog.getValueOf( 'replace', 'txtReplace' ),
\r
714 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
\r
715 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
\r
719 if ( finder.replaceCounter )
\r
721 alert( editor.lang.findAndReplace.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) );
\r
722 editor.fire( 'saveSnapshot' );
\r
725 alert( editor.lang.findAndReplace.notFoundMsg );
\r
737 id : 'txtReplaceCaseChk',
\r
739 label : editor.lang.findAndReplace
\r
744 id : 'txtReplaceWordChk',
\r
746 label : editor.lang.findAndReplace
\r
751 id : 'txtReplaceCyclic',
\r
754 label : editor.lang.findAndReplace
\r
762 onLoad : function()
\r
766 //keep track of the current pattern field in use.
\r
767 var patternField, wholeWordChkField;
\r
769 //Ignore initial page select on dialog show
\r
770 var isUserSelect = false;
\r
771 this.on('hide', function()
\r
773 isUserSelect = false;
\r
775 this.on('show', function()
\r
777 isUserSelect = true;
\r
780 this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc )
\r
782 return function( pageId )
\r
784 originalFunc.call( dialog, pageId );
\r
786 var currPage = dialog._.tabs[ pageId ];
\r
787 var patternFieldInput, patternFieldId, wholeWordChkFieldId;
\r
788 patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace';
\r
789 wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
\r
791 patternField = dialog.getContentElement( pageId,
\r
793 wholeWordChkField = dialog.getContentElement( pageId,
\r
794 wholeWordChkFieldId );
\r
796 // prepare for check pattern text filed 'keyup' event
\r
797 if ( !currPage.initialized )
\r
799 patternFieldInput = CKEDITOR.document
\r
800 .getById( patternField._.inputId );
\r
801 currPage.initialized = true;
\r
805 // synchronize fields on tab switch.
\r
806 syncFieldsBetweenTabs.call( this, pageId );
\r
811 onShow : function()
\r
813 // Establish initial searching start position.
\r
814 finder.searchRange = getSearchRange();
\r
816 if ( startupPage == 'replace' )
\r
817 this.getContentElement( 'replace', 'txtFindReplace' ).focus();
\r
819 this.getContentElement( 'find', 'txtFindFind' ).focus();
\r
821 onHide : function()
\r
823 if ( finder.matchRange && finder.matchRange.isMatched() )
\r
825 finder.matchRange.removeHighlight();
\r
827 editor.getSelection().selectRanges(
\r
828 [ finder.matchRange.toDomRange() ] );
\r
831 // Clear current session before dialog close
\r
832 delete finder.matchRange;
\r
837 CKEDITOR.dialog.add( 'find', function( editor )
\r
839 return findDialog( editor, 'find' );
\r
842 CKEDITOR.dialog.add( 'replace', function( editor )
\r
844 return findDialog( editor, 'replace' );
\r