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',
633 style
: 'width:100%',
637 var dialog
= this.getDialog();
638 if ( !finder
.find( dialog
.getValueOf( 'find', 'txtFindFind' ),
639 dialog
.getValueOf( 'find', 'txtFindCaseChk' ),
640 dialog
.getValueOf( 'find', 'txtFindWordChk' ),
641 dialog
.getValueOf( 'find', 'txtFindCyclic' ) ) )
655 id
: 'txtFindCaseChk',
657 style
: 'margin-top:28px',
658 label
: lang
.matchCase
662 id
: 'txtFindWordChk',
664 label
: lang
.matchWord
668 id
: 'txtFindCyclic',
671 label
: lang
.matchCyclic
679 label
: lang
.replace
,
684 widths
: [ '230px', '90px' ],
689 id
: 'txtFindReplace',
690 label
: lang
.findWhat
,
692 labelLayout
: 'horizontal',
697 id
: 'btnFindReplace',
699 style
: 'width:100%',
700 label
: lang
.replace
,
703 var dialog
= this.getDialog();
704 if ( !finder
.replace( dialog
,
705 dialog
.getValueOf( 'replace', 'txtFindReplace' ),
706 dialog
.getValueOf( 'replace', 'txtReplace' ),
707 dialog
.getValueOf( 'replace', 'txtReplaceCaseChk' ),
708 dialog
.getValueOf( 'replace', 'txtReplaceWordChk' ),
709 dialog
.getValueOf( 'replace', 'txtReplaceCyclic' ) ) )
718 widths
: [ '230px', '90px' ],
724 label
: lang
.replaceWith
,
726 labelLayout
: 'horizontal',
731 id
: 'btnReplaceAll',
733 style
: 'width:100%',
734 label
: lang
.replaceAll
,
738 var dialog
= this.getDialog();
741 finder
.replaceCounter
= 0;
743 // Scope to full document.
744 finder
.searchRange
= getSearchRange( 1 );
745 if ( finder
.matchRange
)
747 finder
.matchRange
.removeHighlight();
748 finder
.matchRange
= null;
750 editor
.fire( 'saveSnapshot' );
751 while ( finder
.replace( dialog
,
752 dialog
.getValueOf( 'replace', 'txtFindReplace' ),
753 dialog
.getValueOf( 'replace', 'txtReplace' ),
754 dialog
.getValueOf( 'replace', 'txtReplaceCaseChk' ),
755 dialog
.getValueOf( 'replace', 'txtReplaceWordChk' ),
759 if ( finder
.replaceCounter
)
761 alert( lang
.replaceSuccessMsg
.replace( /%1/, finder
.replaceCounter
) );
762 editor
.fire( 'saveSnapshot' );
765 alert( lang
.notFoundMsg
);
777 id
: 'txtReplaceCaseChk',
784 id
: 'txtReplaceWordChk',
791 id
: 'txtReplaceCyclic',
806 // Keep track of the current pattern field in use.
807 var patternField
, wholeWordChkField
;
809 // Ignore initial page select on dialog show
810 var isUserSelect
= 0;
811 this.on( 'hide', function()
815 this.on( 'show', function()
820 this.selectPage
= CKEDITOR
.tools
.override( this.selectPage
, function( originalFunc
)
822 return function( pageId
)
824 originalFunc
.call( dialog
, pageId
);
826 var currPage
= dialog
._
.tabs
[ pageId
];
827 var patternFieldInput
, patternFieldId
, wholeWordChkFieldId
;
828 patternFieldId
= pageId
=== 'find' ? 'txtFindFind' : 'txtFindReplace';
829 wholeWordChkFieldId
= pageId
=== 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
831 patternField
= dialog
.getContentElement( pageId
,
833 wholeWordChkField
= dialog
.getContentElement( pageId
,
834 wholeWordChkFieldId
);
836 // Prepare for check pattern text filed 'keyup' event
837 if ( !currPage
.initialized
)
839 patternFieldInput
= CKEDITOR
.document
840 .getById( patternField
._
.inputId
);
841 currPage
.initialized
= true;
844 // Synchronize fields on tab switch.
846 syncFieldsBetweenTabs
.call( this, pageId
);
853 // Establish initial searching start position.
854 finder
.searchRange
= getSearchRange();
856 // Fill in the find field with selected text.
857 var selectedText
= this.getParentEditor().getSelection().getSelectedText(),
858 patternFieldId
= ( startupPage
== 'find' ? 'txtFindFind' : 'txtFindReplace' );
860 var field
= this.getContentElement( startupPage
, patternFieldId
);
861 field
.setValue( selectedText
);
864 this.selectPage( startupPage
);
866 this[ ( startupPage
== 'find' && this._
.editor
.readOnly
? 'hide' : 'show' ) + 'Page' ]( 'replace');
871 if ( finder
.matchRange
&& finder
.matchRange
.isMatched() )
873 finder
.matchRange
.removeHighlight();
876 range
= finder
.matchRange
.toDomRange();
878 editor
.getSelection().selectRanges( [ range
] );
881 // Clear current session before dialog close
882 delete finder
.matchRange
;
886 if ( startupPage
== 'replace' )
887 return this.getContentElement( 'replace', 'txtFindReplace' );
889 return this.getContentElement( 'find', 'txtFindFind' );
894 CKEDITOR
.dialog
.add( 'find', function( editor
)
896 return findDialog( editor
, 'find' );
899 CKEDITOR
.dialog
.add( 'replace', function( editor
)
901 return findDialog( editor
, 'replace' );