JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.6.6.1
[ckeditor.git] / _source / core / dom / range.js
index 67759c8..fbbbe35 100644 (file)
@@ -1,5 +1,5 @@
 /*\r
-Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.\r
+Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.\r
 For licensing, see LICENSE.html or http://ckeditor.com/license\r
 */\r
 \r
@@ -347,53 +347,70 @@ CKEDITOR.dom.range = function( document )
 \r
        // Creates the appropriate node evaluator for the dom walker used inside\r
        // check(Start|End)OfBlock.\r
-       function getCheckStartEndBlockEvalFunction( isStart )\r
+       function getCheckStartEndBlockEvalFunction()\r
        {\r
-               var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true );\r
+               var skipBogus = false,\r
+                       whitespaces = CKEDITOR.dom.walker.whitespaces(),\r
+                       bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),\r
+                       isBogus = CKEDITOR.dom.walker.bogus();\r
+\r
                return function( node )\r
                {\r
-                       // First ignore bookmark nodes.\r
-                       if ( bookmarkEvaluator( node ) )\r
+                       // First skip empty nodes.\r
+                       if ( bookmarkEvaluator( node ) || whitespaces( node ) )\r
                                return true;\r
 \r
-                       if ( node.type == CKEDITOR.NODE_TEXT )\r
-                       {\r
-                               // If there's any visible text, then we're not at the start.\r
-                               if ( node.hasAscendant( 'pre' ) || CKEDITOR.tools.trim( node.getText() ).length )\r
-                                       return false;\r
-                       }\r
-                       else if ( node.type == CKEDITOR.NODE_ELEMENT )\r
+                       // Skip the bogus node at the end of block.\r
+                       if ( isBogus( node ) &&\r
+                                !skipBogus )\r
                        {\r
-                               // If there are non-empty inline elements (e.g. <img />), then we're not\r
-                               // at the start.\r
-                               if ( !inlineChildReqElements[ node.getName() ] )\r
-                               {\r
-                                       // If we're working at the end-of-block, forgive the first <br /> in non-IE\r
-                                       // browsers.\r
-                                       if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr )\r
-                                               hadBr = true;\r
-                                       else\r
-                                               return false;\r
-                               }\r
+                               skipBogus = true;\r
+                               return true;\r
                        }\r
+\r
+                       // If there's any visible text, then we're not at the start.\r
+                       if ( node.type == CKEDITOR.NODE_TEXT &&\r
+                                        ( node.hasAscendant( 'pre' ) ||\r
+                                                CKEDITOR.tools.trim( node.getText() ).length ) )\r
+                               return false;\r
+\r
+                       // If there are non-empty inline elements (e.g. <img />), then we're not\r
+                       // at the start.\r
+                       if ( node.type == CKEDITOR.NODE_ELEMENT && !inlineChildReqElements[ node.getName() ] )\r
+                               return false;\r
+\r
                        return true;\r
                };\r
        }\r
 \r
+\r
+       var isBogus = CKEDITOR.dom.walker.bogus();\r
        // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any\r
        // text node and non-empty elements unless it's being bookmark text.\r
-       function elementBoundaryEval( node )\r
+       function elementBoundaryEval( checkStart )\r
        {\r
-               // Reject any text node unless it's being bookmark\r
-               // OR it's spaces. (#3883)\r
-               return node.type != CKEDITOR.NODE_TEXT\r
-                           && node.getName() in CKEDITOR.dtd.$removeEmpty\r
-                           || !CKEDITOR.tools.trim( node.getText() )\r
-                           || !!node.getParent().data( 'cke-bookmark' );\r
+               var whitespaces = CKEDITOR.dom.walker.whitespaces(),\r
+                       bookmark = CKEDITOR.dom.walker.bookmark( 1 );\r
+\r
+               return function( node )\r
+               {\r
+                       // First skip empty nodes.\r
+                       if ( bookmark( node ) || whitespaces( node ) )\r
+                               return true;\r
+\r
+                       // Tolerant bogus br when checking at the end of block.\r
+                       // Reject any text node unless it's being bookmark\r
+                       // OR it's spaces.\r
+                       // Reject any element unless it's being invisible empty. (#3883)\r
+                       return !checkStart && isBogus( node ) ||\r
+                                                node.type == CKEDITOR.NODE_ELEMENT &&\r
+                                                node.getName() in CKEDITOR.dtd.$removeEmpty;\r
+               };\r
        }\r
 \r
        var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),\r
-               bookmarkEval = new CKEDITOR.dom.walker.bookmark();\r
+               bookmarkEval = new CKEDITOR.dom.walker.bookmark(),\r
+               nbspRegExp = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/;\r
 \r
        function nonWhitespaceOrBookmarkEval( node )\r
        {\r
@@ -506,7 +523,7 @@ CKEDITOR.dom.range = function( document )
                        if ( serializable )\r
                        {\r
                                baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();\r
-                               startNode.setAttribute( 'id', baseId + 'S' );\r
+                               startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );\r
                        }\r
 \r
                        // If collapsed, the endNode will not be created.\r
@@ -537,7 +554,7 @@ CKEDITOR.dom.range = function( document )
                                this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );\r
 \r
                        return {\r
-                               startNode : serializable ? baseId + 'S' : startNode,\r
+                               startNode : serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,\r
                                endNode : serializable ? baseId + 'E' : endNode,\r
                                serializable : serializable,\r
                                collapsed : collapsed\r
@@ -1014,7 +1031,12 @@ CKEDITOR.dom.range = function( document )
                                                        // whitespaces at the end.\r
                                                        isWhiteSpace = false;\r
 \r
-                                                       if ( sibling.type == CKEDITOR.NODE_TEXT )\r
+                                                       if ( sibling.type == CKEDITOR.NODE_COMMENT )\r
+                                                       {\r
+                                                               sibling = sibling.getPrevious();\r
+                                                               continue;\r
+                                                       }\r
+                                                       else if ( sibling.type == CKEDITOR.NODE_TEXT )\r
                                                        {\r
                                                                siblingText = sibling.getText();\r
 \r
@@ -1044,7 +1066,7 @@ CKEDITOR.dom.range = function( document )
                                                                                        sibling = null;\r
                                                                                else\r
                                                                                {\r
-                                                                                       var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );\r
+                                                                                       var allChildren = sibling.$.getElementsByTagName( '*' );\r
                                                                                        for ( var i = 0, child ; child = allChildren[ i++ ] ; )\r
                                                                                        {\r
                                                                                                if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )\r
@@ -1183,7 +1205,7 @@ CKEDITOR.dom.range = function( document )
 \r
                                                                isWhiteSpace = /^[\s\ufeff]/.test( siblingText );\r
                                                        }\r
-                                                       else\r
+                                                       else if ( sibling.type == CKEDITOR.NODE_ELEMENT )\r
                                                        {\r
                                                                // If this is a visible element.\r
                                                                // We need to check for the bookmark attribute because IE insists on\r
@@ -1204,7 +1226,7 @@ CKEDITOR.dom.range = function( document )
                                                                                        sibling = null;\r
                                                                                else\r
                                                                                {\r
-                                                                                       allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );\r
+                                                                                       allChildren = sibling.$.getElementsByTagName( '*' );\r
                                                                                        for ( i = 0 ; child = allChildren[ i++ ] ; )\r
                                                                                        {\r
                                                                                                if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )\r
@@ -1222,6 +1244,8 @@ CKEDITOR.dom.range = function( document )
                                                                                sibling = null;\r
                                                                }\r
                                                        }\r
+                                                       else\r
+                                                               isWhiteSpace = 1;\r
 \r
                                                        if ( isWhiteSpace )\r
                                                        {\r
@@ -1793,7 +1817,7 @@ CKEDITOR.dom.range = function( document )
                        // Create the walker, which will check if we have anything useful\r
                        // in the range.\r
                        var walker = new CKEDITOR.dom.walker( walkerRange );\r
-                       walker.evaluator = elementBoundaryEval;\r
+                       walker.evaluator = elementBoundaryEval( checkStart );\r
 \r
                        return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();\r
                },\r
@@ -1805,20 +1829,15 @@ CKEDITOR.dom.range = function( document )
                        var startContainer = this.startContainer,\r
                                startOffset = this.startOffset;\r
 \r
-                       // If the starting node is a text node, and non-empty before the offset,\r
-                       // then we're surely not at the start of block.\r
-                       if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )\r
+                       // [IE] Special handling for range start in text with a leading NBSP,\r
+                       // we it to be isolated, for bogus check.\r
+                       if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT )\r
                        {\r
                                var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );\r
-                               if ( textBefore.length )\r
-                                       return false;\r
+                               if ( nbspRegExp.test( textBefore ) )\r
+                                       this.trim( 0, 1 );\r
                        }\r
 \r
-                       // Antecipate the trim() call here, so the walker will not make\r
-                       // changes to the DOM, which would not get reflected into this\r
-                       // range otherwise.\r
-                       this.trim();\r
-\r
                        // We need to grab the block element holding the start boundary, so\r
                        // let's use an element path for it.\r
                        var path = new CKEDITOR.dom.elementPath( this.startContainer );\r
@@ -1829,7 +1848,7 @@ CKEDITOR.dom.range = function( document )
                        walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );\r
 \r
                        var walker = new CKEDITOR.dom.walker( walkerRange );\r
-                       walker.evaluator = getCheckStartEndBlockEvalFunction( true );\r
+                       walker.evaluator = getCheckStartEndBlockEvalFunction();\r
 \r
                        return walker.checkBackward();\r
                },\r
@@ -1839,20 +1858,15 @@ CKEDITOR.dom.range = function( document )
                        var endContainer = this.endContainer,\r
                                endOffset = this.endOffset;\r
 \r
-                       // If the ending node is a text node, and non-empty after the offset,\r
-                       // then we're surely not at the end of block.\r
-                       if ( endContainer.type == CKEDITOR.NODE_TEXT )\r
+                       // [IE] Special handling for range end in text with a following NBSP,\r
+                       // we it to be isolated, for bogus check.\r
+                       if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT )\r
                        {\r
                                var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );\r
-                               if ( textAfter.length )\r
-                                       return false;\r
+                               if ( nbspRegExp.test( textAfter ) )\r
+                                       this.trim( 1, 0 );\r
                        }\r
 \r
-                       // Antecipate the trim() call here, so the walker will not make\r
-                       // changes to the DOM, which would not get reflected into this\r
-                       // range otherwise.\r
-                       this.trim();\r
-\r
                        // We need to grab the block element holding the start boundary, so\r
                        // let's use an element path for it.\r
                        var path = new CKEDITOR.dom.elementPath( this.endContainer );\r
@@ -1863,15 +1877,53 @@ CKEDITOR.dom.range = function( document )
                        walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );\r
 \r
                        var walker = new CKEDITOR.dom.walker( walkerRange );\r
-                       walker.evaluator = getCheckStartEndBlockEvalFunction( false );\r
+                       walker.evaluator = getCheckStartEndBlockEvalFunction();\r
 \r
                        return walker.checkForward();\r
                },\r
 \r
                /**\r
-                * Check if elements at which the range boundaries anchor are read-only,\r
-                * with respect to "contenteditable" attribute.\r
+                * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.\r
+                * @param {Function} evaluator Function used as the walker's evaluator.\r
+                * @param {Function} [guard] Function used as the walker's guard.\r
+                * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,\r
+                * default to the root editable if not defined.\r
+                *\r
+                * @return {CKEDITOR.dom.element|null} The returned node from the traversal.\r
+                */\r
+               getPreviousNode : function( evaluator, guard, boundary ) {\r
+\r
+                       var walkerRange = this.clone();\r
+                       walkerRange.collapse( 1 );\r
+                       walkerRange.setStartAt( boundary || this.document.getBody(), CKEDITOR.POSITION_AFTER_START );\r
+\r
+                       var walker = new CKEDITOR.dom.walker( walkerRange );\r
+                       walker.evaluator = evaluator;\r
+                       walker.guard = guard;\r
+                       return walker.previous();\r
+               },\r
+\r
+               /**\r
+                * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.\r
+                * @param {Function} evaluator Function used as the walker's evaluator.\r
+                * @param {Function} [guard] Function used as the walker's guard.\r
+                * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,\r
+                * default to the root editable if not defined.\r
+                *\r
+                * @return {CKEDITOR.dom.element|null} The returned node from the traversal.\r
                 */\r
+               getNextNode: function( evaluator, guard, boundary )\r
+               {\r
+                       var walkerRange = this.clone();\r
+                       walkerRange.collapse();\r
+                       walkerRange.setEndAt( boundary || this.document.getBody(), CKEDITOR.POSITION_BEFORE_END );\r
+\r
+                       var walker = new CKEDITOR.dom.walker( walkerRange );\r
+                       walker.evaluator = evaluator;\r
+                       walker.guard = guard;\r
+                       return walker.next();\r
+               },\r
+\r
                checkReadOnly : ( function()\r
                {\r
                        function checkNodesEditable( node, anotherEnd )\r
@@ -1924,12 +1976,8 @@ CKEDITOR.dom.range = function( document )
                        {\r
                                var next;\r
 \r
-                               if ( node.type == CKEDITOR.NODE_ELEMENT\r
-                                               && node.isEditable( false )\r
-                                               && !CKEDITOR.dtd.$nonEditable[ node.getName() ] )\r
-                               {\r
+                               if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )\r
                                        next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );\r
-                               }\r
 \r
                                if ( !childOnly && !next )\r
                                        next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );\r
@@ -1937,6 +1985,15 @@ CKEDITOR.dom.range = function( document )
                                return next;\r
                        }\r
 \r
+                       // Handle non-editable element e.g. HR.\r
+                       if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) )\r
+                       {\r
+                               this.moveToPosition( el, isMoveToEnd ?\r
+                                                                                CKEDITOR.POSITION_AFTER_END :\r
+                                                                                CKEDITOR.POSITION_BEFORE_START );\r
+                               return true;\r
+                       }\r
+\r
                        var found = 0;\r
 \r
                        while ( el )\r
@@ -1944,7 +2001,11 @@ CKEDITOR.dom.range = function( document )
                                // Stop immediately if we've found a text node.\r
                                if ( el.type == CKEDITOR.NODE_TEXT )\r
                                {\r
-                                       this.moveToPosition( el, isMoveToEnd ?\r
+                                       // Put cursor before block filler.\r
+                                       if ( isMoveToEnd && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )\r
+                                               this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );\r
+                                       else\r
+                                               this.moveToPosition( el, isMoveToEnd ?\r
                                                                 CKEDITOR.POSITION_AFTER_END :\r
                                                                 CKEDITOR.POSITION_BEFORE_START );\r
                                        found = 1;\r
@@ -1961,6 +2022,9 @@ CKEDITOR.dom.range = function( document )
                                                                                                 CKEDITOR.POSITION_AFTER_START );\r
                                                found = 1;\r
                                        }\r
+                                       // Put cursor before padding block br.\r
+                                       else if ( isMoveToEnd && el.is( 'br' ) && this.checkEndOfBlock() )\r
+                                               this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );\r
                                }\r
 \r
                                el = nextDFS( el, found );\r