Răsfoiți Sursa

Merge pull request #39 from ckeditor/t/28

Introduced document colors section to the font/background color plugins
Marek Lewandowski 6 ani în urmă
părinte
comite
2d3b61373e

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

@@ -1,3 +1,6 @@
 <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>
+	<p>
+		Here are <span style="color:rgb(90%,100%,0%);background-color: #454545">some examples</span> of <span style="color:midnightblue;background-color:rgb(150,240,255)">custom colors</span> not defined in <span style="color:rgb(255, 186, 57);background-color:#442200">configuration</span> which might be recognized and used in the <span style="color:rgb(150,250,150);background-color:#153">"document colors"</span> section.
+	</p>
 </div>

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

@@ -174,7 +174,8 @@ ClassicEditor
 					hasBorder: true
 				},
 			],
-			columns: 9
+			columns: 9,
+			documentColors: 18
 		},
 		fontColor: {
 			colors: [
@@ -198,11 +199,15 @@ ClassicEditor
 				'teal',
 				'aqua'
 			],
-			columns: 4
+			columns: 4,
+			documentColors: 12
 		}
 	} )
 	.then( editor => {
-		window.editor = editor;
+		if ( !window.editors ) {
+			window.editors = {};
+		}
+		window.editors[ 'custom-font-color-and-background-color-options' ] = editor;
 	} )
 	.catch( err => {
 		console.error( err.stack );

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

@@ -23,4 +23,7 @@
 		<span style="color:hsl(0,0%,100%);background-color:hsl(0,0%,0%)">This text has white color and black background.</span>
 	</p>
 
+	<p>
+		<span style="color:#505050;background-color:rgb(75, 255, 190)">This text has custom color used for text and background. Those colors are available in the "document colors" section.</span>
+	</p>
 </div>

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

@@ -227,9 +227,40 @@ ClassicEditor
 	.catch( ... );
 ```
 
+### Documents colors
+
+The document colors feature is an additional section in a dropdown color panel. It lists the colors used in the document in the order of occurrence. Duplicated colors are not inserted multiple times. By default number of document colors is limited, but you can adjust it (or disable it) using {@link module:font/fontcolor~FontColorConfig#documentColors `fontColor.documentColors`} or {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#documentColors `fontBackgroundColor.documentColors`} options.
+
+The document colors feature can help with identification of already used colors in a document. This provides a possibility of applying the same color further in the text without searching it over the entire section with predefined colors.
+
+<info-box info>
+**Note:** In case the edited document doesn't contain any font color or background colors, the document colors section is hidden.
+</info-box>
+
+```js
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		fontColor: {
+			documentColors: 10,
+
+			// ...
+		},
+		fontBackgroundColor: {
+			documentColors: 0, // Disables the feature.
+
+			// ...
+		},
+		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 the {@link module:font/fontcolor~FontColorConfig#columns `fontColor.columns`} and {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#columns `fontBackgroundColor.columns`} options to do so.
+It is possible to configure how many colors' columns are displayed in the grid. Changing geometry of a color grid impacts on both section in a dropdown panel (available colors defined in the configuration and document colors sections). Setting {@link module:font/fontcolor~FontColorConfig#columns `fontColor.columns`} and {@link module:font/fontcolor~FontColorConfig#documentColors `fontColor.documentColors`} ( or {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#columns `fontBackgroundColor.columns`} and {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#documentColors `fontBackgroundColor.documentColors`} ) shapes the color grid.
 
 ```js
 ClassicEditor

+ 78 - 0
packages/ckeditor5-font/src/documentcolorcollection.js

@@ -0,0 +1,78 @@
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
+/**
+ * @module font/documentcolorcollection
+ */
+
+/**
+ * Collection to store document colors. It enforces colors to be unique.
+ *
+ * @mixes module:utils/observablemixin~ObservableMixin
+ * @extends module:utils/collection~Collection
+ */
+export default class DocumentColorCollection extends Collection {
+	constructor( options ) {
+		super( options );
+
+		/**
+		 * Indicates whether the collection is empty.
+		 *
+		 * @observable
+		 * @readonly
+		 * @member {Boolean} #isEmpty
+		 */
+		this.set( 'isEmpty', true );
+	}
+
+	/**
+	 * Adds a color into the collection.
+	 *
+	 * Function ensures that no color duplicates are inserted (compared using
+	 * the color value of {@link module:ui/colorgrid/colorgrid~ColorDefinition}).
+	 *
+	 * If the item does not have an id, then it will be automatically generated and set on the item.
+	 *
+	 * @chainable
+	 * @param {module:ui/colorgrid/colorgrid~ColorDefinition} item
+	 * @param {Number} [index] The position of the item in the collection. The item
+	 * is pushed to the collection when `index` not specified.
+	 * @fires add
+	 */
+	add( item, index ) {
+		if ( this.find( element => element.color === item.color ) ) {
+			// No duplicates are allowed.
+			return;
+		}
+
+		super.add( item, index );
+
+		this.set( 'isEmpty', false );
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	remove( subject ) {
+		const ret = super.remove( subject );
+
+		if ( this.length === 0 ) {
+			this.set( 'isEmpty', true );
+		}
+
+		return ret;
+	}
+
+	/**
+	 * Checks if object with given colors is present in collection.
+	 *
+	 * @param {String} color
+	 * @returns {Boolean}
+	 */
+	hasColor( color ) {
+		return !!this.find( item => item.color === color );
+	}
+}
+
+mix( DocumentColorCollection, ObservableMixin );

+ 37 - 1
packages/ckeditor5-font/src/fontbackgroundcolor.js

@@ -141,7 +141,9 @@ export default class FontBackgroundColor extends Plugin {
  */
 
 /**
- * Represents the number of columns in the dropdown. The default value is:
+ * Represents the number of columns in the dropdown.
+ *
+ * The default value is:
  *
  *		const fontBackgroundColorConfig = {
  *			columns: 5
@@ -149,3 +151,37 @@ export default class FontBackgroundColor extends Plugin {
  *
  * @member {Number} module:font/fontbackgroundcolor~FontBackgroundColorConfig#columns
  */
+
+/**
+ * Determines maximum number of available document colors.
+ * Setting it to `0` will disable document colors feature.
+ *
+ * By default it equals to {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#columns} value.
+ *
+ * Examples:
+ *
+ * 	// 1) Neither documentColors nor columns are defined in config.
+ * 	// documentColors will equal 5,
+ * 	// because value will be inherited from `columns`,
+ * 	// which has predefined value 5.
+ * 	const fontBackgroundColorConfig = {}
+ *
+ * 	// 2) documentColors will equal 8, because value will be inherited from `columns`.
+ * 	const fontBackgroundColorConfig = {
+ * 		columns: 8
+ * 	}
+ *
+ * 	// 3) documentColors will equal 24, because has defined own value.
+ * 	const fontBackgroundColorConfig = {
+ * 		columns: 8,
+ * 		documentColors: 24
+ * 	}
+ *
+ * 	// 4) documentColors feature will be disabled.
+ * 	const fontBackgroundColorConfig = {
+ * 		columns: 8,
+ * 		documentColors: 0
+ * 	}
+ *
+ * @member {Number} module:font/fontbackgroundcolor~FontBackgroundColorConfig#documentColors
+ */

+ 37 - 1
packages/ckeditor5-font/src/fontcolor.js

@@ -140,7 +140,9 @@ export default class FontColor extends Plugin {
  */
 
 /**
- * Represents the number of columns in the dropdown. The default value is:
+ * Represents the number of columns in the dropdown.
+ *
+ * The default value is:
  *
  *		const fontColorConfig = {
  *			columns: 5
@@ -148,3 +150,37 @@ export default class FontColor extends Plugin {
  *
  * @member {Number} module:font/fontcolor~FontColorConfig#columns
  */
+
+/**
+ * Determines maximum number of available document colors.
+ * Setting it to `0` will disable document colors feature.
+ *
+ * By default it equals to {@link module:font/fontcolor~FontColorConfig#columns} value.
+ *
+ * Examples:
+ *
+ * 	// 1) Neither documentColors nor columns are defined in config.
+ * 	// documentColors will equal 5,
+ * 	// because value will be inherited from `columns`,
+ * 	// which has predefined value 5.
+ * 	const fontColorConfig = {}
+ *
+ * 	// 2) documentColors will equal 8, because value will be inherited from `columns`.
+ * 	const fontColorConfig = {
+ * 		columns: 8
+ * 	}
+ *
+ * 	// 3) documentColors will equal 24, because has defined own value.
+ * 	const fontColorConfig = {
+ * 		columns: 8,
+ * 		documentColors: 24
+ * 	}
+ *
+ * 	// 4) documentColors feature will be disabled.
+ * 	const fontColorConfig = {
+ * 		columns: 8,
+ * 		documentColors: 0
+ * 	}
+ *
+ * @member {Number} module:font/fontcolor~FontColorConfig#documentColors
+ */

+ 223 - 25
packages/ckeditor5-font/src/ui/colortableview.js

@@ -9,7 +9,11 @@
 
 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 LabelView from '@ckeditor/ckeditor5-ui/src/label/labelview';
+import DocumentColorCollection from '../documentcolorcollection';
+import Template from '@ckeditor/ckeditor5-ui/src/template';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
@@ -20,7 +24,8 @@ import '../../theme/fontcolor.css';
  * A class which represents a view with the following sub–components:
  *
  * * A remove color button,
- * * A {@link module:ui/colorgrid/colorgrid~ColorGridView}.
+ * * A static {@link module:ui/colorgrid/colorgrid~ColorGridView} of colors defined in the configuration,
+ * * A dynamic {@link module:ui/colorgrid/colorgrid~ColorGridView} of colors used in the document.
  *
  * @extends module:ui/view~View
  */
@@ -34,8 +39,10 @@ export default class ColorTableView extends View {
 	 * be displayed in the table.
 	 * @param {Number} config.columns The number of columns in the color grid.
 	 * @param {String} config.removeButtonLabel The label of the button responsible for removing the color.
+	 * @param {String} config.documentColorsLabel The label for the section with the document colors.
+	 * @param {String} config.documentColorsCount The number of colors in document colors section inside dropdown.
 	 */
-	constructor( locale, { colors, columns, removeButtonLabel } ) {
+	constructor( locale, { colors, columns, removeButtonLabel, documentColorsLabel, documentColorsCount } ) {
 		super( locale );
 
 		/**
@@ -90,6 +97,41 @@ export default class ColorTableView extends View {
 		 */
 		this.columns = columns;
 
+		/**
+		 * A collection of definitions stores document colors.
+		 *
+		 * @readonly
+		 * @member {module:font/documentcolorcollection~DocumentColorCollection}
+		 */
+		this.documentColors = new DocumentColorCollection();
+
+		/**
+		 * Maximum number of colors in document colors section.
+		 * If equals 0, then document colors section is not added.
+		 *
+		 * @readonly
+		 * @type {Number}
+		 */
+		this.documentColorsCount = documentColorsCount;
+
+		/**
+		 * Preserves reference to {@link module:ui/colorgrid/colorgrid~ColorGridView} used to create
+		 * default (static) colors set.
+		 *
+		 * @readonly
+		 * @member {module:ui/colorgrid/colorgrid~ColorGridView}
+		 */
+		this.staticColorsGrid = this._createStaticColorsGrid();
+
+		/**
+		 * Preserves reference to {@link module:ui/colorgrid/colorgrid~ColorGridView} used to create
+		 * document colors. It remains undefined if document colors are disabled.
+		 *
+		 * @readonly
+		 * @member {module:ui/colorgrid/colorgrid~ColorGridView}
+		 */
+		this.documentColorGrid;
+
 		/**
 		 * Helps cycling over focusable {@link #items} in the list.
 		 *
@@ -121,16 +163,121 @@ export default class ColorTableView extends View {
 			children: this.items
 		} );
 
-		this.items.add( this.removeColorButton() );
-		this.items.add( this.createStaticColorTable() );
+		this.items.add( this._removeColorButton() );
+		this.items.add( this.staticColorsGrid );
+
+		if ( documentColorsCount ) {
+			// Create a label for document colors.
+			const bind = Template.bind( this.documentColors, this.documentColors );
+			const label = new LabelView( this.locale );
+
+			label.text = documentColorsLabel;
+			label.extendTemplate( {
+				attributes: {
+					class: [
+						'ck',
+						'ck-color-grid__label',
+						bind.if( 'isEmpty', 'ck-hidden' )
+					]
+				}
+			} );
+
+			this.items.add( label );
+
+			this.documentColorsGrid = this._createDocumentColorsGrid();
+			this.items.add( this.documentColorsGrid );
+		}
+	}
+
+	/**
+	 * Method scans through the editor's model and searches for text node attributes with attributeName.
+	 * Found entries are set as document colors.
+	 *
+	 * All the previously stored document colors will be lost in the process.
+	 *
+	 * @param {module:engine/model/model~Model} model Model used as a source to obtain document colors.
+	 * @param {String} attributeName Determines what is the name of a related model's attribute for given dropdown.
+	 */
+	updateDocumentColors( model, attributeName ) {
+		const document = model.document;
+		const maxCount = this.documentColorsCount;
+
+		this.documentColors.clear();
+
+		for ( const rootName of document.getRootNames() ) {
+			const root = document.getRoot( rootName );
+			const range = model.createRangeIn( root );
+
+			for ( const node of range.getItems() ) {
+				if ( node.is( 'textProxy' ) && node.hasAttribute( attributeName ) ) {
+					this._addColorToDocumentColors( node.getAttribute( attributeName ) );
+
+					if ( this.documentColors.length >= maxCount ) {
+						return;
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Method refresh state of `selectedColor` in single or both {@link module:ui/colorgrid/colorgrid~ColorGridView}
+	 * available in {@link module:font/ui/colortableview~ColorTableView}. It guarantees that selection will occur only in one of them.
+	 */
+	updateSelectedColors() {
+		const documentColorsGrid = this.documentColorsGrid;
+		const staticColorsGrid = this.staticColorsGrid;
+		const selectedColor = this.selectedColor;
+
+		if ( !this.documentColors.isEmpty ) {
+			if ( this.documentColors.hasColor( selectedColor ) ) {
+				staticColorsGrid.selectedColor = null;
+				documentColorsGrid.selectedColor = selectedColor;
+			} else {
+				staticColorsGrid.selectedColor = selectedColor;
+				documentColorsGrid.selectedColor = null;
+			}
+		} else {
+			staticColorsGrid.selectedColor = selectedColor;
+		}
+	}
+
+	/**
+	 * @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 element in {@link #items}.
+	 */
+	focus() {
+		this._focusCycler.focusFirst();
+	}
+
+	/**
+	 * Focuses the last focusable element in {@link #items}.
+	 */
+	focusLast() {
+		this._focusCycler.focusLast();
 	}
 
 	/**
 	 * Adds the remove color button as a child of the current view.
 	 *
 	 * @private
+	 * @returns {module:ui/button/buttonview~ButtonView}
 	 */
-	removeColorButton() {
+	_removeColorButton() {
 		const buttonView = new ButtonView();
 
 		buttonView.set( {
@@ -152,45 +299,96 @@ export default class ColorTableView extends View {
 	 * Creates a static color table grid based on the editor configuration.
 	 *
 	 * @private
+	 * @returns {module:ui/colorgrid/colorgrid~ColorGridView}
 	 */
-	createStaticColorTable() {
+	_createStaticColorsGrid() {
 		const colorGrid = new ColorGridView( this.locale, {
 			colorDefinitions: this.colorDefinitions,
 			columns: this.columns
 		} );
 
 		colorGrid.delegate( 'execute' ).to( this );
-		colorGrid.bind( 'selectedColor' ).to( this );
 
 		return colorGrid;
 	}
 
 	/**
-	 * @inheritDoc
+	 * Creates document colors section view and binds it to {@link #documentColors}.
+	 *
+	 * @private
+	 * @returns {module:ui/colorgrid/colorgrid~ColorGridView}
 	 */
-	render() {
-		super.render();
+	_createDocumentColorsGrid() {
+		const bind = Template.bind( this.documentColors, this.documentColors );
+		const documentColorsGrid = new ColorGridView( this.locale, {
+			columns: this.columns
+		} );
 
-		// Items added before rendering should be known to the #focusTracker.
-		for ( const item of this.items ) {
-			this.focusTracker.add( item.element );
-		}
+		documentColorsGrid.delegate( 'execute' ).to( this );
 
-		// Start listening for the keystrokes coming from #element.
-		this.keystrokes.listenTo( this.element );
-	}
+		documentColorsGrid.extendTemplate( {
+			attributes: {
+				class: bind.if( 'isEmpty', 'ck-hidden' )
+			}
+		} );
 
-	/**
-	 * Focuses the first focusable element in {@link #items}.
-	 */
-	focus() {
-		this._focusCycler.focusFirst();
+		documentColorsGrid.items.bindTo( this.documentColors ).using(
+			colorObj => {
+				const colorTile = new ColorTileView();
+
+				colorTile.set( {
+					color: colorObj.color,
+					hasBorder: colorObj.options && colorObj.options.hasBorder
+				} );
+
+				if ( colorObj.label ) {
+					colorTile.set( {
+						label: colorObj.label,
+						tooltip: true
+					} );
+				}
+
+				colorTile.on( 'execute', () => {
+					this.fire( 'execute', {
+						value: colorObj.color
+					} );
+				} );
+
+				return colorTile;
+			}
+		);
+
+		// Selected color should be cleared when document colors became empty.
+		this.documentColors.on( 'change:isEmpty', ( evt, name, val ) => {
+			if ( val ) {
+				documentColorsGrid.selectedColor = null;
+			}
+		} );
+
+		return documentColorsGrid;
 	}
 
 	/**
-	 * Focuses the last focusable element in {@link #items}.
+	 * Method adds a given `color` to the document colors list. If possible, method will attempt to use
+	 * data from the {@link #colorDefinitions} (label, color options).
+	 *
+	 * @private
+	 * @param {String} color String which stores value of recently applied color.
 	 */
-	focusLast() {
-		this._focusCycler.focusLast();
+	_addColorToDocumentColors( color ) {
+		const predefinedColor = this.colorDefinitions
+			.find( definition => definition.color === color );
+
+		if ( !predefinedColor ) {
+			this.documentColors.add( {
+				color,
+				label: color,
+				options: {
+					hasBorder: false
+				}
+			} );
+		} else {
+			this.documentColors.add( Object.assign( {}, predefinedColor ) );
+		}
 	}
 }

+ 26 - 3
packages/ckeditor5-font/src/ui/colorui.js

@@ -40,6 +40,7 @@ export default class ColorUI extends Plugin {
 
 		/**
 		 * The name of the command which will be executed when a color tile is clicked.
+		 *
 		 * @type {String}
 		 */
 		this.commandName = commandName;
@@ -47,6 +48,7 @@ export default class ColorUI extends Plugin {
 		/**
 		 * The name of this component in the {@link module:ui/componentfactory~ComponentFactory}.
 		 * Also the configuration scope name in `editor.config`.
+		 *
 		 * @type {String}
 		 */
 		this.componentName = componentName;
@@ -59,15 +61,24 @@ export default class ColorUI extends Plugin {
 
 		/**
 		 * Label used by the dropdown.
+		 *
 		 * @type {String}
 		 */
 		this.dropdownLabel = dropdownLabel;
 
 		/**
 		 * The number of columns in the color grid.
+		 *
 		 * @type {Number}
 		 */
 		this.columns = editor.config.get( `${ this.componentName }.columns` );
+
+		/**
+		 * Keeps a reference to {@link module:font/ui/colortableview~ColorTableView}.
+		 *
+		 * @member {module:font/ui/colortableview~ColorTableView}
+		 */
+		this.colorTableView;
 	}
 
 	/**
@@ -79,11 +90,12 @@ export default class ColorUI extends Plugin {
 		const command = editor.commands.get( this.commandName );
 		const colorsConfig = normalizeColorOptions( editor.config.get( this.componentName ).colors );
 		const localizedColors = getLocalizedColorOptions( editor, colorsConfig );
+		const documentColorsCount = editor.config.get( `${ this.componentName }.documentColors` );
 
 		// Register the UI component.
 		editor.ui.componentFactory.add( this.componentName, locale => {
 			const dropdownView = createDropdown( locale );
-			const colorTableView = addColorTableToDropdown( {
+			this.colorTableView = addColorTableToDropdown( {
 				dropdownView,
 				colors: localizedColors.map( option => ( {
 					label: option.label,
@@ -93,10 +105,12 @@ export default class ColorUI extends Plugin {
 					}
 				} ) ),
 				columns: this.columns,
-				removeButtonLabel: t( 'Remove color' )
+				removeButtonLabel: t( 'Remove color' ),
+				documentColorsLabel: documentColorsCount !== 0 ? t( 'Document colors' ) : undefined,
+				documentColorsCount: documentColorsCount === undefined ? this.columns : documentColorsCount
 			} );
 
-			colorTableView.bind( 'selectedColor' ).to( command, 'value' );
+			this.colorTableView.bind( 'selectedColor' ).to( command, 'value' );
 
 			dropdownView.buttonView.set( {
 				label: this.dropdownLabel,
@@ -117,6 +131,15 @@ export default class ColorUI extends Plugin {
 				editor.editing.view.focus();
 			} );
 
+			dropdownView.on( 'change:isOpen', ( evt, name, isVisible ) => {
+				if ( isVisible ) {
+					if ( documentColorsCount !== 0 ) {
+						this.colorTableView.updateDocumentColors( editor.model, this.componentName );
+					}
+					this.colorTableView.updateSelectedColors();
+				}
+			} );
+
 			return dropdownView;
 		} );
 	}

+ 4 - 2
packages/ckeditor5-font/src/utils.js

@@ -109,11 +109,13 @@ export function normalizeColorOptions( options ) {
  * @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 The label for the button responsible for removing the color.
+ * @param {String} config.documentColorsLabel The label of a section with document colors.
+ * @param {String} config.documentColorsCount The number of document colors inside dropdown.
  * @returns {module:font/ui/colortableview~ColorTableView} The new color table view.
  */
-export function addColorTableToDropdown( { dropdownView, colors, columns, removeButtonLabel } ) {
+export function addColorTableToDropdown( { dropdownView, colors, columns, removeButtonLabel, documentColorsLabel, documentColorsCount } ) {
 	const locale = dropdownView.locale;
-	const colorTableView = new ColorTableView( locale, { colors, columns, removeButtonLabel } );
+	const colorTableView = new ColorTableView( locale, { colors, columns, removeButtonLabel, documentColorsLabel, documentColorsCount } );
 
 	dropdownView.colorTableView = colorTableView;
 	dropdownView.panelView.children.add( colorTableView );

+ 25 - 0
packages/ckeditor5-font/tests/_utils/testcolorplugin.js

@@ -0,0 +1,25 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ColorUI from './../../src/ui/colorui';
+import FontColorCommand from './../../src/fontcolor/fontcolorcommand';
+
+export default 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 ) );
+		editor.model.schema.extend( '$text', { allowAttributes: 'testColor' } );
+	}
+
+	static get pluginName() {
+		return 'TestColorPlugin';
+	}
+}

+ 61 - 0
packages/ckeditor5-font/tests/documentcolorcollection.js

@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import DocumentColorCollection from '../src/documentcolorcollection';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+
+describe( 'DocumentColorCollection', () => {
+	let documentColorCollection;
+
+	const colors = [
+		{
+			color: '111',
+		},
+		{
+			color: '222'
+		},
+		{
+			color: '333'
+		},
+		{
+			color: '444'
+		}
+	];
+
+	beforeEach( () => {
+		documentColorCollection = new DocumentColorCollection();
+
+		colors.forEach( item => {
+			documentColorCollection.add( item );
+		} );
+	} );
+
+	it( 'constructor()', () => {
+		expect( documentColorCollection ).to.be.instanceOf( DocumentColorCollection );
+		expect( documentColorCollection ).to.be.instanceOf( Collection );
+	} );
+
+	it( 'has observable "isEmpty" parameter', () => {
+		expect( documentColorCollection.isEmpty ).to.be.false;
+
+		documentColorCollection.clear();
+		expect( documentColorCollection.isEmpty ).to.be.true;
+
+		documentColorCollection.add( colors[ 0 ] );
+		expect( documentColorCollection.isEmpty ).to.be.false;
+	} );
+
+	it( 'prevent of adding duplicated colors', () => {
+		expect( documentColorCollection.length ).to.equal( 4 );
+
+		documentColorCollection.add( { color: '111' } );
+		expect( documentColorCollection.length ).to.equal( 4 );
+	} );
+
+	it( 'hasColor()', () => {
+		expect( documentColorCollection.hasColor( '111' ) ).to.be.true;
+		expect( documentColorCollection.hasColor( '555' ) ).to.be.false;
+	} );
+} );

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

@@ -10,3 +10,16 @@
 	<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>
+
+<div id="color-box" style="margin-top: 10px; border: 1px solid black; padding: 5px">
+	<p>You can use this helper to generate text with a particular color / background applied. Change inputs and accept it with enter key.</p>
+	<label>
+		Color:
+		<input type="text" name="color" id="color" placeholder="#3d3d3d">
+	</label>
+	<label>
+		Background Color:
+		<input type="text" name="bgcolor" id="bgcolor" placeholder="pink">
+	</label>
+	<p contenteditable="true"><span>Text for copying.</span></p>
+</div>

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

@@ -38,3 +38,15 @@ ClassicEditor
 	.catch( err => {
 		console.error( err.stack );
 	} );
+
+function updateText( styleName ) {
+	return evt => {
+		const el = document.querySelector( '#color-box > p > span' );
+		if ( el ) {
+			el.style[ styleName ] = evt.target.value;
+		}
+	};
+}
+
+document.getElementById( 'color' ).addEventListener( 'change', updateText( 'color' ) );
+document.getElementById( 'bgcolor' ).addEventListener( 'change', updateText( 'backgroundColor' ) );

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

@@ -12,3 +12,4 @@ The format in the editor content is: `N. [font color]; [background color]`.
 
 - 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 document colors list.

+ 366 - 12
packages/ckeditor5-font/tests/ui/colortableview.js

@@ -3,15 +3,22 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals Event */
+/* globals document,Event */
 
+import TestColorPlugin from '../_utils/testcolorplugin';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 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';
+import ColorTileView from '@ckeditor/ckeditor5-ui/src/colorgrid/colortileview';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
 describe( 'ColorTableView', () => {
 	let locale, colorTableView;
@@ -39,17 +46,46 @@ describe( 'ColorTableView', () => {
 			}
 		}
 	];
+	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
+	};
 
 	beforeEach( () => {
 		locale = { t() {} };
 		colorTableView = new ColorTableView( locale, {
 			colors: colorDefinitions,
 			columns: 5,
-			removeButtonLabel: 'Remove color'
+			removeButtonLabel: 'Remove color',
+			documentColorsLabel: 'Document colors',
+			documentColorsCount: 4
 		} );
 		colorTableView.render();
 	} );
 
+	afterEach( () => {
+		colorTableView.destroy();
+	} );
+
 	testUtils.createSinonSandbox();
 
 	describe( 'constructor()', () => {
@@ -57,6 +93,11 @@ describe( 'ColorTableView', () => {
 			expect( colorTableView.items ).to.be.instanceOf( ViewCollection );
 		} );
 
+		it( 'store color definitions', () => {
+			expect( colorTableView.colorDefinitions ).to.be.instanceOf( Array );
+			expect( colorTableView.colorDefinitions ).to.deep.equal( colorDefinitions );
+		} );
+
 		it( 'creates focus tracker', () => {
 			expect( colorTableView.focusTracker ).to.be.instanceOf( FocusTracker );
 		} );
@@ -77,18 +118,30 @@ describe( 'ColorTableView', () => {
 			expect( colorTableView.removeButtonLabel ).to.equal( 'Remove color' );
 		} );
 
-		it( 'sets number of drawed columns', () => {
+		it( 'sets number of drawn columns', () => {
 			expect( colorTableView.columns ).to.equal( 5 );
 		} );
 
+		it( 'creates collection of document colors', () => {
+			expect( colorTableView.documentColors ).to.be.instanceOf( Collection );
+		} );
+
+		it( 'sets maximum number of document colors', () => {
+			expect( colorTableView.documentColorsCount ).to.equal( 4 );
+		} );
+
 		it( 'creates focus cycler', () => {
 			expect( colorTableView._focusCycler ).to.be.instanceOf( FocusCycler );
 		} );
 
-		it( 'has proper class', () => {
+		it( 'has correct class', () => {
 			expect( colorTableView.element.classList.contains( 'ck' ) ).to.be.true;
 			expect( colorTableView.element.classList.contains( 'ck-color-table' ) ).to.be.true;
 		} );
+
+		it( 'has correct amount of children', () => {
+			expect( colorTableView.items.length ).to.equal( 4 );
+		} );
 	} );
 
 	describe( 'update elements in focus tracker', () => {
@@ -149,14 +202,6 @@ describe( 'ColorTableView', () => {
 			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();
@@ -173,4 +218,313 @@ describe( 'ColorTableView', () => {
 			} );
 		} );
 	} );
+
+	describe( 'document colors', () => {
+		const colorBlack = {
+			color: '#000000',
+			label: 'Black',
+			options: {
+				hasBorder: false
+			}
+		};
+		const colorWhite = {
+			color: '#FFFFFF',
+			label: 'Black',
+			options: {
+				hasBorder: true
+			}
+		};
+		const colorRed = {
+			color: 'rgb(255,0,0)',
+			options: {
+				hasBorder: false
+			}
+		};
+		const colorEmpty = {
+			color: 'hsla(0,0%,0%,0)',
+			options: {
+				hasBorder: true
+			}
+		};
+
+		describe( 'default checks', () => {
+			let documentColorsGridView, documentColors;
+
+			beforeEach( () => {
+				documentColors = colorTableView.documentColors;
+				documentColorsGridView = colorTableView.items.last;
+			} );
+
+			describe( 'model manipulation', () => {
+				it( 'adding items works properly', () => {
+					expect( documentColors.length ).to.equal( 0 );
+
+					documentColors.add( Object.assign( {}, colorBlack ) );
+
+					expect( documentColors.length ).to.equal( 1 );
+					expect( documentColors.first.color ).to.equal( '#000000' );
+					expect( documentColors.first.label ).to.equal( 'Black' );
+					expect( documentColors.first.options.hasBorder ).to.be.false;
+				} );
+
+				it( 'adding multiple times same color should not populate items collection', () => {
+					expect( documentColors.length ).to.equal( 0 );
+
+					documentColors.add( Object.assign( {}, colorBlack ) );
+
+					expect( documentColors.first ).to.own.include( colorBlack );
+					expect( documentColors.length ).to.equal( 1 );
+
+					documentColors.add( Object.assign( {}, colorBlack ) );
+
+					expect( documentColors.first ).to.own.include( colorBlack );
+					expect( documentColors.length ).to.equal( 1 );
+				} );
+
+				it( 'adding duplicated colors don\'t add it to model', () => {
+					expect( documentColors.length ).to.equal( 0 );
+
+					documentColors.add( Object.assign( {}, colorBlack ) );
+					documentColors.add( Object.assign( {}, colorWhite ) );
+					documentColors.add( Object.assign( {}, colorRed ) );
+
+					expect( documentColors.length ).to.equal( 3 );
+					expect( documentColors.get( 0 ) ).to.own.include( colorBlack );
+					expect( documentColors.get( 1 ) ).to.own.include( colorWhite );
+					expect( documentColors.get( 2 ) ).to.own.include( colorRed );
+
+					documentColors.add( Object.assign( {}, colorBlack ) );
+
+					expect( documentColors.length ).to.equal( 3 );
+					expect( documentColors.get( 0 ) ).to.own.include( colorBlack );
+					expect( documentColors.get( 1 ) ).to.own.include( colorWhite );
+					expect( documentColors.get( 2 ) ).to.own.include( colorRed );
+				} );
+
+				it( 'should correctly add disabled colors', () => {
+					expect( documentColors.length ).to.equal( 0 );
+
+					documentColors.add( Object.assign( {}, colorEmpty ) );
+
+					expect( documentColors.length ).to.equal( 1 );
+					expect( documentColors.first.color ).to.equal( 'hsla(0,0%,0%,0)' );
+					expect( documentColors.first.options.hasBorder ).to.be.true;
+				} );
+			} );
+
+			describe( 'events', () => {
+				it( 'added colors delegates execute to parent', () => {
+					const spy = sinon.spy();
+					colorTableView.on( 'execute', spy );
+
+					documentColors.add( Object.assign( {}, colorBlack ) );
+					documentColorsGridView.items.first.element.dispatchEvent( new Event( 'click' ) );
+
+					sinon.assert.calledOnce( spy );
+					sinon.assert.calledWith( spy, sinon.match.any, {
+						value: '#000000'
+					} );
+				} );
+			} );
+
+			describe( 'binding', () => {
+				it( 'add tile item when document colors model is updated', () => {
+					let itm;
+
+					expect( documentColors.length ).to.equal( 0 );
+					expect( documentColorsGridView.items.length ).to.equal( 0 );
+
+					documentColors.add( Object.assign( {}, colorBlack ) );
+					expect( documentColors.length ).to.equal( 1 );
+					expect( documentColorsGridView.items.length ).to.equal( 1 );
+
+					itm = documentColorsGridView.items.first;
+					expect( itm ).to.be.instanceOf( ColorTileView );
+					expect( itm.label ).to.equal( 'Black' );
+					expect( itm.color ).to.equal( '#000000' );
+					expect( itm.hasBorder ).to.be.false;
+
+					documentColors.add( Object.assign( {}, colorEmpty ) );
+					itm = documentColorsGridView.items.get( 1 );
+					expect( itm ).to.be.instanceOf( ColorTileView );
+					expect( itm.color ).to.equal( 'hsla(0,0%,0%,0)' );
+					expect( itm.hasBorder ).to.be.true;
+				} );
+			} );
+		} );
+
+		describe( 'empty', () => {
+			let colorTableView;
+			beforeEach( () => {
+				locale = { t() {} };
+				colorTableView = new ColorTableView( locale, {
+					colors: colorDefinitions,
+					columns: 5,
+					removeButtonLabel: 'Remove color',
+					documentColorsCount: 0
+				} );
+				colorTableView.render();
+			} );
+
+			afterEach( () => {
+				colorTableView.destroy();
+			} );
+
+			it( 'should not add document colors grid to the view', () => {
+				expect( colorTableView.items.length ).to.equal( 2 );
+				expect( colorTableView.documentColors.length ).to.equal( 0 );
+				expect( colorTableView.documentColorsCount ).to.equal( 0 );
+			} );
+		} );
+	} );
+
+	describe( '_addColorToDocumentColors', () => {
+		it( 'add custom color from not defined colors', () => {
+			colorTableView._addColorToDocumentColors( '#123456' );
+			expect( colorTableView.documentColors.get( 0 ) ).to.deep.include( {
+				color: '#123456',
+				label: '#123456',
+				options: {
+					hasBorder: false
+				}
+			} );
+		} );
+
+		it( 'add already define color based on color value', () => {
+			colorTableView._addColorToDocumentColors( 'rgb(255,255,255)' );
+			// Color values are kept without spaces.
+			expect( colorTableView.documentColors.get( 0 ) ).to.deep.include( {
+				color: 'rgb(255,255,255)'
+			} );
+		} );
+	} );
+
+	describe( 'updateSelectedColors() with document colors', () => {
+		let element, editor, model, dropdown, staticColorsGrid, documentColorsGrid;
+
+		beforeEach( () => {
+			element = document.createElement( 'div' );
+			document.body.appendChild( element );
+
+			return ClassicTestEditor
+				.create( element, {
+					plugins: [ Paragraph, TestColorPlugin ],
+					testColor: Object.assign( {
+						documentColors: 3
+					}, testColorConfig )
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+
+					dropdown = editor.ui.componentFactory.create( 'testColor' );
+					dropdown.render();
+
+					staticColorsGrid = dropdown.colorTableView.staticColorsGrid;
+					documentColorsGrid = dropdown.colorTableView.documentColorsGrid;
+
+					global.document.body.appendChild( dropdown.element );
+				} );
+		} );
+
+		afterEach( () => {
+			element.remove();
+			dropdown.element.remove();
+			dropdown.destroy();
+
+			return editor.destroy();
+		} );
+
+		it( 'checkmark is present in document colors section', () => {
+			const command = editor.commands.get( 'testColorCommand' );
+
+			setModelData( model,
+				'<paragraph><$text testColor="red">Foo</$text></paragraph>'
+			);
+			command.value = 'red';
+
+			dropdown.isOpen = true;
+
+			expect( staticColorsGrid.selectedColor ).to.be.null;
+			expect( documentColorsGrid.selectedColor ).to.equal( 'red' );
+
+			const redStaticColorTile = staticColorsGrid.items.find( tile => tile.color === 'red' );
+			const redDocumentColorTile = documentColorsGrid.items.get( 0 );
+
+			expect( redStaticColorTile.isOn ).to.be.false;
+			expect( redDocumentColorTile.isOn ).to.be.true;
+		} );
+
+		it( 'checkmark is present in static colors', () => {
+			const command = editor.commands.get( 'testColorCommand' );
+
+			setModelData( model,
+				'<paragraph><$text testColor="gold">Bar</$text></paragraph>' +
+				'<paragraph><$text testColor="rgb(10,20,30)">Foo</$text></paragraph>' +
+				'<paragraph><$text testColor="#FFAACC">Baz</$text></paragraph>' +
+				'<paragraph><$text testColor="#00FF00">Test</$text></paragraph>'
+			);
+			command.value = '#00FF00';
+
+			dropdown.isOpen = true;
+
+			expect( staticColorsGrid.selectedColor ).to.equal( '#00FF00' );
+			expect( documentColorsGrid.selectedColor ).to.be.null;
+
+			const redStaticColorTile = staticColorsGrid.items.find( tile => tile.color === '#00FF00' );
+			const redDocumentColorTile = documentColorsGrid.items.get( 0 );
+
+			expect( redStaticColorTile.isOn ).to.be.true;
+			expect( redDocumentColorTile.isOn ).to.be.false;
+		} );
+	} );
+
+	describe( 'empty document colors', () => {
+		let editor, element, dropdown, model;
+
+		beforeEach( () => {
+			element = document.createElement( 'div' );
+			document.body.appendChild( element );
+
+			return ClassicTestEditor
+				.create( element, {
+					plugins: [ Paragraph, TestColorPlugin ],
+					testColor: Object.assign( {
+						documentColors: 0
+					}, testColorConfig )
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					dropdown = editor.ui.componentFactory.create( 'testColor' );
+
+					dropdown.render();
+					global.document.body.appendChild( dropdown.element );
+				} );
+		} );
+
+		afterEach( () => {
+			element.remove();
+			dropdown.element.remove();
+			dropdown.destroy();
+
+			return editor.destroy();
+		} );
+
+		it( 'document colors are not created', () => {
+			const colorTableView = dropdown.colorTableView;
+
+			setModelData( model,
+				'<paragraph><$text testColor="gold">Bar</$text></paragraph>' +
+				'<paragraph><$text testColor="rgb(10,20,30)">Foo</$text></paragraph>' +
+				'<paragraph><$text testColor="gold">New Foo</$text></paragraph>' +
+				'<paragraph><$text testColor="#FFAACC">Baz</$text></paragraph>'
+			);
+
+			dropdown.isOpen = true;
+
+			expect( colorTableView.documentColorsCount ).to.equal( 0 );
+			expect( colorTableView.documentColorsLabel ).to.be.undefined;
+		} );
+	} );
 } );

+ 122 - 28
packages/ckeditor5-font/tests/ui/colorui.js

@@ -5,31 +5,15 @@
 
 /* global document */
 
-import ColorUI from './../../src/ui/colorui';
-import FontColorCommand from './../../src/fontcolor/fontcolorcommand';
-
+import TestColorPlugin from '../_utils/testcolorplugin';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 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';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 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',
@@ -65,13 +49,21 @@ describe( 'ColorUI', () => {
 			'Red': 'Czerwony',
 			'Green': 'Zielony'
 		} );
+		addTranslations( 'en', {
+			'Test Color': 'Test Color',
+			'Remove color': 'Remove color',
+			'Yellow': 'Yellow',
+			'White': 'White',
+			'Red': 'Red',
+			'Green': 'Green'
+		} );
 	} );
 
 	after( () => {
 		clearTranslations();
 	} );
 
-	let editor, element, testColorPlugin, command;
+	let editor, element, model, testColorPlugin, command;
 
 	beforeEach( () => {
 		element = document.createElement( 'div' );
@@ -79,11 +71,12 @@ describe( 'ColorUI', () => {
 
 		return ClassicTestEditor
 			.create( element, {
-				plugins: [ TestColorPlugin ],
+				plugins: [ Paragraph, TestColorPlugin ],
 				testColor: testColorConfig
 			} )
 			.then( newEditor => {
 				editor = newEditor;
+				model = editor.model;
 				testColorPlugin = newEditor.plugins.get( 'TestColorPlugin' );
 			} );
 	} );
@@ -108,7 +101,7 @@ describe( 'ColorUI', () => {
 		} );
 
 		it( 'has assigned proper dropdownLabel', () => {
-			expect( testColorPlugin.dropdownLabel ).to.equal( 'Testowy plugin' );
+			expect( testColorPlugin.dropdownLabel ).to.equal( 'Test Color' );
 		} );
 
 		it( 'has assigned proper amount of columns', () => {
@@ -123,21 +116,23 @@ describe( 'ColorUI', () => {
 		beforeEach( () => {
 			command = editor.commands.get( 'testColorCommand' );
 			dropdown = editor.ui.componentFactory.create( 'testColor' );
+
+			dropdown.render();
+		} );
+
+		afterEach( () => {
+			dropdown.destroy();
 		} );
 
 		it( 'button has the base properties', () => {
 			const button = dropdown.buttonView;
 
-			expect( button ).to.have.property( 'label', 'Testowy plugin' );
+			expect( button ).to.have.property( 'label', 'Test Color' );
 			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;
 		} );
 
@@ -151,6 +146,12 @@ describe( 'ColorUI', () => {
 			sinon.assert.calledOnce( focusSpy );
 		} );
 
+		it( 'colorTableView has set proper default attributes', () => {
+			const colorTableView = dropdown.colorTableView;
+
+			expect( colorTableView.documentColorsCount ).to.equal( 3 );
+		} );
+
 		describe( 'model to command binding', () => {
 			it( 'isEnabled', () => {
 				command.isEnabled = false;
@@ -162,6 +163,99 @@ describe( 'ColorUI', () => {
 			} );
 		} );
 
+		describe( 'properly detects document colors on dropdown open', () => {
+			let documentColorsModel, dropdown;
+			beforeEach( () => {
+				dropdown = editor.ui.componentFactory.create( 'testColor' );
+				dropdown.render();
+				documentColorsModel = dropdown.colorTableView.documentColors;
+				global.document.body.appendChild( dropdown.element );
+			} );
+			afterEach( () => {
+				dropdown.element.remove();
+				dropdown.destroy();
+			} );
+
+			it( 'adds to model colors from editor and not duplicates it', () => {
+				setModelData( model,
+					'<paragraph><$text testColor="gold">Bar</$text></paragraph>' +
+					'<paragraph><$text testColor="rgb(10,20,30)">Foo</$text></paragraph>' +
+					'<paragraph><$text testColor="gold">New Foo</$text></paragraph>' +
+					'<paragraph><$text testColor="#FFAACC">Baz</$text></paragraph>'
+				);
+
+				dropdown.isOpen = true;
+
+				expect( documentColorsModel.get( 0 ) ).to.deep.include( {
+					color: 'gold',
+					label: 'gold',
+					options: {
+						hasBorder: false
+					}
+				} );
+
+				expect( documentColorsModel.get( 1 ) ).to.deep.include( {
+					color: 'rgb(10,20,30)',
+					label: 'rgb(10,20,30)',
+					options: {
+						hasBorder: false
+					}
+				} );
+
+				expect( documentColorsModel.get( 2 ) ).to.deep.include( {
+					color: '#FFAACC',
+					label: '#FFAACC',
+					options: {
+						hasBorder: false
+					}
+				} );
+			} );
+
+			it( 'reacts on document model changes', () => {
+				setModelData( model,
+					'<paragraph><$text testColor="rgb(10,20,30)">Foo</$text></paragraph>'
+				);
+
+				dropdown.isOpen = true;
+
+				expect( documentColorsModel.length ).to.equal( 1 );
+				expect( documentColorsModel.get( 0 ) ).to.deep.include( {
+					color: 'rgb(10,20,30)',
+					label: 'rgb(10,20,30)',
+					options: {
+						hasBorder: false
+					}
+				} );
+
+				dropdown.isOpen = false;
+
+				setModelData( model,
+					'<paragraph><$text testColor="gold">Bar</$text></paragraph>' +
+					'<paragraph><$text testColor="#FFAACC">Baz</$text></paragraph>'
+				);
+
+				dropdown.isOpen = true;
+
+				expect( documentColorsModel.length ).to.equal( 2 );
+
+				expect( documentColorsModel.get( 0 ) ).to.deep.include( {
+					color: 'gold',
+					label: 'gold',
+					options: {
+						hasBorder: false
+					}
+				} );
+
+				expect( documentColorsModel.get( 1 ) ).to.deep.include( {
+					color: '#FFAACC',
+					label: '#FFAACC',
+					options: {
+						hasBorder: false
+					}
+				} );
+			} );
+		} );
+
 		describe( 'localization', () => {
 			let editor, editorElement;
 

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

@@ -8,3 +8,7 @@
 	align-items: center;
 	width: 100%;
 }
+
+label.ck.ck-color-grid__label {
+	font-weight: unset;
+}