浏览代码

PoC: Refresh conversion - retain "slot" children.

Maciej Gołaszewski 5 年之前
父节点
当前提交
7be4b066a5

+ 23 - 1
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -146,7 +146,8 @@ export default class DowncastDispatcher {
 			} else if ( entry.type == 'remove' ) {
 				this.convertRemove( entry.position, entry.length, entry.name, writer );
 			} else if ( entry.type == 'refresh' ) {
-				// @if CK_DEBUG // console.warn( 'convertRemove' );
+				// @if CK_DEBUG console.warn( 'convert refresh' );
+				this.convertRefresh( Range._createFromPositionAndShift( entry.position, entry.length ), writer );
 			} else if ( entry.type == 'attribute' ) {
 				this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, writer );
 			} else {
@@ -262,6 +263,27 @@ export default class DowncastDispatcher {
 		this._clearConversionApi();
 	}
 
+	convertRefresh( range, writer ) {
+		this.conversionApi.writer = writer;
+
+		// Create a list of things that can be consumed, consisting of nodes and their attributes.
+		this.conversionApi.consumable = this._createInsertConsumable( range );
+
+		for ( const value of range ) {
+			const item = value.item;
+			const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
+			const data = {
+				item,
+				range: itemRange,
+				isRefresh: true
+			};
+
+			this._testAndFire( 'insert', data );
+		}
+
+		this._clearConversionApi();
+	}
+
 	/**
 	 * Starts model selection conversion.
 	 *

+ 40 - 0
packages/ckeditor5-engine/src/conversion/downcasthelpers.js

@@ -785,6 +785,10 @@ export function wrap( elementCreator ) {
  */
 export function insertElement( elementCreator ) {
 	return ( evt, data, conversionApi ) => {
+		// Cache current view element of a converted element, might be undefined if first insert.
+		const currentView = conversionApi.mapper.toViewElement( data.item );
+
+		// Create view structure:
 		const viewElement = elementCreator( data.item, conversionApi );
 
 		if ( !viewElement ) {
@@ -797,6 +801,42 @@ export function insertElement( elementCreator ) {
 
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
 
+		// A flag was the simplest way of changing default insertElement behavior.
+		if ( data.isRefresh ) {
+			// Because of lack of a better API...
+
+			// Iterate over new view elements to find "interesting" points - those elements that are mapped to the model.
+			for ( const { item } of conversionApi.writer.createRangeIn( viewElement ) ) {
+				const modelItem = conversionApi.mapper.toModelElement( item );
+
+				// At this stage we get the update view element, so any mapped model item might be a potential "slot".
+				if ( modelItem ) {
+					const currentViewItem = conversionApi.mapper._temporalModelToView.get( modelItem );
+
+					// This of course needs better API, but for now it works.
+					// Mapper.bindSlot() creates mappings as mapper.bindElements() but also binds view element from view to the model item.
+					if ( currentViewItem ) {
+						// This allows to have a map: updatedView - model - oldView and to retain previously rendered children
+						// from the "slot" element. Those children can be moved to a newly created slot.
+						conversionApi.writer.move(
+							conversionApi.writer.createRangeIn( currentViewItem ),
+							conversionApi.writer.createPositionAt( item, 0 )
+						);
+					}
+
+					// @todo should be done by conversion API...
+					// Again, no API for this, so we need to stop conversion beneath "slot" by simply consuming whole tree under it.
+					for ( const inner of ModelRange._createOn( modelItem ) ) {
+						conversionApi.consumable.consume( inner.item, 'insert' );
+					}
+				}
+			}
+
+			// At this stage old view can be safely removed.
+			conversionApi.writer.remove( currentView );
+		}
+
+		// Rest of standard insertElement converter.
 		conversionApi.mapper.bindElements( data.item, viewElement );
 		conversionApi.writer.insert( viewPosition, viewElement );
 	};

+ 10 - 0
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -49,6 +49,9 @@ export default class Mapper {
 		 */
 		this._modelToViewMapping = new WeakMap();
 
+		// @todo POC
+		this._temporalModelToView = new WeakMap();
+
 		/**
 		 * View element to model element mapping.
 		 *
@@ -135,6 +138,13 @@ export default class Mapper {
 		this._viewToModelMapping.set( viewElement, modelElement );
 	}
 
+	bindSlotElements( modelElement, viewElement ) {
+		const oldView = this.toViewElement( modelElement );
+
+		this._temporalModelToView.set( modelElement, oldView );
+		this.bindElements( modelElement, viewElement );
+	}
+
 	/**
 	 * Unbinds given {@link module:engine/view/element~Element view element} from the map.
 	 *

+ 1 - 1
tests/manual/article.js

@@ -146,7 +146,7 @@ function downcastBox( modelElement, conversionApi ) {
 		const viewField = writer.createContainerElement( 'div', { class: 'box-content-field' } );
 
 		writer.insert( writer.createPositionAt( contentWrap, field.index ), viewField );
-		conversionApi.mapper.bindElements( field, viewField );
+		conversionApi.mapper.bindSlotElements( field, viewField );
 	}
 
 	// At this point we're inserting whole "component". Equivalent to (JSX-like notation):