Update ckeditor to version 3.2.1
[gopost.git] / ckeditor / _source / plugins / find / dialogs / find.js
blobdd7048cf76f9017f966a076a658bd529118236d7
1 /*
2 Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.
3 For licensing, see LICENSE.html or http://ckeditor.com/license
4 */
6 (function()
8 function nonEmptyText( node )
10 return ( node.type == CKEDITOR.NODE_TEXT && node.getLength() > 0 );
13 /**
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 ) ) );
22 /**
23 * Get the cursor object which represent both current character and it's dom
24 * position thing.
26 var cursorStep = function()
28 return {
29 textNode : this.textNode,
30 offset : this.offset,
31 character : this.textNode ?
32 this.textNode.getText().charAt( this.offset ) : null,
33 hitMatchBoundary : this._.matchBoundary
37 var pages = [ 'find', 'replace' ],
38 fieldsMapping = [
39 [ 'txtFindFind', 'txtFindReplace' ],
40 [ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
41 [ 'txtFindWordChk', 'txtReplaceWordChk' ],
42 [ 'txtFindCyclic', 'txtReplaceCyclic' ] ];
44 /**
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 ) );
75 /**
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 )
84 var walker =
85 new CKEDITOR.dom.walker( range );
86 walker.guard = matchWord ? nonCharactersBoundary : null;
87 walker[ 'evaluator' ] = nonEmptyText;
88 walker.breakOnFalse = true;
90 this._ = {
91 matchWord : matchWord,
92 walker : walker,
93 matchBoundary : false
97 characterWalker.prototype = {
98 next : function()
100 return this.move();
103 back : function()
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.
118 if ( currentTextNode
119 && rtl
120 && this.offset > 0 )
122 this.offset--;
123 return cursorStep.call( this );
125 else if ( currentTextNode
126 && this.offset < currentTextNode.getLength() - 1 )
128 this.offset++;
129 return cursorStep.call( this );
131 else
133 currentTextNode = null;
134 // At the end of the text node, walking foward for the next.
135 while ( !currentTextNode )
137 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 )
144 break;
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;
156 else
157 this.offset = 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 )
171 this._ = {
172 walker : characterWalker,
173 cursors : [],
174 rangeLength : rangeLength,
175 highlightRange : null,
176 isMatched : false
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;
191 if ( textNode )
192 range.setStartAfter( textNode );
193 else
194 return null;
196 else
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 );
205 return range;
208 * Reflect the latest changes from dom range.
210 updateFromDomRange : function( domRange )
212 var cursor,
213 walker = new characterWalker( domRange );
214 this._.cursors = [];
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 )
247 return;
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 )
274 return;
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 )
291 cursors.pop();
293 return retval;
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 )
307 cursors.shift();
309 return retval;
312 getEndCharacter : function()
314 var cursors = this._.cursors;
315 if ( cursors.length < 1 )
316 return null;
318 return cursors[ cursors.length - 1 ].character;
321 getNextCharacterRange : function( maxLength )
323 var lastCursor,
324 nextRangeWalker,
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).
330 else
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 );
351 return range;
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 );
361 return range;
364 var KMP_NOMATCH = 0,
365 KMP_ADVANCED = 1,
366 KMP_MATCHED = 2;
368 * Examination the occurrence of a word which implement KMP algorithm.
370 var kmpMatcher = function( pattern, ignoreCase )
372 var overlap = [ -1 ];
373 if ( ignoreCase )
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;
384 this._ = {
385 overlap : overlap,
386 state : 0,
387 ignoreCase : !!ignoreCase,
388 pattern : pattern
392 kmpMatcher.prototype =
394 feedCharacter : function( c )
396 if ( this._.ignoreCase )
397 c = c.toLowerCase();
399 while ( true )
401 if ( c == this._.pattern.charAt( this._.state ) )
403 this._.state++;
404 if ( this._.state == this._.pattern.length )
406 this._.state = 0;
407 return KMP_MATCHED;
409 return KMP_ADVANCED;
411 else if ( !this._.state )
412 return KMP_NOMATCH;
413 else
414 this._.state = this._.overlap[ this._.state ];
417 return null;
420 reset : function()
422 this._.state = 0;
426 var wordSeparatorRegex =
427 /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;
429 var isWordSeparator = function( c )
431 if ( !c )
432 return true;
433 var code = c.charCodeAt( 0 );
434 return ( code >= 9 && code <= 0xd )
435 || ( code >= 0x2000 && code <= 0x200a )
436 || wordSeparatorRegex.test( c );
439 var finder = {
440 searchRange : null,
441 matchRange : null,
442 find : function( pattern, matchCase, matchWord, matchCyclic, highlightMatched, cyclicRerun )
444 if ( !this.matchRange )
445 this.matchRange =
446 new characterRange(
447 new characterWalker( this.searchRange ),
448 pattern.length );
449 else
451 this.matchRange.removeHighlight();
452 this.matchRange = this.matchRange.getNextCharacterRange( pattern.length );
455 var matcher = new kmpMatcher( pattern, !matchCase ),
456 matchState = KMP_NOMATCH,
457 character = '%';
459 while ( character !== null )
461 this.matchRange.moveNext();
462 while ( ( character = this.matchRange.getEndCharacter() ) )
464 matchState = matcher.feedCharacter( character );
465 if ( matchState == KMP_MATCHED )
466 break;
467 if ( this.matchRange.moveNext().hitMatchBoundary )
468 matcher.reset();
471 if ( matchState == KMP_MATCHED )
473 if ( matchWord )
475 var cursors = this.matchRange.getCursors(),
476 tail = cursors[ cursors.length - 1 ],
477 head = cursors[ 0 ];
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 ) ) )
484 continue;
486 this.matchRange.setMatched();
487 if ( highlightMatched !== false )
488 this.matchRange.highlight();
489 return true;
493 this.matchRange.clearMatched();
494 this.matchRange.removeHighlight();
495 // Clear current session and restart with the default search
496 // range.
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 ] ) );
506 return false;
510 * Record how much replacement occurred toward one replacing.
512 replaceCounter : 0,
514 replace : function( dialog, pattern, newString, matchCase, matchWord,
515 matchCyclic , isReplaceAll )
517 // Successiveness of current replace/find.
518 var result = false;
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 );
529 if ( !isReplaceAll )
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 );
538 if ( !isReplaceAll )
540 selection.selectRanges( [ domRange ] );
541 editor.fire( 'saveSnapshot' );
543 this.matchRange.updateFromDomRange( domRange );
544 if ( !isReplaceAll )
545 this.matchRange.highlight();
546 this.matchRange._.isReplaced = true;
547 this.replaceCounter++;
548 result = true;
550 else
551 result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );
553 return result;
558 * The range in which find/replace happened, receive from user
559 * selection prior.
561 function getSearchRange( isDefault )
563 var searchRange,
564 sel = editor.getSelection(),
565 body = editor.document.getBody();
566 if ( sel && !isDefault )
568 searchRange = sel.getRanges()[ 0 ].clone();
569 searchRange.collapse( true );
571 else
573 searchRange = new CKEDITOR.dom.range();
574 searchRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
576 searchRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
577 return searchRange;
580 return {
581 title : editor.lang.findAndReplace.title,
582 resizable : CKEDITOR.DIALOG_RESIZE_NONE,
583 minWidth : 350,
584 minHeight : 165,
585 buttons : [ CKEDITOR.dialog.cancelButton ], //Cancel button only.
586 contents : [
588 id : 'find',
589 label : editor.lang.findAndReplace.find,
590 title : editor.lang.findAndReplace.find,
591 accessKey : '',
592 elements : [
594 type : 'hbox',
595 widths : [ '230px', '90px' ],
596 children :
599 type : 'text',
600 id : 'txtFindFind',
601 label : editor.lang.findAndReplace.findWhat,
602 isChanged : false,
603 labelLayout : 'horizontal',
604 accessKey : 'F'
607 type : 'button',
608 align : 'left',
609 style : 'width:100%',
610 label : editor.lang.findAndReplace.find,
611 onClick : function()
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
619 .notFoundMsg );
625 type : 'vbox',
626 padding : 0,
627 children :
630 type : 'checkbox',
631 id : 'txtFindCaseChk',
632 isChanged : false,
633 style : 'margin-top:28px',
634 label : editor.lang.findAndReplace.matchCase
637 type : 'checkbox',
638 id : 'txtFindWordChk',
639 isChanged : false,
640 label : editor.lang.findAndReplace.matchWord
643 type : 'checkbox',
644 id : 'txtFindCyclic',
645 isChanged : false,
646 'default' : true,
647 label : editor.lang.findAndReplace.matchCyclic
654 id : 'replace',
655 label : editor.lang.findAndReplace.replace,
656 accessKey : 'M',
657 elements : [
659 type : 'hbox',
660 widths : [ '230px', '90px' ],
661 children :
664 type : 'text',
665 id : 'txtFindReplace',
666 label : editor.lang.findAndReplace.findWhat,
667 isChanged : false,
668 labelLayout : 'horizontal',
669 accessKey : 'F'
672 type : 'button',
673 align : 'left',
674 style : 'width:100%',
675 label : editor.lang.findAndReplace.replace,
676 onClick : function()
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
686 .notFoundMsg );
692 type : 'hbox',
693 widths : [ '230px', '90px' ],
694 children :
697 type : 'text',
698 id : 'txtReplace',
699 label : editor.lang.findAndReplace.replaceWith,
700 isChanged : false,
701 labelLayout : 'horizontal',
702 accessKey : 'R'
705 type : 'button',
706 align : 'left',
707 style : 'width:100%',
708 label : editor.lang.findAndReplace.replaceAll,
709 isChanged : false,
710 onClick : function()
712 var dialog = this.getDialog();
713 var replaceNums;
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' ),
730 false, true ) )
731 { /*jsl:pass*/ }
733 if ( finder.replaceCounter )
735 alert( editor.lang.findAndReplace.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) );
736 editor.fire( 'saveSnapshot' );
738 else
739 alert( editor.lang.findAndReplace.notFoundMsg );
745 type : 'vbox',
746 padding : 0,
747 children :
750 type : 'checkbox',
751 id : 'txtReplaceCaseChk',
752 isChanged : false,
753 label : editor.lang.findAndReplace
754 .matchCase
757 type : 'checkbox',
758 id : 'txtReplaceWordChk',
759 isChanged : false,
760 label : editor.lang.findAndReplace
761 .matchWord
764 type : 'checkbox',
765 id : 'txtReplaceCyclic',
766 isChanged : false,
767 'default' : true,
768 label : editor.lang.findAndReplace
769 .matchCyclic
776 onLoad : function()
778 var dialog = this;
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;
788 } );
789 this.on('show', function()
791 isUserSelect = true;
792 } );
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,
806 patternFieldId );
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;
818 if ( isUserSelect )
819 // synchronize fields on tab switch.
820 syncFieldsBetweenTabs.call( this, pageId );
822 } );
825 onShow : function()
827 // Establish initial searching start position.
828 finder.searchRange = getSearchRange();
830 this.selectPage( startupPage );
832 onHide : function()
834 var range;
835 if ( finder.matchRange && finder.matchRange.isMatched() )
837 finder.matchRange.removeHighlight();
838 editor.focus();
840 range = finder.matchRange.toDomRange();
841 if ( range )
842 editor.getSelection().selectRanges( [ range ] );
845 // Clear current session before dialog close
846 delete finder.matchRange;
848 onFocus : function()
850 if ( startupPage == 'replace' )
851 return this.getContentElement( 'replace', 'txtFindReplace' );
852 else
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' );
867 })();