2 Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.
3 For licensing, see LICENSE.html or http://ckeditor.com/license
8 function nonEmptyText( node
)
10 return ( node
.type
== CKEDITOR
.NODE_TEXT
&& node
.getLength() > 0 );
14 * Elements which break characters been considered as sequence.
16 function nonCharactersBoundary ( node
)
18 return !( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.isBlockBoundary(
19 CKEDITOR
.tools
.extend( {}, CKEDITOR
.dtd
.$empty
, CKEDITOR
.dtd
.$nonEditable
) ) );
23 * Get the cursor object which represent both current character and it's dom
26 var cursorStep = function()
29 textNode
: this.textNode
,
31 character
: this.textNode
?
32 this.textNode
.getText().charAt( this.offset
) : null,
33 hitMatchBoundary
: this._
.matchBoundary
37 var pages
= [ 'find', 'replace' ],
39 [ 'txtFindFind', 'txtFindReplace' ],
40 [ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
41 [ 'txtFindWordChk', 'txtReplaceWordChk' ],
42 [ 'txtFindCyclic', 'txtReplaceCyclic' ] ];
45 * Synchronize corresponding filed values between 'replace' and 'find' pages.
46 * @param {String} currentPageId The page id which receive values.
48 function syncFieldsBetweenTabs( currentPageId
)
50 var sourceIndex
, targetIndex
,
51 sourceField
, targetField
;
53 sourceIndex
= currentPageId
=== 'find' ? 1 : 0;
54 targetIndex
= 1 - sourceIndex
;
55 var i
, l
= fieldsMapping
.length
;
56 for ( i
= 0 ; i
< l
; i
++ )
58 sourceField
= this.getContentElement( pages
[ sourceIndex
],
59 fieldsMapping
[ i
][ sourceIndex
] );
60 targetField
= this.getContentElement( pages
[ targetIndex
],
61 fieldsMapping
[ i
][ targetIndex
] );
63 targetField
.setValue( sourceField
.getValue() );
67 var findDialog = function( editor
, startupPage
)
69 // Style object for highlights: (#5018)
70 // 1. Defined as full match style to avoid compromising ordinary text color styles.
71 // 2. Must be apply onto inner-most text to avoid conflicting with ordinary text color styles visually.
72 var highlightStyle
= new CKEDITOR
.style( CKEDITOR
.tools
.extend( { fullMatch
: true, childRule : function(){ return false; } },
73 editor
.config
.find_highlight
) );
76 * Iterator which walk through the specified range char by char. By
77 * default the walking will not stop at the character boundaries, until
78 * the end of the range is encountered.
79 * @param { CKEDITOR.dom.range } range
80 * @param {Boolean} matchWord Whether the walking will stop at character boundary.
82 var characterWalker = function( range
, matchWord
)
85 new CKEDITOR
.dom
.walker( range
);
86 walker
.guard
= matchWord
? nonCharactersBoundary
: null;
87 walker
[ 'evaluator' ] = nonEmptyText
;
88 walker
.breakOnFalse
= true;
91 matchWord
: matchWord
,
97 characterWalker
.prototype = {
105 return this.move( true );
108 move : function( rtl
)
110 var currentTextNode
= this.textNode
;
111 // Already at the end of document, no more character available.
112 if ( currentTextNode
=== null )
113 return cursorStep
.call( this );
115 this._
.matchBoundary
= false;
117 // There are more characters in the text node, step forward.
123 return cursorStep
.call( this );
125 else if ( currentTextNode
126 && this.offset
< currentTextNode
.getLength() - 1 )
129 return cursorStep
.call( this );
133 currentTextNode
= null;
134 // At the end of the text node, walking foward for the next.
135 while ( !currentTextNode
)
138 this._
.walker
[ rtl
? 'previous' : 'next' ].call( this._
.walker
);
140 // Stop searching if we're need full word match OR
141 // already reach document end.
142 if ( this._
.matchWord
&& !currentTextNode
143 ||this._
.walker
._
.end
)
146 // Marking as match character boundaries.
147 if ( !currentTextNode
148 && !nonCharactersBoundary( this._
.walker
.current
) )
149 this._
.matchBoundary
= true;
152 // Found a fresh text node.
153 this.textNode
= currentTextNode
;
154 if ( currentTextNode
)
155 this.offset
= rtl
? currentTextNode
.getLength() - 1 : 0;
160 return cursorStep
.call( this );
166 * A range of cursors which represent a trunk of characters which try to
167 * match, it has the same length as the pattern string.
169 var characterRange = function( characterWalker
, rangeLength
)
172 walker
: characterWalker
,
174 rangeLength
: rangeLength
,
175 highlightRange
: null,
180 characterRange
.prototype = {
182 * Translate this range to {@link CKEDITOR.dom.range}
184 toDomRange : function()
186 var range
= new CKEDITOR
.dom
.range( editor
.document
);
187 var cursors
= this._
.cursors
;
188 if ( cursors
.length
< 1 )
190 var textNode
= this._
.walker
.textNode
;
192 range
.setStartAfter( textNode
);
198 var first
= cursors
[0],
199 last
= cursors
[ cursors
.length
- 1 ];
201 range
.setStart( first
.textNode
, first
.offset
);
202 range
.setEnd( last
.textNode
, last
.offset
+ 1 );
208 * Reflect the latest changes from dom range.
210 updateFromDomRange : function( domRange
)
213 walker
= new characterWalker( domRange
);
217 cursor
= walker
.next();
218 if ( cursor
.character
)
219 this._
.cursors
.push( cursor
);
221 while ( cursor
.character
);
222 this._
.rangeLength
= this._
.cursors
.length
;
225 setMatched : function()
227 this._
.isMatched
= true;
230 clearMatched : function()
232 this._
.isMatched
= false;
235 isMatched : function()
237 return this._
.isMatched
;
241 * Hightlight the current matched chunk of text.
243 highlight : function()
245 // Do not apply if nothing is found.
246 if ( this._
.cursors
.length
< 1 )
249 // Remove the previous highlight if there's one.
250 if ( this._
.highlightRange
)
251 this.removeHighlight();
253 // Apply the highlight.
254 var range
= this.toDomRange();
255 highlightStyle
.applyToRange( range
);
256 this._
.highlightRange
= range
;
258 // Scroll the editor to the highlighted area.
259 var element
= range
.startContainer
;
260 if ( element
.type
!= CKEDITOR
.NODE_ELEMENT
)
261 element
= element
.getParent();
262 element
.scrollIntoView();
264 // Update the character cursors.
265 this.updateFromDomRange( range
);
269 * Remove highlighted find result.
271 removeHighlight : function()
273 if ( !this._
.highlightRange
)
276 highlightStyle
.removeFromRange( this._
.highlightRange
);
277 this.updateFromDomRange( this._
.highlightRange
);
278 this._
.highlightRange
= null;
281 moveBack : function()
283 var retval
= this._
.walker
.back(),
284 cursors
= this._
.cursors
;
286 if ( retval
.hitMatchBoundary
)
287 this._
.cursors
= cursors
= [];
289 cursors
.unshift( retval
);
290 if ( cursors
.length
> this._
.rangeLength
)
296 moveNext : function()
298 var retval
= this._
.walker
.next(),
299 cursors
= this._
.cursors
;
301 // Clear the cursors queue if we've crossed a match boundary.
302 if ( retval
.hitMatchBoundary
)
303 this._
.cursors
= cursors
= [];
305 cursors
.push( retval
);
306 if ( cursors
.length
> this._
.rangeLength
)
312 getEndCharacter : function()
314 var cursors
= this._
.cursors
;
315 if ( cursors
.length
< 1 )
318 return cursors
[ cursors
.length
- 1 ].character
;
321 getNextCharacterRange : function( maxLength
)
325 cursors
= this._
.cursors
;
327 if ( ( lastCursor
= cursors
[ cursors
.length
- 1 ] ) )
328 nextRangeWalker
= new characterWalker( getRangeAfterCursor( lastCursor
) );
329 // In case it's an empty range (no cursors), figure out next range from walker (#4951).
331 nextRangeWalker
= this._
.walker
;
333 return new characterRange( nextRangeWalker
, maxLength
);
336 getCursors : function()
338 return this._
.cursors
;
343 // The remaining document range after the character cursor.
344 function getRangeAfterCursor( cursor
, inclusive
)
346 var range
= new CKEDITOR
.dom
.range();
347 range
.setStart( cursor
.textNode
,
348 ( inclusive
? cursor
.offset
: cursor
.offset
+ 1 ) );
349 range
.setEndAt( editor
.document
.getBody(),
350 CKEDITOR
.POSITION_BEFORE_END
);
354 // The document range before the character cursor.
355 function getRangeBeforeCursor( cursor
)
357 var range
= new CKEDITOR
.dom
.range();
358 range
.setStartAt( editor
.document
.getBody(),
359 CKEDITOR
.POSITION_AFTER_START
);
360 range
.setEnd( cursor
.textNode
, cursor
.offset
);
368 * Examination the occurrence of a word which implement KMP algorithm.
370 var kmpMatcher = function( pattern
, ignoreCase
)
372 var overlap
= [ -1 ];
374 pattern
= pattern
.toLowerCase();
375 for ( var i
= 0 ; i
< pattern
.length
; i
++ )
377 overlap
.push( overlap
[i
] + 1 );
378 while ( overlap
[ i
+ 1 ] > 0
379 && pattern
.charAt( i
) != pattern
380 .charAt( overlap
[ i
+ 1 ] - 1 ) )
381 overlap
[ i
+ 1 ] = overlap
[ overlap
[ i
+ 1 ] - 1 ] + 1;
387 ignoreCase
: !!ignoreCase
,
392 kmpMatcher
.prototype =
394 feedCharacter : function( c
)
396 if ( this._
.ignoreCase
)
401 if ( c
== this._
.pattern
.charAt( this._
.state
) )
404 if ( this._
.state
== this._
.pattern
.length
)
411 else if ( !this._
.state
)
414 this._
.state
= this._
.overlap
[ this._
.state
];
426 var wordSeparatorRegex
=
427 /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;
429 var isWordSeparator = function( c
)
433 var code
= c
.charCodeAt( 0 );
434 return ( code
>= 9 && code
<= 0xd )
435 || ( code
>= 0x2000 && code
<= 0x200a )
436 || wordSeparatorRegex
.test( c
);
442 find : function( pattern
, matchCase
, matchWord
, matchCyclic
, highlightMatched
, cyclicRerun
)
444 if ( !this.matchRange
)
447 new characterWalker( this.searchRange
),
451 this.matchRange
.removeHighlight();
452 this.matchRange
= this.matchRange
.getNextCharacterRange( pattern
.length
);
455 var matcher
= new kmpMatcher( pattern
, !matchCase
),
456 matchState
= KMP_NOMATCH
,
459 while ( character
!== null )
461 this.matchRange
.moveNext();
462 while ( ( character
= this.matchRange
.getEndCharacter() ) )
464 matchState
= matcher
.feedCharacter( character
);
465 if ( matchState
== KMP_MATCHED
)
467 if ( this.matchRange
.moveNext().hitMatchBoundary
)
471 if ( matchState
== KMP_MATCHED
)
475 var cursors
= this.matchRange
.getCursors(),
476 tail
= cursors
[ cursors
.length
- 1 ],
479 var headWalker
= new characterWalker( getRangeBeforeCursor( head
), true ),
480 tailWalker
= new characterWalker( getRangeAfterCursor( tail
), true );
482 if ( ! ( isWordSeparator( headWalker
.back().character
)
483 && isWordSeparator( tailWalker
.next().character
) ) )
486 this.matchRange
.setMatched();
487 if ( highlightMatched
!== false )
488 this.matchRange
.highlight();
493 this.matchRange
.clearMatched();
494 this.matchRange
.removeHighlight();
495 // Clear current session and restart with the default search
497 // Re-run the finding once for cyclic.(#3517)
498 if ( matchCyclic
&& !cyclicRerun
)
500 this.searchRange
= getSearchRange( true );
501 this.matchRange
= null;
502 return arguments
.callee
.apply( this,
503 Array
.prototype.slice
.call( arguments
).concat( [ true ] ) );
510 * Record how much replacement occurred toward one replacing.
514 replace : function( dialog
, pattern
, newString
, matchCase
, matchWord
,
515 matchCyclic
, isReplaceAll
)
517 // Successiveness of current replace/find.
520 // 1. Perform the replace when there's already a match here.
521 // 2. Otherwise perform the find but don't replace it immediately.
522 if ( this.matchRange
&& this.matchRange
.isMatched()
523 && !this.matchRange
._
.isReplaced
)
525 // Turn off highlight for a while when saving snapshots.
526 this.matchRange
.removeHighlight();
527 var domRange
= this.matchRange
.toDomRange();
528 var text
= editor
.document
.createText( newString
);
531 // Save undo snaps before and after the replacement.
532 var selection
= editor
.getSelection();
533 selection
.selectRanges( [ domRange
] );
534 editor
.fire( 'saveSnapshot' );
536 domRange
.deleteContents();
537 domRange
.insertNode( text
);
540 selection
.selectRanges( [ domRange
] );
541 editor
.fire( 'saveSnapshot' );
543 this.matchRange
.updateFromDomRange( domRange
);
545 this.matchRange
.highlight();
546 this.matchRange
._
.isReplaced
= true;
547 this.replaceCounter
++;
551 result
= this.find( pattern
, matchCase
, matchWord
, matchCyclic
, !isReplaceAll
);
558 * The range in which find/replace happened, receive from user
561 function getSearchRange( isDefault
)
564 sel
= editor
.getSelection(),
565 body
= editor
.document
.getBody();
566 if ( sel
&& !isDefault
)
568 searchRange
= sel
.getRanges()[ 0 ].clone();
569 searchRange
.collapse( true );
573 searchRange
= new CKEDITOR
.dom
.range();
574 searchRange
.setStartAt( body
, CKEDITOR
.POSITION_AFTER_START
);
576 searchRange
.setEndAt( body
, CKEDITOR
.POSITION_BEFORE_END
);
581 title
: editor
.lang
.findAndReplace
.title
,
582 resizable
: CKEDITOR
.DIALOG_RESIZE_NONE
,
585 buttons
: [ CKEDITOR
.dialog
.cancelButton
], //Cancel button only.
589 label
: editor
.lang
.findAndReplace
.find
,
590 title
: editor
.lang
.findAndReplace
.find
,
595 widths
: [ '230px', '90px' ],
601 label
: editor
.lang
.findAndReplace
.findWhat
,
603 labelLayout
: 'horizontal',
609 style
: 'width:100%',
610 label
: editor
.lang
.findAndReplace
.find
,
613 var dialog
= this.getDialog();
614 if ( !finder
.find( dialog
.getValueOf( 'find', 'txtFindFind' ),
615 dialog
.getValueOf( 'find', 'txtFindCaseChk' ),
616 dialog
.getValueOf( 'find', 'txtFindWordChk' ),
617 dialog
.getValueOf( 'find', 'txtFindCyclic' ) ) )
618 alert( editor
.lang
.findAndReplace
631 id
: 'txtFindCaseChk',
633 style
: 'margin-top:28px',
634 label
: editor
.lang
.findAndReplace
.matchCase
638 id
: 'txtFindWordChk',
640 label
: editor
.lang
.findAndReplace
.matchWord
644 id
: 'txtFindCyclic',
647 label
: editor
.lang
.findAndReplace
.matchCyclic
655 label
: editor
.lang
.findAndReplace
.replace
,
660 widths
: [ '230px', '90px' ],
665 id
: 'txtFindReplace',
666 label
: editor
.lang
.findAndReplace
.findWhat
,
668 labelLayout
: 'horizontal',
674 style
: 'width:100%',
675 label
: editor
.lang
.findAndReplace
.replace
,
678 var dialog
= this.getDialog();
679 if ( !finder
.replace( dialog
,
680 dialog
.getValueOf( 'replace', 'txtFindReplace' ),
681 dialog
.getValueOf( 'replace', 'txtReplace' ),
682 dialog
.getValueOf( 'replace', 'txtReplaceCaseChk' ),
683 dialog
.getValueOf( 'replace', 'txtReplaceWordChk' ),
684 dialog
.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
685 alert( editor
.lang
.findAndReplace
693 widths
: [ '230px', '90px' ],
699 label
: editor
.lang
.findAndReplace
.replaceWith
,
701 labelLayout
: 'horizontal',
707 style
: 'width:100%',
708 label
: editor
.lang
.findAndReplace
.replaceAll
,
712 var dialog
= this.getDialog();
715 finder
.replaceCounter
= 0;
717 // Scope to full document.
718 finder
.searchRange
= getSearchRange( true );
719 if ( finder
.matchRange
)
721 finder
.matchRange
.removeHighlight();
722 finder
.matchRange
= null;
724 editor
.fire( 'saveSnapshot' );
725 while ( finder
.replace( dialog
,
726 dialog
.getValueOf( 'replace', 'txtFindReplace' ),
727 dialog
.getValueOf( 'replace', 'txtReplace' ),
728 dialog
.getValueOf( 'replace', 'txtReplaceCaseChk' ),
729 dialog
.getValueOf( 'replace', 'txtReplaceWordChk' ),
733 if ( finder
.replaceCounter
)
735 alert( editor
.lang
.findAndReplace
.replaceSuccessMsg
.replace( /%1/, finder
.replaceCounter
) );
736 editor
.fire( 'saveSnapshot' );
739 alert( editor
.lang
.findAndReplace
.notFoundMsg
);
751 id
: 'txtReplaceCaseChk',
753 label
: editor
.lang
.findAndReplace
758 id
: 'txtReplaceWordChk',
760 label
: editor
.lang
.findAndReplace
765 id
: 'txtReplaceCyclic',
768 label
: editor
.lang
.findAndReplace
780 //keep track of the current pattern field in use.
781 var patternField
, wholeWordChkField
;
783 //Ignore initial page select on dialog show
784 var isUserSelect
= false;
785 this.on('hide', function()
787 isUserSelect
= false;
789 this.on('show', function()
794 this.selectPage
= CKEDITOR
.tools
.override( this.selectPage
, function( originalFunc
)
796 return function( pageId
)
798 originalFunc
.call( dialog
, pageId
);
800 var currPage
= dialog
._
.tabs
[ pageId
];
801 var patternFieldInput
, patternFieldId
, wholeWordChkFieldId
;
802 patternFieldId
= pageId
=== 'find' ? 'txtFindFind' : 'txtFindReplace';
803 wholeWordChkFieldId
= pageId
=== 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
805 patternField
= dialog
.getContentElement( pageId
,
807 wholeWordChkField
= dialog
.getContentElement( pageId
,
808 wholeWordChkFieldId
);
810 // prepare for check pattern text filed 'keyup' event
811 if ( !currPage
.initialized
)
813 patternFieldInput
= CKEDITOR
.document
814 .getById( patternField
._
.inputId
);
815 currPage
.initialized
= true;
819 // synchronize fields on tab switch.
820 syncFieldsBetweenTabs
.call( this, pageId
);
827 // Establish initial searching start position.
828 finder
.searchRange
= getSearchRange();
830 this.selectPage( startupPage
);
835 if ( finder
.matchRange
&& finder
.matchRange
.isMatched() )
837 finder
.matchRange
.removeHighlight();
840 range
= finder
.matchRange
.toDomRange();
842 editor
.getSelection().selectRanges( [ range
] );
845 // Clear current session before dialog close
846 delete finder
.matchRange
;
850 if ( startupPage
== 'replace' )
851 return this.getContentElement( 'replace', 'txtFindReplace' );
853 return this.getContentElement( 'find', 'txtFindFind' );
858 CKEDITOR
.dialog
.add( 'find', function( editor
)
860 return findDialog( editor
, 'find' );
863 CKEDITOR
.dialog
.add( 'replace', function( editor
)
865 return findDialog( editor
, 'replace' );