From f1b420ed489ece1b0f1bffd545e44157601d5367 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Fri, 20 Dec 2019 09:16:37 +0100 Subject: [PATCH 1/4] Added a status bar to grid view. --- src/specialcharacters.js | 18 ++++++-- src/ui/charactergridview.js | 7 ++++ src/ui/characterinfoview.js | 78 +++++++++++++++++++++++++++++++++++ tests/specialcharacters.js | 61 +++++++++++++++++++++------ tests/ui/charactergridview.js | 11 +++++ tests/ui/characterinfoview.js | 74 +++++++++++++++++++++++++++++++++ theme/characterinfo.css | 9 ++++ 7 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 src/ui/characterinfoview.js create mode 100644 tests/ui/characterinfoview.js create mode 100644 theme/characterinfo.css diff --git a/src/specialcharacters.js b/src/specialcharacters.js index a4b1584..c92ebe3 100644 --- a/src/specialcharacters.js +++ b/src/specialcharacters.js @@ -13,6 +13,7 @@ import { createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import SpecialCharactersNavigationView from './ui/specialcharactersnavigationview'; import CharacterGridView from './ui/charactergridview'; +import CharacterInfoView from './ui/characterinfoview'; import specialCharactersIcon from '../theme/icons/specialcharacters.svg'; import '../theme/specialcharacters.css'; @@ -82,12 +83,22 @@ export default class SpecialCharacters extends Plugin { specialCharsGroups.push( ALL_SPECIAL_CHARACTERS_GROUP ); const navigationView = new SpecialCharactersNavigationView( locale, specialCharsGroups ); - const gridView = new CharacterGridView( this.locale, { - columns: 10 - } ); + const gridView = new CharacterGridView( locale ); + const infoView = new CharacterInfoView( locale ); gridView.delegate( 'execute' ).to( dropdownView ); + gridView.on( 'tileHover', ( evt, data ) => { + infoView.set( data ); + } ); + + dropdownView.on( 'change:isOpen', () => { + infoView.set( { + character: null, + name: null + } ); + } ); + // Set the initial content of the special characters grid. this._updateGrid( navigationView.currentGroupName, gridView ); @@ -112,6 +123,7 @@ export default class SpecialCharacters extends Plugin { dropdownView.panelView.children.add( navigationView ); dropdownView.panelView.children.add( gridView ); + dropdownView.panelView.children.add( infoView ); return dropdownView; } ); diff --git a/src/ui/charactergridview.js b/src/ui/charactergridview.js index 9913398..365a587 100644 --- a/src/ui/charactergridview.js +++ b/src/ui/charactergridview.js @@ -88,9 +88,16 @@ export default class CharacterGridView extends View { tile.extendTemplate( { attributes: { title: name + }, + on: { + mouseover: tile.bindTemplate.to( 'mouseover' ) } } ); + tile.on( 'mouseover', () => { + this.fire( 'tileHover', { character, name } ); + } ); + tile.on( 'execute', () => { this.fire( 'execute', { name, character } ); } ); diff --git a/src/ui/characterinfoview.js b/src/ui/characterinfoview.js new file mode 100644 index 0000000..47f64a6 --- /dev/null +++ b/src/ui/characterinfoview.js @@ -0,0 +1,78 @@ +/** + * @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/characterinfoview + */ + +import View from '@ckeditor/ckeditor5-ui/src/view'; + +import '../../theme/characterinfo.css'; + +export default class CharacterInfoView extends View { + constructor( locale ) { + super( locale ); + + const bind = this.bindTemplate; + + this.set( 'character', null ); + this.set( 'name', null ); + + this.bind( 'code' ).to( this, 'character', char => { + if ( char === null ) { + return ''; + } + + const hexCode = char.codePointAt( 0 ).toString( 16 ); + + return 'U+' + ( '0000' + hexCode ).slice( -4 ); + } ); + + this.setTemplate( { + tag: 'div', + children: [ + { + tag: 'span', + attributes: { + class: [ + 'ck-character-info__name' + ] + }, + children: [ + { + text: bind.to( 'name', name => { + if ( !name ) { + // ZWSP to prevent vertical collapsing. + return '\u200B'; + } + + return name; + } ) + } + ] + }, + { + tag: 'span', + attributes: { + class: [ + 'ck-character-info__code' + ] + }, + children: [ + { + text: bind.to( 'code' ) + } + ] + } + ], + attributes: { + class: [ + 'ck', + 'ck-character-info' + ] + } + } ); + } +} diff --git a/tests/specialcharacters.js b/tests/specialcharacters.js index cd896da..c13f1c6 100644 --- a/tests/specialcharacters.js +++ b/tests/specialcharacters.js @@ -13,6 +13,7 @@ import SpecialCharactersMathematical from '../src/specialcharactersmathematical' import SpecialCharactersArrows from '../src/specialcharactersarrows'; import SpecialCharactersNavigationView from '../src/ui/specialcharactersnavigationview'; import CharacterGridView from '../src/ui/charactergridview'; +import CharacterInfoView from '../src/ui/characterinfoview'; import specialCharactersIcon from '../theme/icons/specialcharacters.svg'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; @@ -82,7 +83,11 @@ describe( 'SpecialCharacters', () => { } ); it( 'has a grid view', () => { - expect( dropdown.panelView.children.last ).to.be.instanceOf( CharacterGridView ); + expect( dropdown.panelView.children.get( 1 ) ).to.be.instanceOf( CharacterGridView ); + } ); + + it( 'has a character info view', () => { + expect( dropdown.panelView.children.last ).to.be.instanceOf( CharacterInfoView ); } ); describe( '#buttonView', () => { @@ -102,7 +107,7 @@ describe( 'SpecialCharacters', () => { } ); it( 'executes a command and focuses the editing view', () => { - const grid = dropdown.panelView.children.last; + const grid = dropdown.panelView.children.get( 1 ); const executeSpy = sinon.stub( editor, 'execute' ); const focusSpy = sinon.stub( editor.editing.view, 'focus' ); @@ -119,7 +124,7 @@ describe( 'SpecialCharacters', () => { let grid; beforeEach( () => { - grid = dropdown.panelView.children.last; + grid = dropdown.panelView.children.get( 1 ); } ); it( 'delegates #execute to the dropdown', () => { @@ -144,6 +149,34 @@ describe( 'SpecialCharacters', () => { expect( grid.tiles.get( 0 ).label ).to.equal( '⇐' ); } ); } ); + + describe( 'character info view', () => { + let grid, characterInfo; + + beforeEach( () => { + grid = dropdown.panelView.children.get( 1 ); + characterInfo = dropdown.panelView.children.last; + } ); + + it( 'is empty when the dropdown was shown', () => { + dropdown.fire( 'change:isOpen' ); + + expect( characterInfo.character ).to.equal( null ); + expect( characterInfo.name ).to.equal( null ); + expect( characterInfo.code ).to.equal( '' ); + } ); + + it( 'is updated when the tile fires #mouseover', () => { + const tile = grid.tiles.get( 0 ); + + tile.fire( 'mouseover' ); + + expect( tile.label ).to.equal( '<' ); + expect( characterInfo.character ).to.equal( '<' ); + expect( characterInfo.name ).to.equal( 'Less-than sign' ); + expect( characterInfo.code ).to.equal( 'U+003c' ); + } ); + } ); } ); } ); @@ -163,15 +196,19 @@ describe( 'SpecialCharacters', () => { } ); it( 'works with subsequent calls to the same group', () => { - plugin.addItems( 'Mathematical', [ { - title: 'dot', - character: '.' - } ] ); - - plugin.addItems( 'Mathematical', [ { - title: ',', - character: 'comma' - } ] ); + plugin.addItems( 'Mathematical', [ + { + title: 'dot', + character: '.' + } + ] ); + + plugin.addItems( 'Mathematical', [ + { + title: ',', + character: 'comma' + } + ] ); const groups = [ ...plugin.getGroups() ]; expect( groups ).to.deep.equal( [ 'Mathematical' ] ); diff --git a/tests/ui/charactergridview.js b/tests/ui/charactergridview.js index 7ae7774..0ae9b83 100644 --- a/tests/ui/charactergridview.js +++ b/tests/ui/charactergridview.js @@ -63,5 +63,16 @@ describe( 'CharacterGridView', () => { sinon.assert.calledOnce( spy ); sinon.assert.calledWithExactly( spy, sinon.match.any, { name: 'foo bar baz', character: 'ε' } ); } ); + + it( 'delegates #tileHover from the tile to the grid on hover the tile', () => { + const tile = view.createTile( 'ε', 'foo bar baz' ); + const spy = sinon.spy(); + + view.on( 'tileHover', spy ); + tile.fire( 'mouseover' ); + + sinon.assert.calledOnce( spy ); + sinon.assert.calledWithExactly( spy, sinon.match.any, { name: 'foo bar baz', character: 'ε' } ); + } ); } ); } ); diff --git a/tests/ui/characterinfoview.js b/tests/ui/characterinfoview.js new file mode 100644 index 0000000..9804fe2 --- /dev/null +++ b/tests/ui/characterinfoview.js @@ -0,0 +1,74 @@ +/** + * @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 CharacterInfoView from '../../src/ui/characterinfoview'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +describe( 'CharacterInfoView', () => { + let view; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + view = new CharacterInfoView(); + view.render(); + } ); + + afterEach( () => { + view.destroy(); + } ); + + describe( 'constructor()', () => { + describe( '#character', () => { + it( 'is defined', () => { + expect( view.character ).to.equal( null ); + } ); + } ); + + describe( '#name', () => { + it( 'is defined', () => { + expect( view.name ).to.equal( null ); + } ); + } ); + + describe( '#code', () => { + it( 'is defined', () => { + expect( view.code ).to.equal( '' ); + } ); + + it( 'is bound to #character', () => { + view.set( 'character', 'A' ); + + expect( view.code ).to.equal( 'U+0041' ); + } ); + } ); + + describe( '#element', () => { + it( 'is being created from template', () => { + expect( view.element.classList.contains( 'ck' ) ).to.be.true; + expect( view.element.classList.contains( 'ck-character-info' ) ).to.be.true; + + expect( view.element.firstElementChild.classList.contains( 'ck-character-info__name' ) ).to.be.true; + expect( view.element.lastElementChild.classList.contains( 'ck-character-info__code' ) ).to.be.true; + } ); + + it( 'is being updated when #code and #name have changed', () => { + const infoEl = view.element.firstElementChild; + const codeEl = view.element.lastElementChild; + + expect( infoEl.innerText ).to.equal( '\u200B' ); + expect( codeEl.innerText ).to.equal( '' ); + + view.set( { + character: 'A', + name: 'SYMBOL: A' + } ); + + expect( infoEl.innerText ).to.equal( 'SYMBOL: A' ); + expect( codeEl.innerText ).to.equal( 'U+0041' ); + } ); + } ); + } ); +} ); diff --git a/theme/characterinfo.css b/theme/characterinfo.css new file mode 100644 index 0000000..5fa859d --- /dev/null +++ b/theme/characterinfo.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-info { + display: flex; + justify-content: space-between; +} From 59cd4ff9e22e9b2695d29f0b83a4271dc25ec071 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 2 Jan 2020 13:41:10 +0100 Subject: [PATCH 2/4] Documentation and code refactoring in the CharacterInfoView class module. --- src/ui/characterinfoview.js | 67 +++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/src/ui/characterinfoview.js b/src/ui/characterinfoview.js index 47f64a6..4d1abd4 100644 --- a/src/ui/characterinfoview.js +++ b/src/ui/characterinfoview.js @@ -11,24 +11,45 @@ import View from '@ckeditor/ckeditor5-ui/src/view'; import '../../theme/characterinfo.css'; +/** + * The view displaying detailed information about a special character glyph, e.g. upon + * hovering it with a mouse. + * + * @extends module:ui/view~View + */ export default class CharacterInfoView extends View { constructor( locale ) { super( locale ); const bind = this.bindTemplate; + /** + * The character which info is displayed by the view. For instance, + * "∑" or "¿". + * + * @observable + * @member {String|null} #character + */ this.set( 'character', null ); - this.set( 'name', null ); - this.bind( 'code' ).to( this, 'character', char => { - if ( char === null ) { - return ''; - } - - const hexCode = char.codePointAt( 0 ).toString( 16 ); + /** + * The name of the {@link #character}. For instance, + * "N-ary summation" or "Inverted question mark". + * + * @observable + * @member {String|null} #name + */ + this.set( 'name', null ); - return 'U+' + ( '0000' + hexCode ).slice( -4 ); - } ); + /** + * The "Unicode string" of the {@link #character}. For instance, + * "U+0061". + * + * @observable + * @readonly + * @member {String} #code + */ + this.bind( 'code' ).to( this, 'character', characterToUnicodeString ); this.setTemplate( { tag: 'div', @@ -42,14 +63,8 @@ export default class CharacterInfoView extends View { }, children: [ { - text: bind.to( 'name', name => { - if ( !name ) { - // ZWSP to prevent vertical collapsing. - return '\u200B'; - } - - return name; - } ) + // Note: ZWSP to prevent vertical collapsing. + text: bind.to( 'name', name => name ? name : '\u200B' ) } ] }, @@ -76,3 +91,21 @@ export default class CharacterInfoView extends View { } ); } } + +// Converts a character into a "Unicode string", for instance: +// +// "$" -> "U+0024" +// +// Returns empty string when character is `null`. +// +// @param {String} character +// @returns {String} +function characterToUnicodeString( character ) { + if ( character === null ) { + return ''; + } + + const hexCode = character.codePointAt( 0 ).toString( 16 ); + + return 'U+' + ( '0000' + hexCode ).slice( -4 ); +} From 0017a23aa1f523d82c473705f0845f9e3d102555 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 2 Jan 2020 13:47:22 +0100 Subject: [PATCH 3/4] Docs: Documentation of the CharacterGridView#tileHover event. --- src/ui/charactergridview.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ui/charactergridview.js b/src/ui/charactergridview.js index 365a587..c191450 100644 --- a/src/ui/charactergridview.js +++ b/src/ui/charactergridview.js @@ -65,6 +65,16 @@ export default class CharacterGridView extends View { * @param {String} data.name A name of the tile that caused the event (e.g. "greek small letter epsilon"). * @param {String} data.character A human-readable character displayed as label (e.g. "ε"). */ + + /** + * Fired when a mouse or other pointing device caused the cursor to move onto any {@link #tiles grid tile} + * (similar to the native `mouseover` DOM event). + * + * @event tileHover + * @param {Object} data Additional information about the event. + * @param {String} data.name A name of the tile that caused the event (e.g. "greek small letter epsilon"). + * @param {String} data.character A human-readable character displayed as label (e.g. "ε"). + */ } /** From 0d6b56db59be1635cbfb2843d63f2b3cdc0ceee5 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 2 Jan 2020 13:56:23 +0100 Subject: [PATCH 4/4] OCD [skip ci]. --- src/ui/charactergridview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/charactergridview.js b/src/ui/charactergridview.js index c191450..a97557d 100644 --- a/src/ui/charactergridview.js +++ b/src/ui/charactergridview.js @@ -105,7 +105,7 @@ export default class CharacterGridView extends View { } ); tile.on( 'mouseover', () => { - this.fire( 'tileHover', { character, name } ); + this.fire( 'tileHover', { name, character } ); } ); tile.on( 'execute', () => {