瀏覽代碼

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

t/ckeditor5/1457: Implemented `ColorGridView` and `ColorTileView` components
Marek Lewandowski 6 年之前
父節點
當前提交
0acddd5e31

+ 195 - 0
packages/ckeditor5-ui/src/colorgrid/colorgridview.js

@@ -0,0 +1,195 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/colorgrid/colorgrid
+ */
+
+import View from '../view';
+import ColorTileView from './colortileview';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import FocusCycler from '../focuscycler';
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import '../../theme/components/colorgrid/colorgrid.css';
+
+/**
+ * A grid of {@link module:ui/colorgrid/colortile~ColorTileView color tiles}.
+ *
+ * @extends module:ui/view~View
+ */
+export default class ColorGridView extends View {
+	/**
+	 * Creates an instance of a color grid containing {@link module:ui/colorgrid/colortile~ColorTileView tiles}.
+	 *
+	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
+	 * @param {Object} options Component configuration
+	 * @param {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>} [options.colorDefinitions] Array with definitions
+	 * required to create the {@link module:ui/colorgrid/colortile~ColorTileView tiles}.
+	 * @param {Number} options.columns A number of columns to display the tiles.
+	 */
+	constructor( locale, options ) {
+		super( locale );
+
+		const colorDefinitions = options && options.colorDefinitions || [];
+		const viewStyleAttribute = {};
+
+		if ( options && options.columns ) {
+			viewStyleAttribute.gridTemplateColumns = `repeat( ${ options.columns }, 1fr)`;
+		}
+
+		/**
+		 * The color of the currently selected color tile in {@link #items}.
+		 *
+		 * @type {String}
+		 */
+		this.set( 'selectedColor' );
+
+		/**
+		 * Collection of the child tile views.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.items = this.createCollection();
+
+		/**
+		 * Tracks information about DOM focus in the grid.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
+		/**
+		 * Helps cycling over focusable {@link #items} in the grid.
+		 *
+		 * @readonly
+		 * @protected
+		 * @member {module:ui/focuscycler~FocusCycler}
+		 */
+		this._focusCycler = new FocusCycler( {
+			focusables: this.items,
+			focusTracker: this.focusTracker,
+			keystrokeHandler: this.keystrokes,
+			actions: {
+				// Navigate grid items backwards using the arrowup key.
+				focusPrevious: 'arrowleft',
+
+				// Navigate grid items forwards using the arrowdown key.
+				focusNext: 'arrowright',
+			}
+		} );
+
+		colorDefinitions.forEach( item => {
+			const colorTile = new ColorTileView();
+
+			colorTile.set( {
+				color: item.color,
+				label: item.label,
+				tooltip: true,
+				hasBorder: item.options.hasBorder
+			} );
+
+			colorTile.on( 'execute', () => {
+				this.fire( 'execute', {
+					value: item.color,
+					hasBorder: item.options.hasBorder,
+					label: item.label
+				} );
+			} );
+
+			this.items.add( colorTile );
+		} );
+
+		this.setTemplate( {
+			tag: 'div',
+			children: this.items,
+			attributes: {
+				class: [
+					'ck',
+					'ck-color-grid'
+				],
+				style: viewStyleAttribute
+			}
+		} );
+
+		this.on( 'change:selectedColor', ( evt, name, selectedColor ) => {
+			for ( const item of this.items ) {
+				item.isOn = item.color === selectedColor;
+			}
+		} );
+	}
+
+	/**
+	 * Focuses the first focusable in {@link #items}.
+	 */
+	focus() {
+		if ( this.items.length ) {
+			this.items.first.focus();
+		}
+	}
+
+	/**
+	 * Focuses the last focusable in {@link #items}.
+	 */
+	focusLast() {
+		if ( this.items.length ) {
+			this.items.last.focus();
+		}
+	}
+
+	/**
+	 * @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 );
+		}
+
+		this.items.on( 'add', ( evt, item ) => {
+			this.focusTracker.add( item.element );
+		} );
+
+		this.items.on( 'remove', ( evt, item ) => {
+			this.focusTracker.remove( item.element );
+		} );
+
+		// Start listening for the keystrokes coming from #element.
+		this.keystrokes.listenTo( this.element );
+	}
+}
+
+/**
+ * A color definition used to create a {@link module:ui/colorgrid/colortile~ColorTileView}.
+ *
+ *		{
+ *			color: hsl(0, 0%, 75%),
+ *			label: 'Light Grey',
+ *			options: {
+ *				hasBorder: true
+ *			}
+ *		}
+ *
+ * @typedef {Object} module:ui/colorgrid/colorgrid~ColorDefinition
+ * @type Object
+ *
+ * @property {String} color String representing a color.
+ * It is used as value of background-color style in {@link module:ui/colorgrid/colortile~ColorTileView}.
+ * @property {String} label String used as label for {@link module:ui/colorgrid/colortile~ColorTileView}.
+ * @property {Object} options Additional options passed to create a {@link module:ui/colorgrid/colortile~ColorTileView}.
+ * @property {Boolean} options.hasBorder A flag that indicates if special a CSS class should be added
+ * to {@link module:ui/colorgrid/colortile~ColorTileView}, which renders a border around it.
+ */

+ 54 - 0
packages/ckeditor5-ui/src/colorgrid/colortileview.js

@@ -0,0 +1,54 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/colorgrid/colortile
+ */
+
+import ButtonView from '../button/buttonview';
+import checkIcon from '../../theme/icons/color-tile-check.svg';
+
+/**
+ * This class represents a single color tile in the {@link module:ui/colorgrid/colorgrid~ColorGridView}.
+ *
+ * @extends module:ui/button/buttonview~ButtonView
+ */
+export default class ColorTileView extends ButtonView {
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * String representing a color shown as tile's background.
+		 *
+		 * @type {String}
+		 */
+		this.set( 'color' );
+
+		/**
+		 * A flag that toggles a special CSS class responsible for displaying
+		 * a border around the button.
+		 *
+		 * @type {Boolean}
+		 */
+		this.set( 'hasBorder' );
+
+		this.icon = checkIcon;
+
+		this.extendTemplate( {
+			attributes: {
+				style: {
+					backgroundColor: bind.to( 'color' )
+				},
+				class: [
+					'ck',
+					'ck-color-grid__tile',
+					bind.if( 'hasBorder', 'ck-color-table__color-tile_bordered' )
+				]
+			}
+		} );
+	}
+}

+ 203 - 0
packages/ckeditor5-ui/tests/colorgrid/colorgridview.js

@@ -0,0 +1,203 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals Event */
+
+import ColorGridView from './../../src/colorgrid/colorgridview';
+import ColorTileView from '../../src/colorgrid/colortileview';
+import ViewCollection from '../../src/viewcollection';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import FocusCycler from '../../src/focuscycler';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'ColorGridView', () => {
+	let locale, view;
+
+	const colorDefinitions = [
+		{
+			color: '#000',
+			label: 'Black',
+			options: {
+				hasBorder: false
+			}
+		},
+		{
+			color: 'rgb(255, 255, 255)',
+			label: 'White',
+			options: {
+				hasBorder: true
+			}
+		},
+		{
+			color: 'red',
+			label: 'Red',
+			options: {
+				hasBorder: false
+			}
+		}
+	];
+
+	beforeEach( () => {
+		locale = { t() {} };
+		view = new ColorGridView( locale, { colorDefinitions } );
+		view.render();
+	} );
+
+	afterEach( () => {
+		view.destroy();
+	} );
+
+	testUtils.createSinonSandbox();
+
+	describe( 'constructor()', () => {
+		it( 'creates element from template', () => {
+			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-color-grid' ) ).to.be.true;
+		} );
+
+		it( 'uses the options#columns to control the grid', () => {
+			const view = new ColorGridView( locale, { columns: 3 } );
+			view.render();
+
+			expect( view.element.style.gridTemplateColumns ).to.equal( '1fr 1fr 1fr' );
+
+			view.destroy();
+		} );
+
+		it( 'creates the view without provided color definitions', () => {
+			const view = new ColorGridView( locale );
+			view.render();
+
+			expect( view.items ).to.have.length( 0 );
+
+			view.destroy();
+		} );
+
+		it( 'creates view collection with children', () => {
+			expect( view.items ).to.be.instanceOf( ViewCollection );
+		} );
+
+		it( 'creates focus tracker', () => {
+			expect( view.focusTracker ).to.be.instanceOf( FocusTracker );
+		} );
+
+		it( 'creates keystroke handler', () => {
+			expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
+		} );
+
+		it( 'creates focus cycler', () => {
+			expect( view._focusCycler ).to.be.instanceOf( FocusCycler );
+		} );
+
+		it( 'reacts to changes in #selectedColor by setting the item#isOn', () => {
+			expect( view.items.map( item => item ).some( item => item.isOn ) ).to.be.false;
+
+			view.selectedColor = 'red';
+
+			expect( view.items.get( 2 ).isOn ).to.be.true;
+
+			view.selectedColor = 'rgb(255, 255, 255)';
+
+			expect( view.items.get( 1 ).isOn ).to.be.true;
+			expect( view.items.get( 2 ).isOn ).to.be.false;
+		} );
+
+		describe( 'add colors from definition as child items', () => {
+			it( 'has proper number of elements', () => {
+				expect( view.items.length ).to.equal( 3 );
+			} );
+
+			colorDefinitions.forEach( ( color, index ) => {
+				describe( 'child items has proper attributes', () => {
+					it( `for (index: ${ index }, color: ${ color.color }) child`, () => {
+						const colorTile = view.items.get( index );
+
+						expect( colorTile ).to.be.instanceOf( ColorTileView );
+						expect( colorTile.color ).to.equal( color.color );
+					} );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'fires event for rendered tiles', () => {
+			const spy = sinon.spy();
+			const firstTile = view.items.first;
+
+			view.on( 'execute', spy );
+
+			firstTile.isEnabled = true;
+
+			firstTile.element.dispatchEvent( new Event( 'click' ) );
+			sinon.assert.callCount( spy, 1 );
+
+			firstTile.isEnabled = false;
+
+			firstTile.element.dispatchEvent( new Event( 'click' ) );
+			sinon.assert.callCount( spy, 1 );
+		} );
+	} );
+
+	describe( 'focus', () => {
+		it( 'focuses the tile in DOM', () => {
+			const spy = sinon.spy( view.items.first, 'focus' );
+
+			view.focus();
+
+			sinon.assert.calledOnce( spy );
+
+			view.items.clear();
+			view.focus();
+
+			expect( view.items.length ).to.equal( 0 );
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'focuses last the tile in DOM', () => {
+			const spy = sinon.spy( view.items.last, 'focus' );
+
+			view.focusLast();
+
+			sinon.assert.calledOnce( spy );
+
+			view.items.clear();
+			view.focusLast();
+
+			expect( view.items.length ).to.equal( 0 );
+			sinon.assert.calledOnce( spy );
+		} );
+
+		describe( 'update elements in focus tracker', () => {
+			it( 'adding new element', () => {
+				const spy = sinon.spy( view.focusTracker, 'add' );
+
+				const colorTile = new ColorTileView();
+				colorTile.set( {
+					color: 'yellow',
+					label: 'Yellow',
+					tooltip: true,
+					options: {
+						hasBorder: false
+					}
+				} );
+				view.items.add( colorTile );
+
+				expect( view.items.length ).to.equal( 4 );
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'removes element', () => {
+				const spy = sinon.spy( view.focusTracker, 'remove' );
+
+				view.items.remove( view.items.length - 1 );
+
+				expect( view.items.length ).to.equal( 2 );
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+	} );
+} );

+ 38 - 0
packages/ckeditor5-ui/tests/colorgrid/colortileview.js

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ColorTileView from '../../src/colorgrid/colortileview';
+import ButtonView from '../../src/button/buttonview';
+import checkIcon from '../../theme/icons/color-tile-check.svg';
+
+describe( 'ColorTileView', () => {
+	it( 'inherits from ButtonView', () => {
+		expect( new ColorTileView() ).to.be.instanceOf( ButtonView );
+	} );
+
+	it( 'has proper attributes and classes', () => {
+		const colorTile = new ColorTileView();
+		colorTile.render();
+
+		expect( colorTile.color ).to.be.undefined;
+		expect( colorTile.hasBorder ).to.be.undefined;
+
+		colorTile.set( 'color', 'green' );
+		expect( colorTile.color ).to.equal( 'green' );
+		expect( colorTile.element.style.backgroundColor ).to.equal( 'green' );
+		expect( colorTile.element.classList.contains( 'ck-color-grid__tile' ) ).to.be.true;
+		expect( colorTile.element.classList.contains( 'ck-color-table__color-tile_bordered' ) ).to.be.false;
+
+		colorTile.set( 'hasBorder', true );
+		expect( colorTile.element.classList.contains( 'ck-color-table__color-tile_bordered' ) ).to.be.true;
+	} );
+
+	it( 'has a check icon', () => {
+		const colorTile = new ColorTileView();
+		colorTile.render();
+
+		expect( colorTile.icon ).to.equal( checkIcon );
+	} );
+} );

+ 8 - 0
packages/ckeditor5-ui/theme/components/colorgrid/colorgrid.css

@@ -0,0 +1,8 @@
+/*
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+.ck.ck-color-grid {
+	display: grid;
+}

文件差異過大導致無法顯示
+ 1 - 0
packages/ckeditor5-ui/theme/icons/color-tile-check.svg