瀏覽代碼

Align reconversion guide code examples with the demo code changes.

Maciej Gołaszewski 5 年之前
父節點
當前提交
4911c1765c
共有 1 個文件被更改,包括 389 次插入48 次删除
  1. 389 48
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/element-reconversion.md

+ 389 - 48
packages/ckeditor5-engine/docs/framework/guides/deep-dive/element-reconversion.md

@@ -146,28 +146,34 @@ The side card model structure is represented in the editor's {@link framework/gu
 
 ```js
 // The main element with attributes for type and URL:
-editor.model.schema.register( 'sideCard', {
+schema.register( 'sideCard', {
 	allowWhere: '$block',
 	isObject: true,
 	allowAttributes: [ 'cardType', 'cardURL' ]
 } );
+// Disallow side card nesting.
+schema.addChildCheck( ( context, childDefinition ) => {
+	if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
+		return false;
+	}
+} );
 
 // A text-only title.
-editor.model.schema.register( 'sideCardTitle', {
+schema.register( 'sideCardTitle', {
 	isLimit: true,
 	allowIn: 'sideCard'
 } );
 // Allow text in title...
-editor.model.schema.extend( '$text', { allowIn: 'sideCardTitle' } );
+schema.extend( '$text', { allowIn: 'sideCardTitle' } );
 // ...but disallow any text attribute inside.
-editor.model.schema.addAttributeCheck( context => {
+schema.addAttributeCheck( context => {
 	if ( context.endsWith( 'sideCardTitle $text' ) ) {
 		return false;
 	}
 } );
 
 // A content block which can have any content allowed in $root.
-editor.model.schema.register( 'sideCardSection', {
+schema.register( 'sideCardSection', {
 	isLimit: true,
 	allowIn: 'sideCard',
 	allowContentOf: '$root'
@@ -179,9 +185,9 @@ editor.model.schema.register( 'sideCardSection', {
 To enable an element reconversion define which attributes and children modification the main element will be converted for:
 
 ```js
-editor.conversion.for( 'downcast' ).elementToElement( {
+conversion.for( 'editingDowncast' ).elementToElement( {
 	model: 'sideCard',
-	view: ( modelElement, conversionApi ) => downcastSideCard( modelElement, conversionApi ),
+	view: downcastSideCard( editor, { asWidget: true } ),
 	triggerBy: {
 		attributes: [ 'cardType', 'cardURL' ],
 		children: [ 'sideCardSection' ]
@@ -200,57 +206,68 @@ The above definition will use the `downcastSideCard()` function to re-create the
 The function that creates a complete view for the model element:
 
 ```js
-const downcastSideCard = ( modelElement, { writer, consumable, mapper } ) => {
-	const type = modelElement.getAttribute( 'cardType' ) || 'info';
+const downcastSideCard = ( editor, { asWidget } ) => {
+	return ( modelElement, { writer, consumable, mapper } ) => {
+		const type = modelElement.getAttribute( 'cardType' ) || 'info';
 
-	const sideCardView = writer.createContainerElement( 'aside', {
-		class: `side-card side-card-${ type }`
-	} );
+		// Main view element for the side card.
+		const sideCardView = writer.createContainerElement( 'aside', {
+			class: `side-card side-card-${ type }`
+		} );
 
-	// Create inner views from side card children.
-	for ( const child of modelElement.getChildren() ) {
-		const childView = writer.createEditableElement( 'div' );
+		// Create inner views from side card children.
+		for ( const child of modelElement.getChildren() ) {
+			const childView = writer.createEditableElement( 'div' );
 
-		// Child is either a "title" or "section".
-		if ( child.is( 'element', 'sideCardTitle' ) ) {
-			writer.addClass( 'side-card-title', childView );
-		} else {
-			writer.addClass( 'side-card-section', childView );
-		}
+			// Child is either a "title" or "section".
+			if ( child.is( 'element', 'sideCardTitle' ) ) {
+				writer.addClass( 'side-card-title', childView );
+			} else {
+				writer.addClass( 'side-card-section', childView );
+			}
 
-		// It is important to consume & bind converted elements.
-		consumable.consume( child, 'insert' );
-		mapper.bindElements( child, childView );
+			// It is important to consume & bind converted elements.
+			consumable.consume( child, 'insert' );
+			mapper.bindElements( child, childView );
 
-		// Make it an editable part of the widget.
-		toWidgetEditable( childView, writer );
+			// Make it an editable part of the widget.
+			if ( asWidget ) {
+				toWidgetEditable( childView, writer );
+			}
 
-		writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
-	}
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
+		}
 
-	const urlAttribute = modelElement.getAttribute( 'cardURL' );
+		const urlAttribute = modelElement.getAttribute( 'cardURL' );
 
-	// Do not render empty URL field
-	if ( urlAttribute ) {
-		const urlBox = writer.createRawElement( 'div', {
-			class: 'side-card-url'
-		}, function( domElement ) {
-			domElement.innerText = `URL: "${ urlAttribute }"`;
-		} );
+		// Do not render empty URL field
+		if ( urlAttribute ) {
+			const urlBox = writer.createRawElement( 'div', {
+				class: 'side-card-url'
+			}, function( domElement ) {
+				domElement.innerText = `URL: "${ urlAttribute }"`;
+			} );
 
-		writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
-	}
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
+		}
+
+		// Inner element used to render a simple UI that allows to change side card's attributes.
+		// It will only be needed in the editing view inside a widgetized element.
+		// The data output should not contain this section.
+		if ( asWidget ) {
+			const actionsView = writer.createRawElement( 'div', {
+				class: 'side-card-actions',
+				contenteditable: 'false', 			// Prevent editing of the element:
+				'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside editing view.
+			}, createActionsView( editor, modelElement ) ); // See the full code for details.
 
-	// Inner element used to render simple UI that allows to change side card's attributes.
-	const actionsView = writer.createRawElement( 'div', {
-		class: 'side-card-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( sideCardView, 'end' ), actionsView );
 
-	writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
+			toWidget( sideCardView, writer, { widgetLabel: 'Side card' } );
+		}
 
-	return toWidget( sideCardView, writer, { widgetLabel: 'Side card' } );
+		return sideCardView;
+	};
 };
 ```
 
@@ -264,7 +281,7 @@ The upcast conversion uses standard element-to-element converters for box & titl
 editor.conversion.for( 'upcast' )
 	.elementToElement( {
 		view: { name: 'aside', classes: [ 'side-card' ] },
-		model: upcastInfoBox
+		model: upcastCard // Details in the full source-code.
 	} )
 	.elementToElement( {
 		view: { name: 'div', classes: [ 'side-card-title' ] },
@@ -280,4 +297,328 @@ You can see the details of the upcast converter function (`upcastInfoBox()`) in
 
 ### Full source code
 
-TODO
+```js
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import { toWidget, toWidgetEditable, findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/utils';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
+
+/**
+ * Helper for extracting side card type from a view element based on its CSS class.
+ */
+const getTypeFromViewElement = viewElement => {
+	for ( const type of [ 'info', 'warning' ] ) {
+		if ( viewElement.hasClass( `side-card-${ type }` ) ) {
+			return type;
+		}
+	}
+
+	return 'info';
+};
+
+/**
+ * Single upcast converter to <sideCard/> element with all its attributes.
+ */
+const upcastCard = ( viewElement, { writer } ) => {
+	const sideCard = writer.createElement( 'sideCard' );
+
+	const type = getTypeFromViewElement( viewElement );
+	writer.setAttribute( 'cardType', type, sideCard );
+
+	const urlWrapper = [ ...viewElement.getChildren() ].find( child => {
+		return child.is( 'element', 'div' ) && child.hasClass( 'side-card-url' );
+	} );
+
+	if ( urlWrapper ) {
+		writer.setAttribute( 'cardURL', urlWrapper.getChild( 0 ).data, sideCard );
+	}
+
+	return sideCard;
+};
+
+/**
+ * Helper for creating DOM button with an editor callback.
+ */
+const addActionButton = ( text, callback, domElement, editor ) => {
+	const domDocument = domElement.ownerDocument;
+
+	const button = createElement( domDocument, 'button', {}, [ text ] );
+
+	button.addEventListener( 'click', () => {
+		editor.model.change( callback );
+	} );
+
+	domElement.appendChild( button );
+
+	return button;
+};
+
+/**
+ * Helper function that creates card editing UI inside the card.
+ */
+const createActionsView = ( editor, modelElement ) => function( domElement ) {
+	//
+	// Set URL action button.
+	//
+	addActionButton( 'Set URL', writer => {
+		// eslint-disable-next-line no-alert
+		const newURL = prompt( 'Set URL', modelElement.getAttribute( 'cardURL' ) || '' );
+
+		writer.setAttribute( 'cardURL', newURL, modelElement );
+	}, domElement, editor );
+
+	const currentType = modelElement.getAttribute( 'cardType' );
+	const newType = currentType === 'info' ? 'warning' : 'info';
+
+	//
+	// Change card action button.
+	//
+	addActionButton( 'Change type', writer => {
+		writer.setAttribute( 'cardType', newType, modelElement );
+	}, domElement, editor );
+
+	const childCount = modelElement.childCount;
+
+	//
+	// Add content section to a card action button.
+	//
+	const addButton = addActionButton( 'Add section', writer => {
+		writer.insertElement( 'sideCardSection', modelElement, 'end' );
+	}, domElement, editor );
+
+	// Disable the button so only 1-3 content boxes are in the card (there always will be a title).
+	if ( childCount > 4 ) {
+		addButton.setAttribute( 'disabled', 'disabled' );
+	}
+
+	//
+	// Remove content section from a card action button.
+	//
+	const removeButton = addActionButton( 'Remove section', writer => {
+		writer.remove( modelElement.getChild( childCount - 1 ) );
+	}, domElement, editor );
+
+	// Disable the button so only 1-3 content boxes are in the card (there always will be a title).
+	if ( childCount < 3 ) {
+		removeButton.setAttribute( 'disabled', 'disabled' );
+	}
+};
+
+/**
+ * The downcast converter for <sideCard/> element.
+ *
+ * It returns a full view structure based on the current state of the model element.
+ */
+const downcastSideCard = ( editor, { asWidget } ) => {
+	return ( modelElement, { writer, consumable, mapper } ) => {
+		const type = modelElement.getAttribute( 'cardType' ) || 'info';
+
+		// Main view element for the side card.
+		const sideCardView = writer.createContainerElement( 'aside', {
+			class: `side-card side-card-${ type }`
+		} );
+
+		// Create inner views from side card children.
+		for ( const child of modelElement.getChildren() ) {
+			const childView = writer.createEditableElement( 'div' );
+
+			// Child is either a "title" or "section".
+			if ( child.is( 'element', 'sideCardTitle' ) ) {
+				writer.addClass( 'side-card-title', childView );
+			} else {
+				writer.addClass( 'side-card-section', childView );
+			}
+
+			// It is important to consume & bind converted elements.
+			consumable.consume( child, 'insert' );
+			mapper.bindElements( child, childView );
+
+			// Make it an editable part of the widget.
+			if ( asWidget ) {
+				toWidgetEditable( childView, writer );
+			}
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), childView );
+		}
+
+		const urlAttribute = modelElement.getAttribute( 'cardURL' );
+
+		// Do not render empty URL field
+		if ( urlAttribute ) {
+			const urlBox = writer.createRawElement( 'div', {
+				class: 'side-card-url'
+			}, function( domElement ) {
+				domElement.innerText = `URL: "${ urlAttribute }"`;
+			} );
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), urlBox );
+		}
+
+		// Inner element used to render a simple UI that allows to change side card's attributes.
+		// It will only be needed in the editing view inside a widgetized element.
+		// The data output should not contain this section.
+		if ( asWidget ) {
+			const actionsView = writer.createRawElement( 'div', {
+				class: 'side-card-actions',
+				contenteditable: 'false', 			// Prevent editing of the element:
+				'data-cke-ignore-events': 'true'	// Allows using custom UI elements inside editing view.
+			}, createActionsView( editor, modelElement ) ); // See the full code for details.
+
+			writer.insert( writer.createPositionAt( sideCardView, 'end' ), actionsView );
+
+			toWidget( sideCardView, writer, { widgetLabel: 'Side card' } );
+		}
+
+		return sideCardView;
+	};
+};
+
+class InsertCardCommand extends Command {
+	/**
+	 * Refresh uses schema definition to checks if a sideCard can be inserted in the current selection.
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const validParent = findOptimalInsertionPosition( model.document.selection, model );
+
+		this.isEnabled = model.schema.checkChild( validParent, 'sideCard' );
+	}
+
+	/**
+	 * Creates full side card element with all required children and attributes.
+	 */
+	execute() {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+
+		const insertPosition = findOptimalInsertionPosition( selection, model );
+
+		model.change( writer => {
+			const sideCard = writer.createElement( 'sideCard', { cardType: 'info' } );
+			const title = writer.createElement( 'sideCardTitle' );
+			const section = writer.createElement( 'sideCardSection' );
+			const paragraph = writer.createElement( 'paragraph' );
+
+			writer.insert( title, sideCard, 0 );
+			writer.insert( section, sideCard, 1 );
+			writer.insert( paragraph, section, 0 );
+
+			model.insertContent( sideCard, insertPosition );
+
+			writer.setSelection( writer.createPositionAt( title, 0 ) );
+		} );
+	}
+}
+
+class ComplexBox extends Plugin {
+	constructor( editor ) {
+		super( editor );
+
+		this._defineSchema();
+		this._defineConversion();
+
+		editor.commands.add( 'insertCard', new InsertCardCommand( editor ) );
+
+		this._defineUI();
+	}
+
+	_defineConversion() {
+		const editor = this.editor;
+		const conversion = editor.conversion;
+
+		conversion.for( 'upcast' )
+			.elementToElement( {
+				view: { name: 'aside', classes: [ 'side-card' ] },
+				model: upcastCard
+			} )
+			.elementToElement( {
+				view: { name: 'div', classes: [ 'side-card-title' ] },
+				model: 'sideCardTitle'
+			} )
+			.elementToElement( {
+				view: { name: 'div', classes: [ 'side-card-section' ] },
+				model: 'sideCardSection'
+			} );
+
+		// The downcast conversion must be split as you need a widget in the editing pipeline.
+		conversion.for( 'editingDowncast' ).elementToElement( {
+			model: 'sideCard',
+			view: downcastSideCard( editor, { asWidget: true } ),
+			triggerBy: {
+				attributes: [ 'cardType', 'cardURL' ],
+				children: [ 'sideCardSection' ]
+			}
+		} );
+		// The data downcast is always executed from the current model stat, so the `triggerBy` will take no effect.
+		conversion.for( 'dataDowncast' ).elementToElement( {
+			model: 'sideCard',
+			view: downcastSideCard( editor, { asWidget: false } )
+		} );
+	}
+
+	_defineSchema() {
+		const schema = this.editor.model.schema;
+
+		// The main element with attributes for type and URL:
+		schema.register( 'sideCard', {
+			allowWhere: '$block',
+			isObject: true,
+			allowAttributes: [ 'cardType', 'cardURL' ]
+		} );
+		// Disallow side card nesting.
+		schema.addChildCheck( ( context, childDefinition ) => {
+			if ( [ ...context.getNames() ].includes( 'sideCard' ) && childDefinition.name === 'sideCard' ) {
+				return false;
+			}
+		} );
+
+		// A text-only title.
+		schema.register( 'sideCardTitle', {
+			isLimit: true,
+			allowIn: 'sideCard'
+		} );
+		// Allow text in title...
+		schema.extend( '$text', { allowIn: 'sideCardTitle' } );
+		// ...but disallow any text attribute inside.
+		schema.addAttributeCheck( context => {
+			if ( context.endsWith( 'sideCardTitle $text' ) ) {
+				return false;
+			}
+		} );
+
+		// A content block which can have any content allowed in $root.
+		schema.register( 'sideCardSection', {
+			isLimit: true,
+			allowIn: 'sideCard',
+			allowContentOf: '$root'
+		} );
+	}
+
+	_defineUI() {
+		const editor = this.editor;
+
+		// Defines a simple text button.
+		editor.ui.componentFactory.add( 'insertCard', locale => {
+			const button = new ButtonView( locale );
+
+			const command = editor.commands.get( 'insertCard' );
+
+			button.set( {
+				withText: true,
+				icon: false,
+				label: 'Insert card'
+			} );
+
+			button.bind( 'isEnabled' ).to( command );
+
+			button.on( 'execute', () => {
+				editor.execute( 'insertCard' );
+				editor.editing.view.focus();
+			} );
+
+			return button;
+		} );
+	}
+}
+```