Explorar o código

Passing view writer with conversion API.

Szymon Kupś %!s(int64=8) %!d(string=hai) anos
pai
achega
513e17f61f

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

@@ -78,7 +78,7 @@ export default class EditingController {
 		 * @readonly
 		 * @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} #modelToView
 		 */
-		this.modelToView = new ModelConversionDispatcher( this.model, {
+		this.modelToView = new ModelConversionDispatcher( this.model, this.view, {
 			mapper: this.mapper,
 			viewSelection: this.view.document.selection
 		} );
@@ -92,9 +92,6 @@ export default class EditingController {
 
 			// After the view is ready, convert selection from model to view.
 			this.modelToView.convertSelection( doc.selection );
-
-			// When everything is converted to the view, render it to DOM.
-			this.view.render();
 		}, { priority: 'low' } );
 
 		// Convert selection from view to model.

+ 20 - 9
packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js

@@ -5,11 +5,8 @@
 
 import ViewElement from '../view/element';
 import ViewRange from '../view/range';
-import ViewWriter from '../view/writer';
 import { createViewElementFromHighlightDescriptor } from './model-to-view-converters';
 
-const viewWriter = new ViewWriter();
-
 /**
  * Contains {@link module:engine/model/selection~Selection model selection} to
  * {@link module:engine/view/selection~Selection view selection} converters for
@@ -86,7 +83,7 @@ export function convertCollapsedSelection() {
 
 		const modelPosition = selection.getFirstPosition();
 		const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
-		const brokenPosition = viewWriter.breakAttributes( viewPosition );
+		const brokenPosition = conversionApi.writer.breakAttributes( viewPosition );
 
 		conversionApi.viewSelection.removeAllRanges();
 		conversionApi.viewSelection.addRange( new ViewRange( brokenPosition, brokenPosition ) );
@@ -153,7 +150,14 @@ export function convertSelectionAttribute( elementCreator ) {
 
 		const consumableName = 'selectionAttribute:' + data.key;
 
-		wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
+		wrapCollapsedSelectionPosition(
+			data.selection,
+			conversionApi.viewSelection,
+			viewElement,
+			consumable,
+			consumableName,
+			conversionApi.writer
+		);
 	};
 }
 
@@ -185,12 +189,19 @@ export function convertSelectionMarker( highlightDescriptor ) {
 		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
 		const consumableName = 'selectionMarker:' + data.markerName;
 
-		wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
+		wrapCollapsedSelectionPosition(
+			data.selection,
+			conversionApi.viewSelection,
+			viewElement,
+			consumable,
+			consumableName,
+			conversionApi.writer
+		);
 	};
 }
 
 // Helper function for `convertSelectionAttribute` and `convertSelectionMarker`, which perform similar task.
-function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElement, consumable, consumableName ) {
+function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElement, consumable, consumableName, writer ) {
 	if ( !modelSelection.isCollapsed ) {
 		return;
 	}
@@ -209,7 +220,7 @@ function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElem
 		viewPosition = viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
 	}
 	// End of hack.
-	viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
+	viewPosition = writer.wrapPosition( viewPosition, viewElement );
 
 	viewSelection.removeAllRanges();
 	viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
@@ -260,7 +271,7 @@ export function clearAttributes() {
 			if ( range.isCollapsed ) {
 				// Position might be in the node removed by the view writer.
 				if ( range.end.parent.document ) {
-					viewWriter.mergeAttributes( range.start );
+					conversionApi.writer.mergeAttributes( range.start );
 				}
 			}
 		}

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

@@ -9,9 +9,6 @@ import ViewElement from '../view/element';
 import ViewAttributeElement from '../view/attributeelement';
 import ViewText from '../view/text';
 import ViewRange from '../view/range';
-import WiewWriter from '../view/writer';
-
-const viewWriter = new WiewWriter();
 
 /**
  * Contains model to view converters for
@@ -65,7 +62,7 @@ export function insertElement( elementCreator ) {
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
 
 		conversionApi.mapper.bindElements( data.item, viewElement );
-		viewWriter.insert( viewPosition, viewElement );
+		conversionApi.writer.insert( viewPosition, viewElement );
 	};
 }
 
@@ -88,7 +85,7 @@ export function insertText() {
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
 		const viewText = new ViewText( data.item.data );
 
-		viewWriter.insert( viewPosition, viewText );
+		conversionApi.writer.insert( viewPosition, viewText );
 	};
 }
 
@@ -110,7 +107,7 @@ export function remove() {
 		const viewRange = new ViewRange( viewStart, viewEnd );
 
 		// Trim the range to remove in case some UI elements are on the view range boundaries.
-		const removed = viewWriter.remove( viewRange.getTrimmed() );
+		const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
 
 		// After the range is removed, unbind all view elements from the model.
 		// Range inside view document fragment is used to unbind deeply.
@@ -172,13 +169,14 @@ export function insertUIElement( elementCreator ) {
 		}
 
 		const mapper = conversionApi.mapper;
+		const writer = conversionApi.writer;
 
 		// Add "opening" element.
-		viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
+		writer.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
 
 		// Add "closing" element only if range is not collapsed.
 		if ( !markerRange.isCollapsed ) {
-			viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
+			writer.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
 		}
 
 		evt.stop();
@@ -215,15 +213,16 @@ export function removeUIElement( elementCreator ) {
 		}
 
 		const markerRange = data.markerRange;
+		const writer = conversionApi.writer;
 
 		// When removing the ui elements, we map the model range to view twice, because that view range
 		// may change after the first clearing.
 		if ( !markerRange.isCollapsed ) {
-			viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement );
+			writer.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement );
 		}
 
 		// Remove "opening" element.
-		viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement );
+		writer.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement );
 
 		evt.stop();
 	};
@@ -329,15 +328,16 @@ export function wrap( elementCreator ) {
 		}
 
 		let viewRange = conversionApi.mapper.toViewRange( data.range );
+		const writer = conversionApi.writer;
 
 		// First, unwrap the range from current wrapper.
 		if ( data.attributeOldValue !== null ) {
-			viewRange = viewWriter.unwrap( viewRange, oldViewElement );
+			viewRange = writer.unwrap( viewRange, oldViewElement );
 		}
 
 		// Then wrap with the new wrapper.
 		if ( data.attributeNewValue !== null ) {
-			viewWriter.wrap( viewRange, newViewElement );
+			writer.wrap( viewRange, newViewElement );
 		}
 	};
 }
@@ -379,7 +379,7 @@ export function highlightText( highlightDescriptor ) {
 		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
 		const viewRange = conversionApi.mapper.toViewRange( data.range );
 
-		viewWriter.wrap( viewRange, viewElement );
+		conversionApi.writer.wrap( viewRange, viewElement );
 	};
 }
 
@@ -496,7 +496,7 @@ export function removeHighlight( highlightDescriptor ) {
 
 		for ( const item of Array.from( items ).reverse() ) {
 			if ( item.is( 'textProxy' ) ) {
-				viewWriter.unwrap( ViewRange.createOn( item ), viewHighlightElement );
+				conversionApi.writer.unwrap( ViewRange.createOn( item ), viewHighlightElement );
 			}
 		}
 	};

+ 140 - 111
packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js

@@ -105,7 +105,7 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls.
 	 */
-	constructor( model, conversionApi = {} ) {
+	constructor( model, view, conversionApi = {} ) {
 		/**
 		 * Data model instance bound with this dispatcher.
 		 *
@@ -114,6 +114,8 @@ export default class ModelConversionDispatcher {
 		 */
 		this._model = model;
 
+		this._view = view;
+
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
@@ -128,27 +130,31 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
 	 */
 	convertChanges( differ ) {
-		// First, before changing view structure, remove all markers that has changed.
-		for ( const change of differ.getMarkersToRemove() ) {
-			this.convertMarkerRemove( change.name, change.range );
-		}
+		this._view.change( writer => {
+			this.conversionApi.writer = writer;
 
-		// Convert changes that happened on model tree.
-		for ( const entry of differ.getChanges() ) {
-			if ( entry.type == 'insert' ) {
-				this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ) );
-			} else if ( entry.type == 'remove' ) {
-				this.convertRemove( entry.position, entry.length, entry.name );
-			} else {
-				// entry.type == 'attribute'.
-				this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue );
+			// First, before changing view structure, remove all markers that has changed.
+			for ( const change of differ.getMarkersToRemove() ) {
+				this.convertMarkerRemove( change.name, change.range );
 			}
-		}
 
-		// After the view is updated, convert markers which has changed.
-		for ( const change of differ.getMarkersToAdd() ) {
-			this.convertMarkerAdd( change.name, change.range );
-		}
+			// Convert changes that happened on model tree.
+			for ( const entry of differ.getChanges() ) {
+				if ( entry.type == 'insert' ) {
+					this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ) );
+				} else if ( entry.type == 'remove' ) {
+					this.convertRemove( entry.position, entry.length, entry.name );
+				} else {
+					// entry.type == 'attribute'.
+					this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue );
+				}
+			}
+
+			// After the view is updated, convert markers which has changed.
+			for ( const change of differ.getMarkersToAdd() ) {
+				this.convertMarkerAdd( change.name, change.range );
+			}
+		} );
 	}
 
 	/**
@@ -162,31 +168,35 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/model/range~Range} range Inserted range.
 	 */
 	convertInsert( range ) {
-		// Create a list of things that can be consumed, consisting of nodes and their attributes.
-		const consumable = this._createInsertConsumable( range );
-
-		// Fire a separate insert event for each node and text fragment contained in the range.
-		for ( const value of range ) {
-			const item = value.item;
-			const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
-			const data = {
-				item,
-				range: itemRange
-			};
-
-			this._testAndFire( 'insert', data, consumable );
-
-			// Fire a separate addAttribute event for each attribute that was set on inserted items.
-			// This is important because most attributes converters will listen only to add/change/removeAttribute events.
-			// If we would not add this part, attributes on inserted nodes would not be converted.
-			for ( const key of item.getAttributeKeys() ) {
-				data.attributeKey = key;
-				data.attributeOldValue = null;
-				data.attributeNewValue = item.getAttribute( key );
-
-				this._testAndFire( `attribute:${ key }`, data, consumable );
+		this._view.change( writer => {
+			this.conversionApi.writer = writer;
+
+			// Create a list of things that can be consumed, consisting of nodes and their attributes.
+			const consumable = this._createInsertConsumable( range );
+
+			// Fire a separate insert event for each node and text fragment contained in the range.
+			for ( const value of range ) {
+				const item = value.item;
+				const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
+				const data = {
+					item,
+					range: itemRange
+				};
+
+				this._testAndFire( 'insert', data, consumable );
+
+				// Fire a separate addAttribute event for each attribute that was set on inserted items.
+				// This is important because most attributes converters will listen only to add/change/removeAttribute events.
+				// If we would not add this part, attributes on inserted nodes would not be converted.
+				for ( const key of item.getAttributeKeys() ) {
+					data.attributeKey = key;
+					data.attributeOldValue = null;
+					data.attributeNewValue = item.getAttribute( key );
+
+					this._testAndFire( `attribute:${ key }`, data, consumable );
+				}
 			}
-		}
+		} );
 	}
 
 	/**
@@ -197,7 +207,11 @@ export default class ModelConversionDispatcher {
 	 * @param {String} name Name of removed node.
 	 */
 	convertRemove( position, length, name ) {
-		this.fire( 'remove:' + name, { position, length }, this.conversionApi );
+		this._view.change( writer => {
+			this.conversionApi.writer = writer;
+
+			this.fire( 'remove:' + name, { position, length }, this.conversionApi );
+		} );
 	}
 
 	/**
@@ -212,23 +226,27 @@ export default class ModelConversionDispatcher {
 	 * @param {*} newValue New attribute value or `null` if the attribute has been removed.
 	 */
 	convertAttribute( range, key, oldValue, newValue ) {
-		// Create a list with attributes to consume.
-		const consumable = this._createConsumableForRange( range, `attribute:${ key }` );
+		this._view.change( writer => {
+			this.conversionApi.writer = writer;
+
+			// Create a list with attributes to consume.
+			const consumable = this._createConsumableForRange( range, `attribute:${ key }` );
+
+			// Create a separate attribute event for each node in the range.
+			for ( const value of range ) {
+				const item = value.item;
+				const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
+				const data = {
+					item,
+					range: itemRange,
+					attributeKey: key,
+					attributeOldValue: oldValue,
+					attributeNewValue: newValue
+				};
 
-		// Create a separate attribute event for each node in the range.
-		for ( const value of range ) {
-			const item = value.item;
-			const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
-			const data = {
-				item,
-				range: itemRange,
-				attributeKey: key,
-				attributeOldValue: oldValue,
-				attributeNewValue: newValue
-			};
-
-			this._testAndFire( `attribute:${ key }`, data, consumable );
-		}
+				this._testAndFire( `attribute:${ key }`, data, consumable );
+			}
+		} );
 	}
 
 	/**
@@ -242,41 +260,44 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
 	 */
 	convertSelection( selection ) {
-		const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
-		const consumable = this._createSelectionConsumable( selection, markers );
+		this._view.change( writer => {
+			this.conversionApi.writer = writer;
+			const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
+			const consumable = this._createSelectionConsumable( selection, markers );
 
-		this.fire( 'selection', { selection }, consumable, this.conversionApi );
+			this.fire( 'selection', { selection }, consumable, this.conversionApi );
 
-		for ( const marker of markers ) {
-			const markerRange = marker.getRange();
+			for ( const marker of markers ) {
+				const markerRange = marker.getRange();
 
-			if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
-				continue;
-			}
+				if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
+					continue;
+				}
 
-			const data = {
-				selection,
-				markerName: marker.name,
-				markerRange
-			};
+				const data = {
+					selection,
+					markerName: marker.name,
+					markerRange
+				};
 
-			if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) {
-				this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi );
+				if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) {
+					this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi );
+				}
 			}
-		}
 
-		for ( const key of selection.getAttributeKeys() ) {
-			const data = {
-				selection,
-				key,
-				value: selection.getAttribute( key )
-			};
-
-			// Do not fire event if the attribute has been consumed.
-			if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
-				this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi );
+			for ( const key of selection.getAttributeKeys() ) {
+				const data = {
+					selection,
+					key,
+					value: selection.getAttribute( key )
+				};
+
+				// Do not fire event if the attribute has been consumed.
+				if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
+					this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi );
+				}
 			}
-		}
+		} );
 	}
 
 	/**
@@ -293,36 +314,40 @@ export default class ModelConversionDispatcher {
 			return;
 		}
 
-		// In markers' case, event name == consumable name.
-		const eventName = 'addMarker:' + markerName;
+		this._view.change( writer => {
+			this.conversionApi.writer = writer;
 
-		// When range is collapsed - fire single event with collapsed range in consumable.
-		if ( markerRange.isCollapsed ) {
-			const consumable = new Consumable();
-			consumable.add( markerRange, eventName );
+			// In markers' case, event name == consumable name.
+			const eventName = 'addMarker:' + markerName;
 
-			this.fire( eventName, {
-				markerName,
-				markerRange
-			}, consumable, this.conversionApi );
+			// When range is collapsed - fire single event with collapsed range in consumable.
+			if ( markerRange.isCollapsed ) {
+				const consumable = new Consumable();
+				consumable.add( markerRange, eventName );
 
-			return;
-		}
+				this.fire( eventName, {
+					markerName,
+					markerRange
+				}, consumable, this.conversionApi );
 
-		// Create consumable for each item in range.
-		const consumable = this._createConsumableForRange( markerRange, eventName );
-
-		// Create separate event for each node in the range.
-		for ( const item of markerRange.getItems() ) {
-			// Do not fire event for already consumed items.
-			if ( !consumable.test( item, eventName ) ) {
-				continue;
+				return;
 			}
 
-			const data = { item, range: Range.createOn( item ), markerName, markerRange };
+			// Create consumable for each item in range.
+			const consumable = this._createConsumableForRange( markerRange, eventName );
 
-			this.fire( eventName, data, consumable, this.conversionApi );
-		}
+			// Create separate event for each node in the range.
+			for ( const item of markerRange.getItems() ) {
+				// Do not fire event for already consumed items.
+				if ( !consumable.test( item, eventName ) ) {
+					continue;
+				}
+
+				const data = { item, range: Range.createOn( item ), markerName, markerRange };
+
+				this.fire( eventName, data, consumable, this.conversionApi );
+			}
+		} );
 	}
 
 	/**
@@ -338,7 +363,11 @@ export default class ModelConversionDispatcher {
 			return;
 		}
 
-		this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
+		this._view.change( writer => {
+			this.conversionApi.writer = writer;
+
+			this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
+		} );
 	}
 
 	/**

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

@@ -20,6 +20,7 @@ import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
 import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
 
+import View from '../view/view';
 import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
 import ViewSelection from '../view/selection';
 import ViewDocumentFragment from '../view/documentfragment';
@@ -159,6 +160,7 @@ setData._parse = parse;
  */
 export function stringify( node, selectionOrPositionOrRange = null ) {
 	const model = new Model();
+	const view = new View();
 	const mapper = new Mapper();
 	let selection, range;
 
@@ -192,7 +194,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	// Setup model to view converter.
 	const viewDocumentFragment = new ViewDocumentFragment();
 	const viewSelection = new ViewSelection();
-	const modelToView = new ModelConversionDispatcher( model, { mapper, viewSelection } );
+	const modelToView = new ModelConversionDispatcher( model, view, { mapper, viewSelection } );
 
 	// Bind root elements.
 	mapper.bindElements( node.root, viewDocumentFragment );

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

@@ -59,7 +59,7 @@ describe( 'model-selection-to-view-converters', () => {
 
 		highlightDescriptor = { class: 'marker', priority: 1 };
 
-		dispatcher = new ModelConversionDispatcher( model, { mapper, viewSelection } );
+		dispatcher = new ModelConversionDispatcher( model, view, { mapper, viewSelection } );
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );

+ 5 - 3
packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js

@@ -10,15 +10,17 @@ import ModelElement from '../../src/model/element';
 import ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
 
+import View from '../../src/view/view';
 import ViewContainerElement from '../../src/view/containerelement';
 
 describe( 'ModelConversionDispatcher', () => {
-	let dispatcher, doc, root, differStub, model;
+	let dispatcher, doc, root, differStub, model, view;
 
 	beforeEach( () => {
 		model = new Model();
+		view = new View();
 		doc = model.document;
-		dispatcher = new ModelConversionDispatcher( model );
+		dispatcher = new ModelConversionDispatcher( model, view );
 		root = doc.createRoot();
 
 		differStub = {
@@ -31,7 +33,7 @@ describe( 'ModelConversionDispatcher', () => {
 	describe( 'constructor()', () => {
 		it( 'should create ModelConversionDispatcher with given api', () => {
 			const apiObj = {};
-			const dispatcher = new ModelConversionDispatcher( model, { apiObj } );
+			const dispatcher = new ModelConversionDispatcher( model, view, { apiObj } );
 
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 		} );