]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blobdiff - sources/core/selection.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / core / selection.js
index eef28a316f642b67a814984cbd418bfa32a5e5ac..d44db3bc282dd62f268cd77ce2c20f34468436c3 100644 (file)
@@ -6,9 +6,211 @@
 ( function() {
        var isMSSelection = typeof window.getSelection != 'function',
                nextRev = 1,
-               // #13816
+               // http://dev.ckeditor.com/ticket/13816
                fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ),
-               fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' );
+               fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' ),
+               isSelectingTable;
+
+       // #### table selection : START
+       // @param {CKEDITOR.dom.range[]} ranges
+       // @param {Boolean} allowPartially Whether a collapsed selection within table is recognized to be a valid selection.
+       // This happens for WebKits on MacOS, when you right click inside the table.
+       function isTableSelection( ranges, allowPartially ) {
+               if ( ranges.length === 0 ) {
+                       return false;
+               }
+
+               var node,
+                       i;
+
+               function isPartiallySelected( range ) {
+                       var startCell = range.startContainer.getAscendant( { td: 1, th: 1 }, true ),
+                               endCell = range.endContainer.getAscendant( { td: 1, th: 1 }, true ),
+                               trim = CKEDITOR.tools.trim,
+                               selected;
+
+                       // Check if the selection is inside one cell and we don't have any nested table contents selected.
+                       if ( !startCell || !startCell.equals( endCell ) || startCell.findOne( 'td, th, tr, tbody, table' ) ) {
+                               return false;
+                       }
+
+                       selected = range.cloneContents();
+
+                       // Empty selection is still partially selected.
+                       if ( !selected.getFirst() ) {
+                               return true;
+                       }
+
+                       return trim( selected.getFirst().getText() ) !== trim( startCell.getText() );
+               }
+
+               // Edge case: partially selected text node inside one table cell or cursor inside cell.
+               if ( !allowPartially && ranges.length === 1 &&
+                       ( ranges[ 0 ].collapsed || isPartiallySelected( ranges[ 0 ] ) ) ) {
+                       return false;
+               }
+
+               for ( i = 0; i < ranges.length; i++ ) {
+                       node = ranges[ i ]._getTableElement();
+
+                       if ( !node ) {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+       // After performing fake table selection, the real selection is limited
+       // to the first selected cell. Therefore to check if the real selection
+       // matches the fake selection, we check if the table cell from fake selection's
+       // first range and real selection's range are the same.
+       // Also if the selection is collapsed, we should check if it's placed inside the table
+       // in which the fake selection is or inside nested table. Such selection occurs after right mouse click.
+       function isRealTableSelection( selection, fakeSelection ) {
+               var ranges = selection.getRanges(),
+                       fakeRanges = fakeSelection.getRanges(),
+                       table = ranges.length && ranges[ 0 ]._getTableElement() &&
+                               ranges[ 0 ]._getTableElement().getAscendant( 'table', true ),
+                       fakeTable = fakeRanges.length && fakeRanges[ 0 ]._getTableElement() &&
+                               fakeRanges[ 0 ]._getTableElement().getAscendant( 'table', true ),
+                       isTableRange = ranges.length === 1 && ranges[ 0 ]._getTableElement() &&
+                                       ranges[ 0 ]._getTableElement().is( 'table' ),
+                       isFakeTableRange = fakeRanges.length === 1 && fakeRanges[ 0 ]._getTableElement() &&
+                                       fakeRanges[ 0 ]._getTableElement().is( 'table' );
+
+               function isValidTableSelection( table, fakeTable, ranges, fakeRanges ) {
+                       var isMenuOpen = ranges.length === 1 && ranges[ 0 ].collapsed,
+                               // In case of WebKit on MacOS, when checking real selection, we must allow selection to be partial.
+                               // Otherwise the check will fail for table selection with opened context menu.
+                               isInTable = isTableSelection( ranges, !!CKEDITOR.env.webkit ) && isTableSelection( fakeRanges );
+
+                       return isSameTable( table, fakeTable ) && ( isMenuOpen || isInTable );
+               }
+
+               function isSameTable( table, fakeTable ) {
+                       if ( !table || !fakeTable ) {
+                               return false;
+                       }
+
+                       return table.equals( fakeTable ) || fakeTable.contains( table );
+               }
+
+               if ( isValidTableSelection( table, fakeTable, ranges, fakeRanges ) ) {
+                       // Edge case: when editor contains only table and that table is selected using selectAll command,
+                       // then the selection is not properly refreshed and it must be done manually.
+                       if ( isTableRange && !isFakeTableRange ) {
+                               fakeSelection.selectRanges( ranges );
+                       }
+                       return true;
+               }
+
+               return false;
+       }
+
+       function getSelectedCells( ranges ) {
+               var cells = [],
+                       node,
+                       i;
+
+               function getCellsFromElement( element ) {
+                       var cells = element.find( 'td, th' ),
+                               cellsArray = [],
+                               i;
+
+                       for ( i = 0; i < cells.count(); i++ ) {
+                               cellsArray.push( cells.getItem( i ) );
+                       }
+
+                       return cellsArray;
+               }
+
+               for ( i = 0; i < ranges.length; i++ ) {
+                       node = ranges[ i ]._getTableElement();
+
+                       if ( node.is && node.is( { td: 1, th: 1 } ) ) {
+                               cells.push( node );
+                       } else {
+                               cells = cells.concat( getCellsFromElement( node ) );
+                       }
+               }
+
+               return cells;
+       }
+
+       // Cells in the same row are separated by tab and the rows are separated by new line, e.g.
+       // Cell 1.1     Cell 1.2
+       // Cell 2.1     Cell 2.2
+       function getTextFromSelectedCells( ranges ) {
+               var cells = getSelectedCells( ranges ),
+                       txt = '',
+                       currentRow = [],
+                       lastRow,
+                       i;
+
+               for ( i = 0; i < cells.length; i++ ) {
+                       if ( lastRow && !lastRow.equals( cells[ i ].getAscendant( 'tr' ) ) ) {
+                               txt += currentRow.join( '\t' ) + '\n';
+                               lastRow = cells[ i ].getAscendant( 'tr' );
+                               currentRow = [];
+                       } else if ( i === 0 ) {
+                               lastRow = cells[ i ].getAscendant( 'tr' );
+                       }
+
+                       currentRow.push( cells[ i ].getText() );
+               }
+
+               txt += currentRow.join( '\t' );
+
+               return txt;
+       }
+
+       function performFakeTableSelection( ranges ) {
+               var editor = this.root.editor,
+                       realSelection = editor.getSelection( 1 ),
+                       cache;
+
+               // Cleanup after previous selection - e.g. remove hidden sel container.
+               this.reset();
+
+               // Indicate that the table is being fake-selected to prevent infinite loop
+               // inside `selectRanges`.
+               isSelectingTable = true;
+
+               // Cancel selectionchange for the real selection.
+               realSelection.root.once( 'selectionchange', function( evt ) {
+                       evt.cancel();
+               }, null, null, 0 );
+
+               // Move real selection to the first selected range.
+               realSelection.selectRanges( [ ranges[ 0 ] ] );
+
+               cache = this._.cache;
+
+               // Caches given ranges.
+               cache.ranges = new CKEDITOR.dom.rangeList( ranges );
+               cache.type = CKEDITOR.SELECTION_TEXT;
+               cache.selectedElement = ranges[ 0 ]._getTableElement();
+
+               // `selectedText` should contain text from all selected data ("plain text table")
+               // to be compatible with Firefox's implementation.
+               cache.selectedText = getTextFromSelectedCells( ranges );
+
+               // Properties that will not be available when isFake.
+               cache.nativeSel = null;
+
+               this.isFake = 1;
+               this.rev = nextRev++;
+
+               // Save this selection, so it can be returned by editor.getSelection().
+               editor._.fakeSelection = this;
+
+               isSelectingTable = false;
+
+               // Fire selectionchange, just like a normal selection.
+               this.root.fire( 'selectionchange' );
+       }
+       // #### table selection : END
 
        // #### checkSelectionChange : START
 
 
                if ( sel ) {
                        realSel = this.getSelection( 1 );
-
-                       // If real (not locked/stored) selection was moved from hidden container,
-                       // then the fake-selection must be invalidated.
-                       if ( !realSel || !realSel.isHidden() ) {
+                       // If real (not locked/stored) selection was moved from hidden container
+                       // or is not a table one, then the fake-selection must be invalidated.
+                       if ( !realSel || ( !realSel.isHidden() && !isRealTableSelection( realSel, sel ) ) ) {
                                // Remove the cache from fake-selection references in use elsewhere.
                                sel.reset();
 
 
                var currentPath = this.elementPath();
                if ( !currentPath.compare( this._.selectionPreviousPath ) ) {
-                       // Handle case when dialog inserts new element but parent block and path (so also focus context) does not change. (#13362)
+                       // 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)
                        var sameBlockParent = this._.selectionPreviousPath && this._.selectionPreviousPath.blockLimit.equals( currentPath.blockLimit );
                        // Cache the active element, which we'll eventually lose on Webkit.
                        if ( CKEDITOR.env.webkit && !sameBlockParent )
        // * is a visible node,
        // * is a non-empty element (this rule will accept elements like <strong></strong> because they
        //      they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret).
-       //      See #12621.
+       //      See http://dev.ckeditor.com/ticket/12621.
        function mayAbsorbCaret( node ) {
                if ( isVisible( node ) )
                        return true;
                if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) )
                        return true;
 
-               // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222)
-               // If you found this line confusing check #12655.
+               // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (http://dev.ckeditor.com/ticket/7222)
+               // If you found this line confusing check http://dev.ckeditor.com/ticket/12655.
                if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) )
                        return true;
 
                return fillingChar;
        }
 
-       // Checks if a filling char has been used, eventualy removing it (#1272).
+       // Checks if a filling char has been used, eventually removing it (http://dev.ckeditor.com/ticket/1272).
        function checkFillingCharSequenceNodeReady( editable ) {
                var fillingChar = editable.getCustomData( 'cke-fillingChar' );
 
                        // creating it.
                        if ( fillingChar.getCustomData( 'ready' ) ) {
                                removeFillingCharSequenceNode( editable );
+                               editable.editor.fire( 'selectionCheck' );
                        } else {
                                fillingChar.setCustomData( 'ready', 1 );
                        }
 
                if ( fillingChar ) {
                        // Text selection position might get mangled by
-                       // subsequent dom modification, save it now for restoring. (#8617)
+                       // subsequent dom modification, save it now for restoring. (http://dev.ckeditor.com/ticket/8617)
                        if ( keepSelection !== false ) {
                                var sel = editable.getDocument().getSelection().getNative(),
                                        // Be error proof.
                }
        }
 
-       // #13816
+       // http://dev.ckeditor.com/ticket/13816
        function removeFillingCharSequenceString( str, nbspAware ) {
                if ( nbspAware ) {
                        return str.replace( fillingCharSequenceRegExp, function( m, p ) {
-                               // #10291 if filling char is followed by a space replace it with NBSP.
+                               // http://dev.ckeditor.com/ticket/10291 if filling char is followed by a space replace it with NBSP.
                                return p ? '\xa0' : '';
                        } );
                } else {
                        ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) {
                        // So far we can't say that enclosed element is non-editable. Before checking,
                        // we'll shrink range (clone). Shrinking will stop on non-editable range, or
-                       // innermost element (#11114).
+                       // innermost element (http://dev.ckeditor.com/ticket/11114).
                        clone = range.clone();
                        clone.shrink( CKEDITOR.SHRINK_ELEMENT, true );
 
 
                        // Give the editable an initial selection on first focus,
                        // put selection at a consistent position at the start
-                       // of the contents. (#9507)
+                       // of the contents. (http://dev.ckeditor.com/ticket/9507)
                        if ( CKEDITOR.env.gecko ) {
                                editable.attachListener( editable, 'focus', function( evt ) {
                                        evt.removeListener();
                                        if ( restoreSel !== 0 ) {
                                                var nativ = editor.getSelection().getNative();
                                                // Do it only if the native selection is at an unwanted
-                                               // place (at the very start of the editable). #10119
+                                               // place (at the very start of the editable). http://dev.ckeditor.com/ticket/10119
                                                if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) {
                                                        var rng = editor.createRange();
                                                        rng.moveToElementEditStart( editable );
 
                                        // On Webkit when editor uses divarea, native focus causes editable viewport to scroll
                                        // to the top (when there is no active selection inside while focusing) so the scroll
-                                       // position should be restored after focusing back editable area. (#14659)
+                                       // position should be restored after focusing back editable area. (http://dev.ckeditor.com/ticket/14659)
                                        if ( restoreSel && editor._.previousScrollTop != null && editor._.previousScrollTop != editable.$.scrollTop ) {
                                                editable.$.scrollTop = editor._.previousScrollTop;
                                        }
                                editable.attachListener( editable, 'mousedown', function( evt ) {
                                        // IE scrolls document to top on right mousedown
                                        // when editor has no focus, remember this scroll
-                                       // position and revert it before context menu opens. (#5778)
+                                       // position and revert it before context menu opens. (http://dev.ckeditor.com/ticket/5778)
                                        if ( evt.data.$.button == 2 ) {
                                                var sel = editor.document.getSelection();
                                                if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
 
                                // When content doc is in standards mode, IE doesn't focus the editor when
                                // clicking at the region below body (on html element) content, we emulate
-                               // the normal behavior on old IEs. (#1659, #7932)
+                               // the normal behavior on old IEs. (http://dev.ckeditor.com/ticket/1659, http://dev.ckeditor.com/ticket/7932)
                                if ( doc.$.compatMode != 'BackCompat' ) {
                                        if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
                                                var textRng,
                                                                html.removeListener( 'mousemove', onHover );
                                                                removeListeners();
 
-                                                               // Make it in effect on mouse up. (#9022)
+                                                               // Make it in effect on mouse up. (http://dev.ckeditor.com/ticket/9022)
                                                                textRng.select();
                                                        }
 
                                        if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) {
                                                html.on( 'mousedown', function( evt ) {
                                                        if ( evt.data.getTarget().is( 'html' ) ) {
-                                                               // Limit the text selection mouse move inside of editable. (#9715)
+                                                               // Limit the text selection mouse move inside of editable. (http://dev.ckeditor.com/ticket/9715)
                                                                outerDoc.on( 'mouseup', onSelectEnd );
                                                                html.on( 'mouseup', onSelectEnd );
                                                        }
                        // 2. After the accomplish of keyboard and mouse events.
                        editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor );
                        editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor );
+                       // http://dev.ckeditor.com/ticket/14407 - Don't even let anything happen if the selection is in a non-editable element.
+                       editable.attachListener( editable, 'keydown', function( evt ) {
+                               var sel = this.getSelection( 1 );
+                               if ( nonEditableAscendant( sel ) ) {
+                                       sel.selectElement( nonEditableAscendant( sel ) );
+                                       evt.data.preventDefault();
+                               }
+                       }, editor );
                        // Always fire the selection change on focus gain.
                        // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
                        // we need synchronization between those listeners to not lost cached editor._.previousActive property
                                editor.selectionChange( 1 );
                        } );
 
-                       // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
+                       // http://dev.ckeditor.com/ticket/9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
                        // by dragging and releasing mouse button outside editable. Dragging (mousedown)
                        // has to be initialized in editable, but for mouseup we listen on document element.
                        if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) {
                                        mouseDown = 0;
                                } );
                        }
-                       // In all other cases listen on simple mouseup over editable, as we did before #9699.
+                       // In all other cases listen on simple mouseup over editable, as we did before http://dev.ckeditor.com/ticket/9699.
                        //
                        // Use document instead of editable in non-IEs for observing mouseup
                        // since editable won't fire the event if selection process started within iframe and ended out
-                       // of the editor (#9851).
+                       // of the editor (http://dev.ckeditor.com/ticket/9851).
                        else {
                                editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor );
                        }
                                                case 8: // BACKSPACE
                                                case 45: // INS
                                                case 46: // DEl
-                                                       removeFillingCharSequenceNode( editable );
+                                                       if ( editable.hasFocus ) {
+                                                               removeFillingCharSequenceNode( editable );
+                                                       }
                                        }
 
                                }, null, null, -1 );
                        }
 
-                       // Automatically select non-editable element when navigating into
-                       // it by left/right or backspace/del keys.
                        editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
 
                        function moveRangeToPoint( range, x, y ) {
-                               // Error prune in IE7. (#9034, #9110)
+                               // Error prune in IE7. (http://dev.ckeditor.com/ticket/9034, http://dev.ckeditor.com/ticket/9110)
                                try {
                                        range.moveToPoint( x, y );
                                } catch ( e ) {}
                                        range = sel.createRange();
 
                                // The selection range is reported on host, but actually it should applies to the content doc.
-                               if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ )
+                               // The parentElement may be null for read only mode in IE10 and below (http://dev.ckeditor.com/ticket/9780).
+                               if ( sel.type != 'None' && range.parentElement() && range.parentElement().ownerDocument == doc.$ )
                                        range.select();
                        }
+
+                       function nonEditableAscendant( sel ) {
+                               if ( CKEDITOR.env.ie ) {
+                                       var range = sel.getRanges()[ 0 ],
+                                               ascendant = range ? range.startContainer.getAscendant( function( parent ) {
+                                                               return parent.type == CKEDITOR.NODE_ELEMENT &&
+                                                                       ( parent.getAttribute( 'contenteditable' ) == 'false' || parent.getAttribute( 'contenteditable' ) == 'true' );
+                                                       }, true ) : null ;
+
+                                       return range && ascendant.getAttribute( 'contenteditable' ) == 'false' && ascendant;
+                               }
+                       }
                } );
 
                editor.on( 'setData', function() {
                        // Invalidate locked selection when unloading DOM.
-                       // (#9521, #5217#comment:32 and #11500#comment:11)
+                       // (http://dev.ckeditor.com/ticket/9521, http://dev.ckeditor.com/ticket/5217#comment:32 and http://dev.ckeditor.com/ticket/11500#comment:11)
                        editor.unlockSelection();
 
                        // Webkit's selection will mess up after the data loading.
                        editor.unlockSelection();
                } );
 
-               // IE9 might cease to work if there's an object selection inside the iframe (#7639).
+               // IE9 might cease to work if there's an object selection inside the iframe (http://dev.ckeditor.com/ticket/7639).
                if ( CKEDITOR.env.ie9Compat )
                        editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
 
                // When loaded data are ready check whether hidden selection container was not loaded.
                editor.on( 'loadSnapshot', function() {
                        var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ),
-                               // TODO replace with el.find() which will be introduced in #9764,
+                               // TODO replace with el.find() which will be introduced in http://dev.ckeditor.com/ticket/9764,
                                // because it may happen that hidden sel container won't be the last element.
                                last = editor.editable().getLast( isElement );
 
        } );
 
        // On WebKit only, we need a special "filling" char on some situations
-       // (#1272). Here we set the events that should invalidate that char.
+       // (http://dev.ckeditor.com/ticket/1272). Here we set the events that should invalidate that char.
        if ( CKEDITOR.env.webkit ) {
                CKEDITOR.on( 'instanceReady', function( evt ) {
                        var editor = evt.editor;
 
                        // Filter Undo snapshot's HTML to get rid of Filling Char Sequence.
                        // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
-                       // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (#13816).
+                       // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (http://dev.ckeditor.com/ticket/13816).
                        editor.on( 'getSnapshot', function( evt ) {
                                if ( evt.data ) {
                                        evt.data = removeFillingCharSequenceString( evt.data );
 
                        // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat
                        // instead of #getData because once removed, FCSeq may leave an empty element,
-                       // which should be pruned by the dataProcessor (#13816).
+                       // which should be pruned by the dataProcessor (http://dev.ckeditor.com/ticket/13816).
                        // Note: Used low priority to filter when dataProcessor works on strings,
                        // not pseudo–DOM.
                        editor.on( 'toDataFormat', function( evt ) {
 
                // Selection out of concerned range, empty the selection.
                // TODO check whether this condition cannot be reverted to its old
-               // form (commented out) after we closed #10438.
+               // form (commented out) after we closed http://dev.ckeditor.com/ticket/10438.
                //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
                if ( !(
                        rangeParent &&
                 *
                 *              var selection = editor.getSelection().getNative();
                 *
-                * @returns {Object} The native browser selection object.
+                * @returns {Object} The native browser selection object or null if this is a fake selection.
                 */
                getNative: function() {
                        if ( this._.cache.nativeSel !== undefined )
                 *              alert( ranges.length );
                 *
                 * @method
-                * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only.
+                * @param {Boolean} [onlyEditables] If set to `true`, this function retrieves editable ranges only.
                 * @returns {Array} Range instances that represent the current selection.
                 */
                getRanges: ( function() {
                                                index = -1,
                                                position, distance, container;
 
-                                       // Binary search over all element childs to test the range to see whether
+                                       // Binary search over all element children to test the range to see whether
                                        // range is right on the boundary of one element.
                                        while ( startIndex <= endIndex ) {
                                                index = Math.floor( ( startIndex + endIndex ) / 2 );
                                                        return { container: parent, offset: getNodeIndex( child ) };
                                        }
 
-                                       // All childs are text nodes,
-                                       // or to the right hand of test range are all text nodes. (#6992)
+                                       // All children are text nodes,
+                                       // or to the right hand of test range are all text nodes. (http://dev.ckeditor.com/ticket/6992)
                                        if ( index == -1 || index == siblings.length - 1 && position < 0 ) {
                                                // Adapt test range to embrace the entire parent contents.
                                                testRange.moveToElementText( parent );
 
                                                // IE report line break as CRLF with range.text but
                                                // only LF with textnode.nodeValue, normalize them to avoid
-                                               // breaking character counting logic below. (#3949)
+                                               // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
                                                distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
 
                                                siblings = parent.childNodes;
 
                                                // IE report line break as CRLF with range.text but
                                                // only LF with textnode.nodeValue, normalize them to avoid
-                                               // breaking character counting logic below. (#3949)
+                                               // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
                                                distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
 
                                                // Actual range anchor right beside test range at the inner boundary of text node.
                                                                }
                                                                child = sibling;
                                                        }
-                                                       // Measurement in IE could be somtimes wrong because of <select> element. (#4611)
+                                                       // Measurement in IE could be sometimes wrong because of <select> element. (http://dev.ckeditor.com/ticket/4611)
                                                        catch ( e ) {
                                                                return { container: parent, offset: getNodeIndex( child ) };
                                                        }
                                                boundaryInfo = getBoundaryInformation( nativeRange );
                                                range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
 
-                                               // Correct an invalid IE range case on empty list item. (#5850)
+                                               // Correct an invalid IE range case on empty list item. (http://dev.ckeditor.com/ticket/5850)
                                                if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() )
                                                        range.collapse();
 
                        } )() :
                        function() {
                                // On browsers implementing the W3C range, we simply
-                               // tranform the native ranges in CKEDITOR.dom.range
+                               // transform the native ranges in CKEDITOR.dom.range
                                // instances.
 
                                var ranges = [],
                                        return ranges;
 
                                // Split range into multiple by read-only nodes.
-                               // Clone ranges array to avoid changing cached ranges (#11493).
+                               // Clone ranges array to avoid changing cached ranges (http://dev.ckeditor.com/ticket/11493).
                                return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) );
                        };
                } )(),
 
                                                        // Decrease the range content to exclude particial
                                                        // selected node on the start which doesn't have
-                                                       // visual impact. ( #3231 )
+                                                       // visual impact. ( http://dev.ckeditor.com/ticket/3231 )
                                                        while ( 1 ) {
                                                                var startContainer = range.startContainer,
                                                                        startOffset = range.startOffset;
-                                                               // Limit the fix only to non-block elements.(#3950)
+                                                               // Limit the fix only to non-block elements.(http://dev.ckeditor.com/ticket/3950)
                                                                if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() )
                                                                        range.setStartAfter( startContainer );
                                                                else
                 *              var element = editor.getSelection().getSelectedElement();
                 *              alert( element.getName() );
                 *
-                * @returns {CKEDITOR.dom.element} The selected element. Null if no
+                * @returns {CKEDITOR.dom.element/null} The selected element. `null` if no
                 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
                 */
                getSelectedElement: function() {
 
                        if ( restore ) {
                                var selectedElement = this.getSelectedElement(),
-                                       ranges = !selectedElement && this.getRanges(),
+                                       ranges = this.getRanges(),
                                        faked = this.isFake;
                        }
 
                                if ( !( common && common.getAscendant( 'body', 1 ) ) )
                                        return;
 
-                               if ( faked )
+                               if ( isTableSelection( ranges ) ) {
+                                       // Tables have it's own selection method.
+                                       performFakeTableSelection.call( this, ranges );
+                               } else if ( faked )
                                        this.fake( selectedElement );
                                else if ( selectedElement )
                                        this.selectElement( selectedElement );
                        // Check if there's a hiddenSelectionContainer in editable at some index.
                        // Some ranges may be anchored after the hiddenSelectionContainer and,
                        // once the container is removed while resetting the selection, they
-                       // may need new endOffset (one element less within the range) (#11021 #11393).
+                       // may need new endOffset (one element less within the range) (http://dev.ckeditor.com/ticket/11021 http://dev.ckeditor.com/ticket/11393).
                        if ( hadHiddenSelectionContainer )
                                fixRangesAfterHiddenSelectionContainer( ranges, this.root );
 
                                return;
                        }
 
+                       // Handle special case - fake selection of table cells.
+                       if ( editor && editor.plugins.tableselection &&
+                               CKEDITOR.plugins.tableselection.isSupportedEnvironment &&
+                               isTableSelection( ranges ) && !isSelectingTable
+                       ) {
+                               performFakeTableSelection.call( this, ranges );
+                               return;
+                       }
+
                        if ( isMSSelection ) {
                                var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
                                        fillerTextRegex = /\ufeff|\u00a0/,
                                if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells ||
                                        range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) {
                                        range.shrink( CKEDITOR.NODE_ELEMENT, true );
-                                       // The range might get collapsed (#7975). Update cached variable.
+                                       // The range might get collapsed (http://dev.ckeditor.com/ticket/7975). Update cached variable.
                                        collapsed = range.collapsed;
                                }
 
 
                                        // Append a temporary <span>&#65279;</span> before the selection.
                                        // This is needed to avoid IE destroying selections inside empty
-                                       // inline elements, like <b></b> (#253).
+                                       // inline elements, like <b></b> (http://dev.ckeditor.com/ticket/253).
                                        // It is also needed when placing the selection right after an inline
                                        // element to avoid the selection moving inside of it.
                                        dummySpan = range.document.createElement( 'span' );
-                                       dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See #1359.
+                                       dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See http://dev.ckeditor.com/ticket/1359.
                                        dummySpan.insertBefore( startNode );
 
                                        if ( isStartMarkerAlone ) {
                                                // To expand empty blocks or line spaces after <br>, we need
                                                // instead to have any char, which will be later deleted using the
                                                // selection.
-                                               // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359)
+                                               // \ufeff = Zero Width No-Break Space (U+FEFF). (http://dev.ckeditor.com/ticket/1359)
                                                range.document.createText( '\ufeff' ).insertBefore( startNode );
                                        }
                                }
                        } else {
                                var sel = this.getNative();
 
-                               // getNative() returns null if iframe is "display:none" in FF. (#6577)
+                               // getNative() returns null if iframe is "display:none" in FF. (http://dev.ckeditor.com/ticket/6577)
                                if ( !sel )
                                        return;
 
                                                between.setStart( left.endContainer, left.endOffset );
                                                between.setEnd( right.startContainer, right.startOffset );
 
-                                               // Don't confused by Firefox adjancent multi-ranges
+                                               // Don't confused by Firefox adjacent multi-ranges
                                                // introduced by table cells selection.
                                                if ( !between.collapsed ) {
                                                        between.shrink( CKEDITOR.NODE_ELEMENT, true );
 
                                                        // The following cases has to be considered:
                                                        // 1. <span contenteditable="false">[placeholder]</span>
-                                                       // 2. <input contenteditable="false"  type="radio"/> (#6621)
+                                                       // 2. <input contenteditable="false"  type="radio"/> (http://dev.ckeditor.com/ticket/6621)
                                                        if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) {
                                                                right.setStart( left.startContainer, left.startOffset );
                                                                ranges.splice( i--, 1 );
 
                                        if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) {
                                                // Append a zero-width space so WebKit will not try to
-                                               // move the selection by itself (#1272).
+                                               // move the selection by itself (http://dev.ckeditor.com/ticket/1272).
                                                var fillingChar = createFillingCharSequenceNode( this.root );
                                                range.insertNode( fillingChar );
 
                fake: function( element, ariaLabel ) {
                        var editor = this.root.editor;
 
-                       // Attempt to retreive aria-label if possible (#14539).
+                       // Attempt to retrieve aria-label if possible (http://dev.ckeditor.com/ticket/14539).
                        if ( ariaLabel === undefined && element.hasAttribute( 'aria-label' ) ) {
                                ariaLabel = element.getAttribute( 'aria-label' );
                        }
                        return !!( el && el.data( 'cke-hidden-sel' ) );
                },
 
+               /**
+                * Checks if the selection contains an HTML element inside a table.
+                * Returns `false` for text selection inside a table (e.g. it will return `false`
+                * for text selected in one cell).
+                *
+                *              editor.getSelection().isInTable();
+                *
+                * @since 4.7.0
+                * @param {Boolean} [allowPartialSelection=false] Whether a partial cell selection should be included.
+                * Added in 4.7.2.
+                * @returns {Boolean}
+                */
+               isInTable: function( allowPartialSelection ) {
+                       return isTableSelection( this.getRanges(), allowPartialSelection );
+               },
+
+               /**
+                * Checks if the selection contains only one range which is collapsed.
+                *
+                *              if ( editor.getSelection().isCollapsed() ) {
+                *                      // Do something when the selection is collapsed.
+                *              }
+                *
+                * @since 4.7.3
+                * @returns {Boolean}
+                */
+               isCollapsed: function() {
+                       var ranges = this.getRanges();
+
+                       return ranges.length === 1 && ranges[ 0 ].collapsed;
+               },
+
                /**
                 * Creates a bookmark for each range of this selection (from {@link #getRanges})
                 * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
 
                        // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
                        if ( bookmarks.isFake ) {
-                               node = ranges[ 0 ].getEnclosedNode();
+                               node = isTableSelection( ranges ) ? ranges[ 0 ]._getTableElement() : ranges[ 0 ].getEnclosedNode();
+
                                if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) {
                                        CKEDITOR.warn( 'selection-not-fake' );
                                        bookmarks.isFake = 0;
                                }
                        }
 
-                       if ( bookmarks.isFake )
+                       if ( bookmarks.isFake && !isTableSelection( ranges ) ) {
                                this.fake( node );
-                       else
+                       } else {
                                this.selectRanges( ranges );
+                       }
 
                        return this;
                },
                 * Remove all the selection ranges from the document.
                 */
                removeAllRanges: function() {
-                       // Don't clear selection outside this selection's root (#11500).
+                       // Don't clear selection outside this selection's root (http://dev.ckeditor.com/ticket/11500).
                        if ( this.getType() == CKEDITOR.SELECTION_NONE )
                                return;