Browse Source

Merge branch 'ui' into i/1110

Aleksander Nowodzinski 6 năm trước cách đây
mục cha
commit
7c8fc089f8

+ 2 - 0
packages/ckeditor5-special-characters/src/specialcharacters.js

@@ -12,6 +12,8 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import SpecialCharactersUI from './specialcharactersui';
 import SpecialCharactersEditing from './specialcharactersediting';
 
+import '../theme/specialcharacters.css';
+
 /**
  * The special characters feature.
  *

+ 51 - 47
packages/ckeditor5-special-characters/src/specialcharactersui.js

@@ -10,86 +10,90 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import { createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 import specialCharactersIcon from '../theme/icons/specialcharacters.svg';
-import SpecialCharactersTableView from './ui/specialcharacterstableview';
-import SpecialCharactersSelectView from './ui/specialcharactersselectview';
+import InsertSpecialCharacterCommand from './insertspecialcharactercommand';
+import CharacterGridView from './ui/charactergridview';
+import SpecialCharactersNavigationView from './ui/specialcharactersnavigationview';
 
 /**
  * The special characters UI plugin.
  *
+ * Introduces the `'specialCharacters'` dropdown.
+ *
  * @extends module:core/plugin~Plugin
  */
 export default class SpecialCharactersUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'SpecialCharactersUI';
+	}
+
 	init() {
 		const editor = this.editor;
 		const t = editor.t;
-		const specialCharacterPlugin = editor.plugins.get( 'SpecialCharacters' );
-		const label = t( 'Special characters' );
-		const command = editor.commands.get( 'insertSpecialCharacter' );
+		const specialCharsPlugin = editor.plugins.get( 'SpecialCharacters' );
+		const command = new InsertSpecialCharacterCommand( editor );
+
+		editor.commands.add( 'specialCharacters', command );
 
 		// Add the `specialCharacters` dropdown button to feature components.
 		editor.ui.componentFactory.add( 'specialCharacters', locale => {
-			// Prepare the dropdown element.
 			const dropdownView = createDropdown( locale );
+			const navigationView = new SpecialCharactersNavigationView( locale, specialCharsPlugin.getGroups() );
+			const gridView = new CharacterGridView( this.locale, {
+				columns: 10
+			} );
+
+			gridView.delegate( 'execute' ).to( dropdownView );
+
+			// Set the initial content of the special characters grid.
+			this._updateGrid( specialCharsPlugin, navigationView.currentGroupName, gridView );
+
+			// Update the grid of special characters when a user changed the character group.
+			navigationView.on( 'execute', () => {
+				this._updateGrid( specialCharsPlugin, navigationView.currentGroupName, gridView );
+			} );
 
 			dropdownView.buttonView.set( {
-				label,
+				label: t( 'Special characters' ),
 				icon: specialCharactersIcon,
 				tooltip: true
 			} );
 
 			dropdownView.bind( 'isEnabled' ).to( command );
 
-			const specialCharactersSelectView = new SpecialCharactersSelectView( locale, {
-				labelText: label,
-				selectOptions: getSelectViewOptions()
-			} );
-
-			const symbolTableView = new SpecialCharactersTableView( locale );
-
-			symbolTableView.delegate( 'execute' ).to( dropdownView, 'execute' );
-
 			// Insert a special character when a tile was clicked.
 			dropdownView.on( 'execute', ( evt, data ) => {
-				command.execute( { item: data.title } );
-			} );
-
-			// Draw special characters tiles when the dropdown is open.
-			dropdownView.on( 'change:isOpen', ( evt, name, isVisible ) => {
-				if ( !isVisible ) {
-					return;
-				}
-
-				printCharacters( specialCharactersSelectView, symbolTableView.symbolGridView );
+				editor.execute( 'specialCharacters', { item: data.name } );
+				editor.editing.view.focus();
 			} );
 
-			// Draw special characters tiles for specified category (when a user has changed it).
-			specialCharactersSelectView.on( 'input', () => {
-				printCharacters( specialCharactersSelectView, symbolTableView.symbolGridView );
-			} );
-
-			dropdownView.panelView.children.add( specialCharactersSelectView );
-			dropdownView.panelView.children.add( symbolTableView );
+			dropdownView.panelView.children.add( navigationView );
+			dropdownView.panelView.children.add( gridView );
 
 			return dropdownView;
 		} );
+	}
 
-		function printCharacters( selectView, gridView ) {
-			// TODO: Keyboard navigation.
-			gridView.items.clear();
-
-			const groupName = selectView.value;
-			const characterTitles = specialCharacterPlugin.getCharactersForGroup( groupName );
+	/**
+	 * Updates the symbol grid depending on the currently selected character group.
+	 *
+	 * @private
+	 * @param {module:special-characters/specialcharacters~SpecialCharacters} specialCharsPlugin
+	 * @param {String} currentGroupName
+	 * @param {module:special-characters/ui/charactergridview~CharacterGridView} gridView
+	 */
+	_updateGrid( specialCharsPlugin, currentGroupName, gridView ) {
+		// Updating the grid starts with removing all tiles belonging to the old group.
+		gridView.tiles.clear();
 
-			for ( const title of characterTitles ) {
-				const character = specialCharacterPlugin.getCharacter( title );
+		const characterTitles = specialCharsPlugin.getCharactersForGroup( currentGroupName );
 
-				gridView.items.add( gridView.createSymbolTile( character, title ) );
-			}
-		}
+		for ( const title of characterTitles ) {
+			const character = specialCharsPlugin.getCharacter( title );
 
-		function getSelectViewOptions() {
-			return [ ...specialCharacterPlugin.getGroups() ]
-				.map( groupName => ( { label: groupName, value: groupName } ) );
+			gridView.tiles.add( gridView.createTile( character, title ) );
 		}
 	}
 }

+ 80 - 0
packages/ckeditor5-special-characters/src/ui/charactergridview.js

@@ -0,0 +1,80 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module special-characters/ui/charactergridview
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import '../../theme/charactergrid.css';
+
+/**
+ * A grid of character tiles. Allows browsing special characters and selecting the character to
+ * be inserted into the content.
+ *
+ * @extends module:ui/view~View
+ */
+export default class CharacterGridView extends View {
+	/**
+	 * Creates an instance of a character grid containing tiles representing special characters.
+	 *
+	 * @param {module:utils/locale~Locale} locale The localization services instance.
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		/**
+		 * Collection of the child tile views. Each tile represents some particular character.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.tiles = this.createCollection();
+
+		this.setTemplate( {
+			tag: 'div',
+			children: this.tiles,
+			attributes: {
+				class: [
+					'ck',
+					'ck-character-grid'
+				]
+			}
+		} );
+
+		/**
+		 * Fired when any of {@link #tiles grid tiles} is clicked.
+		 *
+		 * @event execute
+		 * @param {Object} data Additional information about the event.
+		 * @param {String} data.name Name of the tile that caused the event (e.g. "greek small letter epsilon").
+		 */
+	}
+
+	/**
+	 * Creates a new tile for the grid.
+	 *
+	 * @param {String} character A human-readable character displayed as label (e.g. "ε").
+	 * @param {String} name A name of the character (e.g. "greek small letter epsilon").
+	 * @returns {module:ui/button/buttonview~ButtonView}
+	 */
+	createTile( character, name ) {
+		const tile = new ButtonView( this.locale );
+
+		tile.set( {
+			label: character,
+			withText: true,
+			class: 'ck-character-grid__tile'
+		} );
+
+		tile.on( 'execute', () => {
+			this.fire( 'execute', { name } );
+		} );
+
+		return tile;
+	}
+}

+ 141 - 0
packages/ckeditor5-special-characters/src/ui/specialcharactersnavigationview.js

@@ -0,0 +1,141 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module special-characters/ui/specialcharactersnavigationview
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+import Model from '@ckeditor/ckeditor5-ui/src/model';
+import LabelView from '@ckeditor/ckeditor5-ui/src/label/labelview';
+import {
+	createDropdown,
+	addListToDropdown
+} from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+
+/**
+ * A class representing the navigation part of the special characters UI. It is responsible
+ * for describing the feature and allowing the user to select a particular character group.
+ *
+ * @extends module:ui/view~View
+ */
+export default class SpecialCharactersNavigationView extends View {
+	/**
+	 * Creates an instance of the {@link module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView}
+	 * class.
+	 *
+	 * @param {module:utils/locale~Locale} locale The localization services instance.
+	 * @param {Iterable.<String>} groupNames Names of the character groups.
+	 */
+	constructor( locale, groupNames ) {
+		super( locale );
+
+		const t = locale.t;
+
+		/**
+		 * Label of the navigation view describing its purpose.
+		 *
+		 * @member {module:ui/label/labelview~LabelView}
+		 */
+		this.labelView = new LabelView( locale );
+		this.labelView.text = t( 'Special characters' );
+
+		/**
+		 * A dropdown that allows selecting a group of special characters to be displayed.
+		 *
+		 * @member {module:ui/dropdown/dropdownview~DropdownView}
+		 */
+		this.groupDropdownView = this._createGroupDropdown( groupNames );
+
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					'ck-special-characters-navigation'
+				]
+			},
+			children: [
+				this.labelView,
+				this.groupDropdownView
+			]
+		} );
+	}
+
+	/**
+	 * Returns a name of the character group currently selected in the {@link #groupDropdownView}.
+	 *
+	 * @returns {String}
+	 */
+	get currentGroupName() {
+		return this.groupDropdownView.value;
+	}
+
+	/**
+	 * Returns a dropdown that allows selecting character groups.
+	 *
+	 * @private
+	 * @param {Iterable.<String>} groupNames Names of the character groups.
+	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
+	 */
+	_createGroupDropdown( groupNames ) {
+		const locale = this.locale;
+		const t = locale.t;
+		const dropdown = createDropdown( locale );
+		const groupDefinitions = this._getCharacterGroupListItemDefinitions( dropdown, groupNames );
+
+		dropdown.set( 'value', groupDefinitions.first.model.label );
+
+		dropdown.buttonView.bind( 'label' ).to( dropdown, 'value' );
+
+		dropdown.buttonView.set( {
+			isOn: false,
+			withText: true,
+			tooltip: t( 'Character categories' )
+		} );
+
+		dropdown.on( 'execute', evt => {
+			dropdown.value = evt.source.label;
+		} );
+
+		dropdown.delegate( 'execute' ).to( this );
+
+		addListToDropdown( dropdown, groupDefinitions );
+
+		return dropdown;
+	}
+
+	/**
+	 * Returns list item definitions to be used in the character group dropdown
+	 * representing specific character groups.
+	 *
+	 * @private
+	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
+	 * @param {Iterable.<String>} groupNames Names of the character groups.
+	 * @returns {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>}
+	 */
+	_getCharacterGroupListItemDefinitions( dropdown, groupNames ) {
+		const groupDefs = new Collection();
+
+		for ( const name of groupNames ) {
+			const definition = {
+				type: 'button',
+				model: new Model( {
+					label: name,
+					withText: true
+				} )
+			};
+
+			definition.model.bind( 'isOn' ).to( dropdown, 'value', value => {
+				return value === definition.model.label;
+			} );
+
+			groupDefs.add( definition );
+		}
+
+		return groupDefs;
+	}
+}

+ 0 - 83
packages/ckeditor5-special-characters/src/ui/specialcharactersselectview.js

@@ -1,83 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module special-characters/ui/specialcharactersselectview
- */
-
-import View from '@ckeditor/ckeditor5-ui/src/view';
-import LabelView from '@ckeditor/ckeditor5-ui/src/label/labelview';
-import SelectView from '@ckeditor/ckeditor5-ui/src/select/selectview';
-import uid from '@ckeditor/ckeditor5-utils/src/uid';
-
-/**
- * @extends module:ui/view~View
- */
-export default class SpecialCharactersSelectView extends View {
-	/**
-	 * Creates a view to be inserted as a child of {@link module:ui/dropdown/dropdownview~DropdownView}.
-	 *
-	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
-	 * @param {Object} options
-	 * @param {String} options.labelText A label for the select element.
-	 * @param {Array.<module:ui/select/selectview~SelectViewItem>} options.selectOptions Options to chose in the select view.
-	 */
-	constructor( locale, options ) {
-		super( locale );
-
-		const inputUid = `ck-select-${ uid() }`;
-
-		/**
-		 * A label for the select view.
-		 *
-		 * @member {module:ui/label/labelview~LabelView}
-		 */
-		this.label = new LabelView( this.locale );
-
-		/**
-		 * Select view for changing a category of special characters.
-		 *
-		 * @member {module:ui/select/selectview~SelectView}
-		 */
-		this.selectView = new SelectView( this.locale, options.selectOptions );
-		this.selectView.delegate( 'input' ).to( this );
-
-		this.label.for = inputUid;
-		this.label.text = options.labelText;
-		this.label.extendTemplate( {
-			attributes: {
-				class: [
-					'ck',
-					'ck-symbol-grid__label'
-				]
-			}
-		} );
-
-		this.selectView.id = inputUid;
-
-		this.setTemplate( {
-			tag: 'div',
-			attributes: {
-				class: [
-					'ck',
-					'ck-grid-table'
-				]
-			},
-			children: [
-				this.label,
-				this.selectView
-			]
-		} );
-	}
-
-	/**
-	 * Returns a value from the select view.
-	 *
-	 * @returns {String}
-	 */
-	get value() {
-		return this.selectView.element.value;
-	}
-}

+ 0 - 148
packages/ckeditor5-special-characters/src/ui/specialcharacterstableview.js

@@ -1,148 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module special-characters/ui/specialcharacterstableview
- */
-
-import View from '@ckeditor/ckeditor5-ui/src/view';
-import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
-import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
-import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
-import SymbolGridView from './symbolgridview';
-
-// TODO: Keyboard navigation does not work.
-
-/**
- * @extends module:ui/view~View
- */
-export default class SpecialCharactersTableView extends View {
-	/**
-	 * Creates a view to be inserted as a child of {@link module:ui/dropdown/dropdownview~DropdownView}.
-	 *
-	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
-	 */
-	constructor( locale ) {
-		super( locale );
-
-		/**
-		 * A collection of the children of the table.
-		 *
-		 * @readonly
-		 * @member {module:ui/viewcollection~ViewCollection}
-		 */
-		this.items = this.createCollection();
-
-		/**
-		 * Tracks information about the DOM focus in the list.
-		 *
-		 * @readonly
-		 * @member {module:utils/focustracker~FocusTracker}
-		 */
-		this.focusTracker = new FocusTracker();
-
-		/**
-		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
-		 *
-		 * @readonly
-		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
-		 */
-		this.keystrokes = new KeystrokeHandler();
-
-		/**
-		 * An array with definitions of special characters to be displayed in the table.
-		 *
-		 * @member {Array.<module:special-characters/specialcharacters~SpecialCharacterDefinition>}
-		 */
-		this.symbolDefinitions = [];
-
-		/**
-		 * Preserves the reference to {@link module:special-characters/ui/symbolgridview~SymbolGridView} used to create
-		 * the default (static) symbol set.
-		 *
-		 * @readonly
-		 * @member {module:special-characters/ui/symbolgridview~SymbolGridView}
-		 */
-		this.symbolGridView = this._createSymbolGridView();
-
-		/**
-		 * Helps cycling over focusable {@link #items} in the list.
-		 *
-		 * @readonly
-		 * @protected
-		 * @member {module:ui/focuscycler~FocusCycler}
-		 */
-		this._focusCycler = new FocusCycler( {
-			focusables: this.items,
-			focusTracker: this.focusTracker,
-			keystrokeHandler: this.keystrokes,
-			actions: {
-				// Navigate list items backwards using the Arrow Up key.
-				focusPrevious: 'arrowup',
-
-				// Navigate list items forwards using the Arrow Down key.
-				focusNext: 'arrowdown',
-			}
-		} );
-
-		this.setTemplate( {
-			tag: 'div',
-			attributes: {
-				class: [
-					'ck',
-					'ck-grid-table'
-				]
-			},
-			children: this.items
-		} );
-
-		this.items.add( this.symbolGridView );
-	}
-
-	/**
-	 * @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();
-	}
-
-	/**
-	 * Creates a static symbol table grid based on the editor configuration.
-	 *
-	 * @private
-	 * @returns {module:special-characters/ui/symbolgridview~SymbolGridView}
-	 */
-	_createSymbolGridView() {
-		const symbolGrid = new SymbolGridView( this.locale, {
-			symbolDefinitions: this.symbolDefinitions
-		} );
-
-		symbolGrid.delegate( 'execute' ).to( this );
-
-		return symbolGrid;
-	}
-}

+ 0 - 166
packages/ckeditor5-special-characters/src/ui/symbolgridview.js

@@ -1,166 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module special-characters/ui/symbolgridview
- */
-
-import View from '@ckeditor/ckeditor5-ui/src/view';
-import SymbolTileView from './symboltileview';
-import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
-import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
-import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
-import '../../theme/components/symbolgrid/symbolgrid.css';
-
-/**
- * A grid of {@link module:special-characters/ui/symboltileview~SymbolTileView symbol tiles}.
- *
- * @extends module:ui/view~View
- */
-export default class SymbolGridView extends View {
-	/**
-	 * Creates an instance of a symbol grid containing {@link module:special-characters/ui/symboltileview~SymbolTileView tiles}.
-	 *
-	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
-	 * @param {Object} options Component configuration
-	 * @param {Array.<module:special-characters/specialcharacters~SpecialCharacterDefinition>} [options.symbolDefinitions] Array with
-	 * definitions required to create the {@link module:special-characters/ui/symboltileview~SymbolTileView tiles}.
-	 */
-	constructor( locale, options ) {
-		super( locale );
-
-		const symbolDefinitions = options && options.symbolDefinitions || [];
-
-		/**
-		 * 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',
-			}
-		} );
-
-		this.items.on( 'add', ( evt, symbolTile ) => {
-			symbolTile.isOn = symbolTile.symbol === this.selectedsymbol;
-		} );
-
-		symbolDefinitions.forEach( item => {
-			const symbolTile = this.createSymbolTile( item.character, item.title );
-
-			this.items.add( symbolTile );
-		} );
-
-		this.setTemplate( {
-			tag: 'div',
-			children: this.items,
-			attributes: {
-				class: [
-					'ck',
-					'ck-symbol-grid'
-				]
-			}
-		} );
-	}
-
-	/**
-	 * 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 );
-	}
-
-	/**
-	 * Creates a new tile for the grid.
-	 *
-	 * @param {String} character A character that will be displayed on the button.
-	 * @param {String} title A label that described the character.
-	 * @returns {module:special-characters/ui/symboltileview~SymbolTileView}
-	 */
-	createSymbolTile( character, title ) {
-		const symbolTile = new SymbolTileView();
-
-		symbolTile.set( {
-			symbol: character,
-			label: character,
-			tooltip: title,
-			withText: true
-		} );
-
-		symbolTile.on( 'execute', () => {
-			this.fire( 'execute', { title } );
-		} );
-
-		return symbolTile;
-	}
-}

+ 0 - 35
packages/ckeditor5-special-characters/src/ui/symboltileview.js

@@ -1,35 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module special-characters/ui/symboltileview
- */
-
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-
-/**
- * @extends module:ui/button/buttonview~ButtonView
- */
-export default class SymbolTileView extends ButtonView {
-	constructor( locale ) {
-		super( locale );
-
-		/**
-		 * String representing a symbol shown as tile's value.
-		 *
-		 * @type {String}
-		 */
-		this.set( 'symbol' );
-
-		this.extendTemplate( {
-			attributes: {
-				class: [
-					'ck',
-					'ck-symbol-grid__tile',
-				]
-			}
-		} );
-	}
-}

+ 128 - 0
packages/ckeditor5-special-characters/tests/specialcharactersui.js

@@ -0,0 +1,128 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global document */
+
+import SpecialCharacters from '../src/specialcharacters';
+import SpecialCharactersMathematical from '../src/specialcharactersmathematical';
+import SpecialCharactersArrows from '../src/specialcharactersarrows';
+import SpecialCharactersUI from '../src/specialcharactersui';
+import SpecialCharactersNavigationView from '../src/ui/specialcharactersnavigationview';
+import CharacterGridView from '../src/ui/charactergridview';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import specialCharactersIcon from '../theme/icons/specialcharacters.svg';
+import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
+
+describe( 'SpecialCharactersUI', () => {
+	let editor, command, element;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [
+					SpecialCharacters,
+					SpecialCharactersMathematical,
+					SpecialCharactersArrows,
+					SpecialCharactersUI
+				]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				command = editor.commands.get( 'specialCharacters' );
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'should be named', () => {
+		expect( SpecialCharactersUI.pluginName ).to.equal( 'SpecialCharactersUI' );
+	} );
+
+	describe( '"specialCharacters" dropdown', () => {
+		let dropdown;
+
+		beforeEach( () => {
+			dropdown = editor.ui.componentFactory.create( 'specialCharacters' );
+		} );
+
+		afterEach( () => {
+			dropdown.destroy();
+		} );
+
+		it( 'has a navigation view', () => {
+			expect( dropdown.panelView.children.first ).to.be.instanceOf( SpecialCharactersNavigationView );
+		} );
+
+		it( 'has a grid view', () => {
+			expect( dropdown.panelView.children.last ).to.be.instanceOf( CharacterGridView );
+		} );
+
+		describe( '#buttonView', () => {
+			it( 'should get basic properties', () => {
+				expect( dropdown.buttonView.label ).to.equal( 'Special characters' );
+				expect( dropdown.buttonView.icon ).to.equal( specialCharactersIcon );
+				expect( dropdown.buttonView.tooltip ).to.be.true;
+			} );
+
+			it( 'should bind #isEnabled to the command', () => {
+				expect( dropdown.isEnabled ).to.be.true;
+
+				command.isEnabled = false;
+				expect( dropdown.isEnabled ).to.be.false;
+			} );
+		} );
+
+		it( 'executes a command and focuses the editing view', () => {
+			const grid = dropdown.panelView.children.last;
+			const executeSpy = sinon.stub( editor, 'execute' );
+			const focusSpy = sinon.stub( editor.editing.view, 'focus' );
+
+			grid.tiles.get( 2 ).fire( 'execute' );
+
+			sinon.assert.calledOnce( executeSpy );
+			sinon.assert.calledOnce( focusSpy );
+			sinon.assert.calledWithExactly( executeSpy.firstCall, 'specialCharacters', {
+				item: 'greek small letter delta'
+			} );
+		} );
+
+		describe( 'grid view', () => {
+			let grid;
+
+			beforeEach( () => {
+				grid = dropdown.panelView.children.last;
+			} );
+
+			it( 'delegates #execute to the dropdown', () => {
+				const spy = sinon.spy();
+
+				dropdown.on( 'execute', spy );
+				grid.fire( 'execute', { name: 'foo' } );
+
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'has default contents', () => {
+				expect( grid.tiles ).to.have.length.greaterThan( 10 );
+			} );
+
+			it( 'is updated when navigation view fires #execute', () => {
+				const navigation = dropdown.panelView.children.first;
+
+				expect( grid.tiles.get( 0 ).label ).to.equal( 'α' );
+				navigation.groupDropdownView.fire( new EventInfo( { label: 'Arrows' }, 'execute' ) );
+
+				expect( grid.tiles.get( 0 ).label ).to.equal( '⇐' );
+			} );
+		} );
+	} );
+} );

+ 57 - 0
packages/ckeditor5-special-characters/tests/ui/charactergridview.js

@@ -0,0 +1,57 @@
+/**
+ * @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 CharacterGridView from '../../src/ui/charactergridview';
+import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'CharacterGridView', () => {
+	let view;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		view = new CharacterGridView();
+		view.render();
+	} );
+
+	afterEach( () => {
+		view.destroy();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'creates view#tiles collection', () => {
+			expect( view.tiles ).to.be.instanceOf( ViewCollection );
+		} );
+
+		it( 'creates #element from template', () => {
+			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-character-grid' ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'createTile()', () => {
+		it( 'creates a new tile button', () => {
+			const tile = view.createTile( 'ε', 'foo bar baz' );
+
+			expect( tile ).to.be.instanceOf( ButtonView );
+			expect( tile.label ).to.equal( 'ε' );
+			expect( tile.withText ).to.be.true;
+			expect( tile.class ).to.equal( 'ck-character-grid__tile' );
+		} );
+
+		it( 'delegates #execute from the tile to the grid', () => {
+			const tile = view.createTile( 'ε', 'foo bar baz' );
+			const spy = sinon.spy();
+
+			view.on( 'execute', spy );
+			tile.fire( 'execute' );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, sinon.match.any, { name: 'foo bar baz' } );
+		} );
+	} );
+} );

+ 115 - 0
packages/ckeditor5-special-characters/tests/ui/specialcharactersnavigationview.js

@@ -0,0 +1,115 @@
+/**
+ * @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 SpecialCharactersNavigationView from '../../src/ui/specialcharactersnavigationview';
+import LabelView from '@ckeditor/ckeditor5-ui/src/label/labelview';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'SpecialCharactersNavigationView', () => {
+	let view, locale;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		locale = {
+			t: val => val
+		};
+
+		view = new SpecialCharactersNavigationView( locale, [ 'groupA', 'groupB' ] );
+		view.render();
+	} );
+
+	afterEach( () => {
+		view.destroy();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'creates #labelView', () => {
+			expect( view.labelView ).to.be.instanceOf( LabelView );
+			expect( view.labelView.text ).to.equal( 'Special characters' );
+		} );
+
+		it( 'creates #groupDropdownView', () => {
+		} );
+
+		it( 'creates #element from template', () => {
+			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-special-characters-navigation' ) ).to.be.true;
+
+			expect( view.element.firstChild ).to.equal( view.labelView.element );
+			expect( view.element.lastChild ).to.equal( view.groupDropdownView.element );
+		} );
+	} );
+
+	describe( 'currentGroupName()', () => {
+		it( 'returns the #value of #groupDropdownView', () => {
+			expect( view.currentGroupName ).to.equal( 'groupA' );
+
+			view.groupDropdownView.listView.items.last.children.first.fire( 'execute' );
+			expect( view.currentGroupName ).to.equal( 'groupB' );
+		} );
+	} );
+
+	describe( '#groupDropdownView', () => {
+		let groupDropdownView;
+
+		beforeEach( () => {
+			groupDropdownView = view.groupDropdownView;
+		} );
+
+		it( 'has a default #value', () => {
+			expect( view.currentGroupName ).to.equal( 'groupA' );
+		} );
+
+		it( 'binds its buttonView#label to #value', () => {
+			expect( groupDropdownView.buttonView.label ).to.equal( 'groupA' );
+
+			groupDropdownView.listView.items.last.children.first.fire( 'execute' );
+			expect( groupDropdownView.buttonView.label ).to.equal( 'groupB' );
+		} );
+
+		it( 'configures the #buttonView', () => {
+			expect( groupDropdownView.buttonView.isOn ).to.be.false;
+			expect( groupDropdownView.buttonView.withText ).to.be.true;
+			expect( groupDropdownView.buttonView.tooltip ).to.equal( 'Character categories' );
+		} );
+
+		it( 'delegates #execute to the naviation view', () => {
+			const spy = sinon.spy();
+
+			view.on( 'execute', spy );
+
+			groupDropdownView.fire( 'execute' );
+			sinon.assert.calledOnce( spy );
+		} );
+
+		describe( 'character group list items', () => {
+			it( 'have basic properties', () => {
+				expect( groupDropdownView.listView.items
+					.map( item => {
+						const { label, withText } = item.children.first;
+
+						return { label, withText };
+					} ) )
+					.to.deep.equal( [
+						{ label: 'groupA', withText: true },
+						{ label: 'groupB', withText: true },
+					] );
+			} );
+
+			it( 'bind #isOn to the #value of the dropdown', () => {
+				const firstButton = groupDropdownView.listView.items.first.children.last;
+				const lastButton = groupDropdownView.listView.items.last.children.last;
+
+				expect( firstButton.isOn ).to.be.true;
+				expect( lastButton.isOn ).to.be.false;
+
+				groupDropdownView.value = 'groupB';
+				expect( firstButton.isOn ).to.be.false;
+				expect( lastButton.isOn ).to.be.true;
+			} );
+		} );
+	} );
+} );

+ 1 - 1
packages/ckeditor5-special-characters/theme/components/symbolgrid/symbolgrid.css → packages/ckeditor5-special-characters/theme/charactergrid.css

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-.ck.ck-symbol-grid {
+.ck.ck-character-grid {
 	display: grid;
 	grid-template-columns: repeat(10, 1fr);
 }

+ 12 - 0
packages/ckeditor5-special-characters/theme/specialcharacters.css

@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+.ck.ck-special-characters-navigation {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: nowrap;
+	align-items: center;
+	justify-content: space-between;
+}