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

Merge pull request #25 from ckeditor/t/ckeditor5/1457

t/ckeditor5/1457: Implemented Font Color and Font Background Color features
Marek Lewandowski 6 лет назад
Родитель
Сommit
20752ebb10
44 измененных файлов с 3173 добавлено и 26 удалено
  1. 6 1
      packages/ckeditor5-font/README.md
  2. 3 0
      packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.html
  3. 209 0
      packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.js
  4. 8 0
      packages/ckeditor5-font/docs/_snippets/features/font.html
  5. 11 1
      packages/ckeditor5-font/docs/_snippets/features/font.js
  6. 5 2
      packages/ckeditor5-font/docs/api/font.md
  7. 146 1
      packages/ckeditor5-font/docs/features/font.md
  8. 19 1
      packages/ckeditor5-font/lang/contexts.json
  9. 5 1
      packages/ckeditor5-font/src/font.js
  10. 143 0
      packages/ckeditor5-font/src/fontbackgroundcolor.js
  11. 31 0
      packages/ckeditor5-font/src/fontbackgroundcolor/fontbackgroundcolorcommand.js
  12. 121 0
      packages/ckeditor5-font/src/fontbackgroundcolor/fontbackgroundcolorediting.js
  13. 40 0
      packages/ckeditor5-font/src/fontbackgroundcolor/fontbackgroundcolorui.js
  14. 142 0
      packages/ckeditor5-font/src/fontcolor.js
  15. 30 0
      packages/ckeditor5-font/src/fontcolor/fontcolorcommand.js
  16. 121 0
      packages/ckeditor5-font/src/fontcolor/fontcolorediting.js
  17. 40 0
      packages/ckeditor5-font/src/fontcolor/fontcolorui.js
  18. 2 1
      packages/ckeditor5-font/src/fontfamily/fontfamilycommand.js
  19. 1 3
      packages/ckeditor5-font/src/fontfamily/fontfamilyediting.js
  20. 5 4
      packages/ckeditor5-font/src/fontfamily/fontfamilyui.js
  21. 2 1
      packages/ckeditor5-font/src/fontsize/fontsizecommand.js
  22. 1 3
      packages/ckeditor5-font/src/fontsize/fontsizeediting.js
  23. 6 5
      packages/ckeditor5-font/src/fontsize/fontsizeui.js
  24. 283 0
      packages/ckeditor5-font/src/ui/colortableview.js
  25. 131 0
      packages/ckeditor5-font/src/ui/colorui.js
  26. 172 0
      packages/ckeditor5-font/src/utils.js
  27. 4 2
      packages/ckeditor5-font/tests/font.js
  28. 18 0
      packages/ckeditor5-font/tests/fontbackgroundcolor.js
  29. 34 0
      packages/ckeditor5-font/tests/fontbackgroundcolor/fontbackgroundcolorcommand.js
  30. 257 0
      packages/ckeditor5-font/tests/fontbackgroundcolor/fontbackgroundcoloreditng.js
  31. 49 0
      packages/ckeditor5-font/tests/fontbackgroundcolor/fontbackgroundcolorui.js
  32. 18 0
      packages/ckeditor5-font/tests/fontcolor.js
  33. 34 0
      packages/ckeditor5-font/tests/fontcolor/fontcolorcommand.js
  34. 253 0
      packages/ckeditor5-font/tests/fontcolor/fontcolorediting.js
  35. 49 0
      packages/ckeditor5-font/tests/fontcolor/fontcolorui.js
  36. 12 0
      packages/ckeditor5-font/tests/manual/font-color.html
  37. 40 0
      packages/ckeditor5-font/tests/manual/font-color.js
  38. 16 0
      packages/ckeditor5-font/tests/manual/font-color.md
  39. 292 0
      packages/ckeditor5-font/tests/ui/colortableview.js
  40. 265 0
      packages/ckeditor5-font/tests/ui/colorui.js
  41. 137 0
      packages/ckeditor5-font/tests/utils.js
  42. 10 0
      packages/ckeditor5-font/theme/fontcolor.css
  43. 1 0
      packages/ckeditor5-font/theme/icons/font-background.svg
  44. 1 0
      packages/ckeditor5-font/theme/icons/font-color.svg

+ 6 - 1
packages/ckeditor5-font/README.md

@@ -10,7 +10,12 @@ CKEditor 5 font feature
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-font/status.svg)](https://david-dm.org/ckeditor/ckeditor5-font)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-font/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-font?type=dev)
 
-This package implements font size and font family support for CKEditor 5.
+This package implements support for the following CKEditor 5 features:
+
+* font size,
+* font family,
+* font color,
+* font background color.
 
 ## Demo
 

+ 3 - 0
packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.html

@@ -0,0 +1,3 @@
+<div id="snippet-custom-font-color-and-background-color-options">
+	<p><span style="color:yellow;background-color:hsl(0,0%,0%);">Text</span> in this <span style="color:white;background-color:hsl(0,100%,50%);">sample</span> <span style="color:aqua;background-color:hsl(240,100%,50%);">has</span> <span style="color:lime;background-color:hsl(300,100%,20%);">m</span><span style="color:lime;background-color:hsl(300,100%,30%);">u</span><span style="color:lime;background-color:hsl(300,100%,40%);">l</span><span style="background-color:hsl(300,100%,50%);">t</span><span style="background-color:hsl(300,100%,60%);">i</span><span style="background-color:hsl(300,100%,70%);">p</span><span style="background-color:hsl(300,100%,80%);">l</span><span style="background-color:hsl(300,100%,90%);">e</span> <span style="color:purple;">font colors</span> and <span style="background-color:hsl(120,100%,90%);">font background</span> <span style="color:red;background-color:hsl(0,0%,62.50%);">colors</span>.</p>
+</div>

+ 209 - 0
packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.js

@@ -0,0 +1,209 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+/* globals ClassicEditor, console, window, document */
+ClassicEditor
+	.create( document.querySelector( '#snippet-custom-font-color-and-background-color-options' ), {
+		cloudServices: CS_CONFIG,
+		toolbar: {
+			items: [
+				'heading', '|', 'fontColor', 'fontBackgroundColor', 'bulletedList', 'numberedList', 'undo', 'redo'
+			],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		fontBackgroundColor: {
+			colors: [
+				'hsl(0,0%,0%)',
+				'hsl(0,0%,12.5%)',
+				'hsl(0,0%,25%)',
+				'hsl(0,0%,37.5%)',
+				'hsl(0,0%,50%)',
+				'hsl(0,0%,62.50%)',
+				'hsl(0,0%,75%)',
+				'hsl(0,0%,87.5%)',
+				{
+					color: 'hsl(0,0%,100%)',
+					hasBorder: true
+				},
+				'hsl(0,100%,10%)',
+				'hsl(0,100%,20%)',
+				'hsl(0,100%,30%)',
+				'hsl(0,100%,40%)',
+				'hsl(0,100%,50%)',
+				'hsl(0,100%,60%)',
+				'hsl(0,100%,70%)',
+				'hsl(0,100%,80%)',
+				{
+					color: 'hsl(0,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(30,100%,10%)',
+				'hsl(30,100%,20%)',
+				'hsl(30,100%,30%)',
+				'hsl(30,100%,40%)',
+				'hsl(30,100%,50%)',
+				'hsl(30,100%,60%)',
+				'hsl(30,100%,70%)',
+				'hsl(30,100%,80%)',
+				{
+					color: 'hsl(30,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(60,100%,10%)',
+				'hsl(60,100%,20%)',
+				'hsl(60,100%,30%)',
+				'hsl(60,100%,40%)',
+				'hsl(60,100%,50%)',
+				'hsl(60,100%,60%)',
+				'hsl(60,100%,70%)',
+				'hsl(60,100%,80%)',
+				{
+					color: 'hsl(60,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(90,100%,10%)',
+				'hsl(90,100%,20%)',
+				'hsl(90,100%,30%)',
+				'hsl(90,100%,40%)',
+				'hsl(90,100%,50%)',
+				'hsl(90,100%,60%)',
+				'hsl(90,100%,70%)',
+				'hsl(90,100%,80%)',
+				{
+					color: 'hsl(90,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(120,100%,10%)',
+				'hsl(120,100%,20%)',
+				'hsl(120,100%,30%)',
+				'hsl(120,100%,40%)',
+				'hsl(120,100%,50%)',
+				'hsl(120,100%,60%)',
+				'hsl(120,100%,70%)',
+				'hsl(120,100%,80%)',
+				{
+					color: 'hsl(120,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(150,100%,10%)',
+				'hsl(150,100%,20%)',
+				'hsl(150,100%,30%)',
+				'hsl(150,100%,40%)',
+				'hsl(150,100%,50%)',
+				'hsl(150,100%,60%)',
+				'hsl(150,100%,70%)',
+				'hsl(150,100%,80%)',
+				{
+					color: 'hsl(150,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(180,100%,10%)',
+				'hsl(180,100%,20%)',
+				'hsl(180,100%,30%)',
+				'hsl(180,100%,40%)',
+				'hsl(180,100%,50%)',
+				'hsl(180,100%,60%)',
+				'hsl(180,100%,70%)',
+				'hsl(180,100%,80%)',
+				{
+					color: 'hsl(180,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(210,100%,10%)',
+				'hsl(210,100%,20%)',
+				'hsl(210,100%,30%)',
+				'hsl(210,100%,40%)',
+				'hsl(210,100%,50%)',
+				'hsl(210,100%,60%)',
+				'hsl(210,100%,70%)',
+				'hsl(210,100%,80%)',
+				{
+					color: 'hsl(210,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(240,100%,10%)',
+				'hsl(240,100%,20%)',
+				'hsl(240,100%,30%)',
+				'hsl(240,100%,40%)',
+				'hsl(240,100%,50%)',
+				'hsl(240,100%,60%)',
+				'hsl(240,100%,70%)',
+				'hsl(240,100%,80%)',
+				{
+					color: 'hsl(240,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(270,100%,10%)',
+				'hsl(270,100%,20%)',
+				'hsl(270,100%,30%)',
+				'hsl(270,100%,40%)',
+				'hsl(270,100%,50%)',
+				'hsl(270,100%,60%)',
+				'hsl(270,100%,70%)',
+				'hsl(270,100%,80%)',
+				{
+					color: 'hsl(270,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(300,100%,10%)',
+				'hsl(300,100%,20%)',
+				'hsl(300,100%,30%)',
+				'hsl(300,100%,40%)',
+				'hsl(300,100%,50%)',
+				'hsl(300,100%,60%)',
+				'hsl(300,100%,70%)',
+				'hsl(300,100%,80%)',
+				{
+					color: 'hsl(300,100%,90%)',
+					hasBorder: true
+				},
+				'hsl(330,100%,10%)',
+				'hsl(330,100%,20%)',
+				'hsl(330,100%,30%)',
+				'hsl(330,100%,40%)',
+				'hsl(330,100%,50%)',
+				'hsl(330,100%,60%)',
+				'hsl(330,100%,70%)',
+				'hsl(330,100%,80%)',
+				{
+					color: 'hsl(330,100%,90%)',
+					hasBorder: true
+				},
+			],
+			columns: 9
+		},
+		fontColor: {
+			colors: [
+				'black',
+				'gray',
+				'silver',
+				{
+					color: 'white',
+					hasBorder: true
+				},
+				'maroon',
+				'red',
+				'purple',
+				'fuchsia',
+				'green',
+				'lime',
+				'olive',
+				'yellow',
+				'navy',
+				'blue',
+				'teal',
+				'aqua'
+			],
+			columns: 4
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 8 - 0
packages/ckeditor5-font/docs/_snippets/features/font.html

@@ -15,4 +15,12 @@
 		<span style="font-family: 'Courier New', Courier, monospace;">This text has the "Courier New, Courier, monospace" family set.</span>
 	</p>
 
+	<p>
+		<span style="color:hsl(90,75%,60%);background-color:hsl(240,75%,60%)">This text has light green color and blue background.</span>
+	</p>
+
+	<p>
+		<span style="color:hsl(0,0%,100%);background-color:hsl(0,0%,0%)">This text has white color and black background.</span>
+	</p>
+
 </div>

+ 11 - 1
packages/ckeditor5-font/docs/_snippets/features/font.js

@@ -12,7 +12,17 @@ ClassicEditor
 		cloudServices: CS_CONFIG,
 		toolbar: {
 			items: [
-				'heading', '|', 'fontSize', 'fontFamily', '|', 'bulletedList', 'numberedList', 'undo', 'redo'
+				'heading',
+				'|',
+				'fontSize',
+				'fontFamily',
+				'fontColor',
+				'fontBackgroundColor',
+				'|',
+				'bulletedList',
+				'numberedList',
+				'undo',
+				'redo'
 			],
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		}

+ 5 - 2
packages/ckeditor5-font/docs/api/font.md

@@ -14,8 +14,11 @@ Check out the {@link features/font#demo demo in the Font feature} guide.
 
 ## Documentation
 
-See the {@link features/font Font feature} guide
-and {@link module:font/fontfamily~FontFamily} and {@link module:font/fontsize~FontSize} plugins documentation.
+See the {@link features/font Font feature} guide and plugins documentation:
+- {@link module:font/fontfamily~FontFamily}.
+- {@link module:font/fontsize~FontSize}.
+- {@link module:font/fontcolor~FontColor}.
+- {@link module:font/fontbackgroundcolor~FontBackgroundColor}.
 
 ## Installation
 

+ 146 - 1
packages/ckeditor5-font/docs/features/font.md

@@ -8,6 +8,8 @@ category: features
 The {@link module:font/font~Font} plugin enables the following features in the editor:
 * {@link module:font/fontfamily~FontFamily} &ndash; Allows to change the font family by applying inline `<span>` elements with a `font-family` in the `style` attribute.
 * {@link module:font/fontsize~FontSize} &ndash; Allows to control the font size by applying inline `<span>` elements that either have a CSS class or a `font-size` in the `style` attribute.
+* {@link module:font/fontcolor~FontColor} &ndash; Allows to control the font color by applying inline `<span>` elements with a `color` in the `style` attribute.
+* {@link module:font/fontbackgroundcolor~FontBackgroundColor} &ndash; Allows to control the font background color by applying inline `<span>` elements with a `background-color` in the `style` attribute.
 
 ## Demo
 
@@ -145,6 +147,115 @@ ClassicEditor
 
 {@snippet features/custom-font-size-numeric-options}
 
+## Configuring font color and font background color features
+
+Both font color and font background color features are configurable and share the same configuration format.
+
+<info-box info>
+	Please note that {@link module:font/fontcolor~FontColor font color} and {@link module:font/fontbackgroundcolor~FontBackgroundColor font background color} are separate plugins. They must be enabled and configured individually.
+</info-box>
+
+Check out the editor below with both features customized using the editor configuration:
+
+{@snippet features/custom-font-color-and-background-color-options}
+
+### Specifying the available colors
+
+It is possible to configure which colors are available in the color dropdown. Use the {@link module:font/fontcolor~FontColorConfig#colors `fontColor.colors`} and {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#colors `fontBackgroundColor.colors`} configuration options to do so.
+
+```js
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		fontColor: {
+			colors: [
+				{
+					color: 'hsl(0, 0%, 0%)',
+					label: 'Black'
+				},
+				{
+					color: 'hsl(0, 0%, 30%)',
+					label: 'Dim grey'
+				},
+				{
+					color: 'hsl(0, 0%, 60%)',
+					label: 'Grey'
+				},
+				{
+					color: 'hsl(0, 0%, 90%)',
+					label: 'Light grey'
+				},
+				{
+					color: 'hsl(0, 0%, 100%)',
+					label: 'White',
+					hasBorder: true
+				},
+
+				// ...
+			]
+		},
+		fontBackgroundColor: {
+			colors: [
+				{
+					color: 'hsl(0, 75%, 60%)',
+					label: 'Red'
+				},
+				{
+					color: 'hsl(30, 75%, 60%)',
+					label: 'Orange'
+				},
+				{
+					color: 'hsl(60, 75%, 60%)',
+					label: 'Yellow'
+				},
+				{
+					color: 'hsl(90, 75%, 60%)',
+					label: 'Light green'
+				},
+				{
+					color: 'hsl(120, 75%, 60%)',
+					label: 'Green'
+				},
+
+				// ...
+			]
+		},
+		toolbar: [
+			'heading', 'bulletedList', 'numberedList', 'fontColor', 'fontBackgroundColor', 'undo', 'redo'
+		]
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+### Changing the geometry of the color grid
+
+It is also possible to configure in how many columns the colors in the grid are displayed. Use {@link module:font/fontcolor~FontColorConfig#columns `fontColor.columns`} and {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#columns `fontBackgroundColor.columns`} to do so.
+
+<info-box>
+	The configuration of the columns also affects the number of recently used colors displayed under the color grid. The less columns, the fewer recently used colors will be displayed.
+</info-box>
+
+```js
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		fontColor: {
+			columns: 3,
+
+			// ...
+		},
+		fontBackgroundColor: {
+			columns: 6,
+
+			// ...
+		},
+		toolbar: [
+			'heading', 'bulletedList', 'numberedList', 'fontColor', 'fontBackgroundColor', 'undo', 'redo'
+		]
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
 ## Installation
 
 To add this feature to your editor, install the [`@ckeditor/ckeditor5-font`](https://www.npmjs.com/package/@ckeditor/ckeditor5-font) package:
@@ -161,7 +272,7 @@ import Font from '@ckeditor/ckeditor5-font/src/font';
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Font, ... ],
-		toolbar: [ 'fontSize', 'fontFamily', ... ]
+		toolbar: [ 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', ... ]
 	} )
 	.then( ... )
 	.catch( ... );
@@ -256,6 +367,40 @@ The {@link module:font/fontsize~FontSize} plugin registers the following compone
 	editor.execute( 'fontSize' );
 	```
 
+The {@link module:font/fontcolor~FontColor} plugin registers the following components:
+
+* The `'fontColor'` dropdown,
+* The {@link module:font/fontcolor/fontcolorcommand~FontColorCommand `'fontColor'`} command.
+
+	You can change the font color of the current selection by executing the command with a desired value:
+
+	```js
+	editor.execute( 'fontColor', { value: 'rgb(30, 188, 97)' } );
+	```
+
+	Passing an empty value will remove the font color from the selection:
+
+	```js
+	editor.execute( 'fontColor' );
+	```
+
+The {@link module:font/fontbackgroundcolor~FontBackgroundColor} plugin registers the following components:
+
+* The `'fontBackgroundColor'` dropdown,
+* The {@link module:font/fontbackgroundcolor/fontbackgroundcolorcommand~FontBackgroundColorCommand `'fontBackgroundColor'`} command.
+
+	You can change the font background color of the current selection by executing the command with a desired value:
+
+	```js
+	editor.execute( 'fontBackgroundColor', { value: 'rgb(30, 188, 97)' } );
+	```
+
+	Passing an empty value will remove the font background color from the selection:
+
+	```js
+	editor.execute( 'fontBackgroundColor' );
+	```
+
 <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>

+ 19 - 1
packages/ckeditor5-font/lang/contexts.json

@@ -5,5 +5,23 @@
 	"Big": "Dropdown option label for the 'big' font size preset.",
 	"Huge": "Dropdown option label for the 'huge' font size preset.",
 	"Font Family": "Tooltip for the font family dropdown.",
-	"Default": "Dropdown option label for the default font family."
+	"Default": "Dropdown option label for the default font family.",
+	"Font color": "Label of a button that allows selecting a font color.",
+	"Font Background Color": "Label of a button that allows selecting a font background color.",
+	"Remove color": "Label or a button that removes the color from the text (font color and background).",
+	"Black": "Label of a button that applies a black text color (font color and background)",
+	"Dim grey": "Label of a button that applies a dim grey text color (font color and background)",
+	"Grey": "Label of a button that applies a grey text color (font color and background)",
+	"Light grey": "Label of a button that applies a light grey text color (font color and background)",
+	"White": "Label of a button that applies a white text color (font color and background)",
+	"Red": "Label of a button that applies a red text color (font color and background)",
+	"Orange": "Label of a button that applies a orange text color (font color and background)",
+	"Yellow": "Label of a button that applies a yellow text color (font color and background)",
+	"Light green": "Label of a button that applies a light green text color (font color and background)",
+	"Green": "Label of a button that applies a green text color (font color and background)",
+	"Aquamarine": "Label of a button that applies a aquamarine text color (font color and background)",
+	"Turquoise": "Label of a button that applies a turquoise text color (font color and background)",
+	"Light blue": "Label of a button that applies a light blue text color (font color and background)",
+	"Blue": "Label of a button that applies a blue text color (font color and background)",
+	"Purple": "Label of a button that applies a purple text color (font color and background)"
 }

+ 5 - 1
packages/ckeditor5-font/src/font.js

@@ -11,12 +11,16 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import FontFamily from './fontfamily';
 import FontSize from './fontsize';
+import FontColor from './fontcolor';
+import FontBackgroundColor from './fontbackgroundcolor';
 
 /**
  * A plugin that enables a set of text styling features:
  *
  * * {@link module:font/fontsize~FontSize},
  * * {@link module:font/fontfamily~FontFamily}.
+ * * {@link module:font/fontcolor~FontColor},
+ * * {@link module:font/fontbackgroundcolor~FontBackgroundColor}.
  *
  * For a detailed overview, check the {@glink features/font Font feature} documentation
  * and the {@glink api/font package page}.
@@ -28,7 +32,7 @@ export default class Font extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ FontFamily, FontSize ];
+		return [ FontFamily, FontSize, FontColor, FontBackgroundColor ];
 	}
 
 	/**

+ 143 - 0
packages/ckeditor5-font/src/fontbackgroundcolor.js

@@ -0,0 +1,143 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/fontbackgroundcolor
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FontBackgroundColorEditing from './fontbackgroundcolor/fontbackgroundcolorediting';
+import FontBackgroundColorUI from './fontbackgroundcolor/fontbackgroundcolorui';
+
+/**
+ * The font background color plugin.
+ *
+ * For a detailed overview, check the {@glink features/font font feature} documentation
+ * and the {@glink api/font package page}.
+ *
+ * This is a "glue" plugin which loads
+ * the {@link module:font/fontbackgroundcolor/fontbackgroundcolorediting~FontBackgroundColorEditing} and
+ * {@link module:font/fontbackgroundcolor/fontbackgroundcolorui~FontBackgroundColorUI} features in the editor.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class FontBackgroundColor extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ FontBackgroundColorEditing, FontBackgroundColorUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'FontBackgroundColor';
+	}
+}
+
+/**
+ * The configuration of the font background color feature.
+ * This option is used by the {@link module:font/fontbackgroundcolor/fontbackgroundcolorediting~FontBackgroundColorEditing} feature.
+ *
+ *		ClassicEditor
+ *			.create( {
+ *				fontBackgroundColor: ... // Font background color feature configuration.
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
+ *
+ * @interface module:font/fontbackgroundcolor~FontBackgroundColorConfig
+ */
+
+/**
+ * Available font background colors defined as an array of strings or objects.
+ *
+ * The default value registers the following colors:
+ *
+ *		const fontBackgroundColorConfig = {
+ *			colors: [
+ *				{
+ *					color: 'hsl(0, 0%, 0%)',
+ *					label: 'Black'
+ *				},
+ *				{
+ *					color: 'hsl(0, 0%, 30%)',
+ *					label: 'Dim grey'
+ *				},
+ *				{
+ *					color: 'hsl(0, 0%, 60%)',
+ *					label: 'Grey'
+ *				},
+ *				{
+ *					color: 'hsl(0, 0%, 90%)',
+ *					label: 'Light grey'
+ *				},
+ *				{
+ *					color: 'hsl(0, 0%, 100%)',
+ *					label: 'White',
+ *					hasBorder: true
+ *				},
+ *				{
+ *					color: 'hsl(0, 75%, 60%)',
+ *					label: 'Red'
+ *				},
+ *				{
+ *					color: 'hsl(30, 75%, 60%)',
+ *					label: 'Orange'
+ *				},
+ *				{
+ *					color: 'hsl(60, 75%, 60%)',
+ *					label: 'Yellow'
+ *				},
+ *				{
+ *					color: 'hsl(90, 75%, 60%)',
+ *					label: 'Light green'
+ *				},
+ *				{
+ *					color: 'hsl(120, 75%, 60%)',
+ *					label: 'Green'
+ *				},
+ *				{
+ *					color: 'hsl(150, 75%, 60%)',
+ *					label: 'Aquamarine'
+ *				},
+ *				{
+ *					color: 'hsl(180, 75%, 60%)',
+ *					label: 'Turquoise'
+ *				},
+ *				{
+ *					color: 'hsl(210, 75%, 60%)',
+ *					label: 'Light blue'
+ *				},
+ *				{
+ *					color: 'hsl(240, 75%, 60%)',
+ *					label: 'Blue'
+ *				},
+ *				{
+ *					color: 'hsl(270, 75%, 60%)',
+ *					label: 'Purple'
+ *				}
+ *			]
+ *		};
+ *
+ * *Note*: The colors are displayed in the `'fontBackgroundColor'` dropdown.
+ *
+ * @member {Array.<String|Object>} module:font/fontbackgroundcolor~FontBackgroundColorConfig#colors
+ */
+
+/**
+ * Represents the amount of columns in the dropdown. It also represents the number of background colors
+ * recently used by the user displayed in the dropdown. The default value is:
+ *
+ *		const fontBackgroundColorConfig = {
+ *			columns: 5
+ *		}
+ *
+ * @member {Number} module:font/fontbackgroundcolor~FontBackgroundColorConfig#columns
+ */

+ 31 - 0
packages/ckeditor5-font/src/fontbackgroundcolor/fontbackgroundcolorcommand.js

@@ -0,0 +1,31 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/fontbackgroundcolor/fontbackgroundcolorcommand
+ */
+
+import FontCommand from '../fontcommand';
+import { FONT_BACKGROUND_COLOR } from '../utils';
+
+/**
+ * The font background color command. It's used by the
+ * {@link module:font/fontbackgroundcolor/fontbackgroundcolorediting~FontBackgroundColorEditing}
+ * to apply the font background color.
+ *
+ *		editor.execute( 'fontBackgroundColor', { value: 'rgb(250, 20, 20)' } );
+ *
+ * **Note**: Executing the command with the `null` value removes the attribute from the model.
+ *
+ * @extends module:font/fontcommand~FontCommand
+ */
+export default class FontBackgroundColorCommand extends FontCommand {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor, FONT_BACKGROUND_COLOR );
+	}
+}

+ 121 - 0
packages/ckeditor5-font/src/fontbackgroundcolor/fontbackgroundcolorediting.js

@@ -0,0 +1,121 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/fontbackgroundcolor/fontbackgroundcolorediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FontBackgroundColorCommand from './fontbackgroundcolorcommand';
+import { FONT_BACKGROUND_COLOR, renderDowncastElement, renderUpcastAttribute } from '../utils';
+
+/**
+ * The font background color editing feature.
+ *
+ * It introduces the {@link module:font/fontbackgroundcolor/fontbackgroundcolorcommand~FontBackgroundColorCommand command} and
+ * the `fontBackgroundColor` attribute in the {@link module:engine/model/model~Model model} which renders
+ * in the {@link module:engine/view/view view} as a `<span>` element (`<span style="background-color: ...">`),
+ * depending on the {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig configuration}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class FontBackgroundColorEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( FONT_BACKGROUND_COLOR, {
+			colors: [
+				{
+					color: 'hsl(0, 0%, 0%)',
+					label: 'Black'
+				},
+				{
+					color: 'hsl(0, 0%, 30%)',
+					label: 'Dim grey'
+				},
+				{
+					color: 'hsl(0, 0%, 60%)',
+					label: 'Grey'
+				},
+				{
+					color: 'hsl(0, 0%, 90%)',
+					label: 'Light grey'
+				},
+				{
+					color: 'hsl(0, 0%, 100%)',
+					label: 'White',
+					hasBorder: true
+				},
+				{
+					color: 'hsl(0, 75%, 60%)',
+					label: 'Red'
+				},
+				{
+					color: 'hsl(30, 75%, 60%)',
+					label: 'Orange'
+				},
+				{
+					color: 'hsl(60, 75%, 60%)',
+					label: 'Yellow'
+				},
+				{
+					color: 'hsl(90, 75%, 60%)',
+					label: 'Light green'
+				},
+				{
+					color: 'hsl(120, 75%, 60%)',
+					label: 'Green'
+				},
+				{
+					color: 'hsl(150, 75%, 60%)',
+					label: 'Aquamarine'
+				},
+				{
+					color: 'hsl(180, 75%, 60%)',
+					label: 'Turquoise'
+				},
+				{
+					color: 'hsl(210, 75%, 60%)',
+					label: 'Light blue'
+				},
+				{
+					color: 'hsl(240, 75%, 60%)',
+					label: 'Blue'
+				},
+				{
+					color: 'hsl(270, 75%, 60%)',
+					label: 'Purple'
+				}
+			],
+			columns: 5
+		} );
+
+		editor.conversion.for( 'upcast' ).elementToAttribute( {
+			view: {
+				name: 'span',
+				styles: {
+					'background-color': /[\s\S]+/
+				}
+			},
+			model: {
+				key: FONT_BACKGROUND_COLOR,
+				value: renderUpcastAttribute( 'background-color' )
+			}
+		} );
+
+		editor.conversion.for( 'downcast' ).attributeToElement( {
+			model: FONT_BACKGROUND_COLOR,
+			view: renderDowncastElement( 'background-color' )
+		} );
+
+		editor.commands.add( FONT_BACKGROUND_COLOR, new FontBackgroundColorCommand( editor ) );
+
+		// Allow fontBackgroundColor attribute on text nodes.
+		editor.model.schema.extend( '$text', { allowAttributes: FONT_BACKGROUND_COLOR } );
+	}
+}

+ 40 - 0
packages/ckeditor5-font/src/fontbackgroundcolor/fontbackgroundcolorui.js

@@ -0,0 +1,40 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/fontbackgroundcolor/fontbackgroundcolorui
+ */
+
+import ColorUI from '../ui/colorui';
+import { FONT_BACKGROUND_COLOR } from '../utils';
+import fontBackgroundColorIcon from '../../theme/icons/font-background.svg';
+
+/**
+ * The font background color UI plugin. It introduces the `'fontBackgroundColor'` dropdown.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class FontBackgroundColorUI extends ColorUI {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		const t = editor.locale.t;
+
+		super( editor, {
+			commandName: FONT_BACKGROUND_COLOR,
+			componentName: FONT_BACKGROUND_COLOR,
+			icon: fontBackgroundColorIcon,
+			dropdownLabel: t( 'Font Background Color' )
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'FontBackgroundColorUI';
+	}
+}

+ 142 - 0
packages/ckeditor5-font/src/fontcolor.js

@@ -0,0 +1,142 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/fontcolor
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FontColorEditing from './fontcolor/fontcolorediting';
+import FontColorUI from './fontcolor/fontcolorui';
+
+/**
+ * The font color plugin.
+ *
+ * For a detailed overview, check the {@glink features/font font feature} documentation
+ * and the {@glink api/font package page}.
+ *
+ * This is a "glue" plugin which loads the {@link module:font/fontcolor/fontcolorediting~FontColorEditing} and
+ * {@link module:font/fontcolor/fontcolorui~FontColorUI} features in the editor.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class FontColor extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ FontColorEditing, FontColorUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'FontColor';
+	}
+}
+
+/**
+ * The configuration of the font color feature.
+ * This option is used by the {@link module:font/fontcolor/fontcolorediting~FontColorEditing} feature.
+ *
+ *		ClassicEditor
+ *			.create( {
+ *				fontColor: ... // Font color feature configuration.
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
+ *
+ * @interface module:font/fontcolor~FontColorConfig
+ */
+
+/**
+ * Available font colors defined as an array of strings or objects.
+ *
+ * The default value registers the following colors:
+ *
+ *		const fontColorConfig = {
+ *			colors: [
+ *				{
+ *					color: 'hsl(0, 0%, 0%)',
+ *					label: 'Black'
+ *				},
+ *				{
+ *					color: 'hsl(0, 0%, 30%)',
+ *					label: 'Dim grey'
+ *				},
+ *				{
+ *					color: 'hsl(0, 0%, 60%)',
+ *					label: 'Grey'
+ *				},
+ *				{
+ *					color: 'hsl(0, 0%, 90%)',
+ *					label: 'Light grey'
+ *				},
+ *				{
+ *					color: 'hsl(0, 0%, 100%)',
+ *					label: 'White',
+ *					hasBorder: true
+ *				},
+ *				{
+ *					color: 'hsl(0, 75%, 60%)',
+ *					label: 'Red'
+ *				},
+ *				{
+ *					color: 'hsl(30, 75%, 60%)',
+ *					label: 'Orange'
+ *				},
+ *				{
+ *					color: 'hsl(60, 75%, 60%)',
+ *					label: 'Yellow'
+ *				},
+ *				{
+ *					color: 'hsl(90, 75%, 60%)',
+ *					label: 'Light green'
+ *				},
+ *				{
+ *					color: 'hsl(120, 75%, 60%)',
+ *					label: 'Green'
+ *				},
+ *				{
+ *					color: 'hsl(150, 75%, 60%)',
+ *					label: 'Aquamarine'
+ *				},
+ *				{
+ *					color: 'hsl(180, 75%, 60%)',
+ *					label: 'Turquoise'
+ *				},
+ *				{
+ *					color: 'hsl(210, 75%, 60%)',
+ *					label: 'Light blue'
+ *				},
+ *				{
+ *					color: 'hsl(240, 75%, 60%)',
+ *					label: 'Blue'
+ *				},
+ *				{
+ *					color: 'hsl(270, 75%, 60%)',
+ *					label: 'Purple'
+ *				}
+ *			]
+ *		};
+ *
+ * *Note*: The colors are displayed in the `'fontColor'` dropdown.
+ *
+ * @member {Array.<String|Object>} module:font/fontcolor~FontColorConfig#colors
+ */
+
+/**
+ * Represents the amount of columns in the dropdown. It also represents the number of font colors
+ * recently used by the user displayed in the dropdown. The default value is:
+ *
+ *		const fontColorConfig = {
+ *			columns: 5
+ *		}
+ *
+ * @member {Number} module:font/fontcolor~FontColorConfig#columns
+ */

+ 30 - 0
packages/ckeditor5-font/src/fontcolor/fontcolorcommand.js

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/fontcolor/fontcolorcommand
+ */
+
+import FontCommand from '../fontcommand';
+import { FONT_COLOR } from '../utils';
+
+/**
+ * The font color command. It's used by the {@link module:font/fontcolor/fontcolorediting~FontColorEditing}
+ * to apply the font color.
+ *
+ *		editor.execute( 'fontColor', { value: 'rgb(250, 20, 20)' } );
+ *
+ * **Note**: Executing the command with the `null` value removes the attribute from the model.
+ *
+ * @extends module:font/fontcommand~FontCommand
+ */
+export default class FontColorCommand extends FontCommand {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor, FONT_COLOR );
+	}
+}

+ 121 - 0
packages/ckeditor5-font/src/fontcolor/fontcolorediting.js

@@ -0,0 +1,121 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/fontcolor/fontcolorediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FontColorCommand from './fontcolorcommand';
+import { FONT_COLOR, renderDowncastElement, renderUpcastAttribute } from '../utils';
+
+/**
+ * The font color editing feature.
+ *
+ * It introduces the {@link module:font/fontcolor/fontcolorcommand~FontColorCommand command} and
+ * the `fontColor` attribute in the {@link module:engine/model/model~Model model} which renders
+ * in the {@link module:engine/view/view view} as a `<span>` element (`<span style="color: ...">`),
+ * depending on the {@link module:font/fontcolor~FontColorConfig configuration}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class FontColorEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.define( FONT_COLOR, {
+			colors: [
+				{
+					color: 'hsl(0, 0%, 0%)',
+					label: 'Black'
+				},
+				{
+					color: 'hsl(0, 0%, 30%)',
+					label: 'Dim grey'
+				},
+				{
+					color: 'hsl(0, 0%, 60%)',
+					label: 'Grey'
+				},
+				{
+					color: 'hsl(0, 0%, 90%)',
+					label: 'Light grey'
+				},
+				{
+					color: 'hsl(0, 0%, 100%)',
+					label: 'White',
+					hasBorder: true
+				},
+				{
+					color: 'hsl(0, 75%, 60%)',
+					label: 'Red'
+				},
+				{
+					color: 'hsl(30, 75%, 60%)',
+					label: 'Orange'
+				},
+				{
+					color: 'hsl(60, 75%, 60%)',
+					label: 'Yellow'
+				},
+				{
+					color: 'hsl(90, 75%, 60%)',
+					label: 'Light green'
+				},
+				{
+					color: 'hsl(120, 75%, 60%)',
+					label: 'Green'
+				},
+				{
+					color: 'hsl(150, 75%, 60%)',
+					label: 'Aquamarine'
+				},
+				{
+					color: 'hsl(180, 75%, 60%)',
+					label: 'Turquoise'
+				},
+				{
+					color: 'hsl(210, 75%, 60%)',
+					label: 'Light blue'
+				},
+				{
+					color: 'hsl(240, 75%, 60%)',
+					label: 'Blue'
+				},
+				{
+					color: 'hsl(270, 75%, 60%)',
+					label: 'Purple'
+				}
+			],
+			columns: 5
+		} );
+
+		editor.conversion.for( 'upcast' ).elementToAttribute( {
+			view: {
+				name: 'span',
+				styles: {
+					'color': /[\s\S]+/
+				}
+			},
+			model: {
+				key: FONT_COLOR,
+				value: renderUpcastAttribute( 'color' )
+			}
+		} );
+
+		editor.conversion.for( 'downcast' ).attributeToElement( {
+			model: FONT_COLOR,
+			view: renderDowncastElement( 'color' )
+		} );
+
+		editor.commands.add( FONT_COLOR, new FontColorCommand( editor ) );
+
+		// Allow fontColor attribute on text nodes.
+		editor.model.schema.extend( '$text', { allowAttributes: FONT_COLOR } );
+	}
+}

+ 40 - 0
packages/ckeditor5-font/src/fontcolor/fontcolorui.js

@@ -0,0 +1,40 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/fontcolor/fontcolorui
+ */
+
+import ColorUI from '../ui/colorui';
+import { FONT_COLOR } from '../utils';
+import fontColorIcon from '../../theme/icons/font-color.svg';
+
+/**
+ * The font color UI plugin. It introduces the `'fontColor'` dropdown.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class FontColorUI extends ColorUI {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		const t = editor.locale.t;
+
+		super( editor, {
+			commandName: FONT_COLOR,
+			componentName: FONT_COLOR,
+			icon: fontColorIcon,
+			dropdownLabel: t( 'Font Color' )
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'FontColorUI';
+	}
+}

+ 2 - 1
packages/ckeditor5-font/src/fontfamily/fontfamilycommand.js

@@ -8,6 +8,7 @@
  */
 
 import FontCommand from '../fontcommand';
+import { FONT_FAMILY } from '../utils';
 
 /**
  * The font family command. It is used by {@link module:font/fontfamily/fontfamilyediting~FontFamilyEditing}
@@ -24,6 +25,6 @@ export default class FontFamilyCommand extends FontCommand {
 	 * @inheritDoc
 	 */
 	constructor( editor ) {
-		super( editor, 'fontFamily' );
+		super( editor, FONT_FAMILY );
 	}
 }

+ 1 - 3
packages/ckeditor5-font/src/fontfamily/fontfamilyediting.js

@@ -11,9 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import FontFamilyCommand from './fontfamilycommand';
 import { normalizeOptions } from './utils';
-import { buildDefinition } from '../utils';
-
-const FONT_FAMILY = 'fontFamily';
+import { buildDefinition, FONT_FAMILY } from '../utils';
 
 /**
  * The font family editing feature.

+ 5 - 4
packages/ckeditor5-font/src/fontfamily/fontfamilyui.js

@@ -13,6 +13,7 @@ import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
 import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 import { normalizeOptions } from './utils';
+import { FONT_FAMILY } from '../utils';
 import fontFamilyIcon from '../../theme/icons/font-family.svg';
 
 /**
@@ -30,10 +31,10 @@ export default class FontFamilyUI extends Plugin {
 
 		const options = this._getLocalizedOptions();
 
-		const command = editor.commands.get( 'fontFamily' );
+		const command = editor.commands.get( FONT_FAMILY );
 
 		// Register UI component.
-		editor.ui.componentFactory.add( 'fontFamily', locale => {
+		editor.ui.componentFactory.add( FONT_FAMILY, locale => {
 			const dropdownView = createDropdown( locale );
 			addListToDropdown( dropdownView, _prepareListOptions( options, command ) );
 
@@ -76,7 +77,7 @@ export default class FontFamilyUI extends Plugin {
 		const editor = this.editor;
 		const t = editor.t;
 
-		const options = normalizeOptions( editor.config.get( 'fontFamily.options' ) );
+		const options = normalizeOptions( editor.config.get( FONT_FAMILY ).options );
 
 		return options.map( option => {
 			// The only title to localize is "Default" others are font names.
@@ -101,7 +102,7 @@ function _prepareListOptions( options, command ) {
 		const def = {
 			type: 'button',
 			model: new Model( {
-				commandName: 'fontFamily',
+				commandName: FONT_FAMILY,
 				commandParam: option.model,
 				label: option.title,
 				withText: true

+ 2 - 1
packages/ckeditor5-font/src/fontsize/fontsizecommand.js

@@ -8,6 +8,7 @@
  */
 
 import FontCommand from '../fontcommand';
+import { FONT_SIZE } from '../utils';
 
 /**
  * The font size command. It is used by {@link module:font/fontsize/fontsizeediting~FontSizeEditing}
@@ -24,6 +25,6 @@ export default class FontSizeCommand extends FontCommand {
 	 * @inheritDoc
 	 */
 	constructor( editor ) {
-		super( editor, 'fontSize' );
+		super( editor, FONT_SIZE );
 	}
 }

+ 1 - 3
packages/ckeditor5-font/src/fontsize/fontsizeediting.js

@@ -11,9 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import FontSizeCommand from './fontsizecommand';
 import { normalizeOptions } from './utils';
-import { buildDefinition } from '../utils';
-
-const FONT_SIZE = 'fontSize';
+import { buildDefinition, FONT_SIZE } from '../utils';
 
 /**
  * The font size editing feature.

+ 6 - 5
packages/ckeditor5-font/src/fontsize/fontsizeui.js

@@ -12,7 +12,8 @@ import Model from '@ckeditor/ckeditor5-ui/src/model';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
 import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
-import { normalizeOptions } from '../fontsize/utils';
+import { normalizeOptions } from './utils';
+import { FONT_SIZE } from '../utils';
 
 import fontSizeIcon from '../../theme/icons/font-size.svg';
 import '../../theme/fontsize.css';
@@ -32,10 +33,10 @@ export default class FontSizeUI extends Plugin {
 
 		const options = this._getLocalizedOptions();
 
-		const command = editor.commands.get( 'fontSize' );
+		const command = editor.commands.get( FONT_SIZE );
 
 		// Register UI component.
-		editor.ui.componentFactory.add( 'fontSize', locale => {
+		editor.ui.componentFactory.add( FONT_SIZE, locale => {
 			const dropdownView = createDropdown( locale );
 			addListToDropdown( dropdownView, _prepareListOptions( options, command ) );
 
@@ -89,7 +90,7 @@ export default class FontSizeUI extends Plugin {
 			Huge: t( 'Huge' )
 		};
 
-		const options = normalizeOptions( editor.config.get( 'fontSize.options' ) );
+		const options = normalizeOptions( editor.config.get( FONT_SIZE ).options );
 
 		return options.map( option => {
 			const title = localizedTitles[ option.title ];
@@ -115,7 +116,7 @@ function _prepareListOptions( options, command ) {
 		const def = {
 			type: 'button',
 			model: new Model( {
-				commandName: 'fontSize',
+				commandName: FONT_SIZE,
 				commandParam: option.model,
 				label: option.title,
 				class: 'ck-fontsize-option',

+ 283 - 0
packages/ckeditor5-font/src/ui/colortableview.js

@@ -0,0 +1,283 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/ui/colortableview
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import ColorTileView from '@ckeditor/ckeditor5-ui/src/colorgrid/colortileview';
+import ColorGridView from '@ckeditor/ckeditor5-ui/src/colorgrid/colorgridview';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import removeButtonIcon from '@ckeditor/ckeditor5-core/theme/icons/eraser.svg';
+import '../../theme/fontcolor.css';
+
+/**
+ * Class which represents a view with the following sub–components:
+ *
+ * * a remove color button,
+ * * a {@link module:ui/colorgrid/colorgrid~ColorGridView},
+ * * a grid of recently used colors.
+ *
+ * @extends module:ui/view~View
+ */
+export default class ColorTableView extends View {
+	/**
+	 * Creates a view to be inserted as child of {@link module:ui/dropdown/dropdownview~DropdownView}.
+	 *
+	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
+	 * @param {Object} config Configuration object
+	 * @param {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>} config.colors Array with definitions of colors to
+	 * be displayed in the table.
+	 * @param {Number} config.columns Number of columns in the color grid.
+	 * Also determines how many recent color will be displayed.
+	 * @param {String} config.removeButtonLabel A label of a button responsible for removing the color.
+	 */
+	constructor( locale, { colors, columns, removeButtonLabel } ) {
+		super( locale );
+
+		/**
+		 * Collection of the children of the table.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.items = this.createCollection();
+
+		/**
+		 * An array with objects representing colors to be displayed in the grid.
+		 *
+		 * @type {Arrray.<module:ui/colorgrid/colorgrid~ColorDefinition>}
+		 */
+		this.colorDefinitions = colors;
+
+		/**
+		 * Tracks information about DOM focus in the list.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
+		/**
+		 * Keeps value of the command associated with the table for current selection.
+		 *
+		 * @type {String}
+		 */
+		this.set( 'selectedColor' );
+
+		/**
+		 * A label of the button responsible for removing color attributes.
+		 *
+		 * @type {String}
+		 */
+		this.removeButtonLabel = removeButtonLabel;
+
+		/**
+		 * The number of columns in color grid. Also determines the number of recent color to be displayed.
+		 *
+		 * @type {Number}
+		 */
+		this.columns = columns;
+
+		/**
+		 * A collection storing definitions of recently used colors.
+		 *
+		 * @readonly
+		 * @member {module:utils/collection~Collection}
+		 */
+		this.recentlyUsedColors = new Collection();
+
+		/**
+		 * Helps cycling over focusable {@link #items} in the list.
+		 *
+		 * @readonly
+		 * @protected
+		 * @member {module:ui/focuscycler~FocusCycler}
+		 */
+		this._focusCycler = new FocusCycler( {
+			focusables: this.items,
+			focusTracker: this.focusTracker,
+			keystrokeHandler: this.keystrokes,
+			actions: {
+				// Navigate list items backwards using the arrowup key.
+				focusPrevious: 'arrowup',
+
+				// Navigate list items forwards using the arrowdown key.
+				focusNext: 'arrowdown',
+			}
+		} );
+
+		this.initRecentCollection();
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					'ck-color-table'
+				]
+			},
+			children: this.items
+		} );
+
+		this.items.add( this.removeColorButton() );
+		this.items.add( this.createStaticColorTable() );
+		this.items.add( this.recentlyUsed() );
+	}
+
+	/**
+	 * Adds the remove color button as child for current view.
+	 *
+	 * @private
+	 */
+	removeColorButton() {
+		const buttonView = new ButtonView();
+
+		buttonView.set( {
+			withText: true,
+			icon: removeButtonIcon,
+			tooltip: true,
+			label: this.removeButtonLabel
+		} );
+
+		buttonView.class = 'ck-color-table__remove-color';
+		buttonView.on( 'execute', () => {
+			this.fire( 'execute', { value: null } );
+		} );
+
+		return buttonView;
+	}
+
+	/**
+	 * Creates a static color table grid based on editor config.
+	 *
+	 * @private
+	 */
+	createStaticColorTable() {
+		const colorGrid = new ColorGridView( this.locale, {
+			colorDefinitions: this.colorDefinitions,
+			columns: this.columns
+		} );
+
+		colorGrid.delegate( 'execute' ).to( this );
+		colorGrid.bind( 'selectedColor' ).to( this );
+
+		return colorGrid;
+	}
+
+	/**
+	 * Adds recently used colors section view and binds it to {@link #recentlyUsedColors}.
+	 *
+	 * @private
+	 */
+	recentlyUsed() {
+		const recentViews = new ColorGridView( this.locale, { columns: this.columns } );
+
+		recentViews.items.bindTo( this.recentlyUsedColors ).using(
+			colorObj => {
+				const colorTile = new ColorTileView();
+
+				colorTile.set( {
+					color: colorObj.color,
+					hasBorder: colorObj.hasBorder
+				} );
+
+				if ( colorObj.label ) {
+					colorTile.set( {
+						label: colorObj.label,
+						tooltip: true
+					} );
+				}
+
+				if ( colorObj.isEnabled === false ) {
+					colorTile.set( 'isEnabled', false );
+				}
+
+				colorTile.on( 'execute', () => {
+					this.fire( 'execute', {
+						value: colorObj.color,
+						hasBorder: colorObj.hasBorder,
+						label: colorObj.label
+					} );
+				} );
+
+				return colorTile;
+			}
+		);
+
+		this.recentlyUsedColors.on( 'add', ( evt, item ) => {
+			const duplicates = this.recentlyUsedColors.filter( element => element.color === item.color, this );
+
+			if ( duplicates.length === 2 ) {
+				this.recentlyUsedColors.remove( duplicates[ 1 ] );
+			}
+
+			if ( this.recentlyUsedColors.length > this.columns ) {
+				this.recentlyUsedColors.remove( this.recentlyUsedColors.length - 1 );
+			}
+		} );
+
+		recentViews.delegate( 'execute' ).to( this );
+
+		return recentViews;
+	}
+
+	/**
+	 * Populates {@link #recentlyUsedColors} with empty non-clickable buttons, which represent placeholders
+	 * for colors.
+	 *
+	 * @private
+	 */
+	initRecentCollection() {
+		for ( let i = 0; i < this.columns; i++ ) {
+			this.recentlyUsedColors.add( {
+				color: 'hsla(0, 0%, 0%, 0)',
+				isEnabled: false,
+				hasBorder: true
+			} );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		// Items added before rendering should be known to the #focusTracker.
+		for ( const item of this.items ) {
+			this.focusTracker.add( item.element );
+		}
+
+		// Start listening for the keystrokes coming from #element.
+		this.keystrokes.listenTo( this.element );
+	}
+
+	/**
+	 * Focuses the first focusable in {@link #items}.
+	 */
+	focus() {
+		this._focusCycler.focusFirst();
+	}
+
+	/**
+	 * Focuses the last focusable in {@link #items}.
+	 */
+	focusLast() {
+		this._focusCycler.focusLast();
+	}
+}

+ 131 - 0
packages/ckeditor5-font/src/ui/colorui.js

@@ -0,0 +1,131 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module font/ui/colorui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import { createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+import {
+	addColorTableToDropdown,
+	normalizeColorOptions,
+	getLocalizedColorOptions
+} from '../utils';
+
+/**
+ * The color UI plugin which isolates the common logic responsible for displaying dropdowns with color grids.
+ *
+ * It is used to create the `'fontBackgroundColor'` and the `'fontColor'` dropdowns, each hosting
+ * a {@link module:font/ui/colortableview~ColorTableView}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ColorUI extends Plugin {
+	/**
+	 * Creates a plugin which brings dropdown with a pre–configured {@link module:font/ui/colortableview~ColorTableView}
+	 *
+	 * @param {module:core/editor/editor~Editor} editor
+	 * @param {Object} config Configuration object
+	 * @param {String} config.commandName Name of command which will be executed when a color tile is clicked.
+	 * @param {String} config.componentName Name of the dropdown in the {@link module:ui/componentfactory~ComponentFactory}
+	 * and the configuration scope name in `editor.config`.
+	 * @param {String} config.icon SVG icon used by the dropdown.
+	 * @param {String} config.dropdownLabel Label used by the dropdown.
+	 */
+	constructor( editor, { commandName, icon, componentName, dropdownLabel } ) {
+		super( editor );
+
+		/**
+		 * Name of the command which will be executed when a color tile is clicked.
+		 * @type {String}
+		 */
+		this.commandName = commandName;
+
+		/**
+		 * Name of this component in the {@link module:ui/componentfactory~ComponentFactory}.
+		 * Also the configuration scope name in `editor.config`.
+		 * @type {String}
+		 */
+		this.componentName = componentName;
+
+		/**
+		 * SVG icon used by the dropdown.
+		 * @type {String}
+		 */
+		this.icon = icon;
+
+		/**
+		 * Label used by the dropdown.
+		 * @type {String}
+		 */
+		this.dropdownLabel = dropdownLabel;
+
+		/**
+		 * Number of columns in color grid. Determines the number of recent colors to be displayed.
+		 * @type {Number}
+		 */
+		this.columns = editor.config.get( `${ this.componentName }.columns` );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+		const command = editor.commands.get( this.commandName );
+		const colorsConfig = normalizeColorOptions( editor.config.get( this.componentName ).colors );
+		const localizedColors = getLocalizedColorOptions( editor, colorsConfig );
+
+		// Register UI component.
+		editor.ui.componentFactory.add( this.componentName, locale => {
+			const dropdownView = createDropdown( locale );
+			const colorTableView = addColorTableToDropdown( {
+				dropdownView,
+				colors: localizedColors.map( option => ( {
+					label: option.label,
+					color: option.model,
+					options: {
+						hasBorder: option.hasBorder
+					}
+				} ) ),
+				columns: this.columns,
+				removeButtonLabel: t( 'Remove color' )
+			} );
+
+			colorTableView.bind( 'selectedColor' ).to( command, 'value' );
+
+			dropdownView.buttonView.set( {
+				label: this.dropdownLabel,
+				icon: this.icon,
+				tooltip: true
+			} );
+
+			dropdownView.extendTemplate( {
+				attributes: {
+					class: 'ck-color-ui-dropdown'
+				}
+			} );
+
+			dropdownView.bind( 'isEnabled' ).to( command );
+
+			dropdownView.on( 'execute', ( evt, data ) => {
+				if ( data.value !== null ) {
+					colorTableView.recentlyUsedColors.add( {
+						color: data.value,
+						hasBorder: data.hasBorder,
+						label: data.label
+					}, 0 );
+				}
+
+				editor.execute( this.commandName, data );
+				editor.editing.view.focus();
+			} );
+
+			return dropdownView;
+		} );
+	}
+}

+ 172 - 0
packages/ckeditor5-font/src/utils.js

@@ -7,6 +7,28 @@
  * @module font/utils
  */
 
+import ColorTableView from './ui/colortableview';
+
+/**
+ * Name of the font size plugin.
+ */
+export const FONT_SIZE = 'fontSize';
+
+/**
+ * Name of the font family plugin.
+ */
+export const FONT_FAMILY = 'fontFamily';
+
+/**
+ * Name of the font color plugin.
+ */
+export const FONT_COLOR = 'fontColor';
+
+/**
+ * Name of font font background color plugin.
+ */
+export const FONT_BACKGROUND_COLOR = 'fontBackgroundColor';
+
 /**
  * Builds a proper {@link module:engine/conversion/conversion~ConverterDefinition converter definition} out of input data.
  *
@@ -35,3 +57,153 @@ export function buildDefinition( modelAttributeKey, options ) {
 
 	return definition;
 }
+
+/**
+ * A {@link module:font/fontcolor~FontColor font color} and
+ * {@link module:font/fontbackgroundcolor~FontBackgroundColor font background color} helper
+ * responsible for upcasting data to model.
+ *
+ * **Note**: `styleAttr` should be either `'color'` or `'background-color'`.
+ *
+ * @param {String} styleAttr
+ * @return {String}
+ */
+export function renderUpcastAttribute( styleAttr ) {
+	return viewElement => normalizeColorCode( viewElement.getStyle( styleAttr ) );
+}
+
+/**
+ * A {@link module:font/fontcolor~FontColor font color} and
+ * {@link module:font/fontbackgroundcolor~FontBackgroundColor font background color} helper
+ * responsible for downcasting a color attribute to a span element.
+ *
+ * **Note**: `styleAttr` should be either `'color'` or `'background-color'`.
+ *
+ * @param {String} styleAttr
+ */
+export function renderDowncastElement( styleAttr ) {
+	return ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'span', {
+		style: `${ styleAttr }:${ modelAttributeValue }`
+	} );
+}
+
+/**
+ * Creates a unified color definition object from color configuration options.
+ * The object contains both the information necessary to render the UI and initialize a conversion.
+ *
+ * @param {module:ui/colorgrid/colorgrid~ColorDefinition} options
+ * @returns {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>}
+ */
+export function normalizeColorOptions( options ) {
+	return options
+		.map( normalizeSingleColorDefinition )
+		.filter( option => !!option );
+}
+
+/**
+ * Helper which add {@link module:font/ui/colortableview~ColorTableView} to a dropdown with proper initial values.
+ *
+ * @param {Object} config Configuration object
+ * @param {module:ui/dropdown/dropdownview~DropdownView} config.dropdownView Dropdown view to which
+ * a {@link module:font/ui/colortableview~ColorTableView} will be added.
+ * @param {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>} config.colors An array with definitions
+ * representing colors to be displayed in the color table.
+ * @param {String} config.removeButtonLabel A label of a button responsible for removing the color.
+ * @returns {module:font/ui/colortableview~ColorTableView} The new color table view.
+ */
+export function addColorTableToDropdown( { dropdownView, colors, columns, removeButtonLabel } ) {
+	const locale = dropdownView.locale;
+	const colorTableView = new ColorTableView( locale, { colors, columns, removeButtonLabel } );
+
+	dropdownView.colorTableView = colorTableView;
+	dropdownView.panelView.children.add( colorTableView );
+
+	colorTableView.delegate( 'execute' ).to( dropdownView, 'execute' );
+
+	return colorTableView;
+}
+
+/**
+ * Returns color configuration options as defined in the `editor.config.(fontColor|fontBackgroundColor).colors`
+ * but processed to account for editor localization, i.e. to display {@link module:font/fontcolor~FontColorConfig}
+ * or {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig} in the correct language.
+ *
+ * Note: The reason behind this method is that there is no way to use {@link module:utils/locale~Locale#t}
+ * when the user configuration is defined because the editor does not exist yet.
+ *
+ * @param {module:core/editor/editor~Editor} editor An editor instance.
+ * @param {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>} options
+ * @returns {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>}.
+ */
+export function getLocalizedColorOptions( editor, options ) {
+	const t = editor.t;
+	const localizedColorNames = {
+		Black: t( 'Black' ),
+		'Dim grey': t( 'Dim grey' ),
+		Grey: t( 'Grey' ),
+		'Light grey': t( 'Light grey' ),
+		White: t( 'White' ),
+		Red: t( 'Red' ),
+		Orange: t( 'Orange' ),
+		Yellow: t( 'Yellow' ),
+		'Light green': t( 'Light green' ),
+		Green: t( 'Green' ),
+		Aquamarine: t( 'Aquamarine' ),
+		Turquoise: t( 'Turquoise' ),
+		'Light blue': t( 'Light blue' ),
+		Blue: t( 'Blue' ),
+		Purple: t( 'Purple' )
+	};
+
+	return options.map( colorOption => {
+		const label = localizedColorNames[ colorOption.label ];
+
+		if ( label && label != colorOption.label ) {
+			colorOption.label = label;
+		}
+
+		return colorOption;
+	} );
+}
+
+// Fixes color value string
+//
+// @param {String} value
+// @returns {String}
+function normalizeColorCode( value ) {
+	return value.replace( /\s/g, '' );
+}
+
+// Creates normalized color definition from user defined configuration.
+//
+// @param {String|module:ui/colorgrid/colorgrid~ColorDefinition}
+// @returns {module:ui/colorgrid/colorgrid~ColorDefinition}
+function normalizeSingleColorDefinition( color ) {
+	if ( typeof color === 'string' ) {
+		return {
+			model: color.replace( / /g, '' ),
+			label: color,
+			hasBorder: false,
+			view: {
+				name: 'span',
+				styles: {
+					color
+				},
+				priority: 5
+			}
+		};
+	} else {
+		return {
+			model: color.color.replace( / /g, '' ),
+			label: color.label || color.color,
+			hasBorder: color.hasBorder === undefined ? false : color.hasBorder,
+			view: {
+				name: 'span',
+				styles: {
+					color: `${ color.color }`
+				},
+				priority: 5
+			}
+		};
+	}
+}

+ 4 - 2
packages/ckeditor5-font/tests/font.js

@@ -6,10 +6,12 @@
 import Font from './../src/font';
 import FontFamily from './../src/fontfamily';
 import FontSize from './../src/fontsize';
+import FontColor from './../src/fontcolor';
+import FontBackgroundColor from './../src/fontbackgroundcolor';
 
 describe( 'Font', () => {
-	it( 'requires FontSize', () => {
-		expect( Font.requires ).to.deep.equal( [ FontFamily, FontSize ] );
+	it( 'requires FontFamily, FontSize, FontColor, FontBackgroundColor', () => {
+		expect( Font.requires ).to.deep.equal( [ FontFamily, FontSize, FontColor, FontBackgroundColor ] );
 	} );
 
 	it( 'defines plugin name', () => {

+ 18 - 0
packages/ckeditor5-font/tests/fontbackgroundcolor.js

@@ -0,0 +1,18 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import FontBackgroundColor from './../src/fontbackgroundcolor';
+import FontBackgroundColorEditing from './../src/fontbackgroundcolor/fontbackgroundcolorediting';
+import FontBackgroundColorUI from '../src/fontbackgroundcolor/fontbackgroundcolorui';
+
+describe( 'FontBackgroundColor', () => {
+	it( 'requires FontBackgroundColorEditing and FontBackgroundColorUI', () => {
+		expect( FontBackgroundColor.requires ).to.deep.equal( [ FontBackgroundColorEditing, FontBackgroundColorUI ] );
+	} );
+
+	it( 'defines plugin name', () => {
+		expect( FontBackgroundColor.pluginName ).to.equal( 'FontBackgroundColor' );
+	} );
+} );

+ 34 - 0
packages/ckeditor5-font/tests/fontbackgroundcolor/fontbackgroundcolorcommand.js

@@ -0,0 +1,34 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import FontBackgroundColorCommand from '../../src/fontbackgroundcolor/fontbackgroundcolorcommand';
+import FontCommand from '../../src/fontcommand';
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+
+describe( 'FontBackgroundColorCommand', () => {
+	let editor, command;
+
+	beforeEach( () => {
+		return ModelTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+
+				command = new FontBackgroundColorCommand( editor );
+			} );
+	} );
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	it( 'is a FontCommand', () => {
+		expect( FontBackgroundColorCommand.prototype ).to.be.instanceOf( FontCommand );
+		expect( command ).to.be.instanceOf( FontCommand );
+	} );
+
+	it( 'operates on fontBackgroundColor attribute', () => {
+		expect( command ).to.have.property( 'attributeKey', 'fontBackgroundColor' );
+	} );
+} );

+ 257 - 0
packages/ckeditor5-font/tests/fontbackgroundcolor/fontbackgroundcoloreditng.js

@@ -0,0 +1,257 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import FontBackgroundColorEditing from './../../src/fontbackgroundcolor/fontbackgroundcolorediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import {
+	getData as getModelData,
+	setData as setModelData
+} from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'FontBackgroundColorEditing', () => {
+	let editor, doc;
+
+	beforeEach( () => VirtualTestEditor
+		.create( {
+			plugins: [ FontBackgroundColorEditing, Paragraph ]
+		} )
+		.then( newEditor => {
+			editor = newEditor;
+
+			doc = editor.document;
+		} )
+	);
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	it( 'should set proper schema rules', () => {
+		expect( editor.model.schema.checkAttribute( [ '$block', '$text' ], 'fontBackgroundColor' ) ).to.be.true;
+		expect( editor.model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'fontBackgroundColor' ) ).to.be.true;
+
+		expect( editor.model.schema.checkAttribute( [ '$block' ], 'fontBackgroundColor' ) ).to.be.false;
+	} );
+
+	describe( 'config', () => {
+		describe( 'default value', () => {
+			it( 'should be set', () => {
+				expect( editor.config.get( 'fontBackgroundColor.colors' ) ).to.deep.equal( [
+					{
+						color: 'hsl(0, 0%, 0%)',
+						label: 'Black'
+					},
+					{
+						color: 'hsl(0, 0%, 30%)',
+						label: 'Dim grey'
+					},
+					{
+						color: 'hsl(0, 0%, 60%)',
+						label: 'Grey'
+					},
+					{
+						color: 'hsl(0, 0%, 90%)',
+						label: 'Light grey'
+					},
+					{
+						color: 'hsl(0, 0%, 100%)',
+						label: 'White',
+						hasBorder: true
+					},
+					{
+						color: 'hsl(0, 75%, 60%)',
+						label: 'Red'
+					},
+					{
+						color: 'hsl(30, 75%, 60%)',
+						label: 'Orange'
+					},
+					{
+						color: 'hsl(60, 75%, 60%)',
+						label: 'Yellow'
+					},
+					{
+						color: 'hsl(90, 75%, 60%)',
+						label: 'Light green'
+					},
+					{
+						color: 'hsl(120, 75%, 60%)',
+						label: 'Green'
+					},
+					{
+						color: 'hsl(150, 75%, 60%)',
+						label: 'Aquamarine'
+					},
+					{
+						color: 'hsl(180, 75%, 60%)',
+						label: 'Turquoise'
+					},
+					{
+						color: 'hsl(210, 75%, 60%)',
+						label: 'Light blue'
+					},
+					{
+						color: 'hsl(240, 75%, 60%)',
+						label: 'Blue'
+					},
+					{
+						color: 'hsl(270, 75%, 60%)',
+						label: 'Purple'
+					}
+				] );
+				expect( editor.config.get( 'fontBackgroundColor.columns' ) ).to.equal( 5 );
+			} );
+		} );
+	} );
+
+	describe( 'editing pipeline conversion', () => {
+		beforeEach( () => VirtualTestEditor
+			.create( {
+				plugins: [ FontBackgroundColorEditing, Paragraph ],
+				fontBackgroundColor: {
+					colors: [
+						{
+							label: 'Color1',
+							color: '#000'
+						},
+						{
+							label: 'Color2',
+							color: '#123456'
+						},
+						{
+							label: 'Color3',
+							color: 'rgb( 0, 10, 20 )'
+						},
+						'hsl( 200,100%,50%)',
+						{
+							label: 'Color5 - Light Green',
+							color: 'lightgreen'
+						}
+					]
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				doc = editor.model;
+			} )
+		);
+
+		describe( 'convert different color version', () => {
+			const tests = [
+				'#000',
+				'green',
+				'rgb( 0, 10, 20 )',
+				'rgba( 20, 30, 50, 0.4)',
+				'hsl( 10, 20%, 30%)',
+				'hsla( 300, 50%, 100%, .3)',
+				'rgb( 20%, 30%, 40% )',
+				'#345678'
+			];
+
+			tests.forEach( test => {
+				it( `should convert fontBackgroundColor attribute: "${ test }" to proper style value.`, () => {
+					setModelData( doc, `<paragraph>fo<$text fontBackgroundColor="${ test }">o b</$text>ar</paragraph>` );
+
+					expect( editor.getData() ).to.equal( `<p>fo<span style="background-color:${ test };">o b</span>ar</p>` );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'data pipeline conversions', () => {
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ FontBackgroundColorEditing, Paragraph ],
+					fontBackgroundColor: {
+						colors: [
+							{
+								label: 'Color1',
+								color: '#000'
+							},
+							{
+								label: 'Color2',
+								color: '#123456'
+							},
+							{
+								label: 'Color3',
+								color: 'rgb( 0, 10, 20 )'
+							},
+							'hsl( 200,100%,50%)',
+							{
+								label: 'Color5 - Light Green',
+								color: 'lightgreen'
+							}
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					doc = editor.model;
+				} );
+		} );
+
+		it( 'should convert from element with defined style when with other styles', () => {
+			const data = '<p>f<span style="font-size: 18px;background-color: rgb(10, 20, 30);">o</span>o</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph>[]f<$text fontBackgroundColor="rgb(10,20,30)">o</$text>o</paragraph>' );
+
+			expect( editor.getData() ).to.equal( '<p>f<span style="background-color:rgb(10,20,30);">o</span>o</p>' );
+		} );
+
+		describe( 'should convert from different color versions', () => {
+			const tests = [
+				'#000',
+				'green',
+				'rgb( 0, 10, 20 )',
+				'rgba( 20, 30, 50, 0.4)',
+				'hsl( 10, 20%, 30%)',
+				'hsla( 300, 50%, 100%, .3)',
+				'rgb( 20%, 30%, 40% )',
+				'#345678'
+			];
+
+			tests.forEach( test => {
+				it( `should convert fontBackgroundColor attribute: "${ test }" to proper style value.`, () => {
+					const data = `<p>f<span style="background-color: ${ test }">o</span>o</p>`;
+					editor.setData( data );
+
+					expect( getModelData( doc ) )
+						.to.equal( `<paragraph>[]f<$text fontBackgroundColor="${ test.replace( / /g, '' ) }">o</$text>o</paragraph>` );
+
+					expect( editor.getData() )
+						.to.equal( `<p>f<span style="background-color:${ test.replace( / /g, '' ) };">o</span>o</p>` );
+				} );
+			} );
+		} );
+
+		it( 'should convert from complex definition', () => {
+			editor.setData(
+				'<p>f<span style="background-color: lightgreen;">o</span>o</p>' +
+				'<p>f<span style="background-color: hsl( 200, 100%, 50% );">o</span>o</p>' +
+				'<p>b<span style="background-color: rgba(1,2,3,.4);">a</span>r</p>' +
+				'<p>b<span style="background-color:#fff;">a</span>z</p>'
+			);
+
+			expect( getModelData( doc ) ).to.equal(
+				'<paragraph>[]f<$text fontBackgroundColor="lightgreen">o</$text>o</paragraph>' +
+				'<paragraph>f<$text fontBackgroundColor="hsl(200,100%,50%)">o</$text>o</paragraph>' +
+				'<paragraph>b<$text fontBackgroundColor="rgba(1,2,3,.4)">a</$text>r</paragraph>' +
+				'<paragraph>b<$text fontBackgroundColor="#fff">a</$text>z</paragraph>'
+			);
+
+			expect( editor.getData() ).to.equal(
+				'<p>f<span style="background-color:lightgreen;">o</span>o</p>' +
+				'<p>f<span style="background-color:hsl(200,100%,50%);">o</span>o</p>' +
+				'<p>b<span style="background-color:rgba(1,2,3,.4);">a</span>r</p>' +
+				'<p>b<span style="background-color:#fff;">a</span>z</p>'
+			);
+		} );
+	} );
+} );

+ 49 - 0
packages/ckeditor5-font/tests/fontbackgroundcolor/fontbackgroundcolorui.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import FontBackgroundColorEditing from './../../src/fontbackgroundcolor/fontbackgroundcolorediting';
+import FontBackgroundColorUI from './../../src/fontbackgroundcolor/fontbackgroundcolorui';
+import ColorUI from './../../src/ui/colorui';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import fontBackgroundColorIcon from '../../theme/icons/font-background.svg';
+
+describe( 'FontBckgroundColorUI', () => {
+	let element, editor;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [ FontBackgroundColorEditing, FontBackgroundColorUI ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'is ColorUI', () => {
+		expect( FontBackgroundColorUI.prototype ).to.be.instanceOf( ColorUI );
+	} );
+
+	it( 'has properly set initial values', () => {
+		const fontBackgroundColorUIPlugin = editor.plugins.get( 'FontBackgroundColorUI' );
+
+		expect( fontBackgroundColorUIPlugin.commandName ).to.equal( 'fontBackgroundColor' );
+		expect( fontBackgroundColorUIPlugin.componentName ).to.equal( 'fontBackgroundColor' );
+		expect( fontBackgroundColorUIPlugin.icon ).to.equal( fontBackgroundColorIcon );
+		expect( fontBackgroundColorUIPlugin.dropdownLabel ).to.equal( 'Font Background Color' );
+		expect( fontBackgroundColorUIPlugin.columns ).to.equal( 5 );
+	} );
+} );

+ 18 - 0
packages/ckeditor5-font/tests/fontcolor.js

@@ -0,0 +1,18 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import FontColor from './../src/fontcolor';
+import FontColorEditing from './../src/fontcolor/fontcolorediting';
+import FontColorUI from '../src/fontcolor/fontcolorui';
+
+describe( 'FontColor', () => {
+	it( 'requires FontColorEditing and FontColorUI', () => {
+		expect( FontColor.requires ).to.deep.equal( [ FontColorEditing, FontColorUI ] );
+	} );
+
+	it( 'defines plugin name', () => {
+		expect( FontColor.pluginName ).to.equal( 'FontColor' );
+	} );
+} );

+ 34 - 0
packages/ckeditor5-font/tests/fontcolor/fontcolorcommand.js

@@ -0,0 +1,34 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import FontColorCommand from '../../src/fontcolor/fontcolorcommand';
+import FontCommand from '../../src/fontcommand';
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+
+describe( 'FontColorCommand', () => {
+	let editor, command;
+
+	beforeEach( () => {
+		return ModelTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+
+				command = new FontColorCommand( editor );
+			} );
+	} );
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	it( 'is a FontCommand', () => {
+		expect( FontColorCommand.prototype ).to.be.instanceOf( FontCommand );
+		expect( command ).to.be.instanceOf( FontCommand );
+	} );
+
+	it( 'operates on fontColor attribute', () => {
+		expect( command ).to.have.property( 'attributeKey', 'fontColor' );
+	} );
+} );

+ 253 - 0
packages/ckeditor5-font/tests/fontcolor/fontcolorediting.js

@@ -0,0 +1,253 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import FontColorEditing from './../../src/fontcolor/fontcolorediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import {
+	getData as getModelData,
+	setData as setModelData
+} from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'FontColorEditing', () => {
+	let editor, doc;
+
+	beforeEach( () => VirtualTestEditor
+		.create( {
+			plugins: [ FontColorEditing, Paragraph ]
+		} )
+		.then( newEditor => {
+			editor = newEditor;
+
+			doc = editor.document;
+		} )
+	);
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	it( 'should set proper schema rules', () => {
+		expect( editor.model.schema.checkAttribute( [ '$block', '$text' ], 'fontColor' ) ).to.be.true;
+		expect( editor.model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'fontColor' ) ).to.be.true;
+		expect( editor.model.schema.checkAttribute( [ '$block' ], 'fontColor' ) ).to.be.false;
+	} );
+
+	describe( 'config', () => {
+		describe( 'default value', () => {
+			it( 'should be set', () => {
+				expect( editor.config.get( 'fontColor.colors' ) ).to.deep.equal( [
+					{
+						color: 'hsl(0, 0%, 0%)',
+						label: 'Black'
+					},
+					{
+						color: 'hsl(0, 0%, 30%)',
+						label: 'Dim grey'
+					},
+					{
+						color: 'hsl(0, 0%, 60%)',
+						label: 'Grey'
+					},
+					{
+						color: 'hsl(0, 0%, 90%)',
+						label: 'Light grey'
+					},
+					{
+						color: 'hsl(0, 0%, 100%)',
+						label: 'White',
+						hasBorder: true
+					},
+					{
+						color: 'hsl(0, 75%, 60%)',
+						label: 'Red'
+					},
+					{
+						color: 'hsl(30, 75%, 60%)',
+						label: 'Orange'
+					},
+					{
+						color: 'hsl(60, 75%, 60%)',
+						label: 'Yellow'
+					},
+					{
+						color: 'hsl(90, 75%, 60%)',
+						label: 'Light green'
+					},
+					{
+						color: 'hsl(120, 75%, 60%)',
+						label: 'Green'
+					},
+					{
+						color: 'hsl(150, 75%, 60%)',
+						label: 'Aquamarine'
+					},
+					{
+						color: 'hsl(180, 75%, 60%)',
+						label: 'Turquoise'
+					},
+					{
+						color: 'hsl(210, 75%, 60%)',
+						label: 'Light blue'
+					},
+					{
+						color: 'hsl(240, 75%, 60%)',
+						label: 'Blue'
+					},
+					{
+						color: 'hsl(270, 75%, 60%)',
+						label: 'Purple'
+					}
+				] );
+				expect( editor.config.get( 'fontColor.columns' ) ).to.equal( 5 );
+			} );
+		} );
+	} );
+
+	describe( 'editing pipeline conversion', () => {
+		beforeEach( () => VirtualTestEditor
+			.create( {
+				plugins: [ FontColorEditing, Paragraph ],
+				fontColor: {
+					colors: [
+						{
+							label: 'Color1',
+							color: '#000'
+						},
+						{
+							label: 'Color2',
+							color: '#123456'
+						},
+						{
+							label: 'Color3',
+							color: 'rgb( 0, 10, 20 )'
+						},
+						'hsl( 200,100%,50%)',
+						{
+							label: 'Color5 - Light Green',
+							color: 'lightgreen'
+						}
+					]
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				doc = editor.model;
+			} )
+		);
+
+		describe( 'convert different color version', () => {
+			const tests = [
+				'#000',
+				'green',
+				'rgb( 0, 10, 20 )',
+				'rgba( 20, 30, 50, 0.4)',
+				'hsl( 10, 20%, 30%)',
+				'hsla( 300, 50%, 100%, .3)',
+				'rgb( 20%, 30%, 40% )',
+				'#345678'
+			];
+			tests.forEach( test => {
+				it( `should convert fontColor attribute: "${ test }" to proper style value.`, () => {
+					setModelData( doc, `<paragraph>fo<$text fontColor="${ test }">o b</$text>ar</paragraph>` );
+
+					expect( editor.getData() ).to.equal( `<p>fo<span style="color:${ test };">o b</span>ar</p>` );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'data pipeline conversions', () => {
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ FontColorEditing, Paragraph ],
+					fontColor: {
+						colors: [
+							{
+								label: 'Color1',
+								color: '#000'
+							},
+							{
+								label: 'Color2',
+								color: '#123456'
+							},
+							{
+								label: 'Color3',
+								color: 'rgb( 0, 10, 20 )'
+							},
+							'hsl( 200,100%,50%)',
+							{
+								label: 'Color5 - Light Green',
+								color: 'lightgreen'
+							}
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					doc = editor.model;
+				} );
+		} );
+
+		it( 'should convert from element with defined style when with other styles', () => {
+			const data = '<p>f<span style="font-size: 18px;color: rgb(10, 20, 30);">o</span>o</p>';
+
+			editor.setData( data );
+
+			expect( getModelData( doc ) ).to.equal( '<paragraph>[]f<$text fontColor="rgb(10,20,30)">o</$text>o</paragraph>' );
+			expect( editor.getData() ).to.equal( '<p>f<span style="color:rgb(10,20,30);">o</span>o</p>' );
+		} );
+
+		describe( 'should convert from different color versions', () => {
+			const tests = [
+				'#000',
+				'green',
+				'rgb( 0, 10, 20 )',
+				'rgba( 20, 30, 50, 0.4)',
+				'hsl( 10, 20%, 30%)',
+				'hsla( 300, 50%, 100%, .3)',
+				'rgb( 20%, 30%, 40% )',
+				'#345678'
+			];
+
+			tests.forEach( test => {
+				it( `should convert fontColor attribute: "${ test }" to proper style value.`, () => {
+					const data = `<p>f<span style="color: ${ test }">o</span>o</p>`;
+					editor.setData( data );
+
+					expect( getModelData( doc ) )
+						.to.equal( `<paragraph>[]f<$text fontColor="${ test.replace( / /g, '' ) }">o</$text>o</paragraph>` );
+
+					expect( editor.getData() ).to.equal( `<p>f<span style="color:${ test.replace( / /g, '' ) };">o</span>o</p>` );
+				} );
+			} );
+		} );
+
+		it( 'should convert from complex definition', () => {
+			editor.setData(
+				'<p>f<span style="color: lightgreen;">o</span>o</p>' +
+				'<p>f<span style="color: hsl( 200, 100%, 50% );">o</span>o</p>' +
+				'<p>b<span style="color: rgba(1,2,3,.4);">a</span>r</p>' +
+				'<p>b<span style="color:#fff;">a</span>z</p>'
+			);
+
+			expect( getModelData( doc ) ).to.equal(
+				'<paragraph>[]f<$text fontColor="lightgreen">o</$text>o</paragraph>' +
+				'<paragraph>f<$text fontColor="hsl(200,100%,50%)">o</$text>o</paragraph>' +
+				'<paragraph>b<$text fontColor="rgba(1,2,3,.4)">a</$text>r</paragraph>' +
+				'<paragraph>b<$text fontColor="#fff">a</$text>z</paragraph>'
+			);
+
+			expect( editor.getData() ).to.equal(
+				'<p>f<span style="color:lightgreen;">o</span>o</p>' +
+				'<p>f<span style="color:hsl(200,100%,50%);">o</span>o</p>' +
+				'<p>b<span style="color:rgba(1,2,3,.4);">a</span>r</p>' +
+				'<p>b<span style="color:#fff;">a</span>z</p>'
+			);
+		} );
+	} );
+} );

+ 49 - 0
packages/ckeditor5-font/tests/fontcolor/fontcolorui.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import FontColorEditing from './../../src/fontcolor/fontcolorediting';
+import FontColorUI from './../../src/fontcolor/fontcolorui';
+import ColorUI from './../../src/ui/colorui';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import fontColorIcon from '../../theme/icons/font-color.svg';
+
+describe( 'FontColorUI', () => {
+	let element, editor;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [ FontColorEditing, FontColorUI ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'is ColorUI', () => {
+		expect( FontColorUI.prototype ).to.be.instanceOf( ColorUI );
+	} );
+
+	it( 'has properly set initial values', () => {
+		const fontColorUIPlugin = editor.plugins.get( 'FontColorUI' );
+
+		expect( fontColorUIPlugin.commandName ).to.equal( 'fontColor' );
+		expect( fontColorUIPlugin.componentName ).to.equal( 'fontColor' );
+		expect( fontColorUIPlugin.icon ).to.equal( fontColorIcon );
+		expect( fontColorUIPlugin.dropdownLabel ).to.equal( 'Font Color' );
+		expect( fontColorUIPlugin.columns ).to.equal( 5 );
+	} );
+} );

+ 12 - 0
packages/ckeditor5-font/tests/manual/font-color.html

@@ -0,0 +1,12 @@
+<div id="editor">
+	<p><span>01. no-color; no-color</span></p>
+	<p><span style="color:hsl(0, 0%, 100%);background-color:hsl(0, 0%, 0%);">02. color:hsl(0, 0%, 100%);background-color:hsl(0, 0%, 0%);</span></p>
+	<p><span style="color:hsl(0, 75%, 60%);">03. color:hsl(0, 75%, 60%); no-color;</span></p>
+	<p><span style="background-color:hsl(90, 75%, 60%);">04. no-color; background-color:hsl(90, 75%, 60%);</span></p>
+	<p><span style="color:hsl(30, 75%, 60%);background-color:hsl(0, 0%, 30%);">05. color:hsl(30, 75%, 60%); background-color:hsl(0, 0%, 30%);</span></p>
+	<p><span style="color:#00FFFF;background-color:rgb(255, 0, 0);">06. color:#00FFFF;background-color:rgb(255, 0, 0);</span></p>
+	<p><span style="color:hsla( 0, 0%, 0%, .7);background-color:gold;">07. color:hsla( 0, 0%, 0%, .7); background-color:gold;</span></p>
+	<p><span style="color:rgba( 0, 120, 250, 0.8);background-color:hsla(270, 100%, 50%, 0.3);">08. color:;background-color:hsla(270, 100%, 50%, 0.3);</span></p>
+	<p><span style="color:#ddd;background-color:hsl(150, 75%, 60%);">09. color:#ddd;background-color:hsl(150, 75%, 60%);</span></p>
+	<p><span style="color:hsl(270, 75%, 60%);background-color:#d82;">10. color:hsl(270, 75%, 60%);background-color:#d82;</span></p>
+</div>

+ 40 - 0
packages/ckeditor5-font/tests/manual/font-color.js

@@ -0,0 +1,40 @@
+/**
+ * @license Copyright (c) 2003-2019, 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 FontColor from '../../src/fontcolor';
+import FontBackgroundColor from '../../src/fontbackgroundcolor';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [
+			ArticlePluginSet,
+			FontColor,
+			FontBackgroundColor
+		],
+		toolbar: [
+			'heading',
+			'|',
+			'fontColor',
+			'fontBackgroundColor',
+			'bold',
+			'italic',
+			'link',
+			'bulletedList',
+			'numberedList',
+			'blockQuote',
+			'undo',
+			'redo'
+		]
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 16 - 0
packages/ckeditor5-font/tests/manual/font-color.md

@@ -0,0 +1,16 @@
+### Loading
+
+The data should be loaded with different text and background colors in the following order:
+
+* Colors 1-5 use the predefined palette,
+* Colors 6-8 use custom CSS colors, not defined in the configuration,
+* Color 9-10 use a mix from the predefined palette and custom ones.
+
+The format in the editor content is: `N. [font color]; [background color]`.
+
+### Testing
+
+- Change the font color and font background color on selected text.
+- Change the font color and font background color across many paragraphs.
+- Check whether the colors are added to recent colors list.
+- Try to re-apply a color from recent colors list: the color should move to the beginning of the list.

+ 292 - 0
packages/ckeditor5-font/tests/ui/colortableview.js

@@ -0,0 +1,292 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals Event */
+
+import ColorTableView from './../../src/ui/colortableview';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
+import removeButtonIcon from '@ckeditor/ckeditor5-core/theme/icons/eraser.svg';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'ColorTableView', () => {
+	let locale, colorTableView;
+
+	const colorDefinitions = [
+		{
+			color: '#000',
+			label: 'Black',
+			options: {
+				hasBorder: false
+			}
+		},
+		{
+			color: 'rgb(255, 255, 255)',
+			label: 'White',
+			options: {
+				hasBorder: true
+			}
+		},
+		{
+			color: 'red',
+			label: 'Red',
+			options: {
+				hasBorder: false
+			}
+		}
+	];
+
+	beforeEach( () => {
+		locale = { t() {} };
+		colorTableView = new ColorTableView( locale, {
+			colors: colorDefinitions,
+			columns: 5,
+			removeButtonLabel: 'Remove color'
+		} );
+		colorTableView.render();
+	} );
+
+	testUtils.createSinonSandbox();
+
+	describe( 'constructor()', () => {
+		it( 'creates items collection', () => {
+			expect( colorTableView.items ).to.be.instanceOf( ViewCollection );
+		} );
+
+		it( 'creates focus tracker', () => {
+			expect( colorTableView.focusTracker ).to.be.instanceOf( FocusTracker );
+		} );
+
+		it( 'creates keystroke handler', () => {
+			expect( colorTableView.keystrokes ).to.be.instanceOf( KeystrokeHandler );
+		} );
+
+		it( 'creates observable for selected color', () => {
+			expect( colorTableView.selectedColor ).to.be.undefined;
+
+			colorTableView.set( 'selectedColor', 'white' );
+
+			expect( colorTableView.selectedColor ).to.equal( 'white' );
+		} );
+
+		it( 'sets tooltip for the remove color button', () => {
+			expect( colorTableView.removeButtonLabel ).to.equal( 'Remove color' );
+		} );
+
+		it( 'sets number of drawed columns', () => {
+			expect( colorTableView.columns ).to.equal( 5 );
+		} );
+
+		it( 'creaets collection of recently used colors', () => {
+			expect( colorTableView.recentlyUsedColors ).to.be.instanceOf( Collection );
+		} );
+
+		it( 'creates focus cycler', () => {
+			expect( colorTableView._focusCycler ).to.be.instanceOf( FocusCycler );
+		} );
+
+		it( 'has proper class', () => {
+			expect( colorTableView.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( colorTableView.element.classList.contains( 'ck-color-table' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'update elements in focus tracker', () => {
+		it( 'focuses the tile in DOM', () => {
+			const spy = sinon.spy( colorTableView._focusCycler, 'focusFirst' );
+
+			colorTableView.focus();
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'focuses last the tile in DOM', () => {
+			const spy = sinon.spy( colorTableView._focusCycler, 'focusLast' );
+
+			colorTableView.focusLast();
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+
+	describe( 'remove color button', () => {
+		let removeButton;
+
+		beforeEach( () => {
+			removeButton = colorTableView.items.first;
+		} );
+
+		it( 'has proper class', () => {
+			expect( removeButton.element.classList.contains( 'ck-color-table__remove-color' ) ).to.be.true;
+		} );
+
+		it( 'has proper setting', () => {
+			expect( removeButton.withText ).to.be.true;
+			expect( removeButton.icon ).to.equal( removeButtonIcon );
+			expect( removeButton.tooltip ).to.be.true;
+			expect( removeButton.label ).to.equal( 'Remove color' );
+		} );
+
+		it( 'executes event with "null" value', () => {
+			const spy = sinon.spy();
+			colorTableView.on( 'execute', spy );
+
+			removeButton.element.dispatchEvent( new Event( 'click' ) );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWith( spy, sinon.match.any, { value: null } );
+		} );
+	} );
+
+	describe( 'static colors grid', () => {
+		let staticColorTable;
+
+		beforeEach( () => {
+			staticColorTable = colorTableView.items.get( 1 );
+		} );
+
+		it( 'has added 3 children from definition', () => {
+			expect( staticColorTable.items.length ).to.equal( 3 );
+		} );
+
+		it( 'binds #selectedColor to the table', () => {
+			colorTableView.selectedColor = 'foo';
+			expect( staticColorTable.selectedColor ).to.equal( 'foo' );
+
+			colorTableView.selectedColor = 'bar';
+			expect( staticColorTable.selectedColor ).to.equal( 'bar' );
+		} );
+
+		colorDefinitions.forEach( ( item, index ) => {
+			it( `dispatch event to parent element for color: ${ item.color }`, () => {
+				const spy = sinon.spy();
+				colorTableView.on( 'execute', spy );
+
+				staticColorTable.items.get( index ).element.dispatchEvent( new Event( 'click' ) );
+
+				sinon.assert.calledOnce( spy );
+				sinon.assert.calledWith( spy, sinon.match.any, {
+					value: item.color,
+					label: item.label,
+					hasBorder: item.options.hasBorder
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'recent colors grid', () => {
+		const colorBlack = {
+			color: '#000000',
+			label: 'Black',
+			hasBorder: false
+		};
+		const colorWhite = {
+			color: '#FFFFFF',
+			label: 'Black',
+			hasBorder: true
+		};
+		const colorRed = {
+			color: 'rgb(255,0,0)',
+			hasBorder: false
+		};
+		const emptyColor = {
+			color: 'hsla(0, 0%, 0%, 0)',
+			isEnabled: false,
+			hasBorder: true
+		};
+
+		let recentColorsGridView, recentColorModel;
+
+		beforeEach( () => {
+			recentColorModel = colorTableView.recentlyUsedColors;
+			recentColorsGridView = colorTableView.items.last;
+		} );
+
+		describe( 'initialization', () => {
+			it( 'has proper length of populated items', () => {
+				expect( recentColorModel.length ).to.equal( 5 );
+			} );
+
+			for ( let i = 0; i < 5; i++ ) {
+				it( `initialized item with index: "${ i }" has proper attributes`, () => {
+					const modelItem = recentColorModel.get( i );
+					const viewItem = recentColorsGridView.items.get( i );
+
+					expect( modelItem.color ).to.equal( 'hsla(0, 0%, 0%, 0)' );
+					expect( modelItem.isEnabled ).to.be.false;
+					expect( modelItem.hasBorder ).to.be.true;
+					expect( viewItem.isEnabled ).to.be.false;
+					expect( viewItem.color ).to.equal( 'hsla(0, 0%, 0%, 0)' );
+					expect( viewItem.hasBorder ).to.be.true;
+					expect( viewItem.label ).to.be.undefined;
+				} );
+			}
+		} );
+
+		describe( 'model manipulation', () => {
+			it( 'adding item will preserve length of collection', () => {
+				expect( recentColorModel.length ).to.equal( 5 );
+
+				recentColorModel.add( Object.assign( {}, colorBlack ), 0 );
+
+				expect( recentColorModel.length ).to.equal( 5 );
+				expect( recentColorModel.first.color ).to.equal( '#000000' );
+				expect( recentColorModel.first.label ).to.equal( 'Black' );
+				expect( recentColorModel.first.hasBorder ).to.be.false;
+			} );
+
+			it( 'adding multiple times same color should not work', () => {
+				recentColorModel.add( Object.assign( {}, colorBlack ), 0 );
+
+				expect( recentColorModel.first ).to.own.include( colorBlack );
+				expect( recentColorModel.get( 1 ) ).to.own.include( emptyColor );
+
+				recentColorModel.add( Object.assign( {}, colorBlack ), 0 );
+
+				expect( recentColorModel.first ).to.own.include( colorBlack );
+				expect( recentColorModel.get( 1 ) ).to.own.include( emptyColor );
+			} );
+
+			it( 'adding duplicates move color to the front', () => {
+				recentColorModel.add( Object.assign( {}, colorBlack ), 0 );
+				recentColorModel.add( Object.assign( {}, colorWhite ), 0 );
+				recentColorModel.add( Object.assign( {}, colorRed ), 0 );
+
+				expect( recentColorModel.get( 0 ) ).to.own.include( colorRed );
+				expect( recentColorModel.get( 1 ) ).to.own.include( colorWhite );
+				expect( recentColorModel.get( 2 ) ).to.own.include( colorBlack );
+				expect( recentColorModel.get( 3 ) ).to.own.include( emptyColor );
+
+				recentColorModel.add( Object.assign( {}, colorBlack ), 0 );
+
+				expect( recentColorModel.get( 0 ) ).to.own.include( colorBlack );
+				expect( recentColorModel.get( 1 ) ).to.own.include( colorRed );
+				expect( recentColorModel.get( 2 ) ).to.own.include( colorWhite );
+				expect( recentColorModel.get( 3 ) ).to.own.include( emptyColor );
+			} );
+		} );
+
+		describe( 'events', () => {
+			it( 'added colors delegates execute to parent', () => {
+				const spy = sinon.spy();
+				colorTableView.on( 'execute', spy );
+
+				recentColorModel.add( Object.assign( {}, colorBlack ), 0 );
+
+				recentColorsGridView.items.first.element.dispatchEvent( new Event( 'click' ) );
+
+				sinon.assert.calledOnce( spy );
+				sinon.assert.calledWith( spy, sinon.match.any, {
+					value: '#000000',
+					label: 'Black',
+					hasBorder: false
+				} );
+			} );
+		} );
+	} );
+} );

+ 265 - 0
packages/ckeditor5-font/tests/ui/colorui.js

@@ -0,0 +1,265 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, Event */
+
+import ColorUI from './../../src/ui/colorui';
+import FontColorCommand from './../../src/fontcolor/fontcolorcommand';
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
+
+describe( 'ColorUI', () => {
+	class TestColorPlugin extends ColorUI {
+		constructor( editor ) {
+			super( editor, {
+				commandName: 'testColorCommand',
+				componentName: 'testColor',
+				icon: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"></svg>',
+				dropdownLabel: editor.locale.t( 'Test Color' )
+			} );
+
+			editor.commands.add( 'testColorCommand', new FontColorCommand( editor ) );
+		}
+
+		static get pluginName() {
+			return 'TestColorPlugin';
+		}
+	}
+
+	const testColorConfig = {
+		colors: [
+			'yellow',
+			{
+				color: '#000',
+			},
+			{
+				color: 'rgb(255, 255, 255)',
+				label: 'White',
+				hasBorder: true
+			},
+			{
+				color: 'red',
+				label: 'Red'
+			},
+			{
+				color: '#00FF00',
+				label: 'Green',
+				hasBorder: false
+			}
+		],
+		columns: 3
+	};
+
+	testUtils.createSinonSandbox();
+
+	before( () => {
+		addTranslations( 'pl', {
+			'Test Color': 'Testowy plugin',
+			'Remove color': 'Usuń kolor',
+			'Yellow': 'Żółty',
+			'White': 'Biały',
+			'Red': 'Czerwony',
+			'Green': 'Zielony'
+		} );
+	} );
+
+	after( () => {
+		clearTranslations();
+	} );
+
+	let editor, element, testColorPlugin, command;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [ TestColorPlugin ],
+				testColor: testColorConfig
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				testColorPlugin = newEditor.plugins.get( 'TestColorPlugin' );
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'has assigned proper commandName', () => {
+			expect( testColorPlugin.commandName ).to.equal( 'testColorCommand' );
+		} );
+
+		it( 'has assigned proper componentName', () => {
+			expect( testColorPlugin.componentName ).to.equal( 'testColor' );
+		} );
+
+		it( 'has assigned proper icon', () => {
+			expect( testColorPlugin.icon ).to.equal( '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"></svg>' );
+		} );
+
+		it( 'has assigned proper dropdownLabel', () => {
+			expect( testColorPlugin.dropdownLabel ).to.equal( 'Testowy plugin' );
+		} );
+
+		it( 'has assigned proper amount of columns', () => {
+			// Value taken from editor's config above.
+			expect( testColorPlugin.columns ).to.equal( 3 );
+		} );
+	} );
+
+	describe( 'testColor Dropdown', () => {
+		let dropdown;
+
+		beforeEach( () => {
+			command = editor.commands.get( 'testColorCommand' );
+			dropdown = editor.ui.componentFactory.create( 'testColor' );
+		} );
+
+		it( 'button has the base properties', () => {
+			const button = dropdown.buttonView;
+
+			expect( button ).to.have.property( 'label', 'Testowy plugin' );
+			expect( button ).to.have.property( 'tooltip', true );
+			expect( button ).to.have.property( 'icon', '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"></svg>' );
+		} );
+
+		it( 'should add custom CSS class to dropdown', () => {
+			const dropdown = editor.ui.componentFactory.create( 'testColor' );
+
+			dropdown.render();
+
+			expect( dropdown.element.classList.contains( 'ck-color-ui-dropdown' ) ).to.be.true;
+		} );
+
+		it( 'should focus view after command execution from dropdown', () => {
+			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
+			const dropdown = editor.ui.componentFactory.create( 'testColor' );
+
+			dropdown.commandName = 'testColorCommand';
+			dropdown.fire( 'execute', { value: null } );
+
+			sinon.assert.calledOnce( focusSpy );
+		} );
+
+		it( 'static color grid should impact on recent colors', () => {
+			const firstStaticTile = dropdown.colorTableView.items.get( 1 ).items.first;
+			const recentColorsModel = dropdown.colorTableView.recentlyUsedColors;
+			const spy = sinon.spy();
+
+			dropdown.on( 'execute', spy );
+
+			firstStaticTile.element.dispatchEvent( new Event( 'click' ) );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWith( spy, sinon.match.any, {
+				value: 'yellow',
+				label: 'yellow',
+				hasBorder: false
+			} );
+			expect( recentColorsModel.get( 0 ) ).to.include( {
+				color: 'yellow',
+				label: 'yellow',
+				hasBorder: false
+			} );
+		} );
+
+		describe( 'model to command binding', () => {
+			it( 'isEnabled', () => {
+				command.isEnabled = false;
+
+				expect( dropdown.buttonView.isEnabled ).to.be.false;
+
+				command.isEnabled = true;
+				expect( dropdown.buttonView.isEnabled ).to.be.true;
+			} );
+		} );
+
+		describe( 'localization', () => {
+			let editor, editorElement;
+
+			beforeEach( () => {
+				editorElement = document.createElement( 'div' );
+				document.body.appendChild( editorElement );
+
+				return createLocalizedEditor( editorElement )
+					.then( localizedEditor => {
+						editor = localizedEditor;
+					} );
+			} );
+
+			afterEach( () => {
+				editorElement.remove();
+
+				return editor.destroy();
+			} );
+
+			it( 'works for the colorTableView#items in the panel', () => {
+				const colorTableView = dropdown.colorTableView;
+
+				expect( colorTableView.removeButtonLabel ).to.equal( 'Usuń kolor' );
+				expect( colorTableView.items.first.label ).to.equal( 'Usuń kolor' );
+			} );
+
+			describe( 'works for', () => {
+				const colors = [
+					{
+						color: 'yellow',
+						label: 'yellow'
+					},
+					{
+						color: '#000',
+						label: '#000'
+					},
+					{
+						color: 'rgb(255,255,255)',
+						label: 'Biały'
+					},
+					{
+						color: 'red',
+						label: 'Czerwony'
+					},
+					{
+						color: '#00FF00',
+						label: 'Zielony'
+					}
+				];
+
+				colors.forEach( test => {
+					it( `tested color "${ test.color }" translated to "${ test.label }".`, () => {
+						const colorGrid = dropdown.colorTableView.items.get( 1 );
+						const tile = colorGrid.items.find( colorTile => test.color === colorTile.color );
+
+						expect( tile.label ).to.equal( test.label );
+					} );
+				} );
+			} );
+
+			function createLocalizedEditor( editorElement ) {
+				return ClassicTestEditor
+					.create( editorElement, {
+						plugins: [ TestColorPlugin ],
+						testColor: testColorConfig,
+						toolbar: [ 'testColor' ],
+						language: 'pl',
+					} )
+					.then( newEditor => {
+						editor = newEditor;
+						dropdown = editor.ui.componentFactory.create( 'testColor' );
+						command = editor.commands.get( 'testColorCommand' );
+
+						return editor;
+					} );
+			}
+		} );
+	} );
+} );

+ 137 - 0
packages/ckeditor5-font/tests/utils.js

@@ -0,0 +1,137 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import {
+	FONT_COLOR,
+	FONT_BACKGROUND_COLOR,
+	normalizeColorOptions,
+	addColorTableToDropdown
+} from './../src/utils';
+import { createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+import ColorTableView from './../src/ui/colortableview';
+
+describe( 'utils', () => {
+	describe( 'color and background color related', () => {
+		it( 'plugin names has proper values', () => {
+			expect( FONT_COLOR ).to.equal( 'fontColor' );
+			expect( FONT_BACKGROUND_COLOR ).to.equal( 'fontBackgroundColor' );
+		} );
+
+		it( 'normalizeColorOptions can produce the same output object', () => {
+			const normalizedArray = normalizeColorOptions( [
+				'black',
+				{
+					color: 'black'
+				},
+				{
+					color: 'black',
+					label: 'Black'
+				},
+				{
+					color: 'black',
+					label: 'Black',
+					hasBorder: true
+				},
+				{
+					color: 'black',
+					hasBorder: true
+				}
+			] );
+
+			expect( normalizedArray ).to.deep.equal( [
+				{
+					model: 'black',
+					label: 'black',
+					hasBorder: false,
+					view: {
+						name: 'span',
+						styles: {
+							color: 'black'
+						},
+						priority: 5
+					}
+				},
+				{
+					model: 'black',
+					label: 'black',
+					hasBorder: false,
+					view: {
+						name: 'span',
+						styles: {
+							color: 'black'
+						},
+						priority: 5
+					}
+				},
+				{
+					model: 'black',
+					label: 'Black',
+					hasBorder: false,
+					view: {
+						name: 'span',
+						styles: {
+							color: 'black'
+						},
+						priority: 5
+					}
+				},
+				{
+					model: 'black',
+					label: 'Black',
+					hasBorder: true,
+					view: {
+						name: 'span',
+						styles: {
+							color: 'black'
+						},
+						priority: 5
+					}
+				},
+				{
+					model: 'black',
+					label: 'black',
+					hasBorder: true,
+					view: {
+						name: 'span',
+						styles: {
+							color: 'black'
+						},
+						priority: 5
+					}
+				},
+			] );
+		} );
+
+		it( 'adding colors table to dropdown works', () => {
+			const dropdown = createDropdown();
+			dropdown.render();
+
+			addColorTableToDropdown( {
+				dropdownView: dropdown,
+				colors: [
+					{
+						label: 'Black',
+						color: '#000',
+						options: {
+							hasBorder: false
+						}
+					},
+					{
+						label: 'White',
+						color: '#FFFFFF',
+						options: {
+							hasBorder: true
+						}
+					}
+				],
+				columns: 2,
+				removeButtonTooltip: 'Remove Color'
+			} );
+
+			expect( dropdown.colorTableView ).to.be.instanceOf( ColorTableView );
+			expect( dropdown.panelView.children.length ).to.equal( 1 );
+		} );
+	} );
+} );

+ 10 - 0
packages/ckeditor5-font/theme/fontcolor.css

@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+.ck .ck-button.ck-color-table__remove-color {
+	display: flex;
+	align-items: center;
+	width: 100%;
+}

+ 1 - 0
packages/ckeditor5-font/theme/icons/font-background.svg

@@ -0,0 +1 @@
+<svg  viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M4 2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm8.38 9.262H7.62L10 5.506l2.38 5.756zm.532 1.285L14.34 16h1.426L10.804 4H9.196L4.234 16H5.66l1.428-3.453h5.824z" fill-rule="evenodd"/></svg>

+ 1 - 0
packages/ckeditor5-font/theme/icons/font-color.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M12.4 10.3L10 4.5l-2.4 5.8h4.8zm.5 1.2H7.1L5.7 15H4.2l5-12h1.6l5 12h-1.5L13 11.5z"/><path stroke="#333" d="M3 17h14v1H3z"/></g></svg>