瀏覽代碼

Updated docs and tests for ImageStyleEngine.

Szymon Kupś 9 年之前
父節點
當前提交
4fb10c8c5d

+ 101 - 0
packages/ckeditor5-image/src/imagestyle/converters.js

@@ -0,0 +1,101 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagestyle/converters
+ */
+
+import { isImage, getStyleByValue } from './utils.js';
+
+export function addStyle( styles ) {
+	return ( event, data, consumable, conversionApi ) => {
+		// Check if we can consume, and we are adding in image.
+		if ( !consumable.consume( data.item, 'addAttribute:imageStyle' ) || !isImage( data.item ) ) {
+			return;
+		}
+
+		// Check if there is class name associated with given value.
+		const newStyle = getStyleByValue( data.attributeNewValue, styles );
+
+		// Check if new style is allowed in configuration.
+		if ( !newStyle ) {
+			return;
+		}
+
+		conversionApi.mapper.toViewElement( data.item ).addClass( newStyle.className );
+	};
+}
+
+export function changeStyle( styles ) {
+	return ( event, data, consumable, conversionApi ) => {
+		if ( !consumable.consume( data.item, 'changeAttribute:imageStyle' ) || !isImage( data.item ) ) {
+			return;
+		}
+
+		// Check if there is class name associated with given value.
+		const newStyle = getStyleByValue( data.attributeNewValue, styles );
+		const oldStyle = getStyleByValue( data.attributeOldValue, styles );
+
+		// Check if new style is allowed in configuration.
+		if ( !newStyle || !oldStyle ) {
+			return;
+		}
+
+		const viewElement = conversionApi.mapper.toViewElement( data.item );
+		viewElement.removeClass( data.attributeOldValue );
+		viewElement.addClass( newStyle.className );
+	};
+}
+
+export function removeStyle( styles ) {
+	return ( event, data, consumable, conversionApi ) => {
+		if ( !consumable.test( data.item, 'removeAttribute:imageStyle' ) || !isImage( data.item ) ) {
+			return;
+		}
+
+		// Check if there is class name associated with given value.
+		const newStyle = getStyleByValue( data.attributeNewValue, styles );
+		const oldStyle = getStyleByValue( data.attributeOldValue, styles );
+
+		// Check if styles are allowed in configuration.
+		if ( !newStyle || !oldStyle ) {
+			return;
+		}
+
+		consumable.consume( data.item, 'removeAttribute:imageStyle' );
+
+		const viewElement = conversionApi.mapper.toViewElement( data.item );
+		viewElement.removeClass( oldStyle.className );
+	};
+}
+
+export function viewToModelImageStyle( style ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		const viewFigureElement = data.input;
+		const modelImageElement = data.output;
+
+		// *** Step 1: Validate conversion.
+		// Check if view element has proper class to consume.
+		if ( !consumable.test( viewFigureElement, { class: style.className } ) ) {
+			return;
+		}
+
+		// Check if figure is converted to image.
+		if ( !isImage( modelImageElement ) ) {
+			return;
+		}
+
+		// Check if image element can be placed in current context wit additional attribute.
+		const attributes = [ ...modelImageElement.getAttributeKeys(), 'imageStyle' ];
+
+		if ( !conversionApi.schema.check( { name: 'image', inside: data.context, attributes } ) ) {
+			return;
+		}
+
+		// *** Step2: Convert to model.
+		consumable.consume( viewFigureElement, { class: style.className } );
+		modelImageElement.setAttribute( 'imageStyle', style.value );
+	};
+}

+ 0 - 1
packages/ckeditor5-image/src/imagestyle/imagestylecommand.js

@@ -16,7 +16,6 @@ import { isImage, getStyleByValue } from './utils.js';
  * @extends module:core/command/command~Command
  */
 export default class ImageStyleCommand extends Command {
-
 	/**
 	 * Creates instance of the command.
 	 *

+ 31 - 96
packages/ckeditor5-image/src/imagestyle/imagestyleengine.js

@@ -10,8 +10,14 @@
 import Plugin from '../../core/plugin.js';
 import ImageStyleCommand from './imagestylecommand.js';
 import ImageEngine from '../imageengine.js';
-import { isImage, getStyleByValue } from './utils.js';
+import { addStyle, changeStyle, removeStyle, viewToModelImageStyle } from './converters.js';
 
+/**
+ * The image style engine plugin. Sets default configuration, creates converters and registers
+ * {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand ImageStyleCommand}.
+ *
+ * @extends {module:core/plugin~Plugin}
+ */
 export default class ImageStyleEngine extends Plugin {
 	/**
 	 * @inheritDoc
@@ -20,6 +26,9 @@ export default class ImageStyleEngine extends Plugin {
 		return [ ImageEngine ];
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	init() {
 		const editor = this.editor;
 		const doc = editor.document;
@@ -31,23 +40,26 @@ export default class ImageStyleEngine extends Plugin {
 		editor.config.define( 'image.styles', {
 			options: {
 				// This option is equal to situation when no style is applied at all.
-				imageStyleFull: { title: 'Full size image', icon: 'bold', value: null },
+				imageStyleFull: { title: 'Full size image', icon: 'object-center', value: null },
 
 				// This represents side image.
-				imageStyleSide: { title: 'Side image', icon: 'italic', value: 'side', className: 'image-style-side' }
+				imageStyleSide: { title: 'Side image', icon: 'object-right', value: 'side', className: 'image-style-side' }
 			}
 		} );
 
 		// Get configuration.
 		const styles = editor.config.get( 'image.styles.options' );
 
-		// Allow style attribute in image.
-		schema.allow( { name: 'image', attributes: 'style' } );
+		// Allow imageStyle attribute in image.
+		// We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559.
+		schema.allow( { name: 'image', attributes: 'imageStyle', inside: '$root' } );
 
-		// Converters for models element style attribute.
-		editing.modelToView.on( 'addAttribute:style', addStyle( styles ) );
-		editing.modelToView.on( 'changeAttribute:style', changeStyle( styles ) );
-		editing.modelToView.on( 'removeAttribute:style', removeStyle( styles ) );
+		// Converters for models element imageStyle attribute.
+		editing.modelToView.on( 'addAttribute:imageStyle', addStyle( styles ) );
+		data.modelToView.on( 'addAttribute:imageStyle', addStyle( styles ) );
+		editing.modelToView.on( 'changeAttribute:imageStyle', changeStyle( styles ) );
+		data.modelToView.on( 'changeAttribute:imageStyle', changeStyle( styles ) );
+		editing.modelToView.on( 'removeAttribute:imageStyle', removeStyle( styles ) );
 
 		for ( let key in styles ) {
 			const style = styles[ key ];
@@ -64,90 +76,13 @@ export default class ImageStyleEngine extends Plugin {
 	}
 }
 
-function addStyle( styles ) {
-	return ( event, data, consumable, conversionApi ) => {
-		// Check if we can consume, and we are adding in image.
-		if ( !consumable.consume( data.item, 'addAttribute:style' ) || !isImage( data.item ) ) {
-			return;
-		}
-
-		// Check if there is class name associated with given value.
-		const newStyle = getStyleByValue( data.attributeNewValue, styles );
-
-		// Check if new style is allowed in configuration.
-		if ( !newStyle ) {
-			return;
-		}
-
-		conversionApi.mapper.toViewElement( data.item ).addClass( newStyle.className );
-	};
-}
-
-function changeStyle( styles ) {
-	return ( event, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, 'changeAttribute:style' ) || !isImage( data.item ) ) {
-			return;
-		}
-
-		// Check if there is class name associated with given value.
-		const newStyle = getStyleByValue( data.attributeNewValue, styles );
-
-		// Check if new style is allowed in configuration.
-		if ( !newStyle ) {
-			return;
-		}
-
-		const viewElement = conversionApi.mapper.toViewElement( data.item );
-		viewElement.removeClass( data.attributeOldValue );
-		viewElement.addClass( newStyle.className );
-	};
-}
-
-function removeStyle( styles ) {
-	return ( event, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, 'removeAttribute:style' ) || !isImage( data.item ) ) {
-			return;
-		}
-
-		// Check if there is class name associated with given value.
-		const newStyle = getStyleByValue( data.attributeNewValue, styles );
-		const oldStyle = getStyleByValue( data.attributeOldValue, styles );
-
-		// Check if styles are allowed in configuration.
-		if ( !newStyle || !oldStyle ) {
-			return;
-		}
-
-		const viewElement = conversionApi.mapper.toViewElement( data.item );
-		viewElement.removeClass( oldStyle.className );
-	};
-}
-
-function viewToModelImageStyle( style ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewFigureElement = data.input;
-		const modelImageElement = data.output;
-
-		// *** Step 1: Validate conversion.
-		// Check if view element has proper class to consume.
-		if ( !consumable.test( viewFigureElement, { class: style.className } ) ) {
-			return;
-		}
-
-		// Check if figure is converted to image.
-		if ( !isImage( modelImageElement ) ) {
-			return;
-		}
-
-		// Check if image element can be placed in current context wit additional attribute.
-		const attributes = [ ...modelImageElement.getAttributeKeys(), 'style' ];
-
-		if ( !conversionApi.schema.check( { name: 'image', inside: data.context, attributes } ) ) {
-			return;
-		}
-
-		// *** Step2: Convert to model.
-		consumable.consume( viewFigureElement, { class: style.className } );
-		modelImageElement.setAttribute( 'style', style.value );
-	};
-}
+/**
+ * Image style format descriptor.
+ *
+ * @typedef {Object} module:image/imagestyle/imagestyleengine~ImageStyleFormat
+ * @property {String} value Value used to store this style in model.
+ * When value is `null` style will be used as default one. Default style does not apply any CSS class to the view element.
+ * @property {String} icon Icon name to use when creating style's toolbar button.
+ * @property {String} title Style's title.
+ * @property {String} className CSS class used to represent style in view.
+ */

+ 7 - 4
packages/ckeditor5-image/src/imagetoolbar.js

@@ -14,6 +14,7 @@ import ToolbarView from '../ui/toolbar/toolbarview.js';
 import BalloonPanelView from '../ui/balloonpanel/balloonpanelview.js';
 import Template from '../ui/template.js';
 import ClickObserver from 'ckeditor5/engine/view/observer/clickobserver.js';
+import { isImageWidget } from './utils.js';
 
 const arrowVOffset = BalloonPanelView.arrowVerticalOffset;
 const positions = {
@@ -84,9 +85,11 @@ export default class ImageToolbar extends Plugin {
 
 			editingView.addObserver( ClickObserver );
 
-			// Check if the toolbar should be displayed each time the user clicked in editable.
-			editor.listenTo( editingView, 'click', () => {
-				if ( editingView.selection.isFake ) {
+			// Check if the toolbar should be displayed each time view is rendered.
+			editor.listenTo( editingView, 'render', () => {
+				const selectedElement = editingView.selection.getSelectedElement();
+
+				if ( selectedElement && isImageWidget( selectedElement ) ) {
 					attachToolbar();
 
 					// TODO: These 2 need interval–based event debouncing for performance
@@ -99,7 +102,7 @@ export default class ImageToolbar extends Plugin {
 					editor.ui.view.stopListening( window, 'scroll', attachToolbar );
 					editor.ui.view.stopListening( window, 'resize', attachToolbar );
 				}
-			} );
+			}, { priority: 'low' } );
 
 			function attachToolbar() {
 				panel.attachTo( {

+ 72 - 0
packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js

@@ -0,0 +1,72 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from 'tests/core/_utils/virtualtesteditor.js';
+import ImageStyleEngine from 'ckeditor5/image/imagestyle/imagestyleengine.js';
+import ImageEngine from 'ckeditor5/image/imageengine.js';
+import ImageStyleCommand from 'ckeditor5/image/imagestyle/imagestylecommand.js';
+import { getData } from 'ckeditor5/engine/dev-utils/model.js';
+
+describe( 'ImageStyleEngine', () => {
+	let editor, document;
+
+	beforeEach( () => {
+		return VirtualTestEditor.create( {
+			plugins: [ ImageStyleEngine ],
+		} )
+			.then( newEditor => {
+				editor = newEditor;
+				document = editor.document;
+			} );
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine );
+	} );
+
+	it( 'should load image engine', () => {
+		expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine );
+	} );
+
+	it( 'should set schema rules for image style', () => {
+		const schema = document.schema;
+
+		expect( schema.check( { name: 'image', attributes: [ 'imageStyle', 'src' ], inside: '$root' } ) ).to.be.true;
+	} );
+
+	it( 'should register command', () => {
+		expect( editor.commands.has( 'imagestyle' ) ).to.be.true;
+		const command = editor.commands.get( 'imagestyle' );
+
+		expect( command ).to.be.instanceOf( ImageStyleCommand );
+	} );
+
+	// TODO: check default configuration.
+
+	it( 'should convert from view to model', () => {
+		editor.setData( '<figure class="image image-style-side"><img src="foo.png" /></figure>' );
+
+		expect( getData( document, { withoutSelection: true } ) ).to.equal( '<image imageStyle="side" src="foo.png"></image>' );
+	} );
+
+	it( 'should not convert from view to model if class is not defined', () => {
+		editor.setData( '<figure class="image foo-bar"><img src="foo.png" /></figure>' );
+
+		expect( getData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
+	} );
+
+	it( 'should not convert from view to model when not in image figure', () => {
+		editor.setData( '<figure class="image-style-side"></figure>'  );
+
+		expect( getData( document, { withoutSelection: true } ) ).to.equal( '' );
+	} );
+
+	it( 'should not convert from view to model if schema prevents it', () => {
+		document.schema.disallow( { name: 'image', attributes: 'imageStyle' } );
+		editor.setData( '<figure class="image image-style-side"><img src="foo.png" /></figure>' );
+
+		expect( getData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
+	} );
+} );