Sfoglia il codice sorgente

Changed: model <-> view converters refactor + docs.

Szymon Cofalik 9 anni fa
parent
commit
c0a9f537c2

+ 222 - 46
packages/ckeditor5-engine/src/treecontroller/model-to-view-converters.js

@@ -5,53 +5,131 @@
 
 'use strict';
 
-import ViewText from '../treeview/text.js';
-import ModelTreeWalker from './treemodel/treewalker.js';
+import ModelTreeWalker from '../treemodel/treewalker.js';
+import ModelRange from '../treemodel/range.js';
 
-export function insert( nodeCreator ) {
-	return ( evt, data, context ) => {
-		data.consumable.consume( data.item, 'insert' );
+import ViewElement from '../treeview/element.js';
+import ViewText from '../treeview/text.js';
 
-		const viewPosition = context.mapper.toViewPosition( data.range.start );
+/**
+ * Contains {@link engine.treeModel model} to {@link engine.treeView view} converters for
+ * {@link engine.treeController.ModelConversionDispatcher}.
+ *
+ * @namespace engine.treeController.modelToView
+ */
 
-		// TODO: copy element
-		const viewNode = ( nodeCreator instanceof viewNode ) ? nodeCreator : nodeCreator( data, context );
+/**
+ * Function factory, creates a converter that converts node insertion changes from the model to the view.
+ * The view element that will be added to the view depends on passed parameter. If {@link engine.treeView.Element} was passed,
+ * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
+ * {@link engine.treeController.ModelConversionDispatcher.insert dispatcher's insert event}. It's expected that the
+ * function returns a {@link engine.treeView.Element}. The result of the function will be inserted to the view.
+ *
+ * The converter automatically consumes corresponding value from consumables list, stops the event (see
+ * {@link engine.treeController.ModelConversionDispatcher}) and bind model and view elements.
+ *
+ *		modelDispatcher.on( 'insert:element:p', insertElement( new ViewElement( 'p' ) ) );
+ *
+ *		modelDispatcher.on(
+ *			'insert:element:myElem',
+ *			insertElement( ( data, consumable, conversionApi ) => {
+ *				let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) );
+ *
+ *				// Do something fancy with myElem using data/consumable/conversionApi ...
+ *
+ *				return myElem;
+ *			}
+ *		) );
+ *
+ * @external engine.treeController.modelToView
+ * @function engine.treeController.modelToView.insertElement
+ * @param {engine.treeView.Element|Function} elementCreator View element, or function returning a view element, which
+ * will be inserted.
+ * @returns {Function} Insert element event converter.
+ */
+export function insertElement( elementCreator ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		consumable.consume( data.item, 'insert' );
 
-		context.mapper.bind( data.item, viewNode );
+		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+		const viewElement = ( elementCreator instanceof ViewElement ) ?
+			elementCreator.clone( true ) :
+			elementCreator( data, consumable, conversionApi );
 
-		context.writer.insert( viewPosition, viewNode );
+		conversionApi.mapper.bind( data.item, viewElement );
+		conversionApi.writer.insert( viewPosition, viewElement );
 
 		evt.stop();
 	};
 }
 
+/**
+ * Function factory, creates a default model-to-view converter for text insertion changes.
+ *
+ * The converter automatically consumes corresponding value from consumables list, stops the event (see
+ * {@link engine.treeController.ModelConversionDispatcher}) and bind model and view elements.
+ *
+ *		modelDispatcher.on( 'insert:text', insertText() );
+ *
+ * @external engine.treeController.modelToView
+ * @function engine.treeController.modelToView.insertText
+ * @returns {Function} Insert text event converter.
+ */
 export function insertText() {
-	return ( evt, data, context ) => {
-		data.consumable.consume( data.item, 'insert' );
+	return ( evt, data, consumable, conversionApi ) => {
+		consumable.consume( data.item, 'insert' );
 
-		const viewPosition = context.mapper.toViewPosition( data.position );
-		const viewNode = new ViewText( data.item.data );
+		const viewPosition = conversionApi.mapper.toViewPosition( data.position );
+		const viewText = new ViewText( data.item.data );
 
-		context.writer.insert( viewPosition, viewNode );
+		conversionApi.writer.insert( viewPosition, viewText );
 
 		evt.stop();
 	};
 }
 
+/**
+ * Function factory, creates a converter that converts set/change attribute changes from the model to the view. Attributes
+ * from model are converted to the attributes in the view. You may provide a custom function to generate a set of attributes
+ * that will be added/changed. If not provided, model attributes will be converted to view elements attributes on 1-to-1 basis.
+ *
+ * The converter automatically consumes corresponding value from consumables list, stops the event (see
+ * {@link engine.treeController.ModelConversionDispatcher}) and bind model and view elements.
+ *
+ *		modelDispatcher.on( 'addAttribute:customAttr:myElem', setAttribute( ( data ) => {
+ *			let attributes = {};
+ *
+ *			if ( data.item.hasAttribute( 'otherCustomAttr' ) ) {
+ *				// do something with attributes variable ...
+ *			} else {
+ *				// do something else with attributes variable ...
+ *			}
+ *
+ *			return attributes;
+ *		} ) );
+ *
+ * @external engine.treeController.modelToView
+ * @function engine.treeController.modelToView.setAttribute
+ * @param {Function} [attributesCreator] Function returning a `string`, `number` or an `object` which values are `string`s
+ * or `number`s. If `object` is returned, it's keys are used as attributes keys and values as attributes values. The function
+ * is passed all the parameters of the {@link engine.treeController.ModelConversionDispatcher.addAttribute}
+ * or {@link engine.treeController.ModelConversionDispatcher.changeAttribute) event.
+ * @returns {Function} Set/change attribute converter.
+ */
 export function setAttribute( attributesCreator ) {
-	return ( evt, data, context ) => {
+	return ( evt, data, consumable, conversionApi ) => {
 		let attributes;
 
 		if ( !attributesCreator ) {
 			attributes = data.item.getAttribute( data.attributeKey );
 		} else {
-			attributes = attributesCreator( data );
+			attributes = attributesCreator( data, consumable, conversionApi );
 		}
 
 		if ( attributes ) {
-			data.consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
+			consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
 
-			const viewElement = context.mapper.toViewElement( data.item );
+			const viewElement = conversionApi.mapper.toViewElement( data.item );
 
 			if ( typeof attributes === 'string' || typeof attributes === 'number' ) {
 				viewElement.setAttribute( data.attributeKey, attributes );
@@ -66,20 +144,47 @@ export function setAttribute( attributesCreator ) {
 	};
 }
 
+/**
+ * Function factory, creates a converter that converts remove attribute changes from the model to the view. This converter
+ * assumes, that attributes from model were converted to the attributes in the view. You may provide a custom function to
+ * generate a set of attributes that will be removed. If not provided, model attributes will be removed from view elements on 1-to-1 basis.
+ *
+ * The converter automatically consumes corresponding value from consumables list, stops the event (see
+ * {@link engine.treeController.ModelConversionDispatcher}) and bind model and view elements.
+ *
+ *		modelDispatcher.on( 'removeAttribute:customAttr:myElem', removeAttribute( ( data ) => {
+ *			let attributes = {};
+ *
+ *			if ( data.item.hasAttribute( 'otherCustomAttr' ) ) {
+ *				// do something with attributes variable ...
+ *			} else {
+ *				// do something else with attributes variable ...
+ *			}
+ *
+ *			return attributes;
+ *		} ) );
+ *
+ * @external engine.treeController.modelToView
+ * @function engine.treeController.modelToView.removeAttribute
+ * @param {Function} [attributesCreator] Function returning a `string` or an `array` of `string`s. If `array` is returned,
+ * it's values are used as attributes keys to remove. The function is passed all the parameters of the
+ * {@link engine.treeController.ModelConversionDispatcher.removeAttribute} event.
+ * @returns {Function} Remove attribute converter.
+ */
 export function removeAttribute( attributesCreator ) {
-	return ( evt, data, context ) => {
+	return ( evt, data, consumable, conversionApi ) => {
 		let attributeKeys;
 
 		if ( !attributesCreator ) {
 			attributeKeys = data.attributeKey;
 		} else {
-			attributeKeys = attributesCreator( data );
+			attributeKeys = attributesCreator( data, consumable, conversionApi );
 		}
 
 		if ( attributeKeys ) {
-			data.consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
+			consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
 
-			const viewElement = context.mapper.toViewElement( data.item );
+			const viewElement = conversionApi.mapper.toViewElement( data.item );
 
 			if ( typeof attributeKeys === 'string' ) {
 				viewElement.removeAttribute( attributeKeys );
@@ -94,36 +199,98 @@ export function removeAttribute( attributesCreator ) {
 	};
 }
 
-export function wrap( nodeCreator ) { // TODO: priority
-	return ( evt, data, context ) => {
-		data.consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
+/**
+ * Function factory, creates a converter that converts set/change attribute changes from the model to the view. In this case,
+ * model attributes are converted to a view element that will be wrapping view nodes which corresponding model nodes had
+ * the attribute set. This is useful for attributes like `bold`, which may be set on a text nodes in model but are
+ * represented as an element in the view:
+ *
+ *		[paragraph]              MODEL ====> VIEW        <p>
+ *			|- a {bold: true}                             |- <b>
+ *			|- b {bold: true}                             |   |- ab
+ *			|- c                                          |- c
+ *
+ * The wrapping node depends on passed parameter. If {@link engine.treeView.Element} was passed, it will be cloned and
+ * the copy will become the wrapping element. If `Function` is provided, it is passed all the parameters of the
+ * {@link engine.treeController.ModelConversionDispatcher.setAttribute event}. It's expected that the
+ * function returns a {@link engine.treeView.Element}. The result of the function will be the wrapping element.
+ *
+ * The converter automatically consumes corresponding value from consumables list, stops the event (see
+ * {@link engine.treeController.ModelConversionDispatcher}) and bind model and view elements.
+ *
+ *		modelDispatcher.on( 'addAttribute:bold', wrap( new ViewElement( 'strong' ) ) );
+ *
+ * @external engine.treeController.modelToView
+ * @function engine.treeController.modelToView.wrap
+ * @param {engine.treeView.Element|Function} elementCreator View element, or function returning a view element, which will
+ * be used for wrapping.
+ * @returns {Function} Set/change attribute converter.
+ */
+export function wrap( elementCreator ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
 
-		const viewRange = context.mapper.toViewRange( data.range );
+		const viewRange = conversionApi.mapper.toViewRange( data.range );
 
-		const viewNode = ( nodeCreator instanceof viewNode ) ? nodeCreator : nodeCreator( data );
+		const viewElement = ( elementCreator instanceof ViewElement ) ?
+			elementCreator.clone( true ) :
+			elementCreator( data, consumable, conversionApi );
 
-		context.writer.wrap( viewRange, viewNode );
+		conversionApi.writer.wrap( viewRange, viewElement, evt.priority );
 
 		evt.stop();
 	};
 }
 
-export function unwrap( nodeCreator ) {
-	return ( evt, data, context ) => {
-		data.consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
-
-		const viewRange = context.mapper.toViewRange( data.range );
+/**
+ * Function factory, creates a converter that converts remove attribute changes from the model to the view. It assumes, that
+ * attributes from model were converted to elements in the view. This converter will unwrap view nodes from corresponding
+ * view element if given attribute was removed.
+ *
+ * The view element type that will be unwrapped depends on passed parameter.
+ * If {@link engine.treeView.Element} was passed, it will be used to look for similar element in the view for unwrapping. If `Function`
+ * is provided, it is passed all the parameters of the {@link engine.treeController.ModelConversionDispatcher.setAttribute event}.
+ * It's expected that the function returns a {@link engine.treeView.Element}. The result of the function will be used to
+ * look for similar element in the view for unwrapping.
+ *
+ * The converter automatically consumes corresponding value from consumables list, stops the event (see
+ * {@link engine.treeController.ModelConversionDispatcher}) and bind model and view elements.
+ *
+ *		modelDispatcher.on( 'removeAttribute:bold', unwrap( new ViewElement( 'strong' ) ) );
+ *
+ * @see engine.treeController.modelToView.wrap
+ * @external engine.treeController.modelToView
+ * @function engine.treeController.modelToView.unwrap
+ * @param {engine.treeView.Element|Function} elementCreator View element, or function returning a view element, which will
+ * be used for unwrapping.
+ * @returns {Function} Remove attribute converter.
+ */
+export function unwrap( elementCreator ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
 
-		const viewNode = ( nodeCreator instanceof viewNode ) ? nodeCreator : nodeCreator( data );
+		const viewRange = conversionApi.mapper.toViewRange( data.range );
+		const viewNode = ( elementCreator instanceof ViewElement ) ?
+			elementCreator.clone( true ) :
+			elementCreator( data, consumable, conversionApi );
 
-		context.writer.unwrap( viewRange, viewNode );
+		conversionApi.writer.unwrap( viewRange, viewNode );
 
 		evt.stop();
 	};
 }
 
+/**
+ * Function factory, creates a default model-to-view converter for nodes move changes.
+ *
+ *		modelDispatcher.on( 'move', move() );
+ *
+ * @external engine.treeController.modelToView
+ * @function engine.treeController.modelToView.move
+ * @returns {Function} Move event converter.
+ */
 export function move() {
-	return ( evt, data, context ) => {
+	return ( evt, data, conversionApi ) => {
 		const walker = new ModelTreeWalker( { boundaries: data.range, shallow: true } );
 
 		let length = 0;
@@ -132,17 +299,26 @@ export function move() {
 			length += value.length;
 		}
 
-		const sourceModelRange = Range.createFromPositionAndShift( data.sourcePosition, length );
+		const sourceModelRange = ModelRange.createFromPositionAndShift( data.sourcePosition, length );
 
-		const sourceViewRange = context.mapper.toViewRange( sourceModelRange );
-		const targetViewPosition = context.mapper.toViewRange( data.range.start );
+		const sourceViewRange = conversionApi.mapper.toViewRange( sourceModelRange );
+		const targetViewPosition = conversionApi.mapper.toViewRange( data.range.start );
 
-		context.writer.move( sourceViewRange, targetViewPosition );
+		conversionApi.writer.move( sourceViewRange, targetViewPosition );
 	};
 }
 
+/**
+ * Function factory, creates a default model-to-view converter for nodes remove changes.
+ *
+ *		modelDispatcher.on( 'remove', remove() );
+ *
+ * @external engine.treeController.modelToView
+ * @function engine.treeController.modelToView.remove
+ * @returns {Function} Remove event converter.
+ */
 export function remove() {
-	return ( evt, data, context ) => {
+	return ( evt, data, conversionApi ) => {
 		const walker = new ModelTreeWalker( { boundaries: data.range, shallow: true } );
 
 		let length = 0;
@@ -151,10 +327,10 @@ export function remove() {
 			length += value.length;
 		}
 
-		const sourceModelRange = Range.createFromPositionAndShift( data.sourcePosition, length );
-		const sourceViewRange = context.mapper.toViewRange( sourceModelRange );
+		const sourceModelRange = ModelRange.createFromPositionAndShift( data.sourcePosition, length );
+		const sourceViewRange = conversionApi.mapper.toViewRange( sourceModelRange );
 
-		context.writer.remove( sourceViewRange );
+		conversionApi.writer.remove( sourceViewRange );
 	};
 }
 
@@ -162,4 +338,4 @@ function eventNameToConsumableType( evtName ) {
 	const parts = evtName.split( ':' );
 
 	return parts[ 0 ] + ':' + parts[ 1 ];
-}
+}

+ 39 - 7
packages/ckeditor5-engine/src/treecontroller/view-to-model-converters.js

@@ -8,18 +8,50 @@
 import ModelDocumentFragment from '../treemodel/documentfragment.js';
 import ModelText from '../treemodel/text.js';
 
-export function convertChildren() {
-	return ( evt, data, controller ) => {
-		if ( !data.output && controller.consumable.test( data.input ) ) {
-			data.output = new ModelDocumentFragment( controller.convertChildren( data.input, data.context ) );
+/**
+ * Contains {@link engine.treeView view} to {@link engine.treeModel model} converters for
+ * {@link engine.treeController.ViewConversionDispatcher}.
+ *
+ * @namespace engine.treeController.viewToModel
+ */
+
+/**
+ * Function factory, creates a converter that converts {@link engine.treeView.DocumentFragment view document fragment} or
+ * all children of {@link engine.treeView.Element} into {@link engine.treeModel.DocumentFragment model document fragment}.
+ * This is the "entry-point" converter for view to model conversion. This converter starts the conversion of all "children"
+ * of passed view document fragment. Those "children" {@link engine.treeView.Node view nodes} are then handled by other converters.
+ *
+ * This also a "default", last resort converter for all view elements that has not been converted by other converters.
+ * When a view element is converted to the model and it does not have it's converter specified, all of that elements
+ * children will be converted to {@link engine.treeModel.DocumentFragment} and returned.
+ *
+ * @external engine.treeController.viewToModel
+ * @function engine.treeController.viewToModel.convertToModelFragment
+ * @returns {Function} Universal converter for view {@link engine.treeView.DocumentFragment fragments} and
+ * {@link engine.treeView.Element elements} that returns {@link engine.treeModel.DocumentFragment model fragment} with
+ * children of converted view item.
+ */
+export function convertToModelFragment() {
+	return ( evt, data, consumable, conversionApi ) => {
+		if ( !data.output && consumable.test( data.input ) ) {
+			const convertedChildren = conversionApi.convertChildren( data.input, consumable, { context: data.context } );
+
+			data.output = new ModelDocumentFragment( convertedChildren );
 		}
 	};
 }
 
+/**
+ * Function factory, creates a converter that converts {@link engine.treeView.Text} to {@link engine.treeModel.Text}.
+ *
+ * @external engine.treeController.viewToModel
+ * @function engine.treeController.viewToModel.convertText
+ * @returns {Function} {@link engine.treeView.Text View text} converter.
+ */
 export function convertText() {
-	return ( evt, data, controller ) => {
-		if ( controller.consumable.consume( data.input ) ) {
+	return ( evt, data, consumable ) => {
+		if ( consumable.consume( data.input ) ) {
 			data.output = new ModelText( data.input.data );
 		}
 	};
-}
+}