CKEditor5 mention feature

Introduction

This is the third blog post about how to extend CKEditor5 in your APEX applications. You can read the first part about image upload here and the follow-up here.

I really like how we can now mention someone in apps like Slack or Teams just by typing @. Did you know that it is also possible to have the same functionality in your APEX application? Indeed, the CKEditor5 has a plugin for that: Mention. Let's see how to use it.

Configuration

According to the documentation, there are only three attributes for the configuration of this feature:

  • commitKeys: provide an array of the keys to accept a value. By default, the plugin accept Enter and Tab
  • dropdownLimit: number of value to be displayed. Default is 10 and you can use Infinity if you want to show all
  • feeds: an array of mention feed(s), each of them is an object with the following attributes:
    • feed: an array containing the value to be displayed. It can be a static array or a function returning an array. Each object have at least two attributes:
      • id: unique identifier of the mention. Must start with the marker.
      • text: text to be inserted into the editor
    • itemRenderer: a function to customize the look and feel of an item in the dropdown list
    • marker: the special characters use to signal that the user wants to search within the list for example @ or #
    • minimumCharacters: allows you to control if the list should be displayed immediatly or only after the user type at least 3 characters for example.

Stop the theory and let's try!

First working example

Start by activating the plugin and using the simplest example from the documentation itself to get something working quickly by updating the initialization code with:

function(options){
    // enable the mention plugin    
    options.editorOptions.extraPlugins.push(CKEditor5.mention.Mention);

   // configure the mention plugin to use the list of How I met your mother characters
    options.editorOptions.mention = {
        feeds: [
            {
                marker: '@',
                feed: [ '@Barney', '@Lily', '@Marry Ann', '@Marshall', '@Robin', '@Ted' ]
            }
        ]
    };

    return options;
}

Now when you type something starting by @ you will get a list to select from. Pretty cool right?

How to get my datas?

This first working example uses a static list of values which is an array of all names, but in real applications you will use data from your database. The good thing is that by reading the documentation, we know that the feed can be a javascript function returning a function. First change, update the initialization code:

function(options){
    // enable the mention plugin    
    options.editorOptions.extraPlugins.push(CKEditor5.mention.Mention);

    // configure the mention plugin
    options.editorOptions.mention = {
        feeds: [
            {
                marker: '@',
                feed: getUserItems
            }
        ]
    };

    return options;
}

The flow is now based on a function called getUserItems. It is therefore necessary to declare it in the Function and Global Variable Declaration attribute of the APEX page:

function getUserItems( queryText ) {
    return new Promise( resolve => {
        let ret = apex.server.process(
            "GET_USERS",
            {
                x01:queryText.toLowerCase()
            }
        );
        ret.done( function( data ) {
            let users = [];
            if ( data.matchFound ) {
                users = JSON.parse(data.users);
            }
            resolve(users);
        } );
    } );
}

This function will make an AJAX call to get the list of users based on what the user has entered (queryText). From the documentation, we know that the feed is an array of objects with two attributes id and text. A GET_USERS process must therefore be defined to return this array. Make sure to set the execution point to Ajax Callback for this process:

declare
    l_users clob;
begin
    select 
    json_arrayagg(
       json_object(
           key 'id' value '@' ||first || ' '|| last,
           key 'text' value first || ' '|| last
       )
    returning clob)
    into l_users
    from user_data
    where lower(first) like apex_application.g_x01 ||'%'
    or lower(last) like apex_application.g_x01 ||'%' ;

    apex_json.open_object;
    apex_json.write('success', true);
    apex_json.write('matchFound', (l_users is not null));
    apex_json.write('users', l_users);
    apex_json.close_object;
end;

As you can see, the datas are filtered, according to the user input, on the server side before being provided to the editor.

Customize the dropdown list

We now have an example that uses "real" data, but we can do better by customizing the drop-down list. We need to use the itemRenderer attribute to provide a function that will return an item from the list. To do this, we must first update the GET_USERS process to return more data:

declare
    l_users clob;
begin
    select 
    json_arrayagg(
       json_object(
           key 'id' value '@' ||first || ' '|| last,
           key 'text' value first || ' '|| last,
           key 'userId' value id,
           key 'picture' value large
       )
    returning clob)
    into l_users
    from user_data
    where lower(first) like apex_application.g_x01 ||'%'
    or lower(last) like apex_application.g_x01 ||'%' ;

    apex_json.open_object;
    apex_json.write('success', true);
    apex_json.write('matchFound', (l_users is not null));
    apex_json.write('users', l_users);
    apex_json.close_object;
end;

Next, we need to declare the userItemRenderer function to return the HTML template for each element. The idea here is to display the user's picture next to his full name.

function userItemRenderer( item ) {
    const itemElement = document.createElement( 'div' );
    itemElement.classList.add( 'content-list' );
    itemElement.id = `mention-list-item-id-${ item.userId }`;

    const pictureElement = document.createElement( 'img' );
    pictureElement.classList.add('avatar-list');
    pictureElement.setAttribute("src", item.picture);
    itemElement.appendChild( pictureElement );

    const usernameElement = document.createElement( 'span' );
    usernameElement.classList.add( 'user-name' );
    usernameElement.textContent = item.text;
    itemElement.appendChild( usernameElement );

    return itemElement;
}

After that, update the initialization code to use our new function:

function(options){
    // enable the mention plugin    
    options.editorOptions.extraPlugins.push(CKEditor5.mention.Mention);

    // configure the mention plugin
    options.editorOptions.mention = {
        feeds: [
            {
                marker: '@',
                feed: getUserItems,
                itemRenderer: userItemRenderer
            }
        ]
    };

    return options;
}

And finally, add a few lines of CSS to make the magic happen!

img.avatar-list {
    border-radius: 5px!important; 
    width: 32px!important;
    height: 32px!important;
}

.user-name {
    margin-top: .2rem!important;
    margin-left: .5rem!important;
}

If you have follow all the steps, you should see someting like that:

mention.gif

If you want to test by yourself it's here.

I am really excited to see what next we can do with the other plugins. Stay tuned.