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

view.Writer should deeply add and remove attribute elements to/from their clone groups #1403

Merged
merged 1 commit into from
Apr 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions src/view/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -941,12 +941,13 @@ export default class Writer {
if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
// Clone attribute.
const newAttribute = attribute._clone();
this._addToClonedElementsGroup( newAttribute );

// Wrap current node with new attribute.
child._remove();
newAttribute._appendChild( child );

parent._insertChild( i, newAttribute );
this._addToClonedElementsGroup( newAttribute );

wrapPositions.push( new Position( parent, i ) );
}
Expand Down Expand Up @@ -1006,10 +1007,10 @@ export default class Writer {

// Replace wrapper element with its children
child._remove();
this._removeFromClonedElementsGroup( child );

parent._insertChild( i, unwrapped );

this._removeFromClonedElementsGroup( child );

// Save start and end position of moved items.
unwrapPositions.push(
new Position( parent, i ),
Expand Down Expand Up @@ -1415,10 +1416,10 @@ export default class Writer {

// Break element.
const clonedNode = positionParent._clone();
this._addToClonedElementsGroup( clonedNode );

// Insert cloned node to position's parent node.
positionParent.parent._insertChild( offsetAfter, clonedNode );
this._addToClonedElementsGroup( clonedNode );

// Get nodes to move.
const count = positionParent.childCount - positionOffset;
Expand Down Expand Up @@ -1447,6 +1448,19 @@ export default class Writer {
* @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to save.
*/
_addToClonedElementsGroup( element ) {
// Add only if the element is in document tree.
if ( !element.root.is( 'rootElement' ) ) {
return;
}

// Traverse the element's children recursively to find other attribute elements that also might got inserted.
// The loop is at the beginning so we can make fast returns later in the code.
if ( element.is( 'element' ) ) {
for ( const child of element.getChildren() ) {
this._addToClonedElementsGroup( child );
}
}

const id = element.id;

if ( !id ) {
Expand Down Expand Up @@ -1477,6 +1491,14 @@ export default class Writer {
* @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to remove.
*/
_removeFromClonedElementsGroup( element ) {
// Traverse the element's children recursively to find other attribute elements that also got removed.
// The loop is at the beginning so we can make fast returns later in the code.
if ( element.is( 'element' ) ) {
for ( const child of element.getChildren() ) {
this._removeFromClonedElementsGroup( child );
}
}

const id = element.id;

if ( !id ) {
Expand All @@ -1485,6 +1507,10 @@ export default class Writer {

const group = this._cloneGroups.get( id );

if ( !group ) {
return;
}

group.delete( element );
// Not removing group from element on purpose!
// If other parts of code have reference to this element, they will be able to get references to other elements from the group.
Expand Down
84 changes: 76 additions & 8 deletions tests/view/writer/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import createViewRoot from '../_utils/createroot';
describe( 'Writer', () => {
let writer, attributes, root, doc;

before( () => {
beforeEach( () => {
attributes = { foo: 'bar', baz: 'quz' };
doc = new Document();
root = createViewRoot( doc );
Expand Down Expand Up @@ -43,6 +43,9 @@ describe( 'Writer', () => {

describe( 'setSelectionFocus()', () => {
it( 'should use selection._setFocus method internally', () => {
const position = ViewPosition.createAt( root );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unrelated but this test started to fail after changing from before to beforeEach.

writer.setSelection( position );

const spy = sinon.spy( writer.document.selection, '_setFocus' );
writer.setSelectionFocus( root, 0 );

Expand Down Expand Up @@ -255,10 +258,9 @@ describe( 'Writer', () => {

describe( 'manages AttributeElement#_clonesGroup', () => {
it( 'should return all clones of a broken attribute element with id', () => {
const container = writer.createContainerElement( 'div' );
const text = writer.createText( 'abccccde' );

writer.insert( ViewPosition.createAt( container, 0 ), text );
writer.insert( ViewPosition.createAt( root, 0 ), text );

const span = writer.createAttributeElement( 'span', null, { id: 'foo' } );
span._priority = 20;
Expand All @@ -271,23 +273,23 @@ describe( 'Writer', () => {
// <div>a<i>b<span>c</span></i><span>cc</span>de</div>
writer.wrap(
ViewRange.createFromParentsAndOffsets(
container.getChild( 0 ), 1,
container.getChild( 1 ).getChild( 0 ), 1
root.getChild( 0 ), 1,
root.getChild( 1 ).getChild( 0 ), 1
),
i
);

// <div>a<i>b<span>c</span></i><span>c</span><i><span>cc</span>d</i>e</div>
writer.wrap(
ViewRange.createFromParentsAndOffsets(
container.getChild( 2 ).getChild( 0 ), 1,
container.getChild( 3 ), 1
root.getChild( 2 ).getChild( 0 ), 1,
root.getChild( 3 ), 1
),
i
);

// Find all spans.
const allSpans = Array.from( ViewRange.createIn( container ).getItems() ).filter( element => element.is( 'span' ) );
const allSpans = Array.from( ViewRange.createIn( root ).getItems() ).filter( element => element.is( 'span' ) );

// For each of the spans created above...
for ( const oneOfAllSpans of allSpans ) {
Expand All @@ -302,6 +304,72 @@ describe( 'Writer', () => {
expect( brokenArray.length ).to.equal( allSpans.length );
}
} );

it( 'should not create groups for attribute elements that are not created in document root', () => {
const p = writer.createContainerElement( 'p' );
const foo = writer.createText( 'foo' );
writer.insert( ViewPosition.createAt( p, 0 ), foo );
// <p>foo</p>

const span = writer.createAttributeElement( 'span', null, { id: 'span' } );

// <p><span>foo</span></p>
writer.wrap( ViewRange.createFromParentsAndOffsets( foo, 0, foo, 3 ), span );

// Find the span.
const createdSpan = p.getChild( 0 );

expect( createdSpan.getElementsWithSameId().size ).to.equal( 0 );
} );

it( 'should add attribute elements to clone groups deeply', () => {
const p = writer.createContainerElement( 'p' );
const foo = writer.createText( 'foo' );
writer.insert( ViewPosition.createAt( p, 0 ), foo );
// <p>foo</p>

const span = writer.createAttributeElement( 'span', null, { id: 'span' } );

// <p><span>foo</span></p>
writer.wrap( ViewRange.createFromParentsAndOffsets( foo, 0, foo, 3 ), span );

// <div><p><span>foo</span></p>
writer.insert( ViewPosition.createAt( root, 0 ), p );

// Find the span.
const createdSpan = p.getChild( 0 );

expect( Array.from( createdSpan.getElementsWithSameId() ) ).to.deep.equal( [ createdSpan ] );
} );

it( 'should remove attribute elements from clone groups deeply', () => {
const p1 = writer.createContainerElement( 'p' );
const p2 = writer.createContainerElement( 'p' );
const foo = writer.createText( 'foo' );
const bar = writer.createText( 'bar' );

writer.insert( ViewPosition.createAt( root, 0 ), p1 );
writer.insert( ViewPosition.createAt( root, 1 ), p2 );
writer.insert( ViewPosition.createAt( p1, 0 ), foo );
writer.insert( ViewPosition.createAt( p2, 0 ), bar );
// <div><p>foo</p><p>bar</p></div>

const span = writer.createAttributeElement( 'span', null, { id: 'span' } );

// <div><p>fo<span>o</span></p><p>bar</p></div>
writer.wrap( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ), span );

// <div><p>fo<span>o</span></p><p><span>b</span>ar</p></div>
writer.wrap( ViewRange.createFromParentsAndOffsets( bar, 0, bar, 1 ), span );

// <div><p><span>b</span>ar</p></div>
writer.remove( ViewRange.createOn( p1 ) );

// Find the span.
const spanInTree = p2.getChild( 0 );

expect( Array.from( spanInTree.getElementsWithSameId() ) ).to.deep.equal( [ spanInTree ] );
} );
} );

function assertElementAttributes( element, attributes ) {
Expand Down