Преглед изворни кода

Changed: Mapper position mapping now works using callbacks.

Szymon Cofalik пре 9 година
родитељ
комит
34c4c674fb
1 измењених фајлова са 104 додато и 12 уклоњено
  1. 104 12
      packages/ckeditor5-engine/src/conversion/mapper.js

+ 104 - 12
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -4,17 +4,28 @@
  */
 
 import ModelPosition from '../model/position.js';
-import ViewPosition from '../view/position.js';
 import ModelRange from '../model/range.js';
+
+import ViewPosition from '../view/position.js';
 import ViewRange from '../view/range.js';
 import ViewText from '../view/text.js';
 
+import EmitterMixin from '../../utils/emittermixin.js';
+import mix from '../../utils/mix.js';
+
 /**
  * Maps elements and positions between {@link engine.view.Document view} and {@link engine.model model}.
  *
  * Mapper use bound elements to find corresponding elements and positions, so, to get proper results,
  * all model elements should be {@link engine.conversion.Mapper#bindElements bound}.
  *
+ * To map complex model to/from view relations, you may provide custom callbacks for
+ * {@link engine.conversion.Mapper#event:modelToViewPosition modelToViewPosition event} and
+ * {@link engine.conversion.Mapper#event:viewToModelPosition viewToModelPosition event} that are fired whenever
+ * a position mapping request occurs. Those events are fired by {@link engine.conversion.Mapper#toViewPosition toViewPosition}
+ * and {@link engine.conversion.Mapper#toModelPosition toModelPosition} methods. `Mapper` adds it's own default callbacks
+ * with `'lowest'` priority. To override default `Mapper` mapping, add custom callback with higher priority and
+ * stop the event.
  * @memberOf engine.conversion
  */
 export default class Mapper {
@@ -37,6 +48,28 @@ export default class Mapper {
 		 * @member {WeakMap} engine.conversion.Mapper#_viewToModelMapping
 		 */
 		this._viewToModelMapping = new WeakMap();
+
+		// Add default callback for model to view position mapping.
+		this.on( 'modelToViewPosition', ( evt, mapper, modelPosition, data ) => {
+			let viewContainer = this._modelToViewMapping.get( modelPosition.parent );
+
+			data.viewPosition = this._findPositionIn( viewContainer, modelPosition.offset );
+		}, 'lowest' );
+
+		// Add default callback for view to model position mapping.
+		this.on( 'viewToModelPosition', ( evt, mapper, viewPosition, data ) => {
+			let viewBlock = viewPosition.parent;
+			let modelParent = this._viewToModelMapping.get( viewBlock );
+
+			while ( !modelParent ) {
+				viewBlock = viewBlock.parent;
+				modelParent = this._viewToModelMapping.get( viewBlock );
+			}
+
+			let modelOffset = this._toModelOffset( viewPosition.parent, viewPosition.offset, viewBlock );
+
+			data.modelPosition = ModelPosition.createFromParentAndOffset( modelParent, modelOffset );
+		}, 'lowest' );
 	}
 
 	/**
@@ -104,33 +137,35 @@ export default class Mapper {
 	/**
 	 * Gets the corresponding model position.
 	 *
+	 * @fires engine.conversion.Mapper#event:viewToModelPosition
 	 * @param {engine.view.Position} viewPosition View position.
 	 * @returns {engine.model.Position} Corresponding model position.
 	 */
 	toModelPosition( viewPosition ) {
-		let viewBlock = viewPosition.parent;
-		let modelParent =  this._viewToModelMapping.get( viewBlock );
+		const data = {
+			modelPosition: null
+		};
 
-		while ( !modelParent ) {
-			viewBlock = viewBlock.parent;
-			modelParent = this._viewToModelMapping.get( viewBlock );
-		}
+		this.fire( 'viewToModelPosition', this, viewPosition, data );
 
-		let modelOffset = this._toModelOffset( viewPosition.parent, viewPosition.offset, viewBlock );
-
-		return ModelPosition.createFromParentAndOffset( modelParent, modelOffset );
+		return data.modelPosition;
 	}
 
 	/**
 	 * Gets the corresponding view position.
 	 *
+	 * @fires engine.conversion.Mapper#event:modelToViewPosition
 	 * @param {engine.model.Position} modelPosition Model position.
 	 * @returns {engine.view.Position} Corresponding view position.
 	 */
 	toViewPosition( modelPosition ) {
-		let viewContainer = this._modelToViewMapping.get( modelPosition.parent );
+		const data = {
+			viewPosition: null
+		};
+
+		this.fire( 'modelToViewPosition', this, modelPosition, data );
 
-		return this._findPositionIn( viewContainer, modelPosition.offset );
+		return data.viewPosition;
 	}
 
 	/**
@@ -298,3 +333,60 @@ export default class Mapper {
 		return viewPosition;
 	}
 }
+
+mix( Mapper, EmitterMixin );
+
+/**
+ * Fired for each model-to-view position mapping request. The purpose of this event is to enable custom model-to-view position
+ * mapping. Callbacks added to this event take {@link engine.model.Position model position} and are expected to calculate
+ * {@link engine.view.Position view position}. Calculated view position should be added as `viewPosition` value in
+ * `data` object that is passed as one of parameters to the event callback.
+ *
+ * 		// Assume that "captionedImage" model element is converted to <img> and following <span> elements in view,
+ * 		// and the model element is bound to <img> element. Force mapping model positions inside "captionedImage" to that <span> element.
+ *		mapper.on( 'modelToViewPosition', ( evt, mapper, modelPosition, data ) => {
+ *			const positionParent = modelPosition.parent;
+ *
+ *			if ( positionParent.name == 'captionedImage' ) {
+ *				const viewImg = mapper.toViewElement( positionParent );
+ *				const viewCaption = viewImg.nextSibling; // The <span> element.
+ *
+ *				data.viewPosition = new ViewPosition( viewCaption, modelPosition.offset );
+ *				evt.stop();
+ *			}
+ *		} );
+ *
+ * **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
+ * mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also,
+ * the condition that checks if special case scenario happened should be as simple as possible.
+ *
+ * @event engine.conversion.Mapper.modelToViewPosition
+ * @param {engine.conversion.Mapper} mapper Mapper instance that fired the event.
+ * @param {engine.model.Position} modelPosition Model position to be mapped.
+ * @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
+ * `viewPosition` value to that object with calculated {@link engine.view.Position view position}.
+ */
+
+/**
+ * Fired for each view-to-model position mapping request. See {@link engine.conversion.Mapper#event:modelToViewPosition}.
+ *
+ * 		// See example in `modelToViewPosition` event description.
+ * 		// This custom mapping will map positions from <span> element next to <img> to the "captionedImage" element.
+ *		mapper.on( 'viewToModelPosition', ( evt, mapper, viewPosition, data ) => {
+ *			const positionParent = viewPosition.parent;
+ *
+ *			if ( positionParent.hasClass( 'image-caption' ) ) {
+ *				const viewImg = positionParent.previousSibling;
+ *				const modelImg = mapper.toModelElement( viewImg );
+ *
+ *				data.modelPosition = new ModelPosition( modelImg, viewPosition.offset );
+ *				evt.stop();
+ *			}
+ *		} );
+ *
+ * @event engine.conversion.Mapper.viewToModelPosition
+ * @param {engine.conversion.Mapper} mapper Mapper instance that fired the event.
+ * @param {engine.view.Position} viewPosition View position to be mapped.
+ * @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
+ * `modelPosition` value to that object with calculated {@link engine.model.Position model position}.
+ */