Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Introduce word count plugin #2

Merged
merged 35 commits into from
Jul 1, 2019
Merged

Introduce word count plugin #2

merged 35 commits into from
Jul 1, 2019

Conversation

msamsel
Copy link
Contributor

@msamsel msamsel commented Jun 12, 2019

Suggested merge commit message (convention)

Feature: Introduce word count feature. Closes ckeditor/ckeditor5#2571. Closes ckeditor/ckeditor5#1301.


Additional information

Related PR: ckeditor/ckeditor5#1835

@Reinmar Reinmar requested a review from jodator June 25, 2019 13:55
<div class="customized-counter">
<div class="customized-counter__words">
<label>Words:
<progress value="48" max='100'></progress>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/'/"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/48/42

@msamsel
Copy link
Contributor Author

msamsel commented Jun 25, 2019

I add PR with modification of mgit.json.

src/wordcount.js Outdated
init() {
const editor = this.editor;

editor.model.document.on( 'change', throttle( this._calcWordsAndCharacters.bind( this ), 250 ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could listen to change:data events only?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be a change:data - there's no need to updated word count on selection changes.

Copy link
Contributor

@jodator jodator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plugin works nicely, I had some comments regarding wording and some code improvements.

I've checked how fast is the word counting and it looks pretty OK:
Selection_048

However, I found some issues regarding specs:

  1. There's update event instead of onUpdate callback (for me it's fine). Also, we have observable properties.
  2. I'm not sure if the myContainer is implemented the way it was intended. Should the WordCount plugin accept the existing container to be passed to the plugin and to be used instead of creating it internally? Of course only if the wordCount.container is defined.
  3. The Words: NNN. Characters: NNN requirement is done as to blocks - not as inline elements so it displays:
    Words: NNN.
    Characters: NNN
    

@Reinmar could you check those two requirements above?

src/utils.js Outdated
*
* **Note:** Function walks through the entire model. There should be considered throttling during usage.
*
* @param {module:engine/model/node~Node} node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong param: should be Element - the Node doesn't have `getChildren()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually needs to be Item because it's also called with Text and TextProxy.

But, at the same time, you're completely right that neither of them have getChildren(). Which means that if we'd use a statically typed language we'd need to typecast here or solve it differently. That's why I wrote in the ticket that you reported that we should wait with such polishing until we use TS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks so - I wrote that ticket after this review actually. I worry that we can have more such mistype declaration in the docs for Node/Element/Item trio.

src/utils.js Outdated

if ( node.is( 'text' ) || node.is( 'textProxy' ) ) {
text += node.data;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove the indentation here: return on if and remove the else block. Also you could move let text = '' after if then.

The other question is do you consider TreeWalker here? I don't know if its much overhead (currently parsing a 1M characters (few paragraphs) takes below 30 ms so it is performant enough).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made simple code with treewalker and performance dropped significantly.
In my case it was slow down ~50 times, I'm not sure if it's case of my algorithm with tree walker or tree walker itself, however, it seems that walking through children recursively is far more efficient.

  1. current code:
    Screenshot 2019-06-27 at 11 00 25

  2. simple tree walking:

		const txt = ( editor => {
			const position = new Position( editor.model.document.getRoot(), [ 0 ] );
			const walker = new TreeWalker( { startPosition: position } );

			let endTagIndicator = false;
			let outputText = '';

			for ( const element of walker ) {
				if ( element.typy === 'elementStart' && endTagIndicator ) {
					outputText += '\n';
					endTagIndicator = false;
				} else if ( element.type === 'elementEnd' ) {
					endTagIndicator = true;
				} else if ( element.type === 'text' ) {
					outputText += element.item.data;
				}
			}

			return outputText;
		} )( this.editor );

Screenshot 2019-06-27 at 10 59 20

src/wordcount.js Outdated
init() {
const editor = this.editor;

editor.model.document.on( 'change', throttle( this._calcWordsAndCharacters.bind( this ), 250 ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be a change:data - there's no need to updated word count on selection changes.

src/wordcount.js Outdated
const children = [];

if ( displayWords || displayWords === undefined ) {
const wordsLabel = t( 'Words' ) + ':';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/wordcount.js Outdated
}

if ( displayCharacters || displayCharacters === undefined ) {
const charactersLabel = t( 'Characters' ) + ':';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above ☝️ full message to translate.

src/wordcount.js Outdated
}

/**
* Event is fired after {@link #words} and {@link #characters} are updated.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Event is fired after {@link #words} and {@link #characters} are updated.
* Event fired after {@link #words} and {@link #characters} are updated.

src/wordcount.js Outdated
*/

/**
* This option allows on hiding the word counter. The element obtained through
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This option allows on hiding the word counter. The element obtained through
* This option allows for hiding the word counter. The element obtained through

src/wordcount.js Outdated
* The mentioned configuration will result with the followed container:
*
* <div class="ck ck-word-count">
* <div>Characters: 28</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to update the examples code when adding classes to it.

src/wordcount.js Outdated
*/

/**
* This option allows on hiding the character counter. The element obtained through
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This option allows on hiding the character counter. The element obtained through
* This option allows for hiding the character counter. The element obtained through

src/wordcount.js Outdated
* displayCharacters = false
* }
*
* The mentioned configuration will result with the followed container:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The mentioned configuration will result with the followed container:
* The mentioned configuration will result in the following container

@Reinmar
Copy link
Member

Reinmar commented Jun 26, 2019

  • There's update event instead of onUpdate callback (for me it's fine). Also, we have observable properties.

The point of config.wordCount.onUpdate is that it's easier to define such callback together with the rest of the configuration, rather than somewhere in the then() block after the editor is initialised. Similar to https://ckeditor.com/docs/ckeditor5/latest/api/module_autosave_autosave-AutosaveConfig.html. That doesn't mean that there must be no event, though. The config property can be a shorthand for that event.

@jodator
Copy link
Contributor

jodator commented Jun 26, 2019

For the samples in the docs: I'd change the update event sample so the progress bar has also NNN of 1000 words (or whatever is the number and the character box also has meaningful information. Maybe transition blue > yellow > red would be sufficient?

@Reinmar
Copy link
Member

Reinmar commented Jun 26, 2019

  • I'm not sure if the myContainer is implemented the way it was intended. Should the WordCount plugin accept the existing container to be passed to the plugin and to be used instead of creating it internally? Of course only if the wordCount.container is defined.

As shown in the code sample in https://github.com/ckeditor/ckeditor5-word-count/issues/1 both should work. Either you pass the container via config.wordCount.container and the plugin will use it. Or you don't pass it and the plugin creates it on its own and you can access it via editor.plugins.get( 'WordCount' ).wordCountContainer.

@Reinmar
Copy link
Member

Reinmar commented Jun 26, 2019

3. The Words: NNN. Characters: NNN requirement is done as to blocks - not as inline elements so it displays:
Words: NNN. Characters: NNN

No strong opinion here. The styles can be changed, but it'd be good to default to a nice clean markup by default (I guess a bunch of spans with nice BEM classes). CC @oleq @dkonopka

@jodator
Copy link
Contributor

jodator commented Jun 26, 2019

Thanks! So @msamsel so the points 1-2 should be targeted. For the 3 I've already proposed adding BEM classes to those containers and the only remaining thing is how to structure them (div or span?).

@scofalik
Copy link

Did you check how efficient it is for a document with 20+ A4 pages? 50+?

@dkonopka
Copy link

dkonopka commented Jun 27, 2019

WDYT to provide some default styles for the word counter and introduce them in ckeditor5-theme-lark. Some proposal:

Screenshot 2019-06-27 at 11 31 05

Also, it would be nice to add a specific class to div inside .ck-word-count and for elements inside it (I mean something like .ck-word-count__label & .ck-word-count__value );

@jodator
Copy link
Contributor

jodator commented Jun 27, 2019

Also, it would be nice to add a specific class to div inside .ck-word-count and for elements inside it (I mean something like .ck-word-count__label & .ck-word-count__value );

I've already proposed to have the whole label translatable so it would land in one element. The idea behind that was to translate: Words: %0 instead of only Words. Similarly, as we do it in the panel rotator/carousel where we translate %0 of %1 and not only of.

Copy link
Contributor

@jodator jodator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@msamsel Some polishing to do.

src/wordcount.js Outdated Show resolved Hide resolved
src/wordcount.js Outdated Show resolved Hide resolved
src/wordcount.js Outdated Show resolved Hide resolved
src/wordcount.js Outdated Show resolved Hide resolved
src/wordcount.js Outdated Show resolved Hide resolved
it( 'has "WordCount" plugin name', () => {
expect( WordCount.pluginName ).to.equal( 'WordCount' );
} );

it( 'has define "_config" object', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to check for private properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this test

tests/wordcount.js Outdated Show resolved Hide resolved
tests/utils.js Outdated Show resolved Hide resolved
@jodator
Copy link
Contributor

jodator commented Jun 28, 2019

@Reinmar the issues were addressed :) I've created one follow-up: https://github.com/ckeditor/ckeditor5-word-count/issues/5 - but it might be done later together with #4.

Also the idea of breaking up the words: %n in the view is kinda open but I would leave it as is or do it as a follow-up.

@msamsel msamsel requested a review from jodator June 28, 2019 14:08
lang/contexts.json Outdated Show resolved Hide resolved
lang/contexts.json Outdated Show resolved Hide resolved
src/wordcount.js Outdated
*
* @returns {HTMLElement}
*/
getWordCountContainer() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this be a getter? It was proposed as a property in the spec: https://github.com/ckeditor/ckeditor5-word-count/issues/1.

Co-Authored-By: Piotrek Koszuliński <[email protected]>
@Reinmar Reinmar self-assigned this Jul 1, 2019
docs/features/word-count.md Outdated Show resolved Hide resolved
lang/contexts.json Outdated Show resolved Hide resolved
lang/contexts.json Outdated Show resolved Hide resolved
src/utils.js Outdated Show resolved Hide resolved
* @returns {String} Plain text representing model's data
*/
export function modelElementToPlainText( element ) {
if ( element.is( 'text' ) || element.is( 'textProxy' ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the docs this isn't allowed. The element is an element. Please report a followup ticket to clean this up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My other problem with this is that we already have viewItemToPlainText in ckeditor5-clipboard. Ideally, those functions should be kept together, but I don't know where would it be. Engine?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd go with Engine. Ps. this method would be suitable also for other features (AFAIR: Mention or TextWatcher in particular and maybe the Autoformat - the latter should use TextWatcher API anyway).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However this method might soon be changed to follow requirements from #5.

src/utils.js Outdated Show resolved Hide resolved
src/utils.js Outdated Show resolved Hide resolved
@Reinmar
Copy link
Member

Reinmar commented Jul 1, 2019

You missed some old names. I'll fix this.

image

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Word count plugin Wordcount plugin
6 participants