浏览代码

Merge pull request #1 from ckeditor/i/1110

Feature: Introduced the special characters plugin. See ckeditor/ckeditor5#1110.
Kamil Piechaczek 6 年之前
父节点
当前提交
09de9e6ece
共有 29 个文件被更改,包括 2601 次插入0 次删除
  1. 45 0
      packages/ckeditor5-special-characters/src/insertspecialcharactercommand.js
  2. 123 0
      packages/ckeditor5-special-characters/src/specialcharacters.js
  3. 48 0
      packages/ckeditor5-special-characters/src/specialcharactersarrows.js
  4. 190 0
      packages/ckeditor5-special-characters/src/specialcharacterscurrency.js
  5. 45 0
      packages/ckeditor5-special-characters/src/specialcharactersediting.js
  6. 43 0
      packages/ckeditor5-special-characters/src/specialcharactersessentials.js
  7. 542 0
      packages/ckeditor5-special-characters/src/specialcharacterslatin.js
  8. 206 0
      packages/ckeditor5-special-characters/src/specialcharactersmathematical.js
  9. 114 0
      packages/ckeditor5-special-characters/src/specialcharacterstext.js
  10. 100 0
      packages/ckeditor5-special-characters/src/specialcharactersui.js
  11. 88 0
      packages/ckeditor5-special-characters/src/ui/charactergridview.js
  12. 142 0
      packages/ckeditor5-special-characters/src/ui/specialcharactersnavigationview.js
  13. 109 0
      packages/ckeditor5-special-characters/tests/insertspecialcharactercommand.js
  14. 4 0
      packages/ckeditor5-special-characters/tests/manual/specialcharacters.html
  15. 58 0
      packages/ckeditor5-special-characters/tests/manual/specialcharacters.js
  16. 4 0
      packages/ckeditor5-special-characters/tests/manual/specialcharacters.md
  17. 114 0
      packages/ckeditor5-special-characters/tests/specialcharacters.js
  18. 56 0
      packages/ckeditor5-special-characters/tests/specialcharactersarrows.js
  19. 56 0
      packages/ckeditor5-special-characters/tests/specialcharacterscurrency.js
  20. 35 0
      packages/ckeditor5-special-characters/tests/specialcharactersediting.js
  21. 26 0
      packages/ckeditor5-special-characters/tests/specialcharactersessentials.js
  22. 56 0
      packages/ckeditor5-special-characters/tests/specialcharacterslatin.js
  23. 56 0
      packages/ckeditor5-special-characters/tests/specialcharacterstext.js
  24. 129 0
      packages/ckeditor5-special-characters/tests/specialcharactersui.js
  25. 57 0
      packages/ckeditor5-special-characters/tests/ui/charactergridview.js
  26. 133 0
      packages/ckeditor5-special-characters/tests/ui/specialcharactersnavigationview.js
  27. 9 0
      packages/ckeditor5-special-characters/theme/charactergrid.css
  28. 1 0
      packages/ckeditor5-special-characters/theme/icons/specialcharacters.svg
  29. 12 0
      packages/ckeditor5-special-characters/theme/specialcharacters.css

+ 45 - 0
packages/ckeditor5-special-characters/src/insertspecialcharactercommand.js

@@ -0,0 +1,45 @@
+/**
+ * @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/insertspecialcharactercommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+/**
+ * @extends module:core/command~Command
+ */
+export default class InsertSpecialCharacterCommand extends Command {
+	/**
+	 * Creates an instance of the command.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * @readonly
+		 * @private
+		 * @member {module:typing/inputcommand~InputCommand} #_inputCommand
+		 */
+		this._inputCommand = editor.commands.get( 'input' );
+
+		// Use the state of `Input` command to determine whether the special characters could be inserted.
+		this.bind( 'isEnabled' ).to( this._inputCommand, 'isEnabled' );
+	}
+
+	/**
+	 * @param {Object} options
+	 * @param {String} options.item An id of the special character that should be added to the editor.
+	 */
+	execute( options ) {
+		const editor = this.editor;
+		const character = editor.plugins.get( 'SpecialCharacters' ).getCharacter( options.item );
+
+		this._inputCommand.execute( { text: character } );
+	}
+}

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

@@ -0,0 +1,123 @@
+/**
+ * @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/specialcharacters
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import SpecialCharactersUI from './specialcharactersui';
+import SpecialCharactersEditing from './specialcharactersediting';
+
+import '../theme/specialcharacters.css';
+
+/**
+ * The special characters feature.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class SpecialCharacters extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * Registered characters. A pair of a character name and its symbol.
+		 *
+		 * @private
+		 * @member {Map.<String, String>} #_characters
+		 */
+		this._characters = new Map();
+
+		/**
+		 * Registered groups. Each group contains a collection with symbol names.
+		 *
+		 * @private
+		 * @member {Map.<String, Set.<String>>} #_groups
+		 */
+		this._groups = new Map();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ SpecialCharactersEditing, SpecialCharactersUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'SpecialCharacters';
+	}
+
+	/**
+	 * Adds a collection of special characters to specified group. A title of a special character must be unique.
+	 *
+	 * @param {String} groupName
+	 * @param {Array.<module:special-characters/specialcharacters~SpecialCharacterDefinition>} items
+	 */
+	addItems( groupName, items ) {
+		const group = this._getGroup( groupName );
+
+		for ( const item of items ) {
+			group.add( item.title );
+			this._characters.set( item.title, item.character );
+		}
+	}
+
+	/**
+	 * Returns iterator of special characters groups.
+	 *
+	 * @returns {Iterable.<String>}
+	 */
+	getGroups() {
+		return this._groups.keys();
+	}
+
+	/**
+	 * Returns a collection of symbol names (titles).
+	 *
+	 * @param {String} groupName
+	 * @returns {Set.<String>|undefined}
+	 */
+	getCharactersForGroup( groupName ) {
+		return this._groups.get( groupName );
+	}
+
+	/**
+	 * Returns a symbol of the special character for specified name. If the special character couldn't be found, `undefined` is returned.
+	 *
+	 * @param {String} title A title of the special character.
+	 * @returns {String|undefined}
+	 */
+	getCharacter( title ) {
+		return this._characters.get( title );
+	}
+
+	/**
+	 * Returns a group of special characters. If the group with the specified name does not exist, it will be created.
+	 *
+	 * @private
+	 * @param {String} groupName A name of group to create.
+	 */
+	_getGroup( groupName ) {
+		if ( !this._groups.has( groupName ) ) {
+			this._groups.set( groupName, new Set() );
+		}
+
+		return this._groups.get( groupName );
+	}
+}
+
+/**
+ * @typedef {Object} module:special-characters/specialcharacters~SpecialCharacterDefinition
+ *
+ * @property {String} title A unique title of the character.
+ * @property {String} character A symbol that should be inserted to the editor.
+ */

+ 48 - 0
packages/ckeditor5-special-characters/src/specialcharactersarrows.js

@@ -0,0 +1,48 @@
+/**
+ * @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/specialcharacters
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import SpecialCharacters from './specialcharacters';
+
+export default class SpecialCharactersArrows extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [
+			SpecialCharacters
+		];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this.editor.plugins.get( 'SpecialCharacters' ).addItems( 'Arrows', [
+			{ title: 'leftwards double arrow', character: '⇐' },
+			{ title: 'rightwards double arrow', character: '⇒' },
+			{ title: 'upwards double arrow', character: '⇑' },
+			{ title: 'downwards double arrow', character: '⇓' },
+			{ title: 'leftwards dashed arrow', character: '⇠' },
+			{ title: 'rightwards dashed arrow', character: '⇢' },
+			{ title: 'upwards dashed arrow', character: '⇡' },
+			{ title: 'downwards dashed arrow', character: '⇣' },
+			{ title: 'leftwards arrow to bar', character: '⇤' },
+			{ title: 'rightwards arrow to bar', character: '⇥' },
+			{ title: 'upwards arrow to bar', character: '⤒' },
+			{ title: 'downwards arrow to bar', character: '⤓' },
+			{ title: 'up down arrow with base', character: '↨' },
+			{ title: 'back with leftwards arrow above', character: '🔙' },
+			{ title: 'end with leftwards arrow above', character: '🔚' },
+			{ title: 'on with exclamation mark with left right arrow above', character: '🔛' },
+			{ title: 'soon with rightwards arrow above', character: '🔜' },
+			{ title: 'top with upwards arrow above', character: '🔝' }
+		] );
+	}
+}

+ 190 - 0
packages/ckeditor5-special-characters/src/specialcharacterscurrency.js

@@ -0,0 +1,190 @@
+/**
+ * @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/specialcharacters
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import SpecialCharacters from './specialcharacters';
+
+export default class SpecialCharactersCurrency extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [
+			SpecialCharacters
+		];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this.editor.plugins.get( 'SpecialCharacters' ).addItems( 'Currency', [
+			{
+				character: '$',
+				title: 'Dollar sign'
+			},
+			{
+				character: '€',
+				title: 'Euro sign'
+			},
+			{
+				character: '¥',
+				title: 'Yen sign'
+			},
+			{
+				character: '£',
+				title: 'Pound sign'
+			},
+			{
+				character: '¢',
+				title: 'Cent sign'
+			},
+			{
+				character: '₠',
+				title: 'Euro-currency sign'
+			},
+			{
+				character: '₡',
+				title: 'Colon sign'
+			},
+			{
+				character: '₢',
+				title: 'Cruzeiro sign'
+			},
+			{
+				character: '₣',
+				title: 'French franc sign'
+			},
+			{
+				character: '₤',
+				title: 'Lira sign'
+			},
+			{
+				character: '¤',
+				title: 'Currency sign'
+			},
+			{
+				character: '₿',
+				title: 'Bitcoin sign'
+			},
+			{
+				character: '₥',
+				title: 'Mill sign'
+			},
+			{
+				character: '₦',
+				title: 'Naira sign'
+			},
+			{
+				character: '₧',
+				title: 'Peseta sign'
+			},
+			{
+				character: '₨',
+				title: 'Rupee sign'
+			},
+			{
+				character: '₩',
+				title: 'Won sign'
+			},
+			{
+				character: '₪',
+				title: 'New sheqel sign'
+			},
+			{
+				character: '₫',
+				title: 'Dong sign'
+			},
+			{
+				character: '₭',
+				title: 'Kip sign'
+			},
+			{
+				character: '₮',
+				title: 'Tugrik sign'
+			},
+			{
+				character: '₯',
+				title: 'Drachma sign'
+			},
+			{
+				character: '₰',
+				title: 'German penny sign'
+			},
+			{
+				character: '₱',
+				title: 'Peso sign'
+			},
+			{
+				character: '₲',
+				title: 'Guarani sign'
+			},
+			{
+				character: '₳',
+				title: 'Austral sign'
+			},
+			{
+				character: '₴',
+				title: 'Hryvnia sign'
+			},
+			{
+				character: '₵',
+				title: 'Cedi sign'
+			},
+			{
+				character: '₶',
+				title: 'Livre tournois sign'
+			},
+			{
+				character: '₷',
+				title: 'Spesmilo sign'
+			},
+			{
+				character: '₸',
+				title: 'Tenge sign'
+			},
+			{
+				character: '₹',
+				title: 'Indian rupee sign'
+			},
+			{
+				character: '₺',
+				title: 'Turkish lira sign'
+			},
+			{
+				character: '₻',
+				title: 'Nordic mark sign'
+			},
+			{
+				character: '₼',
+				title: 'Manat sign'
+			},
+			{
+				character: '₽',
+				title: 'Ruble sign'
+			}/* ,
+			{
+				character: '円',
+				title: ''
+			},
+			{
+				character: '元',
+				title: ''
+			},
+			{
+				character: '圓',
+				title: ''
+			},
+			{
+				character: '圆',
+				title: ''
+			} */
+		] );
+	}
+}

+ 45 - 0
packages/ckeditor5-special-characters/src/specialcharactersediting.js

@@ -0,0 +1,45 @@
+/**
+ * @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/specialcharactersediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import InsertSpecialCharacterCommand from './insertspecialcharactercommand';
+
+/**
+ * Special characters editing plugin.
+ *
+ * It registers the {@link module:special-characters/insertspecialcharactercommand~InsertSpecialCharacterCommand Special Character} command.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class SpecialCharactersEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'SpecialCharactersEditing';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ Typing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+
+		const command = new InsertSpecialCharacterCommand( editor );
+		editor.commands.add( 'insertSpecialCharacter', command );
+	}
+}

+ 43 - 0
packages/ckeditor5-special-characters/src/specialcharactersessentials.js

@@ -0,0 +1,43 @@
+/**
+ * @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/specialcharacters
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import SpecialCharacters from './specialcharacters';
+import SpecialCharactersCurrency from './specialcharacterscurrency';
+import SpecialCharactersMathematical from './specialcharactersmathematical';
+import SpecialCharactersArrows from './specialcharactersarrows';
+import SpecialCharactersLatin from './specialcharacterslatin';
+import SpecialCharactersText from './specialcharacterstext';
+
+/**
+ * A plugin combining basic set of characters for the special characters plugin.
+ *
+ *		ClassicEditor
+ *			.create( {
+ *				plugins: [ ..., SpecialCharacters, SpecialCharactersEssentials ],
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ */
+export default class SpecialCharactersEssentials extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [
+			SpecialCharacters,
+			SpecialCharactersCurrency,
+			SpecialCharactersText,
+			SpecialCharactersMathematical,
+			SpecialCharactersArrows,
+			SpecialCharactersLatin
+		];
+	}
+}

+ 542 - 0
packages/ckeditor5-special-characters/src/specialcharacterslatin.js

@@ -0,0 +1,542 @@
+/**
+ * @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/specialcharacters
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import SpecialCharacters from './specialcharacters';
+
+export default class SpecialCharactersLatin extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [
+			SpecialCharacters
+		];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this.editor.plugins.get( 'SpecialCharacters' ).addItems( 'Latin', [
+			{
+				character: 'Ā',
+				title: 'Latin capital letter a with macron'
+			},
+			{
+				character: 'ā',
+				title: 'Latin small letter a with macron'
+			},
+			{
+				character: 'Ă',
+				title: 'Latin capital letter a with breve'
+			},
+			{
+				character: 'ă',
+				title: 'Latin small letter a with breve'
+			},
+			{
+				character: 'Ą',
+				title: 'Latin capital letter a with ogonek'
+			},
+			{
+				character: 'ą',
+				title: 'Latin small letter a with ogonek'
+			},
+			{
+				character: 'Ć',
+				title: 'Latin capital letter c with acute'
+			},
+			{
+				character: 'ć',
+				title: 'Latin small letter c with acute'
+			},
+			{
+				character: 'Ĉ',
+				title: 'Latin capital letter c with circumflex'
+			},
+			{
+				character: 'ĉ',
+				title: 'Latin small letter c with circumflex'
+			},
+			{
+				character: 'Ċ',
+				title: 'Latin capital letter c with dot above'
+			},
+			{
+				character: 'ċ',
+				title: 'Latin small letter c with dot above'
+			},
+			{
+				character: 'Č',
+				title: 'Latin capital letter c with caron'
+			},
+			{
+				character: 'č',
+				title: 'Latin small letter c with caron'
+			},
+			{
+				character: 'Ď',
+				title: 'Latin capital letter d with caron'
+			},
+			{
+				character: 'ď',
+				title: 'Latin small letter d with caron'
+			},
+			{
+				character: 'Đ',
+				title: 'Latin capital letter d with stroke'
+			},
+			{
+				character: 'đ',
+				title: 'Latin small letter d with stroke'
+			},
+			{
+				character: 'Ē',
+				title: 'Latin capital letter e with macron'
+			},
+			{
+				character: 'ē',
+				title: 'Latin small letter e with macron'
+			},
+			{
+				character: 'Ĕ',
+				title: 'Latin capital letter e with breve'
+			},
+			{
+				character: 'ĕ',
+				title: 'Latin small letter e with breve'
+			},
+			{
+				character: 'Ė',
+				title: 'Latin capital letter e with dot above'
+			},
+			{
+				character: 'ė',
+				title: 'Latin small letter e with dot above'
+			},
+			{
+				character: 'Ę',
+				title: 'Latin capital letter e with ogonek'
+			},
+			{
+				character: 'ę',
+				title: 'Latin small letter e with ogonek'
+			},
+			{
+				character: 'Ě',
+				title: 'Latin capital letter e with caron'
+			},
+			{
+				character: 'ě',
+				title: 'Latin small letter e with caron'
+			},
+			{
+				character: 'Ĝ',
+				title: 'Latin capital letter g with circumflex'
+			},
+			{
+				character: 'ĝ',
+				title: 'Latin small letter g with circumflex'
+			},
+			{
+				character: 'Ğ',
+				title: 'Latin capital letter g with breve'
+			},
+			{
+				character: 'ğ',
+				title: 'Latin small letter g with breve'
+			},
+			{
+				character: 'Ġ',
+				title: 'Latin capital letter g with dot above'
+			},
+			{
+				character: 'ġ',
+				title: 'Latin small letter g with dot above'
+			},
+			{
+				character: 'Ģ',
+				title: 'Latin capital letter g with cedilla'
+			},
+			{
+				character: 'ģ',
+				title: 'Latin small letter g with cedilla'
+			},
+			{
+				character: 'Ĥ',
+				title: 'Latin capital letter h with circumflex'
+			},
+			{
+				character: 'ĥ',
+				title: 'Latin small letter h with circumflex'
+			},
+			{
+				character: 'Ħ',
+				title: 'Latin capital letter h with stroke'
+			},
+			{
+				character: 'ħ',
+				title: 'Latin small letter h with stroke'
+			},
+			{
+				character: 'Ĩ',
+				title: 'Latin capital letter i with tilde'
+			},
+			{
+				character: 'ĩ',
+				title: 'Latin small letter i with tilde'
+			},
+			{
+				character: 'Ī',
+				title: 'Latin capital letter i with macron'
+			},
+			{
+				character: 'ī',
+				title: 'Latin small letter i with macron'
+			},
+			{
+				character: 'Ĭ',
+				title: 'Latin capital letter i with breve'
+			},
+			{
+				character: 'ĭ',
+				title: 'Latin small letter i with breve'
+			},
+			{
+				character: 'Į',
+				title: 'Latin capital letter i with ogonek'
+			},
+			{
+				character: 'į',
+				title: 'Latin small letter i with ogonek'
+			},
+			{
+				character: 'İ',
+				title: 'Latin capital letter i with dot above'
+			},
+			{
+				character: 'ı',
+				title: 'Latin small letter dotless i'
+			},
+			{
+				character: 'IJ',
+				title: 'Latin capital ligature ij'
+			},
+			{
+				character: 'ij',
+				title: 'Latin small ligature ij'
+			},
+			{
+				character: 'Ĵ',
+				title: 'Latin capital letter j with circumflex'
+			},
+			{
+				character: 'ĵ',
+				title: 'Latin small letter j with circumflex'
+			},
+			{
+				character: 'Ķ',
+				title: 'Latin capital letter k with cedilla'
+			},
+			{
+				character: 'ķ',
+				title: 'Latin small letter k with cedilla'
+			},
+			{
+				character: 'ĸ',
+				title: 'Latin small letter kra'
+			},
+			{
+				character: 'Ĺ',
+				title: 'Latin capital letter l with acute'
+			},
+			{
+				character: 'ĺ',
+				title: 'Latin small letter l with acute'
+			},
+			{
+				character: 'Ļ',
+				title: 'Latin capital letter l with cedilla'
+			},
+			{
+				character: 'ļ',
+				title: 'Latin small letter l with cedilla'
+			},
+			{
+				character: 'Ľ',
+				title: 'Latin capital letter l with caron'
+			},
+			{
+				character: 'ľ',
+				title: 'Latin small letter l with caron'
+			},
+			{
+				character: 'Ŀ',
+				title: 'Latin capital letter l with middle dot'
+			},
+			{
+				character: 'ŀ',
+				title: 'Latin small letter l with middle dot'
+			},
+			{
+				character: 'Ł',
+				title: 'Latin capital letter l with stroke'
+			},
+			{
+				character: 'ł',
+				title: 'Latin small letter l with stroke'
+			},
+			{
+				character: 'Ń',
+				title: 'Latin capital letter n with acute'
+			},
+			{
+				character: 'ń',
+				title: 'Latin small letter n with acute'
+			},
+			{
+				character: 'Ņ',
+				title: 'Latin capital letter n with cedilla'
+			},
+			{
+				character: 'ņ',
+				title: 'Latin small letter n with cedilla'
+			},
+			{
+				character: 'Ň',
+				title: 'Latin capital letter n with caron'
+			},
+			{
+				character: 'ň',
+				title: 'Latin small letter n with caron'
+			},
+			{
+				character: 'ʼn',
+				title: 'Latin small letter n preceded by apostrophe'
+			},
+			{
+				character: 'Ŋ',
+				title: 'Latin capital letter eng'
+			},
+			{
+				character: 'ŋ',
+				title: 'Latin small letter eng'
+			},
+			{
+				character: 'Ō',
+				title: 'Latin capital letter o with macron'
+			},
+			{
+				character: 'ō',
+				title: 'Latin small letter o with macron'
+			},
+			{
+				character: 'Ŏ',
+				title: 'Latin capital letter o with breve'
+			},
+			{
+				character: 'ŏ',
+				title: 'Latin small letter o with breve'
+			},
+			{
+				character: 'Ő',
+				title: 'Latin capital letter o with double acute'
+			},
+			{
+				character: 'ő',
+				title: 'Latin small letter o with double acute'
+			},
+			{
+				character: 'Œ',
+				title: 'Latin capital ligature oe'
+			},
+			{
+				character: 'œ',
+				title: 'Latin small ligature oe'
+			},
+			{
+				character: 'Ŕ',
+				title: 'Latin capital letter r with acute'
+			},
+			{
+				character: 'ŕ',
+				title: 'Latin small letter r with acute'
+			},
+			{
+				character: 'Ŗ',
+				title: 'Latin capital letter r with cedilla'
+			},
+			{
+				character: 'ŗ',
+				title: 'Latin small letter r with cedilla'
+			},
+			{
+				character: 'Ř',
+				title: 'Latin capital letter r with caron'
+			},
+			{
+				character: 'ř',
+				title: 'Latin small letter r with caron'
+			},
+			{
+				character: 'Ś',
+				title: 'Latin capital letter s with acute'
+			},
+			{
+				character: 'ś',
+				title: 'Latin small letter s with acute'
+			},
+			{
+				character: 'Ŝ',
+				title: 'Latin capital letter s with circumflex'
+			},
+			{
+				character: 'ŝ',
+				title: 'Latin small letter s with circumflex'
+			},
+			{
+				character: 'Ş',
+				title: 'Latin capital letter s with cedilla'
+			},
+			{
+				character: 'ş',
+				title: 'Latin small letter s with cedilla'
+			},
+			{
+				character: 'Š',
+				title: 'Latin capital letter s with caron'
+			},
+			{
+				character: 'š',
+				title: 'Latin small letter s with caron'
+			},
+			{
+				character: 'Ţ',
+				title: 'Latin capital letter t with cedilla'
+			},
+			{
+				character: 'ţ',
+				title: 'Latin small letter t with cedilla'
+			},
+			{
+				character: 'Ť',
+				title: 'Latin capital letter t with caron'
+			},
+			{
+				character: 'ť',
+				title: 'Latin small letter t with caron'
+			},
+			{
+				character: 'Ŧ',
+				title: 'Latin capital letter t with stroke'
+			},
+			{
+				character: 'ŧ',
+				title: 'Latin small letter t with stroke'
+			},
+			{
+				character: 'Ũ',
+				title: 'Latin capital letter u with tilde'
+			},
+			{
+				character: 'ũ',
+				title: 'Latin small letter u with tilde'
+			},
+			{
+				character: 'Ū',
+				title: 'Latin capital letter u with macron'
+			},
+			{
+				character: 'ū',
+				title: 'Latin small letter u with macron'
+			},
+			{
+				character: 'Ŭ',
+				title: 'Latin capital letter u with breve'
+			},
+			{
+				character: 'ŭ',
+				title: 'Latin small letter u with breve'
+			},
+			{
+				character: 'Ů',
+				title: 'Latin capital letter u with ring above'
+			},
+			{
+				character: 'ů',
+				title: 'Latin small letter u with ring above'
+			},
+			{
+				character: 'Ű',
+				title: 'Latin capital letter u with double acute'
+			},
+			{
+				character: 'ű',
+				title: 'Latin small letter u with double acute'
+			},
+			{
+				character: 'Ų',
+				title: 'Latin capital letter u with ogonek'
+			},
+			{
+				character: 'ų',
+				title: 'Latin small letter u with ogonek'
+			},
+			{
+				character: 'Ŵ',
+				title: 'Latin capital letter w with circumflex'
+			},
+			{
+				character: 'ŵ',
+				title: 'Latin small letter w with circumflex'
+			},
+			{
+				character: 'Ŷ',
+				title: 'Latin capital letter y with circumflex'
+			},
+			{
+				character: 'ŷ',
+				title: 'Latin small letter y with circumflex'
+			},
+			{
+				character: 'Ÿ',
+				title: 'Latin capital letter y with diaeresis'
+			},
+			{
+				character: 'Ź',
+				title: 'Latin capital letter z with acute'
+			},
+			{
+				character: 'ź',
+				title: 'Latin small letter z with acute'
+			},
+			{
+				character: 'Ż',
+				title: 'Latin capital letter z with dot above'
+			},
+			{
+				character: 'ż',
+				title: 'Latin small letter z with dot above'
+			},
+			{
+				character: 'Ž',
+				title: 'Latin capital letter z with caron'
+			},
+			{
+				character: 'ž',
+				title: 'Latin small letter z with caron'
+			},
+			{
+				character: 'ſ',
+				title: 'Latin small letter long s'
+			}
+		] );
+	}
+}

+ 206 - 0
packages/ckeditor5-special-characters/src/specialcharactersmathematical.js

@@ -0,0 +1,206 @@
+/**
+ * @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/specialcharacters
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import SpecialCharacters from './specialcharacters';
+
+export default class SpecialCharactersMathematical extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [
+			SpecialCharacters
+		];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this.editor.plugins.get( 'SpecialCharacters' ).addItems( 'Mathematical', [
+			{
+				character: '<',
+				title: 'Less-than sign'
+			},
+			{
+				character: '>',
+				title: 'Greater-than sign'
+			},
+			{
+				character: '≤',
+				title: 'Less-than or equal to'
+			},
+			{
+				character: '≥',
+				title: 'Greater-than or equal to'
+			},
+			{
+				character: '–',
+				title: 'En dash'
+			},
+			{
+				character: '—',
+				title: 'Em dash'
+			},
+			{
+				character: '¯',
+				title: 'Macron'
+			},
+			{
+				character: '‾',
+				title: 'Overline'
+			},
+			{
+				character: '°',
+				title: 'Degree sign'
+			},
+			{
+				character: '−',
+				title: 'Minus sign'
+			},
+			{
+				character: '±',
+				title: 'Plus-minus sign'
+			},
+			{
+				character: '÷',
+				title: 'Division sign'
+			},
+			{
+				character: '⁄',
+				title: 'Fraction slash'
+			},
+			{
+				character: '×',
+				title: 'Multiplication sign'
+			},
+			{
+				character: 'ƒ',
+				title: 'Latin small letter f with hook'
+			},
+			{
+				character: '∫',
+				title: 'Integral'
+			},
+			{
+				character: '∑',
+				title: 'N-ary summation'
+			},
+			{
+				character: '∞',
+				title: 'Infinity'
+			},
+			{
+				character: '√',
+				title: 'Square root'
+			},
+			{
+				character: '∼',
+				title: 'Tilde operator'
+			},
+			{
+				character: '≅',
+				title: 'Approximately equal to'
+			},
+			{
+				character: '≈',
+				title: 'Almost equal to'
+			},
+			{
+				character: '≠',
+				title: 'Not equal to'
+			},
+			{
+				character: '≡',
+				title: 'Identical to'
+			},
+			{
+				character: '∈',
+				title: 'Element of'
+			},
+			{
+				character: '∉',
+				title: 'Not an element of'
+			},
+			{
+				character: '∋',
+				title: 'Contains as member'
+			},
+			{
+				character: '∏',
+				title: 'N-ary product'
+			},
+			{
+				character: '∧',
+				title: 'Logical and'
+			},
+			{
+				character: '∨',
+				title: 'Logical or'
+			},
+			{
+				character: '¬',
+				title: 'Not sign'
+			},
+			{
+				character: '∩',
+				title: 'Intersection'
+			},
+			{
+				character: '∪',
+				title: 'Union'
+			},
+			{
+				character: '∂',
+				title: 'Partial differential'
+			},
+			{
+				character: '∀',
+				title: 'For all'
+			},
+			{
+				character: '∃',
+				title: 'There exists'
+			},
+			{
+				character: '∅',
+				title: 'Empty set'
+			},
+			{
+				character: '∇',
+				title: 'Nabla'
+			},
+			{
+				character: '∗',
+				title: 'Asterisk operator'
+			},
+			{
+				character: '∝',
+				title: 'Proportional to'
+			},
+			{
+				character: '∠',
+				title: 'Angle'
+			},
+			{
+				character: '¼',
+				title: 'Vulgar fraction one quarter'
+			},
+			{
+				character: '½',
+				title: 'Vulgar fraction one half'
+			},
+			{
+				character: '¾',
+				title: 'Vulgar fraction three quarters'
+			}
+		] );
+	}
+}

+ 114 - 0
packages/ckeditor5-special-characters/src/specialcharacterstext.js

@@ -0,0 +1,114 @@
+/**
+ * @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/specialcharacters
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import SpecialCharacters from './specialcharacters';
+
+export default class SpecialCharactersText extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [
+			SpecialCharacters
+		];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this.editor.plugins.get( 'SpecialCharacters' ).addItems( 'Text', [
+			{
+				character: '‹',
+				title: 'Single left-pointing angle quotation mark'
+			},
+			{
+				character: '›',
+				title: 'Single right-pointing angle quotation mark'
+			},
+			{
+				character: '«',
+				title: 'Left-pointing double angle quotation mark'
+			},
+			{
+				character: '»',
+				title: 'Right-pointing double angle quotation mark'
+			},
+			{
+				character: '‘',
+				title: 'Left single quotation mark'
+			},
+			{
+				character: '’',
+				title: 'Right single quotation mark'
+			},
+			{
+				character: '“',
+				title: 'Left double quotation mark'
+			},
+			{
+				character: '”',
+				title: 'Right double quotation mark'
+			},
+			{
+				character: '‚',
+				title: 'Single low-9 quotation mark'
+			},
+			{
+				character: '„',
+				title: 'Double low-9 quotation mark'
+			},
+			{
+				character: '¡',
+				title: 'Inverted exclamation mark'
+			},
+			{
+				character: '¿',
+				title: 'Inverted question mark'
+			},
+			{
+				character: '‥',
+				title: 'Two dot leader'
+			},
+			{
+				character: '…',
+				title: 'Horizontal ellipsis'
+			},
+			{
+				character: '‡',
+				title: 'Double dagger'
+			},
+			{
+				character: '‰',
+				title: 'Per mille sign'
+			},
+			{
+				character: '‱',
+				title: 'Per ten thousand sign'
+			},
+			{
+				character: '‼',
+				title: 'Double exclamation mark'
+			},
+			{
+				character: '⁈',
+				title: 'Question exclamation mark'
+			},
+			{
+				character: '⁉',
+				title: 'Exclamation question mark'
+			},
+			{
+				character: '⁇',
+				title: 'Double question mark'
+			}
+		] );
+	}
+}

+ 100 - 0
packages/ckeditor5-special-characters/src/specialcharactersui.js

@@ -0,0 +1,100 @@
+/**
+ * @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/specialcharactersui
+ */
+
+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 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';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+		const specialCharsPlugin = editor.plugins.get( 'SpecialCharacters' );
+
+		// Add the `specialCharacters` dropdown button to feature components.
+		editor.ui.componentFactory.add( 'specialCharacters', locale => {
+			const dropdownView = createDropdown( locale );
+			const navigationView = new SpecialCharactersNavigationView( locale, specialCharsPlugin.getGroups() );
+			const gridView = new CharacterGridView( this.locale, {
+				columns: 10
+			} );
+			const command = editor.commands.get( 'insertSpecialCharacter' );
+
+			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: t( 'Special characters' ),
+				icon: specialCharactersIcon,
+				tooltip: true
+			} );
+
+			dropdownView.bind( 'isEnabled' ).to( command );
+
+			// Insert a special character when a tile was clicked.
+			dropdownView.on( 'execute', ( evt, data ) => {
+				editor.execute( 'insertSpecialCharacter', { item: data.name } );
+				editor.editing.view.focus();
+			} );
+
+			dropdownView.panelView.children.add( navigationView );
+			dropdownView.panelView.children.add( gridView );
+
+			return dropdownView;
+		} );
+	}
+
+	/**
+	 * 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();
+
+		const characterTitles = specialCharsPlugin.getCharactersForGroup( currentGroupName );
+
+		for ( const title of characterTitles ) {
+			const character = specialCharsPlugin.getCharacter( title );
+
+			gridView.tiles.add( gridView.createTile( character, title ) );
+		}
+	}
+}
+

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

@@ -0,0 +1,88 @@
+/**
+ * @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'
+		} );
+
+		// Labels are vital for the users to understand what character they're looking at.
+		// For now we're using native title attribute for that, see #5817.
+		tile.extendTemplate( {
+			attributes: {
+				title: name
+			}
+		} );
+
+		tile.on( 'execute', () => {
+			this.fire( 'execute', { name } );
+		} );
+
+		return tile;
+	}
+}

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

@@ -0,0 +1,142 @@
+/**
+ * @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.groupDropdownView.panelPosition = locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw';
+
+		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;
+	}
+}

+ 109 - 0
packages/ckeditor5-special-characters/tests/insertspecialcharactercommand.js

@@ -0,0 +1,109 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import SpecialCharacters from '../src/specialcharacters';
+
+describe( 'InsertSpecialCharacterCommand', () => {
+	let editor, model, editorElement, command;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ Paragraph, SpecialCharacters ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				command = editor.commands.get( 'insertSpecialCharacter' );
+
+				editor.plugins.get( 'SpecialCharacters' ).addItems( 'Arrows', [
+					{ title: 'arrow left', character: '←' },
+					{ title: 'arrow right', character: '→' }
+				] );
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy()
+			.then( () => {
+				editorElement.remove();
+			} );
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'should be bound to InputCommand#isEnables', () => {
+			const inputCommand = editor.commands.get( 'input' );
+
+			inputCommand.isEnabled = true;
+			expect( command.isEnabled ).to.equal( true );
+
+			inputCommand.isEnabled = false;
+			expect( command.isEnabled ).to.equal( false );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should create a single batch', () => {
+			setModelData( model, '<paragraph>foo[]</paragraph>' );
+
+			const spy = sinon.spy();
+
+			model.document.on( 'change', spy );
+
+			command.execute( { item: 'arrow left' } );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'executes InputCommand#execute()', () => {
+			const inputCommand = editor.commands.get( 'input' );
+
+			setModelData( model, '<paragraph>foo[]</paragraph>' );
+
+			const spy = sinon.stub( inputCommand, 'execute' );
+
+			command.execute( { item: 'arrow left' } );
+
+			sinon.assert.calledWithExactly( spy, { text: '←' } );
+
+			spy.restore();
+		} );
+
+		it( 'does nothing if specified object is invalid', () => {
+			setModelData( model, '<paragraph>foo[]</paragraph>' );
+
+			const spy = sinon.spy();
+
+			model.document.on( 'change', spy );
+
+			command.execute( { foo: 'arrow left' } );
+
+			sinon.assert.notCalled( spy );
+		} );
+
+		it( 'does nothing if specified item name does not exist', () => {
+			setModelData( model, '<paragraph>foo[]</paragraph>' );
+
+			const spy = sinon.spy();
+
+			model.document.on( 'change', spy );
+
+			command.execute( { item: 'arrow up' } );
+
+			sinon.assert.notCalled( spy );
+		} );
+	} );
+} );

文件差异内容过多而无法显示
+ 4 - 0
packages/ckeditor5-special-characters/tests/manual/specialcharacters.html


+ 58 - 0
packages/ckeditor5-special-characters/tests/manual/specialcharacters.js

@@ -0,0 +1,58 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
+import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+import SpecialCharacters from '../../src/specialcharacters';
+import SpecialCharactersEssentials from '../../src/specialcharactersessentials';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		cloudServices: CS_CONFIG,
+		plugins: [ ArticlePluginSet, ImageUpload, EasyImage, SpecialCharacters, SpecialCharactersEssentials ],
+		toolbar: [
+			'heading',
+			'|',
+			'bold', 'italic', 'numberedList', 'bulletedList',
+			'|',
+			'link', 'blockquote', 'imageUpload', 'insertTable', 'mediaEmbed',
+			'|',
+			'undo', 'redo',
+			'|',
+			'specialCharacters'
+		],
+		image: {
+			styles: [
+				'full',
+				'alignLeft',
+				'alignRight'
+			],
+			toolbar: [
+				'imageStyle:alignLeft',
+				'imageStyle:full',
+				'imageStyle:alignRight',
+				'|',
+				'imageTextAlternative'
+			]
+		},
+		table: {
+			contentToolbar: [
+				'tableColumn',
+				'tableRow',
+				'mergeTableCells'
+			]
+		},
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 4 - 0
packages/ckeditor5-special-characters/tests/manual/specialcharacters.md

@@ -0,0 +1,4 @@
+## Testing
+
+1. Use the special characters icon in order to add a special character to the editor.
+1. Use the select in order to change category of displayed special characters.

+ 114 - 0
packages/ckeditor5-special-characters/tests/specialcharacters.js

@@ -0,0 +1,114 @@
+/**
+ * @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 SpecialCharacters from '../src/specialcharacters';
+import SpecialCharactersUI from '../src/specialcharactersui';
+import SpecialCharactersEditing from '../src/specialcharactersediting';
+
+describe( 'SpecialCharacters', () => {
+	let plugin;
+
+	beforeEach( () => {
+		plugin = new SpecialCharacters( {} );
+	} );
+
+	it( 'should require proper plugins', () => {
+		expect( SpecialCharacters.requires ).to.deep.equal( [ SpecialCharactersEditing, SpecialCharactersUI ] );
+	} );
+
+	it( 'should be named', () => {
+		expect( SpecialCharacters.pluginName ).to.equal( 'SpecialCharacters' );
+	} );
+
+	describe( 'addItems()', () => {
+		it( 'adds special characters to the available symbols', () => {
+			plugin.addItems( 'Arrows', [
+				{ title: 'arrow left', character: '←' },
+				{ title: 'arrow right', character: '→' }
+			] );
+
+			expect( plugin._groups.size ).to.equal( 1 );
+			expect( plugin._groups.has( 'Arrows' ) ).to.equal( true );
+
+			expect( plugin._characters.size ).to.equal( 2 );
+			expect( plugin._characters.has( 'arrow left' ) ).to.equal( true );
+			expect( plugin._characters.has( 'arrow right' ) ).to.equal( true );
+		} );
+	} );
+
+	describe( 'getGroups()', () => {
+		it( 'returns iterator of defined groups', () => {
+			plugin.addItems( 'Arrows', [
+				{ title: 'arrow left', character: '←' }
+			] );
+
+			plugin.addItems( 'Mathematical', [
+				{ title: 'precedes', character: '≺' },
+				{ title: 'succeeds', character: '≻' }
+			] );
+
+			const groups = [ ...plugin.getGroups() ];
+			expect( groups ).to.deep.equal( [ 'Arrows', 'Mathematical' ] );
+		} );
+	} );
+
+	describe( 'addItems()', () => {
+		it( 'works with subsequent calls to the same group', () => {
+			plugin.addItems( 'Mathematical', [ {
+				title: 'dot',
+				character: '.'
+			} ] );
+
+			plugin.addItems( 'Mathematical', [ {
+				title: ',',
+				character: 'comma'
+			} ] );
+
+			const groups = [ ...plugin.getGroups() ];
+			expect( groups ).to.deep.equal( [ 'Mathematical' ] );
+		} );
+	} );
+
+	describe( 'getCharactersForGroup()', () => {
+		it( 'returns a collection of defined special characters names', () => {
+			plugin.addItems( 'Mathematical', [
+				{ title: 'precedes', character: '≺' },
+				{ title: 'succeeds', character: '≻' }
+			] );
+
+			const characters = plugin.getCharactersForGroup( 'Mathematical' );
+
+			expect( characters.size ).to.equal( 2 );
+			expect( characters.has( 'precedes' ) ).to.equal( true );
+			expect( characters.has( 'succeeds' ) ).to.equal( true );
+		} );
+
+		it( 'returns undefined for non-existing group', () => {
+			plugin.addItems( 'Mathematical', [
+				{ title: 'precedes', character: '≺' },
+				{ title: 'succeeds', character: '≻' }
+			] );
+
+			const characters = plugin.getCharactersForGroup( 'Foo' );
+
+			expect( characters ).to.be.undefined;
+		} );
+	} );
+
+	describe( 'getCharacter()', () => {
+		it( 'returns a collection of defined special characters names', () => {
+			plugin.addItems( 'Mathematical', [
+				{ title: 'precedes', character: '≺' },
+				{ title: 'succeeds', character: '≻' }
+			] );
+
+			expect( plugin.getCharacter( 'succeeds' ) ).to.equal( '≻' );
+		} );
+
+		it( 'returns undefined for non-existing character', () => {
+			expect( plugin.getCharacter( 'succeeds' ) ).to.be.undefined;
+		} );
+	} );
+} );

+ 56 - 0
packages/ckeditor5-special-characters/tests/specialcharactersarrows.js

@@ -0,0 +1,56 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import SpecialCharacters from '../src/specialcharacters';
+import SpecialCharactersArrows from '../src/specialcharactersarrows';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'SpecialCharactersArrows', () => {
+	testUtils.createSinonSandbox();
+
+	let addItemsSpy, addItemsFirstCallArgs;
+
+	beforeEach( () => {
+		const editorElement = document.createElement( 'div' );
+
+		addItemsSpy = sinon.spy( SpecialCharacters.prototype, 'addItems' );
+
+		document.body.appendChild( editorElement );
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ SpecialCharacters, SpecialCharactersArrows ]
+			} )
+			.then( () => {
+				addItemsFirstCallArgs = addItemsSpy.args[ 0 ];
+			} );
+	} );
+
+	afterEach( () => {
+		addItemsSpy.restore();
+	} );
+
+	it( 'adds new items', () => {
+		expect( addItemsSpy.callCount ).to.equal( 1 );
+	} );
+
+	it( 'properly names the category', () => {
+		expect( addItemsFirstCallArgs[ 0 ] ).to.be.equal( 'Arrows' );
+	} );
+
+	it( 'adds proper characters', () => {
+		expect( addItemsFirstCallArgs[ 1 ] ).to.deep.include( {
+			title: 'rightwards double arrow',
+			character: '⇒'
+		} );
+
+		expect( addItemsFirstCallArgs[ 1 ] ).to.deep.include( {
+			title: 'rightwards arrow to bar',
+			character: '⇥'
+		} );
+	} );
+} );

+ 56 - 0
packages/ckeditor5-special-characters/tests/specialcharacterscurrency.js

@@ -0,0 +1,56 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import SpecialCharacters from '../src/specialcharacters';
+import SpecialCharactersCurrency from '../src/specialcharacterscurrency';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'SpecialCharactersCurrency', () => {
+	testUtils.createSinonSandbox();
+
+	let addItemsSpy, addItemsFirstCallArgs;
+
+	beforeEach( () => {
+		const editorElement = document.createElement( 'div' );
+
+		addItemsSpy = sinon.spy( SpecialCharacters.prototype, 'addItems' );
+
+		document.body.appendChild( editorElement );
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ SpecialCharacters, SpecialCharactersCurrency ]
+			} )
+			.then( () => {
+				addItemsFirstCallArgs = addItemsSpy.args[ 0 ];
+			} );
+	} );
+
+	afterEach( () => {
+		addItemsSpy.restore();
+	} );
+
+	it( 'adds new items', () => {
+		expect( addItemsSpy.callCount ).to.equal( 1 );
+	} );
+
+	it( 'properly names the category', () => {
+		expect( addItemsFirstCallArgs[ 0 ] ).to.be.equal( 'Currency' );
+	} );
+
+	it( 'adds proper characters', () => {
+		expect( addItemsFirstCallArgs[ 1 ] ).to.deep.include( {
+			character: '¢',
+			title: 'Cent sign'
+		} );
+
+		expect( addItemsFirstCallArgs[ 1 ] ).to.deep.include( {
+			character: '₿',
+			title: 'Bitcoin sign'
+		} );
+	} );
+} );

+ 35 - 0
packages/ckeditor5-special-characters/tests/specialcharactersediting.js

@@ -0,0 +1,35 @@
+/**
+ * @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 SpecialCharactersEditing from '../src/specialcharactersediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import InsertSpecialCharacterCommand from '../src/insertspecialcharactercommand';
+
+describe( 'SpecialCharactersEditing', () => {
+	let editor;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ SpecialCharactersEditing, Paragraph ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	it( 'should have proper pluginName', () => {
+		expect( SpecialCharactersEditing.pluginName ).to.equal( 'SpecialCharactersEditing' );
+	} );
+
+	it( 'adds a command', () => {
+		expect( editor.commands.get( 'insertSpecialCharacter' ) ).to.be.instanceOf( InsertSpecialCharacterCommand );
+	} );
+} );

+ 26 - 0
packages/ckeditor5-special-characters/tests/specialcharactersessentials.js

@@ -0,0 +1,26 @@
+/**
+ * @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 SpecialCharactersEssentials from '../src/specialcharactersessentials';
+
+import SpecialCharacters from '../src/specialcharacters';
+import SpecialCharactersCurrency from '../src/specialcharacterscurrency';
+import SpecialCharactersText from '../src/specialcharacterstext';
+import SpecialCharactersMathematical from '../src/specialcharactersmathematical';
+import SpecialCharactersArrows from '../src/specialcharactersarrows';
+import SpecialCharactersLatin from '../src/specialcharacterslatin';
+
+describe( 'SpecialCharactersEssentials', () => {
+	it( 'includes other required plugins', () => {
+		expect( SpecialCharactersEssentials.requires ).to.deep.equal( [
+			SpecialCharacters,
+			SpecialCharactersCurrency,
+			SpecialCharactersText,
+			SpecialCharactersMathematical,
+			SpecialCharactersArrows,
+			SpecialCharactersLatin
+		] );
+	} );
+} );

+ 56 - 0
packages/ckeditor5-special-characters/tests/specialcharacterslatin.js

@@ -0,0 +1,56 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import SpecialCharacters from '../src/specialcharacters';
+import SpecialCharactersLatin from '../src/specialcharacterslatin';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'SpecialCharactersLatin', () => {
+	testUtils.createSinonSandbox();
+
+	let addItemsSpy, addItemsFirstCallArgs;
+
+	beforeEach( () => {
+		const editorElement = document.createElement( 'div' );
+
+		addItemsSpy = sinon.spy( SpecialCharacters.prototype, 'addItems' );
+
+		document.body.appendChild( editorElement );
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ SpecialCharacters, SpecialCharactersLatin ]
+			} )
+			.then( () => {
+				addItemsFirstCallArgs = addItemsSpy.args[ 0 ];
+			} );
+	} );
+
+	afterEach( () => {
+		addItemsSpy.restore();
+	} );
+
+	it( 'adds new items', () => {
+		expect( addItemsSpy.callCount ).to.equal( 1 );
+	} );
+
+	it( 'properly names the category', () => {
+		expect( addItemsFirstCallArgs[ 0 ] ).to.be.equal( 'Latin' );
+	} );
+
+	it( 'adds proper characters', () => {
+		expect( addItemsFirstCallArgs[ 1 ] ).to.deep.include( {
+			character: 'Ō',
+			title: 'Latin capital letter o with macron'
+		} );
+
+		expect( addItemsFirstCallArgs[ 1 ] ).to.deep.include( {
+			character: 'Ō',
+			title: 'Latin capital letter o with macron'
+		} );
+	} );
+} );

+ 56 - 0
packages/ckeditor5-special-characters/tests/specialcharacterstext.js

@@ -0,0 +1,56 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import SpecialCharacters from '../src/specialcharacters';
+import SpecialCharactersText from '../src/specialcharacterstext';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'SpecialCharactersText', () => {
+	testUtils.createSinonSandbox();
+
+	let addItemsSpy, addItemsFirstCallArgs;
+
+	beforeEach( () => {
+		const editorElement = document.createElement( 'div' );
+
+		addItemsSpy = sinon.spy( SpecialCharacters.prototype, 'addItems' );
+
+		document.body.appendChild( editorElement );
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ SpecialCharacters, SpecialCharactersText ]
+			} )
+			.then( () => {
+				addItemsFirstCallArgs = addItemsSpy.args[ 0 ];
+			} );
+	} );
+
+	afterEach( () => {
+		addItemsSpy.restore();
+	} );
+
+	it( 'adds new items', () => {
+		expect( addItemsSpy.callCount ).to.equal( 1 );
+	} );
+
+	it( 'properly names the category', () => {
+		expect( addItemsFirstCallArgs[ 0 ] ).to.be.equal( 'Text' );
+	} );
+
+	it( 'adds proper characters', () => {
+		expect( addItemsFirstCallArgs[ 1 ] ).to.deep.include( {
+			character: '…',
+			title: 'Horizontal ellipsis'
+		} );
+
+		expect( addItemsFirstCallArgs[ 1 ] ).to.deep.include( {
+			character: '“',
+			title: 'Left double quotation mark'
+		} );
+	} );
+} );

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

@@ -0,0 +1,129 @@
+/**
+ * @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( 'insertSpecialCharacter' );
+			} );
+	} );
+
+	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;
+				command.isEnabled = true;
+			} );
+		} );
+
+		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, 'insertSpecialCharacter', {
+				item: 'Less-than or equal to'
+			} );
+		} );
+
+		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' } );
+		} );
+	} );
+} );

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

@@ -0,0 +1,133 @@
+/**
+ * @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( 'has a right #panelPosition (LTR)', () => {
+			expect( groupDropdownView.panelPosition ).to.equal( 'sw' );
+		} );
+
+		it( 'has a right #panelPosition (RTL)', () => {
+			const locale = {
+				uiLanguageDirection: 'rtl',
+				t: val => val
+			};
+
+			view = new SpecialCharactersNavigationView( locale, [ 'groupA', 'groupB' ] );
+			view.render();
+
+			expect( view.groupDropdownView.panelPosition ).to.equal( 'se' );
+
+			view.destroy();
+		} );
+
+		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;
+			} );
+		} );
+	} );
+} );

+ 9 - 0
packages/ckeditor5-special-characters/theme/charactergrid.css

@@ -0,0 +1,9 @@
+/*
+ * 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-character-grid {
+	display: grid;
+	grid-template-columns: repeat(10, 1fr);
+}

+ 1 - 0
packages/ckeditor5-special-characters/theme/icons/specialcharacters.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a8.5 8.5 0 015.476 15.001L17.5 17a1 1 0 012 0v1a1 1 0 01-.883.993L18.5 19h-6v-2.498A6.502 6.502 0 0010 4a6.5 6.5 0 00-2.5 12.502V19h-6a1 1 0 01-1-1v-1a1 1 0 012 0l2.024.001A8.5 8.5 0 0110 2z"/></svg>

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