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

Merge branch 'master' into t/63

# Conflicts:
#	src/commands/mergecellcommand.js
#	src/tableui.js
#	tests/converters/downcast.js
Maciej Gołaszewski 6 лет назад
Родитель
Сommit
34d59d9350
36 измененных файлов с 826 добавлено и 438 удалено
  1. 4 6
      packages/ckeditor5-table/.travis.yml
  2. 32 0
      packages/ckeditor5-table/CHANGELOG.md
  3. 0 2
      packages/ckeditor5-table/README.md
  4. 1 1
      packages/ckeditor5-table/docs/api/table.md
  5. 2 1
      packages/ckeditor5-table/lang/contexts.json
  6. 89 0
      packages/ckeditor5-table/lang/translations/da.po
  7. 89 0
      packages/ckeditor5-table/lang/translations/en-gb.po
  8. 2 2
      packages/ckeditor5-table/lang/translations/nl.po
  9. 18 18
      packages/ckeditor5-table/package.json
  10. 10 4
      packages/ckeditor5-table/src/commands/setheadercolumncommand.js
  11. 17 8
      packages/ckeditor5-table/src/commands/setheaderrowcommand.js
  12. 1 0
      packages/ckeditor5-table/src/converters/downcast.js
  13. 39 32
      packages/ckeditor5-table/src/converters/table-cell-paragraph-post-fixer.js
  14. 87 0
      packages/ckeditor5-table/src/converters/table-cell-refresh-post-fixer.js
  15. 0 198
      packages/ckeditor5-table/src/converters/tablecell-post-fixer.js
  16. 11 5
      packages/ckeditor5-table/src/tableediting.js
  17. 3 0
      packages/ckeditor5-table/src/tabletoolbar.js
  18. 4 2
      packages/ckeditor5-table/src/tableui.js
  19. 6 3
      packages/ckeditor5-table/src/tableutils.js
  20. 80 80
      packages/ckeditor5-table/src/tablewalker.js
  21. 1 1
      packages/ckeditor5-table/src/ui/inserttableview.js
  22. 1 1
      packages/ckeditor5-table/src/utils.js
  23. 2 2
      packages/ckeditor5-table/tests/_utils/utils.js
  24. 24 0
      packages/ckeditor5-table/tests/commands/setheadercolumncommand.js
  25. 36 0
      packages/ckeditor5-table/tests/commands/setheaderrowcommand.js
  26. 10 10
      packages/ckeditor5-table/tests/converters/downcast.js
  27. 1 1
      packages/ckeditor5-table/tests/converters/table-cell-paragraph-post-fixer.js
  28. 55 33
      packages/ckeditor5-table/tests/converters/table-cell-refresh-post-fixer.js
  29. 30 0
      packages/ckeditor5-table/tests/manual/rtl.html
  30. 31 0
      packages/ckeditor5-table/tests/manual/rtl.js
  31. 21 0
      packages/ckeditor5-table/tests/manual/rtl.md
  32. 4 0
      packages/ckeditor5-table/tests/tableediting.js
  33. 8 0
      packages/ckeditor5-table/tests/tabletoolbar.js
  34. 60 2
      packages/ckeditor5-table/tests/tableui.js
  35. 44 23
      packages/ckeditor5-table/tests/tablewalker.js
  36. 3 3
      packages/ckeditor5-table/tests/ui/inserttableview.js

+ 4 - 6
packages/ckeditor5-table/.travis.yml

@@ -1,9 +1,11 @@
 sudo: required
 sudo: required
-dist: trusty
+dist: xenial
 addons:
 addons:
   chrome: stable
   chrome: stable
   firefox: latest
   firefox: latest
 language: node_js
 language: node_js
+services:
+- xvfb
 cache:
 cache:
   yarn: true
   yarn: true
 node_js:
 node_js:
@@ -13,8 +15,6 @@ branches:
   - stable
   - stable
 before_install:
 before_install:
 - export START_TIME=$( date +%s )
 - export START_TIME=$( date +%s )
-- export DISPLAY=:99.0
-- sh -e /etc/init.d/xvfb start
 - npm i -g yarn
 - npm i -g yarn
 install:
 install:
 - yarn add @ckeditor/ckeditor5-dev-tests
 - yarn add @ckeditor/ckeditor5-dev-tests
@@ -28,6 +28,4 @@ after_script:
 - ckeditor5-dev-tests-notify-travis-status
 - ckeditor5-dev-tests-notify-travis-status
 env:
 env:
   global:
   global:
-  - secure: BKC/208PtT8ya6P0gNbEczGvd5UCtvIljxjQokFrFESOAUmqtlmdUjES/MRCEyGn3hEi9cYLPFaiAH68qjl+F1uLSwcqvalTnXMPpUPUjGODHKze+P1BGyP6KjvsTO+Lw0exwisYLChkhkQhXN7uUehxg6pdZ85r4uS3XKeCkVdObC/OPnlEgB2IDULDHnR3EApOR+4vqU1+ci8+12/ct62EPgq8bbxirvk6xoelNJeLVBBgySdC2ltPNVmERq9OqWJfl6zv9y+NbgFcbYNNdvMFgm1WSlf1HzATTNYbpmXz3pf2kSRjy4ikoIlq7qZ3igLJpGrWPQFttmnpxh/kuapSEHX74LqlUi9ctHX4/gO9+NdCriXSERYQXkVO4beHfqfR5S1dADBXHfBJGGxEmDjkAdrWA9ATG4sOD8sX/qZOFI86NsVYnglbAbHfdjLhQVPjqQ+596nSi/C2sZEOtvnNFT6ZkEeQ10aLp1lWJOn79226sVkC6eAIlVlypME9M+C1DLyGIdXzxcc0+nunVF7cMwTXhwW0psToLASuHJjYJ0/X4uzpxLIaGVFQ0PJE5ALtO1HDZqUrPrJSOkYXcZnbDk8FYpkeAvqNbHKUOUWUtVfmtrzvBDGoZrSfgnLI7WfNYUtfuZ0FcCaB5RV4H3iHBcjqhbkBJrcUe7TNPdo=
-  - secure: XbRykrpv5bv7Pu7NNRtAvAJr+MZRTBC+/aHGzW06gzu4msi+yifK3AnE0FvOfMsBaiLMXSoxVyUJwRZmSPa7wXt4ZgUna9Ntbx1I7JxjjbbB3K+lUHY+9r+kGFHDpEYQIMqkrSSUeP2kE/rHn6aLAd+bFsrs1/FyxMefSOE4egYAmYYe7V2LfTFX2/M2QfyYa5lJopJYgil/OzTd5kHr3rqQF4h/2Ld2MuVNsjuob0tJLBlIgdAuOm4mKEnPwyIMRWrQDwPs3DBTIiPBtLJO+/Cd/e7i97M8SscFBtFwg+hisUhzj57ixwJb0IANtOZewsqqmtCNt6JJyWFYH4rvqUWQkeCqOI+Gbi0LI/+RLaTBHVzSFIJFdt/zujNiEUDMzR5pCgNqc9ckmmqriwaURw4EuSUqnYoFc0xp95GKOTbAd6+UZOBfub3aNJBmMCuC2VpGv7RYxrzvOmWljWd5S2E0ZePafYFAiPK5rB5725ah1SohgOr6iQh/7yFSEwqLpXIZSBjkKLd2sLyILjywXxk11aUMPlKgLULVoN5jSpiVbw+j+bDZuNuf7AVSHymPGxBzpkkHu6wsXAqzKUZ0XHVpFSxCmAY74vj+3khRgweO8MQYG61GxWV8lWJK5COFUVfHcHeT5sRZCgq06hHQE5NKKHzukBO3cCeola2D6wU=
-  - secure: MTHJ5n94Ussk/u/YmIdBHsIDRan64VCbZRTklkx9hgtqwXcj+j6jV6S8makAZSCR55iBrscX1nrjROxSM4fbUsy4Owcu2Fik17IgA+xkYk2HgT2aupzLbpBD3DB538tHBxOb7me6m0GiO9Q9T3a2M8wBA4opxFfJX5O5qIy7lkKRnohy/ekGq2Av5cSU54tu0advGLA0okicE8qpkbpkV+DyZsPKYuHtOccJU/KJjX0dAH4/SWRqOeaXqRaOR6i8RcKEn4yEvKvZgqncv6hrzW4rJk1eXHFQCWlMnjIAQvIcpq0cplu5ktHjQ1W7/pTOELTnRLA4j+Oiu5+jZZqWKqTtNiZpATIp/fiDUTODrSIPovPbXZK0PepJhP3inWp2FtXTTj2/NrRtS2fL+U0jsI0wYBBx2WTDbj537BXFHAIRqKUDaEVxSzUGelA4H9STBNWbDXmyTeYzchWpimj/6TwG/VRYjhNQZ1rcwaGKpxNd8VDmDf2QxHcnfAu6+irsJb/0+B8zOj6hzw2ouLSlgEjfv9U5x997jeIUiStQBpdKrNiRw878tOuE0yOGbjIhfkKdo4gmcvDi20RioOs8qiO/tfYjgV/43sMBasrkNVkvv+RITTxUNf5kpWq6UnI8X5VjzWUDAKwELj6kPZl1C6MG0p8Fl8du2F77rnJS2wI=
+  - secure: HS+XKf6cgtVAh3DuLk63Ivau7NXk5E+GB67GU4nYtW7k3Q/jjcbfyprDmX8erD5nAeR/egFLrzUGQVqipWNFIU5MZi8naghuXY/adEKvPTJ/gwNXkZRS0QZzq5Jv0o7j+mfeHRdu78VcwUFzTjr2WBUq6QX1ra+sBo5kmRhoYv2F7jMn+24CEvhK0Vx7n5iYnNGLcP3nKkwRpRNbWxHGK4r0HAGfS3JrgrZGHClIlvzNRomTQy3RObppiqlQjJsebCobtgBOvB33KrpsnG0wCeyrkkAH+/u6IuXSSLibGqprd7todDZTrGRKWg2faicTZ9zZ+DTpSX/1xBWotIgmusIsV8jINQtnHoRp0BXHvcx5wkmFCo73T/squleQhss6wQkeJnOZkQEd0P6CpTQGvrJZjSUxv1pboyEeWgjB86b6jnNefys4GadZE7mbE6cYaKzXGEJMFclH4OBw4uF/VadC75ZEpoIK8YOk8ccSp+bvb3KzrYequrS+b7XidwEPrI4HLC4MESqEfHTKIKZro+Db2xgkxS0ZJl1KF7e8UMW3Zkh5a9AZ8sOrYUmJGVm3LjDeKCa4fe/C/vEEyxVksSZa/Wc440agp4caevbg9ksQ6PfkV6TGLyphhVAYEdT3i7ZiwdwRrU2RTuT35M/3Y6dOJVZGoIW7/LhCD6TRrDw=

+ 32 - 0
packages/ckeditor5-table/CHANGELOG.md

@@ -1,6 +1,38 @@
 Changelog
 Changelog
 =========
 =========
 
 
+## [14.0.0](https://github.com/ckeditor/ckeditor5-table/compare/v13.0.2...v14.0.0) (2019-08-26)
+
+### Features
+
+* `TableWalker` will now return `cell` value also for spanned cells when traversing a table with `includeSpanned` option set to `true`. Additionally, `isSpanned` property was introduced in returned values. ([07e8736](https://github.com/ckeditor/ckeditor5-table/commit/07e8736))
+
+### Bug fixes
+
+* Table cell post-fixer will refresh a cell only when it is needed. Closes [#209](https://github.com/ckeditor/ckeditor5-table/issues/209). ([b29a042](https://github.com/ckeditor/ckeditor5-table/commit/b29a042))
+
+### Other changes
+
+* Adjusted `InsertTableView` for better compatibility with right–to–left (RTL) languages. See [ckeditor/ckeditor5#1151](https://github.com/ckeditor/ckeditor5/issues/1151). ([524586b](https://github.com/ckeditor/ckeditor5-table/commit/524586b))
+* The issue tracker for this package was moved to https://github.com/ckeditor/ckeditor5/issues. See [ckeditor/ckeditor5#1988](https://github.com/ckeditor/ckeditor5/issues/1988). ([3d35af2](https://github.com/ckeditor/ckeditor5-table/commit/3d35af2))
+* The table widget toolbar should have a proper `aria-label` attribute (see [ckeditor/ckeditor5#1404](https://github.com/ckeditor/ckeditor5/issues/1404)). ([b47a94f](https://github.com/ckeditor/ckeditor5-table/commit/b47a94f))
+* Updated translations. ([2ddc9f4](https://github.com/ckeditor/ckeditor5-table/commit/2ddc9f4))
+
+### BREAKING CHANGES
+
+* `TableWalker` will not return `undefined` as `cell` value for spanned cells anymore. Use `isSpanned` instead.
+
+
+## [13.0.2](https://github.com/ckeditor/ckeditor5-table/compare/v13.0.1...v13.0.2) (2019-07-10)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
+## [13.0.1](https://github.com/ckeditor/ckeditor5-table/compare/v13.0.0...v13.0.1) (2019-07-04)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
 ## [13.0.0](https://github.com/ckeditor/ckeditor5-table/compare/v12.0.1...v13.0.0) (2019-06-05)
 ## [13.0.0](https://github.com/ckeditor/ckeditor5-table/compare/v12.0.1...v13.0.0) (2019-06-05)
 
 
 ### Bug fixes
 ### Bug fixes

+ 0 - 2
packages/ckeditor5-table/README.md

@@ -1,10 +1,8 @@
 CKEditor 5 table feature
 CKEditor 5 table feature
 ============================
 ============================
 
 
-[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-table.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-table.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-table.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-table)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-table.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-table)
-[![BrowserStack Status](https://automate.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://automate.browserstack.com/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-table/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-table?branch=master)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-table/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-table?branch=master)
 <br>
 <br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-table/status.svg)](https://david-dm.org/ckeditor/ckeditor5-table)
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-table/status.svg)](https://david-dm.org/ckeditor/ckeditor5-table)

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

@@ -34,5 +34,5 @@ The source code of this package is available on GitHub in https://github.com/cke
 
 
 * [`@ckeditor/ckeditor5-table` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)
 * [`@ckeditor/ckeditor5-table` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-table)
 * [`ckeditor/ckeditor5-table` on GitHub](https://github.com/ckeditor/ckeditor5-table)
 * [`ckeditor/ckeditor5-table` on GitHub](https://github.com/ckeditor/ckeditor5-table)
-* [Issue tracker](https://github.com/ckeditor/ckeditor5-table/issues)
+* [Issue tracker](https://github.com/ckeditor/ckeditor5/issues)
 * [Changelog](https://github.com/ckeditor/ckeditor5-table/blob/master/CHANGELOG.md)
 * [Changelog](https://github.com/ckeditor/ckeditor5-table/blob/master/CHANGELOG.md)

+ 2 - 1
packages/ckeditor5-table/lang/contexts.json

@@ -16,5 +16,6 @@
 	"Merge cell left": "Label for the merge table cell left button.",
 	"Merge cell left": "Label for the merge table cell left button.",
 	"Split cell vertically": "Label for the split table cell vertically button.",
 	"Split cell vertically": "Label for the split table cell vertically button.",
 	"Split cell horizontally": "Label for the split table cell horizontally button.",
 	"Split cell horizontally": "Label for the split table cell horizontally button.",
-	"Merge cells": "Label for the merge table cells button."
+	"Merge cells": "Label for the merge table cells button.",
+	"Table toolbar": "The label used by assistive technologies describing a table toolbar attached to a table widget."
 }
 }

+ 89 - 0
packages/ckeditor5-table/lang/translations/da.po

@@ -0,0 +1,89 @@
+# Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: Danish (https://www.transifex.com/ckeditor/teams/11143/da/)\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the insert table toolbar button."
+msgid "Insert table"
+msgstr "Indsæt tabel"
+
+msgctxt "Label for the set/unset table header column button."
+msgid "Header column"
+msgstr "Headerkolonne"
+
+msgctxt "Label for the insert table column to the left of the current one button."
+msgid "Insert column left"
+msgstr "Indsæt kolonne venstre"
+
+msgctxt "Label for the insert table column to the right of the current one button."
+msgid "Insert column right"
+msgstr "Indsæt kolonne højre"
+
+msgctxt "Label for the delete table column button."
+msgid "Delete column"
+msgstr "Slet kolonne"
+
+msgctxt "Label for the table column dropdown button."
+msgid "Column"
+msgstr "Kolonne"
+
+msgctxt "Label for the set/unset table header row button."
+msgid "Header row"
+msgstr "Headerrække"
+
+msgctxt "Label for the insert row below button."
+msgid "Insert row below"
+msgstr "Indsæt header under"
+
+msgctxt "Label for the insert row above button."
+msgid "Insert row above"
+msgstr "Indsæt header over"
+
+msgctxt "Label for the delete table row button."
+msgid "Delete row"
+msgstr "Slet række"
+
+msgctxt "Label for the table row dropdown button."
+msgid "Row"
+msgstr "Række"
+
+msgctxt "Label for the merge table cell up button."
+msgid "Merge cell up"
+msgstr "Flet celler op"
+
+msgctxt "Label for the merge table cell right button."
+msgid "Merge cell right"
+msgstr "Flet celler højre"
+
+msgctxt "Label for the merge table cell down button."
+msgid "Merge cell down"
+msgstr "Flet celler ned"
+
+msgctxt "Label for the merge table cell left button."
+msgid "Merge cell left"
+msgstr "Flet celler venstre"
+
+msgctxt "Label for the split table cell vertically button."
+msgid "Split cell vertically"
+msgstr "Del celle vertikalt"
+
+msgctxt "Label for the split table cell horizontally button."
+msgid "Split cell horizontally"
+msgstr "Del celle horisontalt"
+
+msgctxt "Label for the merge table cells button."
+msgid "Merge cells"
+msgstr "Flet celler"

+ 89 - 0
packages/ckeditor5-table/lang/translations/en-gb.po

@@ -0,0 +1,89 @@
+# Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: English (United Kingdom) (https://www.transifex.com/ckeditor/teams/11143/en_GB/)\n"
+"Language: en_GB\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the insert table toolbar button."
+msgid "Insert table"
+msgstr "Insert table"
+
+msgctxt "Label for the set/unset table header column button."
+msgid "Header column"
+msgstr "Header column"
+
+msgctxt "Label for the insert table column to the left of the current one button."
+msgid "Insert column left"
+msgstr "Insert column left"
+
+msgctxt "Label for the insert table column to the right of the current one button."
+msgid "Insert column right"
+msgstr "Insert column right"
+
+msgctxt "Label for the delete table column button."
+msgid "Delete column"
+msgstr "Delete column"
+
+msgctxt "Label for the table column dropdown button."
+msgid "Column"
+msgstr "Column"
+
+msgctxt "Label for the set/unset table header row button."
+msgid "Header row"
+msgstr "Header row"
+
+msgctxt "Label for the insert row below button."
+msgid "Insert row below"
+msgstr "Insert row below"
+
+msgctxt "Label for the insert row above button."
+msgid "Insert row above"
+msgstr "Insert row above"
+
+msgctxt "Label for the delete table row button."
+msgid "Delete row"
+msgstr "Delete row"
+
+msgctxt "Label for the table row dropdown button."
+msgid "Row"
+msgstr "Row"
+
+msgctxt "Label for the merge table cell up button."
+msgid "Merge cell up"
+msgstr "Merge cell up"
+
+msgctxt "Label for the merge table cell right button."
+msgid "Merge cell right"
+msgstr "Merge cell right"
+
+msgctxt "Label for the merge table cell down button."
+msgid "Merge cell down"
+msgstr "Merge cell down"
+
+msgctxt "Label for the merge table cell left button."
+msgid "Merge cell left"
+msgstr "Merge cell left"
+
+msgctxt "Label for the split table cell vertically button."
+msgid "Split cell vertically"
+msgstr "Split cell vertically"
+
+msgctxt "Label for the split table cell horizontally button."
+msgid "Split cell horizontally"
+msgstr "Split cell horizontally"
+
+msgctxt "Label for the merge table cells button."
+msgid "Merge cells"
+msgstr "Merge cells"

+ 2 - 2
packages/ckeditor5-table/lang/translations/nl.po

@@ -26,11 +26,11 @@ msgstr "Titel kolom"
 
 
 msgctxt "Label for the insert table column to the left of the current one button."
 msgctxt "Label for the insert table column to the left of the current one button."
 msgid "Insert column left"
 msgid "Insert column left"
-msgstr ""
+msgstr "Kolom links invoegen"
 
 
 msgctxt "Label for the insert table column to the right of the current one button."
 msgctxt "Label for the insert table column to the right of the current one button."
 msgid "Insert column right"
 msgid "Insert column right"
-msgstr ""
+msgstr "Kolom rechts invoegen"
 
 
 msgctxt "Label for the delete table column button."
 msgctxt "Label for the delete table column button."
 msgid "Delete column"
 msgid "Delete column"

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@ckeditor/ckeditor5-table",
   "name": "@ckeditor/ckeditor5-table",
-  "version": "13.0.0",
+  "version": "14.0.0",
   "description": "Table feature for CKEditor 5.",
   "description": "Table feature for CKEditor 5.",
   "keywords": [
   "keywords": [
     "ckeditor",
     "ckeditor",
@@ -10,25 +10,25 @@
     "ckeditor5-plugin"
     "ckeditor5-plugin"
   ],
   ],
   "dependencies": {
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^12.1.1",
-    "@ckeditor/ckeditor5-ui": "^13.0.0",
-    "@ckeditor/ckeditor5-widget": "^11.0.2"
+    "@ckeditor/ckeditor5-core": "^12.3.0",
+    "@ckeditor/ckeditor5-ui": "^14.0.0",
+    "@ckeditor/ckeditor5-widget": "^11.1.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@ckeditor/ckeditor5-alignment": "^11.1.1",
-    "@ckeditor/ckeditor5-block-quote": "^11.1.0",
-    "@ckeditor/ckeditor5-clipboard": "^11.0.2",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.1",
-    "@ckeditor/ckeditor5-engine": "^13.1.1",
-    "@ckeditor/ckeditor5-image": "^13.1.0",
-    "@ckeditor/ckeditor5-list": "^12.0.2",
-    "@ckeditor/ckeditor5-media-embed": "^11.1.1",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.2",
-    "@ckeditor/ckeditor5-typing": "^12.0.2",
-    "@ckeditor/ckeditor5-undo": "^11.0.2",
-    "@ckeditor/ckeditor5-utils": "^12.1.1",
+    "@ckeditor/ckeditor5-alignment": "^11.2.0",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.3",
+    "@ckeditor/ckeditor5-clipboard": "^12.0.2",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.4",
+    "@ckeditor/ckeditor5-engine": "^14.0.0",
+    "@ckeditor/ckeditor5-image": "^14.0.0",
+    "@ckeditor/ckeditor5-list": "^12.1.0",
+    "@ckeditor/ckeditor5-media-embed": "^11.1.4",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.5",
+    "@ckeditor/ckeditor5-typing": "^12.2.0",
+    "@ckeditor/ckeditor5-undo": "^11.0.5",
+    "@ckeditor/ckeditor5-utils": "^14.0.0",
     "eslint": "^5.5.0",
     "eslint": "^5.5.0",
-    "eslint-config-ckeditor5": "^1.0.11",
+    "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",
     "husky": "^1.3.1",
     "lint-staged": "^7.0.0"
     "lint-staged": "^7.0.0"
   },
   },
@@ -39,7 +39,7 @@
   "author": "CKSource (http://cksource.com/)",
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
   "license": "GPL-2.0-or-later",
   "homepage": "https://ckeditor.com/ckeditor-5",
   "homepage": "https://ckeditor.com/ckeditor-5",
-  "bugs": "https://github.com/ckeditor/ckeditor5-table/issues",
+  "bugs": "https://github.com/ckeditor/ckeditor5/issues",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "https://github.com/ckeditor/ckeditor5-table.git"
     "url": "https://github.com/ckeditor/ckeditor5-table.git"

+ 10 - 4
packages/ckeditor5-table/src/commands/setheadercolumncommand.js

@@ -61,22 +61,28 @@ export default class SetHeaderColumnCommand extends Command {
 	 * When the selection is already in a header column, it will set `headingColumns` so the heading section will end before that column.
 	 * When the selection is already in a header column, it will set `headingColumns` so the heading section will end before that column.
 	 *
 	 *
 	 * @fires execute
 	 * @fires execute
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.forceValue] If set, the command will set (`true`) or unset (`false`) header columns according to
+	 * `forceValue` parameter instead of the current model state.
 	 */
 	 */
-	execute() {
+	execute( options = {} ) {
 		const model = this.editor.model;
 		const model = this.editor.model;
 		const doc = model.document;
 		const doc = model.document;
 		const selection = doc.selection;
 		const selection = doc.selection;
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 
 
 		const position = selection.getFirstPosition();
 		const position = selection.getFirstPosition();
-		const tableCell = findAncestor( 'tableCell', position.parent );
+		const tableCell = findAncestor( 'tableCell', position );
 		const tableRow = tableCell.parent;
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 		const table = tableRow.parent;
 
 
-		const currentHeadingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
 		const { column: selectionColumn } = tableUtils.getCellLocation( tableCell );
 		const { column: selectionColumn } = tableUtils.getCellLocation( tableCell );
 
 
-		const headingColumnsToSet = currentHeadingColumns > selectionColumn ? selectionColumn : selectionColumn + 1;
+		if ( options.forceValue === this.value ) {
+			return;
+		}
+
+		const headingColumnsToSet = this.value ? selectionColumn : selectionColumn + 1;
 
 
 		model.change( writer => {
 		model.change( writer => {
 			updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 );
 			updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 );

+ 17 - 8
packages/ckeditor5-table/src/commands/setheaderrowcommand.js

@@ -60,8 +60,11 @@ export default class SetHeaderRowCommand extends Command {
 	 * When the selection is already in a header row, it will set `headingRows` so the heading section will end before that row.
 	 * When the selection is already in a header row, it will set `headingRows` so the heading section will end before that row.
 	 *
 	 *
 	 * @fires execute
 	 * @fires execute
+	 * @param {Object} options
+	 * @param {Boolean} [options.forceValue] If set, the command will set (`true`) or unset (`false`) header rows according to `forceValue`
+	 * parameter instead of the current model state.
 	 */
 	 */
-	execute() {
+	execute( options = {} ) {
 		const model = this.editor.model;
 		const model = this.editor.model;
 		const doc = model.document;
 		const doc = model.document;
 		const selection = doc.selection;
 		const selection = doc.selection;
@@ -74,7 +77,11 @@ export default class SetHeaderRowCommand extends Command {
 		const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
 		const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
 		const selectionRow = tableRow.index;
 		const selectionRow = tableRow.index;
 
 
-		const headingRowsToSet = currentHeadingRows > selectionRow ? selectionRow : selectionRow + 1;
+		if ( options.forceValue === this.value ) {
+			return;
+		}
+
+		const headingRowsToSet = this.value ? selectionRow : selectionRow + 1;
 
 
 		model.change( writer => {
 		model.change( writer => {
 			if ( headingRowsToSet ) {
 			if ( headingRowsToSet ) {
@@ -151,19 +158,21 @@ function splitHorizontally( tableCell, headingRows, writer ) {
 		attributes.rowspan = spanToSet;
 		attributes.rowspan = spanToSet;
 	}
 	}
 
 
+	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
+
+	if ( colspan > 1 ) {
+		attributes.colspan = colspan;
+	}
+
 	const startRow = table.getChildIndex( tableRow );
 	const startRow = table.getChildIndex( tableRow );
 	const endRow = startRow + newRowspan;
 	const endRow = startRow + newRowspan;
 	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeSpanned: true } ) ];
 	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeSpanned: true } ) ];
 
 
 	let columnIndex;
 	let columnIndex;
 
 
-	for ( const { row, column, cell, colspan, cellIndex } of tableMap ) {
-		if ( cell === tableCell ) {
+	for ( const { row, column, cell, cellIndex } of tableMap ) {
+		if ( cell === tableCell && columnIndex === undefined ) {
 			columnIndex = column;
 			columnIndex = column;
-
-			if ( colspan > 1 ) {
-				attributes.colspan = colspan;
-			}
 		}
 		}
 
 
 		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
 		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {

+ 1 - 0
packages/ckeditor5-table/src/converters/downcast.js

@@ -345,6 +345,7 @@ function renameViewTableCell( tableCell, desiredCellElementName, conversionApi,
 		renamedCell = viewWriter.rename( desiredCellElementName, viewCell );
 		renamedCell = viewWriter.rename( desiredCellElementName, viewCell );
 	}
 	}
 
 
+	conversionApi.mapper.unbindViewElement( viewCell );
 	conversionApi.mapper.bindElements( tableCell, renamedCell );
 	conversionApi.mapper.bindElements( tableCell, renamedCell );
 }
 }
 
 

+ 39 - 32
packages/ckeditor5-table/src/converters/table-cell-content-post-fixer.js → packages/ckeditor5-table/src/converters/table-cell-paragraph-post-fixer.js

@@ -4,34 +4,31 @@
  */
  */
 
 
 /**
 /**
- * @module table/converters/table-cell-content-post-fixer
+ * @module table/converters/table-cell-paragraph-post-fixer
  */
  */
 
 
 /**
 /**
- * Injects a table cell post-fixer into the model.
+ * Injects a table cell post-fixer into the model which inserts `paragraph` element into empty table cells.
  *
  *
- * The role of the table post-fixer is to ensure that the table cells have the correct content
- * after a {@link module:engine/model/model~Model#change `change()`} block was executed.
+ * A table cell must contain at least one block element as a child. An empty table cell will have empty `paragraph` as a child.
  *
  *
- * A table cells must contains at least one block as a child. The empty table cell will have empty `<paragraph>` as a child.
- *
- *        <table>
- *            <tableRow>
- *                <tableCell></tableCell>
- *            </tableRow>
- *        </table>
+ *		<table>
+ *			<tableRow>
+ *				<tableCell></tableCell>
+ *			</tableRow>
+ *		</table>
  *
  *
  * Will be fixed to:
  * Will be fixed to:
  *
  *
- *        <table>
- *            <tableRow>
- *                <tableCell><paragraph></paragraph></tableCell>
- *            </tableRow>
- *        </table>
+ *		<table>
+ *			<tableRow>
+ *				<tableCell><paragraph></paragraph></tableCell>
+ *			</tableRow>
+ *		</table>
  *
  *
  * @param {module:engine/model/model~Model} model
  * @param {module:engine/model/model~Model} model
  */
  */
-export default function injectTableCellContentPostFixer( model ) {
+export default function injectTableCellParagraphPostFixer( model ) {
 	model.document.registerPostFixer( writer => tableCellContentsPostFixer( writer, model ) );
 	model.document.registerPostFixer( writer => tableCellContentsPostFixer( writer, model ) );
 }
 }
 
 
@@ -45,24 +42,20 @@ function tableCellContentsPostFixer( writer, model ) {
 	let wasFixed = false;
 	let wasFixed = false;
 
 
 	for ( const entry of changes ) {
 	for ( const entry of changes ) {
-		// Enforce paragraph in tableCell even after other feature remove its contents.
-		if ( entry.type == 'remove' && entry.position.parent.is( 'tableCell' ) ) {
-			wasFixed = fixTableCellContent( entry.position.parent, writer ) || wasFixed;
+		if ( entry.type == 'insert' && entry.name == 'table' ) {
+			wasFixed = fixTable( entry.position.nodeAfter, writer ) || wasFixed;
 		}
 		}
 
 
-		// Analyze table cells on insertion.
-		if ( entry.type == 'insert' ) {
-			if ( entry.name == 'table' ) {
-				wasFixed = fixTable( entry.position.nodeAfter, writer ) || wasFixed;
-			}
+		if ( entry.type == 'insert' && entry.name == 'tableRow' ) {
+			wasFixed = fixTableRow( entry.position.nodeAfter, writer ) || wasFixed;
+		}
 
 
-			if ( entry.name == 'tableRow' ) {
-				wasFixed = fixTableRow( entry.position.nodeAfter, writer ) || wasFixed;
-			}
+		if ( entry.type == 'insert' && entry.name == 'tableCell' ) {
+			wasFixed = fixTableCellContent( entry.position.nodeAfter, writer ) || wasFixed;
+		}
 
 
-			if ( entry.name == 'tableCell' ) {
-				wasFixed = fixTableCellContent( entry.position.nodeAfter, writer ) || wasFixed;
-			}
+		if ( checkTableCellChange( entry ) ) {
+			wasFixed = fixTableCellContent( entry.position.parent, writer ) || wasFixed;
 		}
 		}
 	}
 	}
 
 
@@ -112,7 +105,7 @@ function fixTableCellContent( tableCell, writer ) {
 		return true;
 		return true;
 	}
 	}
 
 
-	// Check table cell children for directly placed $text nodes.
+	// Check table cell children for directly placed text nodes.
 	// Temporary solution. See https://github.com/ckeditor/ckeditor5/issues/1464.
 	// Temporary solution. See https://github.com/ckeditor/ckeditor5/issues/1464.
 	const textNodes = Array.from( tableCell.getChildren() ).filter( child => child.is( 'text' ) );
 	const textNodes = Array.from( tableCell.getChildren() ).filter( child => child.is( 'text' ) );
 
 
@@ -123,3 +116,17 @@ function fixTableCellContent( tableCell, writer ) {
 	// Return true when there were text nodes to fix.
 	// Return true when there were text nodes to fix.
 	return !!textNodes.length;
 	return !!textNodes.length;
 }
 }
+
+// Check if differ change should fix table cell. This happens on:
+// - removing content from table cell (ie tableCell can be left empty).
+// - adding text node directly into a table cell.
+//
+// @param {Object} differ change entry
+// @returns {Boolean}
+function checkTableCellChange( entry ) {
+	if ( !entry.position || !entry.position.parent.is( 'tableCell' ) ) {
+		return false;
+	}
+
+	return entry.type == 'insert' && entry.name == '$text' || entry.type == 'remove';
+}

+ 87 - 0
packages/ckeditor5-table/src/converters/table-cell-refresh-post-fixer.js

@@ -0,0 +1,87 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/converters/table-cell-refresh-post-fixer
+ */
+
+/**
+ * Injects a table cell post-fixer into the model which marks the table cell in the differ to have it re-rendered.
+ *
+ * Model `paragraph` inside a table cell can be rendered as `<span>` or `<p>`. It is rendered as `<span>` if this is the only block
+ * element in that table cell and it doesn't have any attributes. It is rendered as `<p>` otherwise.
+ *
+ * When table cell content changes, for example a second `paragraph` element is added we need to ensure that the first `paragraph` is
+ * re-rendered so it changes to `<p>` from `<span>`. The easiest way to do it is to re-render whole table cell.
+ *
+ * @param {module:engine/model/model~Model} model
+ */
+export default function injectTableCellRefreshPostFixer( model ) {
+	model.document.registerPostFixer( () => tableCellRefreshPostFixer( model ) );
+}
+
+function tableCellRefreshPostFixer( model ) {
+	const differ = model.document.differ;
+
+	// Stores cells to be refreshed so the table cell will be refreshed once for multiple changes.
+	const cellsToRefresh = new Set();
+
+	for ( const change of differ.getChanges() ) {
+		const parent = change.type == 'insert' || change.type == 'remove' ? change.position.parent : change.range.start.parent;
+
+		if ( parent.is( 'tableCell' ) && checkRefresh( parent, change.type ) ) {
+			cellsToRefresh.add( parent );
+		}
+	}
+
+	if ( cellsToRefresh.size ) {
+		for ( const tableCell of cellsToRefresh.values() ) {
+			differ.refreshItem( tableCell );
+		}
+
+		return true;
+	}
+
+	return false;
+}
+
+// Checks if the model table cell requires refreshing to be re-rendered to a proper state in the view.
+//
+// This methods detects changes that will require renaming <span> to <p> (or vice versa) in the view.
+//
+// This method is a simple heuristic that checks only a single change and will sometimes give a false positive result when multiple changes
+// will result in a state that does not require renaming in the view (but will be seen as requiring a refresh).
+//
+// For instance: a `<span>` should be renamed to `<p>` when adding an attribute to a `<paragraph>`.
+// But adding one attribute and removing another one will result in a false positive: the check for added attribute will see one attribute
+// on a paragraph and will falsy qualify such change as adding an attribute to a paragraph without any attribute.
+//
+// @param {module:engine/model/element~Element} tableCell Table cell to check.
+// @param {String} type Type of change.
+function checkRefresh( tableCell, type ) {
+	const hasInnerParagraph = Array.from( tableCell.getChildren() ).some( child => child.is( 'paragraph' ) );
+
+	// If there is no paragraph in table cell then the view doesn't require refreshing.
+	//
+	// Why? What we really want to achieve is to make all the old paragraphs (which weren't added in this batch) to be
+	// converted once again, so that the paragraph-in-table-cell converter can correctly create a `<p>` or a `<span>` element.
+	// If there are no paragraphs in the table cell, we don't care.
+	if ( !hasInnerParagraph ) {
+		return false;
+	}
+
+	// For attribute change we only refresh if there is a single paragraph as in this case we may want to change existing `<span>` to `<p>`.
+	if ( type == 'attribute' ) {
+		const attributesCount = Array.from( tableCell.getChild( 0 ).getAttributeKeys() ).length;
+
+		return tableCell.childCount === 1 && attributesCount < 2;
+	}
+
+	// For other changes (insert, remove) the `<span>` to `<p>` change is needed when:
+	//
+	// - another element is added to a single paragraph (childCount becomes >= 2)
+	// - another element is removed and a single paragraph is left (childCount == 1)
+	return tableCell.childCount <= ( type == 'insert' ? 2 : 1 );
+}

+ 0 - 198
packages/ckeditor5-table/src/converters/tablecell-post-fixer.js

@@ -1,198 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module table/converters/tablecell-post-fixer
- */
-
-/**
- * Injects a table cell post-fixer into the editing controller.
- *
- * The role of the table cell post-fixer is to ensure that the table cell contents in the editing view are properly converted.
- *
- * This post-fixer will ensure that after model changes in the editing view:
- * * single paragraphs are rendered as `<span>
- * * single paragraphs with one or more attributes are rendered as `<p>`
- * * single paragraphs in table cell with other block elements are rendered as `<p>`
- * * paragraphs in table cells with other block elements (including other paragraphs) are rendered as `<p>`.
- *
- * In the model each table cell has always at least one block element inside. If no other block was defined (empty table cell) the table
- * feature will insert empty `<paragraph>`. Similarly text nodes will be wrapped in paragraphs. Rendering in the data pipeline differs
- * from rendering in the editing pipeline - text nodes in single `<paragraph>` are rendered in the data pipeline as direct children
- * of the `<td>` or `<th>` elements. In other cases `<paragraph>` elements are rendered as `<p>` blocks.
- *
- * To ensure proper mappings between model and view elements and positions in the editing pipeline the table feature will always render
- * an element in the view: `<span>` for single or empty `<paragraph>` and `<p>` otherwise.
- *
- * Example:
- *
- *		<table>
- *			<tableRow>
- *				<tableCell><paragraph></paragraph></tableCell>
- *				<tableCell><paragraph>foo</paragraph></tableCell>
- *				<tableCell><paragraph baz="bar">foo</paragraph></tableCell>
- *				<tableCell><heading2>bar</heading2><paragraph>baz</paragraph></tableCell>
- *			</tableRow>
- *		</table>
- *
- * The editor will render in the data pipeline:
- *
- *		<figure>
- *			<table>
- *				<tbody>
- *					<tr>
- *						<td></td>
- *						<td>foo</td>
- *						<td><p baz="bar">foo</p></td>
- *						<td><h3>bar</h3><p>baz</p></td>
- *					</tr>
- *				</tbody>
- *			</table>
- *		</figure>
- *
- * and in the editing view (without widget markup):
- *
- *		<figure>
- *			<table>
- *				<tbody>
- *					<tr>
- *						<td><span></span></td>
- *						<td><span>foo</span></td>
- *						<td><p baz="bar">foo</p></td>
- *						<td><h3>bar</h3><p>baz</p></td>
- *					</tr>
- *				</tbody>
- *			</table>
- *		</figure>
- *
- * @param {module:engine/model/model~Model} model
- * @param {module:engine/controller/editingcontroller~EditingController} editing
- */
-export default function injectTableCellPostFixer( model, editing ) {
-	editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper, editing.view ) );
-}
-
-// The table cell post-fixer.
-//
-// @param {module:engine/view/writer~Writer} writer
-// @param {module:engine/model/model~Model} model
-// @param {module:engine/conversion/mapper~Mapper} mapper
-function tableCellPostFixer( writer, model, mapper, view ) {
-	let wasFixed = false;
-
-	const elementsToCheck = getElementsToCheck( view );
-
-	for ( const element of elementsToCheck ) {
-		wasFixed = ensureProperElementName( element, mapper, writer ) || wasFixed;
-	}
-
-	// Selection in the view might not be updated to renamed elements. Happens mostly when other feature inserts paragraph to the table cell
-	// (ie. when deleting table cell contents) and sets selection to it while table-post fixer changes view <p> to <span> element.
-	// The view.selection would have outdated nodes.
-	if ( wasFixed && selectionNeedsFix( view.document.selection, mapper ) ) {
-		updateRangesInViewSelection( model.document.selection, mapper, writer );
-	}
-
-	return wasFixed;
-}
-
-// Returns view elements changed in current view.change() block.
-//
-// **Note**: Currently it uses private property of the view: _renderer to get changed view elements to check.
-//
-// @param {module:engine/view/view~View} view
-function getElementsToCheck( view ) {
-	const elementsWithChangedAttributes = Array.from( view._renderer.markedAttributes )
-		.filter( el => !!el.parent )
-		.filter( isSpanOrP )
-		.filter( el => isTdOrTh( el.parent ) );
-
-	const changedChildren = Array.from( view._renderer.markedChildren )
-		.filter( el => !!el.parent )
-		.filter( isTdOrTh )
-		.reduce( ( prev, element ) => {
-			const childrenToCheck = Array.from( element.getChildren() ).filter( isSpanOrP );
-
-			return [ ...prev, ...childrenToCheck ];
-		}, [] );
-
-	return [ ...elementsWithChangedAttributes, ...changedChildren ];
-}
-
-// This method checks if view element for model's <paragraph> was properly converter.
-// Paragraph should be either
-// - span: for single paragraph with no attributes.
-// - p   : in other cases.
-function ensureProperElementName( currentViewElement, mapper, writer ) {
-	// This situation may happen if a view element was changed and removed at the same time.
-	// In this case, the view element is already unbound so the post-fixer would crash.
-	if ( !currentViewElement.root.is( 'rootElement' ) ) {
-		return false;
-	}
-
-	const modelParagraph = mapper.toModelElement( currentViewElement );
-	const expectedViewElementName = getExpectedElementName( modelParagraph.parent, modelParagraph );
-
-	if ( currentViewElement.name !== expectedViewElementName ) {
-		// Unbind current view element as it should be cleared from mapper.
-		mapper.unbindViewElement( currentViewElement );
-
-		const renamedViewElement = writer.rename( expectedViewElementName, currentViewElement );
-
-		// Bind paragraph inside table cell to the renamed view element.
-		mapper.bindElements( modelParagraph, renamedViewElement );
-
-		return true;
-	}
-
-	return false;
-}
-
-// Expected view element name depends on model elements:
-// - <paragraph> with any attribute set should be rendered as <p>
-// - all <paragraphs> in <tableCell> that has more then one children should be rendered as <p>
-// - an only <paragraph> child with no attributes should be rendered as <span>
-//
-// @param {module:engine/model/element~Element} tableCell
-// @param {module:engine/model/element~Element} paragraph
-// @returns {String}
-function getExpectedElementName( tableCell, paragraph ) {
-	const isOnlyChild = tableCell.childCount > 1;
-	const hasAttributes = !![ ...paragraph.getAttributes() ].length;
-
-	return ( isOnlyChild || hasAttributes ) ? 'p' : 'span';
-}
-
-// Method to filter out <span> and <p> elements.
-//
-// @param {module:engine/view/element~Element} element
-function isSpanOrP( element ) {
-	return element.is( 'p' ) || element.is( 'span' );
-}
-
-// Method to filter out <td> and <th> elements.
-//
-// @param {module:engine/view/element~Element} element
-function isTdOrTh( element ) {
-	return element.is( 'td' ) || element.is( 'th' );
-}
-
-// Resets view selections based on model selection.
-function updateRangesInViewSelection( selection, mapper, writer ) {
-	const fixedRanges = Array.from( selection.getRanges() )
-		.map( range => mapper.toViewRange( range ) );
-
-	writer.setSelection( fixedRanges, { backward: selection.isBackward } );
-}
-
-// Checks if selection needs to be fixed by ensuring that current view selection position's parents are present in the editable view.
-//
-// @param {module:engine/view/selection~Selection} viewSelection
-function selectionNeedsFix( viewSelection ) {
-	const anchor = viewSelection.anchor;
-	const focus = viewSelection.focus;
-
-	return viewSelection.rangeCount && ( !anchor.root.is( 'rootElement' ) || !focus.root.is( 'rootElement' ) );
-}

+ 11 - 5
packages/ckeditor5-table/src/tableediting.js

@@ -32,8 +32,8 @@ import { findAncestor } from './commands/utils';
 import TableUtils from '../src/tableutils';
 import TableUtils from '../src/tableutils';
 
 
 import injectTableLayoutPostFixer from './converters/table-layout-post-fixer';
 import injectTableLayoutPostFixer from './converters/table-layout-post-fixer';
-import injectTableCellContentsPostFixer from './converters/table-cell-content-post-fixer';
-import injectTableCellPostFixer from './converters/tablecell-post-fixer';
+import injectTableCellParagraphPostFixer from './converters/table-cell-paragraph-post-fixer';
+import injectTableCellRefreshPostFixer from './converters/table-cell-refresh-post-fixer';
 
 
 import '../theme/tableediting.css';
 import '../theme/tableediting.css';
 import TableSelection from './tableselection';
 import TableSelection from './tableselection';
@@ -44,6 +44,13 @@ import TableSelection from './tableselection';
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
 export default class TableEditing extends Plugin {
 export default class TableEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TableEditing';
+	}
+
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
@@ -112,8 +119,6 @@ export default class TableEditing extends Plugin {
 		conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
 		conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
 		conversion.for( 'dataDowncast' ).add( downcastTableHeadingRowsChange() );
 		conversion.for( 'dataDowncast' ).add( downcastTableHeadingRowsChange() );
 
 
-		injectTableCellPostFixer( editor.model, editor.editing );
-
 		// Define all the commands.
 		// Define all the commands.
 		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
 		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
 		editor.commands.add( 'insertTableRowAbove', new InsertRowCommand( editor, { order: 'above' } ) );
 		editor.commands.add( 'insertTableRowAbove', new InsertRowCommand( editor, { order: 'above' } ) );
@@ -133,7 +138,8 @@ export default class TableEditing extends Plugin {
 		editor.commands.add( 'setTableRowHeader', new SetHeaderRowCommand( editor ) );
 		editor.commands.add( 'setTableRowHeader', new SetHeaderRowCommand( editor ) );
 
 
 		injectTableLayoutPostFixer( model );
 		injectTableLayoutPostFixer( model );
-		injectTableCellContentsPostFixer( model );
+		injectTableCellRefreshPostFixer( model );
+		injectTableCellParagraphPostFixer( model );
 
 
 		// Handle tab key navigation.
 		// Handle tab key navigation.
 		this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } );
 		this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } );

+ 3 - 0
packages/ckeditor5-table/src/tabletoolbar.js

@@ -42,6 +42,7 @@ export default class TableToolbar extends Plugin {
 	 */
 	 */
 	afterInit() {
 	afterInit() {
 		const editor = this.editor;
 		const editor = this.editor;
+		const t = editor.t;
 		const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
 		const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
 
 
 		const tableContentToolbarItems = editor.config.get( 'table.contentToolbar' );
 		const tableContentToolbarItems = editor.config.get( 'table.contentToolbar' );
@@ -50,6 +51,7 @@ export default class TableToolbar extends Plugin {
 
 
 		if ( tableContentToolbarItems ) {
 		if ( tableContentToolbarItems ) {
 			widgetToolbarRepository.register( 'tableContent', {
 			widgetToolbarRepository.register( 'tableContent', {
+				ariaLabel: t( 'Table toolbar' ),
 				items: tableContentToolbarItems,
 				items: tableContentToolbarItems,
 				getRelatedElement: getTableWidgetAncestor
 				getRelatedElement: getTableWidgetAncestor
 			} );
 			} );
@@ -57,6 +59,7 @@ export default class TableToolbar extends Plugin {
 
 
 		if ( tableToolbarItems ) {
 		if ( tableToolbarItems ) {
 			widgetToolbarRepository.register( 'table', {
 			widgetToolbarRepository.register( 'table', {
+				ariaLabel: t( 'Table toolbar' ),
 				items: tableToolbarItems,
 				items: tableToolbarItems,
 				getRelatedElement: getSelectedTableWidget
 				getRelatedElement: getSelectedTableWidget
 			} );
 			} );

+ 4 - 2
packages/ckeditor5-table/src/tableui.js

@@ -38,6 +38,8 @@ export default class TableUI extends Plugin {
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
 		const t = this.editor.t;
 		const t = this.editor.t;
+		const contentLanguageDirection = editor.locale.contentLanguageDirection;
+		const isContentLtr = contentLanguageDirection === 'ltr';
 
 
 		editor.ui.componentFactory.add( 'insertTable', locale => {
 		editor.ui.componentFactory.add( 'insertTable', locale => {
 			const command = editor.commands.get( 'insertTable' );
 			const command = editor.commands.get( 'insertTable' );
@@ -86,14 +88,14 @@ export default class TableUI extends Plugin {
 				{
 				{
 					type: 'button',
 					type: 'button',
 					model: {
 					model: {
-						commandName: 'insertTableColumnLeft',
+						commandName: isContentLtr ? 'insertTableColumnLeft' : 'insertTableColumnRight',
 						label: t( 'Insert column left' )
 						label: t( 'Insert column left' )
 					}
 					}
 				},
 				},
 				{
 				{
 					type: 'button',
 					type: 'button',
 					model: {
 					model: {
-						commandName: 'insertTableColumnRight',
+						commandName: isContentLtr ? 'insertTableColumnRight' : 'insertTableColumnLeft',
 						label: t( 'Insert column right' )
 						label: t( 'Insert column right' )
 					}
 					}
 				},
 				},

+ 6 - 3
packages/ckeditor5-table/src/tableutils.js

@@ -180,7 +180,7 @@ export default class TableUtils extends Plugin {
 	 *		|       | c |                   |               | c |
 	 *		|       | c |                   |               | c |
 	 *		+---+---+---+     will give:    +---+---+---+---+---+
 	 *		+---+---+---+     will give:    +---+---+---+---+---+
 	 *		| d | e | f |                   | d |   |   | e | f |
 	 *		| d | e | f |                   | d |   |   | e | f |
-	 *		+---+   +---+                   +---+---+---+  +---+
+	 *		+---+   +---+                   +---+---+---+   +---+
 	 *		| g |   | h |                   | g |   |   |   | h |
 	 *		| g |   | h |                   | g |   |   |   | h |
 	 *		+---+---+---+                   +---+---+---+---+---+
 	 *		+---+---+---+                   +---+---+---+---+---+
 	 *		| i         |                   | i                 |
 	 *		| i         |                   | i                 |
@@ -219,13 +219,16 @@ export default class TableUtils extends Plugin {
 
 
 			const tableWalker = new TableWalker( table, { column: insertAt, includeSpanned: true } );
 			const tableWalker = new TableWalker( table, { column: insertAt, includeSpanned: true } );
 
 
-			for ( const { row, column, cell, colspan, rowspan, cellIndex } of tableWalker ) {
+			for ( const { row, cell, cellIndex } of tableWalker ) {
 				// When iterating over column the table walker outputs either:
 				// When iterating over column the table walker outputs either:
 				// - cells at given column index (cell "e" from method docs),
 				// - cells at given column index (cell "e" from method docs),
 				// - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeSpanned: true`),
 				// - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeSpanned: true`),
 				// - or a cell from the same row which spans over this column (cell "a").
 				// - or a cell from the same row which spans over this column (cell "a").
 
 
-				if ( column !== insertAt ) {
+				const rowspan = parseInt( cell.getAttribute( 'rowspan' ) || 1 );
+				const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
+
+				if ( cell.index !== insertAt && colspan > 1 ) {
 					// If column is different than `insertAt`, it is a cell that spans over an inserted column (cell "a" & "i").
 					// If column is different than `insertAt`, it is a cell that spans over an inserted column (cell "a" & "i").
 					// For such cells expand them by a number of columns inserted.
 					// For such cells expand them by a number of columns inserted.
 					writer.setAttribute( 'colspan', colspan + columnsToInsert, cell );
 					writer.setAttribute( 'colspan', colspan + columnsToInsert, cell );

+ 80 - 80
packages/ckeditor5-table/src/tablewalker.js

@@ -15,7 +15,6 @@ export default class TableWalker {
 	/**
 	/**
 	 * Creates an instance of the table walker.
 	 * Creates an instance of the table walker.
 	 *
 	 *
-	 *
 	 * The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
 	 * The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
 	 * It walks row by row and column by column in order to output values defined in the constructor.
 	 * It walks row by row and column by column in order to output values defined in the constructor.
 	 * By default it will output only those locations that are occupied by a cell. To include also spanned rows and columns,
 	 * By default it will output only those locations that are occupied by a cell. To include also spanned rows and columns,
@@ -23,6 +22,8 @@ export default class TableWalker {
 	 *
 	 *
 	 * The most important values of the iterator are column and row indexes of a cell.
 	 * The most important values of the iterator are column and row indexes of a cell.
 	 *
 	 *
+	 * See {@link module:table/tablewalker~TableWalkerValue} what values are returned by the table walker.
+	 *
 	 * To iterate over a given row:
 	 * To iterate over a given row:
 	 *
 	 *
 	 *		const tableWalker = new TableWalker( table, { startRow: 1, endRow: 2 } );
 	 *		const tableWalker = new TableWalker( table, { startRow: 1, endRow: 2 } );
@@ -54,8 +55,8 @@ export default class TableWalker {
 	 *
 	 *
 	 *		const tableWalker = new TableWalker( table, { startRow: 1, endRow: 1, includeSpanned: true } );
 	 *		const tableWalker = new TableWalker( table, { startRow: 1, endRow: 1, includeSpanned: true } );
 	 *
 	 *
-	 *		for ( const cellInfo of tableWalker ) {
-	 *			console.log( 'Cell at ' + cellInfo.row + ' x ' + cellInfo.column + ' : ' + ( cellInfo.cell ? 'has data' : 'is spanned' ) );
+	 *		for ( const value of tableWalker ) {
+	 *			console.log( 'Cell at ' + value.row + ' x ' + value.column + ' : ' + ( value.isSpanned ? 'is spanned' : 'has data' ) );
 	 *		}
 	 *		}
 	 *
 	 *
 	 * will log in the console for the table from previous example:
 	 * will log in the console for the table from previous example:
@@ -73,7 +74,7 @@ export default class TableWalker {
 	 * @param {Number} [options.column] A column index for which this iterator will output cells.
 	 * @param {Number} [options.column] A column index for which this iterator will output cells.
 	 * @param {Number} [options.startRow=0] A row index for which this iterator should start.
 	 * @param {Number} [options.startRow=0] A row index for which this iterator should start.
 	 * @param {Number} [options.endRow] A row index for which this iterator should end.
 	 * @param {Number} [options.endRow] A row index for which this iterator should end.
-	 * @param {Boolean} [options.includeSpanned] Also return values for spanned cells.
+	 * @param {Boolean} [options.includeSpanned=false] Also return values for spanned cells.
 	 */
 	 */
 	constructor( table, options = {} ) {
 	constructor( table, options = {} ) {
 		/**
 		/**
@@ -151,16 +152,18 @@ export default class TableWalker {
 		 * @member {Number}
 		 * @member {Number}
 		 * @private
 		 * @private
 		 */
 		 */
-		this._cell = 0;
+		this._cellIndex = 0;
 
 
 		/**
 		/**
 		 * Holds a map of spanned cells in a table.
 		 * Holds a map of spanned cells in a table.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {Map<Number, Map.<Number, Number>>}
+		 * @member {Map<Number, Map.<Number, module:engine/model/element~Element>>}
 		 * @private
 		 * @private
 		 */
 		 */
 		this._spannedCells = new Map();
 		this._spannedCells = new Map();
+
+		this._nextCellAtColumn = -1;
 	}
 	}
 
 
 	/**
 	/**
@@ -180,62 +183,53 @@ export default class TableWalker {
 	next() {
 	next() {
 		const row = this.table.getChild( this._row );
 		const row = this.table.getChild( this._row );
 
 
-		// Iterator is done when no row (table end) or the row is after #endRow.
+		// Iterator is done when there's no row (table ended) or the row is after `endRow` limit.
 		if ( !row || this._isOverEndRow() ) {
 		if ( !row || this._isOverEndRow() ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
-		// Spanned cell location handling.
-		if ( this._isSpanned( this._row, this._column ) ) {
-			// Current column must be kept as it will be updated before returning current value.
-			const currentColumn = this._column;
-			const outValue = this._formatOutValue( undefined, currentColumn );
+		let cell, skipCurrentValue, outValue;
 
 
-			// Advance to next column - always.
-			this._column++;
+		if ( this._isSpanned( this._row, this._column ) ) {
+			cell = this._getSpanned( this._row, this._column );
 
 
-			const skipCurrentValue = !this.includeSpanned || this._shouldSkipRow() || this._shouldSkipColumn( currentColumn, 1 );
+			skipCurrentValue = !this.includeSpanned || this._shouldSkipRow() || this._shouldSkipColumn();
+			outValue = this._formatOutValue( cell, this._column, true );
+		} else {
+			cell = row.getChild( this._cellIndex );
 
 
-			// The current value will be returned only if #includedSpanned=true and also current row and column are not skipped.
-			return skipCurrentValue ? this.next() : outValue;
-		}
+			if ( !cell ) {
+				// If there are no more cells left in row advance to the next row.
+				this._row++;
+				this._column = 0;
+				this._cellIndex = 0;
+				this._nextCellAtColumn = -1;
 
 
-		// The cell location is not spanned by other cells.
-		const cell = row.getChild( this._cell );
+				return this.next();
+			}
 
 
-		if ( !cell ) {
-			// If there are no more cells left in row advance to next row.
-			this._row++;
-			// And reset column & cell indexes.
-			this._column = 0;
-			this._cell = 0;
+			const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
+			const rowspan = parseInt( cell.getAttribute( 'rowspan' ) || 1 );
 
 
-			// Return next value.
-			return this.next();
-		}
+			// Record this cell spans if it's not 1x1 cell.
+			if ( colspan > 1 || rowspan > 1 ) {
+				this._recordSpans( this._row, this._column, rowspan, colspan, cell );
+			}
 
 
-		// Read table cell attributes.
-		const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
-		const rowspan = parseInt( cell.getAttribute( 'rowspan' ) || 1 );
+			this._nextCellAtColumn = this._column + colspan;
 
 
-		// Record this cell spans if it's not 1x1 cell.
-		if ( colspan > 1 || rowspan > 1 ) {
-			this._recordSpans( this._row, this._column, rowspan, colspan );
+			skipCurrentValue = this._shouldSkipRow() || this._shouldSkipColumn();
+			outValue = this._formatOutValue( cell, this._column, false, rowspan, colspan );
 		}
 		}
 
 
-		// Current column must be kept as it will be updated before returning current value.
-		const currentColumn = this._column;
-		const outValue = this._formatOutValue( cell, currentColumn, rowspan, colspan );
-
-		// Advance to next column before returning value.
+		// Advance to the next column before returning value.
 		this._column++;
 		this._column++;
 
 
-		// Advance to next cell in a parent row before returning value.
-		this._cell++;
-
-		const skipCurrentValue = this._shouldSkipRow() || this._shouldSkipColumn( currentColumn, colspan );
+		if ( this._column == this._nextCellAtColumn ) {
+			this._cellIndex++;
+		}
 
 
-		// The current value will be returned only if current row & column are not skipped.
+		// The current value will be returned only if current row and column are not skipped.
 		return skipCurrentValue ? this.next() : outValue;
 		return skipCurrentValue ? this.next() : outValue;
 	}
 	}
 
 
@@ -252,8 +246,8 @@ export default class TableWalker {
 	/**
 	/**
 	 * Checks if the current row is over {@link #endRow}.
 	 * Checks if the current row is over {@link #endRow}.
 	 *
 	 *
-	 * @returns {Boolean}
 	 * @private
 	 * @private
+	 * @returns {Boolean}
 	 */
 	 */
 	_isOverEndRow() {
 	_isOverEndRow() {
 		// If {@link #endRow) is defined skip all rows above it.
 		// If {@link #endRow) is defined skip all rows above it.
@@ -263,24 +257,25 @@ export default class TableWalker {
 	/**
 	/**
 	 * A common method for formatting the iterator's output value.
 	 * A common method for formatting the iterator's output value.
 	 *
 	 *
-	 * @param {module:engine/model/element~Element|undefined} cell The table cell to output. It might be undefined for spanned cell
-	 * locations.
-	 * @param {Number} column Column index (use the cached value)
+	 * @private
+	 * @param {module:engine/model/element~Element} cell The table cell to output.
+	 * @param {Number} column Column index (use the cached value).
+	 * @param {Boolean} isSpanned Whether the value is returned for a spanned cell location or actual cell.
 	 * @param {Number} rowspan Rowspan of the current cell.
 	 * @param {Number} rowspan Rowspan of the current cell.
 	 * @param {Number} colspan Colspan of the current cell.
 	 * @param {Number} colspan Colspan of the current cell.
 	 * @returns {{done: boolean, value: {cell: *, row: Number, column: *, rowspan: *, colspan: *, cellIndex: Number}}}
 	 * @returns {{done: boolean, value: {cell: *, row: Number, column: *, rowspan: *, colspan: *, cellIndex: Number}}}
-	 * @private
 	 */
 	 */
-	_formatOutValue( cell, column, rowspan = 1, colspan = 1 ) {
+	_formatOutValue( cell, column, isSpanned, rowspan = 1, colspan = 1 ) {
 		return {
 		return {
 			done: false,
 			done: false,
 			value: {
 			value: {
 				cell,
 				cell,
 				row: this._row,
 				row: this._row,
 				column,
 				column,
+				isSpanned,
 				rowspan,
 				rowspan,
 				colspan,
 				colspan,
-				cellIndex: this._cell
+				cellIndex: this._cellIndex
 			}
 			}
 		};
 		};
 	}
 	}
@@ -288,8 +283,8 @@ export default class TableWalker {
 	/**
 	/**
 	 * Checks if the current row should be skipped.
 	 * Checks if the current row should be skipped.
 	 *
 	 *
-	 * @returns {Boolean}
 	 * @private
 	 * @private
+	 * @returns {Boolean}
 	 */
 	 */
 	_shouldSkipRow() {
 	_shouldSkipRow() {
 		const rowIsBelowStartRow = this._row < this.startRow;
 		const rowIsBelowStartRow = this._row < this.startRow;
@@ -301,33 +296,25 @@ export default class TableWalker {
 	/**
 	/**
 	 * Checks if the current column should be skipped.
 	 * Checks if the current column should be skipped.
 	 *
 	 *
-	 * @param {Number} column
-	 * @param {Number} colspan
-	 * @returns {Boolean}
 	 * @private
 	 * @private
+	 * @returns {Boolean}
 	 */
 	 */
-	_shouldSkipColumn( column, colspan ) {
+	_shouldSkipColumn() {
 		if ( this.column === undefined ) {
 		if ( this.column === undefined ) {
 			// The {@link #column} is not defined so output all columns.
 			// The {@link #column} is not defined so output all columns.
 			return false;
 			return false;
 		}
 		}
 
 
-		// When outputting cells from given column we skip:
-		// - Cells that are not on that column.
-		const isCurrentColumn = column === this.column;
-		// - CSells that are before given column and they overlaps given column.
-		const isPreviousThatOverlapsColumn = column < this.column && column + colspan > this.column;
-
-		return !isCurrentColumn && !isPreviousThatOverlapsColumn;
+		return this.column != this._column;
 	}
 	}
 
 
 	/**
 	/**
 	 * Checks if the current cell location (row x column) is spanned by another cell.
 	 * Checks if the current cell location (row x column) is spanned by another cell.
 	 *
 	 *
+	 * @private
 	 * @param {Number} row Row index of a cell location to check.
 	 * @param {Number} row Row index of a cell location to check.
 	 * @param {Number} column Column index of a cell location to check.
 	 * @param {Number} column Column index of a cell location to check.
 	 * @returns {Boolean}
 	 * @returns {Boolean}
-	 * @private
 	 */
 	 */
 	_isSpanned( row, column ) {
 	_isSpanned( row, column ) {
 		if ( !this._spannedCells.has( row ) ) {
 		if ( !this._spannedCells.has( row ) ) {
@@ -341,25 +328,38 @@ export default class TableWalker {
 		return rowSpans.has( column );
 		return rowSpans.has( column );
 	}
 	}
 
 
+	/**
+	 * Returns the cell element that is spanned over `row` x `column` location.
+	 *
+	 * @private
+	 * @param {Number} row Row index of the cell location.
+	 * @param {Number} column Column index of the cell location.
+	 * @returns {module:engine/model/element~Element}
+	 */
+	_getSpanned( row, column ) {
+		return this._spannedCells.get( row ).get( column );
+	}
+
 	/**
 	/**
 	 * Updates spanned cells map relative to the current cell location and its span dimensions.
 	 * Updates spanned cells map relative to the current cell location and its span dimensions.
 	 *
 	 *
+	 * @private
 	 * @param {Number} row Row index of a cell.
 	 * @param {Number} row Row index of a cell.
 	 * @param {Number} column Column index of a cell.
 	 * @param {Number} column Column index of a cell.
 	 * @param {Number} rowspan Cell height.
 	 * @param {Number} rowspan Cell height.
 	 * @param {Number} colspan Cell width.
 	 * @param {Number} colspan Cell width.
-	 * @private
+	 * @param {module:engine/model/element~Element} cell Cell that is spanned.
 	 */
 	 */
-	_recordSpans( row, column, rowspan, colspan ) {
+	_recordSpans( row, column, rowspan, colspan, cell ) {
 		// This will update all cell locations after current column - ie a cell has colspan set.
 		// This will update all cell locations after current column - ie a cell has colspan set.
 		for ( let columnToUpdate = column + 1; columnToUpdate <= column + colspan - 1; columnToUpdate++ ) {
 		for ( let columnToUpdate = column + 1; columnToUpdate <= column + colspan - 1; columnToUpdate++ ) {
-			this._markSpannedCell( row, columnToUpdate );
+			this._markSpannedCell( row, columnToUpdate, cell );
 		}
 		}
 
 
 		// This will update all rows below current up to row's height.
 		// This will update all rows below current up to row's height.
 		for ( let rowToUpdate = row + 1; rowToUpdate < row + rowspan; rowToUpdate++ ) {
 		for ( let rowToUpdate = row + 1; rowToUpdate < row + rowspan; rowToUpdate++ ) {
 			for ( let columnToUpdate = column; columnToUpdate <= column + colspan - 1; columnToUpdate++ ) {
 			for ( let columnToUpdate = column; columnToUpdate <= column + colspan - 1; columnToUpdate++ ) {
-				this._markSpannedCell( rowToUpdate, columnToUpdate );
+				this._markSpannedCell( rowToUpdate, columnToUpdate, cell );
 			}
 			}
 		}
 		}
 	}
 	}
@@ -367,18 +367,19 @@ export default class TableWalker {
 	/**
 	/**
 	 * Marks the cell location as spanned by another cell.
 	 * Marks the cell location as spanned by another cell.
 	 *
 	 *
+	 * @private
 	 * @param {Number} row Row index of the cell location.
 	 * @param {Number} row Row index of the cell location.
 	 * @param {Number} column Column index of the cell location.
 	 * @param {Number} column Column index of the cell location.
-	 * @private
+	 * @param {module:engine/model/element~Element} cell Cell that is spanned.
 	 */
 	 */
-	_markSpannedCell( row, column ) {
+	_markSpannedCell( row, column, cell ) {
 		if ( !this._spannedCells.has( row ) ) {
 		if ( !this._spannedCells.has( row ) ) {
 			this._spannedCells.set( row, new Map() );
 			this._spannedCells.set( row, new Map() );
 		}
 		}
 
 
 		const rowSpans = this._spannedCells.get( row );
 		const rowSpans = this._spannedCells.get( row );
 
 
-		rowSpans.set( column, true );
+		rowSpans.set( column, cell );
 	}
 	}
 }
 }
 
 
@@ -386,14 +387,13 @@ export default class TableWalker {
  * An object returned by {@link module:table/tablewalker~TableWalker} when traversing table cells.
  * An object returned by {@link module:table/tablewalker~TableWalker} when traversing table cells.
  *
  *
  * @typedef {Object} module:table/tablewalker~TableWalkerValue
  * @typedef {Object} module:table/tablewalker~TableWalkerValue
- * @property {module:engine/model/element~Element} [cell] The current table cell. Might be empty if
- * {@link module:table/tablewalker~TableWalker#includeSpanned} is set to `true`.
+ * @property {module:engine/model/element~Element} cell The current table cell.
  * @property {Number} row The row index of a cell.
  * @property {Number} row The row index of a cell.
  * @property {Number} column The column index of a cell. Column index is adjusted to widths and heights of previous cells.
  * @property {Number} column The column index of a cell. Column index is adjusted to widths and heights of previous cells.
- * @property {Number} [colspan] The `colspan` attribute of a cell. It is always defined even if the model attribute is not present. Not
- * set if {@link module:table/tablewalker~TableWalker#includeSpanned} is set to `true`.
- * @property {Number} [rowspan] The `rowspan` attribute of a cell. It is always defined even if the model attribute is not present. Not
- * set if {@link module:table/tablewalker~TableWalker#includeSpanned} is set to `true`.
- * @property {Number} cellIndex The index of the current cell in a parent row. When using the `includeSpanned` option it will indicate the
- * next child index if #cell is empty (which indicates that the cell is spanned by another cell).
+ * @param {Boolean} isSpanned Whether the value is returned for a spanned cell location or actual cell.
+ * @property {Number} colspan The `colspan` attribute of a cell. It the model attribute is not present, it is set to `1`. For spanned
+ * table locations, it is set to `1`.
+ * @property {Number} rowspan The `rowspan` attribute of a cell. It the model attribute is not present, it is set to `1`. For spanned
+ * table locations, it is set to `1`.
+ * @property {Number} cellIndex The index of the current cell in a parent row.
  */
  */

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

@@ -59,7 +59,7 @@ export default class InsertTableView extends View {
 		 * @member {String} #label
 		 * @member {String} #label
 		 */
 		 */
 		this.bind( 'label' )
 		this.bind( 'label' )
-			.to( this, 'columns', this, 'rows', ( columns, rows ) => `${ rows } x ${ columns }` );
+			.to( this, 'columns', this, 'rows', ( columns, rows ) => `${ rows } × ${ columns }` );
 
 
 		this.setTemplate( {
 		this.setTemplate( {
 			tag: 'div',
 			tag: 'div',

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

@@ -23,7 +23,7 @@ import { findAncestor } from './commands/utils';
 export function toTableWidget( viewElement, writer ) {
 export function toTableWidget( viewElement, writer ) {
 	writer.setCustomProperty( 'table', true, viewElement );
 	writer.setCustomProperty( 'table', true, viewElement );
 
 
-	return toWidget( viewElement, writer, { hasSelectionHandler: true } );
+	return toWidget( viewElement, writer, { hasSelectionHandle: true } );
 }
 }
 
 
 /**
 /**

+ 2 - 2
packages/ckeditor5-table/tests/_utils/utils.js

@@ -107,8 +107,8 @@ export function viewTable( tableData, attributes = {} ) {
 		} ) }</tbody>` : '';
 		} ) }</tbody>` : '';
 
 
 	const figureAttributes = asWidget ?
 	const figureAttributes = asWidget ?
-		'class="ck-widget ck-widget_with-selection-handler table" contenteditable="false"' : 'class="table"';
-	const widgetHandler = '<div class="ck ck-widget__selection-handler"></div>';
+		'class="ck-widget ck-widget_with-selection-handle table" contenteditable="false"' : 'class="table"';
+	const widgetHandler = '<div class="ck ck-widget__selection-handle"></div>';
 
 
 	return `<figure ${ figureAttributes }>${ asWidget ? widgetHandler : '' }<table>${ thead }${ tbody }</table></figure>`;
 	return `<figure ${ figureAttributes }>${ asWidget ? widgetHandler : '' }<table>${ thead }${ tbody }</table></figure>`;
 }
 }

+ 24 - 0
packages/ckeditor5-table/tests/commands/setheadercolumncommand.js

@@ -115,5 +115,29 @@ describe( 'SetHeaderColumnCommand', () => {
 				[ '00', '01[]', '02', '03' ]
 				[ '00', '01[]', '02', '03' ]
 			], { headingColumns: 2 } ) );
 			], { headingColumns: 2 } ) );
 		} );
 		} );
+
+		it( 'should respect forceValue parameter #1', () => {
+			setData( model, modelTable( [
+				[ '00', '01[]', '02', '03' ]
+			], { headingColumns: 3 } ) );
+
+			command.execute( { forceValue: true } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '00', '01[]', '02', '03' ]
+			], { headingColumns: 3 } ) );
+		} );
+
+		it( 'should respect forceValue parameter #2', () => {
+			setData( model, modelTable( [
+				[ '00', '01[]', '02', '03' ]
+			], { headingColumns: 1 } ) );
+
+			command.execute( { forceValue: false } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '00', '01[]', '02', '03' ]
+			], { headingColumns: 1 } ) );
+		} );
 	} );
 	} );
 } );
 } );

+ 36 - 0
packages/ckeditor5-table/tests/commands/setheaderrowcommand.js

@@ -163,6 +163,42 @@ describe( 'SetHeaderRowCommand', () => {
 			] ) );
 			] ) );
 		} );
 		} );
 
 
+		it( 'should respect forceValue parameter #1', () => {
+			setData( model, modelTable( [
+				[ '00' ],
+				[ '[]10' ],
+				[ '20' ],
+				[ '30' ]
+			], { headingRows: 3 } ) );
+
+			command.execute( { forceValue: true } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '00' ],
+				[ '[]10' ],
+				[ '20' ],
+				[ '30' ]
+			], { headingRows: 3 } ) );
+		} );
+
+		it( 'should respect forceValue parameter #2', () => {
+			setData( model, modelTable( [
+				[ '00' ],
+				[ '[]10' ],
+				[ '20' ],
+				[ '30' ]
+			], { headingRows: 1 } ) );
+
+			command.execute( { forceValue: false } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '00' ],
+				[ '[]10' ],
+				[ '20' ],
+				[ '30' ]
+			], { headingRows: 1 } ) );
+		} );
+
 		it( 'should fix rowspaned cells on the edge of an table head section', () => {
 		it( 'should fix rowspaned cells on the edge of an table head section', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ '00', '01', '02' ],
 				[ '00', '01', '02' ],

+ 10 - 10
packages/ckeditor5-table/tests/converters/downcast.js

@@ -333,8 +333,8 @@ describe( 'downcast converters', () => {
 				setModelData( model, modelTable( [ [ '' ] ] ) );
 				setModelData( model, modelTable( [ [ '' ] ] ) );
 
 
 				expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
 				expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
-					'<figure class="ck-widget ck-widget_with-selection-handler table" contenteditable="false">' +
-					'<div class="ck ck-widget__selection-handler"></div>' +
+					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
+					'<div class="ck ck-widget__selection-handle"></div>' +
 						'<table>' +
 						'<table>' +
 							'<tbody>' +
 							'<tbody>' +
 								'<tr>' +
 								'<tr>' +
@@ -566,8 +566,8 @@ describe( 'downcast converters', () => {
 
 
 				expect( formatTable(
 				expect( formatTable(
 					getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
 					getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
-					'<figure class="ck-widget ck-widget_with-selection-handler table" contenteditable="false">' +
-						'<div class="ck ck-widget__selection-handler"></div>' +
+					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
+						'<div class="ck ck-widget__selection-handle"></div>' +
 						'<table>' +
 						'<table>' +
 							'<tbody>' +
 							'<tbody>' +
 								'<tr>' +
 								'<tr>' +
@@ -712,8 +712,8 @@ describe( 'downcast converters', () => {
 				} );
 				} );
 
 
 				expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
 				expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
-					'<figure class="ck-widget ck-widget_with-selection-handler table" contenteditable="false">' +
-					'<div class="ck ck-widget__selection-handler"></div>' +
+					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
+					'<div class="ck ck-widget__selection-handle"></div>' +
 						'<table>' +
 						'<table>' +
 							'<tbody>' +
 							'<tbody>' +
 								'<tr>' +
 								'<tr>' +
@@ -889,8 +889,8 @@ describe( 'downcast converters', () => {
 				} );
 				} );
 
 
 				expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
 				expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
-					'<figure class="ck-widget ck-widget_with-selection-handler table" contenteditable="false">' +
-					'<div class="ck ck-widget__selection-handler"></div>' +
+					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
+					'<div class="ck ck-widget__selection-handle"></div>' +
 						'<table>' +
 						'<table>' +
 							'<thead>' +
 							'<thead>' +
 								'<tr>' +
 								'<tr>' +
@@ -1103,8 +1103,8 @@ describe( 'downcast converters', () => {
 				} );
 				} );
 
 
 				expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
 				expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable(
-					'<figure class="ck-widget ck-widget_with-selection-handler table" contenteditable="false">' +
-					'<div class="ck ck-widget__selection-handler"></div>' +
+					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
+					'<div class="ck ck-widget__selection-handle"></div>' +
 						'<table>' +
 						'<table>' +
 							'<tbody>' +
 							'<tbody>' +
 								'<tr>' +
 								'<tr>' +

+ 1 - 1
packages/ckeditor5-table/tests/converters/table-cell-content-post-fixer.js → packages/ckeditor5-table/tests/converters/table-cell-paragraph-post-fixer.js

@@ -11,7 +11,7 @@ import TableEditing from '../../src/tableediting';
 import { formatTable } from './../_utils/utils';
 import { formatTable } from './../_utils/utils';
 import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 
 
-describe( 'Table cell content post-fixer', () => {
+describe( 'Table cell paragraph post-fixer', () => {
 	let editor, model, root;
 	let editor, model, root;
 
 
 	beforeEach( () => {
 	beforeEach( () => {

+ 55 - 33
packages/ckeditor5-table/tests/converters/tablecell-post-fixer.js → packages/ckeditor5-table/tests/converters/table-cell-refresh-post-fixer.js

@@ -8,7 +8,7 @@
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 
 import { defaultConversion, defaultSchema, formatTable, formattedViewTable, viewTable } from '../_utils/utils';
 import { defaultConversion, defaultSchema, formatTable, formattedViewTable, viewTable } from '../_utils/utils';
-import injectTableCellPostFixer from '../../src/converters/tablecell-post-fixer';
+import injectTableCellRefreshPostFixer from '../../src/converters/table-cell-refresh-post-fixer';
 
 
 import env from '@ckeditor/ckeditor5-utils/src/env';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
@@ -16,8 +16,8 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictest
 
 
 import Delete from '@ckeditor/ckeditor5-typing/src/delete';
 import Delete from '@ckeditor/ckeditor5-typing/src/delete';
 
 
-describe( 'TableCell post-fixer', () => {
-	let editor, model, doc, root, view;
+describe( 'Table cell refresh post-fixer', () => {
+	let editor, model, doc, root, view, refreshItemSpy;
 
 
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
@@ -44,10 +44,12 @@ describe( 'TableCell post-fixer', () => {
 				} );
 				} );
 				editor.conversion.elementToElement( { model: 'block', view: 'div' } );
 				editor.conversion.elementToElement( { model: 'block', view: 'div' } );
 
 
-				model.schema.extend( '$block', { allowAttributes: 'foo' } );
+				model.schema.extend( '$block', { allowAttributes: [ 'foo', 'bar' ] } );
 				editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } );
 				editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } );
+				editor.conversion.attributeToAttribute( { model: 'bar', view: 'bar' } );
 
 
-				injectTableCellPostFixer( model, editor.editing );
+				injectTableCellRefreshPostFixer( model );
+				refreshItemSpy = sinon.spy( model.document.differ, 'refreshItem' );
 			} );
 			} );
 	} );
 	} );
 
 
@@ -69,6 +71,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p>00</p><p></p>' ]
 			[ '<p>00</p><p></p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should rename <span> to <p> on adding other block element to the same table cell', () => {
 	it( 'should rename <span> to <p> on adding other block element to the same table cell', () => {
@@ -89,6 +92,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p>00</p><div></div>' ]
 			[ '<p>00</p><div></div>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should properly rename the same element on consecutive changes', () => {
 	it( 'should properly rename the same element on consecutive changes', () => {
@@ -107,6 +111,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p>00</p><p></p>' ]
 			[ '<p>00</p><p></p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 
 
 		model.change( writer => {
 		model.change( writer => {
 			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
 			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
@@ -115,6 +120,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '00' ]
 			[ '00' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.calledTwice( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should rename <span> to <p> when setting attribute on <paragraph>', () => {
 	it( 'should rename <span> to <p> when setting attribute on <paragraph>', () => {
@@ -129,6 +135,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p foo="bar">00</p>' ]
 			[ '<p foo="bar">00</p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should rename <p> to <span> when removing all but one paragraph inside table cell', () => {
 	it( 'should rename <p> to <span> when removing all but one paragraph inside table cell', () => {
@@ -143,6 +150,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '00' ]
 			[ '00' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should rename <p> to <span> when removing attribute from <paragraph>', () => {
 	it( 'should rename <p> to <span> when removing attribute from <paragraph>', () => {
@@ -157,6 +165,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<span>00</span>' ]
 			[ '<span>00</span>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should keep <p> in the view when <paragraph> attribute value is changed', () => {
 	it( 'should keep <p> in the view when <paragraph> attribute value is changed', () => {
@@ -171,6 +180,42 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p foo="baz">00</p>' ]
 			[ '<p foo="baz">00</p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		// False positive: should not be called.
+		sinon.assert.calledOnce( refreshItemSpy );
+	} );
+
+	it( 'should keep <p> in the view when adding another attribute to a <paragraph> with other attributes', () => {
+		editor.setData( viewTable( [ [ '<p foo="bar">00</p>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.setAttribute( 'bar', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) );
+		} );
+
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<p bar="bar" foo="bar">00</p>' ]
+		], { asWidget: true } ) );
+
+		// False positive
+		sinon.assert.notCalled( refreshItemSpy );
+	} );
+
+	it( 'should keep <p> in the view when adding another attribute to a <paragraph> and removing attribute that is already set', () => {
+		editor.setData( viewTable( [ [ '<p foo="bar">00</p>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.setAttribute( 'bar', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) );
+			writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) );
+		} );
+
+		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<p bar="bar">00</p>' ]
+		], { asWidget: true } ) );
+		// False positive: should not be called.
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should keep <p> in the view when <paragraph> attribute value is changed (table cell with multiple blocks)', () => {
 	it( 'should keep <p> in the view when <paragraph> attribute value is changed (table cell with multiple blocks)', () => {
@@ -185,6 +230,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p foo="baz">00</p><p>00</p>' ]
 			[ '<p foo="baz">00</p><p>00</p>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.notCalled( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should do nothing on rename <paragraph> to other block', () => {
 	it( 'should do nothing on rename <paragraph> to other block', () => {
@@ -199,6 +245,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<div>00</div>' ]
 			[ '<div>00</div>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.notCalled( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should do nothing when setting attribute on block item other then <paragraph>', () => {
 	it( 'should do nothing when setting attribute on block item other then <paragraph>', () => {
@@ -213,36 +260,10 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<div foo="bar">foo</div>' ]
 			[ '<div foo="bar">foo</div>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.notCalled( refreshItemSpy );
 	} );
 	} );
 
 
-	it( 'should not crash when view.change() block was called in model.change()', () => {
-		editor.setData( viewTable( [ [ '<p>foobar</p>' ] ] ) );
-
-		const table = root.getChild( 0 );
-
-		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) )
-			.to.equal( formattedViewTable( [ [ 'foobar' ] ], { asWidget: true } ) );
-
-		expect( () => {
-			model.change( writer => {
-				const tableCell = table.getNodeByPath( [ 0, 0 ] );
-
-				writer.insertElement( 'paragraph', null, writer.createPositionAt( tableCell, 'end' ) );
-				writer.setSelection( writer.createRangeIn( tableCell ) );
-
-				// Do some change in the view while inside model change.
-				editor.editing.view.change( writer => {
-					writer.addClass( 'foo', editor.editing.mapper.toViewElement( tableCell ) );
-				} );
-			} );
-		} ).to.not.throw();
-
-		expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
-			[ { class: 'foo', contents: '<p>{foobar</p><p>]</p>' } ]
-		], { asWidget: true } ) );
-	} );
-
-	it( 'should keep <p> in the view when <paragraph> attribute value is changed (table cell with multiple blocks)', () => {
+	it( 'should rename <p> in to <span> when removing <paragraph> (table cell with 2 paragraphs)', () => {
 		editor.setData( viewTable( [ [ '<p>00</p><p>00</p>' ] ] ) );
 		editor.setData( viewTable( [ [ '<p>00</p><p>00</p>' ] ] ) );
 
 
 		const table = root.getChild( 0 );
 		const table = root.getChild( 0 );
@@ -254,6 +275,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<span>00</span>' ]
 			[ '<span>00</span>' ]
 		], { asWidget: true } ) );
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 	} );
 
 
 	it( 'should update view selection after deleting content', () => {
 	it( 'should update view selection after deleting content', () => {

+ 30 - 0
packages/ckeditor5-table/tests/manual/rtl.html

@@ -0,0 +1,30 @@
+<style>
+	body {
+		font-family: Helvetica, Arial, sans-serif;
+		font-size: 14px;
+	}
+</style>
+
+<div id="editor">
+	<figure class="table">
+		<table>
+			<tbody>
+				<tr>
+					<td>1</td>
+					<td>2</td>
+					<td>3</td>
+				</tr>
+				<tr>
+					<td>4</td>
+					<td>5</td>
+					<td>6</td>
+				</tr>
+				<tr>
+					<td>7</td>
+					<td>8</td>
+					<td>9</td>
+				</tr>
+			</tbody>
+		</table>
+	</figure>
+</div>

+ 31 - 0
packages/ckeditor5-table/tests/manual/rtl.js

@@ -0,0 +1,31 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet ],
+		language: {
+			ui: 'en',
+			content: 'ar'
+		},
+		toolbar: [
+			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
+		],
+		table: {
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ],
+			tableToolbar: [ 'bold', 'italic' ]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

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

@@ -0,0 +1,21 @@
+# Tables and RTL (right–to–left) content
+
+The editor has English UI and Arabic content. In Arabic, the table is mirrored so some features of the editor must work differently to provide the right UX.
+
+## Adding columns
+
+1. Focus the "5" cell.
+2. Using the toolbar menu, add column to the right.
+3. The column should visually appear on the right–hand side of the "5" cell.
+4. Focus the "5" cell.
+5. Using the toolbar menu, add column to the left.
+3. The column should visually appear on the left–hand side of the "5" cell.
+
+## Merging cells
+
+1. Focus the "5" cell.
+2. Using the toolbar menu, merge cell to the right.
+3. "5" and "4" cells should be merged.
+4. Focus the "5" cell.
+5. Using the toolbar menu, merge cell to the left.
+6. "5" and "6" cells should be merged.

+ 4 - 0
packages/ckeditor5-table/tests/tableediting.js

@@ -42,6 +42,10 @@ describe( 'TableEditing', () => {
 		editor.destroy();
 		editor.destroy();
 	} );
 	} );
 
 
+	it( 'should have pluginName', () => {
+		expect( TableEditing.pluginName ).to.equal( 'TableEditing' );
+	} );
+
 	it( 'should set proper schema rules', () => {
 	it( 'should set proper schema rules', () => {
 		// Table:
 		// Table:
 		expect( model.schema.isRegistered( 'table' ) ).to.be.true;
 		expect( model.schema.isRegistered( 'table' ) ).to.be.true;

+ 8 - 0
packages/ckeditor5-table/tests/tabletoolbar.js

@@ -94,6 +94,14 @@ describe( 'TableToolbar', () => {
 					balloonClassName: 'ck-toolbar-container'
 					balloonClassName: 'ck-toolbar-container'
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should set aria-label attribute', () => {
+				toolbar.render();
+
+				expect( toolbar.element.getAttribute( 'aria-label' ) ).to.equal( 'Table toolbar' );
+
+				toolbar.destroy();
+			} );
 		} );
 		} );
 
 
 		describe( 'integration with the editor focus', () => {
 		describe( 'integration with the editor focus', () => {

+ 60 - 2
packages/ckeditor5-table/tests/tableui.js

@@ -229,7 +229,7 @@ describe( 'TableUI', () => {
 			expect( labels ).to.deep.equal( [ 'Header column', '|', 'Insert column left', 'Insert column right', 'Delete column' ] );
 			expect( labels ).to.deep.equal( [ 'Header column', '|', 'Insert column left', 'Insert column right', 'Delete column' ] );
 		} );
 		} );
 
 
-		it( 'should bind items in panel to proper commands', () => {
+		it( 'should bind items in panel to proper commands (LTR content)', () => {
 			const items = dropdown.listView.items;
 			const items = dropdown.listView.items;
 
 
 			const setColumnHeaderCommand = editor.commands.get( 'setTableColumnHeader' );
 			const setColumnHeaderCommand = editor.commands.get( 'setTableColumnHeader' );
@@ -266,6 +266,35 @@ describe( 'TableUI', () => {
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 		} );
 		} );
 
 
+		it( 'should bind items in panel to proper commands (RTL content)', () => {
+			const element = document.createElement( 'div' );
+
+			document.body.appendChild( element );
+
+			return ClassicTestEditor
+				.create( element, {
+					language: {
+						ui: 'en',
+						content: 'ar'
+					},
+					plugins: [ TableEditing, TableUI ]
+				} )
+				.then( editor => {
+					const dropdown = editor.ui.componentFactory.create( 'tableColumn' );
+					const items = dropdown.listView.items;
+
+					expect( items.get( 2 ).children.first.label ).to.equal( 'Insert column left' );
+					expect( items.get( 2 ).children.first.commandName ).to.equal( 'insertTableColumnRight' );
+
+					expect( items.get( 3 ).children.first.label ).to.equal( 'Insert column right' );
+					expect( items.get( 3 ).children.first.commandName ).to.equal( 'insertTableColumnLeft' );
+
+					element.remove();
+
+					return editor.destroy();
+				} );
+		} );
+
 		it( 'should focus view after command execution', () => {
 		it( 'should focus view after command execution', () => {
 			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
 			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
 
 
@@ -333,7 +362,7 @@ describe( 'TableUI', () => {
 			] );
 			] );
 		} );
 		} );
 
 
-		it( 'should bind items in panel to proper commands', () => {
+		it( 'should bind items in panel to proper commands (LTR content)', () => {
 			const items = dropdown.listView.items;
 			const items = dropdown.listView.items;
 
 
 			const mergeCellsCommand = editor.commands.get( 'mergeTableCells' );
 			const mergeCellsCommand = editor.commands.get( 'mergeTableCells' );
@@ -363,6 +392,35 @@ describe( 'TableUI', () => {
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 		} );
 		} );
 
 
+		it( 'should bind items in panel to proper commands (RTL content)', () => {
+			const element = document.createElement( 'div' );
+
+			document.body.appendChild( element );
+
+			return ClassicTestEditor
+				.create( element, {
+					language: {
+						ui: 'en',
+						content: 'ar'
+					},
+					plugins: [ TableEditing, TableUI ]
+				} )
+				.then( editor => {
+					const dropdown = editor.ui.componentFactory.create( 'mergeTableCells' );
+					const items = dropdown.listView.items;
+
+					expect( items.get( 1 ).children.first.label ).to.equal( 'Merge cell right' );
+					expect( items.get( 1 ).children.first.commandName ).to.equal( 'mergeTableCellLeft' );
+
+					expect( items.get( 3 ).children.first.label ).to.equal( 'Merge cell left' );
+					expect( items.get( 3 ).children.first.commandName ).to.equal( 'mergeTableCellRight' );
+
+					element.remove();
+
+					return editor.destroy();
+				} );
+		} );
+
 		it( 'should focus view after command execution', () => {
 		it( 'should focus view after command execution', () => {
 			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
 			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
 
 

+ 44 - 23
packages/ckeditor5-table/tests/tablewalker.js

@@ -37,12 +37,20 @@ describe( 'TableWalker', () => {
 			result.push( tableInfo );
 			result.push( tableInfo );
 		}
 		}
 
 
-		const formattedResult = result.map( ( { row, column, cell, cellIndex } ) => ( {
-			row,
-			column,
-			data: cell && cell.getChild( 0 ).getChild( 0 ).data,
-			index: cellIndex
-		} ) );
+		const formattedResult = result.map( ( { row, column, isSpanned, cell, cellIndex } ) => {
+			const result = {
+				row,
+				column,
+				data: cell && cell.getChild( 0 ).getChild( 0 ).data,
+				index: cellIndex
+			};
+
+			if ( isSpanned ) {
+				result.isSpanned = true;
+			}
+
+			return result;
+		} );
 
 
 		expect( formattedResult ).to.deep.equal( expected );
 		expect( formattedResult ).to.deep.equal( expected );
 	}
 	}
@@ -135,7 +143,7 @@ describe( 'TableWalker', () => {
 			], { endRow: 2 } );
 			], { endRow: 2 } );
 		} );
 		} );
 
 
-		it( 'should iterate over given row 0 only', () => {
+		it( 'should iterate over given row only', () => {
 			testWalker( [
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
 				[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
 				[ '23' ],
 				[ '23' ],
@@ -157,11 +165,11 @@ describe( 'TableWalker', () => {
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ row: 1, column: 0, index: 0, data: '10' },
 				{ row: 1, column: 0, index: 0, data: '10' },
-				{ row: 1, column: 1, index: 1, data: undefined }
+				{ row: 1, column: 1, index: 1, data: '01', isSpanned: true }
 			], { includeSpanned: true } );
 			], { includeSpanned: true } );
 		} );
 		} );
 
 
-		it( 'should output spanned cells as empty cell', () => {
+		it( 'should output spanned cells', () => {
 			testWalker( [
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
 				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
 				[ '12' ],
 				[ '12' ],
@@ -169,17 +177,17 @@ describe( 'TableWalker', () => {
 				[ '30', { colspan: 2, contents: '31' } ]
 				[ '30', { colspan: 2, contents: '31' } ]
 			], [
 			], [
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 0, index: 0, data: '00' },
-				{ row: 0, column: 1, index: 1, data: undefined },
+				{ row: 0, column: 1, index: 0, data: '00', isSpanned: true },
 				{ row: 0, column: 2, index: 1, data: '02' },
 				{ row: 0, column: 2, index: 1, data: '02' },
-				{ row: 1, column: 0, index: 0, data: undefined },
-				{ row: 1, column: 1, index: 0, data: undefined },
+				{ row: 1, column: 0, index: 0, data: '00', isSpanned: true },
+				{ row: 1, column: 1, index: 0, data: '00', isSpanned: true },
 				{ row: 1, column: 2, index: 0, data: '12' },
 				{ row: 1, column: 2, index: 0, data: '12' },
-				{ row: 2, column: 0, index: 0, data: undefined },
-				{ row: 2, column: 1, index: 0, data: undefined },
+				{ row: 2, column: 0, index: 0, data: '00', isSpanned: true },
+				{ row: 2, column: 1, index: 0, data: '00', isSpanned: true },
 				{ row: 2, column: 2, index: 0, data: '22' },
 				{ row: 2, column: 2, index: 0, data: '22' },
 				{ row: 3, column: 0, index: 0, data: '30' },
 				{ row: 3, column: 0, index: 0, data: '30' },
 				{ row: 3, column: 1, index: 1, data: '31' },
 				{ row: 3, column: 1, index: 1, data: '31' },
-				{ row: 3, column: 2, index: 2, data: undefined }
+				{ row: 3, column: 2, index: 1, data: '31', isSpanned: true }
 			], { includeSpanned: true } );
 			], { includeSpanned: true } );
 		} );
 		} );
 
 
@@ -191,7 +199,7 @@ describe( 'TableWalker', () => {
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ row: 1, column: 0, index: 0, data: '10' },
 				{ row: 1, column: 0, index: 0, data: '10' },
-				{ row: 1, column: 1, index: 1, data: undefined }
+				{ row: 1, column: 1, index: 1, data: '01', isSpanned: true }
 			], { includeSpanned: true } );
 			], { includeSpanned: true } );
 		} );
 		} );
 
 
@@ -202,16 +210,16 @@ describe( 'TableWalker', () => {
 				[ '22' ],
 				[ '22' ],
 				[ '30', '31', '32' ]
 				[ '30', '31', '32' ]
 			], [
 			], [
-				{ row: 1, column: 0, index: 0, data: undefined },
-				{ row: 1, column: 1, index: 0, data: undefined },
+				{ row: 1, column: 0, index: 0, data: '00', isSpanned: true },
+				{ row: 1, column: 1, index: 0, data: '00', isSpanned: true },
 				{ row: 1, column: 2, index: 0, data: '12' },
 				{ row: 1, column: 2, index: 0, data: '12' },
-				{ row: 2, column: 0, index: 0, data: undefined },
-				{ row: 2, column: 1, index: 0, data: undefined },
+				{ row: 2, column: 0, index: 0, data: '00', isSpanned: true },
+				{ row: 2, column: 1, index: 0, data: '00', isSpanned: true },
 				{ row: 2, column: 2, index: 0, data: '22' }
 				{ row: 2, column: 2, index: 0, data: '22' }
 			], { includeSpanned: true, startRow: 1, endRow: 2 } );
 			], { includeSpanned: true, startRow: 1, endRow: 2 } );
 		} );
 		} );
 
 
-		it( 'should output rowspanned cells at the end of a table row', () => {
+		it( 'should output rowspanned cells at the end of a table row with startRow & endRow options', () => {
 			testWalker( [
 			testWalker( [
 				[ '00', { rowspan: 2, contents: '01' } ],
 				[ '00', { rowspan: 2, contents: '01' } ],
 				[ '10' ],
 				[ '10' ],
@@ -220,7 +228,7 @@ describe( 'TableWalker', () => {
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ row: 1, column: 0, index: 0, data: '10' },
 				{ row: 1, column: 0, index: 0, data: '10' },
-				{ row: 1, column: 1, index: 1, data: undefined }
+				{ row: 1, column: 1, index: 1, data: '01', isSpanned: true }
 			], { startRow: 0, endRow: 1, includeSpanned: true } );
 			], { startRow: 0, endRow: 1, includeSpanned: true } );
 		} );
 		} );
 	} );
 	} );
@@ -233,9 +241,22 @@ describe( 'TableWalker', () => {
 				[ '22' ],
 				[ '22' ],
 				[ '30', '31', '32' ]
 				[ '30', '31', '32' ]
 			], [
 			], [
-				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 3, column: 1, index: 1, data: '31' }
 				{ row: 3, column: 1, index: 1, data: '31' }
 			], { column: 1 } );
 			], { column: 1 } );
 		} );
 		} );
+
+		it( 'should output only cells on given column, includeSpanned = true', () => {
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 0, column: 1, index: 0, data: '00', isSpanned: true },
+				{ row: 1, column: 1, index: 0, data: '00', isSpanned: true },
+				{ row: 2, column: 1, index: 0, data: '00', isSpanned: true },
+				{ row: 3, column: 1, index: 1, data: '31' }
+			], { column: 1, includeSpanned: true } );
+		} );
 	} );
 	} );
 } );
 } );

+ 3 - 3
packages/ckeditor5-table/tests/ui/inserttableview.js

@@ -39,7 +39,7 @@ describe( 'InsertTableView', () => {
 		} );
 		} );
 
 
 		it( 'sets #label to default rows & columns', () => {
 		it( 'sets #label to default rows & columns', () => {
-			expect( view.label ).to.equal( '0 x 0' );
+			expect( view.label ).to.equal( '0 × 0' );
 		} );
 		} );
 
 
 		it( 'creates #element from template', () => {
 		it( 'creates #element from template', () => {
@@ -85,11 +85,11 @@ describe( 'InsertTableView', () => {
 			it( 'binds #label to rows & columns', () => {
 			it( 'binds #label to rows & columns', () => {
 				view.rows = 3;
 				view.rows = 3;
 
 
-				expect( view.label ).to.equal( '3 x 0' );
+				expect( view.label ).to.equal( '3 × 0' );
 
 
 				view.columns = 7;
 				view.columns = 7;
 
 
-				expect( view.label ).to.equal( '3 x 7' );
+				expect( view.label ).to.equal( '3 × 7' );
 			} );
 			} );
 
 
 			it( 'mousedown event should be prevented', () => {
 			it( 'mousedown event should be prevented', () => {