8
0
Просмотр исходного кода

Merge pull request #6 from ckeditor/t/2

Feature: Initial implementation. Closes #2.
Szymon Kupś 8 лет назад
Родитель
Сommit
8da9e743ae
22 измененных файлов с 1367 добавлено и 1 удалено
  1. 6 0
      packages/ckeditor5-alignment/lang/contexts.json
  2. 10 1
      packages/ckeditor5-alignment/package.json
  3. 36 0
      packages/ckeditor5-alignment/src/alignment.js
  4. 147 0
      packages/ckeditor5-alignment/src/alignmentcommand.js
  5. 200 0
      packages/ckeditor5-alignment/src/alignmentediting.js
  6. 115 0
      packages/ckeditor5-alignment/src/alignmentui.js
  7. 14 0
      packages/ckeditor5-alignment/tests/alignment.js
  8. 162 0
      packages/ckeditor5-alignment/tests/alignmentcommand.js
  9. 244 0
      packages/ckeditor5-alignment/tests/alignmentediting.js
  10. 205 0
      packages/ckeditor5-alignment/tests/alignmentui.js
  11. 97 0
      packages/ckeditor5-alignment/tests/integration.js
  12. 12 0
      packages/ckeditor5-alignment/tests/manual/alignment.html
  13. 25 0
      packages/ckeditor5-alignment/tests/manual/alignment.js
  14. 33 0
      packages/ckeditor5-alignment/tests/manual/alignment.md
  15. 12 0
      packages/ckeditor5-alignment/tests/manual/alignmentconfig.html
  16. 26 0
      packages/ckeditor5-alignment/tests/manual/alignmentconfig.js
  17. 19 0
      packages/ckeditor5-alignment/tests/manual/alignmentconfig.md
  18. BIN
      packages/ckeditor5-alignment/tests/manual/sample.jpg
  19. 1 0
      packages/ckeditor5-alignment/theme/icons/align-center.svg
  20. 1 0
      packages/ckeditor5-alignment/theme/icons/align-justify.svg
  21. 1 0
      packages/ckeditor5-alignment/theme/icons/align-left.svg
  22. 1 0
      packages/ckeditor5-alignment/theme/icons/align-right.svg

+ 6 - 0
packages/ckeditor5-alignment/lang/contexts.json

@@ -0,0 +1,6 @@
+{
+	"Align left": "Toolbar button tooltip for aligning text to the left.",
+	"Align right": "Toolbar button tooltip for aligning text to the right.",
+	"Align center": "Toolbar button tooltip for aligning text to center.",
+	"Justify": "Toolbar button tooltip for making text justified."
+}

+ 10 - 1
packages/ckeditor5-alignment/package.json

@@ -9,9 +9,18 @@
   "dependencies": {
     "@ckeditor/ckeditor5-core": "^1.0.0-alpha.1",
     "@ckeditor/ckeditor5-engine": "^1.0.0-alpha.1",
-    "@ckeditor/ckeditor5-utils": "^1.0.0-alpha.1"
+    "@ckeditor/ckeditor5-ui": "^1.0.0-alpha.1"
   },
   "devDependencies": {
+    "@ckeditor/ckeditor5-block-quote": "^1.0.0-alpha.1",
+    "@ckeditor/ckeditor5-editor-classic": "^1.0.0-alpha.1",
+    "@ckeditor/ckeditor5-enter": "^1.0.0-alpha.1",
+    "@ckeditor/ckeditor5-heading": "^1.0.0-alpha.1",
+    "@ckeditor/ckeditor5-image": "^1.0.0-alpha.1",
+    "@ckeditor/ckeditor5-list": "^1.0.0-alpha.1",
+    "@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.1",
+    "@ckeditor/ckeditor5-typing": "^1.0.0-alpha.1",
+    "@ckeditor/ckeditor5-utils": "^1.0.0-alpha.1",
     "eslint": "^4.8.0",
     "eslint-config-ckeditor5": "^1.0.6",
     "husky": "^0.14.3",

+ 36 - 0
packages/ckeditor5-alignment/src/alignment.js

@@ -0,0 +1,36 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module alignment/alignment
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import AlignmentEditing from './alignmentediting';
+import AlignmentUI from './alignmentui';
+
+/**
+ * The alignment plugin.
+ *
+ * It requires {@link module:alignment/alignmentediting~AlignmentEditing} and {@link module:alignment/alignmentui~AlignmentUI} plugins.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class Alignment extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ AlignmentEditing, AlignmentUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'Alignment';
+	}
+}

+ 147 - 0
packages/ckeditor5-alignment/src/alignmentcommand.js

@@ -0,0 +1,147 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module alignment/alignmentcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import first from '@ckeditor/ckeditor5-utils/src/first';
+
+/**
+ * The alignment command plugin.
+ *
+ * @extends module:core/command~Command
+ */
+export default class AlignmentCommand extends Command {
+	/**
+	 * Creates an instance of the command.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor The editor instance.
+	 * @param {'left'|'right'|'center'|'justify'} type Alignment type to be handled by this command.
+	 * @param {Boolean} isDefault Indicates if command is of default type.
+	 */
+	constructor( editor, type, isDefault ) {
+		super( editor );
+
+		/**
+		 * The type of the list created by the command.
+		 *
+		 * @readonly
+		 * @member {'left'|'right'|'center'|'justify'}
+		 */
+		this.type = type;
+
+		/**
+		 * Whether this command has default type.
+		 *
+		 * @readonly
+		 * @private
+		 * @member {Boolean}
+		 */
+		this._isDefault = isDefault;
+
+		/**
+		 * A flag indicating whether the command is active, which means that the selection starts in a block
+		 * that has defined alignment of the same type.
+		 *
+		 * @observable
+		 * @readonly
+		 * @member {Boolean} #value
+		 */
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const firstBlock = first( this.editor.document.selection.getSelectedBlocks() );
+
+		// As first check whether to enable or disable command as value will be always false if command cannot be enabled.
+		this.isEnabled = !!firstBlock && this._canBeAligned( firstBlock );
+		this.value = this._getValue( firstBlock );
+	}
+
+	/**
+	 * Executes the command.
+	 *
+	 * @protected
+	 * @param {Object} [options] Options for the executed command.
+	 * @param {module:engine/model/batch~Batch} [options.batch] A batch to collect all the change steps.
+	 * A new batch will be created if this option is not set.
+	 */
+	execute( options = {} ) {
+		const editor = this.editor;
+		const document = editor.document;
+
+		document.enqueueChanges( () => {
+			const batch = options.batch || document.batch();
+
+			// Get only those blocks from selected that can have alignment set
+			const blocks = Array.from( document.selection.getSelectedBlocks() ).filter( block => this._canBeAligned( block ) );
+
+			// Remove alignment attribute if current alignment is as selected or is default one.
+			// Default alignment should not be stored in model as it will bloat model data.
+			if ( this.value || this._isDefault ) {
+				removeAlignmentFromSelection( blocks, batch );
+			} else {
+				setAlignmentOnSelection( blocks, batch, this.type );
+			}
+		} );
+	}
+
+	/**
+	 * Checks whether block can have aligned set.
+	 *
+	 * @param {module:engine/model/element~Element} block A block to be checked.
+	 * @returns {Boolean}
+	 * @private
+	 */
+	_canBeAligned( block ) {
+		const schema = this.editor.document.schema;
+
+		// Check if adding alignment attribute to selected block is allowed.
+		return schema.check( {
+			name: block.name,
+			// Apparently I must pass current attributes as otherwise adding alignment on listItem will fail.
+			attributes: [ ...block.getAttributeKeys(), 'alignment' ]
+		} );
+	}
+
+	/**
+	 * Checks the command's {@link #value}.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} firstBlock A first block in selection to be checked.
+	 * @returns {Boolean} The current value.
+	 */
+	_getValue( firstBlock ) {
+		// The #_checkEnabled is checked as first so if command is disabled it's value is also false.
+		if ( !this.isEnabled || !firstBlock ) {
+			return false;
+		}
+
+		const selectionAlignment = firstBlock.getAttribute( 'alignment' );
+
+		// Command's value will be set when commands type is matched in selection or the selection is default one.
+		return selectionAlignment ? selectionAlignment === this.type : this._isDefault;
+	}
+}
+
+// Removes alignment attribute from blocks.
+// @private
+function removeAlignmentFromSelection( blocks, batch ) {
+	for ( const block of blocks ) {
+		batch.removeAttribute( block, 'alignment' );
+	}
+}
+
+// Sets alignment attribute on blocks.
+// @private
+function setAlignmentOnSelection( blocks, batch, type ) {
+	for ( const block of blocks ) {
+		batch.setAttribute( block, 'alignment', type );
+	}
+}

+ 200 - 0
packages/ckeditor5-alignment/src/alignmentediting.js

@@ -0,0 +1,200 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module alignment/alignmentediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import AlignmentCommand from './alignmentcommand';
+
+import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
+import { eventNameToConsumableType } from '@ckeditor/ckeditor5-engine/src/conversion/model-to-view-converters';
+
+import upperFirst from '@ckeditor/ckeditor5-utils/src/lib/lodash/upperFirst';
+
+/**
+ * @extends module:core/plugin~Plugin
+ */
+export default class AlignmentEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'alignment', { styles: [ ...this.constructor.supportedStyles ] } );
+	}
+
+	/**
+	 * List of supported alignment styles:
+	 * - left
+	 * - right
+	 * - center
+	 * - justify
+	 *
+	 * @static
+	 * @readonly
+	 * @member {Array.<String>} module:alignment/alignmentediting~AlignmentEditing.supportedStyles
+	 */
+	static get supportedStyles() {
+		return [ 'left', 'right', 'center', 'justify' ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const doc = editor.document;
+		const schema = doc.schema;
+		const data = editor.data;
+		const editing = editor.editing;
+
+		const enabledStyles = editor.config.get( 'alignment.styles' );
+
+		// Allow alignment attribute on all blocks.
+		schema.allow( { name: '$block', attributes: 'alignment' } );
+
+		attributeToStyleConverter(
+			[ data.modelToView, editing.modelToView ],
+			'alignment',
+			attribute => ( { 'text-align': attribute } ),
+			() => [ 'text-align' ]
+		);
+
+		// Convert `text-align` style property from element to model attribute alignment.
+		buildViewConverter()
+			.for( data.viewToModel )
+			.fromAttribute( 'style', /text-align/ )
+			.toAttribute( viewElement => {
+				const textAlign = viewElement.getStyle( 'text-align' );
+
+				// Do not convert empty, default or unknown alignment values.
+				if ( !textAlign || isDefault( textAlign ) || !enabledStyles.includes( textAlign ) ) {
+					return;
+				}
+
+				return { key: 'alignment', value: textAlign };
+			} );
+
+		// Add only enabled & supported commands.
+		enabledStyles
+			.filter( isSupported )
+			.forEach( style => {
+				editor.commands.add( AlignmentEditing.commandName( style ), new AlignmentCommand( editor, style, isDefault( style ) ) );
+			} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	afterInit() {
+		const schema = this.editor.document.schema;
+
+		// Disallow alignment on caption elements.
+		if ( schema.hasItem( 'caption' ) ) {
+			schema.disallow( { name: 'caption', attributes: 'alignment' } );
+		}
+	}
+
+	/**
+	 * Helper function that returns command name for given style. May produce unknown commands if passed style is not
+	 * in {@link module:alignment/alignmentediting~AlignmentEditing.supportedStyles}.
+	 *
+	 * @param {String} style
+	 * @returns {String}
+	 */
+	static commandName( style ) {
+		return `align${ upperFirst( style ) }`;
+	}
+}
+
+/**
+ * Checks whether passed style is supported by {@link module:alignment/alignmentediting~AlignmentEditing}.
+ *
+ * @param {String} style Style value to check.
+ * @returns {Boolean}
+ */
+export function isSupported( style ) {
+	return AlignmentEditing.supportedStyles.includes( style );
+}
+
+// Defines attribute to style converters.
+// @private
+function attributeToStyleConverter( dispatchers, modelAttributeName, setStyleFn, removeStyleFn ) {
+	for ( const dispatcher of dispatchers ) {
+		dispatcher.on( `addAttribute:${ modelAttributeName }`, setStyle( setStyleFn ) );
+		dispatcher.on( `changeAttribute:${ modelAttributeName }`, setStyle( setStyleFn ) );
+		dispatcher.on( `removeAttribute:${ modelAttributeName }`, removeStyle( removeStyleFn ) );
+	}
+}
+
+// Dispatcher handler responsible for setting style to a view element.
+// @private
+function setStyle( setStyleFn ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
+			return;
+		}
+
+		const styles = setStyleFn( data.attributeNewValue );
+
+		conversionApi.mapper.toViewElement( data.item ).setStyle( styles );
+	};
+}
+
+// Dispatcher handler responsible for removing style attributes from a view element.
+// @private
+function removeStyle( removeStyleFn ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
+			return;
+		}
+
+		const styles = removeStyleFn();
+		const viewElement = conversionApi.mapper.toViewElement( data.item );
+		viewElement.removeStyle( ...styles );
+	};
+}
+
+// Check whether alignment is default one.
+// @protected
+export function isDefault( textAlign ) {
+	// Right now only RTL is supported so 'left' value is always default one.
+	return textAlign === 'left';
+}
+
+/**
+ * The configuration of the {@link module:alignment/alignmentediting~AlignmentEditing Alignment feature}.
+ *
+ * Read more in {@link module:alignment/alignmentediting~AlignmentEditingConfig}.
+ *
+ * @member {module:alignment/alignmentediting~AlignmentEditingConfig} module:core/editor/editorconfig~EditorConfig#alignment
+ */
+
+/**
+ * The configuration of the {@link module:alignment/alignmentediting~AlignmentEditing Alignment feature}.
+ *
+ *		ClassicEditor
+ *			.create( editorElement, {
+ * 				alignment: {
+ *					styles: [ 'left', 'right' ]
+ * 				}
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
+ *
+ * @interface AlignmentEditingConfig
+ */
+
+/**
+ * Enabled alignment styles from supported styles: `left`, `right`, `center` and `justify`. Other values are ignored.
+ *
+ * @member {String} module:alignment/alignmentediting~AlignmentEditingConfig#styles
+ */

+ 115 - 0
packages/ckeditor5-alignment/src/alignmentui.js

@@ -0,0 +1,115 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module alignment/alignmentui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import alignLeftIcon from '../theme/icons/align-left.svg';
+import alignRightIcon from '../theme/icons/align-right.svg';
+import alignCenterIcon from '../theme/icons/align-center.svg';
+import alignJustifyIcon from '../theme/icons/align-justify.svg';
+import AlignmentEditing, { isSupported } from './alignmentediting';
+
+const icons = new Map( [
+	[ 'left', alignLeftIcon ],
+	[ 'right', alignRightIcon ],
+	[ 'center', alignCenterIcon ],
+	[ 'justify', alignJustifyIcon ]
+] );
+
+/**
+ * The default Alignment UI plugin.
+ *
+ * It introduces the `'alignLeft'`, `'alignRight'`, `'alignCenter'` and `'alignJustify'` buttons.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class AlignmentUI extends Plugin {
+	/**
+	 * Returns the localized style titles provided by the plugin.
+	 *
+	 * The following localized titles corresponding with
+	 * {@link module:alignment/alignmentediting~AlignmentEditingConfig#styles} are available:
+	 *
+	 * * `'Left'`,
+	 * * `'Right'`,
+	 * * `'Center'`,
+	 * * `'Justify'`
+	 *
+	 * @readonly
+	 * @type {Object.<String,String>}
+	 */
+	get localizedStylesTitles() {
+		const t = this.editor.t;
+
+		return {
+			'left': t( 'Align left' ),
+			'right': t( 'Align right' ),
+			'center': t( 'Align center' ),
+			'justify': t( 'Justify' )
+		};
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ AlignmentEditing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'AlignmentUI';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const styles = this.editor.config.get( 'alignment.styles' );
+
+		styles
+			.filter( isSupported )
+			.forEach( style => this._addButton( style ) );
+	}
+
+	/**
+	 * Helper method for initializing a button and linking it with an appropriate command.
+	 *
+	 * @private
+	 * @param {String} style The name of style for which add button.
+	 */
+	_addButton( style ) {
+		const editor = this.editor;
+
+		const commandName = AlignmentEditing.commandName( style );
+		const command = editor.commands.get( commandName );
+
+		editor.ui.componentFactory.add( commandName, locale => {
+			const buttonView = new ButtonView( locale );
+
+			buttonView.set( {
+				label: this.localizedStylesTitles[ style ],
+				icon: icons.get( style ),
+				tooltip: true
+			} );
+
+			// Bind button model to command.
+			buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
+
+			// Execute command.
+			this.listenTo( buttonView, 'execute', () => editor.execute( commandName ) );
+
+			return buttonView;
+		} );
+	}
+}

+ 14 - 0
packages/ckeditor5-alignment/tests/alignment.js

@@ -0,0 +1,14 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Alignment from '../src/alignment';
+import AlignmentEditing from '../src/alignmentediting';
+import AlignmentUI from '../src/alignmentui';
+
+describe( 'Alignment', () => {
+	it( 'requires AlignmentEditing and AlignmentUI', () => {
+		expect( Alignment.requires ).to.deep.equal( [ AlignmentEditing, AlignmentUI ] );
+	} );
+} );

+ 162 - 0
packages/ckeditor5-alignment/tests/alignmentcommand.js

@@ -0,0 +1,162 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import AlignmentCommand from '../src/alignmentcommand';
+
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+
+describe( 'AlignmentCommand', () => {
+	let editor, doc, command, defaultAlignmentCommand;
+
+	beforeEach( () => {
+		return ModelTestEditor.create()
+			.then( newEditor => {
+				doc = newEditor.document;
+				command = new AlignmentCommand( newEditor, 'center', false );
+				defaultAlignmentCommand = new AlignmentCommand( newEditor, 'left', true );
+				editor = newEditor;
+
+				editor.commands.add( 'alignCenter', command );
+				editor.commands.add( 'alignLeft', defaultAlignmentCommand );
+
+				doc.schema.registerItem( 'paragraph', '$block' );
+
+				doc.schema.allow( { name: '$block', inside: '$root', attributes: 'alignment' } );
+			} );
+	} );
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	it( 'is a command', () => {
+		expect( AlignmentCommand.prototype ).to.be.instanceOf( Command );
+		expect( command ).to.be.instanceOf( Command );
+	} );
+
+	describe( 'value', () => {
+		it( 'is true when selection is in block with command type alignment', () => {
+			setModelData( doc, '<paragraph alignment="center">x[]x</paragraph>' );
+
+			expect( command ).to.have.property( 'value', true );
+		} );
+
+		it( 'is false when selection is inside block that has different alignment', () => {
+			setModelData( doc, '<paragraph alignment="justify">x[]x</paragraph>' );
+
+			expect( command ).to.have.property( 'value', false );
+		} );
+
+		it( 'is true when selection is in block with default alignment', () => {
+			setModelData( doc, '<paragraph>x[]x</paragraph>' );
+
+			expect( defaultAlignmentCommand ).to.have.property( 'value', true );
+		} );
+
+		it( 'is false when selection is inside block that has different alignment (default style)', () => {
+			setModelData( doc, '<paragraph alignment="justify">x[]x</paragraph>' );
+
+			expect( defaultAlignmentCommand ).to.have.property( 'value', false );
+		} );
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'is true when selection is in a block which can have added alignment', () => {
+			setModelData( doc, '<paragraph>x[]x</paragraph>' );
+
+			expect( command ).to.have.property( 'isEnabled', true );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		describe( 'applying alignment', () => {
+			it( 'adds alignment to block element', () => {
+				setModelData( doc, '<paragraph>x[]x</paragraph>' );
+
+				editor.execute( 'alignCenter' );
+
+				expect( getModelData( doc ) ).to.equal( '<paragraph alignment="center">x[]x</paragraph>' );
+			} );
+
+			it( 'should remove alignment from single block element if already has one', () => {
+				setModelData( doc, '<paragraph alignment="center">x[]x</paragraph>' );
+
+				editor.execute( 'alignCenter' );
+
+				expect( getModelData( doc ) ).to.equal( '<paragraph>x[]x</paragraph>' );
+			} );
+
+			it( 'adds alignment to all selected blocks', () => {
+				setModelData( doc, '<paragraph>x[x</paragraph><paragraph>xx</paragraph><paragraph>x]x</paragraph>' );
+
+				editor.execute( 'alignCenter' );
+
+				expect( getModelData( doc ) ).to.equal(
+					'<paragraph alignment="center">x[x</paragraph>' +
+					'<paragraph alignment="center">xx</paragraph>' +
+					'<paragraph alignment="center">x]x</paragraph>'
+				);
+			} );
+
+			it( 'sets alignment on all selected blocks as first block', () => {
+				setModelData(
+					doc,
+					'<paragraph>x[x</paragraph>' +
+					'<paragraph >xx</paragraph>' +
+					'<paragraph alignment="center">x]x</paragraph>'
+				);
+
+				editor.execute( 'alignCenter' );
+
+				expect( getModelData( doc ) ).to.equal(
+					'<paragraph alignment="center">x[x</paragraph>' +
+					'<paragraph alignment="center">xx</paragraph>' +
+					'<paragraph alignment="center">x]x</paragraph>'
+				);
+			} );
+		} );
+
+		describe( 'applying default alignment', () => {
+			it( 'removes alignment from block element', () => {
+				setModelData( doc, '<paragraph alignment="justify">x[]x</paragraph>' );
+
+				editor.execute( 'alignLeft' );
+
+				expect( getModelData( doc ) ).to.equal( '<paragraph>x[]x</paragraph>' );
+			} );
+
+			it( 'removes alignment from all selected blocks', () => {
+				setModelData( doc,
+					'<paragraph alignment="center">x[x</paragraph>' +
+					'<paragraph alignment="center">xx</paragraph>' +
+					'<paragraph alignment="center">x]x</paragraph>'
+				);
+
+				editor.execute( 'alignLeft' );
+
+				expect( getModelData( doc ) ).to.equal(
+					'<paragraph>x[x</paragraph><paragraph>xx</paragraph><paragraph>x]x</paragraph>'
+				);
+			} );
+
+			it( 'removes alignment from all selected blocks even if one has no alignment defined', () => {
+				setModelData( doc,
+					'<paragraph alignment="center">x[x</paragraph>' +
+					'<paragraph>xx</paragraph>' +
+					'<paragraph alignment="center">x]x</paragraph>'
+				);
+
+				editor.execute( 'alignLeft' );
+
+				expect( getModelData( doc ) ).to.equal(
+					'<paragraph>x[x</paragraph><paragraph>xx</paragraph><paragraph>x]x</paragraph>'
+				);
+			} );
+		} );
+	} );
+} );

+ 244 - 0
packages/ckeditor5-alignment/tests/alignmentediting.js

@@ -0,0 +1,244 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import AlignmentEditing from '../src/alignmentediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import ImageCaptionEngine from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionengine';
+import ListEngine from '@ckeditor/ckeditor5-list/src/listengine';
+import HeadingEngine from '@ckeditor/ckeditor5-heading/src/headingengine';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { eventNameToConsumableType } from '@ckeditor/ckeditor5-engine/src/conversion/model-to-view-converters';
+
+import AlignmentCommand from '../src/alignmentcommand';
+
+describe( 'AlignmentEditing', () => {
+	let editor, doc;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ AlignmentEditing, Paragraph ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+
+				doc = editor.document;
+			} );
+	} );
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	it( 'adds alignment commands', () => {
+		expect( editor.commands.get( 'alignLeft' ) ).to.be.instanceOf( AlignmentCommand );
+		expect( editor.commands.get( 'alignRight' ) ).to.be.instanceOf( AlignmentCommand );
+		expect( editor.commands.get( 'alignCenter' ) ).to.be.instanceOf( AlignmentCommand );
+		expect( editor.commands.get( 'alignJustify' ) ).to.be.instanceOf( AlignmentCommand );
+	} );
+
+	it( 'allows for alignment in $blocks', () => {
+		expect( doc.schema.check( { name: '$block', inside: '$root', attributes: 'alignment' } ) ).to.be.true;
+	} );
+
+	describe( 'integration', () => {
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ AlignmentEditing, ImageCaptionEngine, Paragraph, ListEngine, HeadingEngine ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					doc = editor.document;
+				} );
+		} );
+
+		it( 'is allowed on paragraph', () => {
+			expect( doc.schema.check( { name: 'paragraph', attributes: 'alignment' } ) ).to.be.true;
+		} );
+
+		it( 'is allowed on listItem', () => {
+			expect( doc.schema.check( { name: 'listItem', attributes: [ 'type', 'indent', 'alignment' ] } ) ).to.be.true;
+		} );
+
+		it( 'is allowed on heading', () => {
+			expect( doc.schema.check( { name: 'heading1', attributes: 'alignment' } ) ).to.be.true;
+		} );
+
+		it( 'is disallowed on caption', () => {
+			expect( doc.schema.check( { name: 'caption', attributes: 'alignment' } ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'alignLeft', () => {
+		it( 'adds converters to the data pipeline', () => {
+			const data = '<p style="text-align:left;">x</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph>[]x</paragraph>' );
+			expect( editor.getData() ).to.equal( '<p>x</p>' );
+		} );
+
+		it( 'adds a converter to the view pipeline for removing attribute', () => {
+			setModelData( doc, '<paragraph alignment="center">[]x</paragraph>' );
+
+			expect( editor.getData() ).to.equal( '<p style="text-align:center;">x</p>' );
+
+			const command = editor.commands.get( 'alignLeft' );
+
+			command.execute();
+
+			expect( editor.getData() ).to.equal( '<p>x</p>' );
+		} );
+	} );
+
+	describe( 'alignCenter', () => {
+		it( 'adds converters to the data pipeline', () => {
+			const data = '<p style="text-align:center;">x</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph alignment="center">[]x</paragraph>' );
+			expect( editor.getData() ).to.equal( data );
+		} );
+
+		it( 'adds a converter to the view pipeline', () => {
+			setModelData( doc, '<paragraph alignment="center">[]x</paragraph>' );
+
+			expect( editor.getData() ).to.equal( '<p style="text-align:center;">x</p>' );
+		} );
+
+		it( 'adds a converter to the view pipeline for changing attribute', () => {
+			setModelData( doc, '<paragraph alignment="right">[]x</paragraph>' );
+
+			expect( editor.getData() ).to.equal( '<p style="text-align:right;">x</p>' );
+
+			const command = editor.commands.get( 'alignCenter' );
+
+			command.execute();
+
+			expect( editor.getData() ).to.equal( '<p style="text-align:center;">x</p>' );
+		} );
+	} );
+
+	describe( 'alignRight', () => {
+		it( 'adds converters to the data pipeline', () => {
+			const data = '<p style="text-align:right;">x</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph alignment="right">[]x</paragraph>' );
+			expect( editor.getData() ).to.equal( data );
+		} );
+
+		it( 'adds a converter to the view pipeline', () => {
+			setModelData( doc, '<paragraph alignment="right">[]x</paragraph>' );
+
+			expect( editor.getData() ).to.equal( '<p style="text-align:right;">x</p>' );
+		} );
+	} );
+
+	describe( 'alignJustify', () => {
+		it( 'adds converters to the data pipeline', () => {
+			const data = '<p style="text-align:justify;">x</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph alignment="justify">[]x</paragraph>' );
+			expect( editor.getData() ).to.equal( data );
+		} );
+
+		it( 'adds a converter to the view pipeline', () => {
+			setModelData( doc, '<paragraph alignment="justify">[]x</paragraph>' );
+
+			expect( editor.getData() ).to.equal( '<p style="text-align:justify;">x</p>' );
+		} );
+	} );
+
+	describe( 'should be extensible', () => {
+		it( 'converters in the data pipeline', () => {
+			blockDefaultConversion( editor.data.modelToView );
+			blockDefaultConversion( editor.editing.modelToView );
+
+			const data = '<p style="text-align:justify;">x</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph alignment="justify">[]x</paragraph>' );
+
+			expect( editor.getData() ).to.equal( '<p>x</p>' );
+
+			const command = editor.commands.get( 'alignLeft' );
+			command.execute();
+
+			expect( editor.getData() ).to.equal( '<p>x</p>' );
+		} );
+	} );
+
+	describe( 'should work with broken styles', () => {
+		it( 'should ignore empty style', () => {
+			const data = '<p style="text-align:">x</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph>[]x</paragraph>' );
+			expect( editor.getData() ).to.equal( '<p>x</p>' );
+		} );
+
+		it( 'should ignore not known style', () => {
+			const data = '<p style="text-align:unset;">x</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph>[]x</paragraph>' );
+			expect( editor.getData() ).to.equal( '<p>x</p>' );
+		} );
+	} );
+
+	describe( 'config', () => {
+		describe( 'styles', () => {
+			describe( 'default value', () => {
+				it( 'should be set', () => {
+					expect( editor.config.get( 'alignment.styles' ) ).to.deep.equal( [ 'left', 'right', 'center', 'justify' ] );
+				} );
+			} );
+
+			it( 'should customize commands', () => {
+				return VirtualTestEditor
+					.create( {
+						alignment: { styles: [ 'left', 'right' ] },
+						plugins: [ AlignmentEditing, Paragraph ]
+					} )
+					.then( editor => {
+						expect( editor.commands.get( 'alignLeft' ), 'adds alignLeft' ).to.be.instanceof( AlignmentCommand );
+						expect( editor.commands.get( 'alignRight' ), 'adds alignLeft' ).to.be.instanceof( AlignmentCommand );
+						expect( editor.commands.get( 'alignCenter' ), 'does not add alignCenter' ).to.be.undefined;
+						expect( editor.commands.get( 'alignJustify' ), 'does not add alignJustify' ).to.be.undefined;
+					} );
+			} );
+		} );
+	} );
+} );
+
+function blockDefaultConversion( dispatcher ) {
+	dispatcher.on(
+		'addAttribute:alignment',
+		( evt, data, consumable ) => consumable.consume( data.item, eventNameToConsumableType( evt.name ) ),
+		{ 'priority': 'high' }
+	);
+	dispatcher.on(
+		'changeAttribute:alignment',
+		( evt, data, consumable ) => consumable.consume( data.item, eventNameToConsumableType( evt.name ) ),
+		{ 'priority': 'high' }
+	);
+	dispatcher.on(
+		'removeAttribute:alignment',
+		( evt, data, consumable ) => consumable.consume( data.item, eventNameToConsumableType( evt.name ) ),
+		{ 'priority': 'high' }
+	);
+}

+ 205 - 0
packages/ckeditor5-alignment/tests/alignmentui.js

@@ -0,0 +1,205 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import AlignmentEditing from '../src/alignmentediting';
+import AlignmentUI from '../src/alignmentui';
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+
+describe( 'Alignment', () => {
+	let editor, command, element, button;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [ AlignmentEditing, AlignmentUI ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'requires AlignmentEditing', () => {
+		expect( AlignmentUI.requires ).to.deep.equal( [ AlignmentEditing ] );
+	} );
+
+	describe( 'localizedStylesTitles()', () => {
+		it( 'should return localized titles of styles', () => {
+			const editorMock = { t: str => str };
+
+			const plugin = new AlignmentUI( editorMock );
+
+			expect( plugin.localizedStylesTitles ).to.deep.equal( {
+				'left': 'Align left',
+				'right': 'Align right',
+				'center': 'Align center',
+				'justify': 'Justify'
+			} );
+		} );
+	} );
+
+	describe( 'alignLeft button', () => {
+		beforeEach( () => {
+			command = editor.commands.get( 'alignLeft' );
+			button = editor.ui.componentFactory.create( 'alignLeft' );
+		} );
+
+		it( 'has the base properties', () => {
+			expect( button ).to.have.property( 'label', 'Align left' );
+			expect( button ).to.have.property( 'icon' );
+			expect( button ).to.have.property( 'tooltip', true );
+		} );
+
+		it( 'has isOn bound to command\'s value', () => {
+			command.value = false;
+			expect( button ).to.have.property( 'isOn', false );
+
+			command.value = true;
+			expect( button ).to.have.property( 'isOn', true );
+		} );
+
+		it( 'has isEnabled bound to command\'s isEnabled', () => {
+			command.isEnabled = true;
+			expect( button ).to.have.property( 'isEnabled', true );
+
+			command.isEnabled = false;
+			expect( button ).to.have.property( 'isEnabled', false );
+		} );
+
+		it( 'executes command when it\'s executed', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			button.fire( 'execute' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'alignLeft' );
+		} );
+	} );
+
+	describe( 'alignRight button', () => {
+		beforeEach( () => {
+			command = editor.commands.get( 'alignRight' );
+			button = editor.ui.componentFactory.create( 'alignRight' );
+		} );
+
+		it( 'has the base properties', () => {
+			expect( button ).to.have.property( 'label', 'Align right' );
+			expect( button ).to.have.property( 'icon' );
+			expect( button ).to.have.property( 'tooltip', true );
+		} );
+
+		it( 'has isOn bound to command\'s value', () => {
+			command.value = false;
+			expect( button ).to.have.property( 'isOn', false );
+
+			command.value = true;
+			expect( button ).to.have.property( 'isOn', true );
+		} );
+
+		it( 'has isEnabled bound to command\'s isEnabled', () => {
+			command.isEnabled = true;
+			expect( button ).to.have.property( 'isEnabled', true );
+
+			command.isEnabled = false;
+			expect( button ).to.have.property( 'isEnabled', false );
+		} );
+
+		it( 'executes command when it\'s executed', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			button.fire( 'execute' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'alignRight' );
+		} );
+	} );
+
+	describe( 'alignCenter button', () => {
+		beforeEach( () => {
+			command = editor.commands.get( 'alignCenter' );
+			button = editor.ui.componentFactory.create( 'alignCenter' );
+		} );
+
+		it( 'has the base properties', () => {
+			expect( button ).to.have.property( 'label', 'Align center' );
+			expect( button ).to.have.property( 'icon' );
+			expect( button ).to.have.property( 'tooltip', true );
+		} );
+
+		it( 'has isOn bound to command\'s value', () => {
+			command.value = false;
+			expect( button ).to.have.property( 'isOn', false );
+
+			command.value = true;
+			expect( button ).to.have.property( 'isOn', true );
+		} );
+
+		it( 'has isEnabled bound to command\'s isEnabled', () => {
+			command.isEnabled = true;
+			expect( button ).to.have.property( 'isEnabled', true );
+
+			command.isEnabled = false;
+			expect( button ).to.have.property( 'isEnabled', false );
+		} );
+
+		it( 'executes command when it\'s executed', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			button.fire( 'execute' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'alignCenter' );
+		} );
+	} );
+
+	describe( 'alignJustify button', () => {
+		beforeEach( () => {
+			command = editor.commands.get( 'alignJustify' );
+			button = editor.ui.componentFactory.create( 'alignJustify' );
+		} );
+
+		it( 'has the base properties', () => {
+			expect( button ).to.have.property( 'label', 'Justify' );
+			expect( button ).to.have.property( 'icon' );
+			expect( button ).to.have.property( 'tooltip', true );
+		} );
+
+		it( 'has isOn bound to command\'s value', () => {
+			command.value = false;
+			expect( button ).to.have.property( 'isOn', false );
+
+			command.value = true;
+			expect( button ).to.have.property( 'isOn', true );
+		} );
+
+		it( 'has isEnabled bound to command\'s isEnabled', () => {
+			command.isEnabled = true;
+			expect( button ).to.have.property( 'isEnabled', true );
+
+			command.isEnabled = false;
+			expect( button ).to.have.property( 'isEnabled', false );
+		} );
+
+		it( 'executes command when it\'s executed', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			button.fire( 'execute' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'alignJustify' );
+		} );
+	} );
+} );

+ 97 - 0
packages/ckeditor5-alignment/tests/integration.js

@@ -0,0 +1,97 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import Alignment from '../src/alignment';
+import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Heading from '@ckeditor/ckeditor5-heading/src/heading';
+import Image from '@ckeditor/ckeditor5-image/src/image';
+import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
+import List from '@ckeditor/ckeditor5-list/src/list';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Delete from '@ckeditor/ckeditor5-typing/src/delete';
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'Alignment', () => {
+	let editor, doc, element;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [ Alignment, BlockQuote, Paragraph, Heading, Image, ImageCaption, List, Enter, Delete ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				doc = editor.document;
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'compatibility with images', () => {
+		it( 'does not work inside image caption', () => {
+			setModelData( doc, '<image src="foo.png"><caption>Foo[]</caption></image>' );
+
+			editor.execute( 'alignCenter' );
+
+			expect( getModelData( doc ) ).to.equal( '<image src="foo.png"><caption>Foo[]</caption></image>' );
+		} );
+		it( 'does not work inside image caption when selection overlaps image', () => {
+			setModelData(
+				doc,
+				'<paragraph>foo[foo</paragraph>' +
+				'<image src="foo.png"><caption>bar</caption></image>' +
+				'<paragraph>baz]baz</paragraph>'
+			);
+
+			editor.execute( 'alignCenter' );
+
+			expect( getModelData( doc ) ).to.equal(
+				'<paragraph alignment="center">foo[foo</paragraph>' +
+				'<image src="foo.png"><caption>bar</caption></image>' +
+				'<paragraph alignment="center">baz]baz</paragraph>'
+			);
+		} );
+	} );
+
+	describe( 'compatibility with blockQuote', () => {
+		it( 'does work inside BlockQuote on paragraph', () => {
+			setModelData( doc, '<blockQuote><paragraph>Foo[]</paragraph></blockQuote>' );
+
+			editor.execute( 'alignCenter' );
+
+			expect( getModelData( doc ) ).to.equal( '<blockQuote><paragraph alignment="center">Foo[]</paragraph></blockQuote>' );
+		} );
+
+		it( 'does work inside blockQuote on heading', () => {
+			setModelData( doc, '<blockQuote><heading1>Foo[]</heading1></blockQuote>' );
+
+			editor.execute( 'alignCenter' );
+
+			expect( getModelData( doc ) ).to.equal( '<blockQuote><heading1 alignment="center">Foo[]</heading1></blockQuote>' );
+		} );
+
+		it( 'does work inside blockQuote on listItem', () => {
+			setModelData( doc, '<blockQuote><listItem indent="0" type="numbered">Foo[]</listItem></blockQuote>' );
+
+			editor.execute( 'alignCenter' );
+
+			expect( getModelData( doc ) ).to.equal(
+				'<blockQuote><listItem alignment="center" indent="0" type="numbered">Foo[]</listItem></blockQuote>'
+			);
+		} );
+	} );
+} );

+ 12 - 0
packages/ckeditor5-alignment/tests/manual/alignment.html

@@ -0,0 +1,12 @@
+<div id="editor">
+	<p><strong>This should be aligned to the left</strong>This is a test for text alignment feature.</p>
+	<figure class="image">
+		<img src="sample.jpg" alt="CKEditor logo" />
+		<figcaption>An image</figcaption>
+	</figure>
+	<p style="text-align:right"><strong>This should be aligned to the right</strong>. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
+
+	<p style="text-align:center"><strong>This should be centered</strong>. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
+
+	<p style="text-align:justify"><strong>This should be justified</strong>. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
+</div>

+ 25 - 0
packages/ckeditor5-alignment/tests/manual/alignment.js

@@ -0,0 +1,25 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import Alignment from '../../src/alignment';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, Alignment ],
+		toolbar: [
+			'headings', 'bold', 'italic', 'link', 'alignLeft', 'alignRight', 'alignCenter', 'alignJustify', 'bulletedList', 'numberedList',
+			'blockQuote', 'undo', 'redo'
+		]
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 33 - 0
packages/ckeditor5-alignment/tests/manual/alignment.md

@@ -0,0 +1,33 @@
+### Loading
+
+1. The data should be loaded with:
+  * first paragraph with default (left) alignment,
+  * second paragraph aligned to right,
+  * third paragraph should be centered,
+  * fourth paragraph should be justified,
+  * an image with caption between first and second paragraphs.
+2. Toolbar should have four buttons for alignment control: left, right, center and justify.
+
+### Testing
+
+You should be able to:
+1. Change alignment on any paragraph.
+2. Change alignment on any selection of paragraphs.
+
+You should not be able to change alignment of figcaption by:
+1. Placing cursor inside caption.
+2. Selecting image with other paragraphs.
+
+You should be able to create blocks that can be aligned:
+1. paragraphs
+2. headings
+3. lists
+
+Alignment UI should:
+1. highlight "left" button when selection is inside left aligned block (default one)
+2. highlight "right" button when selection is inside right aligned block
+3. highlight "center" button when selection is inside centered block
+4. highlight "justify" button when selection is inside justified block
+5. be disabled if in figcaption
+6. only one button should be active at once
+

+ 12 - 0
packages/ckeditor5-alignment/tests/manual/alignmentconfig.html

@@ -0,0 +1,12 @@
+<div id="editor">
+	<p style="text-align:left"><strong>This paragraph has text-align:left style</strong></p>
+
+	<p style="text-align:right"><strong>This paragraph has text-align:right style</strong></p>
+
+	<p style="text-align:center"><strong>This paragraph has text-align:center style</strong></p>
+
+	<p style="text-align:justify"><strong>This paragraph has text-align:justify style</strong>. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+		eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
+		ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+		pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
+</div>

+ 26 - 0
packages/ckeditor5-alignment/tests/manual/alignmentconfig.js

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import Alignment from '../../src/alignment';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, Alignment ],
+		toolbar: [
+			'headings', 'bold', 'italic', 'link', 'alignCenter', 'alignJustify', 'bulletedList', 'numberedList',
+			'blockQuote', 'undo', 'redo'
+		],
+		alignment: { styles: [ 'center', 'justify' ] }
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 19 - 0
packages/ckeditor5-alignment/tests/manual/alignmentconfig.md

@@ -0,0 +1,19 @@
+### Loading
+
+1. The data should be loaded with:
+  * four paragraphs each with different text-alignemnt set,
+  * two first paragraphs should be aligned to left,
+  * third paragraph should be centered,
+  * fourth paragraph should be justified,
+  * an image with caption between first and second paragraphs.
+2. Toolbar should have only two buttons for alignment control: center and justify.
+
+### Testing
+
+"Center" alignment button:
+1. should be highlighted only in third paragraph,
+2. should enable/disable center alignment on any paragraph.
+
+"Justify" alignment button should be:
+1. highlighted only in third paragraph,
+2. should enable/disable justify alignment on any paragraph.

BIN
packages/ckeditor5-alignment/tests/manual/sample.jpg


+ 1 - 0
packages/ckeditor5-alignment/theme/icons/align-center.svg

@@ -0,0 +1 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M16 10v1H4v-1h12zm-1 6v1H5v-1h10zm3-3v1H2v-1h16zm-2-9v1H4V4h12zm2 3v1H2V7h16z" fill="#444" fill-rule="evenodd"/></svg>

+ 1 - 0
packages/ckeditor5-alignment/theme/icons/align-justify.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path d="M18 10v1H2v-1zm-3 6v1H2v-1zm3-3v1H2v-1zm0-9v1H2V4zm0 3v1H2V7z" fill="#444" fill-rule="evenodd"/></svg>

+ 1 - 0
packages/ckeditor5-alignment/theme/icons/align-left.svg

@@ -0,0 +1 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M3 10v1h12v-1H3zm0 6v1h10v-1H3zm0-3v1h15v-1H3zm0-9v1h12V4H3zm0 3v1h15V7H3z" fill="#444" fill-rule="evenodd"/></svg>

+ 1 - 0
packages/ckeditor5-alignment/theme/icons/align-right.svg

@@ -0,0 +1 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17 10v1H5v-1h12zm0 6v1H7v-1h10zm0-3v1H2v-1h15zm0-9v1H5V4h12zm0 3v1H2V7h15z" fill="#444" fill-rule="evenodd"/></svg>