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.
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.
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:
The project can be split into the following modules:
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.
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.
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:
The project can be split into the following modules:
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.
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.
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:
The live server extension should be set up and ready to use from your VSCode.
Add the barebone codes required to initialize the project. This includes adding a basic HTML template and initializing TinyMCE editor on it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Custom Action Demo For TinyMCE</title>
</head>
<body>
</body>
</html>
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>
head
tag<script src="https://cloud.tinymce.com/stable/tinymce.min.js"></script>
<script>
tinymce.init({
selector: "textarea",
});
</script>
At this point, we should have the TinyMCE initialized on our template.
Add the basic barebone code for the custom plugin that we will be integrating with the editor.
<script>
tinymce.init({
selector: "textarea",
plugins: "synonym",
toolbar: "synonym",
});
</script>
<script src="js/synonym.js"></script>
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 () {},
});
});
<script>
tinymce.init({
selector: "textarea",
plugins: "synonym",
toolbar:
"undo redo | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | indent outdent | synonym",
});
</script>
We should now have our custom plugin visible in the toolbar.
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.
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:
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:
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:
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
The expected result is as follows:
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.
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";
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");
});
});
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.