/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
import Model from '../../src/model/model';
import ModelElement from '../../src/model/element';
import ModelText from '../../src/model/text';
import ModelRange from '../../src/model/range';
import ModelPosition from '../../src/model/position';
import ViewElement from '../../src/view/element';
import ViewContainerElement from '../../src/view/containerelement';
import ViewAttributeElement from '../../src/view/attributeelement';
import ViewUIElement from '../../src/view/uielement';
import ViewText from '../../src/view/text';
import {
insertElement,
insertUIElement,
changeAttribute,
wrap,
removeUIElement,
highlightElement,
highlightText,
removeHighlight,
createViewElementFromHighlightDescriptor
} from '../../src/conversion/model-to-view-converters';
import EditingController from '../../src/controller/editingcontroller';
describe( 'model-to-view-converters', () => {
let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model;
beforeEach( () => {
model = new Model();
modelDoc = model.document;
modelRoot = modelDoc.createRoot();
controller = new EditingController( model );
viewRoot = controller.view.getRoot();
// Set name of view root the same as dom root.
// This is a mock of attaching view root to dom root.
controller.view.getRoot()._name = 'div';
dispatcher = controller.modelToView;
dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );
dispatcher.on( 'attribute:class', changeAttribute() );
modelRootStart = ModelPosition.createAt( modelRoot, 0 );
} );
function viewAttributesToString( item ) {
let result = '';
for ( const key of item.getAttributeKeys() ) {
const 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 ( const 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', () => {
model.change( writer => {
writer.insert( new ModelText( 'foobar' ), modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '
foobar
' );
} );
it( 'should support unicode', () => {
model.change( writer => {
writer.insert( new ModelText( 'நிலைக்கு' ), modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( 'நிலைக்கு
' );
} );
it( 'should be possible to override it', () => {
dispatcher.on( 'insert:$text', ( evt, data, consumable ) => {
consumable.consume( data.item, 'insert' );
}, { priority: 'high' } );
model.change( writer => {
writer.insert( new ModelText( 'foobar' ), modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
describe( 'insertElement', () => {
it( 'should convert element insertion in model to and map positions for future converting', () => {
const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should take view element function generator as a parameter', () => {
const elementGenerator = ( modelItem, consumable ) => {
if ( consumable.consume( modelItem, 'attribute:nice' ) ) {
return new ViewContainerElement( 'div' );
}
// Test if default converter will be fired for paragraph, if `null` is returned and consumable was not consumed.
return null;
};
dispatcher.on( 'insert:paragraph', insertElement( elementGenerator ), { priority: 'high' } );
const niceP = new ModelElement( 'paragraph', { nice: true }, new ModelText( 'foo' ) );
const badP = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
model.change( writer => {
writer.insert( [ niceP, badP ], modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
describe( 'setAttribute', () => {
it( 'should convert attribute insert/change/remove on a model node', () => {
const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.setAttribute( 'class', 'bar', modelElement );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeAttribute( 'class', modelElement );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
const themeConverter = ( value, data ) => {
if ( data.item instanceof ModelElement && data.item.childCount > 0 ) {
value += ' fix-content';
}
return { key: 'class', value };
};
dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) );
const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
const modelDiv = new ModelElement( 'div', { theme: 'nice' } );
model.change( writer => {
writer.insert( [ modelParagraph, modelDiv ], modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.setAttribute( 'theme', 'awesome', modelParagraph );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeAttribute( 'theme', modelParagraph );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should be possible to override setAttribute', () => {
const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
dispatcher.on( 'attribute:class', ( evt, data, consumable ) => {
consumable.consume( data.item, 'attribute:class' );
}, { priority: 'high' } );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
// No attribute set.
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
describe( 'wrap', () => {
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 viewB = new ViewAttributeElement( 'b' );
dispatcher.on( 'attribute:bold', wrap( viewB ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeAttribute( 'bold', ModelRange.createIn( modelElement ) );
} );
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 elementGenerator = value => {
if ( value == 'bold' ) {
return new ViewAttributeElement( 'b' );
}
};
dispatcher.on( 'attribute:style', wrap( elementGenerator ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeAttribute( 'style', ModelRange.createIn( modelElement ) );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should update range on re-wrapping attribute (#475)', () => {
const modelElement = new ModelElement( 'paragraph', null, [
new ModelText( 'x' ),
new ModelText( 'foo', { link: 'http://foo.com' } ),
new ModelText( 'x' )
] );
const elementGenerator = href => new ViewAttributeElement( 'a', { href } );
dispatcher.on( 'attribute:link', wrap( elementGenerator ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
// Set new attribute on old link but also on non-linked characters.
model.change( writer => {
writer.setAttribute( 'link', 'http://foobar.com', ModelRange.createIn( modelElement ) );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should support unicode', () => {
const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
const viewB = new ViewAttributeElement( 'b' );
dispatcher.on( 'attribute:bold', wrap( viewB ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeAttribute( 'bold', ModelRange.createIn( modelElement ) );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should be possible to override wrap', () => {
const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
const viewB = new ViewAttributeElement( 'b' );
dispatcher.on( 'attribute:bold', wrap( viewB ) );
dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => {
consumable.consume( data.item, 'attribute:bold' );
}, { priority: 'high' } );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should not convert and not consume if creator function returned null', () => {
const elementGenerator = () => null;
sinon.spy( dispatcher, 'fire' );
const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) );
dispatcher.on( 'attribute:italic', wrap( elementGenerator ) );
dispatcher.on( 'attribute:italic', ( evt, data, consumable ) => {
expect( consumable.test( data.item, 'attribute:italic' ) ).to.be.true;
} );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
expect( dispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true;
} );
} );
describe( 'insertUIElement/removeUIElement', () => {
let modelText, modelElement, range;
beforeEach( () => {
modelText = new ModelText( 'foobar' );
modelElement = new ModelElement( 'paragraph', null, modelText );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
} );
describe( 'collapsed range', () => {
beforeEach( () => {
range = ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 3 );
} );
it( 'should insert and remove ui element - element as a creator', () => {
const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
model.change( writer => {
writer.setMarker( 'marker', range );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should insert and remove ui element - function as a creator', () => {
const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
dispatcher.on( 'addMarker:marker', insertUIElement( () => viewUi ) );
dispatcher.on( 'removeMarker:marker', removeUIElement( () => viewUi ) );
model.change( writer => {
writer.setMarker( 'marker', range );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should not convert if consumable was consumed', () => {
const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
sinon.spy( dispatcher, 'fire' );
dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
consumable.consume( data.markerRange, 'addMarker:marker' );
}, { priority: 'high' } );
dispatcher.convertMarkerAdd( 'marker', range );
expect( viewToString( viewRoot ) ).to.equal( '' );
expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
} );
it( 'should not convert if creator returned null', () => {
dispatcher.on( 'addMarker:marker', insertUIElement( () => null ) );
dispatcher.on( 'removeMarker:marker', removeUIElement( () => null ) );
model.change( writer => {
writer.setMarker( 'marker', range );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
describe( 'non-collapsed range', () => {
beforeEach( () => {
range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 5 );
} );
it( 'should insert and remove ui element - element as a creator', () => {
const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
model.change( writer => {
writer.setMarker( 'marker', range );
} );
expect( viewToString( viewRoot ) )
.to.equal( '' );
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should insert and remove ui element - function as a creator', () => {
const viewUi = data => new ViewUIElement( 'span', { 'class': data.markerName } );
dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
model.change( writer => {
writer.setMarker( 'marker', range );
} );
expect( viewToString( viewRoot ) )
.to.equal( '' );
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should insert and remove different opening and ending element', () => {
function creator( data ) {
if ( data.isOpening ) {
return new ViewUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
}
return new ViewUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
}
dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
model.change( writer => {
writer.setMarker( 'marker', range );
} );
expect( viewToString( viewRoot ) ).to.equal(
''
);
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should not convert if consumable was consumed', () => {
const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
sinon.spy( dispatcher, 'fire' );
dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
consumable.consume( data.item, 'addMarker:marker' );
}, { priority: 'high' } );
dispatcher.convertMarkerAdd( 'marker', range );
expect( viewToString( viewRoot ) ).to.equal( '' );
expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
} );
} );
} );
// Remove converter is by default already added in `EditingController` instance.
describe( 'remove', () => {
it( 'should remove items from view accordingly to changes in model #1', () => {
const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
model.change( writer => {
writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should be possible to overwrite', () => {
dispatcher.on( 'remove', evt => evt.stop(), { priority: 'high' } );
const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
model.change( writer => {
writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should support unicode', () => {
const modelElement = new ModelElement( 'paragraph', null, 'நிலைக்கு' );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
model.change( writer => {
writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 0, modelElement, 6 ) );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should not remove view ui elements that are placed next to removed content', () => {
modelRoot.appendChildren( new ModelText( 'fozbar' ) );
viewRoot.appendChildren( [
new ViewText( 'foz' ),
new ViewUIElement( 'span' ),
new ViewText( 'bar' )
] );
// Remove 'b'.
model.change( writer => {
writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
} );
expect( viewToString( viewRoot ) ).to.equal( 'fozar
' );
// Remove 'z'.
model.change( writer => {
writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 3 ) );
} );
expect( viewToString( viewRoot ) ).to.equal( 'foar
' );
} );
it( 'should remove correct amount of text when it is split by view ui element', () => {
modelRoot.appendChildren( new ModelText( 'fozbar' ) );
viewRoot.appendChildren( [
new ViewText( 'foz' ),
new ViewUIElement( 'span' ),
new ViewText( 'bar' )
] );
// Remove 'zb'.
model.change( writer => {
writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 4 ) );
} );
expect( viewToString( viewRoot ) ).to.equal( 'foar
' );
} );
it( 'should unbind elements', () => {
const modelElement = new ModelElement( 'paragraph' );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
const viewElement = controller.mapper.toViewElement( modelElement );
expect( viewElement ).not.to.be.undefined;
expect( controller.mapper.toModelElement( viewElement ) ).to.equal( modelElement );
model.change( writer => {
writer.remove( modelElement );
} );
expect( controller.mapper.toViewElement( modelElement ) ).to.be.undefined;
expect( controller.mapper.toModelElement( viewElement ) ).to.be.undefined;
} );
it( 'should not break when remove() is used as part of unwrapping', () => {
const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
const modelWidget = new ModelElement( 'widget', null, modelP );
dispatcher.on( 'insert:widget', insertElement( () => new ViewContainerElement( 'widget' ) ) );
model.change( writer => {
writer.insert( modelWidget, modelRootStart );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
const viewP = controller.mapper.toViewElement( modelP );
expect( viewP ).not.to.be.undefined;
model.change( writer => {
writer.unwrap( modelWidget );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
// `modelP` is now bound with newly created view element.
expect( controller.mapper.toViewElement( modelP ) ).not.to.equal( viewP );
// `viewP` is no longer bound with model element.
expect( controller.mapper.toModelElement( viewP ) ).to.be.undefined;
// View element from view root is bound to `modelP`.
expect( controller.mapper.toModelElement( viewRoot.getChild( 0 ) ) ).to.equal( modelP );
} );
it( 'should work correctly if container element after ui element is removed', () => {
// Prepare a model and view structure.
// This is done outside of conversion to put view ui elements inside easily.
const modelP1 = new ModelElement( 'paragraph' );
const modelP2 = new ModelElement( 'paragraph' );
const viewP1 = new ViewContainerElement( 'p' );
const viewUi1 = new ViewUIElement( 'span' );
const viewUi2 = new ViewUIElement( 'span' );
const viewP2 = new ViewContainerElement( 'p' );
modelRoot.appendChildren( [ modelP1, modelP2 ] );
viewRoot.appendChildren( [ viewP1, viewUi1, viewUi2, viewP2 ] );
controller.mapper.bindElements( modelP1, viewP1 );
controller.mapper.bindElements( modelP2, viewP2 );
// Remove second paragraph element.
model.change( writer => {
writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should work correctly if container element after text node is removed', () => {
const modelText = new ModelText( 'foo' );
const modelP = new ModelElement( 'paragraph' );
model.change( writer => {
writer.insert( [ modelText, modelP ], modelRootStart );
} );
model.change( writer => {
writer.remove( modelP );
} );
expect( viewToString( viewRoot ) ).to.equal( 'foo
' );
} );
} );
describe( 'highlight', () => {
describe( 'on text', () => {
const highlightDescriptor = {
class: 'highlight-class',
priority: 7,
attributes: { title: 'title' }
};
let markerRange;
beforeEach( () => {
const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
model.change( writer => {
writer.insert( [ modelElement1, modelElement2 ], modelRootStart );
} );
markerRange = ModelRange.createIn( modelRoot );
} );
it( 'should wrap and unwrap text nodes', () => {
dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) );
model.change( writer => {
writer.setMarker( 'marker', markerRange );
} );
expect( viewToString( viewRoot ) ).to.equal(
'' +
'
' +
'foo' +
'
' +
'
' +
'bar' +
'
' +
'
'
);
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should be possible to overwrite', () => {
dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) );
const newDescriptor = { class: 'override-class' };
dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } );
dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } );
model.change( writer => {
writer.setMarker( 'marker', markerRange );
} );
expect( viewToString( viewRoot ) ).to.equal(
'' +
'
' +
'foo' +
'
' +
'
' +
'bar' +
'
' +
'
'
);
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should do nothing if descriptor is not provided or generating function returns null', () => {
dispatcher.on( 'addMarker:marker', highlightText( () => null ), { priority: 'high' } );
dispatcher.on( 'addMarker:marker', highlightElement( () => null ), { priority: 'high' } );
dispatcher.on( 'removeMarker:marker', removeHighlight( () => null ), { priority: 'high' } );
model.change( writer => {
writer.setMarker( 'marker', markerRange );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
describe( 'on element', () => {
const highlightDescriptor = {
class: 'highlight-class',
priority: 7,
attributes: { title: 'title' },
id: 'customId'
};
let markerRange;
beforeEach( () => {
// Provide converter for div element. View div element will have custom highlight handling.
dispatcher.on( 'insert:div', insertElement( () => {
const viewContainer = new ViewContainerElement( 'div' );
viewContainer.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
element.addClass( descriptor.class );
} );
viewContainer.setCustomProperty( 'removeHighlight', element => {
element.setAttribute( 'class', '' );
} );
return viewContainer;
} ) );
const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) );
model.change( writer => {
writer.insert( modelElement, modelRootStart );
} );
markerRange = ModelRange.createOn( modelElement );
dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) );
} );
it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => {
model.change( writer => {
writer.setMarker( 'marker', markerRange );
} );
expect( viewToString( viewRoot ) ).to.equal(
''
);
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should be possible to override', () => {
const newDescriptor = { class: 'override-class' };
dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } );
dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } );
model.change( writer => {
writer.setMarker( 'marker', markerRange );
} );
expect( viewToString( viewRoot ) ).to.equal(
''
);
model.change( writer => {
writer.removeMarker( 'marker' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
it( 'should use default priority and id if not provided', () => {
const viewDiv = viewRoot.getChild( 0 );
dispatcher.on( 'addMarker:marker2', highlightText( () => null ) );
dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) );
dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) );
viewDiv.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
expect( descriptor.priority ).to.equal( 10 );
expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
} );
viewDiv.setCustomProperty( 'removeHighlight', ( element, id ) => {
expect( id ).to.equal( 'marker:foo-bar-baz' );
} );
model.change( writer => {
writer.setMarker( 'marker2', markerRange );
} );
} );
it( 'should do nothing if descriptor is not provided', () => {
dispatcher.on( 'addMarker:marker2', highlightText( () => null ) );
dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) );
dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) );
model.change( writer => {
writer.setMarker( 'marker2', markerRange );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
model.change( writer => {
writer.removeMarker( 'marker2' );
} );
expect( viewToString( viewRoot ) ).to.equal( '' );
} );
} );
} );
describe( 'createViewElementFromHighlightDescriptor()', () => {
it( 'should return attribute element from descriptor object', () => {
const descriptor = {
class: 'foo-class',
attributes: { one: '1', two: '2' },
priority: 7,
};
const element = createViewElementFromHighlightDescriptor( descriptor );
expect( element.is( 'attributeElement' ) ).to.be.true;
expect( element.name ).to.equal( 'span' );
expect( element.priority ).to.equal( 7 );
expect( element.hasClass( 'foo-class' ) ).to.be.true;
for ( const key of Object.keys( descriptor.attributes ) ) {
expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
}
} );
it( 'should return attribute element from descriptor object - array with classes', () => {
const descriptor = {
class: [ 'foo-class', 'bar-class' ],
attributes: { one: '1', two: '2' },
priority: 7,
};
const element = createViewElementFromHighlightDescriptor( descriptor );
expect( element.is( 'attributeElement' ) ).to.be.true;
expect( element.name ).to.equal( 'span' );
expect( element.priority ).to.equal( 7 );
expect( element.hasClass( 'foo-class' ) ).to.be.true;
expect( element.hasClass( 'bar-class' ) ).to.be.true;
for ( const key of Object.keys( descriptor.attributes ) ) {
expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
}
} );
it( 'should create element without class', () => {
const descriptor = {
attributes: { one: '1', two: '2' },
priority: 7,
};
const element = createViewElementFromHighlightDescriptor( descriptor );
expect( element.is( 'attributeElement' ) ).to.be.true;
expect( element.name ).to.equal( 'span' );
expect( element.priority ).to.equal( 7 );
for ( const key of Object.keys( descriptor.attributes ) ) {
expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
}
} );
it( 'should create element without priority', () => {
const descriptor = {
class: 'foo-class',
attributes: { one: '1', two: '2' },
};
const element = createViewElementFromHighlightDescriptor( descriptor );
expect( element.is( 'attributeElement' ) ).to.be.true;
expect( element.name ).to.equal( 'span' );
expect( element.priority ).to.equal( 10 );
expect( element.hasClass( 'foo-class' ) ).to.be.true;
for ( const key of Object.keys( descriptor.attributes ) ) {
expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
}
} );
it( 'should create element without attributes', () => {
const descriptor = {
class: 'foo-class',
priority: 7
};
const element = createViewElementFromHighlightDescriptor( descriptor );
expect( element.is( 'attributeElement' ) ).to.be.true;
expect( element.name ).to.equal( 'span' );
expect( element.priority ).to.equal( 7 );
expect( element.hasClass( 'foo-class' ) ).to.be.true;
} );
it( 'should create similar elements if they are created using same descriptor id', () => {
const a = createViewElementFromHighlightDescriptor( {
id: 'id',
class: 'classA',
priority: 1
} );
const b = createViewElementFromHighlightDescriptor( {
id: 'id',
class: 'classB',
priority: 2
} );
expect( a.isSimilar( b ) ).to.be.true;
} );
it( 'should create non-similar elements if they have different descriptor id', () => {
const a = createViewElementFromHighlightDescriptor( {
id: 'a',
class: 'foo',
priority: 1
} );
const b = createViewElementFromHighlightDescriptor( {
id: 'b',
class: 'foo',
priority: 1
} );
expect( a.isSimilar( b ) ).to.be.false;
} );
} );
} );