瀏覽代碼

Update the reconversion guide content.

Maciej Gołaszewski 5 年之前
父節點
當前提交
fb326dcdd6

+ 6 - 0
packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.html

@@ -6,6 +6,12 @@
 		border-radius: 8px;
 	}
 
+	.ck-content .info-box-actions button {
+		font-size: 0.8em;
+		border: 1px solid hsl(0, 0%, 80%);
+		padding: 2px;
+	}
+
 	.ck-content .info-box-title {
 		margin-bottom: 0.5em;
 		font-size: 1.4em;

+ 69 - 20
packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.js

@@ -3,9 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals ClassicEditor, console, window, document */
+/* globals ClassicEditor, console, window, prompt, document */
 
 import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 
 const getTypeFromViewElement = viewElement => {
 	for ( const type of [ 'info', 'warning' ] ) {
@@ -28,28 +29,69 @@ const upcastInfoBox = ( viewElement, { writer } ) => {
 	return complexInfoBox;
 };
 
-const downcastInfoBox = ( modelElement, { writer, consumable, mapper } ) => {
-	const complexInfoBoxView = writer.createContainerElement( 'div', { class: 'info-box' } );
+const renderActionsView = ( editor, modelElement ) => function( domElement ) {
+	const domDocument = domElement.ownerDocument;
+	const urlButton = createElement( domDocument, 'button', {}, 'Set URL' );
 
-	const type = modelElement.getAttribute( 'infoBoxType' ) || 'info';
+	urlButton.addEventListener( 'click', () => {
+		// eslint-disable-next-line no-alert
+		const newURL = prompt( 'Set URL', modelElement.getAttribute( 'infoBoxURL' ) || '' );
 
-	writer.addClass( `info-box-${ type }`, complexInfoBoxView );
+		editor.model.change( writer => {
+			writer.setAttribute( 'infoBoxURL', newURL, modelElement );
+		} );
+	} );
 
-	for ( const child of modelElement.getChildren() ) {
-		const childView = writer.createContainerElement( 'div' );
+	const currentType = modelElement.getAttribute( 'infoBoxType' );
+	const newType = currentType === 'info' ? 'warning' : 'info';
 
-		if ( child.is( 'element', 'complexInfoBoxTitle' ) ) {
-			writer.addClass( 'info-box-title', childView );
-		} else {
-			writer.addClass( 'info-box-content', childView );
-		}
+	const typeButton = createElement( domDocument, 'button', {}, `Change to ${ newType }` );
 
-		consumable.consume( child, 'insert' );
-		mapper.bindElements( child, childView );
-		writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), childView );
-	}
+	typeButton.addEventListener( 'click', () => {
+		editor.model.change( writer => {
+			writer.setAttribute( 'infoBoxType', newType, modelElement );
+		} );
+	} );
 
-	return complexInfoBoxView;
+	domElement.appendChild( urlButton );
+	domElement.appendChild( typeButton );
+};
+
+// TODO: simplify to hide complexity
+const downcastInfoBox = editor => {
+	return ( modelElement, { writer, consumable, mapper } ) => {
+		const complexInfoBoxView = writer.createContainerElement( 'div', {
+			class: 'info-box'
+		} );
+
+		const type = modelElement.getAttribute( 'infoBoxType' ) || 'info';
+
+		writer.addClass( `info-box-${ type }`, complexInfoBoxView );
+
+		const actionsView = writer.createRawElement( 'div', {
+			class: 'info-box-actions',
+			contenteditable: 'false', 			// Prevent editing of the element:
+			'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside editing view.
+		}, renderActionsView( editor, modelElement ) );
+
+		writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), actionsView );
+
+		for ( const child of modelElement.getChildren() ) {
+			const childView = writer.createContainerElement( 'div' );
+
+			if ( child.is( 'element', 'complexInfoBoxTitle' ) ) {
+				writer.addClass( 'info-box-title', childView );
+			} else {
+				writer.addClass( 'info-box-content', childView );
+			}
+
+			consumable.consume( child, 'insert' );
+			mapper.bindElements( child, childView );
+			writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), childView );
+		}
+
+		return complexInfoBoxView;
+	};
 };
 
 class ComplexInfoBox {
@@ -76,11 +118,16 @@ class ComplexInfoBox {
 		// The downcast conversion must be split as you need a widget in the editing pipeline.
 		editor.conversion.for( 'downcast' ).elementToElement( {
 			model: 'complexInfoBox',
-			view: downcastInfoBox
+			view: downcastInfoBox( editor ),
+			triggerBy: {
+				attributes: [ 'infoBoxType', 'infoBoxURL' ],
+				children: [ 'complexInfoBoxContent' ]
+			}
 		} );
 	}
 
 	_defineSchema( editor ) {
+		// The main element with attributes for type and URL:
 		editor.model.schema.register( 'complexInfoBox', {
 			allowWhere: '$block',
 			allowContentOf: '$root',
@@ -88,12 +135,14 @@ class ComplexInfoBox {
 			allowAttributes: [ 'infoBoxType', 'infoBoxURL' ]
 		} );
 
+		// A text-only title.
 		editor.model.schema.register( 'complexInfoBoxTitle', {
 			isLimit: true,
-			allowIn: 'complexInfoBox',
-			allowContentOf: '$block'
+			allowIn: 'complexInfoBox'
 		} );
+		editor.model.schema.extend( '$text', { allowIn: 'complexInfoBoxTitle' } );
 
+		// A content which can have any content allowed in $root.
 		editor.model.schema.register( 'complexInfoBoxContent', {
 			isLimit: true,
 			allowIn: 'complexInfoBox',

+ 214 - 2
packages/ckeditor5-engine/docs/framework/guides/deep-dive/element-reconversion.md

@@ -1,6 +1,7 @@
 ---
 category: framework-deep-dive-conversion
 order: 50
+since: 24.0.0
 ---
 
 # Element reconversion
@@ -30,5 +31,216 @@ The above demo assumes that each box:
 
 - have 1 title
 - have multiple content-boxes
-- have type
-- have URL associated
+- have a type ("info" or "warning")
+- have a URL
+
+Those can be represented in the editor's {@link framework/guides/deep-dive/schema schema} as follows:
+
+```js
+// The main element with attributes for type and URL:
+editor.model.schema.register( 'complexInfoBox', {
+	allowWhere: '$block',
+	allowContentOf: '$root',
+	isObject: true,
+	allowAttributes: [ 'infoBoxType', 'infoBoxURL' ]
+} );
+
+// A text-only title.
+editor.model.schema.register( 'complexInfoBoxTitle', {
+	isLimit: true,
+	allowIn: 'complexInfoBox'
+} );
+editor.model.schema.extend( '$text', {
+	allowIn: 'complexInfoBoxTitle'
+} );
+
+// A content which can have any content allowed in $root.
+editor.model.schema.register( 'complexInfoBoxContent', {
+	isLimit: true,
+	allowIn: 'complexInfoBox',
+	allowContentOf: '$root'
+} );
+```
+
+### Atomic converters vs element reconversion
+
+Most editor features are written using atomic converters for every element or attribute. This approach allows great level of customization and separation of concerns. For example, table features can be added or removed without the need to change the main table converter. However, this approach in many cases is overly complicated, especially if the feature you work doesn't need to be extensible.
+
+An element reconversion comes handy for cases where you need to:
+
+* convert a relatively simple model to a complex view structure
+* writing a one, functional converter is easier to grasp in your project
+
+An additional bonus of using an element reconversion is that the parts of model tree that hasn't been changed, like paragraph and text inside your feature element, will not be reconverted. In other words, their view elements are memoized and re-used inside changed parent.
+
+### Enabling element reconversion
+
+Element reconversion is enabled by setting reconversion triggers in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} downcast helper.
+
+The model element can be reconverted when:
+
+* one or many attributes changes (using `triggerBy.attributes`) or
+* a child is inserted or removed (using `triggerBy.children`)
+
+<info-box>
+Note that, when using `children` configuration option the current implementation assumes that downcast converter will either:
+* handle element and its children conversion at once
+* will have a "flat" structure
+</info-box>
+
+A simplified model markup for the info box from the {@link #demo}:
+
+```html
+<complexInfoBox infoBoxType="info" infoBoxURL="http://cksource.com">
+	<infoBoxTitle>A title</infoBoxTitle>
+	<infoBoxContent>
+		<paragrahp>A content</paragrahp>
+	</infoBoxContent>
+</complexInfoBox>
+```
+
+This can be converted using below definition:
+
+```js
+editor.conversion.for( 'downcast' ).elementToElement( {
+	model: 'complexInfoBox',
+	view: ( modelElement, conversionApi ) => {
+		// ... converter details
+	},
+	triggerBy: {
+		attributes: [ 'infoBoxType', 'infoBoxURL' ],
+		children: [ 'complexInfoBoxContent' ]
+	}
+} );
+```
+
+The function that creates a complete view for the model element:
+
+```js
+const downcastInfoBox = ( modelElement, { writer, consumable, mapper } ) => {
+	const type = modelElement.getAttribute( 'infoBoxType' ) || 'info';
+
+	const complexInfoBoxView = writer.createContainerElement( 'div', {
+		class: `info-box info-box-${ type }`
+	} );
+
+	// Inner element used to render simple UI that allows to change info box's attributes.
+	const actionsView = writer.createRawElement( 'div', {
+		class: 'info-box-actions',
+		contenteditable: 'false', 			// Prevent editing of the element:
+		'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside editing view.
+	}, renderActionsView( editor, modelElement ) ); // See the full code for details.
+
+	writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), actionsView );
+
+	// Create inner views from info box children.
+	for ( const child of modelElement.getChildren() ) {
+		const childView = writer.createContainerElement( 'div' );
+
+		// Child is either a "title" or "content".
+		if ( child.is( 'element', 'complexInfoBoxTitle' ) ) {
+			writer.addClass( 'info-box-title', childView );
+		} else {
+			writer.addClass( 'info-box-content', childView );
+		}
+
+		// It is important to consume & bind converted elements.
+		consumable.consume( child, 'insert' );
+		mapper.bindElements( child, childView );
+
+		// Append converted view to parent.
+		writer.insert( writer.createPositionAt( complexInfoBoxView, 'end' ), childView );
+	}
+
+	return complexInfoBoxView;
+};
+```
+
+As you can observe in the above code this converter takes model element and its direct children and return a complex view structure:
+
+```html
+<div class="info-box info-box-info">
+	<div class="info-box-actions" contenteditable="false" data-cke-ignore-events="true">
+		<!-- simple form elements -->
+	</div>
+	<div class="info-box-title"></div>
+	<div class="info-box-content"></div>
+</div>
+```
+
+By using `mapper.bindElements( child, childView )` for `<infoBoxTitle>` and `<infoBoxContent>` you define which view elements corresponds to which model elements. This allows the editor's conversion to re-use existing view elements for title and content children, so they will not be re-converted without a need.
+
+I think that an image here might be useful. To not loose the idea - it might be something like exchanging layers from current view to result view.
+
+	Example of changing a type:
+
+	From:
+
+	```html
+	<div class="info-box info-box-info">
+		<div class="info-box-actions" contenteditable="false" data-cke-ignore-events="true">
+			<!-- simple form elements -->
+		</div>
+		<div class="info-box-title">A title</div>
+		<div class="info-box-content">
+			<p>A content</p>
+		</div>
+	</div>
+	```
+	During rendering
+	```html
+	<!-- OLD VIEW -->
+	<div class="info-box info-box-info">
+		<div class="info-box-actions" contenteditable="false" data-cke-ignore-events="true">
+			<!-- simple form elements -->
+		</div>
+		<div class="info-box-title">A title</div>
+		<div class="info-box-content">
+			<p>A content</p>
+		</div>
+	</div>
+	<!-- RECONVERTED VIEW -->
+	<div class="info-box info-box-waring"> <!-- CHANGED -->
+		<div class="info-box-actions" contenteditable="false" data-cke-ignore-events="true">
+			<!-- simple form elements -->
+		</div>
+		<div class="info-box-title"></div>
+		<div class="info-box-content"></div>
+	</div>
+	```
+
+	Result:
+
+	```html
+	<div class="info-box info-box-warning">
+		<div class="info-box-actions" contenteditable="false" data-cke-ignore-events="true">
+			<!-- simple form elements -->
+		</div>
+		<div class="info-box-title">A title</div>
+		<div class="info-box-content">
+			<p>A content</p>
+		</div>
+	</div>
+	```
+
+### Upcast conversion
+
+THe upcast conversion uses standard element-to-element converters for box & title and a custom converter for the info box to extract metadata from the data.
+
+```js
+editor.conversion.for( 'upcast' )
+	.elementToElement( {
+		view: { name: 'div', classes: [ 'info-box' ] },
+		model: upcastInfoBox
+	} )
+	.elementToElement( {
+		view: { name: 'div', classes: [ 'info-box-title' ] },
+		model: 'complexInfoBoxTitle'
+	} )
+	.elementToElement( {
+		view: { name: 'div', classes: [ 'info-box-content' ] },
+		model: 'complexInfoBoxContent'
+	} );
+```
+
+You can see the details of the upcast converter function (`upcastInfoBox()`) in the full source code at the end of this guide.