/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* bender-tags: conversion */
'use strict';
import ModelDocument from '/ckeditor5/engine/model/document.js';
import ModelElement from '/ckeditor5/engine/model/element.js';
import ModelText from '/ckeditor5/engine/model/text.js';
import ModelRange from '/ckeditor5/engine/model/range.js';
import ModelPosition from '/ckeditor5/engine/model/position.js';
import ViewElement from '/ckeditor5/engine/view/element.js';
import ViewContainerElement from '/ckeditor5/engine/view/containerelement.js';
import ViewAttributeElement from '/ckeditor5/engine/view/attributeelement.js';
import ViewText from '/ckeditor5/engine/view/text.js';
import Mapper from '/ckeditor5/engine/conversion/mapper.js';
import ModelConversionDispatcher from '/ckeditor5/engine/conversion/modelconversiondispatcher.js';
import {
insertElement,
insertText,
setAttribute,
removeAttribute,
wrap,
unwrap,
move,
remove
} from '/ckeditor5/engine/conversion/model-to-view-converters.js';
let dispatcher, modelDoc, modelRoot, mapper, viewRoot;
beforeEach( () => {
modelDoc = new ModelDocument();
modelRoot = modelDoc.createRoot();
viewRoot = new ViewContainerElement( 'div' );
mapper = new Mapper();
mapper.bindElements( modelRoot, viewRoot );
dispatcher = new ModelConversionDispatcher( { mapper } );
} );
function viewAttributesToString( item ) {
let result = '';
for ( let key of item.getAttributeKeys() ) {
let value = item.getAttribute( key );
if ( value ) {
result += ' ' + key + '="' + value + '"';
}
}
return result;
}
function viewToString( item ) {
let result = '';
if ( item instanceof ViewText ) {
result = item.data;
} else {
// ViewElement or ViewDocumentFragment.
for ( let child of item.getChildren() ) {
result += viewToString( child );
}
if ( item instanceof ViewElement ) {
result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '' + item.name + '>';
}
}
return result;
}
describe( 'insertText', () => {
it( 'should convert text insertion in model to view text', () => {
modelRoot.appendChildren( 'foobar' );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
expect( viewToString( viewRoot ) ).to.equal( '
foobar
' );
} );
} );
describe( 'insertElement', () => {
it( 'should convert element insertion in model to and map positions for future converting', () => {
const modelElement = new ModelElement( 'paragraph', null, 'foobar' );
const viewElement = new ViewContainerElement( 'p' );
modelRoot.appendChildren( modelElement );
dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should take view element function generator as a parameter', () => {
const elementGenerator = ( data, consumable ) => {
if ( consumable.consume( data.item, 'addAttribute:nice' ) ) {
return new ViewContainerElement( 'div' );
} else {
return new ViewContainerElement( 'p' );
}
};
const niceP = new ModelElement( 'myParagraph', { nice: true }, 'foo' );
const badP = new ModelElement( 'myParagraph', null, 'bar' );
modelRoot.appendChildren( [ niceP, badP ] );
dispatcher.on( 'insert:myParagraph', insertElement( elementGenerator ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
describe( 'setAttribute/removeAttribute', () => {
it( 'should convert attribute insert/change/remove on a model node', () => {
const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, 'foobar' );
const viewElement = new ViewContainerElement( 'p' );
modelRoot.appendChildren( modelElement );
dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.on( 'addAttribute:class', setAttribute() );
dispatcher.on( 'changeAttribute:class', setAttribute() );
dispatcher.on( 'removeAttribute:class', removeAttribute() );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
expect( viewToString( viewRoot ) ).to.equal( '' );
modelElement.setAttribute( 'class', 'bar' );
dispatcher.convertAttribute( 'changeAttribute', ModelRange.createOnElement( modelElement ), 'class', 'foo', 'bar' );
expect( viewToString( viewRoot ) ).to.equal( '' );
modelElement.removeAttribute( 'class' );
dispatcher.convertAttribute( 'removeAttribute', ModelRange.createOnElement( modelElement ), 'class', 'bar', null );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, 'foobar' );
const modelDiv = new ModelElement( 'div', { theme: 'nice' } );
const themeConverter = ( value, key, data ) => {
if ( data.item instanceof ModelElement && data.item.getChildCount() > 0 ) {
value += ' ' + 'fix-content';
}
return { key: 'class', value };
};
modelRoot.appendChildren( [ modelParagraph, modelDiv ] );
dispatcher.on( 'insert:paragraph', insertElement( new ViewContainerElement( 'p' ) ) );
dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.on( 'addAttribute:theme', setAttribute( themeConverter ) );
dispatcher.on( 'changeAttribute:theme', setAttribute( themeConverter ) );
dispatcher.on( 'removeAttribute:theme', removeAttribute( themeConverter ) );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
expect( viewToString( viewRoot ) ).to.equal( '' );
modelParagraph.setAttribute( 'theme', 'awesome' );
dispatcher.convertAttribute( 'changeAttribute', ModelRange.createOnElement( modelParagraph ), 'theme', 'nice', 'awesome' );
expect( viewToString( viewRoot ) ).to.equal( '' );
modelParagraph.removeAttribute( 'theme' );
dispatcher.convertAttribute( 'removeAttribute', ModelRange.createOnElement( modelParagraph ), 'theme', 'awesome', null );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
describe( 'wrap/unwrap', () => {
it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
const viewP = new ViewContainerElement( 'p' );
const viewB = new ViewAttributeElement( 'b' );
modelRoot.appendChildren( modelElement );
dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.on( 'addAttribute:bold', wrap( viewB ) );
dispatcher.on( 'removeAttribute:bold', unwrap( viewB ) );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
expect( viewToString( viewRoot ) ).to.equal( '' );
for ( let value of ModelRange.createFromElement( modelElement ) ) {
value.item.removeAttribute( 'bold' );
}
dispatcher.convertAttribute( 'removeAttribute', ModelRange.createFromElement( modelElement ), 'bold', true, null );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
const viewP = new ViewContainerElement( 'p' );
const elementGenerator = ( value ) => {
if ( value == 'bold' ) {
return new ViewAttributeElement( 'b' );
}
};
modelRoot.appendChildren( modelElement );
dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.on( 'addAttribute:style', wrap( elementGenerator ) );
dispatcher.on( 'removeAttribute:style', unwrap( elementGenerator ) );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
expect( viewToString( viewRoot ) ).to.equal( '' );
for ( let value of ModelRange.createFromElement( modelElement ) ) {
value.item.removeAttribute( 'style' );
}
dispatcher.convertAttribute( 'removeAttribute', ModelRange.createFromElement( modelElement ), 'style', 'bold', null );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should update range on re-wrapping attribute (#475)', () => {
const modelElement = new ModelElement( 'paragraph', null,
[ 'x', new ModelText( 'foo', { link: 'http://foo.com' } ), 'x' ] );
const viewP = new ViewContainerElement( 'p' );
const elementGenerator = ( href ) => new ViewAttributeElement( 'a', { href } );
modelRoot.appendChildren( modelElement );
dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.on( 'addAttribute:link', wrap( elementGenerator ) );
dispatcher.on( 'changeAttribute:link', wrap( elementGenerator ) );
dispatcher.convertInsert(
ModelRange.createFromElement( modelRoot )
);
expect( viewToString( viewRoot ) ).to.equal( '' );
for ( let value of ModelRange.createFromElement( modelElement ) ) {
value.item.setAttribute( 'link', 'http://foobar.com' );
}
dispatcher.convertAttribute(
'changeAttribute',
ModelRange.createFromElement( modelElement ),
'link',
'http://foo.com',
'http://foobar.com'
);
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
describe( 'move', () => {
it( 'should move items in view accordingly to changes in model', () => {
const modelDivA = new ModelElement( 'div', null, [ 'foo', new ModelElement( 'image' ) ] );
const modelDivB = new ModelElement( 'div', null, [ 'xxyy' ] );
modelRoot.appendChildren( [ modelDivA, modelDivB ] );
dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
dispatcher.on( 'insert:image', insertElement( new ViewContainerElement( 'img' ) ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.on( 'move', move() );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
const removedNodes = modelDivA.removeChildren( 2, 2 );
modelDivB.insertChildren( 2, removedNodes );
dispatcher.convertMove(
ModelPosition.createFromParentAndOffset( modelDivA, 2 ),
ModelRange.createFromParentsAndOffsets( modelDivB, 2, modelDivB, 4 )
);
expect( viewToString( viewRoot ) ).to.equal( 'fo
xxo
![]()
yy
' );
} );
} );
describe( 'remove', () => {
it( 'should remove items from view accordingly to changes in model', () => {
const modelDiv = new ModelElement( 'div', null, [ 'foo', new ModelElement( 'image' ) ] );
modelRoot.appendChildren( modelDiv );
dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
dispatcher.on( 'insert:image', insertElement( new ViewContainerElement( 'img' ) ) );
dispatcher.on( 'insert:$text', insertText() );
dispatcher.on( 'remove', remove() );
dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
const removedNodes = modelDiv.removeChildren( 2, 2 );
modelDoc.graveyard.insertChildren( 2, removedNodes );
dispatcher.convertRemove(
ModelPosition.createFromParentAndOffset( modelDiv, 2 ),
ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 2 )
);
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );