8
0
Просмотр исходного кода

Merge branch 'master' into i/6194

Marek Lewandowski 6 лет назад
Родитель
Сommit
c08111a104
60 измененных файлов с 3521 добавлено и 246 удалено
  1. 16 1
      packages/ckeditor5-table/CHANGELOG.md
  2. 40 0
      packages/ckeditor5-table/docs/_snippets/features/table-styling-colors.html
  3. 123 0
      packages/ckeditor5-table/docs/_snippets/features/table-styling-colors.js
  4. 73 51
      packages/ckeditor5-table/docs/_snippets/features/table-styling.html
  5. 93 1
      packages/ckeditor5-table/docs/_snippets/features/table-styling.js
  6. 19 19
      packages/ckeditor5-table/docs/_snippets/features/table.html
  7. 1 1
      packages/ckeditor5-table/docs/api/table.md
  8. 339 38
      packages/ckeditor5-table/docs/features/table.md
  9. 17 17
      packages/ckeditor5-table/package.json
  10. 18 2
      packages/ckeditor5-table/src/commands/removecolumncommand.js
  11. 33 7
      packages/ckeditor5-table/src/commands/removerowcommand.js
  12. 5 4
      packages/ckeditor5-table/src/commands/utils.js
  13. 1 1
      packages/ckeditor5-table/src/tablecellproperties.js
  14. 2 2
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellbackgroundcolorcommand.js
  15. 3 3
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellbordercolorcommand.js
  16. 3 3
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderstylecommand.js
  17. 2 2
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderwidthcommand.js
  18. 2 2
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellheightcommand.js
  19. 2 2
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js
  20. 3 2
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpaddingcommand.js
  21. 3 3
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js
  22. 5 5
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellverticalalignmentcommand.js
  23. 4 4
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellwidthcommand.js
  24. 8 8
      packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting.js
  25. 7 7
      packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js
  26. 97 0
      packages/ckeditor5-table/src/tableclipboard.js
  27. 1 1
      packages/ckeditor5-table/src/tableediting.js
  28. 1 1
      packages/ckeditor5-table/src/tableproperties.js
  29. 2 2
      packages/ckeditor5-table/src/tableproperties/commands/tablealignmentcommand.js
  30. 2 2
      packages/ckeditor5-table/src/tableproperties/commands/tablebackgroundcolorcommand.js
  31. 2 2
      packages/ckeditor5-table/src/tableproperties/commands/tablebordercolorcommand.js
  32. 2 2
      packages/ckeditor5-table/src/tableproperties/commands/tableborderstylecommand.js
  33. 3 3
      packages/ckeditor5-table/src/tableproperties/commands/tableborderwidthcommand.js
  34. 4 4
      packages/ckeditor5-table/src/tableproperties/commands/tableheightcommand.js
  35. 3 3
      packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js
  36. 4 4
      packages/ckeditor5-table/src/tableproperties/commands/tablewidthcommand.js
  37. 282 0
      packages/ckeditor5-table/src/tableselection.js
  38. 49 0
      packages/ckeditor5-table/src/tableselection/converters.js
  39. 152 0
      packages/ckeditor5-table/src/tableselection/croptable.js
  40. 86 0
      packages/ckeditor5-table/src/tableselection/mouseeventsobserver.js
  41. 160 0
      packages/ckeditor5-table/src/tableselection/mouseselectionhandler.js
  42. 3 3
      packages/ckeditor5-table/src/ui/colorinputview.js
  43. 1 1
      packages/ckeditor5-table/src/ui/formheaderview.js
  44. 2 2
      packages/ckeditor5-table/src/ui/formrowview.js
  45. 13 13
      packages/ckeditor5-table/src/ui/utils.js
  46. 66 4
      packages/ckeditor5-table/tests/_utils/utils.js
  47. 20 4
      packages/ckeditor5-table/tests/commands/removecolumncommand.js
  48. 7 7
      packages/ckeditor5-table/tests/commands/removerowcommand.js
  49. 174 0
      packages/ckeditor5-table/tests/manual/tableclipboard.html
  50. 44 0
      packages/ckeditor5-table/tests/manual/tableclipboard.js
  51. 21 0
      packages/ckeditor5-table/tests/manual/tableclipboard.md
  52. 184 0
      packages/ckeditor5-table/tests/manual/tableselection.html
  53. 78 0
      packages/ckeditor5-table/tests/manual/tableselection.js
  54. 9 0
      packages/ckeditor5-table/tests/manual/tableselection.md
  55. 335 0
      packages/ckeditor5-table/tests/tableclipboard.js
  56. 1 3
      packages/ckeditor5-table/tests/tableediting.js
  57. 339 0
      packages/ckeditor5-table/tests/tableselection.js
  58. 50 0
      packages/ckeditor5-table/tests/tableselection/mouseeventsobserver.js
  59. 485 0
      packages/ckeditor5-table/tests/tableselection/mouseselectionhandler.js
  60. 17 0
      packages/ckeditor5-table/theme/tableselection.css

+ 16 - 1
packages/ckeditor5-table/CHANGELOG.md

@@ -1,6 +1,21 @@
 Changelog
 =========
 
+## [17.0.0](https://github.com/ckeditor/ckeditor5-table/compare/v16.0.0...v17.0.0) (2020-02-19)
+
+### Features
+
+* Implemented the table and table cell properties forms. Closes [ckeditor/ckeditor5#3287](https://github.com/ckeditor/ckeditor5/issues/3287).
+
+### Bug fixes
+
+* Fixed a bug with spellchecking or pasting via the context menu into a table cell crashing the editor. Closes [ckeditor/ckeditor5#6062](https://github.com/ckeditor/ckeditor5/issues/6062). ([be0d759](https://github.com/ckeditor/ckeditor5-table/commit/be0d759))
+
+### Other changes
+
+* Implemented lazy loading for the table dropdown. This will reduce editor initialization time. Closes [ckeditor/ckeditor5#6193](https://github.com/ckeditor/ckeditor5/issues/6193). ([5daa487](https://github.com/ckeditor/ckeditor5-table/commit/5daa487))
+
+
 ## [16.0.0](https://github.com/ckeditor/ckeditor5-table/compare/v15.0.0...v16.0.0) (2019-12-04)
 
 ### Bug fixes
@@ -9,7 +24,7 @@ Changelog
 
 ### Other changes
 
-* Updated translations. ([7b2df5c](https://github.com/ckeditor/ckeditor5-table/commit/7b2df5c)) 
+* Updated translations. ([7b2df5c](https://github.com/ckeditor/ckeditor5-table/commit/7b2df5c))
 
 
 ## [15.0.0](https://github.com/ckeditor/ckeditor5-table/compare/v14.0.0...v15.0.0) (2019-10-23)

+ 40 - 0
packages/ckeditor5-table/docs/_snippets/features/table-styling-colors.html

@@ -0,0 +1,40 @@
+<div id="snippet-table-styling-colors">
+	<h3>A beautiful color palette</h3>
+	<figure class="table">
+		<table>
+			<tbody>
+				<tr>
+					<td style="width:120px;text-align:center;background-color:hsl(4,90%,58%)">Red</td>
+					<td style="width:120px;text-align:center;background-color:hsl(340,82%,52%)">Pink</td>
+					<td style="width:120px;text-align:center;background-color:hsl(291,64%,42%)">Purple</td>
+					<td style="width:120px;text-align:center;background-color:hsl(262,52%,47%)">Deep Purple</td>
+				</tr>
+				<tr>
+					<td style="width:120px;text-align:center;background-color:hsl(231,48%,48%)">Indigo</td>
+					<td style="width:120px;text-align:center;background-color:hsl(207,90%,54%)">Blue</td>
+					<td style="width:120px;text-align:center;background-color:hsl(199,98%,48%)">Light Blue</td>
+					<td style="width:120px;text-align:center;background-color:hsl(187,100%,42%)">Cyan</td>
+				</tr>
+				<tr>
+					<td style="width:120px;text-align:center;background-color:hsl(174,100%,29%)">Teal</td>
+					<td style="width:120px;text-align:center;background-color:hsl(122,39%,49%)">Green</td>
+					<td style="width:120px;text-align:center;background-color:hsl(88,50%,53%)">Light Green</td>
+					<td style="width:120px;text-align:center;background-color:hsl(66,70%,54%)">Lime</td>
+				</tr>
+				<tr>
+					<td style="width:120px;text-align:center;background-color:hsl(49,98%,60%)">Yellow</td>
+					<td style="width:120px;text-align:center;background-color:hsl(45,100%,51%)">Amber</td>
+					<td style="width:120px;text-align:center;background-color:hsl(36,100%,50%)">Orange</td>
+					<td style="width:120px;text-align:center;background-color:hsl(14,91%,54%)">Deep Orange</td>
+				</tr>
+				<tr>
+					<td style="width:120px;text-align:center;background-color:hsl(15,25%,34%)">Brown</td>
+					<td style="width:120px;text-align:center;background-color:hsl(0,0%,62%)">Grey</td>
+					<td style="width:120px;text-align:center;background-color:hsl(200,18%,46%)">Blue Grey</td>
+					<td style="width:120px;text-align:center;background-color:hsl(200,18%,100%)">White</td>
+				</tr>
+			</tbody>
+		</table>
+	</figure>
+</div>
+

+ 123 - 0
packages/ckeditor5-table/docs/_snippets/features/table-styling-colors.js

@@ -0,0 +1,123 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals ClassicEditor, CKEditorPlugins, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+const customColorPalette = [
+	{
+		color: 'hsl(4, 90%, 58%)',
+		label: 'Red'
+	},
+	{
+		color: 'hsl(340, 82%, 52%)',
+		label: 'Pink'
+	},
+	{
+		color: 'hsl(291, 64%, 42%)',
+		label: 'Purple'
+	},
+	{
+		color: 'hsl(262, 52%, 47%)',
+		label: 'Deep Purple'
+	},
+	{
+		color: 'hsl(231, 48%, 48%)',
+		label: 'Indigo'
+	},
+	{
+		color: 'hsl(207, 90%, 54%)',
+		label: 'Blue'
+	},
+	{
+		color: 'hsl(199, 98%, 48%)',
+		label: 'Light Blue'
+	},
+	{
+		color: 'hsl(187, 100%, 42%)',
+		label: 'Cyan'
+	},
+	{
+		color: 'hsl(174, 100%, 29%)',
+		label: 'Teal'
+	},
+	{
+		color: 'hsl(122, 39%, 49%)',
+		label: 'Green'
+	},
+	{
+		color: 'hsl(88, 50%, 53%)',
+		label: 'Light Green'
+	},
+	{
+		color: 'hsl(66, 70%, 54%)',
+		label: 'Lime'
+	},
+	{
+		color: 'hsl(49, 98%, 60%)',
+		label: 'Yellow'
+	},
+	{
+		color: 'hsl(45, 100%, 51%)',
+		label: 'Amber'
+	},
+	{
+		color: 'hsl(36, 100%, 50%)',
+		label: 'Orange'
+	},
+	{
+		color: 'hsl(14, 91%, 54%)',
+		label: 'Deep Orange'
+	},
+	{
+		color: 'hsl(15, 25%, 34%)',
+		label: 'Brown'
+	},
+	{
+		color: 'hsl(0, 0%, 62%)',
+		label: 'Grey'
+	},
+	{
+		color: 'hsl(200, 18%, 46%)',
+		label: 'Blue Grey'
+	},
+	{
+		color: 'hsl(200, 18%, 100%)',
+		label: 'White'
+	},
+];
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-table-styling-colors' ), {
+		extraPlugins: [
+			CKEditorPlugins.TableProperties,
+			CKEditorPlugins.TableCellProperties,
+		],
+		cloudServices: CS_CONFIG,
+		toolbar: {
+			items: [
+				'insertTable', '|', 'heading', '|', 'bold', 'italic', '|', 'undo', 'redo'
+			],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		table: {
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ],
+			tableProperties: {
+				borderColors: customColorPalette,
+				backgroundColors: customColorPalette
+			},
+			tableCellProperties: {
+				borderColors: customColorPalette,
+				backgroundColors: customColorPalette
+			}
+		}
+	} )
+	.then( editor => {
+		window.editorStyling = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 73 - 51
packages/ckeditor5-table/docs/_snippets/features/table-styling.html

@@ -1,54 +1,76 @@
 <div id="snippet-table-styling">
-	<p>The ultimate table of <a href="https://en.wikipedia.org/wiki/List_of_coffee_drinks">coffee drinks</a>:</p>
-	<table>
-		<thead>
-			<tr>
-				<th rowspan="2"></th>
-				<th scope="col" rowspan="2">Espresso</th>
-				<th scope="colgroup" colspan="2">Milk</th>
-				<th scope="col" rowspan="2">Water</th>
-			</tr>
-			<tr>
-				<th scope="col">Steamed</th>
-				<th scope="col">Foam</th>
-			</tr>
-		</thead>
-		<tbody>
-			<tr>
-				<th scope="row">Cappuccino</th>
-				<td>yes</td>
-				<td>yes</td>
-				<td>yes</td>
-				<td>no</td>
-			</tr>
-			<tr>
-				<th scope="row">Macchiato</th>
-				<td>yes</td>
-				<td>no</td>
-				<td>yes</td>
-				<td>no</td>
-			</tr>
-			<tr>
-				<th scope="row">Latte</th>
-				<td>yes</td>
-				<td>yes</td>
-				<td>no</td>
-				<td>no</td>
-			</tr>
-			<tr>
-				<th scope="row">Americano</th>
-				<td>yes</td>
-				<td>no</td>
-				<td>no</td>
-				<td>yes</td>
-			</tr>
-		</tbody>
-	</table>
+	<h3>Balance Sheet &amp; Cash Flow Statement Information</h3>
+	<figure class="table">
+		<table style="border-bottom:1px solid;border-left:1px solid;border-right:1px solid;border-top:1px solid;">
+			<thead>
+				<tr>
+					<th style="background-color:hsl(0,0%,90%);width:317px;" colspan="1" rowspan="2"><p>&nbsp;</p><p>&nbsp;</p></th>
+					<th style="background-color:hsl(180,97%,31%);text-align:center;width:224px;" colspan="3">Full Year (unaudited)</th>
+					<th style="background-color:hsl(180,97%,31%);text-align:center;width:360px;" colspan="4">2013 (unaudited)</th>
+				</tr>
+				<tr>
+					<th style="background-color:hsl(180,52%,58%);width:75px;">2011</th>
+					<th style="background-color:hsl(180,52%,58%);width:75px;">2012</th>
+					<th style="background-color:hsl(180,52%,58%);width:75px;">2013</th>
+					<th style="background-color:hsl(180,52%,58%);width:75px;">Q1</th>
+					<th style="background-color:hsl(180,52%,58%);width:75px;">Q2</th>
+					<th style="background-color:hsl(180,52%,58%);width:75px;">Q3</th>
+					<th style="background-color:hsl(180,52%,58%);width:75px;">Q4</th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					<th style="background-color:hsl(27,100%,85%);text-align:center;width:317px;"><strong>Cash, Cash Equivalents&nbsp;</strong></th>
+					<td style="width:75px;">$44,626</td>
+					<td style="width:75px;">$48,088</td>
+					<td style="width:75px;">$58,717</td>
+					<td style="width:75px;">$50,098</td>
+					<td style="background-color:hsl(90,75%,60%);border-bottom:2px solid hsl(120,100%,25%);border-left:2px solid hsl(120,100%,25%);border-right:2px solid hsl(120,100%,25%);border-top:2px solid hsl(120,100%,25%);width:75px;">$54,432</td>
+					<td style="width:75px;">$56,523</td>
+					<td style="width:75px;">$58,717</td>
+				</tr>
+				<tr>
+					<th style="background-color:hsl(27,100%,85%);text-align:center;width:317px;"><strong>Accounts Receivable</strong></th>
+					<td style="width:75px;">$5,427</td>
+					<td style="width:75px;">$6,769</td>
+					<td style="background-color:hsl(0,100%,89%);border-bottom:2px solid hsl(0,75%,60%);border-left:2px solid hsl(0,75%,60%);border-right:2px solid hsl(0,75%,60%);border-top:2px solid hsl(0,75%,60%);width:75px;">$8,098</td>
+					<td style="width:75px;">$6,526</td>
+					<td style="width:75px;">$6,792</td>
+					<td style="width:75px;">$7,083</td>
+					<td style="width:75px;">$8,098</td>
+				</tr>
+				<tr>
+					<th style="background-color:hsl(27,100%,85%);text-align:center;width:317px;"><strong>Property and Equipment, Net</strong></th>
+					<td style="width:75px;">$9,603</td>
+					<td style="width:75px;">$11,854</td>
+					<td style="width:75px;">$16,524</td>
+					<td style="width:75px;">$12,300</td>
+					<td style="width:75px;">$12,912</td>
+					<td style="width:75px;">$14,867</td>
+					<td style="width:75px;">$16,524</td>
+				</tr>
+				<tr>
+					<th style="background-color:hsl(27,100%,85%);text-align:center;width:317px;"><strong>Total Assets</strong></th>
+					<td style="background-color:hsl(0,100%,89%);border-bottom:2px solid hsl(0,75%,60%);border-left:2px solid hsl(0,75%,60%);border-right:2px solid hsl(0,75%,60%);border-top:2px solid hsl(0,75%,60%);width:75px;">$72,574</td>
+					<td style="width:75px;">$93,798</td>
+					<td style="width:75px;">$110,920</td>
+					<td style="width:75px;">$96,692</td>
+					<td style="width:75px;">$101,182</td>
+					<td style="background-color:hsl(90,75%,60%);border-bottom:2px solid hsl(120,100%,25%);border-left:2px solid hsl(120,100%,25%);border-right:2px solid hsl(120,100%,25%);border-top:2px solid hsl(120,100%,25%);width:75px;">$105,068</td>
+					<td style="width:75px;">$110,920</td>
+				</tr>
+				<tr>
+					<th style="background-color:hsl(27,100%,85%);text-align:center;width:317px;"><strong>Cash Flow from Operations</strong></th>
+					<td style="width:75px;">$14,565</td>
+					<td style="width:75px;">$16,619</td>
+					<td style="width:75px;">$18,659</td>
+					<td style="width:75px;">$3,633</td>
+					<td style="width:75px;">$4,705</td>
+					<td style="width:75px;">$5,083</td>
+					<td style="width:75px;">$5,238</td>
+				</tr>
+			</tbody>
+		</table>
+	</figure>
 </div>
 
-<style>
-	.ck.ck-content table td,
-	.ck.ck-content table th {
-		vertical-align: middle;
-	}
-</style>

+ 93 - 1
packages/ckeditor5-table/docs/_snippets/features/table-styling.js

@@ -7,6 +7,90 @@
 
 import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
 
+const COLOR_PALETTE = [
+	{
+		color: 'hsl(0, 0%, 0%)',
+		label: 'Black'
+	},
+	{
+		color: 'hsl(0, 0%, 30%)',
+		label: 'Dim grey'
+	},
+	{
+		color: 'hsl(0, 0%, 60%)',
+		label: 'Grey'
+	},
+	{
+		color: 'hsl(0, 0%, 90%)',
+		label: 'Light grey'
+	},
+	{
+		color: 'hsl(0, 0%, 100%)',
+		label: 'White',
+		hasBorder: true
+	},
+	{
+		color: 'hsl(0, 100%, 89%)',
+		label: 'Pink'
+	},
+	{
+		color: 'hsl(0, 75%, 60%)',
+		label: 'Red'
+	},
+	{
+		color: 'hsl(60, 75%, 60%)',
+		label: 'Yellow'
+	},
+	{
+		color: 'hsl(27, 100%, 85%)',
+		label: 'Light Orange'
+	},
+	{
+		color: 'hsl(30, 75%, 60%)',
+		label: 'Orange'
+	},
+	{
+		color: 'hsl(90, 75%, 60%)',
+		label: 'Light green'
+	},
+	{
+		color: 'hsl(120, 75%, 60%)',
+		label: 'Green'
+	},
+	{
+		color: 'hsl(150, 75%, 60%)',
+		label: 'Aquamarine'
+	},
+	{
+		color: 'hsl(120, 100%, 25%)',
+		label: 'Dark green'
+	},
+	{
+		color: 'hsl(180, 75%, 60%)',
+		label: 'Turquoise'
+	},
+	{
+		color: 'hsl(180, 52%, 58%)',
+		label: 'Light Aqua',
+	},
+	{
+		color: 'hsl(180, 97%, 31%)',
+		label: 'Aqua'
+	},
+	{
+		color: 'hsl(210, 75%, 60%)',
+		label: 'Light blue'
+	},
+	{
+		color: 'hsl(240, 75%, 60%)',
+		label: 'Blue'
+	},
+	{
+		color: 'hsl(270, 75%, 60%)',
+		label: 'Purple'
+	}
+];
+
 ClassicEditor
 	.create( document.querySelector( '#snippet-table-styling' ), {
 		extraPlugins: [
@@ -21,7 +105,15 @@ ClassicEditor
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},
 		table: {
-			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ]
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ],
+			tableProperties: {
+				borderColors: COLOR_PALETTE,
+				backgroundColors: COLOR_PALETTE
+			},
+			tableCellProperties: {
+				borderColors: COLOR_PALETTE,
+				backgroundColors: COLOR_PALETTE
+			}
 		}
 	} )
 	.then( editor => {

+ 19 - 19
packages/ckeditor5-table/docs/_snippets/features/table.html

@@ -4,9 +4,9 @@
 		<thead>
 			<tr>
 				<th rowspan="2"></th>
-				<th scope="col" rowspan="2">Espresso</th>
-				<th scope="colgroup" colspan="2">Milk</th>
-				<th scope="col" rowspan="2">Water</th>
+				<th scope="col" rowspan="2">☕️ Espresso</th>
+				<th scope="colgroup" colspan="2">🥛 Milk</th>
+				<th scope="col" rowspan="2">🚰 Water</th>
 			</tr>
 			<tr>
 				<th scope="col">Steamed</th>
@@ -16,31 +16,31 @@
 		<tbody>
 			<tr>
 				<th scope="row">Cappuccino</th>
-				<td>yes</td>
-				<td>yes</td>
-				<td>yes</td>
-				<td>no</td>
+				<td>yes</td>
+				<td>yes</td>
+				<td>yes</td>
+				<td>no</td>
 			</tr>
 			<tr>
 				<th scope="row">Macchiato</th>
-				<td>yes</td>
-				<td>no</td>
-				<td>yes</td>
-				<td>no</td>
+				<td>yes</td>
+				<td>no</td>
+				<td>yes</td>
+				<td>no</td>
 			</tr>
 			<tr>
 				<th scope="row">Latte</th>
-				<td>yes</td>
-				<td>yes</td>
-				<td>no</td>
-				<td>no</td>
+				<td>yes</td>
+				<td>yes</td>
+				<td>no</td>
+				<td>no</td>
 			</tr>
 			<tr>
 				<th scope="row">Americano</th>
-				<td>yes</td>
-				<td>no</td>
-				<td>no</td>
-				<td>yes</td>
+				<td>yes</td>
+				<td>no</td>
+				<td>no</td>
+				<td>yes</td>
 			</tr>
 		</tbody>
 	</table>

+ 1 - 1
packages/ckeditor5-table/docs/api/table.md

@@ -10,7 +10,7 @@ This package implements the table feature for CKEditor 5.
 
 ## Demo
 
-Check out the {@link features/table#demo demo in the Table feature} guide.
+Check out the {@link features/table#demos demos in the Table feature} guide.
 
 ## Documentation
 

+ 339 - 38
packages/ckeditor5-table/docs/features/table.md

@@ -7,27 +7,45 @@ category: features
 
 The {@link module:table/table~Table} feature offers table creation and editing tools that help content authors bring tabular data into their documents.
 
-## Demo
+## Demos
+
+### Basic table features
+
+The editor bellow shows the basic set of table features focusing on the **structure and semantics**. These features allow users to insert new tables into the content, add or remove columns and rows, define headers, and merge multiple cells. It is also worth noting that you will find them out–of–the–box in all {@link builds/guides/overview ready–to–use editor builds}.
 
 {@snippet features/table}
 
-## Styling tables
+Use the **"Insert table"** button in the toolbar to create new tables. Focus any cell in the table to display the toolbar with buttons that will help you further shape the structure of the table.
+
+### Table and cell styling tools
+
+In addition to the default table features described in the [previous section](#basic-table-features), the editor below comes with some additional tools that will help you modify **the look of tables and table cells**, for instance, their border color and style, background color, padding, or text alignment.
 
 {@snippet features/table-styling}
 
+Put the caret anywhere inside the table and click the **"Table properties"** button in the toolbar to open a pop–up with multiple options that will allow you to shape the look of the entire table. If you click the **"Cell properties"** button, a similar interface will appear with styling options for individual table cells.
+
+[Learn more](#configuring-styling-tools) about configuring color palettes in the table styling pop–up interfaces.
+
+<info-box>
+	By default, table styling tools are not included in the {@link builds/guides/overview ready–to–use editor builds} and must be installed separately. See the [installation](#table-and-cell-styling-tools-2) section to learn how to enable them in your editor.
+</info-box>
+
 ## Installation
 
+### Basic table features
+
 <info-box info>
-	This feature is enabled by default in all builds. The installation instructions are for developers interested in building their own, custom rich text editor.
+	The basic table features are enabled by default in all builds. The installation instructions are for developers interested in building their own, custom rich text editor.
 </info-box>
 
-To add this feature to your editor, install the [`@ckeditor/ckeditor5-table`](https://www.npmjs.com/package/@ckeditor/ckeditor5-table) package:
+To add only the basic table features to your editor, install the [`@ckeditor/ckeditor5-table`](https://www.npmjs.com/package/@ckeditor/ckeditor5-table) package:
 
-```bash
+```
 npm install --save @ckeditor/ckeditor5-table
 ```
 
-Then add `Table` and `TableToolbar` plugins to your plugin list and configure the table toolbar:
+Then add the `Table` and `TableToolbar` plugins to your plugin list and configure the table toolbar:
 
 ```js
 import Table from '@ckeditor/ckeditor5-table/src/table';
@@ -45,14 +63,147 @@ ClassicEditor
 	.catch( ... );
 ```
 
+### Table and cell styling tools
+
+To enable not only the [basic table features](#basic-table-features-2) but also the rich table and cell styling tools in your editor, install the [`@ckeditor/ckeditor5-table`](https://www.npmjs.com/package/@ckeditor/ckeditor5-table) package:
+
+```
+npm install --save @ckeditor/ckeditor5-table
+```
+
+Then add the `Table`, `TableToolbar`, **`TableProperties`**, and **`TableCellProperties`** plugins to your plugin list and configure the table toolbar:
+
+```js
+import Table from '@ckeditor/ckeditor5-table/src/table';
+import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
+import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
+import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Table, TableToolbar, TableProperties, TableCellProperties, Bold, ... ],
+		toolbar: [ 'insertTable', ... ],
+		table: {
+			contentToolbar: [
+				'tableColumn', 'tableRow', 'mergeTableCells',
+				'tableProperties', 'tableCellProperties'
+			],
+
+			// Configuration of the TableProperties plugin.
+			tableProperties: {
+				// ...
+			},
+
+			// Configuration of the TableCellProperties plugin.
+			tableCellProperties: {
+				// ...
+			}
+		}
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+<info-box info>
+	Learn more about [configuring color palettes](#configuring-styling-tools) in the table and table cell property pop–ups.
+</info-box>
+
+<info-box info>
+	Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
+</info-box>
+
+## Configuring styling tools
+
+<info-box>
+	By default, table styling tools are not included in {@link builds/guides/overview ready–to–use editor builds} and must be installed separately. See the [installation](#table-and-cell-styling-tools-2) section to learn how to enable them in your editor.
+</info-box>
+
+Among other formatting options, table and cell styling tools allow users to create tables with colorful backgrounds and borders. These colors can be easily picked using color palettes in the **"Table properties"** and **"Cell properties"** pop–ups. To help users choose the right colors for the content, the color palettes can be pre–configured, like in the editor below:
+
+{@snippet features/table-styling-colors}
+
+With the selection inside any table cell, use the **"Table properties"** and **"Cell properties"** buttons in the toolbar to inspect available styling and color options.
+
+### Customizing color palettes
+
+You can use these specific configuration options to define customized color palettes for background and border colors to match your document:
+
+* {@link module:table/table~TableConfig#tableProperties `tableProperties.borderColors`} &ndash; Defines the color palette for table borders.
+* {@link module:table/table~TableConfig#tableProperties `tableProperties.backgroundColors`} &ndash; Defines the color palette for table background.
+* {@link module:table/table~TableConfig#tableCellProperties `tableCellProperties.borderColors`} &ndash; Defines the color palette for cell borders.
+* {@link module:table/table~TableConfig#tableCellProperties `tableCellProperties.backgroundColors`} &ndash; Defines the color palette for cell background.
+
+<info-box>
+	The above configurations **do not** impact the {@link builds/guides/integration/basic-api#setting-the-editor-data data loaded into the editor}, i.e. they do not limit or filter the colors in the data. They are used only in the user interface allowing users to pick colors in a more convenient way.
+</info-box>
+
+For instance, to define the same color palette for all border and background configurations, use the following code snippet:
+
+```js
+const customColorPalette = [
+	{
+		color: 'hsl(4, 90%, 58%)',
+		label: 'Red'
+	},
+	{
+		color: 'hsl(340, 82%, 52%)',
+		label: 'Pink'
+	},
+	{
+		color: 'hsl(291, 64%, 42%)',
+		label: 'Purple'
+	},
+	{
+		color: 'hsl(262, 52%, 47%)',
+		label: 'Deep Purple'
+	},
+	{
+		color: 'hsl(231, 48%, 48%)',
+		label: 'Indigo'
+	},
+	{
+		color: 'hsl(207, 90%, 54%)',
+		label: 'Blue'
+	},
+
+	// ...
+];
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Table, TableToolbar, TableProperties, TableCellProperties, Bold, ... ],
+		toolbar: [ 'insertTable', ... ],
+		table: {
+			contentToolbar: [
+				'tableColumn', 'tableRow', 'mergeTableCells',
+				'tableProperties', 'tableCellProperties'
+			],
+
+			// Set the palettes for tables.
+			tableProperties: {
+				borderColors: customColorPalette,
+				backgroundColors: customColorPalette
+			},
+
+			// Set the palettes for table cells.
+			tableCellProperties: {
+				borderColors: customColorPalette,
+				backgroundColors: customColorPalette
+			}
+		}
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
 ## Block vs inline content in table cells
 
-The table feature allows creating block content (like paragraphs, lists, headings, etc.) in table cells. However, if a table cell contains just one paragraph and this paragraph has no special attributes (like text alignment), this cell content is considered "inline" and the paragraph is not rendered.
+The table feature allows creating block content (like paragraphs, lists, headings, etc.) in table cells. However, if a table cell contains just one paragraph and this paragraph has no special attributes (like text alignment), the cell content is considered "inline" and the paragraph is not rendered.
 
-This means that a table cell can be in two states: with inline content or with block content. The reason for this differentiation is that most tables contain only inline content (e.g. in the [demo](#demo) above) and it is common for "data tables" to not contain any block content. In such scenario, printing out `<p>` elements would be semantically incorrect and also unnecessary. There are, however, scenarios where the user wants to create, for example, a list inside the table and then support for block content is necessary, too.
+This means that a table cell can be in two states: with inline content or with block content. The reason for this differentiation is that most tables contain only inline content (e.g. in the [demo](#demos) above) and it is common for "data tables" to not contain any block content. In such scenario, printing out `<p>` elements would be semantically incorrect and also unnecessary. There are, however, scenarios where the user wants to create, for example, a list inside the table and then support for block content is necessary, too.
 
 <info-box>
-	"Rendering" here means the view layer. In the model a cell is always filled with at least a `<paragraph>`. It is because of consistency, as since a cell always has some block content, the text is never directly inside `<tableCell>`. This also allows features like <kbd>Enter</kbd> support to work out of the box (since a `<paragraph>` exists in the model, it can be split despite the fact that it is not present in the view).
+	"Rendering" here means the view layer. In the model a cell is always filled with at least a `<paragraph>`. This is because of consistency, as &mdash; since a cell always has some block content &mdash; the text is never directly inside `<tableCell>`. This also allows features like <kbd>Enter</kbd> support to work out of the box (since a `<paragraph>` exists in the model, it can be split despite the fact that it is not present in the view).
 </info-box>
 
 ### Inline content
@@ -87,7 +238,7 @@ The above model structure will be rendered to the {@link module:editor-classic/c
 </figure>
 ```
 
-In the editing view (the editable container in which the user edits the content) additional `<span>` elements are created to compensate for the hidden `<paragraph>` elements:
+In the editing view (the editable container in which the user edits the content), additional `<span>` elements are created to compensate for the hidden `<paragraph>` elements:
 
 ```html
 <figure class="table">
@@ -149,42 +300,192 @@ The above model structure will be rendered to the data and to the editing view a
 ```
 
 <info-box info>
-	At the moment it is not possible to completely disallow block content in tables. See the [discussion on GitHub](https://github.com/ckeditor/ckeditor5-table/issues/101) about adding a configuration option that would enable that. 👍 if you need this feature.
+	At the moment it is not possible to completely disallow block content in tables. See the [discussion on GitHub](https://github.com/ckeditor/ckeditor5-table/issues/101) about adding a configuration option that would enable that. Add a 👍 if you need this feature.
 </info-box>
 
 ## Common API
 
-The {@link module:table/table~Table} plugin registers the following UI components:
-
-* The `'insertTable'` dropdown.
-* The `'tableColumn'` dropdown.
-* The `'tableRow'` dropdown.
-* The `'mergeTableCells'` dropdown.
-
-And the following commands:
-
-| {@link framework/guides/architecture/core-editor-architecture#commands Command} name | Implemented by |
-|----------------------------------|-----------------------------------------------------------------------------|
-| `'insertTable'`                  | {@link module:table/commands/inserttablecommand~InsertTableCommand}         |
-| `'insertTableColumnLeft'`        | {@link module:table/commands/insertcolumncommand~InsertColumnCommand}       |
-| `'insertTableColumnRight'`       | {@link module:table/commands/insertcolumncommand~InsertColumnCommand}       |
-| `'insertTableRowAbove'`          | {@link module:table/commands/insertrowcommand~InsertRowCommand}             |
-| `'insertTableRowBelow'`          | {@link module:table/commands/insertrowcommand~InsertRowCommand}             |
-| `'removeTableColumn'`            | {@link module:table/commands/removecolumncommand~RemoveColumnCommand}       |
-| `'removeTableRow'`               | {@link module:table/commands/removerowcommand~RemoveRowCommand}             |
-| `'setTableColumnHeader'`         | {@link module:table/commands/setheadercolumncommand~SetHeaderColumnCommand} |
-| `'setTableRowHeader'`            | {@link module:table/commands/setheaderrowcommand~SetHeaderRowCommand}       |
-| `'mergeTableCellRight'`          | {@link module:table/commands/mergecellcommand~MergeCellCommand}             |
-| `'mergeTableCellLeft'`           | {@link module:table/commands/mergecellcommand~MergeCellCommand}             |
-| `'mergeTableCellUp'`             | {@link module:table/commands/mergecellcommand~MergeCellCommand}             |
-| `'mergeTableCellDown'`           | {@link module:table/commands/mergecellcommand~MergeCellCommand}             |
-| `'splitTableCellVertically'`     | {@link module:table/commands/splitcellcommand~SplitCellCommand}             |
-| `'splitTableCellHorizontally'`   | {@link module:table/commands/splitcellcommand~SplitCellCommand}             |
+### UI components
+
+The table plugins register the following UI components:
+
+<table>
+	<thead>
+		<th>{@link builds/guides/integration/configuration#toolbar-setup Component} name</th>
+		<th>Registered by</th>
+	</thead>
+	<tbody>
+		<tr>
+			<td>The <code>'insertTable'</code> dropdown</td>
+			<td rowspan="4">{@link module:table/table~Table}</td>
+		</tr>
+		<tr>
+			<td>The <code>'tableColumn'</code> dropdown</td>
+		</tr>
+		<tr>
+			<td>The <code>'tableRow'</code> dropdown</td>
+		</tr>
+		<tr>
+			<td>The <code>'mergeTableCells'</code> dropdown</td>
+		</tr>
+		<tr>
+			<td>The <code>'tableProperties'</code> button</td>
+			<td>{@link module:table/tableproperties~TableProperties}</td>
+		</tr>
+		<tr>
+			<td>The <code>'tableCellProperties'</code> button</td>
+			<td>{@link module:table/tablecellproperties~TableCellProperties}</td>
+		</tr>
+	</tbody>
+</table>
+
+#### Toolbars
 
 The {@link module:table/tabletoolbar~TableToolbar} plugin introduces two balloon toolbars for tables.
-* The content toolbar shows up when table cell is selected and is anchored to the table. It is possible to {@link module:table/table~TableConfig#contentToolbar configure} its content. Normally, it contains the table-related tools such as `'tableColumn'`, `'tableRow'`, and `'mergeTableCells'` dropdowns.
+* The content toolbar shows up when a table cell is selected and it is anchored to the table. It is possible to {@link module:table/table~TableConfig#contentToolbar configure} its content. Normally, the toolbar contains the table-related tools such as `'tableColumn'`, `'tableRow'`, and `'mergeTableCells'` dropdowns.
 * The table toolbar shows up when the whole table is selected, for instance using the widget handler. It is possible to {@link module:table/table~TableConfig#tableToolbar configure} its content.
 
+### Editor commands
+
+<table>
+	<thead>
+		<tr>
+			<th>{@link framework/guides/architecture/core-editor-architecture#commands Command} name</th>
+			<th>Command class</th>
+			<th>Belongs to (top–level plugin)</th>
+		</tr>
+	</thead>
+	<tbody>
+		<tr>
+			<td><code>'insertTable'</code></td>
+			<td>{@link module:table/commands/inserttablecommand~InsertTableCommand}</td>
+			<td rowspan="15">{@link module:table/table~Table}</td>
+		</tr>
+		<tr>
+			<td><code>'insertTableColumnLeft'</code></td>
+			<td>{@link module:table/commands/insertcolumncommand~InsertColumnCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'insertTableColumnRight'</code></td>
+			<td>{@link module:table/commands/insertcolumncommand~InsertColumnCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'insertTableRowAbove'</code></td>
+			<td>{@link module:table/commands/insertrowcommand~InsertRowCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'insertTableRowBelow'</code></td>
+			<td>{@link module:table/commands/insertrowcommand~InsertRowCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'removeTableColumn'</code></td>
+			<td>{@link module:table/commands/removecolumncommand~RemoveColumnCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'removeTableRow'</code></td>
+			<td>{@link module:table/commands/removerowcommand~RemoveRowCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'setTableColumnHeader'</code></td>
+			<td>{@link module:table/commands/setheadercolumncommand~SetHeaderColumnCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'setTableRowHeader'</code></td>
+			<td>{@link module:table/commands/setheaderrowcommand~SetHeaderRowCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'mergeTableCellRight'</code></td>
+			<td>{@link module:table/commands/mergecellcommand~MergeCellCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'mergeTableCellLeft'</code></td>
+			<td>{@link module:table/commands/mergecellcommand~MergeCellCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'mergeTableCellUp'</code></td>
+			<td>{@link module:table/commands/mergecellcommand~MergeCellCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'mergeTableCellDown'</code></td>
+			<td>{@link module:table/commands/mergecellcommand~MergeCellCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'splitTableCellVertically'</code></td>
+			<td>{@link module:table/commands/splitcellcommand~SplitCellCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'splitTableCellHorizontally'</code></td>
+			<td>{@link module:table/commands/splitcellcommand~SplitCellCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableBorderColor'</code></td>
+			<td>{@link module:table/tableproperties/commands/tablebordercolorcommand~TableBorderColorCommand}</td>
+			<td rowspan="7">{@link module:table/tableproperties~TableProperties}</td>
+		</tr>
+		<tr>
+			<td><code>'tableBorderStyle'</code></td>
+			<td>{@link module:table/tableproperties/commands/tableborderstylecommand~TableBorderStyleCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableBorderWidth'</code></td>
+			<td>{@link module:table/tableproperties/commands/tableborderwidthcommand~TableBorderWidthCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableAlignment'</code></td>
+			<td>{@link module:table/tableproperties/commands/tablealignmentcommand~TableAlignmentCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableWidth'</code></td>
+			<td>{@link module:table/tableproperties/commands/tablewidthcommand~TableWidthCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableHeight'</code></td>
+			<td>{@link module:table/tableproperties/commands/tableheightcommand~TableHeightCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableBackgroundColor'</code></td>
+			<td>{@link module:table/tableproperties/commands/tablebackgroundcolorcommand~TableBackgroundColorCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellBorderStyle'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellborderstylecommand~TableCellBorderStyleCommand}</td>
+			<td rowspan="9">{@link module:table/tablecellproperties~TableCellProperties}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellBorderColor'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellbordercolorcommand~TableCellBorderColorCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellBorderWidth'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellborderwidthcommand~TableCellBorderWidthCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellHorizontalAlignment'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellhorizontalalignmentcommand~TableCellHorizontalAlignmentCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellWidth'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellwidthcommand~TableCellWidthCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellHeight'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellheightcommand~TableCellHeightCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellPadding'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellpaddingcommand~TableCellPaddingCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellBackgroundColor'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellbackgroundcolorcommand~TableCellBackgroundColorCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'tableCellVerticalAlignment'</code></td>
+			<td>{@link module:table/tablecellproperties/commands/tablecellverticalalignmentcommand~TableCellVerticalAlignmentCommand}</td>
+		</tr>
+	</tbody>
+</table>
+
 <info-box>
 	We recommend using the official {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} for development and debugging. It will give you tons of useful information about the state of the editor such as internal data structures, selection, commands, and many more.
 </info-box>

+ 17 - 17
packages/ckeditor5-table/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-table",
-  "version": "16.0.0",
+  "version": "17.0.0",
   "description": "Table feature for CKEditor 5.",
   "keywords": [
     "ckeditor",
@@ -10,25 +10,25 @@
     "ckeditor5-plugin"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^16.0.0",
-    "@ckeditor/ckeditor5-ui": "^16.0.0",
-    "@ckeditor/ckeditor5-widget": "^16.0.0",
+    "@ckeditor/ckeditor5-core": "^17.0.0",
+    "@ckeditor/ckeditor5-ui": "^17.0.0",
+    "@ckeditor/ckeditor5-widget": "^17.0.0",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-alignment": "^16.0.0",
-    "@ckeditor/ckeditor5-block-quote": "^16.0.0",
-    "@ckeditor/ckeditor5-clipboard": "^16.0.0",
-    "@ckeditor/ckeditor5-editor-classic": "^16.0.0",
-    "@ckeditor/ckeditor5-engine": "^16.0.0",
-    "@ckeditor/ckeditor5-image": "^16.0.0",
-    "@ckeditor/ckeditor5-indent": "^16.0.0",
-    "@ckeditor/ckeditor5-list": "^16.0.0",
-    "@ckeditor/ckeditor5-media-embed": "^16.0.0",
-    "@ckeditor/ckeditor5-paragraph": "^16.0.0",
-    "@ckeditor/ckeditor5-typing": "^16.0.0",
-    "@ckeditor/ckeditor5-undo": "^16.0.0",
-    "@ckeditor/ckeditor5-utils": "^16.0.0",
+    "@ckeditor/ckeditor5-alignment": "^17.0.0",
+    "@ckeditor/ckeditor5-block-quote": "^17.0.0",
+    "@ckeditor/ckeditor5-clipboard": "^17.0.0",
+    "@ckeditor/ckeditor5-editor-classic": "^17.0.0",
+    "@ckeditor/ckeditor5-engine": "^17.0.0",
+    "@ckeditor/ckeditor5-image": "^17.0.0",
+    "@ckeditor/ckeditor5-indent": "^17.0.0",
+    "@ckeditor/ckeditor5-list": "^17.0.0",
+    "@ckeditor/ckeditor5-media-embed": "^17.0.0",
+    "@ckeditor/ckeditor5-paragraph": "^17.0.0",
+    "@ckeditor/ckeditor5-typing": "^17.0.0",
+    "@ckeditor/ckeditor5-undo": "^17.0.0",
+    "@ckeditor/ckeditor5-utils": "^17.0.0",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",

+ 18 - 2
packages/ckeditor5-table/src/commands/removecolumncommand.js

@@ -51,7 +51,6 @@ export default class RemoveColumnCommand extends Command {
 		const table = tableRow.parent;
 
 		const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
-		const row = table.getChildIndex( tableRow );
 
 		// Cache the table before removing or updating colspans.
 		const tableMap = [ ...new TableWalker( table ) ];
@@ -59,10 +58,12 @@ export default class RemoveColumnCommand extends Command {
 		// Get column index of removed column.
 		const cellData = tableMap.find( value => value.cell === tableCell );
 		const removedColumn = cellData.column;
+		const selectionRow = cellData.row;
+		const cellToFocus = getCellToFocus( tableCell );
 
 		model.change( writer => {
 			// Update heading columns attribute if removing a row from head section.
-			if ( headingColumns && row <= headingColumns ) {
+			if ( headingColumns && selectionRow <= headingColumns ) {
 				writer.setAttribute( 'headingColumns', headingColumns - 1, table );
 			}
 
@@ -75,6 +76,21 @@ export default class RemoveColumnCommand extends Command {
 					writer.remove( cell );
 				}
 			}
+
+			writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );
 		} );
 	}
 }
+
+// Returns a proper table cell to focus after removing a column. It should be a next sibling to selection visually stay in place but:
+// - selection is on last table cell it will return previous cell.
+// - table cell is spanned over 2+ columns - it will be truncated so the selection should stay in that cell.
+function getCellToFocus( tableCell ) {
+	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
+
+	if ( colspan > 1 ) {
+		return tableCell;
+	}
+
+	return tableCell.nextSibling ? tableCell.nextSibling : tableCell.previousSibling;
+}

+ 33 - 7
packages/ckeditor5-table/src/commands/removerowcommand.js

@@ -48,30 +48,35 @@ export default class RemoveRowCommand extends Command {
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 
-		const currentRow = table.getChildIndex( tableRow );
+		const removedRow = table.getChildIndex( tableRow );
+
+		const tableMap = [ ...new TableWalker( table, { endRow: removedRow } ) ];
+
+		const cellData = tableMap.find( value => value.cell === tableCell );
+
 		const headingRows = table.getAttribute( 'headingRows' ) || 0;
 
+		const columnToFocus = cellData.column;
+
 		model.change( writer => {
-			if ( headingRows && currentRow <= headingRows ) {
+			if ( headingRows && removedRow <= headingRows ) {
 				updateNumericAttribute( 'headingRows', headingRows - 1, table, writer, 0 );
 			}
 
-			const tableMap = [ ...new TableWalker( table, { endRow: currentRow } ) ];
-
 			const cellsToMove = new Map();
 
 			// Get cells from removed row that are spanned over multiple rows.
 			tableMap
-				.filter( ( { row, rowspan } ) => row === currentRow && rowspan > 1 )
+				.filter( ( { row, rowspan } ) => row === removedRow && rowspan > 1 )
 				.forEach( ( { column, cell, rowspan } ) => cellsToMove.set( column, { cell, rowspanToSet: rowspan - 1 } ) );
 
 			// Reduce rowspan on cells that are above removed row and overlaps removed row.
 			tableMap
-				.filter( ( { row, rowspan } ) => row <= currentRow - 1 && row + rowspan > currentRow )
+				.filter( ( { row, rowspan } ) => row <= removedRow - 1 && row + rowspan > removedRow )
 				.forEach( ( { cell, rowspan } ) => updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer ) );
 
 			// Move cells to another row.
-			const targetRow = currentRow + 1;
+			const targetRow = removedRow + 1;
 			const tableWalker = new TableWalker( table, { includeSpanned: true, startRow: targetRow, endRow: targetRow } );
 
 			let previousCell;
@@ -93,6 +98,27 @@ export default class RemoveRowCommand extends Command {
 			}
 
 			writer.remove( tableRow );
+
+			const cellToFocus = getCellToFocus( table, removedRow, columnToFocus );
+			writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );
 		} );
 	}
 }
+
+// Returns a cell that should be focused before removing the row, belonging to the same column as the currently focused cell.
+function getCellToFocus( table, removedRow, columnToFocus ) {
+	const row = table.getChild( removedRow );
+
+	// Default to first table cell.
+	let cellToFocus = row.getChild( 0 );
+	let column = 0;
+
+	for ( const tableCell of row.getChildren() ) {
+		if ( column > columnToFocus ) {
+			return cellToFocus;
+		}
+
+		cellToFocus = tableCell;
+		column += parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
+	}
+}

+ 5 - 4
packages/ckeditor5-table/src/commands/utils.js

@@ -10,14 +10,15 @@
 import { isObject } from 'lodash-es';
 
 /**
- * Returns the parent element of the given name. Returns undefined if the position is not inside the desired parent.
+ * Returns the parent element of the given name. Returns undefined if the position or the element is not inside the desired parent.
  *
  * @param {String} parentName The name of the parent element to find.
- * @param {module:engine/model/position~Position|module:engine/model/position~Position} position The position to start searching.
+ * @param {module:engine/model/position~Position|module:engine/model/position~Position} positionOrElement The position or
+ * the parentElement to start searching.
  * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
  */
-export function findAncestor( parentName, position ) {
-	let parent = position.parent;
+export function findAncestor( parentName, positionOrElement ) {
+	let parent = positionOrElement.parent;
 
 	while ( parent ) {
 		if ( parent.name === parentName ) {

+ 1 - 1
packages/ckeditor5-table/src/tablecellproperties.js

@@ -61,7 +61,7 @@ export default class TableCellProperties extends Plugin {
  *			}
  *		};
  *
- * **Note**: The configurations do not impact the data being loaded into the editor,
+ * **Note**: The configurations do not impact the data loaded into the editor,
  * i.e. they do not limit or filter the colors in the data. They are used only in the user interface
  * allowing users to pick colors in a more convenient way.
  *

+ 2 - 2
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellbackgroundcolorcommand.js

@@ -13,7 +13,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  * The table cell background color command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellBackgroundColor'` editor command.
+ * the `'tableCellBackgroundColor'` editor command.
  *
  * To change the background color of selected cells, execute the command:
  *
@@ -21,7 +21,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *			value: '#f00'
  *		} );
  *
- * @extends module:core/command~Command
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellBackgroundColorCommand extends TableCellPropertyCommand {
 	/**

+ 3 - 3
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellbordercolorcommand.js

@@ -14,7 +14,7 @@ import { getSingleValue } from '../../commands/utils';
  * The table cell border color command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellBorderColor'` editor command.
+ * the `'tableCellBorderColor'` editor command.
  *
  * To change the border color of selected cells, execute the command:
  *
@@ -22,11 +22,11 @@ import { getSingleValue } from '../../commands/utils';
  *			value: '#f00'
  *		} );
  *
- * @extends module:core/command~Command
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellBorderColorCommand extends TableCellPropertyCommand {
 	/**
-	 * Creates a new `TableCellBorderWidthCommand` instance.
+	 * Creates a new `TableCellBorderColorCommand` instance.
 	 *
 	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
 	 */

+ 3 - 3
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderstylecommand.js

@@ -14,7 +14,7 @@ import { getSingleValue } from '../../commands/utils';
  * The table cell border style command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellBorderStyle'` editor command.
+ * the `'tableCellBorderStyle'` editor command.
  *
  * To change the border style of selected cells, execute the command:
  *
@@ -22,11 +22,11 @@ import { getSingleValue } from '../../commands/utils';
  *			value: 'dashed'
  *		} );
  *
- * @extends module:core/command~Command
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellBorderStyleCommand extends TableCellPropertyCommand {
 	/**
-	 * Creates a new `TableCellBorderWidthCommand` instance.
+	 * Creates a new `TableCellBorderStyleCommand` instance.
 	 *
 	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
 	 */

+ 2 - 2
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderwidthcommand.js

@@ -14,7 +14,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  * The table cell border width command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellBorderWidth'` editor command.
+ * the `'tableCellBorderWidth'` editor command.
  *
  * To change the border width of selected cells, execute the command:
  *
@@ -30,7 +30,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *
  * will set the `borderWidth` attribute to `'5px'` in the model.
  *
- * @extends module:core/command~Command
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellBorderWidthCommand extends TableCellPropertyCommand {
 	/**

+ 2 - 2
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellheightcommand.js

@@ -14,7 +14,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  * The table cell height command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellHeight'` editor command.
+ * the `'tableCellHeight'` editor command.
  *
  * To change the height of selected cells, execute the command:
  *
@@ -30,7 +30,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *
  * will set the `height` attribute to `'50px'` in the model.
  *
- * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellHeightCommand extends TableCellPropertyCommand {
 	/**

+ 2 - 2
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js

@@ -13,7 +13,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  * The table cell horizontal alignment command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellHorizontalAlignment'` editor command.
+ * the `'tableCellHorizontalAlignment'` editor command.
  *
  * To change the horizontal text alignment of selected cells, execute the command:
  *
@@ -21,7 +21,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *			value: 'right'
  *		} );
  *
- * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellHorizontalAlignmentCommand extends TableCellPropertyCommand {
 	/**

+ 3 - 2
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpaddingcommand.js

@@ -14,7 +14,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  * The table cell padding command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellPadding'` editor command.
+ * the `'tableCellPadding'` editor command.
  *
  * To change the padding of selected cells, execute the command:
  *
@@ -29,7 +29,8 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *		} );
  *
  * will set the `padding` attribute to `'5px'` in the model.
- * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ *
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellPaddingCommand extends TableCellPropertyCommand {
 	/**

+ 3 - 3
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js

@@ -50,9 +50,9 @@ export default class TableCellPropertyCommand extends Command {
 	 * @fires execute
 	 * @param {Object} [options]
 	 * @param {*} [options.value] If set, the command will set the attribute on selected table cells.
-	 * If it is not set, the command will remove the attribute from selected table cells.
+	 * If it is not set, the command will remove the attribute from the selected table cells.
 	 * @param {module:engine/model/batch~Batch} [options.batch] Pass the model batch instance to the command to aggregate changes,
-	 * e.g. allow a single undo step for multiple executions.
+	 * for example to allow a single undo step for multiple executions.
 	 */
 	execute( options = {} ) {
 		const model = this.editor.model;
@@ -89,7 +89,7 @@ export default class TableCellPropertyCommand extends Command {
 	}
 
 	/**
-	 * Returns the proper model value. Can be used to add a default unit to numeric values.
+	 * Returns the proper model value. It can be used to add a default unit to numeric values.
 	 *
 	 * @private
 	 * @param {*} value

+ 5 - 5
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellverticalalignmentcommand.js

@@ -13,7 +13,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  * The table cell vertical alignment command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellVerticalAlignment'` editor command.
+ * the `'tableCellVerticalAlignment'` editor command.
  *
  * To change the vertical text alignment of selected cells, execute the command:
  *
@@ -21,15 +21,15 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *			value: 'top'
  *		} );
  *
- * The following values are allowed corresponding to
- * [the `vertical-align` CSS attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align):
+ * The following values, corresponding to the
+ * [`vertical-align` CSS attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align), are allowed:
  *
  * * `'top'`
  * * `'bottom'`
  *
- * The `'middle'` value is default one so there's no need to set this value.
+ * The `'middle'` value is the default one so there is no need to set it.
  *
- * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellVerticalAlignmentCommand extends TableCellPropertyCommand {
 	/**

+ 4 - 4
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellwidthcommand.js

@@ -14,7 +14,7 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  * The table cell width command.
  *
  * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
- * `'tableCellWidth'` editor command.
+ * the `'tableCellWidth'` editor command.
  *
  * To change the width of selected cells, execute the command:
  *
@@ -22,15 +22,15 @@ import TableCellPropertyCommand from './tablecellpropertycommand';
  *			value: '50px'
  *		} );
  *
- * **Note**: This command adds a default `'px'` unit to a numeric values. Executing:
+ * **Note**: This command adds a default `'px'` unit to numeric values. Executing:
  *
  *		editor.execute( 'tableCellWidth', {
  *			value: '50'
  *		} );
  *
- * Will set `width` attribute to `'50px'` in the model.
+ * will set the `width` attribute to `'50px'` in the model.
  *
- * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
  */
 export default class TableCellWidthCommand extends TableCellPropertyCommand {
 	/**

+ 8 - 8
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting.js

@@ -29,21 +29,21 @@ const VALIGN_VALUES_REG_EXP = /^(top|bottom)$/;
 /**
  * The table cell properties editing feature.
  *
- * Introduces table cells's model attributes and their conversion:
+ * Introduces table cell model attributes and their conversion:
  *
  * - border: `borderStyle`, `borderColor` and `borderWidth`
  * - background color: `backgroundColor`
  * - cell padding: `padding`
  * - horizontal and vertical alignment: `horizontalAlignment`, `verticalAlignment`
- * - cell width & height: `width` & `height`
+ * - cell width and height: `width`, `height`
  *
  * It also registers commands used to manipulate the above attributes:
  *
- * - border: `'tableCellBorderStyle'`, `'tableCellBorderColor'` and `'tableCellBorderWidth'` commands
- * - background color: `'tableCellBackgroundColor'`
- * - cell padding: `'tableCellPadding'`
- * - horizontal and vertical alignment: `'tableCellHorizontalAlignment'`, `'tableCellVerticalAlignment'`
- * - width & height: `'tableCellWidth'` & `'tableCellHeight'`
+ * - border: the `'tableCellBorderStyle'`, `'tableCellBorderColor'` and `'tableCellBorderWidth'` commands
+ * - background color: the `'tableCellBackgroundColor'` command
+ * - cell padding: the `'tableCellPadding'` command
+ * - horizontal and vertical alignment: the `'tableCellHorizontalAlignment'` and `'tableCellVerticalAlignment'` commands
+ * - width and height: the `'tableCellWidth'` and `'tableCellHeight'` commands
  *
  * @extends module:core/plugin~Plugin
  */
@@ -99,7 +99,7 @@ export default class TableCellPropertiesEditing extends Plugin {
 	}
 }
 
-// Enables `'borderStyle'`, `'borderColor'` and `'borderWidth'` attributes for table cells.
+// Enables the `'borderStyle'`, `'borderColor'` and `'borderWidth'` attributes for table cells.
 //
 // @param {module:engine/model/schema~Schema} schema
 // @param {module:engine/conversion/conversion~Conversion} conversion

+ 7 - 7
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js

@@ -34,7 +34,7 @@ const ERROR_TEXT_TIMEOUT = 500;
 
 /**
  * The table cell properties UI plugin. It introduces the `'tableCellProperties'` button
- * that opens a form allowing to specify visual styling of a table cell.
+ * that opens a form allowing to specify the visual styling of a table cell.
  *
  * It uses the
  * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
@@ -272,7 +272,7 @@ export default class TableCellPropertiesUI extends Plugin {
 	/**
 	 * Shows the {@link #view} in the {@link #_balloon}.
 	 *
-	 * **Note**: Each time a view is shown, the new {@link #_undoStepBatch} is created that contains
+	 * **Note**: Each time a view is shown, a new {@link #_undoStepBatch} is created. It contains
 	 * all changes made to the document when the view is visible, allowing a single undo step
 	 * for all of them.
 	 *
@@ -322,7 +322,7 @@ export default class TableCellPropertiesUI extends Plugin {
 	}
 
 	/**
-	 * Returns `true` when the {@link #view} is the visible in the {@link #_balloon}.
+	 * Returns `true` when the {@link #view} is visible in the {@link #_balloon}.
 	 *
 	 * @private
 	 * @type {Boolean}
@@ -342,7 +342,7 @@ export default class TableCellPropertiesUI extends Plugin {
 	}
 
 	/**
-	 * Creates a callback that when executed upon {@link #view view's} property change
+	 * Creates a callback that when executed upon the {@link #view view's} property change
 	 * executes a related editor command with the new property value.
 	 *
 	 * @private
@@ -359,9 +359,9 @@ export default class TableCellPropertiesUI extends Plugin {
 	}
 
 	/**
-	 * Creates a callback that when executed upon {@link #view view's} property change:
-	 * * executes a related editor command with the new property value if the value is valid,
-	 * * or sets the error text next to the invalid field, if the value did not pass the validation.
+	 * Creates a callback that when executed upon the {@link #view view's} property change:
+	 * * Executes a related editor command with the new property value if the value is valid,
+	 * * Or sets the error text next to the invalid field, if the value did not pass the validation.
 	 *
 	 * @private
 	 * @param {Object} options

+ 97 - 0
packages/ckeditor5-table/src/tableclipboard.js

@@ -0,0 +1,97 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/tableclipboard
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import TableSelection from './tableselection';
+
+/**
+ * The table clipboard integration plugin.
+ *
+ * It introduces the ability to copy selected table cells.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class TableClipboard extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TableClipboard';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ TableSelection ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const viewDocument = editor.editing.view.document;
+
+		/**
+		 * A table selection plugin instance.
+		 *
+		 * @private
+		 * @readonly
+		 * @member {module:table/tableselection~TableSelection} module:tableclipboard~TableClipboard#_tableSelection
+		 */
+		this._tableSelection = editor.plugins.get( 'TableSelection' );
+
+		this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopy( evt, data ), { priority: 'normal' } );
+		this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCut( evt, data ), { priority: 'high' } );
+	}
+
+	/**
+	 * A clipboard "copy" event handler.
+	 *
+	 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event.
+	 * @param {Object} data Clipboard event data.
+	 * @private
+	 */
+	_onCopy( evt, data ) {
+		const tableSelection = this._tableSelection;
+
+		if ( !tableSelection.hasMultiCellSelection ) {
+			return;
+		}
+
+		data.preventDefault();
+		evt.stop();
+
+		const dataController = this.editor.data;
+		const viewDocument = this.editor.editing.view.document;
+
+		const content = dataController.toView( tableSelection.getSelectionAsFragment() );
+
+		viewDocument.fire( 'clipboardOutput', {
+			dataTransfer: data.dataTransfer,
+			content,
+			method: evt.name
+		} );
+	}
+
+	/**
+	 * A clipboard "cut" event handler.
+	 *
+	 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event.
+	 * @param {Object} data Clipboard event data.
+	 * @private
+	 */
+	_onCut( evt, data ) {
+		if ( this._tableSelection.hasMultiCellSelection ) {
+			data.preventDefault();
+			evt.stop();
+		}
+	}
+}

+ 1 - 1
packages/ckeditor5-table/src/tableediting.js

@@ -75,7 +75,7 @@ export default class TableEditing extends Plugin {
 		schema.register( 'tableCell', {
 			allowIn: 'tableRow',
 			allowAttributes: [ 'colspan', 'rowspan' ],
-			isLimit: true
+			isObject: true
 		} );
 
 		// Allow all $block content inside table cell.

+ 1 - 1
packages/ckeditor5-table/src/tableproperties.js

@@ -62,7 +62,7 @@ export default class TableProperties extends Plugin {
  *			}
  *		};
  *
- * **Note**: The configurations do not impact the data being loaded into the editor,
+ * **Note**: The configurations do not impact the data loaded into the editor,
  * i.e. they do not limit or filter the colors in the data. They are used only in the user interface
  * allowing users to pick colors in a more convenient way.
  *

+ 2 - 2
packages/ckeditor5-table/src/tableproperties/commands/tablealignmentcommand.js

@@ -13,7 +13,7 @@ import TablePropertyCommand from './tablepropertycommand';
  * The table alignment command.
  *
  * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
- * `'tableAlignment'` editor command.
+ * the `'tableAlignment'` editor command.
  *
  * To change the alignment of the selected table, execute the command:
  *
@@ -21,7 +21,7 @@ import TablePropertyCommand from './tablepropertycommand';
  *			value: 'right'
  *		} );
  *
- * @extends module:table/tableproperties/commands/tablepropertycommand
+ * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
  */
 export default class TableAlignmentCommand extends TablePropertyCommand {
 	/**

+ 2 - 2
packages/ckeditor5-table/src/tableproperties/commands/tablebackgroundcolorcommand.js

@@ -13,7 +13,7 @@ import TablePropertyCommand from './tablepropertycommand';
  * The table background color command.
  *
  * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
- * `'tableBackgroundColor'` editor command.
+ * the `'tableBackgroundColor'` editor command.
  *
  * To change the background color of the selected table, execute the command:
  *
@@ -21,7 +21,7 @@ import TablePropertyCommand from './tablepropertycommand';
  *			value: '#f00'
  *		} );
  *
- * @extends module:table/tableproperties/commands/tablepropertycommand
+ * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
  */
 export default class TableBackgroundColorCommand extends TablePropertyCommand {
 	/**

+ 2 - 2
packages/ckeditor5-table/src/tableproperties/commands/tablebordercolorcommand.js

@@ -14,7 +14,7 @@ import { getSingleValue } from '../../commands/utils';
  * The table border color command.
  *
  * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
- * `'tableBorderColor'` editor command.
+ * the `'tableBorderColor'` editor command.
  *
  * To change the border color of the selected table, execute the command:
  *
@@ -22,7 +22,7 @@ import { getSingleValue } from '../../commands/utils';
  *			value: '#f00'
  *		} );
  *
- * @extends module:table/tableproperties/commands/tablepropertycommand
+ * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
  */
 export default class TableBorderColorCommand extends TablePropertyCommand {
 	/**

+ 2 - 2
packages/ckeditor5-table/src/tableproperties/commands/tableborderstylecommand.js

@@ -14,7 +14,7 @@ import { getSingleValue } from '../../commands/utils';
  * The table style border command.
  *
  * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
- * `'tableBorderStyle'` editor command.
+ * the `'tableBorderStyle'` editor command.
  *
  * To change the border style of the selected table, execute the command:
  *
@@ -22,7 +22,7 @@ import { getSingleValue } from '../../commands/utils';
  *			value: 'dashed'
  *		} );
  *
- * @extends module:table/tableproperties/commands/tablepropertycommand
+ * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
  */
 export default class TableBorderStyleCommand extends TablePropertyCommand {
 	/**

+ 3 - 3
packages/ckeditor5-table/src/tableproperties/commands/tableborderwidthcommand.js

@@ -14,7 +14,7 @@ import TablePropertyCommand from './tablepropertycommand';
  * The table width border command.
  *
  * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
- * `'tableBorderWidth'` editor command.
+ * the `'tableBorderWidth'` editor command.
  *
  * To change the border width of the selected table, execute the command:
  *
@@ -28,9 +28,9 @@ import TablePropertyCommand from './tablepropertycommand';
  *			value: '5'
  *		} );
  *
- * Will set the `borderWidth` attribute to `'5px'` in the model.
+ * will set the `borderWidth` attribute to `'5px'` in the model.
  *
- * @extends module:table/tableproperties/commands/tablepropertycommand
+ * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
  */
 export default class TableBorderWidthCommand extends TablePropertyCommand {
 	/**

+ 4 - 4
packages/ckeditor5-table/src/tableproperties/commands/tableheightcommand.js

@@ -14,9 +14,9 @@ import { addDefaultUnitToNumericValue } from '../../commands/utils';
  * The table height command.
  *
  * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
- * `'tableHeight'` editor command.
+ * the `'tableHeight'` editor command.
  *
- * To change height of the selected table, execute the command:
+ * To change the height of the selected table, execute the command:
  *
  *		editor.execute( 'tableHeight', {
  *			value: '500px'
@@ -28,9 +28,9 @@ import { addDefaultUnitToNumericValue } from '../../commands/utils';
  *			value: '50'
  *		} );
  *
- * Will set the `height` attribute to `'50px'` in the model.
+ * will set the `height` attribute to `'50px'` in the model.
  *
- * @extends module:core/command~Command
+ * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
  */
 export default class TableHeightCommand extends TablePropertyCommand {
 	/**

+ 3 - 3
packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js

@@ -14,7 +14,7 @@ import { findAncestor } from '../../commands/utils';
 /**
  * The table cell attribute command.
  *
- * The command is a base command for other table property commands.
+ * This command is a base command for other table property commands.
  *
  * @extends module:core/command~Command
  */
@@ -52,7 +52,7 @@ export default class TablePropertyCommand extends Command {
 	 * @param {*} [options.value] If set, the command will set the attribute on the selected table.
 	 * If not set, the command will remove the attribute from the selected table.
 	 * @param {module:engine/model/batch~Batch} [options.batch] Pass the model batch instance to the command to aggregate changes,
-	 * e.g. allow a single undo step for multiple executions.
+	 * for example, to allow a single undo step for multiple executions.
 	 */
 	execute( options = {} ) {
 		const model = this.editor.model;
@@ -88,7 +88,7 @@ export default class TablePropertyCommand extends Command {
 	}
 
 	/**
-	 * Returns the proper model value. Can be used to add a default unit to numeric values.
+	 * Returns the proper model value. It can be used to add a default unit to numeric values.
 	 *
 	 * @private
 	 * @param {*} value

+ 4 - 4
packages/ckeditor5-table/src/tableproperties/commands/tablewidthcommand.js

@@ -14,9 +14,9 @@ import TablePropertyCommand from './tablepropertycommand';
  * The table width command.
  *
  * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
- * `'tableWidth'` editor command.
+ * the `'tableWidth'` editor command.
  *
- * To change width of the selected table, execute the command:
+ * To change the width of the selected table, execute the command:
  *
  *		editor.execute( 'tableWidth', {
  *			value: '400px'
@@ -28,9 +28,9 @@ import TablePropertyCommand from './tablepropertycommand';
  *			value: '50'
  *		} );
  *
- * Will set the `width` attribute to `'50px'` in the model.
+ * will set the `width` attribute to `'50px'` in the model.
  *
- * @extends module:table/tableproperties/commands/tablepropertycommand
+ * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
  */
 export default class TableWidthCommand extends TablePropertyCommand {
 	/**

+ 282 - 0
packages/ckeditor5-table/src/tableselection.js

@@ -0,0 +1,282 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/tableselection
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import TableWalker from './tablewalker';
+import TableUtils from './tableutils';
+import { setupTableSelectionHighlighting } from './tableselection/converters';
+import MouseSelectionHandler from './tableselection/mouseselectionhandler';
+import { findAncestor } from './commands/utils';
+import cropTable from './tableselection/croptable';
+
+import '../theme/tableselection.css';
+
+/**
+ * The table selection plugin.
+ *
+ * It introduces the ability to select table cells. The table selection is described by two nodes: start and end.
+ * Both are the oposite corners of an rectangle that spans over them.
+ *
+ * Consider a table:
+ *
+ *		    0   1   2   3
+ *		  +---+---+---+---+
+ *		0 | a | b | c | d |
+ *		  +-------+   +---+
+ *		1 | e | f |   | g |
+ *		  +---+---+---+---+
+ *		2 | h | i     | j |
+ *		  +---+---+---+---+
+ *
+ * Setting the table selection start in table cell "b" and the end in table cell "g" will select table cells: "b", "c", "d", "f", and "g".
+ * The cells that span over multiple rows or columns can extend over the selection rectangle. For instance, setting a selection from
+ * the table cell "a" to the table cell "i" will create a selection in which the table cell "i" will be (partially) outside
+ * the rectangle of selected cells: "a", "b", "e", "f", "h", and "i".
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class TableSelection extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TableSelection';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ TableUtils ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * A mouse selection handler.
+		 *
+		 * @private
+		 * @readonly
+		 * @member {module:table/tableselection/mouseselectionhandler~MouseSelectionHandler}
+		 */
+		this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing );
+
+		/**
+		 * A reference to the table utilities used across the class.
+		 *
+		 * @private
+		 * @readonly
+		 * @member {module:table/tableutils~TableUtils} #_tableUtils
+		 */
+	}
+
+	/**
+	 * A flag indicating that there are selected table cells and the selection includes more than one table cell.
+	 *
+	 * @type {Boolean}
+	 */
+	get hasMultiCellSelection() {
+		return !!this._startElement && !!this._endElement && this._startElement !== this._endElement;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const selection = editor.model.document.selection;
+
+		this._tableUtils = editor.plugins.get( 'TableUtils' );
+
+		setupTableSelectionHighlighting( editor, this );
+
+		selection.on( 'change:range', () => this._clearSelectionOnExternalChange( selection ) );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		super.destroy();
+		this._mouseHandler.stopListening();
+	}
+
+	/**
+	 * Marks the table cell as a start of a table selection.
+	 *
+	 *		editor.plugins.get( 'TableSelection' ).startSelectingFrom( tableCell );
+	 *
+	 * This method will clear the previous selection. The model selection will not be updated until
+	 * the {@link #setSelectingTo} method is used.
+	 *
+	 * @param {module:engine/model/element~Element} tableCell
+	 */
+	startSelectingFrom( tableCell ) {
+		this.clearSelection();
+
+		this._startElement = tableCell;
+		this._endElement = tableCell;
+	}
+
+	/**
+	 * Updates the current table selection end element. Table selection is defined by a start and an end element.
+	 * This method updates the end element. Must be preceded by {@link #startSelectingFrom}.
+	 *
+	 *		editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
+	 *
+	 *		editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
+	 *
+	 * This method will update model selection if start and end cells are different and belongs to the same table.
+	 *
+	 * @param {module:engine/model/element~Element} tableCell
+	 */
+	setSelectingTo( tableCell ) {
+		if ( !this._startElement ) {
+			this._startElement = tableCell;
+		}
+
+		const table = this._startElement.parent.parent;
+
+		// Do not add tableCell to selection if it is from other table or is already set as end element.
+		if ( table !== tableCell.parent.parent || this._endElement === tableCell ) {
+			return;
+		}
+
+		this._endElement = tableCell;
+		this._updateModelSelection();
+	}
+
+	/**
+	 * Stops the selection process (but do not clear the current selection).
+	 * The selection process is finished but the selection in the model remains.
+	 *
+	 *		editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
+	 *		editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
+	 *		editor.plugins.get( 'TableSelection' ).stopSelection();
+	 *
+	 * To clear the selection use {@link #clearSelection}.
+	 *
+	 * @param {module:engine/model/element~Element} [tableCell]
+	 */
+	stopSelection( tableCell ) {
+		if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) {
+			this._endElement = tableCell;
+		}
+
+		this._updateModelSelection();
+	}
+
+	/**
+	 * Stops the current selection process and clears the table selection in the model.
+	 *
+	 *		editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
+	 *		editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
+	 *		editor.plugins.get( 'TableSelection' ).stopSelection();
+	 *
+	 *		editor.plugins.get( 'TableSelection' ).clearSelection();
+	 */
+	clearSelection() {
+		this._startElement = undefined;
+		this._endElement = undefined;
+	}
+
+	/**
+	 * Returns an iterator for selected table cells.
+	 *
+	 *		tableSelection.startSelectingFrom( startTableCell );
+	 *		tableSelection.stopSelection( endTableCell );
+	 *
+	 *		const selectedTableCells = Array.from( tableSelection.getSelectedTableCells() );
+	 *		// The above array will represent a rectangular table selection.
+	 *
+	 * @returns {Iterable.<module:engine/model/element~Element>}
+	 */
+	* getSelectedTableCells() {
+		if ( !this.hasMultiCellSelection ) {
+			return;
+		}
+
+		const startLocation = this._tableUtils.getCellLocation( this._startElement );
+		const endLocation = this._tableUtils.getCellLocation( this._endElement );
+
+		const startRow = Math.min( startLocation.row, endLocation.row );
+		const endRow = Math.max( startLocation.row, endLocation.row );
+
+		const startColumn = Math.min( startLocation.column, endLocation.column );
+		const endColumn = Math.max( startLocation.column, endLocation.column );
+
+		for ( const cellInfo of new TableWalker( findAncestor( 'table', this._startElement ), { startRow, endRow } ) ) {
+			if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
+				yield cellInfo.cell;
+			}
+		}
+	}
+
+	/**
+	 * Returns selected table fragment as a document fragment.
+	 *
+	 * @returns {module:engine/model/documentfragment~DocumentFragment|undefined}
+	 */
+	getSelectionAsFragment() {
+		if ( !this.hasMultiCellSelection ) {
+			return;
+		}
+
+		return this.editor.model.change( writer => {
+			const documentFragment = writer.createDocumentFragment();
+
+			const table = cropTable( this.getSelectedTableCells(), this._tableUtils, writer );
+			writer.insert( table, documentFragment, 0 );
+
+			return documentFragment;
+		} );
+	}
+
+	/**
+	 * Synchronizes the model selection with currently selected table cells.
+	 *
+	 * @private
+	 */
+	_updateModelSelection() {
+		if ( !this.hasMultiCellSelection ) {
+			return;
+		}
+
+		const editor = this.editor;
+		const model = editor.model;
+
+		const modelRanges = [];
+
+		for ( const tableCell of this.getSelectedTableCells() ) {
+			modelRanges.push( model.createRangeOn( tableCell ) );
+		}
+
+		// Update model's selection
+		model.change( writer => {
+			writer.setSelection( modelRanges );
+		} );
+	}
+
+	/**
+	 * Checks if the selection has changed via an external change and if it is required to clear the internal state of the plugin.
+	 *
+	 * @param {module:engine/model/documentselection~DocumentSelection} selection
+	 * @private
+	 */
+	_clearSelectionOnExternalChange( selection ) {
+		if ( selection.rangeCount <= 1 && this.hasMultiCellSelection ) {
+			this.clearSelection();
+		}
+	}
+}

+ 49 - 0
packages/ckeditor5-table/src/tableselection/converters.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/tableselection/converters
+ */
+
+/**
+ * Adds a visual highlight style to selected table cells.
+ *
+ * @param {module:core/editor/editor~Editor} editor
+ * @param {module:table/tableselection~TableSelection} tableSelection
+ */
+export function setupTableSelectionHighlighting( editor, tableSelection ) {
+	const highlighted = new Set();
+
+	editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+		const view = editor.editing.view;
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
+
+		if ( tableSelection.hasMultiCellSelection ) {
+			clearHighlightedTableCells( highlighted, view );
+
+			for ( const tableCell of tableSelection.getSelectedTableCells() ) {
+				const viewElement = conversionApi.mapper.toViewElement( tableCell );
+
+				viewWriter.addClass( 'ck-editor__editable_selected', viewElement );
+				highlighted.add( viewElement );
+			}
+
+			viewWriter.setSelection( viewSelection.getRanges(), { fake: true, label: 'TABLE' } );
+		} else {
+			clearHighlightedTableCells( highlighted, view );
+		}
+	}, { priority: 'lowest' } ) );
+}
+
+function clearHighlightedTableCells( highlighted, view ) {
+	const previous = [ ...highlighted.values() ];
+
+	view.change( writer => {
+		for ( const previouslyHighlighted of previous ) {
+			writer.removeClass( 'ck-editor__editable_selected', previouslyHighlighted );
+		}
+	} );
+}

+ 152 - 0
packages/ckeditor5-table/src/tableselection/croptable.js

@@ -0,0 +1,152 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/tableselection/croptable
+ */
+
+import { findAncestor } from '../commands/utils';
+
+/**
+ * Returns cropped table from selected table cells.
+ *
+ * This is to be used with table selection
+ *
+ *		tableSelection.startSelectingFrom( startCell )
+ *		tableSelection.setSelectingFrom( endCell )
+ *
+ *		const croppedTable = cropTable( tableSelection.getSelectedTableCells );
+ *
+ * **Note**: This function is used also by {@link module:table/tableselection~TableSelection#getSelectionAsFragment}
+ *
+ * @param {Iterable.<module:engine/model/element~Element>} selectedTableCellsIterator
+ * @param {module:table/tableutils~TableUtils} tableUtils
+ * @param {module:engine/model/writer~Writer} writer
+ * @returns {module:engine/model/element~Element}
+ */
+export default function cropTable( selectedTableCellsIterator, tableUtils, writer ) {
+	const selectedTableCells = Array.from( selectedTableCellsIterator );
+	const startElement = selectedTableCells[ 0 ];
+	const endElement = selectedTableCells[ selectedTableCells.length - 1 ];
+
+	const { row: startRow, column: startColumn } = tableUtils.getCellLocation( startElement );
+
+	const tableCopy = makeTableCopy( selectedTableCells, startColumn, writer, tableUtils );
+
+	const { row: endRow, column: endColumn } = tableUtils.getCellLocation( endElement );
+	const selectionWidth = endColumn - startColumn + 1;
+	const selectionHeight = endRow - startRow + 1;
+
+	trimTable( tableCopy, selectionWidth, selectionHeight, writer, tableUtils );
+
+	const sourceTable = findAncestor( 'table', startElement );
+	addHeadingsToTableCopy( tableCopy, sourceTable, startRow, startColumn, writer );
+
+	return tableCopy;
+}
+
+// Creates a table copy from a selected table cells.
+//
+// It fills "gaps" in copied table - ie when cell outside copied range was spanning over selection.
+function makeTableCopy( selectedTableCells, startColumn, writer, tableUtils ) {
+	const tableCopy = writer.createElement( 'table' );
+
+	const rowToCopyMap = new Map();
+	const copyToOriginalColumnMap = new Map();
+
+	for ( const tableCell of selectedTableCells ) {
+		const row = findAncestor( 'tableRow', tableCell );
+
+		if ( !rowToCopyMap.has( row ) ) {
+			const rowCopy = row._clone();
+			writer.append( rowCopy, tableCopy );
+			rowToCopyMap.set( row, rowCopy );
+		}
+
+		const tableCellCopy = tableCell._clone( true );
+		const { column } = tableUtils.getCellLocation( tableCell );
+
+		copyToOriginalColumnMap.set( tableCellCopy, column );
+
+		writer.append( tableCellCopy, rowToCopyMap.get( row ) );
+	}
+
+	addMissingTableCells( tableCopy, startColumn, copyToOriginalColumnMap, writer, tableUtils );
+
+	return tableCopy;
+}
+
+// Fills gaps for spanned cell from outside the selection range.
+function addMissingTableCells( tableCopy, startColumn, copyToOriginalColumnMap, writer, tableUtils ) {
+	for ( const row of tableCopy.getChildren() ) {
+		for ( const tableCell of Array.from( row.getChildren() ) ) {
+			const { column } = tableUtils.getCellLocation( tableCell );
+
+			const originalColumn = copyToOriginalColumnMap.get( tableCell );
+			const shiftedColumn = originalColumn - startColumn;
+
+			if ( column !== shiftedColumn ) {
+				for ( let i = 0; i < shiftedColumn - column; i++ ) {
+					const prepCell = writer.createElement( 'tableCell' );
+					writer.insert( prepCell, writer.createPositionBefore( tableCell ) );
+
+					const paragraph = writer.createElement( 'paragraph' );
+
+					writer.insert( paragraph, prepCell, 0 );
+					writer.insertText( '', paragraph, 0 );
+				}
+			}
+		}
+	}
+}
+
+// Trims table to a given dimensions.
+function trimTable( table, width, height, writer, tableUtils ) {
+	for ( const row of table.getChildren() ) {
+		for ( const tableCell of row.getChildren() ) {
+			const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
+			const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
+
+			const { row, column } = tableUtils.getCellLocation( tableCell );
+
+			if ( column + colspan > width ) {
+				const newSpan = width - column;
+
+				if ( newSpan > 1 ) {
+					writer.setAttribute( 'colspan', newSpan, tableCell );
+				} else {
+					writer.removeAttribute( 'colspan', tableCell );
+				}
+			}
+
+			if ( row + rowspan > height ) {
+				const newSpan = height - row;
+
+				if ( newSpan > 1 ) {
+					writer.setAttribute( 'rowspan', newSpan, tableCell );
+				} else {
+					writer.removeAttribute( 'rowspan', tableCell );
+				}
+			}
+		}
+	}
+}
+
+// Sets proper heading attributes to copied table.
+function addHeadingsToTableCopy( tableCopy, sourceTable, startRow, startColumn, writer ) {
+	const headingRows = parseInt( sourceTable.getAttribute( 'headingRows' ) || 0 );
+
+	if ( headingRows > 0 ) {
+		const copiedRows = headingRows - startRow;
+		writer.setAttribute( 'headingRows', copiedRows, tableCopy );
+	}
+
+	const headingColumns = parseInt( sourceTable.getAttribute( 'headingColumns' ) || 0 );
+
+	if ( headingColumns > 0 ) {
+		const copiedColumns = headingColumns - startColumn;
+		writer.setAttribute( 'headingColumns', copiedColumns, tableCopy );
+	}
+}

+ 86 - 0
packages/ckeditor5-table/src/tableselection/mouseeventsobserver.js

@@ -0,0 +1,86 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/tableselection/mouseeventsobserver
+ */
+
+import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver';
+
+/**
+ * The mouse selection events observer.
+ *
+ * It registers listeners for DOM events:
+ *
+ * - `'mousemove'`
+ * - `'mouseup'`
+ * - `'mouseleave'`
+ *
+ * Note that this observer is disabled by default. To enable this observer it needs to be added to
+ * {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method.
+ *
+ * It is registered by {@link module:table/tableselection/mouseselectionhandler~MouseSelectionHandler}.
+ *
+ * @extends module:engine/view/observer/domeventobserver~DomEventObserver
+ */
+export default class MouseEventsObserver extends DomEventObserver {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( view ) {
+		super( view );
+
+		this.domEventType = [ 'mousemove', 'mouseup', 'mouseleave' ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	onDomEvent( domEvent ) {
+		this.fire( domEvent.type, domEvent );
+	}
+}
+
+/**
+ * Fired when the mouse button is released over one of the editables.
+ *
+ * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}.
+ *
+ * Note that this event is not available by default. To make it available
+ * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added
+ * to {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method.
+ *
+ * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver
+ * @event module:engine/view/document~Document#event:mouseup
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
+ */
+
+/**
+ * Fired when the mouse is moved over one of the editables.
+ *
+ * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}.
+ *
+ * Note that this event is not available by default. To make it available
+ * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added
+ * to {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method.
+ *
+ * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver
+ * @event module:engine/view/document~Document#event:mousemove
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
+ */
+
+/**
+ * Fired when the mouse is moved out of one of the editables.
+ *
+ * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}.
+ *
+ * Note that this event is not available by default. To make it available
+ * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added
+ * to {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method.
+ *
+ * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver
+ * @event module:engine/view/document~Document#event:mouseleave
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
+ */

+ 160 - 0
packages/ckeditor5-table/src/tableselection/mouseselectionhandler.js

@@ -0,0 +1,160 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/tableselection/mouseselectionhandler
+ */
+
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+
+import { findAncestor } from '../commands/utils';
+import MouseEventsObserver from './mouseeventsobserver';
+
+/**
+ * A mouse selection handler class for the table selection.
+ *
+ * It registers the {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} to observe view document mouse events
+ * and invoke proper {@link module:table/tableselection~TableSelection} actions.
+ */
+export default class MouseSelectionHandler {
+	/**
+	 * Creates an instance of the `MouseSelectionHandler`.
+	 *
+	 * @param {module:table/tableselection~TableSelection} tableSelection
+	 * @param {module:engine/controller/editingcontroller~EditingController} editing
+	 */
+	constructor( tableSelection, editing ) {
+		/**
+		 * The table selection plugin instance.
+		 *
+		 * @private
+		 * @readonly
+		 * @member {module:table/tableselection~TableSelection}
+		 */
+		this._tableSelection = tableSelection;
+
+		/**
+		 * A flag indicating that the mouse selection is "active". A selection is "active" if it was started and not yet finished.
+		 * A selection can be "active", for instance, if a user moves a mouse over a table while holding a mouse button down.
+		 *
+		 * @readonly
+		 * @member {Boolean}
+		 */
+		this.isSelecting = false;
+
+		/**
+		 * Editing mapper.
+		 *
+		 * @private
+		 * @readonly
+		 * @member {module:engine/conversion/mapper~Mapper}
+		 */
+		this._mapper = editing.mapper;
+
+		const view = editing.view;
+
+		// Currently the MouseObserver only handles `mouseup` events.
+		view.addObserver( MouseEventsObserver );
+
+		this.listenTo( view.document, 'mousedown', ( event, domEventData ) => this._handleMouseDown( domEventData ) );
+		this.listenTo( view.document, 'mousemove', ( event, domEventData ) => this._handleMouseMove( domEventData ) );
+		this.listenTo( view.document, 'mouseup', ( event, domEventData ) => this._handleMouseUp( domEventData ) );
+	}
+
+	/**
+	 * Handles starting a selection when "mousedown" event has table cell as a DOM target.
+	 *
+	 * If there is no table cell in the event target, it will clear the previous selection.
+	 *
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 * @private
+	 */
+	_handleMouseDown( domEventData ) {
+		const tableCell = this._getModelTableCellFromDomEvent( domEventData );
+
+		if ( !tableCell ) {
+			this._tableSelection.clearSelection();
+			this._tableSelection.stopSelection();
+
+			return;
+		}
+
+		this.isSelecting = true;
+		this._tableSelection.startSelectingFrom( tableCell );
+	}
+
+	/**
+	 * Handles updating the table selection when the "mousemove" event has a table cell as a DOM target.
+	 *
+	 * Does nothing if there is no table cell in event target or the selection has not been started yet.
+	 *
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 * @private
+	 */
+	_handleMouseMove( domEventData ) {
+		if ( !isButtonPressed( domEventData ) ) {
+			this._tableSelection.stopSelection();
+
+			return;
+		}
+
+		const tableCell = this._getModelTableCellFromDomEvent( domEventData );
+
+		if ( !tableCell ) {
+			return;
+		}
+
+		this._tableSelection.setSelectingTo( tableCell );
+	}
+
+	/**
+	 * Handles ending (not clearing) the table selection on the "mouseup" event.
+	 *
+	 * Does nothing if the selection has not been started yet.
+	 *
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 * @private
+	 */
+	_handleMouseUp( domEventData ) {
+		if ( !this.isSelecting ) {
+			return;
+		}
+
+		const tableCell = this._getModelTableCellFromDomEvent( domEventData );
+
+		// Selection can be stopped if table cell is undefined.
+		this._tableSelection.stopSelection( tableCell );
+	}
+
+	/**
+	 * Finds a model table cell for a given DOM event.
+	 *
+	 * @private
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 * @returns {module:engine/model/element~Element|undefined} Returns model table cell or undefined event target is not
+	 * a mapped table cell.
+	 */
+	_getModelTableCellFromDomEvent( domEventData ) {
+		const viewTargetElement = domEventData.target;
+		const modelElement = this._mapper.toModelElement( viewTargetElement );
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		if ( modelElement.is( 'tableCell' ) ) {
+			return modelElement;
+		}
+
+		return findAncestor( 'tableCell', modelElement );
+	}
+}
+
+mix( MouseSelectionHandler, ObservableMixin );
+
+function isButtonPressed( domEventData ) {
+	return !!domEventData.domEvent.buttons;
+}

+ 3 - 3
packages/ckeditor5-table/src/ui/colorinputview.js

@@ -47,7 +47,7 @@ export default class ColorInputView extends View {
 		this.set( 'value', '' );
 
 		/**
-		 * The `id` attribute of the input (i.e. to pair with a `<label>` element).
+		 * The `id` attribute of the input (i.e. to pair with the `<label>` element).
 		 *
 		 * @observable
 		 * @member {String} #id
@@ -74,7 +74,7 @@ export default class ColorInputView extends View {
 		this.set( 'hasError', false );
 
 		/**
-		 * The `id` of the element describing this field, e.g. when it has
+		 * The `id` of the element describing this field. When the field has
 		 * some error, it helps screen readers read the error text.
 		 *
 		 * @observable
@@ -83,7 +83,7 @@ export default class ColorInputView extends View {
 		this.set( 'ariaDescribedById' );
 
 		/**
-		 * Cached reference to the options passed to the constructor.
+		 * A cached reference to the options passed to the constructor.
 		 *
 		 * @member {Object}
 		 */

+ 1 - 1
packages/ckeditor5-table/src/ui/formheaderview.js

@@ -51,7 +51,7 @@ export default class FormHeaderView extends View {
 		this.set( 'class', options.class || null );
 
 		/**
-		 * Label of the header.
+		 * The label of the header.
 		 *
 		 * @readonly
 		 * @member {String} #label

+ 2 - 2
packages/ckeditor5-table/src/ui/formrowview.js

@@ -12,7 +12,7 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
 import '../../theme/formrow.css';
 
 /**
- * The class representing a single row in the complex form,
+ * The class representing a single row in a complex form,
  * used by {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView}.
  *
  * **Note**: For now this class is private. When more use cases arrive (beyond ckeditor5-table),
@@ -30,7 +30,7 @@ export default class FormRowView extends View {
 	 * @param {Array.<module:ui/view~View>} options.children
 	 * @param {String} [options.class]
 	 * @param {module:ui/view~View} [options.labelView] When passed, the row gets the `group` and `aria-labelledby`
-	 * DOM attributes and get described by the label.
+	 * DOM attributes and gets described by the label.
 	 */
 	constructor( locale, options = {} ) {
 		super( locale );

+ 13 - 13
packages/ckeditor5-table/src/ui/utils.js

@@ -34,7 +34,7 @@ const isEmpty = val => val === '';
  * with respect to the table in the editor content, if one is selected.
  *
  * @param {module:core/editor/editor~Editor} editor The editor instance.
- * @param {String} target Either "cell" or "table". Determines the the target the balloon will
+ * @param {String} target Either "cell" or "table". Determines the target the balloon will
  * be attached to.
  */
 export function repositionContextualBalloon( editor, target ) {
@@ -154,7 +154,7 @@ export function colorFieldValidator( value ) {
 }
 
 /**
- * Returns `true` when the passed value is an empty string, number without unit or a valid CSS length expression.
+ * Returns `true` when the passed value is an empty string, a number without a unit or a valid CSS length expression.
  * Otherwise, `false` is returned.
  *
  * See {@link module:engine/view/styles/utils~isLength}.
@@ -170,7 +170,7 @@ export function lengthFieldValidator( value ) {
 }
 
 /**
- * Returns `true` when the passed value is an empty string, number without unit or a valid CSS length expression.
+ * Returns `true` when the passed value is an empty string, a number without a unit or a valid CSS length expression.
  * Otherwise, `false` is returned.
  *
  * See {@link module:engine/view/styles/utils~isLength}.
@@ -220,7 +220,7 @@ export function getBorderStyleDefinitions( view ) {
 }
 
 /**
- * A helper that fills a toolbar toolbar with buttons that:
+ * A helper that fills a toolbar with buttons that:
  *
  * * have some labels,
  * * have some icons,
@@ -233,7 +233,7 @@ export function getBorderStyleDefinitions( view ) {
  * @param {module:ui/toolbar/toolbarview~ToolbarView} options.toolbar
  * @param {Object.<String,String>} labels
  * @param {String} propertyName
- * @param {Function} nameToValue Function that maps button name to value. By default names are the same as values.
+ * @param {Function} nameToValue A function that maps a button name to a value. By default names are the same as values.
  */
 export function fillToolbar( { view, icons, toolbar, labels, propertyName, nameToValue } ) {
 	for ( const name in labels ) {
@@ -393,18 +393,18 @@ export const defaultColors = [
 ];
 
 /**
- * Returns a creator for color input with a label.
+ * Returns a creator for a color input with a label.
  *
- * For given options, it returns a function that creates an instance of
+ * For given options, it returns a function that creates an instance of a
  * {@link module:table/ui/colorinputview~ColorInputView color input} logically related to
- * a {@link module:ui/labeledview/labeledview~LabeledView labeled view} in DOM.
+ * a {@link module:ui/labeledview/labeledview~LabeledView labeled view} in the DOM.
  *
  * The helper does the following:
  *
- * * It sets color input's `id` and `ariaDescribedById` attributes.
- * * It binds color input's `isReadOnly` to the labeled view.
- * * It binds color input's `hasError` to the labeled view.
- * * It enables a logic that cleans up the error when user starts typing in the color input.
+ * * It sets the color input `id` and `ariaDescribedById` attributes.
+ * * It binds the color input `isReadOnly` to the labeled view.
+ * * It binds the color input `hasError` to the labeled view.
+ * * It enables a logic that cleans up the error when the user starts typing in the color input.
  *
  * Usage:
  *
@@ -414,7 +414,7 @@ export const defaultColors = [
  *		} );
  *
  *		const labeledInputView = new LabeledView( locale, colorInputCreator );
- *		console.log( labeledInputView.view ); // An color input instance.
+ *		console.log( labeledInputView.view ); // A color input instance.
  *
  * @private
  * @param options Color input options.

+ 66 - 4
packages/ckeditor5-table/tests/_utils/utils.js

@@ -196,7 +196,7 @@ export function defaultSchema( schema, registerParagraph = true ) {
 	schema.register( 'tableCell', {
 		allowIn: 'tableRow',
 		allowAttributes: [ 'colspan', 'rowspan' ],
-		isLimit: true
+		isObject: true
 	} );
 
 	// Allow all $block content inside table cell.
@@ -322,11 +322,59 @@ export function assertTableStyle( editor, tableStyle, figureStyle ) {
 export function assertTableCellStyle( editor, tableCellStyle ) {
 	assertEqualMarkup( editor.getData(),
 		'<figure class="table"><table><tbody><tr>' +
-			`<td${ tableCellStyle ? ` style="${ tableCellStyle }"` : '' }>foo</td>` +
+		`<td${ tableCellStyle ? ` style="${ tableCellStyle }"` : '' }>foo</td>` +
 		'</tr></tbody></table></figure>'
 	);
 }
 
+/**
+ * A helper method for asserting selected table cells.
+ *
+ * To check if a table has expected cells selected pass two dimensional array of truthy and falsy values:
+ *
+ *		assertSelectedCells( model, [
+ *			[ 0, 1 ],
+ *			[ 0, 1 ]
+ *		] );
+ *
+ * The above call will check if table has second column selected (assuming no spans).
+ *
+ * **Note**: This function operates on child indexes - not rows/columns.
+ */
+export function assertSelectedCells( model, tableMap ) {
+	const tableIndex = 0;
+
+	for ( let rowIndex = 0; rowIndex < tableMap.length; rowIndex++ ) {
+		const row = tableMap[ rowIndex ];
+
+		for ( let cellIndex = 0; cellIndex < row.length; cellIndex++ ) {
+			const expectSelected = row[ cellIndex ];
+
+			if ( expectSelected ) {
+				assertNodeIsSelected( model, [ tableIndex, rowIndex, cellIndex ] );
+			} else {
+				assertNodeIsNotSelected( model, [ tableIndex, rowIndex, cellIndex ] );
+			}
+		}
+	}
+}
+
+function assertNodeIsSelected( model, path ) {
+	const modelRoot = model.document.getRoot();
+	const node = modelRoot.getNodeByPath( path );
+	const selectionRanges = Array.from( model.document.selection.getRanges() );
+
+	expect( selectionRanges.some( range => range.containsItem( node ) ), `Expected node [${ path }] to be selected` ).to.be.true;
+}
+
+function assertNodeIsNotSelected( model, path ) {
+	const modelRoot = model.document.getRoot();
+	const node = modelRoot.getNodeByPath( path );
+	const selectionRanges = Array.from( model.document.selection.getRanges() );
+
+	expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true;
+}
+
 // Formats table cell attributes
 //
 // @param {Object} attributes Attributes of a cell.
@@ -356,20 +404,24 @@ function makeRows( tableData, options ) {
 				let contents = isObject ? tableCellData.contents : tableCellData;
 
 				let resultingCellElement = cellElement;
+				let isSelected = false;
 
 				if ( isObject ) {
 					if ( tableCellData.isHeading ) {
 						resultingCellElement = headingElement;
 					}
 
+					isSelected = !!tableCellData.isSelected;
+
 					delete tableCellData.contents;
 					delete tableCellData.isHeading;
+					delete tableCellData.isSelected;
 				}
 
 				const attributes = isObject ? tableCellData : {};
 
 				if ( asWidget ) {
-					attributes.class = WIDGET_TABLE_CELL_CLASS + ( attributes.class ? ` ${ attributes.class }` : '' );
+					attributes.class = getClassToSet( attributes );
 					attributes.contenteditable = 'true';
 				}
 
@@ -381,7 +433,9 @@ function makeRows( tableData, options ) {
 				}
 
 				const formattedAttributes = formatAttributes( attributes );
-				tableRowString += `<${ resultingCellElement }${ formattedAttributes }>${ contents }</${ resultingCellElement }>`;
+				const tableCell = `<${ resultingCellElement }${ formattedAttributes }>${ contents }</${ resultingCellElement }>`;
+
+				tableRowString += isSelected ? `[${ tableCell }]` : tableCell;
 
 				return tableRowString;
 			}, '' );
@@ -389,3 +443,11 @@ function makeRows( tableData, options ) {
 			return `${ previousRowsString }<${ rowElement }>${ tableRowString }</${ rowElement }>`;
 		}, '' );
 }
+
+// Properly handles passed CSS class - editor do sort them.
+function getClassToSet( attributes ) {
+	return ( WIDGET_TABLE_CELL_CLASS + ( attributes.class ? ` ${ attributes.class }` : '' ) )
+		.split( ' ' )
+		.sort()
+		.join( ' ' );
+}

+ 20 - 4
packages/ckeditor5-table/tests/commands/removecolumncommand.js

@@ -72,7 +72,7 @@ describe( 'RemoveColumnCommand', () => {
 
 			assertEqualMarkup( getData( model ), modelTable( [
 				[ '00', '02' ],
-				[ '<paragraph>10[]</paragraph>', '12' ],
+				[ '10', '[]12' ],
 				[ '20', '22' ]
 			] ) );
 		} );
@@ -87,7 +87,7 @@ describe( 'RemoveColumnCommand', () => {
 			command.execute();
 
 			assertEqualMarkup( getData( model ), modelTable( [
-				[ '<paragraph>[]01</paragraph>' ],
+				[ '[]01' ],
 				[ '11' ],
 				[ '21' ]
 			] ) );
@@ -104,7 +104,7 @@ describe( 'RemoveColumnCommand', () => {
 
 			assertEqualMarkup( getData( model ), modelTable( [
 				[ '01' ],
-				[ '<paragraph>[]11</paragraph>' ],
+				[ '[]11' ],
 				[ '21' ]
 			], { headingColumns: 1 } ) );
 		} );
@@ -123,7 +123,7 @@ describe( 'RemoveColumnCommand', () => {
 			assertEqualMarkup( getData( model ), modelTable( [
 				[ { colspan: 3, contents: '00' }, '03' ],
 				[ { colspan: 2, contents: '10' }, '13' ],
-				[ { colspan: 2, contents: '<paragraph>20[]</paragraph>' }, '23' ],
+				[ { colspan: 2, contents: '20' }, '[]23' ],
 				[ '30', '31', '33' ],
 				[ '40', '41', '43' ]
 
@@ -145,5 +145,21 @@ describe( 'RemoveColumnCommand', () => {
 				[ '21', '22', '23' ]
 			] ) );
 		} );
+
+		it( 'should move focus to previous column of removed cell if in last column', () => {
+			setData( model, modelTable( [
+				[ '00', '01', '02' ],
+				[ '10', '11', '12[]' ],
+				[ '20', '21', '22' ]
+			] ) );
+
+			command.execute();
+
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '00', '01' ],
+				[ '10', '[]11' ],
+				[ '20', '21' ]
+			] ) );
+		} );
 	} );
 } );

+ 7 - 7
packages/ckeditor5-table/tests/commands/removerowcommand.js

@@ -65,8 +65,8 @@ describe( 'RemoveRowCommand', () => {
 			command.execute();
 
 			assertEqualMarkup( getData( model ), modelTable( [
-				[ '00', '<paragraph>01[]</paragraph>' ],
-				[ '20', '21' ]
+				[ '00', '01' ],
+				[ '[]20', '21' ]
 			] ) );
 		} );
 
@@ -80,7 +80,7 @@ describe( 'RemoveRowCommand', () => {
 			command.execute();
 
 			assertEqualMarkup( getData( model ), modelTable( [
-				[ '<paragraph>[]10</paragraph>', '11' ],
+				[ '[]10', '11' ],
 				[ '20', '21' ]
 			] ) );
 		} );
@@ -95,8 +95,8 @@ describe( 'RemoveRowCommand', () => {
 			command.execute();
 
 			assertEqualMarkup( getData( model ), modelTable( [
-				[ '00', '<paragraph>01[]</paragraph>' ],
-				[ '20', '21' ]
+				[ '00', '01' ],
+				[ '[]20', '21' ]
 			], { headingRows: 1 } ) );
 		} );
 
@@ -112,8 +112,8 @@ describe( 'RemoveRowCommand', () => {
 
 			assertEqualMarkup( getData( model ), modelTable( [
 				[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
-				[ '13', '<paragraph>14[]</paragraph>' ],
-				[ '30', '31', '32', '33', '34' ]
+				[ '13', '14' ],
+				[ '30', '31', '[]32', '33', '34' ]
 			] ) );
 		} );
 

+ 174 - 0
packages/ckeditor5-table/tests/manual/tableclipboard.html

@@ -0,0 +1,174 @@
+<style>
+	body {
+		font-family: Helvetica, Arial, sans-serif;
+		font-size: 14px;
+	}
+
+	.print-selected {
+		background: #b8e3ff;
+	}
+</style>
+
+<div style="display:flex">
+	<div style="flex:1;">
+
+		<h3>A "content" test editor</h3>
+
+		<div id="editor-content">
+			<p>Jelly-o topping chocolate danish. Powder donut dragée cupcake sesame snaps cotton candy. Cotton candy pudding apple pie liquorice
+				sugar plum cookie. Powder gummi bears macaroon. Gingerbread soufflé liquorice jelly-o marzipan pudding. Toffee sweet candy canes danish
+				macaroon cotton candy fruitcake. Sesame snaps cake cookie chocolate cupcake. Ice cream pie apple pie sweet. Wafer ice cream gingerbread
+				fruitcake donut jelly sweet. Sugar plum chocolate gummi bears. Jelly-o oat cake wafer brownie gingerbread pie dragée. Marshmallow
+				marzipan candy canes. Cotton candy liquorice cake soufflé pie candy.</p>
+
+			<figure class="table" style="width:60%;">
+				<table
+					style="background-color:hsl(0,0%,90%);border-bottom:2px solid hsl(30, 75%, 60%);border-left:2px solid hsl(30, 75%, 60%);border-right:2px solid hsl(30, 75%, 60%);border-top:2px solid hsl(30, 75%, 60%);">
+					<thead>
+						<tr>
+							<th>0</th>
+							<th>1</th>
+							<th>2</th>
+							<th>3</th>
+							<th>4</th>
+						</tr>
+					</thead>
+					<tbody>
+						<tr>
+							<td>a</td>
+							<td>b</td>
+							<td style="border-bottom:4px dashed hsl(0, 75%, 60%);border-left:4px dashed hsl(0, 75%, 60%);border-right:4px dashed hsl(0, 75%, 60%);border-top:4px dashed hsl(0, 75%, 60%);height:30px;text-align:right;vertical-align:top;width:30px;">
+								c
+							</td>
+							<td>d</td>
+							<td><i><strong>e</strong></i></td>
+						</tr>
+						<tr>
+							<td style="background-color:hsl(210,75%,60%);">f</td>
+							<td style="background-color:hsl(210,75%,60%);">g</td>
+							<td style="background-color:hsl(210,75%,60%);">h</td>
+							<td style="background-color:hsl(210,75%,60%);">i</td>
+							<td style="background-color:hsl(210,75%,60%);"><i><strong>j</strong></i></td>
+						</tr>
+						<tr>
+							<td>k</td>
+							<td>l</td>
+							<td>m</td>
+							<td>n</td>
+							<td><i><strong>o</strong></i></td>
+						</tr>
+						<tr>
+							<td>p</td>
+							<td>q</td>
+							<td>r</td>
+							<td>s</td>
+							<td>t</td>
+						</tr>
+					</tbody>
+				</table>
+			</figure>
+		</div>
+
+		<h3>A "geometry" test editor</h3>
+
+		<div id="editor-geometry">
+			<figure class="table">
+				<table>
+					<thead>
+						<tr>
+							<th>a</th>
+							<th>b</th>
+							<th>c</th>
+							<th>d</th>
+							<th>e</th>
+							<th>f</th>
+							<th>g</th>
+							<th>h</th>
+							<th>i</th>
+						</tr>
+					</thead>
+					<tbody>
+						<tr>
+							<td>00</td>
+							<td>01</td>
+							<td rowspan="4">02</td>
+							<td>03</td>
+							<td colspan="2" rowspan="7">04</td>
+							<td>07</td>
+							<td>07</td>
+							<td>08</td>
+						</tr>
+						<tr>
+							<td>10</td>
+							<td>11</td>
+							<td>13</td>
+							<td>17</td>
+							<td>17</td>
+							<td>18</td>
+						</tr>
+						<tr>
+							<td>20</td>
+							<td>21</td>
+							<td>23</td>
+							<td colspan="3">27</td>
+						</tr>
+						<tr>
+							<td>30</td>
+							<td>31</td>
+							<td>33</td>
+							<td>37</td>
+							<td colspan="2">37</td>
+						</tr>
+						<tr>
+							<td colspan="4">40</td>
+							<td>47</td>
+							<td>47</td>
+							<td>48</td>
+						</tr>
+						<tr>
+							<td>50</td>
+							<td>51</td>
+							<td>52</td>
+							<td>53</td>
+							<td rowspan="4">57</td>
+							<td>57</td>
+							<td>58</td>
+						</tr>
+						<tr>
+							<td>60</td>
+							<td colspan="3">61</td>
+							<td>67</td>
+							<td>68</td>
+						</tr>
+						<tr>
+							<td>70</td>
+							<td rowspan="2">71</td>
+							<td>72</td>
+							<td>73</td>
+							<td>74</td>
+							<td>75</td>
+							<td>77</td>
+							<td>78</td>
+						</tr>
+						<tr>
+							<td>80</td>
+							<td>82</td>
+							<td>83</td>
+							<td>84</td>
+							<td>85</td>
+							<td>87</td>
+							<td>88</td>
+						</tr>
+					</tbody>
+				</table>
+			</figure>
+		</div>
+	</div>
+
+	<div style="padding-left: 2em;flex: 1;">
+		<h3>Content editable to test paste</h3>
+		<div contenteditable="true" class="ck-content" style="padding: 1em;border: 1px dotted #333;">
+			paste here
+		</div>
+	</div>
+</div>

+ 44 - 0
packages/ckeditor5-table/tests/manual/tableclipboard.js

@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, document, window, CKEditorInspector */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import Table from '../../src/table';
+import TableToolbar from '../../src/tabletoolbar';
+import TableSelection from '../../src/tableselection';
+import TableClipboard from '../../src/tableclipboard';
+import TableProperties from '../../src/tableproperties';
+import TableCellProperties from '../../src/tablecellproperties';
+
+window.editors = {};
+
+createEditor( '#editor-content', 'content' );
+createEditor( '#editor-geometry', 'geometry' );
+
+function createEditor( target, inspectorName ) {
+	ClassicEditor
+		.create( document.querySelector( target ), {
+			plugins: [ ArticlePluginSet, Table, TableToolbar, TableSelection, TableClipboard, TableProperties, TableCellProperties ],
+			toolbar: [
+				'heading', '|',
+				'insertTable', '|',
+				'bold', 'italic', 'link', '|',
+				'bulletedList', 'numberedList', 'blockQuote', '|',
+				'undo', 'redo'
+			],
+			table: {
+				contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ]
+			}
+		} )
+		.then( editor => {
+			window.editors[ inspectorName ] = editor;
+			CKEditorInspector.attach( inspectorName, editor );
+		} )
+		.catch( err => {
+			console.error( err.stack );
+		} );
+}

+ 21 - 0
packages/ckeditor5-table/tests/manual/tableclipboard.md

@@ -0,0 +1,21 @@
+### Testing
+
+Copying selected table cells:
+
+1. Select a fragment of table cell.
+2. Use copy shortcut <kbd>ctrl</kbd>+<kbd>C</kbd>.
+3. Paste selected content:
+    - somewhere in the document.
+    - in the editable field on the right.
+4. The pasted table should:
+    - be rectangular (no missing nor exceeding table cells)
+    - have proper headings
+5. The editors are exposed as:
+    - `window.editor.content` and "content" editor in CKEditor inspector
+    - `window.editor.geometry` and "geometry" editor in CKEditor inspector
+
+Note that table copy:
+
+- have cut disabled
+- paste in table is not possible
+- pasted table can be fixed by a post-fixer (use content editable to verify what's being copied)

+ 184 - 0
packages/ckeditor5-table/tests/manual/tableselection.html

@@ -0,0 +1,184 @@
+<style>
+	body {
+		font-family: Helvetica, Arial, sans-serif;
+		font-size: 14px;
+	}
+
+	.print-selected {
+		background: #b8e3ff;
+	}
+
+	/* This shouldn't be needed. See https://github.com/ckeditor/ckeditor5/issues/6314 */
+	.external-source td, .external-source th {
+		border: solid 1px hsl(0, 0%, 85%);
+	}
+</style>
+
+<h3>A "content" test editor</h3>
+
+<div id="editor-content">
+	<p>Jelly-o topping chocolate danish. Powder donut dragée cupcake sesame snaps cotton candy. Cotton candy pudding apple pie liquorice
+	sugar plum cookie. Powder gummi bears macaroon. Gingerbread soufflé liquorice jelly-o marzipan pudding. Toffee sweet candy canes danish
+	macaroon cotton candy fruitcake. Sesame snaps cake cookie chocolate cupcake. Ice cream pie apple pie sweet. Wafer ice cream gingerbread
+	fruitcake donut jelly sweet. Sugar plum chocolate gummi bears. Jelly-o oat cake wafer brownie gingerbread pie dragée. Marshmallow
+	marzipan candy canes. Cotton candy liquorice cake soufflé pie candy.</p>
+	<figure class="table">
+		<table>
+			<thead>
+				<tr>
+					<th>0</th>
+					<th>1</th>
+					<th>2</th>
+					<th>3</th>
+					<th>4</th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					<td>a</td>
+					<td>b</td>
+					<td><strong>c</strong></td>
+					<td><a href="https://example.com">d</a></td>
+					<td>e</td>
+				</tr>
+				<tr>
+					<td><i>f</i></td>
+					<td><i>g</i></td>
+					<td><i><strong>h</strong></i></td>
+					<td><i>i</i></td>
+					<td><i>j</i></td>
+				</tr>
+				<tr>
+					<td>k</td>
+					<td>l</td>
+					<td><strong>m</strong></td>
+					<td>n</td>
+					<td>o</td>
+				</tr>
+				<tr>
+					<td>p</td>
+					<td>
+						<ul>
+							<li>q</li>
+							<li>q</li>
+						</ul>
+					</td>
+					<td>
+						<ol>
+							<li>r</li>
+							<li>r</li>
+						</ol>
+					</td>
+					<td>s</td>
+					<td>
+						<blockquote><p>t</p></blockquote>
+						<p>t</p></td>
+				</tr>
+			</tbody>
+		</table>
+	</figure>
+	<p>Halvah oat cake lemon drops. Cake tart caramels. Topping soufflé cheesecake. Chocolate bar sugar plum pastry sesame snaps bear claw
+		gummies cotton candy topping. Tootsie roll topping cake chocolate bar marshmallow lemon drops. Cheesecake cookie croissant chupa
+		chups. Biscuit jujubes lollipop fruitcake sesame snaps halvah. Marzipan croissant dessert chocolate cheesecake halvah jelly beans.
+		Caramels pastry bear claw oat cake sugar plum muffin sweet roll cake. Chocolate bar sweet roll sweet roll bonbon apple pie pastry
+		lemon drops icing. Apple pie wafer marzipan. Cake donut macaroon pudding. Gummi bears wafer toffee chocolate bar bear claw.
+		Fruitcake jelly chocolate bar.</p>
+</div>
+
+<h3>A "geometry" test editor</h3>
+
+<div id="editor-geometry">
+	<figure class="table">
+		<table>
+			<thead>
+				<tr>
+					<th>a</th>
+					<th>b</th>
+					<th>c</th>
+					<th>d</th>
+					<th>e</th>
+					<th>f</th>
+					<th>g</th>
+					<th>h</th>
+					<th>i</th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					<td>00</td>
+					<td>01</td>
+					<td rowspan="4">02</td>
+					<td>03</td>
+					<td colspan="2" rowspan="7">04</td>
+					<td>07</td>
+					<td>07</td>
+					<td>08</td>
+				</tr>
+				<tr>
+					<td>10</td>
+					<td>11</td>
+					<td>13</td>
+					<td>17</td>
+					<td>17</td>
+					<td>18</td>
+				</tr>
+				<tr>
+					<td>20</td>
+					<td>21</td>
+					<td>23</td>
+					<td colspan="3">27</td>
+				</tr>
+				<tr>
+					<td>30</td>
+					<td>31</td>
+					<td>33</td>
+					<td>37</td>
+					<td colspan="2">37</td>
+				</tr>
+				<tr>
+					<td colspan="4">40</td>
+					<td>47</td>
+					<td>47</td>
+					<td>48</td>
+				</tr>
+				<tr>
+					<td>50</td>
+					<td>51</td>
+					<td>52</td>
+					<td>53</td>
+					<td rowspan="4">57</td>
+					<td>57</td>
+					<td>58</td>
+				</tr>
+				<tr>
+					<td>60</td>
+					<td colspan="3">61</td>
+					<td>67</td>
+					<td>68</td>
+				</tr>
+				<tr>
+					<td>70</td>
+					<td rowspan="2">71</td>
+					<td>72</td>
+					<td>73</td>
+					<td>74</td>
+					<td>75</td>
+					<td>77</td>
+					<td>78</td>
+				</tr>
+				<tr>
+					<td>80</td>
+					<td>82</td>
+					<td>83</td>
+					<td>84</td>
+					<td>85</td>
+					<td>87</td>
+					<td>88</td>
+				</tr>
+			</tbody>
+		</table>
+	</figure>
+</div>
+
+<h2>Model contents:</h2>
+<pre><code id="model"></code></pre>

+ 78 - 0
packages/ckeditor5-table/tests/manual/tableselection.js

@@ -0,0 +1,78 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, window, document, global, CKEditorInspector */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import Table from '../../src/table';
+import TableToolbar from '../../src/tabletoolbar';
+import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import TableSelection from '../../src/tableselection';
+import TableClipboard from '../../src/tableclipboard';
+import TableProperties from '../../src/tableproperties';
+import TableCellProperties from '../../src/tablecellproperties';
+
+window.editors = {};
+
+createEditor( '#editor-content', 'content' );
+createEditor( '#editor-geometry', 'geometry' );
+
+function createEditor( target, inspectorName ) {
+	ClassicEditor
+		.create( document.querySelector( target ), {
+			plugins: [ ArticlePluginSet, Table, TableToolbar, TableSelection, TableClipboard, TableProperties, TableCellProperties ],
+			toolbar: [
+				'heading', '|',
+				'insertTable', '|',
+				'bold', 'italic', 'link', '|',
+				'bulletedList', 'numberedList', 'blockQuote', '|',
+				'undo', 'redo'
+			],
+			table: {
+				contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ]
+			}
+		} )
+		.then( editor => {
+			window.editors[ inspectorName ] = editor;
+			CKEditorInspector.attach( inspectorName, editor );
+
+			editor.model.document.on( 'change', () => {
+				printModelContents( editor );
+			} );
+		} )
+		.catch( err => {
+			console.error( err.stack );
+		} );
+}
+
+const modelDiv = global.document.querySelector( '#model' );
+
+function printModelContents( editor ) {
+	modelDiv.innerHTML = formatTable( getData( editor.model ) )
+		.replace( /</g, '&lt;' )
+		.replace( />/g, '&gt;' )
+		.replace( /\n/g, '<br>' )
+		.replace( /\[/g, '<span class="print-selected">[' )
+		.replace( /]/g, ']</span>' );
+}
+
+function formatTable( tableString ) {
+	return tableString
+		.replace( /<table>/g, '\n<table' )
+		.replace( /<table /g, '\n<table ' )
+		.replace( /<tableRow>/g, '\n<tableRow>\n    ' )
+		.replace( /<thead>/g, '\n<thead>\n    ' )
+		.replace( /<tbody>/g, '\n<tbody>\n    ' )
+		.replace( /<tr>/g, '\n<tr>\n    ' )
+		.replace( /<\/tableRow>/g, '\n</tableRow>' )
+		.replace( /<\/thead>/g, '\n</thead>' )
+		.replace( /<\/tbody>/g, '\n</tbody>' )
+		.replace( /<\/tr>/g, '\n</tr>' )
+		.replace( /<\/table>/g, '\n</table>' )
+		.replace( /tableCell/g, 'cell' )
+		.replace( /tableRow/g, 'row' )
+		.replace( /paragraph/g, 'p' );
+}

+ 9 - 0
packages/ckeditor5-table/tests/manual/tableselection.md

@@ -0,0 +1,9 @@
+### Testing
+
+Selecting table cells:
+
+1. It should be possible to select multiple table cells.
+2. Observe selection inn the below model representation - for a block selection the table cells should be selected.
+3. The editors are exposed as:
+    - `window.editor.content` and "content" editor in CKEditor inspector
+    - `window.editor.geometry` and "geometry" editor in CKEditor inspector

+ 335 - 0
packages/ckeditor5-table/tests/tableclipboard.js

@@ -0,0 +1,335 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import TableEditing from '../src/tableediting';
+import { modelTable, viewTable } from './_utils/utils';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment';
+import { stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import TableClipboard from '../src/tableclipboard';
+
+describe( 'table clipboard', () => {
+	let editor, model, modelRoot, tableSelection, viewDocument;
+
+	beforeEach( async () => {
+		editor = await VirtualTestEditor.create( {
+			plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard ]
+		} );
+
+		model = editor.model;
+		modelRoot = model.document.getRoot();
+		viewDocument = editor.editing.view.document;
+		tableSelection = editor.plugins.get( 'TableSelection' );
+
+		setModelData( model, modelTable( [
+			[ '00[]', '01', '02' ],
+			[ '10', '11', '12' ],
+			[ '20', '21', '22' ]
+		] ) );
+	} );
+
+	afterEach( async () => {
+		await editor.destroy();
+	} );
+
+	describe( 'Clipboard integration', () => {
+		describe( 'copy', () => {
+			it( 'should to nothing for normal selection in table', () => {
+				const dataTransferMock = createDataTransfer();
+				const spy = sinon.spy();
+
+				viewDocument.on( 'clipboardOutput', spy );
+
+				viewDocument.fire( 'copy', {
+					dataTransfer: dataTransferMock,
+					preventDefault: sinon.spy()
+				} );
+
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'should copy selected table cells as standalone table', done => {
+				const dataTransferMock = createDataTransfer();
+				const preventDefaultSpy = sinon.spy();
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) );
+
+				viewDocument.on( 'clipboardOutput', ( evt, data ) => {
+					expect( preventDefaultSpy.calledOnce ).to.be.true;
+					expect( data.method ).to.equal( 'copy' );
+
+					expect( data.dataTransfer ).to.equal( dataTransferMock );
+
+					expect( data.content ).is.instanceOf( ViewDocumentFragment );
+					expect( stringifyView( data.content ) ).to.equal( viewTable( [
+						[ '01', '02' ],
+						[ '11', '12' ]
+					] ) );
+
+					done();
+				} );
+
+				viewDocument.fire( 'copy', {
+					dataTransfer: dataTransferMock,
+					preventDefault: preventDefaultSpy
+				} );
+			} );
+
+			it( 'should trim selected table to a selection rectangle (inner cell with colspan, no colspan after trim)', done => {
+				setModelData( model, modelTable( [
+					[ '00[]', '01', '02' ],
+					[ '10', { contents: '11', colspan: 2 } ],
+					[ '20', '21', '22' ]
+				] ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ]
+				] ), done );
+			} );
+
+			it( 'should trim selected table to a selection rectangle (inner cell with colspan, has colspan after trim)', done => {
+				setModelData( model, modelTable( [
+					[ '00[]', '01', '02' ],
+					[ { contents: '10', colspan: 3 } ],
+					[ '20', '21', '22' ]
+				] ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '00', '01' ],
+					[ { contents: '10', colspan: 2 } ],
+					[ '20', '21' ]
+				] ), done );
+			} );
+
+			it( 'should trim selected table to a selection rectangle (inner cell with rowspan, no colspan after trim)', done => {
+				setModelData( model, modelTable( [
+					[ '00[]', '01', '02' ],
+					[ '10', { contents: '11', rowspan: 2 }, '12' ],
+					[ '20', '21', '22' ]
+				] ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '00', '01', '02' ],
+					[ '10', '11', '12' ]
+				] ), done );
+			} );
+
+			it( 'should trim selected table to a selection rectangle (inner cell with rowspan, has rowspan after trim)', done => {
+				setModelData( model, modelTable( [
+					[ '00[]', { contents: '01', rowspan: 3 }, '02' ],
+					[ '10', '12' ],
+					[ '20', '22' ]
+				] ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '00', { contents: '01', rowspan: 2 }, '02' ],
+					[ '10', '12' ]
+				] ), done );
+			} );
+
+			it( 'should prepend spanned columns with empty cells (outside cell with colspan)', done => {
+				setModelData( model, modelTable( [
+					[ '00[]', '01', '02' ],
+					[ { contents: '10', colspan: 2 }, '12' ],
+					[ '20', '21', '22' ]
+				] ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '01', '02' ],
+					[ '', '12' ],
+					[ '21', '22' ]
+				] ), done );
+			} );
+
+			it( 'should prepend spanned columns with empty cells (outside cell with rowspan)', done => {
+				setModelData( model, modelTable( [
+					[ '00[]', { contents: '01', rowspan: 2 }, '02' ],
+					[ '10', '12' ],
+					[ '20', '21', '22' ]
+				] ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '10', '', '12' ],
+					[ '20', '21', '22' ]
+				] ), done );
+			} );
+
+			it( 'should fix selected table to a selection rectangle (hardcore case)', done => {
+				// This test check how previous simple rules run together (mixed prepending and trimming).
+				// In the example below a selection is set from cell "32" to "88"
+				//
+				//                    Input table:                                         Copied table:
+				//
+				//   +----+----+----+----+----+----+----+----+----+
+				//   | 00 | 01 | 02 | 03 | 04      | 06 | 07 | 08 |
+				//   +----+----+    +----+         +----+----+----+
+				//   | 10 | 11 |    | 13 |         | 16 | 17 | 18 |
+				//   +----+----+    +----+         +----+----+----+             +----+----+----+---------+----+----+
+				//   | 20 | 21 |    | 23 |         | 26           |             | 21 |    | 23 |    |    | 26 |    |
+				//   +----+----+    +----+         +----+----+----+             +----+----+----+----+----+----+----+
+				//   | 30 | 31 |    | 33 |         | 36 | 37      |             | 31 |    | 33 |    |    | 36 | 37 |
+				//   +----+----+----+----+         +----+----+----+             +----+----+----+----+----+----+----+
+				//   | 40                |         | 46 | 47 | 48 |             |    |    |    |    |    | 46 | 47 |
+				//   +----+----+----+----+         +----+----+----+     ==>     +----+----+----+----+----+----+----+
+				//   | 50 | 51 | 52 | 53 |         | 56 | 57 | 58 |             | 51 | 52 | 53 |    |    | 56 | 57 |
+				//   +----+----+----+----+----+----+    +----+----+             +----+----+----+----+----+----+----+
+				//   | 60 | 61           | 64 | 65 |    | 67 | 68 |             | 61 |    |    | 64 | 65 |    | 67 |
+				//   +----+----+----+----+----+----+    +----+----+             +----+----+----+----+----+----+----+
+				//   | 70 | 71 | 72 | 73 | 74 | 75 |    | 77 | 78 |             | 71 | 72 | 73 | 74 | 75 |    | 77 |
+				//   +----+    +----+----+----+----+    +----+----+             +----+----+----+----+----+----+----+
+				//   | 80 |    | 82 | 83 | 84 | 85 |    | 87 | 88 |
+				//   +----+----+----+----+----+----+----+----+----+
+				//
+				setModelData( model, modelTable( [
+					[ '00', '01', { contents: '02', rowspan: 4 }, '03', { contents: '04', colspan: 2, rowspan: 7 }, '07', '07', '08' ],
+					[ '10', '11', '13', '17', '17', '18' ],
+					[ '20', '21', '23', { contents: '27', colspan: 3 } ],
+					[ '30', '31', '33', '37', { contents: '37', colspan: 2 } ],
+					[ { contents: '40', colspan: 4 }, '47', '47', '48' ],
+					[ '50', '51', '52', '53', { contents: '57', rowspan: 4 }, '57', '58' ],
+					[ '60', { contents: '61', colspan: 3 }, '67', '68' ],
+					[ '70', { contents: '71', rowspan: 2 }, '72', '73', '74', '75', '77', '78' ],
+					[ '80', '82', '83', '84', '85', '87', '88' ]
+				] ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 7, 6 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '21', '', '23', '', '', { contents: '27', colspan: 2 } ],
+					[ '31', '', '33', '', '', '37', '37' ],
+					[ '', '', '', '', '', '47', '47' ],
+					[ '51', '52', '53', '', '', { contents: '57', rowspan: 3 }, '57' ],
+					[ { contents: '61', colspan: 3 }, '', '', '', '67' ],
+					[ '71', '72', '73', '74', '75', '77' ]
+				] ), done );
+			} );
+
+			it( 'should update table heading attributes (selection with headings)', done => {
+				setModelData( model, modelTable( [
+					[ '00', '01', '02', '03', '04' ],
+					[ '10', '11', '12', '13', '14' ],
+					[ '20', '21', '22', '23', '24' ],
+					[ '30', '31', '32', '33', '34' ],
+					[ '40', '41', '42', '43', '44' ]
+				], { headingRows: 3, headingColumns: 2 } ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 3, 3 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '11', '12', '13' ],
+					[ '21', '22', '23' ],
+					[ { contents: '31', isHeading: true }, '32', '33' ] // TODO: bug in viewTable
+				], { headingRows: 2, headingColumns: 1 } ), done );
+			} );
+
+			it( 'should update table heading attributes (selection without headings)', done => {
+				setModelData( model, modelTable( [
+					[ '00', '01', '02', '03', '04' ],
+					[ '10', '11', '12', '13', '14' ],
+					[ '20', '21', '22', '23', '24' ],
+					[ '30', '31', '32', '33', '34' ],
+					[ '40', '41', '42', '43', '44' ]
+				], { headingRows: 3, headingColumns: 2 } ) );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 3, 2 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 4, 4 ] ) );
+
+				assertClipboardCopy( viewTable( [
+					[ '32', '33', '34' ],
+					[ '42', '43', '44' ]
+				] ), done );
+			} );
+		} );
+
+		describe( 'cut', () => {
+			it( 'is disabled for multi-range selection over a table', () => {
+				const dataTransferMock = createDataTransfer();
+				const preventDefaultSpy = sinon.spy();
+				const spy = sinon.spy();
+
+				viewDocument.on( 'clipboardOutput', spy );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) );
+
+				viewDocument.fire( 'cut', {
+					dataTransfer: dataTransferMock,
+					preventDefault: preventDefaultSpy
+				} );
+
+				sinon.assert.notCalled( spy );
+				sinon.assert.calledOnce( preventDefaultSpy );
+			} );
+
+			it( 'is not disabled normal selection over a table', () => {
+				const dataTransferMock = createDataTransfer();
+				const spy = sinon.spy();
+
+				viewDocument.on( 'clipboardOutput', spy );
+
+				viewDocument.fire( 'cut', {
+					dataTransfer: dataTransferMock,
+					preventDefault: sinon.spy()
+				} );
+
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+	} );
+
+	function assertClipboardCopy( expectedViewTable, callback ) {
+		viewDocument.on( 'clipboardOutput', ( evt, data ) => {
+			expect( stringifyView( data.content ) ).to.equal( expectedViewTable );
+
+			callback();
+		} );
+
+		viewDocument.fire( 'copy', {
+			dataTransfer: createDataTransfer(),
+			preventDefault: sinon.spy()
+		} );
+	}
+
+	function createDataTransfer() {
+		const store = new Map();
+
+		return {
+			setData( type, data ) {
+				store.set( type, data );
+			},
+
+			getData( type ) {
+				return store.get( type );
+			}
+		};
+	}
+} );

+ 1 - 3
packages/ckeditor5-table/tests/tableediting.js

@@ -259,9 +259,7 @@ describe( 'TableEditing', () => {
 
 		describe( 'on TAB', () => {
 			it( 'should do nothing if selection is not in a table', () => {
-				setModelData( model, '<paragraph>[]</paragraph>' + modelTable( [
-					[ '11', '12' ]
-				] ) );
+				setModelData( model, '<paragraph>[]</paragraph>' + modelTable( [ [ '11', '12' ] ] ) );
 
 				editor.editing.view.document.fire( 'keydown', domEvtDataStub );
 

+ 339 - 0
packages/ckeditor5-table/tests/tableselection.js

@@ -0,0 +1,339 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import TableEditing from '../src/tableediting';
+import TableSelection from '../src/tableselection';
+import { assertSelectedCells, modelTable, viewTable } from './_utils/utils';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import DocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
+
+describe( 'table selection', () => {
+	let editor, model, tableSelection, modelRoot;
+
+	beforeEach( async () => {
+		editor = await VirtualTestEditor.create( {
+			plugins: [ TableEditing, TableSelection, Paragraph ]
+		} );
+
+		model = editor.model;
+		modelRoot = model.document.getRoot();
+		tableSelection = editor.plugins.get( TableSelection );
+
+		setModelData( model, modelTable( [
+			[ '11[]', '12', '13' ],
+			[ '21', '22', '23' ],
+			[ '31', '32', '33' ]
+		] ) );
+	} );
+
+	afterEach( async () => {
+		await editor.destroy();
+	} );
+
+	describe( 'TableSelection', () => {
+		describe( 'startSelectingFrom()', () => {
+			it( 'should not change model selection', () => {
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change', spy );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+
+				sinon.assert.notCalled( spy );
+			} );
+		} );
+
+		describe( 'setSelectingTo()', () => {
+			it( 'should set model selection on passed cell if startSelectingFrom() was not used', () => {
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change', spy );
+
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				sinon.assert.calledOnce( spy );
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0 ],
+					[ 0, 0, 0 ],
+					[ 0, 0, 0 ]
+				] );
+			} );
+
+			it( 'should change model selection if valid selection will be set', () => {
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change', spy );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'should not change model selection if passed table cell is from other table then start cell', () => {
+				setModelData( model,
+					modelTable( [
+						[ '11[]', '12', '13' ],
+						[ '21', '22', '23' ],
+						[ '31', '32', '33' ]
+					] ) +
+					modelTable( [
+						[ 'a', 'b' ],
+						[ 'c', 'd' ]
+					] )
+				);
+
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change', spy );
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 1, 0, 1 ] ) );
+
+				sinon.assert.notCalled( spy );
+			} );
+
+			it( 'should select two table cells', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0 ],
+					[ 0, 0, 0 ],
+					[ 0, 0, 0 ]
+				] );
+			} );
+
+			it( 'should select four table cells for diagonal selection', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0 ],
+					[ 1, 1, 0 ],
+					[ 0, 0, 0 ]
+				] );
+			} );
+
+			it( 'should select row table cells', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 2 ] ) );
+
+				assertSelectedCells( model, [
+					[ 1, 1, 1 ],
+					[ 0, 0, 0 ],
+					[ 0, 0, 0 ]
+				] );
+			} );
+
+			it( 'should select column table cells', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
+
+				assertSelectedCells( model, [
+					[ 0, 1, 0 ],
+					[ 0, 1, 0 ],
+					[ 0, 1, 0 ]
+				] );
+			} );
+
+			it( 'should create proper selection on consecutive changes', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
+
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
+
+				assertSelectedCells( model, [
+					[ 0, 0, 0 ],
+					[ 0, 1, 0 ],
+					[ 0, 1, 0 ]
+				] );
+
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				assertSelectedCells( model, [
+					[ 0, 1, 0 ],
+					[ 0, 1, 0 ],
+					[ 0, 0, 0 ]
+				] );
+
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) );
+
+				assertSelectedCells( model, [
+					[ 0, 0, 0 ],
+					[ 0, 1, 1 ],
+					[ 0, 1, 1 ]
+				] );
+			} );
+		} );
+
+		describe( 'stopSelection()', () => {
+			it( 'should not clear currently selected cells if not cell was passed', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				tableSelection.stopSelection();
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0 ],
+					[ 0, 0, 0 ],
+					[ 0, 0, 0 ]
+				] );
+			} );
+
+			it( 'should change model selection if cell was passed', () => {
+				const spy = sinon.spy();
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+
+				model.document.selection.on( 'change', spy );
+				tableSelection.stopSelection( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'should extend selection to passed table cell', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+
+				tableSelection.stopSelection( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0 ],
+					[ 0, 0, 0 ],
+					[ 0, 0, 0 ]
+				] );
+			} );
+		} );
+
+		describe( 'clearSelection()', () => {
+			it( 'should not change model selection', () => {
+				const spy = sinon.spy();
+
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				model.document.selection.on( 'change', spy );
+
+				tableSelection.clearSelection();
+
+				sinon.assert.notCalled( spy );
+			} );
+
+			it( 'should not reset model selections', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				tableSelection.clearSelection();
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0 ],
+					[ 0, 0, 0 ],
+					[ 0, 0, 0 ]
+				] );
+			} );
+		} );
+
+		describe( '* getSelectedTableCells()', () => {
+			it( 'should return nothing if selection is not started', () => {
+				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [] );
+			} );
+
+			it( 'should return two table cells', () => {
+				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+				const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
+
+				tableSelection.startSelectingFrom( firstCell );
+				tableSelection.setSelectingTo( lastCell );
+
+				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
+					firstCell, lastCell
+				] );
+			} );
+
+			it( 'should return four table cells for diagonal selection', () => {
+				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+				const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
+
+				tableSelection.startSelectingFrom( firstCell );
+				tableSelection.setSelectingTo( lastCell );
+
+				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
+					firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), modelRoot.getNodeByPath( [ 0, 1, 0 ] ), lastCell
+				] );
+			} );
+
+			it( 'should return row table cells', () => {
+				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+				const lastCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] );
+
+				tableSelection.startSelectingFrom( firstCell );
+				tableSelection.setSelectingTo( lastCell );
+
+				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
+					firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), lastCell
+				] );
+			} );
+
+			it( 'should return column table cells', () => {
+				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
+				const lastCell = modelRoot.getNodeByPath( [ 0, 2, 1 ] );
+
+				tableSelection.startSelectingFrom( firstCell );
+				tableSelection.setSelectingTo( lastCell );
+
+				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
+					firstCell, modelRoot.getNodeByPath( [ 0, 1, 1 ] ), lastCell
+				] );
+			} );
+		} );
+
+		describe( 'getSelectionAsFragment()', () => {
+			it( 'should return undefined if no table cells are selected', () => {
+				expect( tableSelection.getSelectionAsFragment() ).to.be.undefined;
+			} );
+
+			it( 'should return document fragment for selected table cells', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
+
+				expect( tableSelection.getSelectionAsFragment() ).to.be.instanceOf( DocumentFragment );
+			} );
+		} );
+
+		describe( 'behavior', () => {
+			it( 'should clear selection on external changes', () => {
+				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+				editor.model.change( writer => {
+					writer.setSelection( modelRoot.getNodeByPath( [ 0, 0, 0, 0 ] ), 0 );
+				} );
+
+				assertSelectedCells( model, [
+					[ 0, 0, 0 ],
+					[ 0, 0, 0 ],
+					[ 0, 0, 0 ]
+				] );
+
+				expect( editor.editing.view.document.selection.isFake ).to.be.false;
+				assertEqualMarkup( getViewData( editor.editing.view ), viewTable( [
+					[ '{}11', '12', '13' ],
+					[ '21', '22', '23' ],
+					[ '31', '32', '33' ]
+				], { asWidget: true } ) );
+			} );
+		} );
+	} );
+} );

+ 50 - 0
packages/ckeditor5-table/tests/tableselection/mouseeventsobserver.js

@@ -0,0 +1,50 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import View from '@ckeditor/ckeditor5-engine/src/view/view';
+import MouseEventsObserver from '../../src/tableselection/mouseeventsobserver';
+
+describe( 'table selection', () => {
+	describe( 'MouseEventsObserver', () => {
+		let view, viewDocument, observer;
+
+		beforeEach( () => {
+			view = new View();
+			viewDocument = view.document;
+			observer = view.addObserver( MouseEventsObserver );
+		} );
+
+		afterEach( () => {
+			view.destroy();
+		} );
+
+		it( 'should define domEventTypes', () => {
+			expect( observer.domEventType ).to.deep.equal( [
+				'mousemove',
+				'mouseup',
+				'mouseleave'
+			] );
+		} );
+
+		describe( 'onDomEvent', () => {
+			for ( const eventName of [ 'mousemove', 'mouseup', 'mouseleave' ] ) {
+				it( `should fire ${ eventName } with the right event data`, () => {
+					const spy = sinon.spy();
+
+					viewDocument.on( eventName, spy );
+
+					observer.onDomEvent( { type: eventName, target: document.body } );
+
+					expect( spy.calledOnce ).to.be.true;
+
+					const data = spy.args[ 0 ][ 1 ];
+					expect( data.domTarget ).to.equal( document.body );
+				} );
+			}
+		} );
+	} );
+} );

+ 485 - 0
packages/ckeditor5-table/tests/tableselection/mouseselectionhandler.js

@@ -0,0 +1,485 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+
+import TableEditing from '../../src/tableediting';
+import TableSelection from '../../src/tableselection';
+import { assertSelectedCells, modelTable, viewTable } from '../_utils/utils';
+
+describe( 'table selection', () => {
+	let editor, model, view, viewDoc;
+
+	beforeEach( async () => {
+		editor = await VirtualTestEditor.create( {
+			plugins: [ TableEditing, TableSelection, Paragraph ]
+		} );
+
+		model = editor.model;
+		view = editor.editing.view;
+		viewDoc = view.document;
+
+		setModelData( model, modelTable( [
+			[ '11[]', '12', '13' ],
+			[ '21', '22', '23' ],
+			[ '31', '32', '33' ]
+		] ) );
+	} );
+
+	afterEach( async () => {
+		await editor.destroy();
+	} );
+
+	describe( 'MouseSelectionHandler', () => {
+		it( 'should not start table selection when mouse move is inside one table cell', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			pressMouseButtonOver( getTableCell( '00' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			movePressedMouseOver( getTableCell( '00' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+		} );
+
+		it( 'should start table selection when mouse move expands over two cells', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			pressMouseButtonOver( getTableCell( '00' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			movePressedMouseOver( getTableCell( '01' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ]
+			] );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[
+					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
+				],
+				[ '10', '11' ]
+			], { asWidget: true } ) );
+		} );
+
+		it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			pressMouseButtonOver( getTableCell( '00' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			movePressedMouseOver( getTableCell( '11' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 1, 1 ]
+			] );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[
+					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
+				],
+				[
+					{ contents: '10', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '11', class: 'ck-editor__editable_selected', isSelected: true }
+				]
+			], { asWidget: true } ) );
+		} );
+
+		it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => {
+			setModelData( model, modelTable( [
+				[ '00', '01' ],
+				[ '10', '[]11' ]
+			] ) );
+
+			pressMouseButtonOver( getTableCell( '11' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '00', '01' ],
+				[ '10', '[]11' ]
+			] ) );
+
+			movePressedMouseOver( getTableCell( '00' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 1, 1 ]
+			] );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[
+					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
+				],
+				[
+					{ contents: '10', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '11', class: 'ck-editor__editable_selected', isSelected: true }
+				]
+			], { asWidget: true } ) );
+		} );
+
+		it( 'should update view selection after changing selection rect', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01', '02' ],
+				[ '10', '11', '12' ],
+				[ '20', '21', '22' ]
+			] ) );
+
+			pressMouseButtonOver( getTableCell( '00' ) );
+			movePressedMouseOver( getTableCell( '22' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1, 1 ],
+				[ 1, 1, 1 ],
+				[ 1, 1, 1 ]
+			] );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[
+					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '02', class: 'ck-editor__editable_selected', isSelected: true }
+				],
+				[
+					{ contents: '10', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '11', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '12', class: 'ck-editor__editable_selected', isSelected: true }
+				],
+				[
+					{ contents: '20', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '21', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '22', class: 'ck-editor__editable_selected', isSelected: true }
+				]
+			], { asWidget: true } ) );
+
+			movePressedMouseOver( getTableCell( '11' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1, 0 ],
+				[ 1, 1, 0 ],
+				[ 0, 0, 0 ]
+			] );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[
+					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true },
+					'02'
+				],
+				[
+					{ contents: '10', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '11', class: 'ck-editor__editable_selected', isSelected: true },
+					'12'
+				],
+				[
+					'20',
+					'21',
+					'22'
+				]
+			], { asWidget: true } ) );
+		} );
+
+		it( 'should stop selecting after "mouseup" event', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			pressMouseButtonOver( getTableCell( '00' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			movePressedMouseOver( getTableCell( '01' ) );
+			releaseMouseButtonOver( getTableCell( '01' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ]
+			] );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[
+					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
+				],
+				[ '10', '11' ]
+			], { asWidget: true } ) );
+
+			moveReleasedMouseOver( getTableCell( '11' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should do nothing on "mouseup" event', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			releaseMouseButtonOver( getTableCell( '01' ) );
+
+			assertSelectedCells( model, [
+				[ 0, 0 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should stop selection mode on "mouseleve" event if next "mousemove" has no button pressed', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			pressMouseButtonOver( getTableCell( '00' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			movePressedMouseOver( getTableCell( '01' ) );
+			makeMouseLeave();
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ]
+			] );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[
+					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
+				],
+				[ '10', '11' ]
+			], { asWidget: true } ) );
+
+			moveReleasedMouseOver( getTableCell( '11' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should continue selection mode on "mouseleve" and "mousemove" if mouse button is pressed', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			pressMouseButtonOver( getTableCell( '00' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			movePressedMouseOver( getTableCell( '01' ) );
+			makeMouseLeave();
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ]
+			] );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[
+					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
+					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
+				],
+				[ '10', '11' ]
+			], { asWidget: true } ) );
+
+			movePressedMouseOver( getTableCell( '11' ) );
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 1, 1 ]
+			] );
+		} );
+
+		it( 'should do nothing on "mouseleve" event', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			makeMouseLeave();
+
+			assertSelectedCells( model, [
+				[ 0, 0 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should do nothing on "mousedown" event over ui element (click on selection handle)', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			const uiElement = viewDoc.getRoot()
+				.getChild( 0 )
+				.getChild( 0 ); // selection handler;
+
+			fireEvent( view, 'mousedown', addTarget( uiElement ), mouseButtonPressed );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+		} );
+
+		it( 'should do nothing on "mousemove" event over ui element (click on selection handle)', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			const uiElement = viewDoc.getRoot()
+				.getChild( 0 )
+				.getChild( 0 ); // selection handler;
+
+			fireEvent( view, 'mousemove', addTarget( uiElement ), mouseButtonPressed );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) );
+		} );
+
+		it( 'should clear view table selection after mouse click outside table', () => {
+			setModelData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) + '<paragraph>foo</paragraph>' );
+
+			pressMouseButtonOver( getTableCell( '00' ) );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ]
+			] ) + '<paragraph>foo</paragraph>' );
+
+			movePressedMouseOver( getTableCell( '01' ) );
+
+			const paragraph = viewDoc.getRoot().getChild( 1 );
+
+			fireEvent( view, 'mousemove', addTarget( paragraph ) );
+			fireEvent( view, 'mousedown', addTarget( paragraph ) );
+			fireEvent( view, 'mouseup', addTarget( paragraph ) );
+
+			// The click in the DOM would trigger selection change and it will set the selection:
+			model.change( writer => {
+				writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) );
+			} );
+
+			assertEqualMarkup( getViewData( view ), viewTable( [
+				[ '00', '01' ],
+				[ '10', '11' ]
+			], { asWidget: true } ) + '<p>{}foo</p>' );
+		} );
+	} );
+
+	function getTableCell( data ) {
+		for ( const value of view.createRangeIn( viewDoc.getRoot() ) ) {
+			if ( value.type === 'text' && value.item.data === data ) {
+				return value.item.parent.parent;
+			}
+		}
+	}
+
+	function makeMouseLeave() {
+		fireEvent( view, 'mouseleave' );
+	}
+
+	function pressMouseButtonOver( target ) {
+		fireEvent( view, 'mousedown', addTarget( target ), mouseButtonPressed );
+	}
+
+	function movePressedMouseOver( target ) {
+		moveMouseOver( target, mouseButtonPressed );
+	}
+
+	function moveReleasedMouseOver( target ) {
+		moveMouseOver( target, mouseButtonReleased );
+	}
+
+	function moveMouseOver( target, ...decorators ) {
+		fireEvent( view, 'mousemove', addTarget( target ), ...decorators );
+	}
+
+	function releaseMouseButtonOver( target ) {
+		fireEvent( view, 'mouseup', addTarget( target ), mouseButtonReleased );
+	}
+
+	function addTarget( target ) {
+		return domEventData => {
+			domEventData.target = target;
+		};
+	}
+
+	function mouseButtonPressed( domEventData ) {
+		domEventData.domEvent.buttons = 1;
+	}
+
+	function mouseButtonReleased( domEventData ) {
+		domEventData.domEvent.buttons = 0;
+	}
+
+	function fireEvent( view, eventName, ...decorators ) {
+		const domEvtDataStub = {
+			domEvent: {
+				buttons: 0
+			},
+			target: undefined,
+			preventDefault: sinon.spy(),
+			stopPropagation: sinon.spy()
+		};
+
+		for ( const decorator of decorators ) {
+			decorator( domEvtDataStub );
+		}
+
+		viewDoc.fire( eventName, domEvtDataStub );
+	}
+} );

+ 17 - 0
packages/ckeditor5-table/theme/tableselection.css

@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/*
+ * Note: This file should contain the wireframe styles only. But since there are no such styles,
+ * it acts as a message to the builder telling that it should look for the corresponding styles
+ * **in the theme** when compiling the editor.
+ */
+
+.ck.ck-editor__editable .table table {
+	& td.ck-editor__editable_selected,
+	& th.ck-editor__editable_selected {
+		box-shadow: inset 0 0 0 1px var(--ck-color-focus-border);
+	}
+}