Table of Contents
Tags
Share
Developing a custom Rich Text Editor widget is a frequently encountered task that may be required for a variety of purposes. For example, on one of our projects we needed to create a custom widget in the Rich Text Editor to support managing footnotes. Initially, footnoted content was coming from another source in a special format and we had a front-end only code to parse and show this structure properly.
Then the authors decided to use both content sources: external from the API and the AEM-managed.
What Stands Behind AEM RTE Plugin?
As an RTE field is widely used by the authors, and it provides such flexibility and freedom to create and manage texts, we decided to have a new widget in the RTE field which can produce the same HTML markup. This allowed our existing Front-end library to function the same as before, no matter whether the content source was from the API or AEM-managed components.
In addition to that, an RTE field has options which can be mixed with the new functionality. For example, to make a footnote bold or italic or to insert a link inside.
AEM RTE Plugins: Initial HTML Markup
Initially the API provided the following html markup — the markup the existing FE js library understands:
<span class="c-footnote-container" data-delim=",">
<fe-footnote class="c-footnote sup">
<span class="footnote-content">Simple Footnote</span>
<sup><span class="footnote__number">*</span></sup>
</fe-footnote>
</span>
AEM RTE Plugin That Seals the Deal
The idea was to add a new icon to the RTE field to manage footnotes:
1. If we put an RTE cursor to any blank space – a popup appears with no data:

2. If we select any text and click this icon — the same popup appears with this text as a Footnote title:

3. If we click on a previously-added footnote — the popup opens with the filled data (the same image as in case no. 2)
4. In the RTE field itself, a footnote should be clearly visible (it has a custom style)

Custom Client Lib: AEM RTE Magic
This is a custom AEM clientlib to support footnotes
/apps/test-com/clientlibs/clientlib-author/rte/js/footnotebuilder.js:
We’ll start by defining constants and adding this new plugin to the UI settings.
(function ($, CUI) {
const BUILDER_URL = "/apps/test-com/components/dialogs/rte/footnote/cq:dialog.html",
GROUP = "footnotebuilder",
FOOTNOTE_BUILDER_FEATURE = "footnoteBuilder",
REQUESTER = "requester",
TCP_DIALOG = "touchUIFootnoteBuilderDialog",
FOOTNOTES_NAME_IN_POPOVER = "footnotes",
DELIM_NAME_IN_POPOVER = "delim",
url = document.location.pathname;
if (url.indexOf(BUILDER_URL) !== 0) {
addPluginToDefaultUISettings();
addDialogTemplate();
}
The functions to add a new plugin to the toolbar and the dialog template:
function addPluginToDefaultUISettings() {
let toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.inline.toolbar;
toolbar.splice(3, 0, GROUP + "#" + FOOTNOTE_BUILDER_FEATURE);
toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.fullscreen.toolbar;
toolbar.splice(3, 0, GROUP + "#" + FOOTNOTE_BUILDER_FEATURE);
}
function addDialogTemplate() {
const pickerUrl = BUILDER_URL + "?" + REQUESTER + "=" + GROUP;
const html = "<iframe width='400px' height='350px' frameBorder='0' src='" + pickerUrl + "'></iframe>";
if (_.isUndefined(CUI.rte.Templates)) {
CUI.rte.Templates = {};
}
if (_.isUndefined(CUI.rte.templates)) {
CUI.rte.templates = {};
}
CUI.rte.templates['dlg-' + TCP_DIALOG] = CUI.rte.Templates['dlg-' + TCP_DIALOG] = Handlebars.compile(html);
}
Footnote builder plugin itself:
- UI initialization — adding an icon to the RTE toolbar
- On-click widget handling — a custom dialog to enter footnote data
- Calling a command to process HTML transformation
const FootnoteBuilderDialog = new Class({
extend: CUI.rte.ui.cui.AbstractDialog,
toString: "FootnoteBuilderDialog",
initialize: function (config) {
this.exec = config.execute;
},
getDataType: function () {
return TCP_DIALOG;
}
});
const TouchUIFootnoteBuilderPlugin = new Class({
toString: "TouchUIFootnoteBuilderPlugin",
extend: CUI.rte.plugins.Plugin,
pickerUI: null,
getFeatures: function () {
return [FOOTNOTE_BUILDER_FEATURE];
},
initializeUI: function (tbGenerator) {
let plg = CUI.rte.plugins;
if (!this.isFeatureEnabled(FOOTNOTE_BUILDER_FEATURE)) {
return;
}
this.pickerUI = tbGenerator.createElement(FOOTNOTE_BUILDER_FEATURE, this, false, {title: "Footnote Builder"});
tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 10);
let groupFeature = GROUP + "#" + FOOTNOTE_BUILDER_FEATURE;
tbGenerator.registerIcon(groupFeature, "note");
},
execute: function (id, value, envOptions) {
const context = envOptions.editContext,
selection = CUI.rte.Selection.createProcessingSelection(context),
ek = this.editorKernel;
let tag = CUI.rte.Common.getTagInPath(context, selection.startNode, "span", {"class": "c-footnote-container"}),
plugin = this,
dialog,
delim = $(tag).attr("data-delim"),
footnotes = (tag !== null) ? $(tag).find("fe-footnote span.footnote-content").map(function () {
return encodeURIComponent($(this).html());
}).get() : [],
dm = ek.getDialogManager(),
$container = CUI.rte.UIUtils.getUIContainer($(context.root)),
propConfig = {
'parameters': {
'command': this.pluginId + '#' + FOOTNOTE_BUILDER_FEATURE
}
},
selectedHTML = getSelectionHTML(CUI.rte.Selection.getSelection(context));
if(selectedHTML && selectedHTML !== "" && tag == null) {
footnotes = [selectedHTML];
}
if (this.eaemFootnoteBuilderDialog) {
dialog = this.eaemFootnoteBuilderDialog;
} else {
dialog = new FootnoteBuilderDialog();
dialog.attach(propConfig, $container, this.editorKernel);
dialog.$dialog.find("iframe").attr("src", getPickerIFrameUrl(delim, footnotes));
this.eaemFootnoteBuilderDialog = dialog;
}
dm.show(dialog);
registerReceiveDataListener(receiveMessage);
function getSelectionHTML(selection) {
let range = selection.getRangeAt(0);
let clonedSelection = range.cloneContents();
let div = document.createElement('div');
div.appendChild(clonedSelection);
return div.innerHTML;
}
function getPickerIFrameUrl(delim, footnotes) {
let pickerUrl = BUILDER_URL + "?" + REQUESTER + "=" + GROUP;
if (!_.isEmpty(delim)) {
pickerUrl = pickerUrl + "&" + DELIM_NAME_IN_POPOVER + "=" + delim;
}
if (!_.isEmpty(footnotes)) {
pickerUrl = pickerUrl + "&" + FOOTNOTES_NAME_IN_POPOVER + "=" + footnotes;
}
return pickerUrl;
}
function removeReceiveDataListener(handler) {
if (window.removeEventListener) {
window.removeEventListener("message", handler);
} else if (window.detachEvent) {
window.detachEvent("onmessage", handler);
}
}
function registerReceiveDataListener(handler) {
if (window.addEventListener) {
window.addEventListener("message", handler, false);
} else if (window.attachEvent) {
window.attachEvent("onmessage", handler);
}
}
function receiveMessage(event) {
if (_.isEmpty(event.data)) {
return;
}
let message = JSON.parse(event.data),
action;
if (!message || message.sender !== GROUP) {
return;
}
action = message.action;
if (action === "submit") {
if (!_.isEmpty(message.data)) {
ek.relayCmd(id, message.data);
}
}
plugin.eaemFootnoteBuilderDialog = null;
dialog.hide();
removeReceiveDataListener(receiveMessage);
}
},
updateState: function (selDef) {
let hasUC = this.editorKernel.queryState(FOOTNOTE_BUILDER_FEATURE, selDef);
if (this.pickerUI != null) {
this.pickerUI.setSelected(hasUC);
}
}
});
CUI.rte.plugins.PluginRegistry.register(GROUP, TouchUIFootnoteBuilderPlugin);
RTE command TouchUIFootnoteBuilderCmd which is being called above: HTML transformation is being performed here:
let TouchUIFootnoteBuilderCmd = new Class({
toString: "TouchUIFootnoteBuilderCmd",
extend: CUI.rte.commands.Command,
isCommand: function (cmdStr) {
return (cmdStr.toLowerCase() === FOOTNOTE_BUILDER_FEATURE);
},
getProcessingOptions: function () {
let cmd = CUI.rte.commands.Command;
return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST;
},
execute: function (execDef) {
const delim = execDef.value ? (execDef.value[DELIM_NAME_IN_POPOVER] || ",") : ",",
footnotes = execDef.value ? execDef.value[FOOTNOTES_NAME_IN_POPOVER] : undefined;
const component = execDef.component;
let parentContainer = CUI.rte.Common.getTagInPath(execDef.editContext, execDef.selection.startNode, "span", {"class": "c-footnote-container"})
if (parentContainer !== null) {
$(this).data('timeout', setTimeout(function () {
CUI.rte.Selection.selectNode(execDef.editContext, parentContainer);
component.relayCmd("Delete");
}, 500));
}
if (_.isEmpty(footnotes)) {
return;
}
let html = this.buildFootnoteHTML(delim, footnotes);
$(this).data('timeout', setTimeout(function () {
component.relayCmd("InsertHTML", html);
}, 500));
},
buildFootnoteHTML: function (delim, footnotes) {
let html = "<span class='c-footnote-container' data-delim='" + delim + "'>";
for (let i = 0; i < footnotes.length; i++) {
html += "<fe-footnote class='c-footnote sup'><span class='footnote-content'>"
+ footnotes[i].replace(/href="([^"]+)"/g, "href='$1' _rte_href='$1'") +
"</span><sup>" + ((i !== 0) ? delim : "") +
"<span class='footnote__number'>*</span></sup></fe-footnote>";
}
return html + "</span>";
}
});
CUI.rte.commands.CommandRegistry.register(FOOTNOTE_BUILDER_FEATURE, TouchUIFootnoteBuilderCmd);
}(jQuery, window.CUI));
And finally, an iframe containing the dialog, its styling, configuring its header and buttons
(function ($, $document) {
const SENDER = "footnotebuilder",
REQUESTER = "requester",
DELIM = "delim",
FOOTNOTES = "footnotes";
if (queryParameters()[REQUESTER] !== SENDER) {
return;
}
$(function () {
_.defer(stylePopoverIframe);
});
function queryParameters() {
let result = {}, param,
params = document.location.search.split(/?|&/);
params.forEach(function (it) {
if (_.isEmpty(it)) {
return;
}
param = it.split("=");
result[param[0]] = param[1];
});
return result;
}
function stylePopoverIframe() {
const $dialog = $("coral-dialog");
const $dialogWrapper = $("coral-dialog div.coral3-Dialog-wrapper");
if (_.isEmpty($dialog) || _.isEmpty($dialogWrapper)) {
return;
}
$dialog.css("overflow", "hidden").css("background-color", "#fff");
$dialogWrapper.css("width", "100%").css("height", "100%")
.css("overflow", "auto").css("-webkit-overflow-scrolling", "touch");
$dialog[0].open = true;
let delim = queryParameters()[DELIM],
$delimField = $document.find("*[name='./delim']"),
footnotes = queryParameters()[FOOTNOTES] ? queryParameters()[FOOTNOTES].split(",") : [],
$footnotesField = $document.find("coral-multifield[data-granite-coral-multifield-name='./footnotes']"),
$addButton = $footnotesField.find("button[coral-multifield-add]");
if (!_.isEmpty(delim)) {
delim = decodeURIComponent(delim);
$delimField[0].value = delim;
}
for (let i = 0; i < footnotes.length; i++) {
$addButton.click();
$(this).data('timeout', setTimeout(function () {
$footnotesField.find("input[name='./footnotes']:eq(" + i + ")")[0].value = decodeURIComponent(footnotes[i]);
}, 500));
}
adjustHeader($dialog);
$(this).data('timeout', setTimeout(function () {
$(document).trigger("foundation-contentloaded");
}, 500));
}
function adjustHeader($dialog) {
const $header = $dialog.css("background-color", "#fff")
.find(".coral3-Dialog-header");
$header.find(".cq-dialog-submit").click(function (event) {
event.preventDefault();
let $rtes = $dialog.find("div [data-cq-richtext-editable][name='./footnotes']"),
valid = true;
_.each($rtes, function (rte) {
let api = $(rte).adaptTo("foundation-validation");
if(!api || !api.checkValidity()) {
valid = false;
}
if(api) {
api.updateUI();
}
});
if (valid) {
sendDataMessage();
}
});
$header.find(".cq-dialog-cancel").click(function (event) {
event.preventDefault();
$dialog.remove();
sendCancelMessage();
});
}
function sendCancelMessage() {
const message = {
sender: SENDER,
action: "cancel"
};
parent.postMessage(JSON.stringify(message), "*");
}
function sendDataMessage() {
let message = {
sender: SENDER,
action: "submit",
data: {}
}, $dialog, delim, footnotes;
$dialog = $(".cq-dialog");
delim = $dialog.find("[name='./" + DELIM + "']").val();
footnotes = $dialog.find("input[name='./" + FOOTNOTES + "']").map(function () {
return $(this).val().replace(/n+$/g, "").replace(/^<p>/g,'').replace(/</p>$/g, '');
}).get();
message.data[DELIM] = delim;
message.data[FOOTNOTES] = footnotes;
parent.postMessage(JSON.stringify(message), "*");
}
})(jQuery, jQuery(document));
The custom css /apps/test-com/clientlibs/clientlib-author/rte/css/rte.css:
span.footnote-content {
vertical-align: super;
}
span.c-footnote-container {
color: blue;
}
coral-multifield.fe-footnotes coral-icon.coral-Form-fielderror {
right: 100px !important;
}
And the popup dialog /apps/test-com/components/dialogs/rte/footnote/cq:dialog.xml:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Footnote"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<delim
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Delim"
name="./delim"/>
<footnotes
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}false"
granite:class="fe-footnotes"
required="{Boolean}true"
fieldLabel="Footnotes">
<field jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/authoring/dialog/richtext"
name="./footnotes"
required="{Boolean}true"
useFixedInlineToolbar="{Boolean}true">
<rtePlugins jcr:primaryType="nt:unstructured">
<subsuperscript jcr:primaryType="nt:unstructured" features="*"/>
</rtePlugins>
<uiSettings jcr:primaryType="nt:unstructured">
<cui jcr:primaryType="nt:unstructured">
<inline jcr:primaryType="nt:unstructured"
toolbar="[format#bold,format#italic,format#underline,links#modifylink,links#unlink,subsuperscript#subscript,subsuperscript#superscript]">
</inline>
</cui>
</uiSettings>
</field>
</footnotes>
</items>
</column>
</items>
</content>
</jcr:root>
How to Use AEM RTE Custom Plugin
The usage of the new widget is very simple. You just add it to an RTE definition:
<rtePlugins jcr:primaryType="nt:unstructured">
…
<footnotebuilder jcr:primaryType="nt:unstructured" features="*"/>
…
</rtePlugins>
<uiSettings jcr:primaryType="nt:unstructured">
<cui jcr:primaryType="nt:unstructured">
<inline jcr:primaryType="nt:unstructured" toolbar="[...,footnotebuilder#footnoteBuilder]">
</inline>
<dialogFullScreen jcr:primaryType="nt:unstructured" toolbar="[...,footnotebuilder#footnoteBuilder]">
</dialogFullScreen>
</cui>
</uiSettings>
The Exadel Team Creates an AEM RTE Custom Plugin
As a result, we now have a new widget inside the RTE field which gives us the ability to create AEM-managed footnotes without changing front-end library code. The widget, as a part of RTE, is easy to use because it extends the basic RTE and is easy for authors to get used to and start working with it. Inside the RTE text field, the footnotes are clearly visible and will not confuse editors. It’s easy to create a new footnote as well as to edit an existing one.
This is pretty much it. The client lib is ready to be used in any component with an RTE field. Stay tuned for more articles from the Exadel Marketing Technology Team!
Author: Exadel Digital Experience Team
Don’t let AI put your business at risk.
Secure your AI with Exadel’s Anchored AI.








