2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
8 * File overview: DOM iterator which iterates over list items, lines and paragraphs.
15 * Represents the iterator class. It can be used to iterate
16 * over all elements (or even text nodes in case of {@link #enlargeBr} set to `false`)
17 * which establish "paragraph-like" spaces within the passed range.
19 * // <h1>[foo</h1><p>bar]</p>
20 * var iterator = range.createIterator();
21 * iterator.getNextParagraph(); // h1 element
22 * iterator.getNextParagraph(); // p element
24 * // <ul><li>[foo</li><li>bar]</li>
25 * // With enforceRealBlocks set to false the iterator will return two list item elements.
26 * // With enforceRealBlocks set to true the iterator will return two paragraphs and the DOM will be changed to:
27 * // <ul><li><p>foo</p></li><li><p>bar</p></li>
29 * @class CKEDITOR.dom.iterator
30 * @constructor Creates an iterator class instance.
31 * @param {CKEDITOR.dom.range} range
33 function iterator( range
) {
34 if ( arguments
.length
< 1 )
39 * @property {CKEDITOR.dom.range}
44 * @property {Boolean} [forceBrBreak=false]
46 this.forceBrBreak
= 0;
48 // (http://dev.ckeditor.com/ticket/3730).
50 * Whether to include `<br>` elements in the enlarged range. Should be
51 * set to `false` when using the iterator in the {@link CKEDITOR#ENTER_BR} mode.
53 * @property {Boolean} [enlargeBr=true]
58 * Whether the iterator should create a transformable block
59 * if the current one contains text and cannot be transformed.
60 * For example new blocks will be established in elements like
63 * @property {Boolean} [enforceRealBlocks=false]
65 this.enforceRealBlocks
= 0;
67 this._
|| ( this._
= {} );
71 * Default iterator's filter. It is set only for nested iterators.
75 * @property {CKEDITOR.filter} filter
79 * Iterator's active filter. It is set by the {@link #getNextParagraph} method
80 * when it enters a nested editable.
84 * @property {CKEDITOR.filter} activeFilter
87 var beginWhitespaceRegex
= /^[\r\n\t ]+$/,
88 // Ignore bookmark nodes.(http://dev.ckeditor.com/ticket/3783)
89 bookmarkGuard
= CKEDITOR
.dom
.walker
.bookmark( false, true ),
90 whitespacesGuard
= CKEDITOR
.dom
.walker
.whitespaces( true ),
91 skipGuard = function( node
) {
92 return bookmarkGuard( node
) && whitespacesGuard( node
);
94 listItemNames
= { dd: 1, dt: 1, li: 1 };
96 iterator
.prototype = {
98 * Returns the next paragraph-like element or `null` if the end of a range is reached.
100 * @param {String} [blockTag='p'] Name of a block element which will be established by
101 * the iterator in block-less elements (see {@link #enforceRealBlocks}).
103 getNextParagraph: function( blockTag
) {
104 // The block element to be returned.
107 // The range object used to identify the paragraph contents.
110 // Indicats that the current element in the loop is the last one.
113 // Instructs to cleanup remaining BRs.
114 var removePreviousBr
, removeLastBr
;
116 blockTag
= blockTag
|| 'p';
118 // We're iterating over nested editable.
119 if ( this._
.nestedEditable
) {
120 // Get next block from nested iterator and returns it if was found.
121 block
= this._
.nestedEditable
.iterator
.getNextParagraph( blockTag
);
123 // Inherit activeFilter from the nested iterator.
124 this.activeFilter
= this._
.nestedEditable
.iterator
.activeFilter
;
128 // No block in nested iterator means that we reached the end of the nested editable.
129 // Reset the active filter to the default filter (or undefined if this iterator didn't have it).
130 this.activeFilter
= this.filter
;
132 // Try to find next nested editable or get back to parent (this) iterator.
133 if ( startNestedEditableIterator( this, blockTag
, this._
.nestedEditable
.container
, this._
.nestedEditable
.remaining
) ) {
134 // Inherit activeFilter from the nested iterator.
135 this.activeFilter
= this._
.nestedEditable
.iterator
.activeFilter
;
136 return this._
.nestedEditable
.iterator
.getNextParagraph( blockTag
);
138 this._
.nestedEditable
= null;
142 // Block-less range should be checked first.
143 if ( !this.range
.root
.getDtd()[ blockTag
] )
146 // This is the first iteration. Let's initialize it.
147 if ( !this._
.started
)
148 range
= startIterator
.call( this );
150 var currentNode
= this._
.nextNode
,
151 lastNode
= this._
.lastNode
;
153 this._
.nextNode
= null;
154 while ( currentNode
) {
155 // closeRange indicates that a paragraph boundary has been found,
156 // so the range can be closed.
158 parentPre
= currentNode
.hasAscendant( 'pre' );
160 // includeNode indicates that the current node is good to be part
161 // of the range. By default, any non-element node is ok for it.
162 var includeNode
= ( currentNode
.type
!= CKEDITOR
.NODE_ELEMENT
),
163 continueFromSibling
= 0;
165 // If it is an element node, let's check if it can be part of the range.
166 if ( !includeNode
) {
167 var nodeName
= currentNode
.getName();
169 // Non-editable block was found - return it and move to processing
170 // its nested editables if they exist.
171 if ( CKEDITOR
.dtd
.$block
[ nodeName
] && currentNode
.getAttribute( 'contenteditable' ) == 'false' ) {
174 // Setup iterator for first of nested editables.
175 // If there's no editable, then algorithm will move to next element after current block.
176 startNestedEditableIterator( this, blockTag
, block
);
178 // Gets us straight to the end of getParagraph() because block variable is set.
180 } else if ( currentNode
.isBlockBoundary( this.forceBrBreak
&& !parentPre
&& { br: 1 } ) ) {
181 // <br> boundaries must be part of the range. It will
182 // happen only if ForceBrBreak.
183 if ( nodeName
== 'br' )
185 else if ( !range
&& !currentNode
.getChildCount() && nodeName
!= 'hr' ) {
186 // If we have found an empty block, and haven't started
187 // the range yet, it means we must return this block.
189 isLast
= currentNode
.equals( lastNode
);
193 // The range must finish right before the boundary,
194 // including possibly skipped empty spaces. (http://dev.ckeditor.com/ticket/1603)
196 range
.setEndAt( currentNode
, CKEDITOR
.POSITION_BEFORE_START
);
198 // The found boundary must be set as the next one at this
199 // point. (http://dev.ckeditor.com/ticket/1717)
200 if ( nodeName
!= 'br' ) {
201 this._
.nextNode
= currentNode
;
207 // If we have child nodes, let's check them.
208 if ( currentNode
.getFirst() ) {
209 // If we don't have a range yet, let's start it.
211 range
= this.range
.clone();
212 range
.setStartAt( currentNode
, CKEDITOR
.POSITION_BEFORE_START
);
215 currentNode
= currentNode
.getFirst();
220 } else if ( currentNode
.type
== CKEDITOR
.NODE_TEXT
) {
221 // Ignore normal whitespaces (i.e. not including or
222 // other unicode whitespaces) before/after a block node.
223 if ( beginWhitespaceRegex
.test( currentNode
.getText() ) )
227 // The current node is good to be part of the range and we are
228 // starting a new range, initialize it first.
229 if ( includeNode
&& !range
) {
230 range
= this.range
.clone();
231 range
.setStartAt( currentNode
, CKEDITOR
.POSITION_BEFORE_START
);
234 // The last node has been found.
235 isLast
= ( ( !closeRange
|| includeNode
) && currentNode
.equals( lastNode
) );
237 // If we are in an element boundary, let's check if it is time
238 // to close the range, otherwise we include the parent within it.
239 if ( range
&& !closeRange
) {
240 while ( !currentNode
.getNext( skipGuard
) && !isLast
) {
241 var parentNode
= currentNode
.getParent();
243 if ( parentNode
.isBlockBoundary( this.forceBrBreak
&& !parentPre
&& { br: 1 } ) ) {
246 isLast
= isLast
|| ( parentNode
.equals( lastNode
) );
247 // Make sure range includes bookmarks at the end of the block. (http://dev.ckeditor.com/ticket/7359)
248 range
.setEndAt( parentNode
, CKEDITOR
.POSITION_BEFORE_END
);
252 currentNode
= parentNode
;
254 isLast
= ( currentNode
.equals( lastNode
) );
255 continueFromSibling
= 1;
259 // Now finally include the node.
261 range
.setEndAt( currentNode
, CKEDITOR
.POSITION_AFTER_END
);
263 currentNode
= this._getNextSourceNode( currentNode
, continueFromSibling
, lastNode
);
264 isLast
= !currentNode
;
266 // We have found a block boundary. Let's close the range and move out of the
268 if ( isLast
|| ( closeRange
&& range
) )
272 // Now, based on the processed range, look for (or create) the block to be returned.
274 // If no range has been found, this is the end.
276 this._
.docEndMarker
&& this._
.docEndMarker
.remove();
277 this._
.nextNode
= null;
281 var startPath
= new CKEDITOR
.dom
.elementPath( range
.startContainer
, range
.root
);
282 var startBlockLimit
= startPath
.blockLimit
,
283 checkLimits
= { div: 1, th: 1, td: 1 };
284 block
= startPath
.block
;
286 if ( !block
&& startBlockLimit
&& !this.enforceRealBlocks
&& checkLimits
[ startBlockLimit
.getName() ] &&
287 range
.checkStartOfBlock() && range
.checkEndOfBlock() && !startBlockLimit
.equals( range
.root
) ) {
288 block
= startBlockLimit
;
289 } else if ( !block
|| ( this.enforceRealBlocks
&& block
.is( listItemNames
) ) ) {
290 // Create the fixed block.
291 block
= this.range
.document
.createElement( blockTag
);
293 // Move the contents of the temporary range to the fixed block.
294 range
.extractContents().appendTo( block
);
297 // Insert the fixed block into the DOM.
298 range
.insertNode( block
);
300 removePreviousBr
= removeLastBr
= true;
301 } else if ( block
.getName() != 'li' ) {
302 // If the range doesn't includes the entire contents of the
303 // block, we must split it, isolating the range in a dedicated
305 if ( !range
.checkStartOfBlock() || !range
.checkEndOfBlock() ) {
306 // The resulting block will be a clone of the current one.
307 block
= block
.clone( false );
309 // Extract the range contents, moving it to the new block.
310 range
.extractContents().appendTo( block
);
313 // Split the block. At this point, the range will be in the
314 // right position for our intents.
315 var splitInfo
= range
.splitBlock();
317 removePreviousBr
= !splitInfo
.wasStartOfBlock
;
318 removeLastBr
= !splitInfo
.wasEndOfBlock
;
320 // Insert the new block into the DOM.
321 range
.insertNode( block
);
323 } else if ( !isLast
) {
324 // LIs are returned as is, with all their children (due to the
325 // nested lists). But, the next node is the node right after
326 // the current range, which could be an <li> child (nested
327 // lists) or the next sibling <li>.
329 this._
.nextNode
= ( block
.equals( lastNode
) ? null : this._getNextSourceNode( range
.getBoundaryNodes().endNode
, 1, lastNode
) );
333 if ( removePreviousBr
) {
334 var previousSibling
= block
.getPrevious();
335 if ( previousSibling
&& previousSibling
.type
== CKEDITOR
.NODE_ELEMENT
) {
336 if ( previousSibling
.getName() == 'br' )
337 previousSibling
.remove();
338 else if ( previousSibling
.getLast() && previousSibling
.getLast().$.nodeName
.toLowerCase() == 'br' )
339 previousSibling
.getLast().remove();
343 if ( removeLastBr
) {
344 var lastChild
= block
.getLast();
345 if ( lastChild
&& lastChild
.type
== CKEDITOR
.NODE_ELEMENT
&& lastChild
.getName() == 'br' ) {
346 // Remove br filler on browser which do not need it.
347 if ( !CKEDITOR
.env
.needsBrFiller
|| lastChild
.getPrevious( bookmarkGuard
) || lastChild
.getNext( bookmarkGuard
) )
352 // Get a reference for the next element. This is important because the
353 // above block can be removed or changed, so we can rely on it for the
355 if ( !this._
.nextNode
) {
356 this._
.nextNode
= ( isLast
|| block
.equals( lastNode
) || !lastNode
) ? null : this._getNextSourceNode( block
, 1, lastNode
);
363 * Gets the next element to check or `null` when the `lastNode` or the
364 * {@link #range}'s {@link CKEDITOR.dom.range#root root} is reached. Bookmarks are skipped.
368 * @param {CKEDITOR.dom.node} node
369 * @param {Boolean} startFromSibling
370 * @param {CKEDITOR.dom.node} lastNode
371 * @returns {CKEDITOR.dom.node}
373 _getNextSourceNode: function( node
, startFromSibling
, lastNode
) {
374 var rootNode
= this.range
.root
,
377 // Here we are checking in guard function whether current element
378 // reach lastNode(default behaviour) and root node to prevent against
379 // getting out of editor instance root DOM object.
380 // http://dev.ckeditor.com/ticket/12484
381 function guardFunction( node
) {
382 return !( node
.equals( lastNode
) || node
.equals( rootNode
) );
385 next
= node
.getNextSourceNode( startFromSibling
, null, guardFunction
);
386 while ( !bookmarkGuard( next
) ) {
387 next
= next
.getNextSourceNode( startFromSibling
, null, guardFunction
);
393 // @context CKEDITOR.dom.iterator
394 // @returns Collapsed range which will be reused when during furter processing.
395 function startIterator() {
396 var range
= this.range
.clone(),
397 // Indicate at least one of the range boundaries is inside a preformat block.
400 // (http://dev.ckeditor.com/ticket/12178)
401 // Remember if following situation takes place:
402 // * startAtInnerBoundary: <p>foo[</p>...
403 // * endAtInnerBoundary: ...<p>]bar</p>
404 // Because information about line break will be lost when shrinking range.
405 // Note that we test only if path block exist, because we must properly shrink
406 // range containing table and/or table cells.
407 // Note: When range is collapsed there's no way it can be shrinked.
408 // By checking if range is collapsed we also prevent http://dev.ckeditor.com/ticket/12308.
409 startPath
= range
.startPath(),
410 endPath
= range
.endPath(),
411 startAtInnerBoundary
= !range
.collapsed
&& rangeAtInnerBlockBoundary( range
, startPath
.block
),
412 endAtInnerBoundary
= !range
.collapsed
&& rangeAtInnerBlockBoundary( range
, endPath
.block
, 1 );
414 // Shrink the range to exclude harmful "noises" (http://dev.ckeditor.com/ticket/4087, http://dev.ckeditor.com/ticket/4450, http://dev.ckeditor.com/ticket/5435).
415 range
.shrink( CKEDITOR
.SHRINK_ELEMENT
, true );
417 if ( startAtInnerBoundary
)
418 range
.setStartAt( startPath
.block
, CKEDITOR
.POSITION_BEFORE_END
);
419 if ( endAtInnerBoundary
)
420 range
.setEndAt( endPath
.block
, CKEDITOR
.POSITION_AFTER_START
);
422 touchPre
= range
.endContainer
.hasAscendant( 'pre', true ) || range
.startContainer
.hasAscendant( 'pre', true );
424 range
.enlarge( this.forceBrBreak
&& !touchPre
|| !this.enlargeBr
? CKEDITOR
.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR
.ENLARGE_BLOCK_CONTENTS
);
426 if ( !range
.collapsed
) {
427 var walker
= new CKEDITOR
.dom
.walker( range
.clone() ),
428 ignoreBookmarkTextEvaluator
= CKEDITOR
.dom
.walker
.bookmark( true, true );
429 // Avoid anchor inside bookmark inner text.
430 walker
.evaluator
= ignoreBookmarkTextEvaluator
;
431 this._
.nextNode
= walker
.next();
432 // TODO: It's better to have walker.reset() used here.
433 walker
= new CKEDITOR
.dom
.walker( range
.clone() );
434 walker
.evaluator
= ignoreBookmarkTextEvaluator
;
435 var lastNode
= walker
.previous();
436 this._
.lastNode
= lastNode
.getNextSourceNode( true, null, range
.root
);
438 // We may have an empty text node at the end of block due to [3770].
439 // If that node is the lastNode, it would cause our logic to leak to the
440 // next block.(http://dev.ckeditor.com/ticket/3887)
441 if ( this._
.lastNode
&& this._
.lastNode
.type
== CKEDITOR
.NODE_TEXT
&& !CKEDITOR
.tools
.trim( this._
.lastNode
.getText() ) && this._
.lastNode
.getParent().isBlockBoundary() ) {
442 var testRange
= this.range
.clone();
443 testRange
.moveToPosition( this._
.lastNode
, CKEDITOR
.POSITION_AFTER_END
);
444 if ( testRange
.checkEndOfBlock() ) {
445 var path
= new CKEDITOR
.dom
.elementPath( testRange
.endContainer
, testRange
.root
),
446 lastBlock
= path
.block
|| path
.blockLimit
;
447 this._
.lastNode
= lastBlock
.getNextSourceNode( true );
451 // The end of document or range.root was reached, so we need a marker node inside.
452 if ( !this._
.lastNode
|| !range
.root
.contains( this._
.lastNode
) ) {
453 this._
.lastNode
= this._
.docEndMarker
= range
.document
.createText( '' );
454 this._
.lastNode
.insertAfter( lastNode
);
457 // Let's reuse this variable.
466 // Does a nested editables lookup inside editablesContainer.
467 // If remainingEditables is set will lookup inside this array.
468 // @param {CKEDITOR.dom.element} editablesContainer
469 // @param {CKEDITOR.dom.element[]} [remainingEditables]
470 function getNestedEditableIn( editablesContainer
, remainingEditables
) {
471 if ( remainingEditables
== null )
472 remainingEditables
= findNestedEditables( editablesContainer
);
476 while ( ( editable
= remainingEditables
.shift() ) ) {
477 if ( isIterableEditable( editable
) )
478 return { element: editable
, remaining: remainingEditables
};
484 // Checkes whether we can iterate over this editable.
485 function isIterableEditable( editable
) {
486 // Reject blockless editables.
487 return editable
.getDtd().p
;
490 // Finds nested editables within container. Does not return
491 // editables nested in another editable (twice).
492 function findNestedEditables( container
) {
495 container
.forEach( function( element
) {
496 if ( element
.getAttribute( 'contenteditable' ) == 'true' ) {
497 editables
.push( element
);
498 return false; // Skip children.
500 }, CKEDITOR
.NODE_ELEMENT
, true );
505 // Looks for a first nested editable after previousEditable (if passed) and creates
506 // nested iterator for it.
507 function startNestedEditableIterator( parentIterator
, blockTag
, editablesContainer
, remainingEditables
) {
508 var editable
= getNestedEditableIn( editablesContainer
, remainingEditables
);
513 var filter
= CKEDITOR
.filter
.instances
[ editable
.element
.data( 'cke-filter' ) ];
515 // If current editable has a filter and this filter does not allow for block tag,
516 // search for next nested editable in remaining ones.
517 if ( filter
&& !filter
.check( blockTag
) )
518 return startNestedEditableIterator( parentIterator
, blockTag
, editablesContainer
, editable
.remaining
);
520 var range
= new CKEDITOR
.dom
.range( editable
.element
);
521 range
.selectNodeContents( editable
.element
);
523 var iterator
= range
.createIterator();
524 // This setting actually does not change anything in this case,
525 // because entire range contents is selected, so there're no <br>s to be included.
526 // But it seems right to copy it too.
527 iterator
.enlargeBr
= parentIterator
.enlargeBr
;
528 // Inherit configuration from parent iterator.
529 iterator
.enforceRealBlocks
= parentIterator
.enforceRealBlocks
;
530 // Set the activeFilter (which can be overriden when this iteator will start nested iterator)
531 // and the default filter, which will make it possible to reset to
532 // current iterator's activeFilter after leaving nested editable.
533 iterator
.activeFilter
= iterator
.filter
= filter
;
535 parentIterator
._
.nestedEditable
= {
536 element: editable
.element
,
537 container: editablesContainer
,
538 remaining: editable
.remaining
,
545 // Checks whether range starts or ends at inner block boundary.
546 // See usage comments to learn more.
547 function rangeAtInnerBlockBoundary( range
, block
, checkEnd
) {
551 var testRange
= range
.clone();
552 testRange
.collapse( !checkEnd
);
553 return testRange
.checkBoundaryOfElement( block
, checkEnd
? CKEDITOR
.START : CKEDITOR
.END
);
557 * Creates a {@link CKEDITOR.dom.iterator} instance for this range.
559 * @member CKEDITOR.dom.range
560 * @returns {CKEDITOR.dom.iterator}
562 CKEDITOR
.dom
.range
.prototype.createIterator = function() {
563 return new iterator( this );