/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* 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](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html)
* ideas and features, adding several range manipulation tools to it, but it's
* not intended to be compatible with it.
*
* // 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();
*
* Usually you will want to work on a ranges rooted in the editor's {@link CKEDITOR.editable editable}
* element. Such ranges can be created with a shorthand method – {@link CKEDITOR.editor#createRange editor.createRange}.
*
* var range = editor.createRange();
* range.root.equals( editor.editable() ); // -> true
*
* Note that the {@link #root} of a range is an important property, which limits many
* algorithms implemented in range's methods. Therefore it is crucial, especially
* when using ranges inside inline editors, to specify correct root, so using
* the {@link CKEDITOR.editor#createRange} method is highly recommended.
*
* ### Selection
*
* Range is only a logical representation of a piece of content in a DOM. It should not
* be confused with a {@link CKEDITOR.dom.selection selection} which represents "physically
* marked" content. It is possible to create unlimited number of various ranges, when
* only one real selection may exist at a time in a document. Ranges are used to read position
* of selection in the DOM and to move selection to new positions.
*
* The editor selection may be retrieved using the {@link CKEDITOR.editor#getSelection} method:
*
* var sel = editor.getSelection(),
* ranges = sel.getRange(); // CKEDITOR.dom.rangeList instance.
*
* var range = ranges[ 0 ];
* range.root; // -> editor's editable element.
*
* A range can also be selected:
*
* var range = editor.createRange();
* range.selectNodeContents( editor.editable() );
* sel.selectRanges( [ range ] );
*
* @class
* @constructor Creates a {@link CKEDITOR.dom.range} instance that can be used inside a specific DOM Document.
* @param {CKEDITOR.dom.document/CKEDITOR.dom.element} root The document or element
* within which the range will be scoped.
* @todo global "TODO" - precise algorithms descriptions needed for the most complex methods like #enlarge.
*/
CKEDITOR.dom.range = function( root ) {
/**
* Node within which the range begins.
*
* var range = new CKEDITOR.dom.range( editor.document );
* range.selectNodeContents( editor.document.getBody() );
* alert( range.startContainer.getName() ); // 'body'
*
* @readonly
* @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
*/
this.startContainer = null;
/**
* Offset within the starting node of the range.
*
* var range = new CKEDITOR.dom.range( editor.document );
* range.selectNodeContents( editor.document.getBody() );
* alert( range.startOffset ); // 0
*
* @readonly
* @property {Number}
*/
this.startOffset = null;
/**
* Node within which the range ends.
*
* var range = new CKEDITOR.dom.range( editor.document );
* range.selectNodeContents( editor.document.getBody() );
* alert( range.endContainer.getName() ); // 'body'
*
* @readonly
* @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
*/
this.endContainer = null;
/**
* Offset within the ending node of the range.
*
* var range = new CKEDITOR.dom.range( editor.document );
* range.selectNodeContents( editor.document.getBody() );
* alert( range.endOffset ); // == editor.document.getBody().getChildCount()
*
* @readonly
* @property {Number}
*/
this.endOffset = null;
/**
* Indicates that this is a collapsed range. A collapsed range has its
* start and end boundaries at the very same point so nothing is contained
* in it.
*
* var range = new CKEDITOR.dom.range( editor.document );
* range.selectNodeContents( editor.document.getBody() );
* alert( range.collapsed ); // false
* range.collapse();
* alert( range.collapsed ); // true
*
* @readonly
*/
this.collapsed = true;
var isDocRoot = root instanceof CKEDITOR.dom.document;
/**
* The document within which the range can be used.
*
* // Selects the body contents of the range document.
* range.selectNodeContents( range.document.getBody() );
*
* @readonly
* @property {CKEDITOR.dom.document}
*/
this.document = isDocRoot ? root : root.getDocument();
/**
* The ancestor DOM element within which the range manipulation are limited.
*
* @readonly
* @property {CKEDITOR.dom.element}
*/
this.root = isDocRoot ? root.getBody() : root;
};
( function() {
// Updates the "collapsed" property for the given range object.
var updateCollapsed = function( range ) {
range.collapsed = ( range.startContainer && range.endContainer && range.startContainer.equals( range.endContainer ) && range.startOffset == range.endOffset );
};
// This is a shared function used to delete, extract and clone the range
// contents.
// V2
var execContentsAction = function( range, action, docFrag, mergeThen ) {
range.optimizeBookmark();
var startNode = range.startContainer;
var endNode = range.endContainer;
var startOffset = range.startOffset;
var endOffset = range.endOffset;
var removeStartNode;
var removeEndNode;
// For text containers, we must simply split the node and point to the
// second part. The removal will be handled by the rest of the code .
if ( endNode.type == CKEDITOR.NODE_TEXT )
endNode = endNode.split( endOffset );
else {
// If the end container has children and the offset is pointing
// to a child, then we should start from it.
if ( endNode.getChildCount() > 0 ) {
// If the offset points after the last node.
if ( endOffset >= endNode.getChildCount() ) {
// Let's create a temporary node and mark it for removal.
endNode = endNode.append( range.document.createText( '' ) );
removeEndNode = true;
} else
endNode = endNode.getChild( endOffset );
}
}
// For text containers, we must simply split the node. The removal will
// be handled by the rest of the code .
if ( startNode.type == CKEDITOR.NODE_TEXT ) {
startNode.split( startOffset );
// In cases the end node is the same as the start node, the above
// splitting will also split the end, so me must move the end to
// the second part of the split.
if ( startNode.equals( endNode ) )
endNode = startNode.getNext();
} else {
// If the start container has children and the offset is pointing
// to a child, then we should start from its previous sibling.
// If the offset points to the first node, we don't have a
// sibling, so let's use the first one, but mark it for removal.
if ( !startOffset ) {
// Let's create a temporary node and mark it for removal.
startNode = startNode.append( range.document.createText( '' ), 1 );
removeStartNode = true;
} else if ( startOffset >= startNode.getChildCount() ) {
// Let's create a temporary node and mark it for removal.
startNode = startNode.append( range.document.createText( '' ) );
removeStartNode = true;
} else
startNode = startNode.getChild( startOffset ).getPrevious();
}
// Get the parent nodes tree for the start and end boundaries.
var startParents = startNode.getParents();
var endParents = endNode.getParents();
// Compare them, to find the top most siblings.
var i, topStart, topEnd;
for ( i = 0; i < startParents.length; i++ ) {
topStart = startParents[ i ];
topEnd = endParents[ i ];
// The compared nodes will match until we find the top most
// siblings (different nodes that have the same parent).
// "i" will hold the index in the parents array for the top
// most element.
if ( !topStart.equals( topEnd ) )
break;
}
var clone = docFrag,
levelStartNode, levelClone, currentNode, currentSibling;
// Remove all successive sibling nodes for every node in the
// startParents tree.
for ( var j = i; j < startParents.length; j++ ) {
levelStartNode = startParents[ j ];
// For Extract and Clone, we must clone this level.
if ( clone && !levelStartNode.equals( startNode ) ) // action = 0 = Delete
levelClone = clone.append( levelStartNode.clone() );
currentNode = levelStartNode.getNext();
while ( currentNode ) {
// Stop processing when the current node matches a node in the
// endParents tree or if it is the endNode.
if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) )
break;
// Cache the next sibling.
currentSibling = currentNode.getNext();
// If cloning, just clone it.
if ( action == 2 ) // 2 = Clone
clone.append( currentNode.clone( true ) );
else {
// Both Delete and Extract will remove the node.
currentNode.remove();
// When Extracting, move the removed node to the docFrag.
if ( action == 1 ) // 1 = Extract
clone.append( currentNode );
}
currentNode = currentSibling;
}
if ( clone )
clone = levelClone;
}
clone = docFrag;
// Remove all previous sibling nodes for every node in the
// endParents tree.
for ( var k = i; k < endParents.length; k++ ) {
levelStartNode = endParents[ k ];
// For Extract and Clone, we must clone this level.
if ( action > 0 && !levelStartNode.equals( endNode ) ) // action = 0 = Delete
levelClone = clone.append( levelStartNode.clone() );
// The processing of siblings may have already been done by the parent.
if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode ) {
currentNode = levelStartNode.getPrevious();
while ( currentNode ) {
// Stop processing when the current node matches a node in the
// startParents tree or if it is the startNode.
if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) )
break;
// Cache the next sibling.
currentSibling = currentNode.getPrevious();
// If cloning, just clone it.
if ( action == 2 ) // 2 = Clone
clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild );
else {
// Both Delete and Extract will remove the node.
currentNode.remove();
// When Extracting, mode the removed node to the docFrag.
if ( action == 1 ) // 1 = Extract
clone.$.insertBefore( currentNode.$, clone.$.firstChild );
}
currentNode = currentSibling;
}
}
if ( clone )
clone = levelClone;
}
if ( action == 2 ) // 2 = Clone.
{
// No changes in the DOM should be done, so fix the split text (if any).
var startTextNode = range.startContainer;
if ( startTextNode.type == CKEDITOR.NODE_TEXT ) {
startTextNode.$.data += startTextNode.$.nextSibling.data;
startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling );
}
var endTextNode = range.endContainer;
if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling ) {
endTextNode.$.data += endTextNode.$.nextSibling.data;
endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling );
}
} else {
// Collapse the range.
// If a node has been partially selected, collapse the range between
// topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs).
if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) ) {
var endIndex = topEnd.getIndex();
// If the start node is to be removed, we must correct the
// index to reflect the removal.
if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode )
endIndex--;
// Merge splitted parents.
if ( mergeThen && topStart.type == CKEDITOR.NODE_ELEMENT ) {
var span = CKEDITOR.dom.element.createFromHtml( '', range.document );
span.insertAfter( topStart );
topStart.mergeSiblings( false );
range.moveToBookmark( { startNode: span } );
} else
range.setStart( topEnd.getParent(), endIndex );
}
// Collapse it to the start.
range.collapse( true );
}
// Cleanup any marked node.
if ( removeStartNode )
startNode.remove();
if ( removeEndNode && endNode.$.parentNode )
endNode.remove();
};
var inlineChildReqElements = { abbr: 1, acronym: 1, b: 1, bdo: 1, big: 1, cite: 1, code: 1, del: 1,
dfn: 1, em: 1, font: 1, i: 1, ins: 1, label: 1, kbd: 1, q: 1, samp: 1, small: 1, span: 1, strike: 1,
strong: 1, sub: 1, sup: 1, tt: 1, u: 1, 'var': 1 };
// Creates the appropriate node evaluator for the dom walker used inside
// check(Start|End)OfBlock.
function getCheckStartEndBlockEvalFunction() {
var skipBogus = false,
whitespaces = CKEDITOR.dom.walker.whitespaces(),
bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
isBogus = CKEDITOR.dom.walker.bogus();
return function( node ) {
// First skip empty nodes
if ( bookmarkEvaluator( node ) || whitespaces( node ) )
return true;
// Skip the bogus node at the end of block.
if ( isBogus( node ) && !skipBogus ) {
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. [text node] [text node] [text node] [text node][text node] foo[ foo[ foo[ bar foo[ bar foo[ foo[bar] foo[bar ] [foo] bar ... [text...), by comparing the document position
// with 'enlargeable' node.
this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() || enlargeable && blockBoundary.contains( enlargeable ) ) ? 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.
// Set up new range and reset all flags (blockBoundary, inNonEditable, tailBr).
walkerRange = this.clone();
walkerRange.collapse();
walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
walker = new CKEDITOR.dom.walker( walkerRange );
// tailBrGuard only used for on range end.
walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard;
blockBoundary = inNonEditable = tailBr = null;
// End the range right before the block boundary node.
enlargeable = walker.lastForward();
// It's the body which stop the enlarging if no block boundary found.
blockBoundary = blockBoundary || boundary;
// Close the range either before the found block start (text] ...), then we're not
// at the start.
if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( inlineChildReqElements ) )
return false;
return true;
};
}
var isBogus = CKEDITOR.dom.walker.bogus(),
nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/,
editableEval = CKEDITOR.dom.walker.editable(),
notIgnoredEval = CKEDITOR.dom.walker.ignored( true );
// Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
// text node and non-empty elements unless it's being bookmark text.
function elementBoundaryEval( checkStart ) {
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.is( CKEDITOR.dtd.$removeEmpty );
};
}
function getNextEditableNode( isPrevious ) {
return function() {
var first;
return this[ isPrevious ? 'getPreviousNode' : 'getNextNode' ]( function( node ) {
// Cache first not ignorable node.
if ( !first && notIgnoredEval( node ) )
first = node;
// Return true if found editable node, but not a bogus next to start of our lookup (first != bogus).
return editableEval( node ) && !( isBogus( node ) && node.equals( first ) );
} );
};
}
CKEDITOR.dom.range.prototype = {
/**
* Clones this range.
*
* @returns {CKEDITOR.dom.range}
*/
clone: function() {
var clone = new CKEDITOR.dom.range( this.root );
clone.startContainer = this.startContainer;
clone.startOffset = this.startOffset;
clone.endContainer = this.endContainer;
clone.endOffset = this.endOffset;
clone.collapsed = this.collapsed;
return clone;
},
/**
* Makes range collapsed by moving its start point (or end point if `toStart==true`)
* to the second end.
*
* @param {Boolean} toStart Collapse range "to start".
*/
collapse: function( toStart ) {
if ( toStart ) {
this.endContainer = this.startContainer;
this.endOffset = this.startOffset;
} else {
this.startContainer = this.endContainer;
this.startOffset = this.endOffset;
}
this.collapsed = true;
},
/**
* The content nodes of the range are cloned and added to a document fragment, which is returned.
*
* **Note:** Text selection may lost after invoking this method (caused by text node splitting).
*
* @returns {CKEDITOR.dom.documentFragment} Document fragment containing clone of range's content.
*/
cloneContents: function() {
var docFrag = new CKEDITOR.dom.documentFragment( this.document );
if ( !this.collapsed )
execContentsAction( this, 2, docFrag );
return docFrag;
},
/**
* Deletes the content nodes of the range permanently from the DOM tree.
*
* @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
*/
deleteContents: function( mergeThen ) {
if ( this.collapsed )
return;
execContentsAction( this, 0, null, mergeThen );
},
/**
* The content nodes of the range are cloned and added to a document fragment,
* meanwhile they are removed permanently from the DOM tree.
*
* @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
* @returns {CKEDITOR.dom.documentFragment} Document fragment containing extracted content.
*/
extractContents: function( mergeThen ) {
var docFrag = new CKEDITOR.dom.documentFragment( this.document );
if ( !this.collapsed )
execContentsAction( this, 1, docFrag, mergeThen );
return docFrag;
},
/**
* Creates a bookmark object, which can be later used to restore the
* range by using the {@link #moveToBookmark} function.
*
* This is an "intrusive" way to create a bookmark. It includes `` tags
* in the range boundaries. The advantage of it is that it is possible to
* handle DOM mutations when moving back to the bookmark.
*
* **Note:** The inclusion of nodes in the DOM is a design choice and
* should not be changed as there are other points in the code that may be
* using those nodes to perform operations.
*
* @param {Boolean} [serializable] Indicates that the bookmark nodes
* must contain IDs, which can be used to restore the range even
* when these nodes suffer mutations (like cloning or `innerHTML` change).
* @returns {Object} And object representing a bookmark.
* @returns {CKEDITOR.dom.node/String} return.startNode Node or element ID.
* @returns {CKEDITOR.dom.node/String} return.endNode Node or element ID.
* @returns {Boolean} return.serializable
* @returns {Boolean} return.collapsed
*/
createBookmark: function( serializable ) {
var startNode, endNode;
var baseId;
var clone;
var collapsed = this.collapsed;
startNode = this.document.createElement( 'span' );
startNode.data( 'cke-bookmark', 1 );
startNode.setStyle( 'display', 'none' );
// For IE, it must have something inside, otherwise it may be
// removed during DOM operations.
startNode.setHtml( ' ' );
if ( serializable ) {
baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
}
// If collapsed, the endNode will not be created.
if ( !collapsed ) {
endNode = startNode.clone();
endNode.setHtml( ' ' );
if ( serializable )
endNode.setAttribute( 'id', baseId + 'E' );
clone = this.clone();
clone.collapse();
clone.insertNode( endNode );
}
clone = this.clone();
clone.collapse( true );
clone.insertNode( startNode );
// Update the range position.
if ( endNode ) {
this.setStartAfter( startNode );
this.setEndBefore( endNode );
} else
this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
return {
startNode: serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
endNode: serializable ? baseId + 'E' : endNode,
serializable: serializable,
collapsed: collapsed
};
},
/**
* Creates a "non intrusive" and "mutation sensible" bookmark. This
* kind of bookmark should be used only when the DOM is supposed to
* remain stable after its creation.
*
* @param {Boolean} [normalized] Indicates that the bookmark must
* be normalized. When normalized, the successive text nodes are
* considered a single node. To successfully load a normalized
* bookmark, the DOM tree must also be normalized before calling
* {@link #moveToBookmark}.
* @returns {Object} An object representing the bookmark.
* @returns {Array} return.start Start container's address (see {@link CKEDITOR.dom.node#getAddress}).
* @returns {Array} return.end Start container's address.
* @returns {Number} return.startOffset
* @returns {Number} return.endOffset
* @returns {Boolean} return.collapsed
* @returns {Boolean} return.normalized
* @returns {Boolean} return.is2 This is "bookmark2".
*/
createBookmark2: ( function() {
// Returns true for limit anchored in element and placed between text nodes.
//
// v
//
Foo bar
* range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START ); * // Range will be moved to:Foo ^bar
* * See also {@link #setStartAt} and {@link #setEndAt}. * * @param {CKEDITOR.dom.node} node The node according to which position will be set. * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START}, * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END}, * {@link CKEDITOR#POSITION_AFTER_END}. */ moveToPosition: function( node, position ) { this.setStartAt( node, position ); this.collapse( true ); }, /** * Moves the range to the exact position of the specified range. * * @param {CKEDITOR.dom.range} range */ moveToRange: function( range ) { this.setStart( range.startContainer, range.startOffset ); this.setEnd( range.endContainer, range.endOffset ); }, /** * Select nodes content. Range will start and end inside this node. * * @param {CKEDITOR.dom.node} node */ selectNodeContents: function( node ) { this.setStart( node, 0 ); this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() ); }, /** * Sets the start position of a range. * * @param {CKEDITOR.dom.node} startNode The node to start the range. * @param {Number} startOffset An integer greater than or equal to zero * representing the offset for the start of the range from the start * of `startNode`. */ setStart: function( startNode, startOffset ) { // W3C requires a check for the new position. If it is after the end // boundary, the range should be collapsed to the new start. It seams // we will not need this check for our use of this class so we can // ignore it for now. // Fixing invalid range start inside dtd empty elements. if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] ) startOffset = startNode.getIndex(), startNode = startNode.getParent(); this.startContainer = startNode; this.startOffset = startOffset; if ( !this.endContainer ) { this.endContainer = startNode; this.endOffset = startOffset; } updateCollapsed( this ); }, /** * Sets the end position of a Range. * * @param {CKEDITOR.dom.node} endNode The node to end the range. * @param {Number} endOffset An integer greater than or equal to zero * representing the offset for the end of the range from the start * of `endNode`. */ setEnd: function( endNode, endOffset ) { // W3C requires a check for the new position. If it is before the start // boundary, the range should be collapsed to the new end. It seams we // will not need this check for our use of this class so we can ignore // it for now. // Fixing invalid range end inside dtd empty elements. if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] ) endOffset = endNode.getIndex() + 1, endNode = endNode.getParent(); this.endContainer = endNode; this.endOffset = endOffset; if ( !this.startContainer ) { this.startContainer = endNode; this.startOffset = endOffset; } updateCollapsed( this ); }, /** * Sets start of this range after the specified node. * * // Range:foobar^
* range.setStartAfter( textFoo ); * // The range will be changed to: * //foo[bar]
* * @param {CKEDITOR.dom.node} node */ setStartAfter: function( node ) { this.setStart( node.getParent(), node.getIndex() + 1 ); }, /** * Sets start of this range after the specified node. * * // Range:foobar^
* range.setStartBefore( elB ); * // The range will be changed to: * //foo[bar]
* * @param {CKEDITOR.dom.node} node */ setStartBefore: function( node ) { this.setStart( node.getParent(), node.getIndex() ); }, /** * Sets end of this range after the specified node. * * // Range:foo^bar
* range.setEndAfter( elB ); * // The range will be changed to: * //foo[bar]
* * @param {CKEDITOR.dom.node} node */ setEndAfter: function( node ) { this.setEnd( node.getParent(), node.getIndex() + 1 ); }, /** * Sets end of this range before the specified node. * * // Range:^foobar
* range.setStartAfter( textBar ); * // The range will be changed to: * //[foo]bar
* * @param {CKEDITOR.dom.node} node */ setEndBefore: function( node ) { this.setEnd( node.getParent(), node.getIndex() ); }, /** * Moves the start of this range to given position according to specified node. * * // HTML:Foo bar^
* range.setStartAt( elB, CKEDITOR.POSITION_AFTER_START ); * // The range will be changed to: * //Foo [bar]
* * See also {@link #setEndAt} and {@link #moveToPosition}. * * @param {CKEDITOR.dom.node} node The node according to which position will be set. * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START}, * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END}, * {@link CKEDITOR#POSITION_AFTER_END}. */ setStartAt: function( node, position ) { switch ( position ) { case CKEDITOR.POSITION_AFTER_START: this.setStart( node, 0 ); break; case CKEDITOR.POSITION_BEFORE_END: if ( node.type == CKEDITOR.NODE_TEXT ) this.setStart( node, node.getLength() ); else this.setStart( node, node.getChildCount() ); break; case CKEDITOR.POSITION_BEFORE_START: this.setStartBefore( node ); break; case CKEDITOR.POSITION_AFTER_END: this.setStartAfter( node ); } updateCollapsed( this ); }, /** * Moves the end of this range to given position according to specified node. * * // HTML:^Foo bar
* range.setEndAt( textBar, CKEDITOR.POSITION_BEFORE_START ); * // The range will be changed to: * //[Foo ]bar
* * See also {@link #setStartAt} and {@link #moveToPosition}. * * @param {CKEDITOR.dom.node} node The node according to which position will be set. * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START}, * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END}, * {@link CKEDITOR#POSITION_AFTER_END}. */ setEndAt: function( node, position ) { switch ( position ) { case CKEDITOR.POSITION_AFTER_START: this.setEnd( node, 0 ); break; case CKEDITOR.POSITION_BEFORE_END: if ( node.type == CKEDITOR.NODE_TEXT ) this.setEnd( node, node.getLength() ); else this.setEnd( node, node.getChildCount() ); break; case CKEDITOR.POSITION_BEFORE_START: this.setEndBefore( node ); break; case CKEDITOR.POSITION_AFTER_END: this.setEndAfter( node ); } updateCollapsed( this ); }, /** * Wraps inline content found around the range's start or end boundary * with a block element. * * // Assuming the following range: * //foo
* // The result of executing: * range.fixBlock( true, 'p' ); * // will be: * //ba^r
bom
foo
* * Non-collapsed range: * * // Assuming the following range: * // ba[rfoo
bo]m * // The result of executing: * range.fixBlock( false, 'p' ); * // will be: * // ba[rfoo
bo]m
* * @param {Boolean} [isStart=false] Whether the start or end boundary of a range should be checked. * @param {String} blockTag The name of a block element in which content will be wrapped. * For example: `'p'`. * @returns {CKEDITOR.dom.element} Created block wrapper. */ fixBlock: function( isStart, blockTag ) { var bookmark = this.createBookmark(), fixedBlock = this.document.createElement( blockTag ); this.collapse( isStart ); this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS ); this.extractContents().appendTo( fixedBlock ); fixedBlock.trim(); fixedBlock.appendBogus(); this.insertNode( fixedBlock ); this.moveToBookmark( bookmark ); return fixedBlock; }, /** * @todo */ splitBlock: function( blockTag ) { var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ), endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root ); var startBlockLimit = startPath.blockLimit, endBlockLimit = endPath.blockLimit; var startBlock = startPath.block, endBlock = endPath.block; var elementPath = null; // Do nothing if the boundaries are in different block limits. if ( !startBlockLimit.equals( endBlockLimit ) ) return null; // Get or fix current blocks. if ( blockTag != 'br' ) { if ( !startBlock ) { startBlock = this.fixBlock( true, blockTag ); endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block; } if ( !endBlock ) endBlock = this.fixBlock( false, blockTag ); } // Get the range position. var isStartOfBlock = startBlock && this.checkStartOfBlock(), isEndOfBlock = endBlock && this.checkEndOfBlock(); // Delete the current contents. // TODO: Why is 2.x doing CheckIsEmpty()? this.deleteContents(); if ( startBlock && startBlock.equals( endBlock ) ) { if ( isEndOfBlock ) { elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ); this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END ); endBlock = null; } else if ( isStartOfBlock ) { elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ); this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START ); startBlock = null; } else { endBlock = this.splitElement( startBlock ); // In Gecko, the last child node must be a bogusText
`, the start editing point is * `^ Text
` (inside ``). * * @param {CKEDITOR.dom.element} el The element into which look for the * editing spot. * @param {Boolean} isMoveToEnd Whether move to the end editable position. * @returns {Boolean} Whether range was moved. */ moveToElementEditablePosition: function( el, isMoveToEnd ) { function nextDFS( node, childOnly ) { var next; if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) ) next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval ); if ( !childOnly && !next ) next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval ); return next; } // Handle non-editable element e.g. HR. if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) { this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ); return true; } var found = 0; while ( el ) { // Stop immediately if we've found a text node. if ( el.type == CKEDITOR.NODE_TEXT ) { // Put cursor before block filler. if ( isMoveToEnd && this.endContainer && 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 ); 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.endContainer && this.checkEndOfBlock() ) this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START ); // Special case - non-editable block. Select entire element, because it does not make sense // to place collapsed selection next to it, because browsers can't handle that. else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) { this.setStartBefore( el ); this.setEndAfter( el ); return true; } } el = nextDFS( el, found ); } return !!found; }, /** * Moves the range boundaries to the closest editing point after/before an * element. * * For example, if the start element has `id="start"`, * `foostart
`, the closest previous editing point is * `foo^start
` (between `` and ``). * * See also: {@link #moveToElementEditablePosition}. * * @since 4.3 * @param {CKEDITOR.dom.element} element The starting element. * @param {Boolean} isMoveToEnd Whether move to the end of editable. Otherwise, look back. * @returns {Boolean} Whether the range was moved. */ moveToClosestEditablePosition: function( element, isMoveToEnd ) { // We don't want to modify original range if there's no editable position. var range = new CKEDITOR.dom.range( this.root ), found = 0, sibling, positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ]; // Set collapsed range at one of ends of element. range.moveToPosition( element, positions[ isMoveToEnd ? 0 : 1 ] ); // Start element isn't a block, so we can automatically place range // next to it. if ( !element.is( CKEDITOR.dtd.$block ) ) found = 1; else { // Look for first node that fulfills eval function and place range next to it. sibling = range[ isMoveToEnd ? 'getNextEditableNode' : 'getPreviousEditableNode' ](); if ( sibling ) { found = 1; // Special case - eval accepts block element only if it's a non-editable block, // which we want to select, not place collapsed selection next to it (which browsers // can't handle). if ( sibling.type == CKEDITOR.NODE_ELEMENT && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) { range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START ); range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END ); } else range.moveToPosition( sibling, positions[ isMoveToEnd ? 1 : 0 ] ); } } if ( found ) this.moveToRange( range ); return !!found; }, /** * See {@link #moveToElementEditablePosition}. * * @returns {Boolean} Whether range was moved. */ moveToElementEditStart: function( target ) { return this.moveToElementEditablePosition( target ); }, /** * See {@link #moveToElementEditablePosition}. * * @returns {Boolean} Whether range was moved. */ moveToElementEditEnd: function( target ) { return this.moveToElementEditablePosition( target, true ); }, /** * Get the single node enclosed within the range if there's one. * * @returns {CKEDITOR.dom.node} */ getEnclosedNode: function() { var walkerRange = this.clone(); // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780) walkerRange.optimize(); if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT ) return null; var walker = new CKEDITOR.dom.walker( walkerRange ), isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ), isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ); walker.evaluator = function( node ) { return isNotWhitespaces( node ) && isNotBookmarks( node ); }; var node = walker.next(); walker.reset(); return node && node.equals( walker.previous() ) ? node : null; }, /** * Get the node adjacent to the range start or {@link #startContainer}. * * @returns {CKEDITOR.dom.node} */ getTouchedStartNode: function() { var container = this.startContainer; if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) return container; return container.getChild( this.startOffset ) || container; }, /** * Get the node adjacent to the range end or {@link #endContainer}. * * @returns {CKEDITOR.dom.node} */ getTouchedEndNode: function() { var container = this.endContainer; if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) return container; return container.getChild( this.endOffset - 1 ) || container; }, /** * Gets next node which can be a container of a selection. * This methods mimics a behavior of right/left arrow keys in case of * collapsed selection. It does not return an exact position (with offset) though, * but just a selection's container. * * Note: use this method on a collapsed range. * * @since 4.3 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text} */ getNextEditableNode: getNextEditableNode(), /** * See {@link #getNextEditableNode}. * * @since 4.3 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text} */ getPreviousEditableNode: getNextEditableNode( 1 ), /** * Scrolls the start of current range into view. */ scrollIntoView: function() { // The reference element contains a zero-width space to avoid // a premature removal. The view is to be scrolled with respect // to this element. var reference = new CKEDITOR.dom.element.createFromHtml( ' ', this.document ), afterCaretNode, startContainerText, isStartText; var range = this.clone(); // Work with the range to obtain a proper caret position. range.optimize(); // Currently in a text node, so we need to split it into two // halves and put the reference between. if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) { // Keep the original content. It will be restored. startContainerText = range.startContainer.getText(); // Split the startContainer at the this position. afterCaretNode = range.startContainer.split( range.startOffset ); // Insert the reference between two text nodes. reference.insertAfter( range.startContainer ); } // If not in a text node, simply insert the reference into the range. else range.insertNode( reference ); // Scroll with respect to the reference element. reference.scrollIntoView(); // Get rid of split parts if "in a text node" case. // Revert the original text of the startContainer. if ( isStartText ) { range.startContainer.setText( startContainerText ); afterCaretNode.remove(); } // Get rid of the reference node. It is no longer necessary. reference.remove(); } }; } )(); /** * Indicates a position after start of a node. * * // When used according to an element: * //