X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;ds=sidebyside;f=sources%2Fcore%2Fselection.js;h=d44db3bc282dd62f268cd77ce2c20f34468436c3;hb=1794320dcfdfcd19572fb1676294f9853a6bbc20;hp=eef28a316f642b67a814984cbd418bfa32a5e5ac;hpb=c63493c899de714b05b0521bb38aab60d19030ef;p=perso%2FImmae%2FProjets%2Fpackagist%2Fludivine-ckeditor-component.git
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