@ckeditor/ckeditor5-table 27.1.0 → 29.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +3 -3
  3. package/build/table.js +1 -1
  4. package/build/translations/ar.js +1 -0
  5. package/build/translations/az.js +1 -0
  6. package/build/translations/bg.js +1 -0
  7. package/build/translations/cs.js +1 -0
  8. package/build/translations/da.js +1 -0
  9. package/build/translations/de-ch.js +1 -0
  10. package/build/translations/de.js +1 -0
  11. package/build/translations/en-au.js +1 -0
  12. package/build/translations/en-gb.js +1 -0
  13. package/build/translations/es.js +1 -0
  14. package/build/translations/et.js +1 -0
  15. package/build/translations/fa.js +1 -0
  16. package/build/translations/fi.js +1 -0
  17. package/build/translations/fr.js +1 -0
  18. package/build/translations/gl.js +1 -0
  19. package/build/translations/hi.js +1 -0
  20. package/build/translations/hr.js +1 -0
  21. package/build/translations/hu.js +1 -0
  22. package/build/translations/id.js +1 -0
  23. package/build/translations/it.js +1 -0
  24. package/build/translations/ja.js +1 -0
  25. package/build/translations/ko.js +1 -0
  26. package/build/translations/ku.js +1 -0
  27. package/build/translations/lt.js +1 -0
  28. package/build/translations/lv.js +1 -0
  29. package/build/translations/nb.js +1 -0
  30. package/build/translations/ne.js +1 -0
  31. package/build/translations/nl.js +1 -0
  32. package/build/translations/no.js +1 -0
  33. package/build/translations/pl.js +1 -0
  34. package/build/translations/pt-br.js +1 -0
  35. package/build/translations/ro.js +1 -0
  36. package/build/translations/ru.js +1 -0
  37. package/build/translations/sk.js +1 -0
  38. package/build/translations/sq.js +1 -0
  39. package/build/translations/sr-latn.js +1 -0
  40. package/build/translations/sr.js +1 -0
  41. package/build/translations/sv.js +1 -0
  42. package/build/translations/th.js +1 -0
  43. package/build/translations/tk.js +1 -0
  44. package/build/translations/tr.js +1 -0
  45. package/build/translations/ug.js +1 -0
  46. package/build/translations/uk.js +1 -0
  47. package/build/translations/vi.js +1 -0
  48. package/build/translations/zh-cn.js +1 -0
  49. package/build/translations/zh.js +1 -0
  50. package/ckeditor5-metadata.json +174 -0
  51. package/lang/contexts.json +4 -1
  52. package/lang/translations/ar.po +12 -0
  53. package/lang/translations/az.po +12 -0
  54. package/lang/translations/bg.po +12 -0
  55. package/lang/translations/cs.po +12 -0
  56. package/lang/translations/da.po +12 -0
  57. package/lang/translations/de-ch.po +12 -0
  58. package/lang/translations/de.po +12 -0
  59. package/lang/translations/en-au.po +12 -0
  60. package/lang/translations/en-gb.po +12 -0
  61. package/lang/translations/en.po +12 -0
  62. package/lang/translations/es.po +12 -0
  63. package/lang/translations/et.po +12 -0
  64. package/lang/translations/fa.po +12 -0
  65. package/lang/translations/fi.po +12 -0
  66. package/lang/translations/fr.po +12 -0
  67. package/lang/translations/gl.po +12 -0
  68. package/lang/translations/hi.po +12 -0
  69. package/lang/translations/hr.po +12 -0
  70. package/lang/translations/hu.po +23 -11
  71. package/lang/translations/id.po +23 -11
  72. package/lang/translations/it.po +12 -0
  73. package/lang/translations/ja.po +12 -0
  74. package/lang/translations/ko.po +12 -0
  75. package/lang/translations/ku.po +12 -0
  76. package/lang/translations/lt.po +12 -0
  77. package/lang/translations/lv.po +12 -0
  78. package/lang/translations/nb.po +12 -0
  79. package/lang/translations/ne.po +12 -0
  80. package/lang/translations/nl.po +12 -0
  81. package/lang/translations/no.po +12 -0
  82. package/lang/translations/pl.po +12 -0
  83. package/lang/translations/pt-br.po +12 -0
  84. package/lang/translations/ro.po +51 -39
  85. package/lang/translations/ru.po +12 -0
  86. package/lang/translations/sk.po +12 -0
  87. package/lang/translations/sq.po +12 -0
  88. package/lang/translations/sr-latn.po +12 -0
  89. package/lang/translations/sr.po +12 -0
  90. package/lang/translations/sv.po +12 -0
  91. package/lang/translations/th.po +12 -0
  92. package/lang/translations/tk.po +12 -0
  93. package/lang/translations/tr.po +12 -0
  94. package/lang/translations/ug.po +12 -0
  95. package/lang/translations/uk.po +12 -0
  96. package/lang/translations/vi.po +12 -0
  97. package/lang/translations/zh-cn.po +12 -0
  98. package/lang/translations/zh.po +12 -0
  99. package/package.json +25 -23
  100. package/src/commands/insertcolumncommand.js +2 -3
  101. package/src/commands/insertrowcommand.js +2 -3
  102. package/src/commands/inserttablecommand.js +22 -7
  103. package/src/commands/mergecellcommand.js +5 -3
  104. package/src/commands/removerowcommand.js +8 -5
  105. package/src/converters/downcast.js +4 -5
  106. package/src/converters/table-caption-post-fixer.js +69 -0
  107. package/src/converters/table-cell-paragraph-post-fixer.js +3 -1
  108. package/src/converters/table-layout-post-fixer.js +13 -8
  109. package/src/converters/tableproperties.js +82 -23
  110. package/src/converters/upcasttable.js +63 -0
  111. package/src/index.js +18 -33
  112. package/src/table.js +17 -0
  113. package/src/tablecaption/tablecaptionediting.js +153 -0
  114. package/src/tablecaption/tablecaptionui.js +71 -0
  115. package/src/tablecaption/toggletablecaptioncommand.js +120 -0
  116. package/src/tablecaption/utils.js +93 -0
  117. package/src/tablecaption.js +35 -0
  118. package/src/tablecellproperties/commands/tablecellbackgroundcolorcommand.js +3 -2
  119. package/src/tablecellproperties/commands/tablecellbordercolorcommand.js +10 -3
  120. package/src/tablecellproperties/commands/tablecellborderstylecommand.js +10 -3
  121. package/src/tablecellproperties/commands/tablecellborderwidthcommand.js +17 -4
  122. package/src/tablecellproperties/commands/tablecellheightcommand.js +10 -3
  123. package/src/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js +3 -2
  124. package/src/tablecellproperties/commands/tablecellpaddingcommand.js +17 -4
  125. package/src/tablecellproperties/commands/tablecellpropertycommand.js +28 -2
  126. package/src/tablecellproperties/commands/tablecellverticalalignmentcommand.js +3 -2
  127. package/src/tablecellproperties/commands/tablecellwidthcommand.js +10 -3
  128. package/src/tablecellproperties/tablecellpropertiesediting.js +164 -65
  129. package/src/tablecellproperties/tablecellpropertiesui.js +76 -16
  130. package/src/tablecellproperties/ui/tablecellpropertiesview.js +39 -13
  131. package/src/tablecellproperties.js +42 -2
  132. package/src/tableediting.js +76 -6
  133. package/src/tablekeyboard.js +3 -2
  134. package/src/tableproperties/commands/tablealignmentcommand.js +3 -2
  135. package/src/tableproperties/commands/tablebackgroundcolorcommand.js +3 -2
  136. package/src/tableproperties/commands/tablebordercolorcommand.js +10 -3
  137. package/src/tableproperties/commands/tableborderstylecommand.js +10 -3
  138. package/src/tableproperties/commands/tableborderwidthcommand.js +17 -4
  139. package/src/tableproperties/commands/tableheightcommand.js +10 -3
  140. package/src/tableproperties/commands/tablepropertycommand.js +28 -2
  141. package/src/tableproperties/commands/tablewidthcommand.js +10 -3
  142. package/src/tableproperties/tablepropertiesediting.js +104 -47
  143. package/src/tableproperties/tablepropertiesui.js +68 -15
  144. package/src/tableproperties/ui/tablepropertiesview.js +26 -11
  145. package/src/tableproperties.js +38 -2
  146. package/src/tableui.js +10 -1
  147. package/src/tableutils.js +41 -5
  148. package/src/tablewalker.js +36 -1
  149. package/src/ui/colorinputview.js +11 -7
  150. package/src/utils/structure.js +4 -3
  151. package/src/utils/table-properties.js +41 -0
  152. package/src/utils/ui/table-properties.js +29 -7
  153. package/src/utils/ui/widget.js +7 -15
  154. package/theme/table.css +17 -1
  155. package/theme/tablecaption.css +53 -0
  156. package/build/table.js.map +0 -1
@@ -247,3 +247,15 @@ msgstr "尺寸代碼錯誤。試試看 \"10px\" 或 \"2em\" 或簡單寫 \"2\"
247
247
  msgctxt "The label used by assistive technologies describing a button that opens a color picker, where user can choose a configured color for a certain properties (eg.: background color, color, border-color etc.)."
248
248
  msgid "Color picker"
249
249
  msgstr "顏色選擇"
250
+
251
+ msgctxt "The button label for the table toolbar hiding caption attached to the table."
252
+ msgid "Toggle caption off"
253
+ msgstr ""
254
+
255
+ msgctxt "The button label for the table toolbar showing caption attached to the table."
256
+ msgid "Toggle caption on"
257
+ msgstr ""
258
+
259
+ msgctxt "The placeholder text for the table caption displayed when the caption is empty."
260
+ msgid "Enter table caption"
261
+ msgstr ""
package/package.json CHANGED
@@ -1,39 +1,40 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-table",
3
- "version": "27.1.0",
3
+ "version": "29.2.0",
4
4
  "description": "Table feature for CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
7
7
  "ckeditor5",
8
8
  "ckeditor 5",
9
9
  "ckeditor5-feature",
10
- "ckeditor5-plugin"
10
+ "ckeditor5-plugin",
11
+ "ckeditor5-dll"
11
12
  ],
12
13
  "main": "src/index.js",
13
14
  "dependencies": {
14
- "ckeditor5": "^27.1.0",
15
+ "ckeditor5": "^29.2.0",
15
16
  "lodash-es": "^4.17.15"
16
17
  },
17
18
  "devDependencies": {
18
- "@ckeditor/ckeditor5-alignment": "^27.1.0",
19
- "@ckeditor/ckeditor5-block-quote": "^27.1.0",
20
- "@ckeditor/ckeditor5-clipboard": "^27.1.0",
21
- "@ckeditor/ckeditor5-core": "^27.1.0",
22
- "@ckeditor/ckeditor5-dev-utils": "^24.0.0",
23
- "@ckeditor/ckeditor5-editor-classic": "^27.1.0",
24
- "@ckeditor/ckeditor5-engine": "^27.1.0",
25
- "@ckeditor/ckeditor5-horizontal-line": "^27.1.0",
26
- "@ckeditor/ckeditor5-image": "^27.1.0",
27
- "@ckeditor/ckeditor5-indent": "^27.1.0",
28
- "@ckeditor/ckeditor5-list": "^27.1.0",
29
- "@ckeditor/ckeditor5-media-embed": "^27.1.0",
30
- "@ckeditor/ckeditor5-paragraph": "^27.1.0",
31
- "@ckeditor/ckeditor5-theme-lark": "^27.1.0",
32
- "@ckeditor/ckeditor5-typing": "^27.1.0",
33
- "@ckeditor/ckeditor5-ui": "^27.1.0",
34
- "@ckeditor/ckeditor5-undo": "^27.1.0",
35
- "@ckeditor/ckeditor5-utils": "^27.1.0",
36
- "@ckeditor/ckeditor5-widget": "^27.1.0",
19
+ "@ckeditor/ckeditor5-alignment": "^29.2.0",
20
+ "@ckeditor/ckeditor5-block-quote": "^29.2.0",
21
+ "@ckeditor/ckeditor5-clipboard": "^29.2.0",
22
+ "@ckeditor/ckeditor5-core": "^29.2.0",
23
+ "@ckeditor/ckeditor5-dev-utils": "^25.4.0",
24
+ "@ckeditor/ckeditor5-editor-classic": "^29.2.0",
25
+ "@ckeditor/ckeditor5-engine": "^29.2.0",
26
+ "@ckeditor/ckeditor5-horizontal-line": "^29.2.0",
27
+ "@ckeditor/ckeditor5-image": "^29.2.0",
28
+ "@ckeditor/ckeditor5-indent": "^29.2.0",
29
+ "@ckeditor/ckeditor5-list": "^29.2.0",
30
+ "@ckeditor/ckeditor5-media-embed": "^29.2.0",
31
+ "@ckeditor/ckeditor5-paragraph": "^29.2.0",
32
+ "@ckeditor/ckeditor5-theme-lark": "^29.2.0",
33
+ "@ckeditor/ckeditor5-typing": "^29.2.0",
34
+ "@ckeditor/ckeditor5-ui": "^29.2.0",
35
+ "@ckeditor/ckeditor5-undo": "^29.2.0",
36
+ "@ckeditor/ckeditor5-utils": "^29.2.0",
37
+ "@ckeditor/ckeditor5-widget": "^29.2.0",
37
38
  "json-diff": "^0.5.4",
38
39
  "webpack": "^4.43.0",
39
40
  "webpack-cli": "^3.3.11"
@@ -55,7 +56,8 @@
55
56
  "lang",
56
57
  "src",
57
58
  "theme",
58
- "build"
59
+ "build",
60
+ "ckeditor5-metadata.json"
59
61
  ],
60
62
  "scripts": {
61
63
  "dll:build": "webpack"
@@ -52,10 +52,9 @@ export default class InsertColumnCommand extends Command {
52
52
  */
53
53
  refresh() {
54
54
  const selection = this.editor.model.document.selection;
55
+ const isAnyCellSelected = !!getSelectionAffectedTableCells( selection ).length;
55
56
 
56
- const tableParent = selection.getFirstPosition().findAncestor( 'table' );
57
-
58
- this.isEnabled = !!tableParent;
57
+ this.isEnabled = isAnyCellSelected;
59
58
  }
60
59
 
61
60
  /**
@@ -52,10 +52,9 @@ export default class InsertRowCommand extends Command {
52
52
  */
53
53
  refresh() {
54
54
  const selection = this.editor.model.document.selection;
55
+ const isAnyCellSelected = !!getSelectionAffectedTableCells( selection ).length;
55
56
 
56
- const tableParent = selection.getFirstPosition().findAncestor( 'table' );
57
-
58
- this.isEnabled = !!tableParent;
57
+ this.isEnabled = isAnyCellSelected;
59
58
  }
60
59
 
61
60
  /**
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { Command } from 'ckeditor5/src/core';
11
- import { findOptimalInsertionPosition, checkSelectionOnObject } from 'ckeditor5/src/widget';
11
+ import { findOptimalInsertionRange } from 'ckeditor5/src/widget';
12
12
 
13
13
  /**
14
14
  * The insert table command.
@@ -30,8 +30,7 @@ export default class InsertTableCommand extends Command {
30
30
  const selection = model.document.selection;
31
31
  const schema = model.schema;
32
32
 
33
- this.isEnabled = isAllowedInParent( selection, schema ) &&
34
- !checkSelectionOnObject( selection, schema );
33
+ this.isEnabled = isAllowedInParent( selection, schema );
35
34
  }
36
35
 
37
36
  /**
@@ -42,21 +41,37 @@ export default class InsertTableCommand extends Command {
42
41
  * @param {Object} options
43
42
  * @param {Number} [options.rows=2] The number of rows to create in the inserted table.
44
43
  * @param {Number} [options.columns=2] The number of columns to create in the inserted table.
45
- * @param {Number} [options.headingRows=0] The number of heading rows.
46
- * @param {Number} [options.headingColumns=0] The number of heading columns.
44
+ * @param {Number} [options.headingRows] The number of heading rows.
45
+ * If not provided it will default to {@link module:table/table~TableConfig#defaultHeadings `config.table.defaultHeadings.rows`}
46
+ * table config.
47
+ * @param {Number} [options.headingColumns] The number of heading columns.
48
+ * If not provided it will default to {@link module:table/table~TableConfig#defaultHeadings `config.table.defaultHeadings.columns`}
49
+ * table config.
47
50
  * @fires execute
48
51
  */
49
52
  execute( options = {} ) {
50
53
  const model = this.editor.model;
51
54
  const selection = model.document.selection;
52
55
  const tableUtils = this.editor.plugins.get( 'TableUtils' );
56
+ const config = this.editor.config.get( 'table' );
53
57
 
54
- const insertPosition = findOptimalInsertionPosition( selection, model );
58
+ const insertionRange = findOptimalInsertionRange( selection, model );
59
+
60
+ const defaultRows = config.defaultHeadings.rows;
61
+ const defaultColumns = config.defaultHeadings.columns;
62
+
63
+ if ( options.headingRows === undefined && defaultRows ) {
64
+ options.headingRows = defaultRows;
65
+ }
66
+
67
+ if ( options.headingColumns === undefined && defaultColumns ) {
68
+ options.headingColumns = defaultColumns;
69
+ }
55
70
 
56
71
  model.change( writer => {
57
72
  const table = tableUtils.createTable( writer, options );
58
73
 
59
- model.insertContent( table, insertPosition );
74
+ model.insertContent( table, insertionRange );
60
75
 
61
76
  writer.setSelection( writer.createPositionAt( table.getNodeByPath( [ 0, 0, 0 ] ), 0 ) );
62
77
  } );
@@ -133,7 +133,7 @@ export default class MergeCellCommand extends Command {
133
133
  // First get the cell on proper direction.
134
134
  const cellToMerge = this.isHorizontal ?
135
135
  getHorizontalCell( tableCell, this.direction, tableUtils ) :
136
- getVerticalCell( tableCell, this.direction );
136
+ getVerticalCell( tableCell, this.direction, tableUtils );
137
137
 
138
138
  if ( !cellToMerge ) {
139
139
  return;
@@ -155,6 +155,7 @@ export default class MergeCellCommand extends Command {
155
155
  //
156
156
  // @param {module:engine/model/element~Element} tableCell
157
157
  // @param {String} direction
158
+ // @param {module:table/tableutils~TableUtils} tableUtils
158
159
  // @returns {module:engine/model/node~Node|null}
159
160
  function getHorizontalCell( tableCell, direction, tableUtils ) {
160
161
  const tableRow = tableCell.parent;
@@ -195,15 +196,16 @@ function getHorizontalCell( tableCell, direction, tableUtils ) {
195
196
  //
196
197
  // @param {module:engine/model/element~Element} tableCell
197
198
  // @param {String} direction
199
+ // @param {module:table/tableutils~TableUtils} tableUtils
198
200
  // @returns {module:engine/model/node~Node|null}
199
- function getVerticalCell( tableCell, direction ) {
201
+ function getVerticalCell( tableCell, direction, tableUtils ) {
200
202
  const tableRow = tableCell.parent;
201
203
  const table = tableRow.parent;
202
204
 
203
205
  const rowIndex = table.getChildIndex( tableRow );
204
206
 
205
207
  // Don't search for mergeable cell if direction points out of the table.
206
- if ( ( direction == 'down' && rowIndex === table.childCount - 1 ) || ( direction == 'up' && rowIndex === 0 ) ) {
208
+ if ( ( direction == 'down' && rowIndex === tableUtils.getRows( table ) - 1 ) || ( direction == 'up' && rowIndex === 0 ) ) {
207
209
  return;
208
210
  }
209
211
 
@@ -51,23 +51,25 @@ export default class RemoveRowCommand extends Command {
51
51
  */
52
52
  execute() {
53
53
  const model = this.editor.model;
54
+ const tableUtils = this.editor.plugins.get( 'TableUtils' );
55
+
54
56
  const referenceCells = getSelectionAffectedTableCells( model.document.selection );
55
57
  const removedRowIndexes = getRowIndexes( referenceCells );
56
58
 
57
59
  const firstCell = referenceCells[ 0 ];
58
60
  const table = firstCell.findAncestor( 'table' );
59
61
 
60
- const columnIndexToFocus = this.editor.plugins.get( 'TableUtils' ).getCellLocation( firstCell ).column;
62
+ const columnIndexToFocus = tableUtils.getCellLocation( firstCell ).column;
61
63
 
62
64
  model.change( writer => {
63
65
  const rowsToRemove = removedRowIndexes.last - removedRowIndexes.first + 1;
64
66
 
65
- this.editor.plugins.get( 'TableUtils' ).removeRows( table, {
67
+ tableUtils.removeRows( table, {
66
68
  at: removedRowIndexes.first,
67
69
  rows: rowsToRemove
68
70
  } );
69
71
 
70
- const cellToFocus = getCellToFocus( table, removedRowIndexes.first, columnIndexToFocus );
72
+ const cellToFocus = getCellToFocus( table, removedRowIndexes.first, columnIndexToFocus, tableUtils.getRows( table ) );
71
73
 
72
74
  writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );
73
75
  } );
@@ -77,8 +79,9 @@ export default class RemoveRowCommand extends Command {
77
79
  // Returns a cell that should be focused before removing the row, belonging to the same column as the currently focused cell.
78
80
  // * If the row was not the last one, the cell to focus will be in the row that followed it (before removal).
79
81
  // * If the row was the last one, the cell to focus will be in the row that preceded it (before removal).
80
- function getCellToFocus( table, removedRowIndex, columnToFocus ) {
81
- const row = table.getChild( removedRowIndex ) || table.getChild( table.childCount - 1 );
82
+ function getCellToFocus( table, removedRowIndex, columnToFocus, tableRowCount ) {
83
+ // Don't go beyond last row's index.
84
+ const row = table.getChild( Math.min( removedRowIndex, tableRowCount - 1 ) );
82
85
 
83
86
  // Default to first table cell.
84
87
  let cellToFocus = row.getChild( 0 );
@@ -74,7 +74,8 @@ export function downcastInsertTable( options = {} ) {
74
74
  for ( const tableRow of table.getChildren() ) {
75
75
  const rowIndex = tableRow.index;
76
76
 
77
- if ( !viewRows.has( rowIndex ) ) {
77
+ // Make sure that this is a table row and not some other element (i.e., caption).
78
+ if ( tableRow.is( 'element', 'tableRow' ) && !viewRows.has( rowIndex ) ) {
78
79
  viewRows.set( rowIndex, createTr( tableElement, tableRow, rowIndex, tableAttributes, conversionApi ) );
79
80
  }
80
81
  }
@@ -257,9 +258,7 @@ export function convertParagraphInTableCell( modelElement, conversionApi ) {
257
258
  }
258
259
 
259
260
  if ( isSingleParagraphWithoutAttributes( modelElement ) ) {
260
- // Use display:inline-block to force Chrome/Safari to limit text mutations to this element.
261
- // See #6062.
262
- return writer.createContainerElement( 'span', { style: 'display:inline-block' } );
261
+ return writer.createContainerElement( 'span', { class: 'ck-table-bogus-paragraph' } );
263
262
  } else {
264
263
  return writer.createContainerElement( 'p' );
265
264
  }
@@ -270,7 +269,7 @@ export function convertParagraphInTableCell( modelElement, conversionApi ) {
270
269
  *
271
270
  * The paragraph should be converted in the editing view to:
272
271
  *
273
- * * If returned `true` - to a `<span style="display:inline-block">`
272
+ * * If returned `true` - to a `<span class="ck-table-bogus-paragraph">`
274
273
  * * If returned `false` - to a `<p>`
275
274
  *
276
275
  * @param {module:engine/model/element~Element} modelElement
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+
6
+ /**
7
+ * @module table/converters/table-caption-post-fixer
8
+ */
9
+
10
+ /**
11
+ * Injects a table caption post-fixer into the model.
12
+ *
13
+ * The role of the table caption post-fixer is to ensure that the table with caption have the correct structure
14
+ * after a {@link module:engine/model/model~Model#change `change()`} block was executed.
15
+ *
16
+ * The correct structure means that:
17
+ *
18
+ * * If there are many caption model element, they are merged into one model.
19
+ * * A final, merged caption model is placed at the end of the table.
20
+ *
21
+ * @param {module:engine/model/model~Model} model
22
+ */
23
+ export default function injectTableCaptionPostFixer( model ) {
24
+ model.document.registerPostFixer( writer => tableCaptionPostFixer( writer, model ) );
25
+ }
26
+
27
+ // The table caption post-fixer.
28
+ //
29
+ // @param {module:engine/model/writer~Writer} writer
30
+ // @param {module:engine/model/model~Model} model
31
+ function tableCaptionPostFixer( writer, model ) {
32
+ const changes = model.document.differ.getChanges();
33
+ let wasFixed = false;
34
+
35
+ for ( const entry of changes ) {
36
+ if ( entry.type != 'insert' ) {
37
+ continue;
38
+ }
39
+
40
+ const positionParent = entry.position.parent;
41
+
42
+ if ( positionParent.is( 'element', 'table' ) || entry.name == 'table' ) {
43
+ const table = entry.name == 'table' ? entry.position.nodeAfter : entry.position.parent;
44
+ const captionsToMerge = Array.from( table.getChildren() ).filter( child => child.is( 'element', 'caption' ) );
45
+ const firstCaption = captionsToMerge.shift();
46
+
47
+ if ( !firstCaption ) {
48
+ continue;
49
+ }
50
+
51
+ // Move all the contents of the captions to the first one.
52
+ for ( const caption of captionsToMerge ) {
53
+ writer.move( writer.createRangeIn( caption ), firstCaption, 'end' );
54
+ writer.remove( caption );
55
+ }
56
+
57
+ // Make sure the final caption is at the end of the table.
58
+ if ( firstCaption.nextSibling ) {
59
+ writer.move( writer.createRangeOn( firstCaption ), table, 'end' );
60
+ wasFixed = true;
61
+ }
62
+
63
+ // Do we merged captions and/or moved the single caption to the end of the table?
64
+ wasFixed = !!captionsToMerge.length || wasFixed;
65
+ }
66
+ }
67
+
68
+ return wasFixed;
69
+ }
@@ -70,7 +70,9 @@ function fixTable( table, writer ) {
70
70
  let wasFixed = false;
71
71
 
72
72
  for ( const row of table.getChildren() ) {
73
- wasFixed = fixTableRow( row, writer ) || wasFixed;
73
+ if ( row.is( 'element', 'tableRow' ) ) {
74
+ wasFixed = fixTableRow( row, writer ) || wasFixed;
75
+ }
74
76
  }
75
77
 
76
78
  return wasFixed;
@@ -291,12 +291,13 @@ function fixTableCellsRowspan( table, writer ) {
291
291
  function fixTableRowsSizes( table, writer ) {
292
292
  let wasFixed = false;
293
293
 
294
- const rowsLengths = getRowsLengths( table );
294
+ const childrenLengths = getChildrenLengths( table );
295
295
  const rowsToRemove = [];
296
296
 
297
297
  // Find empty rows.
298
- for ( const [ rowIndex, size ] of rowsLengths.entries() ) {
299
- if ( !size ) {
298
+ for ( const [ rowIndex, size ] of childrenLengths.entries() ) {
299
+ // Ignore all non-row models.
300
+ if ( !size && table.getChild( rowIndex ).is( 'element', 'tableRow' ) ) {
300
301
  rowsToRemove.push( rowIndex );
301
302
  }
302
303
  }
@@ -309,10 +310,13 @@ function fixTableRowsSizes( table, writer ) {
309
310
 
310
311
  for ( const rowIndex of rowsToRemove.reverse() ) {
311
312
  writer.remove( table.getChild( rowIndex ) );
312
- rowsLengths.splice( rowIndex, 1 );
313
+ childrenLengths.splice( rowIndex, 1 );
313
314
  }
314
315
  }
315
316
 
317
+ // Filter out everything that's not a table row.
318
+ const rowsLengths = childrenLengths.filter( ( row, rowIndex ) => table.getChild( rowIndex ).is( 'element', 'tableRow' ) );
319
+
316
320
  // Verify if all the rows have the same number of columns.
317
321
  const tableSize = rowsLengths[ 0 ];
318
322
  const isValid = rowsLengths.every( length => length === tableSize );
@@ -346,7 +350,8 @@ function fixTableRowsSizes( table, writer ) {
346
350
  // @returns {Array.<{{cell, rowspan}}>}
347
351
  function findCellsToTrim( table ) {
348
352
  const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
349
- const maxRows = table.childCount;
353
+ const maxRows = Array.from( table.getChildren() )
354
+ .reduce( ( count, row ) => row.is( 'element', 'tableRow' ) ? count + 1 : count, 0 );
350
355
 
351
356
  const cellsToTrim = [];
352
357
 
@@ -376,12 +381,12 @@ function findCellsToTrim( table ) {
376
381
  //
377
382
  // @param {module:engine/model/element~Element} table
378
383
  // @returns {Array.<Number>}
379
- function getRowsLengths( table ) {
384
+ function getChildrenLengths( table ) {
380
385
  // TableWalker will not provide items for the empty rows, we need to pre-fill this array.
381
386
  const lengths = new Array( table.childCount ).fill( 0 );
382
387
 
383
- for ( const { row } of new TableWalker( table, { includeAllSlots: true } ) ) {
384
- lengths[ row ]++;
388
+ for ( const { rowIndex } of new TableWalker( table, { includeAllSlots: true } ) ) {
389
+ lengths[ rowIndex ]++;
385
390
  }
386
391
 
387
392
  return lengths;
@@ -11,21 +11,33 @@
11
11
  * Conversion helper for upcasting attributes using normalized styles.
12
12
  *
13
13
  * @param {module:engine/conversion/conversion~Conversion} conversion
14
- * @param {String} modelElement
15
- * @param {String} modelAttribute
16
- * @param {String} styleName
14
+ * @param {Object} options
15
+ * @param {String} options.modelAttribute The attribute to set.
16
+ * @param {String} options.styleName The style name to convert.
17
+ * @param {String} options.viewElement The view element name that should be converted.
18
+ * @param {String} options.defaultValue The default value for the specified `modelAttribute`.
19
+ * @param {Boolean} [options.reduceBoxSides=false]
17
20
  */
18
- export function upcastStyleToAttribute( conversion, modelElement, modelAttribute, styleName ) {
21
+ export function upcastStyleToAttribute( conversion, options ) {
22
+ const { viewElement, defaultValue, modelAttribute, styleName, reduceBoxSides = false } = options;
23
+
19
24
  conversion.for( 'upcast' ).attributeToAttribute( {
20
25
  view: {
26
+ name: viewElement,
21
27
  styles: {
22
28
  [ styleName ]: /[\s\S]+/
23
29
  }
24
30
  },
25
31
  model: {
26
- name: modelElement,
27
32
  key: modelAttribute,
28
- value: viewElement => viewElement.getNormalizedStyle( styleName )
33
+ value: viewElement => {
34
+ const normalized = viewElement.getNormalizedStyle( styleName );
35
+ const value = reduceBoxSides ? reduceBoxSidesValue( normalized ) : normalized;
36
+
37
+ if ( defaultValue !== value ) {
38
+ return value;
39
+ }
40
+ }
29
41
  }
30
42
  } );
31
43
  }
@@ -35,8 +47,12 @@ export function upcastStyleToAttribute( conversion, modelElement, modelAttribute
35
47
  *
36
48
  * @param {module:engine/conversion/conversion~Conversion} conversion
37
49
  * @param {String} viewElementName
50
+ * @param {Object} defaultBorder The default border values.
51
+ * @param {String} defaultBorder.color The default `borderColor` value.
52
+ * @param {String} defaultBorder.style The default `borderStyle` value.
53
+ * @param {String} defaultBorder.width The default `borderWidth` value.
38
54
  */
39
- export function upcastBorderStyles( conversion, viewElementName ) {
55
+ export function upcastBorderStyles( conversion, viewElementName, defaultBorder ) {
40
56
  conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:' + viewElementName, ( evt, data, conversionApi ) => {
41
57
  // If the element was not converted by element-to-element converter,
42
58
  // we should not try to convert the style. See #8393.
@@ -44,13 +60,21 @@ export function upcastBorderStyles( conversion, viewElementName ) {
44
60
  return;
45
61
  }
46
62
 
47
- // TODO: this is counter-intuitive: ie.: if only `border-top` is defined then `hasStyle( 'border' )` also returns true.
48
- // TODO: this might needs to be fixed in styles normalizer.
63
+ // Check the most detailed properties. These will be always set directly or
64
+ // when using the "group" properties like: `border-(top|right|bottom|left)` or `border`.
49
65
  const stylesToConsume = [
50
- 'border-top',
51
- 'border-right',
52
- 'border-bottom',
53
- 'border-left'
66
+ 'border-top-width',
67
+ 'border-top-color',
68
+ 'border-top-style',
69
+ 'border-bottom-width',
70
+ 'border-bottom-color',
71
+ 'border-bottom-style',
72
+ 'border-right-width',
73
+ 'border-right-color',
74
+ 'border-right-style',
75
+ 'border-left-width',
76
+ 'border-left-color',
77
+ 'border-left-style'
54
78
  ].filter( styleName => data.viewItem.hasStyle( styleName ) );
55
79
 
56
80
  if ( !stylesToConsume.length ) {
@@ -70,9 +94,29 @@ export function upcastBorderStyles( conversion, viewElementName ) {
70
94
 
71
95
  conversionApi.consumable.consume( data.viewItem, matcherPattern );
72
96
 
73
- conversionApi.writer.setAttribute( 'borderStyle', data.viewItem.getNormalizedStyle( 'border-style' ), modelElement );
74
- conversionApi.writer.setAttribute( 'borderColor', data.viewItem.getNormalizedStyle( 'border-color' ), modelElement );
75
- conversionApi.writer.setAttribute( 'borderWidth', data.viewItem.getNormalizedStyle( 'border-width' ), modelElement );
97
+ const normalizedBorder = {
98
+ style: data.viewItem.getNormalizedStyle( 'border-style' ),
99
+ color: data.viewItem.getNormalizedStyle( 'border-color' ),
100
+ width: data.viewItem.getNormalizedStyle( 'border-width' )
101
+ };
102
+
103
+ const reducedBorder = {
104
+ style: reduceBoxSidesValue( normalizedBorder.style ),
105
+ color: reduceBoxSidesValue( normalizedBorder.color ),
106
+ width: reduceBoxSidesValue( normalizedBorder.width )
107
+ };
108
+
109
+ if ( reducedBorder.style !== defaultBorder.style ) {
110
+ conversionApi.writer.setAttribute( 'borderStyle', reducedBorder.style, modelElement );
111
+ }
112
+
113
+ if ( reducedBorder.color !== defaultBorder.color ) {
114
+ conversionApi.writer.setAttribute( 'borderColor', reducedBorder.color, modelElement );
115
+ }
116
+
117
+ if ( reducedBorder.width !== defaultBorder.width ) {
118
+ conversionApi.writer.setAttribute( 'borderWidth', reducedBorder.width, modelElement );
119
+ }
76
120
  } ) );
77
121
  }
78
122
 
@@ -80,11 +124,12 @@ export function upcastBorderStyles( conversion, viewElementName ) {
80
124
  * Conversion helper for downcasting an attribute to a style.
81
125
  *
82
126
  * @param {module:engine/conversion/conversion~Conversion} conversion
83
- * @param {String} modelElement
84
- * @param {String} modelAttribute
85
- * @param {String} styleName
127
+ * @param {Object} options
128
+ * @param {String} options.modelElement
129
+ * @param {String} options.modelAttribute
130
+ * @param {String} options.styleName
86
131
  */
87
- export function downcastAttributeToStyle( conversion, modelElement, modelAttribute, styleName ) {
132
+ export function downcastAttributeToStyle( conversion, { modelElement, modelAttribute, styleName } ) {
88
133
  conversion.for( 'downcast' ).attributeToAttribute( {
89
134
  model: {
90
135
  name: modelElement,
@@ -103,10 +148,11 @@ export function downcastAttributeToStyle( conversion, modelElement, modelAttribu
103
148
  * Conversion helper for downcasting attributes from the model table to a view table (not to `<figure>`).
104
149
  *
105
150
  * @param {module:engine/conversion/conversion~Conversion} conversion
106
- * @param {String} modelAttribute
107
- * @param {String} styleName
151
+ * @param {Object} options
152
+ * @param {String} options.modelAttribute
153
+ * @param {String} options.styleName
108
154
  */
109
- export function downcastTableAttribute( conversion, modelAttribute, styleName ) {
155
+ export function downcastTableAttribute( conversion, { modelAttribute, styleName } ) {
110
156
  conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( `attribute:${ modelAttribute }:table`, ( evt, data, conversionApi ) => {
111
157
  const { item, attributeNewValue } = data;
112
158
  const { mapper, writer } = conversionApi;
@@ -124,3 +170,16 @@ export function downcastTableAttribute( conversion, modelAttribute, styleName )
124
170
  }
125
171
  } ) );
126
172
  }
173
+
174
+ // Reduces the full top, right, bottom, left object to a single string if all sides are equal.
175
+ function reduceBoxSidesValue( style ) {
176
+ if ( !style ) {
177
+ return;
178
+ }
179
+
180
+ const commonValue = [ 'top', 'right', 'bottom', 'left' ]
181
+ .map( side => style[ side ] )
182
+ .reduce( ( result, side ) => result == side ? result : null );
183
+
184
+ return commonValue || style;
185
+ }