@ckeditor/ckeditor5-table 32.0.0 → 34.1.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.
- package/LICENSE.md +2 -2
- package/README.md +2 -1
- package/build/table.js +2 -2
- package/build/translations/el.js +1 -0
- package/build/translations/en-au.js +1 -1
- package/build/translations/es.js +1 -1
- package/build/translations/hr.js +1 -1
- package/build/translations/lv.js +1 -1
- package/build/translations/sk.js +1 -1
- package/build/translations/ur.js +1 -0
- package/ckeditor5-metadata.json +19 -0
- package/lang/translations/el.po +261 -0
- package/lang/translations/en-au.po +3 -3
- package/lang/translations/es.po +32 -32
- package/lang/translations/hr.po +3 -3
- package/lang/translations/lv.po +40 -40
- package/lang/translations/sk.po +3 -3
- package/lang/translations/ur.po +261 -0
- package/package.json +26 -21
- package/src/commands/insertcolumncommand.js +4 -4
- package/src/commands/insertrowcommand.js +4 -4
- package/src/commands/inserttablecommand.js +1 -5
- package/src/commands/mergecellcommand.js +4 -5
- package/src/commands/mergecellscommand.js +5 -4
- package/src/commands/removecolumncommand.js +8 -7
- package/src/commands/removerowcommand.js +5 -6
- package/src/commands/selectcolumncommand.js +4 -4
- package/src/commands/selectrowcommand.js +5 -5
- package/src/commands/setheadercolumncommand.js +5 -4
- package/src/commands/setheaderrowcommand.js +7 -4
- package/src/commands/splitcellcommand.js +4 -4
- package/src/converters/downcast.js +76 -407
- package/src/converters/{table-cell-refresh-post-fixer.js → table-cell-refresh-handler.js} +8 -19
- package/src/converters/table-headings-refresh-handler.js +68 -0
- package/src/index.js +3 -0
- package/src/plaintableoutput.js +151 -0
- package/src/tablecellproperties/commands/tablecellpropertycommand.js +4 -3
- package/src/tableclipboard.js +18 -15
- package/src/tablecolumnresize/constants.js +32 -0
- package/src/tablecolumnresize/converters.js +126 -0
- package/src/tablecolumnresize/tablecolumnresizeediting.js +758 -0
- package/src/tablecolumnresize/utils.js +367 -0
- package/src/tablecolumnresize.js +36 -0
- package/src/tableediting.js +51 -32
- package/src/tablekeyboard.js +73 -70
- package/src/tablemouse.js +6 -4
- package/src/tableselection.js +9 -8
- package/src/tableutils.js +310 -0
- package/theme/table.css +1 -1
- package/theme/tablecolumnresize.css +59 -0
- package/src/converters/table-heading-rows-refresh-post-fixer.js +0 -72
- package/src/utils/selection.js +0 -276
|
@@ -13,123 +13,57 @@ import { toWidget, toWidgetEditable } from 'ckeditor5/src/widget';
|
|
|
13
13
|
/**
|
|
14
14
|
* Model table element to view table element conversion helper.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @param {
|
|
19
|
-
* @
|
|
20
|
-
* @returns {Function} Conversion helper.
|
|
16
|
+
* @param {module:table/tableutils~TableUtils} tableUtils The `TableUtils` plugin instance.
|
|
17
|
+
* @param {Object} [options]
|
|
18
|
+
* @param {Boolean} [options.asWidget] If set to `true`, the downcast conversion will produce a widget.
|
|
19
|
+
* @returns {Function} Element creator.
|
|
21
20
|
*/
|
|
22
|
-
export function
|
|
23
|
-
return
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
export function downcastTable( tableUtils, options = {} ) {
|
|
22
|
+
return ( table, { writer } ) => {
|
|
23
|
+
const headingRows = table.getAttribute( 'headingRows' ) || 0;
|
|
24
|
+
const tableSections = [];
|
|
25
|
+
|
|
26
|
+
// Table head slot.
|
|
27
|
+
if ( headingRows > 0 ) {
|
|
28
|
+
tableSections.push(
|
|
29
|
+
writer.createContainerElement( 'thead', null,
|
|
30
|
+
writer.createSlot( element => element.is( 'element', 'tableRow' ) && element.index < headingRows )
|
|
31
|
+
)
|
|
32
|
+
);
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const tableElement = conversionApi.writer.createContainerElement( 'table' );
|
|
38
|
-
conversionApi.writer.insert( conversionApi.writer.createPositionAt( figureElement, 0 ), tableElement );
|
|
39
|
-
|
|
40
|
-
let tableWidget;
|
|
41
|
-
|
|
42
|
-
if ( asWidget ) {
|
|
43
|
-
tableWidget = toTableWidget( figureElement, conversionApi.writer );
|
|
35
|
+
// Table body slot.
|
|
36
|
+
if ( headingRows < tableUtils.getRows( table ) ) {
|
|
37
|
+
tableSections.push(
|
|
38
|
+
writer.createContainerElement( 'tbody', null,
|
|
39
|
+
writer.createSlot( element => element.is( 'element', 'tableRow' ) && element.index >= headingRows )
|
|
40
|
+
)
|
|
41
|
+
);
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
headingRows: table.getAttribute( 'headingRows' ) || 0,
|
|
50
|
-
headingColumns: table.getAttribute( 'headingColumns' ) || 0
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Cache for created table rows.
|
|
54
|
-
const viewRows = new Map();
|
|
44
|
+
const figureElement = writer.createContainerElement( 'figure', { class: 'table' }, [
|
|
45
|
+
// Table with proper sections (thead, tbody).
|
|
46
|
+
writer.createContainerElement( 'table', null, tableSections ),
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const tableRow = table.getChild( row );
|
|
60
|
-
const trElement = viewRows.get( row ) || createTr( tableElement, tableRow, row, tableAttributes, conversionApi );
|
|
61
|
-
viewRows.set( row, trElement );
|
|
62
|
-
|
|
63
|
-
// Consume table cell - it will be always consumed as we convert whole table at once.
|
|
64
|
-
conversionApi.consumable.consume( cell, 'insert' );
|
|
48
|
+
// Slot for the rest (for example caption).
|
|
49
|
+
writer.createSlot( element => !element.is( 'element', 'tableRow' ) )
|
|
50
|
+
] );
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Insert empty TR elements if there are any rows without anchored cells. Since the model is always normalized
|
|
72
|
-
// this can happen only in the document fragment that only part of the table is down-casted.
|
|
73
|
-
for ( const tableRow of table.getChildren() ) {
|
|
74
|
-
const rowIndex = tableRow.index;
|
|
75
|
-
|
|
76
|
-
// Make sure that this is a table row and not some other element (i.e., caption).
|
|
77
|
-
if ( tableRow.is( 'element', 'tableRow' ) && !viewRows.has( rowIndex ) ) {
|
|
78
|
-
viewRows.set( rowIndex, createTr( tableElement, tableRow, rowIndex, tableAttributes, conversionApi ) );
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
|
|
83
|
-
|
|
84
|
-
conversionApi.mapper.bindElements( table, asWidget ? tableWidget : figureElement );
|
|
85
|
-
conversionApi.writer.insert( viewPosition, asWidget ? tableWidget : figureElement );
|
|
86
|
-
} );
|
|
52
|
+
return options.asWidget ? toTableWidget( figureElement, writer ) : figureElement;
|
|
53
|
+
};
|
|
87
54
|
}
|
|
88
55
|
|
|
89
56
|
/**
|
|
90
|
-
* Model row element to view `<tr>` element conversion helper.
|
|
91
|
-
*
|
|
92
|
-
* This conversion helper creates the whole `<tr>` element with child elements.
|
|
57
|
+
* Model table row element to view `<tr>` element conversion helper.
|
|
93
58
|
*
|
|
94
|
-
* @returns {Function}
|
|
59
|
+
* @returns {Function} Element creator.
|
|
95
60
|
*/
|
|
96
|
-
export function
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const table = tableRow.parent;
|
|
105
|
-
|
|
106
|
-
const figureElement = conversionApi.mapper.toViewElement( table );
|
|
107
|
-
const tableElement = getViewTable( figureElement );
|
|
108
|
-
|
|
109
|
-
const row = table.getChildIndex( tableRow );
|
|
110
|
-
|
|
111
|
-
const tableWalker = new TableWalker( table, { row } );
|
|
112
|
-
|
|
113
|
-
const tableAttributes = {
|
|
114
|
-
headingRows: table.getAttribute( 'headingRows' ) || 0,
|
|
115
|
-
headingColumns: table.getAttribute( 'headingColumns' ) || 0
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Cache for created table rows.
|
|
119
|
-
const viewRows = new Map();
|
|
120
|
-
|
|
121
|
-
for ( const tableSlot of tableWalker ) {
|
|
122
|
-
const trElement = viewRows.get( row ) || createTr( tableElement, tableRow, row, tableAttributes, conversionApi );
|
|
123
|
-
viewRows.set( row, trElement );
|
|
124
|
-
|
|
125
|
-
// Consume table cell - it will be always consumed as we convert whole row at once.
|
|
126
|
-
conversionApi.consumable.consume( tableSlot.cell, 'insert' );
|
|
127
|
-
|
|
128
|
-
const insertPosition = conversionApi.writer.createPositionAt( trElement, 'end' );
|
|
129
|
-
|
|
130
|
-
createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, { asWidget: true } );
|
|
131
|
-
}
|
|
132
|
-
} );
|
|
61
|
+
export function downcastRow() {
|
|
62
|
+
return ( tableRow, { writer } ) => {
|
|
63
|
+
return tableRow.isEmpty ?
|
|
64
|
+
writer.createEmptyElement( 'tr' ) :
|
|
65
|
+
writer.createContainerElement( 'tr' );
|
|
66
|
+
};
|
|
133
67
|
}
|
|
134
68
|
|
|
135
69
|
/**
|
|
@@ -138,129 +72,65 @@ export function downcastInsertRow() {
|
|
|
138
72
|
* This conversion helper will create proper `<th>` elements for table cells that are in the heading section (heading row or column)
|
|
139
73
|
* and `<td>` otherwise.
|
|
140
74
|
*
|
|
141
|
-
* @
|
|
75
|
+
* @param {Object} [options]
|
|
76
|
+
* @param {Boolean} [options.asWidget] If set to `true`, the downcast conversion will produce a widget.
|
|
77
|
+
* @returns {Function} Element creator.
|
|
142
78
|
*/
|
|
143
|
-
export function
|
|
144
|
-
return
|
|
145
|
-
const tableCell = data.item;
|
|
146
|
-
|
|
147
|
-
if ( !conversionApi.consumable.consume( tableCell, 'insert' ) ) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
79
|
+
export function downcastCell( options = {} ) {
|
|
80
|
+
return ( tableCell, { writer } ) => {
|
|
151
81
|
const tableRow = tableCell.parent;
|
|
152
82
|
const table = tableRow.parent;
|
|
153
83
|
const rowIndex = table.getChildIndex( tableRow );
|
|
154
84
|
|
|
155
85
|
const tableWalker = new TableWalker( table, { row: rowIndex } );
|
|
86
|
+
const headingRows = table.getAttribute( 'headingRows' ) || 0;
|
|
87
|
+
const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
|
|
156
88
|
|
|
157
|
-
|
|
158
|
-
headingRows: table.getAttribute( 'headingRows' ) || 0,
|
|
159
|
-
headingColumns: table.getAttribute( 'headingColumns' ) || 0
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// We need to iterate over a table in order to get proper row & column values from a walker
|
|
89
|
+
// We need to iterate over a table in order to get proper row & column values from a walker.
|
|
163
90
|
for ( const tableSlot of tableWalker ) {
|
|
164
|
-
if ( tableSlot.cell
|
|
165
|
-
const
|
|
166
|
-
const
|
|
91
|
+
if ( tableSlot.cell == tableCell ) {
|
|
92
|
+
const isHeading = tableSlot.row < headingRows || tableSlot.column < headingColumns;
|
|
93
|
+
const cellElementName = isHeading ? 'th' : 'td';
|
|
167
94
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return;
|
|
95
|
+
return options.asWidget ?
|
|
96
|
+
toWidgetEditable( writer.createEditableElement( cellElementName ), writer ) :
|
|
97
|
+
writer.createContainerElement( cellElementName );
|
|
172
98
|
}
|
|
173
99
|
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Conversion helper that acts on heading column table attribute change.
|
|
179
|
-
*
|
|
180
|
-
* Depending on changed attributes this converter will rename `<td` to `<th>` elements or vice versa depending on the cell column index.
|
|
181
|
-
*
|
|
182
|
-
* @returns {Function} Conversion helper.
|
|
183
|
-
*/
|
|
184
|
-
export function downcastTableHeadingColumnsChange() {
|
|
185
|
-
return dispatcher => dispatcher.on( 'attribute:headingColumns:table', ( evt, data, conversionApi ) => {
|
|
186
|
-
const table = data.item;
|
|
187
|
-
|
|
188
|
-
if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const tableAttributes = {
|
|
193
|
-
headingRows: table.getAttribute( 'headingRows' ) || 0,
|
|
194
|
-
headingColumns: table.getAttribute( 'headingColumns' ) || 0
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const oldColumns = data.attributeOldValue;
|
|
198
|
-
const newColumns = data.attributeNewValue;
|
|
199
|
-
|
|
200
|
-
const lastColumnToCheck = ( oldColumns > newColumns ? oldColumns : newColumns ) - 1;
|
|
201
|
-
|
|
202
|
-
for ( const tableSlot of new TableWalker( table, { endColumn: lastColumnToCheck } ) ) {
|
|
203
|
-
renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi );
|
|
204
|
-
}
|
|
205
|
-
} );
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Conversion helper that acts on a removed row.
|
|
210
|
-
*
|
|
211
|
-
* @returns {Function} Conversion helper.
|
|
212
|
-
*/
|
|
213
|
-
export function downcastRemoveRow() {
|
|
214
|
-
return dispatcher => dispatcher.on( 'remove:tableRow', ( evt, data, conversionApi ) => {
|
|
215
|
-
// Prevent default remove converter.
|
|
216
|
-
evt.stop();
|
|
217
|
-
const viewWriter = conversionApi.writer;
|
|
218
|
-
const mapper = conversionApi.mapper;
|
|
219
|
-
|
|
220
|
-
const viewStart = mapper.toViewPosition( data.position ).getLastMatchingPosition( value => !value.item.is( 'element', 'tr' ) );
|
|
221
|
-
const viewItem = viewStart.nodeAfter;
|
|
222
|
-
const tableSection = viewItem.parent;
|
|
223
|
-
const viewTable = tableSection.parent;
|
|
224
|
-
|
|
225
|
-
// Remove associated <tr> from the view.
|
|
226
|
-
const removeRange = viewWriter.createRangeOn( viewItem );
|
|
227
|
-
const removed = viewWriter.remove( removeRange );
|
|
228
|
-
|
|
229
|
-
for ( const child of viewWriter.createRangeIn( removed ).getItems() ) {
|
|
230
|
-
mapper.unbindViewElement( child );
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Cleanup: Ensure that thead & tbody sections are removed if left empty after removing rows. See #6437, #6391.
|
|
234
|
-
removeTableSectionIfEmpty( 'thead', viewTable, conversionApi );
|
|
235
|
-
removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi );
|
|
236
|
-
}, { priority: 'higher' } );
|
|
100
|
+
};
|
|
237
101
|
}
|
|
238
102
|
|
|
239
103
|
/**
|
|
240
104
|
* Overrides paragraph inside table cell conversion.
|
|
241
105
|
*
|
|
242
106
|
* This converter:
|
|
243
|
-
* * should be used to override default paragraph conversion
|
|
244
|
-
* * It will only convert
|
|
107
|
+
* * should be used to override default paragraph conversion.
|
|
108
|
+
* * It will only convert `<paragraph>` placed directly inside `<tableCell>`.
|
|
245
109
|
* * For a single paragraph without attributes it returns `<span>` to simulate data table.
|
|
246
110
|
* * For all other cases it returns `<p>` element.
|
|
247
111
|
*
|
|
248
|
-
* @param {
|
|
249
|
-
* @param {
|
|
250
|
-
* @returns {
|
|
112
|
+
* @param {Object} [options]
|
|
113
|
+
* @param {Boolean} [options.asWidget] If set to `true`, the downcast conversion will produce a widget.
|
|
114
|
+
* @returns {Function} Element creator.
|
|
251
115
|
*/
|
|
252
|
-
export function convertParagraphInTableCell(
|
|
253
|
-
|
|
116
|
+
export function convertParagraphInTableCell( options = {} ) {
|
|
117
|
+
return ( modelElement, { writer, consumable, mapper } ) => {
|
|
118
|
+
if ( !modelElement.parent.is( 'element', 'tableCell' ) ) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
254
121
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
122
|
+
if ( !isSingleParagraphWithoutAttributes( modelElement ) ) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
258
125
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
126
|
+
if ( options.asWidget ) {
|
|
127
|
+
return writer.createContainerElement( 'span', { class: 'ck-table-bogus-paragraph' } );
|
|
128
|
+
} else {
|
|
129
|
+
// Additional requirement for data pipeline to have backward compatible data tables.
|
|
130
|
+
consumable.consume( modelElement, 'insert' );
|
|
131
|
+
mapper.bindElements( modelElement, mapper.toViewElement( modelElement.parent ) );
|
|
132
|
+
}
|
|
133
|
+
};
|
|
264
134
|
}
|
|
265
135
|
|
|
266
136
|
/**
|
|
@@ -277,7 +147,7 @@ export function convertParagraphInTableCell( modelElement, conversionApi ) {
|
|
|
277
147
|
export function isSingleParagraphWithoutAttributes( modelElement ) {
|
|
278
148
|
const tableCell = modelElement.parent;
|
|
279
149
|
|
|
280
|
-
const isSingleParagraph = tableCell.childCount
|
|
150
|
+
const isSingleParagraph = tableCell.childCount == 1;
|
|
281
151
|
|
|
282
152
|
return isSingleParagraph && !hasAnyAttribute( modelElement );
|
|
283
153
|
}
|
|
@@ -296,207 +166,6 @@ function toTableWidget( viewElement, writer ) {
|
|
|
296
166
|
return toWidget( viewElement, writer, { hasSelectionHandle: true } );
|
|
297
167
|
}
|
|
298
168
|
|
|
299
|
-
// Renames an existing table cell in the view to a given element name.
|
|
300
|
-
//
|
|
301
|
-
// **Note** This method will not do anything if a view table cell has not been converted yet.
|
|
302
|
-
//
|
|
303
|
-
// @param {module:engine/model/element~Element} tableCell
|
|
304
|
-
// @param {String} desiredCellElementName
|
|
305
|
-
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
|
|
306
|
-
function renameViewTableCell( tableCell, desiredCellElementName, conversionApi ) {
|
|
307
|
-
const viewWriter = conversionApi.writer;
|
|
308
|
-
const viewCell = conversionApi.mapper.toViewElement( tableCell );
|
|
309
|
-
|
|
310
|
-
const editable = viewWriter.createEditableElement( desiredCellElementName, viewCell.getAttributes() );
|
|
311
|
-
const renamedCell = toWidgetEditable( editable, viewWriter );
|
|
312
|
-
|
|
313
|
-
viewWriter.insert( viewWriter.createPositionAfter( viewCell ), renamedCell );
|
|
314
|
-
viewWriter.move( viewWriter.createRangeIn( viewCell ), viewWriter.createPositionAt( renamedCell, 0 ) );
|
|
315
|
-
viewWriter.remove( viewWriter.createRangeOn( viewCell ) );
|
|
316
|
-
|
|
317
|
-
conversionApi.mapper.unbindViewElement( viewCell );
|
|
318
|
-
conversionApi.mapper.bindElements( tableCell, renamedCell );
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Renames a table cell element in the view according to its location in the table.
|
|
322
|
-
//
|
|
323
|
-
// @param {module:table/tablewalker~TableSlot} tableSlot
|
|
324
|
-
// @param {{headingColumns, headingRows}} tableAttributes
|
|
325
|
-
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
|
|
326
|
-
function renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi ) {
|
|
327
|
-
const { cell } = tableSlot;
|
|
328
|
-
|
|
329
|
-
// Check whether current columnIndex is overlapped by table cells from previous rows.
|
|
330
|
-
const desiredCellElementName = getCellElementName( tableSlot, tableAttributes );
|
|
331
|
-
|
|
332
|
-
const viewCell = conversionApi.mapper.toViewElement( cell );
|
|
333
|
-
|
|
334
|
-
// If in single change we're converting attribute changes and inserting cell the table cell might not be inserted into view
|
|
335
|
-
// because of child conversion is done after parent.
|
|
336
|
-
if ( viewCell && viewCell.name !== desiredCellElementName ) {
|
|
337
|
-
renameViewTableCell( cell, desiredCellElementName, conversionApi );
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Creates a table cell element in the view.
|
|
342
|
-
//
|
|
343
|
-
// @param {module:table/tablewalker~TableSlot} tableSlot
|
|
344
|
-
// @param {module:engine/view/position~Position} insertPosition
|
|
345
|
-
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
|
|
346
|
-
function createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options ) {
|
|
347
|
-
const asWidget = options && options.asWidget;
|
|
348
|
-
const cellElementName = getCellElementName( tableSlot, tableAttributes );
|
|
349
|
-
|
|
350
|
-
const cellElement = asWidget ?
|
|
351
|
-
toWidgetEditable( conversionApi.writer.createEditableElement( cellElementName ), conversionApi.writer ) :
|
|
352
|
-
conversionApi.writer.createContainerElement( cellElementName );
|
|
353
|
-
|
|
354
|
-
const tableCell = tableSlot.cell;
|
|
355
|
-
|
|
356
|
-
const firstChild = tableCell.getChild( 0 );
|
|
357
|
-
const isSingleParagraph = tableCell.childCount === 1 && firstChild.name === 'paragraph';
|
|
358
|
-
|
|
359
|
-
conversionApi.writer.insert( insertPosition, cellElement );
|
|
360
|
-
|
|
361
|
-
conversionApi.mapper.bindElements( tableCell, cellElement );
|
|
362
|
-
|
|
363
|
-
// Additional requirement for data pipeline to have backward compatible data tables.
|
|
364
|
-
if ( !asWidget && isSingleParagraph && !hasAnyAttribute( firstChild ) ) {
|
|
365
|
-
const innerParagraph = tableCell.getChild( 0 );
|
|
366
|
-
|
|
367
|
-
conversionApi.consumable.consume( innerParagraph, 'insert' );
|
|
368
|
-
|
|
369
|
-
conversionApi.mapper.bindElements( innerParagraph, cellElement );
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Creates a `<tr>` view element.
|
|
374
|
-
//
|
|
375
|
-
// @param {module:engine/view/element~Element} tableElement
|
|
376
|
-
// @param {module:engine/model/element~Element} tableRow
|
|
377
|
-
// @param {Number} rowIndex
|
|
378
|
-
// @param {{headingColumns, headingRows}} tableAttributes
|
|
379
|
-
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
|
|
380
|
-
// @returns {module:engine/view/element~Element}
|
|
381
|
-
function createTr( tableElement, tableRow, rowIndex, tableAttributes, conversionApi ) {
|
|
382
|
-
// Will always consume since we're converting <tableRow> element from a parent <table>.
|
|
383
|
-
conversionApi.consumable.consume( tableRow, 'insert' );
|
|
384
|
-
|
|
385
|
-
const trElement = tableRow.isEmpty ?
|
|
386
|
-
conversionApi.writer.createEmptyElement( 'tr' ) :
|
|
387
|
-
conversionApi.writer.createContainerElement( 'tr' );
|
|
388
|
-
|
|
389
|
-
conversionApi.mapper.bindElements( tableRow, trElement );
|
|
390
|
-
|
|
391
|
-
const headingRows = tableAttributes.headingRows;
|
|
392
|
-
const tableSection = getOrCreateTableSection( getSectionName( rowIndex, tableAttributes ), tableElement, conversionApi );
|
|
393
|
-
|
|
394
|
-
const offset = headingRows > 0 && rowIndex >= headingRows ? rowIndex - headingRows : rowIndex;
|
|
395
|
-
const position = conversionApi.writer.createPositionAt( tableSection, offset );
|
|
396
|
-
|
|
397
|
-
conversionApi.writer.insert( position, trElement );
|
|
398
|
-
|
|
399
|
-
return trElement;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Returns `th` for heading cells and `td` for other cells for the current table walker value.
|
|
403
|
-
//
|
|
404
|
-
// @param {module:table/tablewalker~TableSlot} tableSlot
|
|
405
|
-
// @param {{headingColumns, headingRows}} tableAttributes
|
|
406
|
-
// @returns {String}
|
|
407
|
-
function getCellElementName( tableSlot, tableAttributes ) {
|
|
408
|
-
const { row, column } = tableSlot;
|
|
409
|
-
const { headingColumns, headingRows } = tableAttributes;
|
|
410
|
-
|
|
411
|
-
// Column heading are all tableCells in the first `columnHeading` rows.
|
|
412
|
-
const isColumnHeading = headingRows && headingRows > row;
|
|
413
|
-
|
|
414
|
-
// So a whole row gets <th> element.
|
|
415
|
-
if ( isColumnHeading ) {
|
|
416
|
-
return 'th';
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Row heading are tableCells which columnIndex is lower then headingColumns.
|
|
420
|
-
const isRowHeading = headingColumns && headingColumns > column;
|
|
421
|
-
|
|
422
|
-
return isRowHeading ? 'th' : 'td';
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Returns the table section name for the current table walker value.
|
|
426
|
-
//
|
|
427
|
-
// @param {Number} row
|
|
428
|
-
// @param {{headingColumns, headingRows}} tableAttributes
|
|
429
|
-
// @returns {String}
|
|
430
|
-
function getSectionName( row, tableAttributes ) {
|
|
431
|
-
return row < tableAttributes.headingRows ? 'thead' : 'tbody';
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Creates or returns an existing `<tbody>` or `<thead>` element with caching.
|
|
435
|
-
//
|
|
436
|
-
// @param {String} sectionName
|
|
437
|
-
// @param {module:engine/view/element~Element} viewTable
|
|
438
|
-
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
|
|
439
|
-
// @param {Object} cachedTableSection An object that stores cached elements.
|
|
440
|
-
// @returns {module:engine/view/containerelement~ContainerElement}
|
|
441
|
-
function getOrCreateTableSection( sectionName, viewTable, conversionApi ) {
|
|
442
|
-
const viewTableSection = getExistingTableSectionElement( sectionName, viewTable );
|
|
443
|
-
|
|
444
|
-
return viewTableSection ? viewTableSection : createTableSection( sectionName, viewTable, conversionApi );
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Finds an existing `<tbody>` or `<thead>` element or returns undefined.
|
|
448
|
-
//
|
|
449
|
-
// @param {String} sectionName
|
|
450
|
-
// @param {module:engine/view/element~Element} tableElement
|
|
451
|
-
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
|
|
452
|
-
function getExistingTableSectionElement( sectionName, tableElement ) {
|
|
453
|
-
for ( const tableSection of tableElement.getChildren() ) {
|
|
454
|
-
if ( tableSection.name == sectionName ) {
|
|
455
|
-
return tableSection;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Creates a table section at the end of the table.
|
|
461
|
-
//
|
|
462
|
-
// @param {String} sectionName
|
|
463
|
-
// @param {module:engine/view/element~Element} tableElement
|
|
464
|
-
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
|
|
465
|
-
// @returns {module:engine/view/containerelement~ContainerElement}
|
|
466
|
-
function createTableSection( sectionName, tableElement, conversionApi ) {
|
|
467
|
-
const tableChildElement = conversionApi.writer.createContainerElement( sectionName );
|
|
468
|
-
|
|
469
|
-
const insertPosition = conversionApi.writer.createPositionAt( tableElement, sectionName == 'tbody' ? 'end' : 0 );
|
|
470
|
-
|
|
471
|
-
conversionApi.writer.insert( insertPosition, tableChildElement );
|
|
472
|
-
|
|
473
|
-
return tableChildElement;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Removes an existing `<tbody>` or `<thead>` element if it is empty.
|
|
477
|
-
//
|
|
478
|
-
// @param {String} sectionName
|
|
479
|
-
// @param {module:engine/view/element~Element} tableElement
|
|
480
|
-
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
|
|
481
|
-
function removeTableSectionIfEmpty( sectionName, tableElement, conversionApi ) {
|
|
482
|
-
const tableSection = getExistingTableSectionElement( sectionName, tableElement );
|
|
483
|
-
|
|
484
|
-
if ( tableSection && tableSection.childCount === 0 ) {
|
|
485
|
-
conversionApi.writer.remove( conversionApi.writer.createRangeOn( tableSection ) );
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Finds a '<table>' element inside the `<figure>` widget.
|
|
490
|
-
//
|
|
491
|
-
// @param {module:engine/view/element~Element} viewFigure
|
|
492
|
-
function getViewTable( viewFigure ) {
|
|
493
|
-
for ( const child of viewFigure.getChildren() ) {
|
|
494
|
-
if ( child.name === 'table' ) {
|
|
495
|
-
return child;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
169
|
// Checks if an element has any attributes set.
|
|
501
170
|
//
|
|
502
171
|
// @param {module:engine/model/element~Element element
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { isSingleParagraphWithoutAttributes } from './downcast';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* A table cell refresh handler which marks the table cell in the differ to have it re-rendered.
|
|
14
14
|
*
|
|
15
15
|
* Model `paragraph` inside a table cell can be rendered as `<span>` or `<p>`. It is rendered as `<span>` if this is the only block
|
|
16
16
|
* element in that table cell and it does not have any attributes. It is rendered as `<p>` otherwise.
|
|
@@ -19,16 +19,12 @@ import { isSingleParagraphWithoutAttributes } from './downcast';
|
|
|
19
19
|
* re-rendered so it changes from `<span>` to `<p>`. The easiest way to do it is to re-render the entire table cell.
|
|
20
20
|
*
|
|
21
21
|
* @param {module:engine/model/model~Model} model
|
|
22
|
-
* @param {module:engine/
|
|
22
|
+
* @param {module:engine/controller/editingcontroller~EditingController} editing
|
|
23
23
|
*/
|
|
24
|
-
export default function
|
|
25
|
-
|
|
26
|
-
}
|
|
24
|
+
export default function tableCellRefreshHandler( model, editing ) {
|
|
25
|
+
const differ = model.document.differ;
|
|
27
26
|
|
|
28
|
-
function tableCellRefreshPostFixer( differ, mapper ) {
|
|
29
27
|
// Stores cells to be refreshed, so the table cell will be refreshed once for multiple changes.
|
|
30
|
-
|
|
31
|
-
// 1. Gather all changes inside table cell.
|
|
32
28
|
const cellsToCheck = new Set();
|
|
33
29
|
|
|
34
30
|
for ( const change of differ.getChanges() ) {
|
|
@@ -39,20 +35,13 @@ function tableCellRefreshPostFixer( differ, mapper ) {
|
|
|
39
35
|
}
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
// @if CK_DEBUG_TABLE // console.log( `Post-fixing table: Checking table cell to refresh (${ cellsToCheck.size }).` );
|
|
43
|
-
// @if CK_DEBUG_TABLE // let paragraphsRefreshed = 0;
|
|
44
|
-
|
|
45
38
|
for ( const tableCell of cellsToCheck.values() ) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
const paragraphsToRefresh = Array.from( tableCell.getChildren() ).filter( child => shouldRefresh( child, editing.mapper ) );
|
|
40
|
+
|
|
41
|
+
for ( const paragraph of paragraphsToRefresh ) {
|
|
42
|
+
editing.reconvertItem( paragraph );
|
|
49
43
|
}
|
|
50
44
|
}
|
|
51
|
-
|
|
52
|
-
// Always return false to prevent the refresh post-fixer from re-running on the same set of changes and going into an infinite loop.
|
|
53
|
-
// This "post-fixer" does not change the model structure so there shouldn't be need to run other post-fixers again.
|
|
54
|
-
// See https://github.com/ckeditor/ckeditor5/issues/1936 & https://github.com/ckeditor/ckeditor5/issues/8200.
|
|
55
|
-
return false;
|
|
56
45
|
}
|
|
57
46
|
|
|
58
47
|
// Check if given model element needs refreshing.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. 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-heading-rows-refresh-post-fixer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import TableWalker from '../tablewalker';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A table headings refresh handler which marks the table cells or rows in the differ to have it re-rendered
|
|
14
|
+
* if the headings attribute changed.
|
|
15
|
+
*
|
|
16
|
+
* Table heading rows and heading columns are represented in the model by a `headingRows` and `headingColumns` attributes.
|
|
17
|
+
*
|
|
18
|
+
* When table headings attribute changes, all the cells/rows are marked to re-render to change between `<td>` and `<th>`.
|
|
19
|
+
*
|
|
20
|
+
* @param {module:engine/model/model~Model} model
|
|
21
|
+
* @param {module:engine/controller/editingcontroller~EditingController} editing
|
|
22
|
+
*/
|
|
23
|
+
export default function tableHeadingsRefreshHandler( model, editing ) {
|
|
24
|
+
const differ = model.document.differ;
|
|
25
|
+
|
|
26
|
+
for ( const change of differ.getChanges() ) {
|
|
27
|
+
let table;
|
|
28
|
+
let isRowChange = false;
|
|
29
|
+
|
|
30
|
+
if ( change.type == 'attribute' ) {
|
|
31
|
+
const element = change.range.start.nodeAfter;
|
|
32
|
+
|
|
33
|
+
if ( !element || !element.is( 'element', 'table' ) ) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ( change.attributeKey != 'headingRows' && change.attributeKey != 'headingColumns' ) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
table = element;
|
|
42
|
+
isRowChange = change.attributeKey == 'headingRows';
|
|
43
|
+
} else if ( change.name == 'tableRow' || change.name == 'tableCell' ) {
|
|
44
|
+
table = change.position.findAncestor( 'table' );
|
|
45
|
+
isRowChange = change.name == 'tableRow';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if ( !table ) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const headingRows = table.getAttribute( 'headingRows' ) || 0;
|
|
53
|
+
const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
|
|
54
|
+
|
|
55
|
+
const tableWalker = new TableWalker( table );
|
|
56
|
+
|
|
57
|
+
for ( const tableSlot of tableWalker ) {
|
|
58
|
+
const isHeading = tableSlot.row < headingRows || tableSlot.column < headingColumns;
|
|
59
|
+
const expectedElementName = isHeading ? 'th' : 'td';
|
|
60
|
+
|
|
61
|
+
const viewElement = editing.mapper.toViewElement( tableSlot.cell );
|
|
62
|
+
|
|
63
|
+
if ( viewElement && viewElement.is( 'element' ) && viewElement.name != expectedElementName ) {
|
|
64
|
+
editing.reconvertItem( isRowChange ? tableSlot.cell.parent : tableSlot.cell );
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|