X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;ds=sidebyside;f=_source%2Fcore%2Fdom%2Frange.js;h=fbbbe357f1b48d368dca10fa21a148fac4184002;hb=a272c66d841421f8bf933c16535bdcde1c4649fc;hp=fedbfb97e45684a2612f0d548a5921c3e1fe1fe6;hpb=48b1db88210b4160dce439c6e3e32e14af8c106b;p=ckeditor.git
diff --git a/_source/core/dom/range.js b/_source/core/dom/range.js
index fedbfb9..fbbbe35 100644
--- a/_source/core/dom/range.js
+++ b/_source/core/dom/range.js
@@ -1,19 +1,90 @@
/*
-Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
/**
- * @class
+ * Creates a CKEDITOR.dom.range instance that can be used inside a specific
+ * DOM Document.
+ * @class Represents a delimited piece of content in a DOM Document.
+ * It is contiguous in the sense that it can be characterized as selecting all
+ * of the content between a pair of boundary-points.
+ *
+ * This class shares much of the W3C
+ * Document Object Model Range
+ * ideas and features, adding several range manipulation tools to it, but it's
+ * not intended to be compatible with it.
+ * @param {CKEDITOR.dom.document} document The document into which the range
+ * features will be available.
+ * @example
+ * // Create a range for the entire contents of the editor document body.
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * // Delete the contents.
+ * range.deleteContents();
*/
CKEDITOR.dom.range = function( document )
{
+ /**
+ * Node within which the range begins.
+ * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT}
+ * @example
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.startContainer.getName() ); // "body"
+ */
this.startContainer = null;
+
+ /**
+ * Offset within the starting node of the range.
+ * @type {Number}
+ * @example
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.startOffset ); // "0"
+ */
this.startOffset = null;
+
+ /**
+ * Node within which the range ends.
+ * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT}
+ * @example
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.endContainer.getName() ); // "body"
+ */
this.endContainer = null;
+
+ /**
+ * Offset within the ending node of the range.
+ * @type {Number}
+ * @example
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
+ */
this.endOffset = null;
+
+ /**
+ * Indicates that this is a collapsed range. A collapsed range has it's
+ * start and end boudaries at the very same point so nothing is contained
+ * in it.
+ * @example
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.collapsed ); // "false"
+ * range.collapse();
+ * alert( range.collapsed ); // "true"
+ */
this.collapsed = true;
+ /**
+ * The document within which the range can be used.
+ * @type {CKEDITOR.dom.document}
+ * @example
+ * // Selects the body contents of the range document.
+ * range.selectNodeContents( range.document.getBody() );
+ */
this.document = document;
};
@@ -276,53 +347,70 @@ CKEDITOR.dom.range = function( document )
// Creates the appropriate node evaluator for the dom walker used inside
// check(Start|End)OfBlock.
- function getCheckStartEndBlockEvalFunction( isStart )
+ function getCheckStartEndBlockEvalFunction()
{
- var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true );
+ var skipBogus = false,
+ whitespaces = CKEDITOR.dom.walker.whitespaces(),
+ bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
+ isBogus = CKEDITOR.dom.walker.bogus();
+
return function( node )
{
- // First ignore bookmark nodes.
- if ( bookmarkEvaluator( node ) )
+ // First skip empty nodes.
+ if ( bookmarkEvaluator( node ) || whitespaces( node ) )
return true;
- if ( node.type == CKEDITOR.NODE_TEXT )
+ // Skip the bogus node at the end of block.
+ if ( isBogus( node ) &&
+ !skipBogus )
{
- // If there's any visible text, then we're not at the start.
- if ( CKEDITOR.tools.trim( node.getText() ).length )
- return false;
- }
- else if ( node.type == CKEDITOR.NODE_ELEMENT )
- {
- // If there are non-empty inline elements (e.g. ), then we're not
- // at the start.
- if ( !inlineChildReqElements[ node.getName() ] )
- {
- // If we're working at the end-of-block, forgive the first
in non-IE
- // browsers.
- if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr )
- hadBr = true;
- else
- return false;
- }
+ skipBogus = true;
+ return true;
}
+
+ // If there's any visible text, then we're not at the start.
+ if ( node.type == CKEDITOR.NODE_TEXT &&
+ ( node.hasAscendant( 'pre' ) ||
+ CKEDITOR.tools.trim( node.getText() ).length ) )
+ return false;
+
+ // If there are non-empty inline elements (e.g. ), then we're not
+ // at the start.
+ if ( node.type == CKEDITOR.NODE_ELEMENT && !inlineChildReqElements[ node.getName() ] )
+ return false;
+
return true;
};
}
+
+ var isBogus = CKEDITOR.dom.walker.bogus();
// Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
// text node and non-empty elements unless it's being bookmark text.
- function elementBoundaryEval( node )
+ function elementBoundaryEval( checkStart )
{
- // Reject any text node unless it's being bookmark
- // OR it's spaces. (#3883)
- return node.type != CKEDITOR.NODE_TEXT
- && node.getName() in CKEDITOR.dtd.$removeEmpty
- || !CKEDITOR.tools.trim( node.getText() )
- || !!node.getParent().data( 'cke-bookmark' );
+ var whitespaces = CKEDITOR.dom.walker.whitespaces(),
+ bookmark = CKEDITOR.dom.walker.bookmark( 1 );
+
+ return function( node )
+ {
+ // First skip empty nodes.
+ if ( bookmark( node ) || whitespaces( node ) )
+ return true;
+
+ // Tolerant bogus br when checking at the end of block.
+ // Reject any text node unless it's being bookmark
+ // OR it's spaces.
+ // Reject any element unless it's being invisible empty. (#3883)
+ return !checkStart && isBogus( node ) ||
+ node.type == CKEDITOR.NODE_ELEMENT &&
+ node.getName() in CKEDITOR.dtd.$removeEmpty;
+ };
}
var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(),
- bookmarkEval = new CKEDITOR.dom.walker.bookmark();
+ bookmarkEval = new CKEDITOR.dom.walker.bookmark(),
+ nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/;
function nonWhitespaceOrBookmarkEval( node )
{
@@ -435,7 +523,7 @@ CKEDITOR.dom.range = function( document )
if ( serializable )
{
baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
- startNode.setAttribute( 'id', baseId + 'S' );
+ startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
}
// If collapsed, the endNode will not be created.
@@ -466,7 +554,7 @@ CKEDITOR.dom.range = function( document )
this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
return {
- startNode : serializable ? baseId + 'S' : startNode,
+ startNode : serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
endNode : serializable ? baseId + 'E' : endNode,
serializable : serializable,
collapsed : collapsed
@@ -518,6 +606,10 @@ CKEDITOR.dom.range = function( document )
startContainer = child;
startOffset = 0;
}
+
+ // Get the normalized offset.
+ if ( child && child.type == CKEDITOR.NODE_ELEMENT )
+ startOffset = child.getIndex( 1 );
}
// Normalize the start.
@@ -546,6 +638,10 @@ CKEDITOR.dom.range = function( document )
endContainer = child;
endOffset = 0;
}
+
+ // Get the normalized offset.
+ if ( child && child.type == CKEDITOR.NODE_ELEMENT )
+ endOffset = child.getIndex( 1 );
}
// Normalize the end.
@@ -935,7 +1031,12 @@ CKEDITOR.dom.range = function( document )
// whitespaces at the end.
isWhiteSpace = false;
- if ( sibling.type == CKEDITOR.NODE_TEXT )
+ if ( sibling.type == CKEDITOR.NODE_COMMENT )
+ {
+ sibling = sibling.getPrevious();
+ continue;
+ }
+ else if ( sibling.type == CKEDITOR.NODE_TEXT )
{
siblingText = sibling.getText();
@@ -965,7 +1066,7 @@ CKEDITOR.dom.range = function( document )
sibling = null;
else
{
- var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
+ var allChildren = sibling.$.getElementsByTagName( '*' );
for ( var i = 0, child ; child = allChildren[ i++ ] ; )
{
if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
@@ -1104,7 +1205,7 @@ CKEDITOR.dom.range = function( document )
isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
}
- else
+ else if ( sibling.type == CKEDITOR.NODE_ELEMENT )
{
// If this is a visible element.
// We need to check for the bookmark attribute because IE insists on
@@ -1125,7 +1226,7 @@ CKEDITOR.dom.range = function( document )
sibling = null;
else
{
- allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
+ allChildren = sibling.$.getElementsByTagName( '*' );
for ( i = 0 ; child = allChildren[ i++ ] ; )
{
if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
@@ -1143,6 +1244,8 @@ CKEDITOR.dom.range = function( document )
sibling = null;
}
}
+ else
+ isWhiteSpace = 1;
if ( isWhiteSpace )
{
@@ -1241,6 +1344,22 @@ CKEDITOR.dom.range = function( document )
CKEDITOR.POSITION_AFTER_START :
CKEDITOR.POSITION_AFTER_END );
+ // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
+ if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS )
+ {
+ var theRange = this.clone();
+ walker = new CKEDITOR.dom.walker( theRange );
+
+ var whitespaces = CKEDITOR.dom.walker.whitespaces(),
+ bookmark = CKEDITOR.dom.walker.bookmark();
+
+ walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); };
+ var previous = walker.previous();
+ if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
+ return;
+ }
+
+
// Enlarging the end boundary.
walkerRange = this.clone();
walkerRange.collapse();
@@ -1698,7 +1817,7 @@ CKEDITOR.dom.range = function( document )
// Create the walker, which will check if we have anything useful
// in the range.
var walker = new CKEDITOR.dom.walker( walkerRange );
- walker.evaluator = elementBoundaryEval;
+ walker.evaluator = elementBoundaryEval( checkStart );
return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
},
@@ -1710,20 +1829,15 @@ CKEDITOR.dom.range = function( document )
var startContainer = this.startContainer,
startOffset = this.startOffset;
- // If the starting node is a text node, and non-empty before the offset,
- // then we're surely not at the start of block.
- if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
+ // [IE] Special handling for range start in text with a leading NBSP,
+ // we it to be isolated, for bogus check.
+ if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
{
var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
- if ( textBefore.length )
- return false;
+ if ( nbspRegExp.test( textBefore ) )
+ this.trim( 0, 1 );
}
- // Antecipate the trim() call here, so the walker will not make
- // changes to the DOM, which would not get reflected into this
- // range otherwise.
- this.trim();
-
// We need to grab the block element holding the start boundary, so
// let's use an element path for it.
var path = new CKEDITOR.dom.elementPath( this.startContainer );
@@ -1734,7 +1848,7 @@ CKEDITOR.dom.range = function( document )
walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
var walker = new CKEDITOR.dom.walker( walkerRange );
- walker.evaluator = getCheckStartEndBlockEvalFunction( true );
+ walker.evaluator = getCheckStartEndBlockEvalFunction();
return walker.checkBackward();
},
@@ -1744,20 +1858,15 @@ CKEDITOR.dom.range = function( document )
var endContainer = this.endContainer,
endOffset = this.endOffset;
- // If the ending node is a text node, and non-empty after the offset,
- // then we're surely not at the end of block.
- if ( endContainer.type == CKEDITOR.NODE_TEXT )
+ // [IE] Special handling for range end in text with a following NBSP,
+ // we it to be isolated, for bogus check.
+ if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT )
{
var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
- if ( textAfter.length )
- return false;
+ if ( nbspRegExp.test( textAfter ) )
+ this.trim( 1, 0 );
}
- // Antecipate the trim() call here, so the walker will not make
- // changes to the DOM, which would not get reflected into this
- // range otherwise.
- this.trim();
-
// We need to grab the block element holding the start boundary, so
// let's use an element path for it.
var path = new CKEDITOR.dom.elementPath( this.endContainer );
@@ -1768,15 +1877,53 @@ CKEDITOR.dom.range = function( document )
walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
var walker = new CKEDITOR.dom.walker( walkerRange );
- walker.evaluator = getCheckStartEndBlockEvalFunction( false );
+ walker.evaluator = getCheckStartEndBlockEvalFunction();
return walker.checkForward();
},
/**
- * Check if elements at which the range boundaries anchor are read-only,
- * with respect to "contenteditable" attribute.
+ * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
+ * @param {Function} evaluator Function used as the walker's evaluator.
+ * @param {Function} [guard] Function used as the walker's guard.
+ * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
+ * default to the root editable if not defined.
+ *
+ * @return {CKEDITOR.dom.element|null} The returned node from the traversal.
+ */
+ getPreviousNode : function( evaluator, guard, boundary ) {
+
+ var walkerRange = this.clone();
+ walkerRange.collapse( 1 );
+ walkerRange.setStartAt( boundary || this.document.getBody(), CKEDITOR.POSITION_AFTER_START );
+
+ var walker = new CKEDITOR.dom.walker( walkerRange );
+ walker.evaluator = evaluator;
+ walker.guard = guard;
+ return walker.previous();
+ },
+
+ /**
+ * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
+ * @param {Function} evaluator Function used as the walker's evaluator.
+ * @param {Function} [guard] Function used as the walker's guard.
+ * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
+ * default to the root editable if not defined.
+ *
+ * @return {CKEDITOR.dom.element|null} The returned node from the traversal.
*/
+ getNextNode: function( evaluator, guard, boundary )
+ {
+ var walkerRange = this.clone();
+ walkerRange.collapse();
+ walkerRange.setEndAt( boundary || this.document.getBody(), CKEDITOR.POSITION_BEFORE_END );
+
+ var walker = new CKEDITOR.dom.walker( walkerRange );
+ walker.evaluator = evaluator;
+ walker.guard = guard;
+ return walker.next();
+ },
+
checkReadOnly : ( function()
{
function checkNodesEditable( node, anotherEnd )
@@ -1791,7 +1938,7 @@ CKEDITOR.dom.range = function( document )
return 0;
}
// Range enclosed entirely in an editable element.
- else if ( node.is( 'body' )
+ else if ( node.is( 'html' )
|| node.getAttribute( 'contentEditable' ) == 'true'
&& ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
{
@@ -1825,47 +1972,65 @@ CKEDITOR.dom.range = function( document )
*/
moveToElementEditablePosition : function( el, isMoveToEnd )
{
- var isEditable;
+ function nextDFS( node, childOnly )
+ {
+ var next;
- // Empty elements are rejected.
- if ( CKEDITOR.dtd.$empty[ el.getName() ] )
- return false;
+ if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
+ next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
+
+ if ( !childOnly && !next )
+ next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );
+
+ return next;
+ }
- while ( el && el.type == CKEDITOR.NODE_ELEMENT )
+ // Handle non-editable element e.g. HR.
+ if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) )
{
- isEditable = el.isEditable();
-
- // If an editable element is found, move inside it.
- if ( isEditable )
- this.moveToPosition( el, isMoveToEnd ?
- CKEDITOR.POSITION_BEFORE_END :
- CKEDITOR.POSITION_AFTER_START );
- // Stop immediately if we've found a non editable inline element (e.g ).
- else if ( CKEDITOR.dtd.$inline[ el.getName() ] )
- {
- this.moveToPosition( el, isMoveToEnd ?
- CKEDITOR.POSITION_AFTER_END :
- CKEDITOR.POSITION_BEFORE_START );
- return true;
- }
+ this.moveToPosition( el, isMoveToEnd ?
+ CKEDITOR.POSITION_AFTER_END :
+ CKEDITOR.POSITION_BEFORE_START );
+ return true;
+ }
- // Non-editable non-inline elements are to be bypassed, getting the next one.
- if ( CKEDITOR.dtd.$empty[ el.getName() ] )
- el = el[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval );
- else
- el = el[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval );
+ var found = 0;
+ while ( el )
+ {
// Stop immediately if we've found a text node.
- if ( el && el.type == CKEDITOR.NODE_TEXT )
+ if ( el.type == CKEDITOR.NODE_TEXT )
{
- this.moveToPosition( el, isMoveToEnd ?
+ // Put cursor before block filler.
+ if ( isMoveToEnd && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
+ this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
+ else
+ this.moveToPosition( el, isMoveToEnd ?
CKEDITOR.POSITION_AFTER_END :
CKEDITOR.POSITION_BEFORE_START );
- return true;
+ found = 1;
+ break;
}
+
+ // If an editable element is found, move inside it, but not stop the searching.
+ if ( el.type == CKEDITOR.NODE_ELEMENT )
+ {
+ if ( el.isEditable() )
+ {
+ this.moveToPosition( el, isMoveToEnd ?
+ CKEDITOR.POSITION_BEFORE_END :
+ CKEDITOR.POSITION_AFTER_START );
+ found = 1;
+ }
+ // Put cursor before padding block br.
+ else if ( isMoveToEnd && el.is( 'br' ) && this.checkEndOfBlock() )
+ this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
+ }
+
+ el = nextDFS( el, found );
}
- return isEditable;
+ return !!found;
},
/**