2018-06-17 16:07:19 +00:00
/ * *
* @ license Copyright ( c ) 2003 - 2018 , CKSource - Frederico Knabben . All rights reserved .
* For licensing , see LICENSE . md or https : //ckeditor.com/legal/ckeditor-oss-license
2014-04-11 20:07:18 +00:00
* /
/ * *
* @ ignore
2018-06-17 16:07:19 +00:00
* File overview : DOM iterator which iterates over list items , lines and paragraphs .
2014-04-11 20:07:18 +00:00
* /
'use strict' ;
( function ( ) {
/ * *
2018-06-17 16:07:19 +00:00
* Represents the iterator class . It can be used to iterate
2014-04-11 20:07:18 +00:00
* over all elements ( or even text nodes in case of { @ link # enlargeBr } set to ` false ` )
2018-06-17 16:07:19 +00:00
* which establish "paragraph-like" spaces within the passed range .
2014-04-11 20:07:18 +00:00
*
* // <h1>[foo</h1><p>bar]</p>
* var iterator = range . createIterator ( ) ;
* iterator . getNextParagraph ( ) ; // h1 element
* iterator . getNextParagraph ( ) ; // p element
*
* // <ul><li>[foo</li><li>bar]</li>
2018-06-17 16:07:19 +00:00
* // With enforceRealBlocks set to false the iterator will return two list item elements.
* // With enforceRealBlocks set to true the iterator will return two paragraphs and the DOM will be changed to:
2014-04-11 20:07:18 +00:00
* // <ul><li><p>foo</p></li><li><p>bar</p></li>
*
* @ class CKEDITOR . dom . iterator
* @ constructor Creates an iterator class instance .
* @ param { CKEDITOR . dom . range } range
* /
function iterator ( range ) {
if ( arguments . length < 1 )
return ;
2018-06-17 16:07:19 +00:00
/ * *
* @ readonly
* @ property { CKEDITOR . dom . range }
* /
2014-04-11 20:07:18 +00:00
this . range = range ;
2018-06-17 16:07:19 +00:00
/ * *
* @ property { Boolean } [ forceBrBreak = false ]
* /
2014-04-11 20:07:18 +00:00
this . forceBrBreak = 0 ;
2018-06-17 16:07:19 +00:00
// (https://dev.ckeditor.com/ticket/3730).
2014-04-11 20:07:18 +00:00
/ * *
2018-06-17 16:07:19 +00:00
* Whether to include ` <br> ` elements in the enlarged range . Should be
* set to ` false ` when using the iterator in the { @ link CKEDITOR # ENTER _BR } mode .
2014-04-11 20:07:18 +00:00
*
* @ property { Boolean } [ enlargeBr = true ]
* /
this . enlargeBr = 1 ;
/ * *
2018-06-17 16:07:19 +00:00
* Whether the iterator should create a transformable block
* if the current one contains text and cannot be transformed .
2014-04-11 20:07:18 +00:00
* For example new blocks will be established in elements like
* ` <li> ` or ` <td> ` .
*
* @ property { Boolean } [ enforceRealBlocks = false ]
* /
this . enforceRealBlocks = 0 ;
this . _ || ( this . _ = { } ) ;
}
/ * *
* Default iterator ' s filter . It is set only for nested iterators .
*
* @ since 4.3
* @ readonly
* @ property { CKEDITOR . filter } filter
* /
/ * *
* Iterator ' s active filter . It is set by the { @ link # getNextParagraph } method
2018-06-17 16:07:19 +00:00
* when it enters a nested editable .
2014-04-11 20:07:18 +00:00
*
* @ since 4.3
* @ readonly
* @ property { CKEDITOR . filter } activeFilter
* /
var beginWhitespaceRegex = /^[\r\n\t ]+$/ ,
2018-06-17 16:07:19 +00:00
// Ignore bookmark nodes.(https://dev.ckeditor.com/ticket/3783)
2014-04-11 20:07:18 +00:00
bookmarkGuard = CKEDITOR . dom . walker . bookmark ( false , true ) ,
whitespacesGuard = CKEDITOR . dom . walker . whitespaces ( true ) ,
skipGuard = function ( node ) {
return bookmarkGuard ( node ) && whitespacesGuard ( node ) ;
2018-06-17 16:07:19 +00:00
} ,
listItemNames = { dd : 1 , dt : 1 , li : 1 } ;
2014-04-11 20:07:18 +00:00
iterator . prototype = {
/ * *
2018-06-17 16:07:19 +00:00
* Returns the next paragraph - like element or ` null ` if the end of a range is reached .
2014-04-11 20:07:18 +00:00
*
* @ param { String } [ blockTag = 'p' ] Name of a block element which will be established by
2018-06-17 16:07:19 +00:00
* the iterator in block - less elements ( see { @ link # enforceRealBlocks } ) .
2014-04-11 20:07:18 +00:00
* /
getNextParagraph : function ( blockTag ) {
// The block element to be returned.
var block ;
// The range object used to identify the paragraph contents.
var range ;
// Indicats that the current element in the loop is the last one.
var isLast ;
// Instructs to cleanup remaining BRs.
var removePreviousBr , removeLastBr ;
blockTag = blockTag || 'p' ;
// We're iterating over nested editable.
if ( this . _ . nestedEditable ) {
// Get next block from nested iterator and returns it if was found.
block = this . _ . nestedEditable . iterator . getNextParagraph ( blockTag ) ;
if ( block ) {
// Inherit activeFilter from the nested iterator.
this . activeFilter = this . _ . nestedEditable . iterator . activeFilter ;
return block ;
}
// No block in nested iterator means that we reached the end of the nested editable.
// Reset the active filter to the default filter (or undefined if this iterator didn't have it).
this . activeFilter = this . filter ;
// Try to find next nested editable or get back to parent (this) iterator.
if ( startNestedEditableIterator ( this , blockTag , this . _ . nestedEditable . container , this . _ . nestedEditable . remaining ) ) {
// Inherit activeFilter from the nested iterator.
this . activeFilter = this . _ . nestedEditable . iterator . activeFilter ;
return this . _ . nestedEditable . iterator . getNextParagraph ( blockTag ) ;
2018-06-17 16:07:19 +00:00
} else {
2014-04-11 20:07:18 +00:00
this . _ . nestedEditable = null ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:07:18 +00:00
}
// Block-less range should be checked first.
if ( ! this . range . root . getDtd ( ) [ blockTag ] )
return null ;
// This is the first iteration. Let's initialize it.
if ( ! this . _ . started )
range = startIterator . call ( this ) ;
var currentNode = this . _ . nextNode ,
lastNode = this . _ . lastNode ;
this . _ . nextNode = null ;
while ( currentNode ) {
// closeRange indicates that a paragraph boundary has been found,
// so the range can be closed.
var closeRange = 0 ,
parentPre = currentNode . hasAscendant ( 'pre' ) ;
// includeNode indicates that the current node is good to be part
// of the range. By default, any non-element node is ok for it.
var includeNode = ( currentNode . type != CKEDITOR . NODE _ELEMENT ) ,
continueFromSibling = 0 ;
// If it is an element node, let's check if it can be part of the range.
if ( ! includeNode ) {
var nodeName = currentNode . getName ( ) ;
// Non-editable block was found - return it and move to processing
// its nested editables if they exist.
if ( CKEDITOR . dtd . $block [ nodeName ] && currentNode . getAttribute ( 'contenteditable' ) == 'false' ) {
block = currentNode ;
// Setup iterator for first of nested editables.
// If there's no editable, then algorithm will move to next element after current block.
startNestedEditableIterator ( this , blockTag , block ) ;
// Gets us straight to the end of getParagraph() because block variable is set.
break ;
} else if ( currentNode . isBlockBoundary ( this . forceBrBreak && ! parentPre && { br : 1 } ) ) {
// <br> boundaries must be part of the range. It will
// happen only if ForceBrBreak.
if ( nodeName == 'br' )
includeNode = 1 ;
else if ( ! range && ! currentNode . getChildCount ( ) && nodeName != 'hr' ) {
// If we have found an empty block, and haven't started
// the range yet, it means we must return this block.
block = currentNode ;
isLast = currentNode . equals ( lastNode ) ;
break ;
}
// The range must finish right before the boundary,
2018-06-17 16:07:19 +00:00
// including possibly skipped empty spaces. (https://dev.ckeditor.com/ticket/1603)
2014-04-11 20:07:18 +00:00
if ( range ) {
range . setEndAt ( currentNode , CKEDITOR . POSITION _BEFORE _START ) ;
// The found boundary must be set as the next one at this
2018-06-17 16:07:19 +00:00
// point. (https://dev.ckeditor.com/ticket/1717)
if ( nodeName != 'br' ) {
2014-04-11 20:07:18 +00:00
this . _ . nextNode = currentNode ;
2018-06-17 16:07:19 +00:00
}
2014-04-11 20:07:18 +00:00
}
closeRange = 1 ;
} else {
// If we have child nodes, let's check them.
if ( currentNode . getFirst ( ) ) {
// If we don't have a range yet, let's start it.
if ( ! range ) {
range = this . range . clone ( ) ;
range . setStartAt ( currentNode , CKEDITOR . POSITION _BEFORE _START ) ;
}
currentNode = currentNode . getFirst ( ) ;
continue ;
}
includeNode = 1 ;
}
} else if ( currentNode . type == CKEDITOR . NODE _TEXT ) {
// Ignore normal whitespaces (i.e. not including or
// other unicode whitespaces) before/after a block node.
if ( beginWhitespaceRegex . test ( currentNode . getText ( ) ) )
includeNode = 0 ;
}
// The current node is good to be part of the range and we are
// starting a new range, initialize it first.
if ( includeNode && ! range ) {
range = this . range . clone ( ) ;
range . setStartAt ( currentNode , CKEDITOR . POSITION _BEFORE _START ) ;
}
// The last node has been found.
isLast = ( ( ! closeRange || includeNode ) && currentNode . equals ( lastNode ) ) ;
// If we are in an element boundary, let's check if it is time
// to close the range, otherwise we include the parent within it.
if ( range && ! closeRange ) {
while ( ! currentNode . getNext ( skipGuard ) && ! isLast ) {
var parentNode = currentNode . getParent ( ) ;
if ( parentNode . isBlockBoundary ( this . forceBrBreak && ! parentPre && { br : 1 } ) ) {
closeRange = 1 ;
includeNode = 0 ;
isLast = isLast || ( parentNode . equals ( lastNode ) ) ;
2018-06-17 16:07:19 +00:00
// Make sure range includes bookmarks at the end of the block. (https://dev.ckeditor.com/ticket/7359)
2014-04-11 20:07:18 +00:00
range . setEndAt ( parentNode , CKEDITOR . POSITION _BEFORE _END ) ;
break ;
}
currentNode = parentNode ;
includeNode = 1 ;
isLast = ( currentNode . equals ( lastNode ) ) ;
continueFromSibling = 1 ;
}
}
// Now finally include the node.
if ( includeNode )
range . setEndAt ( currentNode , CKEDITOR . POSITION _AFTER _END ) ;
2018-06-17 16:07:19 +00:00
currentNode = this . _getNextSourceNode ( currentNode , continueFromSibling , lastNode ) ;
2014-04-11 20:07:18 +00:00
isLast = ! currentNode ;
// We have found a block boundary. Let's close the range and move out of the
// loop.
if ( isLast || ( closeRange && range ) )
break ;
}
// Now, based on the processed range, look for (or create) the block to be returned.
if ( ! block ) {
// If no range has been found, this is the end.
if ( ! range ) {
this . _ . docEndMarker && this . _ . docEndMarker . remove ( ) ;
this . _ . nextNode = null ;
return null ;
}
var startPath = new CKEDITOR . dom . elementPath ( range . startContainer , range . root ) ;
var startBlockLimit = startPath . blockLimit ,
checkLimits = { div : 1 , th : 1 , td : 1 } ;
block = startPath . block ;
2018-06-17 16:07:19 +00:00
if ( ! block && startBlockLimit && ! this . enforceRealBlocks && checkLimits [ startBlockLimit . getName ( ) ] &&
range . checkStartOfBlock ( ) && range . checkEndOfBlock ( ) && ! startBlockLimit . equals ( range . root ) ) {
2014-04-11 20:07:18 +00:00
block = startBlockLimit ;
2018-06-17 16:07:19 +00:00
} else if ( ! block || ( this . enforceRealBlocks && block . is ( listItemNames ) ) ) {
2014-04-11 20:07:18 +00:00
// Create the fixed block.
block = this . range . document . createElement ( blockTag ) ;
// Move the contents of the temporary range to the fixed block.
range . extractContents ( ) . appendTo ( block ) ;
block . trim ( ) ;
// Insert the fixed block into the DOM.
range . insertNode ( block ) ;
removePreviousBr = removeLastBr = true ;
} else if ( block . getName ( ) != 'li' ) {
// If the range doesn't includes the entire contents of the
// block, we must split it, isolating the range in a dedicated
// block.
if ( ! range . checkStartOfBlock ( ) || ! range . checkEndOfBlock ( ) ) {
// The resulting block will be a clone of the current one.
block = block . clone ( false ) ;
// Extract the range contents, moving it to the new block.
range . extractContents ( ) . appendTo ( block ) ;
block . trim ( ) ;
// Split the block. At this point, the range will be in the
// right position for our intents.
var splitInfo = range . splitBlock ( ) ;
removePreviousBr = ! splitInfo . wasStartOfBlock ;
removeLastBr = ! splitInfo . wasEndOfBlock ;
// Insert the new block into the DOM.
range . insertNode ( block ) ;
}
} else if ( ! isLast ) {
// LIs are returned as is, with all their children (due to the
// nested lists). But, the next node is the node right after
// the current range, which could be an <li> child (nested
// lists) or the next sibling <li>.
2018-06-17 16:07:19 +00:00
this . _ . nextNode = ( block . equals ( lastNode ) ? null : this . _getNextSourceNode ( range . getBoundaryNodes ( ) . endNode , 1 , lastNode ) ) ;
2014-04-11 20:07:18 +00:00
}
}
if ( removePreviousBr ) {
var previousSibling = block . getPrevious ( ) ;
if ( previousSibling && previousSibling . type == CKEDITOR . NODE _ELEMENT ) {
if ( previousSibling . getName ( ) == 'br' )
previousSibling . remove ( ) ;
else if ( previousSibling . getLast ( ) && previousSibling . getLast ( ) . $ . nodeName . toLowerCase ( ) == 'br' )
previousSibling . getLast ( ) . remove ( ) ;
}
}
if ( removeLastBr ) {
var lastChild = block . getLast ( ) ;
if ( lastChild && lastChild . type == CKEDITOR . NODE _ELEMENT && lastChild . getName ( ) == 'br' ) {
// Remove br filler on browser which do not need it.
if ( ! CKEDITOR . env . needsBrFiller || lastChild . getPrevious ( bookmarkGuard ) || lastChild . getNext ( bookmarkGuard ) )
lastChild . remove ( ) ;
}
}
// Get a reference for the next element. This is important because the
// above block can be removed or changed, so we can rely on it for the
// next interation.
2018-06-17 16:07:19 +00:00
if ( ! this . _ . nextNode ) {
this . _ . nextNode = ( isLast || block . equals ( lastNode ) || ! lastNode ) ? null : this . _getNextSourceNode ( block , 1 , lastNode ) ;
}
2014-04-11 20:07:18 +00:00
return block ;
2018-06-17 16:07:19 +00:00
} ,
/ * *
* Gets the next element to check or ` null ` when the ` lastNode ` or the
* { @ link # range } ' s { @ link CKEDITOR . dom . range # root root } is reached . Bookmarks are skipped .
*
* @ since 4.4 . 6
* @ private
* @ param { CKEDITOR . dom . node } node
* @ param { Boolean } startFromSibling
* @ param { CKEDITOR . dom . node } lastNode
* @ returns { CKEDITOR . dom . node }
* /
_getNextSourceNode : function ( node , startFromSibling , lastNode ) {
var rootNode = this . range . root ,
next ;
// Here we are checking in guard function whether current element
// reach lastNode(default behaviour) and root node to prevent against
// getting out of editor instance root DOM object.
// https://dev.ckeditor.com/ticket/12484
function guardFunction ( node ) {
return ! ( node . equals ( lastNode ) || node . equals ( rootNode ) ) ;
}
next = node . getNextSourceNode ( startFromSibling , null , guardFunction ) ;
while ( ! bookmarkGuard ( next ) ) {
next = next . getNextSourceNode ( startFromSibling , null , guardFunction ) ;
}
return next ;
2014-04-11 20:07:18 +00:00
}
} ;
// @context CKEDITOR.dom.iterator
// @returns Collapsed range which will be reused when during furter processing.
function startIterator ( ) {
var range = this . range . clone ( ) ,
// Indicate at least one of the range boundaries is inside a preformat block.
2018-06-17 16:07:19 +00:00
touchPre ,
// (https://dev.ckeditor.com/ticket/12178)
// Remember if following situation takes place:
// * startAtInnerBoundary: <p>foo[</p>...
// * endAtInnerBoundary: ...<p>]bar</p>
// Because information about line break will be lost when shrinking range.
// Note that we test only if path block exist, because we must properly shrink
// range containing table and/or table cells.
// Note: When range is collapsed there's no way it can be shrinked.
// By checking if range is collapsed we also prevent https://dev.ckeditor.com/ticket/12308.
startPath = range . startPath ( ) ,
endPath = range . endPath ( ) ,
startAtInnerBoundary = ! range . collapsed && rangeAtInnerBlockBoundary ( range , startPath . block ) ,
endAtInnerBoundary = ! range . collapsed && rangeAtInnerBlockBoundary ( range , endPath . block , 1 ) ;
// Shrink the range to exclude harmful "noises" (https://dev.ckeditor.com/ticket/4087, https://dev.ckeditor.com/ticket/4450, https://dev.ckeditor.com/ticket/5435).
2014-04-11 20:07:18 +00:00
range . shrink ( CKEDITOR . SHRINK _ELEMENT , true ) ;
2018-06-17 16:07:19 +00:00
if ( startAtInnerBoundary )
range . setStartAt ( startPath . block , CKEDITOR . POSITION _BEFORE _END ) ;
if ( endAtInnerBoundary )
range . setEndAt ( endPath . block , CKEDITOR . POSITION _AFTER _START ) ;
2014-04-11 20:07:18 +00:00
touchPre = range . endContainer . hasAscendant ( 'pre' , true ) || range . startContainer . hasAscendant ( 'pre' , true ) ;
range . enlarge ( this . forceBrBreak && ! touchPre || ! this . enlargeBr ? CKEDITOR . ENLARGE _LIST _ITEM _CONTENTS : CKEDITOR . ENLARGE _BLOCK _CONTENTS ) ;
if ( ! range . collapsed ) {
var walker = new CKEDITOR . dom . walker ( range . clone ( ) ) ,
ignoreBookmarkTextEvaluator = CKEDITOR . dom . walker . bookmark ( true , true ) ;
// Avoid anchor inside bookmark inner text.
walker . evaluator = ignoreBookmarkTextEvaluator ;
this . _ . nextNode = walker . next ( ) ;
// TODO: It's better to have walker.reset() used here.
walker = new CKEDITOR . dom . walker ( range . clone ( ) ) ;
walker . evaluator = ignoreBookmarkTextEvaluator ;
var lastNode = walker . previous ( ) ;
2018-06-17 16:07:19 +00:00
this . _ . lastNode = lastNode . getNextSourceNode ( true , null , range . root ) ;
2014-04-11 20:07:18 +00:00
// We may have an empty text node at the end of block due to [3770].
// If that node is the lastNode, it would cause our logic to leak to the
2018-06-17 16:07:19 +00:00
// next block.(https://dev.ckeditor.com/ticket/3887)
2014-04-11 20:07:18 +00:00
if ( this . _ . lastNode && this . _ . lastNode . type == CKEDITOR . NODE _TEXT && ! CKEDITOR . tools . trim ( this . _ . lastNode . getText ( ) ) && this . _ . lastNode . getParent ( ) . isBlockBoundary ( ) ) {
var testRange = this . range . clone ( ) ;
testRange . moveToPosition ( this . _ . lastNode , CKEDITOR . POSITION _AFTER _END ) ;
if ( testRange . checkEndOfBlock ( ) ) {
var path = new CKEDITOR . dom . elementPath ( testRange . endContainer , testRange . root ) ,
lastBlock = path . block || path . blockLimit ;
this . _ . lastNode = lastBlock . getNextSourceNode ( true ) ;
}
}
// The end of document or range.root was reached, so we need a marker node inside.
if ( ! this . _ . lastNode || ! range . root . contains ( this . _ . lastNode ) ) {
this . _ . lastNode = this . _ . docEndMarker = range . document . createText ( '' ) ;
this . _ . lastNode . insertAfter ( lastNode ) ;
}
// Let's reuse this variable.
range = null ;
}
this . _ . started = 1 ;
return range ;
}
// Does a nested editables lookup inside editablesContainer.
// If remainingEditables is set will lookup inside this array.
// @param {CKEDITOR.dom.element} editablesContainer
// @param {CKEDITOR.dom.element[]} [remainingEditables]
function getNestedEditableIn ( editablesContainer , remainingEditables ) {
2018-06-17 16:07:19 +00:00
if ( remainingEditables == null )
2014-04-11 20:07:18 +00:00
remainingEditables = findNestedEditables ( editablesContainer ) ;
var editable ;
while ( ( editable = remainingEditables . shift ( ) ) ) {
if ( isIterableEditable ( editable ) )
return { element : editable , remaining : remainingEditables } ;
}
return null ;
}
// Checkes whether we can iterate over this editable.
function isIterableEditable ( editable ) {
// Reject blockless editables.
return editable . getDtd ( ) . p ;
}
// Finds nested editables within container. Does not return
// editables nested in another editable (twice).
function findNestedEditables ( container ) {
var editables = [ ] ;
container . forEach ( function ( element ) {
if ( element . getAttribute ( 'contenteditable' ) == 'true' ) {
editables . push ( element ) ;
return false ; // Skip children.
}
} , CKEDITOR . NODE _ELEMENT , true ) ;
return editables ;
}
// Looks for a first nested editable after previousEditable (if passed) and creates
// nested iterator for it.
function startNestedEditableIterator ( parentIterator , blockTag , editablesContainer , remainingEditables ) {
var editable = getNestedEditableIn ( editablesContainer , remainingEditables ) ;
if ( ! editable )
return 0 ;
var filter = CKEDITOR . filter . instances [ editable . element . data ( 'cke-filter' ) ] ;
// If current editable has a filter and this filter does not allow for block tag,
// search for next nested editable in remaining ones.
if ( filter && ! filter . check ( blockTag ) )
return startNestedEditableIterator ( parentIterator , blockTag , editablesContainer , editable . remaining ) ;
var range = new CKEDITOR . dom . range ( editable . element ) ;
range . selectNodeContents ( editable . element ) ;
var iterator = range . createIterator ( ) ;
// This setting actually does not change anything in this case,
// because entire range contents is selected, so there're no <br>s to be included.
// But it seems right to copy it too.
iterator . enlargeBr = parentIterator . enlargeBr ;
// Inherit configuration from parent iterator.
iterator . enforceRealBlocks = parentIterator . enforceRealBlocks ;
// Set the activeFilter (which can be overriden when this iteator will start nested iterator)
// and the default filter, which will make it possible to reset to
// current iterator's activeFilter after leaving nested editable.
iterator . activeFilter = iterator . filter = filter ;
parentIterator . _ . nestedEditable = {
element : editable . element ,
container : editablesContainer ,
remaining : editable . remaining ,
iterator : iterator
} ;
return 1 ;
}
2018-06-17 16:07:19 +00:00
// Checks whether range starts or ends at inner block boundary.
// See usage comments to learn more.
function rangeAtInnerBlockBoundary ( range , block , checkEnd ) {
if ( ! block )
return false ;
var testRange = range . clone ( ) ;
testRange . collapse ( ! checkEnd ) ;
return testRange . checkBoundaryOfElement ( block , checkEnd ? CKEDITOR . START : CKEDITOR . END ) ;
}
2014-04-11 20:07:18 +00:00
/ * *
2018-06-17 16:07:19 +00:00
* Creates a { @ link CKEDITOR . dom . iterator } instance for this range .
2014-04-11 20:07:18 +00:00
*
* @ member CKEDITOR . dom . range
* @ returns { CKEDITOR . dom . iterator }
* /
CKEDITOR . dom . range . prototype . createIterator = function ( ) {
return new iterator ( this ) ;
} ;
} ) ( ) ;