Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 43b6ea9

Browse files
author
Piotr Jasiun
authored
Merge pull request #1073 from ckeditor/t/1072
Fix: AttributeElement with bogus <br /> will now be placed after all UI elements which will fix how those elements are rendered. Closes #1072.
2 parents 070c313 + 535ed5c commit 43b6ea9

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

src/conversion/model-selection-to-view-converters.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,36 @@ function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElem
200200
}
201201

202202
let viewPosition = viewSelection.getFirstPosition();
203+
204+
// This hack is supposed to place attribute element *after* all ui elements if the attribute element would be
205+
// the only non-ui child and thus receive a block filler.
206+
// This is needed to properly render ui elements. Block filler is a <br /> element. If it is placed before
207+
// UI element, the ui element will most probably be incorrectly rendered (in next line). #1072.
208+
if ( shouldPushAttributeElement( viewPosition.parent ) ) {
209+
viewPosition = viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
210+
}
211+
// End of hack.
212+
203213
viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
204214

205215
viewSelection.removeAllRanges();
206216
viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
207217
}
208218

219+
function shouldPushAttributeElement( parent ) {
220+
if ( !parent.is( 'element' ) ) {
221+
return false;
222+
}
223+
224+
for ( const child of parent.getChildren() ) {
225+
if ( !child.is( 'uiElement' ) ) {
226+
return false;
227+
}
228+
}
229+
230+
return true;
231+
}
232+
209233
/**
210234
* Function factory, creates a converter that clears artifacts after the previous
211235
* {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty

tests/conversion/model-selection-to-view-converters.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ModelPosition from '../../src/model/position';
1111
import ViewDocument from '../../src/view/document';
1212
import ViewContainerElement from '../../src/view/containerelement';
1313
import ViewAttributeElement from '../../src/view/attributeelement';
14+
import ViewUIElement from '../../src/view/uielement';
1415
import { mergeAttributes } from '../../src/view/writer';
1516

1617
import Mapper from '../../src/conversion/mapper';
@@ -314,6 +315,69 @@ describe( 'model-selection-to-view-converters', () => {
314315
.to.equal( '<div>foo{}bar</div>' );
315316
} );
316317

318+
// #1072 - if the container has only ui elements, collapsed selection attribute should be rendered after those ui elements.
319+
it( 'selection with attribute before ui element - no non-ui children', () => {
320+
setModelData( modelDoc, '' );
321+
322+
// Add two ui elements to view.
323+
viewRoot.appendChildren( [
324+
new ViewUIElement( 'span' ),
325+
new ViewUIElement( 'span' )
326+
] );
327+
328+
modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 0 ] ) ) ] );
329+
modelSelection.setAttribute( 'bold', true );
330+
331+
// Convert model to view.
332+
dispatcher.convertSelection( modelSelection, [] );
333+
334+
// Stringify view and check if it is same as expected.
335+
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
336+
.to.equal( '<div><span></span><span></span><strong>[]</strong></div>' );
337+
} );
338+
339+
// #1072.
340+
it( 'selection with attribute before ui element - has non-ui children #1', () => {
341+
setModelData( modelDoc, 'x' );
342+
343+
modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) ] );
344+
modelSelection.setAttribute( 'bold', true );
345+
346+
// Convert model to view.
347+
dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
348+
349+
// Add ui element to view.
350+
const uiElement = new ViewUIElement( 'span' );
351+
viewRoot.insertChildren( 1, uiElement );
352+
353+
dispatcher.convertSelection( modelSelection, [] );
354+
355+
// Stringify view and check if it is same as expected.
356+
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
357+
.to.equal( '<div>x<strong>[]</strong><span></span></div>' );
358+
} );
359+
360+
// #1072.
361+
it( 'selection with attribute before ui element - has non-ui children #2', () => {
362+
setModelData( modelDoc, '<$text bold="true">x</$text>y' );
363+
364+
modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) ] );
365+
modelSelection.setAttribute( 'bold', true );
366+
367+
// Convert model to view.
368+
dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
369+
370+
// Add ui element to view.
371+
const uiElement = new ViewUIElement( 'span' );
372+
viewRoot.insertChildren( 1, uiElement );
373+
374+
dispatcher.convertSelection( modelSelection, [] );
375+
376+
// Stringify view and check if it is same as expected.
377+
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
378+
.to.equal( '<div><strong>x{}</strong><span></span>y</div>' );
379+
} );
380+
317381
it( 'consumes consumable values properly', () => {
318382
// Add callbacks that will fire before default ones.
319383
// This should prevent default callbacks doing anything.

0 commit comments

Comments
 (0)