8
0
Pārlūkot izejas kodu

Merge branch 'master' into i/6621-select-all

Tomek Wytrębowicz 5 gadi atpakaļ
vecāks
revīzija
92bfde38ab

+ 3 - 1
.travis.yml

@@ -17,7 +17,9 @@ before_install:
 install:
 - yarn install
 script:
-- ./scripts/continuous-integration-run-tests.sh
+- node ./scripts/continuous-integration-script.js
+- yarn run lint
+- yarn run stylelint
 - yarn run docs:api --validate-only
 - 'if [ $TRAVIS_TEST_RESULT -eq 0 ]; then
     travis_wait 30 yarn run docs:build-and-publish-nightly;

+ 4 - 1
package.json

@@ -79,7 +79,7 @@
     "@ckeditor/ckeditor5-comments": "^19.0.1",
     "@ckeditor/ckeditor5-dev-docs": "^11.1.0",
     "@ckeditor/ckeditor5-dev-env": "^18.0.0",
-    "@ckeditor/ckeditor5-dev-tests": "^19.0.0",
+    "@ckeditor/ckeditor5-dev-tests": "^19.1.0",
     "@ckeditor/ckeditor5-dev-utils": "^13.0.0",
     "@ckeditor/ckeditor5-dev-webpack-plugin": "^9.0.0",
     "@ckeditor/ckeditor5-inspector": "^2.0.0",
@@ -88,6 +88,7 @@
     "@ckeditor/ckeditor5-track-changes": "^19.0.1",
     "@wiris/mathtype-ckeditor5": "^7.19.0",
     "babel-standalone": "^6.26.0",
+    "coveralls": "^3.1.0",
     "css-loader": "^1.0.0",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^2.0.0",
@@ -159,6 +160,8 @@
   },
   "eslintIgnore": [
     "build/**",
+    "packages/*/build/**",
+    "packages/*/src/lib/**",
     "coverage/**"
   ],
   "workspaces": {

+ 2 - 1
packages/ckeditor5-table/package.json

@@ -30,7 +30,8 @@
     "@ckeditor/ckeditor5-typing": "^19.0.0",
     "@ckeditor/ckeditor5-undo": "^19.0.0",
     "@ckeditor/ckeditor5-utils": "^19.0.0",
-    "json-diff": "^0.5.4"
+    "json-diff": "^0.5.4",
+    "lodash-es": "^4.17.10"
   },
   "engines": {
     "node": ">=8.0.0",

+ 6 - 25
packages/ckeditor5-table/src/commands/mergecellcommand.js

@@ -9,10 +9,7 @@
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import TableWalker from '../tablewalker';
-import {
-	updateNumericAttribute,
-	isHeadingColumnCell
-} from './utils';
+import { isHeadingColumnCell, findAncestor } from './utils';
 import { getTableCellsContainingSelection } from '../utils';
 
 /**
@@ -83,6 +80,7 @@ export default class MergeCellCommand extends Command {
 		const model = this.editor.model;
 		const doc = model.document;
 		const tableCell = getTableCellsContainingSelection( doc.selection )[ 0 ];
+
 		const cellToMerge = this.value;
 		const direction = this.direction;
 
@@ -108,7 +106,10 @@ export default class MergeCellCommand extends Command {
 
 			// Remove empty row after merging.
 			if ( !removedTableCellRow.childCount ) {
-				removeEmptyRow( removedTableCellRow, writer );
+				const tableUtils = this.editor.plugins.get( 'TableUtils' );
+				const table = findAncestor( 'table', removedTableCellRow );
+
+				tableUtils.removeRows( table, { at: removedTableCellRow.index, batch: writer.batch } );
 			}
 		} );
 	}
@@ -243,26 +244,6 @@ function getVerticalCell( tableCell, direction ) {
 	return cellToMergeData && cellToMergeData.cell;
 }
 
-// Properly removes an empty row from a table. It will update the `rowspan` attribute of cells that overlap the removed row.
-//
-// @param {module:engine/model/element~Element} removedTableCellRow
-// @param {module:engine/model/writer~Writer} writer
-function removeEmptyRow( removedTableCellRow, writer ) {
-	const table = removedTableCellRow.parent;
-
-	const removedRowIndex = table.getChildIndex( removedTableCellRow );
-
-	for ( const { cell, row, rowspan } of new TableWalker( table, { endRow: removedRowIndex } ) ) {
-		const overlapsRemovedRow = row + rowspan - 1 >= removedRowIndex;
-
-		if ( overlapsRemovedRow ) {
-			updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer );
-		}
-	}
-
-	writer.remove( removedTableCellRow );
-}
-
 // Merges two table cells. It will ensure that after merging cells with an empty paragraph, the resulting table cell will only have one
 // paragraph. If one of the merged table cells is empty, the merged table cell will have the contents of the non-empty table cell.
 // If both are empty, the merged table cell will have only one empty paragraph.

+ 13 - 25
packages/ckeditor5-table/src/commands/mergecellscommand.js

@@ -8,7 +8,6 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import TableWalker from '../tablewalker';
 import { findAncestor, updateNumericAttribute } from './utils';
 import TableUtils from '../tableutils';
 import { getColumnIndexes, getRowIndexes, getSelectedTableCells } from '../utils';
@@ -57,38 +56,27 @@ export default class MergeCellsCommand extends Command {
 			updateNumericAttribute( 'colspan', mergeWidth, firstTableCell, writer );
 			updateNumericAttribute( 'rowspan', mergeHeight, firstTableCell, writer );
 
+			const emptyRowsIndexes = [];
+
 			for ( const tableCell of selectedTableCells ) {
 				const tableRow = tableCell.parent;
-				mergeTableCells( tableCell, firstTableCell, writer );
-				removeRowIfEmpty( tableRow, writer );
-			}
 
-			writer.setSelection( firstTableCell, 'in' );
-		} );
-	}
-}
+				mergeTableCells( tableCell, firstTableCell, writer );
 
-// Properly removes an empty row from a table. Updates the `rowspan` attribute of cells that overlap the removed row.
-//
-// @param {module:engine/model/element~Element} row
-// @param {module:engine/model/writer~Writer} writer
-function removeRowIfEmpty( row, writer ) {
-	if ( row.childCount ) {
-		return;
-	}
+				if ( !tableRow.childCount ) {
+					emptyRowsIndexes.push( tableRow.index );
+				}
+			}
 
-	const table = row.parent;
-	const removedRowIndex = table.getChildIndex( row );
+			if ( emptyRowsIndexes.length ) {
+				const table = findAncestor( 'table', firstTableCell );
 
-	for ( const { cell, row, rowspan } of new TableWalker( table, { endRow: removedRowIndex } ) ) {
-		const overlapsRemovedRow = row + rowspan - 1 >= removedRowIndex;
+				emptyRowsIndexes.reverse().forEach( row => tableUtils.removeRows( table, { at: row, batch: writer.batch } ) );
+			}
 
-		if ( overlapsRemovedRow ) {
-			updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer );
-		}
+			writer.setSelection( firstTableCell, 'in' );
+		} );
 	}
-
-	writer.remove( row );
 }
 
 // Merges two table cells. It will ensure that after merging cells with empty paragraphs the resulting table cell will only have one

+ 23 - 11
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js

@@ -32,6 +32,19 @@ import { debounce } from 'lodash-es';
 
 const ERROR_TEXT_TIMEOUT = 500;
 
+// Map of view properties and related commands.
+const propertyToCommandMap = {
+	borderStyle: 'tableCellBorderStyle',
+	borderColor: 'tableCellBorderColor',
+	borderWidth: 'tableCellBorderWidth',
+	width: 'tableCellWidth',
+	height: 'tableCellHeight',
+	padding: 'tableCellPadding',
+	backgroundColor: 'tableCellBackgroundColor',
+	horizontalAlignment: 'tableCellHorizontalAlignment',
+	verticalAlignment: 'tableCellVerticalAlignment'
+};
+
 /**
  * The table cell properties UI plugin. It introduces the `'tableCellProperties'` button
  * that opens a form allowing to specify the visual styling of a table cell.
@@ -110,6 +123,13 @@ export default class TableCellPropertiesUI extends Plugin {
 
 			this.listenTo( view, 'execute', () => this._showView() );
 
+			const commands = Object.values( propertyToCommandMap )
+				.map( commandName => editor.commands.get( commandName ) );
+
+			view.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => (
+				areEnabled.some( isCommandEnabled => isCommandEnabled )
+			) );
+
 			return view;
 		} );
 	}
@@ -256,17 +276,9 @@ export default class TableCellPropertiesUI extends Plugin {
 	_fillViewFormFromCommandValues() {
 		const commands = this.editor.commands;
 
-		this.view.set( {
-			borderStyle: commands.get( 'tableCellBorderStyle' ).value || '',
-			borderColor: commands.get( 'tableCellBorderColor' ).value || '',
-			borderWidth: commands.get( 'tableCellBorderWidth' ).value || '',
-			width: commands.get( 'tableCellWidth' ).value || '',
-			height: commands.get( 'tableCellHeight' ).value || '',
-			padding: commands.get( 'tableCellPadding' ).value || '',
-			backgroundColor: commands.get( 'tableCellBackgroundColor' ).value || '',
-			horizontalAlignment: commands.get( 'tableCellHorizontalAlignment' ).value || '',
-			verticalAlignment: commands.get( 'tableCellVerticalAlignment' ).value || ''
-		} );
+		Object.entries( propertyToCommandMap )
+			.map( ( [ property, commandName ] ) => [ property, commands.get( commandName ).value || '' ] )
+			.forEach( ( [ property, value ] ) => this.view.set( property, value ) );
 	}
 
 	/**

+ 21 - 9
packages/ckeditor5-table/src/tableproperties/tablepropertiesui.js

@@ -32,6 +32,17 @@ import { debounce } from 'lodash-es';
 
 const ERROR_TEXT_TIMEOUT = 500;
 
+// Map of view properties and related commands.
+const propertyToCommandMap = {
+	borderStyle: 'tableBorderStyle',
+	borderColor: 'tableBorderColor',
+	borderWidth: 'tableBorderWidth',
+	backgroundColor: 'tableBackgroundColor',
+	width: 'tableWidth',
+	height: 'tableHeight',
+	alignment: 'tableAlignment'
+};
+
 /**
  * The table properties UI plugin. It introduces the `'tableProperties'` button
  * that opens a form allowing to specify visual styling of an entire table.
@@ -110,6 +121,13 @@ export default class TablePropertiesUI extends Plugin {
 
 			this.listenTo( view, 'execute', () => this._showView() );
 
+			const commands = Object.values( propertyToCommandMap )
+				.map( commandName => editor.commands.get( commandName ) );
+
+			view.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => (
+				areEnabled.some( isCommandEnabled => isCommandEnabled )
+			) );
+
 			return view;
 		} );
 	}
@@ -248,15 +266,9 @@ export default class TablePropertiesUI extends Plugin {
 	_fillViewFormFromCommandValues() {
 		const commands = this.editor.commands;
 
-		this.view.set( {
-			borderStyle: commands.get( 'tableBorderStyle' ).value || '',
-			borderColor: commands.get( 'tableBorderColor' ).value || '',
-			borderWidth: commands.get( 'tableBorderWidth' ).value || '',
-			backgroundColor: commands.get( 'tableBackgroundColor' ).value || '',
-			width: commands.get( 'tableWidth' ).value || '',
-			height: commands.get( 'tableHeight' ).value || '',
-			alignment: commands.get( 'tableAlignment' ).value || ''
-		} );
+		Object.entries( propertyToCommandMap )
+			.map( ( [ property, commandName ] ) => [ property, commands.get( commandName ).value || '' ] )
+			.forEach( ( [ property, value ] ) => this.view.set( property, value ) );
 	}
 
 	/**

+ 27 - 16
packages/ckeditor5-table/src/tableutils.js

@@ -289,17 +289,21 @@ export default class TableUtils extends Plugin {
 		const last = first + rowsToRemove - 1;
 		const batch = options.batch || 'default';
 
-		// Removing rows from table requires most calculations to be done prior to changing table structure.
+		model.enqueueChange( batch, writer => {
+			// Removing rows from the table require that most calculations to be done prior to changing table structure.
+			// Preparations must be done in the same enqueueChange callback to use the current table structure.
 
-		// 1. Preparation - get row-spanned cells that have to be modified after removing rows.
-		const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow( table, first, last );
+			// 1. Preparation - get row-spanned cells that have to be modified after removing rows.
+			const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow( table, first, last );
+
+			// 2. Execution
 
-		// 2. Execution
-		model.enqueueChange( batch, writer => {
 			// 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows.
 			// This will fill any gaps in a rows below that previously were empty because of row-spanned cells.
-			const rowAfterRemovedSection = last + 1;
-			moveCellsToRow( table, rowAfterRemovedSection, cellsToMove, writer );
+			if ( cellsToMove.size ) {
+				const rowAfterRemovedSection = last + 1;
+				moveCellsToRow( table, rowAfterRemovedSection, cellsToMove, writer );
+			}
 
 			// 2b. Remove all required rows.
 			for ( let i = last; i >= first; i-- ) {
@@ -354,6 +358,8 @@ export default class TableUtils extends Plugin {
 		model.change( writer => {
 			adjustHeadingColumns( table, { first, last }, writer );
 
+			const emptyRowsIndexes = [];
+
 			for ( let removedColumnIndex = last; removedColumnIndex >= first; removedColumnIndex-- ) {
 				for ( const { cell, column, colspan } of [ ...new TableWalker( table ) ] ) {
 					// If colspaned cell overlaps removed column decrease its span.
@@ -368,11 +374,13 @@ export default class TableUtils extends Plugin {
 						// If the cell was the last one in the row, get rid of the entire row.
 						// https://github.com/ckeditor/ckeditor5/issues/6429
 						if ( !cellRow.childCount ) {
-							this.removeRows( table, { at: cellRow.index } );
+							emptyRowsIndexes.push( cellRow.index );
 						}
 					}
 				}
 			}
+
+			emptyRowsIndexes.reverse().forEach( row => this.removeRows( table, { at: row, batch: writer.batch } ) );
 		} );
 	}
 
@@ -753,17 +761,20 @@ function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
 
 // Calculates a new heading rows value for removing rows from heading section.
 function updateHeadingRows( table, first, last, model, batch ) {
-	const headingRows = table.getAttribute( 'headingRows' ) || 0;
+	// Must be done after the changes in table structure (removing rows).
+	// Otherwise the downcast converter for headingRows attribute will fail.
+	// See https://github.com/ckeditor/ckeditor5/issues/6391.
+	//
+	// Must be completely wrapped in enqueueChange to get the current table state (after applying other enqueued changes).
+	model.enqueueChange( batch, writer => {
+		const headingRows = table.getAttribute( 'headingRows' ) || 0;
 
-	if ( first < headingRows ) {
-		const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;
+		if ( first < headingRows ) {
+			const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;
 
-		// Must be done after the changes in table structure (removing rows).
-		// Otherwise the downcast converter for headingRows attribute will fail. ckeditor/ckeditor5#6391.
-		model.enqueueChange( batch, writer => {
 			updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
-		} );
-	}
+		}
+	} );
 }
 
 // Finds cells that will be:

+ 15 - 0
packages/ckeditor5-table/tests/_utils/utils.js

@@ -507,6 +507,9 @@ export function createTableAsciiArt( model, table ) {
 	const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ];
 	const columns = lastColumn + 1;
 
+	const headingRows = parseInt( table.getAttribute( 'headingRows' ) ) || 0;
+	const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) ) || 0;
+
 	let result = '';
 
 	for ( let row = 0; row <= lastRow; row++ ) {
@@ -539,6 +542,10 @@ export function createTableAsciiArt( model, table ) {
 			if ( column == lastColumn ) {
 				gridLine += '+';
 				contentLine += '|';
+
+				if ( headingRows && row == headingRows ) {
+					gridLine += ' <-- heading rows';
+				}
 			}
 		}
 		result += gridLine + '\n';
@@ -546,6 +553,14 @@ export function createTableAsciiArt( model, table ) {
 
 		if ( row == lastRow ) {
 			result += `+${ '----+'.repeat( columns ) }`;
+
+			if ( headingRows && row == headingRows - 1 ) {
+				result += ' <-- heading rows';
+			}
+
+			if ( headingColumns > 0 ) {
+				result += `\n${ '     '.repeat( headingColumns ) }^-- heading columns`;
+			}
 		}
 	}
 

+ 94 - 0
packages/ckeditor5-table/tests/commands/mergecellcommand.js

@@ -701,6 +701,53 @@ describe( 'MergeCellCommand', () => {
 					[ '40', '41' ]
 				] ) );
 			} );
+
+			it( 'should adjust heading rows if empty row was removed ', () => {
+				// +----+----+
+				// | 00 | 01 |
+				// +    +----+
+				// |    | 11 |
+				// +----+----+ <-- heading rows
+				// | 20 | 21 |
+				// +----+----+
+				setData( model, modelTable( [
+					[ { contents: '00', rowspan: 2 }, '[]01' ],
+					[ '11' ],
+					[ '20', '21' ]
+				], { headingRows: 2 } ) );
+
+				command.execute();
+
+				assertEqualMarkup( getData( model ), modelTable( [
+					[ '00', '<paragraph>[01</paragraph><paragraph>11]</paragraph>' ],
+					[ '20', '21' ]
+				], { headingRows: 1 } ) );
+			} );
+
+			it( 'should create one undo step (1 batch)', () => {
+				// +----+----+
+				// | 00 | 01 |
+				// +    +----+
+				// |    | 11 |
+				// +----+----+ <-- heading rows
+				// | 20 | 21 |
+				// +----+----+
+				setData( model, modelTable( [
+					[ { contents: '00', rowspan: 2 }, '[]01' ],
+					[ '11' ],
+					[ '20', '21' ]
+				], { headingRows: 2 } ) );
+
+				const createdBatches = new Set();
+
+				model.on( 'applyOperation', ( evt, [ operation ] ) => {
+					createdBatches.add( operation.batch );
+				} );
+
+				command.execute();
+
+				expect( createdBatches.size ).to.equal( 1 );
+			} );
 		} );
 	} );
 
@@ -959,6 +1006,53 @@ describe( 'MergeCellCommand', () => {
 					[ '40', '41' ]
 				] ) );
 			} );
+
+			it( 'should adjust heading rows if empty row was removed ', () => {
+				// +----+----+
+				// | 00 | 01 |
+				// +    +----+
+				// |    | 11 |
+				// +----+----+ <-- heading rows
+				// | 20 | 21 |
+				// +----+----+
+				setData( model, modelTable( [
+					[ { contents: '00', rowspan: 2 }, '01' ],
+					[ '[]11' ],
+					[ '20', '21' ]
+				], { headingRows: 2 } ) );
+
+				command.execute();
+
+				assertEqualMarkup( getData( model ), modelTable( [
+					[ '00', '<paragraph>[01</paragraph><paragraph>11]</paragraph>' ],
+					[ '20', '21' ]
+				], { headingRows: 1 } ) );
+			} );
+
+			it( 'should create one undo step (1 batch)', () => {
+				// +----+----+
+				// | 00 | 01 |
+				// +    +----+
+				// |    | 11 |
+				// +----+----+ <-- heading rows
+				// | 20 | 21 |
+				// +----+----+
+				setData( model, modelTable( [
+					[ { contents: '00', rowspan: 2 }, '01' ],
+					[ '[]11' ],
+					[ '20', '21' ]
+				], { headingRows: 2 } ) );
+
+				const createdBatches = new Set();
+
+				model.on( 'applyOperation', ( evt, [ operation ] ) => {
+					createdBatches.add( operation.batch );
+				} );
+
+				command.execute();
+
+				expect( createdBatches.size ).to.equal( 1 );
+			} );
 		} );
 	} );
 } );

+ 97 - 0
packages/ckeditor5-table/tests/commands/mergecellscommand.js

@@ -514,6 +514,103 @@ describe( 'MergeCellsCommand', () => {
 				] ) );
 			} );
 
+			it( 'should decrease heading rows if some heading rows were removed', () => {
+				setData( model, modelTable( [
+					[ '00' ],
+					[ '10' ],
+					[ '20' ]
+				], { headingRows: 2 } ) );
+
+				selectNodes( [
+					[ 0, 0, 0 ],
+					[ 0, 1, 0 ]
+				] );
+
+				command.execute();
+
+				assertEqualMarkup( getData( model ), modelTable( [
+					[
+						'<paragraph>[00</paragraph><paragraph>10]</paragraph>'
+					],
+					[ '20' ]
+				], { headingRows: 1 } ) );
+			} );
+
+			it( 'should decrease heading rows if multiple heading rows were removed', () => {
+				// +----+----+
+				// | 00 | 01 |
+				// +    +----+
+				// |    | 11 |
+				// +----+----+
+				// | 20 | 21 |
+				// +----+----+
+				// | 30 | 31 |
+				// +    +----+
+				// |    | 41 |
+				// +----+----+ <-- heading rows
+				// | 50 | 51 |
+				// +----+----+
+				setData( model, modelTable( [
+					[ { contents: '00', rowspan: 2 }, '01' ],
+					[ '11' ],
+					[ '20', '21' ],
+					[ { contents: '30', rowspan: 2 }, '31' ],
+					[ '41' ],
+					[ '50', '51' ]
+				], { headingRows: 5 } ) );
+
+				selectNodes( [
+					[ 0, 0, 1 ],
+					[ 0, 1, 0 ],
+					[ 0, 2, 1 ],
+					[ 0, 3, 1 ],
+					[ 0, 4, 0 ]
+				] );
+
+				command.execute();
+
+				const contents = [ '[01', '11', '21', '31', '41]' ].map( content => `<paragraph>${ content }</paragraph>` ).join( '' );
+
+				// +----+----+
+				// | 00 | 01 |
+				// +----+    +
+				// | 20 |    |
+				// +----+    +
+				// | 30 |    |
+				// +----+----+ <-- heading rows
+				// | 50 | 51 |
+				// +----+----+
+				assertEqualMarkup( getData( model ), modelTable( [
+					[ '00', { contents, rowspan: 3 } ],
+					[ '20' ],
+					[ '30' ],
+					[ '50', '51' ]
+				], { headingRows: 3 } ) );
+			} );
+
+			it( 'should create one undo step (1 batch)', () => {
+				setData( model, modelTable( [
+					[ '00' ],
+					[ '10' ],
+					[ '20' ]
+				], { headingRows: 2 } ) );
+
+				selectNodes( [
+					[ 0, 0, 0 ],
+					[ 0, 1, 0 ]
+				] );
+
+				const createdBatches = new Set();
+
+				model.on( 'applyOperation', ( evt, [ operation ] ) => {
+					createdBatches.add( operation.batch );
+				} );
+
+				command.execute();
+
+				expect( createdBatches.size ).to.equal( 1 );
+			} );
+
 			it( 'should decrease rowspan if cell overlaps removed row', () => {
 				setData( model, modelTable( [
 					[ '00', { rowspan: 2, contents: '01' }, { rowspan: 3, contents: '02' } ],

+ 2 - 1
packages/ckeditor5-table/tests/manual/tablemocking.js

@@ -11,6 +11,7 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import { diffString } from 'json-diff';
+import { debounce } from 'lodash-es';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import TableWalker from '../../src/tablewalker';
 
@@ -67,7 +68,7 @@ ClassicEditor
 			updateAsciiAndDiff();
 		} );
 
-		editor.model.document.on( 'change:data', updateAsciiAndDiff );
+		editor.model.document.on( 'change:data', debounce( () => updateAsciiAndDiff(), 100 ) );
 		updateAsciiAndDiff();
 
 		function updateAsciiAndDiff() {

+ 22 - 0
packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js

@@ -121,6 +121,28 @@ describe( 'table cell properties', () => {
 					tableCellPropertiesButton.fire( 'execute' );
 					sinon.assert.calledOnce( spy );
 				} );
+
+				it( 'should be disabled if all of the table cell properties commands are disabled', () => {
+					[
+						'tableCellBorderStyle',
+						'tableCellBorderColor',
+						'tableCellBorderWidth',
+						'tableCellWidth',
+						'tableCellHeight',
+						'tableCellPadding',
+						'tableCellBackgroundColor',
+						'tableCellHorizontalAlignment',
+						'tableCellVerticalAlignment'
+					].forEach( command => {
+						editor.commands.get( command ).isEnabled = false;
+					} );
+
+					expect( tableCellPropertiesButton.isEnabled ).to.be.false;
+
+					editor.commands.get( 'tableCellBackgroundColor' ).isEnabled = true;
+
+					expect( tableCellPropertiesButton.isEnabled ).to.be.true;
+				} );
 			} );
 		} );
 

+ 20 - 0
packages/ckeditor5-table/tests/tableproperties/tablepropertiesui.js

@@ -120,6 +120,26 @@ describe( 'table properties', () => {
 					tablePropertiesButton.fire( 'execute' );
 					sinon.assert.calledOnce( spy );
 				} );
+
+				it( 'should be disabled if all of the table properties commands are disabled', () => {
+					[
+						'tableBorderStyle',
+						'tableBorderColor',
+						'tableBorderWidth',
+						'tableBackgroundColor',
+						'tableWidth',
+						'tableHeight',
+						'tableAlignment'
+					].forEach( command => {
+						editor.commands.get( command ).isEnabled = false;
+					} );
+
+					expect( tablePropertiesButton.isEnabled ).to.be.false;
+
+					editor.commands.get( 'tableBackgroundColor' ).isEnabled = true;
+
+					expect( tablePropertiesButton.isEnabled ).to.be.true;
+				} );
 			} );
 		} );
 

+ 14 - 0
packages/ckeditor5-table/tests/tableutils.js

@@ -1368,6 +1368,20 @@ describe( 'TableUtils', () => {
 					[ '21', '22' ]
 				] ) );
 			} );
+
+			it( 'should remove the column properly when multiple rows should be removed (because of to row-spans)', () => {
+				setData( model, modelTable( [
+					[ '00', { contents: '01', rowspan: 3 }, { contents: '02', rowspan: 3 } ],
+					[ '10' ],
+					[ '20' ]
+				] ) );
+
+				tableUtils.removeColumns( root.getNodeByPath( [ 0 ] ), { at: 0 } );
+
+				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+					[ '01', '02' ]
+				] ) );
+			} );
 		} );
 
 		describe( 'multiple columns', () => {

+ 0 - 102
scripts/continuous-integration-run-tests.sh

@@ -1,102 +0,0 @@
-#!/bin/bash
-
-# @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
-# For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
-
-packages=$(ls packages -1 | sed -e 's#^ckeditor5\?-\(.\+\)$#\1#')
-
-errorOccured=0
-
-rm -r -f .nyc_output
-mkdir .nyc_output
-
-failedTestsPackages=""
-failedCoveragePackages=""
-
-RED='\033[0;31m'
-NC='\033[0m'
-
-# Travis functions inspired by https://github.com/travis-ci/travis-rubies/blob/a10ba31e3f508650204017332a608ef9bce2c733/build.sh.
-function travis_nanoseconds() {
-  local cmd="date"
-  local format="+%s%N"
-  local os=$(uname)
-
-  if hash gdate > /dev/null 2>&1; then
-    cmd="gdate" # use gdate if available
-  elif [[ "$os" = Darwin ]]; then
-    format="+%s000000000" # fallback to second precision on darwin (does not support %N)
-  fi
-
-  $cmd -u $format
-}
-
-travis_time_start() {
-  travis_timer_id=$(printf %08x $(( RANDOM * RANDOM )))
-  travis_start_time=$(travis_nanoseconds)
-  echo -en "travis_time:start:$travis_timer_id\r${ANSI_CLEAR}"
-}
-
-travis_time_finish() {
-  local result=$?
-  travis_end_time=$(travis_nanoseconds)
-  local duration=$(($travis_end_time-$travis_start_time))
-  echo -en "\ntravis_time:end:$travis_timer_id:start=$travis_start_time,finish=$travis_end_time,duration=$duration\r${ANSI_CLEAR}"
-  return $result
-}
-
-
-fold_start() {
-  echo -e "travis_fold:start:$1\033[33;1m$2\033[0m"
-  travis_time_start
-}
-
-fold_end() {
-  travis_time_finish
-  echo -e "\ntravis_fold:end:$1\n"
-
-}
-
-for package in $packages; do
-
-  fold_start "pkg-$package" "Testing $package${NC}"
-
-  yarn run test -f $package --reporter=dots --production --coverage
-
-  if [ "$?" -ne "0" ]; then
-    echo
-
-    echo -e "💥 ${RED}$package${NC} failed to pass unit tests 💥"
-    failedTestsPackages="$failedTestsPackages $package"
-    errorOccured=1
-  fi
-
-  cp coverage/*/coverage-final.json .nyc_output
-
-  npx nyc check-coverage --branches 100 --functions 100 --lines 100 --statements 100
-
-  if [ "$?" -ne "0" ]; then
-    echo -e "💥 ${RED}$package${NC} doesn't have required code coverage 💥"
-    failedCoveragePackages="$failedCoveragePackages $package"
-    errorOccured=1
-  fi
-
-  fold_end "pkg-$package"
-done;
-
-if [ "$errorOccured" -eq "1" ]; then
-  echo
-  echo "---"
-  echo
-
-  if ! [[ -z $failedTestsPackages ]]; then
-    echo -e "Following packages did not pass unit tests:${RED}$failedTestsPackages${NC}"
-  fi
-
-  if ! [[ -z $failedCoveragePackages ]]; then
-    echo -e "Following packages did not provide required code coverage:${RED}$failedCoveragePackages${NC}"
-  fi
-
-  echo
-  exit 1 # Will break the CI build
-fi

+ 152 - 0
scripts/continuous-integration-script.js

@@ -0,0 +1,152 @@
+#!/usr/bin/env node
+
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* eslint-env node */
+
+'use strict';
+
+const childProcess = require( 'child_process' );
+const crypto = require( 'crypto' );
+const fs = require( 'fs' );
+const path = require( 'path' );
+const glob = require( 'glob' );
+
+const failedChecks = {
+	dependency: new Set(),
+	unitTests: new Set(),
+	codeCoverage: new Set()
+};
+
+const RED = '\x1B[0;31m';
+const YELLOW = '\x1B[33;1m';
+const NO_COLOR = '\x1B[0m';
+
+const travis = {
+	_lastTimerId: null,
+	_lastStartTime: null,
+
+	foldStart( packageName, foldLabel ) {
+		console.log( `travis_fold:start:${ packageName }${ YELLOW }${ foldLabel }${ NO_COLOR }` );
+		this._timeStart();
+	},
+
+	foldEnd( packageName ) {
+		this._timeFinish();
+		console.log( `\ntravis_fold:end:${ packageName }\n` );
+	},
+
+	_timeStart() {
+		const nanoSeconds = process.hrtime.bigint();
+
+		this._lastTimerId = crypto.createHash( 'md5' ).update( nanoSeconds.toString() ).digest( 'hex' );
+		this._lastStartTime = nanoSeconds;
+
+		// Intentional direct write to stdout, to manually control EOL.
+		process.stdout.write( `travis_time:start:${ this._lastTimerId }\r\n` );
+	},
+
+	_timeFinish() {
+		const travisEndTime = process.hrtime.bigint();
+		const duration = travisEndTime - this._lastStartTime;
+
+		// Intentional direct write to stdout, to manually control EOL.
+		process.stdout.write( `\ntravis_time:end:${ this._lastTimerId }:start=${ this._lastStartTime },` +
+			`finish=${ travisEndTime },duration=${ duration }\r\n` );
+	}
+};
+
+childProcess.execSync( 'rm -r -f .nyc_output' );
+childProcess.execSync( 'mkdir .nyc_output' );
+childProcess.execSync( 'rm -r -f .out' );
+childProcess.execSync( 'mkdir .out' );
+
+const packages = childProcess.execSync( 'ls packages -1', {
+	encoding: 'utf8'
+} ).toString().trim().split( '\n' );
+
+for ( const fullPackageName of packages ) {
+	const simplePackageName = fullPackageName.replace( /^ckeditor5?-/, '' );
+	const foldLabelName = 'pkg-' + simplePackageName;
+
+	travis.foldStart( foldLabelName, `Testing ${ fullPackageName }${ NO_COLOR }` );
+
+	appendCoverageReport();
+
+	runSubprocess( 'npx', [ 'ckeditor5-dev-tests-check-dependencies', `packages/${ fullPackageName }` ], simplePackageName, 'dependency',
+		'have a dependency problem' );
+
+	const testArguments = [ 'run', 'test', '-f', simplePackageName, '--reporter=dots', '--production', '--coverage' ];
+	runSubprocess( 'yarn', testArguments, simplePackageName, 'unitTests', 'failed to pass unit tests' );
+
+	childProcess.execSync( 'cp coverage/*/coverage-final.json .nyc_output' );
+
+	const nyc = [ 'nyc', 'check-coverage', '--branches', '100', '--functions', '100', '--lines', '100', '--statements', '100' ];
+	runSubprocess( 'npx', nyc, simplePackageName, 'codeCoverage', 'doesn\'t have required code coverage' );
+
+	travis.foldEnd( foldLabelName );
+}
+
+console.log( 'Uploading combined code coverage report…' );
+childProcess.execSync( 'npx coveralls < .out/combined_lcov.info' );
+console.log( 'Done' );
+
+if ( Object.values( failedChecks ).some( checksSet => checksSet.size > 0 ) ) {
+	console.log( '\n---\n' );
+
+	showFailedCheck( 'dependency', 'The following packages have dependencies that are not included in its package.json' );
+	showFailedCheck( 'unitTests', 'The following packages did not pass unit tests' );
+	showFailedCheck( 'codeCoverage', 'The following packages did not provide required code coverage' );
+
+	process.exit( 1 ); // Exit code 1 will break the CI build.
+}
+
+/*
+ * @param {String} binaryName - Name of a CLI binary to be called.
+ * @param {String[]} cliArguments - An array of arguments to be passed to the `binaryName`.
+ * @param {String} packageName - Checked package name.
+ * @param {String} checkName - A key associated with the problem in the `failedChecks` dictionary.
+ * @param {String} failMessage - Message to be shown if check failed.
+ */
+function runSubprocess( binaryName, cliArguments, packageName, checkName, failMessage ) {
+	const subprocess = childProcess.spawnSync( binaryName, cliArguments, {
+		encoding: 'utf8',
+		shell: true
+	} );
+
+	console.log( subprocess.stdout );
+
+	if ( subprocess.stderr ) {
+		console.log( subprocess.stderr );
+	}
+
+	if ( subprocess.status !== 0 ) {
+		failedChecks.unitTests.add( packageName );
+		console.log( `💥 ${ RED }${ packageName }${ NO_COLOR } ` + failMessage + ' 💥' );
+	}
+}
+
+function showFailedCheck( checkKey, errorMessage ) {
+	const failedPackages = failedChecks[ checkKey ];
+
+	if ( failedPackages.size ) {
+		console.log( `${ errorMessage }: ${ RED }${ Array.from( failedPackages.values() ).join( ', ' ) }${ NO_COLOR }` );
+	}
+}
+
+function appendCoverageReport() {
+	// Appends coverage data to the combined code coverage info file. It's used because all the results
+	// needs to be uploaded at once (#6742).
+	const matches = glob.sync( 'coverage/*/lcov.info' );
+
+	matches.forEach( filePath => {
+		const buffer = fs.readFileSync( filePath );
+
+		fs.writeFileSync( [ '.out', 'combined_lcov.info' ].join( path.sep ), buffer, {
+			flag: 'as'
+		} );
+	} );
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 264 - 293
yarn.lock


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels