Goal
Create a Touch UI RTE (Rich Text Editor) plugin for entering Structured Content, converted into HTML and added in RTE based on required functionality
With the solution discussed in this post, author can enter content in a form opened from RTE, convert into HTML, add as a tooltip for selected text
For demo purposes, dialog of core text component v2 was modified to add the configuration - /apps/core/wcm/components/text/v2/text/cq:dialog/content/items/tabs/items/properties/items/columns/items/column/items/text/rtePlugins
Demo | Package Install | Github
Plugin Configuration
Add node experience-aem under rtePlugins and set features=*
Add experience-aem#structuredContentModal to uiSettings > cui > dialogFullScreen for showing the ellipsis toolbar icon in full screen
RTE Toolbar Icon
Content Form (for Tooltip) in Full Screen
Tooltip applied to text
Tooltip content in CRX
Solution
1) Login to CRXDE Lite, add nt:folder /apps/eaem-touchui-rte-structured-content
2) To show the tooltip form on plugin icon click, create /apps/eaem-touchui-rte-structured-content/structured-content of type cq:Page (line 19, check the clientlib category eaem.rte.structured.content.plugin)
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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" jcr:primaryType="cq:Page"> <jcr:content jcr:mixinTypes="[sling:VanityPath]" jcr:primaryType="nt:unstructured" jcr:title="Experience AEM Structured Content" sling:resourceType="granite/ui/components/coral/foundation/page"> <head jcr:primaryType="nt:unstructured"> <favicon jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/page/favicon"/> <viewport jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/> <clientlibs jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs" categories="[coralui3,granite.ui.coral.foundation,granite.ui.shell,dam.gui.admin.coral,eaem.rte.structured.content.plugin]"/> </head> <body jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/page/body"> <items jcr:primaryType="nt:unstructured"> <form jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form" class="foundation-form content-container" maximized="{Boolean}true" style="vertical"> <items jcr:primaryType="nt:unstructured"> <wizard jcr:primaryType="nt:unstructured" jcr:title="Add tooltip content..." sling:resourceType="granite/ui/components/coral/foundation/wizard"> <items jcr:primaryType="nt:unstructured"> <tooltip jcr:primaryType="nt:unstructured" jcr:title="Tooltip" sling:resourceType="granite/ui/components/coral/foundation/container" margin="{Boolean}true"> <items jcr:primaryType="nt:unstructured"> <columns jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns" margin="{Boolean}true"> <items jcr:primaryType="nt:unstructured"> <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container"> <items jcr:primaryType="nt:unstructured"> <title jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldDescription="Enter Title" fieldLabel="Title" name="title"/> <description jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textarea" fieldDescription="Enter Description" fieldLabel="Description" name="description"/> </items> </column> </items> </columns> </items> </tooltip> <parentConfig jcr:primaryType="nt:unstructured"> <prev granite:class="foundation-wizard-control" jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" text="Cancel"> <granite:data jcr:primaryType="nt:unstructured" foundation-wizard-control-action="cancel"/> </prev> <next granite:class="foundation-wizard-control" jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/button" disabled="{Boolean}true" text="Create tooltip" type="submit" variant="primary"> <granite:data jcr:primaryType="nt:unstructured" foundation-wizard-control-action="next"/> </next> </parentConfig> </items> </wizard> </items> </form> </items> </body> </jcr:content> </jcr:root>
3) Create clientlib (cq:ClientLibraryFolder) /apps/eaem-touchui-dialog-rte-color-picker/clientlib set property categories to [cq.authoring.dialog.all, eaem.rte.structured.content.plugin] and dependencies to [lodash]
4) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/css.txt, add the following content
rte-structured-content.css
5) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/rte-structured-content.css, add the following code
.eaem-rte-structured-dialog { width: 80%; margin-left: -20%; height: 83%; margin-top: -20%; box-sizing: content-box; z-index: 10100; } .eaem-rte-structured-dialog > iframe { width: 100%; height: 100%; border: 1px solid #888; }
6) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/js.txt, add the following content
rte-structured-content.js
7) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/rte-structured-content.js, add the following code. #25 function getHtmlFromContent() converts the form content into tooltip html and adds it for selected text. Logic in this function can be adjusted accordingly, to create HTML from form content and add in RTE
(function($, CUI, $document){ var GROUP = "experience-aem", STRUCTURED_CONTENT_FEATURE = "structuredContentModal", TCP_DIALOG = "eaemTouchUIStructuredContentModalDialog", CONTENT_IN_DIALOG = "content", REQUESTER = "requester", CANCEL_CSS = "[data-foundation-wizard-control-action='cancel']", MODAL_URL = "/apps/eaem-touchui-rte-structured-content/structured-content.html", $eaemStructuredModal, url = document.location.pathname; if( url.indexOf(MODAL_URL) !== 0 ){ addPluginToDefaultUISettings(); addDialogTemplate(); addPlugin(); }else{ $document.on("foundation-contentloaded", fillDefaultValues); $document.on("click", CANCEL_CSS, sendCancelMessage); $document.submit(sendTextAttributes); } function getHtmlFromContent(selectedText, content){ var tooltipText = content.title + " : " + content.description; return "<span title='" + tooltipText + "' class='eaem-dotted-underline' data-content='" + JSON.stringify(content) + "'>" + selectedText + "</span>"; } function setWidgetValue(form, selector, value){ Coral.commons.ready(form.querySelector(selector), function (field) { field.value = _.isEmpty(value) ? "" : decodeURIComponent(value); }); } function queryParameters() { var 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 sendTextAttributes(){ var message = { sender: GROUP, action: "submit", data: {} }, $form = $("form"), $field; _.each($form.find("[name]"), function(field){ $field = $(field); message.data[$field.attr("name")] = $field.val(); }); parent.postMessage(JSON.stringify(message), "*"); } function fillDefaultValues(){ var queryParams = queryParameters(), form = $("form")[0]; if(_.isEmpty(queryParams[CONTENT_IN_DIALOG])){ return; } var content = JSON.parse(decodeURIComponent(queryParams[CONTENT_IN_DIALOG])); _.each(content, function(value, key){ setWidgetValue(form, "[name='" + key + "']", value); }); } function sendCancelMessage(){ var message = { sender: GROUP, action: "cancel" }; getParent().postMessage(JSON.stringify(message), "*"); } function getParent() { if (window.opener) { return window.opener; } return parent; } function closeDialogModal(event){ event = event.originalEvent || {}; if (_.isEmpty(event.data)) { return; } var message, action; try{ message = JSON.parse(event.data); }catch(err){ return; } if (!message || message.sender !== GROUP) { return; } action = message.action; if(action === "submit"){ var ek = $eaemStructuredModal.eaemModalPlugin.editorKernel, tooltipHtml = getHtmlFromContent(window.getSelection().toString(), message.data); ek.execCmd('inserthtml', tooltipHtml); ek.focus(); } var modal = $eaemStructuredModal.data('modal'); modal.hide(); modal.$element.remove(); } function addPluginToDefaultUISettings(){ var toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.inline.toolbar; toolbar.splice(3, 0, GROUP + "#" + STRUCTURED_CONTENT_FEATURE); toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.fullscreen.toolbar; toolbar.splice(3, 0, GROUP + "#" + STRUCTURED_CONTENT_FEATURE); } function addDialogTemplate(){ var url = MODAL_URL + "?" + REQUESTER + "=" + GROUP; var html = "<iframe width='600px' height='500px' frameBorder='0' src='" + url + "'></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); } function addPlugin(){ var TouchUIStructuredContentModalPlugin = new Class({ toString: "TouchUIStructuredContentModalPlugin", extend: CUI.rte.plugins.Plugin, modalUI: null, getFeatures: function() { return [ STRUCTURED_CONTENT_FEATURE ]; }, initializeUI: function(tbGenerator) { var plg = CUI.rte.plugins; if (!this.isFeatureEnabled(STRUCTURED_CONTENT_FEATURE)) { return; } this.modalUI = tbGenerator.createElement(STRUCTURED_CONTENT_FEATURE, this, false, { title: "Add tooltip" }); tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.modalUI, 10); var groupFeature = GROUP + "#" + STRUCTURED_CONTENT_FEATURE; tbGenerator.registerIcon(groupFeature, "more"); $(window).off('message', closeDialogModal).on('message', closeDialogModal); }, execute: function (id, value, envOptions) { if(!isValidSelection()){ return; } var context = envOptions.editContext, selection = CUI.rte.Selection.createProcessingSelection(context), startNode = selection.startNode; if ( (selection.startOffset === startNode.length) && (startNode != selection.endNode)) { startNode = startNode.nextSibling; } var tag = CUI.rte.Common.getTagInPath(context, startNode, "span"), plugin = this, dialog, content = $(tag).data("content"); this.showDialogModal(getModalIFrameUrl(content)); function isValidSelection(){ var winSel = window.getSelection(); return winSel && winSel.rangeCount == 1 && winSel.getRangeAt(0).toString().length > 0; } function getModalIFrameUrl(content){ var url = MODAL_URL + "?" + REQUESTER + "=" + GROUP; if(_.isObject(content)){ url = url + "&" + CONTENT_IN_DIALOG + "=" + JSON.stringify(content); } return url; } }, showDialogModal: function(url){ var self = this, $iframe = $('<iframe>'), $modal = $('<div>').addClass('eaem-rte-structured-dialog coral-Modal'); $iframe.attr('src', url).appendTo($modal); $modal.appendTo('body').modal({ type: 'default', buttons: [], visible: true }); $eaemStructuredModal = $modal; $eaemStructuredModal.eaemModalPlugin = self; $modal.nextAll(".coral-Modal-backdrop").addClass("cfm-coral2-backdrop"); }, //to mark the icon selected/deselected updateState: function(selDef) { var hasUC = this.editorKernel.queryState(STRUCTURED_CONTENT_FEATURE, selDef); if (this.modalUI != null) { this.modalUI.setSelected(hasUC); } } }); CUI.rte.plugins.PluginRegistry.register(GROUP,TouchUIStructuredContentModalPlugin); } }(jQuery, window.CUI,jQuery(document)));
Thanks for the Plugin. Looks like there is an issue for retaining the values for the tooltip while re-editing the dialog the values aren't displaying and on the node the values are storing but multiple values( meaning whatever we have entered all long got added on to it, it is not removing the previously entered text.)
ReplyDeleteExample:
title="testing : tooltip" class="eaem-dotted-underline">title="qwerty : asdfg" class="eaem-dotted-underline" data-content="{"title":"45678","description":""}">testing
Does this work on core component too?
ReplyDeleteNo.. its not retaining the values, on any edit or modify it stores in jcr as multi values.
ReplyDeleteI based my custom plugin on this one so i had to fix the editing issue. Basically i reworked the handling of the submit action in closeDialogModal:
ReplyDeleteIn case someone still needs it here is the fix:
if(action === "submit"){
var ek = $eaemStructuredModal.eaemModalPlugin.editorKernel,
context = ek.getEditContext(),
selection = CUI.rte.Selection.createProcessingSelection(context),
startNode = selection.startNode;
if (startNode.parentNode.nodeName === "SPAN" && startNode.parentNode.className === "eaem-dotted-underline") {
// editing of existing tooltip in chrome
startNode.parentNode.setAttribute("title", message.data.title);
startNode.parentNode.setAttribute("data-content", JSON.stringify(message.data));
} else {
if (window.getSelection().toString() === startNode.nextSibling.innerText) {
// editing of existing tooltip in firefox
startNode.nextSibling.setAttribute("title", message.data.title);
startNode.nextSibling.setAttribute("data-content", JSON.stringify(message.data));
} else {
// initial setting of tooltip in any browser
var tooltipHtml = getHtmlFromContent(window.getSelection().toString(), message.data);
ek.execCmd('inserthtml', tooltipHtml);
}
}
ek.focus();
}
This works very well. Can't we use jQuery instead lodash ?
ReplyDelete