From 9f4fd8d0454d397087f4c19db3cd90d8b9567a77 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 3 Jun 2019 09:30:11 +0200 Subject: [PATCH 01/35] Add required dependencies. --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index fd64683..d674e96 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "ckeditor5-plugin" ], "dependencies": { + "@ckeditor/ckeditor5-core": "^12.1.0", + "lodash-es": "^4.17.10" }, "devDependencies": { "eslint": "^5.5.0", From ad4684e41f8abb33ad4cf75c62b474f3042a7c6d Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 6 Jun 2019 11:14:27 +0200 Subject: [PATCH 02/35] Add plugins base. --- src/utils.js | 42 ++++++++++ src/wordcount.js | 207 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 src/utils.js create mode 100644 src/wordcount.js diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..5b9e280 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,42 @@ +/** + * @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 wordcount/utils + */ + +/** + * Function walks through all model's nodes. It obtains a plain text from each {@link module:engine/model/text~Text} + * and {@link module:engine/model/textproxy~TextProxy}. All sections, which are not a text, are separated with a new line (`\n`). + * + * **Note:** Function walks through entire model. There should be considered throttling it during usage. + * + * @param {module:engine/model/node~Node} node + * @returns {String} Plain text representing model's data + */ +export function modelElementToPlainText( node ) { + let text = ''; + + if ( node.is( 'text' ) || node.is( 'textProxy' ) ) { + text += node.data; + } else { + let prev = null; + + for ( const child of node.getChildren() ) { + const childText = modelElementToPlainText( child ); + + // If last block was finish, start from new line. + if ( prev && prev.is( 'element' ) ) { + text += '\n'; + } + + text += childText; + + prev = child; + } + } + + return text; +} diff --git a/src/wordcount.js b/src/wordcount.js new file mode 100644 index 0000000..9625288 --- /dev/null +++ b/src/wordcount.js @@ -0,0 +1,207 @@ +/** + * @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 wordcount/wordcount + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import { modelElementToPlainText } from './utils'; +import { throttle } from 'lodash-es'; +import View from '@ckeditor/ckeditor5-ui/src/view'; +import Template from '@ckeditor/ckeditor5-ui/src/template'; + +/** + * The word count plugin. + * + * This plugin calculate all words and characters in all {@link module:engine/model/text~Text text nodes} available in the model. + * It also provides an HTML element, which updates it states whenever editor's content is changed. + * + * Firstly model's data are convert to plain text using {@link module:wordcount/utils.modelElementToPlainText}. + * Based on such created plain text there are determined amount of words and characters in your text. Please keep in mind + * that every block in editor is separate with a new line character, which is included into calculation. + * + * Few examples how word and character calculation are made: + * foo + * bar + * // Words: 2, Characters: 7 + * + * <$text bold="true">foobar + * // Words: 1, Characters: 6 + * + * *&^%) + * // Words: 0, Characters: 5 + * + * foo(bar) + * //Words: 2, Characters: 8 + * + * 12345 + * // Words: 1, Characters: 5 + * + * @extends module:core/plugin~Plugin + */ +export default class WordCount extends Plugin { + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + /** + * Property stores number of characters detected by {@link module:wordcount/wordcount~WordCount WordCount plugin}. + * + * @observable + * @readonly + * @member {Number} module:wordcount/wordcount~WordCount#characters + */ + this.set( 'characters', 0 ); + + /** + * Property stores number of words detected by {@link module:wordcount/wordcount~WordCount WordCount plugin}. + * + * @observable + * @readonly + * @member {Number} module:wordcount/wordcount~WordCount#words + */ + this.set( 'words', 0 ); + + /** + * Keeps reference to {@link module:ui/view~View view object} used to display self-updating HTML container. + * + * @private + * @readonly + * @type {module:ui/view~View} + */ + this._outputView; + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'WordCount'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + + editor.model.document.on( 'change', throttle( this._calcWordsAndCharacters.bind( this ), 250 ) ); + } + + /** + * @inheritDoc + */ + destroy() { + this._outputView.element.remove(); + this._outputView.destroy(); + + super.destroy(); + } + + /** + * Method creates self-updated HTML element. Repeated executions returns the same element. + * Returned element has followed HTML structure: + * + *
+ *
Words: 4
+ *
Characters: 28
+ *
+ * + * @returns {HTMLElement} + */ + getWordCountContainer() { + if ( !this._outputView ) { + this._outputView = new View(); + + const editor = this.editor; + const t = editor.t; + const displayWords = editor.config.get( 'wordCount.displayWords' ); + const displayCharacters = editor.config.get( 'wordCount.displayCharacters' ); + const bind = Template.bind( this, this ); + const children = []; + + if ( displayWords || displayWords === undefined ) { + const wordsLabel = t( 'Words' ); + + children.push( { + tag: 'div', + children: [ + { + text: [ + wordsLabel, + ': ', + bind.to( 'words' ) + ] + } + ] + } ); + } + + if ( displayCharacters || displayCharacters === undefined ) { + const charactersLabel = t( 'Characters' ); + + children.push( { + tag: 'div', + children: [ + { + text: [ + charactersLabel, + ': ', + bind.to( 'characters' ) + ] + } + ] + } ); + } + + this._outputView.setTemplate( { + tag: 'div', + attributes: { + class: [ + 'ck', + 'ck-word-count' + ] + }, + children + } ); + + this._outputView.render(); + } + + return this._outputView.element; + } + + /** + * Determines amount of words and characters in current editor's model and assigns it to {@link #characters} and {@link #words}. + * It also fires {@link #event:update}. + * + * @private + * @fires update + */ + _calcWordsAndCharacters() { + const txt = modelElementToPlainText( this.editor.model.document.getRoot() ); + + this.characters = txt.length; + + this.words = ( txt.match( /[_a-zA-Z0-9À-ž]+/gu ) || [] ).length; + + this.fire( 'update', { + words: this.words, + characters: this.characters + } ); + } +} + +/** + * Event is fired after {@link #words} and {@link #characters} are updated. + * + * @event update + * @param {Object} data + * @param {Number} data.words number of words in current model + * @param {Number} data.characters number of characters in current model + */ From 76d748583e52760476d70df8ba82a8ee1fe87f52 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 6 Jun 2019 11:15:19 +0200 Subject: [PATCH 03/35] Add some simple unit test. --- tests/manual/wordcount.html | 11 +++++++++++ tests/manual/wordcount.js | 33 +++++++++++++++++++++++++++++++++ tests/manual/wordcount.md | 1 + 3 files changed, 45 insertions(+) create mode 100644 tests/manual/wordcount.html create mode 100644 tests/manual/wordcount.js create mode 100644 tests/manual/wordcount.md diff --git a/tests/manual/wordcount.html b/tests/manual/wordcount.html new file mode 100644 index 0000000..af8cdb3 --- /dev/null +++ b/tests/manual/wordcount.html @@ -0,0 +1,11 @@ + +

Test editor

+
+ Hello world +
+
+
diff --git a/tests/manual/wordcount.js b/tests/manual/wordcount.js new file mode 100644 index 0000000..7b8430b --- /dev/null +++ b/tests/manual/wordcount.js @@ -0,0 +1,33 @@ +/** + * @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 WordCount from '../../src/wordcount'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet, WordCount ], + toolbar: [ + 'heading', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'link', 'undo', 'redo' + ] + } ) + .then( editor => { + window.editor = editor; + + const wordCountPlugin = editor.plugins.get( 'WordCount' ); + const wordCount = wordCountPlugin.getWordCountContainer(); + + wordCountPlugin.on( 'update', ( evt, payload ) => { + console.log( JSON.stringify( payload ) ); + } ); + + document.getElementById( 'words' ).appendChild( wordCount ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/wordcount.md b/tests/manual/wordcount.md new file mode 100644 index 0000000..a43b27e --- /dev/null +++ b/tests/manual/wordcount.md @@ -0,0 +1 @@ +Test it From 8e59ff9d075cbed6abb1bfa403dbffc55a9b7bd2 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 10 Jun 2019 11:17:06 +0200 Subject: [PATCH 04/35] Fix docs typo. --- src/wordcount.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wordcount.js b/src/wordcount.js index 9625288..80b0d81 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -24,6 +24,7 @@ import Template from '@ckeditor/ckeditor5-ui/src/template'; * that every block in editor is separate with a new line character, which is included into calculation. * * Few examples how word and character calculation are made: + * * foo * bar * // Words: 2, Characters: 7 From 1f0a05b9126336a5e8c14deebc3af98a3dc704f6 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 11 Jun 2019 10:51:02 +0200 Subject: [PATCH 05/35] Small typo and white characters improvements. --- package.json | 11 +++++++---- src/wordcount.js | 6 ++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index d674e96..f4c7e50 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,17 @@ "ckeditor", "ckeditor5", "ckeditor 5", - "ckeditor5-feature", - "ckeditor5-plugin" + "ckeditor5-feature", + "ckeditor5-plugin" ], "dependencies": { - "@ckeditor/ckeditor5-core": "^12.1.0", - "lodash-es": "^4.17.10" + "@ckeditor/ckeditor5-core": "^12.1.0", + "lodash-es": "^4.17.10" }, "devDependencies": { + "@ckeditor/ckeditor5-engine": "^13.1.1", + "@ckeditor/ckeditor5-paragraph": "^11.0.2", + "@ckeditor/ckeditor5-utils": "^12.1.1", "eslint": "^5.5.0", "eslint-config-ckeditor5": "^1.0.11", "husky": "^1.3.1", diff --git a/src/wordcount.js b/src/wordcount.js index 80b0d81..4fa01dd 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -127,7 +127,7 @@ export default class WordCount extends Plugin { const children = []; if ( displayWords || displayWords === undefined ) { - const wordsLabel = t( 'Words' ); + const wordsLabel = t( 'Words' ) + ':'; children.push( { tag: 'div', @@ -135,7 +135,6 @@ export default class WordCount extends Plugin { { text: [ wordsLabel, - ': ', bind.to( 'words' ) ] } @@ -144,7 +143,7 @@ export default class WordCount extends Plugin { } if ( displayCharacters || displayCharacters === undefined ) { - const charactersLabel = t( 'Characters' ); + const charactersLabel = t( 'Characters' ) + ':'; children.push( { tag: 'div', @@ -152,7 +151,6 @@ export default class WordCount extends Plugin { { text: [ charactersLabel, - ': ', bind.to( 'characters' ) ] } From faff505882c01063da7407c5f01cc6882cc58a5a Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 11 Jun 2019 10:51:26 +0200 Subject: [PATCH 06/35] Add unit tests for word count plugin. --- tests/utils.js | 24 +++++ tests/wordcount.js | 264 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 tests/utils.js create mode 100644 tests/wordcount.js diff --git a/tests/utils.js b/tests/utils.js new file mode 100644 index 0000000..911241e --- /dev/null +++ b/tests/utils.js @@ -0,0 +1,24 @@ +/** + * @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 { modelElementToPlainText } from '../src/utils'; + +import Element from '@ckeditor/ckeditor5-engine/src/model/element'; +import Text from '@ckeditor/ckeditor5-engine/src/model/text'; + +describe( 'modelElementToPlainText()', () => { + it( 'should extract only plain text', () => { + const text1 = new Text( 'Foo' ); + const text2 = new Text( 'Bar', { bold: true } ); + const text3 = new Text( 'Baz', { bold: true, underline: true } ); + + const innerElement1 = new Element( 'paragraph', null, [ text1 ] ); + const innerElement2 = new Element( 'paragraph', null, [ text2, text3 ] ); + + const mainElement = new Element( 'container', null, [ innerElement1, innerElement2 ] ); + + expect( modelElementToPlainText( mainElement ) ).to.equal( 'Foo\nBarBaz' ); + } ); +} ); diff --git a/tests/wordcount.js b/tests/wordcount.js new file mode 100644 index 0000000..be5a748 --- /dev/null +++ b/tests/wordcount.js @@ -0,0 +1,264 @@ +/** + * @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 HTMLElement, setTimeout, document */ + +import WordCount from '../src/wordcount'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; + +describe( 'WordCount', () => { + testUtils.createSinonSandbox(); + + let wordCountPlugin, editor, model; + + beforeEach( () => { + return VirtualTestEditor.create( { + plugins: [ WordCount, Paragraph ] + } ) + .then( _editor => { + editor = _editor; + model = editor.model; + wordCountPlugin = editor.plugins.get( 'WordCount' ); + + model.schema.extend( '$text', { allowAttributes: 'foo' } ); + } ); + } ); + + describe( 'constructor()', () => { + it( 'has defined "words" property', () => { + expect( wordCountPlugin.words ).to.equal( 0 ); + } ); + + it( 'has defined "characters" property', () => { + expect( wordCountPlugin.characters ).to.equal( 0 ); + } ); + + it( 'has defined "_outputView" property', () => { + expect( wordCountPlugin._outputView ).to.be.undefined; + } ); + + it( 'has "WordCount" plugin name', () => { + expect( WordCount.pluginName ).to.equal( 'WordCount' ); + } ); + } ); + + describe( 'functionality', () => { + it( 'counts words', () => { + expect( wordCountPlugin.words ).to.equal( 0 ); + + setModelData( model, 'Foo(bar)baz' + + '<$text foo="true">Hello world.' + + '1234' + + '(-@#$%^*())' ); + + wordCountPlugin._calcWordsAndCharacters(); + + expect( wordCountPlugin.words ).to.equal( 6 ); + } ); + + it( 'counts characters', () => { + setModelData( model, '<$text foo="true">Hello world.' ); + + wordCountPlugin._calcWordsAndCharacters(); + + expect( wordCountPlugin.characters ).to.equal( 12 ); + } ); + + describe( 'update event', () => { + it( 'fires update event with actual amount of characters and words', () => { + const fake = sinon.fake(); + wordCountPlugin.on( 'update', fake ); + + wordCountPlugin._calcWordsAndCharacters(); + + sinon.assert.calledOnce( fake ); + sinon.assert.calledWithExactly( fake, sinon.match.any, { words: 0, characters: 0 } ); + + // _calcWordsAndCharacters is throttled, so for this test case is run manually + setModelData( model, '<$text foo="true">Hello world.' ); + wordCountPlugin._calcWordsAndCharacters(); + + sinon.assert.calledTwice( fake ); + sinon.assert.calledWithExactly( fake, sinon.match.any, { words: 2, characters: 12 } ); + } ); + } ); + } ); + + describe( 'self-updating element', () => { + let container; + beforeEach( () => { + container = wordCountPlugin.getWordCountContainer(); + } ); + + it( 'provides html element', () => { + expect( container ).to.be.instanceof( HTMLElement ); + } ); + + it( 'provided element has proper structure', () => { + expect( container.tagName ).to.equal( 'DIV' ); + expect( container.classList.contains( 'ck' ) ).to.be.true; + expect( container.classList.contains( 'ck-word-count' ) ).to.be.true; + + const children = Array.from( container.children ); + expect( children.length ).to.equal( 2 ); + expect( children[ 0 ].tagName ).to.equal( 'DIV' ); + expect( children[ 0 ].innerHTML ).to.equal( 'Words: 0' ); + expect( children[ 1 ].tagName ).to.equal( 'DIV' ); + expect( children[ 1 ].innerHTML ).to.equal( 'Characters: 0' ); + } ); + + it( 'updates container content', () => { + expect( container.innerText ).to.equal( 'Words: 0Characters: 0' ); + + setModelData( model, 'Foo(bar)baz' + + '<$text foo="true">Hello world.' ); + + wordCountPlugin._calcWordsAndCharacters(); + + // There is \n between paragraph which has to be included into calculations + expect( container.innerText ).to.equal( 'Words: 5Characters: 24' ); + } ); + + it( 'subsequent calls provides the same element', () => { + const newContainer = wordCountPlugin.getWordCountContainer(); + + expect( container ).to.equal( newContainer ); + } ); + + describe( 'destroy()', () => { + it( 'html element is removed and cleanup', done => { + const frag = document.createDocumentFragment(); + + frag.appendChild( container ); + + expect( frag.querySelector( '*' ) ).to.be.instanceof( HTMLElement ); + + editor.destroy() + .then( () => { + expect( frag.querySelector( '*' ) ).to.be.null; + } ) + .then( done ) + .catch( done ); + } ); + + it( 'method is called', done => { + const spy = sinon.spy( wordCountPlugin, 'destroy' ); + + editor.destroy() + .then( () => { + sinon.assert.calledOnce( spy ); + } ) + .then( done ) + .catch( done ); + } ); + } ); + } ); + + describe( '_calcWordsAndCharacters and throttle', () => { + beforeEach( done => { + // We need to flush initial throttle value after editor's initialization + setTimeout( () => { + done(); + }, 255 ); + } ); + + it( 'gets update after model data change', done => { + const fake = sinon.fake(); + + wordCountPlugin.on( 'update', fake ); + + // Initial change in model should be immediately reflected in word-count + setModelData( model, 'Hello world.' ); + + sinon.assert.calledOnce( fake ); + sinon.assert.calledWith( fake, sinon.match.any, { words: 2, characters: 12 } ); + + // Subsequent updates should be throttle and run with last parameters + setTimeout( () => { + sinon.assert.calledTwice( fake ); + sinon.assert.calledWith( fake, sinon.match.any, { words: 2, characters: 9 } ); + + done(); + }, 255 ); + + setModelData( model, 'Hello world' ); + setModelData( model, 'Hello worl' ); + setModelData( model, 'Hello wor' ); + } ); + } ); + + describe( 'custom config options', () => { + it( 'displayWords = false', done => { + VirtualTestEditor.create( { + plugins: [ WordCount, Paragraph ], + wordCount: { + displayWords: false + } + } ) + .then( editor => { + const wordCountPlugin = editor.plugins.get( 'WordCount' ); + const container = wordCountPlugin.getWordCountContainer(); + + expect( container.innerText ).to.equal( 'Characters: 0' ); + } ) + .then( done ) + .catch( done ); + } ); + + it( 'displayCharacters = false', done => { + VirtualTestEditor.create( { + plugins: [ WordCount, Paragraph ], + wordCount: { + displayCharacters: false + } + } ) + .then( editor => { + const wordCountPlugin = editor.plugins.get( 'WordCount' ); + const container = wordCountPlugin.getWordCountContainer(); + + expect( container.innerText ).to.equal( 'Words: 0' ); + } ) + .then( done ) + .catch( done ); + } ); + } ); + + describe( 'translations', () => { + before( () => { + addTranslations( 'pl', { + Words: 'Słowa', + Characters: 'Znaki' + } ); + addTranslations( 'en', { + Words: 'Words', + Characters: 'Characters' + } ); + } ); + + after( () => { + clearTranslations(); + } ); + + it( 'applies proper language translations', done => { + VirtualTestEditor.create( { + plugins: [ WordCount, Paragraph ], + language: 'pl' + } ) + .then( editor => { + const wordCountPlugin = editor.plugins.get( 'WordCount' ); + const container = wordCountPlugin.getWordCountContainer(); + + expect( container.innerText ).to.equal( 'Słowa: 0Znaki: 0' ); + } ) + .then( done ) + .catch( done ); + } ); + } ); +} ); From a92751db29bc09dcaaee4081dfa8a37b59ff9751 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 11 Jun 2019 11:04:30 +0200 Subject: [PATCH 07/35] Update manual test description ad functions. --- tests/manual/wordcount.html | 3 ++- tests/manual/wordcount.js | 4 ++++ tests/manual/wordcount.md | 9 ++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/manual/wordcount.html b/tests/manual/wordcount.html index af8cdb3..da10204 100644 --- a/tests/manual/wordcount.html +++ b/tests/manual/wordcount.html @@ -4,8 +4,9 @@ }

Test editor

+
- Hello world + Hello world.
diff --git a/tests/manual/wordcount.js b/tests/manual/wordcount.js index 7b8430b..69dc22c 100644 --- a/tests/manual/wordcount.js +++ b/tests/manual/wordcount.js @@ -27,6 +27,10 @@ ClassicEditor } ); document.getElementById( 'words' ).appendChild( wordCount ); + + document.getElementById( 'destroy-editor' ).addEventListener( 'click', () => { + editor.destroy(); + } ); } ) .catch( err => { console.error( err.stack ); diff --git a/tests/manual/wordcount.md b/tests/manual/wordcount.md index a43b27e..67b7eec 100644 --- a/tests/manual/wordcount.md +++ b/tests/manual/wordcount.md @@ -1 +1,8 @@ -Test it +1. Try to type in editor. Container below should be automatically updated with new amount of words and characters. +2. Special characters are treat as separators for words. For example + * `Hello world` - 2 words + * `Hello(World)` - 2 words + * `Hello\nWorld` - 2 words +3. Numbers are treat as words. +4. There are logged values of `WordCount:event-update` in the console. Values should change in same way as container in html. +5. After destroy container with word and character values should be removed. From fe53c68056db236557192e8d8f2ef7b271590dd5 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 11 Jun 2019 12:33:51 +0200 Subject: [PATCH 08/35] Extend documentation for word count plugin. --- src/wordcount.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/wordcount.js b/src/wordcount.js index 4fa01dd..ee15836 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -17,7 +17,7 @@ import Template from '@ckeditor/ckeditor5-ui/src/template'; * The word count plugin. * * This plugin calculate all words and characters in all {@link module:engine/model/text~Text text nodes} available in the model. - * It also provides an HTML element, which updates it states whenever editor's content is changed. + * It also provides an HTML element, which updates its states whenever editor's content is changed. * * Firstly model's data are convert to plain text using {@link module:wordcount/utils.modelElementToPlainText}. * Based on such created plain text there are determined amount of words and characters in your text. Please keep in mind @@ -204,3 +204,63 @@ export default class WordCount extends Plugin { * @param {Number} data.words number of words in current model * @param {Number} data.characters number of characters in current model */ + +/** + * The configuration of the word count feature. + * + * ClassicEditor + * .create( { + * wordCount: ... // Word count feature configuration. + * } ) + * .then( ... ) + * .catch( ... ); + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + * + * @interface module:wordcount/wordcount~WordCountConfig + */ + +/** + * The configuration of the word count feature. + * It is introduced by the {@link module:wordcount/wordcount~WordCount} feature. + * + * Read more in {@link module:wordcount/wordcount~WordCountConfig}. + * + * @member {module:wordcount/wordcount~WordCountConfig} module:core/editor/editorconfig~EditorConfig#wordCount + */ + +/** + * This options allows on hiding word counter. The element obtained through + * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} will only preserve + * characters part. Word counter is displayed by default, when configuration option is not defined. + * + * const wordCountConfig = { + * displayWords = false + * } + * + * Mentioned configuration will result with followed container: + * + *
+ *
Characters: 28
+ *
+ * + * @member {Boolean} module:wordcount/wordcount~WordCountConfig#displayWords + */ + +/** + * This options allows on hiding character counter. The element obtained through + * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} will only preserve + * words part. Character counter is displayed by default, when configuration option is not defined. + * + * const wordCountConfig = { + * displayCharacters = false + * } + * + * Mentioned configuration will result with followed container: + * + *
+ *
Words: 4
+ *
+ * + * @member {Boolean} module:wordcount/wordcount~WordCountConfig#displayCharacters + */ From a7b485f234b73de9b689c8515c568b8f3d893a76 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 11 Jun 2019 15:39:08 +0200 Subject: [PATCH 09/35] Add docs article describing the feature. --- .../features/build-word-count-source.html | 0 .../features/build-word-count-source.js | 14 +++ .../_snippets/features/word-count-update.html | 36 ++++++ docs/_snippets/features/word-count-update.js | 48 ++++++++ docs/_snippets/features/word-count.html | 13 +++ docs/_snippets/features/word-count.js | 39 +++++++ docs/features/word-count.md | 108 ++++++++++++++++++ 7 files changed, 258 insertions(+) create mode 100644 docs/_snippets/features/build-word-count-source.html create mode 100644 docs/_snippets/features/build-word-count-source.js create mode 100644 docs/_snippets/features/word-count-update.html create mode 100644 docs/_snippets/features/word-count-update.js create mode 100644 docs/_snippets/features/word-count.html create mode 100644 docs/_snippets/features/word-count.js create mode 100644 docs/features/word-count.md diff --git a/docs/_snippets/features/build-word-count-source.html b/docs/_snippets/features/build-word-count-source.html new file mode 100644 index 0000000..e69de29 diff --git a/docs/_snippets/features/build-word-count-source.js b/docs/_snippets/features/build-word-count-source.js new file mode 100644 index 0000000..bc27dbc --- /dev/null +++ b/docs/_snippets/features/build-word-count-source.js @@ -0,0 +1,14 @@ +/** + * @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 window */ + +import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor'; + +import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount'; + +ClassicEditor.builtinPlugins.push( WordCount ); + +window.ClassicEditor = ClassicEditor; diff --git a/docs/_snippets/features/word-count-update.html b/docs/_snippets/features/word-count-update.html new file mode 100644 index 0000000..23bb149 --- /dev/null +++ b/docs/_snippets/features/word-count-update.html @@ -0,0 +1,36 @@ + + + +
+

A black hole is a region of spacetime exhibiting gravitational acceleration so strong that nothing—no particles or even electromagnetic radiation such as light—can escape from it.[6] The theory of general relativity predicts that a sufficiently compact mass can deform spacetime to form a black hole.

+
+
+
+ +
+
+ Characters: +
+
+
diff --git a/docs/_snippets/features/word-count-update.js b/docs/_snippets/features/word-count-update.js new file mode 100644 index 0000000..9bbe4d4 --- /dev/null +++ b/docs/_snippets/features/word-count-update.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 + */ + +/* global window, document, console, ClassicEditor */ + +ClassicEditor + .create( document.querySelector( '#demo-editor-update' ), { + toolbar: { + items: [ + 'heading', + 'bold', + 'italic', + 'bulletedList', + 'numberedList', + 'blockQuote', + 'link', + '|', + 'mediaEmbed', + 'insertTable', + '|', + 'undo', + 'redo' + ], + viewportTopOffset: window.getViewportTopOffsetConfig() + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] + } + } ) + .then( editor => { + const wordCountPlugin = editor.plugins.get( 'WordCount' ); + + const progressBar = document.querySelector( '.customized-counter progress' ); + const colorBox = document.querySelector( '.customized-counter__color-box' ); + + wordCountPlugin.on( 'update', updateHandler ); + + function updateHandler( evt, payload ) { + progressBar.value = payload.words; + colorBox.style.setProperty( '--hue', payload.characters * 3 ); + } + } ) + .catch( err => { + console.error( err.stack ); + } ); + diff --git a/docs/_snippets/features/word-count.html b/docs/_snippets/features/word-count.html new file mode 100644 index 0000000..db05f3f --- /dev/null +++ b/docs/_snippets/features/word-count.html @@ -0,0 +1,13 @@ + +
+

The Battle of Westerplatte was one of the first battles in Germany's invasion of Poland, marking the start of World War II in Europe. Beginning on 1 September 1939, German army, naval and air forces and Danzig police assaulted Poland's Military Transit Depot (Wojskowa Składnica Tranzytowa, or WST) on the Westerplatte peninsula in the harbor of the Free City of Danzig. The Poles held out for seven days and repelled 13 assaults that included dive-bomber attacks and naval shelling.

+

Westerplatte's defense served as an inspiration for the Polish Army and people in the face of German advances elsewhere, and is still regarded as a symbol of resistance in modern Poland.

+
+
+
diff --git a/docs/_snippets/features/word-count.js b/docs/_snippets/features/word-count.js new file mode 100644 index 0000000..b0fd93c --- /dev/null +++ b/docs/_snippets/features/word-count.js @@ -0,0 +1,39 @@ +/** + * @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, window, console, ClassicEditor */ + +ClassicEditor + .create( document.querySelector( '#demo-editor' ), { + toolbar: { + items: [ + 'heading', + 'bold', + 'italic', + 'bulletedList', + 'numberedList', + 'blockQuote', + 'link', + '|', + 'mediaEmbed', + 'insertTable', + '|', + 'undo', + 'redo' + ], + viewportTopOffset: window.getViewportTopOffsetConfig() + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] + } + } ) + .then( editor => { + window.editor = editor; + + document.getElementById( 'demo-word-counter' ).appendChild( editor.plugins.get( 'WordCount' ).getWordCountContainer() ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/features/word-count.md b/docs/features/word-count.md new file mode 100644 index 0000000..69bb2b2 --- /dev/null +++ b/docs/features/word-count.md @@ -0,0 +1,108 @@ +--- +category: features +--- + +{@snippet features/build-word-count-source} + +# Word count + +The {@link module:wordcount/wordcount~WordCount} features provide a possibility to track the number of words and characters written in the editor. + +## Demo + +{@snippet features/word-count} + +```html +
+

Hello world.

+
+
+
+``` + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + // configuration details + } ) + .then( editor => { + const wordCountPlugin = editor.plugins.get( 'WordCount' ); + const wordCountWrapper = document.querySelector( '.word-count' ); + + wordCountWrapper.appendChild( wordCounterPlugin.getWordCountContainer() ); + } ) + .catch( err => { + console.error( err.stack ); + } ); +``` + +## Configuring options + +There are two options which change the output container. If there is set {@link module:wordcount/wordcount~WordCountConfig#displayWords} to `false`, then the section with word counter is removed from self-updating output container. In a similar way works second option {@link module:wordcount/wordcount~WordCountConfig#displayCharacters} with character container. + +## Update event + +Word count feature emits an {@link module:wordcount/wordcount~WordCount#event:update update event} whenever there is a change in a model. This allows on having own callback with customized behavior reacting on this change. + +Below you can find an example, where the background color of a square is changed according to the number of characters in the editor. There is also a progress bar which indicates how many words is in it (the maximal value of the progress bar is set to 100, however, you can write further and progress bar remain in the maximal state). + +{@snippet features/word-count-update} + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + // configuration details + } ) + .then( editor => { + const wordCountPlugin = editor.plugins.get( 'WordCount' ); + + wordCountPlugin.on( 'update', ( evt, payload ) => { + // payload is an object with "words" and "characters" field + doSthWithNewWordsNumber( payload.words ); + doSthWithNewCharactersNumber( payload.characters ); + } ); + + } ) + .catch( err => { + console.error( err.stack ); + } ); +``` + +## Installation + +To add this feature to your rich-text editor, install the [`@ckeditor/ckeditor5-word-count`](https://www.npmjs.com/package/@ckeditor/ckeditor5-word-count) package: + +```bash +npm install --save @ckeditor/ckeditor5-word-count +``` + +And add it to your plugin list configuration: + +```js +import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ WordCount, ... ], + } ) + .then( ... ) + .catch( ... ); +``` + + + Read more about {@link builds/guides/integration/installing-plugins installing plugins}. + + +## Common API + +The {@link module:wordcount/wordcount~WordCount} plugin provides: + * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} method. It returns a self-updating HTML Element which might be used to track the current amount of words and characters in the editor. There is a possibility to remove "Words" or "Characters" counter with proper configuration of {@link module:wordcount/wordcount~WordCountConfig#displayWords} and {@link module:wordcount/wordcount~WordCountConfig#displayCharacters}, + * {@link module:wordcount/wordcount~WordCount#event:update update event} which provides more versatile option to handle changes of words' and characters' number. There is a possibility to run own callback function with updated values. + + + We recommend using the official {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} for development and debugging. It will give you tons of useful information about the state of the editor such as internal data structures, selection, commands, and many more. + + +## Contribute + +The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-word-count. From 5b5ab056b30b85ec8689198ecb940f00333a5ee0 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 12 Jun 2019 14:07:49 +0200 Subject: [PATCH 10/35] Docs improvements. --- src/utils.js | 4 ++-- src/wordcount.js | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/utils.js b/src/utils.js index 5b9e280..6943f8b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,10 +8,10 @@ */ /** - * Function walks through all model's nodes. It obtains a plain text from each {@link module:engine/model/text~Text} + * Function walks through all the model's nodes. It obtains a plain text from each {@link module:engine/model/text~Text} * and {@link module:engine/model/textproxy~TextProxy}. All sections, which are not a text, are separated with a new line (`\n`). * - * **Note:** Function walks through entire model. There should be considered throttling it during usage. + * **Note:** Function walks through the entire model. There should be considered throttling during usage. * * @param {module:engine/model/node~Node} node * @returns {String} Plain text representing model's data diff --git a/src/wordcount.js b/src/wordcount.js index ee15836..0fdc401 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -16,14 +16,14 @@ import Template from '@ckeditor/ckeditor5-ui/src/template'; /** * The word count plugin. * - * This plugin calculate all words and characters in all {@link module:engine/model/text~Text text nodes} available in the model. - * It also provides an HTML element, which updates its states whenever editor's content is changed. + * This plugin calculates all words and characters in all {@link module:engine/model/text~Text text nodes} available in the model. + * It also provides an HTML element, which updates its states whenever the editor's content is changed. * * Firstly model's data are convert to plain text using {@link module:wordcount/utils.modelElementToPlainText}. * Based on such created plain text there are determined amount of words and characters in your text. Please keep in mind - * that every block in editor is separate with a new line character, which is included into calculation. + * that every block in the editor is separate with a newline character, which is included in the calculation. * - * Few examples how word and character calculation are made: + * Few examples of how word and character calculation are made: * * foo * bar @@ -105,7 +105,7 @@ export default class WordCount extends Plugin { } /** - * Method creates self-updated HTML element. Repeated executions returns the same element. + * Method creates self-updated HTML element. Repeated executions return the same element. * Returned element has followed HTML structure: * *
@@ -176,7 +176,7 @@ export default class WordCount extends Plugin { } /** - * Determines amount of words and characters in current editor's model and assigns it to {@link #characters} and {@link #words}. + * Determines the number of words and characters in the current editor's model and assigns it to {@link #characters} and {@link #words}. * It also fires {@link #event:update}. * * @private @@ -230,15 +230,15 @@ export default class WordCount extends Plugin { */ /** - * This options allows on hiding word counter. The element obtained through + * This option allows on hiding the word counter. The element obtained through * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} will only preserve - * characters part. Word counter is displayed by default, when configuration option is not defined. + * the characters part. Word counter is displayed by default when this configuration option is not defined. * * const wordCountConfig = { * displayWords = false * } * - * Mentioned configuration will result with followed container: + * The mentioned configuration will result with the followed container: * *
*
Characters: 28
@@ -248,15 +248,15 @@ export default class WordCount extends Plugin { */ /** - * This options allows on hiding character counter. The element obtained through + * This option allows on hiding the character counter. The element obtained through * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} will only preserve - * words part. Character counter is displayed by default, when configuration option is not defined. + * the words part. Character counter is displayed by default when this configuration option is not defined. * * const wordCountConfig = { * displayCharacters = false * } * - * Mentioned configuration will result with followed container: + * The mentioned configuration will result with the followed container: * *
*
Words: 4
From 88858fd73728d033cd656ca1c0264610c6ce682c Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 12 Jun 2019 14:37:50 +0200 Subject: [PATCH 11/35] Improve snippet's style. --- docs/_snippets/features/word-count-update.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/_snippets/features/word-count-update.html b/docs/_snippets/features/word-count-update.html index 23bb149..a2a96d0 100644 --- a/docs/_snippets/features/word-count-update.html +++ b/docs/_snippets/features/word-count-update.html @@ -9,6 +9,8 @@ } .customized-counter { + border: 3px solid #333; + padding-left: 5px; margin-bottom: 15px; } From 244f4adaca27918a4c17127a939a3abe4d2a67bb Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 26 Jun 2019 15:23:49 +0200 Subject: [PATCH 12/35] Fix typos in snippets. --- docs/_snippets/features/word-count-update.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_snippets/features/word-count-update.html b/docs/_snippets/features/word-count-update.html index a2a96d0..f221b06 100644 --- a/docs/_snippets/features/word-count-update.html +++ b/docs/_snippets/features/word-count-update.html @@ -28,7 +28,7 @@
From 5470728e0e89a9ceee204c2b12e9e7a0f9ceec03 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 26 Jun 2019 16:48:39 +0200 Subject: [PATCH 13/35] Minor tweaks to feature and tests with review. --- docs/features/word-count.md | 30 +++++++++++++----------------- src/utils.js | 15 +++++++-------- src/wordcount.js | 32 ++++++++++++++++---------------- tests/manual/wordcount.md | 8 ++++---- tests/utils.js | 20 +++++++++++--------- tests/wordcount.js | 20 +++++++++----------- 6 files changed, 60 insertions(+), 65 deletions(-) diff --git a/docs/features/word-count.md b/docs/features/word-count.md index 69bb2b2..35ec65b 100644 --- a/docs/features/word-count.md +++ b/docs/features/word-count.md @@ -6,7 +6,7 @@ category: features # Word count -The {@link module:wordcount/wordcount~WordCount} features provide a possibility to track the number of words and characters written in the editor. +The {@link module:wordcount/wordcount~WordCount} feature provides a possibility to track the number of words and characters written in the editor. ## Demo @@ -16,7 +16,7 @@ The {@link module:wordcount/wordcount~WordCount} features provide a possibility

Hello world.

-
+
``` @@ -27,22 +27,20 @@ ClassicEditor } ) .then( editor => { const wordCountPlugin = editor.plugins.get( 'WordCount' ); - const wordCountWrapper = document.querySelector( '.word-count' ); + const wordCountWrapper = document.getElementById( 'word-count' ); wordCountWrapper.appendChild( wordCounterPlugin.getWordCountContainer() ); } ) - .catch( err => { - console.error( err.stack ); - } ); + .catch( ... ); ``` ## Configuring options -There are two options which change the output container. If there is set {@link module:wordcount/wordcount~WordCountConfig#displayWords} to `false`, then the section with word counter is removed from self-updating output container. In a similar way works second option {@link module:wordcount/wordcount~WordCountConfig#displayCharacters} with character container. +There are two options which change the output container. If the {@link module:wordcount/wordcount~WordCountConfig#displayWords} is set to to `false`, then the section with word counter is hidden. Similarly, when the {@link module:wordcount/wordcount~WordCountConfig#displayCharacters} is set to `false` it will hide the character counter. ## Update event -Word count feature emits an {@link module:wordcount/wordcount~WordCount#event:update update event} whenever there is a change in a model. This allows on having own callback with customized behavior reacting on this change. +Word count feature emits an {@link module:wordcount/wordcount~WordCount#event:update update event} whenever there is a change in the model. This allows implementing customized behavior that reacts to word count updates. Below you can find an example, where the background color of a square is changed according to the number of characters in the editor. There is also a progress bar which indicates how many words is in it (the maximal value of the progress bar is set to 100, however, you can write further and progress bar remain in the maximal state). @@ -56,16 +54,14 @@ ClassicEditor .then( editor => { const wordCountPlugin = editor.plugins.get( 'WordCount' ); - wordCountPlugin.on( 'update', ( evt, payload ) => { - // payload is an object with "words" and "characters" field - doSthWithNewWordsNumber( payload.words ); - doSthWithNewCharactersNumber( payload.characters ); + wordCountPlugin.on( 'update', ( evt, data ) => { + // data is an object with "words" and "characters" field + doSthWithNewWordsNumber( data.words ); + doSthWithNewCharactersNumber( data.characters ); } ); } ) - .catch( err => { - console.error( err.stack ); - } ); + .catch( ... ); ``` ## Installation @@ -96,8 +92,8 @@ ClassicEditor ## Common API The {@link module:wordcount/wordcount~WordCount} plugin provides: - * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} method. It returns a self-updating HTML Element which might be used to track the current amount of words and characters in the editor. There is a possibility to remove "Words" or "Characters" counter with proper configuration of {@link module:wordcount/wordcount~WordCountConfig#displayWords} and {@link module:wordcount/wordcount~WordCountConfig#displayCharacters}, - * {@link module:wordcount/wordcount~WordCount#event:update update event} which provides more versatile option to handle changes of words' and characters' number. There is a possibility to run own callback function with updated values. + * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} method. It returns a self-updating HTML element which is updated with the current number of words and characters in the editor. There is a possibility to remove "Words" or "Characters" counters with proper configuration of {@link module:wordcount/wordcount~WordCountConfig#displayWords} and {@link module:wordcount/wordcount~WordCountConfig#displayCharacters}, + * {@link module:wordcount/wordcount~WordCount#event:update update event} which is fired whenever the plugins update the number of counted words and characters. There is a possibility to run own callback function with updated values. Please note that update event is throttled. We recommend using the official {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} for development and debugging. It will give you tons of useful information about the state of the editor such as internal data structures, selection, commands, and many more. diff --git a/src/utils.js b/src/utils.js index 6943f8b..ed1dc91 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,23 +8,22 @@ */ /** - * Function walks through all the model's nodes. It obtains a plain text from each {@link module:engine/model/text~Text} - * and {@link module:engine/model/textproxy~TextProxy}. All sections, which are not a text, are separated with a new line (`\n`). + * Returns plain text representation of an element and it's children. The blocks are separated by a newline (\n ). * - * **Note:** Function walks through the entire model. There should be considered throttling during usage. + * **Note:** Function walks through the entire model, which might be very spread. There should be considered throttling it during usage. * - * @param {module:engine/model/node~Node} node + * @param {module:engine/model/element~Element} element * @returns {String} Plain text representing model's data */ -export function modelElementToPlainText( node ) { +export function modelElementToPlainText( element ) { let text = ''; - if ( node.is( 'text' ) || node.is( 'textProxy' ) ) { - text += node.data; + if ( element.is( 'text' ) || element.is( 'textProxy' ) ) { + text += element.data; } else { let prev = null; - for ( const child of node.getChildren() ) { + for ( const child of element.getChildren() ) { const childText = modelElementToPlainText( child ); // If last block was finish, start from new line. diff --git a/src/wordcount.js b/src/wordcount.js index 0fdc401..32c4eb1 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -51,7 +51,7 @@ export default class WordCount extends Plugin { super( editor ); /** - * Property stores number of characters detected by {@link module:wordcount/wordcount~WordCount WordCount plugin}. + * The number of characters in the editor. * * @observable * @readonly @@ -60,7 +60,7 @@ export default class WordCount extends Plugin { this.set( 'characters', 0 ); /** - * Property stores number of words detected by {@link module:wordcount/wordcount~WordCount WordCount plugin}. + * The number of words in the editor. * * @observable * @readonly @@ -69,7 +69,7 @@ export default class WordCount extends Plugin { this.set( 'words', 0 ); /** - * Keeps reference to {@link module:ui/view~View view object} used to display self-updating HTML container. + * A reference to a {@link module:ui/view~View view object} which contains self-updating HTML container. * * @private * @readonly @@ -91,7 +91,7 @@ export default class WordCount extends Plugin { init() { const editor = this.editor; - editor.model.document.on( 'change', throttle( this._calcWordsAndCharacters.bind( this ), 250 ) ); + editor.model.document.on( 'change:data', throttle( this._calculateWordsAndCharacters.bind( this ), 250 ) ); } /** @@ -116,16 +116,16 @@ export default class WordCount extends Plugin { * @returns {HTMLElement} */ getWordCountContainer() { + const editor = this.editor; + const t = editor.t; + const displayWords = editor.config.get( 'wordCount.displayWords' ); + const displayCharacters = editor.config.get( 'wordCount.displayCharacters' ); + const bind = Template.bind( this, this ); + const children = []; + if ( !this._outputView ) { this._outputView = new View(); - const editor = this.editor; - const t = editor.t; - const displayWords = editor.config.get( 'wordCount.displayWords' ); - const displayCharacters = editor.config.get( 'wordCount.displayCharacters' ); - const bind = Template.bind( this, this ); - const children = []; - if ( displayWords || displayWords === undefined ) { const wordsLabel = t( 'Words' ) + ':'; @@ -182,7 +182,7 @@ export default class WordCount extends Plugin { * @private * @fires update */ - _calcWordsAndCharacters() { + _calculateWordsAndCharacters() { const txt = modelElementToPlainText( this.editor.model.document.getRoot() ); this.characters = txt.length; @@ -197,7 +197,7 @@ export default class WordCount extends Plugin { } /** - * Event is fired after {@link #words} and {@link #characters} are updated. + * Event fired after {@link #words} and {@link #characters} are updated. * * @event update * @param {Object} data @@ -230,7 +230,7 @@ export default class WordCount extends Plugin { */ /** - * This option allows on hiding the word counter. The element obtained through + * This option allows for hiding the word counter. The element obtained through * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} will only preserve * the characters part. Word counter is displayed by default when this configuration option is not defined. * @@ -248,7 +248,7 @@ export default class WordCount extends Plugin { */ /** - * This option allows on hiding the character counter. The element obtained through + * This option allows for hiding the character counter. The element obtained through * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} will only preserve * the words part. Character counter is displayed by default when this configuration option is not defined. * @@ -256,7 +256,7 @@ export default class WordCount extends Plugin { * displayCharacters = false * } * - * The mentioned configuration will result with the followed container: + * The mentioned configuration will result in the following container * *
*
Words: 4
diff --git a/tests/manual/wordcount.md b/tests/manual/wordcount.md index 67b7eec..7ca1ab0 100644 --- a/tests/manual/wordcount.md +++ b/tests/manual/wordcount.md @@ -1,8 +1,8 @@ -1. Try to type in editor. Container below should be automatically updated with new amount of words and characters. -2. Special characters are treat as separators for words. For example +1. Try to type in the editor. The container below should be automatically updated with the current amount of words and characters. +2. Special characters are treated as separators for words. For example * `Hello world` - 2 words * `Hello(World)` - 2 words * `Hello\nWorld` - 2 words -3. Numbers are treat as words. +3. Numbers are treated as words. 4. There are logged values of `WordCount:event-update` in the console. Values should change in same way as container in html. -5. After destroy container with word and character values should be removed. +5. After destroying the editor, the container with word and character values should be also removed. diff --git a/tests/utils.js b/tests/utils.js index 911241e..e060dc7 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -8,17 +8,19 @@ import { modelElementToPlainText } from '../src/utils'; import Element from '@ckeditor/ckeditor5-engine/src/model/element'; import Text from '@ckeditor/ckeditor5-engine/src/model/text'; -describe( 'modelElementToPlainText()', () => { - it( 'should extract only plain text', () => { - const text1 = new Text( 'Foo' ); - const text2 = new Text( 'Bar', { bold: true } ); - const text3 = new Text( 'Baz', { bold: true, underline: true } ); +describe( 'utils', () => { + describe( 'modelElementToPlainText()', () => { + it( 'should extract only plain text', () => { + const text1 = new Text( 'Foo' ); + const text2 = new Text( 'Bar', { bold: true } ); + const text3 = new Text( 'Baz', { bold: true, underline: true } ); - const innerElement1 = new Element( 'paragraph', null, [ text1 ] ); - const innerElement2 = new Element( 'paragraph', null, [ text2, text3 ] ); + const innerElement1 = new Element( 'paragraph', null, [ text1 ] ); + const innerElement2 = new Element( 'paragraph', null, [ text2, text3 ] ); - const mainElement = new Element( 'container', null, [ innerElement1, innerElement2 ] ); + const mainElement = new Element( 'container', null, [ innerElement1, innerElement2 ] ); - expect( modelElementToPlainText( mainElement ) ).to.equal( 'Foo\nBarBaz' ); + expect( modelElementToPlainText( mainElement ) ).to.equal( 'Foo\nBarBaz' ); + } ); } ); } ); diff --git a/tests/wordcount.js b/tests/wordcount.js index be5a748..86e013b 100644 --- a/tests/wordcount.js +++ b/tests/wordcount.js @@ -58,7 +58,7 @@ describe( 'WordCount', () => { '1234' + '(-@#$%^*())' ); - wordCountPlugin._calcWordsAndCharacters(); + wordCountPlugin._calculateWordsAndCharacters(); expect( wordCountPlugin.words ).to.equal( 6 ); } ); @@ -66,7 +66,7 @@ describe( 'WordCount', () => { it( 'counts characters', () => { setModelData( model, '<$text foo="true">Hello world.' ); - wordCountPlugin._calcWordsAndCharacters(); + wordCountPlugin._calculateWordsAndCharacters(); expect( wordCountPlugin.characters ).to.equal( 12 ); } ); @@ -76,14 +76,14 @@ describe( 'WordCount', () => { const fake = sinon.fake(); wordCountPlugin.on( 'update', fake ); - wordCountPlugin._calcWordsAndCharacters(); + wordCountPlugin._calculateWordsAndCharacters(); sinon.assert.calledOnce( fake ); sinon.assert.calledWithExactly( fake, sinon.match.any, { words: 0, characters: 0 } ); - // _calcWordsAndCharacters is throttled, so for this test case is run manually + // _calculateWordsAndCharacters is throttled, so for this test case is run manually setModelData( model, '<$text foo="true">Hello world.' ); - wordCountPlugin._calcWordsAndCharacters(); + wordCountPlugin._calculateWordsAndCharacters(); sinon.assert.calledTwice( fake ); sinon.assert.calledWithExactly( fake, sinon.match.any, { words: 2, characters: 12 } ); @@ -120,7 +120,7 @@ describe( 'WordCount', () => { setModelData( model, 'Foo(bar)baz' + '<$text foo="true">Hello world.' ); - wordCountPlugin._calcWordsAndCharacters(); + wordCountPlugin._calculateWordsAndCharacters(); // There is \n between paragraph which has to be included into calculations expect( container.innerText ).to.equal( 'Words: 5Characters: 24' ); @@ -133,7 +133,7 @@ describe( 'WordCount', () => { } ); describe( 'destroy()', () => { - it( 'html element is removed and cleanup', done => { + it( 'html element is removed', done => { const frag = document.createDocumentFragment(); frag.appendChild( container ); @@ -161,12 +161,10 @@ describe( 'WordCount', () => { } ); } ); - describe( '_calcWordsAndCharacters and throttle', () => { + describe( '_calculateWordsAndCharacters and throttle', () => { beforeEach( done => { // We need to flush initial throttle value after editor's initialization - setTimeout( () => { - done(); - }, 255 ); + setTimeout( done, 255 ); } ); it( 'gets update after model data change', done => { From 0a9fd8263f2f7170da6fd527ca0df462d951c6e3 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 27 Jun 2019 11:30:06 +0200 Subject: [PATCH 14/35] Simplify model to plain text transformation. --- src/utils.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/utils.js b/src/utils.js index ed1dc91..44f3a50 100644 --- a/src/utils.js +++ b/src/utils.js @@ -16,25 +16,24 @@ * @returns {String} Plain text representing model's data */ export function modelElementToPlainText( element ) { - let text = ''; - if ( element.is( 'text' ) || element.is( 'textProxy' ) ) { - text += element.data; - } else { - let prev = null; - - for ( const child of element.getChildren() ) { - const childText = modelElementToPlainText( child ); + return element.data; + } - // If last block was finish, start from new line. - if ( prev && prev.is( 'element' ) ) { - text += '\n'; - } + let text = ''; + let prev = null; - text += childText; + for ( const child of element.getChildren() ) { + const childText = modelElementToPlainText( child ); - prev = child; + // If last block was finish, start from new line. + if ( prev && prev.is( 'element' ) ) { + text += '\n'; } + + text += childText; + + prev = child; } return text; From fb86541000d1cd6804025a8ceab13af1dcac7c9a Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 27 Jun 2019 12:46:01 +0200 Subject: [PATCH 15/35] Improve way of translations usage. --- lang/contexts.json | 4 ++++ src/wordcount.js | 38 ++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 lang/contexts.json diff --git a/lang/contexts.json b/lang/contexts.json new file mode 100644 index 0000000..6a59f9e --- /dev/null +++ b/lang/contexts.json @@ -0,0 +1,4 @@ +{ + "Words: %0": "Label showing how many words is in the editor", + "Characters: %0": "Label showing how many characters is in the editor" +} diff --git a/src/wordcount.js b/src/wordcount.js index 32c4eb1..0fbf910 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -68,6 +68,26 @@ export default class WordCount extends Plugin { */ this.set( 'words', 0 ); + /** + * Label used to display words value in {@link #getWordCountContainer output contianer} + * + * @observable + * @private + * @readonly + * @member {String} module:wordcount/wordcount~WordCount#_wordsLabel + */ + this.set( '_wordsLabel' ); + + /** + * Label used to display characters value in {@link #getWordCountContainer output contianer} + * + * @observable + * @private + * @readonly + * @member {String} module:wordcount/wordcount~WordCount#_charactersLabel + */ + this.set( '_charactersLabel' ); + /** * A reference to a {@link module:ui/view~View view object} which contains self-updating HTML container. * @@ -127,32 +147,30 @@ export default class WordCount extends Plugin { this._outputView = new View(); if ( displayWords || displayWords === undefined ) { - const wordsLabel = t( 'Words' ) + ':'; + this.bind( '_wordsLabel' ).to( this, 'words', words => { + return t( 'Words: %0', [ words ] ); + } ); children.push( { tag: 'div', children: [ { - text: [ - wordsLabel, - bind.to( 'words' ) - ] + text: [ bind.to( '_wordsLabel' ) ] } ] } ); } if ( displayCharacters || displayCharacters === undefined ) { - const charactersLabel = t( 'Characters' ) + ':'; + this.bind( '_charactersLabel' ).to( this, 'characters', words => { + return t( 'Characters: %0', [ words ] ); + } ); children.push( { tag: 'div', children: [ { - text: [ - charactersLabel, - bind.to( 'characters' ) - ] + text: [ bind.to( '_charactersLabel' ) ] } ] } ); From 06a2ca4f8d983b062b2bbb14b9a6dac3d987226f Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 27 Jun 2019 12:57:58 +0200 Subject: [PATCH 16/35] Add class for output contianer --- src/wordcount.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/wordcount.js b/src/wordcount.js index 0fbf910..f610719 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -157,7 +157,10 @@ export default class WordCount extends Plugin { { text: [ bind.to( '_wordsLabel' ) ] } - ] + ], + attributes: { + class: 'ck-word-count__words' + } } ); } @@ -172,7 +175,10 @@ export default class WordCount extends Plugin { { text: [ bind.to( '_charactersLabel' ) ] } - ] + ], + attributes: { + class: 'ck-word-count__characters' + } } ); } From 334afa9a88f45a844faf94e0a68cb135d6c25969 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 27 Jun 2019 13:09:56 +0200 Subject: [PATCH 17/35] Add classes to output container. --- src/wordcount.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wordcount.js b/src/wordcount.js index f610719..d9962d4 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -129,8 +129,8 @@ export default class WordCount extends Plugin { * Returned element has followed HTML structure: * *
- *
Words: 4
- *
Characters: 28
+ *
Words: 4
+ *
Characters: 28
*
* * @returns {HTMLElement} @@ -265,7 +265,7 @@ export default class WordCount extends Plugin { * The mentioned configuration will result with the followed container: * *
- *
Characters: 28
+ *
Characters: 28
*
* * @member {Boolean} module:wordcount/wordcount~WordCountConfig#displayWords @@ -283,7 +283,7 @@ export default class WordCount extends Plugin { * The mentioned configuration will result in the following container * *
- *
Words: 4
+ *
Words: 4
*
* * @member {Boolean} module:wordcount/wordcount~WordCountConfig#displayCharacters From 0942361b04a36a1254c0f89e3c96604376b02114 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 27 Jun 2019 16:38:10 +0200 Subject: [PATCH 18/35] Add unit test for more complicated structures to convert. --- tests/utils.js | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/tests/utils.js b/tests/utils.js index e060dc7..ff07949 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -7,6 +7,16 @@ import { modelElementToPlainText } from '../src/utils'; import Element from '@ckeditor/ckeditor5-engine/src/model/element'; import Text from '@ckeditor/ckeditor5-engine/src/model/text'; +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; +import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting'; +import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting'; +import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; +import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; describe( 'utils', () => { describe( 'modelElementToPlainText()', () => { @@ -22,5 +32,100 @@ describe( 'utils', () => { expect( modelElementToPlainText( mainElement ) ).to.equal( 'Foo\nBarBaz' ); } ); + + describe( 'complex structures', () => { + let editor, model; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Enter, ShiftEnter, Paragraph, BoldEditing, LinkEditing, BlockQuoteEditing, ListEditing, TableEditing ] + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'extracts plain text from blockqoutes', () => { + setModelData( model, '
' + + 'Hello' + + 'world' + + 'foo' + + 'bar' + + '
' ); + + expect( modelElementToPlainText( model.document.getRoot() ) ).to.equal( 'Hello\nworld\nfoo\nbar' ); + } ); + + it( 'extracts plain text from tables', () => { + setModelData( model, '' + + '' + + '' + + 'Foo' + + '' + + '' + + 'Bar' + + '' + + '' + + '' + + '' + + 'Baz' + + '' + + '' + + 'Foo' + + '' + + '' + + '
' ); + + expect( modelElementToPlainText( model.document.getRoot() ) ).to.equal( 'Foo\nBar\nBaz\nFoo' ); + } ); + + it( 'extracts plain text with soft break', () => { + setModelData( model, 'Foobar' ); + + expect( modelElementToPlainText( model.document.getRoot() ) ).to.equal( 'Foo\nbar' ); + } ); + + it( 'extracts plain text with inline styles', () => { + setModelData( model, 'F<$text bold="true">oo<$text href="url">Bar' ); + + expect( modelElementToPlainText( model.document.getRoot() ) ).to.equal( 'FooBar' ); + } ); + + it( 'extracts plain text from mixing structure', () => { + setModelData( model, '' + + '<$text bold="true">111<$text href="url" bold="true">222333' + + '
' + + '444555' + + '' + + '' + + '666' + + '7<$text bold="true">77' + + '' + + '' + + '888' + + '999' + + '' + + '
' + + '
' + + '' + + '000' + + '
' + + '111' + + '222' + + '
' + + '
' + + '
' ); + + expect( modelElementToPlainText( model.document.getRoot() ) ).to.equal( + '111222333\n444\n555\n666\n777\n888\n999\n000\n111\n222' + ); + } ); + } ); } ); } ); From f49ac899279e3e69240b83f248ee4081bdd43349 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 28 Jun 2019 09:17:00 +0200 Subject: [PATCH 19/35] Fix localization tests. --- tests/wordcount.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/wordcount.js b/tests/wordcount.js index 86e013b..b91b326 100644 --- a/tests/wordcount.js +++ b/tests/wordcount.js @@ -231,12 +231,12 @@ describe( 'WordCount', () => { describe( 'translations', () => { before( () => { addTranslations( 'pl', { - Words: 'Słowa', - Characters: 'Znaki' + 'Words: %0': 'Słowa: %0', + 'Characters: %0': 'Znaki: %0' } ); addTranslations( 'en', { - Words: 'Words', - Characters: 'Characters' + 'Words: %0': 'Words: %0', + 'Characters: %0': 'Characters: %0' } ); } ); @@ -260,3 +260,4 @@ describe( 'WordCount', () => { } ); } ); } ); + From 0a7318d758560ee3f0bcf4b36aa8f6549d48f6ce Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 28 Jun 2019 12:46:17 +0200 Subject: [PATCH 20/35] Add unit test for integration wordcount with selection change in the model. --- tests/wordcount.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/wordcount.js b/tests/wordcount.js index b91b326..4fcdad0 100644 --- a/tests/wordcount.js +++ b/tests/wordcount.js @@ -12,6 +12,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; describe( 'WordCount', () => { testUtils.createSinonSandbox(); @@ -190,6 +191,35 @@ describe( 'WordCount', () => { setModelData( model, 'Hello worl' ); setModelData( model, 'Hello wor' ); } ); + + it( 'is not update after selection change', done => { + setModelData( model, 'Hello[] world.' ); + + const fake = sinon.fake(); + const fakeSelectionChange = sinon.fake(); + + wordCountPlugin.on( 'update', fake ); + model.document.on( 'change', fakeSelectionChange ); + + model.change( writer => { + const range = writer.createRange( new Position( model.document.getRoot(), [ 0, 1 ] ) ); + + writer.setSelection( range ); + } ); + + model.change( writer => { + const range = writer.createRange( new Position( model.document.getRoot(), [ 0, 10 ] ) ); + + writer.setSelection( range ); + } ); + + setTimeout( () => { + sinon.assert.notCalled( fake ); + sinon.assert.called( fakeSelectionChange ); + + done(); + }, 255 ); + } ); } ); describe( 'custom config options', () => { From fbdd2a80f977fdeab838c07837ea0747fd0e8762 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 28 Jun 2019 13:10:23 +0200 Subject: [PATCH 21/35] Add support for configuration option, 'onUpdate' and 'container' --- src/wordcount.js | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/wordcount.js b/src/wordcount.js index d9962d4..283a61f 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -9,7 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import { modelElementToPlainText } from './utils'; -import { throttle } from 'lodash-es'; +import { throttle, isElement } from 'lodash-es'; import View from '@ckeditor/ckeditor5-ui/src/view'; import Template from '@ckeditor/ckeditor5-ui/src/template'; @@ -88,6 +88,14 @@ export default class WordCount extends Plugin { */ this.set( '_charactersLabel' ); + /** + * The configuration of this plugins. + * + * @private + * @type {Object} + */ + this._config = editor.config.get( 'wordCount' ) || {}; + /** * A reference to a {@link module:ui/view~View view object} which contains self-updating HTML container. * @@ -112,6 +120,16 @@ export default class WordCount extends Plugin { const editor = this.editor; editor.model.document.on( 'change:data', throttle( this._calculateWordsAndCharacters.bind( this ), 250 ) ); + + if ( typeof this._config.onUpdate == 'function' ) { + this.on( 'update', ( evt, data ) => { + this._config.onUpdate( data ); + } ); + } + + if ( isElement( this._config.container ) ) { + this._config.container.appendChild( this.getWordCountContainer() ); + } } /** @@ -288,3 +306,29 @@ export default class WordCount extends Plugin { * * @member {Boolean} module:wordcount/wordcount~WordCountConfig#displayCharacters */ + +/** + * This configuration takes a function, which is executed whenever the word-count plugin update its values. + * Function is called with one argument, which is object with `words` and `characters` keys containing + * an amount of detected words and characters in the document. + * + * const wordCountConfig = { + * onUpdate: function( values ) { + * doSthWithWordNumber( values.words ); + * doSthWithCharacterNumber( values.characters ); + * } + * } + * + * @member {Function} module:wordcount/wordcount~WordCountConfig#onUpdate + */ + +/** + * This option allows on providing an HTML element where + * {@link module:wordcount/wordcount~WordCount#getWordCountContainer word count container} will be append autoamtically. + * + * const wordCountConfig = { + * container: document.getElementById( 'container-for-word-count' ); + * } + * + * @member {Function} module:wordcount/wordcount~WordCountConfig#container + */ From 405390272c81f5b2a4f31e5ad9416b2808c66ca5 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 28 Jun 2019 13:24:39 +0200 Subject: [PATCH 22/35] Update manual test to use new configuration optiona. --- tests/manual/wordcount.html | 2 +- tests/manual/wordcount.js | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/manual/wordcount.html b/tests/manual/wordcount.html index da10204..69db8f6 100644 --- a/tests/manual/wordcount.html +++ b/tests/manual/wordcount.html @@ -8,5 +8,5 @@

Test editor

Hello world.
-
+
diff --git a/tests/manual/wordcount.js b/tests/manual/wordcount.js index 69dc22c..17f53b0 100644 --- a/tests/manual/wordcount.js +++ b/tests/manual/wordcount.js @@ -14,20 +14,17 @@ ClassicEditor plugins: [ ArticlePluginSet, WordCount ], toolbar: [ 'heading', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'link', 'undo', 'redo' - ] + ], + wordCount: { + onUpdate: values => { + console.log( `Values from 'onUpdate': ${ JSON.stringify( values ) }` ); + }, + container: document.getElementById( 'other-words-container' ) + } } ) .then( editor => { window.editor = editor; - const wordCountPlugin = editor.plugins.get( 'WordCount' ); - const wordCount = wordCountPlugin.getWordCountContainer(); - - wordCountPlugin.on( 'update', ( evt, payload ) => { - console.log( JSON.stringify( payload ) ); - } ); - - document.getElementById( 'words' ).appendChild( wordCount ); - document.getElementById( 'destroy-editor' ).addEventListener( 'click', () => { editor.destroy(); } ); From 22050badd7a28427a7bf9d4d695eae0b61293b3c Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 28 Jun 2019 14:31:27 +0200 Subject: [PATCH 23/35] Add unit test covers new config options. Remove test which has no sense. --- tests/wordcount.js | 58 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/tests/wordcount.js b/tests/wordcount.js index 4fcdad0..3433219 100644 --- a/tests/wordcount.js +++ b/tests/wordcount.js @@ -14,6 +14,9 @@ import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-util import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; +// Delay related to word-count throttling. +const DELAY = 255; + describe( 'WordCount', () => { testUtils.createSinonSandbox(); @@ -41,13 +44,13 @@ describe( 'WordCount', () => { expect( wordCountPlugin.characters ).to.equal( 0 ); } ); - it( 'has defined "_outputView" property', () => { - expect( wordCountPlugin._outputView ).to.be.undefined; - } ); - it( 'has "WordCount" plugin name', () => { expect( WordCount.pluginName ).to.equal( 'WordCount' ); } ); + + it( 'has define "_config" object', () => { + expect( wordCountPlugin._config ).to.deep.equal( {} ); + } ); } ); describe( 'functionality', () => { @@ -165,7 +168,7 @@ describe( 'WordCount', () => { describe( '_calculateWordsAndCharacters and throttle', () => { beforeEach( done => { // We need to flush initial throttle value after editor's initialization - setTimeout( done, 255 ); + setTimeout( done, DELAY ); } ); it( 'gets update after model data change', done => { @@ -185,7 +188,7 @@ describe( 'WordCount', () => { sinon.assert.calledWith( fake, sinon.match.any, { words: 2, characters: 9 } ); done(); - }, 255 ); + }, DELAY ); setModelData( model, 'Hello world' ); setModelData( model, 'Hello worl' ); @@ -218,7 +221,7 @@ describe( 'WordCount', () => { sinon.assert.called( fakeSelectionChange ); done(); - }, 255 ); + }, DELAY ); } ); } ); @@ -256,6 +259,47 @@ describe( 'WordCount', () => { .then( done ) .catch( done ); } ); + + it( 'should call function register under config.wordCount.onUpdate', () => { + const fake = sinon.fake(); + return VirtualTestEditor.create( { + plugins: [ WordCount, Paragraph ], + wordCount: { + onUpdate: fake + } + } ) + .then( editor => { + sinon.assert.calledWithExactly( fake, { words: 0, characters: 0 } ); + + setModelData( editor.model, 'Foo Bar' ); + } ) + .then( () => new Promise( resolve => { + setTimeout( resolve, DELAY ); + } ) ) + .then( () => { + sinon.assert.calledWithExactly( fake, { words: 2, characters: 7 } ); + } ); + } ); + + it( 'should append word count container in element referenced in config.wordCount.container', () => { + const element = document.createElement( 'div' ); + + expect( element.children.length ).to.equal( 0 ); + + return VirtualTestEditor.create( { + plugins: [ WordCount, Paragraph ], + wordCount: { + container: element + } + } ) + .then( editor => { + expect( element.children.length ).to.equal( 1 ); + + const wordCountPlugin = editor.plugins.get( 'WordCount' ); + + expect( element.firstElementChild ).to.equal( wordCountPlugin.getWordCountContainer() ); + } ); + } ); } ); describe( 'translations', () => { From da6b71832dd5da6eac8e19aebc0e44962cc622ca Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 28 Jun 2019 14:35:55 +0200 Subject: [PATCH 24/35] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Piotrek Koszuliński --- docs/features/word-count.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/word-count.md b/docs/features/word-count.md index 35ec65b..fbaadd8 100644 --- a/docs/features/word-count.md +++ b/docs/features/word-count.md @@ -23,7 +23,7 @@ The {@link module:wordcount/wordcount~WordCount} feature provides a possibility ```js ClassicEditor .create( document.querySelector( '#editor' ), { - // configuration details + // Configuration details. } ) .then( editor => { const wordCountPlugin = editor.plugins.get( 'WordCount' ); @@ -34,7 +34,7 @@ ClassicEditor .catch( ... ); ``` -## Configuring options +## Configuration options There are two options which change the output container. If the {@link module:wordcount/wordcount~WordCountConfig#displayWords} is set to to `false`, then the section with word counter is hidden. Similarly, when the {@link module:wordcount/wordcount~WordCountConfig#displayCharacters} is set to `false` it will hide the character counter. From 2677157b16939ca18966685e9d841c655efaa14e Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 28 Jun 2019 15:49:14 +0200 Subject: [PATCH 25/35] Apply suggestions from code review Co-Authored-By: Maciej --- src/wordcount.js | 10 +++++----- tests/utils.js | 2 +- tests/wordcount.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wordcount.js b/src/wordcount.js index 283a61f..6345afc 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -308,9 +308,9 @@ export default class WordCount extends Plugin { */ /** - * This configuration takes a function, which is executed whenever the word-count plugin update its values. - * Function is called with one argument, which is object with `words` and `characters` keys containing - * an amount of detected words and characters in the document. + * This configuration takes a function, which is executed whenever the word-count plugin updates its values. + * This function is called with one argument, which is an object with `words` and `characters` keys containing + * a number of detected words and characters in the document. * * const wordCountConfig = { * onUpdate: function( values ) { @@ -324,11 +324,11 @@ export default class WordCount extends Plugin { /** * This option allows on providing an HTML element where - * {@link module:wordcount/wordcount~WordCount#getWordCountContainer word count container} will be append autoamtically. + * {@link module:wordcount/wordcount~WordCount#getWordCountContainer word count container} will be appended automatically. * * const wordCountConfig = { * container: document.getElementById( 'container-for-word-count' ); * } * - * @member {Function} module:wordcount/wordcount~WordCountConfig#container + * @member {HTMLElement} module:wordcount/wordcount~WordCountConfig#container */ diff --git a/tests/utils.js b/tests/utils.js index ff07949..34dc704 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -97,7 +97,7 @@ describe( 'utils', () => { expect( modelElementToPlainText( model.document.getRoot() ) ).to.equal( 'FooBar' ); } ); - it( 'extracts plain text from mixing structure', () => { + it( 'extracts plain text from mixed structure', () => { setModelData( model, '' + '<$text bold="true">111<$text href="url" bold="true">222333' + '
' + diff --git a/tests/wordcount.js b/tests/wordcount.js index 3433219..565dd60 100644 --- a/tests/wordcount.js +++ b/tests/wordcount.js @@ -260,7 +260,7 @@ describe( 'WordCount', () => { .catch( done ); } ); - it( 'should call function register under config.wordCount.onUpdate', () => { + it( 'should call function registered under config.wordCount.onUpdate', () => { const fake = sinon.fake(); return VirtualTestEditor.create( { plugins: [ WordCount, Paragraph ], From c42606d5d6a04583c7081130d929b54e9588d533 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 28 Jun 2019 15:51:57 +0200 Subject: [PATCH 26/35] Remove unit test, which checks existence of private property. --- tests/wordcount.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/wordcount.js b/tests/wordcount.js index 565dd60..c314d9b 100644 --- a/tests/wordcount.js +++ b/tests/wordcount.js @@ -47,10 +47,6 @@ describe( 'WordCount', () => { it( 'has "WordCount" plugin name', () => { expect( WordCount.pluginName ).to.equal( 'WordCount' ); } ); - - it( 'has define "_config" object', () => { - expect( wordCountPlugin._config ).to.deep.equal( {} ); - } ); } ); describe( 'functionality', () => { From dae254fb0ba1bf635a0716264dbf933dc450b420 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 1 Jul 2019 09:21:11 +0200 Subject: [PATCH 27/35] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Piotrek Koszuliński --- lang/contexts.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/contexts.json b/lang/contexts.json index 6a59f9e..ff9e3bd 100644 --- a/lang/contexts.json +++ b/lang/contexts.json @@ -1,4 +1,4 @@ { - "Words: %0": "Label showing how many words is in the editor", - "Characters: %0": "Label showing how many characters is in the editor" + "Words: %0": "Label showing the number of words in the editor content", + "Characters: %0": "Label showing the number of characters in the editor content" } From 57bfe06cd6ac2db37022c1b42fab1038170ff677 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 1 Jul 2019 09:47:59 +0200 Subject: [PATCH 28/35] Transform method into getter. Update docs and tests. --- docs/features/word-count.md | 4 ++-- src/wordcount.js | 18 +++++++++--------- tests/manual/wordcount.html | 2 +- tests/manual/wordcount.js | 2 +- tests/wordcount.js | 12 ++++++------ 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/features/word-count.md b/docs/features/word-count.md index fbaadd8..9534bc8 100644 --- a/docs/features/word-count.md +++ b/docs/features/word-count.md @@ -29,7 +29,7 @@ ClassicEditor const wordCountPlugin = editor.plugins.get( 'WordCount' ); const wordCountWrapper = document.getElementById( 'word-count' ); - wordCountWrapper.appendChild( wordCounterPlugin.getWordCountContainer() ); + wordCountWrapper.appendChild( wordCounterPlugin.wordCountContainer ); } ) .catch( ... ); ``` @@ -92,7 +92,7 @@ ClassicEditor ## Common API The {@link module:wordcount/wordcount~WordCount} plugin provides: - * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} method. It returns a self-updating HTML element which is updated with the current number of words and characters in the editor. There is a possibility to remove "Words" or "Characters" counters with proper configuration of {@link module:wordcount/wordcount~WordCountConfig#displayWords} and {@link module:wordcount/wordcount~WordCountConfig#displayCharacters}, + * {@link module:wordcount/wordcount~WordCount#wordCountContainer} method. It returns a self-updating HTML element which is updated with the current number of words and characters in the editor. There is a possibility to remove "Words" or "Characters" counters with proper configuration of {@link module:wordcount/wordcount~WordCountConfig#displayWords} and {@link module:wordcount/wordcount~WordCountConfig#displayCharacters}, * {@link module:wordcount/wordcount~WordCount#event:update update event} which is fired whenever the plugins update the number of counted words and characters. There is a possibility to run own callback function with updated values. Please note that update event is throttled. diff --git a/src/wordcount.js b/src/wordcount.js index 6345afc..1c3ad27 100644 --- a/src/wordcount.js +++ b/src/wordcount.js @@ -69,7 +69,7 @@ export default class WordCount extends Plugin { this.set( 'words', 0 ); /** - * Label used to display words value in {@link #getWordCountContainer output contianer} + * Label used to display words value in {@link #wordCountContainer output container} * * @observable * @private @@ -79,7 +79,7 @@ export default class WordCount extends Plugin { this.set( '_wordsLabel' ); /** - * Label used to display characters value in {@link #getWordCountContainer output contianer} + * Label used to display characters value in {@link #wordCountContainer output container} * * @observable * @private @@ -128,7 +128,7 @@ export default class WordCount extends Plugin { } if ( isElement( this._config.container ) ) { - this._config.container.appendChild( this.getWordCountContainer() ); + this._config.container.appendChild( this.wordCountContainer ); } } @@ -143,7 +143,7 @@ export default class WordCount extends Plugin { } /** - * Method creates self-updated HTML element. Repeated executions return the same element. + * Creates self-updated HTML element. Repeated executions return the same element. * Returned element has followed HTML structure: * *
@@ -151,9 +151,9 @@ export default class WordCount extends Plugin { *
Characters: 28
*
* - * @returns {HTMLElement} + * @type {HTMLElement} */ - getWordCountContainer() { + get wordCountContainer() { const editor = this.editor; const t = editor.t; const displayWords = editor.config.get( 'wordCount.displayWords' ); @@ -273,7 +273,7 @@ export default class WordCount extends Plugin { /** * This option allows for hiding the word counter. The element obtained through - * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} will only preserve + * {@link module:wordcount/wordcount~WordCount#wordCountContainer} will only preserve * the characters part. Word counter is displayed by default when this configuration option is not defined. * * const wordCountConfig = { @@ -291,7 +291,7 @@ export default class WordCount extends Plugin { /** * This option allows for hiding the character counter. The element obtained through - * {@link module:wordcount/wordcount~WordCount#getWordCountContainer} will only preserve + * {@link module:wordcount/wordcount~WordCount#wordCountContainer} will only preserve * the words part. Character counter is displayed by default when this configuration option is not defined. * * const wordCountConfig = { @@ -324,7 +324,7 @@ export default class WordCount extends Plugin { /** * This option allows on providing an HTML element where - * {@link module:wordcount/wordcount~WordCount#getWordCountContainer word count container} will be appended automatically. + * {@link module:wordcount/wordcount~WordCount#wordCountContainer word count container} will be appended automatically. * * const wordCountConfig = { * container: document.getElementById( 'container-for-word-count' ); diff --git a/tests/manual/wordcount.html b/tests/manual/wordcount.html index 69db8f6..a364cf1 100644 --- a/tests/manual/wordcount.html +++ b/tests/manual/wordcount.html @@ -8,5 +8,5 @@

Test editor

Hello world.
-
+
diff --git a/tests/manual/wordcount.js b/tests/manual/wordcount.js index 17f53b0..628e33e 100644 --- a/tests/manual/wordcount.js +++ b/tests/manual/wordcount.js @@ -19,7 +19,7 @@ ClassicEditor onUpdate: values => { console.log( `Values from 'onUpdate': ${ JSON.stringify( values ) }` ); }, - container: document.getElementById( 'other-words-container' ) + container: document.getElementById( 'words-container' ) } } ) .then( editor => { diff --git a/tests/wordcount.js b/tests/wordcount.js index c314d9b..36099db 100644 --- a/tests/wordcount.js +++ b/tests/wordcount.js @@ -94,7 +94,7 @@ describe( 'WordCount', () => { describe( 'self-updating element', () => { let container; beforeEach( () => { - container = wordCountPlugin.getWordCountContainer(); + container = wordCountPlugin.wordCountContainer; } ); it( 'provides html element', () => { @@ -127,7 +127,7 @@ describe( 'WordCount', () => { } ); it( 'subsequent calls provides the same element', () => { - const newContainer = wordCountPlugin.getWordCountContainer(); + const newContainer = wordCountPlugin.wordCountContainer; expect( container ).to.equal( newContainer ); } ); @@ -231,7 +231,7 @@ describe( 'WordCount', () => { } ) .then( editor => { const wordCountPlugin = editor.plugins.get( 'WordCount' ); - const container = wordCountPlugin.getWordCountContainer(); + const container = wordCountPlugin.wordCountContainer; expect( container.innerText ).to.equal( 'Characters: 0' ); } ) @@ -248,7 +248,7 @@ describe( 'WordCount', () => { } ) .then( editor => { const wordCountPlugin = editor.plugins.get( 'WordCount' ); - const container = wordCountPlugin.getWordCountContainer(); + const container = wordCountPlugin.wordCountContainer; expect( container.innerText ).to.equal( 'Words: 0' ); } ) @@ -293,7 +293,7 @@ describe( 'WordCount', () => { const wordCountPlugin = editor.plugins.get( 'WordCount' ); - expect( element.firstElementChild ).to.equal( wordCountPlugin.getWordCountContainer() ); + expect( element.firstElementChild ).to.equal( wordCountPlugin.wordCountContainer ); } ); } ); } ); @@ -321,7 +321,7 @@ describe( 'WordCount', () => { } ) .then( editor => { const wordCountPlugin = editor.plugins.get( 'WordCount' ); - const container = wordCountPlugin.getWordCountContainer(); + const container = wordCountPlugin.wordCountContainer; expect( container.innerText ).to.equal( 'Słowa: 0Znaki: 0' ); } ) From 8c63c46036613b6b64eb08256fe2924f04d1f3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 1 Jul 2019 09:58:45 +0200 Subject: [PATCH 29/35] Fixed a typo. --- docs/features/word-count.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/word-count.md b/docs/features/word-count.md index 9534bc8..5b393dc 100644 --- a/docs/features/word-count.md +++ b/docs/features/word-count.md @@ -29,7 +29,7 @@ ClassicEditor const wordCountPlugin = editor.plugins.get( 'WordCount' ); const wordCountWrapper = document.getElementById( 'word-count' ); - wordCountWrapper.appendChild( wordCounterPlugin.wordCountContainer ); + wordCountWrapper.appendChild( wordCountPlugin.wordCountContainer ); } ) .catch( ... ); ``` From 6510e03f04eabf0300c51f587d39d92ba2c0faec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 1 Jul 2019 10:00:27 +0200 Subject: [PATCH 30/35] Improved text formatting. --- lang/contexts.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/contexts.json b/lang/contexts.json index ff9e3bd..5bf1288 100644 --- a/lang/contexts.json +++ b/lang/contexts.json @@ -1,4 +1,4 @@ { - "Words: %0": "Label showing the number of words in the editor content", - "Characters: %0": "Label showing the number of characters in the editor content" + "Words: %0": "Label showing the number of words in the editor content.", + "Characters: %0": "Label showing the number of characters in the editor content." } From efb7224731f9d84aa16f0c49e3088daca78c3da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 1 Jul 2019 10:08:55 +0200 Subject: [PATCH 31/35] Doc string wording. --- src/utils.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils.js b/src/utils.js index 44f3a50..7904815 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,9 +8,7 @@ */ /** - * Returns plain text representation of an element and it's children. The blocks are separated by a newline (\n ). - * - * **Note:** Function walks through the entire model, which might be very spread. There should be considered throttling it during usage. + * Returns a plain text representation of an element and its children. * * @param {module:engine/model/element~Element} element * @returns {String} Plain text representing model's data From f90e5ef6ed2be0901d41e85623c0c89502f21103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 1 Jul 2019 10:45:08 +0200 Subject: [PATCH 32/35] Tests: Updated property name. --- docs/_snippets/features/word-count.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_snippets/features/word-count.js b/docs/_snippets/features/word-count.js index b0fd93c..6482d79 100644 --- a/docs/_snippets/features/word-count.js +++ b/docs/_snippets/features/word-count.js @@ -32,7 +32,7 @@ ClassicEditor .then( editor => { window.editor = editor; - document.getElementById( 'demo-word-counter' ).appendChild( editor.plugins.get( 'WordCount' ).getWordCountContainer() ); + document.getElementById( 'demo-word-counter' ).appendChild( editor.plugins.get( 'WordCount' ).wordCountContainer ); } ) .catch( err => { console.error( err.stack ); From cfcff7d87e4cd7bb3807be66f2c2da399fab794e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 1 Jul 2019 10:47:49 +0200 Subject: [PATCH 33/35] Added missing dependencies. --- package.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/package.json b/package.json index f4c7e50..339fa02 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,19 @@ ], "dependencies": { "@ckeditor/ckeditor5-core": "^12.1.0", + "@ckeditor/ckeditor5-ui": "^13.0.0", "lodash-es": "^4.17.10" }, "devDependencies": { + "@ckeditor/ckeditor5-basic-styles": "^11.1.1", + "@ckeditor/ckeditor5-block-quote": "^11.1.0", + "@ckeditor/ckeditor5-editor-classic": "^12.1.1", "@ckeditor/ckeditor5-engine": "^13.1.1", + "@ckeditor/ckeditor5-enter": "^11.0.2", + "@ckeditor/ckeditor5-link": "^11.0.2", + "@ckeditor/ckeditor5-list": "^12.0.2", "@ckeditor/ckeditor5-paragraph": "^11.0.2", + "@ckeditor/ckeditor5-table": "^13.0.0", "@ckeditor/ckeditor5-utils": "^12.1.1", "eslint": "^5.5.0", "eslint-config-ckeditor5": "^1.0.11", From 55c2ca41980dd517ecd47fe1ded87bce876117c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 1 Jul 2019 11:23:53 +0200 Subject: [PATCH 34/35] Wording. --- docs/_snippets/features/word-count-update.html | 14 +++++++------- docs/_snippets/features/word-count-update.js | 4 ++-- docs/_snippets/features/word-count.html | 4 ++-- docs/_snippets/features/word-count.js | 2 +- docs/features/word-count.md | 2 +- src/wordcount.js | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/_snippets/features/word-count-update.html b/docs/_snippets/features/word-count-update.html index f221b06..f341e05 100644 --- a/docs/_snippets/features/word-count-update.html +++ b/docs/_snippets/features/word-count-update.html @@ -1,6 +1,6 @@