Преглед изворни кода

Merge branch 'master' into t/63

# Conflicts:
#	src/commands/mergecellcommand.js
#	src/tableui.js
#	tests/converters/downcast.js
Maciej Gołaszewski пре 6 година
родитељ
комит
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
-dist: trusty
+dist: xenial
 addons:
   chrome: stable
   firefox: latest
 language: node_js
+services:
+- xvfb
 cache:
   yarn: true
 node_js:
@@ -13,8 +15,6 @@ branches:
   - stable
 before_install:
 - export START_TIME=$( date +%s )
-- export DISPLAY=:99.0
-- sh -e /etc/init.d/xvfb start
 - npm i -g yarn
 install:
 - yarn add @ckeditor/ckeditor5-dev-tests
@@ -28,6 +28,4 @@ after_script:
 - ckeditor5-dev-tests-notify-travis-status
 env:
   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
 =========
 
+## [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)
 
 ### Bug fixes

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

@@ -1,10 +1,8 @@
 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)
 [![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)
 <br>
 [![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 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)

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

@@ -16,5 +16,6 @@
 	"Merge cell left": "Label for the merge table cell left button.",
 	"Split cell vertically": "Label for the split table cell vertically 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."
 msgid "Insert column left"
-msgstr ""
+msgstr "Kolom links invoegen"
 
 msgctxt "Label for the insert table column to the right of the current one button."
 msgid "Insert column right"
-msgstr ""
+msgstr "Kolom rechts invoegen"
 
 msgctxt "Label for the delete table column button."
 msgid "Delete column"

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

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-table",
-  "version": "13.0.0",
+  "version": "14.0.0",
   "description": "Table feature for CKEditor 5.",
   "keywords": [
     "ckeditor",
@@ -10,25 +10,25 @@
     "ckeditor5-plugin"
   ],
   "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": {
-    "@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-config-ckeditor5": "^1.0.11",
+    "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",
     "lint-staged": "^7.0.0"
   },
@@ -39,7 +39,7 @@
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
   "homepage": "https://ckeditor.com/ckeditor-5",
-  "bugs": "https://github.com/ckeditor/ckeditor5-table/issues",
+  "bugs": "https://github.com/ckeditor/ckeditor5/issues",
   "repository": {
     "type": "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.
 	 *
 	 * @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 doc = model.document;
 		const selection = doc.selection;
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 
 		const position = selection.getFirstPosition();
-		const tableCell = findAncestor( 'tableCell', position.parent );
+		const tableCell = findAncestor( 'tableCell', position );
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 
-		const currentHeadingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
 		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 => {
 			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.
 	 *
 	 * @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 doc = model.document;
 		const selection = doc.selection;
@@ -74,7 +77,11 @@ export default class SetHeaderRowCommand extends Command {
 		const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
 		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 => {
 			if ( headingRowsToSet ) {
@@ -151,19 +158,21 @@ function splitHorizontally( tableCell, headingRows, writer ) {
 		attributes.rowspan = spanToSet;
 	}
 
+	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
+
+	if ( colspan > 1 ) {
+		attributes.colspan = colspan;
+	}
+
 	const startRow = table.getChildIndex( tableRow );
 	const endRow = startRow + newRowspan;
 	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeSpanned: true } ) ];
 
 	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;
-
-			if ( colspan > 1 ) {
-				attributes.colspan = colspan;
-			}
 		}
 
 		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 );
 	}
 
+	conversionApi.mapper.unbindViewElement( viewCell );
 	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:
  *
- *        <table>
- *            <tableRow>
- *                <tableCell><paragraph></paragraph></tableCell>
- *            </tableRow>
- *        </table>
+ *		<table>
+ *			<tableRow>
+ *				<tableCell><paragraph></paragraph></tableCell>
+ *			</tableRow>
+ *		</table>
  *
  * @param {module:engine/model/model~Model} model
  */
-export default function injectTableCellContentPostFixer( model ) {
+export default function injectTableCellParagraphPostFixer( model ) {
 	model.document.registerPostFixer( writer => tableCellContentsPostFixer( writer, model ) );
 }
 
@@ -45,24 +42,20 @@ function tableCellContentsPostFixer( writer, model ) {
 	let wasFixed = false;
 
 	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;
 	}
 
-	// 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.
 	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 !!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 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 TableSelection from './tableselection';
@@ -44,6 +44,13 @@ import TableSelection from './tableselection';
  * @extends module:core/plugin~Plugin
  */
 export default class TableEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TableEditing';
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -112,8 +119,6 @@ export default class TableEditing extends Plugin {
 		conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
 		conversion.for( 'dataDowncast' ).add( downcastTableHeadingRowsChange() );
 
-		injectTableCellPostFixer( editor.model, editor.editing );
-
 		// Define all the commands.
 		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
 		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 ) );
 
 		injectTableLayoutPostFixer( model );
-		injectTableCellContentsPostFixer( model );
+		injectTableCellRefreshPostFixer( model );
+		injectTableCellParagraphPostFixer( model );
 
 		// Handle tab key navigation.
 		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() {
 		const editor = this.editor;
+		const t = editor.t;
 		const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
 
 		const tableContentToolbarItems = editor.config.get( 'table.contentToolbar' );
@@ -50,6 +51,7 @@ export default class TableToolbar extends Plugin {
 
 		if ( tableContentToolbarItems ) {
 			widgetToolbarRepository.register( 'tableContent', {
+				ariaLabel: t( 'Table toolbar' ),
 				items: tableContentToolbarItems,
 				getRelatedElement: getTableWidgetAncestor
 			} );
@@ -57,6 +59,7 @@ export default class TableToolbar extends Plugin {
 
 		if ( tableToolbarItems ) {
 			widgetToolbarRepository.register( 'table', {
+				ariaLabel: t( 'Table toolbar' ),
 				items: tableToolbarItems,
 				getRelatedElement: getSelectedTableWidget
 			} );

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

@@ -38,6 +38,8 @@ export default class TableUI extends Plugin {
 	init() {
 		const editor = this.editor;
 		const t = this.editor.t;
+		const contentLanguageDirection = editor.locale.contentLanguageDirection;
+		const isContentLtr = contentLanguageDirection === 'ltr';
 
 		editor.ui.componentFactory.add( 'insertTable', locale => {
 			const command = editor.commands.get( 'insertTable' );
@@ -86,14 +88,14 @@ export default class TableUI extends Plugin {
 				{
 					type: 'button',
 					model: {
-						commandName: 'insertTableColumnLeft',
+						commandName: isContentLtr ? 'insertTableColumnLeft' : 'insertTableColumnRight',
 						label: t( 'Insert column left' )
 					}
 				},
 				{
 					type: 'button',
 					model: {
-						commandName: 'insertTableColumnRight',
+						commandName: isContentLtr ? 'insertTableColumnRight' : 'insertTableColumnLeft',
 						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 |
 	 *		+---+---+---+     will give:    +---+---+---+---+---+
 	 *		| d | e | f |                   | d |   |   | e | f |
-	 *		+---+   +---+                   +---+---+---+  +---+
+	 *		+---+   +---+                   +---+---+---+   +---+
 	 *		| g |   | h |                   | g |   |   |   | h |
 	 *		+---+---+---+                   +---+---+---+---+---+
 	 *		| i         |                   | i                 |
@@ -219,13 +219,16 @@ export default class TableUtils extends Plugin {
 
 			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:
 				// - 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`),
 				// - 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").
 					// For such cells expand them by a number of columns inserted.
 					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.
 	 *
-	 *
 	 * 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.
 	 * 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.
 	 *
+	 * See {@link module:table/tablewalker~TableWalkerValue} what values are returned by the table walker.
+	 *
 	 * To iterate over a given row:
 	 *
 	 *		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 } );
 	 *
-	 *		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:
@@ -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.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 {Boolean} [options.includeSpanned] Also return values for spanned cells.
+	 * @param {Boolean} [options.includeSpanned=false] Also return values for spanned cells.
 	 */
 	constructor( table, options = {} ) {
 		/**
@@ -151,16 +152,18 @@ export default class TableWalker {
 		 * @member {Number}
 		 * @private
 		 */
-		this._cell = 0;
+		this._cellIndex = 0;
 
 		/**
 		 * Holds a map of spanned cells in a table.
 		 *
 		 * @readonly
-		 * @member {Map<Number, Map.<Number, Number>>}
+		 * @member {Map<Number, Map.<Number, module:engine/model/element~Element>>}
 		 * @private
 		 */
 		this._spannedCells = new Map();
+
+		this._nextCellAtColumn = -1;
 	}
 
 	/**
@@ -180,62 +183,53 @@ export default class TableWalker {
 	next() {
 		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() ) {
 			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++;
 
-		// 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;
 	}
 
@@ -252,8 +246,8 @@ export default class TableWalker {
 	/**
 	 * Checks if the current row is over {@link #endRow}.
 	 *
-	 * @returns {Boolean}
 	 * @private
+	 * @returns {Boolean}
 	 */
 	_isOverEndRow() {
 		// 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.
 	 *
-	 * @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} colspan Colspan of the current cell.
 	 * @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 {
 			done: false,
 			value: {
 				cell,
 				row: this._row,
 				column,
+				isSpanned,
 				rowspan,
 				colspan,
-				cellIndex: this._cell
+				cellIndex: this._cellIndex
 			}
 		};
 	}
@@ -288,8 +283,8 @@ export default class TableWalker {
 	/**
 	 * Checks if the current row should be skipped.
 	 *
-	 * @returns {Boolean}
 	 * @private
+	 * @returns {Boolean}
 	 */
 	_shouldSkipRow() {
 		const rowIsBelowStartRow = this._row < this.startRow;
@@ -301,33 +296,25 @@ export default class TableWalker {
 	/**
 	 * Checks if the current column should be skipped.
 	 *
-	 * @param {Number} column
-	 * @param {Number} colspan
-	 * @returns {Boolean}
 	 * @private
+	 * @returns {Boolean}
 	 */
-	_shouldSkipColumn( column, colspan ) {
+	_shouldSkipColumn() {
 		if ( this.column === undefined ) {
 			// The {@link #column} is not defined so output all columns.
 			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.
 	 *
+	 * @private
 	 * @param {Number} row Row index of a cell location to check.
 	 * @param {Number} column Column index of a cell location to check.
 	 * @returns {Boolean}
-	 * @private
 	 */
 	_isSpanned( row, column ) {
 		if ( !this._spannedCells.has( row ) ) {
@@ -341,25 +328,38 @@ export default class TableWalker {
 		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.
 	 *
+	 * @private
 	 * @param {Number} row Row index of a cell.
 	 * @param {Number} column Column index of a cell.
 	 * @param {Number} rowspan Cell height.
 	 * @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.
 		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.
 		for ( let rowToUpdate = row + 1; rowToUpdate < row + rowspan; rowToUpdate++ ) {
 			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.
 	 *
+	 * @private
 	 * @param {Number} row Row 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 ) ) {
 			this._spannedCells.set( row, new Map() );
 		}
 
 		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.
  *
  * @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} 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
 		 */
 		this.bind( 'label' )
-			.to( this, 'columns', this, 'rows', ( columns, rows ) => `${ rows } x ${ columns }` );
+			.to( this, 'columns', this, 'rows', ( columns, rows ) => `${ rows } × ${ columns }` );
 
 		this.setTemplate( {
 			tag: 'div',

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

@@ -23,7 +23,7 @@ import { findAncestor } from './commands/utils';
 export function toTableWidget( viewElement, writer ) {
 	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>` : '';
 
 	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>`;
 }

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

@@ -115,5 +115,29 @@ describe( 'SetHeaderColumnCommand', () => {
 				[ '00', '01[]', '02', '03' ]
 			], { 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', () => {
 			setData( model, modelTable( [
 				[ '00', '01', '02' ],

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

@@ -333,8 +333,8 @@ describe( 'downcast converters', () => {
 				setModelData( model, modelTable( [ [ '' ] ] ) );
 
 				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>' +
 							'<tbody>' +
 								'<tr>' +
@@ -566,8 +566,8 @@ describe( 'downcast converters', () => {
 
 				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>' +
 							'<tbody>' +
 								'<tr>' +
@@ -712,8 +712,8 @@ describe( 'downcast converters', () => {
 				} );
 
 				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>' +
 							'<tbody>' +
 								'<tr>' +
@@ -889,8 +889,8 @@ describe( 'downcast converters', () => {
 				} );
 
 				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>' +
 							'<thead>' +
 								'<tr>' +
@@ -1103,8 +1103,8 @@ describe( 'downcast converters', () => {
 				} );
 
 				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>' +
 							'<tbody>' +
 								'<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 UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 
-describe( 'Table cell content post-fixer', () => {
+describe( 'Table cell paragraph post-fixer', () => {
 	let editor, model, root;
 
 	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 { 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 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';
 
-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();
 
@@ -44,10 +44,12 @@ describe( 'TableCell post-fixer', () => {
 				} );
 				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: '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( [
 			[ '<p>00</p><p></p>' ]
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
 	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( [
 			[ '<p>00</p><div></div>' ]
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
 	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( [
 			[ '<p>00</p><p></p>' ]
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 
 		model.change( writer => {
 			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
@@ -115,6 +120,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '00' ]
 		], { asWidget: true } ) );
+		sinon.assert.calledTwice( refreshItemSpy );
 	} );
 
 	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( [
 			[ '<p foo="bar">00</p>' ]
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
 	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( [
 			[ '00' ]
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
 	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( [
 			[ '<span>00</span>' ]
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
 	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( [
 			[ '<p foo="baz">00</p>' ]
 		], { 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)', () => {
@@ -185,6 +230,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<p foo="baz">00</p><p>00</p>' ]
 		], { asWidget: true } ) );
+		sinon.assert.notCalled( refreshItemSpy );
 	} );
 
 	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( [
 			[ '<div>00</div>' ]
 		], { asWidget: true } ) );
+		sinon.assert.notCalled( refreshItemSpy );
 	} );
 
 	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( [
 			[ '<div foo="bar">foo</div>' ]
 		], { 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>' ] ] ) );
 
 		const table = root.getChild( 0 );
@@ -254,6 +275,7 @@ describe( 'TableCell post-fixer', () => {
 		expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
 			[ '<span>00</span>' ]
 		], { asWidget: true } ) );
+		sinon.assert.calledOnce( refreshItemSpy );
 	} );
 
 	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();
 	} );
 
+	it( 'should have pluginName', () => {
+		expect( TableEditing.pluginName ).to.equal( 'TableEditing' );
+	} );
+
 	it( 'should set proper schema rules', () => {
 		// Table:
 		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'
 				} );
 			} );
+
+			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', () => {

+ 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' ] );
 		} );
 
-		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 setColumnHeaderCommand = editor.commands.get( 'setTableColumnHeader' );
@@ -266,6 +266,35 @@ describe( 'TableUI', () => {
 			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', () => {
 			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 mergeCellsCommand = editor.commands.get( 'mergeTableCells' );
@@ -363,6 +392,35 @@ describe( 'TableUI', () => {
 			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', () => {
 			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 );
 		}
 
-		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 );
 	}
@@ -135,7 +143,7 @@ describe( 'TableWalker', () => {
 			], { endRow: 2 } );
 		} );
 
-		it( 'should iterate over given row 0 only', () => {
+		it( 'should iterate over given row only', () => {
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
 				[ '23' ],
@@ -157,11 +165,11 @@ describe( 'TableWalker', () => {
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ 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 } );
 		} );
 
-		it( 'should output spanned cells as empty cell', () => {
+		it( 'should output spanned cells', () => {
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
 				[ '12' ],
@@ -169,17 +177,17 @@ describe( 'TableWalker', () => {
 				[ '30', { colspan: 2, contents: '31' } ]
 			], [
 				{ 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: 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: 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: 3, column: 0, index: 0, data: '30' },
 				{ 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 } );
 		} );
 
@@ -191,7 +199,7 @@ describe( 'TableWalker', () => {
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ 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 } );
 		} );
 
@@ -202,16 +210,16 @@ describe( 'TableWalker', () => {
 				[ '22' ],
 				[ '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: 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' }
 			], { 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( [
 				[ '00', { rowspan: 2, contents: '01' } ],
 				[ '10' ],
@@ -220,7 +228,7 @@ describe( 'TableWalker', () => {
 				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 0, column: 1, index: 1, data: '01' },
 				{ 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 } );
 		} );
 	} );
@@ -233,9 +241,22 @@ describe( 'TableWalker', () => {
 				[ '22' ],
 				[ '30', '31', '32' ]
 			], [
-				{ row: 0, column: 0, index: 0, data: '00' },
 				{ row: 3, column: 1, index: 1, data: '31' }
 			], { 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', () => {
-			expect( view.label ).to.equal( '0 x 0' );
+			expect( view.label ).to.equal( '0 × 0' );
 		} );
 
 		it( 'creates #element from template', () => {
@@ -85,11 +85,11 @@ describe( 'InsertTableView', () => {
 			it( 'binds #label to rows & columns', () => {
 				view.rows = 3;
 
-				expect( view.label ).to.equal( '3 x 0' );
+				expect( view.label ).to.equal( '3 × 0' );
 
 				view.columns = 7;
 
-				expect( view.label ).to.equal( '3 x 7' );
+				expect( view.label ).to.equal( '3 × 7' );
 			} );
 
 			it( 'mousedown event should be prevented', () => {