8
0
Pārlūkot izejas kodu

Merge pull request #39 from ckeditor/t/28

Introduced document colors section to the font/background color plugins
Marek Lewandowski 6 gadi atpakaļ
vecāks
revīzija
2d3b61373e

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 3 - 0
packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.html


+ 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>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 32 - 1
packages/ckeditor5-font/docs/features/font.md


+ 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 );
 
 		/**
@@ -91,6 +98,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.
 		 *
 		 * @readonly
@@ -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;
+}