` in case there are some markers on it and transparentRendering will render it anyway.
+ const viewElement = writer.createContainerElement( 'p' );
+
+ writer.setCustomProperty( 'dataPipeline:transparentRendering', true, viewElement );
+
return viewElement;
};
}
diff --git a/packages/ckeditor5-list/tests/documentlist/integrations/markers.js b/packages/ckeditor5-list/tests/documentlist/integrations/markers.js
new file mode 100644
index 00000000000..0a74addc3c0
--- /dev/null
+++ b/packages/ckeditor5-list/tests/documentlist/integrations/markers.js
@@ -0,0 +1,352 @@
+/**
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global document */
+
+import DocumentListEditing from '../../../src/documentlist/documentlistediting';
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import ImageBlockEditing from '@ckeditor/ckeditor5-image/src/image/imageblockediting';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import stubUid from '../_utils/uid';
+
+describe( 'DocumentListEditing integrations: markers', () => {
+ let element, editor, model, root;
+
+ testUtils.createSinonSandbox();
+
+ beforeEach( async () => {
+ element = document.createElement( 'div' );
+ document.body.appendChild( element );
+
+ editor = await ClassicTestEditor.create( element, {
+ plugins: [ Paragraph, DocumentListEditing, ImageBlockEditing ]
+ } );
+
+ model = editor.model;
+ root = model.document.getRoot();
+
+ editor.conversion.for( 'upcast' ).dataToMarker( { view: 'foo' } );
+ editor.conversion.for( 'dataDowncast' ).markerToData( { model: 'foo' } );
+
+ stubUid();
+ } );
+
+ afterEach( async () => {
+ element.remove();
+
+ await editor.destroy();
+ } );
+
+ function addMarker( range ) {
+ model.change( writer => {
+ writer.addMarker( 'foo:bar', {
+ usingOperation: true,
+ affectsData: true,
+ range
+ } );
+ } );
+ }
+
+ function checkMarker( range ) {
+ const marker = model.markers.get( 'foo:bar' );
+
+ expect( marker ).to.not.be.null;
+ expect( marker.getRange().isEqual( range ) ).to.be.true;
+ }
+
+ describe( 'list item with a single paragraph', () => {
+ beforeEach( () => {
+ setModelData( model,
+ 'A'
+ );
+ } );
+
+ it( 'marker beginning before a paragraph and ending inside', () => {
+ const range = model.createRange(
+ model.createPositionBefore( root.getChild( 0 ) ),
+ model.createPositionAt( root.getChild( 0 ), 'end' )
+ );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
A
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker beginning before an empty paragraph and ending inside', () => {
+ model.change( writer => writer.remove( root.getChild( 0 ).getChild( 0 ) ) );
+
+ const range = model.createRange(
+ model.createPositionBefore( root.getChild( 0 ) ),
+ model.createPositionAt( root.getChild( 0 ), 'end' )
+ );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker beginning before a paragraph and ending after it', () => {
+ const range = model.createRangeOn( root.getChild( 0 ) );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
A
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker inside a paragraph', () => {
+ const range = model.createRangeIn( root.getChild( 0 ) );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ 'A' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker inside an empty paragraph', () => {
+ model.change( writer => writer.remove( root.getChild( 0 ).getChild( 0 ) ) );
+
+ const range = model.createRangeIn( root.getChild( 0 ) );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ ' ' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+ } );
+
+ describe( 'list item with multiple paragraphs', () => {
+ beforeEach( () => {
+ setModelData( model,
+ 'A' +
+ 'B'
+ );
+ } );
+
+ it( 'marker beginning before a paragraph and ending inside', () => {
+ const range = model.createRange(
+ model.createPositionBefore( root.getChild( 0 ) ),
+ model.createPositionAt( root.getChild( 0 ), 'end' )
+ );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
A
' +
+ '
B
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker beginning before a paragraph and ending inside the next paragraph', () => {
+ const range = model.createRange(
+ model.createPositionBefore( root.getChild( 0 ) ),
+ model.createPositionAt( root.getChild( 1 ), 'end' )
+ );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
A
' +
+ '
B
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker beginning before an empty paragraph and ending inside the next paragraph', () => {
+ model.change( writer => {
+ writer.remove( root.getChild( 0 ).getChild( 0 ) );
+ writer.remove( root.getChild( 1 ).getChild( 0 ) );
+ } );
+
+ const range = model.createRange(
+ model.createPositionBefore( root.getChild( 0 ) ),
+ model.createPositionAt( root.getChild( 1 ), 'end' )
+ );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker beginning before a paragraph and ending after the next paragraph', () => {
+ const range = model.createRangeIn( root );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
A
' +
+ '
B
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker starting in a paragraph and ending in next paragraph', () => {
+ const range = model.createRange(
+ model.createPositionAt( root.getChild( 0 ), 'end' ),
+ model.createPositionAt( root.getChild( 1 ), 0 )
+ );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
A
' +
+ '
B
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+
+ it( 'marker starting in a empty paragraph and ending in next empty paragraph', () => {
+ model.change( writer => {
+ writer.remove( root.getChild( 0 ).getChild( 0 ) );
+ writer.remove( root.getChild( 1 ).getChild( 0 ) );
+ } );
+
+ const range = model.createRange(
+ model.createPositionAt( root.getChild( 0 ), 'end' ),
+ model.createPositionAt( root.getChild( 1 ), 0 )
+ );
+
+ addMarker( range );
+
+ const data = editor.getData();
+
+ expect( data ).to.equal(
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
'
+ );
+
+ editor.setData( data );
+
+ checkMarker( range );
+ expect( editor.getData() ).to.equal( data );
+ } );
+ } );
+} );
diff --git a/packages/ckeditor5-table/src/converters/downcast.js b/packages/ckeditor5-table/src/converters/downcast.js
index c0b69f7fef6..9543670d223 100644
--- a/packages/ckeditor5-table/src/converters/downcast.js
+++ b/packages/ckeditor5-table/src/converters/downcast.js
@@ -114,7 +114,7 @@ export function downcastCell( options = {} ) {
* @returns {Function} Element creator.
*/
export function convertParagraphInTableCell( options = {} ) {
- return ( modelElement, { writer, consumable, mapper } ) => {
+ return ( modelElement, { writer } ) => {
if ( !modelElement.parent.is( 'element', 'tableCell' ) ) {
return;
}
@@ -126,9 +126,12 @@ export function convertParagraphInTableCell( options = {} ) {
if ( options.asWidget ) {
return writer.createContainerElement( 'span', { class: 'ck-table-bogus-paragraph' } );
} else {
- // Additional requirement for data pipeline to have backward compatible data tables.
- consumable.consume( modelElement, 'insert' );
- mapper.bindElements( modelElement, mapper.toViewElement( modelElement.parent ) );
+ // Using `
` in case there are some markers on it and transparentRendering will render it anyway.
+ const viewElement = writer.createContainerElement( 'p' );
+
+ writer.setCustomProperty( 'dataPipeline:transparentRendering', true, viewElement );
+
+ return viewElement;
}
};
}
diff --git a/packages/ckeditor5-table/src/converters/upcasttable.js b/packages/ckeditor5-table/src/converters/upcasttable.js
index 47f23a49029..e6a7032cfb9 100644
--- a/packages/ckeditor5-table/src/converters/upcasttable.js
+++ b/packages/ckeditor5-table/src/converters/upcasttable.js
@@ -146,18 +146,33 @@ export function skipEmptyTableRow() {
*/
export function ensureParagraphInTableCell( elementName ) {
return dispatcher => {
- dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
+ dispatcher.on( `element:${ elementName }`, ( evt, data, { writer } ) => {
// The default converter will create a model range on converted table cell.
if ( !data.modelRange ) {
return;
}
+ const tableCell = data.modelRange.start.nodeAfter;
+ const modelCursor = writer.createPositionAt( tableCell, 0 );
+
// Ensure a paragraph in the model for empty table cells for converted table cells.
if ( data.viewItem.isEmpty ) {
- const tableCell = data.modelRange.start.nodeAfter;
- const modelCursor = conversionApi.writer.createPositionAt( tableCell, 0 );
+ writer.insertElement( 'paragraph', modelCursor );
+
+ return;
+ }
- conversionApi.writer.insertElement( 'paragraph', modelCursor );
+ const childNodes = Array.from( tableCell.getChildren() );
+
+ // In case there are only markers inside the table cell then move them to the paragraph.
+ if ( childNodes.every( node => node.is( 'element', '$marker' ) ) ) {
+ const paragraph = writer.createElement( 'paragraph' );
+
+ writer.insert( paragraph, writer.createPositionAt( tableCell, 0 ) );
+
+ for ( const node of childNodes ) {
+ writer.move( writer.createRangeOn( node ), writer.createPositionAt( paragraph, 'end' ) );
+ }
}
}, { priority: 'low' } );
};
diff --git a/packages/ckeditor5-table/src/tableediting.js b/packages/ckeditor5-table/src/tableediting.js
index f8377501d9f..c4dc7f98f41 100644
--- a/packages/ckeditor5-table/src/tableediting.js
+++ b/packages/ckeditor5-table/src/tableediting.js
@@ -152,11 +152,6 @@ export default class TableEditing extends Plugin {
view: 'rowspan'
} );
- // Manually adjust model position mappings in a special case, when a table cell contains a paragraph, which is bound
- // to its parent (to the table cell). This custom model-to-view position mapping is necessary in data pipeline only,
- // because only during this conversion a paragraph can be bound to its parent.
- editor.data.mapper.on( 'modelToViewPosition', mapTableCellModelPositionToView() );
-
// Define the config.
editor.config.define( 'table.defaultHeadings.rows', 0 );
editor.config.define( 'table.defaultHeadings.columns', 0 );
@@ -197,40 +192,6 @@ export default class TableEditing extends Plugin {
}
}
-// Creates a mapper callback to adjust model position mappings in a table cell containing a paragraph, which is bound to its parent
-// (to the table cell). Only positions after this paragraph have to be adjusted, because after binding this paragraph to the table cell,
-// elements located after this paragraph would point either to a non-existent offset inside `tableCell` (if paragraph is empty), or after
-// the first character of the paragraph's text. See https://github.com/ckeditor/ckeditor5/issues/10116.
-//
-// ^ ->
^
-//
-// foobar^ ->
foobar^
-//
-// @returns {Function}
-function mapTableCellModelPositionToView() {
- return ( evt, data ) => {
- const modelParent = data.modelPosition.parent;
- const modelNodeBefore = data.modelPosition.nodeBefore;
-
- if ( !modelParent.is( 'element', 'tableCell' ) ) {
- return;
- }
-
- if ( !modelNodeBefore || !modelNodeBefore.is( 'element', 'paragraph' ) ) {
- return;
- }
-
- const viewNodeBefore = data.mapper.toViewElement( modelNodeBefore );
- const viewParent = data.mapper.toViewElement( modelParent );
-
- if ( viewNodeBefore === viewParent ) {
- // Since the paragraph has already been bound to its parent, update the current position in the model with paragraph's
- // max offset, so it points to the place which should normally (in all other cases) be the end position of this paragraph.
- data.viewPosition = data.mapper.findPositionIn( viewParent, modelNodeBefore.maxOffset );
- }
- };
-}
-
// Returns fixed colspan and rowspan attrbutes values.
//
// @private
diff --git a/packages/ckeditor5-table/tests/converters/upcasttable.js b/packages/ckeditor5-table/tests/converters/upcasttable.js
index 029a4e63561..24f575dcfb2 100644
--- a/packages/ckeditor5-table/tests/converters/upcasttable.js
+++ b/packages/ckeditor5-table/tests/converters/upcasttable.js
@@ -333,7 +333,7 @@ describe( 'upcastTable()', () => {
);
expectModel(
- ''
+ ''
);
} );
diff --git a/packages/ckeditor5-table/tests/table-integration.js b/packages/ckeditor5-table/tests/table-integration.js
index f86ec0a8a14..6835da9140e 100644
--- a/packages/ckeditor5-table/tests/table-integration.js
+++ b/packages/ckeditor5-table/tests/table-integration.js
@@ -253,7 +253,7 @@ describe( 'Table feature – integration with markers', () => {
editor.setData( '