aboutsummaryrefslogtreecommitdiff
path: root/sources/core
diff options
context:
space:
mode:
Diffstat (limited to 'sources/core')
-rw-r--r--sources/core/_bootstrap.js6
-rw-r--r--sources/core/ckeditor.js2
-rw-r--r--sources/core/command.js2
-rw-r--r--sources/core/creators/themedui.js12
-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
-rw-r--r--sources/core/editable.js121
-rw-r--r--sources/core/editor.js111
-rw-r--r--sources/core/env.js2
-rw-r--r--sources/core/filter.js37
-rw-r--r--sources/core/focusmanager.js18
-rw-r--r--sources/core/htmldataprocessor.js75
-rw-r--r--sources/core/htmlparser.js2
-rw-r--r--sources/core/htmlparser/basicwriter.js2
-rw-r--r--sources/core/htmlparser/element.js6
-rw-r--r--sources/core/htmlparser/fragment.js14
-rw-r--r--sources/core/lang.js2
-rw-r--r--sources/core/loader.js4
-rw-r--r--sources/core/scriptloader.js4
-rw-r--r--sources/core/selection.js403
-rw-r--r--sources/core/skin.js2
-rw-r--r--sources/core/style.js65
-rw-r--r--sources/core/template.js69
-rw-r--r--sources/core/tools.js167
33 files changed, 1062 insertions, 389 deletions
diff --git a/sources/core/_bootstrap.js b/sources/core/_bootstrap.js
index 0163912..cc7fb38 100644
--- a/sources/core/_bootstrap.js
+++ b/sources/core/_bootstrap.js
@@ -8,7 +8,7 @@
8 */ 8 */
9 9
10( function() { 10( function() {
11 // Disable HC detection in WebKit. (#5429) 11 // Disable HC detection in WebKit. (http://dev.ckeditor.com/ticket/5429)
12 if ( CKEDITOR.env.webkit ) 12 if ( CKEDITOR.env.webkit )
13 CKEDITOR.env.hc = false; 13 CKEDITOR.env.hc = false;
14 else { 14 else {
@@ -19,13 +19,13 @@
19 hcDetect.appendTo( CKEDITOR.document.getHead() ); 19 hcDetect.appendTo( CKEDITOR.document.getHead() );
20 20
21 // Update CKEDITOR.env. 21 // Update CKEDITOR.env.
22 // Catch exception needed sometimes for FF. (#4230) 22 // Catch exception needed sometimes for FF. (http://dev.ckeditor.com/ticket/4230)
23 try { 23 try {
24 var top = hcDetect.getComputedStyle( 'border-top-color' ), 24 var top = hcDetect.getComputedStyle( 'border-top-color' ),
25 right = hcDetect.getComputedStyle( 'border-right-color' ); 25 right = hcDetect.getComputedStyle( 'border-right-color' );
26 26
27 // We need to check if getComputedStyle returned any value, because on FF 27 // We need to check if getComputedStyle returned any value, because on FF
28 // it returnes empty string if CKEditor is loaded in hidden iframe. (#11121) 28 // it returnes empty string if CKEditor is loaded in hidden iframe. (http://dev.ckeditor.com/ticket/11121)
29 CKEDITOR.env.hc = !!( top && top == right ); 29 CKEDITOR.env.hc = !!( top && top == right );
30 } catch ( e ) { 30 } catch ( e ) {
31 CKEDITOR.env.hc = false; 31 CKEDITOR.env.hc = false;
diff --git a/sources/core/ckeditor.js b/sources/core/ckeditor.js
index 2756204..95dd67a 100644
--- a/sources/core/ckeditor.js
+++ b/sources/core/ckeditor.js
@@ -126,7 +126,7 @@ CKEDITOR.remove = function( editor ) {
126 }; 126 };
127 127
128 /** 128 /**
129 * Returns a string will all CSS rules passed to the {@link CKEDITOR#addCss} method. 129 * Returns a string with all CSS rules passed to the {@link CKEDITOR#addCss} method.
130 * 130 *
131 * @returns {String} A string containing CSS rules. 131 * @returns {String} A string containing CSS rules.
132 */ 132 */
diff --git a/sources/core/command.js b/sources/core/command.js
index 0446d24..128cea4 100644
--- a/sources/core/command.js
+++ b/sources/core/command.js
@@ -47,7 +47,7 @@ CKEDITOR.command = function( editor, commandDefinition ) {
47 if ( this.state == CKEDITOR.TRISTATE_DISABLED || !this.checkAllowed() ) 47 if ( this.state == CKEDITOR.TRISTATE_DISABLED || !this.checkAllowed() )
48 return false; 48 return false;
49 49
50 if ( this.editorFocus ) // Give editor focus if necessary (#4355). 50 if ( this.editorFocus ) // Give editor focus if necessary (http://dev.ckeditor.com/ticket/4355).
51 editor.focus(); 51 editor.focus();
52 52
53 if ( this.fire( 'exec' ) === false ) 53 if ( this.fire( 'exec' ) === false )
diff --git a/sources/core/creators/themedui.js b/sources/core/creators/themedui.js
index 04927d0..4e7a93e 100644
--- a/sources/core/creators/themedui.js
+++ b/sources/core/creators/themedui.js
@@ -280,10 +280,10 @@ CKEDITOR.replaceClass = 'ckeditor';
280 outer = container; 280 outer = container;
281 } 281 }
282 282
283 // Set as border box width. (#5353) 283 // Set as border box width. (http://dev.ckeditor.com/ticket/5353)
284 outer.setSize( 'width', width, true ); 284 outer.setSize( 'width', width, true );
285 285
286 // WebKit needs to refresh the iframe size to avoid rendering issues. (1/2) (#8348) 286 // WebKit needs to refresh the iframe size to avoid rendering issues. (1/2) (http://dev.ckeditor.com/ticket/8348)
287 contentsFrame && ( contentsFrame.style.width = '1%' ); 287 contentsFrame && ( contentsFrame.style.width = '1%' );
288 288
289 // Get the height delta between the outer table and the content area. 289 // Get the height delta between the outer table and the content area.
@@ -295,7 +295,7 @@ CKEDITOR.replaceClass = 'ckeditor';
295 295
296 contents.setStyle( 'height', resultContentsHeight + 'px' ); 296 contents.setStyle( 'height', resultContentsHeight + 'px' );
297 297
298 // WebKit needs to refresh the iframe size to avoid rendering issues. (2/2) (#8348) 298 // WebKit needs to refresh the iframe size to avoid rendering issues. (2/2) (http://dev.ckeditor.com/ticket/8348)
299 contentsFrame && ( contentsFrame.style.width = '100%' ); 299 contentsFrame && ( contentsFrame.style.width = '100%' );
300 300
301 // Emit a resize event. 301 // Emit a resize event.
@@ -337,7 +337,7 @@ CKEDITOR.replaceClass = 'ckeditor';
337 // replacement will be done later in the editor creation lifecycle. 337 // replacement will be done later in the editor creation lifecycle.
338 element.setStyle( 'visibility', 'hidden' ); 338 element.setStyle( 'visibility', 'hidden' );
339 339
340 // #8031 Remember if textarea was required and remove the attribute. 340 // http://dev.ckeditor.com/ticket/8031 Remember if textarea was required and remove the attribute.
341 editor._.required = element.hasAttribute( 'required' ); 341 editor._.required = element.hasAttribute( 'required' );
342 element.removeAttribute( 'required' ); 342 element.removeAttribute( 'required' );
343 } 343 }
@@ -422,7 +422,7 @@ CKEDITOR.replaceClass = 'ckeditor';
422 topHtml: topHtml ? '<span id="' + editor.ui.spaceId( 'top' ) + '" class="cke_top cke_reset_all" role="presentation" style="height:auto">' + topHtml + '</span>' : '', 422 topHtml: topHtml ? '<span id="' + editor.ui.spaceId( 'top' ) + '" class="cke_top cke_reset_all" role="presentation" style="height:auto">' + topHtml + '</span>' : '',
423 contentId: editor.ui.spaceId( 'contents' ), 423 contentId: editor.ui.spaceId( 'contents' ),
424 bottomHtml: bottomHtml ? '<span id="' + editor.ui.spaceId( 'bottom' ) + '" class="cke_bottom cke_reset_all" role="presentation">' + bottomHtml + '</span>' : '', 424 bottomHtml: bottomHtml ? '<span id="' + editor.ui.spaceId( 'bottom' ) + '" class="cke_bottom cke_reset_all" role="presentation">' + bottomHtml + '</span>' : '',
425 outerEl: CKEDITOR.env.ie ? 'span' : 'div' // #9571 425 outerEl: CKEDITOR.env.ie ? 'span' : 'div' // http://dev.ckeditor.com/ticket/9571
426 } ) ); 426 } ) );
427 427
428 if ( elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) { 428 if ( elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) {
@@ -451,7 +451,7 @@ CKEDITOR.replaceClass = 'ckeditor';
451 // Disable browser context menu for editor's chrome. 451 // Disable browser context menu for editor's chrome.
452 container.disableContextMenu(); 452 container.disableContextMenu();
453 453
454 // Redirect the focus into editor for webkit. (#5713) 454 // Redirect the focus into editor for webkit. (http://dev.ckeditor.com/ticket/5713)
455 CKEDITOR.env.webkit && container.on( 'focus', function() { 455 CKEDITOR.env.webkit && container.on( 'focus', function() {
456 editor.focus(); 456 editor.focus();
457 } ); 457 } );
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();
diff --git a/sources/core/editable.js b/sources/core/editable.js
index c50ec7f..6b3fa9f 100644
--- a/sources/core/editable.js
+++ b/sources/core/editable.js
@@ -76,7 +76,7 @@
76 76
77 // [Edge] Starting from EdgeHTML 14.14393, it does not support `setActive`. We need to use focus which 77 // [Edge] Starting from EdgeHTML 14.14393, it does not support `setActive`. We need to use focus which
78 // causes unexpected scroll. Store scrollTop value so it can be restored after focusing editor. 78 // causes unexpected scroll. Store scrollTop value so it can be restored after focusing editor.
79 // Scroll only happens if the editor is focused for the first time. (#14825) 79 // Scroll only happens if the editor is focused for the first time. (http://dev.ckeditor.com/ticket/14825)
80 if ( CKEDITOR.env.edge && CKEDITOR.env.version > 14 && !this.hasFocus && this.getDocument().equals( CKEDITOR.document ) ) { 80 if ( CKEDITOR.env.edge && CKEDITOR.env.version > 14 && !this.hasFocus && this.getDocument().equals( CKEDITOR.document ) ) {
81 this.editor._.previousScrollTop = this.$.scrollTop; 81 this.editor._.previousScrollTop = this.$.scrollTop;
82 } 82 }
@@ -87,7 +87,15 @@
87 if ( CKEDITOR.env.ie && !( CKEDITOR.env.edge && CKEDITOR.env.version > 14 ) && this.getDocument().equals( CKEDITOR.document ) ) { 87 if ( CKEDITOR.env.ie && !( CKEDITOR.env.edge && CKEDITOR.env.version > 14 ) && this.getDocument().equals( CKEDITOR.document ) ) {
88 this.$.setActive(); 88 this.$.setActive();
89 } else { 89 } else {
90 this.$.focus(); 90 // We have no control over exactly what happens when the native `focus` method is called,
91 // so save the scroll position and restore it later.
92 if ( CKEDITOR.env.chrome ) {
93 var scrollPos = this.$.scrollTop;
94 this.$.focus();
95 this.$.scrollTop = scrollPos;
96 } else {
97 this.$.focus();
98 }
91 } 99 }
92 } catch ( e ) { 100 } catch ( e ) {
93 // IE throws unspecified error when focusing editable after closing dialog opened on nested editable. 101 // IE throws unspecified error when focusing editable after closing dialog opened on nested editable.
@@ -95,7 +103,7 @@
95 throw e; 103 throw e;
96 } 104 }
97 105
98 // Remedy if Safari doens't applies focus properly. (#279) 106 // Remedy if Safari doens't applies focus properly. (http://dev.ckeditor.com/ticket/279)
99 if ( CKEDITOR.env.safari && !this.isInline() ) { 107 if ( CKEDITOR.env.safari && !this.isInline() ) {
100 active = CKEDITOR.document.getActive(); 108 active = CKEDITOR.document.getActive();
101 if ( !active.equals( this.getWindow().getFrame() ) ) 109 if ( !active.equals( this.getWindow().getFrame() ) )
@@ -117,7 +125,7 @@
117 125
118 // The "focusin/focusout" events bubbled, e.g. If there are elements with layout 126 // The "focusin/focusout" events bubbled, e.g. If there are elements with layout
119 // they fire this event when clicking in to edit them but it must be ignored 127 // they fire this event when clicking in to edit them but it must be ignored
120 // to allow edit their contents. (#4682) 128 // to allow edit their contents. (http://dev.ckeditor.com/ticket/4682)
121 fn = isNotBubbling( fn, this ); 129 fn = isNotBubbling( fn, this );
122 args[ 0 ] = name; 130 args[ 0 ] = name;
123 args[ 1 ] = fn; 131 args[ 1 ] = fn;
@@ -252,7 +260,7 @@
252 * @param {String} text 260 * @param {String} text
253 */ 261 */
254 insertText: function( text ) { 262 insertText: function( text ) {
255 // Focus the editor before calling transformPlainTextToHtml. (#12726) 263 // Focus the editor before calling transformPlainTextToHtml. (http://dev.ckeditor.com/ticket/12726)
256 this.editor.focus(); 264 this.editor.focus();
257 this.insertHtml( this.transformPlainTextToHtml( text ), 'text' ); 265 this.insertHtml( this.transformPlainTextToHtml( text ), 'text' );
258 }, 266 },
@@ -350,7 +358,7 @@
350 insertElement: function( element, range ) { 358 insertElement: function( element, range ) {
351 var editor = this.editor; 359 var editor = this.editor;
352 360
353 // Prepare for the insertion. For example - focus editor (#11848). 361 // Prepare for the insertion. For example - focus editor (http://dev.ckeditor.com/ticket/11848).
354 editor.focus(); 362 editor.focus();
355 editor.fire( 'saveSnapshot' ); 363 editor.fire( 'saveSnapshot' );
356 364
@@ -363,12 +371,12 @@
363 range = selection.getRanges()[ 0 ]; 371 range = selection.getRanges()[ 0 ];
364 } 372 }
365 373
366 // Insert element into first range only and ignore the rest (#11183). 374 // Insert element into first range only and ignore the rest (http://dev.ckeditor.com/ticket/11183).
367 if ( this.insertElementIntoRange( element, range ) ) { 375 if ( this.insertElementIntoRange( element, range ) ) {
368 range.moveToPosition( element, CKEDITOR.POSITION_AFTER_END ); 376 range.moveToPosition( element, CKEDITOR.POSITION_AFTER_END );
369 377
370 // If we're inserting a block element, the new cursor position must be 378 // If we're inserting a block element, the new cursor position must be
371 // optimized. (#3100,#5436,#8950) 379 // optimized. (http://dev.ckeditor.com/ticket/3100,http://dev.ckeditor.com/ticket/5436,http://dev.ckeditor.com/ticket/8950)
372 if ( isBlock ) { 380 if ( isBlock ) {
373 // Find next, meaningful element. 381 // Find next, meaningful element.
374 var next = element.getNext( function( node ) { 382 var next = element.getNext( function( node ) {
@@ -456,7 +464,7 @@
456 range.splitElement( current ); 464 range.splitElement( current );
457 465
458 // If we're in an empty block which indicate a new paragraph, 466 // If we're in an empty block which indicate a new paragraph,
459 // simply replace it with the inserting block.(#3664) 467 // simply replace it with the inserting block.(http://dev.ckeditor.com/ticket/3664)
460 else if ( range.checkStartOfBlock() && range.checkEndOfBlock() ) { 468 else if ( range.checkStartOfBlock() && range.checkEndOfBlock() ) {
461 range.setStartBefore( current ); 469 range.setStartBefore( current );
462 range.collapse( true ); 470 range.collapse( true );
@@ -770,7 +778,7 @@
770 range.checkEndOfBlock() && 778 range.checkEndOfBlock() &&
771 path.block && 779 path.block &&
772 !range.root.equals( path.block ) && 780 !range.root.equals( path.block ) &&
773 // Do not remove a block with bookmarks. (#13465) 781 // Do not remove a block with bookmarks. (http://dev.ckeditor.com/ticket/13465)
774 !hasBookmarks( path.block ) ) { 782 !hasBookmarks( path.block ) ) {
775 range.moveToPosition( path.block, CKEDITOR.POSITION_BEFORE_START ); 783 range.moveToPosition( path.block, CKEDITOR.POSITION_BEFORE_START );
776 path.block.remove(); 784 path.block.remove();
@@ -832,7 +840,7 @@
832 840
833 // IE considers control-type element as separate 841 // IE considers control-type element as separate
834 // focus host when selected, avoid destroying the 842 // focus host when selected, avoid destroying the
835 // selection in such case. (#5812) (#8949) 843 // selection in such case. (http://dev.ckeditor.com/ticket/5812) (http://dev.ckeditor.com/ticket/8949)
836 if ( ieSel && ieSel.type == 'Control' ) 844 if ( ieSel && ieSel.type == 'Control' )
837 return; 845 return;
838 846
@@ -884,14 +892,14 @@
884 }, null, null, -1 ); 892 }, null, null, -1 );
885 893
886 if ( CKEDITOR.env.webkit ) { 894 if ( CKEDITOR.env.webkit ) {
887 // [WebKit] Save scrollTop value so it can be used when restoring locked selection. (#14659) 895 // [WebKit] Save scrollTop value so it can be used when restoring locked selection. (http://dev.ckeditor.com/ticket/14659)
888 this.on( 'scroll', function() { 896 this.on( 'scroll', function() {
889 editor._.previousScrollTop = editor.editable().$.scrollTop; 897 editor._.previousScrollTop = editor.editable().$.scrollTop;
890 }, null, null, -1 ); 898 }, null, null, -1 );
891 } 899 }
892 900
893 // [Edge] This is the other part of the workaround for Edge which restores saved 901 // [Edge] This is the other part of the workaround for Edge which restores saved
894 // scrollTop value and removes listener which is not needed anymore. (#14825) 902 // scrollTop value and removes listener which is not needed anymore. (http://dev.ckeditor.com/ticket/14825)
895 if ( CKEDITOR.env.edge && CKEDITOR.env.version > 14 ) { 903 if ( CKEDITOR.env.edge && CKEDITOR.env.version > 14 ) {
896 904
897 var fixScrollOnFocus = function() { 905 var fixScrollOnFocus = function() {
@@ -967,7 +975,7 @@
967 // Pass this configuration to styles system. 975 // Pass this configuration to styles system.
968 this.setCustomData( 'cke_includeReadonly', !editor.config.disableReadonlyStyling ); 976 this.setCustomData( 'cke_includeReadonly', !editor.config.disableReadonlyStyling );
969 977
970 // Prevent the browser opening read-only links. (#6032 & #10912) 978 // Prevent the browser opening read-only links. (http://dev.ckeditor.com/ticket/6032 & http://dev.ckeditor.com/ticket/10912)
971 this.attachListener( this, 'click', function( evt ) { 979 this.attachListener( this, 'click', function( evt ) {
972 evt = evt.data; 980 evt = evt.data;
973 981
@@ -980,7 +988,7 @@
980 var backspaceOrDelete = { 8: 1, 46: 1 }; 988 var backspaceOrDelete = { 8: 1, 46: 1 };
981 989
982 // Override keystrokes which should have deletion behavior 990 // Override keystrokes which should have deletion behavior
983 // on fully selected element . (#4047) (#7645) 991 // on fully selected element . (http://dev.ckeditor.com/ticket/4047) (http://dev.ckeditor.com/ticket/7645)
984 this.attachListener( editor, 'key', function( evt ) { 992 this.attachListener( editor, 'key', function( evt ) {
985 if ( editor.readOnly ) 993 if ( editor.readOnly )
986 return true; 994 return true;
@@ -990,10 +998,15 @@
990 var keyCode = evt.data.domEvent.getKey(), 998 var keyCode = evt.data.domEvent.getKey(),
991 isHandled; 999 isHandled;
992 1000
1001 // Prevent of reading path of empty range (http://dev.ckeditor.com/ticket/13096, #457).
1002 var sel = editor.getSelection();
1003 if ( sel.getRanges().length === 0 ) {
1004 return;
1005 }
1006
993 // Backspace OR Delete. 1007 // Backspace OR Delete.
994 if ( keyCode in backspaceOrDelete ) { 1008 if ( keyCode in backspaceOrDelete ) {
995 var sel = editor.getSelection(), 1009 var selected,
996 selected,
997 range = sel.getRanges()[ 0 ], 1010 range = sel.getRanges()[ 0 ],
998 path = range.startPath(), 1011 path = range.startPath(),
999 block, 1012 block,
@@ -1001,16 +1014,17 @@
1001 next, 1014 next,
1002 rtl = keyCode == 8; 1015 rtl = keyCode == 8;
1003 1016
1017
1004 if ( 1018 if (
1005 // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (#10055) 1019 // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (http://dev.ckeditor.com/ticket/10055)
1006 ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 && ( selected = sel.getSelectedElement() ) ) || 1020 ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 && ( selected = sel.getSelectedElement() ) ) ||
1007 // Remove the entire list/table on fully selected content. (#7645) 1021 // Remove the entire list/table on fully selected content. (http://dev.ckeditor.com/ticket/7645)
1008 ( selected = getSelectedTableList( sel ) ) ) { 1022 ( selected = getSelectedTableList( sel ) ) ) {
1009 // Make undo snapshot. 1023 // Make undo snapshot.
1010 editor.fire( 'saveSnapshot' ); 1024 editor.fire( 'saveSnapshot' );
1011 1025
1012 // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will 1026 // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will
1013 // break up the selection, safely manage it here. (#4795) 1027 // break up the selection, safely manage it here. (http://dev.ckeditor.com/ticket/4795)
1014 range.moveToPosition( selected, CKEDITOR.POSITION_BEFORE_START ); 1028 range.moveToPosition( selected, CKEDITOR.POSITION_BEFORE_START );
1015 // Remove the control manually. 1029 // Remove the control manually.
1016 selected.remove(); 1030 selected.remove();
@@ -1020,7 +1034,7 @@
1020 1034
1021 isHandled = 1; 1035 isHandled = 1;
1022 } else if ( range.collapsed ) { 1036 } else if ( range.collapsed ) {
1023 // Handle the following special cases: (#6217) 1037 // Handle the following special cases: (http://dev.ckeditor.com/ticket/6217)
1024 // 1. Del/Backspace key before/after table; 1038 // 1. Del/Backspace key before/after table;
1025 // 2. Backspace Key after start of table. 1039 // 2. Backspace Key after start of table.
1026 if ( ( block = path.block ) && 1040 if ( ( block = path.block ) &&
@@ -1095,28 +1109,28 @@
1095 editor.fire( 'doubleclick', data ); 1109 editor.fire( 'doubleclick', data );
1096 } ); 1110 } );
1097 1111
1098 // Prevent automatic submission in IE #6336 1112 // Prevent automatic submission in IE http://dev.ckeditor.com/ticket/6336
1099 CKEDITOR.env.ie && this.attachListener( this, 'click', blockInputClick ); 1113 CKEDITOR.env.ie && this.attachListener( this, 'click', blockInputClick );
1100 1114
1101 // Gecko/Webkit need some help when selecting control type elements. (#3448) 1115 // Gecko/Webkit need some help when selecting control type elements. (http://dev.ckeditor.com/ticket/3448)
1102 // We apply same behavior for IE Edge. (#13386) 1116 // We apply same behavior for IE Edge. (http://dev.ckeditor.com/ticket/13386)
1103 if ( !CKEDITOR.env.ie || CKEDITOR.env.edge ) { 1117 if ( !CKEDITOR.env.ie || CKEDITOR.env.edge ) {
1104 this.attachListener( this, 'mousedown', function( ev ) { 1118 this.attachListener( this, 'mousedown', function( ev ) {
1105 var control = ev.data.getTarget(); 1119 var control = ev.data.getTarget();
1106 // #11727. Note: htmlDP assures that input/textarea/select have contenteditable=false 1120 // http://dev.ckeditor.com/ticket/11727. Note: htmlDP assures that input/textarea/select have contenteditable=false
1107 // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false, 1121 // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false,
1108 // and therefore those elements are correctly selected by this code. 1122 // and therefore those elements are correctly selected by this code.
1109 if ( control.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control.isReadOnly() ) { 1123 if ( control.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control.isReadOnly() ) {
1110 editor.getSelection().selectElement( control ); 1124 editor.getSelection().selectElement( control );
1111 1125
1112 // Prevent focus from stealing from the editable. (#9515) 1126 // Prevent focus from stealing from the editable. (http://dev.ckeditor.com/ticket/9515)
1113 if ( control.is( 'input', 'textarea', 'select' ) ) 1127 if ( control.is( 'input', 'textarea', 'select' ) )
1114 ev.data.preventDefault(); 1128 ev.data.preventDefault();
1115 } 1129 }
1116 } ); 1130 } );
1117 } 1131 }
1118 1132
1119 // For some reason, after click event is done, IE Edge loses focus on the selected element. (#13386) 1133 // For some reason, after click event is done, IE Edge loses focus on the selected element. (http://dev.ckeditor.com/ticket/13386)
1120 if ( CKEDITOR.env.edge ) { 1134 if ( CKEDITOR.env.edge ) {
1121 this.attachListener( this, 'mouseup', function( ev ) { 1135 this.attachListener( this, 'mouseup', function( ev ) {
1122 var selectedElement = ev.data.getTarget(); 1136 var selectedElement = ev.data.getTarget();
@@ -1127,7 +1141,7 @@
1127 } 1141 }
1128 1142
1129 // Prevent right click from selecting an empty block even 1143 // Prevent right click from selecting an empty block even
1130 // when selection is anchored inside it. (#5845) 1144 // when selection is anchored inside it. (http://dev.ckeditor.com/ticket/5845)
1131 if ( CKEDITOR.env.gecko ) { 1145 if ( CKEDITOR.env.gecko ) {
1132 this.attachListener( this, 'mouseup', function( ev ) { 1146 this.attachListener( this, 'mouseup', function( ev ) {
1133 if ( ev.data.$.button == 2 ) { 1147 if ( ev.data.$.button == 2 ) {
@@ -1158,7 +1172,7 @@
1158 } 1172 }
1159 1173
1160 // Prevent Webkit/Blink from going rogue when joining 1174 // Prevent Webkit/Blink from going rogue when joining
1161 // blocks on BACKSPACE/DEL (#11861,#9998). 1175 // blocks on BACKSPACE/DEL (http://dev.ckeditor.com/ticket/11861,http://dev.ckeditor.com/ticket/9998).
1162 if ( CKEDITOR.env.webkit ) { 1176 if ( CKEDITOR.env.webkit ) {
1163 this.attachListener( editor, 'key', function( evt ) { 1177 this.attachListener( editor, 'key', function( evt ) {
1164 if ( editor.readOnly ) { 1178 if ( editor.readOnly ) {
@@ -1172,8 +1186,14 @@
1172 if ( !( key in backspaceOrDelete ) ) 1186 if ( !( key in backspaceOrDelete ) )
1173 return; 1187 return;
1174 1188
1189 // Prevent of reading path of empty range (http://dev.ckeditor.com/ticket/13096, #457).
1190 var sel = editor.getSelection();
1191 if ( sel.getRanges().length === 0 ) {
1192 return;
1193 }
1194
1175 var backspace = key == 8, 1195 var backspace = key == 8,
1176 range = editor.getSelection().getRanges()[ 0 ], 1196 range = sel.getRanges()[ 0 ],
1177 startPath = range.startPath(); 1197 startPath = range.startPath();
1178 1198
1179 if ( range.collapsed ) { 1199 if ( range.collapsed ) {
@@ -1184,7 +1204,7 @@
1184 return; 1204 return;
1185 } 1205 }
1186 1206
1187 // Scroll to the new position of the caret (#11960). 1207 // Scroll to the new position of the caret (http://dev.ckeditor.com/ticket/11960).
1188 editor.getSelection().scrollIntoView(); 1208 editor.getSelection().scrollIntoView();
1189 editor.fire( 'saveSnapshot' ); 1209 editor.fire( 'saveSnapshot' );
1190 1210
@@ -1241,6 +1261,7 @@
1241 * @member CKEDITOR.editor 1261 * @member CKEDITOR.editor
1242 * @param {CKEDITOR.dom.element/CKEDITOR.editable} [elementOrEditable] The 1262 * @param {CKEDITOR.dom.element/CKEDITOR.editable} [elementOrEditable] The
1243 * DOM element to become the editable or a {@link CKEDITOR.editable} object. 1263 * DOM element to become the editable or a {@link CKEDITOR.editable} object.
1264 * @returns {CKEDITOR.dom.element/null} The editor's editable element, or `null` if not available.
1244 */ 1265 */
1245 CKEDITOR.editor.prototype.editable = function( element ) { 1266 CKEDITOR.editor.prototype.editable = function( element ) {
1246 var editable = this._.editable; 1267 var editable = this._.editable;
@@ -1263,7 +1284,7 @@
1263 CKEDITOR.on( 'instanceLoaded', function( evt ) { 1284 CKEDITOR.on( 'instanceLoaded', function( evt ) {
1264 var editor = evt.editor; 1285 var editor = evt.editor;
1265 1286
1266 // and flag that the element was locked by our code so it'll be editable by the editor functions (#6046). 1287 // and flag that the element was locked by our code so it'll be editable by the editor functions (http://dev.ckeditor.com/ticket/6046).
1267 editor.on( 'insertElement', function( evt ) { 1288 editor.on( 'insertElement', function( evt ) {
1268 var element = evt.data; 1289 var element = evt.data;
1269 if ( element.type == CKEDITOR.NODE_ELEMENT && ( element.is( 'input' ) || element.is( 'textarea' ) ) ) { 1290 if ( element.type == CKEDITOR.NODE_ELEMENT && ( element.is( 'input' ) || element.is( 'textarea' ) ) ) {
@@ -1278,9 +1299,9 @@
1278 if ( editor.readOnly ) 1299 if ( editor.readOnly )
1279 return; 1300 return;
1280 1301
1281 // Auto fixing on some document structure weakness to enhance usabilities. (#3190 and #3189) 1302 // Auto fixing on some document structure weakness to enhance usabilities. (http://dev.ckeditor.com/ticket/3190 and http://dev.ckeditor.com/ticket/3189)
1282 var sel = editor.getSelection(); 1303 var sel = editor.getSelection();
1283 // Do it only when selection is not locked. (#8222) 1304 // Do it only when selection is not locked. (http://dev.ckeditor.com/ticket/8222)
1284 if ( sel && !sel.isLocked ) { 1305 if ( sel && !sel.isLocked ) {
1285 var isDirty = editor.checkDirty(); 1306 var isDirty = editor.checkDirty();
1286 1307
@@ -1330,7 +1351,7 @@
1330 } ); 1351 } );
1331 } ); 1352 } );
1332 1353
1333 // #9222: Show text cursor in Gecko. 1354 // http://dev.ckeditor.com/ticket/9222: Show text cursor in Gecko.
1334 // Show default cursor over control elements on all non-IEs. 1355 // Show default cursor over control elements on all non-IEs.
1335 CKEDITOR.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' ); 1356 CKEDITOR.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' );
1336 1357
@@ -1347,8 +1368,8 @@
1347 // Matching an empty paragraph at the end of document. 1368 // Matching an empty paragraph at the end of document.
1348 emptyParagraphRegexp = /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>|&nbsp;|\u00A0|&#160;)?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi; 1369 emptyParagraphRegexp = /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>|&nbsp;|\u00A0|&#160;)?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;
1349 1370
1350 // Auto-fixing block-less content by wrapping paragraph (#3190), prevent 1371 // Auto-fixing block-less content by wrapping paragraph (http://dev.ckeditor.com/ticket/3190), prevent
1351 // non-exitable-block by padding extra br.(#3189) 1372 // non-exitable-block by padding extra br.(http://dev.ckeditor.com/ticket/3189)
1352 // Returns truly value when dom was changed, falsy otherwise. 1373 // Returns truly value when dom was changed, falsy otherwise.
1353 function fixDom( evt ) { 1374 function fixDom( evt ) {
1354 var editor = evt.editor, 1375 var editor = evt.editor,
@@ -1369,7 +1390,7 @@
1369 } 1390 }
1370 1391
1371 // When we're in block enter mode, a new paragraph will be established 1392 // When we're in block enter mode, a new paragraph will be established
1372 // to encapsulate inline contents inside editable. (#3657) 1393 // to encapsulate inline contents inside editable. (http://dev.ckeditor.com/ticket/3657)
1373 // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection 1394 // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection
1374 // inside non-editable content. This happens e.g. if non-editable block is the only 1395 // inside non-editable content. This happens e.g. if non-editable block is the only
1375 // content of editable. 1396 // content of editable.
@@ -1397,7 +1418,7 @@
1397 1418
1398 selectionUpdateNeeded = 1; 1419 selectionUpdateNeeded = 1;
1399 1420
1400 // Cancel this selection change in favor of the next (correct). (#6811) 1421 // Cancel this selection change in favor of the next (correct). (http://dev.ckeditor.com/ticket/6811)
1401 evt.cancel(); 1422 evt.cancel();
1402 } 1423 }
1403 } 1424 }
@@ -1413,13 +1434,13 @@
1413 if ( selection.isFake ) 1434 if ( selection.isFake )
1414 return 0; 1435 return 0;
1415 1436
1416 // Ensure bogus br could help to move cursor (out of styles) to the end of block. (#7041) 1437 // Ensure bogus br could help to move cursor (out of styles) to the end of block. (http://dev.ckeditor.com/ticket/7041)
1417 var pathBlock = path.block || path.blockLimit, 1438 var pathBlock = path.block || path.blockLimit,
1418 lastNode = pathBlock && pathBlock.getLast( isNotEmpty ); 1439 lastNode = pathBlock && pathBlock.getLast( isNotEmpty );
1419 1440
1420 // Check some specialities of the current path block: 1441 // Check some specialities of the current path block:
1421 // 1. It is really displayed as block; (#7221) 1442 // 1. It is really displayed as block; (http://dev.ckeditor.com/ticket/7221)
1422 // 2. It doesn't end with one inner block; (#7467) 1443 // 2. It doesn't end with one inner block; (http://dev.ckeditor.com/ticket/7467)
1423 // 3. It doesn't have bogus br yet. 1444 // 3. It doesn't have bogus br yet.
1424 if ( 1445 if (
1425 pathBlock && pathBlock.isBlockBoundary() && 1446 pathBlock && pathBlock.isBlockBoundary() &&
@@ -1556,7 +1577,7 @@
1556 // Whether in given context (pathBlock, pathBlockLimit and editor settings) 1577 // Whether in given context (pathBlock, pathBlockLimit and editor settings)
1557 // editor should automatically wrap inline contents with blocks. 1578 // editor should automatically wrap inline contents with blocks.
1558 function shouldAutoParagraph( editor, pathBlock, pathBlockLimit ) { 1579 function shouldAutoParagraph( editor, pathBlock, pathBlockLimit ) {
1559 // Check whether pathBlock equals pathBlockLimit to support nested editable (#12162). 1580 // Check whether pathBlock equals pathBlockLimit to support nested editable (http://dev.ckeditor.com/ticket/12162).
1560 return editor.config.autoParagraph !== false && 1581 return editor.config.autoParagraph !== false &&
1561 editor.activeEnterMode != CKEDITOR.ENTER_BR && 1582 editor.activeEnterMode != CKEDITOR.ENTER_BR &&
1562 ( 1583 (
@@ -1623,7 +1644,7 @@
1623 1644
1624 // Select range and stop execution. 1645 // Select range and stop execution.
1625 // If data has been totally emptied after the filtering, 1646 // If data has been totally emptied after the filtering,
1626 // any insertion is pointless (#10339). 1647 // any insertion is pointless (http://dev.ckeditor.com/ticket/10339).
1627 if ( data && processDataForInsertion( that, data ) ) { 1648 if ( data && processDataForInsertion( that, data ) ) {
1628 // DATA INSERTION 1649 // DATA INSERTION
1629 insertDataIntoRange( that ); 1650 insertDataIntoRange( that );
@@ -2008,7 +2029,7 @@
2008 nodeName = node.getName(); 2029 nodeName = node.getName();
2009 2030
2010 // Extract only the list items, when insertion happens 2031 // Extract only the list items, when insertion happens
2011 // inside of a list, reads as rearrange list items. (#7957) 2032 // inside of a list, reads as rearrange list items. (http://dev.ckeditor.com/ticket/7957)
2012 if ( insideOfList && nodeName in CKEDITOR.dtd.$list ) { 2033 if ( insideOfList && nodeName in CKEDITOR.dtd.$list ) {
2013 nodesData = nodesData.concat( extractNodesData( node, that ) ); 2034 nodesData = nodesData.concat( extractNodesData( node, that ) );
2014 continue; 2035 continue;
@@ -2256,7 +2277,7 @@
2256 } 2277 }
2257 2278
2258 // Don't use String.replace because it fails in IE7 if special replacement 2279 // Don't use String.replace because it fails in IE7 if special replacement
2259 // characters ($$, $&, etc.) are in data (#10367). 2280 // characters ($$, $&, etc.) are in data (http://dev.ckeditor.com/ticket/10367).
2260 return wrapper.getOuterHtml().split( '{cke-peak}' ).join( data ); 2281 return wrapper.getOuterHtml().split( '{cke-peak}' ).join( data );
2261 } 2282 }
2262 2283
@@ -2517,7 +2538,7 @@
2517 if ( ( bogus = startBlock.getBogus() ) ) 2538 if ( ( bogus = startBlock.getBogus() ) )
2518 bogus.remove(); 2539 bogus.remove();
2519 2540
2520 // Changing end container to element from text node (#12503). 2541 // Changing end container to element from text node (http://dev.ckeditor.com/ticket/12503).
2521 range.enlarge( CKEDITOR.ENLARGE_INLINE ); 2542 range.enlarge( CKEDITOR.ENLARGE_INLINE );
2522 2543
2523 // Delete range contents. Do NOT merge. Merging is weird. 2544 // Delete range contents. Do NOT merge. Merging is weird.
@@ -2540,7 +2561,7 @@
2540 range = editor.getSelection().getRanges()[ 0 ]; 2561 range = editor.getSelection().getRanges()[ 0 ];
2541 range.collapse( 1 ); 2562 range.collapse( 1 );
2542 2563
2543 // Optimizing range containers from text nodes to elements (#12503). 2564 // Optimizing range containers from text nodes to elements (http://dev.ckeditor.com/ticket/12503).
2544 range.optimize(); 2565 range.optimize();
2545 if ( range.startContainer.getHtml() === '' ) { 2566 if ( range.startContainer.getHtml() === '' ) {
2546 range.startContainer.appendBogus(); 2567 range.startContainer.appendBogus();
@@ -2762,7 +2783,7 @@
2762 while ( ( next = endBookmark.getNext() ) ) { 2783 while ( ( next = endBookmark.getNext() ) ) {
2763 next.insertAfter( startBookmark ); 2784 next.insertAfter( startBookmark );
2764 2785
2765 // Update startBookmark after insertion to avoid the reversal of nodes (#13449). 2786 // Update startBookmark after insertion to avoid the reversal of nodes (http://dev.ckeditor.com/ticket/13449).
2766 startBookmark = next; 2787 startBookmark = next;
2767 } 2788 }
2768 2789
@@ -2913,7 +2934,7 @@
2913 2934
2914 walker.guard = function( node, leaving ) { 2935 walker.guard = function( node, leaving ) {
2915 // Guard may be executed on some node boundaries multiple times, 2936 // Guard may be executed on some node boundaries multiple times,
2916 // what results in creating more than one range for each selected cell. (#12964) 2937 // what results in creating more than one range for each selected cell. (http://dev.ckeditor.com/ticket/12964)
2917 if ( node.type == CKEDITOR.NODE_ELEMENT ) { 2938 if ( node.type == CKEDITOR.NODE_ELEMENT ) {
2918 var key = 'visited_' + ( leaving ? 'out' : 'in' ); 2939 var key = 'visited_' + ( leaving ? 'out' : 'in' );
2919 if ( node.getCustomData( key ) ) { 2940 if ( node.getCustomData( key ) ) {
diff --git a/sources/core/editor.js b/sources/core/editor.js
index 8dfce7f..640cec0 100644
--- a/sources/core/editor.js
+++ b/sources/core/editor.js
@@ -29,7 +29,7 @@
29 // Call the CKEDITOR.event constructor to initialize this instance. 29 // Call the CKEDITOR.event constructor to initialize this instance.
30 CKEDITOR.event.call( this ); 30 CKEDITOR.event.call( this );
31 31
32 // Make a clone of the config object, to avoid having it touched by our code. (#9636) 32 // Make a clone of the config object, to avoid having it touched by our code. (http://dev.ckeditor.com/ticket/9636)
33 instanceConfig = instanceConfig && CKEDITOR.tools.clone( instanceConfig ); 33 instanceConfig = instanceConfig && CKEDITOR.tools.clone( instanceConfig );
34 34
35 // if editor is created off one page element. 35 // if editor is created off one page element.
@@ -223,7 +223,7 @@
223 function updateCommandsContext( editor, path, forceRefresh ) { 223 function updateCommandsContext( editor, path, forceRefresh ) {
224 // Commands cannot be refreshed without a path. In edge cases 224 // Commands cannot be refreshed without a path. In edge cases
225 // it may happen that there's no selection when this function is executed. 225 // it may happen that there's no selection when this function is executed.
226 // For example when active filter is changed in #10877. 226 // For example when active filter is changed in http://dev.ckeditor.com/ticket/10877.
227 if ( !path ) 227 if ( !path )
228 return; 228 return;
229 229
@@ -270,7 +270,7 @@
270 } else { 270 } else {
271 // Load the custom configuration file. 271 // Load the custom configuration file.
272 // To resolve customConfig race conflicts, use scriptLoader#queue 272 // To resolve customConfig race conflicts, use scriptLoader#queue
273 // instead of scriptLoader#load (#6504). 273 // instead of scriptLoader#load (http://dev.ckeditor.com/ticket/6504).
274 CKEDITOR.scriptLoader.queue( customConfig, function() { 274 CKEDITOR.scriptLoader.queue( customConfig, function() {
275 // If the CKEDITOR.editorConfig function has been properly 275 // If the CKEDITOR.editorConfig function has been properly
276 // defined in the custom configuration file, cache it. 276 // defined in the custom configuration file, cache it.
@@ -654,24 +654,49 @@
654 return editor.blockless ? CKEDITOR.ENTER_BR : enterMode; 654 return editor.blockless ? CKEDITOR.ENTER_BR : enterMode;
655 } 655 }
656 656
657 // Create DocumentFragment from specified ranges. For now it handles only tables in Firefox 657 // Create DocumentFragment from specified ranges. For now it handles only tables
658 // and returns DocumentFragment from the 1. range for other cases. (#13884) 658 // and returns DocumentFragment from the 1. range for other cases. (http://dev.ckeditor.com/ticket/13884)
659 function createDocumentFragmentFromRanges( ranges, editable ) { 659 function createDocumentFragmentFromRanges( ranges, editable ) {
660 var docFragment = new CKEDITOR.dom.documentFragment(), 660 var docFragment = new CKEDITOR.dom.documentFragment(),
661 tableClone, 661 tableClone,
662 currentRow, 662 currentRow,
663 currentRowClone; 663 currentRowClone;
664 664
665 // We must handle two cases here:
666 // 1. <tr>[<td>Cell</td>]</tr> (IE9+, Edge, Chrome, Firefox)
667 // 2. <td>[Cell]</td> (IE8-, Safari)
668 function isSelectedCell( range ) {
669 var start = range.startContainer,
670 end = range.endContainer;
671
672 if ( start.is && ( start.is( 'tr' ) ||
673 ( start.is( 'td' ) && start.equals( end ) && range.endOffset === start.getChildCount() ) ) ) {
674 return true;
675 }
676
677 return false;
678 }
679
680 function cloneCell( range ) {
681 var start = range.startContainer;
682
683 if ( start.is( 'tr' ) ) {
684 return range.cloneContents();
685 }
686
687 return start.clone( true );
688 }
689
665 for ( var i = 0; i < ranges.length; i++ ) { 690 for ( var i = 0; i < ranges.length; i++ ) {
666 var range = ranges[ i ], 691 var range = ranges[ i ],
667 container = range.startContainer; 692 container = range.startContainer.getAscendant( 'tr', true );
668 693
669 if ( container.getName && container.getName() == 'tr' ) { 694 if ( isSelectedCell( range ) ) {
670 if ( !tableClone ) { 695 if ( !tableClone ) {
671 tableClone = container.getAscendant( 'table' ).clone(); 696 tableClone = container.getAscendant( 'table' ).clone();
672 tableClone.append( container.getAscendant( 'tbody' ).clone() ); 697 tableClone.append( container.getAscendant( { thead: 1, tbody: 1, tfoot: 1 } ).clone() );
673 docFragment.append( tableClone ); 698 docFragment.append( tableClone );
674 tableClone = tableClone.findOne( 'tbody' ); 699 tableClone = tableClone.findOne( 'thead, tbody, tfoot' );
675 } 700 }
676 701
677 if ( !( currentRow && currentRow.equals( container ) ) ) { 702 if ( !( currentRow && currentRow.equals( container ) ) ) {
@@ -680,7 +705,7 @@
680 tableClone.append( currentRowClone ); 705 tableClone.append( currentRowClone );
681 } 706 }
682 707
683 currentRowClone.append( range.cloneContents() ); 708 currentRowClone.append( cloneCell( range ) );
684 } else { 709 } else {
685 // If there was something else copied with table, 710 // If there was something else copied with table,
686 // append it to DocumentFragment. 711 // append it to DocumentFragment.
@@ -717,7 +742,7 @@
717 // This guarantees that commands added before first editor#mode 742 // This guarantees that commands added before first editor#mode
718 // aren't immediately updated, but waits for editor#mode and that 743 // aren't immediately updated, but waits for editor#mode and that
719 // commands added later are immediately refreshed, even when added 744 // commands added later are immediately refreshed, even when added
720 // before instanceReady. #10103, #10249 745 // before instanceReady. http://dev.ckeditor.com/ticket/10103, http://dev.ckeditor.com/ticket/10249
721 if ( this.mode ) 746 if ( this.mode )
722 updateCommand( this, cmd ); 747 updateCommand( this, cmd );
723 748
@@ -761,7 +786,7 @@
761 } ); 786 } );
762 } 787 }
763 788
764 // Remove 'submit' events registered on form element before destroying.(#3988) 789 // Remove 'submit' events registered on form element before destroying.(http://dev.ckeditor.com/ticket/3988)
765 editor.on( 'destroy', function() { 790 editor.on( 'destroy', function() {
766 form.removeListener( 'submit', onSubmit ); 791 form.removeListener( 'submit', onSubmit );
767 } ); 792 } );
@@ -771,7 +796,7 @@
771 function onSubmit( evt ) { 796 function onSubmit( evt ) {
772 editor.updateElement(); 797 editor.updateElement();
773 798
774 // #8031 If textarea had required attribute and editor is empty fire 'required' event and if 799 // http://dev.ckeditor.com/ticket/8031 If textarea had required attribute and editor is empty fire 'required' event and if
775 // it was cancelled, prevent submitting the form. 800 // it was cancelled, prevent submitting the form.
776 if ( editor._.required && !element.getValue() && editor.fire( 'required' ) === false ) { 801 if ( editor._.required && !element.getValue() && editor.fire( 'required' ) === false ) {
777 // When user press save button event (evt) is undefined (see save plugin). 802 // When user press save button event (evt) is undefined (see save plugin).
@@ -860,10 +885,10 @@
860 * 885 *
861 * editorInstance.execCommand( 'bold' ); 886 * editorInstance.execCommand( 'bold' );
862 * 887 *
863 * @param {String} commandName The indentifier name of the command. 888 * @param {String} commandName The identifier name of the command.
864 * @param {Object} [data] The data to be passed to the command. 889 * @param {Object} [data] The data to be passed to the command. It defaults to
865 * @returns {Boolean} `true` if the command was executed 890 * an empty object starting from 4.7.0.
866 * successfully, otherwise `false`. 891 * @returns {Boolean} `true` if the command was executed successfully, `false` otherwise.
867 * @see CKEDITOR.editor#addCommand 892 * @see CKEDITOR.editor#addCommand
868 */ 893 */
869 execCommand: function( commandName, data ) { 894 execCommand: function( commandName, data ) {
@@ -871,7 +896,7 @@
871 896
872 var eventData = { 897 var eventData = {
873 name: commandName, 898 name: commandName,
874 commandData: data, 899 commandData: data || {},
875 command: command 900 command: command
876 }; 901 };
877 902
@@ -967,7 +992,7 @@
967 } 992 }
968 else { 993 else {
969 // If we don't have a proper element, set data to an empty string, 994 // If we don't have a proper element, set data to an empty string,
970 // as this method is expected to return a string. (#13385) 995 // as this method is expected to return a string. (http://dev.ckeditor.com/ticket/13385)
971 data = ''; 996 data = '';
972 } 997 }
973 } 998 }
@@ -1069,7 +1094,7 @@
1069 this.readOnly = isReadOnly; 1094 this.readOnly = isReadOnly;
1070 1095
1071 // Block or release BACKSPACE key according to current read-only 1096 // Block or release BACKSPACE key according to current read-only
1072 // state to prevent browser's history navigation (#9761). 1097 // state to prevent browser's history navigation (http://dev.ckeditor.com/ticket/9761).
1073 this.keystrokeHandler.blockedKeystrokes[ 8 ] = +isReadOnly; 1098 this.keystrokeHandler.blockedKeystrokes[ 8 ] = +isReadOnly;
1074 1099
1075 this.editable().setReadOnly( isReadOnly ); 1100 this.editable().setReadOnly( isReadOnly );
@@ -1198,17 +1223,20 @@
1198 */ 1223 */
1199 extractSelectedHtml: function( toString, removeEmptyBlock ) { 1224 extractSelectedHtml: function( toString, removeEmptyBlock ) {
1200 var editable = this.editable(), 1225 var editable = this.editable(),
1201 ranges = this.getSelection().getRanges(); 1226 ranges = this.getSelection().getRanges(),
1227 docFragment = new CKEDITOR.dom.documentFragment(),
1228 i;
1202 1229
1203 if ( !editable || ranges.length === 0 ) { 1230 if ( !editable || ranges.length === 0 ) {
1204 return null; 1231 return null;
1205 } 1232 }
1206 1233
1207 var range = ranges[ 0 ], 1234 for ( i = 0; i < ranges.length; i++ ) {
1208 docFragment = editable.extractHtmlFromRange( range, removeEmptyBlock ); 1235 docFragment.append( editable.extractHtmlFromRange( ranges[ i ], removeEmptyBlock ) );
1236 }
1209 1237
1210 if ( !removeEmptyBlock ) { 1238 if ( !removeEmptyBlock ) {
1211 this.getSelection().selectRanges( [ range ] ); 1239 this.getSelection().selectRanges( [ ranges[ 0 ] ] );
1212 } 1240 }
1213 1241
1214 return toString ? docFragment.getHtml() : docFragment; 1242 return toString ? docFragment.getHtml() : docFragment;
@@ -1325,28 +1353,33 @@
1325 1353
1326 /** 1354 /**
1327 * Returns the keystroke that is assigned to a specified {@link CKEDITOR.command}. If no keystroke is assigned, 1355 * Returns the keystroke that is assigned to a specified {@link CKEDITOR.command}. If no keystroke is assigned,
1328 * it returns null. 1356 * it returns `null`.
1357 *
1358 * Since version 4.7.0 this function also accepts a `command` parameter as a string.
1329 * 1359 *
1330 * @since 4.6.0 1360 * @since 4.6.0
1331 * @param {CKEDITOR.command} command 1361 * @param {CKEDITOR.command/String} command The {@link CKEDITOR.command} instance or a string with the command name.
1332 * @returns {Number} The keystroke assigned to the provided command or null if there is no keystroke. 1362 * @returns {Number/null} The keystroke assigned to the provided command or `null` if there is no keystroke.
1333 */ 1363 */
1334 getCommandKeystroke: function( command ) { 1364 getCommandKeystroke: function( command ) {
1335 var commandName = command.name, 1365 var commandInstance = ( typeof command === 'string' ? this.getCommand( command ) : command );
1336 keystrokes = this.keystrokeHandler.keystrokes,
1337 key;
1338 1366
1339 // Some commands have a fake keystroke - for example CUT/COPY/PASTE commands are handled natively. 1367 if ( commandInstance ) {
1340 if ( command.fakeKeystroke ) { 1368 var commandName = CKEDITOR.tools.object.findKey( this.commands, commandInstance ),
1341 return command.fakeKeystroke; 1369 keystrokes = this.keystrokeHandler.keystrokes,
1342 } 1370 key;
1343 1371
1344 for ( key in keystrokes ) { 1372 // Some commands have a fake keystroke - for example CUT/COPY/PASTE commands are handled natively.
1345 if ( keystrokes.hasOwnProperty( key ) && keystrokes[ key ] == commandName ) { 1373 if ( commandInstance.fakeKeystroke ) {
1346 return key; 1374 return commandInstance.fakeKeystroke;
1347 } 1375 }
1348 }
1349 1376
1377 for ( key in keystrokes ) {
1378 if ( keystrokes.hasOwnProperty( key ) && keystrokes[ key ] == commandName ) {
1379 return key;
1380 }
1381 }
1382 }
1350 return null; 1383 return null;
1351 }, 1384 },
1352 1385
@@ -1539,7 +1572,7 @@ CKEDITOR.ELEMENT_MODE_INLINE = 3;
1539 * @member CKEDITOR.config 1572 * @member CKEDITOR.config
1540 */ 1573 */
1541 1574
1542 /** 1575/**
1543 * Customizes the {@link CKEDITOR.editor#title human-readable title} of this editor. This title is displayed in 1576 * Customizes the {@link CKEDITOR.editor#title human-readable title} of this editor. This title is displayed in
1544 * tooltips and impacts various [accessibility aspects](#!/guide/dev_a11y-section-announcing-the-editor-on-the-page), 1577 * tooltips and impacts various [accessibility aspects](#!/guide/dev_a11y-section-announcing-the-editor-on-the-page),
1545 * e.g. it is commonly used by screen readers for distinguishing editor instances and for navigation. 1578 * e.g. it is commonly used by screen readers for distinguishing editor instances and for navigation.
diff --git a/sources/core/env.js b/sources/core/env.js
index 43b608a..cbf089c 100644
--- a/sources/core/env.js
+++ b/sources/core/env.js
@@ -136,7 +136,7 @@ if ( !CKEDITOR.env ) {
136 var domain = document.domain, 136 var domain = document.domain,
137 hostname = window.location.hostname; 137 hostname = window.location.hostname;
138 138
139 return domain != hostname && domain != ( '[' + hostname + ']' ); // IPv6 IP support (#5434) 139 return domain != hostname && domain != ( '[' + hostname + ']' ); // IPv6 IP support (http://dev.ckeditor.com/ticket/5434)
140 }, 140 },
141 141
142 /** 142 /**
diff --git a/sources/core/filter.js b/sources/core/filter.js
index db68530..3e64fc5 100644
--- a/sources/core/filter.js
+++ b/sources/core/filter.js
@@ -158,7 +158,8 @@
158 }, 158 },
159 // Object: element name => array of transformations groups. 159 // Object: element name => array of transformations groups.
160 transformations: {}, 160 transformations: {},
161 cachedTests: {} 161 cachedTests: {},
162 cachedChecks: {}
162 }; 163 };
163 164
164 // Register filter instance. 165 // Register filter instance.
@@ -299,7 +300,7 @@
299 if ( el.attributes[ 'data-cke-filter' ] == 'off' ) 300 if ( el.attributes[ 'data-cke-filter' ] == 'off' )
300 return false; 301 return false;
301 302
302 // (#10260) Don't touch elements like spans with data-cke-* attribute since they're 303 // (http://dev.ckeditor.com/ticket/10260) Don't touch elements like spans with data-cke-* attribute since they're
303 // responsible e.g. for placing markers, bookmarks, odds and stuff. 304 // responsible e.g. for placing markers, bookmarks, odds and stuff.
304 // We love 'em and we don't wanna lose anything during the filtering. 305 // We love 'em and we don't wanna lose anything during the filtering.
305 // '|' is to avoid tricky joints like data-="foo" + cke-="bar". Yes, they're possible. 306 // '|' is to avoid tricky joints like data-="foo" + cke-="bar". Yes, they're possible.
@@ -346,7 +347,7 @@
346 if ( !element.parent ) 347 if ( !element.parent )
347 continue; 348 continue;
348 349
349 // Handle custom elements as inline elements (#12683). 350 // Handle custom elements as inline elements (http://dev.ckeditor.com/ticket/12683).
350 parentDtd = DTD[ element.parent.name ] || DTD.span; 351 parentDtd = DTD[ element.parent.name ] || DTD.span;
351 352
352 switch ( check.check ) { 353 switch ( check.check ) {
@@ -806,6 +807,32 @@
806 } )(), 807 } )(),
807 808
808 /** 809 /**
810 * Returns a clone of this filter instance.
811 *
812 * @since 4.7.3
813 * @returns {CKEDITOR.filter}
814 */
815 clone: function() {
816 var ret = new CKEDITOR.filter(),
817 clone = CKEDITOR.tools.clone;
818
819 // Cloning allowed content related things.
820 ret.allowedContent = clone( this.allowedContent );
821 ret._.allowedRules = clone( this._.allowedRules );
822
823 // Disallowed content rules.
824 ret.disallowedContent = clone( this.disallowedContent );
825 ret._.disallowedRules = clone( this._.disallowedRules );
826
827 ret._.transformations = clone( this._.transformations );
828
829 ret.disabled = this.disabled;
830 ret.editor = this.editor;
831
832 return ret;
833 },
834
835 /**
809 * Destroys the filter instance and removes it from the global {@link CKEDITOR.filter#instances} object. 836 * Destroys the filter instance and removes it from the global {@link CKEDITOR.filter#instances} object.
810 * 837 *
811 * @since 4.4.5 838 * @since 4.4.5
@@ -2201,7 +2228,7 @@
2201 var widths = element.styles.margin.match( /(\-?[\.\d]+\w+)/g ) || [ '0px' ]; 2228 var widths = element.styles.margin.match( /(\-?[\.\d]+\w+)/g ) || [ '0px' ];
2202 switch ( widths.length ) { 2229 switch ( widths.length ) {
2203 case 1: 2230 case 1:
2204 element.styles.margin = widths[0]; 2231 mapStyles( [ 0, 0, 0, 0 ] );
2205 break; 2232 break;
2206 case 2: 2233 case 2:
2207 mapStyles( [ 0, 1, 0, 1 ] ); 2234 mapStyles( [ 0, 1, 0, 1 ] );
@@ -2291,7 +2318,7 @@
2291 * * {@link CKEDITOR.filter.allowedContentRules} &ndash; defined rules will be added 2318 * * {@link CKEDITOR.filter.allowedContentRules} &ndash; defined rules will be added
2292 * to the {@link CKEDITOR.editor#filter}. 2319 * to the {@link CKEDITOR.editor#filter}.
2293 * * `true` &ndash; will disable the filter (data will not be filtered, 2320 * * `true` &ndash; will disable the filter (data will not be filtered,
2294 * all features will be activated). 2321 * all features will be activated). Reading [security best practices](#!/guide/dev_best_practices) before setting `true` is recommended.
2295 * * default &ndash; the filter will be configured by loaded features 2322 * * default &ndash; the filter will be configured by loaded features
2296 * (toolbar items, commands, etc.). 2323 * (toolbar items, commands, etc.).
2297 * 2324 *
diff --git a/sources/core/focusmanager.js b/sources/core/focusmanager.js
index 6fc9969..45c3137 100644
--- a/sources/core/focusmanager.js
+++ b/sources/core/focusmanager.js
@@ -147,34 +147,28 @@
147 * @member CKEDITOR.focusManager 147 * @member CKEDITOR.focusManager
148 */ 148 */
149 blur: function( noDelay ) { 149 blur: function( noDelay ) {
150 if ( this._.locked ) 150 if ( this._.locked ) {
151 return; 151 return;
152 }
152 153
153 function doBlur() { 154 function doBlur() {
154 var editor = this._.editor;
155
156 if ( this.hasFocus ) { 155 if ( this.hasFocus ) {
157 this.hasFocus = false; 156 this.hasFocus = false;
158 157
159 // Blink browsers leave selection in `[contenteditable=true]`
160 // when it's blurred and it's neccessary to remove it manually for inline editor. (#13446)
161 if ( CKEDITOR.env.chrome && editor.editable().isInline() ) {
162 editor.window.$.getSelection().removeAllRanges();
163 }
164
165 var ct = this._.editor.container; 158 var ct = this._.editor.container;
166 ct && ct.removeClass( 'cke_focus' ); 159 ct && ct.removeClass( 'cke_focus' );
167 this._.editor.fire( 'blur' ); 160 this._.editor.fire( 'blur' );
168 } 161 }
169 } 162 }
170 163
171 if ( this._.timer ) 164 if ( this._.timer ) {
172 clearTimeout( this._.timer ); 165 clearTimeout( this._.timer );
166 }
173 167
174 var delay = CKEDITOR.focusManager._.blurDelay; 168 var delay = CKEDITOR.focusManager._.blurDelay;
175 if ( noDelay || !delay ) 169 if ( noDelay || !delay ) {
176 doBlur.call( this ); 170 doBlur.call( this );
177 else { 171 } else {
178 this._.timer = CKEDITOR.tools.setTimeout( function() { 172 this._.timer = CKEDITOR.tools.setTimeout( function() {
179 delete this._.timer; 173 delete this._.timer;
180 doBlur.call( this ); 174 doBlur.call( this );
diff --git a/sources/core/htmldataprocessor.js b/sources/core/htmldataprocessor.js
index 56764be..79e996b 100644
--- a/sources/core/htmldataprocessor.js
+++ b/sources/core/htmldataprocessor.js
@@ -56,7 +56,7 @@
56 // it up and apply the filter. 56 // it up and apply the filter.
57 data = protectSource( data, editor ); 57 data = protectSource( data, editor );
58 58
59 // Protect content of textareas. (#9995) 59 // Protect content of textareas. (http://dev.ckeditor.com/ticket/9995)
60 // Do this before protecting attributes to avoid breaking: 60 // Do this before protecting attributes to avoid breaking:
61 // <textarea><img src="..." /></textarea> 61 // <textarea><img src="..." /></textarea>
62 data = protectElements( data, protectTextareaRegex ); 62 data = protectElements( data, protectTextareaRegex );
@@ -67,23 +67,23 @@
67 data = protectAttributes( data ); 67 data = protectAttributes( data );
68 68
69 // Protect elements than can't be set inside a DIV. E.g. IE removes 69 // Protect elements than can't be set inside a DIV. E.g. IE removes
70 // style tags from innerHTML. (#3710) 70 // style tags from innerHTML. (http://dev.ckeditor.com/ticket/3710)
71 data = protectElements( data, protectElementsRegex ); 71 data = protectElements( data, protectElementsRegex );
72 72
73 // Certain elements has problem to go through DOM operation, protect 73 // Certain elements has problem to go through DOM operation, protect
74 // them by prefixing 'cke' namespace. (#3591) 74 // them by prefixing 'cke' namespace. (http://dev.ckeditor.com/ticket/3591)
75 data = protectElementsNames( data ); 75 data = protectElementsNames( data );
76 76
77 // All none-IE browsers ignore self-closed custom elements, 77 // All none-IE browsers ignore self-closed custom elements,
78 // protecting them into open-close. (#3591) 78 // protecting them into open-close. (http://dev.ckeditor.com/ticket/3591)
79 data = protectSelfClosingElements( data ); 79 data = protectSelfClosingElements( data );
80 80
81 // Compensate one leading line break after <pre> open as browsers 81 // Compensate one leading line break after <pre> open as browsers
82 // eat it up. (#5789) 82 // eat it up. (http://dev.ckeditor.com/ticket/5789)
83 data = protectPreFormatted( data ); 83 data = protectPreFormatted( data );
84 84
85 // There are attributes which may execute JavaScript code inside fixBin. 85 // There are attributes which may execute JavaScript code inside fixBin.
86 // Encode them greedily. They will be unprotected right after getting HTML from fixBin. (#10) 86 // Encode them greedily. They will be unprotected right after getting HTML from fixBin. (http://dev.ckeditor.com/ticket/10)
87 data = protectInsecureAttributes( data ); 87 data = protectInsecureAttributes( data );
88 88
89 var fixBin = evtData.context || editor.editable().getName(), 89 var fixBin = evtData.context || editor.editable().getName(),
@@ -99,7 +99,7 @@
99 // Call the browser to help us fixing a possibly invalid HTML 99 // Call the browser to help us fixing a possibly invalid HTML
100 // structure. 100 // structure.
101 var el = editor.document.createElement( fixBin ); 101 var el = editor.document.createElement( fixBin );
102 // Add fake character to workaround IE comments bug. (#3801) 102 // Add fake character to workaround IE comments bug. (http://dev.ckeditor.com/ticket/3801)
103 el.setHtml( 'a' + data ); 103 el.setHtml( 'a' + data );
104 data = el.getHtml().substr( 1 ); 104 data = el.getHtml().substr( 1 );
105 105
@@ -128,7 +128,7 @@
128 data = CKEDITOR.htmlParser.fragment.fromHtml( data, evtData.context, fixBodyTag ); 128 data = CKEDITOR.htmlParser.fragment.fromHtml( data, evtData.context, fixBodyTag );
129 129
130 // The empty root element needs to be fixed by adding 'p' or 'div' into it. 130 // The empty root element needs to be fixed by adding 'p' or 'div' into it.
131 // This avoids the need to create that element on the first focus (#12630). 131 // This avoids the need to create that element on the first focus (http://dev.ckeditor.com/ticket/12630).
132 if ( fixBodyTag ) { 132 if ( fixBodyTag ) {
133 fixEmptyRoot( data, fixBodyTag ); 133 fixEmptyRoot( data, fixBodyTag );
134 } 134 }
@@ -163,7 +163,7 @@
163 editor.on( 'toDataFormat', function( evt ) { 163 editor.on( 'toDataFormat', function( evt ) {
164 var data = evt.data.dataValue; 164 var data = evt.data.dataValue;
165 165
166 // #10854 - we need to strip leading blockless <br> which FF adds 166 // http://dev.ckeditor.com/ticket/10854 - we need to strip leading blockless <br> which FF adds
167 // automatically when editable contains only non-editable content. 167 // automatically when editable contains only non-editable content.
168 // We do that for every browser (so it's a constant behavior) and 168 // We do that for every browser (so it's a constant behavior) and
169 // not in BR mode, in which chance of valid leading blockless <br> is higher. 169 // not in BR mode, in which chance of valid leading blockless <br> is higher.
@@ -192,7 +192,7 @@
192 data.writeChildrenHtml( writer ); 192 data.writeChildrenHtml( writer );
193 data = writer.getHtml( true ); 193 data = writer.getHtml( true );
194 194
195 // Restore those non-HTML protected source. (#4475,#4880) 195 // Restore those non-HTML protected source. (http://dev.ckeditor.com/ticket/4475,http://dev.ckeditor.com/ticket/4880)
196 data = unprotectRealComments( data ); 196 data = unprotectRealComments( data );
197 data = unprotectSource( data, editor ); 197 data = unprotectSource( data, editor );
198 198
@@ -448,7 +448,7 @@
448 return false; 448 return false;
449 449
450 // 1. For IE version >=8, empty blocks are displayed correctly themself in wysiwiyg; 450 // 1. For IE version >=8, empty blocks are displayed correctly themself in wysiwiyg;
451 // 2. For the rest, at least table cell and list item need no filler space. (#6248) 451 // 2. For the rest, at least table cell and list item need no filler space. (http://dev.ckeditor.com/ticket/6248)
452 if ( !isOutput && !CKEDITOR.env.needsBrFiller && 452 if ( !isOutput && !CKEDITOR.env.needsBrFiller &&
453 ( document.documentMode > 7 || 453 ( document.documentMode > 7 ||
454 block.name in CKEDITOR.dtd.tr || 454 block.name in CKEDITOR.dtd.tr ||
@@ -484,7 +484,7 @@
484 } 484 }
485 485
486 // Regex to scan for &nbsp; at the end of blocks, which are actually placeholders. 486 // Regex to scan for &nbsp; at the end of blocks, which are actually placeholders.
487 // Safari transforms the &nbsp; to \xa0. (#4172) 487 // Safari transforms the &nbsp; to \xa0. (http://dev.ckeditor.com/ticket/4172)
488 var tailNbspRegex = /(?:&nbsp;|\xa0)$/; 488 var tailNbspRegex = /(?:&nbsp;|\xa0)$/;
489 489
490 var protectedSourceMarker = '{cke_protected}'; 490 var protectedSourceMarker = '{cke_protected}';
@@ -563,18 +563,35 @@
563 // active in the editing area (IE|WebKit). 563 // active in the editing area (IE|WebKit).
564 [ ( /^on/ ), 'data-cke-pa-on' ], 564 [ ( /^on/ ), 'data-cke-pa-on' ],
565 565
566 // Prevent iframe's srcdoc attribute from being evaluated in the editable.
567 [ ( /^srcdoc/ ), 'data-cke-pa-srcdoc' ],
568
566 // Don't let some old expando enter editor. Concerns only IE8, 569 // Don't let some old expando enter editor. Concerns only IE8,
567 // but for consistency remove on all browsers. 570 // but for consistency remove on all browsers.
568 [ ( /^data-cke-expando$/ ), '' ] 571 [ ( /^data-cke-expando$/ ), '' ]
569 ] 572 ],
573
574 elements: {
575 // Prevent iframe's src attribute with javascript code or data protocol from being evaluated in the editable.
576 iframe: function( element ) {
577 if ( element.attributes && element.attributes.src ) {
578
579 var src = element.attributes.src.toLowerCase().replace( /[^a-z]/gi, '' );
580 if ( src.indexOf( 'javascript' ) === 0 || src.indexOf( 'data' ) === 0 ) {
581 element.attributes[ 'data-cke-pa-src' ] = element.attributes.src;
582 delete element.attributes.src;
583 }
584 }
585 }
586 }
570 }; 587 };
571 588
572 // Disable form elements editing mode provided by some browsers. (#5746) 589 // Disable form elements editing mode provided by some browsers. (http://dev.ckeditor.com/ticket/5746)
573 function protectReadOnly( element ) { 590 function protectReadOnly( element ) {
574 var attrs = element.attributes; 591 var attrs = element.attributes;
575 592
576 // We should flag that the element was locked by our code so 593 // We should flag that the element was locked by our code so
577 // it'll be editable by the editor functions (#6046). 594 // it'll be editable by the editor functions (http://dev.ckeditor.com/ticket/6046).
578 if ( attrs.contenteditable != 'false' ) 595 if ( attrs.contenteditable != 'false' )
579 attrs[ 'data-cke-editable' ] = attrs.contenteditable ? 'true' : 1; 596 attrs[ 'data-cke-editable' ] = attrs.contenteditable ? 'true' : 1;
580 597
@@ -602,7 +619,7 @@
602 } 619 }
603 }, 620 },
604 621
605 // Remove empty link but not empty anchor. (#3829, #13516) 622 // Remove empty link but not empty anchor. (http://dev.ckeditor.com/ticket/3829, http://dev.ckeditor.com/ticket/13516)
606 a: function( element ) { 623 a: function( element ) {
607 var attrs = element.attributes; 624 var attrs = element.attributes;
608 625
@@ -641,7 +658,7 @@
641 if ( attribs[ 'data-cke-temp' ] ) 658 if ( attribs[ 'data-cke-temp' ] )
642 return false; 659 return false;
643 660
644 // Remove duplicated attributes - #3789. 661 // Remove duplicated attributes - http://dev.ckeditor.com/ticket/3789.
645 var attributeNames = [ 'name', 'href', 'src' ], 662 var attributeNames = [ 'name', 'href', 'src' ],
646 savedAttributeName; 663 savedAttributeName;
647 for ( var i = 0; i < attributeNames.length; i++ ) { 664 for ( var i = 0; i < attributeNames.length; i++ ) {
@@ -653,7 +670,7 @@
653 return element; 670 return element;
654 }, 671 },
655 672
656 // The contents of table should be in correct order (#4809). 673 // The contents of table should be in correct order (http://dev.ckeditor.com/ticket/4809).
657 table: function( element ) { 674 table: function( element ) {
658 // Clone the array as it would become empty during the sort call. 675 // Clone the array as it would become empty during the sort call.
659 var children = element.children.slice( 0 ); 676 var children = element.children.slice( 0 );
@@ -712,7 +729,7 @@
712 title: function( element ) { 729 title: function( element ) {
713 var titleText = element.children[ 0 ]; 730 var titleText = element.children[ 0 ];
714 731
715 // Append text-node to title tag if not present (i.e. non-IEs) (#9882). 732 // Append text-node to title tag if not present (i.e. non-IEs) (http://dev.ckeditor.com/ticket/9882).
716 !titleText && append( element, titleText = new CKEDITOR.htmlParser.text() ); 733 !titleText && append( element, titleText = new CKEDITOR.htmlParser.text() );
717 734
718 // Transfer data-saved title to title tag. 735 // Transfer data-saved title to title tag.
@@ -733,7 +750,7 @@
733 750
734 if ( CKEDITOR.env.ie ) { 751 if ( CKEDITOR.env.ie ) {
735 // IE outputs style attribute in capital letters. We should convert 752 // IE outputs style attribute in capital letters. We should convert
736 // them back to lower case, while not hurting the values (#5930) 753 // them back to lower case, while not hurting the values (http://dev.ckeditor.com/ticket/5930)
737 defaultHtmlFilterRulesForAll.attributes.style = function( value ) { 754 defaultHtmlFilterRulesForAll.attributes.style = function( value ) {
738 return value.replace( /(^|;)([^\:]+)/g, function( match ) { 755 return value.replace( /(^|;)([^\:]+)/g, function( match ) {
739 return match.toLowerCase(); 756 return match.toLowerCase();
@@ -741,7 +758,7 @@
741 }; 758 };
742 } 759 }
743 760
744 // Disable form elements editing mode provided by some browsers. (#5746) 761 // Disable form elements editing mode provided by some browsers. (http://dev.ckeditor.com/ticket/5746)
745 function unprotectReadyOnly( element ) { 762 function unprotectReadyOnly( element ) {
746 var attrs = element.attributes; 763 var attrs = element.attributes;
747 switch ( attrs[ 'data-cke-editable' ] ) { 764 switch ( attrs[ 'data-cke-editable' ] ) {
@@ -773,7 +790,7 @@
773 // 790 //
774 // 'data-x' => '&lt;a href=&quot;X&quot;' 791 // 'data-x' => '&lt;a href=&quot;X&quot;'
775 // 792 //
776 // which, can be easily filtered out (#11508). 793 // which, can be easily filtered out (http://dev.ckeditor.com/ticket/11508).
777 protectAttributeRegex = /([\w-:]+)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi, 794 protectAttributeRegex = /([\w-:]+)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi,
778 protectAttributeNameRegex = /^(href|src|name)$/i; 795 protectAttributeNameRegex = /^(href|src|name)$/i;
779 796
@@ -790,8 +807,8 @@
790 function protectAttributes( html ) { 807 function protectAttributes( html ) {
791 return html.replace( protectElementRegex, function( element, tag, attributes ) { 808 return html.replace( protectElementRegex, function( element, tag, attributes ) {
792 return '<' + tag + attributes.replace( protectAttributeRegex, function( fullAttr, attrName ) { 809 return '<' + tag + attributes.replace( protectAttributeRegex, function( fullAttr, attrName ) {
793 // Avoid corrupting the inline event attributes (#7243). 810 // Avoid corrupting the inline event attributes (http://dev.ckeditor.com/ticket/7243).
794 // We should not rewrite the existed protected attributes, e.g. clipboard content from editor. (#5218) 811 // We should not rewrite the existed protected attributes, e.g. clipboard content from editor. (http://dev.ckeditor.com/ticket/5218)
795 if ( protectAttributeNameRegex.test( attrName ) && attributes.indexOf( 'data-cke-saved-' + attrName ) == -1 ) 812 if ( protectAttributeNameRegex.test( attrName ) && attributes.indexOf( 'data-cke-saved-' + attrName ) == -1 )
796 return ' data-cke-saved-' + fullAttr + ' data-cke-' + CKEDITOR.rnd + '-' + fullAttr; 813 return ' data-cke-saved-' + fullAttr + ' data-cke-' + CKEDITOR.rnd + '-' + fullAttr;
797 814
@@ -880,7 +897,7 @@
880 // <noscript> tags (get lost in IE and messed up in FF). 897 // <noscript> tags (get lost in IE and messed up in FF).
881 /<noscript[\s\S]*?<\/noscript>/gi, 898 /<noscript[\s\S]*?<\/noscript>/gi,
882 899
883 // Avoid meta tags being stripped (#8117). 900 // Avoid meta tags being stripped (http://dev.ckeditor.com/ticket/8117).
884 /<meta[\s\S]*?\/?>/gi 901 /<meta[\s\S]*?\/?>/gi
885 ].concat( protectRegexes ); 902 ].concat( protectRegexes );
886 903
@@ -894,7 +911,7 @@
894 911
895 for ( var i = 0; i < regexes.length; i++ ) { 912 for ( var i = 0; i < regexes.length; i++ ) {
896 data = data.replace( regexes[ i ], function( match ) { 913 data = data.replace( regexes[ i ], function( match ) {
897 match = match.replace( tempRegex, // There could be protected source inside another one. (#3869). 914 match = match.replace( tempRegex, // There could be protected source inside another one. (http://dev.ckeditor.com/ticket/3869).
898 function( $, isComment, id ) { 915 function( $, isComment, id ) {
899 return protectedHtml[ id ]; 916 return protectedHtml[ id ];
900 } ); 917 } );
@@ -912,7 +929,7 @@
912 929
913 // Different protection pattern is used for those that 930 // Different protection pattern is used for those that
914 // live in attributes to avoid from being HTML encoded. 931 // live in attributes to avoid from being HTML encoded.
915 // Why so serious? See #9205, #8216, #7805, #11754, #11846. 932 // Why so serious? See http://dev.ckeditor.com/ticket/9205, http://dev.ckeditor.com/ticket/8216, http://dev.ckeditor.com/ticket/7805, http://dev.ckeditor.com/ticket/11754, http://dev.ckeditor.com/ticket/11846.
916 data = data.replace( /<\w+(?:\s+(?:(?:[^\s=>]+\s*=\s*(?:[^'"\s>]+|'[^']*'|"[^"]*"))|[^\s=\/>]+))+\s*\/?>/g, function( match ) { 933 data = data.replace( /<\w+(?:\s+(?:(?:[^\s=>]+\s*=\s*(?:[^'"\s>]+|'[^']*'|"[^"]*"))|[^\s=\/>]+))+\s*\/?>/g, function( match ) {
917 return match.replace( /<!--\{cke_protected\}([^>]*)-->/g, function( match, data ) { 934 return match.replace( /<!--\{cke_protected\}([^>]*)-->/g, function( match, data ) {
918 store[ store.id ] = decodeURIComponent( data ); 935 store[ store.id ] = decodeURIComponent( data );
@@ -922,7 +939,7 @@
922 939
923 // This RegExp searches for innerText in all the title/iframe/textarea elements. 940 // This RegExp searches for innerText in all the title/iframe/textarea elements.
924 // This is because browser doesn't allow HTML in these elements, that's why we can't 941 // This is because browser doesn't allow HTML in these elements, that's why we can't
925 // nest comments in there. (#11223) 942 // nest comments in there. (http://dev.ckeditor.com/ticket/11223)
926 data = data.replace( /<(title|iframe|textarea)([^>]*)>([\s\S]*?)<\/\1>/g, function( match, tagName, tagAttributes, innerText ) { 943 data = data.replace( /<(title|iframe|textarea)([^>]*)>([\s\S]*?)<\/\1>/g, function( match, tagName, tagAttributes, innerText ) {
927 return '<' + tagName + tagAttributes + '>' + unprotectSource( unprotectRealComments( innerText ), editor ) + '</' + tagName + '>'; 944 return '<' + tagName + tagAttributes + '>' + unprotectSource( unprotectRealComments( innerText ), editor ) + '</' + tagName + '>';
928 } ); 945 } );
@@ -971,7 +988,7 @@
971 * {@link CKEDITOR.htmlParser.fragment} {@link CKEDITOR.htmlParser.element}. 988 * {@link CKEDITOR.htmlParser.fragment} {@link CKEDITOR.htmlParser.element}.
972 * * 5-9: Data is available in the parsed format, but {@link CKEDITOR.htmlDataProcessor#dataFilter} 989 * * 5-9: Data is available in the parsed format, but {@link CKEDITOR.htmlDataProcessor#dataFilter}
973 * is not applied yet. 990 * is not applied yet.
974 * * 6: Data is filtered with the {CKEDITOR.filter content filter}. 991 * * 6: Data is filtered with the {@link CKEDITOR.filter content filter}.
975 * * 10: Data is processed with {@link CKEDITOR.htmlDataProcessor#dataFilter}. 992 * * 10: Data is processed with {@link CKEDITOR.htmlDataProcessor#dataFilter}.
976 * * 10-14: Data is available in the parsed format and {@link CKEDITOR.htmlDataProcessor#dataFilter} 993 * * 10-14: Data is available in the parsed format and {@link CKEDITOR.htmlDataProcessor#dataFilter}
977 * has already been applied. 994 * has already been applied.
diff --git a/sources/core/htmlparser.js b/sources/core/htmlparser.js
index 8c30992..c95257c 100644
--- a/sources/core/htmlparser.js
+++ b/sources/core/htmlparser.js
@@ -163,7 +163,7 @@ CKEDITOR.htmlParser = function() {
163 tagName = tagName.toLowerCase(); 163 tagName = tagName.toLowerCase();
164 164
165 // There are some tag names that can break things, so let's 165 // There are some tag names that can break things, so let's
166 // simply ignore them when parsing. (#5224) 166 // simply ignore them when parsing. (http://dev.ckeditor.com/ticket/5224)
167 if ( /="/.test( tagName ) ) 167 if ( /="/.test( tagName ) )
168 continue; 168 continue;
169 169
diff --git a/sources/core/htmlparser/basicwriter.js b/sources/core/htmlparser/basicwriter.js
index 529fbf1..61447f0 100644
--- a/sources/core/htmlparser/basicwriter.js
+++ b/sources/core/htmlparser/basicwriter.js
@@ -67,7 +67,7 @@ CKEDITOR.htmlParser.basicWriter = CKEDITOR.tools.createClass( {
67 * @param {String} attValue The attribute value. 67 * @param {String} attValue The attribute value.
68 */ 68 */
69 attribute: function( attName, attValue ) { 69 attribute: function( attName, attValue ) {
70 // Browsers don't always escape special character in attribute values. (#4683, #4719). 70 // Browsers don't always escape special character in attribute values. (http://dev.ckeditor.com/ticket/4683, http://dev.ckeditor.com/ticket/4719).
71 if ( typeof attValue == 'string' ) 71 if ( typeof attValue == 'string' )
72 attValue = CKEDITOR.tools.htmlEncodeAttr( attValue ); 72 attValue = CKEDITOR.tools.htmlEncodeAttr( attValue );
73 73
diff --git a/sources/core/htmlparser/element.js b/sources/core/htmlparser/element.js
index 0ed750e..224d3e6 100644
--- a/sources/core/htmlparser/element.js
+++ b/sources/core/htmlparser/element.js
@@ -35,7 +35,7 @@ CKEDITOR.htmlParser.element = function( name, attributes ) {
35 */ 35 */
36 this.children = []; 36 this.children = [];
37 37
38 // Reveal the real semantic of our internal custom tag name (#6639), 38 // Reveal the real semantic of our internal custom tag name (http://dev.ckeditor.com/ticket/6639),
39 // when resolving whether it's block like. 39 // when resolving whether it's block like.
40 var realName = name || '', 40 var realName = name || '',
41 prefixed = realName.match( /^cke:(.*)/ ); 41 prefixed = realName.match( /^cke:(.*)/ );
@@ -543,8 +543,8 @@ CKEDITOR.htmlParser.cssStyle = function() {
543 543
544 if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' ) 544 if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' )
545 changes.push( 'nonEditable', true ); 545 changes.push( 'nonEditable', true );
546 // A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372, #11698). 546 // A context to be given nestedEditable must be nonEditable first (by inheritance) (http://dev.ckeditor.com/ticket/11372, http://dev.ckeditor.com/ticket/11698).
547 // Special case: #11504 - filter starts on <body contenteditable=true>, 547 // Special case: http://dev.ckeditor.com/ticket/11504 - filter starts on <body contenteditable=true>,
548 // so ctx.nonEditable has not been yet set to true. 548 // so ctx.nonEditable has not been yet set to true.
549 else if ( ctx.nonEditable && !ctx.nestedEditable && this.attributes.contenteditable == 'true' ) 549 else if ( ctx.nonEditable && !ctx.nestedEditable && this.attributes.contenteditable == 'true' )
550 changes.push( 'nestedEditable', true ); 550 changes.push( 'nestedEditable', true );
diff --git a/sources/core/htmlparser/fragment.js b/sources/core/htmlparser/fragment.js
index f696a12..7ef915c 100644
--- a/sources/core/htmlparser/fragment.js
+++ b/sources/core/htmlparser/fragment.js
@@ -61,7 +61,7 @@ CKEDITOR.htmlParser.fragment = function() {
61 if ( node.attributes[ 'data-cke-survive' ] ) 61 if ( node.attributes[ 'data-cke-survive' ] )
62 return false; 62 return false;
63 63
64 // Empty link is to be removed when empty but not anchor. (#7894) 64 // Empty link is to be removed when empty but not anchor. (http://dev.ckeditor.com/ticket/7894)
65 return node.name == 'a' && node.attributes.href || CKEDITOR.dtd.$removeEmpty[ node.name ]; 65 return node.name == 'a' && node.attributes.href || CKEDITOR.dtd.$removeEmpty[ node.name ];
66 } 66 }
67 67
@@ -130,7 +130,7 @@ CKEDITOR.htmlParser.fragment = function() {
130 i--; 130 i--;
131 } else { 131 } else {
132 // Some element of the same type cannot be nested, flat them, 132 // Some element of the same type cannot be nested, flat them,
133 // e.g. <a href="#">foo<a href="#">bar</a></a>. (#7894) 133 // e.g. <a href="#">foo<a href="#">bar</a></a>. (http://dev.ckeditor.com/ticket/7894)
134 if ( pendingName == currentNode.name ) 134 if ( pendingName == currentNode.name )
135 addElement( currentNode, currentNode.parent, 1 ), i--; 135 addElement( currentNode, currentNode.parent, 1 ), i--;
136 } 136 }
@@ -143,7 +143,7 @@ CKEDITOR.htmlParser.fragment = function() {
143 addElement( pendingBRs.shift(), currentNode ); 143 addElement( pendingBRs.shift(), currentNode );
144 } 144 }
145 145
146 // Rtrim empty spaces on block end boundary. (#3585) 146 // Rtrim empty spaces on block end boundary. (http://dev.ckeditor.com/ticket/3585)
147 function removeTailWhitespace( element ) { 147 function removeTailWhitespace( element ) {
148 if ( element._.isBlockLike && element.name != 'pre' && element.name != 'textarea' ) { 148 if ( element._.isBlockLike && element.name != 'pre' && element.name != 'textarea' ) {
149 149
@@ -275,10 +275,10 @@ CKEDITOR.htmlParser.fragment = function() {
275 // If the element cannot be child of the current element. 275 // If the element cannot be child of the current element.
276 if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) { 276 if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) {
277 // Current node doesn't have a close tag, time for a close 277 // Current node doesn't have a close tag, time for a close
278 // as this element isn't fit in. (#7497) 278 // as this element isn't fit in. (http://dev.ckeditor.com/ticket/7497)
279 if ( currentNode.isOptionalClose ) 279 if ( currentNode.isOptionalClose )
280 parser.onTagClose( currentName ); 280 parser.onTagClose( currentName );
281 // Fixing malformed nested lists by moving it into a previous list item. (#3828) 281 // Fixing malformed nested lists by moving it into a previous list item. (http://dev.ckeditor.com/ticket/3828)
282 else if ( tagName in listBlocks && currentName in listBlocks ) { 282 else if ( tagName in listBlocks && currentName in listBlocks ) {
283 var children = currentNode.children, 283 var children = currentNode.children,
284 lastChild = children[ children.length - 1 ]; 284 lastChild = children[ children.length - 1 ];
@@ -291,7 +291,7 @@ CKEDITOR.htmlParser.fragment = function() {
291 currentNode = lastChild; 291 currentNode = lastChild;
292 } 292 }
293 // Establish new list root for orphan list items, but NOT to create 293 // Establish new list root for orphan list items, but NOT to create
294 // new list for the following ones, fix them instead. (#6975) 294 // new list for the following ones, fix them instead. (http://dev.ckeditor.com/ticket/6975)
295 // <dl><dt>foo<dd>bar</dl> 295 // <dl><dt>foo<dd>bar</dl>
296 // <ul><li>foo<li>bar</ul> 296 // <ul><li>foo<li>bar</ul>
297 else if ( tagName in CKEDITOR.dtd.$listItem && 297 else if ( tagName in CKEDITOR.dtd.$listItem &&
@@ -409,7 +409,7 @@ CKEDITOR.htmlParser.fragment = function() {
409 var currentName = currentNode.name, 409 var currentName = currentNode.name,
410 currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd; 410 currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd;
411 411
412 // Fix orphan text in list/table. (#8540) (#8870) 412 // Fix orphan text in list/table. (http://dev.ckeditor.com/ticket/8540) (http://dev.ckeditor.com/ticket/8870)
413 if ( !inTextarea && !currentDtd[ '#' ] && currentName in nonBreakingBlocks ) { 413 if ( !inTextarea && !currentDtd[ '#' ] && currentName in nonBreakingBlocks ) {
414 parser.onTagOpen( structureFixes[ currentName ] || '' ); 414 parser.onTagOpen( structureFixes[ currentName ] || '' );
415 parser.onText( text ); 415 parser.onText( text );
diff --git a/sources/core/lang.js b/sources/core/lang.js
index 8766055..06f3a2a 100644
--- a/sources/core/lang.js
+++ b/sources/core/lang.js
@@ -18,7 +18,7 @@
18 */ 18 */
19 languages: { 19 languages: {
20 af: 1, ar: 1, az: 1, bg: 1, bn: 1, bs: 1, ca: 1, cs: 1, cy: 1, da: 1, de: 1, 'de-ch': 1, el: 1, 20 af: 1, ar: 1, az: 1, bg: 1, bn: 1, bs: 1, ca: 1, cs: 1, cy: 1, da: 1, de: 1, 'de-ch': 1, el: 1,
21 'en-au': 1, 'en-ca': 1, 'en-gb': 1, en: 1, eo: 1, es: 1, et: 1, eu: 1, fa: 1, fi: 1, fo: 1, 21 'en-au': 1, 'en-ca': 1, 'en-gb': 1, en: 1, eo: 1, es: 1, 'es-mx':1, et: 1, eu: 1, fa: 1, fi: 1, fo: 1,
22 'fr-ca': 1, fr: 1, gl: 1, gu: 1, he: 1, hi: 1, hr: 1, hu: 1, id: 1, is: 1, it: 1, ja: 1, ka: 1, 22 'fr-ca': 1, fr: 1, gl: 1, gu: 1, he: 1, hi: 1, hr: 1, hu: 1, id: 1, is: 1, it: 1, ja: 1, ka: 1,
23 km: 1, ko: 1, ku: 1, lt: 1, lv: 1, mk: 1, mn: 1, ms: 1, nb: 1, nl: 1, no: 1, oc: 1, pl: 1, 'pt-br': 1, 23 km: 1, ko: 1, ku: 1, lt: 1, lv: 1, mk: 1, mn: 1, ms: 1, nb: 1, nl: 1, no: 1, oc: 1, pl: 1, 'pt-br': 1,
24 pt: 1, ro: 1, ru: 1, si: 1, sk: 1, sl: 1, sq: 1, 'sr-latn': 1, sr: 1, sv: 1, th: 1, tr: 1, tt: 1, ug: 1, 24 pt: 1, ro: 1, ru: 1, si: 1, sk: 1, sl: 1, sq: 1, 'sr-latn': 1, sr: 1, sv: 1, th: 1, tr: 1, tt: 1, ug: 1,
diff --git a/sources/core/loader.js b/sources/core/loader.js
index 249a8cf..dc02511 100644
--- a/sources/core/loader.js
+++ b/sources/core/loader.js
@@ -142,7 +142,7 @@ if ( !CKEDITOR.loader ) {
142 } 142 }
143 143
144 // We must guarantee the execution order of the scripts, so we 144 // We must guarantee the execution order of the scripts, so we
145 // need to load them one by one. (#4145) 145 // need to load them one by one. (http://dev.ckeditor.com/ticket/4145)
146 // The following if/else block has been taken from the scriptloader core code. 146 // The following if/else block has been taken from the scriptloader core code.
147 if ( typeof script.onreadystatechange !== 'undefined' ) { 147 if ( typeof script.onreadystatechange !== 'undefined' ) {
148 /** @ignore */ 148 /** @ignore */
@@ -156,7 +156,7 @@ if ( !CKEDITOR.loader ) {
156 /** @ignore */ 156 /** @ignore */
157 script.onload = function() { 157 script.onload = function() {
158 // Some browsers, such as Safari, may call the onLoad function 158 // Some browsers, such as Safari, may call the onLoad function
159 // immediately. Which will break the loading sequence. (#3661) 159 // immediately. Which will break the loading sequence. (http://dev.ckeditor.com/ticket/3661)
160 setTimeout( function() { 160 setTimeout( function() {
161 onScriptLoaded( scriptName ); 161 onScriptLoaded( scriptName );
162 }, 0 ); 162 }, 0 );
diff --git a/sources/core/scriptloader.js b/sources/core/scriptloader.js
index a2b9cca..356996f 100644
--- a/sources/core/scriptloader.js
+++ b/sources/core/scriptloader.js
@@ -119,7 +119,7 @@ CKEDITOR.scriptLoader = ( function() {
119 } ); 119 } );
120 120
121 if ( callback ) { 121 if ( callback ) {
122 // The onload or onerror event does not fire in IE8 and IE9 Quirks Mode (#14849). 122 // The onload or onerror event does not fire in IE8 and IE9 Quirks Mode (http://dev.ckeditor.com/ticket/14849).
123 if ( CKEDITOR.env.ie && ( CKEDITOR.env.version <= 8 || CKEDITOR.env.ie9Compat ) ) { 123 if ( CKEDITOR.env.ie && ( CKEDITOR.env.version <= 8 || CKEDITOR.env.ie9Compat ) ) {
124 script.$.onreadystatechange = function() { 124 script.$.onreadystatechange = function() {
125 if ( script.$.readyState == 'loaded' || script.$.readyState == 'complete' ) { 125 if ( script.$.readyState == 'loaded' || script.$.readyState == 'complete' ) {
@@ -130,7 +130,7 @@ CKEDITOR.scriptLoader = ( function() {
130 } else { 130 } else {
131 script.$.onload = function() { 131 script.$.onload = function() {
132 // Some browsers, such as Safari, may call the onLoad function 132 // Some browsers, such as Safari, may call the onLoad function
133 // immediately. Which will break the loading sequence. (#3661) 133 // immediately. Which will break the loading sequence. (http://dev.ckeditor.com/ticket/3661)
134 setTimeout( function() { 134 setTimeout( function() {
135 onLoad( url, true ); 135 onLoad( url, true );
136 }, 0 ); 136 }, 0 );
diff --git a/sources/core/selection.js b/sources/core/selection.js
index eef28a3..d44db3b 100644
--- a/sources/core/selection.js
+++ b/sources/core/selection.js
@@ -6,9 +6,211 @@
6( function() { 6( function() {
7 var isMSSelection = typeof window.getSelection != 'function', 7 var isMSSelection = typeof window.getSelection != 'function',
8 nextRev = 1, 8 nextRev = 1,
9 // #13816 9 // http://dev.ckeditor.com/ticket/13816
10 fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ), 10 fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ),
11 fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' ); 11 fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' ),
12 isSelectingTable;
13
14 // #### table selection : START
15 // @param {CKEDITOR.dom.range[]} ranges
16 // @param {Boolean} allowPartially Whether a collapsed selection within table is recognized to be a valid selection.
17 // This happens for WebKits on MacOS, when you right click inside the table.
18 function isTableSelection( ranges, allowPartially ) {
19 if ( ranges.length === 0 ) {
20 return false;
21 }
22
23 var node,
24 i;
25
26 function isPartiallySelected( range ) {
27 var startCell = range.startContainer.getAscendant( { td: 1, th: 1 }, true ),
28 endCell = range.endContainer.getAscendant( { td: 1, th: 1 }, true ),
29 trim = CKEDITOR.tools.trim,
30 selected;
31
32 // Check if the selection is inside one cell and we don't have any nested table contents selected.
33 if ( !startCell || !startCell.equals( endCell ) || startCell.findOne( 'td, th, tr, tbody, table' ) ) {
34 return false;
35 }
36
37 selected = range.cloneContents();
38
39 // Empty selection is still partially selected.
40 if ( !selected.getFirst() ) {
41 return true;
42 }
43
44 return trim( selected.getFirst().getText() ) !== trim( startCell.getText() );
45 }
46
47 // Edge case: partially selected text node inside one table cell or cursor inside cell.
48 if ( !allowPartially && ranges.length === 1 &&
49 ( ranges[ 0 ].collapsed || isPartiallySelected( ranges[ 0 ] ) ) ) {
50 return false;
51 }
52
53 for ( i = 0; i < ranges.length; i++ ) {
54 node = ranges[ i ]._getTableElement();
55
56 if ( !node ) {
57 return false;
58 }
59 }
60
61 return true;
62 }
63
64 // After performing fake table selection, the real selection is limited
65 // to the first selected cell. Therefore to check if the real selection
66 // matches the fake selection, we check if the table cell from fake selection's
67 // first range and real selection's range are the same.
68 // Also if the selection is collapsed, we should check if it's placed inside the table
69 // in which the fake selection is or inside nested table. Such selection occurs after right mouse click.
70 function isRealTableSelection( selection, fakeSelection ) {
71 var ranges = selection.getRanges(),
72 fakeRanges = fakeSelection.getRanges(),
73 table = ranges.length && ranges[ 0 ]._getTableElement() &&
74 ranges[ 0 ]._getTableElement().getAscendant( 'table', true ),
75 fakeTable = fakeRanges.length && fakeRanges[ 0 ]._getTableElement() &&
76 fakeRanges[ 0 ]._getTableElement().getAscendant( 'table', true ),
77 isTableRange = ranges.length === 1 && ranges[ 0 ]._getTableElement() &&
78 ranges[ 0 ]._getTableElement().is( 'table' ),
79 isFakeTableRange = fakeRanges.length === 1 && fakeRanges[ 0 ]._getTableElement() &&
80 fakeRanges[ 0 ]._getTableElement().is( 'table' );
81
82 function isValidTableSelection( table, fakeTable, ranges, fakeRanges ) {
83 var isMenuOpen = ranges.length === 1 && ranges[ 0 ].collapsed,
84 // In case of WebKit on MacOS, when checking real selection, we must allow selection to be partial.
85 // Otherwise the check will fail for table selection with opened context menu.
86 isInTable = isTableSelection( ranges, !!CKEDITOR.env.webkit ) && isTableSelection( fakeRanges );
87
88 return isSameTable( table, fakeTable ) && ( isMenuOpen || isInTable );
89 }
90
91 function isSameTable( table, fakeTable ) {
92 if ( !table || !fakeTable ) {
93 return false;
94 }
95
96 return table.equals( fakeTable ) || fakeTable.contains( table );
97 }
98
99 if ( isValidTableSelection( table, fakeTable, ranges, fakeRanges ) ) {
100 // Edge case: when editor contains only table and that table is selected using selectAll command,
101 // then the selection is not properly refreshed and it must be done manually.
102 if ( isTableRange && !isFakeTableRange ) {
103 fakeSelection.selectRanges( ranges );
104 }
105 return true;
106 }
107
108 return false;
109 }
110
111 function getSelectedCells( ranges ) {
112 var cells = [],
113 node,
114 i;
115
116 function getCellsFromElement( element ) {
117 var cells = element.find( 'td, th' ),
118 cellsArray = [],
119 i;
120
121 for ( i = 0; i < cells.count(); i++ ) {
122 cellsArray.push( cells.getItem( i ) );
123 }
124
125 return cellsArray;
126 }
127
128 for ( i = 0; i < ranges.length; i++ ) {
129 node = ranges[ i ]._getTableElement();
130
131 if ( node.is && node.is( { td: 1, th: 1 } ) ) {
132 cells.push( node );
133 } else {
134 cells = cells.concat( getCellsFromElement( node ) );
135 }
136 }
137
138 return cells;
139 }
140
141 // Cells in the same row are separated by tab and the rows are separated by new line, e.g.
142 // Cell 1.1 Cell 1.2
143 // Cell 2.1 Cell 2.2
144 function getTextFromSelectedCells( ranges ) {
145 var cells = getSelectedCells( ranges ),
146 txt = '',
147 currentRow = [],
148 lastRow,
149 i;
150
151 for ( i = 0; i < cells.length; i++ ) {
152 if ( lastRow && !lastRow.equals( cells[ i ].getAscendant( 'tr' ) ) ) {
153 txt += currentRow.join( '\t' ) + '\n';
154 lastRow = cells[ i ].getAscendant( 'tr' );
155 currentRow = [];
156 } else if ( i === 0 ) {
157 lastRow = cells[ i ].getAscendant( 'tr' );
158 }
159
160 currentRow.push( cells[ i ].getText() );
161 }
162
163 txt += currentRow.join( '\t' );
164
165 return txt;
166 }
167
168 function performFakeTableSelection( ranges ) {
169 var editor = this.root.editor,
170 realSelection = editor.getSelection( 1 ),
171 cache;
172
173 // Cleanup after previous selection - e.g. remove hidden sel container.
174 this.reset();
175
176 // Indicate that the table is being fake-selected to prevent infinite loop
177 // inside `selectRanges`.
178 isSelectingTable = true;
179
180 // Cancel selectionchange for the real selection.
181 realSelection.root.once( 'selectionchange', function( evt ) {
182 evt.cancel();
183 }, null, null, 0 );
184
185 // Move real selection to the first selected range.
186 realSelection.selectRanges( [ ranges[ 0 ] ] );
187
188 cache = this._.cache;
189
190 // Caches given ranges.
191 cache.ranges = new CKEDITOR.dom.rangeList( ranges );
192 cache.type = CKEDITOR.SELECTION_TEXT;
193 cache.selectedElement = ranges[ 0 ]._getTableElement();
194
195 // `selectedText` should contain text from all selected data ("plain text table")
196 // to be compatible with Firefox's implementation.
197 cache.selectedText = getTextFromSelectedCells( ranges );
198
199 // Properties that will not be available when isFake.
200 cache.nativeSel = null;
201
202 this.isFake = 1;
203 this.rev = nextRev++;
204
205 // Save this selection, so it can be returned by editor.getSelection().
206 editor._.fakeSelection = this;
207
208 isSelectingTable = false;
209
210 // Fire selectionchange, just like a normal selection.
211 this.root.fire( 'selectionchange' );
212 }
213 // #### table selection : END
12 214
13 // #### checkSelectionChange : START 215 // #### checkSelectionChange : START
14 216
@@ -22,10 +224,9 @@
22 224
23 if ( sel ) { 225 if ( sel ) {
24 realSel = this.getSelection( 1 ); 226 realSel = this.getSelection( 1 );
25 227 // If real (not locked/stored) selection was moved from hidden container
26 // If real (not locked/stored) selection was moved from hidden container, 228 // or is not a table one, then the fake-selection must be invalidated.
27 // then the fake-selection must be invalidated. 229 if ( !realSel || ( !realSel.isHidden() && !isRealTableSelection( realSel, sel ) ) ) {
28 if ( !realSel || !realSel.isHidden() ) {
29 // Remove the cache from fake-selection references in use elsewhere. 230 // Remove the cache from fake-selection references in use elsewhere.
30 sel.reset(); 231 sel.reset();
31 232
@@ -47,7 +248,7 @@
47 248
48 var currentPath = this.elementPath(); 249 var currentPath = this.elementPath();
49 if ( !currentPath.compare( this._.selectionPreviousPath ) ) { 250 if ( !currentPath.compare( this._.selectionPreviousPath ) ) {
50 // Handle case when dialog inserts new element but parent block and path (so also focus context) does not change. (#13362) 251 // Handle case when dialog inserts new element but parent block and path (so also focus context) does not change. (http://dev.ckeditor.com/ticket/13362)
51 var sameBlockParent = this._.selectionPreviousPath && this._.selectionPreviousPath.blockLimit.equals( currentPath.blockLimit ); 252 var sameBlockParent = this._.selectionPreviousPath && this._.selectionPreviousPath.blockLimit.equals( currentPath.blockLimit );
52 // Cache the active element, which we'll eventually lose on Webkit. 253 // Cache the active element, which we'll eventually lose on Webkit.
53 if ( CKEDITOR.env.webkit && !sameBlockParent ) 254 if ( CKEDITOR.env.webkit && !sameBlockParent )
@@ -97,7 +298,7 @@
97 // * is a visible node, 298 // * is a visible node,
98 // * is a non-empty element (this rule will accept elements like <strong></strong> because they 299 // * is a non-empty element (this rule will accept elements like <strong></strong> because they
99 // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret). 300 // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret).
100 // See #12621. 301 // See http://dev.ckeditor.com/ticket/12621.
101 function mayAbsorbCaret( node ) { 302 function mayAbsorbCaret( node ) {
102 if ( isVisible( node ) ) 303 if ( isVisible( node ) )
103 return true; 304 return true;
@@ -138,8 +339,8 @@
138 if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) ) 339 if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) )
139 return true; 340 return true;
140 341
141 // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222) 342 // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (http://dev.ckeditor.com/ticket/7222)
142 // If you found this line confusing check #12655. 343 // If you found this line confusing check http://dev.ckeditor.com/ticket/12655.
143 if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) ) 344 if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) )
144 return true; 345 return true;
145 346
@@ -155,7 +356,7 @@
155 return fillingChar; 356 return fillingChar;
156 } 357 }
157 358
158 // Checks if a filling char has been used, eventualy removing it (#1272). 359 // Checks if a filling char has been used, eventually removing it (http://dev.ckeditor.com/ticket/1272).
159 function checkFillingCharSequenceNodeReady( editable ) { 360 function checkFillingCharSequenceNodeReady( editable ) {
160 var fillingChar = editable.getCustomData( 'cke-fillingChar' ); 361 var fillingChar = editable.getCustomData( 'cke-fillingChar' );
161 362
@@ -164,6 +365,7 @@
164 // creating it. 365 // creating it.
165 if ( fillingChar.getCustomData( 'ready' ) ) { 366 if ( fillingChar.getCustomData( 'ready' ) ) {
166 removeFillingCharSequenceNode( editable ); 367 removeFillingCharSequenceNode( editable );
368 editable.editor.fire( 'selectionCheck' );
167 } else { 369 } else {
168 fillingChar.setCustomData( 'ready', 1 ); 370 fillingChar.setCustomData( 'ready', 1 );
169 } 371 }
@@ -175,7 +377,7 @@
175 377
176 if ( fillingChar ) { 378 if ( fillingChar ) {
177 // Text selection position might get mangled by 379 // Text selection position might get mangled by
178 // subsequent dom modification, save it now for restoring. (#8617) 380 // subsequent dom modification, save it now for restoring. (http://dev.ckeditor.com/ticket/8617)
179 if ( keepSelection !== false ) { 381 if ( keepSelection !== false ) {
180 var sel = editable.getDocument().getSelection().getNative(), 382 var sel = editable.getDocument().getSelection().getNative(),
181 // Be error proof. 383 // Be error proof.
@@ -211,11 +413,11 @@
211 } 413 }
212 } 414 }
213 415
214 // #13816 416 // http://dev.ckeditor.com/ticket/13816
215 function removeFillingCharSequenceString( str, nbspAware ) { 417 function removeFillingCharSequenceString( str, nbspAware ) {
216 if ( nbspAware ) { 418 if ( nbspAware ) {
217 return str.replace( fillingCharSequenceRegExp, function( m, p ) { 419 return str.replace( fillingCharSequenceRegExp, function( m, p ) {
218 // #10291 if filling char is followed by a space replace it with NBSP. 420 // http://dev.ckeditor.com/ticket/10291 if filling char is followed by a space replace it with NBSP.
219 return p ? '\xa0' : ''; 421 return p ? '\xa0' : '';
220 } ); 422 } );
221 } else { 423 } else {
@@ -391,7 +593,7 @@
391 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) { 593 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) {
392 // So far we can't say that enclosed element is non-editable. Before checking, 594 // So far we can't say that enclosed element is non-editable. Before checking,
393 // we'll shrink range (clone). Shrinking will stop on non-editable range, or 595 // we'll shrink range (clone). Shrinking will stop on non-editable range, or
394 // innermost element (#11114). 596 // innermost element (http://dev.ckeditor.com/ticket/11114).
395 clone = range.clone(); 597 clone = range.clone();
396 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true ); 598 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true );
397 599
@@ -525,7 +727,7 @@
525 727
526 // Give the editable an initial selection on first focus, 728 // Give the editable an initial selection on first focus,
527 // put selection at a consistent position at the start 729 // put selection at a consistent position at the start
528 // of the contents. (#9507) 730 // of the contents. (http://dev.ckeditor.com/ticket/9507)
529 if ( CKEDITOR.env.gecko ) { 731 if ( CKEDITOR.env.gecko ) {
530 editable.attachListener( editable, 'focus', function( evt ) { 732 editable.attachListener( editable, 'focus', function( evt ) {
531 evt.removeListener(); 733 evt.removeListener();
@@ -533,7 +735,7 @@
533 if ( restoreSel !== 0 ) { 735 if ( restoreSel !== 0 ) {
534 var nativ = editor.getSelection().getNative(); 736 var nativ = editor.getSelection().getNative();
535 // Do it only if the native selection is at an unwanted 737 // Do it only if the native selection is at an unwanted
536 // place (at the very start of the editable). #10119 738 // place (at the very start of the editable). http://dev.ckeditor.com/ticket/10119
537 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) { 739 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) {
538 var rng = editor.createRange(); 740 var rng = editor.createRange();
539 rng.moveToElementEditStart( editable ); 741 rng.moveToElementEditStart( editable );
@@ -553,7 +755,7 @@
553 755
554 // On Webkit when editor uses divarea, native focus causes editable viewport to scroll 756 // On Webkit when editor uses divarea, native focus causes editable viewport to scroll
555 // to the top (when there is no active selection inside while focusing) so the scroll 757 // to the top (when there is no active selection inside while focusing) so the scroll
556 // position should be restored after focusing back editable area. (#14659) 758 // position should be restored after focusing back editable area. (http://dev.ckeditor.com/ticket/14659)
557 if ( restoreSel && editor._.previousScrollTop != null && editor._.previousScrollTop != editable.$.scrollTop ) { 759 if ( restoreSel && editor._.previousScrollTop != null && editor._.previousScrollTop != editable.$.scrollTop ) {
558 editable.$.scrollTop = editor._.previousScrollTop; 760 editable.$.scrollTop = editor._.previousScrollTop;
559 } 761 }
@@ -605,7 +807,7 @@
605 editable.attachListener( editable, 'mousedown', function( evt ) { 807 editable.attachListener( editable, 'mousedown', function( evt ) {
606 // IE scrolls document to top on right mousedown 808 // IE scrolls document to top on right mousedown
607 // when editor has no focus, remember this scroll 809 // when editor has no focus, remember this scroll
608 // position and revert it before context menu opens. (#5778) 810 // position and revert it before context menu opens. (http://dev.ckeditor.com/ticket/5778)
609 if ( evt.data.$.button == 2 ) { 811 if ( evt.data.$.button == 2 ) {
610 var sel = editor.document.getSelection(); 812 var sel = editor.document.getSelection();
611 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE ) 813 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
@@ -624,7 +826,7 @@
624 826
625 // When content doc is in standards mode, IE doesn't focus the editor when 827 // When content doc is in standards mode, IE doesn't focus the editor when
626 // clicking at the region below body (on html element) content, we emulate 828 // clicking at the region below body (on html element) content, we emulate
627 // the normal behavior on old IEs. (#1659, #7932) 829 // the normal behavior on old IEs. (http://dev.ckeditor.com/ticket/1659, http://dev.ckeditor.com/ticket/7932)
628 if ( doc.$.compatMode != 'BackCompat' ) { 830 if ( doc.$.compatMode != 'BackCompat' ) {
629 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) { 831 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
630 var textRng, 832 var textRng,
@@ -661,7 +863,7 @@
661 html.removeListener( 'mousemove', onHover ); 863 html.removeListener( 'mousemove', onHover );
662 removeListeners(); 864 removeListeners();
663 865
664 // Make it in effect on mouse up. (#9022) 866 // Make it in effect on mouse up. (http://dev.ckeditor.com/ticket/9022)
665 textRng.select(); 867 textRng.select();
666 } 868 }
667 869
@@ -690,7 +892,7 @@
690 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) { 892 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) {
691 html.on( 'mousedown', function( evt ) { 893 html.on( 'mousedown', function( evt ) {
692 if ( evt.data.getTarget().is( 'html' ) ) { 894 if ( evt.data.getTarget().is( 'html' ) ) {
693 // Limit the text selection mouse move inside of editable. (#9715) 895 // Limit the text selection mouse move inside of editable. (http://dev.ckeditor.com/ticket/9715)
694 outerDoc.on( 'mouseup', onSelectEnd ); 896 outerDoc.on( 'mouseup', onSelectEnd );
695 html.on( 'mouseup', onSelectEnd ); 897 html.on( 'mouseup', onSelectEnd );
696 } 898 }
@@ -704,6 +906,14 @@
704 // 2. After the accomplish of keyboard and mouse events. 906 // 2. After the accomplish of keyboard and mouse events.
705 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor ); 907 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor );
706 editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor ); 908 editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor );
909 // http://dev.ckeditor.com/ticket/14407 - Don't even let anything happen if the selection is in a non-editable element.
910 editable.attachListener( editable, 'keydown', function( evt ) {
911 var sel = this.getSelection( 1 );
912 if ( nonEditableAscendant( sel ) ) {
913 sel.selectElement( nonEditableAscendant( sel ) );
914 evt.data.preventDefault();
915 }
916 }, editor );
707 // Always fire the selection change on focus gain. 917 // Always fire the selection change on focus gain.
708 // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and 918 // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
709 // we need synchronization between those listeners to not lost cached editor._.previousActive property 919 // we need synchronization between those listeners to not lost cached editor._.previousActive property
@@ -713,7 +923,7 @@
713 editor.selectionChange( 1 ); 923 editor.selectionChange( 1 );
714 } ); 924 } );
715 925
716 // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed 926 // http://dev.ckeditor.com/ticket/9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
717 // by dragging and releasing mouse button outside editable. Dragging (mousedown) 927 // by dragging and releasing mouse button outside editable. Dragging (mousedown)
718 // has to be initialized in editable, but for mouseup we listen on document element. 928 // has to be initialized in editable, but for mouseup we listen on document element.
719 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) { 929 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) {
@@ -727,11 +937,11 @@
727 mouseDown = 0; 937 mouseDown = 0;
728 } ); 938 } );
729 } 939 }
730 // In all other cases listen on simple mouseup over editable, as we did before #9699. 940 // In all other cases listen on simple mouseup over editable, as we did before http://dev.ckeditor.com/ticket/9699.
731 // 941 //
732 // Use document instead of editable in non-IEs for observing mouseup 942 // Use document instead of editable in non-IEs for observing mouseup
733 // since editable won't fire the event if selection process started within iframe and ended out 943 // since editable won't fire the event if selection process started within iframe and ended out
734 // of the editor (#9851). 944 // of the editor (http://dev.ckeditor.com/ticket/9851).
735 else { 945 else {
736 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor ); 946 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor );
737 } 947 }
@@ -753,18 +963,18 @@
753 case 8: // BACKSPACE 963 case 8: // BACKSPACE
754 case 45: // INS 964 case 45: // INS
755 case 46: // DEl 965 case 46: // DEl
756 removeFillingCharSequenceNode( editable ); 966 if ( editable.hasFocus ) {
967 removeFillingCharSequenceNode( editable );
968 }
757 } 969 }
758 970
759 }, null, null, -1 ); 971 }, null, null, -1 );
760 } 972 }
761 973
762 // Automatically select non-editable element when navigating into
763 // it by left/right or backspace/del keys.
764 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 ); 974 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
765 975
766 function moveRangeToPoint( range, x, y ) { 976 function moveRangeToPoint( range, x, y ) {
767 // Error prune in IE7. (#9034, #9110) 977 // Error prune in IE7. (http://dev.ckeditor.com/ticket/9034, http://dev.ckeditor.com/ticket/9110)
768 try { 978 try {
769 range.moveToPoint( x, y ); 979 range.moveToPoint( x, y );
770 } catch ( e ) {} 980 } catch ( e ) {}
@@ -785,14 +995,27 @@
785 range = sel.createRange(); 995 range = sel.createRange();
786 996
787 // The selection range is reported on host, but actually it should applies to the content doc. 997 // The selection range is reported on host, but actually it should applies to the content doc.
788 if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ ) 998 // The parentElement may be null for read only mode in IE10 and below (http://dev.ckeditor.com/ticket/9780).
999 if ( sel.type != 'None' && range.parentElement() && range.parentElement().ownerDocument == doc.$ )
789 range.select(); 1000 range.select();
790 } 1001 }
1002
1003 function nonEditableAscendant( sel ) {
1004 if ( CKEDITOR.env.ie ) {
1005 var range = sel.getRanges()[ 0 ],
1006 ascendant = range ? range.startContainer.getAscendant( function( parent ) {
1007 return parent.type == CKEDITOR.NODE_ELEMENT &&
1008 ( parent.getAttribute( 'contenteditable' ) == 'false' || parent.getAttribute( 'contenteditable' ) == 'true' );
1009 }, true ) : null ;
1010
1011 return range && ascendant.getAttribute( 'contenteditable' ) == 'false' && ascendant;
1012 }
1013 }
791 } ); 1014 } );
792 1015
793 editor.on( 'setData', function() { 1016 editor.on( 'setData', function() {
794 // Invalidate locked selection when unloading DOM. 1017 // Invalidate locked selection when unloading DOM.
795 // (#9521, #5217#comment:32 and #11500#comment:11) 1018 // (http://dev.ckeditor.com/ticket/9521, http://dev.ckeditor.com/ticket/5217#comment:32 and http://dev.ckeditor.com/ticket/11500#comment:11)
796 editor.unlockSelection(); 1019 editor.unlockSelection();
797 1020
798 // Webkit's selection will mess up after the data loading. 1021 // Webkit's selection will mess up after the data loading.
@@ -806,7 +1029,7 @@
806 editor.unlockSelection(); 1029 editor.unlockSelection();
807 } ); 1030 } );
808 1031
809 // IE9 might cease to work if there's an object selection inside the iframe (#7639). 1032 // IE9 might cease to work if there's an object selection inside the iframe (http://dev.ckeditor.com/ticket/7639).
810 if ( CKEDITOR.env.ie9Compat ) 1033 if ( CKEDITOR.env.ie9Compat )
811 editor.on( 'beforeDestroy', clearSelection, null, null, 9 ); 1034 editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
812 1035
@@ -822,7 +1045,7 @@
822 // When loaded data are ready check whether hidden selection container was not loaded. 1045 // When loaded data are ready check whether hidden selection container was not loaded.
823 editor.on( 'loadSnapshot', function() { 1046 editor.on( 'loadSnapshot', function() {
824 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ), 1047 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ),
825 // TODO replace with el.find() which will be introduced in #9764, 1048 // TODO replace with el.find() which will be introduced in http://dev.ckeditor.com/ticket/9764,
826 // because it may happen that hidden sel container won't be the last element. 1049 // because it may happen that hidden sel container won't be the last element.
827 last = editor.editable().getLast( isElement ); 1050 last = editor.editable().getLast( isElement );
828 1051
@@ -869,7 +1092,7 @@
869 } ); 1092 } );
870 1093
871 // On WebKit only, we need a special "filling" char on some situations 1094 // On WebKit only, we need a special "filling" char on some situations
872 // (#1272). Here we set the events that should invalidate that char. 1095 // (http://dev.ckeditor.com/ticket/1272). Here we set the events that should invalidate that char.
873 if ( CKEDITOR.env.webkit ) { 1096 if ( CKEDITOR.env.webkit ) {
874 CKEDITOR.on( 'instanceReady', function( evt ) { 1097 CKEDITOR.on( 'instanceReady', function( evt ) {
875 var editor = evt.editor; 1098 var editor = evt.editor;
@@ -884,7 +1107,7 @@
884 1107
885 // Filter Undo snapshot's HTML to get rid of Filling Char Sequence. 1108 // Filter Undo snapshot's HTML to get rid of Filling Char Sequence.
886 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's 1109 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
887 // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (#13816). 1110 // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (http://dev.ckeditor.com/ticket/13816).
888 editor.on( 'getSnapshot', function( evt ) { 1111 editor.on( 'getSnapshot', function( evt ) {
889 if ( evt.data ) { 1112 if ( evt.data ) {
890 evt.data = removeFillingCharSequenceString( evt.data ); 1113 evt.data = removeFillingCharSequenceString( evt.data );
@@ -893,7 +1116,7 @@
893 1116
894 // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat 1117 // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat
895 // instead of #getData because once removed, FCSeq may leave an empty element, 1118 // instead of #getData because once removed, FCSeq may leave an empty element,
896 // which should be pruned by the dataProcessor (#13816). 1119 // which should be pruned by the dataProcessor (http://dev.ckeditor.com/ticket/13816).
897 // Note: Used low priority to filter when dataProcessor works on strings, 1120 // Note: Used low priority to filter when dataProcessor works on strings,
898 // not pseudo–DOM. 1121 // not pseudo–DOM.
899 editor.on( 'toDataFormat', function( evt ) { 1122 editor.on( 'toDataFormat', function( evt ) {
@@ -1138,7 +1361,7 @@
1138 1361
1139 // Selection out of concerned range, empty the selection. 1362 // Selection out of concerned range, empty the selection.
1140 // TODO check whether this condition cannot be reverted to its old 1363 // TODO check whether this condition cannot be reverted to its old
1141 // form (commented out) after we closed #10438. 1364 // form (commented out) after we closed http://dev.ckeditor.com/ticket/10438.
1142 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) { 1365 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
1143 if ( !( 1366 if ( !(
1144 rangeParent && 1367 rangeParent &&
@@ -1180,7 +1403,7 @@
1180 * 1403 *
1181 * var selection = editor.getSelection().getNative(); 1404 * var selection = editor.getSelection().getNative();
1182 * 1405 *
1183 * @returns {Object} The native browser selection object. 1406 * @returns {Object} The native browser selection object or null if this is a fake selection.
1184 */ 1407 */
1185 getNative: function() { 1408 getNative: function() {
1186 if ( this._.cache.nativeSel !== undefined ) 1409 if ( this._.cache.nativeSel !== undefined )
@@ -1272,7 +1495,7 @@
1272 * alert( ranges.length ); 1495 * alert( ranges.length );
1273 * 1496 *
1274 * @method 1497 * @method
1275 * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only. 1498 * @param {Boolean} [onlyEditables] If set to `true`, this function retrieves editable ranges only.
1276 * @returns {Array} Range instances that represent the current selection. 1499 * @returns {Array} Range instances that represent the current selection.
1277 */ 1500 */
1278 getRanges: ( function() { 1501 getRanges: ( function() {
@@ -1303,7 +1526,7 @@
1303 index = -1, 1526 index = -1,
1304 position, distance, container; 1527 position, distance, container;
1305 1528
1306 // Binary search over all element childs to test the range to see whether 1529 // Binary search over all element children to test the range to see whether
1307 // range is right on the boundary of one element. 1530 // range is right on the boundary of one element.
1308 while ( startIndex <= endIndex ) { 1531 while ( startIndex <= endIndex ) {
1309 index = Math.floor( ( startIndex + endIndex ) / 2 ); 1532 index = Math.floor( ( startIndex + endIndex ) / 2 );
@@ -1319,8 +1542,8 @@
1319 return { container: parent, offset: getNodeIndex( child ) }; 1542 return { container: parent, offset: getNodeIndex( child ) };
1320 } 1543 }
1321 1544
1322 // All childs are text nodes, 1545 // All children are text nodes,
1323 // or to the right hand of test range are all text nodes. (#6992) 1546 // or to the right hand of test range are all text nodes. (http://dev.ckeditor.com/ticket/6992)
1324 if ( index == -1 || index == siblings.length - 1 && position < 0 ) { 1547 if ( index == -1 || index == siblings.length - 1 && position < 0 ) {
1325 // Adapt test range to embrace the entire parent contents. 1548 // Adapt test range to embrace the entire parent contents.
1326 testRange.moveToElementText( parent ); 1549 testRange.moveToElementText( parent );
@@ -1328,7 +1551,7 @@
1328 1551
1329 // IE report line break as CRLF with range.text but 1552 // IE report line break as CRLF with range.text but
1330 // only LF with textnode.nodeValue, normalize them to avoid 1553 // only LF with textnode.nodeValue, normalize them to avoid
1331 // breaking character counting logic below. (#3949) 1554 // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
1332 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 1555 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1333 1556
1334 siblings = parent.childNodes; 1557 siblings = parent.childNodes;
@@ -1364,7 +1587,7 @@
1364 1587
1365 // IE report line break as CRLF with range.text but 1588 // IE report line break as CRLF with range.text but
1366 // only LF with textnode.nodeValue, normalize them to avoid 1589 // only LF with textnode.nodeValue, normalize them to avoid
1367 // breaking character counting logic below. (#3949) 1590 // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
1368 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 1591 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1369 1592
1370 // Actual range anchor right beside test range at the inner boundary of text node. 1593 // Actual range anchor right beside test range at the inner boundary of text node.
@@ -1381,7 +1604,7 @@
1381 } 1604 }
1382 child = sibling; 1605 child = sibling;
1383 } 1606 }
1384 // Measurement in IE could be somtimes wrong because of <select> element. (#4611) 1607 // Measurement in IE could be sometimes wrong because of <select> element. (http://dev.ckeditor.com/ticket/4611)
1385 catch ( e ) { 1608 catch ( e ) {
1386 return { container: parent, offset: getNodeIndex( child ) }; 1609 return { container: parent, offset: getNodeIndex( child ) };
1387 } 1610 }
@@ -1413,7 +1636,7 @@
1413 boundaryInfo = getBoundaryInformation( nativeRange ); 1636 boundaryInfo = getBoundaryInformation( nativeRange );
1414 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 1637 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1415 1638
1416 // Correct an invalid IE range case on empty list item. (#5850) 1639 // Correct an invalid IE range case on empty list item. (http://dev.ckeditor.com/ticket/5850)
1417 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() ) 1640 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() )
1418 range.collapse(); 1641 range.collapse();
1419 1642
@@ -1445,7 +1668,7 @@
1445 } )() : 1668 } )() :
1446 function() { 1669 function() {
1447 // On browsers implementing the W3C range, we simply 1670 // On browsers implementing the W3C range, we simply
1448 // tranform the native ranges in CKEDITOR.dom.range 1671 // transform the native ranges in CKEDITOR.dom.range
1449 // instances. 1672 // instances.
1450 1673
1451 var ranges = [], 1674 var ranges = [],
@@ -1478,7 +1701,7 @@
1478 return ranges; 1701 return ranges;
1479 1702
1480 // Split range into multiple by read-only nodes. 1703 // Split range into multiple by read-only nodes.
1481 // Clone ranges array to avoid changing cached ranges (#11493). 1704 // Clone ranges array to avoid changing cached ranges (http://dev.ckeditor.com/ticket/11493).
1482 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) ); 1705 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) );
1483 }; 1706 };
1484 } )(), 1707 } )(),
@@ -1512,11 +1735,11 @@
1512 1735
1513 // Decrease the range content to exclude particial 1736 // Decrease the range content to exclude particial
1514 // selected node on the start which doesn't have 1737 // selected node on the start which doesn't have
1515 // visual impact. ( #3231 ) 1738 // visual impact. ( http://dev.ckeditor.com/ticket/3231 )
1516 while ( 1 ) { 1739 while ( 1 ) {
1517 var startContainer = range.startContainer, 1740 var startContainer = range.startContainer,
1518 startOffset = range.startOffset; 1741 startOffset = range.startOffset;
1519 // Limit the fix only to non-block elements.(#3950) 1742 // Limit the fix only to non-block elements.(http://dev.ckeditor.com/ticket/3950)
1520 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() ) 1743 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() )
1521 range.setStartAfter( startContainer ); 1744 range.setStartAfter( startContainer );
1522 else 1745 else
@@ -1558,7 +1781,7 @@
1558 * var element = editor.getSelection().getSelectedElement(); 1781 * var element = editor.getSelection().getSelectedElement();
1559 * alert( element.getName() ); 1782 * alert( element.getName() );
1560 * 1783 *
1561 * @returns {CKEDITOR.dom.element} The selected element. Null if no 1784 * @returns {CKEDITOR.dom.element/null} The selected element. `null` if no
1562 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}. 1785 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
1563 */ 1786 */
1564 getSelectedElement: function() { 1787 getSelectedElement: function() {
@@ -1641,7 +1864,7 @@
1641 1864
1642 if ( restore ) { 1865 if ( restore ) {
1643 var selectedElement = this.getSelectedElement(), 1866 var selectedElement = this.getSelectedElement(),
1644 ranges = !selectedElement && this.getRanges(), 1867 ranges = this.getRanges(),
1645 faked = this.isFake; 1868 faked = this.isFake;
1646 } 1869 }
1647 1870
@@ -1655,7 +1878,10 @@
1655 if ( !( common && common.getAscendant( 'body', 1 ) ) ) 1878 if ( !( common && common.getAscendant( 'body', 1 ) ) )
1656 return; 1879 return;
1657 1880
1658 if ( faked ) 1881 if ( isTableSelection( ranges ) ) {
1882 // Tables have it's own selection method.
1883 performFakeTableSelection.call( this, ranges );
1884 } else if ( faked )
1659 this.fake( selectedElement ); 1885 this.fake( selectedElement );
1660 else if ( selectedElement ) 1886 else if ( selectedElement )
1661 this.selectElement( selectedElement ); 1887 this.selectElement( selectedElement );
@@ -1727,7 +1953,7 @@
1727 // Check if there's a hiddenSelectionContainer in editable at some index. 1953 // Check if there's a hiddenSelectionContainer in editable at some index.
1728 // Some ranges may be anchored after the hiddenSelectionContainer and, 1954 // Some ranges may be anchored after the hiddenSelectionContainer and,
1729 // once the container is removed while resetting the selection, they 1955 // once the container is removed while resetting the selection, they
1730 // may need new endOffset (one element less within the range) (#11021 #11393). 1956 // may need new endOffset (one element less within the range) (http://dev.ckeditor.com/ticket/11021 http://dev.ckeditor.com/ticket/11393).
1731 if ( hadHiddenSelectionContainer ) 1957 if ( hadHiddenSelectionContainer )
1732 fixRangesAfterHiddenSelectionContainer( ranges, this.root ); 1958 fixRangesAfterHiddenSelectionContainer( ranges, this.root );
1733 1959
@@ -1755,6 +1981,15 @@
1755 return; 1981 return;
1756 } 1982 }
1757 1983
1984 // Handle special case - fake selection of table cells.
1985 if ( editor && editor.plugins.tableselection &&
1986 CKEDITOR.plugins.tableselection.isSupportedEnvironment &&
1987 isTableSelection( ranges ) && !isSelectingTable
1988 ) {
1989 performFakeTableSelection.call( this, ranges );
1990 return;
1991 }
1992
1758 if ( isMSSelection ) { 1993 if ( isMSSelection ) {
1759 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), 1994 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
1760 fillerTextRegex = /\ufeff|\u00a0/, 1995 fillerTextRegex = /\ufeff|\u00a0/,
@@ -1788,7 +2023,7 @@
1788 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells || 2023 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells ||
1789 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) { 2024 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) {
1790 range.shrink( CKEDITOR.NODE_ELEMENT, true ); 2025 range.shrink( CKEDITOR.NODE_ELEMENT, true );
1791 // The range might get collapsed (#7975). Update cached variable. 2026 // The range might get collapsed (http://dev.ckeditor.com/ticket/7975). Update cached variable.
1792 collapsed = range.collapsed; 2027 collapsed = range.collapsed;
1793 } 2028 }
1794 2029
@@ -1830,18 +2065,18 @@
1830 2065
1831 // Append a temporary <span>&#65279;</span> before the selection. 2066 // Append a temporary <span>&#65279;</span> before the selection.
1832 // This is needed to avoid IE destroying selections inside empty 2067 // This is needed to avoid IE destroying selections inside empty
1833 // inline elements, like <b></b> (#253). 2068 // inline elements, like <b></b> (http://dev.ckeditor.com/ticket/253).
1834 // It is also needed when placing the selection right after an inline 2069 // It is also needed when placing the selection right after an inline
1835 // element to avoid the selection moving inside of it. 2070 // element to avoid the selection moving inside of it.
1836 dummySpan = range.document.createElement( 'span' ); 2071 dummySpan = range.document.createElement( 'span' );
1837 dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See #1359. 2072 dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See http://dev.ckeditor.com/ticket/1359.
1838 dummySpan.insertBefore( startNode ); 2073 dummySpan.insertBefore( startNode );
1839 2074
1840 if ( isStartMarkerAlone ) { 2075 if ( isStartMarkerAlone ) {
1841 // To expand empty blocks or line spaces after <br>, we need 2076 // To expand empty blocks or line spaces after <br>, we need
1842 // instead to have any char, which will be later deleted using the 2077 // instead to have any char, which will be later deleted using the
1843 // selection. 2078 // selection.
1844 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) 2079 // \ufeff = Zero Width No-Break Space (U+FEFF). (http://dev.ckeditor.com/ticket/1359)
1845 range.document.createText( '\ufeff' ).insertBefore( startNode ); 2080 range.document.createText( '\ufeff' ).insertBefore( startNode );
1846 } 2081 }
1847 } 2082 }
@@ -1873,7 +2108,7 @@
1873 } else { 2108 } else {
1874 var sel = this.getNative(); 2109 var sel = this.getNative();
1875 2110
1876 // getNative() returns null if iframe is "display:none" in FF. (#6577) 2111 // getNative() returns null if iframe is "display:none" in FF. (http://dev.ckeditor.com/ticket/6577)
1877 if ( !sel ) 2112 if ( !sel )
1878 return; 2113 return;
1879 2114
@@ -1889,7 +2124,7 @@
1889 between.setStart( left.endContainer, left.endOffset ); 2124 between.setStart( left.endContainer, left.endOffset );
1890 between.setEnd( right.startContainer, right.startOffset ); 2125 between.setEnd( right.startContainer, right.startOffset );
1891 2126
1892 // Don't confused by Firefox adjancent multi-ranges 2127 // Don't confused by Firefox adjacent multi-ranges
1893 // introduced by table cells selection. 2128 // introduced by table cells selection.
1894 if ( !between.collapsed ) { 2129 if ( !between.collapsed ) {
1895 between.shrink( CKEDITOR.NODE_ELEMENT, true ); 2130 between.shrink( CKEDITOR.NODE_ELEMENT, true );
@@ -1898,7 +2133,7 @@
1898 2133
1899 // The following cases has to be considered: 2134 // The following cases has to be considered:
1900 // 1. <span contenteditable="false">[placeholder]</span> 2135 // 1. <span contenteditable="false">[placeholder]</span>
1901 // 2. <input contenteditable="false" type="radio"/> (#6621) 2136 // 2. <input contenteditable="false" type="radio"/> (http://dev.ckeditor.com/ticket/6621)
1902 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) { 2137 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) {
1903 right.setStart( left.startContainer, left.startOffset ); 2138 right.setStart( left.startContainer, left.startOffset );
1904 ranges.splice( i--, 1 ); 2139 ranges.splice( i--, 1 );
@@ -1913,7 +2148,7 @@
1913 2148
1914 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) { 2149 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) {
1915 // Append a zero-width space so WebKit will not try to 2150 // Append a zero-width space so WebKit will not try to
1916 // move the selection by itself (#1272). 2151 // move the selection by itself (http://dev.ckeditor.com/ticket/1272).
1917 var fillingChar = createFillingCharSequenceNode( this.root ); 2152 var fillingChar = createFillingCharSequenceNode( this.root );
1918 range.insertNode( fillingChar ); 2153 range.insertNode( fillingChar );
1919 2154
@@ -1972,7 +2207,7 @@
1972 fake: function( element, ariaLabel ) { 2207 fake: function( element, ariaLabel ) {
1973 var editor = this.root.editor; 2208 var editor = this.root.editor;
1974 2209
1975 // Attempt to retreive aria-label if possible (#14539). 2210 // Attempt to retrieve aria-label if possible (http://dev.ckeditor.com/ticket/14539).
1976 if ( ariaLabel === undefined && element.hasAttribute( 'aria-label' ) ) { 2211 if ( ariaLabel === undefined && element.hasAttribute( 'aria-label' ) ) {
1977 ariaLabel = element.getAttribute( 'aria-label' ); 2212 ariaLabel = element.getAttribute( 'aria-label' );
1978 } 2213 }
@@ -2031,6 +2266,38 @@
2031 }, 2266 },
2032 2267
2033 /** 2268 /**
2269 * Checks if the selection contains an HTML element inside a table.
2270 * Returns `false` for text selection inside a table (e.g. it will return `false`
2271 * for text selected in one cell).
2272 *
2273 * editor.getSelection().isInTable();
2274 *
2275 * @since 4.7.0
2276 * @param {Boolean} [allowPartialSelection=false] Whether a partial cell selection should be included.
2277 * Added in 4.7.2.
2278 * @returns {Boolean}
2279 */
2280 isInTable: function( allowPartialSelection ) {
2281 return isTableSelection( this.getRanges(), allowPartialSelection );
2282 },
2283
2284 /**
2285 * Checks if the selection contains only one range which is collapsed.
2286 *
2287 * if ( editor.getSelection().isCollapsed() ) {
2288 * // Do something when the selection is collapsed.
2289 * }
2290 *
2291 * @since 4.7.3
2292 * @returns {Boolean}
2293 */
2294 isCollapsed: function() {
2295 var ranges = this.getRanges();
2296
2297 return ranges.length === 1 && ranges[ 0 ].collapsed;
2298 },
2299
2300 /**
2034 * Creates a bookmark for each range of this selection (from {@link #getRanges}) 2301 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2035 * by calling the {@link CKEDITOR.dom.range#createBookmark} method, 2302 * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
2036 * with extra care taken to avoid interference among those ranges. The arguments 2303 * with extra care taken to avoid interference among those ranges. The arguments
@@ -2083,17 +2350,19 @@
2083 2350
2084 // It may happen that the content change during loading, before selection is set so bookmark leads to text node. 2351 // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
2085 if ( bookmarks.isFake ) { 2352 if ( bookmarks.isFake ) {
2086 node = ranges[ 0 ].getEnclosedNode(); 2353 node = isTableSelection( ranges ) ? ranges[ 0 ]._getTableElement() : ranges[ 0 ].getEnclosedNode();
2354
2087 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) { 2355 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) {
2088 CKEDITOR.warn( 'selection-not-fake' ); 2356 CKEDITOR.warn( 'selection-not-fake' );
2089 bookmarks.isFake = 0; 2357 bookmarks.isFake = 0;
2090 } 2358 }
2091 } 2359 }
2092 2360
2093 if ( bookmarks.isFake ) 2361 if ( bookmarks.isFake && !isTableSelection( ranges ) ) {
2094 this.fake( node ); 2362 this.fake( node );
2095 else 2363 } else {
2096 this.selectRanges( ranges ); 2364 this.selectRanges( ranges );
2365 }
2097 2366
2098 return this; 2367 return this;
2099 }, 2368 },
@@ -2130,7 +2399,7 @@
2130 * Remove all the selection ranges from the document. 2399 * Remove all the selection ranges from the document.
2131 */ 2400 */
2132 removeAllRanges: function() { 2401 removeAllRanges: function() {
2133 // Don't clear selection outside this selection's root (#11500). 2402 // Don't clear selection outside this selection's root (http://dev.ckeditor.com/ticket/11500).
2134 if ( this.getType() == CKEDITOR.SELECTION_NONE ) 2403 if ( this.getType() == CKEDITOR.SELECTION_NONE )
2135 return; 2404 return;
2136 2405
diff --git a/sources/core/skin.js b/sources/core/skin.js
index 4f0ee7c..290157d 100644
--- a/sources/core/skin.js
+++ b/sources/core/skin.js
@@ -125,7 +125,7 @@
125 offset = overrideOffset || ( icon && icon.offset ); 125 offset = overrideOffset || ( icon && icon.offset );
126 bgsize = overrideBgsize || ( icon && icon.bgsize ) || '16px'; 126 bgsize = overrideBgsize || ( icon && icon.bgsize ) || '16px';
127 127
128 // If we use apostrophes in background-image, we must escape apostrophes in path (just to be sure). (#13361) 128 // If we use apostrophes in background-image, we must escape apostrophes in path (just to be sure). (http://dev.ckeditor.com/ticket/13361)
129 if ( path ) 129 if ( path )
130 path = path.replace( /'/g, '\\\'' ); 130 path = path.replace( /'/g, '\\\'' );
131 131
diff --git a/sources/core/style.js b/sources/core/style.js
index efb9d7f..b3cf0bc 100644
--- a/sources/core/style.js
+++ b/sources/core/style.js
@@ -235,7 +235,7 @@ CKEDITOR.STYLE_OBJECT = 3;
235 var initialEnterMode = this._.enterMode; 235 var initialEnterMode = this._.enterMode;
236 236
237 // Before CKEditor 4.4 style knew nothing about editor, so in order to provide enterMode 237 // Before CKEditor 4.4 style knew nothing about editor, so in order to provide enterMode
238 // which should be used developers were forced to hack the style object (see #10190). 238 // which should be used developers were forced to hack the style object (see http://dev.ckeditor.com/ticket/10190).
239 // Since CKEditor 4.4 style knows about editor (at least when it's being applied/removed), but we 239 // Since CKEditor 4.4 style knows about editor (at least when it's being applied/removed), but we
240 // use _.enterMode for backward compatibility with those hacks. 240 // use _.enterMode for backward compatibility with those hacks.
241 // Note: we should not change style's enter mode if it was already set. 241 // Note: we should not change style's enter mode if it was already set.
@@ -569,7 +569,7 @@ CKEDITOR.STYLE_OBJECT = 3;
569 var styleVal = stylesDef[ style ], 569 var styleVal = stylesDef[ style ],
570 text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' ); 570 text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' );
571 571
572 // Some browsers don't support 'inherit' property value, leave them intact. (#5242) 572 // Some browsers don't support 'inherit' property value, leave them intact. (http://dev.ckeditor.com/ticket/5242)
573 if ( styleVal == 'inherit' ) 573 if ( styleVal == 'inherit' )
574 specialStylesText += text; 574 specialStylesText += text;
575 else 575 else
@@ -1024,7 +1024,7 @@ CKEDITOR.STYLE_OBJECT = 3;
1024 if ( !CKEDITOR.env.ie ) 1024 if ( !CKEDITOR.env.ie )
1025 styleNode.$.normalize(); 1025 styleNode.$.normalize();
1026 } 1026 }
1027 // Style already inherit from parents, left just to clear up any internal overrides. (#5931) 1027 // Style already inherit from parents, left just to clear up any internal overrides. (http://dev.ckeditor.com/ticket/5931)
1028 else { 1028 else {
1029 styleNode = new CKEDITOR.dom.element( 'span' ); 1029 styleNode = new CKEDITOR.dom.element( 'span' );
1030 styleRange.extractContents().appendTo( styleNode ); 1030 styleRange.extractContents().appendTo( styleNode );
@@ -1042,7 +1042,7 @@ CKEDITOR.STYLE_OBJECT = 3;
1042 // Remove the bookmark nodes. 1042 // Remove the bookmark nodes.
1043 range.moveToBookmark( boundaryNodes ); 1043 range.moveToBookmark( boundaryNodes );
1044 1044
1045 // Minimize the result range to exclude empty text nodes. (#5374) 1045 // Minimize the result range to exclude empty text nodes. (http://dev.ckeditor.com/ticket/5374)
1046 range.shrink( CKEDITOR.SHRINK_TEXT ); 1046 range.shrink( CKEDITOR.SHRINK_TEXT );
1047 1047
1048 // Get inside the remaining element if range.shrink( TEXT ) has failed because of non-editable elements inside. 1048 // Get inside the remaining element if range.shrink( TEXT ) has failed because of non-editable elements inside.
@@ -1058,27 +1058,31 @@ CKEDITOR.STYLE_OBJECT = 3;
1058 range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 ); 1058 range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 );
1059 1059
1060 var bookmark = range.createBookmark(), 1060 var bookmark = range.createBookmark(),
1061 startNode = bookmark.startNode; 1061 startNode = bookmark.startNode,
1062 alwaysRemoveElement = this._.definition.alwaysRemoveElement;
1062 1063
1063 if ( range.collapsed ) { 1064 if ( range.collapsed ) {
1064 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent(), range.root ), 1065 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent(), range.root ),
1065 // The topmost element in elementspatch which we should jump out of. 1066 // The topmost element in elements path which we should jump out of.
1066 boundaryElement; 1067 boundaryElement;
1067 1068
1068
1069 for ( var i = 0, element; i < startPath.elements.length && ( element = startPath.elements[ i ] ); i++ ) { 1069 for ( var i = 0, element; i < startPath.elements.length && ( element = startPath.elements[ i ] ); i++ ) {
1070 // 1. If it's collaped inside text nodes, try to remove the style from the whole element. 1070 // 1. If it's collaped inside text nodes, try to remove the style from the whole element.
1071 // 1071 //
1072 // 2. Otherwise if it's collapsed on element boundaries, moving the selection 1072 // 2. Otherwise if it's collapsed on element boundaries, moving the selection
1073 // outside the styles instead of removing the whole tag, 1073 // outside the styles instead of removing the whole tag,
1074 // also make sure other inner styles were well preserverd.(#3309) 1074 // also make sure other inner styles were well preserved.(http://dev.ckeditor.com/ticket/3309)
1075 if ( element == startPath.block || element == startPath.blockLimit ) 1075 //
1076 // 3. Force removing the element even if it's an boundary element when alwaysRemoveElement is true.
1077 // Without it, the links won't be unlinked if the cursor is placed right before/after it. (http://dev.ckeditor.com/ticket/13062)
1078 if ( element == startPath.block || element == startPath.blockLimit ) {
1076 break; 1079 break;
1080 }
1077 1081
1078 if ( this.checkElementRemovable( element ) ) { 1082 if ( this.checkElementRemovable( element ) ) {
1079 var isStart; 1083 var isStart;
1080 1084
1081 if ( range.collapsed && ( range.checkBoundaryOfElement( element, CKEDITOR.END ) || ( isStart = range.checkBoundaryOfElement( element, CKEDITOR.START ) ) ) ) { 1085 if ( !alwaysRemoveElement && range.collapsed && ( range.checkBoundaryOfElement( element, CKEDITOR.END ) || ( isStart = range.checkBoundaryOfElement( element, CKEDITOR.START ) ) ) ) {
1082 boundaryElement = element; 1086 boundaryElement = element;
1083 boundaryElement.match = isStart ? 'start' : 'end'; 1087 boundaryElement.match = isStart ? 'start' : 'end';
1084 } else { 1088 } else {
@@ -1087,10 +1091,11 @@ CKEDITOR.STYLE_OBJECT = 3;
1087 // no difference that they're separate entities in the DOM tree. So, merge 1091 // no difference that they're separate entities in the DOM tree. So, merge
1088 // them before removal. 1092 // them before removal.
1089 element.mergeSiblings(); 1093 element.mergeSiblings();
1090 if ( element.is( this.element ) ) 1094 if ( element.is( this.element ) ) {
1091 removeFromElement.call( this, element ); 1095 removeFromElement.call( this, element );
1092 else 1096 } else {
1093 removeOverrides( element, getOverrides( this )[ element.getName() ] ); 1097 removeOverrides( element, getOverrides( this )[ element.getName() ] );
1098 }
1094 } 1099 }
1095 } 1100 }
1096 } 1101 }
@@ -1235,7 +1240,7 @@ CKEDITOR.STYLE_OBJECT = 3;
1235 } 1240 }
1236 1241
1237 function applyObjectStyle( range ) { 1242 function applyObjectStyle( range ) {
1238 // Selected or parent element. (#9651) 1243 // Selected or parent element. (http://dev.ckeditor.com/ticket/9651)
1239 var start = range.getEnclosedNode() || range.getCommonAncestor( false, true ), 1244 var start = range.getEnclosedNode() || range.getCommonAncestor( false, true ),
1240 element = new CKEDITOR.dom.elementPath( start, range.root ).contains( this.element, 1 ); 1245 element = new CKEDITOR.dom.elementPath( start, range.root ).contains( this.element, 1 );
1241 1246
@@ -1276,7 +1281,7 @@ CKEDITOR.STYLE_OBJECT = 3;
1276 var iterator = range.createIterator(); 1281 var iterator = range.createIterator();
1277 iterator.enforceRealBlocks = true; 1282 iterator.enforceRealBlocks = true;
1278 1283
1279 // make recognize <br /> tag as a separator in ENTER_BR mode (#5121) 1284 // make recognize <br /> tag as a separator in ENTER_BR mode (http://dev.ckeditor.com/ticket/5121)
1280 if ( this._.enterMode ) 1285 if ( this._.enterMode )
1281 iterator.enlargeBr = ( this._.enterMode != CKEDITOR.ENTER_BR ); 1286 iterator.enlargeBr = ( this._.enterMode != CKEDITOR.ENTER_BR );
1282 1287
@@ -1326,7 +1331,7 @@ CKEDITOR.STYLE_OBJECT = 3;
1326 1331
1327 // Replace the original block with new one, with special treatment 1332 // Replace the original block with new one, with special treatment
1328 // for <pre> blocks to make sure content format is well preserved, and merging/splitting adjacent 1333 // for <pre> blocks to make sure content format is well preserved, and merging/splitting adjacent
1329 // when necessary. (#3188) 1334 // when necessary. (http://dev.ckeditor.com/ticket/3188)
1330 function replaceBlock( block, newBlock ) { 1335 function replaceBlock( block, newBlock ) {
1331 // Block is to be removed, create a temp element to 1336 // Block is to be removed, create a temp element to
1332 // save contents. 1337 // save contents.
@@ -1502,11 +1507,11 @@ CKEDITOR.STYLE_OBJECT = 3;
1502 1507
1503 // Remove definition attributes/style from the elemnt. 1508 // Remove definition attributes/style from the elemnt.
1504 for ( var attName in attributes ) { 1509 for ( var attName in attributes ) {
1505 // The 'class' element value must match (#1318). 1510 // The 'class' element value must match (http://dev.ckeditor.com/ticket/1318).
1506 if ( ( attName == 'class' || this._.definition.fullMatch ) && element.getAttribute( attName ) != normalizeProperty( attName, attributes[ attName ] ) ) 1511 if ( ( attName == 'class' || this._.definition.fullMatch ) && element.getAttribute( attName ) != normalizeProperty( attName, attributes[ attName ] ) )
1507 continue; 1512 continue;
1508 1513
1509 // Do not touch data-* attributes (#11011) (#11258). 1514 // Do not touch data-* attributes (http://dev.ckeditor.com/ticket/11011) (http://dev.ckeditor.com/ticket/11258).
1510 if ( keepDataAttrs && attName.slice( 0, 5 ) == 'data-' ) 1515 if ( keepDataAttrs && attName.slice( 0, 5 ) == 'data-' )
1511 continue; 1516 continue;
1512 1517
@@ -1515,7 +1520,7 @@ CKEDITOR.STYLE_OBJECT = 3;
1515 } 1520 }
1516 1521
1517 for ( var styleName in styles ) { 1522 for ( var styleName in styles ) {
1518 // Full match style insist on having fully equivalence. (#5018) 1523 // Full match style insist on having fully equivalence. (http://dev.ckeditor.com/ticket/5018)
1519 if ( this._.definition.fullMatch && element.getStyle( styleName ) != normalizeProperty( styleName, styles[ styleName ], true ) ) 1524 if ( this._.definition.fullMatch && element.getStyle( styleName ) != normalizeProperty( styleName, styles[ styleName ], true ) )
1520 continue; 1525 continue;
1521 1526
@@ -1649,7 +1654,7 @@ CKEDITOR.STYLE_OBJECT = 3;
1649 // Create the element. 1654 // Create the element.
1650 el = new CKEDITOR.dom.element( elementName, targetDocument ); 1655 el = new CKEDITOR.dom.element( elementName, targetDocument );
1651 1656
1652 // #6226: attributes should be copied before the new ones are applied 1657 // http://dev.ckeditor.com/ticket/6226: attributes should be copied before the new ones are applied
1653 if ( element ) 1658 if ( element )
1654 element.copyAttributes( el ); 1659 element.copyAttributes( el );
1655 1660
@@ -1797,7 +1802,7 @@ CKEDITOR.STYLE_OBJECT = 3;
1797 // @returns {Boolean} 1802 // @returns {Boolean}
1798 function compareCssText( source, target ) { 1803 function compareCssText( source, target ) {
1799 function filter( string, propertyName ) { 1804 function filter( string, propertyName ) {
1800 // In case of font-families we'll skip quotes. (#10750) 1805 // In case of font-families we'll skip quotes. (http://dev.ckeditor.com/ticket/10750)
1801 return propertyName.toLowerCase() == 'font-family' ? string.replace( /["']/g, '' ) : string; 1806 return propertyName.toLowerCase() == 'font-family' ? string.replace( /["']/g, '' ) : string;
1802 } 1807 }
1803 1808
@@ -1824,13 +1829,25 @@ CKEDITOR.STYLE_OBJECT = 3;
1824 var doc = selection.document, 1829 var doc = selection.document,
1825 ranges = selection.getRanges(), 1830 ranges = selection.getRanges(),
1826 func = remove ? this.removeFromRange : this.applyToRange, 1831 func = remove ? this.removeFromRange : this.applyToRange,
1827 range; 1832 originalRanges,
1833 range,
1834 i;
1835
1836 // In case of fake table selection, we would like to apply all styles and then select
1837 // the original ranges. Otherwise browsers would complain about discontiguous selection.
1838 if ( selection.isFake && selection.isInTable() ) {
1839 originalRanges = [];
1840
1841 for ( i = 0; i < ranges.length; i++ ) {
1842 originalRanges.push( ranges[ i ].clone() );
1843 }
1844 }
1828 1845
1829 var iterator = ranges.createIterator(); 1846 var iterator = ranges.createIterator();
1830 while ( ( range = iterator.getNextRange() ) ) 1847 while ( ( range = iterator.getNextRange() ) )
1831 func.call( this, range, editor ); 1848 func.call( this, range, editor );
1832 1849
1833 selection.selectRanges( ranges ); 1850 selection.selectRanges( originalRanges || ranges );
1834 doc.removeCustomData( 'doc_processing_style' ); 1851 doc.removeCustomData( 'doc_processing_style' );
1835 } 1852 }
1836} )(); 1853} )();
@@ -1901,7 +1918,7 @@ CKEDITOR.styleCommand.prototype.exec = function( editor ) {
1901 */ 1918 */
1902CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' ); 1919CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' );
1903 1920
1904// Backward compatibility (#5025). 1921// Backward compatibility (http://dev.ckeditor.com/ticket/5025).
1905CKEDITOR.addStylesSet = CKEDITOR.tools.bind( CKEDITOR.stylesSet.add, CKEDITOR.stylesSet ); 1922CKEDITOR.addStylesSet = CKEDITOR.tools.bind( CKEDITOR.stylesSet.add, CKEDITOR.stylesSet );
1906CKEDITOR.loadStylesSet = function( name, url, callback ) { 1923CKEDITOR.loadStylesSet = function( name, url, callback ) {
1907 CKEDITOR.stylesSet.addExternal( name, url, '' ); 1924 CKEDITOR.stylesSet.addExternal( name, url, '' );
@@ -2004,7 +2021,7 @@ CKEDITOR.tools.extend( CKEDITOR.editor.prototype, {
2004 return; 2021 return;
2005 } 2022 }
2006 2023
2007 // #5352 Allow to define the styles directly in the config object 2024 // http://dev.ckeditor.com/ticket/5352 Allow to define the styles directly in the config object
2008 if ( configStyleSet instanceof Array ) { 2025 if ( configStyleSet instanceof Array ) {
2009 editor._.stylesDefinitions = configStyleSet; 2026 editor._.stylesDefinitions = configStyleSet;
2010 callback( configStyleSet ); 2027 callback( configStyleSet );
diff --git a/sources/core/template.js b/sources/core/template.js
index 2b9e932..a627c34 100644
--- a/sources/core/template.js
+++ b/sources/core/template.js
@@ -9,11 +9,7 @@
9 */ 9 */
10 10
11( function() { 11( function() {
12 var cache = {}, 12 var rePlaceholder = /{([^}]+)}/g;
13 rePlaceholder = /{([^}]+)}/g,
14 reEscapableChars = /([\\'])/g,
15 reNewLine = /\n/g,
16 reCarriageReturn = /\r/g;
17 13
18 /** 14 /**
19 * Lightweight template used to build the output string from variables. 15 * Lightweight template used to build the output string from variables.
@@ -27,42 +23,35 @@
27 * @param {String} source The template source. 23 * @param {String} source The template source.
28 */ 24 */
29 CKEDITOR.template = function( source ) { 25 CKEDITOR.template = function( source ) {
30 // Builds an optimized function body for the output() method, focused on performance. 26 /**
31 // For example, if we have this "source": 27 * The current template source.
32 // '<div style="{style}">{editorName}</div>' 28 *
33 // ... the resulting function body will be (apart from the "buffer" handling): 29 * @readonly
34 // return [ '<div style="', data['style'] == undefined ? '{style}' : data['style'], '">', data['editorName'] == undefined ? '{editorName}' : data['editorName'], '</div>' ].join(''); 30 * @member CKEDITOR.template
31 * @property {String}
32 */
33 this.source = String( source );
34 };
35 35
36 // Try to read from the cache. 36 /**
37 if ( cache[ source ] ) 37 * Processes the template, filling its variables with the provided data.
38 this.output = cache[ source ]; 38 *
39 else { 39 * @method
40 var fn = source 40 * @member CKEDITOR.template
41 // Escape chars like slash "\" or single quote "'". 41 * @param {Object} data An object containing properties whose values will be
42 .replace( reEscapableChars, '\\$1' ) 42 * used to fill the template variables. The property names must match the
43 .replace( reNewLine, '\\n' ) 43 * template variables names. Variables without matching properties will be
44 .replace( reCarriageReturn, '\\r' ) 44 * kept untouched.
45 // Inject the template keys replacement. 45 * @param {Array} [buffer] An array that the output data will be pushed into.
46 .replace( rePlaceholder, function( m, key ) { 46 * The number of entries appended to the array is unknown.
47 return "',data['" + key + "']==undefined?'{" + key + "}':data['" + key + "'],'"; 47 * @returns {String/Number} If `buffer` has not been provided, the processed
48 } ); 48 * template output data; otherwise the new length of `buffer`.
49 */
50 CKEDITOR.template.prototype.output = function( data, buffer ) {
51 var output = this.source.replace( rePlaceholder, function( fullMatch, dataKey ) {
52 return data[ dataKey ] !== undefined ? data[ dataKey ] : fullMatch;
53 } );
49 54
50 fn = "return buffer?buffer.push('" + fn + "'):['" + fn + "'].join('');"; 55 return buffer ? buffer.push( output ) : output;
51 this.output = cache[ source ] = Function( 'data', 'buffer', fn );
52 }
53 }; 56 };
54} )(); 57} )();
55
56/**
57 * Processes the template, filling its variables with the provided data.
58 *
59 * @method output
60 * @param {Object} data An object containing properties which values will be
61 * used to fill the template variables. The property names must match the
62 * template variables names. Variables without matching properties will be
63 * kept untouched.
64 * @param {Array} [buffer] An array into which the output data will be pushed into.
65 * The number of entries appended to the array is unknown.
66 * @returns {String/Number} If `buffer` has not been provided, the processed
67 * template output data, otherwise the new length of `buffer`.
68 */
diff --git a/sources/core/tools.js b/sources/core/tools.js
index 7e0083f..a4b736d 100644
--- a/sources/core/tools.js
+++ b/sources/core/tools.js
@@ -1021,7 +1021,7 @@
1021 styleText = CKEDITOR.tools.normalizeHex( CKEDITOR.tools.convertRgbToHex( styleText ) ); 1021 styleText = CKEDITOR.tools.normalizeHex( CKEDITOR.tools.convertRgbToHex( styleText ) );
1022 } 1022 }
1023 1023
1024 // IE will leave a single semicolon when failed to parse the style text. (#3891) 1024 // IE will leave a single semicolon when failed to parse the style text. (http://dev.ckeditor.com/ticket/3891)
1025 if ( !styleText || styleText == ';' ) 1025 if ( !styleText || styleText == ';' )
1026 return retval; 1026 return retval;
1027 1027
@@ -1463,6 +1463,37 @@
1463 }, 1463 },
1464 1464
1465 /** 1465 /**
1466 * Detects which mouse button generated a given DOM event.
1467 *
1468 * @since 4.7.3
1469 * @param {CKEDITOR.dom.event} evt DOM event.
1470 * @returns {Number|Boolean} Returns a number indicating the mouse button or `false`
1471 * if the mouse button cannot be determined.
1472 */
1473 getMouseButton: function( evt ) {
1474 var evtData = evt.data,
1475 domEvent = evtData && evtData.$;
1476
1477 if ( !( evtData && domEvent ) ) {
1478 // Added in case when there's no data available. That's the case in some unit test in built version which
1479 // mock event but doesn't put data object.
1480 return false;
1481 }
1482
1483 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
1484 if ( domEvent.button === 4 ) {
1485 return CKEDITOR.MOUSE_BUTTON_MIDDLE;
1486 } else if ( domEvent.button === 1 ) {
1487 return CKEDITOR.MOUSE_BUTTON_LEFT;
1488 } else {
1489 return CKEDITOR.MOUSE_BUTTON_RIGHT;
1490 }
1491 }
1492
1493 return domEvent.button;
1494 },
1495
1496 /**
1466 * A set of functions for operations on styles. 1497 * A set of functions for operations on styles.
1467 * 1498 *
1468 * @property {CKEDITOR.tools.style} 1499 * @property {CKEDITOR.tools.style}
@@ -1627,6 +1658,21 @@
1627 yellowgreen: '#9ACD32' 1658 yellowgreen: '#9ACD32'
1628 }, 1659 },
1629 1660
1661 _borderStyle: [
1662 'none',
1663 'hidden',
1664 'dotted',
1665 'dashed',
1666 'solid',
1667 'double',
1668 'groove',
1669 'ridge',
1670 'inset',
1671 'outset'
1672 ],
1673
1674 _widthRegExp: /^(thin|medium|thick|[\+-]?\d+(\.\d+)?[a-z%]+|[\+-]?0+(\.0+)?|\.\d+[a-z%]+)$/,
1675
1630 _rgbaRegExp: /rgba?\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*(?:,\s*[0-9.]+\s*)?\)/gi, 1676 _rgbaRegExp: /rgba?\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*(?:,\s*[0-9.]+\s*)?\)/gi,
1631 1677
1632 _hslaRegExp: /hsla?\(\s*[0-9.]+\s*,\s*\d+%\s*,\s*\d+%\s*(?:,\s*[0-9.]+\s*)?\)/gi, 1678 _hslaRegExp: /hsla?\(\s*[0-9.]+\s*,\s*\d+%\s*,\s*\d+%\s*(?:,\s*[0-9.]+\s*)?\)/gi,
@@ -1647,10 +1693,8 @@
1647 * @member CKEDITOR.tools.style.parse 1693 * @member CKEDITOR.tools.style.parse
1648 */ 1694 */
1649 background: function( value ) { 1695 background: function( value ) {
1650 var ret = [], 1696 var ret = {},
1651 colors = []; 1697 colors = this._findColor( value );
1652
1653 colors = this._findColor( value );
1654 1698
1655 if ( colors.length ) { 1699 if ( colors.length ) {
1656 ret.color = colors[ 0 ]; 1700 ret.color = colors[ 0 ];
@@ -1715,6 +1759,51 @@
1715 }, 1759 },
1716 1760
1717 /** 1761 /**
1762 * Parses the `border` CSS property shorthand format.
1763 * This CSS property does not support inheritance (https://www.w3.org/TR/css3-background/#the-border-shorthands).
1764 *
1765 * console.log( CKEDITOR.tools.style.parse.border( '3px solid #ffeedd' ) );
1766 * // Logs: { width: "3px", style: "solid", color: "#ffeedd" }
1767 *
1768 * @param {String} value The `border` property value.
1769 * @returns {Object}
1770 * @returns {String} return.width The border-width attribute.
1771 * @returns {String} return.style The border-style attribute.
1772 * @returns {String} return.color The border-color attribute.
1773 * @member CKEDITOR.tools.style.parse
1774 */
1775 border: function( value ) {
1776 var ret = {},
1777 input = value.split( /\s+/ );
1778
1779 CKEDITOR.tools.array.forEach( input, function( val ) {
1780 if ( !ret.color ) {
1781 var parseColor = CKEDITOR.tools.style.parse._findColor( val );
1782 if ( parseColor.length ) {
1783 ret.color = parseColor[ 0 ];
1784 return;
1785 }
1786 }
1787
1788 if ( !ret.style ) {
1789 if ( CKEDITOR.tools.indexOf( CKEDITOR.tools.style.parse._borderStyle, val ) !== -1 ) {
1790 ret.style = val;
1791 return;
1792 }
1793 }
1794
1795 if ( !ret.width ) {
1796 if ( CKEDITOR.tools.style.parse._widthRegExp.test( val ) ) {
1797 ret.width = val;
1798 return;
1799 }
1800 }
1801
1802 } );
1803 return ret;
1804 },
1805
1806 /**
1718 * Searches the `value` for any CSS color occurrences and returns it. 1807 * Searches the `value` for any CSS color occurrences and returns it.
1719 * 1808 *
1720 * @private 1809 * @private
@@ -1846,6 +1935,39 @@
1846 } 1935 }
1847 return acc; 1936 return acc;
1848 } 1937 }
1938 },
1939
1940 /**
1941 * A set of object helpers.
1942 *
1943 * @property {CKEDITOR.tools.object}
1944 * @member CKEDITOR.tools
1945 */
1946 object: {
1947 /**
1948 * Returns the first key from `obj` which has a given `value`.
1949 *
1950 * @param {Object} obj An object whose `key` is looked for.
1951 * @param {Mixed} value An object's `value` to be looked for.
1952 * @returns {String/null} Matched `key` or `null` if not found.
1953 * @member CKEDITOR.tools.object
1954 */
1955
1956 findKey: function( obj, value ) {
1957 if ( typeof obj !== 'object' ) {
1958 return null;
1959 }
1960
1961 var key;
1962
1963 for ( key in obj ) {
1964 if ( obj[ key ] === value ) {
1965 return key;
1966 }
1967 }
1968
1969 return null;
1970 }
1849 } 1971 }
1850 }; 1972 };
1851 1973
@@ -1889,7 +2011,35 @@
1889 */ 2011 */
1890 CKEDITOR.tools.array.isArray = CKEDITOR.tools.isArray; 2012 CKEDITOR.tools.array.isArray = CKEDITOR.tools.isArray;
1891 2013
2014 /**
2015 * Left mouse button.
2016 *
2017 * @since 4.7.3
2018 * @readonly
2019 * @property {Number} [=0]
2020 * @member CKEDITOR
2021 */
2022 CKEDITOR.MOUSE_BUTTON_LEFT = 0;
1892 2023
2024 /**
2025 * Middle mouse button.
2026 *
2027 * @since 4.7.3
2028 * @readonly
2029 * @property {Number} [=1]
2030 * @member CKEDITOR
2031 */
2032 CKEDITOR.MOUSE_BUTTON_MIDDLE = 1;
2033
2034 /**
2035 * Right mouse button.
2036 *
2037 * @since 4.7.3
2038 * @readonly
2039 * @property {Number} [=2]
2040 * @member CKEDITOR
2041 */
2042 CKEDITOR.MOUSE_BUTTON_RIGHT = 2;
1893 2043
1894 /** 2044 /**
1895 * The namespace containing functions to work on CSS properties. 2045 * The namespace containing functions to work on CSS properties.
@@ -1911,6 +2061,13 @@
1911 * @since 4.6.1 2061 * @since 4.6.1
1912 * @class CKEDITOR.tools.array 2062 * @class CKEDITOR.tools.array
1913 */ 2063 */
2064
2065 /**
2066 * The namespace with helper functions and polyfills for objects.
2067 *
2068 * @since 4.7.1
2069 * @class CKEDITOR.tools.object
2070 */
1914} )(); 2071} )();
1915 2072
1916// PACKAGER_RENAME( CKEDITOR.tools ) 2073// PACKAGER_RENAME( CKEDITOR.tools )