summaryrefslogtreecommitdiff
path: root/core/dom/walker.js
diff options
context:
space:
mode:
Diffstat (limited to 'core/dom/walker.js')
-rw-r--r--core/dom/walker.js477
1 files changed, 477 insertions, 0 deletions
diff --git a/core/dom/walker.js b/core/dom/walker.js
new file mode 100644
index 0000000..0f360e0
--- /dev/null
+++ b/core/dom/walker.js
@@ -0,0 +1,477 @@
+/**
+ * @license Copyright (c) 2003-2013, 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 ) {
+ var range = this.range;
+
+ // Return null if we have reached the end.
+ if ( this._.end )
+ return null;
+
+ // This is the first call. Initialize it.
+ if ( !this._.start ) {
+ this._.start = 1;
+
+ // A collapsed range must return null at first call.
+ if ( range.collapsed ) {
+ this.end();
+ return null;
+ }
+
+ // Move outside of text node edges.
+ range.optimize();
+ }
+
+ var node,
+ startCt = range.startContainer,
+ endCt = range.endContainer,
+ startOffset = range.startOffset,
+ endOffset = range.endOffset,
+ guard,
+ userGuard = this.guard,
+ type = this.type,
+ getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' );
+
+ // Create the LTR guard function, if necessary.
+ if ( !rtl && !this._.guardLTR ) {
+ // The node that stops walker from moving up.
+ var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt : endCt.getParent();
+
+ // The node that stops the walker from going to next.
+ var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt.getChild( endOffset ) : endCt.getNext();
+
+ this._.guardLTR = function( node, movingOut ) {
+ return ( ( !movingOut || !limitLTR.equals( node ) ) && ( !blockerLTR || !node.equals( blockerLTR ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
+ };
+ }
+
+ // Create the RTL guard function, if necessary.
+ if ( rtl && !this._.guardRTL ) {
+ // The node that stops walker from moving up.
+ var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startCt : startCt.getParent();
+
+ // The node that stops the walker from going to next.
+ var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startOffset ? startCt.getChild( startOffset - 1 ) : null : startCt.getPrevious();
+
+ this._.guardRTL = function( node, movingOut ) {
+ return ( ( !movingOut || !limitRTL.equals( node ) ) && ( !blockerRTL || !node.equals( blockerRTL ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
+ };
+ }
+
+ // 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 = endCt;
+
+ if ( node.type == CKEDITOR.NODE_ELEMENT ) {
+ if ( endOffset > 0 )
+ node = node.getChild( endOffset - 1 );
+ else
+ node = ( guard( node, true ) === false ) ? null : node.getPreviousSourceNode( true, type, guard );
+ }
+ } else {
+ node = startCt;
+
+ if ( node.type == CKEDITOR.NODE_ELEMENT ) {
+ if ( !( node = node.getChild( startOffset ) ) )
+ node = ( guard( startCt, true ) === false ) ? null : startCt.getNextSourceNode( true, type, guard );
+ }
+ }
+
+ if ( node && guard( node ) === false )
+ node = null;
+ }
+
+ 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;
+ }
+
+ /**
+ * 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.
+ *
+ * @class
+ */
+ CKEDITOR.dom.walker = CKEDITOR.tools.createClass({
+ /**
+ * Creates a walker class instance.
+ *
+ * @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.
+ *
+ * @property {Function} evaluator
+ */
+ // 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.
+
+ * @property {Function} guard
+ */
+ // 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
+ // * &lt;body&gt; 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: {
+ /**
+ * Stops 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 );
+ },
+
+ /**
+ * Resets walker.
+ */
+ 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 };
+
+ /**
+ * @member CKEDITOR.dom.element
+ * @todo
+ */
+ CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) {
+ var nodeNameMatches = customNodeNames ? CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$block, customNodeNames || {} ) : CKEDITOR.dtd.$block;
+
+ // 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() ];
+ };
+
+ /**
+ * @static
+ * @todo
+ */
+ CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) {
+ return function( node, type ) {
+ return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( customNodeNames ) );
+ };
+ };
+
+ /**
+ * @static
+ * @todo
+ */
+ 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.
+ *
+ * @static
+ * @param {Boolean} [contentOnly=false] Whether only test against the text content of
+ * bookmark node instead of the element itself (default).
+ * @param {Boolean} [isReject=false] Whether should return `false` for the bookmark
+ * node instead of `true` (default).
+ * @returns {Function}
+ */
+ 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.type != CKEDITOR.NODE_ELEMENT && ( 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.
+ *
+ * @static
+ * @param {Boolean} [isReject=false]
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.whitespaces = function( isReject ) {
+ return function( node ) {
+ var isWhitespace;
+ if ( node && node.type == CKEDITOR.NODE_TEXT ) {
+ // whitespace, as well as the text cursor filler node we used in Webkit. (#9384)
+ isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
+ CKEDITOR.env.webkit && node.getText() == '\u200b';
+ }
+
+ return !! ( isReject ^ isWhitespace );
+ };
+ };
+
+ /**
+ * Whether the node is invisible in wysiwyg mode.
+ *
+ * @static
+ * @param {Boolean} [isReject=false]
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.invisible = function( isReject ) {
+ var whitespace = CKEDITOR.dom.walker.whitespaces();
+ return function( node ) {
+ var invisible;
+
+ if ( whitespace( node ) )
+ invisible = 1;
+ else {
+ // Visibility should be checked on element.
+ if ( node.type == CKEDITOR.NODE_TEXT )
+ node = node.getParent();
+
+ // Nodes that take no spaces in wysiwyg:
+ // 1. White-spaces but not including NBSP;
+ // 2. Empty inline elements, e.g. <b></b> we're checking here
+ // 'offsetHeight' instead of 'offsetWidth' for properly excluding
+ // all sorts of empty paragraph, e.g. <br />.
+ invisible = !node.$.offsetHeight;
+ }
+
+ return !!( isReject ^ invisible );
+ };
+ };
+
+ /**
+ * @static
+ * @param {Number} type
+ * @param {Boolean} [isReject=false]
+ * @returns {Function}
+ * @todo
+ */
+ CKEDITOR.dom.walker.nodeType = function( type, isReject ) {
+ return function( node ) {
+ return !!( isReject ^ ( node.type == type ) );
+ };
+ };
+
+ /**
+ * @static
+ * @param {Boolean} [isReject=false]
+ * @returns {Function}
+ * @todo
+ */
+ CKEDITOR.dom.walker.bogus = function( isReject ) {
+ function nonEmpty( node ) {
+ return !isWhitespaces( node ) && !isBookmark( node );
+ }
+
+ return function( node ) {
+ var isBogus = !CKEDITOR.env.ie ? node.is && node.is( 'br' ) : node.getText && tailNbspRegex.test( node.getText() );
+
+ if ( isBogus ) {
+ var parent = node.getParent(),
+ next = node.getNext( nonEmpty );
+
+ isBogus = parent.isBlockBoundary() && ( !next || next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() );
+ }
+
+ return !!( isReject ^ isBogus );
+ };
+ };
+
+ var tailNbspRegex = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,
+ isWhitespaces = CKEDITOR.dom.walker.whitespaces(),
+ isBookmark = CKEDITOR.dom.walker.bookmark(),
+ toSkip = function( node ) {
+ return isBookmark( node ) || isWhitespaces( node ) || node.type == CKEDITOR.NODE_ELEMENT && node.getName() in CKEDITOR.dtd.$inline && !( node.getName() in CKEDITOR.dtd.$empty );
+ };
+
+ /**
+ * Check if there's a filler node at the end of an element, and return it.
+ *
+ * @member CKEDITOR.dom.element
+ * @returns {Boolean}
+ */
+ CKEDITOR.dom.element.prototype.getBogus = function() {
+ // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070).
+ var tail = this;
+ do {
+ tail = tail.getPreviousSourceNode();
+ }
+ while ( toSkip( tail ) )
+
+ if ( tail && ( !CKEDITOR.env.ie ? tail.is && tail.is( 'br' ) : tail.getText && tailNbspRegex.test( tail.getText() ) ) ) {
+ return tail;
+ }
+ return false;
+ };
+
+})();