]>
git.immae.eu Git - perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git/blob - sources/plugins/find/dialogs/find.js
2 * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
9 function findEvaluator( node
) {
10 return node
.type
== CKEDITOR
.NODE_TEXT
&& node
.getLength() > 0 && ( !isReplace
|| !node
.isReadOnly() );
13 // Elements which break characters been considered as sequence.
14 function nonCharactersBoundary( node
) {
15 return !( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.isBlockBoundary( CKEDITOR
.tools
.extend( {}, CKEDITOR
.dtd
.$empty
, CKEDITOR
.dtd
.$nonEditable
) ) );
18 // Get the cursor object which represent both current character and it's dom
20 var cursorStep = function() {
22 textNode: this.textNode
,
24 character: this.textNode
? this.textNode
.getText().charAt( this.offset
) : null,
25 hitMatchBoundary: this._
.matchBoundary
29 var pages
= [ 'find', 'replace' ],
31 [ 'txtFindFind', 'txtFindReplace' ],
32 [ 'txtFindCaseChk', 'txtReplaceCaseChk' ],
33 [ 'txtFindWordChk', 'txtReplaceWordChk' ],
34 [ 'txtFindCyclic', 'txtReplaceCyclic' ]
37 // Synchronize corresponding filed values between 'replace' and 'find' pages.
38 // @param {String} currentPageId The page id which receive values.
39 function syncFieldsBetweenTabs( currentPageId
) {
40 var sourceIndex
, targetIndex
, sourceField
, targetField
;
42 sourceIndex
= currentPageId
=== 'find' ? 1 : 0;
43 targetIndex
= 1 - sourceIndex
;
45 l
= fieldsMapping
.length
;
46 for ( i
= 0; i
< l
; i
++ ) {
47 sourceField
= this.getContentElement( pages
[ sourceIndex
], fieldsMapping
[ i
][ sourceIndex
] );
48 targetField
= this.getContentElement( pages
[ targetIndex
], fieldsMapping
[ i
][ targetIndex
] );
50 targetField
.setValue( sourceField
.getValue() );
54 function findDialog( editor
, startupPage
) {
55 // Style object for highlights: (#5018)
56 // 1. Defined as full match style to avoid compromising ordinary text color styles.
57 // 2. Must be apply onto inner-most text to avoid conflicting with ordinary text color styles visually.
58 var highlightConfig
= {
60 'data-cke-highlight': 1
64 childRule: function() {
68 var highlightStyle
= new CKEDITOR
.style( CKEDITOR
.tools
.extend( highlightConfig
, editor
.config
.find_highlight
, true ) );
70 // Iterator which walk through the specified range char by char. By
71 // default the walking will not stop at the character boundaries, until
72 // the end of the range is encountered.
73 // @param { CKEDITOR.dom.range } range
74 // @param {Boolean} matchWord Whether the walking will stop at character boundary.
75 function characterWalker( range
, matchWord
) {
77 var walker
= new CKEDITOR
.dom
.walker( range
);
78 walker
.guard
= matchWord
? nonCharactersBoundary : function( node
) {
79 !nonCharactersBoundary( node
) && ( self
._
.matchBoundary
= true );
81 walker
.evaluator
= findEvaluator
;
82 walker
.breakOnFalse
= 1;
84 if ( range
.startContainer
.type
== CKEDITOR
.NODE_TEXT
) {
85 this.textNode
= range
.startContainer
;
86 this.offset
= range
.startOffset
- 1;
96 characterWalker
.prototype = {
102 return this.move( true );
105 move: function( rtl
) {
106 var currentTextNode
= this.textNode
;
107 // Already at the end of document, no more character available.
108 if ( currentTextNode
=== null )
109 return cursorStep
.call( this );
111 this._
.matchBoundary
= false;
113 // There are more characters in the text node, step forward.
114 if ( currentTextNode
&& rtl
&& this.offset
> 0 ) {
116 return cursorStep
.call( this );
117 } else if ( currentTextNode
&& this.offset
< currentTextNode
.getLength() - 1 ) {
119 return cursorStep
.call( this );
121 currentTextNode
= null;
122 // At the end of the text node, walking foward for the next.
123 while ( !currentTextNode
) {
124 currentTextNode
= this._
.walker
[ rtl
? 'previous' : 'next' ].call( this._
.walker
);
126 // Stop searching if we're need full word match OR
127 // already reach document end.
128 if ( this._
.matchWord
&& !currentTextNode
|| this._
.walker
._
.end
)
131 // Found a fresh text node.
132 this.textNode
= currentTextNode
;
133 if ( currentTextNode
)
134 this.offset
= rtl
? currentTextNode
.getLength() - 1 : 0;
139 return cursorStep
.call( this );
145 * A range of cursors which represent a trunk of characters which try to
146 * match, it has the same length as the pattern string.
148 * **Note:** This class isn't accessible from global scope.
151 * @class CKEDITOR.plugins.find.characterRange
152 * @constructor Creates a characterRange class instance.
154 var characterRange = function( characterWalker
, rangeLength
) {
156 walker: characterWalker
,
158 rangeLength: rangeLength
,
159 highlightRange: null,
164 characterRange
.prototype = {
166 * Translate this range to {@link CKEDITOR.dom.range}.
168 toDomRange: function() {
169 var range
= editor
.createRange();
170 var cursors
= this._
.cursors
;
171 if ( cursors
.length
< 1 ) {
172 var textNode
= this._
.walker
.textNode
;
174 range
.setStartAfter( textNode
);
178 var first
= cursors
[ 0 ],
179 last
= cursors
[ cursors
.length
- 1 ];
181 range
.setStart( first
.textNode
, first
.offset
);
182 range
.setEnd( last
.textNode
, last
.offset
+ 1 );
189 * Reflect the latest changes from dom range.
191 updateFromDomRange: function( domRange
) {
193 walker
= new characterWalker( domRange
);
196 cursor
= walker
.next();
197 if ( cursor
.character
) this._
.cursors
.push( cursor
);
199 while ( cursor
.character
);
200 this._
.rangeLength
= this._
.cursors
.length
;
203 setMatched: function() {
204 this._
.isMatched
= true;
207 clearMatched: function() {
208 this._
.isMatched
= false;
211 isMatched: function() {
212 return this._
.isMatched
;
216 * Hightlight the current matched chunk of text.
218 highlight: function() {
219 // Do not apply if nothing is found.
220 if ( this._
.cursors
.length
< 1 )
223 // Remove the previous highlight if there's one.
224 if ( this._
.highlightRange
)
225 this.removeHighlight();
227 // Apply the highlight.
228 var range
= this.toDomRange(),
229 bookmark
= range
.createBookmark();
230 highlightStyle
.applyToRange( range
, editor
);
231 range
.moveToBookmark( bookmark
);
232 this._
.highlightRange
= range
;
234 // Scroll the editor to the highlighted area.
235 var element
= range
.startContainer
;
236 if ( element
.type
!= CKEDITOR
.NODE_ELEMENT
)
237 element
= element
.getParent();
238 element
.scrollIntoView();
240 // Update the character cursors.
241 this.updateFromDomRange( range
);
245 * Remove highlighted find result.
247 removeHighlight: function() {
248 if ( !this._
.highlightRange
)
251 var bookmark
= this._
.highlightRange
.createBookmark();
252 highlightStyle
.removeFromRange( this._
.highlightRange
, editor
);
253 this._
.highlightRange
.moveToBookmark( bookmark
);
254 this.updateFromDomRange( this._
.highlightRange
);
255 this._
.highlightRange
= null;
258 isReadOnly: function() {
259 if ( !this._
.highlightRange
)
262 return this._
.highlightRange
.startContainer
.isReadOnly();
265 moveBack: function() {
266 var retval
= this._
.walker
.back(),
267 cursors
= this._
.cursors
;
269 if ( retval
.hitMatchBoundary
)
270 this._
.cursors
= cursors
= [];
272 cursors
.unshift( retval
);
273 if ( cursors
.length
> this._
.rangeLength
)
279 moveNext: function() {
280 var retval
= this._
.walker
.next(),
281 cursors
= this._
.cursors
;
283 // Clear the cursors queue if we've crossed a match boundary.
284 if ( retval
.hitMatchBoundary
)
285 this._
.cursors
= cursors
= [];
287 cursors
.push( retval
);
288 if ( cursors
.length
> this._
.rangeLength
)
294 getEndCharacter: function() {
295 var cursors
= this._
.cursors
;
296 if ( cursors
.length
< 1 )
299 return cursors
[ cursors
.length
- 1 ].character
;
302 getNextCharacterRange: function( maxLength
) {
303 var lastCursor
, nextRangeWalker
,
304 cursors
= this._
.cursors
;
306 if ( ( lastCursor
= cursors
[ cursors
.length
- 1 ] ) && lastCursor
.textNode
)
307 nextRangeWalker
= new characterWalker( getRangeAfterCursor( lastCursor
) );
308 // In case it's an empty range (no cursors), figure out next range from walker (#4951).
310 nextRangeWalker
= this._
.walker
;
312 return new characterRange( nextRangeWalker
, maxLength
);
315 getCursors: function() {
316 return this._
.cursors
;
321 // The remaining document range after the character cursor.
322 function getRangeAfterCursor( cursor
, inclusive
) {
323 var range
= editor
.createRange();
324 range
.setStart( cursor
.textNode
, ( inclusive
? cursor
.offset : cursor
.offset
+ 1 ) );
325 range
.setEndAt( editor
.editable(), CKEDITOR
.POSITION_BEFORE_END
);
329 // The document range before the character cursor.
330 function getRangeBeforeCursor( cursor
) {
331 var range
= editor
.createRange();
332 range
.setStartAt( editor
.editable(), CKEDITOR
.POSITION_AFTER_START
);
333 range
.setEnd( cursor
.textNode
, cursor
.offset
);
341 // Examination the occurrence of a word which implement KMP algorithm.
342 var kmpMatcher = function( pattern
, ignoreCase
) {
343 var overlap
= [ -1 ];
345 pattern
= pattern
.toLowerCase();
346 for ( var i
= 0; i
< pattern
.length
; i
++ ) {
347 overlap
.push( overlap
[ i
] + 1 );
348 while ( overlap
[ i
+ 1 ] > 0 && pattern
.charAt( i
) != pattern
.charAt( overlap
[ i
+ 1 ] - 1 ) )
349 overlap
[ i
+ 1 ] = overlap
[ overlap
[ i
+ 1 ] - 1 ] + 1;
355 ignoreCase: !!ignoreCase
,
360 kmpMatcher
.prototype = {
361 feedCharacter: function( c
) {
362 if ( this._
.ignoreCase
)
366 if ( c
== this._
.pattern
.charAt( this._
.state
) ) {
368 if ( this._
.state
== this._
.pattern
.length
) {
373 } else if ( !this._
.state
) {
376 this._
.state
= this._
.overlap
[this._
.state
];
386 var wordSeparatorRegex
= /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/;
388 var isWordSeparator = function( c ) {
391 var code = c.charCodeAt( 0 );
392 return ( code >= 9 && code <= 0xd ) || ( code >= 0x2000 && code <= 0x200a ) || wordSeparatorRegex.test( c );
398 find: function( pattern, matchCase, matchWord, matchCyclic, highlightMatched, cyclicRerun ) {
399 if ( !this.matchRange )
400 this.matchRange = new characterRange( new characterWalker( this.searchRange ), pattern.length );
402 this.matchRange.removeHighlight();
403 this.matchRange = this.matchRange.getNextCharacterRange( pattern.length );
406 var matcher = new kmpMatcher( pattern, !matchCase ),
407 matchState = KMP_NOMATCH,
410 while ( character !== null ) {
411 this.matchRange.moveNext();
412 while ( ( character = this.matchRange.getEndCharacter() ) ) {
413 matchState = matcher.feedCharacter( character );
414 if ( matchState == KMP_MATCHED )
416 if ( this.matchRange.moveNext().hitMatchBoundary )
420 if ( matchState == KMP_MATCHED ) {
422 var cursors = this.matchRange.getCursors(),
423 tail = cursors[ cursors.length - 1 ],
426 var rangeBefore = getRangeBeforeCursor( head ),
427 rangeAfter = getRangeAfterCursor( tail );
429 // The word boundary checks requires to trim the text nodes. (#9036)
433 var headWalker = new characterWalker( rangeBefore, true ),
434 tailWalker = new characterWalker( rangeAfter, true );
436 if ( !( isWordSeparator( headWalker.back().character ) && isWordSeparator( tailWalker.next().character ) ) )
439 this.matchRange.setMatched();
440 if ( highlightMatched !== false )
441 this.matchRange.highlight();
446 this.matchRange.clearMatched();
447 this.matchRange.removeHighlight();
448 // Clear current session and restart with the default search
450 // Re-run the finding once for cyclic.(#3517)
451 if ( matchCyclic && !cyclicRerun ) {
452 this.searchRange = getSearchRange( 1 );
453 this.matchRange = null;
454 return arguments.callee.apply( this, Array.prototype.slice.call( arguments ).concat( [ true ] ) );
460 // Record how much replacement occurred toward one replacing.
463 replace: function( dialog, pattern, newString, matchCase, matchWord, matchCyclic, isReplaceAll ) {
466 // Successiveness of current replace/find.
469 // 1. Perform the replace when there's already a match here.
470 // 2. Otherwise perform the find but don't replace it immediately.
471 if ( this.matchRange && this.matchRange.isMatched() && !this.matchRange._.isReplaced && !this.matchRange.isReadOnly() ) {
472 // Turn off highlight for a while when saving snapshots.
473 this.matchRange.removeHighlight();
474 var domRange = this.matchRange.toDomRange();
475 var text = editor.document.createText( newString );
476 if ( !isReplaceAll ) {
477 // Save undo snaps before and after the replacement.
478 var selection = editor.getSelection();
479 selection.selectRanges( [ domRange ] );
480 editor.fire( 'saveSnapshot' );
482 domRange.deleteContents();
483 domRange.insertNode( text );
484 if ( !isReplaceAll ) {
485 selection.selectRanges( [ domRange ] );
486 editor.fire( 'saveSnapshot' );
488 this.matchRange.updateFromDomRange( domRange );
490 this.matchRange.highlight();
491 this.matchRange._.isReplaced = true;
492 this.replaceCounter++;
495 result = this.find( pattern, matchCase, matchWord, matchCyclic, !isReplaceAll );
504 // The range in which find/replace happened, receive from user
506 function getSearchRange( isDefault ) {
508 sel = editor.getSelection(),
509 range = sel.getRanges()[ 0 ],
510 editable = editor.editable();
512 // Blink browsers return empty array of ranges when editor is in read-only mode
513 // and it hasn't got focus, so instead of selection, we check for range itself. (#12848)
514 if ( range && !isDefault ) {
515 searchRange = range.clone();
516 searchRange.collapse( true );
518 searchRange = editor.createRange();
519 searchRange.setStartAt( editable, CKEDITOR.POSITION_AFTER_START );
521 searchRange.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );
525 var lang = editor.lang.find;
528 resizable: CKEDITOR.DIALOG_RESIZE_NONE,
532 // Close button only.
533 CKEDITOR.dialog.cancelButton( editor, {
534 label: editor.lang.common.close
544 widths: [ '230px', '90px' ],
548 label: lang.findWhat,
550 labelLayout: 'horizontal',
559 onClick: function() {
560 var dialog = this.getDialog();
562 dialog.getValueOf( 'find', 'txtFindFind' ),
563 dialog.getValueOf( 'find', 'txtFindCaseChk' ),
564 dialog.getValueOf( 'find', 'txtFindWordChk' ),
565 dialog.getValueOf( 'find', 'txtFindCyclic' )
567 alert( lang.notFoundMsg ); // jshint ignore:line
574 label: CKEDITOR.tools.htmlEncode( lang.findOptions ),
575 style: 'margin-top:29px',
581 id: 'txtFindCaseChk',
583 label: lang.matchCase
587 id: 'txtFindWordChk',
589 label: lang.matchWord
596 label: lang.matchCyclic
607 widths: [ '230px', '90px' ],
610 id: 'txtFindReplace',
611 label: lang.findWhat,
613 labelLayout: 'horizontal',
618 id: 'btnFindReplace',
622 onClick: function() {
623 var dialog = this.getDialog();
624 if ( !finder.replace(
626 dialog.getValueOf( 'replace', 'txtFindReplace' ),
627 dialog.getValueOf( 'replace', 'txtReplace' ),
628 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
629 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
630 dialog.getValueOf( 'replace', 'txtReplaceCyclic' )
632 alert( lang.notFoundMsg ); // jshint ignore:line
639 widths: [ '230px', '90px' ],
643 label: lang.replaceWith,
645 labelLayout: 'horizontal',
653 label: lang.replaceAll,
655 onClick: function() {
656 var dialog = this.getDialog();
658 finder.replaceCounter = 0;
660 // Scope to full document.
661 finder.searchRange = getSearchRange( 1 );
662 if ( finder.matchRange ) {
663 finder.matchRange.removeHighlight();
664 finder.matchRange = null;
666 editor.fire( 'saveSnapshot' );
667 while ( finder.replace(
669 dialog.getValueOf( 'replace', 'txtFindReplace' ),
670 dialog.getValueOf( 'replace', 'txtReplace' ),
671 dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ),
672 dialog.getValueOf( 'replace', 'txtReplaceWordChk' ),
679 if ( finder.replaceCounter ) {
680 alert( lang.replaceSuccessMsg.replace( /%1/, finder.replaceCounter ) ); // jshint ignore:line
681 editor.fire( 'saveSnapshot' );
683 alert( lang.notFoundMsg ); // jshint ignore:line
690 label: CKEDITOR.tools.htmlEncode( lang.findOptions ),
696 id: 'txtReplaceCaseChk',
698 label: lang.matchCase
702 id: 'txtReplaceWordChk',
704 label: lang.matchWord
708 id: 'txtReplaceCyclic',
711 label: lang.matchCyclic
719 // Keep track of the current pattern field in use.
720 var patternField, wholeWordChkField;
722 // Ignore initial page select on dialog show
723 var isUserSelect = 0;
724 this.on( 'hide', function() {
727 this.on( 'show', function() {
731 this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc ) {
732 return function( pageId ) {
733 originalFunc.call( dialog, pageId );
735 var currPage = dialog._.tabs[ pageId ];
736 var patternFieldInput, patternFieldId, wholeWordChkFieldId;
737 patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace';
738 wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk';
740 patternField = dialog.getContentElement( pageId, patternFieldId );
741 wholeWordChkField = dialog.getContentElement( pageId, wholeWordChkFieldId );
743 // Prepare for check pattern text filed 'keyup' event
744 if ( !currPage.initialized ) {
745 patternFieldInput = CKEDITOR.document.getById( patternField._.inputId );
746 currPage.initialized = true;
749 // Synchronize fields on tab switch.
751 syncFieldsBetweenTabs.call( this, pageId );
757 // Establish initial searching start position.
758 finder.searchRange = getSearchRange();
760 // Fill in the find field with selected text.
761 var selectedText = this.getParentEditor().getSelection().getSelectedText(),
762 patternFieldId = ( startupPage == 'find' ? 'txtFindFind' : 'txtFindReplace' );
764 var field = this.getContentElement( startupPage, patternFieldId );
765 field.setValue( selectedText );
768 this.selectPage( startupPage );
770 this[ ( startupPage == 'find' && this._.editor.readOnly ? 'hide' : 'show' ) + 'Page' ]( 'replace' );
774 if ( finder.matchRange && finder.matchRange.isMatched() ) {
775 finder.matchRange.removeHighlight();
778 range = finder.matchRange.toDomRange();
780 editor.getSelection().selectRanges( [ range ] );
783 // Clear current session before dialog close
784 delete finder.matchRange;
786 onFocus: function() {
787 if ( startupPage == 'replace' )
788 return this.getContentElement( 'replace', 'txtFindReplace' );
790 return this.getContentElement( 'find', 'txtFindFind' );
795 CKEDITOR.dialog.add( 'find', function( editor ) {
796 return findDialog( editor, 'find' );
799 CKEDITOR.dialog.add( 'replace', function( editor ) {
800 return findDialog( editor, 'replace' );