X-Git-Url: https://git.immae.eu/?p=perso%2FImmae%2FProjets%2Fpackagist%2Fludivine-ckeditor-component.git;a=blobdiff_plain;f=sources%2Fcore%2Fselection.js;h=d44db3bc282dd62f268cd77ce2c20f34468436c3;hp=eef28a316f642b67a814984cbd418bfa32a5e5ac;hb=1794320dcfdfcd19572fb1676294f9853a6bbc20;hpb=7183f6a6a21ad9124e70c997e0168459f377a9f2 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 @@ ( 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 @@ -22,10 +224,9 @@ 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(); @@ -47,7 +248,7 @@ 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 ) @@ -97,7 +298,7 @@ // * is a visible node, // * is a non-empty element (this rule will accept elements like because they // they were not accepted by the isVisible() check, not not
which cannot absorb the caret). - // See #12621. + // See http://dev.ckeditor.com/ticket/12621. function mayAbsorbCaret( node ) { if ( isVisible( node ) ) return true; @@ -138,8 +339,8 @@ if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) ) return true; - // Empty block/inline element is also affected. ^,

^

(#7222) - // If you found this line confusing check #12655. + // Empty block/inline element is also affected. ^,

^

(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; @@ -155,7 +356,7 @@ 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' ); @@ -164,6 +365,7 @@ // creating it. if ( fillingChar.getCustomData( 'ready' ) ) { removeFillingCharSequenceNode( editable ); + editable.editor.fire( 'selectionCheck' ); } else { fillingChar.setCustomData( 'ready', 1 ); } @@ -175,7 +377,7 @@ 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. @@ -211,11 +413,11 @@ } } - // #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 { @@ -391,7 +593,7 @@ ( 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 ); @@ -525,7 +727,7 @@ // 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(); @@ -533,7 +735,7 @@ 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 ); @@ -553,7 +755,7 @@ // 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; } @@ -605,7 +807,7 @@ 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 ) @@ -624,7 +826,7 @@ // 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, @@ -661,7 +863,7 @@ 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(); } @@ -690,7 +892,7 @@ 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 ); } @@ -704,6 +906,14 @@ // 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 @@ -713,7 +923,7 @@ 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 ) ) { @@ -727,11 +937,11 @@ 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 ); } @@ -753,18 +963,18 @@ 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 ) {} @@ -785,14 +995,27 @@ 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. @@ -806,7 +1029,7 @@ 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 ); @@ -822,7 +1045,7 @@ // 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 ); @@ -869,7 +1092,7 @@ } ); // 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; @@ -884,7 +1107,7 @@ // 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 ); @@ -893,7 +1116,7 @@ // 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 ) { @@ -1138,7 +1361,7 @@ // 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 && @@ -1180,7 +1403,7 @@ * * 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 ) @@ -1272,7 +1495,7 @@ * 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() { @@ -1303,7 +1526,7 @@ 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 ); @@ -1319,8 +1542,8 @@ 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 ); @@ -1328,7 +1551,7 @@ // 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; @@ -1364,7 +1587,7 @@ // 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. @@ -1381,7 +1604,7 @@ } child = sibling; } - // Measurement in IE could be somtimes wrong because of element. (http://dev.ckeditor.com/ticket/4611) catch ( e ) { return { container: parent, offset: getNodeIndex( child ) }; } @@ -1413,7 +1636,7 @@ 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(); @@ -1445,7 +1668,7 @@ } )() : 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 = [], @@ -1478,7 +1701,7 @@ 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() ) ); }; } )(), @@ -1512,11 +1735,11 @@ // 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 @@ -1558,7 +1781,7 @@ * 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() { @@ -1641,7 +1864,7 @@ if ( restore ) { var selectedElement = this.getSelectedElement(), - ranges = !selectedElement && this.getRanges(), + ranges = this.getRanges(), faked = this.isFake; } @@ -1655,7 +1878,10 @@ 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 ); @@ -1727,7 +1953,7 @@ // 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 ); @@ -1755,6 +1981,15 @@ 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/, @@ -1788,7 +2023,7 @@ 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; } @@ -1830,18 +2065,18 @@ // Append a temporary  before the selection. // This is needed to avoid IE destroying selections inside empty - // inline elements, like (#253). + // inline elements, like (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( '' ); // Zero Width No-Break Space (U+FEFF). See #1359. + dummySpan.setHtml( '' ); // 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
, 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 ); } } @@ -1873,7 +2108,7 @@ } 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; @@ -1889,7 +2124,7 @@ 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 ); @@ -1898,7 +2133,7 @@ // The following cases has to be considered: // 1. [placeholder] - // 2. (#6621) + // 2. (http://dev.ckeditor.com/ticket/6621) if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) { right.setStart( left.startContainer, left.startOffset ); ranges.splice( i--, 1 ); @@ -1913,7 +2148,7 @@ 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 ); @@ -1972,7 +2207,7 @@ 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' ); } @@ -2030,6 +2265,38 @@ 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, @@ -2083,17 +2350,19 @@ // 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; }, @@ -2130,7 +2399,7 @@ * 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;