2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 // This function is to be called under a "walker" instance scope.
8 function iterate( rtl
, breakOnFalse
) {
9 var range
= this.range
;
11 // Return null if we have reached the end.
15 // This is the first call. Initialize it.
16 if ( !this._
.start
) {
19 // A collapsed range must return null at first call.
20 if ( range
.collapsed
) {
25 // Move outside of text node edges.
30 startCt
= range
.startContainer
,
31 endCt
= range
.endContainer
,
32 startOffset
= range
.startOffset
,
33 endOffset
= range
.endOffset
,
35 userGuard
= this.guard
,
37 getSourceNodeFn
= ( rtl
? 'getPreviousSourceNode' : 'getNextSourceNode' );
39 // Create the LTR guard function, if necessary.
40 if ( !rtl
&& !this._
.guardLTR
) {
41 // The node that stops walker from moving up.
42 var limitLTR
= endCt
.type
== CKEDITOR
.NODE_ELEMENT
? endCt : endCt
.getParent();
44 // The node that stops the walker from going to next.
45 var blockerLTR
= endCt
.type
== CKEDITOR
.NODE_ELEMENT
? endCt
.getChild( endOffset
) : endCt
.getNext();
47 this._
.guardLTR = function( node
, movingOut
) {
48 return ( ( !movingOut
|| !limitLTR
.equals( node
) ) && ( !blockerLTR
|| !node
.equals( blockerLTR
) ) && ( node
.type
!= CKEDITOR
.NODE_ELEMENT
|| !movingOut
|| !node
.equals( range
.root
) ) );
52 // Create the RTL guard function, if necessary.
53 if ( rtl
&& !this._
.guardRTL
) {
54 // The node that stops walker from moving up.
55 var limitRTL
= startCt
.type
== CKEDITOR
.NODE_ELEMENT
? startCt : startCt
.getParent();
57 // The node that stops the walker from going to next.
58 var blockerRTL
= startCt
.type
== CKEDITOR
.NODE_ELEMENT
? startOffset
? startCt
.getChild( startOffset
- 1 ) : null : startCt
.getPrevious();
60 this._
.guardRTL = function( node
, movingOut
) {
61 return ( ( !movingOut
|| !limitRTL
.equals( node
) ) && ( !blockerRTL
|| !node
.equals( blockerRTL
) ) && ( node
.type
!= CKEDITOR
.NODE_ELEMENT
|| !movingOut
|| !node
.equals( range
.root
) ) );
65 // Define which guard function to use.
66 var stopGuard
= rtl
? this._
.guardRTL : this._
.guardLTR
;
68 // Make the user defined guard function participate in the process,
69 // otherwise simply use the boundary guard.
71 guard = function( node
, movingOut
) {
72 if ( stopGuard( node
, movingOut
) === false )
75 return userGuard( node
, movingOut
);
82 node
= this.current
[ getSourceNodeFn
]( false, type
, guard
);
84 // Get the first node to be returned.
88 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
) {
90 node
= node
.getChild( endOffset
- 1 );
92 node
= ( guard( node
, true ) === false ) ? null : node
.getPreviousSourceNode( true, type
, guard
);
97 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
) {
98 if ( !( node
= node
.getChild( startOffset
) ) )
99 node
= ( guard( startCt
, true ) === false ) ? null : startCt
.getNextSourceNode( true, type
, guard
);
103 if ( node
&& guard( node
) === false )
107 while ( node
&& !this._
.end
) {
110 if ( !this.evaluator
|| this.evaluator( node
) !== false ) {
113 } else if ( breakOnFalse
&& this.evaluator
) {
117 node
= node
[ getSourceNodeFn
]( false, type
, guard
);
121 return this.current
= null;
124 function iterateToLast( rtl
) {
128 while ( ( node
= iterate
.call( this, rtl
) ) )
135 * Utility class to "walk" the DOM inside range boundaries. If the
136 * range starts or ends in the middle of the text node, this node will
137 * be included as a whole. Outside changes to the range may break the walker.
139 * The walker may return nodes that are not totally included in the
140 * range boundaries. Let us take the following range representation,
141 * where the square brackets indicate the boundaries:
143 * [<p>Some <b>sample] text</b>
145 * While walking forward into the above range, the following nodes are
146 * returned: `<p>`, `"Some "`, `<b>` and `"sample"`. Going
147 * backwards instead we have: `"sample"` and `"Some "`. So note that the
148 * walker always returns nodes when "entering" them, but not when
149 * "leaving" them. The {@link #guard} function is instead called both when
150 * entering and when leaving nodes.
154 CKEDITOR
.dom
.walker
= CKEDITOR
.tools
.createClass( {
156 * Creates a walker class instance.
159 * @param {CKEDITOR.dom.range} range The range within which to walk.
161 $: function( range
) {
165 * A function executed for every matched node to check whether
166 * it is to be considered in the walk or not. If not provided, all
167 * matched nodes are considered good.
169 * If the function returns `false`, the node is ignored.
171 * @property {Function} evaluator
173 // this.evaluator = null;
176 * A function executed for every node the walk passes by to check
177 * whether the walk is to be finished. It is called both when
178 * entering and when exiting nodes, as well as for the matched nodes.
180 * If this function returns `false`, the walking ends and no more
181 * nodes are evaluated.
183 * @property {Function} guard
185 // this.guard = null;
193 // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes.
194 // * @param {CKEDITOR.dom.node} startNode The node from which the walk
196 // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered
197 // * in the walk. No more nodes are retrieved after touching or
198 // * passing it. If not provided, the walker stops at the
199 // * <body> closing boundary.
200 // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the
203 // createOnNodes : function( startNode, endNode, startInclusive, endInclusive )
205 // var range = new CKEDITOR.dom.range();
207 // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ;
209 // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ;
212 // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ;
214 // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ;
216 // return new CKEDITOR.dom.walker( range );
222 * Stops walking. No more nodes are retrieved if this function is called.
229 * Retrieves the next node (on the right).
231 * @returns {CKEDITOR.dom.node} The next node or `null` if no more
232 * nodes are available.
235 return iterate
.call( this );
239 * Retrieves the previous node (on the left).
241 * @returns {CKEDITOR.dom.node} The previous node or `null` if no more
242 * nodes are available.
244 previous: function() {
245 return iterate
.call( this, 1 );
249 * Checks all nodes on the right, executing the evaluation function.
251 * @returns {Boolean} `false` if the evaluator function returned
252 * `false` for any of the matched nodes. Otherwise `true`.
254 checkForward: function() {
255 return iterate
.call( this, 0, 1 ) !== false;
259 * Check all nodes on the left, executing the evaluation function.
261 * @returns {Boolean} `false` if the evaluator function returned
262 * `false` for any of the matched nodes. Otherwise `true`.
264 checkBackward: function() {
265 return iterate
.call( this, 1, 1 ) !== false;
269 * Executes a full walk forward (to the right), until no more nodes
270 * are available, returning the last valid node.
272 * @returns {CKEDITOR.dom.node} The last node on the right or `null`
273 * if no valid nodes are available.
275 lastForward: function() {
276 return iterateToLast
.call( this );
280 * Executes a full walk backwards (to the left), until no more nodes
281 * are available, returning the last valid node.
283 * @returns {CKEDITOR.dom.node} The last node on the left or `null`
284 * if no valid nodes are available.
286 lastBackward: function() {
287 return iterateToLast
.call( this, 1 );
301 // Anything whose display computed style is block, list-item, table,
302 // table-row-group, table-header-group, table-footer-group, table-row,
303 // table-column-group, table-column, table-cell, table-caption, or whose node
304 // name is hr, br (when enterMode is br only) is a block boundary.
305 var blockBoundaryDisplayMatch
= {
306 block: 1, 'list-item': 1, table: 1, 'table-row-group': 1,
307 'table-header-group': 1, 'table-footer-group': 1, 'table-row': 1, 'table-column-group': 1,
308 'table-column': 1, 'table-cell': 1, 'table-caption': 1
310 outOfFlowPositions
= { absolute: 1, fixed: 1 };
313 * Checks whether an element is displayed as a block.
315 * @member CKEDITOR.dom.element
316 * @param [customNodeNames] Custom list of nodes which will extend
317 * the default {@link CKEDITOR.dtd#$block} list.
320 CKEDITOR
.dom
.element
.prototype.isBlockBoundary = function( customNodeNames
) {
321 // Whether element is in normal page flow. Floated or positioned elements are out of page flow.
322 // Don't consider floated or positioned formatting as block boundary, fall back to dtd check in that case. (#6297)
323 var inPageFlow
= this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions
);
325 if ( inPageFlow
&& blockBoundaryDisplayMatch
[ this.getComputedStyle( 'display' ) ] )
328 // Either in $block or in customNodeNames if defined.
329 return !!( this.is( CKEDITOR
.dtd
.$block
) || customNodeNames
&& this.is( customNodeNames
) );
333 * Returns a function which checks whether the node is a block boundary.
334 * See {@link CKEDITOR.dom.element#isBlockBoundary}.
337 * @param customNodeNames
338 * @returns {Function}
340 CKEDITOR
.dom
.walker
.blockBoundary = function( customNodeNames
) {
341 return function( node
) {
342 return !( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.isBlockBoundary( customNodeNames
) );
350 CKEDITOR
.dom
.walker
.listItemBoundary = function() {
351 return this.blockBoundary( { br: 1 } );
355 * Returns a function which checks whether the node is a bookmark node or the bookmark node
359 * @param {Boolean} [contentOnly=false] Whether only test against the text content of
360 * a bookmark node instead of the element itself (default).
361 * @param {Boolean} [isReject=false] Whether to return `false` for the bookmark
362 * node instead of `true` (default).
363 * @returns {Function}
365 CKEDITOR
.dom
.walker
.bookmark = function( contentOnly
, isReject
) {
366 function isBookmarkNode( node
) {
367 return ( node
&& node
.getName
&& node
.getName() == 'span' && node
.data( 'cke-bookmark' ) );
370 return function( node
) {
371 var isBookmark
, parent
;
372 // Is bookmark inner text node?
373 isBookmark
= ( node
&& node
.type
!= CKEDITOR
.NODE_ELEMENT
&& ( parent
= node
.getParent() ) && isBookmarkNode( parent
) );
375 isBookmark
= contentOnly
? isBookmark : isBookmark
|| isBookmarkNode( node
);
376 return !!( isReject
^ isBookmark
);
381 * Returns a function which checks whether the node is a text node containing only whitespace characters.
384 * @param {Boolean} [isReject=false]
385 * @returns {Function}
387 CKEDITOR
.dom
.walker
.whitespaces = function( isReject
) {
388 return function( node
) {
390 if ( node
&& node
.type
== CKEDITOR
.NODE_TEXT
) {
391 // Whitespace, as well as the Filling Char Sequence text node used in Webkit. (#9384, #13816)
392 isWhitespace
= !CKEDITOR
.tools
.trim( node
.getText() ) ||
393 CKEDITOR
.env
.webkit
&& node
.getText() == CKEDITOR
.dom
.selection
.FILLING_CHAR_SEQUENCE
;
396 return !!( isReject
^ isWhitespace
);
401 * Returns a function which checks whether the node is invisible in the WYSIWYG mode.
404 * @param {Boolean} [isReject=false]
405 * @returns {Function}
407 CKEDITOR
.dom
.walker
.invisible = function( isReject
) {
408 var whitespace
= CKEDITOR
.dom
.walker
.whitespaces(),
409 // #12221 (Chrome) plus #11111 (Safari).
410 offsetWidth0
= CKEDITOR
.env
.webkit
? 1 : 0;
412 return function( node
) {
415 if ( whitespace( node
) )
418 // Visibility should be checked on element.
419 if ( node
.type
== CKEDITOR
.NODE_TEXT
)
420 node
= node
.getParent();
422 // Nodes that take no spaces in wysiwyg:
423 // 1. White-spaces but not including NBSP.
424 // 2. Empty inline elements, e.g. <b></b>.
425 // 3. <br> elements (bogus, surrounded by text) (#12423).
426 invisible
= node
.$.offsetWidth
<= offsetWidth0
;
429 return !!( isReject
^ invisible
);
434 * Returns a function which checks whether the node type is equal to the passed one.
437 * @param {Number} type
438 * @param {Boolean} [isReject=false]
439 * @returns {Function}
441 CKEDITOR
.dom
.walker
.nodeType = function( type
, isReject
) {
442 return function( node
) {
443 return !!( isReject
^ ( node
.type
== type
) );
448 * Returns a function which checks whether the node is a bogus (filler) node from
449 * `contenteditable` element's point of view.
452 * @param {Boolean} [isReject=false]
453 * @returns {Function}
455 CKEDITOR
.dom
.walker
.bogus = function( isReject
) {
456 function nonEmpty( node
) {
457 return !isWhitespaces( node
) && !isBookmark( node
);
460 return function( node
) {
461 var isBogus
= CKEDITOR
.env
.needsBrFiller
? node
.is
&& node
.is( 'br' ) : node
.getText
&& tailNbspRegex
.test( node
.getText() );
464 var parent
= node
.getParent(),
465 next
= node
.getNext( nonEmpty
);
467 isBogus
= parent
.isBlockBoundary() && ( !next
|| next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.isBlockBoundary() );
470 return !!( isReject
^ isBogus
);
475 * Returns a function which checks whether the node is a temporary element
476 * (element with the `data-cke-temp` attribute) or its child.
480 * @param {Boolean} [isReject=false] Whether to return `false` for the
481 * temporary element instead of `true` (default).
482 * @returns {Function}
484 CKEDITOR
.dom
.walker
.temp = function( isReject
) {
485 return function( node
) {
486 if ( node
.type
!= CKEDITOR
.NODE_ELEMENT
)
487 node
= node
.getParent();
489 var isTemp
= node
&& node
.hasAttribute( 'data-cke-temp' );
491 return !!( isReject
^ isTemp
);
495 var tailNbspRegex
= /^[\t\r\n ]*(?: 
;|\xa0)$/,
496 isWhitespaces
= CKEDITOR
.dom
.walker
.whitespaces(),
497 isBookmark
= CKEDITOR
.dom
.walker
.bookmark(),
498 isTemp
= CKEDITOR
.dom
.walker
.temp(),
499 toSkip = function( node
) {
500 return isBookmark( node
) ||
501 isWhitespaces( node
) ||
502 node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( CKEDITOR
.dtd
.$inline
) && !node
.is( CKEDITOR
.dtd
.$empty
);
506 * Returns a function which checks whether the node should be ignored in terms of "editability".
510 * * whitespaces (see {@link CKEDITOR.dom.walker#whitespaces}),
511 * * bookmarks (see {@link CKEDITOR.dom.walker#bookmark}),
512 * * temporary elements (see {@link CKEDITOR.dom.walker#temp}).
516 * @param {Boolean} [isReject=false] Whether to return `false` for the
517 * ignored element instead of `true` (default).
518 * @returns {Function}
520 CKEDITOR
.dom
.walker
.ignored = function( isReject
) {
521 return function( node
) {
522 var isIgnored
= isWhitespaces( node
) || isBookmark( node
) || isTemp( node
);
524 return !!( isReject
^ isIgnored
);
528 var isIgnored
= CKEDITOR
.dom
.walker
.ignored();
531 * Returns a function which checks whether the node is empty.
535 * @param {Boolean} [isReject=false] Whether to return `false` for the
536 * ignored element instead of `true` (default).
537 * @returns {Function}
539 CKEDITOR
.dom
.walker
.empty = function( isReject
) {
540 return function( node
) {
542 l
= node
.getChildCount();
544 for ( ; i
< l
; ++i
) {
545 if ( !isIgnored( node
.getChild( i
) ) ) {
554 var isEmpty
= CKEDITOR
.dom
.walker
.empty();
556 function filterTextContainers( dtd
) {
560 for ( name
in dtd
) {
561 if ( CKEDITOR
.dtd
[ name
][ '#' ] )
568 * A hash of element names which in browsers that {@link CKEDITOR.env#needsBrFiller do not need `<br>` fillers}
569 * can be selection containers despite being empty.
573 * @property {Object} validEmptyBlockContainers
575 var validEmptyBlocks
= CKEDITOR
.dom
.walker
.validEmptyBlockContainers
= CKEDITOR
.tools
.extend(
576 filterTextContainers( CKEDITOR
.dtd
.$block
),
577 { caption: 1, td: 1, th: 1 }
580 function isEditable( node
) {
581 // Skip temporary elements, bookmarks and whitespaces.
582 if ( isIgnored( node
) )
585 if ( node
.type
== CKEDITOR
.NODE_TEXT
)
588 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
) {
589 // All inline and non-editable elements are valid editable places.
590 // Note: the <hr> is currently the only element in CKEDITOR.dtd.$empty and CKEDITOR.dtd.$block,
591 // but generally speaking we need an intersection of these two sets.
592 // Note: non-editable block has to be treated differently (should be selected entirely).
593 if ( node
.is( CKEDITOR
.dtd
.$inline
) || node
.is( 'hr' ) || node
.getAttribute( 'contenteditable' ) == 'false' )
596 // Empty blocks are editable on IE.
597 if ( !CKEDITOR
.env
.needsBrFiller
&& node
.is( validEmptyBlocks
) && isEmpty( node
) )
601 // Skip all other nodes.
606 * Returns a function which checks whether the node can be a container or a sibling
607 * of the selection end.
611 * * text nodes (but not whitespaces),
613 * * intersection of {@link CKEDITOR.dtd#$empty} and {@link CKEDITOR.dtd#$block} (currently
614 * it is only `<hr>`),
615 * * non-editable blocks (special case — such blocks cannot be containers nor
616 * siblings, they need to be selected entirely),
617 * * empty {@link #validEmptyBlockContainers blocks} which can contain text
618 * ({@link CKEDITOR.env#needsBrFiller old IEs only}).
622 * @param {Boolean} [isReject=false] Whether to return `false` for the
623 * ignored element instead of `true` (default).
624 * @returns {Function}
626 CKEDITOR
.dom
.walker
.editable = function( isReject
) {
627 return function( node
) {
628 return !!( isReject
^ isEditable( node
) );
633 * Checks if there is a filler node at the end of an element, and returns it.
635 * @member CKEDITOR.dom.element
636 * @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`.
638 CKEDITOR
.dom
.element
.prototype.getBogus = function() {
639 // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070).
642 tail
= tail
.getPreviousSourceNode();
644 while ( toSkip( tail
) );
646 if ( tail
&& ( CKEDITOR
.env
.needsBrFiller
? tail
.is
&& tail
.is( 'br' ) : tail
.getText
&& tailNbspRegex
.test( tail
.getText() ) ) )