ソースを参照

Merge pull request #775 from ckeditor/t/702

T/702 Selection breaks ViewAttributeElement created by marker conversion
Piotr Jasiun 9 年 前
コミット
4c9d107da5

+ 4 - 1
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -101,7 +101,10 @@ export default class EditingController {
 
 		// Convert model selection to view.
 		this._listener.listenTo( this.model, 'changesDone', () => {
-			this.modelToView.convertSelection( model.selection );
+			const selection = model.selection;
+			const markers = Array.from( model.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
+
+			this.modelToView.convertSelection( selection, markers );
 			this.view.render();
 		}, { priority: 'low' } );
 

+ 3 - 1
packages/ckeditor5-engine/src/conversion/buildmodelconverter.js

@@ -17,7 +17,7 @@ import {
 	unwrapRange
 } from './model-to-view-converters';
 
-import { convertSelectionAttribute } from './model-selection-to-view-converters';
+import { convertSelectionAttribute, convertSelectionMarker } from './model-selection-to-view-converters';
 
 import ViewAttributeElement from '../view/attributeelement';
 import ViewContainerElement from '../view/containerelement';
@@ -246,6 +246,8 @@ class ModelConverterBuilder {
 
 				dispatcher.on( 'addMarker:' + this._from.name, wrapRange( element ), { priority } );
 				dispatcher.on( 'removeMarker:' + this._from.name, unwrapRange( element ), { priority } );
+
+				dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( element ), { priority } );
 			}
 		}
 	}

+ 44 - 14
packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js

@@ -145,29 +145,59 @@ export function convertCollapsedSelection() {
  */
 export function convertSelectionAttribute( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
-		const selection = data.selection;
+		const viewElement = elementCreator instanceof ViewElement ?
+			elementCreator.clone( true ) :
+			elementCreator( data.value, data, data.selection, consumable, conversionApi );
 
-		if ( !selection.isCollapsed ) {
-			return;
-		}
+		const consumableName = 'selectionAttribute:' + data.key;
 
-		if ( !consumable.consume( selection, 'selectionAttribute:' + data.key ) ) {
-			return;
-		}
-
-		let viewPosition = conversionApi.viewSelection.getFirstPosition();
-		conversionApi.viewSelection.removeAllRanges();
+		wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
+	};
+}
 
+/**
+ * Performs similar conversion as {@link ~convertSelectionAttribute}, but depends on a marker name of a marker in which
+ * collapsed selection is placed.
+ *
+ *		modelDispatcher.on( 'selectionMarker:searchResult', wrapRange( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
+ *
+ * **Note:** You can use the same `elementCreator` function for this converter factory
+ * and {@link module:engine/conversion/model-to-view-converters~wrapRange}.
+ *
+ * @see {~convertSelectionAttribute}
+ * @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View element,
+ * or function returning a view element, which will be used for wrapping.
+ * @returns {Function} Selection converter.
+ */
+export function convertSelectionMarker( elementCreator ) {
+	return ( evt, data, consumable, conversionApi ) => {
 		const viewElement = elementCreator instanceof ViewElement ?
-				elementCreator.clone( true ) :
-				elementCreator( data.value, data, selection, consumable, conversionApi );
+			elementCreator.clone( true ) :
+			elementCreator( data, consumable, conversionApi );
 
-		viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
+		const consumableName = 'selectionMarker:' + data.name;
 
-		conversionApi.viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
+		wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
 	};
 }
 
+// Helper function for `convertSelectionAttribute` and `convertSelectionMarker`, which perform similar task.
+function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElement, consumable, consumableName ) {
+	if ( !modelSelection.isCollapsed ) {
+		return;
+	}
+
+	if ( !consumable.consume( modelSelection, consumableName ) ) {
+		return;
+	}
+
+	let viewPosition = viewSelection.getFirstPosition();
+	viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
+
+	viewSelection.removeAllRanges();
+	viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
+}
+
 /**
  * Function factory, creates a converter that clears artifacts after the previous
  * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty

+ 26 - 9
packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js

@@ -308,18 +308,30 @@ export default class ModelConversionDispatcher {
 	 * @fires selection
 	 * @fires selectionAttribute
 	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
+	 * @param {Array.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
 	 */
-	convertSelection( selection ) {
-		const consumable = this._createSelectionConsumable( selection );
-		const data = {
-			selection: selection
-		};
+	convertSelection( selection, markers ) {
+		const consumable = this._createSelectionConsumable( selection, markers );
 
-		this.fire( 'selection', data, consumable, this.conversionApi );
+		this.fire( 'selection', { selection }, consumable, this.conversionApi );
+
+		for ( let marker of markers ) {
+			const data = {
+				selection: selection,
+				name: marker.name
+			};
+
+			if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) {
+				this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi );
+			}
+		}
 
 		for ( let key of selection.getAttributeKeys() ) {
-			data.key = key;
-			data.value = selection.getAttribute( key );
+			const data = {
+				selection: selection,
+				key: key,
+				value: selection.getAttribute( key )
+			};
 
 			// Do not fire event if the attribute has been consumed.
 			if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
@@ -397,13 +409,18 @@ export default class ModelConversionDispatcher {
 	 *
 	 * @private
 	 * @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
+	 * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
 	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
 	 */
-	_createSelectionConsumable( selection ) {
+	_createSelectionConsumable( selection, markers ) {
 		const consumable = new Consumable();
 
 		consumable.add( selection, 'selection' );
 
+		for ( let marker of markers ) {
+			consumable.add( selection, 'selectionMarker:' + marker.name );
+		}
+
 		for ( let key of selection.getAttributeKeys() ) {
 			consumable.add( selection, 'selectionAttribute:' + key );
 		}

+ 1 - 1
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -236,7 +236,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 
 	// Convert model selection to view selection.
 	if ( selection ) {
-		modelToView.convertSelection( selection );
+		modelToView.convertSelection( selection, [] );
 	}
 
 	// Parse view to data string.

+ 8 - 8
packages/ckeditor5-engine/src/model/document.js

@@ -59,14 +59,6 @@ export default class Document {
 		this.version = 0;
 
 		/**
-		 * Selection done on this document.
-		 *
-		 * @readonly
-		 * @member {module:engine/model/liveselection~LiveSelection}
-		 */
-		this.selection = new LiveSelection( this );
-
-		/**
 		 * Schema for this document.
 		 *
 		 * @member {module:engine/model/schema~Schema}
@@ -92,6 +84,14 @@ export default class Document {
 		this.markers = new MarkerCollection();
 
 		/**
+		 * Selection done on this document.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/liveselection~LiveSelection}
+		 */
+		this.selection = new LiveSelection( this );
+
+		/**
 		 * Array of pending changes. See: {@link #enqueueChanges}.
 		 *
 		 * @private

+ 8 - 8
packages/ckeditor5-engine/src/model/liveselection.js

@@ -29,17 +29,18 @@ const attrOpTypes = new Set(
  * {@link module:engine/model/document~Document document} and has it ranges updated accordingly. Internal implementation of this
  * mechanism bases on {@link module:engine/model/liverange~LiveRange live ranges}.
  *
- * Differences between {@link module:engine/model/selection~Selection} and `LiveSelection` are two:
+ * Differences between {@link module:engine/model/selection~Selection} and `LiveSelection` are:
  * * there is always a range in `LiveSelection` - even if no ranges were added there is a
  * {@link module:engine/model/liveselection~LiveSelection#_getDefaultRange "default range"} present in the selection,
- * * ranges added to this selection updates automatically when the document changes.
+ * * ranges added to this selection updates automatically when the document changes,
+ * * attributes of `LiveSelection` are updated automatically according to selection ranges.
  *
  * Since `LiveSelection` uses {@link module:engine/model/liverange~LiveRange live ranges}
  * and is updated when {@link module:engine/model/document~Document document}
  * changes, it cannot be set on {@link module:engine/model/node~Node nodes}
  * that are inside {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
  * If you need to represent a selection in document fragment,
- * use {@link module:engine/model/selection~Selection "normal" selection} instead.
+ * use {@link module:engine/model/selection~Selection Selection class} instead.
  *
  * @extends module:engine/model/selection~Selection
  */
@@ -72,9 +73,8 @@ export default class LiveSelection extends Selection {
 		 */
 		this._attributePriority = new Map();
 
-		// Whenever attribute operation is performed on document, update attributes. This is not the most efficient
-		// way to update selection attributes, but should be okay for now. `_updateAttributes` will be fired too often,
-		// but it won't change attributes or fire `change:attribute` event if not needed.
+		// Whenever attribute operation is performed on document, update selection attributes.
+		// This is not the most efficient way to update selection attributes, but should be okay for now.
 		this.listenTo( this._document, 'change', ( evt, type ) => {
 			if ( attrOpTypes.has( type ) ) {
 				this._updateAttributes( false );
@@ -638,9 +638,9 @@ export default class LiveSelection extends Selection {
 
 		const newRange = this._prepareRange( selectionRange );
 		const index = this._ranges.indexOf( gyRange );
-		this._ranges.splice( index, 1, newRange );
-
 		gyRange.detach();
+
+		this._ranges.splice( index, 1, newRange );
 	}
 }
 

+ 14 - 0
packages/ckeditor5-engine/src/model/markercollection.js

@@ -116,6 +116,20 @@ export default class MarkerCollection {
 	}
 
 	/**
+	 * Returns iterator that iterates over all markers, which ranges contain given {@link module:engine/model/position~Position position}.
+	 *
+	 * @param {module:engine/model/position~Position} position
+	 * @returns {Iterator.<~Marker>}
+	 */
+	*getMarkersAtPosition( position ) {
+		for ( let marker of this ) {
+			if ( marker.getRange().containsPosition( position ) ) {
+				yield marker;
+			}
+		}
+	}
+
+	/**
 	 * Destroys markers collection.
 	 */
 	destroy() {

+ 1 - 1
packages/ckeditor5-engine/src/model/selection.js

@@ -46,7 +46,7 @@ export default class Selection {
 		 * List of attributes set on current selection.
 		 *
 		 * @protected
-		 * @member {Map} module:engine/model/liveselection~LiveSelection#_attrs
+		 * @member {Map} module:engine/model/selection~Selection#_attrs
 		 */
 		this._attrs = new Map();
 	}

+ 2 - 0
packages/ckeditor5-engine/src/view/writer.js

@@ -887,6 +887,8 @@ function wrapChildren( parent, startOffset, endOffset, attribute ) {
 	let offsetChange = 0;
 
 	for ( let position of wrapPositions ) {
+		position.offset -= offsetChange;
+
 		// Do not merge with elements outside selected children.
 		if ( position.offset == startOffset ) {
 			continue;

+ 4 - 4
packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js

@@ -212,7 +212,7 @@ describe( 'Model converter builder', () => {
 
 			// Convert stuff.
 			dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
-			dispatcher.convertSelection( modelDoc.selection );
+			dispatcher.convertSelection( modelDoc.selection, [] );
 
 			// Check if view structure is ok.
 			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
@@ -226,7 +226,7 @@ describe( 'Model converter builder', () => {
 
 			// Change selection attribute, convert it.
 			modelDoc.selection.setAttribute( 'italic', 'i' );
-			dispatcher.convertSelection( modelDoc.selection );
+			dispatcher.convertSelection( modelDoc.selection, [] );
 
 			// Check if view structure has changed.
 			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><i></i><em>oo</em></div>' );
@@ -240,7 +240,7 @@ describe( 'Model converter builder', () => {
 
 			// Some more tests checking how selection attributes changes are converted:
 			modelDoc.selection.removeAttribute( 'italic' );
-			dispatcher.convertSelection( modelDoc.selection );
+			dispatcher.convertSelection( modelDoc.selection, [] );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><em>oo</em></div>' );
 			ranges = Array.from( viewSelection.getRanges() );
@@ -248,7 +248,7 @@ describe( 'Model converter builder', () => {
 			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
 
 			modelDoc.selection.setAttribute( 'italic', 'em' );
-			dispatcher.convertSelection( modelDoc.selection );
+			dispatcher.convertSelection( modelDoc.selection, [] );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
 			ranges = Array.from( viewSelection.getRanges() );

+ 95 - 9
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -19,6 +19,7 @@ import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 	convertSelectionAttribute,
+	convertSelectionMarker,
 	clearAttributes,
 	clearFakeSelection
 } from '../../src/conversion/model-selection-to-view-converters';
@@ -26,12 +27,17 @@ import {
 import {
 	insertElement,
 	insertText,
-	wrapItem
+	wrapItem,
+	wrapRange,
+	unwrapRange
 } from '../../src/conversion/model-to-view-converters';
 
 import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
+const spanViewElementForMarker = new ViewAttributeElement( 'span' );
+spanViewElementForMarker.priority = 1;
+
 describe( 'model-selection-to-view-converters', () => {
 	let dispatcher, mapper;
 	let modelDoc, modelRoot, modelSelection;
@@ -55,6 +61,8 @@ describe( 'model-selection-to-view-converters', () => {
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+		dispatcher.on( 'addMarker:marker', wrapRange( spanViewElementForMarker ) );
+		dispatcher.on( 'removeMarker:marker', unwrapRange( spanViewElementForMarker ) );
 
 		// Default selection converters.
 		dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
@@ -71,6 +79,7 @@ describe( 'model-selection-to-view-converters', () => {
 			// Selection converters for selection attributes.
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
 			dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
+			dispatcher.on( 'selectionMarker:marker', convertSelectionMarker( spanViewElementForMarker ) );
 		} );
 
 		describe( 'range selection', () => {
@@ -207,6 +216,81 @@ describe( 'model-selection-to-view-converters', () => {
 				);
 			} );
 
+			it( 'in attribute and marker', () => {
+				setModelData( modelDoc, 'fo<$text bold="true">ob</$text>ar' );
+				const marker = modelDoc.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+
+				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+
+				// Update selection attributes according to model.
+				modelSelection.refreshAttributes();
+
+				// Remove view children manually (without firing additional conversion).
+				viewRoot.removeChildren( 0, viewRoot.childCount );
+
+				// Convert model to view.
+				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
+				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
+
+				const markers = Array.from( modelDoc.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
+				dispatcher.convertSelection( modelSelection, markers );
+
+				// Stringify view and check if it is same as expected.
+				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
+					.to.equal( '<div>f<span>o<strong>o{}b</strong>a</span>r</div>' );
+			} );
+
+			it( 'in attribute and marker - no attribute', () => {
+				setModelData( modelDoc, 'fo<$text bold="true">ob</$text>ar' );
+				const marker = modelDoc.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+
+				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+
+				// Update selection attributes according to model.
+				modelSelection.refreshAttributes();
+
+				modelSelection.removeAttribute( 'bold' );
+
+				// Remove view children manually (without firing additional conversion).
+				viewRoot.removeChildren( 0, viewRoot.childCount );
+
+				// Convert model to view.
+				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
+				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
+
+				const markers = Array.from( modelDoc.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
+				dispatcher.convertSelection( modelSelection, markers );
+
+				// Stringify view and check if it is same as expected.
+				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
+					.to.equal( '<div>f<span>o<strong>o</strong>[]<strong>b</strong>a</span>r</div>' );
+			} );
+
+			it( 'in marker - using view element as element creator function for selection marker conversion', () => {
+				dispatcher.on( 'selectionMarker:marker2', convertSelectionMarker(
+					( data ) => new ViewAttributeElement( 'span', { 'class': data.name } )
+				) );
+
+				setModelData( modelDoc, 'foobar' );
+				const marker = modelDoc.markers.set( 'marker2', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+
+				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+
+				// Remove view children manually (without firing additional conversion).
+				viewRoot.removeChildren( 0, viewRoot.childCount );
+
+				// Convert model to view.
+				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
+				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
+
+				const markers = Array.from( modelDoc.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
+				dispatcher.convertSelection( modelSelection, markers );
+
+				// Stringify view and check if it is same as expected.
+				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
+					.to.equal( '<div>foo<span class="marker2">[]</span>bar</div>' );
+			} );
+
 			it( 'consumes consumable values properly', () => {
 				// Add callbacks that will fire before default ones.
 				// This should prevent default callbacks doing anything.
@@ -218,7 +302,7 @@ describe( 'model-selection-to-view-converters', () => {
 					expect( consumable.consume( data.selection, 'selectionAttribute:bold' ) ).to.be.true;
 				}, { priority: 'high' } );
 
-				// Similar test case as above
+				// Similar test case as above.
 				test(
 					[ 3, 3 ],
 					'f<$text bold="true">ooba</$text>r',
@@ -280,7 +364,7 @@ describe( 'model-selection-to-view-converters', () => {
 				const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
 				modelDoc.selection.setRanges( [ modelRange ] );
 
-				dispatcher.convertSelection( modelDoc.selection );
+				dispatcher.convertSelection( modelDoc.selection, [] );
 
 				expect( viewSelection.rangeCount ).to.equal( 1 );
 
@@ -305,7 +389,7 @@ describe( 'model-selection-to-view-converters', () => {
 				const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
 				modelDoc.selection.setRanges( [ modelRange ] );
 
-				dispatcher.convertSelection( modelDoc.selection );
+				dispatcher.convertSelection( modelDoc.selection, [] );
 
 				expect( viewSelection.rangeCount ).to.equal( 1 );
 
@@ -319,7 +403,7 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.on( 'selection', clearFakeSelection() );
 				viewSelection.setFake( true );
 
-				dispatcher.convertSelection( modelSelection );
+				dispatcher.convertSelection( modelSelection, [] );
 
 				expect( viewSelection.isFake ).to.be.false;
 			} );
@@ -499,13 +583,15 @@ describe( 'model-selection-to-view-converters', () => {
 		// Manually set selection ranges using passed `selectionPaths`.
 		const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ];
 		const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ];
+
 		const startPos = new ModelPosition( modelRoot, startPath );
 		const endPos = new ModelPosition( modelRoot, endPath );
+
 		const isBackward = selectionPaths[ 2 ] === 'backward';
 		modelSelection.setRanges( [ new ModelRange( startPos, endPos ) ], isBackward );
 
-		// Updated selection attributes according to model.
-		modelSelection._updateAttributes();
+		// Update selection attributes according to model.
+		modelSelection.refreshAttributes();
 
 		// And add or remove passed attributes.
 		for ( let key in selectionAttributes ) {
@@ -518,12 +604,12 @@ describe( 'model-selection-to-view-converters', () => {
 			}
 		}
 
-		// Remove view children since we do not want to convert deletion.
+		// Remove view children manually (without firing additional conversion).
 		viewRoot.removeChildren( 0, viewRoot.childCount );
 
 		// Convert model to view.
 		dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
-		dispatcher.convertSelection( modelSelection );
+		dispatcher.convertSelection( modelSelection, [] );
 
 		// Stringify view and check if it is same as expected.
 		expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '<div>' + expectedView + '</div>' );

+ 32 - 4
packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js

@@ -447,7 +447,7 @@ describe( 'ModelConversionDispatcher', () => {
 		it( 'should fire selection event', () => {
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convertSelection( doc.selection );
+			dispatcher.convertSelection( doc.selection, [] );
 
 			expect( dispatcher.fire.calledWith(
 				'selection',
@@ -468,7 +468,7 @@ describe( 'ModelConversionDispatcher', () => {
 				expect( consumable.test( data.selection, 'selectionAttribute:italic' ) ).to.be.null;
 			} );
 
-			dispatcher.convertSelection( doc.selection );
+			dispatcher.convertSelection( doc.selection, [] );
 		} );
 
 		it( 'should fire attributes events for selection', () => {
@@ -480,7 +480,7 @@ describe( 'ModelConversionDispatcher', () => {
 					.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true );
 			} );
 
-			dispatcher.convertSelection( doc.selection );
+			dispatcher.convertSelection( doc.selection, [] );
 
 			expect( dispatcher.fire.calledWith( 'selectionAttribute:bold' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'selectionAttribute:italic' ) ).to.be.false;
@@ -499,10 +499,38 @@ describe( 'ModelConversionDispatcher', () => {
 					.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true );
 			} );
 
-			dispatcher.convertSelection( doc.selection );
+			dispatcher.convertSelection( doc.selection, [] );
 
 			expect( dispatcher.fire.calledWith( 'selectionAttribute:bold' ) ).to.be.false;
 		} );
+
+		it( 'should fire events for each marker which contains selection', () => {
+			doc.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+
+			sinon.spy( dispatcher, 'fire' );
+
+			const markers = Array.from( doc.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
+			dispatcher.convertSelection( doc.selection, markers );
+
+			expect( dispatcher.fire.calledWith( 'selectionMarker:name' ) ).to.be.true;
+		} );
+
+		it( 'should not fire events if information about marker has been consumed', () => {
+			doc.markers.set( 'foo', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+			doc.markers.set( 'bar', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+
+			sinon.spy( dispatcher, 'fire' );
+
+			dispatcher.on( 'selectionMarker:foo', ( evt, data, consumable ) => {
+				consumable.consume( data.selection, 'selectionMarker:bar' );
+			} );
+
+			const markers = Array.from( doc.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
+			dispatcher.convertSelection( doc.selection, markers );
+
+			expect( dispatcher.fire.calledWith( 'selectionMarker:foo' ) ).to.be.true;
+			expect( dispatcher.fire.calledWith( 'selectionMarker:bar' ) ).to.be.false;
+		} );
 	} );
 
 	describe( 'convertMarker', () => {

+ 11 - 0
packages/ckeditor5-engine/tests/model/markercollection.js

@@ -167,6 +167,17 @@ describe( 'MarkerCollection', () => {
 		} );
 	} );
 
+	describe( 'getMarkersAtPosition', () => {
+		it( 'should return iterator iterating over all markers that contains given position', () => {
+			markers.set( 'a', range );
+			const markerB = markers.set( 'b', range2 );
+
+			const result = Array.from( markers.getMarkersAtPosition( Position.createAt( root, 1 ) ) );
+
+			expect( result ).to.deep.equal( [ markerB ] );
+		} );
+	} );
+
 	describe( 'destroy', () => {
 		it( 'should make MarkerCollection stop listening to all events and destroy all markers', () => {
 			const markerA = markers.set( 'a', range );

+ 12 - 0
packages/ckeditor5-engine/tests/view/writer/wrap.js

@@ -192,6 +192,18 @@ describe( 'writer', () => {
 			);
 		} );
 
+		it( 'merges wrapped nodes #6', () => {
+			test(
+				'<container:div>f{o<attribute:strong>ob</attribute:strong>a}r</container:div>',
+				'<attribute:span view-priority="1"></attribute:span>',
+				'<container:div>f[' +
+					'<attribute:span view-priority="1">o' +
+						'<attribute:strong view-priority="10">ob</attribute:strong>' +
+					'a</attribute:span>' +
+				']r</container:div>'
+			);
+		} );
+
 		it( 'should wrap single element by merging attributes', () => {
 			test(
 				'<container:p>[<attribute:b view-priority="1" foo="bar" one="two"></attribute:b>]</container:p>',