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 bar
+ +

simple text

diff --git a/tests/plugins/tableselection/integrations/clipboard/pasteflow.js b/tests/plugins/tableselection/integrations/clipboard/pasteflow.js index 8e111fe0e96..889496afb08 100644 --- a/tests/plugins/tableselection/integrations/clipboard/pasteflow.js +++ b/tests/plugins/tableselection/integrations/clipboard/pasteflow.js @@ -13,32 +13,50 @@ } }; - var tests = { - 'test paste flow': function( editor, bot ) { - bender.tools.testInputOut( 'simple-paste', function( source, expected ) { - var beforePasteStub = sinon.stub(), - pasteStub = sinon.stub(), - afterPasteStub = sinon.stub(); - bot.setHtmlWithSelection( source ); - - editor.on( 'beforePaste', beforePasteStub ); - editor.on( 'paste', pasteStub, null, null, 0 ); - editor.once( 'afterPaste', function() { - resume( function() { - afterPasteStub(); - - bender.assert.beautified.html( expected, bot.editor.getData() ); - - assert.areSame( 1, beforePasteStub.callCount, 'beforePaste even count' ); - assert.areSame( 1, pasteStub.callCount, 'paste event count' ); - assert.areSame( 1, afterPasteStub.callCount, 'afterPaste event count' ); - } ); + function testPasteFlow( bot, caseName, fixture ) { + var editor = bot.editor; + + bender.tools.testInputOut( caseName, function( source, expected ) { + var beforePasteStub = sinon.stub(), + pasteStub = sinon.stub(), + afterPasteStub = sinon.stub(), + removeBeforeStub, + removePasteStub, + removeAfterStub; + + bot.setHtmlWithSelection( source ); + + removeBeforeStub = editor.on( 'beforePaste', beforePasteStub ); + removePasteStub = editor.on( 'paste', pasteStub, null, null, 0 ); + removeAfterStub = editor.on( 'afterPaste', afterPasteStub ); + + editor.once( 'afterPaste', function() { + resume( function() { + bender.assert.beautified.html( expected, bot.editor.getData() ); + + removeBeforeStub.removeListener(); + removePasteStub.removeListener(); + removeAfterStub.removeListener(); + + assert.areSame( 1, beforePasteStub.callCount, 'beforePaste even count' ); + assert.areSame( 1, pasteStub.callCount, 'paste event count' ); + assert.areSame( 1, afterPasteStub.callCount, 'afterPaste event count' ); } ); + }, null, null, 999 ); + + bender.tools.emulatePaste( editor, CKEDITOR.document.getById( fixture ).getOuterHtml() ); - bender.tools.emulatePaste( editor, CKEDITOR.document.getById( '2cells1row' ).getOuterHtml() ); + wait(); + } ); + } + + var tests = { + 'test paste flow (tabular content)': function( editor, bot ) { + testPasteFlow( bot, 'tabular-paste', '2cells1row' ); + }, - wait(); - } ); + 'test paste flow (non-tabular content)': function( editor, bot ) { + testPasteFlow( bot, 'nontabular-paste', 'paragraph' ); } }; diff --git a/tests/plugins/tableselection/integrations/clipboard/pastemerge.html b/tests/plugins/tableselection/integrations/clipboard/pastemerge.html index 315468976fd..81fc644bcaa 100644 --- a/tests/plugins/tableselection/integrations/clipboard/pastemerge.html +++ b/tests/plugins/tableselection/integrations/clipboard/pastemerge.html @@ -164,6 +164,201 @@ + + + + + + + + @@ -181,4 +376,24 @@ -
foo21 22
\ No newline at end of file + + + + + + + + + + + + + + + + + + + + +
row1row1row1
row2row2row2
row3row3row3
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, 'xalt' ); + + wait(); + } + + var tests = { + 'the copied image to table shoud be initialized (collapsed selection)': function( editor, bot ) { + bot.setHtmlWithSelection( '
Cel^l
' ); + + editor.undoManager.reset(); + pasteImage( editor, function() { + testUndo( editor, [ 0 ] ); + } ); + }, + + 'the copied image to table shoud be initialized (multiple selection)': function( editor, bot ) { + bot.setHtmlWithSelection( '[][]
Cell 1Cell 2
' ); + + 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.1Cell 1.2
Cell 2.1Cell 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' ); },