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

Merge pull request #1 from ckeditor/t/ckeditor5/1636

Feature: Implemented `Indent` and `IndentBlock` features. Closes #3. Closes #5.
Piotrek Koszuliński 6 лет назад
Родитель
Сommit
6eb615512d
34 измененных файлов с 2018 добавлено и 47 удалено
  1. 1 1
      packages/ckeditor5-indent/LICENSE.md
  2. 8 8
      packages/ckeditor5-indent/README.md
  3. 0 0
      packages/ckeditor5-indent/docs/_snippets/features/build-indent-source.html
  4. 16 0
      packages/ckeditor5-indent/docs/_snippets/features/build-indent-source.js
  5. 37 0
      packages/ckeditor5-indent/docs/_snippets/features/custom-indent-block-classes.html
  6. 43 0
      packages/ckeditor5-indent/docs/_snippets/features/custom-indent-block-classes.js
  7. 21 0
      packages/ckeditor5-indent/docs/_snippets/features/indent.html
  8. 34 0
      packages/ckeditor5-indent/docs/_snippets/features/indent.js
  9. 0 34
      packages/ckeditor5-indent/docs/api/indent-block.md
  10. 34 0
      packages/ckeditor5-indent/docs/api/indent.md
  11. 178 0
      packages/ckeditor5-indent/docs/features/indent.md
  12. 8 4
      packages/ckeditor5-indent/package.json
  13. 51 0
      packages/ckeditor5-indent/src/indent.js
  14. 268 0
      packages/ckeditor5-indent/src/indentblock.js
  15. 113 0
      packages/ckeditor5-indent/src/indentblockcommand.js
  16. 61 0
      packages/ckeditor5-indent/src/indentcommandbehavior/indentusingclasses.js
  17. 72 0
      packages/ckeditor5-indent/src/indentcommandbehavior/indentusingoffset.js
  18. 40 0
      packages/ckeditor5-indent/src/indentediting.js
  19. 72 0
      packages/ckeditor5-indent/src/indentui.js
  20. 50 0
      packages/ckeditor5-indent/tests/indent.js
  21. 82 0
      packages/ckeditor5-indent/tests/indentblock-integration.js
  22. 233 0
      packages/ckeditor5-indent/tests/indentblock.js
  23. 255 0
      packages/ckeditor5-indent/tests/indentblockcommand.js
  24. 50 0
      packages/ckeditor5-indent/tests/indentediting.js
  25. 82 0
      packages/ckeditor5-indent/tests/indentui.js
  26. 49 0
      packages/ckeditor5-indent/tests/manual/indent-block-classes.html
  27. 55 0
      packages/ckeditor5-indent/tests/manual/indent-block-classes.js
  28. 9 0
      packages/ckeditor5-indent/tests/manual/indent-block-classes.md
  29. 35 0
      packages/ckeditor5-indent/tests/manual/indent-block.html
  30. 48 0
      packages/ckeditor5-indent/tests/manual/indent-block.js
  31. 11 0
      packages/ckeditor5-indent/tests/manual/indent-block.md
  32. BIN
      packages/ckeditor5-indent/tests/manual/sample.jpg
  33. 1 0
      packages/ckeditor5-indent/theme/icons/indent.svg
  34. 1 0
      packages/ckeditor5-indent/theme/icons/outdent.svg

+ 1 - 1
packages/ckeditor5-indent/LICENSE.md

@@ -1,7 +1,7 @@
 Software License Agreement
 ==========================
 
-**CKEditor 5 Indent Block Feature** – https://github.com/ckeditor/ckeditor5-indent-block <br>
+**CKEditor 5 Indent Block Feature** – https://github.com/ckeditor/ckeditor5-indent <br>
 Copyright (c) 2003-2019, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
 
 Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).

+ 8 - 8
packages/ckeditor5-indent/README.md

@@ -1,20 +1,20 @@
-CKEditor 5 indent block feature
-===========================
+CKEditor 5 block indentation feature
+====================================
 
 [![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-indent-block.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-indent-block)
-[![Build Status](https://travis-ci.org/ckeditor/ckeditor5-indent-block.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-indent-block)
+[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-indent.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-indent)
+[![Build Status](https://travis-ci.org/ckeditor/ckeditor5-indent.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-indent)
 [![BrowserStack Status](https://automate.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://automate.browserstack.com/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
-[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-indent-block/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-indent-block?branch=master)
+[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-indent/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-indent?branch=master)
 <br>
-[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-indent-block/status.svg)](https://david-dm.org/ckeditor/ckeditor5-indent-block)
-[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-indent-block/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-indent-block?type=dev)
+[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-indent/status.svg)](https://david-dm.org/ckeditor/ckeditor5-indent)
+[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-indent/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-indent?type=dev)
 
 This package implements block indentation support for CKEditor 5.
 
 ## Documentation
 
-See the [`@ckeditor/ckeditor5-indent-block` package](https://ckeditor.com/docs/ckeditor5/latest/api/indent-block.html) page in [CKEditor 5 documentation](https://ckeditor.com/docs/ckeditor5/latest/).
+See the [`@ckeditor/ckeditor5-indent` package](https://ckeditor.com/docs/ckeditor5/latest/api/indent.html) page in [CKEditor 5 documentation](https://ckeditor.com/docs/ckeditor5/latest/).
 
 ## License
 

+ 0 - 0
packages/ckeditor5-indent/docs/_snippets/features/build-indent-source.html


+ 16 - 0
packages/ckeditor5-indent/docs/_snippets/features/build-indent-source.js

@@ -0,0 +1,16 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+
+import Indent from '@ckeditor/ckeditor5-indent/src/indent';
+import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock';
+
+ClassicEditor.builtinPlugins.push( Indent );
+ClassicEditor.builtinPlugins.push( IndentBlock );
+
+window.ClassicEditor = ClassicEditor;

+ 37 - 0
packages/ckeditor5-indent/docs/_snippets/features/custom-indent-block-classes.html

@@ -0,0 +1,37 @@
+<style>
+	.custom-block-indent-a {
+		margin-left: 10%;
+	}
+
+	.custom-block-indent-b {
+		margin-left: 20%;
+	}
+
+	.custom-block-indent-c {
+		margin-left: 30%;
+	}
+
+	.custom-block-indent-d {
+		margin-left: 40%;
+	}
+
+	.custom-block-indent-e {
+		margin-left: 50%;
+	}
+</style>
+
+<div id="snippet-indent-block-classes">
+	<h2 class="custom-block-indent-b">
+		This heading has second indentation class set.
+	</h2>
+
+	<p>
+		This paragraph has no indentation.<br>
+		Cookie chupa chups marshmallow jelly beans. Bear claw chocolate bar chupa chups oat cake caramels chupa chups oat cake marzipan. Candy chupa chups chocolate. Cake soufflé candy canes fruitcake jelly-o oat cake. Macaroon tootsie roll candy marzipan sesame snaps cupcake wafer gingerbread. Oat cake cake cake sesame snaps pastry halvah.
+	</p>
+
+	<p class="custom-block-indent-e">
+		This paragraph has fifth (last) indentation class set.<br>
+		Cookie chupa chups marshmallow jelly beans. Bear claw chocolate bar chupa chups oat cake caramels chupa chups oat cake marzipan. Candy chupa chups chocolate. Cake soufflé candy canes fruitcake jelly-o oat cake. Macaroon tootsie roll candy marzipan sesame snaps cupcake wafer gingerbread. Oat cake cake cake sesame snaps pastry halvah.
+	</p>
+</div>

+ 43 - 0
packages/ckeditor5-indent/docs/_snippets/features/custom-indent-block-classes.js

@@ -0,0 +1,43 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-indent-block-classes' ), {
+		cloudServices: CS_CONFIG,
+		toolbar: {
+			items: [
+				'heading',
+				'|',
+				'outdent',
+				'indent',
+				'|',
+				'bulletedList',
+				'numberedList',
+				'|',
+				'undo',
+				'redo'
+			],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		indentBlock: {
+			classes: [
+				'custom-block-indent-a',
+				'custom-block-indent-b',
+				'custom-block-indent-c',
+				'custom-block-indent-d',
+				'custom-block-indent-e'
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 21 - 0
packages/ckeditor5-indent/docs/_snippets/features/indent.html

@@ -0,0 +1,21 @@
+<div id="snippet-indent">
+	<p>
+		This paragraph has no indentation.<br>
+		Cookie chupa chups marshmallow jelly beans. Bear claw chocolate bar chupa chups oat cake caramels chupa chups oat cake marzipan. Candy chupa chups chocolate. Cake soufflé candy canes fruitcake jelly-o oat cake. Macaroon tootsie roll candy marzipan sesame snaps cupcake wafer gingerbread. Oat cake cake cake sesame snaps pastry halvah.
+	</p>
+
+	<p style="margin-left:100px;">
+		This paragraph has indentation of 100px (second indentation step).<br>
+		Cookie chupa chups marshmallow jelly beans. Bear claw chocolate bar chupa chups oat cake caramels chupa chups oat cake marzipan. Candy chupa chups chocolate. Cake soufflé candy canes fruitcake jelly-o oat cake. Macaroon tootsie roll candy marzipan sesame snaps cupcake wafer gingerbread. Oat cake cake cake sesame snaps pastry halvah.
+	</p>
+
+	<p style="margin-left:75px;">
+		This paragraph has indentation of 75px (between indentation step 1 and 2).<br>
+		Cookie chupa chups marshmallow jelly beans. Bear claw chocolate bar chupa chups oat cake caramels chupa chups oat cake marzipan. Candy chupa chups chocolate. Cake soufflé candy canes fruitcake jelly-o oat cake. Macaroon tootsie roll candy marzipan sesame snaps cupcake wafer gingerbread. Oat cake cake cake sesame snaps pastry halvah.
+	</p>
+
+	<p style="margin-left:50px;">
+		This paragraph has indentation of 50px (second indentation step).<br>
+		Cookie chupa chups marshmallow jelly beans. Bear claw chocolate bar chupa chups oat cake caramels chupa chups oat cake marzipan. Candy chupa chups chocolate. Cake soufflé candy canes fruitcake jelly-o oat cake. Macaroon tootsie roll candy marzipan sesame snaps cupcake wafer gingerbread. Oat cake cake cake sesame snaps pastry halvah.
+	</p>
+</div>

+ 34 - 0
packages/ckeditor5-indent/docs/_snippets/features/indent.js

@@ -0,0 +1,34 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-indent' ), {
+		cloudServices: CS_CONFIG,
+		toolbar: {
+			items: [
+				'heading',
+				'|',
+				'outdent',
+				'indent',
+				'|',
+				'bulletedList',
+				'numberedList',
+				'|',
+				'undo',
+				'redo'
+			],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 0 - 34
packages/ckeditor5-indent/docs/api/indent-block.md

@@ -1,34 +0,0 @@
----
-category: api-reference
----
-
-# CKEditor 5 indent block feature
-
-[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-indent-block.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-indent-block)
-
-This package implements the indent block feature for CKEditor 5.
-
-## Demo
-
-Check out the {@link features/indent-block#demo demo in the indent block feature} guide.
-
-## Documentation
-
-See the {@link module:indent-block/indentblock~IndentBlock} plugin documentation.
-
-## Installation
-
-```bash
-npm install --save @ckeditor/ckeditor5-indent-block
-```
-
-## Contribute
-
-The source code of this package is available on GitHub in https://github.com/ckeditor/ckeditor5-indent-block.
-
-## External links
-
-* [`@ckeditor/ckeditor5-indent-block` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-indent-block)
-* [`ckeditor/ckeditor5-indent-block` on GitHub](https://github.com/ckeditor/ckeditor5-indent-block)
-* [Issue tracker](https://github.com/ckeditor/ckeditor5-indent-block/issues)
-* [Changelog](https://github.com/ckeditor/ckeditor5-indent-block/blob/master/CHANGELOG.md)

+ 34 - 0
packages/ckeditor5-indent/docs/api/indent.md

@@ -0,0 +1,34 @@
+---
+category: api-reference
+---
+
+# CKEditor 5 block indent feature
+
+[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-indent.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-indent)
+
+This package implements the indent feature for CKEditor 5.
+
+## Demo
+
+Check out the {@link features/indent#demo demo in the block indentation feature} guide.
+
+## Documentation
+
+See the {@link features/indent Block indentation} guide and the {@link module:indent/indent~Indent} plugin documentation.
+
+## Installation
+
+```bash
+npm install --save @ckeditor/ckeditor5-indent
+```
+
+## Contribute
+
+The source code of this package is available on GitHub in https://github.com/ckeditor/ckeditor5-indent.
+
+## External links
+
+* [`@ckeditor/ckeditor5-indent` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-indent)
+* [`ckeditor/ckeditor5-indent` on GitHub](https://github.com/ckeditor/ckeditor5-indent)
+* [Issue tracker](https://github.com/ckeditor/ckeditor5-indent/issues)
+* [Changelog](https://github.com/ckeditor/ckeditor5-indent/blob/master/CHANGELOG.md)

+ 178 - 0
packages/ckeditor5-indent/docs/features/indent.md

@@ -0,0 +1,178 @@
+---
+title: Block indentation
+category: features
+---
+
+{@snippet features/build-indent-source}
+
+## Demo
+
+The indentation feature allows to set indentation of text blocks like paragraphs or headings and lists.
+
+{@snippet features/indent}
+
+## Configuring the block indentation feature
+
+### Using offset and unit
+
+By default, the block indentation feature increases or decreases the current indentation by the given offset, using the given unit.
+
+The editor used in the {@link features/indent#demo demo} section above uses the default configuration, which defines a `40px` indentation step.
+
+You can change that value to, for example, `1em`:
+
+```js
+import Indent from '@ckeditor/ckeditor5-indent/src/indent';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Indent, ... ],
+		toolbar: {
+			items: [ 'heading', '|', 'outdent', 'indent', '|', 'bulletedList', 'numberedList', '|', 'undo', 'redo' ]
+		},
+		indentBlock: {
+			offset: 1,
+			unit: 'em'
+		}
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+### Using CSS classes
+
+Alternatively, the block indentation feature can be configured to set indentation by applying one of defined CSS classes:
+
+```js
+import Indent from '@ckeditor/ckeditor5-indent/src/indent';
+import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Indent, IndentBlock, ... ],
+		toolbar: {
+			items: [ 'heading', '|', 'outdent', 'indent', '|', 'bulletedList', 'numberedList', '|', 'undo', 'redo' ]
+		},
+		indentBlock: {
+			classes: [
+				'custom-block-indent-a', // First step - smallest indentation.
+				'custom-block-indent-b',
+				'custom-block-indent-c',
+				'custom-block-indent-d',
+				'custom-block-indent-e'  // Last step - biggest indentation.
+			]
+		}
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+The editor will restrict indentation levels to a set of provided classes. The biggest indentation is the class that has last index in the array.
+
+In the demo below the CSS classes are defined as follows:
+
+```css
+	.custom-block-indent-a {
+		margin-left: 10%;
+	}
+
+	.custom-block-indent-b {
+		margin-left: 20%;
+	}
+
+	.custom-block-indent-c {
+		margin-left: 30%;
+	}
+
+	.custom-block-indent-d {
+		margin-left: 40%;
+	}
+
+	.custom-block-indent-e {
+		margin-left: 50%;
+	}
+```
+
+{@snippet features/custom-indent-block-classes}
+
+## Indenting lists
+
+The same set of buttons (`outdent`, `indent`), besides controlling block indentation, allows indenting list items (nesting them). This is completely transparent to the user.
+
+From the code perspective, the buttons are implemented by the {@link module:indent/indent~Indent} plugin, but neither those buttons nor respective commands implement any functionality by default.
+
+The target behavior comes in two other plugins:
+
+* {@link module:indent/indentblock~IndentBlock} &mdash; The indent block feature controls the indentation of elements such as paragraphs and headings.
+* {@link module:list/list~List} &mdash; The list feature implements the indentation (nesting) of lists.
+
+This means, that if you want to allow indenting lists only, you can do that by load only the `Indent` and `List` plugins. If you want the full behavior, you need to load all 3 plugins.
+
+## Installation
+
+To add this feature to your editor, install the [`@ckeditor/ckeditor5-indent`](https://www.npmjs.com/package/@ckeditor/ckeditor5-indent) package:
+
+```bash
+npm install --save @ckeditor/ckeditor5-indent
+```
+
+Then add it to your plugin list and the toolbar configuration:
+
+```js
+import Indent from '@ckeditor/ckeditor5-indent/src/indent';
+import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Indent, IndentBlock, ... ],
+		toolbar: [ 'outdent', 'indent', ... ]
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+<info-box info>
+	Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
+</info-box>
+
+## Common API
+
+The {@link module:indent/indent~Indent} plugin registers the following components:
+
+* The `'indent'` command.
+
+	Note, this command does not implement any behavior itself. It executes either `indentBlock` (described below) or `indentList`, depending on which of these commands is enabled.
+
+	Read more in the [Indenting lists](#indenting-lists) section above.
+
+* The `'outdent'` command.
+
+	Note, this command does not implement any behavior itself. It executes either `outdentBlock` (described below) or `outdentList`, depending on which of these commands is enabled.
+
+	Read more in the [Indenting lists](#indenting-lists) section above.
+
+The {@link module:indent/indentblock~IndentBlock} plugin registers the following components:
+
+* The {@link module:indent/indentblockcommand~IndentBlockCommand `'indentBlock'`} command.
+
+	You can increase the indentation of the block in which the selection is set by:
+
+	```js
+	editor.execute( 'indentBlock' );
+	```
+
+* The {@link module:indent/indentblockcommand~IndentBlockCommand `'outdentBlock'`} command.
+
+	You can decrease the indentation of the block in which the selection is set by:
+
+	```js
+	editor.execute( 'outdentBlock' );
+	```
+
+<info-box>
+	We recommend using the official {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} for development and debugging. It will give you tons of useful information about the state of the editor such as internal data structures, selection, commands, and many more.
+</info-box>
+
+## Contribute
+
+The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-font.

+ 8 - 4
packages/ckeditor5-indent/package.json

@@ -1,7 +1,7 @@
 {
-  "name": "@ckeditor/ckeditor5-indent-block",
+  "name": "@ckeditor/ckeditor5-indent",
   "version": "0.0.1",
-  "description": "Block indent feature for CKEditor 5.",
+  "description": "Block indentation feature for CKEditor 5.",
   "keywords": [
     "ckeditor",
     "ckeditor5",
@@ -10,8 +10,12 @@
     "ckeditor5-plugin"
   ],
   "dependencies": {
+    "@ckeditor/ckeditor5-core": "^12.1.1",
+    "@ckeditor/ckeditor5-utils": "^12.1.1"
   },
   "devDependencies": {
+    "@ckeditor/ckeditor5-ui": "^13.0.0",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.1",
     "eslint": "^5.15.3",
     "eslint-config-ckeditor5": "^1.0.11",
     "husky": "^1.3.1",
@@ -24,10 +28,10 @@
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
   "homepage": "https://ckeditor.com/ckeditor-5",
-  "bugs": "https://github.com/ckeditor/ckeditor5-indent-block/issues",
+  "bugs": "https://github.com/ckeditor/ckeditor5-indent/issues",
   "repository": {
     "type": "git",
-    "url": "https://github.com/ckeditor/ckeditor5-indent-block.git"
+    "url": "https://github.com/ckeditor/ckeditor5-indent.git"
   },
   "files": [
     "lang",

+ 51 - 0
packages/ckeditor5-indent/src/indent.js

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module indent/indent
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import IndentEditing from './indentediting';
+import IndentUI from './indentui';
+
+/**
+ * The indent feature.
+ *
+ * This plugin acts as a single entry point plugin for other features that implement indenting of elements like lists or paragraphs.
+ *
+ * The compatible features are:
+ *
+ * - the {@link module:list/list~List} or {@link module:list/listediting~ListEditing} feature for list indentation
+ * * the {@link module:list/list~List} or {@link module:list/listediting~ListEditing} feature for list indentation
+ *
+ * This is a "glue" plugin which loads the following plugins:
+ *
+ * * The {@link module:indent/indentediting~IndentEditing indent editing feature} and
+ * * The {@link module:indent/indentui~IndentUI indent UI feature}.
+ *
+ * The dependent plugins register the `'indent'` and `'outdent'` commands and it introduce the `'indent'` and `'outdent'` buttons
+ * which allow to increase or decrease text indentation of supported elements.
+ *
+ * **Note**: In order the commands and buttons to work at least one of compatible features is required.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class Indent extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'Indent';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ IndentEditing, IndentUI ];
+	}
+}

+ 268 - 0
packages/ckeditor5-indent/src/indentblock.js

@@ -0,0 +1,268 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module indent/indentblock
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import IndentBlockCommand from './indentblockcommand';
+import IndentUsingOffset from './indentcommandbehavior/indentusingoffset';
+import IndentUsingClasses from './indentcommandbehavior/indentusingclasses';
+
+/**
+ * The block indentation feature.
+ *
+ * It registers the `'indentBlock'` and `'outdentBlock'` commands.
+ *
+ * If the plugin {@link module:indent/indent~Indent} is defined it also attaches the `'indentBlock'` and `'outdentBlock'` commands to
+ * `'indent'` and `'outdent'` commands.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class IndentBlock extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( 'indentBlock', {
+			offset: 40,
+			unit: 'px'
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'IndentBlock';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const schema = editor.model.schema;
+		const conversion = editor.conversion;
+		const configuration = editor.config.get( 'indentBlock' );
+
+		// Enable block indentation by default in paragraph and default headings.
+		const knownElements = [ 'paragraph', 'heading1', 'heading2', 'heading3', 'heading4', 'heading5', 'heading6' ];
+
+		knownElements.forEach( elementName => {
+			if ( schema.isRegistered( elementName ) ) {
+				schema.extend( elementName, { allowAttributes: 'blockIndent' } );
+			}
+		} );
+
+		const useOffsetConfig = !configuration.classes || !configuration.classes.length;
+
+		const indentConfig = Object.assign( { direction: 'forward' }, configuration );
+		const outdentConfig = Object.assign( { direction: 'backward' }, configuration );
+
+		if ( useOffsetConfig ) {
+			this._setupConversionUsingOffset( conversion );
+			editor.commands.add( 'indentBlock', new IndentBlockCommand( editor, new IndentUsingOffset( indentConfig ) ) );
+			editor.commands.add( 'outdentBlock', new IndentBlockCommand( editor, new IndentUsingOffset( outdentConfig ) ) );
+		} else {
+			this._setupConversionUsingClasses( configuration.classes );
+			editor.commands.add( 'indentBlock', new IndentBlockCommand( editor, new IndentUsingClasses( indentConfig ) ) );
+			editor.commands.add( 'outdentBlock', new IndentBlockCommand( editor, new IndentUsingClasses( outdentConfig ) ) );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	afterInit() {
+		const editor = this.editor;
+		const indentCommand = editor.commands.get( 'indent' );
+		const outdentCommand = editor.commands.get( 'outdent' );
+
+		indentCommand.registerChildCommand( editor.commands.get( 'indentBlock' ) );
+		outdentCommand.registerChildCommand( editor.commands.get( 'outdentBlock' ) );
+	}
+
+	/**
+	 * Setups conversion for using offset indents.
+	 *
+	 * @private
+	 */
+	_setupConversionUsingOffset() {
+		const conversion = this.editor.conversion;
+
+		conversion.for( 'upcast' ).attributeToAttribute( {
+			view: {
+				styles: {
+					'margin-left': /[\s\S]+/
+				}
+			},
+			model: {
+				key: 'blockIndent',
+				value: viewElement => viewElement.getStyle( 'margin-left' )
+			}
+		} );
+
+		// The margin shorthand should also work.
+		conversion.for( 'upcast' ).attributeToAttribute( {
+			view: {
+				styles: {
+					'margin': /[\s\S]+/
+				}
+			},
+			model: {
+				key: 'blockIndent',
+				value: viewElement => normalizeToMarginLeftStyle( viewElement.getStyle( 'margin' ) )
+			}
+		} );
+
+		conversion.for( 'downcast' ).attributeToAttribute( {
+			model: 'blockIndent',
+			view: modelAttributeValue => {
+				return {
+					key: 'style',
+					value: {
+						'margin-left': modelAttributeValue
+					}
+				};
+			}
+		} );
+	}
+
+	/**
+	 * Setups conversion for using classes.
+	 *
+	 * @param {Array.<String>} classes
+	 * @private
+	 */
+	_setupConversionUsingClasses( classes ) {
+		const definition = {
+			model: {
+				key: 'blockIndent',
+				values: []
+			},
+			view: {}
+		};
+
+		for ( const className of classes ) {
+			definition.model.values.push( className );
+			definition.view[ className ] = {
+				key: 'class',
+				value: [ className ]
+			};
+		}
+
+		this.editor.conversion.attributeToAttribute( definition );
+	}
+}
+
+// Normalizes the margin shorthand value to the value of `margin-left` CSS property.
+//
+// As such it will return:
+// - '1em' for '1em'
+// - '1em' for '2px 1em'
+// - '1em' for '2px 1em 3px'
+// - '1em' for '2px 10px 3px 1em'
+//
+// @param {String} Margin style value.
+// @returns {String} Extracted value of margin-left.
+function normalizeToMarginLeftStyle( marginStyleValue ) {
+	// Splits the margin shorthand, ie margin: 2em 4em.
+	const marginEntries = marginStyleValue.split( ' ' );
+
+	let left;
+
+	// If only one value defined, ie: `margin: 1px`.
+	left = marginEntries[ 0 ];
+
+	// If only two values defined, ie: `margin: 1px 2px`.
+	if ( marginEntries[ 1 ] ) {
+		left = marginEntries[ 1 ];
+	}
+
+	// If four values defined, ie: `margin: 1px 2px 3px 4px`.
+	if ( marginEntries[ 3 ] ) {
+		left = marginEntries[ 3 ];
+	}
+
+	return left;
+}
+
+/**
+ * The configuration of the {@link module:indent/indentblock~IndentBlock block indentation feature}.
+ *
+ * Read more in {@link module:indent/indentblock~IndentBlockConfig}.
+ *
+ * @member {module:indent/indentblock~IndentBlockConfig} module:core/editor/editorconfig~EditorConfig#indentBlock
+ */
+
+/**
+ * The configuration of the block indentation feature.
+ *
+ * If no {@link module:indent/indentblock~IndentBlockConfig#classes} are set the block indentation feature will use
+ * {@link module:indent/indentblock~IndentBlockConfig#offset} and {@link module:indent/indentblock~IndentBlockConfig#unit} to
+ * create indentation steps.
+ *
+ *		ClassicEditor
+ *			.create( editorElement, {
+ * 				indentBlock: {
+ *					offset: 2,
+ *					unit: 'em'
+ * 				}
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * Alternatively the block indentation feature may set one of defined {@link module:indent/indentblock~IndentBlockConfig#classes} as
+ * indentation steps:
+ *
+ *		ClassicEditor
+ *			.create( editorElement, {
+ * 				indentBlock: {
+ *					classes: [
+ *						'indent-a', // First step - smallest indentation.
+ *						'indent-b',
+ *						'indent-c',
+ *						'indent-d',
+ *						'indent-e' // Last step - biggest indentation.
+ *					]
+ * 				}
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * In the above example only 5 indentation steps will be available.
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
+ *
+ * @interface IndentBlockConfig
+ */
+
+/**
+ * The size in indentation {@link module:indent/indentblock~IndentBlockConfig#unit units} of each indentation step.
+ *
+ * @default 40
+ * @member {Number} module:indent/indentblock~IndentBlockConfig#offset
+ */
+
+/**
+ * The unit used for indentation {@link module:indent/indentblock~IndentBlockConfig#offset}.
+ *
+ * @default 'px'
+ * @member {String} module:indent/indentblock~IndentBlockConfig#unit
+ */
+
+/**
+ * An optional list of classes to use for indenting the contents. If not set or set to empty array, no classes will be used and instead
+ * the {@link module:indent/indentblock~IndentBlockConfig#unit `indentBlock.unit`} and
+ * {@link module:indent/indentblock~IndentBlockConfig#offset `indentBlock.offset`} properties will be used.
+ *
+ * @default undefined
+ * @member {Array.<String>|undefined} module:indent/indentblock~IndentBlockConfig#classes
+ */

+ 113 - 0
packages/ckeditor5-indent/src/indentblockcommand.js

@@ -0,0 +1,113 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module indent/indentblockcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import first from '@ckeditor/ckeditor5-utils/src/first';
+
+/**
+ * The indent block command.
+ *
+ * The command is registered by the {@link module:indent/indentblock~IndentBlock} as `'indentBlock'` - for indenting blocks and
+ * `'outdentBlock'` - for outdenting blocks.
+ *
+ * To increase block indentation at current selection execute the command:
+ *
+ *		editor.execute( 'indentBlock' );
+ *
+ * To decrease block indentation at current selection execute the command:
+ *
+ *		editor.execute( 'outdentBlock' );
+ *
+ * @extends module:core/command~Command
+ */
+export default class IndentBlockCommand extends Command {
+	/**
+	 * Creates an instance of the command.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor instance.
+	 * @param {module:indent/indentblockcommand~IndentBehavior} indentBehavior
+	 */
+	constructor( editor, indentBehavior ) {
+		super( editor );
+
+		/**
+		 * Command's indentation behavior.
+		 *
+		 * @type {module:indent/indentblockcommand~IndentBehavior}
+		 * @private
+		 */
+		this._indentBehavior = indentBehavior;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		// Check whether any of position's ancestor is a list item.
+		const editor = this.editor;
+		const model = editor.model;
+
+		const block = first( model.document.selection.getSelectedBlocks() );
+
+		// If selection is not in a list item, the command is disabled.
+		if ( !block || !model.schema.checkAttribute( block, 'blockIndent' ) ) {
+			this.isEnabled = false;
+
+			return;
+		}
+
+		this.isEnabled = this._indentBehavior.checkEnabled( block.getAttribute( 'blockIndent' ) );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	execute() {
+		const model = this.editor.model;
+		const doc = model.document;
+
+		const itemsToChange = Array.from( doc.selection.getSelectedBlocks() );
+
+		model.change( writer => {
+			for ( const item of itemsToChange ) {
+				const currentIndent = item.getAttribute( 'blockIndent' );
+
+				const nextIndent = this._indentBehavior.getNextIndent( currentIndent );
+
+				if ( nextIndent ) {
+					writer.setAttribute( 'blockIndent', nextIndent, item );
+				} else {
+					writer.removeAttribute( 'blockIndent', item );
+				}
+			}
+		} );
+	}
+}
+
+/**
+ * Provides indentation behavior to {@link module:indent/indentblockcommand~IndentBlockCommand}.
+ *
+ * @interface module:indent/indentblockcommand~IndentBehavior
+ */
+
+/**
+ * Performs check if command should be enabled.
+ *
+ * @method #checkEnabled
+ * @param {String} indentAttributeValue Current indent attribute value.
+ * @returns {Boolean}
+ */
+
+/**
+ * Returns new indent attribute value based on current indent.
+ *
+ * @method #getNextIndent
+ * @param {String} indentAttributeValue Current indent attribute value.
+ * @returns {String|undefined}
+ */

+ 61 - 0
packages/ckeditor5-indent/src/indentcommandbehavior/indentusingclasses.js

@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module indent/indentcommandbehavior/indentusingclasses
+ */
+
+/**
+ * The block indentation behavior that uses classes to set indentation.
+ *
+ * @implements module:indent/indentblockcommand~IndentBehavior
+ */
+export default class IndentUsingClasses {
+	/**
+	 * Creates an instance of the command.
+	 *
+	 * @param {Object} config
+	 * @param {String} config.direction The direction of indentation.
+	 * @param {Array.<String>} config.classes List of classes used for indentation.
+	 */
+	constructor( config ) {
+		/**
+		 * The direction of indentation.
+		 *
+		 * @type {Boolean}
+		 */
+		this.isForward = config.direction === 'forward';
+
+		/**
+		 * List of classes used for indentation.
+		 *
+		 * @type {Array.<String>}
+		 */
+		this.classes = config.classes;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	checkEnabled( indentAttributeValue ) {
+		const currentIndex = this.classes.indexOf( indentAttributeValue );
+
+		if ( this.isForward ) {
+			return currentIndex < this.classes.length - 1;
+		} else {
+			return currentIndex >= 0;
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	getNextIndent( indentAttributeValue ) {
+		const currentIndex = this.classes.indexOf( indentAttributeValue );
+		const indexStep = this.isForward ? 1 : -1;
+
+		return this.classes[ currentIndex + indexStep ];
+	}
+}

+ 72 - 0
packages/ckeditor5-indent/src/indentcommandbehavior/indentusingoffset.js

@@ -0,0 +1,72 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module indent/indentcommandbehavior/indentusingoffset
+ */
+
+/**
+ * @implements module:indent/indentblockcommand~IndentBehavior
+ */
+export default class IndentUsingOffset {
+	/**
+	 * Creates an instance of the behavior.
+	 *
+	 * @param {Object} config
+	 * @param {String} config.direction The direction of indentation.
+	 * @param {Number} config.offset Offset of next indentation step.
+	 * @param {String} config.unit Indentation unit.
+	 */
+	constructor( config ) {
+		/**
+		 * The direction of indentation.
+		 *
+		 * @type {Boolean}
+		 */
+		this.isForward = config.direction === 'forward';
+
+		/**
+		 * Offset of next indentation step.
+		 *
+		 * @type {Number}
+		 */
+		this.offset = config.offset;
+
+		/**
+		 * Indentation unit.
+		 *
+		 * @type {String}
+		 */
+		this.unit = config.unit;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	checkEnabled( indentAttributeValue ) {
+		const currentOffset = parseFloat( indentAttributeValue || 0 );
+
+		// The command is always enabled for forward indentation.
+		return this.isForward || currentOffset > 0;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	getNextIndent( indentAttributeValue ) {
+		const currentOffset = parseFloat( indentAttributeValue || 0 );
+		const isSameUnit = !indentAttributeValue || indentAttributeValue.endsWith( this.unit );
+
+		if ( !isSameUnit ) {
+			return this.isForward ? this.offset + this.unit : undefined;
+		}
+
+		const nextOffset = this.isForward ? this.offset : -this.offset;
+
+		const offsetToSet = currentOffset + nextOffset;
+
+		return offsetToSet > 0 ? offsetToSet + this.unit : undefined;
+	}
+}

+ 40 - 0
packages/ckeditor5-indent/src/indentediting.js

@@ -0,0 +1,40 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module indent/indentediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import MultiCommand from '@ckeditor/ckeditor5-core/src/multicommand';
+
+/**
+ * The indent editing feature.
+ *
+ * This plugin registers the `'indent'` and `'outdent'` commands.
+ *
+ * **Note**: In order the commands to work at least one of compatible features is required. Read more in
+ * {@link module:indent/indent~Indent indent feature} api docs.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class IndentEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'IndentEditing';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+
+		editor.commands.add( 'indent', new MultiCommand( editor ) );
+		editor.commands.add( 'outdent', new MultiCommand( editor ) );
+	}
+}

+ 72 - 0
packages/ckeditor5-indent/src/indentui.js

@@ -0,0 +1,72 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module indent/indentui
+ */
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import indentIcon from '../theme/icons/indent.svg';
+import outdentIcon from '../theme/icons/outdent.svg';
+
+/**
+ * The indent feature.
+ *
+ * This plugin registers the `'indent'` and `'outdent'` buttons.
+ *
+ * **Note**: In order the commands to work at least one of compatible features is required. Read more in
+ * {@link module:indent/indent~Indent indent feature} api docs.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class IndentUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'IndentUI';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		this._defineButton( 'indent', t( 'Increase indent' ), indentIcon );
+		this._defineButton( 'outdent', t( 'Decrease indent' ), outdentIcon );
+	}
+
+	/**
+	 * Defines an UI button.
+	 *
+	 * @param {String} commandName
+	 * @param {String} label
+	 * @param {String} icon
+	 * @private
+	 */
+	_defineButton( commandName, label, icon ) {
+		const editor = this.editor;
+
+		editor.ui.componentFactory.add( commandName, locale => {
+			const command = editor.commands.get( commandName );
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label,
+				icon,
+				tooltip: true
+			} );
+
+			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
+
+			this.listenTo( view, 'execute', () => editor.execute( commandName ) );
+
+			return view;
+		} );
+	}
+}

+ 50 - 0
packages/ckeditor5-indent/tests/indent.js

@@ -0,0 +1,50 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+import Indent from '../src/indent';
+import IndentEditing from '../src/indentediting';
+import IndentUI from '../src/indentui';
+
+describe( 'Indent', () => {
+	let editor, element;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, { plugins: [ Indent ] } )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		if ( editor ) {
+			return editor.destroy();
+		}
+	} );
+
+	it( 'should be named', () => {
+		expect( Indent.pluginName ).to.equal( 'Indent' );
+	} );
+
+	it( 'should load the IndentUI plugin', () => {
+		expect( editor.plugins.get( IndentUI ) ).to.be.instanceOf( IndentUI );
+	} );
+
+	it( 'should load the IndentEditing plugin', () => {
+		expect( editor.plugins.get( IndentEditing ) ).to.be.instanceOf( IndentEditing );
+	} );
+} );

+ 82 - 0
packages/ckeditor5-indent/tests/indentblock-integration.js

@@ -0,0 +1,82 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
+
+import IndentEditing from '../src/indentediting';
+import IndentBlock from '../src/indentblock';
+
+describe( 'IndentBlock - integration', () => {
+	let editor, model, doc;
+
+	testUtils.createSinonSandbox();
+
+	afterEach( () => {
+		if ( editor ) {
+			return editor.destroy();
+		}
+	} );
+
+	describe( 'with paragraph', () => {
+		beforeEach( () => {
+			return createTestEditor( { indentBlock: { offset: 50, unit: 'px' } } )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					doc = model.document;
+				} );
+		} );
+
+		it( 'should work with paragraph set', () => {
+			editor.setData( '<p style="margin-left:50px">foo</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+			expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( '50px' );
+
+			expect( editor.getData() ).to.equal( '<p style="margin-left:50px;">foo</p>' );
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+				.to.equal( '<p style="margin-left:50px">foo</p>' );
+		} );
+	} );
+
+	describe( 'with heading', () => {
+		beforeEach( () => {
+			return createTestEditor( {
+				plugins: [ Paragraph, HeadingEditing, IndentEditing, IndentBlock ],
+				indentBlock: { offset: 50, unit: 'px' }
+			} ).then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				doc = model.document;
+			} );
+		} );
+
+		it( 'should work with default headings set', () => {
+			editor.setData( '<p style="margin-left:50px">foo</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+			expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( '50px' );
+
+			expect( editor.getData() ).to.equal( '<p style="margin-left:50px;">foo</p>' );
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+				.to.equal( '<p style="margin-left:50px">foo</p>' );
+		} );
+	} );
+
+	function createTestEditor( extraConfig = {} ) {
+		return VirtualTestEditor
+			.create( Object.assign( {
+				plugins: [ Paragraph, IndentEditing, IndentBlock ]
+			}, extraConfig ) );
+	}
+} );

+ 233 - 0
packages/ckeditor5-indent/tests/indentblock.js

@@ -0,0 +1,233 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
+
+import IndentEditing from '../src/indentediting';
+import IndentBlock from '../src/indentblock';
+import IndentBlockCommand from '../src/indentblockcommand';
+
+describe( 'IndentBlock', () => {
+	let editor, model, doc;
+
+	testUtils.createSinonSandbox();
+
+	afterEach( () => {
+		if ( editor ) {
+			return editor.destroy();
+		}
+	} );
+
+	it( 'should be named', () => {
+		expect( IndentBlock.pluginName ).to.equal( 'IndentBlock' );
+	} );
+
+	it( 'should be loaded', () => {
+		return createTestEditor()
+			.then( newEditor => {
+				expect( newEditor.plugins.get( IndentBlock ) ).to.be.instanceOf( IndentBlock );
+			} );
+	} );
+
+	it( 'should set proper schema rules', () => {
+		return createTestEditor()
+			.then( newEditor => {
+				model = newEditor.model;
+
+				expect( model.schema.checkAttribute( [ 'paragraph' ], 'blockIndent' ) ).to.be.true;
+				expect( model.schema.checkAttribute( [ 'heading1' ], 'blockIndent' ) ).to.be.true;
+				expect( model.schema.checkAttribute( [ 'heading2' ], 'blockIndent' ) ).to.be.true;
+				expect( model.schema.checkAttribute( [ 'heading3' ], 'blockIndent' ) ).to.be.true;
+			} );
+	} );
+
+	it( 'should register indent block command', () => {
+		return createTestEditor()
+			.then( newEditor => {
+				const command = newEditor.commands.get( 'indentBlock' );
+
+				expect( command ).to.be.instanceof( IndentBlockCommand );
+			} );
+	} );
+
+	describe( 'config', () => {
+		describe( 'default value', () => {
+			it( 'should be set', () => {
+				return createTestEditor().then( editor => {
+					expect( editor.config.get( 'indentBlock' ) ).to.deep.equal( { offset: 40, unit: 'px' } );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'conversion', () => {
+		describe( 'using offset', () => {
+			beforeEach( () => {
+				return createTestEditor( { indentBlock: { offset: 50, unit: 'px' } } )
+					.then( newEditor => {
+						editor = newEditor;
+						model = editor.model;
+						doc = model.document;
+					} );
+			} );
+
+			it( 'should convert margin-left to indent attribute (known offset)', () => {
+				editor.setData( '<p style="margin-left:50px">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+				expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( '50px' );
+
+				expect( editor.getData() ).to.equal( '<p style="margin-left:50px;">foo</p>' );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+					.to.equal( '<p style="margin-left:50px">foo</p>' );
+			} );
+
+			it( 'should convert margin-left to indent attribute (any offset)', () => {
+				editor.setData( '<p style="margin-left:42em">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+				expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( '42em' );
+
+				expect( editor.getData() ).to.equal( '<p style="margin-left:42em;">foo</p>' );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+					.to.equal( '<p style="margin-left:42em">foo</p>' );
+			} );
+
+			it( 'should convert margin shortcut to indent attribute (one entry)', () => {
+				editor.setData( '<p style="margin:42em">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+				expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( '42em' );
+
+				expect( editor.getData() ).to.equal( '<p style="margin-left:42em;">foo</p>' );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+					.to.equal( '<p style="margin-left:42em">foo</p>' );
+			} );
+
+			it( 'should convert margin shortcut to indent attribute (two entries)', () => {
+				editor.setData( '<p style="margin:24em 42em">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+				expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( '42em' );
+
+				expect( editor.getData() ).to.equal( '<p style="margin-left:42em;">foo</p>' );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+					.to.equal( '<p style="margin-left:42em">foo</p>' );
+			} );
+
+			it( 'should convert margin shortcut to indent attribute (three entries)', () => {
+				editor.setData( '<p style="margin:24em 42em 20em">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+				expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( '42em' );
+
+				expect( editor.getData() ).to.equal( '<p style="margin-left:42em;">foo</p>' );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+					.to.equal( '<p style="margin-left:42em">foo</p>' );
+			} );
+
+			it( 'should convert margin shortcut to indent attribute (four entries)', () => {
+				editor.setData( '<p style="margin:24em 40em 24em 42em">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+				expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( '42em' );
+
+				expect( editor.getData() ).to.equal( '<p style="margin-left:42em;">foo</p>' );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+					.to.equal( '<p style="margin-left:42em">foo</p>' );
+			} );
+
+			it( 'should not convert class to indent attribute', () => {
+				editor.setData( '<p class="indent-1">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.false;
+
+				const expectedView = '<p>foo</p>';
+
+				expect( editor.getData() ).to.equal( expectedView );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
+			} );
+		} );
+
+		describe( 'using classes', () => {
+			beforeEach( () => {
+				return createTestEditor( {
+					indentBlock: {
+						classes: [ 'indent-1', 'indent-2', 'indent-3', 'indent-4' ]
+					}
+				} ).then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					doc = model.document;
+				} );
+			} );
+
+			it( 'should convert class to indent attribute', () => {
+				editor.setData( '<p class="indent-1">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.true;
+				expect( paragraph.getAttribute( 'blockIndent' ) ).to.equal( 'indent-1' );
+
+				const expectedView = '<p class="indent-1">foo</p>';
+
+				expect( editor.getData() ).to.equal( expectedView );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
+			} );
+
+			it( 'should not convert unknown class to indent attribute', () => {
+				editor.setData( '<p class="indent-7">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.false;
+
+				const expectedView = '<p>foo</p>';
+
+				expect( editor.getData() ).to.equal( expectedView );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
+			} );
+
+			it( 'should not convert margin-left to indent attribute (known offset)', () => {
+				editor.setData( '<p style="margin-left:50px">foo</p>' );
+
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				expect( paragraph.hasAttribute( 'blockIndent' ) ).to.be.false;
+
+				const expectedView = '<p>foo</p>';
+
+				expect( editor.getData() ).to.equal( expectedView );
+				expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
+			} );
+		} );
+	} );
+
+	function createTestEditor( extraConfig = {} ) {
+		return VirtualTestEditor
+			.create( Object.assign( {
+				plugins: [ Paragraph, HeadingEditing, IndentEditing, IndentBlock ]
+			}, extraConfig ) );
+	}
+} );

+ 255 - 0
packages/ckeditor5-indent/tests/indentblockcommand.js

@@ -0,0 +1,255 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import IndentBlockCommand from '../src/indentblockcommand';
+import IndentUsingClasses from '../src/indentcommandbehavior/indentusingclasses';
+import IndentUsingOffset from '../src/indentcommandbehavior/indentusingoffset';
+
+describe( 'IndentBlockCommand', () => {
+	let editor, command, model;
+
+	beforeEach( () => {
+		return ModelTestEditor
+			.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				model.schema.register( 'paragraph', {
+					inheritAllFrom: '$block',
+					allowAttributes: [ 'blockIndent' ]
+				} );
+				model.schema.register( 'block', { inheritAllFrom: '$block' } );
+			} );
+	} );
+
+	afterEach( () => {
+		command.destroy();
+
+		return editor.destroy();
+	} );
+
+	describe( 'indent', () => {
+		describe( 'using classes', () => {
+			beforeEach( () => {
+				command = new IndentBlockCommand( editor, new IndentUsingClasses( {
+					classes: [
+						'indent-1',
+						'indent-2',
+						'indent-3',
+						'indent-4'
+					],
+					direction: 'forward'
+				} ) );
+			} );
+
+			describe( 'isEnabled', () => {
+				it( 'should be false in block that does not support indent', () => {
+					setData( model, '<block>f[]oo</block>' );
+					expect( command.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be true in non-indented block', () => {
+					setData( model, '<paragraph>f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be true in indented block and there are still indentation classes', () => {
+					setData( model, '<paragraph blockIndent="indent-2">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be false in indented block in last indentation class', () => {
+					setData( model, '<paragraph blockIndent="indent-4">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.false;
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should set first indent class for non-indented block', () => {
+					setData( model, '<paragraph>f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="indent-1">f[]oo</paragraph>' );
+				} );
+
+				it( 'should set next indent class for indented block', () => {
+					setData( model, '<paragraph blockIndent="indent-2">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="indent-3">f[]oo</paragraph>' );
+				} );
+			} );
+		} );
+
+		describe( 'using offset', () => {
+			beforeEach( () => {
+				command = new IndentBlockCommand( editor, new IndentUsingOffset( {
+					offset: 50,
+					unit: 'px',
+					direction: 'forward'
+				} ) );
+			} );
+
+			describe( 'isEnabled', () => {
+				it( 'should be false in block that does not support indent', () => {
+					setData( model, '<block>f[]oo</block>' );
+					expect( command.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be true in non-indented block', () => {
+					setData( model, '<paragraph>f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be true in indented block', () => {
+					setData( model, '<paragraph blockIndent="50px">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be true in indented block with different unit', () => {
+					setData( model, '<paragraph blockIndent="2em">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should set first offset for non-indented block', () => {
+					setData( model, '<paragraph>f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="50px">f[]oo</paragraph>' );
+				} );
+
+				it( 'should calculate next offset for indented block', () => {
+					setData( model, '<paragraph blockIndent="100px">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="150px">f[]oo</paragraph>' );
+				} );
+
+				it( 'should calculate next offset for indented block even if current indent is not tied to offset', () => {
+					setData( model, '<paragraph blockIndent="27px">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="77px">f[]oo</paragraph>' );
+				} );
+
+				it( 'should set first offset if current indent has different unit', () => {
+					setData( model, '<paragraph blockIndent="3mm">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="50px">f[]oo</paragraph>' );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'outdent', () => {
+		describe( 'using classes', () => {
+			beforeEach( () => {
+				command = new IndentBlockCommand( editor, new IndentUsingClasses( {
+					classes: [
+						'indent-1',
+						'indent-2',
+						'indent-3',
+						'indent-4'
+					],
+					direction: 'backward'
+				} ) );
+			} );
+
+			describe( 'isEnabled', () => {
+				it( 'should be false in block that does not support indent', () => {
+					setData( model, '<block>f[]oo</block>' );
+					expect( command.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false in non-indented block', () => {
+					setData( model, '<paragraph>f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be true in indented block onf first indentation class', () => {
+					setData( model, '<paragraph blockIndent="indent-1">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be true in indented block and there are still indentation classes', () => {
+					setData( model, '<paragraph blockIndent="indent-2">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be true in indented block in last indentation class', () => {
+					setData( model, '<paragraph blockIndent="indent-4">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should set previous indent class for indented block', () => {
+					setData( model, '<paragraph blockIndent="indent-2">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="indent-1">f[]oo</paragraph>' );
+				} );
+			} );
+		} );
+
+		describe( 'using offset', () => {
+			beforeEach( () => {
+				command = new IndentBlockCommand( editor, new IndentUsingOffset( {
+					offset: 50,
+					unit: 'px',
+					direction: 'backward'
+				} ) );
+			} );
+
+			describe( 'isEnabled', () => {
+				it( 'should be false in block that does not support indent', () => {
+					setData( model, '<block>f[]oo</block>' );
+					expect( command.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be false in non-indented block', () => {
+					setData( model, '<paragraph>f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.false;
+				} );
+
+				it( 'should be true in indented block', () => {
+					setData( model, '<paragraph blockIndent="50px">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+
+				it( 'should be true in indented block with different unit', () => {
+					setData( model, '<paragraph blockIndent="2em">f[]oo</paragraph>' );
+					expect( command.isEnabled ).to.be.true;
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should set remove offset if indent is on first offset', () => {
+					setData( model, '<paragraph blockIndent="50px">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph>f[]oo</paragraph>' );
+				} );
+
+				it( 'should calculate next offset for indented block', () => {
+					setData( model, '<paragraph blockIndent="100px">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="50px">f[]oo</paragraph>' );
+				} );
+
+				it( 'should calculate next offset for indented block even if current indent is not tied to offset', () => {
+					setData( model, '<paragraph blockIndent="92px">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph blockIndent="42px">f[]oo</paragraph>' );
+				} );
+
+				it( 'should remove offset if current indent has different unit', () => {
+					setData( model, '<paragraph blockIndent="3mm">f[]oo</paragraph>' );
+					command.execute();
+					expect( getData( model ) ).to.equal( '<paragraph>f[]oo</paragraph>' );
+				} );
+			} );
+		} );
+	} );
+} );

+ 50 - 0
packages/ckeditor5-indent/tests/indentediting.js

@@ -0,0 +1,50 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import MultiCommand from '@ckeditor/ckeditor5-core/src/multicommand';
+
+import IndentEditing from '../src/indentediting';
+
+describe( 'IndentEditing', () => {
+	let editor;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( { plugins: [ IndentEditing ] } )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		if ( editor ) {
+			return editor.destroy();
+		}
+	} );
+
+	it( 'should be named', () => {
+		expect( IndentEditing.pluginName ).to.equal( 'IndentEditing' );
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( IndentEditing ) ).to.be.instanceOf( IndentEditing );
+	} );
+
+	it( 'should register indent command', () => {
+		const command = editor.commands.get( 'indent' );
+
+		expect( command ).to.be.instanceof( MultiCommand );
+	} );
+
+	it( 'should register outdent command', () => {
+		const command = editor.commands.get( 'outdent' );
+
+		expect( command ).to.be.instanceof( MultiCommand );
+	} );
+} );

+ 82 - 0
packages/ckeditor5-indent/tests/indentui.js

@@ -0,0 +1,82 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import IndentEditing from '../src/indentediting';
+import IndentUI from '../src/indentui';
+
+describe( 'IndentUI', () => {
+	let editor, element;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, { plugins: [ IndentUI, IndentEditing ] } )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		if ( editor ) {
+			return editor.destroy();
+		}
+	} );
+
+	it( 'should be named', () => {
+		expect( IndentUI.pluginName ).to.equal( 'IndentUI' );
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( IndentUI ) ).to.be.instanceOf( IndentUI );
+	} );
+
+	it( 'should set up button for indent', () => {
+		const indentButton = editor.ui.componentFactory.create( 'indent' );
+
+		expect( indentButton ).to.be.instanceOf( ButtonView );
+		expect( indentButton.label ).to.equal( 'Increase indent' );
+		expect( indentButton.icon ).to.match( /<svg / );
+	} );
+
+	it( 'should set up button for outdent', () => {
+		const outdentButton = editor.ui.componentFactory.create( 'outdent' );
+
+		expect( outdentButton ).to.be.instanceOf( ButtonView );
+		expect( outdentButton.label ).to.equal( 'Decrease indent' );
+		expect( outdentButton.icon ).to.match( /<svg / );
+	} );
+
+	it( 'should execute indent command on button execute', () => {
+		const button = editor.ui.componentFactory.create( 'indent' );
+		const spy = sinon.spy( editor, 'execute' );
+
+		button.fire( 'execute' );
+
+		sinon.assert.calledOnce( spy );
+		sinon.assert.calledWithExactly( spy, 'indent' );
+	} );
+
+	it( 'should execute outdent command on button execute', () => {
+		const button = editor.ui.componentFactory.create( 'outdent' );
+		const spy = sinon.spy( editor, 'execute' );
+
+		button.fire( 'execute' );
+
+		sinon.assert.calledOnce( spy );
+		sinon.assert.calledWithExactly( spy, 'outdent' );
+	} );
+} );

+ 49 - 0
packages/ckeditor5-indent/tests/manual/indent-block-classes.html

@@ -0,0 +1,49 @@
+<head>
+	<style>
+		body {
+			max-width: 800px;
+			margin: 20px auto;
+		}
+
+		.indent-1 {
+			margin-left: 20px;
+		}
+
+		.indent-2 {
+			margin-left: 40px;
+		}
+
+		.indent-3 {
+			margin-left: 60px;
+		}
+
+		.indent-4 {
+			margin-left: 80px;
+		}
+	</style>
+</head>
+<div id="editor">
+	<h2>Heading 1</h2>
+	<p class="indent-1">Paragraph</p>
+	<p><strong>Bold</strong> <i>Italic</i> <a href="https://ckeditor.com">Link</a></p>
+	<ul>
+		<li>UL List item 1</li>
+		<li>UL List item 2</li>
+	</ul>
+	<ol>
+		<li>OL List item 1</li>
+		<li>OL List item 2</li>
+	</ol>
+	<figure class="image image-style-side">
+		<img alt="bar" src="sample.jpg">
+		<figcaption>Caption</figcaption>
+	</figure>
+	<blockquote>
+		<p>Quote</p>
+		<ul>
+			<li>Quoted UL List item 1</li>
+			<li>Quoted UL List item 2</li>
+		</ul>
+		<p>Quote</p>
+	</blockquote>
+</div>

+ 55 - 0
packages/ckeditor5-indent/tests/manual/indent-block-classes.js

@@ -0,0 +1,55 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+
+import Indent from '../../src/indent';
+import IndentBlock from '../../src/indentblock';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, Indent, IndentBlock ],
+		toolbar: [
+			'heading',
+			'|',
+			'outdent',
+			'indent',
+			'|',
+			'bulletedList',
+			'numberedList',
+			'|',
+			'blockQuote',
+			'insertTable',
+			'undo',
+			'redo'
+		],
+		image: {
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		},
+		indentBlock: {
+			classes: [
+				'indent-1',
+				'indent-2',
+				'indent-3',
+				'indent-4'
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 9 - 0
packages/ckeditor5-indent/tests/manual/indent-block-classes.md

@@ -0,0 +1,9 @@
+## Loading
+
+The editor should be loaded with first paragraph indented with class `indent-1`.
+
+## Testing
+
+- The indentation should be done by changing classes `indent-1` to `indent-4`.
+- The indent button should indent list, block and heading up to `indent-4`.
+- The outdent button should outdent list, block and heading.

+ 35 - 0
packages/ckeditor5-indent/tests/manual/indent-block.html

@@ -0,0 +1,35 @@
+<head>
+	<style>
+		body {
+			max-width: 800px;
+			margin: 20px auto;
+		}
+	</style>
+</head>
+<div id="editor">
+	<h2>Heading 1</h2>
+	<p style="margin-left:1.5em;">Paragraph indented 1.5em</p>
+	<p style="margin-left:30px;">Paragraph indented 30px</p>
+	<p style="margin:0 0 0 40px;">Paragraph indented with margin: 0 0 0 40px</p>
+	<p><strong>Bold</strong> <i>Italic</i> <a href="https://ckeditor.com">Link</a></p>
+	<ul>
+		<li>UL List item 1</li>
+		<li>UL List item 2</li>
+	</ul>
+	<ol>
+		<li>OL List item 1</li>
+		<li>OL List item 2</li>
+	</ol>
+	<figure class="image image-style-side">
+		<img alt="bar" src="sample.jpg">
+		<figcaption>Caption</figcaption>
+	</figure>
+	<blockquote>
+		<p>Quote</p>
+		<ul>
+			<li>Quoted UL List item 1</li>
+			<li>Quoted UL List item 2</li>
+		</ul>
+		<p>Quote</p>
+	</blockquote>
+</div>

+ 48 - 0
packages/ckeditor5-indent/tests/manual/indent-block.js

@@ -0,0 +1,48 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+
+import Indent from '../../src/indent';
+import IndentBlock from '../../src/indentblock';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, Indent, IndentBlock ],
+		toolbar: [
+			'heading',
+			'|',
+			'outdent',
+			'indent',
+			'|',
+			'bulletedList',
+			'numberedList',
+			'|',
+			'blockQuote',
+			'insertTable',
+			'undo',
+			'redo'
+		],
+		image: {
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 11 - 0
packages/ckeditor5-indent/tests/manual/indent-block.md

@@ -0,0 +1,11 @@
+## Loading
+
+The editor should be loaded with first paragraph indented by `1.5em`.
+
+## Testing
+
+- The indent button should indent list, block and heading.
+- The outdent button should outdent list, block and heading.
+- The indentation should be done in steps by `40px`.
+- Indenting paragraph with `1.5em` should first set it to `40px`.
+- Outdenting paragraph with `1.5em` should remove indent.

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


+ 1 - 0
packages/ckeditor5-indent/theme/icons/indent.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 3.75c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm5 6c0 .414.336.75.75.75h9.5a.75.75 0 1 0 0-1.5h-9.5a.75.75 0 0 0-.75.75zM2.75 16.5h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 1 0 0 1.5z"/><path d="M1.632 6.95L5.02 9.358a.4.4 0 0 1-.013.661l-3.39 2.207A.4.4 0 0 1 1 11.892V7.275a.4.4 0 0 1 .632-.326z"/></svg>

+ 1 - 0
packages/ckeditor5-indent/theme/icons/outdent.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 3.75c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm5 6c0 .414.336.75.75.75h9.5a.75.75 0 1 0 0-1.5h-9.5a.75.75 0 0 0-.75.75zM2.75 16.5h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 1 0 0 1.5z"/><path d="M4.368 6.95L.98 9.358a.4.4 0 0 0 .013.661l3.39 2.207A.4.4 0 0 0 5 11.892V7.275a.4.4 0 0 0-.632-.326z"/></svg>