Objective

Build a plugin on top of the TinyMCE rich text editor, to look-up for the synonyms of the typed-in words and insert them at the cursor position in the editor on selection.

Project Context

What is a rich text editor? Check this out first.


TinyMCE editor is widely used while building the admin and content creation consoles for Content Management Systems, where the content needs to support special formatting options. TinyMCE is also very extensible in the sense that one could write their own functionalities as plugins and integrate it with the editor seamlessly.


Let's assume that we have a page in our CMS where the content creators create and edit their contents. Now while writing a content, they would like to use a different word that has the same meaning of a particular word they have in mind. Of course, they could use google in a new tab to find out the synonym, but wouldn't it be easier if they could just do it without leaving the editor? That's what we are essentially going to enable our users to do.


The finished project would look like the following:

1-project-tinymce-synonymns-plugin

Project Stages

2-project-tinymce-synonymns-plugin-steps

High-Level Approach

The project can be split into the following modules:

  • Create the basic website using HTML (templates).
  • Style the website using CSS.
  • Create the proposed custom plugin using JavaScript scripting.
  • Finally for testing the plugin Jasmine Unit tests will be used.

Applications

This plugin can be used in the admin interfaces of blogging sites or any other text content creation web applications that use TinyMCE as the rich text editor, to drastically increase the productivity of the user.

Objective

Build a plugin on top of the TinyMCE rich text editor, to look-up for the synonyms of the typed-in words and insert them at the cursor position in the editor on selection.

Project Context

What is a rich text editor? Check this out first.


TinyMCE editor is widely used while building the admin and content creation consoles for Content Management Systems, where the content needs to support special formatting options. TinyMCE is also very extensible in the sense that one could write their own functionalities as plugins and integrate it with the editor seamlessly.


Let's assume that we have a page in our CMS where the content creators create and edit their contents. Now while writing a content, they would like to use a different word that has the same meaning of a particular word they have in mind. Of course, they could use google in a new tab to find out the synonym, but wouldn't it be easier if they could just do it without leaving the editor? That's what we are essentially going to enable our users to do.


The finished project would look like the following:

1-project-tinymce-synonymns-plugin

Project Stages

2-project-tinymce-synonymns-plugin-steps

High-Level Approach

The project can be split into the following modules:

  • Create the basic website using HTML (templates).
  • Style the website using CSS.
  • Create the proposed custom plugin using JavaScript scripting.
  • Finally for testing the plugin Jasmine Unit tests will be used.

Applications

This plugin can be used in the admin interfaces of blogging sites or any other text content creation web applications that use TinyMCE as the rich text editor, to drastically increase the productivity of the user.

Setup the development environment

Start the project by setting up the environment and tools required to develop the project. Setting up this recommended environment will ease the development process and improve your overall experience of building the project.

Requirements

  • Download and install a code editor preferably the Visual Studio Code editor.

  • Install the Live Server extension for VSCode, so that live preview of the changes made to your web app can be tracked in a browser (in which the app will run) in real time.

  • The file structure of the project can be organized as follows:

    3-project-tinymce-synonymns-plugin-File-structure

    • index.html - The main site's HTML template file to embed the TinyMCE Editor.
    • css - The directory to place all the stylesheets.
    • js - This will contain the Javascript files that contain the functionality code for the plugin.
    • test - This directory will contain all your test suites.

Expected Outcome

The live server extension should be set up and ready to use from your VSCode.

Add starter code files

Add the barebone codes required to initialize the project. This includes adding a basic HTML template and initializing TinyMCE editor on it.

Requirements

  • Create a simple HTML barebone template that would contain our editor.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Custom Action Demo For TinyMCE</title>
  </head>
 
  <body>
  </body>
</html>
 
  • Add a textarea tag within the body which will be used by TinyMCE to create the rich text editor.
<textarea name="demo-editor">
  This editor has a synonym lookup feature!
</textarea>
  • Initialize the TinyMCE rich text editor on the base template that we created in the previous step by adding the following scripts in the head tag
<script src="https://cloud.tinymce.com/stable/tinymce.min.js"></script>
<script>
  tinymce.init({
    selector: "textarea",
  });
</script>
 

Expected Outcome

At this point, we should have the TinyMCE initialized on our template.

4-project-tinymce-synonymns-plugin-init-tinymce

Initialize the code required for the custom plugin

Add the basic barebone code for the custom plugin that we will be integrating with the editor.

Requirements

  • Add the basic code to integrate a custom plugin into the TinyMCE Editor.
<script>
  tinymce.init({
    selector: "textarea",
    plugins: "synonym",
    toolbar: "synonym",
  });
</script>
<script src="js/synonym.js"></script>
 
  • Add the custom plugin starter code into a JS file named synonym.js to get started with the plugin logic.
tinymce.PluginManager.add("synonym", function (editor, url) {
  /*add the custom button to the editor toolbar and customize the action events and UI*/
  editor.addButton("synonym", {
    text: "Lookup Synonyms",
    icon: false,
    onclick: function () {},
  });
});
  • At this point, we should have our custom plugin visible in the toolbar.

5-project-tinymce-synonymns-task-editor-init

  • Add the code to display our custom plugin in the editor's toolbar.
<script>
  tinymce.init({
    selector: "textarea",
    plugins: "synonym",
    toolbar:
      "undo redo | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | indent outdent | synonym",
  });
</script>

Expected Outcome

We should now have our custom plugin visible in the toolbar.

6-project-tinymce-synonymns-plugin-init

Implement the plugin's core functionality

Implement the core functionality of searching for synonyms from within the editor. To improve the user experience, create the plugin's main interface as a popup within the editor. On selecting a word from the displayed results list in the popup, the selected word should be inserted in the editor at the cursor's current position.

Requirements

  • Use the editor.windowManager instance to create a custom popup window which should be displayed on click of our custom plugin in the toolbar.

  • Add a separate CSS file to style the popup and link the stylesheet in the HTML template's head tag.

  • At this point we should have a UI with the behaviour as shown in the image below:

7-project-tinymce-synonymns-popup-ui

  • Use the Datamuse words endpoint to fetch the synonyms of the entered word on submit.

  • The sample code will be as follows:

onsubmit: function (e) {
  e.preventDefault();
  var resultsDOM = document.getElementById("results");
  if (e.data && e.data.searchWord) {
    if (/^[a-zA-Z\s]*$/.test(e.data.searchWord)) {
      editor.fetchSynonyms(e.data.searchWord);
    } else resultsDOM.innerHTML = "Please enter a valid word.";
  } else if (e.data.searchWord.length === 0) {
    if (resultsDOM) {
      resultsDOM.innerHTML = "Please enter a word in the input box.";
    }
  }
}
  • Create a fetchSynonyms method that will make a GET request to the endpoint https://api.datamuse.com//words?ml=" + cleansedSearchTerm where cleansedSearchTerm is the parameter (searchTerm) passed to the fetchSynonyms after doing a basic input sanitization.

  • Use a CSS loader/spinner to improve the UX while the request is in transit.

  • After the API integration, our app should have a similar behaviour as depicted below:

8-project-tinymce-synonymns-api-integration

  • Use the editor.insertContent method to insert the selected word from the list into the editor at the current cursor position.

  • Use the editor.windowManager object's close method to close the popup once the word is inserted into the editor.

  • Use CSS to make the editor occupy full screen width and height.

  • Use the developer tools in your browser to figure out the classes of the elements added by the TinyMCE library to achieve this.

  • The styled editor should have a visual resemblance to the image below:

9-project-tinymce-synonymns-editor-styling

References

Tip

If you are stuck at some point or wondering how a certain thing needs to be done, the first place to turn to is the official documentation. You are most likely to find what you are looking for, there. TinyMCE plugin development documentation

Expected Outcome

The expected result is as follows:

10-project-tinymce-synonymns-functional-plugin

Write the unit test cases for the plugin

Testing your code is crucial. This will help you find critical bugs during the very early stages of the development process and will also give the confidence to refactor and extend the codebase later without breaking the existing functionalities.

Requirements

  • Verify that your code is stable and is working as properly by writing unit tests for your custom plugin.

  • It is essential that you cover edge cases and data mocking.

  • It is recommended that you use Jasmine testing library for this, but feel free to achieve this task with any other javascript testing library of your choice.

  • Sample data mocks:

var cleanData =
  '[{"word":"daybreak","score":106006,"tags":["syn","n","adv","adj"]},{"word":"dawn","score":104382,"tags":["syn","n"]},{"word":"sunrise","score":104345,"tags":["syn","n"]},{"word":"forenoon","score":103266,"tags":["syn","n"]},{"word":"sunup","score":99814,"tags":["syn","n"]},{"word":"good morning","score":95682,"tags":["syn","n"]},{"word":"dawning","score":93876,"tags":["syn","n","v"]},{"word":"aurora","score":93767,"tags":["syn","n"]},{"word":"dayspring","score":93445,"tags":["syn","n"]},{"word":"antemeridian","score":90849,"tags":["syn","adj"]},{"word":"break of day","score":90849,"tags":["syn","n"]},{"word":"break of the day","score":90849,"tags":["syn","n"]},{"word":"cockcrow","score":90849,"tags":["syn","n"]},{"word":"first light","score":90849,"tags":["syn","n"]},{"word":"morning time","score":90849,"tags":["syn","n"]},{"word":"afternoon","score":90848,"tags":["n","adj"]},{"word":"evening","score":85304,"tags":["n","v"]},{"word":"night","score":83064,"tags":["n"]},{"word":"day","score":81771,"tags":["n","adj"]},{"word":"noon","score":79046,"tags":["n"]},{"word":"tomorrow","score":77860,"tags":["n","adv"]},{"word":"today","score":77021,"tags":["n","adj"]},{"word":"hour","score":76476,"tags":["n"]},{"word":"breakfast","score":75101,"tags":["n","adv"]},{"word":"days","score":74454,"tags":["n"]},{"word":"early","score":74313,"tags":["adj","adv","n"]},{"word":"eve","score":71197,"tags":["n"]},{"word":"daylight","score":70670,"tags":["n","adv"]},{"word":"earlier","score":70453,"tags":["adv","adj"]},{"word":"matinee","score":69099,"tags":["n"]},{"word":"quarterbacking","score":67677,"tags":["n"]},{"word":"hello","score":67608,"tags":["n"]},{"word":"bonjour","score":66861,"tags":["n"]},{"word":"cheerio","score":66747,"tags":["n"]},{"word":"bye","score":66722,"tags":["n"]},{"word":"back","score":66691,"tags":["adv"]},{"word":"blow","score":66431,"tags":["n"]},{"word":"hallo","score":66368,"tags":["n"]},{"word":"baltimore","score":66319,"tags":["n","prop"]},{"word":"awakening","score":66248,"tags":["n","v"]},{"word":"howdy","score":65646,"tags":["n"]},{"word":"first","score":64567,"tags":["adj","adv"]},{"word":"wound","score":64334,"tags":["n"]},{"word":"butt","score":64272,"tags":["n"]},{"word":"hey","score":64238,"tags":["n","prop"]},{"word":"matin","score":64219,"tags":["n"]},{"word":"hola","score":63728,"tags":["n"]},{"word":"greetings","score":63607,"tags":["n"]},{"word":"mat","score":63403,"tags":["n"]},{"word":"hiya","score":63380,"tags":["n"]},{"word":"chant","score":63267,"tags":["n"]},{"word":"chen","score":63061,"tags":["n","prop"]},{"word":"sabah","score":62946,"tags":["n","prop"]},{"word":"walrus","score":62707,"tags":["n"]},{"word":"future","score":62487,"tags":["n"]},{"word":"check-in","score":61852,"tags":["n"]},{"word":"kch","score":61852,"tags":["n","prop"]},{"word":"matinée","score":61852,"tags":["n"]},{"word":"mañana","score":61852,"tags":["n"]},{"word":"alba","score":61851,"tags":["n"]},{"word":"rch","score":61851,"tags":["n"]},{"word":"midday","score":61849,"tags":["n"]},{"word":"midmorning","score":61847,"tags":["n"]},{"word":"lunchtime","score":61846,"tags":["n"]},{"word":"yesterday","score":61845,"tags":["n","adv","adj"]},{"word":"midafternoon","score":61843,"tags":["n"]},{"word":"midnight","score":61841,"tags":["n"]},{"word":"monday","score":61839,"tags":["n","adj","prop"]},{"word":"thursday","score":61838,"tags":["n","adj","prop"]},{"word":"friday","score":61837,"tags":["n","adj","prop"]},{"word":"predawn","score":61836,"tags":["n"]},{"word":"weekday","score":61834,"tags":["n","adj"]},{"word":"noontime","score":61831,"tags":["n"]},{"word":"sunday","score":61830,"tags":["n","adj","prop"]},{"word":"wednesday","score":61828,"tags":["n","adj","prop"]},{"word":"tonight","score":61827,"tags":["n","adv"]},{"word":"tuesday","score":61826,"tags":["n","adj","prop"]},{"word":"saturday","score":61823,"tags":["n","adj","prop"]},{"word":"wake","score":61822,"tags":["n","v"]},{"word":"lunch","score":61821,"tags":["n"]},{"word":"nap","score":61820,"tags":["n"]},{"word":"noonday","score":61819,"tags":["n"]},{"word":"week","score":61818,"tags":["n"]},{"word":"amorwe","score":61817,"tags":["n"]},{"word":"weekend","score":61816,"tags":["n"]},{"word":"hours","score":61815,"tags":["n"]},{"word":"yestermorn","score":61814,"tags":["n"]},{"word":"tomorn","score":61813,"tags":["n"]},{"word":"ante meridiem","score":61812,"tags":["n"]},{"word":"overmorrow","score":61811,"tags":["n"]},{"word":"post meridiem","score":61810,"tags":["n"]},{"word":"undermeal","score":61809,"tags":["n"]},{"word":"yesternoon","score":61808,"tags":["n"]},{"word":"month","score":61807,"tags":["n"]},{"word":"noonstead","score":61806,"tags":["n"]},{"word":"nudiustertian","score":61805,"tags":["n"]},{"word":"nychthemeron","score":61804,"tags":["n"]},{"word":"session","score":61802,"tags":["n"]},{"word":"noctidial","score":61801,"tags":["n"]},{"word":"pip emma","score":61800,"tags":["n","prop"]}]';
var faultyData =
  '[word:"daybreak","score":106006,"tags":["syn","n","adv","adj"]}]';
var emptySearch = "";
var invalidTerm = "Aerplane123$%^&";
var validTerm1 = "Ring the bells";
var validTerm2 = "Water";
  • Sample test suite:
describe("Search Term Validity Tests", function () {
  it("Should accept proper single word search", function () {
    synonymPlugin.onsubmit(validTerm2);
    expect(synonymPlugin.canCallFetch).toEqual(true);
  });
  it("Should accept words separated by space", function () {
    synonymPlugin.onsubmit(validTerm1);
    expect(synonymPlugin.canCallFetch).toEqual(true);
  });
  it("Should accept only alphabets and space as search terms", function () {
    synonymPlugin.onsubmit(invalidTerm);
    expect(synonymPlugin.canCallFetch).toEqual(false);
  });
  it("Should handle empty search term scenario", function () {
    expect(function () {
      synonymPlugin.onsubmit(emptySearch);
    }).toThrowError("search text cannot be empty");
  });
});

References

Tip

If you decide to go ahead with Jasmine for testing, you could read the instruction to perform a standalone installation, from the docs at their github repository.

Expected Outcome

  • This will vary depending on the testing library you used for this task. The output should be that you should have mocked all data and should have achieved a decent coverage on your tests.
  • If you used jasmine standalone installation to write your tests, you should see something similar to the following by opening the HTML file contained within the testing directory 11-project-tinymce-synonymns-unit-tests