diff --git a/src/insertspecialcharactercommand.js b/src/insertspecialcharactercommand.js new file mode 100644 index 0000000..b32bfbb --- /dev/null +++ b/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 } ); + } +} diff --git a/src/specialcharacters.js b/src/specialcharacters.js new file mode 100644 index 0000000..a52a587 --- /dev/null +++ b/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.} #_characters + */ + this._characters = new Map(); + + /** + * Registered groups. Each group contains a collection with symbol names. + * + * @private + * @member {Map.>} #_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.} 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.} + */ + getGroups() { + return this._groups.keys(); + } + + /** + * Returns a collection of symbol names (titles). + * + * @param {String} groupName + * @returns {Set.|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. + */ diff --git a/src/specialcharactersarrows.js b/src/specialcharactersarrows.js new file mode 100644 index 0000000..7494a26 --- /dev/null +++ b/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: '🔝' } + ] ); + } +} diff --git a/src/specialcharacterscurrency.js b/src/specialcharacterscurrency.js new file mode 100644 index 0000000..5c1d05d --- /dev/null +++ b/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: '' + } */ + ] ); + } +} diff --git a/src/specialcharactersediting.js b/src/specialcharactersediting.js new file mode 100644 index 0000000..942a3bb --- /dev/null +++ b/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 ); + } +} diff --git a/src/specialcharactersessentials.js b/src/specialcharactersessentials.js new file mode 100644 index 0000000..30262d7 --- /dev/null +++ b/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 + ]; + } +} diff --git a/src/specialcharacterslatin.js b/src/specialcharacterslatin.js new file mode 100644 index 0000000..2cccff0 --- /dev/null +++ b/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' + } + ] ); + } +} diff --git a/src/specialcharactersmathematical.js b/src/specialcharactersmathematical.js new file mode 100644 index 0000000..19aaa39 --- /dev/null +++ b/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' + } + ] ); + } +} diff --git a/src/specialcharacterstext.js b/src/specialcharacterstext.js new file mode 100644 index 0000000..aa04a26 --- /dev/null +++ b/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' + } + ] ); + } +} diff --git a/src/specialcharactersui.js b/src/specialcharactersui.js new file mode 100644 index 0000000..3a7f5f7 --- /dev/null +++ b/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 ) ); + } + } +} + diff --git a/src/ui/charactergridview.js b/src/ui/charactergridview.js new file mode 100644 index 0000000..2988eee --- /dev/null +++ b/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; + } +} diff --git a/src/ui/specialcharactersnavigationview.js b/src/ui/specialcharactersnavigationview.js new file mode 100644 index 0000000..c830b28 --- /dev/null +++ b/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.} 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.} 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.} groupNames Names of the character groups. + * @returns {Iterable.} + */ + _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; + } +} diff --git a/tests/insertspecialcharactercommand.js b/tests/insertspecialcharactercommand.js new file mode 100644 index 0000000..0be356e --- /dev/null +++ b/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, 'foo[]' ); + + 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, 'foo[]' ); + + 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, 'foo[]' ); + + 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, 'foo[]' ); + + const spy = sinon.spy(); + + model.document.on( 'change', spy ); + + command.execute( { item: 'arrow up' } ); + + sinon.assert.notCalled( spy ); + } ); + } ); +} ); diff --git a/tests/manual/specialcharacters.html b/tests/manual/specialcharacters.html new file mode 100644 index 0000000..4805f37 --- /dev/null +++ b/tests/manual/specialcharacters.html @@ -0,0 +1,4 @@ +

Editor

+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris viverra ipsum a sapien accumsan, in fringilla ligula congue. Suspendisse eget urna ac nulla dignissim sollicitudin vel non sem. Curabitur consequat nisi vel orci mollis tincidunt. Nam eget sapien non ligula aliquet commodo vel sed lectus. Sed arcu orci, vehicula vitae augue lobortis, posuere tristique nisl. Sed eleifend venenatis magna in elementum. Cras sit amet arcu mi. Suspendisse vel purus a ex maximus pharetra quis in massa. Mauris pellentesque leo sed mi faucibus molestie. Cras felis justo, volutpat sed erat at, lacinia fermentum nunc. Pellentesque est leo, dignissim at odio sit amet, vulputate placerat turpis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla eget pharetra enim. Donec fermentum ligula est, quis ultrices arcu tristique eu. Vivamus a dui sem.

+
diff --git a/tests/manual/specialcharacters.js b/tests/manual/specialcharacters.js new file mode 100644 index 0000000..44d1113 --- /dev/null +++ b/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 ); + } ); diff --git a/tests/manual/specialcharacters.md b/tests/manual/specialcharacters.md new file mode 100644 index 0000000..f4a3884 --- /dev/null +++ b/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. diff --git a/tests/specialcharacters.js b/tests/specialcharacters.js new file mode 100644 index 0000000..570686d --- /dev/null +++ b/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; + } ); + } ); +} ); diff --git a/tests/specialcharactersarrows.js b/tests/specialcharactersarrows.js new file mode 100644 index 0000000..28b0f5f --- /dev/null +++ b/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: '⇥' + } ); + } ); +} ); diff --git a/tests/specialcharacterscurrency.js b/tests/specialcharacterscurrency.js new file mode 100644 index 0000000..eba96c3 --- /dev/null +++ b/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' + } ); + } ); +} ); diff --git a/tests/specialcharactersediting.js b/tests/specialcharactersediting.js new file mode 100644 index 0000000..986da6b --- /dev/null +++ b/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 ); + } ); +} ); diff --git a/tests/specialcharactersessentials.js b/tests/specialcharactersessentials.js new file mode 100644 index 0000000..8185efc --- /dev/null +++ b/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 + ] ); + } ); +} ); diff --git a/tests/specialcharacterslatin.js b/tests/specialcharacterslatin.js new file mode 100644 index 0000000..404dc68 --- /dev/null +++ b/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' + } ); + } ); +} ); diff --git a/tests/specialcharacterstext.js b/tests/specialcharacterstext.js new file mode 100644 index 0000000..a8e2be4 --- /dev/null +++ b/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' + } ); + } ); +} ); diff --git a/tests/specialcharactersui.js b/tests/specialcharactersui.js new file mode 100644 index 0000000..50bfd06 --- /dev/null +++ b/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( '⇐' ); + } ); + } ); + } ); +} ); diff --git a/tests/ui/charactergridview.js b/tests/ui/charactergridview.js new file mode 100644 index 0000000..768fa4d --- /dev/null +++ b/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' } ); + } ); + } ); +} ); diff --git a/tests/ui/specialcharactersnavigationview.js b/tests/ui/specialcharactersnavigationview.js new file mode 100644 index 0000000..76ad90a --- /dev/null +++ b/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; + } ); + } ); + } ); +} ); diff --git a/theme/charactergrid.css b/theme/charactergrid.css new file mode 100644 index 0000000..000ade0 --- /dev/null +++ b/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); +} diff --git a/theme/icons/specialcharacters.svg b/theme/icons/specialcharacters.svg new file mode 100644 index 0000000..502552d --- /dev/null +++ b/theme/icons/specialcharacters.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/specialcharacters.css b/theme/specialcharacters.css new file mode 100644 index 0000000..aacc5d5 --- /dev/null +++ b/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; +}