/* * FCKeditor - The text editor for Internet - http://www.fckeditor.net * Copyright (C) 2003-2007 Frederico Caldeira Knabben * * == BEGIN LICENSE == * * Licensed under the terms of any of the following licenses at your * choice: * * - GNU General Public License Version 2 or later (the "GPL") * http://www.gnu.org/licenses/gpl.html * * - GNU Lesser General Public License Version 2.1 or later (the "LGPL") * http://www.gnu.org/licenses/lgpl.html * * - Mozilla Public License Version 1.1 or later (the "MPL") * http://www.mozilla.org/MPL/MPL-1.1.html * * == END LICENSE == * * This class partially implements the W3C DOM Range for browser that don't * support the standards (like IE): * http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html */ var FCKW3CRange = function( parentDocument ) { this._Document = parentDocument ; this.startContainer = null ; this.startOffset = null ; this.endContainer = null ; this.endOffset = null ; this.collapsed = true ; } FCKW3CRange.CreateRange = function( parentDocument ) { // We could opt to use the Range implentation of the browsers. The problem // is that every browser have different bugs on their implementations, // mostly related to different interpretations of the W3C specifications. // So, for now, let's use our implementation and pray for browsers fixings // soon. Otherwise will go crazy on trying to find out workarounds. /* // Get the browser implementation of the range, if available. if ( parentDocument.createRange ) { var range = parentDocument.createRange() ; if ( typeof( range.startContainer ) != 'undefined' ) return range ; } */ return new FCKW3CRange( parentDocument ) ; } FCKW3CRange.CreateFromRange = function( parentDocument, sourceRange ) { var range = FCKW3CRange.CreateRange( parentDocument ) ; range.setStart( sourceRange.startContainer, sourceRange.startOffset ) ; range.setEnd( sourceRange.endContainer, sourceRange.endOffset ) ; return range ; } FCKW3CRange.prototype = { _UpdateCollapsed : function() { this.collapsed = ( this.startContainer == this.endContainer && this.startOffset == this.endOffset ) ; }, // 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. setStart : function( refNode, offset ) { this.startContainer = refNode ; this.startOffset = offset ; if ( !this.endContainer ) { this.endContainer = refNode ; this.endOffset = offset ; } this._UpdateCollapsed() ; }, // 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. setEnd : function( refNode, offset ) { this.endContainer = refNode ; this.endOffset = offset ; if ( !this.startContainer ) { this.startContainer = refNode ; this.startOffset = offset ; } this._UpdateCollapsed() ; }, setStartAfter : function( refNode ) { this.setStart( refNode.parentNode, FCKDomTools.GetIndexOf( refNode ) + 1 ) ; }, setStartBefore : function( refNode ) { this.setStart( refNode.parentNode, FCKDomTools.GetIndexOf( refNode ) ) ; }, setEndAfter : function( refNode ) { this.setEnd( refNode.parentNode, FCKDomTools.GetIndexOf( refNode ) + 1 ) ; }, setEndBefore : function( refNode ) { this.setEnd( refNode.parentNode, FCKDomTools.GetIndexOf( refNode ) ) ; }, 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 ; }, selectNodeContents : function( refNode ) { this.setStart( refNode, 0 ) ; this.setEnd( refNode, refNode.nodeType == 3 ? refNode.data.length : refNode.childNodes.length ) ; }, insertNode : function( newNode ) { var startContainer = this.startContainer ; var startOffset = this.startOffset ; // If we are in a text node. if ( startContainer.nodeType == 3 ) { startContainer.splitText( startOffset ) ; // Check if it is necessary to update the end boundary. if ( startContainer == this.endContainer ) this.setEnd( startContainer.nextSibling, this.endOffset - this.startOffset ) ; // Insert the new node it after the text node. FCKDomTools.InsertAfterNode( startContainer, newNode ) ; return ; } else { // Simply insert the new node before the current start node. startContainer.insertBefore( newNode, startContainer.childNodes[ startOffset ] || null ) ; // Check if it is necessary to update the end boundary. if ( startContainer == this.endContainer ) { this.endOffset++ ; this.collapsed = false ; } } }, deleteContents : function() { if ( this.collapsed ) return ; this._ExecContentsAction( 0 ) ; }, extractContents : function() { var docFrag = new FCKDocumentFragment( this._Document ) ; if ( !this.collapsed ) this._ExecContentsAction( 1, docFrag ) ; return docFrag ; }, // The selection may be lost when clonning (due to the splitText() call). cloneContents : function() { var docFrag = new FCKDocumentFragment( this._Document ) ; if ( !this.collapsed ) this._ExecContentsAction( 2, docFrag ) ; return docFrag ; }, _ExecContentsAction : function( action, docFrag ) { var startNode = this.startContainer ; var endNode = this.endContainer ; var startOffset = this.startOffset ; var endOffset = this.endOffset ; var removeStartNode = false ; var removeEndNode = false ; // Check the start and end nodes and make the necessary removals or changes. // Start from the end, otherwise DOM mutations (splitText) made in the // start boundary may interfere on the results here. // 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.nodeType == 3 ) endNode = endNode.splitText( endOffset ) ; else { // If the end container has children and the offset is pointing // to a child, then we should start from it. if ( endNode.childNodes.length > 0 ) { // If the offset points after the last node. if ( endOffset > endNode.childNodes.length - 1 ) { // Let's create a temporary node and mark it for removal. endNode = FCKDomTools.InsertAfterNode( endNode.lastChild, this._Document.createTextNode('') ) ; removeEndNode = true ; } else endNode = endNode.childNodes[ endOffset ] ; } } // For text containers, we must simply split the node. The removal will // be handled by the rest of the code . if ( startNode.nodeType == 3 ) { startNode.splitText( 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 == endNode ) endNode = startNode.nextSibling ; } else { // If the start container has children and the offset is pointing // to a child, then we should start from its previous sibling. if ( startNode.childNodes.length > 0 && startOffset <= startNode.childNodes.length - 1 ) { // 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 == 0 ) { // Let's create a temporary node and mark it for removal. startNode = startNode.insertBefore( this._Document.createTextNode(''), startNode.firstChild ) ; removeStartNode = true ; } else startNode = startNode.childNodes[ startOffset ].previousSibling ; } } // Get the parent nodes tree for the start and end boundaries. var startParents = FCKDomTools.GetParents( startNode ) ; var endParents = FCKDomTools.GetParents( endNode ) ; // 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 parants array for the top // most element. if ( topStart != topEnd ) break ; } var clone, levelStartNode, levelClone, currentNode, currentSibling ; if ( docFrag ) clone = docFrag.RootNode ; // 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 != startNode ) // action = 0 = Delete levelClone = clone.appendChild( levelStartNode.cloneNode( levelStartNode == startNode ) ) ; currentNode = levelStartNode.nextSibling ; while( currentNode ) { // Stop processing when the current node matches a node in the // endParents tree or if it is the endNode. if ( currentNode == endParents[j] || currentNode == endNode ) break ; // Cache the next sibling. currentSibling = currentNode.nextSibling ; // If clonning, just clone it. if ( action == 2 ) // 2 = Clone clone.appendChild( currentNode.cloneNode( true ) ) ; else { // Both Delete and Extract will remove the node. currentNode.parentNode.removeChild( currentNode ) ; // When Extracting, move the removed node to the docFrag. if ( action == 1 ) // 1 = Extract clone.appendChild( currentNode ) ; } currentNode = currentSibling ; } if ( clone ) clone = levelClone ; } if ( docFrag ) clone = docFrag.RootNode ; // 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 != endNode ) // action = 0 = Delete levelClone = clone.appendChild( levelStartNode.cloneNode( levelStartNode == endNode ) ) ; // The processing of siblings may have already been done by the parent. if ( !startParents[k] || levelStartNode.parentNode != startParents[k].parentNode ) { currentNode = levelStartNode.previousSibling ; while( currentNode ) { // Stop processing when the current node matches a node in the // startParents tree or if it is the startNode. if ( currentNode == startParents[k] || currentNode == startNode ) break ; // Cache the next sibling. currentSibling = currentNode.previousSibling ; // If clonning, 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.parentNode.removeChild( currentNode ) ; // 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 = this.startContainer ; if ( startTextNode.nodeType == 3 ) { startTextNode.data += startTextNode.nextSibling.data ; startTextNode.parentNode.removeChild( startTextNode.nextSibling ) ; } var endTextNode = this.endContainer ; if ( endTextNode.nodeType == 3 && 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 ) ) this.setStart( topEnd.parentNode, FCKDomTools.GetIndexOf( topEnd ) ) ; // Collapse it to the start. this.collapse( true ) ; } // Cleanup any marked node. if( removeStartNode ) startNode.parentNode.removeChild( startNode ) ; if( removeEndNode && endNode.parentNode ) endNode.parentNode.removeChild( endNode ) ; }, cloneRange : function() { return FCKW3CRange.CreateFromRange( this._Document, this ) ; }, toString : function() { var docFrag = this.cloneContents() ; var tmpDiv = this._Document.createElement( 'div' ) ; docFrag.AppendTo( tmpDiv ) ; return tmpDiv.textContent || tmpDiv.innerText ; } } ;