Oracle APEX - TinyMCE Word & Character count #JoelKallmanDay

Photo by RetroSupply on Unsplash

Oracle APEX - TinyMCE Word & Character count #JoelKallmanDay

Introduction

I could say that I started my blog on October 11, 2021, but that wouldn't be true because my first post dates back to November 2020 (published on another platform). However, I consider that I started blogging on #JoelKallmanDay.

Exactly 1 year ago, I wrote an article on the same subject. Why start all over again, you might ask? Well, the Oracle APEX team decided to change the library used to manage rich text fields. By deprecating CKEditor, it was necessary to be able to migrate customers for whom we had implemented a character/word counter.

Let's see how to do that!

TinyMCE: what is this?

If you haven't heard of it, it's the new library delivered with Oracle APEX 23.1 to replace CKEditor5. It has advantages like a simpler API, but some disadvantages like fewer plug-ins and we still don't have access to all the parameters in the initialization code. But we can still do great things with it!

Enabling the wordcount plugin

To activate the wordcount plug-in, go to the attributes of your page item and fill in the JavaScript initialization function with the code below:

function(options){
    options.editorOptions.plugins += " wordcount";
    if ( Array.isArray( options.editorOptions.toolbar ) ) {
        options.editorOptions.toolbar.push( { name: "wordcount", items: [ "wordcount" ] } );
    } else {
        options.editorOptions.toolbar += " wordcount";
    }
    return options;
}

Wait, what? That's all we have to do? Yes, but the default function is rather limited since it only adds a button to the toolbar, so the user has to click on it to see the number of characters/words they've typed. Here's what it looks like:

This works and displays the number of characters/words typed in the field, but what if I want these numbers to change as you type?

Displaying live numbers

Activating the plugin will give us access to an API for retrieving counters, as explained in the documentation. We will use two functions in this API

//Accessing the wordcount plugin for the active editor
var wordcount = tinymce.activeEditor.plugins.wordcount;
//Display the number of word for the active editor
console.log(wordcount.body.getWordCount());
//Display the number of character for the active editor
console.log(wordcount.body.getCharacterCount());

We now need to trigger an event when someone types into the editor, and to do this we can use the init_instance_callback function

function(options){
    //Enbale the wordcount plugin
    options.editorOptions.plugins += " wordcount";
    //Declare the init instance callback
    options.editorOptions.init_instance_callback = function(editor) {
        // Get the wordcount for the active editor (used to update the counters)
        let formWrapper = tinymce.activeEditor.container.parentElement.parentElement;
        //Function that updates the counters
        function writeWordCount() {
            // Get the wordcount for the active editor
            let wordcount   = tinymce.activeEditor.plugins.wordcount;
            // Write the updated numbers in the counter
            apex.jQuery(formWrapper).parent().find(".tinymce-word-count").text(wordcount.body.getCharacterCount() + " characters | " + wordcount.body.getWordCount() + " words");
        }

        // During initialization we need to insert the wordcount div and write the counter
        apex.jQuery(formWrapper).after('<div class="tinymce-word-count" role="region" aria-label="Word count"></div>');
        writeWordCount();

        // listen to the input event to update the counters
        editor.on("input", apex.util.debounce( writeWordCount, 100 ));
    }
    return options;
}

We then add the following CSS code to style the counters

.tinymce-word-count {
    display: flex;
    justify-content: flex-end;
    padding: 0.5rem;
    border: 1px solid var(--tm-color-toolbar-border,#eee);
    border-bottom-left-radius: var(--tm-border-radius,10px);
    border-bottom-right-radius: var(--tm-border-radius,10px);
    border-top: none;
    line-height: 1rem;
    font-size: 0.6875rem;
}

Run your page and voilà, it just works 😍

Is it possible to make a more fun counter?

Oh yes, it is!!! We're going to display a fun gauge as well as the word count in the editor, let's see it!

First, we need to update the editor's JavaScript initialization function with the code below

function(options){
    //Enbale the wordcount plugin
    options.editorOptions.plugins += " wordcount";
    //Declare the init instance callback
    options.editorOptions.init_instance_callback = function(editor) {
        // Get the wordcount for the active editor (used to update the counters)
        let formWrapper = tinymce.activeEditor.container.parentElement.parentElement;
        //Function that updates the counters
        function writeWordCount() {
            const wordcount = tinymce.activeEditor.plugins.wordcount, //wordcount for the active editor
                  maxChar   = 100; // character limit
            let formWrapper = tinymce.activeEditor.container.parentElement.parentElement, 
                gauge = formWrapper.parentElement.getElementsByClassName("character-gauge")[0],
                wordCounter = formWrapper.parentElement.getElementsByClassName("word-number")[0],
                chars = wordcount.body.getCharacterCount(),
                words = wordcount.body.getWordCount(),
                percentage = Math.round( ( chars / maxChar ) * 100 );
                // We want to display minus
                if (chars > maxChar) {
                    chars = ( chars - maxChar ) * -1;
                }

                // Update the gauge
                gauge.style.setProperty('--char_count', "'" + chars + "'");
                gauge.style.setProperty('--progress', "" + percentage + "%");

                // Update the color
                if (percentage >= 75 && percentage < 100) {
                    gauge.style.setProperty('--gauge-foreground-color', "#ffc36e");
                } else if (percentage >= 100) {
                    gauge.style.setProperty('--gauge-foreground-color', "#ff5151");
                }

            // Write the numbers of words in the counter
            apex.jQuery(wordCounter).text(words + " words in the post");
        }

        // During initialization we need to insert the wordcount div
        apex.jQuery(formWrapper).after('<div class="tinymce-word-count-advanced" role="region" aria-label="Word count"><div class="word-number"></div><div class="character-gauge"></div></div>');
        // Write the initial count after initialization
        writeWordCount();
        // listen to the input event to update the counters
        editor.on("input", apex.util.debounce( writeWordCount, 100 ));
    }
    return options;
}

In this code, we set the character limit to 100, then calculate the percentage based on the current number of characters. We then use this calculation to update a few CSS variables used to style the progression.

Here's the CSS code we need to add to the page

tinymce-word-count-advanced {
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 0.5rem;
    border: 1px solid var(--tm-color-toolbar-border,#eee);
    border-bottom-left-radius: var(--tm-border-radius,10px);
    border-bottom-right-radius: var(--tm-border-radius,10px);
    border-top: none;
    line-height: 1rem;
    font-size: 0.6875rem;
    align-items: center;
}

.character-gauge {
    --char_count: "0";
    --gauge-background-color: #e6e6e6;
    --gauge-foreground-color: #54bdff;
    --progress: 48%;
    justify-self: end;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1rem;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    background: radial-gradient(closest-side, white 79%, transparent 80% 100%),
    conic-gradient(var(--gauge-foreground-color) var(--progress), var(--gauge-background-color) 0);
}

.character-gauge::after{
    content: var(--char_count);
}

Now we can enjoy our wonderful word and character counter!

Conclusion

In this article, we look at how to use the TinyMCE editor wordcount plugin in your Oracle APEX application. It allows you to retrieve the number of words and characters from the active editor and display them as you wish.

I hope you will enjoy reading it and, if you want to test it, you can find all three examples here: https://apex.oracle.com/pls/apex/r/louis/examples/tinymce-word-count