/* Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.html or http://ckeditor.com/license */ (function() { // This function is to be called under a "walker" instance scope. function iterate( rtl, breakOnFalse ) { // Return null if we have reached the end. if ( this._.end ) return null; var node, range = this.range, guard, userGuard = this.guard, type = this.type, getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' ); // This is the first call. Initialize it. if ( !this._.start ) { this._.start = 1; // Trim text nodes and optmize the range boundaries. DOM changes // may happen at this point. range.trim(); // A collapsed range must return null at first call. if ( range.collapsed ) { this.end(); return null; } } // Create the LTR guard function, if necessary. if ( !rtl && !this._.guardLTR ) { // Gets the node that stops the walker when going LTR. var limitLTR = range.endContainer, blockerLTR = limitLTR.getChild( range.endOffset ); this._.guardLTR = function( node, movingOut ) { return ( ( !movingOut || !limitLTR.equals( node ) ) && ( !blockerLTR || !node.equals( blockerLTR ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || node.getName() != 'body' ) ); }; } // Create the RTL guard function, if necessary. if ( rtl && !this._.guardRTL ) { // Gets the node that stops the walker when going LTR. var limitRTL = range.startContainer, blockerRTL = ( range.startOffset > 0 ) && limitRTL.getChild( range.startOffset - 1 ); this._.guardRTL = function( node, movingOut ) { return ( ( !movingOut || !limitRTL.equals( node ) ) && ( !blockerRTL || !node.equals( blockerRTL ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || node.getName() != 'body' ) ); }; } // Define which guard function to use. var stopGuard = rtl ? this._.guardRTL : this._.guardLTR; // Make the user defined guard function participate in the process, // otherwise simply use the boundary guard. if ( userGuard ) { guard = function( node, movingOut ) { if ( stopGuard( node, movingOut ) === false ) return false; return userGuard( node, movingOut ); }; } else guard = stopGuard; if ( this.current ) node = this.current[ getSourceNodeFn ]( false, type, guard ); else { // Get the first node to be returned. if ( rtl ) { node = range.endContainer; if ( range.endOffset > 0 ) { node = node.getChild( range.endOffset - 1 ); if ( guard( node ) === false ) node = null; } else node = ( guard ( node, true ) === false ) ? null : node.getPreviousSourceNode( true, type, guard ); } else { node = range.startContainer; node = node.getChild( range.startOffset ); if ( node ) { if ( guard( node ) === false ) node = null; } else node = ( guard ( range.startContainer, true ) === false ) ? null : range.startContainer.getNextSourceNode( true, type, guard ) ; } } while ( node && !this._.end ) { this.current = node; if ( !this.evaluator || this.evaluator( node ) !== false ) { if ( !breakOnFalse ) return node; } else if ( breakOnFalse && this.evaluator ) return false; node = node[ getSourceNodeFn ]( false, type, guard ); } this.end(); return this.current = null; } function iterateToLast( rtl ) { var node, last = null; while ( ( node = iterate.call( this, rtl ) ) ) last = node; return last; } CKEDITOR.dom.walker = CKEDITOR.tools.createClass( { /** * Utility class to "walk" the DOM inside a range boundaries. If * necessary, partially included nodes (text nodes) are broken to * reflect the boundaries limits, so DOM and range changes may happen. * Outside changes to the range may break the walker. * * The walker may return nodes that are not totaly included into the * range boundaires. Let's take the following range representation, * where the square brackets indicate the boundaries: * * [<p>Some <b>sample] text</b> * * While walking forward into the above range, the following nodes are * returned: <p>, "Some ", <b> and "sample". Going * backwards instead we have: "sample" and "Some ". So note that the * walker always returns nodes when "entering" them, but not when * "leaving" them. The guard function is instead called both when * entering and leaving nodes. * * @constructor * @param {CKEDITOR.dom.range} range The range within which walk. */ $ : function( range ) { this.range = range; /** * A function executed for every matched node, to check whether * it's to be considered into the walk or not. If not provided, all * matched nodes are considered good. * If the function returns "false" the node is ignored. * @name CKEDITOR.dom.walker.prototype.evaluator * @property * @type Function */ // this.evaluator = null; /** * A function executed for every node the walk pass by to check * whether the walk is to be finished. It's called when both * entering and exiting nodes, as well as for the matched nodes. * If this function returns "false", the walking ends and no more * nodes are evaluated. * @name CKEDITOR.dom.walker.prototype.guard * @property * @type Function */ // this.guard = null; /** @private */ this._ = {}; }, // statics : // { // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes. // * @param {CKEDITOR.dom.node} startNode The node from wich the walk // * will start. // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered // * in the walk. No more nodes are retrieved after touching or // * passing it. If not provided, the walker stops at the // * <body> closing boundary. // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the // * provided nodes. // */ // createOnNodes : function( startNode, endNode, startInclusive, endInclusive ) // { // var range = new CKEDITOR.dom.range(); // if ( startNode ) // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ; // else // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ; // // if ( endNode ) // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ; // else // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ; // // return new CKEDITOR.dom.walker( range ); // } // }, // proto : { /** * Stop walking. No more nodes are retrieved if this function gets * called. */ end : function() { this._.end = 1; }, /** * Retrieves the next node (at right). * @returns {CKEDITOR.dom.node} The next node or null if no more * nodes are available. */ next : function() { return iterate.call( this ); }, /** * Retrieves the previous node (at left). * @returns {CKEDITOR.dom.node} The previous node or null if no more * nodes are available. */ previous : function() { return iterate.call( this, 1 ); }, /** * Check all nodes at right, executing the evaluation fuction. * @returns {Boolean} "false" if the evaluator function returned * "false" for any of the matched nodes. Otherwise "true". */ checkForward : function() { return iterate.call( this, 0, 1 ) !== false; }, /** * Check all nodes at left, executing the evaluation fuction. * @returns {Boolean} "false" if the evaluator function returned * "false" for any of the matched nodes. Otherwise "true". */ checkBackward : function() { return iterate.call( this, 1, 1 ) !== false; }, /** * Executes a full walk forward (to the right), until no more nodes * are available, returning the last valid node. * @returns {CKEDITOR.dom.node} The last node at the right or null * if no valid nodes are available. */ lastForward : function() { return iterateToLast.call( this ); }, /** * Executes a full walk backwards (to the left), until no more nodes * are available, returning the last valid node. * @returns {CKEDITOR.dom.node} The last node at the left or null * if no valid nodes are available. */ lastBackward : function() { return iterateToLast.call( this, 1 ); }, reset : function() { delete this.current; this._ = {}; } } }); /* * Anything whose display computed style is block, list-item, table, * table-row-group, table-header-group, table-footer-group, table-row, * table-column-group, table-column, table-cell, table-caption, or whose node * name is hr, br (when enterMode is br only) is a block boundary. */ var blockBoundaryDisplayMatch = { block : 1, 'list-item' : 1, table : 1, 'table-row-group' : 1, 'table-header-group' : 1, 'table-footer-group' : 1, 'table-row' : 1, 'table-column-group' : 1, 'table-column' : 1, 'table-cell' : 1, 'table-caption' : 1 }; CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) { var nodeNameMatches = CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$block, customNodeNames || {} ); // Don't consider floated formatting as block boundary, fall back to dtd check in that case. (#6297) return this.getComputedStyle( 'float' ) == 'none' && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] || nodeNameMatches[ this.getName() ]; }; CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) { return function( node , type ) { return ! ( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( customNodeNames ) ); }; }; CKEDITOR.dom.walker.listItemBoundary = function() { return this.blockBoundary( { br : 1 } ); }; /** * Whether the to-be-evaluated node is a bookmark node OR bookmark node * inner contents. * @param {Boolean} contentOnly Whether only test againt the text content of * bookmark node instead of the element itself(default). * @param {Boolean} isReject Whether should return 'false' for the bookmark * node instead of 'true'(default). */ CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) { function isBookmarkNode( node ) { return ( node && node.getName && node.getName() == 'span' && node.data( 'cke-bookmark' ) ); } return function( node ) { var isBookmark, parent; // Is bookmark inner text node? isBookmark = ( node && !node.getName && ( parent = node.getParent() ) && isBookmarkNode( parent ) ); // Is bookmark node? isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node ); return !! ( isReject ^ isBookmark ); }; }; /** * Whether the node is a text node containing only whitespaces characters. * @param isReject */ CKEDITOR.dom.walker.whitespaces = function( isReject ) { return function( node ) { var isWhitespace = node && ( node.type == CKEDITOR.NODE_TEXT ) && !CKEDITOR.tools.trim( node.getText() ); return !! ( isReject ^ isWhitespace ); }; }; /** * Whether the node is invisible in wysiwyg mode. * @param isReject */ CKEDITOR.dom.walker.invisible = function( isReject ) { var whitespace = CKEDITOR.dom.walker.whitespaces(); return function( node ) { // Nodes that take no spaces in wysiwyg: // 1. White-spaces but not including NBSP; // 2. Empty inline elements, e.g. we're checking here // 'offsetHeight' instead of 'offsetWidth' for properly excluding // all sorts of empty paragraph, e.g.
. var isInvisible = whitespace( node ) || node.is && !node.$.offsetHeight; return !! ( isReject ^ isInvisible ); }; }; var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/, isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( 1 ), isNotBookmark = CKEDITOR.dom.walker.bookmark( 0, 1 ), fillerEvaluator = function( element ) { return isNotBookmark( element ) && isNotWhitespaces( element ); }; // Check if there's a filler node at the end of an element, and return it. CKEDITOR.dom.element.prototype.getBogus = function() { var tail = this.getLast( fillerEvaluator ); if ( tail && ( !CKEDITOR.env.ie ? tail.is && tail.is( 'br' ) : tail.getText && tailNbspRegex.test( tail.getText() ) ) ) { return tail; } return false; }; })();