Index: /CKEditor/trunk/_source/core/config.js =================================================================== --- /CKEditor/trunk/_source/core/config.js (revision 3091) +++ /CKEditor/trunk/_source/core/config.js (revision 3092) @@ -148,5 +148,5 @@ */ - plugins : 'basicstyles,blockquote,button,clipboard,elementspath,horizontalrule,htmldataprocessor,image,indent,justify,keystrokes,link,list,newpage,pagebreak,pastefromword,pastetext,preview,print,removeformat,smiley,sourcearea,table,specialchar,tab,templates,toolbar,undo,wysiwygarea', + plugins : 'basicstyles,blockquote,button,clipboard,elementspath,find,horizontalrule,htmldataprocessor,image,indent,justify,keystrokes,link,list,newpage,pagebreak,pastefromword,pastetext,preview,print,removeformat,smiley,sourcearea,table,specialchar,tab,templates,toolbar,undo,wysiwygarea', /** Index: /CKEditor/trunk/_source/core/dom/document.js =================================================================== --- /CKEditor/trunk/_source/core/dom/document.js (revision 3091) +++ /CKEditor/trunk/_source/core/dom/document.js (revision 3092) @@ -71,5 +71,5 @@ createText : function( text ) { - return new CKEDITOR.dom.text( '', this ); + return new CKEDITOR.dom.text( text, this ); }, Index: /CKEditor/trunk/_source/core/dom/element.js =================================================================== --- /CKEditor/trunk/_source/core/dom/element.js (revision 3091) +++ /CKEditor/trunk/_source/core/dom/element.js (revision 3092) @@ -974,4 +974,84 @@ } } + }, + + getPositionedAncestor : function() + { + var current = this; + while ( current.getName() != 'html' ) + { + if ( current.getComputedStyle( 'position' ) != 'static' ) + return current; + + current = current.getParent(); + } + return null; + }, + + getDocumentPosition : function() + { + var x = 0, y = 0, current = this, previous = null; + while ( current && !( current.getName() == 'body' || current.getName() == 'html' ) ) + { + x += current.$.offsetLeft - current.$.scrollLeft; + y += current.$.offsetTop - current.$.scrollTop; + + if ( !CKEDITOR.env.opera ) + { + var scrollElement = previous; + while ( scrollElement && !scrollElement.equals( current ) ) + { + x -= scrollElement.$.scrollLeft; + y -= scrollElement.$.scrollTop; + scrollElement = scrollElement.getParent(); + } } + + previous = current; + current = new CKEDITOR.dom.element( current.$.offsetParent ); + } + + // document.body is a special case when it comes to offsetTop and offsetLeft + // values. + // 1. It matters if document.body itself is a positioned element; + // 2. It matters when we're in IE and the element has no positioned ancestor. + // Otherwise the values should be ignored. + if ( this.getComputedStyle( 'position' ) != 'static' || ( CKEDITOR.env.ie && this.getPositionedAncestor() == null ) ) + { + x += this.getDocument().getBody().$.offsetLeft; + y += this.getDocument().getBody().$.offsetTop; + } + + return { x : x, y : y }; + }, + + scrollIntoView : function( alignTop ) + { + // Get the element window. + var win = this.getWindow(), + winHeight = win.getViewPaneSize().height; + + // Starts from the offset that will be scrolled with the negative value of + // the visible window height. + var offset = winHeight * -1; + + // Append the height if we are about the align the bottom. + if ( !alignTop ) + { + offset += this.$.offsetHeight || 0; + + // Consider the margin in the scroll, which is ok for our current needs, but + // needs investigation if we will be using this function in other places. + offset += parseInt( this.getComputedStyle( 'marginBottom' ) || 0, 10 ) || 0; + } + + // Append the offsets for the entire element hierarchy. + var elementPosition = this.getDocumentPosition(); + offset += elementPosition.y; + + // Scroll the window to the desired position, if not already visible. + var currentScroll = win.getScrollPosition().y; + if ( offset > 0 && ( offset > currentScroll || offset < currentScroll - winHeight ) ) + win.$.scrollTo( 0, offset ); + } }); Index: /CKEditor/trunk/_source/core/dom/range.js =================================================================== --- /CKEditor/trunk/_source/core/dom/range.js (revision 3091) +++ /CKEditor/trunk/_source/core/dom/range.js (revision 3092) @@ -467,5 +467,6 @@ // In this case, move the start information to that text // node. - if ( child && child.type == CKEDITOR.NODE_TEXT && child.getPrevious().type == CKEDITOR.NODE_TEXT ) + if ( child && child.type == CKEDITOR.NODE_TEXT + && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT ) { startContainer = child; @@ -494,5 +495,6 @@ // In this case, move the start information to that // text node. - if ( child && child.type == CKEDITOR.NODE_TEXT && child.getPrevious().type == CKEDITOR.NODE_TEXT ) + if ( child && child.type == CKEDITOR.NODE_TEXT + && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT ) { endContainer = child; Index: /CKEditor/trunk/_source/core/dom/text.js =================================================================== --- /CKEditor/trunk/_source/core/dom/text.js (revision 3091) +++ /CKEditor/trunk/_source/core/dom/text.js (revision 3092) @@ -80,4 +80,14 @@ split : function( offset ) { + // If the offset is after the last char, IE creates the text node + // on split, but don't include it into the DOM. So, we have to do + // that manually here. + if ( CKEDITOR.env.ie && offset == this.getLength() ) + { + var next = this.getDocument().createText( '' ); + next.insertAfter( this ); + return next; + } + return new CKEDITOR.dom.text( this.$.splitText( offset ) ); }, Index: /CKEditor/trunk/_source/lang/en.js =================================================================== --- /CKEditor/trunk/_source/lang/en.js (revision 3091) +++ /CKEditor/trunk/_source/lang/en.js (revision 3092) @@ -177,5 +177,6 @@ matchWord : 'Match whole word', matchCyclic : 'Match cyclic', - replaceAll : 'Replace All' + replaceAll : 'Replace All', + replaceSuccessMsg : '%1 occurrence(s) replaced.' }, Index: /CKEditor/trunk/_source/plugins/find/dialogs/find.js =================================================================== --- /CKEditor/trunk/_source/plugins/find/dialogs/find.js (revision 3092) +++ /CKEditor/trunk/_source/plugins/find/dialogs/find.js (revision 3092) @@ -0,0 +1,815 @@ +/* +Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. +For licensing, see LICENSE.html or http://ckeditor.com/license +*/ + +(function() +{ + // Element tag names which prevent characters counting. + var characterBoundaryElementsEnum = + { + address :1, blockquote :1, dl :1, h1 :1, h2 :1, h3 :1, + h4 :1, h5 :1, h6 :1, p :1, pre :1, li :1, dt :1, de :1, div :1, td:1, th:1 + }; + + var guardDomWalkerNonEmptyTextNode = function( evt ) + { + if ( evt.data.to && evt.data.to.type == CKEDITOR.NODE_TEXT + && evt.data.to.$.length > 0 ) + this.stop(); + CKEDITOR.dom.domWalker.blockBoundary( { br : 1 } ).call( this, evt ); + }; + + + /** + * Get the cursor object which represent both current character and it's dom + * position thing. + */ + var cursorStep = function() + { + var obj = { + textNode : this.textNode, + offset : this.offset, + character : this.textNode ? this.textNode.getText().charAt( this.offset ) : null, + hitMatchBoundary : this._.matchBoundary + }; + return obj; + }; + + var pages = [ 'find', 'replace' ], + fieldsMapping = [ + [ 'txtFindFind', 'txtFindReplace' ], + [ 'txtFindCaseChk', 'txtReplaceCaseChk' ], + [ 'txtFindWordChk', 'txtReplaceWordChk' ], + [ 'txtFindCyclic', 'txtReplaceCyclic' ] ]; + + /** + * Synchronize corresponding filed values between 'replace' and 'find' pages. + * @param {String} currentPageId The page id which receive values. + */ + function syncFieldsBetweenTabs( currentPageId ) + { + var sourceIndex, targetIndex, + sourceField, targetField; + + sourceIndex = currentPageId === 'find' ? 1 : 0; + targetIndex = 1 - sourceIndex; + var i, l = fieldsMapping.length; + for ( i = 0 ; i < l ; i++ ) + { + var sourceField = this.getContentElement( pages[ sourceIndex ], + fieldsMapping[ i ][ sourceIndex ] ); + var targetField = this.getContentElement( pages[ targetIndex ], + fieldsMapping[ i ][ targetIndex ] ); + + targetField.setValue( sourceField.getValue() ); + } + } + + var findDialog = function( editor, startupPage ) + { + // Style object for highlights. + var highlightStyle = new CKEDITOR.style( editor.config.find_highlight ); + + /** + * Iterator which walk through document char by char. + * @param {Object} start + * @param {Number} offset + */ + var characterWalker = function( start, offset ) + { + var isCursor = typeof( start.textNode ) !== 'undefined'; + this.textNode = isCursor ? start.textNode : start; + this.offset = isCursor ? start.offset : offset; + this._ = { + walker : new CKEDITOR.dom.domWalker( this.textNode ), + matchBoundary : false + }; + }; + + characterWalker.prototype = { + next : function() + { + // Already at the end of document, no more character available. + if( this.textNode == null ) + return cursorStep.call( this ); + + this._.matchBoundary = false; + + // If there are more characters in the text node, get it and + // raise an event. + if( this.textNode.type == CKEDITOR.NODE_TEXT + && this.offset < this.textNode.getLength() - 1 ) + { + this.offset++; + return cursorStep.call( this ); + } + + // If we are at the end of the text node, use dom walker to get + // the next text node. + var data = null; + while ( !data || ( data.node && data.node.type != + CKEDITOR.NODE_TEXT ) ) + { + data = this._.walker.forward( + guardDomWalkerNonEmptyTextNode ); + + // Block boundary? BR? Document boundary? + if ( !data.node + || ( data.node.type !== CKEDITOR.NODE_TEXT + && data.node.getName() in + characterBoundaryElementsEnum ) ) + this._.matchBoundary = true; + } + this.textNode = data.node; + this.offset = 0; + return cursorStep.call( this ); + }, + + back : function() + { + this._.matchBoundary = false; + + // More characters -> decrement offset and return. + if ( this.textNode.type == CKEDITOR.NODE_TEXT && this.offset > 0 ) + { + this.offset--; + return cursorStep.call( this ); + } + + // Start of text node -> use dom walker to get the previous text node. + var data = null; + while ( !data + || ( data.node && data.node.type != CKEDITOR.NODE_TEXT ) ) + { + data = this._.walker.reverse( guardDomWalkerNonEmptyTextNode ); + + // Block boundary? BR? Document boundary? + if ( !data.node || ( data.node.type !== CKEDITOR.NODE_TEXT && + data.node.getName() in characterBoundaryElementsEnum ) ) + this._.matchBoundary = true; + } + this.textNode = data.node; + this.offset = data.node.length - 1; + return cursorStep.call( this ); + } + }; + + /** + * A range of cursors which represent a trunk of characters which try to + * match, it has the same length as the pattern string. + */ + var characterRange = function( characterWalker, rangeLength ) + { + this._ = { + walker : characterWalker, + cursors : [], + rangeLength : rangeLength, + highlightRange : null, + isMatched : false + }; + }; + + characterRange.prototype = { + /** + * Translate this range to {@link CKEDITOR.dom.range} + */ + toDomRange : function() + { + var cursors = this._.cursors; + if ( cursors.length < 1 ) + return null; + + var first = cursors[0], + last = cursors[ cursors.length - 1 ], + range = new CKEDITOR.dom.range( editor.document ); + + range.setStart( first.textNode, first.offset ); + range.setEnd( last.textNode, last.offset + 1 ); + return range; + }, + + updateFromDomRange : function( domRange ) + { + var startNode = domRange.startContainer, + startIndex = domRange.startOffset, + endNode = domRange.endContainer, + endIndex = domRange.endOffset, + boundaryNodes = domRange.getBoundaryNodes(); + + if ( startNode.type != CKEDITOR.NODE_TEXT ) + { + startNode = boundaryNodes.startNode; + while ( startNode.type != CKEDITOR.NODE_TEXT ) + startNode = startNode.getFirst(); + startIndex = 0; + } + + if ( endNode.type != CKEDITOR.NODE_TEXT ) + { + endNode = boundaryNodes.endNode; + while ( endNode.type != CKEDITOR.NODE_TEXT ) + endNode = endNode.getLast(); + endIndex = endNode.getLength(); + } + + // If the endNode is an empty text node, our walker would just walk through + // it without stopping. So need to backtrack to the nearest non-emtpy text + // node. + if ( endNode.getLength() < 1 ) + { + while ( ( endNode = endNode.getPreviousSourceNode() ) && !( endNode.type == CKEDITOR.NODE_TEXT && endNode.getLength() > 0 ) ); + endIndex = endNode.getLength(); + } + + var cursor = new characterWalker( startNode, startIndex ); + this._.cursors = [ cursor ]; + if ( !( cursor.textNode.equals( endNode ) && cursor.offset == endIndex - 1 ) ) + { + do + { + cursor = new characterWalker( cursor ); + cursor.next(); + this._.cursors.push( cursor ); + } + while ( !( cursor.textNode.equals( endNode ) && cursor.offset == endIndex - 1 ) ); + } + + this._.rangeLength = this._.cursors.length; + }, + + setMatched : function() + { + this._.isMatched = true; + this.highlight(); + }, + + clearMatched : function() + { + this._.isMatched = false; + this.removeHighlight(); + }, + + isMatched : function() + { + return this._.isMatched; + }, + + /** + * Hightlight the current matched chunk of text. + */ + highlight : function() + { + // Do not apply if nothing is found. + if ( this._.cursors.length < 1 ) + return; + + // Remove the previous highlight if there's one. + if ( this._.highlightRange ) + this.removeHighlight(); + + // Apply the highlight. + var range = this.toDomRange(); + highlightStyle.applyToRange( range ); + this._.highlightRange = range; + + // Scroll the editor to the highlighted area. + var element = range.startContainer; + if ( element.type != CKEDITOR.NODE_ELEMENT ) + element = element.getParent(); + element.scrollIntoView(); + + // Update the character cursors. + this.updateFromDomRange( range ); + }, + + /** + * Remove highlighted find result. + */ + removeHighlight : function() + { + if ( this._.highlightRange == null ) + return; + + highlightStyle.removeFromRange( this._.highlightRange ); + this.updateFromDomRange( this._.highlightRange ); + this._.highlightRange = null; + }, + + moveBack : function() + { + var retval = this._.walker.back(), + cursors = this._.cursors; + + if ( retval.hitMatchBoundary ) + this._.cursors = cursors = []; + + cursors.unshift( retval ); + if ( cursors.length > this._.rangeLength ) + cursors.pop(); + + return retval; + }, + + moveNext : function() + { + var retval = this._.walker.next(), + cursors = this._.cursors; + + // Clear the cursors queue if we've crossed a match boundary. + if ( retval.hitMatchBoundary ) + this._.cursors = cursors = []; + + cursors.push( retval ); + if ( cursors.length > this._.rangeLength ) + cursors.shift(); + + return retval; + }, + + getEndCharacter : function() + { + var cursors = this._.cursors; + if ( cursors.length < 1 ) + return null; + + return cursors[ cursors.length - 1 ].character; + }, + + getNextRange : function( maxLength ) + { + var cursors = this._.cursors; + if ( cursors.length < 1 ) + return null; + + var next = new characterWalker( cursors[ cursors.length - 1 ] ); + return new characterRange( next, maxLength ); + }, + + getCursors : function() + { + return this._.cursors; + } + }; + + var KMP_NOMATCH = 0, + KMP_ADVANCED = 1, + KMP_MATCHED = 2; + /** + * Examination the occurrence of a word which implement KMP algorithm. + */ + var kmpMatcher = function( pattern, ignoreCase ) + { + var overlap = [ -1 ]; + if ( ignoreCase ) + pattern = pattern.toLowerCase(); + for ( var i = 0 ; i < pattern.length ; i++ ) + { + overlap.push( overlap[i] + 1 ); + while ( overlap[ i + 1 ] > 0 + && pattern.charAt( i ) != pattern + .charAt( overlap[ i + 1 ] - 1 ) ) + overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1; + } + + this._ = { + overlap : overlap, + state : 0, + ignoreCase : !!ignoreCase, + pattern : pattern + }; + }; + + kmpMatcher.prototype = + { + feedCharacter : function( c ) + { + if ( this._.ignoreCase ) + c = c.toLowerCase(); + + while ( true ) + { + if ( c == this._.pattern.charAt( this._.state ) ) + { + this._.state++; + if ( this._.state == this._.pattern.length ) + { + this._.state = 0; + return KMP_MATCHED; + } + return KMP_ADVANCED; + } + else if ( this._.state == 0 ) + return KMP_NOMATCH; + else + this._.state = this._.overlap[ this._.state ]; + } + + return null; + }, + + reset : function() + { + this._.state = 0; + } + }; + + var wordSeparatorRegex = + /[.,"'?!;: \u0085\u00a0\u1680\u280e\u2028\u2029\u202f\u205f\u3000]/; + + var isWordSeparator = function( c ) + { + if ( !c ) + return true; + var code = c.charCodeAt( 0 ); + return ( code >= 9 && code <= 0xd ) + || ( code >= 0x2000 && code <= 0x200a ) + || wordSeparatorRegex.test( c ); + }; + + var finder = { + startCursor : null, + range : null, + find : function( pattern, matchCase, matchWord, matchCyclic ) + { + if( !this.range ) + this.range = new characterRange( new characterWalker( this.startCursor ), pattern.length ); + else + { + this.range.removeHighlight(); + this.range = this.range.getNextRange( pattern.length ); + } + + var matcher = new kmpMatcher( pattern, !matchCase ), + matchState = KMP_NOMATCH, + character = '%'; + + while ( character != null ) + { + this.range.moveNext(); + while ( ( character = this.range.getEndCharacter() ) ) + { + matchState = matcher.feedCharacter( character ); + if ( matchState == KMP_MATCHED ) + break; + if ( this.range.moveNext().hitMatchBoundary ) + matcher.reset(); + } + + if ( matchState == KMP_MATCHED ) + { + if ( matchWord ) + { + var cursors = this.range.getCursors(), + tail = cursors[ cursors.length - 1 ], + head = cursors[ 0 ], + headWalker = new characterWalker( head ), + tailWalker = new characterWalker( tail ); + + if ( ! ( isWordSeparator( + headWalker.back().character ) + && isWordSeparator( + tailWalker.next().character ) ) ) + continue; + } + + this.range.setMatched(); + return true; + } + } + + this.range.clearMatched(); + + // clear current session and restart from beginning + if ( matchCyclic ) + this.range = null; + + return false; + }, + + /** + * Record how much replacement occurred toward one replacing. + */ + replaceCounter : 0, + + replace : function( dialog, pattern, newString, matchCase, matchWord, + matchCyclic, matchReplaceAll ) + { + var replaceResult = false; + if ( this.range && this.range.isMatched() ) + { + var domRange = this.range.toDomRange(); + var text = editor.document.createText( newString ); + domRange.deleteContents(); + domRange.insertNode( text ); + this.range.updateFromDomRange( domRange ); + + this.replaceCounter++; + replaceResult = true; + } + + var findResult = this.find( pattern, matchCase, matchWord, matchCyclic ); + if ( findResult && matchReplaceAll ) + this.replace.apply( this, Array.prototype.slice.call( arguments ) ); + return matchReplaceAll ? + this.replaceCounter : replaceResult || findResult; + } + }; + + /** + * Get the default cursor which is the start of this document. + */ + function getDefaultStartCursor() + { + return { textNode : editor.document.getBody(), offset: 0 }; + } + + /** + * Get cursor that indicate search begin with, receive from user + * selection prior. + */ + function getStartCursor() + { + if ( CKEDITOR.env.ie ) + this.restoreSelection(); + + var sel = editor.getSelection(); + if ( sel ) + { + var lastRange = sel.getRanges()[ sel.getRanges().length - 1 ]; + return { + textNode : lastRange.getBoundaryNodes().endNode, + offset : lastRange.endContainer.type === + CKEDITOR.NODE_ELEMENT ? + 0 : lastRange.endOffset + }; + } + else + return getDefaultStartCursor(); + } + + return { + title : editor.lang.findAndReplace.title, + resizable : CKEDITOR.DIALOG_RESIZE_NONE, + minWidth : 400, + minHeight : 255, + buttons : [ CKEDITOR.dialog.cancelButton ], //Cancel button only. + contents : [ + { + id : 'find', + label : editor.lang.findAndReplace.find, + title : editor.lang.findAndReplace.find, + accessKey : '', + elements : [ + { + type : 'hbox', + widths : [ '230px', '90px' ], + children : + [ + { + type : 'text', + id : 'txtFindFind', + label : editor.lang.findAndReplace.findWhat, + isChanged : false, + labelLayout : 'horizontal', + accessKey : 'F' + }, + { + type : 'button', + align : 'left', + style : 'width:100%', + label : editor.lang.findAndReplace.find, + onClick : function() + { + var dialog = this.getDialog(); + if ( !finder.find( dialog.getValueOf( 'find', 'txtFindFind' ), + dialog.getValueOf( 'find', 'txtFindCaseChk' ), + dialog.getValueOf( 'find', 'txtFindWordChk' ), + dialog.getValueOf( 'find', 'txtFindCyclic' ) ) ) + alert( editor.lang.findAndReplace + .notFoundMsg ); + } + } + ] + }, + { + type : 'vbox', + padding : 0, + children : + [ + { + type : 'checkbox', + id : 'txtFindCaseChk', + isChanged : false, + style : 'margin-top:28px', + label : editor.lang.findAndReplace.matchCase + }, + { + type : 'checkbox', + id : 'txtFindWordChk', + isChanged : false, + label : editor.lang.findAndReplace.matchWord + }, + { + type : 'checkbox', + id : 'txtFindCyclic', + isChanged : false, + 'default' : true, + label : editor.lang.findAndReplace.matchCyclic + } + ] + } + ] + }, + { + id : 'replace', + label : editor.lang.findAndReplace.replace, + accessKey : 'M', + elements : [ + { + type : 'hbox', + widths : [ '230px', '90px' ], + children : + [ + { + type : 'text', + id : 'txtFindReplace', + label : editor.lang.findAndReplace.findWhat, + isChanged : false, + labelLayout : 'horizontal', + accessKey : 'F' + }, + { + type : 'button', + align : 'left', + style : 'width:100%', + label : editor.lang.findAndReplace.replace, + onClick : function() + { + var dialog = this.getDialog(); + if ( !finder.replace( dialog, + dialog.getValueOf( 'replace', 'txtFindReplace' ), + dialog.getValueOf( 'replace', 'txtReplace' ), + dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ), + dialog.getValueOf( 'replace', 'txtReplaceWordChk' ), + dialog.getValueOf( 'replace', 'txtReplaceCyclic' ) ) ) + alert( editor.lang.findAndReplace + .notFoundMsg ); + } + } + ] + }, + { + type : 'hbox', + widths : [ '230px', '90px' ], + children : + [ + { + type : 'text', + id : 'txtReplace', + label : editor.lang.findAndReplace.replaceWith, + isChanged : false, + labelLayout : 'horizontal', + accessKey : 'R' + }, + { + type : 'button', + align : 'left', + style : 'width:100%', + label : editor.lang.findAndReplace.replaceAll, + isChanged : false, + onClick : function() + { + var dialog = this.getDialog(); + var replaceNums; + + finder.replaceCounter = 0; + if ( ( replaceNums = finder.replace( dialog, + dialog.getValueOf( 'replace', 'txtFindReplace' ), + dialog.getValueOf( 'replace', 'txtReplace' ), + dialog.getValueOf( 'replace', 'txtReplaceCaseChk' ), + dialog.getValueOf( 'replace', 'txtReplaceWordChk' ), + dialog.getValueOf( 'replace', 'txtReplaceCyclic' ), true ) ) ) + alert( editor.lang.findAndReplace.replaceSuccessMsg.replace( /%1/, replaceNums ) ); + else + alert( editor.lang.findAndReplace.notFoundMsg ); + } + } + ] + }, + { + type : 'vbox', + padding : 0, + children : + [ + { + type : 'checkbox', + id : 'txtReplaceCaseChk', + isChanged : false, + label : editor.lang.findAndReplace + .matchCase + }, + { + type : 'checkbox', + id : 'txtReplaceWordChk', + isChanged : false, + label : editor.lang.findAndReplace + .matchWord + }, + { + type : 'checkbox', + id : 'txtReplaceCyclic', + isChanged : false, + 'default' : true, + label : editor.lang.findAndReplace + .matchCyclic + } + ] + } + ] + } + ], + onLoad : function() + { + var dialog = this; + + //keep track of the current pattern field in use. + var patternField, wholeWordChkField; + + //Ignore initial page select on dialog show + var isUserSelect = false; + this.on('hide', function() + { + isUserSelect = false; + } ); + this.on('show', function() + { + isUserSelect = true; + } ); + + this.selectPage = CKEDITOR.tools.override( this.selectPage, function( originalFunc ) + { + return function( pageId ) + { + originalFunc.call( dialog, pageId ); + + var currPage = dialog._.tabs[ pageId ]; + var patternFieldInput, patternFieldId, wholeWordChkFieldId; + patternFieldId = pageId === 'find' ? 'txtFindFind' : 'txtFindReplace'; + wholeWordChkFieldId = pageId === 'find' ? 'txtFindWordChk' : 'txtReplaceWordChk'; + + patternField = dialog.getContentElement( pageId, + patternFieldId ); + wholeWordChkField = dialog.getContentElement( pageId, + wholeWordChkFieldId ); + + // prepare for check pattern text filed 'keyup' event + if ( !currPage.initialized ) + { + patternFieldInput = CKEDITOR.document + .getById( patternField._.inputId ); + currPage.initialized = true; + } + + if( isUserSelect ) + // synchronize fields on tab switch. + syncFieldsBetweenTabs.call( this, pageId ); + }; + } ); + + }, + onShow : function() + { + // Establish initial searching start position. + finder.startCursor = getStartCursor.call( this ); + + if ( startupPage == 'replace' ) + this.getContentElement( 'replace', 'txtFindReplace' ).focus(); + else + this.getContentElement( 'find', 'txtFindFind' ).focus(); + }, + onHide : function() + { + if ( finder.range && finder.range.isMatched() ) + { + finder.range.removeHighlight(); + editor.getSelection().selectRanges( + [ finder.range.toDomRange() ] ); + } + + // Clear current session before dialog close + delete finder.range; + } + }; + }; + + CKEDITOR.dialog.add( 'find', function( editor ){ + return findDialog( editor, 'find' ) + } + ); + + CKEDITOR.dialog.add( 'replace', function( editor ){ + return findDialog( editor, 'replace' ) + } + ); +})(); Index: /CKEditor/trunk/_source/plugins/find/plugin.js =================================================================== --- /CKEditor/trunk/_source/plugins/find/plugin.js (revision 3092) +++ /CKEditor/trunk/_source/plugins/find/plugin.js (revision 3092) @@ -0,0 +1,33 @@ +/* +Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. +For licensing, see LICENSE.html or http://ckeditor.com/license +*/ + +CKEDITOR.plugins.add( 'find', +{ + init : function( editor ) + { + var forms = CKEDITOR.plugins.find; + editor.ui.addButton( 'Find', + { + label : editor.lang.findAndReplace.find, + command : 'find' + }); + editor.addCommand( 'find', new CKEDITOR.dialogCommand( 'find' ) ); + + editor.ui.addButton( 'Replace', + { + label : editor.lang.findAndReplace.replace, + command : 'replace' + }); + editor.addCommand( 'replace', new CKEDITOR.dialogCommand( 'replace' ) ); + + CKEDITOR.dialog.add( 'find', this.path + 'dialogs/find.js' ); + CKEDITOR.dialog.add( 'replace', this.path + 'dialogs/find.js' ); + }, + + requires : [ 'styles' ] +} ); + +// Styles for highlighting search results. +CKEDITOR.config.find_highlight = { element : 'span', styles : { 'background-color' : '#004', 'color' : '#fff' } }; Index: /CKEditor/trunk/_source/plugins/styles/plugin.js =================================================================== --- /CKEditor/trunk/_source/plugins/styles/plugin.js (revision 3091) +++ /CKEditor/trunk/_source/plugins/styles/plugin.js (revision 3092) @@ -99,18 +99,29 @@ }; + var applyStyle = function( document, remove ) + { + // Get all ranges from the selection. + var selection = document.getSelection(); + var ranges = selection.getRanges(); + var func = remove ? this.removeFromRange : this.applyToRange; + + // Apply the style to the ranges. + for ( var i = 0 ; i < ranges.length ; i++ ) + func.call( this, ranges[ i ] ); + + // Select the ranges again. + selection.selectRanges( ranges ); + }; + CKEDITOR.style.prototype = { apply : function( document ) { - // Get all ranges from the selection. - var selection = document.getSelection(); - var ranges = selection.getRanges(); - - // Apply the style to the ranges. - for ( var i = 0 ; i < ranges.length ; i++ ) - this.applyToRange( ranges[ i ] ); - - // Select the ranges again. - selection.selectRanges( ranges ); + applyStyle.call( this, document, false ); + }, + + remove : function( document ) + { + applyStyle.call( this, document, true ); }, @@ -122,4 +133,12 @@ : this.type == CKEDITOR.STYLE_BLOCK ? applyBlockStyle + : null ).call( this, range ); + }, + + removeFromRange : function( range ) + { + return ( this.removeFromRange = + this.type == CKEDITOR.STYLE_INLINE ? + removeInlineStyle : null ).call( this, range ); }, @@ -360,5 +379,5 @@ // Here we do some cleanup, removing all duplicated // elements from the style element. - removeFromElement( this, styleNode ); + removeFromInsideElement( this, styleNode ); // Insert it into the range position (it is collapsed after @@ -390,10 +409,107 @@ }; + var removeInlineStyle = function( range ) + { + /* + * Make sure our range has included all "collpased" parent inline nodes so + * that our operation logic can be simpler. + */ + range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); + + var bookmark = range.createBookmark( true ), + startNode = range.document.getById( bookmark.startNode ), + startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ); + + if ( range.collapsed ) + { + /* + * If the range is collapsed, try to remove the style from all ancestor + * elements, until either a block boundary is reached, or the style is + * removed. + */ + for ( var i = 0, element ; i < startPath.elements.length && ( element = startPath.elements[i] ) ; i++ ) + { + if ( element == startPath.block || element == startPath.blockLimit ) + break; + + if ( this.checkElementRemovable( element ) ) + { + removeFromElement( this, element ); + break; + } + } + } + else + { + /* + * Now our range isn't collapsed. Lets walk from the start node to the end + * node via DFS and remove the styles one-by-one. + */ + var endNode = range.document.getById( bookmark.endNode ), + endPath = new CKEDITOR.dom.elementPath( endNode.getParent() ); + currentNode = startNode; + + // Find out the ancestor that needs to be broken down at startNode and endNode. + var breakStart = null, breakEnd = null; + for ( var i = 0 ; i < startPath.elements.length ; i++ ) + { + if ( this.checkElementRemovable( startPath.elements[ i ] ) ) + { + breakStart = startPath.elements[ i ]; + break; + } + } + for ( var i = 0 ; i < endPath.elements.length ; i++ ) + { + if ( this.checkElementRemovable( endPath.elements[ i ] ) ) + { + breakEnd = endPath.elements[ i ]; + break; + } + } + + if ( breakEnd ) + endNode.breakParent( breakEnd ); + if ( breakStart ) + startNode.breakParent( breakStart ); + + // Now, do the DFS walk. + while ( ( currentNode = currentNode.getNextSourceNode() ) && !currentNode.equals( endNode ) ) + { + if ( currentNode.type == CKEDITOR.NODE_ELEMENT ) + removeFromElement( this, currentNode ); + } + } + + range.moveToBookmark( bookmark ); + }; + var applyBlockStyle = function( range ) { }; + // Removes a style from an element itself, don't care about its subtree. + var removeFromElement = function( style, element ) + { + var def = style._.definition, + attributes = def.attributes, + styles = def.styles; + + for ( var attName in attributes ) + { + // The 'class' element value must match (#1318). + if ( attName == 'class' && element.getAttribute( attName ) != attributes[ attName ] ) + continue; + element.removeAttribute( attName ); + } + + for ( var styleName in styles ) + element.removeStyle( styleName ); + + removeNoAttribsElement( element ); + }; + // Removes a style from inside an element. - var removeFromElement = function( style, element ) + var removeFromInsideElement = function( style, element ) { var def = style._.definition; @@ -404,23 +520,5 @@ for ( var i = innerElements.count() ; --i >= 0 ; ) - { - var innerElement = innerElements.getItem( i ); - - for ( var attName in attribs ) - { - // The 'class' element value must match (#1318). - if ( attName == 'class' && innerElement.getAttribute( 'class' ) != attribs[ attName ] ) - continue; - - innerElement.removeAttribute( attName ); - } - - for ( var styleName in styles ) - { - innerElement.removeStyle( styleName ); - } - - removeNoAttribsElement( innerElement ); - } + removeFromElement( style, innerElements.getItem( i ) ); }; @@ -597,5 +695,10 @@ if ( doc ) - this.style.apply( doc ); + { + if ( this.state == CKEDITOR.TRISTATE_OFF ) + this.style.apply( doc ); + else if ( this.state == CKEDITOR.TRISTATE_ON ) + this.style.remove( doc ); + } return !!doc; Index: /CKEditor/trunk/_source/plugins/toolbar/plugin.js =================================================================== --- /CKEditor/trunk/_source/plugins/toolbar/plugin.js (revision 3091) +++ /CKEditor/trunk/_source/plugins/toolbar/plugin.js (revision 3092) @@ -211,4 +211,5 @@ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo', '-', + 'Find', 'Replace', '-', 'Bold', 'Italic', 'Underline', 'Strike', '-', 'NumberedList', 'BulletedList', '-', Index: /CKEditor/trunk/_source/tests/core/dom/text.html =================================================================== --- /CKEditor/trunk/_source/tests/core/dom/text.html (revision 3091) +++ /CKEditor/trunk/_source/tests/core/dom/text.html (revision 3092) @@ -59,4 +59,49 @@ }, + test_split1 : function() + { + var div = CKEDITOR.document.getById( 'playground' ); + div.setHtml( '01234' ); + + var text = div.getFirst(), + next = text.split( 3 ); + + assert.areSame( '012', text.getText(), 'text.getText() is wrong' ); + assert.areSame( '34', next.getText(), 'next.getText() is wrong' ); + + assert.areSame( div.$, next.$.parentNode, 'parentNode is wrong' ); + assert.areSame( text.$, next.$.previousSibling, 'sibling is wrong' ); + }, + + test_split2 : function() + { + var div = CKEDITOR.document.getById( 'playground' ); + div.setHtml( '01234' ); + + var text = div.getFirst(), + next = text.split( 5 ); + + assert.areSame( '01234', text.getText(), 'text.getText() is wrong' ); + assert.areSame( '', next.getText(), 'next.getText() is wrong' ); + + assert.areSame( div.$, next.$.parentNode, 'parentNode is wrong' ); + assert.areSame( text.$, next.$.previousSibling, 'sibling is wrong' ); + }, + + test_split3 : function() + { + var div = CKEDITOR.document.getById( 'playground' ); + div.setHtml( '01234' ); + + var text = div.getFirst(), + next = text.split( 0 ); + + assert.areSame( '', text.getText(), 'text.getText() is wrong' ); + assert.areSame( '01234', next.getText(), 'next.getText() is wrong' ); + + assert.areSame( div.$, next.$.parentNode, 'parentNode is wrong' ); + assert.areSame( text.$, next.$.previousSibling, 'sibling is wrong' ); + }, + name : document.title }; @@ -67,5 +112,5 @@
-0123456789
+