aboutsummaryrefslogtreecommitdiff
path: root/sources/core/dom
diff options
context:
space:
mode:
Diffstat (limited to 'sources/core/dom')
-rw-r--r--sources/core/dom/document.js2
-rw-r--r--sources/core/dom/domobject.js4
-rw-r--r--sources/core/dom/element.js36
-rw-r--r--sources/core/dom/elementpath.js22
-rw-r--r--sources/core/dom/iterator.js20
-rw-r--r--sources/core/dom/node.js10
-rw-r--r--sources/core/dom/nodelist.js17
-rw-r--r--sources/core/dom/range.js200
-rw-r--r--sources/core/dom/rangelist.js2
-rw-r--r--sources/core/dom/text.js2
-rw-r--r--sources/core/dom/walker.js10
11 files changed, 237 insertions, 88 deletions
diff --git a/sources/core/dom/document.js b/sources/core/dom/document.js
index 51c5b19..ebf0bab 100644
--- a/sources/core/dom/document.js
+++ b/sources/core/dom/document.js
@@ -262,7 +262,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.document.prototype, {
262 * @param {String} html The HTML defining the document content. 262 * @param {String} html The HTML defining the document content.
263 */ 263 */
264 write: function( html ) { 264 write: function( html ) {
265 // Don't leave any history log in IE. (#5657) 265 // Don't leave any history log in IE. (http://dev.ckeditor.com/ticket/5657)
266 this.$.open( 'text/html', 'replace' ); 266 this.$.open( 'text/html', 'replace' );
267 267
268 // Support for custom document.domain in IE. 268 // Support for custom document.domain in IE.
diff --git a/sources/core/dom/domobject.js b/sources/core/dom/domobject.js
index 4c593ff..f4e258a 100644
--- a/sources/core/dom/domobject.js
+++ b/sources/core/dom/domobject.js
@@ -41,7 +41,7 @@ CKEDITOR.dom.domObject.prototype = ( function() {
41 return function( domEvent ) { 41 return function( domEvent ) {
42 // In FF, when reloading the page with the editor focused, it may 42 // In FF, when reloading the page with the editor focused, it may
43 // throw an error because the CKEDITOR global is not anymore 43 // throw an error because the CKEDITOR global is not anymore
44 // available. So, we check it here first. (#2923) 44 // available. So, we check it here first. (http://dev.ckeditor.com/ticket/2923)
45 if ( typeof CKEDITOR != 'undefined' ) 45 if ( typeof CKEDITOR != 'undefined' )
46 domObject.fire( eventName, new CKEDITOR.dom.event( domEvent ) ); 46 domObject.fire( eventName, new CKEDITOR.dom.event( domEvent ) );
47 }; 47 };
@@ -138,7 +138,7 @@ CKEDITOR.dom.domObject.prototype = ( function() {
138 } 138 }
139 139
140 // Remove events from events object so fire() method will not call 140 // Remove events from events object so fire() method will not call
141 // listeners (#11400). 141 // listeners (http://dev.ckeditor.com/ticket/11400).
142 CKEDITOR.event.prototype.removeAllListeners.call( this ); 142 CKEDITOR.event.prototype.removeAllListeners.call( this );
143 } 143 }
144 }; 144 };
diff --git a/sources/core/dom/element.js b/sources/core/dom/element.js
index e02ff17..31451f9 100644
--- a/sources/core/dom/element.js
+++ b/sources/core/dom/element.js
@@ -308,7 +308,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
308 */ 308 */
309 appendText: function( text ) { 309 appendText: function( text ) {
310 // On IE8 it is impossible to append node to script tag, so we use its text. 310 // On IE8 it is impossible to append node to script tag, so we use its text.
311 // On the contrary, on Safari the text property is unpredictable in links. (#13232) 311 // On the contrary, on Safari the text property is unpredictable in links. (http://dev.ckeditor.com/ticket/13232)
312 if ( this.$.text != null && CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) 312 if ( this.$.text != null && CKEDITOR.env.ie && CKEDITOR.env.version < 9 )
313 this.$.text += text; 313 this.$.text += text;
314 else 314 else
@@ -377,7 +377,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
377 377
378 // In case of Internet Explorer, we must check if there is no background-color 378 // In case of Internet Explorer, we must check if there is no background-color
379 // added to the element. In such case, we have to overwrite it to prevent "switching it off" 379 // added to the element. In such case, we have to overwrite it to prevent "switching it off"
380 // by a browser (#14667). 380 // by a browser (http://dev.ckeditor.com/ticket/14667).
381 if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) { 381 if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
382 tmpElement = new CKEDITOR.dom.element( 'div' ); 382 tmpElement = new CKEDITOR.dom.element( 'div' );
383 383
@@ -452,7 +452,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
452 */ 452 */
453 getHtml: function() { 453 getHtml: function() {
454 var retval = this.$.innerHTML; 454 var retval = this.$.innerHTML;
455 // Strip <?xml:namespace> tags in IE. (#3341). 455 // Strip <?xml:namespace> tags in IE. (http://dev.ckeditor.com/ticket/3341).
456 return CKEDITOR.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval; 456 return CKEDITOR.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval;
457 }, 457 },
458 458
@@ -467,7 +467,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
467 getOuterHtml: function() { 467 getOuterHtml: function() {
468 if ( this.$.outerHTML ) { 468 if ( this.$.outerHTML ) {
469 // IE includes the <?xml:namespace> tag in the outerHTML of 469 // IE includes the <?xml:namespace> tag in the outerHTML of
470 // namespaced element. So, we must strip it here. (#3341) 470 // namespaced element. So, we must strip it here. (http://dev.ckeditor.com/ticket/3341)
471 return this.$.outerHTML.replace( /<\?[^>]*>/, '' ); 471 return this.$.outerHTML.replace( /<\?[^>]*>/, '' );
472 } 472 }
473 473
@@ -618,7 +618,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
618 return this.$[ name ]; 618 return this.$[ name ];
619 619
620 case 'style': 620 case 'style':
621 // IE does not return inline styles via getAttribute(). See #2947. 621 // IE does not return inline styles via getAttribute(). See http://dev.ckeditor.com/ticket/2947.
622 return this.$.style.cssText; 622 return this.$.style.cssText;
623 623
624 case 'contenteditable': 624 case 'contenteditable':
@@ -679,7 +679,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
679 function( propertyName ) { 679 function( propertyName ) {
680 var style = this.getWindow().$.getComputedStyle( this.$, null ); 680 var style = this.getWindow().$.getComputedStyle( this.$, null );
681 681
682 // Firefox may return null if we call the above on a hidden iframe. (#9117) 682 // Firefox may return null if we call the above on a hidden iframe. (http://dev.ckeditor.com/ticket/9117)
683 return style ? style.getPropertyValue( propertyName ) : ''; 683 return style ? style.getPropertyValue( propertyName ) : '';
684 } : function( propertyName ) { 684 } : function( propertyName ) {
685 return this.$.currentStyle[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ]; 685 return this.$.currentStyle[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ];
@@ -972,7 +972,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
972 elementWindow, elementWindowFrame; 972 elementWindow, elementWindowFrame;
973 973
974 // Webkit and Opera report non-zero offsetHeight despite that 974 // Webkit and Opera report non-zero offsetHeight despite that
975 // element is inside an invisible iframe. (#4542) 975 // element is inside an invisible iframe. (http://dev.ckeditor.com/ticket/4542)
976 if ( isVisible && CKEDITOR.env.webkit ) { 976 if ( isVisible && CKEDITOR.env.webkit ) {
977 elementWindow = this.getWindow(); 977 elementWindow = this.getWindow();
978 978
@@ -1033,7 +1033,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1033 // attribute, which will be marked as "specified", even if the 1033 // attribute, which will be marked as "specified", even if the
1034 // outerHTML of the element is not displaying the class attribute. 1034 // outerHTML of the element is not displaying the class attribute.
1035 // Note : I was not able to reproduce it outside the editor, 1035 // Note : I was not able to reproduce it outside the editor,
1036 // but I've faced it while working on the TC of #1391. 1036 // but I've faced it while working on the TC of http://dev.ckeditor.com/ticket/1391.
1037 if ( this.getAttribute( 'class' ) ) { 1037 if ( this.getAttribute( 'class' ) ) {
1038 return true; 1038 return true;
1039 } 1039 }
@@ -1057,7 +1057,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1057 var attrs = this.$.attributes, 1057 var attrs = this.$.attributes,
1058 attrsNum = attrs.length; 1058 attrsNum = attrs.length;
1059 1059
1060 // The _moz_dirty attribute might get into the element after pasting (#5455) 1060 // The _moz_dirty attribute might get into the element after pasting (http://dev.ckeditor.com/ticket/5455)
1061 var execludeAttrs = { 'data-cke-expando': 1, _moz_dirty: 1 }; 1061 var execludeAttrs = { 'data-cke-expando': 1, _moz_dirty: 1 };
1062 1062
1063 return attrsNum > 0 && ( attrsNum > 2 || !execludeAttrs[ attrs[ 0 ].nodeName ] || ( attrsNum == 2 && !execludeAttrs[ attrs[ 1 ].nodeName ] ) ); 1063 return attrsNum > 0 && ( attrsNum > 2 || !execludeAttrs[ attrs[ 0 ].nodeName ] || ( attrsNum == 2 && !execludeAttrs[ attrs[ 1 ].nodeName ] ) );
@@ -1164,7 +1164,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1164 function mergeElements( element, sibling, isNext ) { 1164 function mergeElements( element, sibling, isNext ) {
1165 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) { 1165 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) {
1166 // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>, 1166 // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>,
1167 // queuing them to be moved later. (#5567) 1167 // queuing them to be moved later. (http://dev.ckeditor.com/ticket/5567)
1168 var pendingNodes = []; 1168 var pendingNodes = [];
1169 1169
1170 while ( sibling.data( 'cke-bookmark' ) || sibling.isEmptyInlineRemoveable() ) { 1170 while ( sibling.data( 'cke-bookmark' ) || sibling.isEmptyInlineRemoveable() ) {
@@ -1194,7 +1194,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1194 } 1194 }
1195 1195
1196 return function( inlineOnly ) { 1196 return function( inlineOnly ) {
1197 // Merge empty links and anchors also. (#5567) 1197 // Merge empty links and anchors also. (http://dev.ckeditor.com/ticket/5567)
1198 if ( !( inlineOnly === false || CKEDITOR.dtd.$removeEmpty[ this.getName() ] || this.is( 'a' ) ) ) { 1198 if ( !( inlineOnly === false || CKEDITOR.dtd.$removeEmpty[ this.getName() ] || this.is( 'a' ) ) ) {
1199 return; 1199 return;
1200 } 1200 }
@@ -1253,7 +1253,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1253 }; 1253 };
1254 } else if ( CKEDITOR.env.ie8Compat && CKEDITOR.env.secure ) { 1254 } else if ( CKEDITOR.env.ie8Compat && CKEDITOR.env.secure ) {
1255 return function( name, value ) { 1255 return function( name, value ) {
1256 // IE8 throws error when setting src attribute to non-ssl value. (#7847) 1256 // IE8 throws error when setting src attribute to non-ssl value. (http://dev.ckeditor.com/ticket/7847)
1257 if ( name == 'src' && value.match( /^http:\/\// ) ) { 1257 if ( name == 'src' && value.match( /^http:\/\// ) ) {
1258 try { 1258 try {
1259 standard.apply( this, arguments ); 1259 standard.apply( this, arguments );
@@ -1501,7 +1501,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1501 clientLeft = $docElem.clientLeft || body.$.clientLeft || 0, 1501 clientLeft = $docElem.clientLeft || body.$.clientLeft || 0,
1502 needAdjustScrollAndBorders = true; 1502 needAdjustScrollAndBorders = true;
1503 1503
1504 // #3804: getBoundingClientRect() works differently on IE and non-IE 1504 // http://dev.ckeditor.com/ticket/3804: getBoundingClientRect() works differently on IE and non-IE
1505 // browsers, regarding scroll positions. 1505 // browsers, regarding scroll positions.
1506 // 1506 //
1507 // On IE, the top position of the <html> element is always 0, no matter 1507 // On IE, the top position of the <html> element is always 0, no matter
@@ -1516,12 +1516,12 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1516 needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem ); 1516 needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem );
1517 } 1517 }
1518 1518
1519 // #12747. 1519 // http://dev.ckeditor.com/ticket/12747.
1520 if ( needAdjustScrollAndBorders ) { 1520 if ( needAdjustScrollAndBorders ) {
1521 var scrollRelativeLeft, 1521 var scrollRelativeLeft,
1522 scrollRelativeTop; 1522 scrollRelativeTop;
1523 1523
1524 // See #12758 to know more about document.(documentElement|body).scroll(Left|Top) in Webkit. 1524 // See http://dev.ckeditor.com/ticket/12758 to know more about document.(documentElement|body).scroll(Left|Top) in Webkit.
1525 if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version >= 12 ) ) { 1525 if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version >= 12 ) ) {
1526 scrollRelativeLeft = body.$.scrollLeft || $docElem.scrollLeft; 1526 scrollRelativeLeft = body.$.scrollLeft || $docElem.scrollLeft;
1527 scrollRelativeTop = body.$.scrollTop || $docElem.scrollTop; 1527 scrollRelativeTop = body.$.scrollTop || $docElem.scrollTop;
@@ -1603,7 +1603,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1603 parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight; 1603 parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight;
1604 1604
1605 // Skip body element, which will report wrong clientHeight when containing 1605 // Skip body element, which will report wrong clientHeight when containing
1606 // floated content. (#9523) 1606 // floated content. (http://dev.ckeditor.com/ticket/9523)
1607 if ( overflowed && !parent.is( 'body' ) ) 1607 if ( overflowed && !parent.is( 'body' ) )
1608 this.scrollIntoParent( parent, alignToTop, 1 ); 1608 this.scrollIntoParent( parent, alignToTop, 1 );
1609 1609
@@ -1677,7 +1677,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1677 } 1677 }
1678 1678
1679 // [WebKit] Reset stored scrollTop value to not break scrollIntoView() method flow. 1679 // [WebKit] Reset stored scrollTop value to not break scrollIntoView() method flow.
1680 // Scrolling breaks when range.select() is used right after element.scrollIntoView(). (#14659) 1680 // Scrolling breaks when range.select() is used right after element.scrollIntoView(). (http://dev.ckeditor.com/ticket/14659)
1681 if ( CKEDITOR.env.webkit ) { 1681 if ( CKEDITOR.env.webkit ) {
1682 var editor = this.getEditor( false ); 1682 var editor = this.getEditor( false );
1683 1683
@@ -1857,7 +1857,7 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab
1857 this.getParent( true ) && this.$.parentNode.replaceChild( newNode.$, this.$ ); 1857 this.getParent( true ) && this.$.parentNode.replaceChild( newNode.$, this.$ );
1858 newNode.$[ 'data-cke-expando' ] = this.$[ 'data-cke-expando' ]; 1858 newNode.$[ 'data-cke-expando' ] = this.$[ 'data-cke-expando' ];
1859 this.$ = newNode.$; 1859 this.$ = newNode.$;
1860 // Bust getName's cache. (#8663) 1860 // Bust getName's cache. (http://dev.ckeditor.com/ticket/8663)
1861 delete this.getName; 1861 delete this.getName;
1862 }, 1862 },
1863 1863
diff --git a/sources/core/dom/elementpath.js b/sources/core/dom/elementpath.js
index 1a3aed0..dd50f10 100644
--- a/sources/core/dom/elementpath.js
+++ b/sources/core/dom/elementpath.js
@@ -57,6 +57,11 @@
57 // Backward compact. 57 // Backward compact.
58 root = root || startNode.getDocument().getBody(); 58 root = root || startNode.getDocument().getBody();
59 59
60 // Assign root value if startNode is null (#424)(https://dev.ckeditor.com/ticket/17028).
61 if ( !e ) {
62 e = root;
63 }
64
60 do { 65 do {
61 if ( e.type == CKEDITOR.NODE_ELEMENT ) { 66 if ( e.type == CKEDITOR.NODE_ELEMENT ) {
62 elements.push( e ); 67 elements.push( e );
@@ -84,7 +89,7 @@
84 block = e; 89 block = e;
85 90
86 if ( pathBlockLimitElements[ elementName ] ) { 91 if ( pathBlockLimitElements[ elementName ] ) {
87 // End level DIV is considered as the block, if no block is available. (#525) 92 // End level DIV is considered as the block, if no block is available. (http://dev.ckeditor.com/ticket/525)
88 // But it must NOT be the root element (checked above). 93 // But it must NOT be the root element (checked above).
89 if ( !block && elementName == 'div' && !checkHasBlock( e ) ) 94 if ( !block && elementName == 'div' && !checkHasBlock( e ) )
90 block = e; 95 block = e;
@@ -181,7 +186,9 @@ CKEDITOR.dom.elementPath.prototype = {
181 * @returns {CKEDITOR.dom.element} The first matched dom element or `null`. 186 * @returns {CKEDITOR.dom.element} The first matched dom element or `null`.
182 */ 187 */
183 contains: function( query, excludeRoot, fromTop ) { 188 contains: function( query, excludeRoot, fromTop ) {
184 var evaluator; 189 var i = 0,
190 evaluator;
191
185 if ( typeof query == 'string' ) 192 if ( typeof query == 'string' )
186 evaluator = function( node ) { 193 evaluator = function( node ) {
187 return node.getName() == query; 194 return node.getName() == query;
@@ -203,14 +210,21 @@ CKEDITOR.dom.elementPath.prototype = {
203 210
204 var elements = this.elements, 211 var elements = this.elements,
205 length = elements.length; 212 length = elements.length;
206 excludeRoot && length--; 213
214 if ( excludeRoot ) {
215 if ( !fromTop ) {
216 length -= 1;
217 } else {
218 i += 1;
219 }
220 }
207 221
208 if ( fromTop ) { 222 if ( fromTop ) {
209 elements = Array.prototype.slice.call( elements, 0 ); 223 elements = Array.prototype.slice.call( elements, 0 );
210 elements.reverse(); 224 elements.reverse();
211 } 225 }
212 226
213 for ( var i = 0; i < length; i++ ) { 227 for ( ; i < length; i++ ) {
214 if ( evaluator( elements[ i ] ) ) 228 if ( evaluator( elements[ i ] ) )
215 return elements[ i ]; 229 return elements[ i ];
216 } 230 }
diff --git a/sources/core/dom/iterator.js b/sources/core/dom/iterator.js
index 50056ec..9176e33 100644
--- a/sources/core/dom/iterator.js
+++ b/sources/core/dom/iterator.js
@@ -45,7 +45,7 @@
45 */ 45 */
46 this.forceBrBreak = 0; 46 this.forceBrBreak = 0;
47 47
48 // (#3730). 48 // (http://dev.ckeditor.com/ticket/3730).
49 /** 49 /**
50 * Whether to include `<br>` elements in the enlarged range. Should be 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. 51 * set to `false` when using the iterator in the {@link CKEDITOR#ENTER_BR} mode.
@@ -85,7 +85,7 @@
85 */ 85 */
86 86
87 var beginWhitespaceRegex = /^[\r\n\t ]+$/, 87 var beginWhitespaceRegex = /^[\r\n\t ]+$/,
88 // Ignore bookmark nodes.(#3783) 88 // Ignore bookmark nodes.(http://dev.ckeditor.com/ticket/3783)
89 bookmarkGuard = CKEDITOR.dom.walker.bookmark( false, true ), 89 bookmarkGuard = CKEDITOR.dom.walker.bookmark( false, true ),
90 whitespacesGuard = CKEDITOR.dom.walker.whitespaces( true ), 90 whitespacesGuard = CKEDITOR.dom.walker.whitespaces( true ),
91 skipGuard = function( node ) { 91 skipGuard = function( node ) {
@@ -191,12 +191,12 @@
191 } 191 }
192 192
193 // The range must finish right before the boundary, 193 // The range must finish right before the boundary,
194 // including possibly skipped empty spaces. (#1603) 194 // including possibly skipped empty spaces. (http://dev.ckeditor.com/ticket/1603)
195 if ( range ) { 195 if ( range ) {
196 range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START ); 196 range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
197 197
198 // The found boundary must be set as the next one at this 198 // The found boundary must be set as the next one at this
199 // point. (#1717) 199 // point. (http://dev.ckeditor.com/ticket/1717)
200 if ( nodeName != 'br' ) { 200 if ( nodeName != 'br' ) {
201 this._.nextNode = currentNode; 201 this._.nextNode = currentNode;
202 } 202 }
@@ -244,7 +244,7 @@
244 closeRange = 1; 244 closeRange = 1;
245 includeNode = 0; 245 includeNode = 0;
246 isLast = isLast || ( parentNode.equals( lastNode ) ); 246 isLast = isLast || ( parentNode.equals( lastNode ) );
247 // Make sure range includes bookmarks at the end of the block. (#7359) 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 ); 248 range.setEndAt( parentNode, CKEDITOR.POSITION_BEFORE_END );
249 break; 249 break;
250 } 250 }
@@ -377,7 +377,7 @@
377 // Here we are checking in guard function whether current element 377 // Here we are checking in guard function whether current element
378 // reach lastNode(default behaviour) and root node to prevent against 378 // reach lastNode(default behaviour) and root node to prevent against
379 // getting out of editor instance root DOM object. 379 // getting out of editor instance root DOM object.
380 // #12484 380 // http://dev.ckeditor.com/ticket/12484
381 function guardFunction( node ) { 381 function guardFunction( node ) {
382 return !( node.equals( lastNode ) || node.equals( rootNode ) ); 382 return !( node.equals( lastNode ) || node.equals( rootNode ) );
383 } 383 }
@@ -397,7 +397,7 @@
397 // Indicate at least one of the range boundaries is inside a preformat block. 397 // Indicate at least one of the range boundaries is inside a preformat block.
398 touchPre, 398 touchPre,
399 399
400 // (#12178) 400 // (http://dev.ckeditor.com/ticket/12178)
401 // Remember if following situation takes place: 401 // Remember if following situation takes place:
402 // * startAtInnerBoundary: <p>foo[</p>... 402 // * startAtInnerBoundary: <p>foo[</p>...
403 // * endAtInnerBoundary: ...<p>]bar</p> 403 // * endAtInnerBoundary: ...<p>]bar</p>
@@ -405,13 +405,13 @@
405 // Note that we test only if path block exist, because we must properly shrink 405 // Note that we test only if path block exist, because we must properly shrink
406 // range containing table and/or table cells. 406 // range containing table and/or table cells.
407 // Note: When range is collapsed there's no way it can be shrinked. 407 // Note: When range is collapsed there's no way it can be shrinked.
408 // By checking if range is collapsed we also prevent #12308. 408 // By checking if range is collapsed we also prevent http://dev.ckeditor.com/ticket/12308.
409 startPath = range.startPath(), 409 startPath = range.startPath(),
410 endPath = range.endPath(), 410 endPath = range.endPath(),
411 startAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, startPath.block ), 411 startAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, startPath.block ),
412 endAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, endPath.block, 1 ); 412 endAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, endPath.block, 1 );
413 413
414 // Shrink the range to exclude harmful "noises" (#4087, #4450, #5435). 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 ); 415 range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
416 416
417 if ( startAtInnerBoundary ) 417 if ( startAtInnerBoundary )
@@ -437,7 +437,7 @@
437 437
438 // We may have an empty text node at the end of block due to [3770]. 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 439 // If that node is the lastNode, it would cause our logic to leak to the
440 // next block.(#3887) 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() ) { 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(); 442 var testRange = this.range.clone();
443 testRange.moveToPosition( this._.lastNode, CKEDITOR.POSITION_AFTER_END ); 443 testRange.moveToPosition( this._.lastNode, CKEDITOR.POSITION_AFTER_END );
diff --git a/sources/core/dom/node.js b/sources/core/dom/node.js
index 51bba18..69b223e 100644
--- a/sources/core/dom/node.js
+++ b/sources/core/dom/node.js
@@ -200,7 +200,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
200 } 200 }
201 201
202 // IE8 rename HTML5 nodes by adding `:` at the begging of the tag name when the node is cloned, 202 // IE8 rename HTML5 nodes by adding `:` at the begging of the tag name when the node is cloned,
203 // so `<figure>` will be `<:figure>` after 'cloneNode'. We need to fix it (#13101). 203 // so `<figure>` will be `<:figure>` after 'cloneNode'. We need to fix it (http://dev.ckeditor.com/ticket/13101).
204 function renameNodes( node ) { 204 function renameNodes( node ) {
205 if ( node.type != CKEDITOR.NODE_ELEMENT && node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) 205 if ( node.type != CKEDITOR.NODE_ELEMENT && node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
206 return; 206 return;
@@ -804,7 +804,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
804 } else if ( trimmed.length < originalLength ) { 804 } else if ( trimmed.length < originalLength ) {
805 child.split( originalLength - trimmed.length ); 805 child.split( originalLength - trimmed.length );
806 806
807 // IE BUG: child.remove() may raise JavaScript errors here. (#81) 807 // IE BUG: child.remove() may raise JavaScript errors here. (http://dev.ckeditor.com/ticket/81)
808 this.$.removeChild( this.$.firstChild ); 808 this.$.removeChild( this.$.firstChild );
809 } 809 }
810 } 810 }
@@ -829,7 +829,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
829 child.split( trimmed.length ); 829 child.split( trimmed.length );
830 830
831 // IE BUG: child.getNext().remove() may raise JavaScript errors here. 831 // IE BUG: child.getNext().remove() may raise JavaScript errors here.
832 // (#81) 832 // (http://dev.ckeditor.com/ticket/81)
833 this.$.lastChild.parentNode.removeChild( this.$.lastChild ); 833 this.$.lastChild.parentNode.removeChild( this.$.lastChild );
834 } 834 }
835 } 835 }
@@ -840,7 +840,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
840 child = this.$.lastChild; 840 child = this.$.lastChild;
841 841
842 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) { 842 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) {
843 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324). 843 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (http://dev.ckeditor.com/ticket/324).
844 child.parentNode.removeChild( child ); 844 child.parentNode.removeChild( child );
845 } 845 }
846 } 846 }
@@ -874,7 +874,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
874 if ( this.type != CKEDITOR.NODE_ELEMENT ) 874 if ( this.type != CKEDITOR.NODE_ELEMENT )
875 element = this.getParent(); 875 element = this.getParent();
876 876
877 // Prevent Edge crash (#13609, #13919). 877 // Prevent Edge crash (http://dev.ckeditor.com/ticket/13609, http://dev.ckeditor.com/ticket/13919).
878 if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) { 878 if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) {
879 checkOnlyAttributes = true; 879 checkOnlyAttributes = true;
880 } 880 }
diff --git a/sources/core/dom/nodelist.js b/sources/core/dom/nodelist.js
index 165a415..07af314 100644
--- a/sources/core/dom/nodelist.js
+++ b/sources/core/dom/nodelist.js
@@ -5,7 +5,7 @@
5 5
6/** 6/**
7 * Represents a list of {@link CKEDITOR.dom.node} objects. 7 * Represents a list of {@link CKEDITOR.dom.node} objects.
8 * It's a wrapper for native nodes list. 8 * It is a wrapper for a native nodes list.
9 * 9 *
10 * var nodeList = CKEDITOR.document.getBody().getChildren(); 10 * var nodeList = CKEDITOR.document.getBody().getChildren();
11 * alert( nodeList.count() ); // number [0;N] 11 * alert( nodeList.count() ); // number [0;N]
@@ -20,7 +20,7 @@ CKEDITOR.dom.nodeList = function( nativeList ) {
20 20
21CKEDITOR.dom.nodeList.prototype = { 21CKEDITOR.dom.nodeList.prototype = {
22 /** 22 /**
23 * Get count of nodes in this list. 23 * Gets the count of nodes in this list.
24 * 24 *
25 * @returns {Number} 25 * @returns {Number}
26 */ 26 */
@@ -29,7 +29,7 @@ CKEDITOR.dom.nodeList.prototype = {
29 }, 29 },
30 30
31 /** 31 /**
32 * Get node from the list. 32 * Gets the node from the list.
33 * 33 *
34 * @returns {CKEDITOR.dom.node} 34 * @returns {CKEDITOR.dom.node}
35 */ 35 */
@@ -39,5 +39,16 @@ CKEDITOR.dom.nodeList.prototype = {
39 39
40 var $node = this.$[ index ]; 40 var $node = this.$[ index ];
41 return $node ? new CKEDITOR.dom.node( $node ) : null; 41 return $node ? new CKEDITOR.dom.node( $node ) : null;
42 },
43
44 /**
45 * Returns a node list as an array.
46 *
47 * @returns {CKEDITOR.dom.node[]}
48 */
49 toArray: function() {
50 return CKEDITOR.tools.array.map( this.$, function( nativeEl ) {
51 return new CKEDITOR.dom.node( nativeEl );
52 } );
42 } 53 }
43}; 54};
diff --git a/sources/core/dom/range.js b/sources/core/dom/range.js
index 6407074..742c24c 100644
--- a/sources/core/dom/range.js
+++ b/sources/core/dom/range.js
@@ -204,9 +204,14 @@ CKEDITOR.dom.range = function( root ) {
204 // This allows us to not think about startNode == endNode case later on. 204 // This allows us to not think about startNode == endNode case later on.
205 // We do that only when cloning, because in other cases we can safely split this text node 205 // We do that only when cloning, because in other cases we can safely split this text node
206 // and hence we can easily handle this case as many others. 206 // and hence we can easily handle this case as many others.
207 if ( isClone && endNode.type == CKEDITOR.NODE_TEXT && startNode.equals( endNode ) ) { 207
208 startNode = range.document.createText( startNode.substring( startOffset, endOffset ) ); 208 // We need to handle situation when selection startNode is type of NODE_ELEMENT (#426).
209 docFrag.append( startNode ); 209 if ( isClone &&
210 endNode.type == CKEDITOR.NODE_TEXT &&
211 ( startNode.equals( endNode ) || ( startNode.type === CKEDITOR.NODE_ELEMENT && startNode.getFirst().equals( endNode ) ) ) ) {
212
213 // Here we should always be inside one text node.
214 docFrag.append( range.document.createText( endNode.substring( startOffset, endOffset ) ) );
210 return; 215 return;
211 } 216 }
212 217
@@ -214,7 +219,7 @@ CKEDITOR.dom.range = function( root ) {
214 // second part. The removal will be handled by the rest of the code. 219 // second part. The removal will be handled by the rest of the code.
215 if ( endNode.type == CKEDITOR.NODE_TEXT ) { 220 if ( endNode.type == CKEDITOR.NODE_TEXT ) {
216 // If Extract or Delete we can split the text node, 221 // If Extract or Delete we can split the text node,
217 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark the text node for cloning. 222 // but if Clone (2), then we cannot modify the DOM (http://dev.ckeditor.com/ticket/11586) so we mark the text node for cloning.
218 if ( !isClone ) { 223 if ( !isClone ) {
219 endNode = endNode.split( endOffset ); 224 endNode = endNode.split( endOffset );
220 } else { 225 } else {
@@ -243,7 +248,7 @@ CKEDITOR.dom.range = function( root ) {
243 // be handled by the rest of the code . 248 // be handled by the rest of the code .
244 if ( startNode.type == CKEDITOR.NODE_TEXT ) { 249 if ( startNode.type == CKEDITOR.NODE_TEXT ) {
245 // If Extract or Delete we can split the text node, 250 // If Extract or Delete we can split the text node,
246 // but if Clone (2), then we cannot modify the DOM (#11586) so we mark 251 // but if Clone (2), then we cannot modify the DOM (http://dev.ckeditor.com/ticket/11586) so we mark
247 // the text node for cloning. 252 // the text node for cloning.
248 if ( !isClone ) { 253 if ( !isClone ) {
249 startNode.split( startOffset ); 254 startNode.split( startOffset );
@@ -373,7 +378,7 @@ CKEDITOR.dom.range = function( root ) {
373 // If we don't do that, in next iterations nodes will be appended to wrong parent. 378 // If we don't do that, in next iterations nodes will be appended to wrong parent.
374 // 379 //
375 // We can just take first child because the algorithm guarantees 380 // We can just take first child because the algorithm guarantees
376 // that this will be the only child on this level. (#13568) 381 // that this will be the only child on this level. (http://dev.ckeditor.com/ticket/13568)
377 levelParent = levelParent.getChild( 0 ); 382 levelParent = levelParent.getChild( 0 );
378 } 383 }
379 } 384 }
@@ -405,7 +410,7 @@ CKEDITOR.dom.range = function( root ) {
405 410
406 // When Extracting, move the removed node to the docFrag. 411 // When Extracting, move the removed node to the docFrag.
407 if ( isExtract ) { 412 if ( isExtract ) {
408 newParent.append( node ); 413 newParent.append( node, toStart );
409 } 414 }
410 } 415 }
411 416
@@ -574,7 +579,7 @@ CKEDITOR.dom.range = function( root ) {
574 // Tolerant bogus br when checking at the end of block. 579 // Tolerant bogus br when checking at the end of block.
575 // Reject any text node unless it's being bookmark 580 // Reject any text node unless it's being bookmark
576 // OR it's spaces. 581 // OR it's spaces.
577 // Reject any element unless it's being invisible empty. (#3883) 582 // Reject any element unless it's being invisible empty. (http://dev.ckeditor.com/ticket/3883)
578 return !checkStart && isBogus( node ) || 583 return !checkStart && isBogus( node ) ||
579 node.type == CKEDITOR.NODE_ELEMENT && 584 node.type == CKEDITOR.NODE_ELEMENT &&
580 node.is( CKEDITOR.dtd.$removeEmpty ); 585 node.is( CKEDITOR.dtd.$removeEmpty );
@@ -1052,7 +1057,7 @@ CKEDITOR.dom.range = function( root ) {
1052 } 1057 }
1053 1058
1054 // Sometimes the endNode will come right before startNode for collapsed 1059 // Sometimes the endNode will come right before startNode for collapsed
1055 // ranges. Fix it. (#3780) 1060 // ranges. Fix it. (http://dev.ckeditor.com/ticket/3780)
1056 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING ) 1061 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
1057 startNode = endNode; 1062 startNode = endNode;
1058 1063
@@ -1324,13 +1329,13 @@ CKEDITOR.dom.range = function( root ) {
1324 1329
1325 isWhiteSpace = /[\s\ufeff]$/.test( siblingText ); 1330 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
1326 } else { 1331 } else {
1327 // #12221 (Chrome) plus #11111 (Safari). 1332 // http://dev.ckeditor.com/ticket/12221 (Chrome) plus http://dev.ckeditor.com/ticket/11111 (Safari).
1328 var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0; 1333 var offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
1329 1334
1330 // If this is a visible element. 1335 // If this is a visible element.
1331 // We need to check for the bookmark attribute because IE insists on 1336 // We need to check for the bookmark attribute because IE insists on
1332 // rendering the display:none nodes we use for bookmarks. (#3363) 1337 // rendering the display:none nodes we use for bookmarks. (http://dev.ckeditor.com/ticket/3363)
1333 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041) 1338 // Line-breaks (br) are rendered with zero width, which we don't want to include. (http://dev.ckeditor.com/ticket/7041)
1334 if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) { 1339 if ( ( sibling.$.offsetWidth > offsetWidth0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1335 // We'll accept it only if we need 1340 // We'll accept it only if we need
1336 // whitespace, and this is an inline 1341 // whitespace, and this is an inline
@@ -1399,7 +1404,7 @@ CKEDITOR.dom.range = function( root ) {
1399 1404
1400 // Process the end boundary. This is basically the same 1405 // Process the end boundary. This is basically the same
1401 // code used for the start boundary, with small changes to 1406 // code used for the start boundary, with small changes to
1402 // make it work in the oposite side (to the right). This 1407 // make it work in the opposite side (to the right). This
1403 // makes it difficult to reuse the code here. So, fixes to 1408 // makes it difficult to reuse the code here. So, fixes to
1404 // the above code are likely to be replicated here. 1409 // the above code are likely to be replicated here.
1405 1410
@@ -1482,7 +1487,7 @@ CKEDITOR.dom.range = function( root ) {
1482 } 1487 }
1483 } 1488 }
1484 } else { 1489 } else {
1485 // Get the node right after the boudary to be checked 1490 // Get the node right after the boundary to be checked
1486 // first. 1491 // first.
1487 sibling = container.getChild( offset ); 1492 sibling = container.getChild( offset );
1488 1493
@@ -1525,8 +1530,8 @@ CKEDITOR.dom.range = function( root ) {
1525 } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) { 1530 } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) {
1526 // If this is a visible element. 1531 // If this is a visible element.
1527 // We need to check for the bookmark attribute because IE insists on 1532 // We need to check for the bookmark attribute because IE insists on
1528 // rendering the display:none nodes we use for bookmarks. (#3363) 1533 // rendering the display:none nodes we use for bookmarks. (http://dev.ckeditor.com/ticket/3363)
1529 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041) 1534 // Line-breaks (br) are rendered with zero width, which we don't want to include. (http://dev.ckeditor.com/ticket/7041)
1530 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) { 1535 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
1531 // We'll accept it only if we need 1536 // We'll accept it only if we need
1532 // whitespace, and this is an inline 1537 // whitespace, and this is an inline
@@ -1617,7 +1622,7 @@ CKEDITOR.dom.range = function( root ) {
1617 boundaryGuard = function( node ) { 1622 boundaryGuard = function( node ) {
1618 // We should not check contents of non-editable elements. It may happen 1623 // We should not check contents of non-editable elements. It may happen
1619 // that inline widget has display:table child which should not block range#enlarge. 1624 // that inline widget has display:table child which should not block range#enlarge.
1620 // When encoutered non-editable element... 1625 // When encountered non-editable element...
1621 if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) { 1626 if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) {
1622 if ( inNonEditable ) { 1627 if ( inNonEditable ) {
1623 // ... in which we already were, reset it (because we're leaving it) and return. 1628 // ... in which we already were, reset it (because we're leaving it) and return.
@@ -1639,7 +1644,7 @@ CKEDITOR.dom.range = function( root ) {
1639 blockBoundary = node; 1644 blockBoundary = node;
1640 return retval; 1645 return retval;
1641 }, 1646 },
1642 // Record the encounted 'tailBr' for later use. 1647 // Record the encountered 'tailBr' for later use.
1643 tailBrGuard = function( node ) { 1648 tailBrGuard = function( node ) {
1644 var retval = boundaryGuard( node ); 1649 var retval = boundaryGuard( node );
1645 if ( !retval && node.is && node.is( 'br' ) ) 1650 if ( !retval && node.is && node.is( 'br' ) )
@@ -1660,7 +1665,7 @@ CKEDITOR.dom.range = function( root ) {
1660 this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() || 1665 this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() ||
1661 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END ); 1666 enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END );
1662 1667
1663 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490) 1668 // Avoid enlarging the range further when end boundary spans right after the BR. (http://dev.ckeditor.com/ticket/7490)
1664 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) { 1669 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) {
1665 var theRange = this.clone(); 1670 var theRange = this.clone();
1666 walker = new CKEDITOR.dom.walker( theRange ); 1671 walker = new CKEDITOR.dom.walker( theRange );
@@ -1715,18 +1720,28 @@ CKEDITOR.dom.range = function( root ) {
1715 }, 1720 },
1716 1721
1717 /** 1722 /**
1718 * Descrease the range to make sure that boundaries 1723 * Decreases the range to make sure that boundaries
1719 * always anchor beside text nodes or innermost element. 1724 * always anchor beside text nodes or the innermost element.
1720 * 1725 *
1721 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}). 1726 * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
1722 * 1727 *
1723 * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element. 1728 * * {@link CKEDITOR#SHRINK_ELEMENT} &ndash; Shrinks the range boundaries to the edge of the innermost element.
1724 * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text 1729 * * {@link CKEDITOR#SHRINK_TEXT} &ndash; Shrinks the range boundaries to anchor by the side of enclosed text
1725 * node, range remains if there's no text nodes on boundaries at all. 1730 * node. The range remains if there are no text nodes available on boundaries.
1726 * 1731 *
1727 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node. 1732 * @param {Boolean} [selectContents=false] Whether the resulting range anchors at the inner OR outer boundary of the node.
1733 * @param {Boolean/Object} [options=true] If this parameter is of a Boolean type, it is treated as
1734 * `options.shrinkOnBlockBoundary`. This parameter was added in 4.7.0.
1735 * @param {Boolean} [options.shrinkOnBlockBoundary=true] Whether the block boundary should be included in
1736 * the shrunk range.
1737 * @param {Boolean} [options.skipBogus=false] Whether bogus `<br>` elements should be ignored while
1738 * `mode` is set to {@link CKEDITOR#SHRINK_TEXT}. This option was added in 4.7.0.
1728 */ 1739 */
1729 shrink: function( mode, selectContents, shrinkOnBlockBoundary ) { 1740 shrink: function( mode, selectContents, options ) {
1741 var shrinkOnBlockBoundary = typeof options === 'boolean' ? options :
1742 ( options && typeof options.shrinkOnBlockBoundary === 'boolean' ? options.shrinkOnBlockBoundary : true ),
1743 skipBogus = options && options.skipBogus;
1744
1730 // Unable to shrink a collapsed range. 1745 // Unable to shrink a collapsed range.
1731 if ( !this.collapsed ) { 1746 if ( !this.collapsed ) {
1732 mode = mode || CKEDITOR.SHRINK_TEXT; 1747 mode = mode || CKEDITOR.SHRINK_TEXT;
@@ -1749,7 +1764,7 @@ CKEDITOR.dom.range = function( root ) {
1749 walkerRange.setStartAfter( startContainer ); 1764 walkerRange.setStartAfter( startContainer );
1750 else { 1765 else {
1751 // Enlarge the range properly to avoid walker making 1766 // Enlarge the range properly to avoid walker making
1752 // DOM changes caused by triming the text nodes later. 1767 // DOM changes caused by trimming the text nodes later.
1753 walkerRange.setStartBefore( startContainer ); 1768 walkerRange.setStartBefore( startContainer );
1754 moveStart = 0; 1769 moveStart = 0;
1755 } 1770 }
@@ -1767,7 +1782,8 @@ CKEDITOR.dom.range = function( root ) {
1767 } 1782 }
1768 1783
1769 var walker = new CKEDITOR.dom.walker( walkerRange ), 1784 var walker = new CKEDITOR.dom.walker( walkerRange ),
1770 isBookmark = CKEDITOR.dom.walker.bookmark(); 1785 isBookmark = CKEDITOR.dom.walker.bookmark(),
1786 isBogus = CKEDITOR.dom.walker.bogus();
1771 1787
1772 walker.evaluator = function( node ) { 1788 walker.evaluator = function( node ) {
1773 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT ); 1789 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
@@ -1775,6 +1791,11 @@ CKEDITOR.dom.range = function( root ) {
1775 1791
1776 var currentElement; 1792 var currentElement;
1777 walker.guard = function( node, movingOut ) { 1793 walker.guard = function( node, movingOut ) {
1794 // Skipping bogus before other cases (http://dev.ckeditor.com/ticket/17010).
1795 if ( skipBogus && isBogus( node ) ) {
1796 return true;
1797 }
1798
1778 if ( isBookmark( node ) ) 1799 if ( isBookmark( node ) )
1779 return true; 1800 return true;
1780 1801
@@ -1816,7 +1837,7 @@ CKEDITOR.dom.range = function( root ) {
1816 1837
1817 /** 1838 /**
1818 * Inserts a node at the start of the range. The range will be expanded 1839 * Inserts a node at the start of the range. The range will be expanded
1819 * the contain the node. 1840 * to contain the node.
1820 * 1841 *
1821 * @param {CKEDITOR.dom.node} node 1842 * @param {CKEDITOR.dom.node} node
1822 */ 1843 */
@@ -1843,7 +1864,7 @@ CKEDITOR.dom.range = function( root ) {
1843 }, 1864 },
1844 1865
1845 /** 1866 /**
1846 * Moves the range to given position according to specified node. 1867 * Moves the range to a given position according to the specified node.
1847 * 1868 *
1848 * // HTML: <p>Foo <b>bar</b></p> 1869 * // HTML: <p>Foo <b>bar</b></p>
1849 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START ); 1870 * range.moveToPosition( elB, CKEDITOR.POSITION_BEFORE_START );
@@ -1851,7 +1872,7 @@ CKEDITOR.dom.range = function( root ) {
1851 * 1872 *
1852 * See also {@link #setStartAt} and {@link #setEndAt}. 1873 * See also {@link #setStartAt} and {@link #setEndAt}.
1853 * 1874 *
1854 * @param {CKEDITOR.dom.node} node The node according to which position will be set. 1875 * @param {CKEDITOR.dom.node} node The node according to which the position will be set.
1855 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START}, 1876 * @param {Number} position One of {@link CKEDITOR#POSITION_BEFORE_START},
1856 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END}, 1877 * {@link CKEDITOR#POSITION_AFTER_START}, {@link CKEDITOR#POSITION_BEFORE_END},
1857 * {@link CKEDITOR#POSITION_AFTER_END}. 1878 * {@link CKEDITOR#POSITION_AFTER_END}.
@@ -2118,7 +2139,7 @@ CKEDITOR.dom.range = function( root ) {
2118 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it 2139 // So even if the initial range was placed before the bogus <br>, after creating the bookmark it
2119 // is placed before the bookmark. 2140 // is placed before the bookmark.
2120 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case. 2141 // Fortunately, getBogus() is able to skip the bookmark so it finds the bogus <br> in this case.
2121 // We remove incorrectly placed one and add a brand new one. (#13001) 2142 // We remove incorrectly placed one and add a brand new one. (http://dev.ckeditor.com/ticket/13001)
2122 var bogus = fixedBlock.getBogus(); 2143 var bogus = fixedBlock.getBogus();
2123 if ( bogus ) { 2144 if ( bogus ) {
2124 bogus.remove(); 2145 bogus.remove();
@@ -2340,7 +2361,7 @@ CKEDITOR.dom.range = function( root ) {
2340 this.trim( 0, 1 ); 2361 this.trim( 0, 1 );
2341 } 2362 }
2342 2363
2343 // Antecipate the trim() call here, so the walker will not make 2364 // Anticipate the trim() call here, so the walker will not make
2344 // changes to the DOM, which would not get reflected into this 2365 // changes to the DOM, which would not get reflected into this
2345 // range otherwise. 2366 // range otherwise.
2346 this.trim(); 2367 this.trim();
@@ -2379,7 +2400,7 @@ CKEDITOR.dom.range = function( root ) {
2379 this.trim( 1, 0 ); 2400 this.trim( 1, 0 );
2380 } 2401 }
2381 2402
2382 // Antecipate the trim() call here, so the walker will not make 2403 // Anticipate the trim() call here, so the walker will not make
2383 // changes to the DOM, which would not get reflected into this 2404 // changes to the DOM, which would not get reflected into this
2384 // range otherwise. 2405 // range otherwise.
2385 this.trim(); 2406 this.trim();
@@ -2636,7 +2657,7 @@ CKEDITOR.dom.range = function( root ) {
2636 getEnclosedNode: function() { 2657 getEnclosedNode: function() {
2637 var walkerRange = this.clone(); 2658 var walkerRange = this.clone();
2638 2659
2639 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780) 2660 // Optimize and analyze the range to avoid DOM destructive nature of walker. (http://dev.ckeditor.com/ticket/5780)
2640 walkerRange.optimize(); 2661 walkerRange.optimize();
2641 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT ) 2662 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
2642 return null; 2663 return null;
@@ -2703,6 +2724,53 @@ CKEDITOR.dom.range = function( root ) {
2703 getPreviousEditableNode: getNextEditableNode( 1 ), 2724 getPreviousEditableNode: getNextEditableNode( 1 ),
2704 2725
2705 /** 2726 /**
2727 * Returns any table element, like `td`, `tbody`, `table` etc. from a given range. The element
2728 * is returned only if the range is contained within one table (might be a nested
2729 * table, but it cannot be two different tables on the same DOM level).
2730 *
2731 * @private
2732 * @since 4.7
2733 * @param {Object} [tableElements] Mapping of element names that should be considered.
2734 * @returns {CKEDITOR.dom.element/null}
2735 */
2736 _getTableElement: function( tableElements ) {
2737 tableElements = tableElements || {
2738 td: 1,
2739 th: 1,
2740 tr: 1,
2741 tbody: 1,
2742 thead: 1,
2743 tfoot: 1,
2744 table: 1
2745 };
2746
2747 var start = this.startContainer,
2748 end = this.endContainer,
2749 startTable = start.getAscendant( 'table', true ),
2750 endTable = end.getAscendant( 'table', true );
2751
2752 // Super weird edge case in Safari: if there is a table with only one cell inside and that cell
2753 // is selected, then the end boundary of the table is moved into editor's editable.
2754 // That case is also present when selecting the last cell inside nested table.
2755 if ( CKEDITOR.env.safari && startTable && end.equals( this.root ) ) {
2756 return start.getAscendant( tableElements, true );
2757 }
2758
2759 if ( this.getEnclosedNode() ) {
2760 return this.getEnclosedNode().getAscendant( tableElements, true );
2761 }
2762
2763 // Ensure that selection starts and ends in the same table or one of the table is inside the other.
2764 if ( startTable && endTable && ( startTable.equals( endTable ) || startTable.contains( endTable ) ||
2765 endTable.contains( startTable ) ) ) {
2766
2767 return start.getAscendant( tableElements, true );
2768 }
2769
2770 return null;
2771 },
2772
2773 /**
2706 * Scrolls the start of current range into view. 2774 * Scrolls the start of current range into view.
2707 */ 2775 */
2708 scrollIntoView: function() { 2776 scrollIntoView: function() {
@@ -2819,10 +2887,10 @@ CKEDITOR.dom.range = function( root ) {
2819 continue; 2887 continue;
2820 } 2888 }
2821 2889
2822 // It's not enough to get elements from common ancestor, because it migth contain too many matches. 2890 // It's not enough to get elements from common ancestor, because it might contain too many matches.
2823 // We need to ensure that returned items are between boundary points. 2891 // We need to ensure that returned items are between boundary points.
2824 isStartGood = ( curItem.getPosition( boundaries.startNode ) & CKEDITOR.POSITION_FOLLOWING ) || boundaries.startNode.equals( curItem ); 2892 isStartGood = ( curItem.getPosition( boundaries.startNode ) & CKEDITOR.POSITION_FOLLOWING ) || boundaries.startNode.equals( curItem );
2825 isEndGood = ( curItem.getPosition( boundaries.endNode ) & ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IS_CONTAINED ) ); 2893 isEndGood = ( curItem.getPosition( boundaries.endNode ) & ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IS_CONTAINED ) ) || boundaries.endNode.equals( curItem );
2826 2894
2827 if ( isStartGood && isEndGood ) { 2895 if ( isStartGood && isEndGood ) {
2828 ret.push( curItem ); 2896 ret.push( curItem );
@@ -2834,6 +2902,62 @@ CKEDITOR.dom.range = function( root ) {
2834 } 2902 }
2835 }; 2903 };
2836 2904
2905 /**
2906 * Merges every subsequent range in given set, returning a smaller array of ranges.
2907 *
2908 * Note that each range in the returned value will be enlarged with `CKEDITOR.ENLARGE_ELEMENT` value.
2909 *
2910 * @since 4.7.0
2911 * @static
2912 * @param {CKEDITOR.dom.range[]} ranges
2913 * @returns {CKEDITOR.dom.range[]} Set of merged ranges.
2914 * @member CKEDITOR.dom.range
2915 */
2916 CKEDITOR.dom.range.mergeRanges = function( ranges ) {
2917 return CKEDITOR.tools.array.reduce( ranges, function( ret, rng ) {
2918 // Last range ATM.
2919 var lastRange = ret[ ret.length - 1 ],
2920 isContinuation = false;
2921
2922 // Make a clone, we don't want to modify input.
2923 rng = rng.clone();
2924 rng.enlarge( CKEDITOR.ENLARGE_ELEMENT );
2925
2926 if ( lastRange ) {
2927 // The trick is to create a range spanning the gap between the two ranges. Then iterate over
2928 // each node found in this gap. If it contains anything other than whitespace, then it means it
2929 // is not a continuation.
2930 var gapRange = new CKEDITOR.dom.range( rng.root ),
2931 walker = new CKEDITOR.dom.walker( gapRange ),
2932 isWhitespace = CKEDITOR.dom.walker.whitespaces(),
2933 nodeInBetween;
2934
2935 gapRange.setStart( lastRange.endContainer, lastRange.endOffset );
2936 gapRange.setEnd( rng.startContainer, rng.startOffset );
2937
2938 nodeInBetween = walker.next();
2939
2940 while ( isWhitespace( nodeInBetween ) || rng.endContainer.equals( nodeInBetween ) ) {
2941 // We don't care about whitespaces, and range container. Also we skip the endContainer,
2942 // as it will also be provided by the iterator (as it visits it's opening tag).
2943 nodeInBetween = walker.next();
2944 }
2945
2946 // Simply, if anything has been found there's a content in between the two.
2947 isContinuation = !nodeInBetween;
2948 }
2949
2950 if ( isContinuation ) {
2951 // If last range ends, where the current range starts, then let's merge it.
2952 lastRange.setEnd( rng.endContainer, rng.endOffset );
2953 } else {
2954 // In other case just push cur range into the stack.
2955 ret.push( rng );
2956 }
2957
2958 return ret;
2959 }, [] );
2960 };
2837 2961
2838} )(); 2962} )();
2839 2963
diff --git a/sources/core/dom/rangelist.js b/sources/core/dom/rangelist.js
index 250dfd9..15ccb88 100644
--- a/sources/core/dom/rangelist.js
+++ b/sources/core/dom/rangelist.js
@@ -164,7 +164,7 @@
164 }; 164 };
165 165
166 // Update the specified range which has been mangled by previous insertion of 166 // Update the specified range which has been mangled by previous insertion of
167 // range bookmark nodes.(#3256) 167 // range bookmark nodes.(http://dev.ckeditor.com/ticket/3256)
168 function updateDirtyRange( bookmark, dirtyRange, checkEnd ) { 168 function updateDirtyRange( bookmark, dirtyRange, checkEnd ) {
169 var serializable = bookmark.serializable, 169 var serializable = bookmark.serializable,
170 container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ], 170 container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ],
diff --git a/sources/core/dom/text.js b/sources/core/dom/text.js
index ce20ffe..e77a3d9 100644
--- a/sources/core/dom/text.js
+++ b/sources/core/dom/text.js
@@ -106,7 +106,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.text.prototype, {
106 retval.insertAfter( this ); 106 retval.insertAfter( this );
107 } else { 107 } else {
108 // IE BUG: IE8+ does not update the childNodes array in DOM after splitText(), 108 // IE BUG: IE8+ does not update the childNodes array in DOM after splitText(),
109 // we need to make some DOM changes to make it update. (#3436) 109 // we need to make some DOM changes to make it update. (http://dev.ckeditor.com/ticket/3436)
110 var workaround = doc.createText( '' ); 110 var workaround = doc.createText( '' );
111 workaround.insertAfter( retval ); 111 workaround.insertAfter( retval );
112 workaround.remove(); 112 workaround.remove();
diff --git a/sources/core/dom/walker.js b/sources/core/dom/walker.js
index cec4574..8665909 100644
--- a/sources/core/dom/walker.js
+++ b/sources/core/dom/walker.js
@@ -319,7 +319,7 @@
319 */ 319 */
320 CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) { 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. 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) 322 // Don't consider floated or positioned formatting as block boundary, fall back to dtd check in that case. (http://dev.ckeditor.com/ticket/6297)
323 var inPageFlow = this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions ); 323 var inPageFlow = this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions );
324 324
325 if ( inPageFlow && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] ) 325 if ( inPageFlow && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] )
@@ -388,7 +388,7 @@
388 return function( node ) { 388 return function( node ) {
389 var isWhitespace; 389 var isWhitespace;
390 if ( node && node.type == CKEDITOR.NODE_TEXT ) { 390 if ( node && node.type == CKEDITOR.NODE_TEXT ) {
391 // Whitespace, as well as the Filling Char Sequence text node used in Webkit. (#9384, #13816) 391 // Whitespace, as well as the Filling Char Sequence text node used in Webkit. (http://dev.ckeditor.com/ticket/9384, http://dev.ckeditor.com/ticket/13816)
392 isWhitespace = !CKEDITOR.tools.trim( node.getText() ) || 392 isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
393 CKEDITOR.env.webkit && node.getText() == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE; 393 CKEDITOR.env.webkit && node.getText() == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE;
394 } 394 }
@@ -406,7 +406,7 @@
406 */ 406 */
407 CKEDITOR.dom.walker.invisible = function( isReject ) { 407 CKEDITOR.dom.walker.invisible = function( isReject ) {
408 var whitespace = CKEDITOR.dom.walker.whitespaces(), 408 var whitespace = CKEDITOR.dom.walker.whitespaces(),
409 // #12221 (Chrome) plus #11111 (Safari). 409 // http://dev.ckeditor.com/ticket/12221 (Chrome) plus http://dev.ckeditor.com/ticket/11111 (Safari).
410 offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0; 410 offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
411 411
412 return function( node ) { 412 return function( node ) {
@@ -422,7 +422,7 @@
422 // Nodes that take no spaces in wysiwyg: 422 // Nodes that take no spaces in wysiwyg:
423 // 1. White-spaces but not including NBSP. 423 // 1. White-spaces but not including NBSP.
424 // 2. Empty inline elements, e.g. <b></b>. 424 // 2. Empty inline elements, e.g. <b></b>.
425 // 3. <br> elements (bogus, surrounded by text) (#12423). 425 // 3. <br> elements (bogus, surrounded by text) (http://dev.ckeditor.com/ticket/12423).
426 invisible = node.$.offsetWidth <= offsetWidth0; 426 invisible = node.$.offsetWidth <= offsetWidth0;
427 } 427 }
428 428
@@ -636,7 +636,7 @@
636 * @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`. 636 * @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`.
637 */ 637 */
638 CKEDITOR.dom.element.prototype.getBogus = function() { 638 CKEDITOR.dom.element.prototype.getBogus = function() {
639 // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070). 639 // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (http://dev.ckeditor.com/ticket/7070).
640 var tail = this; 640 var tail = this;
641 do { 641 do {
642 tail = tail.getPreviousSourceNode(); 642 tail = tail.getPreviousSourceNode();