Przeglądaj źródła

Add a chapter on adding custom attributes and lasses to elements inside figures.

Maciej Gołaszewski 6 lat temu
rodzic
commit
333904b780

+ 28 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.html

@@ -0,0 +1,28 @@
+<head>
+	<style>
+		.ck-content figure.table-foo table,
+		.ck-content figure.img-foo {
+			border: 2px solid deeppink;
+		}
+	</style>
+</head>
+
+<div id="snippet-custom-figure-attributes">
+	<h2>Image:</h2>
+	<figure class="image">
+		<img alt="bar" class="img-foo" id="img-foo-1" src="%BASE_PATH%/assets/img/fields.jpg">
+		<figcaption>Caption</figcaption>
+	</figure>
+
+	<figure class="image img-foo img-bar">
+		<img alt="bar" id="img-foo-2" src="%BASE_PATH%/assets/img/fields.jpg">
+		<figcaption>Caption</figcaption>
+	</figure>
+
+	<h2>Table:</h2>
+	<table class="table-foo" id="table-foo-1" >
+		<tr>
+			<td>foo</td>
+		</tr>
+	</table>
+</div>

+ 175 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js

@@ -0,0 +1,175 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+/**
+ * Plugin that converts custom attributes for elements that are wrapped in <figure> in the view.
+ */
+class CustomFigureAttributes extends Plugin {
+	init() {
+		const editor = this.editor;
+
+		// Define on wchich elements the css classes should be preserved:
+		setupCustomClassConversion( 'img', 'image', editor );
+		setupCustomClassConversion( 'table', 'table', editor );
+
+		editor.conversion.for( 'upcast' ).add( upcastCustomClasses( 'figure' ), { priority: 'low' } );
+
+		// Define custom attributes that should be preserved.
+		setupCustomAttributeConversion( 'img', 'image', 'id', editor );
+		setupCustomAttributeConversion( 'table', 'table', 'id', editor );
+	}
+}
+
+/**
+ * Setups conversion that preservers classes on img/table elements
+ */
+function setupCustomClassConversion( viewElementName, modelElementName, editor ) {
+	// The 'customClass' attribute will store custom classes from data in the model so schema definitions to allow this attribute.
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ 'customClass' ] } );
+
+	// Define upcast converters for <img> and <table> elements with "low" priority so they are run after default converters.
+	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( viewElementName ), { priority: 'low' } );
+
+	// Define downcast converters for model element with "low" priority so they are run after default converters.
+	editor.conversion.for( 'downcast' ).add( downcastCustomClasses( modelElementName ), { priority: 'low' } );
+}
+
+/**
+ * Setups conversion for custom attribute on view elements contained inside figure.
+ *
+ * This method:
+ *
+ * - adds proper schema rules
+ * - adds an upcast converter
+ * - adds a downcast converter
+ */
+function setupCustomAttributeConversion( viewElementName, modelElementName, viewAttribute, editor ) {
+	// Extend schema to store attribute in the model.
+	const modelAttribute = `custom${ viewAttribute }`;
+
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ modelAttribute ] } );
+
+	editor.conversion.for( 'upcast' ).add( upcastAttribute( viewElementName, viewAttribute, modelAttribute ) );
+	editor.conversion.for( 'downcast' ).add( downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) );
+}
+
+/**
+ * Creates upcast converter that will pass all classes from view element to model element.
+ */
+function upcastCustomClasses( elementName ) {
+	return dispatcher => dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		// The upcast conversion pick up classes from base element and from figure element also so it should be extensible.
+		const currentAttributeValue = modelElement.getAttribute( 'customClass' ) || [];
+
+		currentAttributeValue.push( ...viewItem.getClassNames() );
+
+		conversionApi.writer.setAttribute( 'customClass', currentAttributeValue, modelElement );
+	} );
+}
+
+/**
+ * Creates downcast converter that add classes defined in `customClass` attribute to given view element.
+ *
+ * This converter expects that view element is nested in figure element.
+ */
+function downcastCustomClasses( modelElementName ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+
+		if ( !viewFigure ) {
+			return;
+		}
+
+		// The below code assumes that classes are set on <figure> element...
+		conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewFigure );
+
+		// ... but if you preferIf the classes should be passed to the <img> find the view element inside figure:
+		//
+		// const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+		//
+		// conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewElement );
+	} );
+}
+
+/**
+ * Helper method that search for given view element in all children of model element.
+ *
+ * @param {module:engine/view/item~Item} viewElement
+ * @param {String} viewElementName
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
+ * @return {module:engine/view/item~Item}
+ */
+function findViewChild( viewElement, viewElementName, conversionApi ) {
+	const viewChildren = Array.from( conversionApi.writer.createRangeIn( viewElement ).getItems() );
+
+	return viewChildren.find( item => item.is( viewElementName ) );
+}
+
+/**
+ * Returns custom attribute upcast converter.
+ */
+function upcastAttribute( viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `element:${ viewElementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( modelAttribute, viewItem.getAttribute( viewAttribute ), modelElement );
+	} );
+}
+
+/**
+ * Returns custom attribute downcast converter.
+ */
+function downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+		const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+
+		if ( !viewElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( viewAttribute, modelElement.getAttribute( modelAttribute ), viewElement );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-custom-figure-attributes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ CustomFigureAttributes ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 180 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/extending-content.md

@@ -661,3 +661,183 @@ ClassicEditor
 		console.error( err.stack );
 	} );
 ```
+
+#### Adding extra attributes to elements contained in figure
+
+The {@link features/image Image} and {@link features/table Table} features wraps view elements (`<img>` for Image nad `<table>` for Table) in `<figure>`. During the downcast conversion the model element is mapped to `<figure>` not the inner element. In such cases the default `conversion.attributeToAttribute()` conversion helpers could lost information on which element the attribute should be set. To overcome this limitation it is sufficient to write a custom converter that add custom attributes to elements already converted by base features. The key point is to add those converters with lower priority the base conversters so they will be called after the base ones. 
+
+##### Demo
+
+{@snippet framework/extending-content-custom-figure-attributes}
+
+##### Code
+
+The sample below is extensible - to add own attributes to preserve just add another `setupCustomAttributeConversion()` call with desired names.
+
+```js
+/**
+ * Plugin that converts custom attributes for elements that are wrapped in <figure> in the view.
+ */
+class CustomFigureAttributes extends Plugin {
+	init() {
+		const editor = this.editor;
+
+		// Define on wchich elements the css classes should be preserved:
+		setupCustomClassConversion( 'img', 'image', editor );
+		setupCustomClassConversion( 'table', 'table', editor );
+
+		editor.conversion.for( 'upcast' ).add( upcastCustomClasses( 'figure' ), { priority: 'low' } );
+
+		// Define custom attributes that should be preserved.
+		setupCustomAttributeConversion( 'img', 'image', 'id', editor );
+		setupCustomAttributeConversion( 'table', 'table', 'id', editor );
+	}
+}
+
+/**
+ * Setups conversion that preservers classes on img/table elements
+ */
+function setupCustomClassConversion( viewElementName, modelElementName, editor ) {
+	// The 'customClass' attribute will store custom classes from data in the model so schema definitions to allow this attribute.
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ 'customClass' ] } );
+
+	// Define upcast converters for <img> and <table> elements with "low" priority so they are run after default converters.
+	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( viewElementName ), { priority: 'low' } );
+
+	// Define downcast converters for model element with "low" priority so they are run after default converters.
+	editor.conversion.for( 'downcast' ).add( downcastCustomClasses( modelElementName ), { priority: 'low' } );
+}
+
+/**
+ * Setups conversion for custom attribute on view elements contained inside figure.
+ *
+ * This method:
+ *
+ * - adds proper schema rules
+ * - adds an upcast converter
+ * - adds a downcast converter
+ */
+function setupCustomAttributeConversion( viewElementName, modelElementName, viewAttribute, editor ) {
+	// Extend schema to store attribute in the model.
+	const modelAttribute = `custom${ viewAttribute }`;
+
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ modelAttribute ] } );
+
+	editor.conversion.for( 'upcast' ).add( upcastAttribute( viewElementName, viewAttribute, modelAttribute ) );
+	editor.conversion.for( 'downcast' ).add( downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) );
+}
+
+/**
+ * Creates upcast converter that will pass all classes from view element to model element.
+ */
+function upcastCustomClasses( elementName ) {
+	return dispatcher => dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		// The upcast conversion pick up classes from base element and from figure element also so it should be extensible.
+		const currentAttributeValue = modelElement.getAttribute( 'customClass' ) || [];
+
+		currentAttributeValue.push( ...viewItem.getClassNames() );
+
+		conversionApi.writer.setAttribute( 'customClass', currentAttributeValue, modelElement );
+	} );
+}
+
+/**
+ * Creates downcast converter that add classes defined in `customClass` attribute to given view element.
+ *
+ * This converter expects that view element is nested in figure element.
+ */
+function downcastCustomClasses( modelElementName ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+
+		if ( !viewFigure ) {
+			return;
+		}
+
+		// The below code assumes that classes are set on <figure> element...
+		conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewFigure );
+
+		// ... but if you preferIf the classes should be passed to the <img> find the view element inside figure:
+		//
+		// const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+		//
+		// conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewElement );
+	} );
+}
+
+/**
+ * Helper method that search for given view element in all children of model element.
+ *
+ * @param {module:engine/view/item~Item} viewElement
+ * @param {String} viewElementName
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
+ * @return {module:engine/view/item~Item}
+ */
+function findViewChild( viewElement, viewElementName, conversionApi ) {
+	const viewChildren = Array.from( conversionApi.writer.createRangeIn( viewElement ).getItems() );
+
+	return viewChildren.find( item => item.is( viewElementName ) );
+}
+
+/**
+ * Returns custom attribute upcast converter.
+ */
+function upcastAttribute( viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `element:${ viewElementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( modelAttribute, viewItem.getAttribute( viewAttribute ), modelElement );
+	} );
+}
+
+/**
+ * Returns custom attribute downcast converter.
+ */
+function downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+		const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+
+		if ( !viewElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( viewAttribute, modelElement.getAttribute( modelAttribute ), viewElement );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ CustomFigureAttributes ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```