Goal
Touch UI Rich Text Editor (RTE) Plugin to open a Dialog, Select Image and add it in RTE
Dialog for the plugin can be configured with any standard Touch UI Widgets. In this post, we configure a textfield - /libs/granite/ui/components/foundation/form/textfield for entering alt text and path browser - /libs/granite/ui/components/foundation/form/pathbrowser for selecting image
Demo | Package Install
Component Dialog RTE Config
Add the image insert plugin - touchuiinsertimage, available in group experience-aem in component dialog eg. /libs/foundation/components/text/dialog/items/tab1/items/text/rtePlugins (for demonstration only; never modify foundation components)
Plugin Dialog with Path Browser Config
Plugin Dialog
Plugin Dialog with Picker
Plugin Dialog with Image Selected
Image Shown in RTE
Image Source in RTE
Solution
1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-rte-browse-insert-image
2) Add the dialog configuration for RTE Plugin, shown in popover window when user clicks on image plugin icon; create node of type sling:Folder - /apps/touchui-rte-browse-insert-image/popover with the following configuration
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="sling:Folder" jcr:title="Pick an Image" sling:resourceType="cq/gui/components/authoring/dialog"/>
3) Add dialog content /apps/touchui-rte-browse-insert-image/popover/content; #5 attribute eaem-rte-iframe-content marks this dialog RTE specific
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container" eaem-rte-iframe-content=""> <layout jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns" margin="{Boolean}false"/> <items jcr:primaryType="nt:unstructured"> <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container"> <items jcr:primaryType="nt:unstructured"> <alt jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/textfield" fieldLabel="Alt Text" name="./alt"/> <image jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/pathbrowser" fieldLabel="Image" name="./image" rootPath="/content/dam"/> </items> </column> </items> </jcr:root>
4) Dialog in CRXDE Lite
5) Create clientlib (type cq:ClientLibraryFolder) /apps/touchui-rte-browse-insert-image/clientlib, set property categories of String type to rte.coralui2
6) Create file ( type nt:file ) /apps/touchui-rte-browse-insert-image/clientlib/js.txt, add the following
image-insert.js
popover.js
7) Create file ( type nt:file ) /apps/touchui-rte-browse-insert-image/clientlib/image-insert.js, add the following code. This file contains logic for image plugin; receiving selected image from popover iframe showing the dialog and adding it in RTE
(function ($, $document, Handlebars) { var ExperienceAEM = { GROUP: "experience-aem", TIM_FEATURE: "touchuiinsertimage", TIM_DIALOG: "touchuiinsertimagedialog", CONTENT_URL: "/apps/touchui-rte-browse-insert-image/popover.html", EAEM_RTE_IFRAME_CONTENT: "eaem-rte-iframe-content" }; ExperienceAEM.TIM_UI_SETTING = ExperienceAEM.GROUP + "#" + ExperienceAEM.TIM_FEATURE; //extend toolbar builder to register insert image ExperienceAEM.CuiToolbarBuilder = new Class({ toString: "EAEMCuiToolbarBuilder", extend: CUI.rte.ui.cui.CuiToolbarBuilder, _getUISettings: function (options) { var uiSettings = this.superClass._getUISettings(options); //inline toolbar var toolbar = uiSettings["inline"]["toolbar"], feature = ExperienceAEM.TIM_UI_SETTING; //uncomment this to make image insert available for inline toolbar /*if (toolbar.indexOf(feature) == -1) { var index = toolbar.indexOf("fullscreen#start"); toolbar.splice(index, 0, feature); toolbar.splice(index + 1, 0, "-"); }*/ //add image insert to fullscreen toolbar toolbar = uiSettings["fullscreen"]["toolbar"]; if (toolbar.indexOf(feature) == -1) { toolbar.splice(3, 0, feature); } if (!this._getClassesForCommand(feature)) { this.registerAdditionalClasses(feature, "coral-Icon coral-Icon--image"); } return uiSettings; } }); //popover dialog thats hosts iframe ExperienceAEM.InsertImageDialog = new Class({ extend: CUI.rte.ui.cui.AbstractBaseDialog, toString: "EAEMInsertImageDialog", getDataType: function () { return ExperienceAEM.TIM_DIALOG; } }); //extend the CUI dialog manager to register popover dialog ExperienceAEM.DialogManager = new Class({ toString: "EAEMDialogManager", extend: CUI.rte.ui.cui.CuiDialogManager, create: function (dialogId, config) { if (dialogId !== ExperienceAEM.TIM_DIALOG) { return this.superClass.create.call(this, dialogId, config); } var context = this.editorKernel.getEditContext(); var $container = CUI.rte.UIUtils.getUIContainer($(context.root)); var dialog = new ExperienceAEM.InsertImageDialog(); dialog.attach(config, $container, this.editorKernel, true); return dialog; } }); //extend the toolkit implementation for returning custom toolbar builder and dialog manager ExperienceAEM.ToolkitImpl = new Class({ toString: "EAEMToolkitImpl", extend: CUI.rte.ui.cui.ToolkitImpl, createToolbarBuilder: function () { return new ExperienceAEM.CuiToolbarBuilder(); }, createDialogManager: function (editorKernel) { return new ExperienceAEM.DialogManager(editorKernel); } }); CUI.rte.ui.ToolkitRegistry.register("cui", ExperienceAEM.ToolkitImpl); ExperienceAEM.TouchUIInsertImagePlugin = new Class({ toString: "TouchUIInsertImagePlugin", extend: CUI.rte.plugins.Plugin, pickerUI: null, getFeatures: function () { return [ ExperienceAEM.TIM_FEATURE ]; }, initializeUI: function (tbGenerator) { var plg = CUI.rte.plugins; if (this.isFeatureEnabled(ExperienceAEM.TIM_FEATURE)) { this.pickerUI = tbGenerator.createElement(ExperienceAEM.TIM_FEATURE, this, true, "Insert Image"); tbGenerator.addElement(ExperienceAEM.GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 120); } }, execute: function (id) { var ek = this.editorKernel, dm = ek.getDialogManager(); var dialogConfig = { parameters: { "command": ExperienceAEM.TIM_UI_SETTING } }; var dialog = this.dialog = dm.create(ExperienceAEM.TIM_DIALOG, dialogConfig); dm.prepareShow(this.dialog); dm.show(this.dialog); var $popover = this.dialog.$dialog.find(".coral-Popover-content"); loadPopoverUI($popover); function loadPopoverUI($popover) { $popover.parent().css("width", ".1px").height(".1px").css("border", "none"); $popover.css("width", ".1px").height(".1px"); $popover.find("iframe").attr("src", ExperienceAEM.CONTENT_URL); //receive the dialog values from child window registerReceiveDataListener(receiveMessage); } function receiveMessage(event) { if (_.isEmpty(event.data)) { return; } var message = JSON.parse(event.data); if(!message || message.sender != ExperienceAEM.EAEM_RTE_IFRAME_CONTENT){ return; } var action = message.action; if(action == "submit"){ var data = message.data; if(!_.isEmpty(data) && !_.isEmpty(data.imagePath)){ ek.relayCmd(id, message.data); } } dialog.hide(); removeReceiveDataListener(receiveMessage); } 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); } } }, //to mark the icon selected/deselected updateState: function (selDef) { var hasUC = this.editorKernel.queryState(ExperienceAEM.TIM_FEATURE, selDef); if (this.pickerUI != null) { this.pickerUI.setSelected(hasUC); } } }); CUI.rte.plugins.PluginRegistry.register(ExperienceAEM.GROUP, ExperienceAEM.TouchUIInsertImagePlugin); ExperienceAEM.InsertImageCmd = new Class({ toString: "InsertImageCmd", extend: CUI.rte.commands.Command, isCommand: function (cmdStr) { return (cmdStr.toLowerCase() == ExperienceAEM.TIM_FEATURE); }, getProcessingOptions: function () { var cmd = CUI.rte.commands.Command; return cmd.PO_BOOKMARK | cmd.PO_SELECTION; }, execute: function (execDef) { var data = execDef.value, path = data.imagePath, alt = data.altText || "", width = 100, height = 100, imageUrl = CUI.rte.Utils.processUrl(path, CUI.rte.Utils.URL_IMAGE), imgHtml = ""; imgHtml += "<img src=\"" + imageUrl + "\" alt=\"" + alt + "\""; imgHtml += " " + CUI.rte.Common.SRC_ATTRIB + "=\"" + path + "\""; imgHtml += " width=\"" + width + "\""; imgHtml += " height=\"" + height + "\""; imgHtml += ">"; execDef.editContext.doc.execCommand("insertHTML", false, imgHtml); } }); CUI.rte.commands.CommandRegistry.register(ExperienceAEM.GROUP, ExperienceAEM.InsertImageCmd); //returns the picker dialog html //Handlebars doesn't do anything useful here, but the framework expects a template function cpTemplate() { CUI.rte.Templates["dlg-" + ExperienceAEM.TIM_DIALOG] = Handlebars.compile('<div data-rte-dialog="' + ExperienceAEM.TIM_DIALOG + '" class="coral--dark coral-Popover coral-RichText-dialog">' + '<iframe width="1100px" height="700px"></iframe>' + '</div>'); } cpTemplate(); })(jQuery, jQuery(document), Handlebars);
8) Create file ( type nt:file ) /apps/touchui-rte-browse-insert-image/clientlib/popover.js, add the following code. This file contains logic for sending the dialog values like image selected to parent window RTE
(function($, $document){ //dialogs marked with eaem-rte-iframe-content data attribute execute the below logic //to send dialog values to parent window RTE var EAEM_RTE_IFRAME_CONTENT = "eaem-rte-iframe-content", HELP_BUTTON_SEL = ".cq-dialog-help", CANCEL_BUTTON_SEL = ".cq-dialog-cancel", SUBMIT_BUTTON_SEL = ".cq-dialog-submit", ALT_TEXT_NAME = "./alt", IMAGE_NAME = "./image"; $document.on("foundation-contentloaded", stylePopoverIframe); function stylePopoverIframe(){ var $iframeContent = $("[" + 'data-' + EAEM_RTE_IFRAME_CONTENT + "]"); if(_.isEmpty($iframeContent)){ return } var $form = $iframeContent.closest("form"), $cancel = $form.find(CANCEL_BUTTON_SEL), $submit = $form.find(SUBMIT_BUTTON_SEL); $form.css("border", "solid 2px"); $form.find(HELP_BUTTON_SEL).hide(); $document.off("click", CANCEL_BUTTON_SEL); $document.off("click", SUBMIT_BUTTON_SEL); $document.off("submit"); $cancel.click(sendCloseMessage); $submit.click(sendDataMessage); } function sendCloseMessage(){ var message = { sender: EAEM_RTE_IFRAME_CONTENT, action: "close" }; parent.postMessage(JSON.stringify(message), "*"); } function sendDataMessage(){ var message = { sender: EAEM_RTE_IFRAME_CONTENT, action: "submit", data:{ altText: $("[name='" + ALT_TEXT_NAME + "']").val(), imagePath: $("[name='" + IMAGE_NAME + "']").val() } }; parent.postMessage(JSON.stringify(message), "*"); } })(jQuery, jQuery(document));
Good one sreekanth & thanks for sharing
ReplyDeleteThanks Sreekanth
ReplyDeletevery usefull, a feature that is often requested
This comment has been removed by the author.
ReplyDeleteFirst off, thank you very much Sreekanth for this post! This is a feature that our users have wanted since we started on our project. I am currently trying to implement both this and the color picker but I'm having issues with that. I am only able to get this to work when I purposely break the color-picker-plugin.js. If I do that then the image browser icon shows up in the toolbar for the full screen rich text editor. Otherwise it does not show up at all. Were you able to get both of these to work together?
ReplyDeleteBrendan, thanks for reporting; i think because of
DeleteCUI.rte.ui.ToolkitRegistry.register("cui", ExperienceAEM.ToolkitImpl);
the last one loading wins (overrrides previous register); ill work on a fix
Hello Sreekanth,
DeleteJust curious if you've had an opportunity to look at this at all? I did some digging of my own and found the ToolKitRegistry.js file but don't have enough understanding of it to correct this issue.
Brendan,
Deletecheck the following demo (shows configuration too)
https://drive.google.com/file/d/0B4d6KmbLkAumZk5JV0ZZWHBFenc/view?usp=sharing
the fix is not robust enough, not finding enough time... but if you'd like to try it out
https://drive.google.com/file/d/0B4d6KmbLkAumN1VDXzVqRUJ1TVU/view?usp=sharing
Thanks again Sreekanth,
DeleteJust curious if you plan on revisiting this at all? If not could you please be more specific on what specifically is not robust enough about the fix?
Thanks again Sreekanth,
DeleteJust curious if you plan on revisiting this at all? If not could you please be more specific on what specifically is not robust enough about the fix?
Sreekanth,
ReplyDeleteI was able to get the colorpicker and image insert to work together by making the following changes:
- Remove the custom EAEMCuiToolbarBuilder and EAEMDialogManager classes, instead extending CUI.rte.ui.cui.CuiToolbarBuilder and CUI.rte.ui.cui.CuiDialogManager directly - e.g. CUI.rte.ui.cui.CuiDialogManager = new Class({ extend: CUI.rte.ui.cui.CuiDialogManager, ... });
- Remove the EAEMToolkitImpl class and the line `CUI.rte.ui.ToolkitRegistry.register("cui", EAEMToolkitImpl);` as it seems no longer necessary
- Place the colorpicker and image insert plugins into different GROUP names, so that they dont collide when `CUI.rte.plugins.PluginRegistry.register(GROUP, EAEMColorPickerPlugin);` is called.
Looks like I then also needed to change calls of `this.superClass._getUISettings(options)` and `this.superClass.create.call(this, dialogId, config)` to `this.inherited(arguments)`.
DeleteSreekanth,
ReplyDeleteIn Chrome, the inserthtml command is adding garbage styling into the img tag for me, and also adding a span with font size and color around the text after the inserted image.
To work around that, I replaced
execDef.editContext.doc.execCommand("insertHTML", false, imgHtml);
with
var range = execDef.editContext.win.getSelection().getRangeAt(0);
range.deleteContents();
var el = execDef.editContext.doc.createElement("div");
el.innerHTML = imgHtml;
range.insertNode(el.firstChild);
Also, one other bug fix.
ReplyDeleteI added `dialog.restoreSelectionOnHide = false;` because otherwise after adding an image the cursor position would be saved in the old location, and if I tried using any of the other RTE tools it would move my cursor from the current position back to the position of the image.
thanks Brett for the fixes
DeleteBrett / sreekanth,
Delete`dialog.restoreSelectionOnHide = false;` is not working as expected and still cursor is moving from the current position back to the position of the image.
Hi Brett,
DeleteWhere in the code should 'dialog.restoreSelectionOnHide = false;' be added?
This comment has been removed by the author.
DeleteHi everyone,
Deletejust in case someone need an info to fix the cursor position issue, I've added "dialog.restoreSelectionOnHide = false;" just before the return statement of the "create" method of the class ExperienceAEM.DialogManager
I am using this example to build a plugin that can create a span with some text inside. It is adding the span and text fine, but something is wrong with the cursor focus. For instance, after the span is saved in the RTE, if I hit space, it adds the space inside of the span instead of after it. Anyone know how to properly set the selection / context so once you've done insertHtml it places the cursor outside of the element you created?
DeleteHi Sreekanth,
ReplyDeleteI have installed this package and did the node config as per the demo, but I could not see the image button on my RTE touch UI, could please let me know if I miss anything. Thanks in advance.
This comment has been removed by the author.
ReplyDeleteHey Sreekanth, Thank you for the post. i have a question for you , is there a way that i could collect all the image paths and store them at the text node along with the "text" property, say another property containing the image paths?
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteHi, Can anyone share me the popover.html file ,which is being used in the js file ?
ReplyDeleteCONTENT_URL: "/apps/touchui-rte-browse-insert-image/popover.html
Thanks
Hi Febi,
Deletethe content of "popover.html" is generated automatically from the ".content.xml" dialog in the "popover" directory. So in CONTENT_URL you have to specify the path where you have inserted the "popover" folder of the plugin
Hi - we're using AEM 6.2 and I keep getting this error after selecting the image in the "pick an image" dialogue window.
ReplyDelete_is not defined.
It appears to be coming from the popover.js file:
if(_.isEmpty($iframeContent)){
return;
This comment has been removed by the author.
DeleteHi Sreekanth,
ReplyDeleteThanks for your solution which is the requirement of every client in almost all the projects for an RTE. As per the above comments I see there are few fixes made and few issues with 6.2, May I use this now directly on 6.2 version of AEM or any changes to be made for the above files and package you've given ?
Thanks a lot for your work and sharing it with the community.
It does not work on 6.3. Someone who has succeeded can help me
ReplyDeleteDid anybody tried this on 6.3 SP1, if any, please share your findings.
ReplyDeleteHi Sreekanth,
ReplyDeleteThis plugin works well in AEM 6.1 and 6.2 but doesnot seems to work in AEM6.4. I tried placing the categories to rte.coralui3 also, but it didn't worked. Any suggestions?
Hi Sreekanth, I am following your post but not able to do this image customization plugin under RTE in AEM 6.5 for Touch UI. Also I have posted my issue in the AEM forum https://experienceleaguecommunities.adobe.com/t5/adobe-experience-manager/how-to-add-core-image-component-in-rte-customization/m-p/586959#M146566
ReplyDelete