diff --git a/CHANGES.md b/CHANGES.md
index 82173e9b3f7..b6dc4d4546e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,7 @@ New Features:
Fixed Issues:
* [#663](https://github.com/ckeditor/ckeditor-dev/issues/663): [Chrome] Fixed: Clicking on scrollbar throws an `Uncaught TypeError: element.is is not a function` error.
+* [#520](https://github.com/ckeditor/ckeditor-dev/issues/520): Fixed: Widgets cannot be properly pasted into a table cell.
* [#579](https://github.com/ckeditor/ckeditor-dev/issues/579): Fixed: Internal `cke_table-faked-selection-table` class visible in Stylesheet Classes field in [Table Properties](http://ckeditor.com/addon/table) dialog.
* [#545](https://github.com/ckeditor/ckeditor-dev/issues/545): [Edge] Fixed: Error thrown when pressing the Select All button in a [Source Mode](http://ckeditor.com/addon/sourcearea).
* [#582](https://github.com/ckeditor/ckeditor-dev/issues/582): Fixed: Double slash in path to stylesheet needed by [Table Selection](http://ckeditor.com/addon/tableselection) plugin. Thanks to [Marius Dumitru Florea](https://github.com/mflorea)!
diff --git a/core/selection.js b/core/selection.js
index 9d555c806aa..856bac49649 100644
--- a/core/selection.js
+++ b/core/selection.js
@@ -2273,10 +2273,12 @@
* editor.getSelection().isInTable();
*
* @since 4.7.0
+ * @param {Boolean} [allowPartialSelection=false] Whether partial cell selection should be included.
+ * This parameter was added in 4.7.2.
* @returns {Boolean}
*/
- isInTable: function() {
- return isTableSelection( this.getRanges() );
+ isInTable: function( allowPartialSelection ) {
+ return isTableSelection( this.getRanges(), allowPartialSelection );
},
/**
diff --git a/plugins/tableselection/plugin.js b/plugins/tableselection/plugin.js
index 20738fc3876..ddf7237e84c 100644
--- a/plugins/tableselection/plugin.js
+++ b/plugins/tableselection/plugin.js
@@ -30,10 +30,6 @@
j,
cell;
- function getRowIndex( rowOrCell ) {
- return rowOrCell.getAscendant( 'tr', true ).$.rowIndex;
- }
-
// Support selection that began in outer's table, but ends in nested one.
if ( firstTable.contains( lastTable ) ) {
last = last.getAscendant( { td: 1, th: 1 } );
@@ -103,6 +99,22 @@
return domEvent.button === 0;
}
+ // Checks whether a given range fully contains a table element (cell/tbody/table etc).
+ // @param {CKEDITOR.dom.range} range
+ // @returns {Boolean}
+ function rangeContainsTableElement( range ) {
+ if ( range ) {
+ // Clone the range as we're going to enlarge it, and we don't want to modify the input.
+ range = range.clone();
+
+ range.enlarge( CKEDITOR.ENLARGE_ELEMENT );
+
+ var enclosedNode = range.getEnclosedNode();
+
+ return enclosedNode && enclosedNode.is && enclosedNode.is( CKEDITOR.dtd.$tableContent );
+ }
+ }
+
function getFakeSelectedTable( editor ) {
var selectedCell = editor.editable().findOne( '.' + fakeSelectedClass );
@@ -257,6 +269,10 @@
editor.fire( 'unlockSnapshot' );
}
+ function getRowIndex( rowOrCell ) {
+ return rowOrCell.getAscendant( 'tr', true ).$.rowIndex;
+ }
+
function fakeSelectionMouseHandler( evt ) {
// Prevent of throwing error in console if target is undefined (#515).
if ( !evt.data.getTarget().getName ) {
@@ -434,7 +450,7 @@
function fakeSelectionCopyCutHandler( evt ) {
var editor = evt.editor || evt.sender.editor,
- selection = editor.getSelection();
+ selection = editor.getSelection();
if ( !selection.isInTable() ) {
return;
@@ -443,68 +459,63 @@
copyTable( editor, evt.name === 'cut' );
}
- function fakeSelectionPasteHandler( evt ) {
- var editor = evt.editor,
- dataProcessor = editor.dataProcessor,
- selection = editor.getSelection(),
- tmpContainer = new CKEDITOR.dom.element( 'body' ),
- newRowsCount = 0,
- newColsCount = 0,
- pastedTableColCount = 0,
- selectedTableColCount = 0,
- markers = {},
- boundarySelection,
- selectedTable,
- selectedTableMap,
- selectedCells,
- pastedTable,
- pastedTableMap,
- firstCell,
- startIndex,
- firstRow,
- lastCell,
- endIndex,
- lastRow,
- currentRow,
- prevCell,
- cellToPaste,
- cellToReplace,
- i,
- j;
+ // A helper object abstracting table selection.
+ // By calling setSelectedCells() method it will automatically determine what's
+ // the first/last cell or row.
+ //
+ // Note: ATM the type does not make an actual selection, it just holds the data.
+ //
+ // @param {CKEDITOR.dom.element[]} [cells] An array of cells considered to be selected.
+ function TableSelection( cells ) {
+ this._reset();
+
+ if ( cells ) {
+ this.setSelectedCells( cells );
+ }
+ }
- // Check if the selection is collapsed on the beginning of the row (1) or at the end (2).
- function isBoundarySelection( selection ) {
- var ranges = selection.getRanges(),
- range = ranges[ 0 ],
- row = range.endContainer.getAscendant( 'tr', true );
+ TableSelection.prototype = {};
- if ( row && range.collapsed ) {
- if ( range.checkBoundaryOfElement( row, CKEDITOR.START ) ) {
- return 1;
- } else if ( range.checkBoundaryOfElement( row, CKEDITOR.END ) ) {
- return 2;
- }
- }
+ // Resets the initial state of table selection.
+ TableSelection.prototype._reset = function() {
+ this.cells = {
+ first: null,
+ last: null,
+ all: []
+ };
- return 0;
- }
+ this.rows = {
+ first: null,
+ last: null
+ };
+ };
- function getLongestRowLength( map ) {
- var longest = 0,
- i;
+ // Sets the cells that are selected in the table. Based on this it figures out what cell is
+ // the first, and the last. Also sets rows property accordingly.
+ // Note: ATM the type does not make an actual selection, it just holds the data.
+ //
+ // @param {CKEDITOR.dom.element[]} [cells] An array of cells considered to be selected.
+ TableSelection.prototype.setSelectedCells = function( cells ) {
+ this._reset();
+ // Make sure we're not modifying input array.
+ cells = cells.slice( 0 );
+ this._arraySortByDOMOrder( cells );
- for ( i = 0; i < map.length; i++ ) {
- if ( map[ i ].length > longest ) {
- longest = map[ i ].length;
- }
- }
+ this.cells.all = cells;
- return longest;
- }
+ this.cells.first = cells[ 0 ];
+ this.cells.last = cells[ cells.length - 1 ];
+
+ this.rows.first = cells[ 0 ].getAscendant( 'tr' );
+ this.rows.last = this.cells.last.getAscendant( 'tr' );
+ };
+ // Returns a table map, returned by {@link CKEDITOR.tools#buildTableMap}.
+ // @returns {HTMLElement[]}
+ TableSelection.prototype.getTableMap = function() {
function getRealCellPosition( cell ) {
var table = cell.getAscendant( 'table' ),
- rowIndex = cell.getParent().$.rowIndex,
+ rowIndex = getRowIndex( cell ),
map = CKEDITOR.tools.buildTableMap( table ),
i;
@@ -515,169 +526,311 @@
}
}
- if ( !dataProcessor ) {
- dataProcessor = new CKEDITOR.htmlDataProcessor( editor );
- }
+ var startIndex = getCellColIndex( this.cells.first, true ),
+ endIndex = getRealCellPosition( this.cells.last );
- // Pasted value must be filtered using dataProcessor to strip all unsafe code
- // before inserting it into temporary container.
- tmpContainer.setHtml( dataProcessor.toHtml( evt.data.dataValue ), {
- fixForBody: false
- } );
- pastedTable = tmpContainer.findOne( 'table' );
+ return CKEDITOR.tools.buildTableMap( this._getTable(), getRowIndex( this.rows.first ), startIndex,
+ getRowIndex( this.rows.last ), endIndex );
+ };
- if ( !selection.getRanges().length || !selection.isInTable() && !( boundarySelection = isBoundarySelection( selection ) ) ) {
+ TableSelection.prototype._getTable = function() {
+ return this.rows.first.getAscendant( 'table' );
+ };
+
+ // @param {Number} count Number of rows to be inserted.
+ // @param {Boolean} [insertBefore=false] If set to `true` new rows will be prepended.
+ // @param {Boolean} [clearSelection=false] If set to `true`, it will set selected cells to the one inserted.
+ TableSelection.prototype.insertRow = function( count, insertBefore, clearSelection ) {
+ if ( typeof count === 'undefined' ) {
+ count = 1;
+ } else if ( count <= 0 ) {
return;
}
- selectedCells = getSelectedCells( selection );
+ var cellIndexFirst = this.cells.first.$.cellIndex,
+ cellIndexLast = this.cells.last.$.cellIndex,
+ selectedCells = clearSelection ? [] : this.cells.all,
+ row,
+ newCells;
+
+ for ( var i = 0; i < count; i++ ) {
+ // In case of clearSelection we need explicitly use cached cells, as selectedCells is empty.
+ row = insertRow( clearSelection ? this.cells.all : selectedCells, insertBefore );
+
+ // Append cells from added row.
+ newCells = CKEDITOR.tools.array.filter( row.find( 'td, th' ).toArray(), function( cell ) {
+ return clearSelection ?
+ true : cell.$.cellIndex >= cellIndexFirst && cell.$.cellIndex <= cellIndexLast;
+ } );
+
+ // setSelectedCells will take care of refreshing the whole state at once.
+ if ( insertBefore ) {
+ selectedCells = newCells.concat( selectedCells );
+ } else {
+ selectedCells = selectedCells.concat( newCells );
+ }
+ }
+
+ this.setSelectedCells( selectedCells );
+ };
- if ( !selectedCells.length ) {
+ // @param {Number} count Number of columns to be inserted.
+ TableSelection.prototype.insertColumn = function( count ) {
+ if ( typeof count === 'undefined' ) {
+ count = 1;
+ } else if ( count <= 0 ) {
return;
}
- evt.stop();
+ var cells = this.cells,
+ selectedCells = cells.all,
+ minRowIndex = getRowIndex( cells.first ),
+ maxRowIndex = getRowIndex( cells.last );
- selectedTable = selectedCells[ 0 ].getAscendant( 'table' );
- selectedCells = getSelectedCells( selection, selectedTable );
- firstCell = selectedCells[ 0 ];
- firstRow = firstCell.getParent();
- lastCell = selectedCells[ selectedCells.length - 1 ];
- lastRow = lastCell.getParent();
-
- // Empty all selected cells.
- if ( !boundarySelection ) {
- for ( i = 0; i < selectedCells.length; i++ ) {
- selectedCells[ i ].setHtml( '' );
- }
+ function limitCells( cell ) {
+ var parentRowIndex = getRowIndex( cell );
+
+ return parentRowIndex >= minRowIndex && parentRowIndex <= maxRowIndex;
}
- // Handle mixed content (if the table is not the only child in the tmpContainer, we
- // are probably dealing with mixed content). We handle also non-table content here.
- if ( tmpContainer.getChildCount() > 1 || !pastedTable ) {
- selectedCells[ 0 ].setHtml( tmpContainer.getHtml() );
+ for ( var i = 0; i < count; i++ ) {
+ // Prepend added cells, then pass it to setSelectionCells so that it will take care of refreshing
+ // the whole state. Note that returned cells needs to be filtered, so that only cells that
+ // should get selected are added to the selectedCells array.
+ selectedCells = selectedCells.concat( CKEDITOR.tools.array.filter( insertColumn( selectedCells ), limitCells ) );
+ }
+
+ this.setSelectedCells( selectedCells );
+ };
- editor.fire( 'saveSnapshot' );
+ // Clears the content of selected cells.
+ //
+ // @param {CKEDITOR.dom.element[]} [cells] If given, this cells will be cleared.
+ TableSelection.prototype.emptyCells = function( cells ) {
+ cells = cells || this.cells.all;
- return;
+ for ( var i = 0; i < cells.length; i++ ) {
+ cells[ i ].setHtml( '' );
}
+ };
- // In case of boundary selection, insert new row before/after selected one, select it
- // and resume the rest of the algorithm.
- if ( boundarySelection ) {
- endIndex = firstRow.getChildCount();
- firstRow = lastRow = new CKEDITOR.dom.element( 'tr' );
- firstRow[ 'insert' + ( boundarySelection === 1 ? 'Before' : 'After' ) ]( firstCell.getParent() );
+ // Sorts given arr according to DOM position.
+ //
+ // @param {CKEDITOR.dom.node[]} arr
+ TableSelection.prototype._arraySortByDOMOrder = function( arr ) {
+ arr.sort( function( el1, el2 ) {
+ return el1.getPosition( el2 ) & CKEDITOR.POSITION_PRECEDING ? -1 : 1;
+ } );
+ };
- for ( i = 0; i < endIndex; i++ ) {
- firstCell = new CKEDITOR.dom.element( 'td' );
- firstCell.appendTo( firstRow );
+ var fakeSelectionPasteHandler = {
+ onPaste: pasteListener,
+ // Check if the selection is collapsed on the beginning of the row (1) or at the end (2).
+ isBoundarySelection: function( selection ) {
+ var ranges = selection.getRanges(),
+ range = ranges[ 0 ],
+ row = range.endContainer.getAscendant( 'tr', true );
+
+ if ( row && range.collapsed ) {
+ if ( range.checkBoundaryOfElement( row, CKEDITOR.START ) ) {
+ return 1;
+ } else if ( range.checkBoundaryOfElement( row, CKEDITOR.END ) ) {
+ return 2;
+ }
}
- firstCell = firstRow.getFirst();
- lastCell = firstRow.getLast();
+ return 0;
+ },
- selection.selectElement( firstRow );
- selectedCells = getSelectedCells( selection );
- }
+ // Looks for a table in a given pasted content string. Returns it as a
+ // CKEDITOR.dom.element instance or null if mixed content, or more than one table found.
+ findTableInPastedContent: function( editor, dataValue ) {
+ var dataProcessor = editor.dataProcessor,
+ tmpContainer = new CKEDITOR.dom.element( 'body' );
- // Build table map only for selected fragment.
- selectedTableMap = CKEDITOR.tools.buildTableMap( selectedTable, firstRow.$.rowIndex,
- getCellColIndex( firstCell, true ), lastRow.$.rowIndex, getRealCellPosition( lastCell ) );
- pastedTableMap = CKEDITOR.tools.buildTableMap( pastedTable );
+ if ( !dataProcessor ) {
+ dataProcessor = new CKEDITOR.htmlDataProcessor( editor );
+ }
+ // Pasted value must be filtered using dataProcessor to strip all unsafe code
+ // before inserting it into temporary container.
+ tmpContainer.setHtml( dataProcessor.toHtml( dataValue ), {
+ fixForBody: false
+ } );
- // Now we compare the dimensions of the pasted table and the selected one.
- // If the pasted one is bigger, we add missing rows and columns.
- pastedTableColCount = getLongestRowLength( pastedTableMap );
- selectedTableColCount = getLongestRowLength( selectedTableMap );
+ return tmpContainer.getChildCount() > 1 ? null : tmpContainer.findOne( 'table' );
+ },
- if ( pastedTableMap.length > selectedTableMap.length ) {
- newRowsCount = pastedTableMap.length - selectedTableMap.length;
+ // Performs an actual paste into selectedTableMap based on content in pastedTableMap.
+ pasteTable: function( tableSel, selectedTableMap, pastedTableMap ) {
+ var cellToReplace,
+ // Index of first selected cell, it needs to be reused later, to calculate the
+ // proper position of newly pasted cells.
+ startIndex = getCellColIndex( tableSel.cells.first, true ),
+ selectedTable = tableSel._getTable(),
+ markers = {},
+ currentRow,
+ prevCell,
+ cellToPaste,
+ i,
+ j;
+
+ // And now paste!
+ for ( i = 0; i < pastedTableMap.length; i++ ) {
+ currentRow = new CKEDITOR.dom.element( selectedTable.$.rows[ tableSel.rows.first.$.rowIndex + i ] );
+
+ for ( j = 0; j < pastedTableMap[ i ].length; j++ ) {
+ cellToPaste = new CKEDITOR.dom.element( pastedTableMap[ i ][ j ] );
+
+ if ( selectedTableMap[ i ] && selectedTableMap[ i ][ j ] ) {
+ cellToReplace = new CKEDITOR.dom.element( selectedTableMap[ i ][ j ] );
+ } else {
+ cellToReplace = null;
+ }
- for ( i = 0; i < newRowsCount; i++ ) {
- insertRow( selectedCells );
+ // Only try to paste cells that aren't already pasted (it can occur if the pasted cell
+ // has [colspan] or [rowspan]).
+ if ( cellToPaste && !cellToPaste.getCustomData( 'processed' ) ) {
+ // If the cell to being replaced has [colspan], it could have been already
+ // replaced. In that case, it won't have parent.
+ if ( cellToReplace && cellToReplace.getParent() ) {
+ cellToPaste.replace( cellToReplace );
+ } else if ( j === 0 || pastedTableMap[ i ][ j - 1 ] ) {
+ if ( j !== 0 ) {
+ prevCell = new CKEDITOR.dom.element( pastedTableMap[ i ][ j - 1 ] );
+ } else {
+ prevCell = null;
+ }
+
+ // If the cell that should be replaced is not in the table, we must cover at least 3 cases:
+ // 1. Pasting cell in the same row as the previous pasted cell.
+ // 2. Pasting cell into the next row at the proper position.
+ // 3. If the selection started from the left edge of the table,
+ // prepending the proper row with the cell.
+ if ( prevCell && currentRow.equals( prevCell.getParent() ) ) {
+ cellToPaste.insertAfter( prevCell );
+ } else if ( startIndex > 0 ) {
+ // It might happen that there's no cell with startIndex, as it might be used by a rowspan.
+ if ( currentRow.$.cells[ startIndex ] ) {
+ cellToPaste.insertAfter( new CKEDITOR.dom.element( currentRow.$.cells[ startIndex ] ) );
+ } else {
+ // Since rowspans are erased from current selection, we want need to append a cell.
+ currentRow.append( cellToPaste );
+ }
+ } else {
+ currentRow.append( cellToPaste, true );
+ }
+ }
+
+ CKEDITOR.dom.element.setMarker( markers, cellToPaste, 'processed', true );
+ } else if ( cellToPaste.getCustomData( 'processed' ) && cellToReplace ) {
+ // If the cell was already pasted, but the cell to replace still exists (e.g. pasted
+ // cell has [colspan]), remove it.
+ cellToReplace.remove();
+ }
+ }
}
+
+ CKEDITOR.dom.element.clearAllMarkers( markers );
}
+ };
- if ( pastedTableColCount > selectedTableColCount ) {
- newColsCount = pastedTableColCount - selectedTableColCount;
+ function pasteListener( evt ) {
+ var editor = evt.editor,
+ selection = editor.getSelection(),
+ selectedCells = getSelectedCells( selection ),
+ pastedTable = this.findTableInPastedContent( editor, evt.data.dataValue ),
+ boundarySelection = selection.isInTable( true ) && this.isBoundarySelection( selection ),
+ tableSel,
+ selectedTable,
+ selectedTableMap,
+ pastedTableMap;
- for ( i = 0; i < newColsCount; i++ ) {
- insertColumn( selectedCells );
- }
+ function getLongestRowLength( map ) {
+ return Math.max.apply( null, CKEDITOR.tools.array.map( map, function( rowMap ) {
+ return rowMap.length;
+ }, 0 ) );
}
- // Get all selected cells (original ones + newly inserted ones).
- firstCell = selectedCells[ 0 ];
- firstRow = firstCell.getParent();
- lastCell = selectedCells[ selectedCells.length - 1 ];
- lastRow = new CKEDITOR.dom.element( selectedTable.$.rows[ lastCell.getParent().$.rowIndex + newRowsCount ] );
- lastCell = lastRow.getChild( lastCell.$.cellIndex + newColsCount );
+ function selectCellContents( cell ) {
+ var range = editor.createRange();
- // These indexes would be reused later, to calculate the proper position of newly pasted cells.
- startIndex = getCellColIndex( selectedCells[ 0 ], true );
- endIndex = getRealCellPosition( lastCell );
+ range.selectNodeContents( cell );
+ range.select();
+ }
- // Rebuild map for selected table.
- selectedTableMap = CKEDITOR.tools.buildTableMap( selectedTable, firstRow.$.rowIndex, startIndex,
- lastRow.$.rowIndex, endIndex );
+ // Do not customize paste process in following cases:
+ // No cells are selected.
+ if ( !selectedCells.length ||
+ // It's single range that does not fully contain table element and is not boundary, e.g. collapsed selection within
+ // cell, part of cell etc.
+ ( selectedCells.length === 1 && !rangeContainsTableElement( selection.getRanges()[ 0 ] ) && !boundarySelection ) ||
+ // It's a boundary position but with no table pasted.
+ ( boundarySelection && !pastedTable ) ) {
+ return;
+ }
- // And now paste!
- for ( i = 0; i < pastedTableMap.length; i++ ) {
- currentRow = new CKEDITOR.dom.element( selectedTable.$.rows[ firstRow.$.rowIndex + i ] );
+ selectedTable = selectedCells[ 0 ].getAscendant( 'table' );
+ tableSel = new TableSelection( getSelectedCells( selection, selectedTable ) );
- for ( j = 0; j < pastedTableMap[ i ].length; j++ ) {
- cellToPaste = new CKEDITOR.dom.element( pastedTableMap[ i ][ j ] );
+ function getLastArrayItem( arr ) {
+ return arr[ arr.length - 1 ];
+ }
- if ( selectedTableMap[ i ] && selectedTableMap[ i ][ j ] ) {
- cellToReplace = new CKEDITOR.dom.element( selectedTableMap[ i ][ j ] );
- } else {
- cellToReplace = null;
- }
+ // Schedule selecting appropriate table cells after pasting. It covers both table and not-table
+ // content (#520).
+ editor.once( 'afterPaste', function() {
+ var toSelect = pastedTableMap ?
+ getCellsBetween( new CKEDITOR.dom.element( pastedTableMap[ 0 ][ 0 ] ),
+ new CKEDITOR.dom.element( getLastArrayItem( getLastArrayItem( pastedTableMap ) ) ) ) :
+ tableSel.cells.all;
- // Only try to paste cells that aren't already pasted (it can occur if the pasted cell
- // has [colspan] or [rowspan]).
- if ( cellToPaste && !cellToPaste.getCustomData( 'processed' ) ) {
- // If the cell to being replaced has [colspan], it could have been already
- // replaced. In that case, it won't have parent.
- if ( cellToReplace && cellToReplace.getParent() ) {
- cellToPaste.replace( cellToReplace );
- } else if ( j === 0 || pastedTableMap[ i ][ j - 1 ] ) {
- if ( j !== 0 ) {
- prevCell = new CKEDITOR.dom.element( pastedTableMap[ i ][ j - 1 ] );
- } else {
- prevCell = null;
- }
+ fakeSelectCells( editor, toSelect );
+ } );
- // If the cell that should be replaced is not in the table, we must cover at least 3 cases:
- // 1. Pasting cell in the same row as the previous pasted cell.
- // 2. Pasting cell into the next row at the proper position.
- // 3. If the selection started from the left edge of the table,
- // prepending the proper row with the cell.
- if ( prevCell && currentRow.equals( prevCell.getParent() ) ) {
- cellToPaste.insertAfter( prevCell );
- } else if ( startIndex > 0 ) {
- cellToPaste.insertAfter( new CKEDITOR.dom.element( currentRow.$.cells[ startIndex ] ) );
- } else {
- currentRow.append( cellToPaste, true );
- }
- }
+ // In case of mixed content or non table content just select first cell, and erase content of other selected cells.
+ // Selection is left in first cell, so that default CKEditor logic puts pasted content in the selection (#520).
+ if ( !pastedTable ) {
+ selectCellContents( tableSel.cells.first );
+
+ // Due to limitations of our undo manager, in case of mixed content
+ // cells must be emptied after pasting (#520).
+ editor.once( 'afterPaste', function() {
+ editor.fire( 'lockSnapshot' );
+ tableSel.emptyCells( tableSel.cells.all.slice( 1 ) );
+ editor.fire( 'unlockSnapshot' );
+ } );
- CKEDITOR.dom.element.setMarker( markers, cellToPaste, 'processed', true );
- } else if ( cellToPaste.getCustomData( 'processed' ) && cellToReplace ) {
- // If the cell was already pasted, but the cell to replace still exists (e.g. pasted
- // cell has [colspan]), remove it.
- cellToReplace.remove();
- }
- }
+ return;
}
- CKEDITOR.dom.element.clearAllMarkers( markers );
+ // Preventing other paste handlers should be done after all early returns (#520).
+ evt.stop();
+
+ // In case of boundary selection, insert new row before/after selected one, select it
+ // and resume the rest of the algorithm.
+ if ( boundarySelection ) {
+ tableSel.insertRow( 1, boundarySelection === 1, true );
+ selection.selectElement( tableSel.rows.first );
+ } else {
+ // Otherwise simply clear all the selected cells.
+ tableSel.emptyCells();
+ }
+
+ // Build table map only for selected fragment.
+ selectedTableMap = tableSel.getTableMap();
+ pastedTableMap = CKEDITOR.tools.buildTableMap( pastedTable );
+
+ tableSel.insertRow( pastedTableMap.length - selectedTableMap.length );
+
+ // Now we compare the dimensions of the pasted table and the selected one.
+ // If the pasted one is bigger, we add missing rows and columns.
+ tableSel.insertColumn( getLongestRowLength( pastedTableMap ) - getLongestRowLength( selectedTableMap ) );
+
+ // Rebuild map for selected table.
+ selectedTableMap = tableSel.getTableMap();
- // Select newly pasted cells.
- fakeSelectCells( editor,
- getCellsBetween( new CKEDITOR.dom.element( pastedTableMap[ 0 ][ 0 ] ), cellToPaste ) );
+ this.pasteTable( tableSel, selectedTableMap, pastedTableMap );
editor.fire( 'saveSnapshot' );
@@ -959,7 +1112,7 @@
}
} );
- editor.on( 'paste', fakeSelectionPasteHandler );
+ editor.on( 'paste', fakeSelectionPasteHandler.onPaste, fakeSelectionPasteHandler );
customizeTableCommand( editor, [
'rowInsertBefore',
diff --git a/plugins/tabletools/plugin.js b/plugins/tabletools/plugin.js
index f415c018f4c..23d961cd177 100644
--- a/plugins/tabletools/plugin.js
+++ b/plugins/tabletools/plugin.js
@@ -8,13 +8,14 @@
isArray = CKEDITOR.tools.isArray;
function getSelectedCells( selection, table ) {
+ var retval = [],
+ database = {};
+
if ( !selection ) {
- return;
+ return retval;
}
var ranges = selection.getRanges();
- var retval = [];
- var database = {};
function isInTable( cell ) {
if ( !table ) {
@@ -146,6 +147,8 @@
}
insertBefore ? newRow.insertBefore( row ) : newRow.insertAfter( row );
+
+ return newRow;
}
function deleteRows( selectionOrRow ) {
@@ -264,6 +267,7 @@
var map = CKEDITOR.tools.buildTableMap( table ),
cloneCol = [],
nextCol = [],
+ addedCells = [],
height = map.length;
for ( var i = 0; i < height; i++ ) {
@@ -288,11 +292,14 @@
cell.removeAttribute( 'colSpan' );
cell.appendBogus();
cell[ insertBefore ? 'insertBefore' : 'insertAfter' ].call( cell, originalCell );
+ addedCells.push( cell );
cell = cell.$;
}
i += cell.rowSpan - 1;
}
+
+ return addedCells;
}
function deleteColumns( selection ) {
diff --git a/tests/plugins/tableselection/_helpers/tableselection.js b/tests/plugins/tableselection/_helpers/tableselection.js
index df229e95d67..caf5c79eedc 100644
--- a/tests/plugins/tableselection/_helpers/tableselection.js
+++ b/tests/plugins/tableselection/_helpers/tableselection.js
@@ -79,16 +79,17 @@
window.createPasteTestCase = function( fixtureId, pasteFixtureId ) {
return function( editor, bot ) {
bender.tools.testInputOut( fixtureId, function( source, expected ) {
- editor.once( 'paste', function() {
+ editor.once( 'afterPaste', function() {
resume( function() {
shrinkSelections( editor );
bender.assert.beautified.html( expected, bender.tools.getHtmlWithSelection( editor ) );
} );
- }, null, null, 1 );
+ }, null, null, 999 );
bot.setHtmlWithSelection( source );
- bender.tools.emulatePaste( editor, CKEDITOR.document.getById( pasteFixtureId ).getOuterHtml() );
+ // Use clone, so that pasted table does not have an ID.
+ bender.tools.emulatePaste( editor, CKEDITOR.document.getById( pasteFixtureId ).clone( true ).getOuterHtml() );
wait();
} );
diff --git a/tests/plugins/tableselection/integrations/clipboard/pasteflow.html b/tests/plugins/tableselection/integrations/clipboard/pasteflow.html
index a3e79800bab..8214c6572f9 100644
--- a/tests/plugins/tableselection/integrations/clipboard/pasteflow.html
+++ b/tests/plugins/tableselection/integrations/clipboard/pasteflow.html
@@ -1,4 +1,4 @@
-
+
+
+
+
+
+
+
+
foo |
@@ -181,4 +376,24 @@
21 |
22 |
-
\ No newline at end of file
+
+
+
+
+
+ row1 |
+ row1 |
+ row1 |
+
+
+ row2 |
+ row2 |
+ row2 |
+
+
+ row3 |
+ row3 |
+ row3 |
+
+
+
diff --git a/tests/plugins/tableselection/integrations/clipboard/pastemerge.js b/tests/plugins/tableselection/integrations/clipboard/pastemerge.js
index f54c7435e42..ce4f04d34c2 100644
--- a/tests/plugins/tableselection/integrations/clipboard/pastemerge.js
+++ b/tests/plugins/tableselection/integrations/clipboard/pastemerge.js
@@ -34,7 +34,16 @@
'test merge multi rows after empty cell': createPasteTestCase( 'merge-rows-after-empty', '2cells2rows' ),
- 'test merge multi rows before empty cell': createPasteTestCase( 'merge-rows-before-empty', '2cells2rows' )
+ 'test merge multi rows before empty cell': createPasteTestCase( 'merge-rows-before-empty', '2cells2rows' ),
+
+ 'test table expansion on larger table': createPasteTestCase( 'merge-larger-table', '3cells3rows' ),
+
+ 'test partial paste doesnt cause merge': createPasteTestCase( 'dont-merge-partial-selection', '2cells1row' ),
+
+ 'test paste bigger table into a smaller selection': createPasteTestCase( 'merge-bigger-table', '3cells3rows' ),
+
+ // This case includes some rowspan trickery.
+ 'test paste smaller table into a bigger selection edge case': createPasteTestCase( 'merge-smaller-table-edge', '3cells3rows' )
};
tests = bender.tools.createTestsForEditors( CKEDITOR.tools.objectKeys( bender.editors ), tests );
diff --git a/tests/plugins/tableselection/integrations/image2/_assets/bar.png b/tests/plugins/tableselection/integrations/image2/_assets/bar.png
new file mode 100644
index 00000000000..e54c62fcb2c
Binary files /dev/null and b/tests/plugins/tableselection/integrations/image2/_assets/bar.png differ
diff --git a/tests/plugins/tableselection/integrations/image2/paste.js b/tests/plugins/tableselection/integrations/image2/paste.js
new file mode 100644
index 00000000000..cc81fdaa91a
--- /dev/null
+++ b/tests/plugins/tableselection/integrations/image2/paste.js
@@ -0,0 +1,90 @@
+/* bender-tags: editor,unit,widget */
+/* bender-ckeditor-plugins: image2,undo,tableselection */
+/* bender-include: ../../_helpers/tableselection.js */
+/* global tableSelectionHelpers */
+
+( function() {
+ 'use strict';
+
+ bender.editors = {
+ classic: {},
+ inline: {
+ creator: 'inline'
+ }
+ };
+
+ function testUndo( editor, selectedCells ) {
+ var undoManager = editor.undoManager,
+ ranges = editor.getSelection().getRanges(),
+ cells = editor.editable().find( 'td, th' ),
+ i;
+
+ editor.once( 'afterCommandExec', function() {
+ resume( function() {
+ ranges = editor.getSelection().getRanges();
+ cells = editor.editable().find( 'td, th' );
+
+ assert.isFalse( undoManager.undoable(), 'Paste generated only 1 undo step' );
+ assert.isTrue( undoManager.redoable(), 'Paste can be repeated' );
+
+ // Until issue with selecting only first cell after undo, this assertion
+ // does not make any sense.
+ //assert.areSame( selectedCells.length, ranges.length, 'Appropriate number of ranges are selected' );
+
+ for ( i = 0; i < ranges.length; i++ ) {
+ var cell = ranges[ i ]._getTableElement();
+
+ assert.isTrue( cell.equals( cells.getItem( selectedCells[ i ] ) ),
+ 'Appropriate cell is selected' );
+ }
+ } );
+ } );
+
+ editor.execCommand( 'undo' );
+
+ wait();
+ }
+
+ function pasteImage( editor, callback ) {
+ editor.once( 'afterPaste', function() {
+ resume( function() {
+ var images = editor.editable().find( '.cke_widget_image' );
+
+ assert.areSame( 1, images.count(), 'There is only one image' );
+ assert.isFalse( images.getItem( 0 ).hasClass( 'cke_widget_new' ), 'The image widget is initialized' );
+
+ callback();
+ } );
+ } );
+
+ bender.tools.emulatePaste( editor, '' );
+
+ wait();
+ }
+
+ var tests = {
+ 'the copied image to table shoud be initialized (collapsed selection)': function( editor, bot ) {
+ bot.setHtmlWithSelection( '' );
+
+ editor.undoManager.reset();
+ pasteImage( editor, function() {
+ testUndo( editor, [ 0 ] );
+ } );
+ },
+
+ 'the copied image to table shoud be initialized (multiple selection)': function( editor, bot ) {
+ bot.setHtmlWithSelection( '' );
+
+ editor.undoManager.reset();
+ pasteImage( editor, function() {
+ testUndo( editor, [ 0, 1 ] );
+ } );
+ }
+ };
+
+ tests = bender.tools.createTestsForEditors( CKEDITOR.tools.objectKeys( bender.editors ), tests );
+
+ tableSelectionHelpers.ignoreUnsupportedEnvironment( tests );
+
+ bender.test( tests );
+} )();
diff --git a/tests/plugins/tableselection/integrations/tabletools/tabletools.js b/tests/plugins/tableselection/integrations/tabletools/tabletools.js
index 3b503dc81b6..ecbe6c7cd61 100644
--- a/tests/plugins/tableselection/integrations/tabletools/tabletools.js
+++ b/tests/plugins/tableselection/integrations/tabletools/tabletools.js
@@ -161,6 +161,14 @@
assert.isTrue( expectedCell.equals( selectedCells[ 0 ] ), 'Correct table cell is selected.' );
},
+ 'test getSelectedCells API': function() {
+ arrayAssert.itemsAreEqual( [], CKEDITOR.plugins.tabletools.getSelectedCells( null ), 'Return for null' );
+
+ var emptySelectionMock = { getRanges: sinon.stub().returns( [ ] ) };
+
+ arrayAssert.itemsAreEqual( [], CKEDITOR.plugins.tabletools.getSelectedCells( emptySelectionMock ), 'Ret for empty range list' );
+ },
+
'test delete all cells': function( editor, bot ) {
doCommandTest( bot, 'cellDelete', { 'case': 'delete-all-cells', cells: [ 0, 1, 2, 3 ], skipCheckingSelection: true } );
}
diff --git a/tests/plugins/tableselection/manual/integrations/image2/_assets/bar.png b/tests/plugins/tableselection/manual/integrations/image2/_assets/bar.png
new file mode 100644
index 00000000000..e54c62fcb2c
Binary files /dev/null and b/tests/plugins/tableselection/manual/integrations/image2/_assets/bar.png differ
diff --git a/tests/plugins/tableselection/manual/integrations/image2/paste.html b/tests/plugins/tableselection/manual/integrations/image2/paste.html
new file mode 100644
index 00000000000..8769c4ae530
--- /dev/null
+++ b/tests/plugins/tableselection/manual/integrations/image2/paste.html
@@ -0,0 +1,21 @@
+
+
+
+
+ Cell 1.1 |
+ Cell 1.2 |
+
+
+ Cell 2.1 |
+ Cell 2.2 |
+
+
+
+
+
diff --git a/tests/plugins/tableselection/manual/integrations/image2/paste.md b/tests/plugins/tableselection/manual/integrations/image2/paste.md
new file mode 100644
index 00000000000..cc4a80a0c70
--- /dev/null
+++ b/tests/plugins/tableselection/manual/integrations/image2/paste.md
@@ -0,0 +1,45 @@
+@bender-ui: collapsed
+@bender-tags: tc, 520, 4.7.2
+@bender-ckeditor-plugins: wysiwygarea, toolbar, tableselection, elementspath, undo, image2
+
+## Case 1
+
+1. Copy image.
+2. Put cursor in one of the cells.
+3. Paste image.
+4. Click image.
+
+**Expected result:**
+
+Image is inserted as widget.
+
+**Unexpected result:**
+
+Image is not a widget.
+
+
+## Case 2
+
+1. Copy image.
+2. Select some cells.
+3. Paste image.
+4. Click image.
+
+**Expected result:**
+
+* Image is inserted as widget into the first selected cell.
+* Other cells are emptied.
+* All selected cells are still selected.
+
+**Unexpected result:**
+
+* Image is not inserted.
+* Selection is modified.
+* There are selection-connected errors in browser's console.
+
+## Undo/redo
+
+After pasting check if undo/redo is working correctly:
+
+* The undo is enabled.
+* Paste generates only one snapshot.
diff --git a/tests/plugins/tabletools/tabletools.html b/tests/plugins/tabletools/tabletools.html
index 9701eaa4100..03d1d77ddce 100644
--- a/tests/plugins/tabletools/tabletools.html
+++ b/tests/plugins/tabletools/tabletools.html
@@ -1530,4 +1530,6 @@
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/tests/plugins/tabletools/tabletools.js b/tests/plugins/tabletools/tabletools.js
index e5b3fcf6eca..0fd1518585f 100644
--- a/tests/plugins/tabletools/tabletools.js
+++ b/tests/plugins/tabletools/tabletools.js
@@ -38,6 +38,22 @@
this.doTest( 'add-row-after-multi', 'rowInsertAfter' );
},
+ 'test tabletools.insertRow() return value': function() {
+ var doc = CKEDITOR.document,
+ playground = doc.getById( 'playground' ),
+ table,
+ ret;
+
+ playground.setHtml( doc.findOne( '#row-height-conversion' ).getValue() );
+
+ table = playground.findOne( 'table' );
+
+ ret = CKEDITOR.plugins.tabletools.insertRow( [ table.findOne( 'td' ) ] );
+
+ assert.isInstanceOf( CKEDITOR.dom.element, ret, 'Returned type' );
+ assert.areSame( table.find( 'tr' ).getItem( 1 ), ret, 'Returned element' );
+ },
+
'test insert col before': function() {
this.doTest( 'add-col-before', 'columnInsertBefore' );
this.doTest( 'add-col-before-2', 'columnInsertBefore' );
@@ -55,6 +71,24 @@
this.doTest( 'add-col-after-multi', 'columnInsertAfter' );
},
+ 'test tabletools.insertColumn() return value': function() {
+ var doc = CKEDITOR.document,
+ playground = doc.getById( 'playground' ),
+ table,
+ ret;
+
+ playground.setHtml( doc.findOne( '#delete-cell-trailing' ).getValue() );
+
+ table = playground.findOne( 'table' );
+
+ ret = CKEDITOR.plugins.tabletools.insertColumn( [ table.find( 'td' ).getItem( 1 ), table.find( 'td' ).getItem( 3 ) ] );
+
+ assert.isArray( ret, 'Return type' );
+ assert.areSame( 2, ret.length, 'Returned items' );
+ assert.areSame( table.find( 'td' ).getItem( 2 ), ret[ 0 ], 'Returned element 0' );
+ assert.areSame( table.find( 'td' ).getItem( 5 ), ret[ 1 ], 'Returned element 1' );
+ },
+
'test merge cells': function() {
this.doTest( 'merge-cells', 'cellMerge' );
},