AEM 6.5.12.0 - Content Fragment Editor Composite Multifield with Visibility Rules and Mixins RTE, String, Number

Goal

Create a Composite Multifield with RTEs for Content Fragments. To top up, the composite mf supports mixins (text boxes, number fields, RTEs etc), by using naming convention based visibility rules. So a multifield item can be of type string and another multifield item can be of type number the next a multistring added in RTE. Also, the extension provides collapsing and expanding of individual multifield items for better UX...

Say a field projectId can be of type string or multi string or number in the multifield... 

              1. User adds the select with name projectId_FieldType and options FIELD_STRINGFIELD_NUMBER, FIELD_MULTI_STRING

              2. The string type widget is named FIELD_STRING_projectId

              3. The number type widget is named FIELD_NUMBER_projectId

              3. The multi string type widget is named FIELD_MULTI_STRING_projectId

Demo | Demo with Date WidgetPackage Install | CF Model | Github | Demo with RTE Expand


Create the Mixin CF Model with Naming Conventions


Mixin CF with RTEs


Mixin CF with RTEs Collapsed



Data in CRX



Solution

1) Create a clientlib /apps/eaem-cf-mf-rte/clientlib-mf-rte with categories=dam.cfm.authoring.contenteditor.v2 and dependencies=[lodash.compat], add below code to support composite multifield and naming convention based visibility. Following code assumes the multifield is named "keyValues". This clientlib provides the composite multifield runtime behavior...

(function ($) {
const URL = document.location.pathname,
CFFW = ".coral-Form-fieldwrapper",
MASTER = "master",
CFM_EDITOR_SEL = ".content-fragment-editor",
CORAL_MULTIFIELD = "coral-multifield",
CORAL_MULTIFIELD_ITEM = "coral-multifield-item",
SUMMARY_FIELD = "[name='key']",
CORAL_MULTIFIELD_ITEM_CONTENT = "coral-multifield-item-content",
EAEM_SUMMARY = "eaem-summary",
FIELD_MULTI_STRING = "FIELD_MULTI_STRING",
KV_MF_SELECTOR = "[data-granite-coral-multifield-name='keyValues']",
RTE_PAGE_URL = "/apps/eaem-cf-mf-rte/rte-page.html",
MF_RTE_NAME = "eaem-mf-rte",
FIELD_TYPE_SELECTOR = "coral-select[name$='FieldType']";
let initialized = false;

if( !isCFEditor() ){
return;
}

init();

function init(){
if(initialized){
return;
}

initialized = true;

window.Dam.CFM.Core.registerReadyHandler(() => {
extendRequestSave();

hideTabHeaders();

addKeyValueMultiFieldListener();

addRTEDataListener();

Dam.CFM.editor.UI.addBeforeApplyHandler( () => {
Dam.CFM.EditSession.notifyActiveSession();
Dam.CFM.EditSession.setDirty(true);
});
});
}

function addRTEDataListener(){
$(window).off('message', receiveMessage).on('message', receiveMessage);

function receiveMessage(event) {
event = event.originalEvent || {};

if (_.isEmpty(event.data)) {
return;
}

let message;

try{
message = JSON.parse(event.data);
}catch(err){
return;
}

$("[" + MF_RTE_NAME + "=" + message.rteName + "]").val(message.content);
}
}

function hideTabHeaders(){
$("coral-tablist").last().hide();
}

function addKeyValueMultiFieldListener(){
const $kvMulti = $(KV_MF_SELECTOR);

createTemplateFromLastParkedTab();

$kvMulti.on("coral-collection:add", function(event){
Coral.commons.ready(event.detail.item, addFieldGrouping);
});

Coral.commons.ready($kvMulti[0], splitKeyValueJSONIntoFields);
}

function splitKeyValueJSONIntoFields(kvMFField){
const kvMFName = $(kvMFField).attr("data-granite-coral-multifield-name");

_.each(kvMFField.items.getAll(), function(item) {
const $content = $(item).find("coral-multifield-item-content");
let jsonData = $content.find("[name=" + kvMFName + "]").val();

if(!jsonData){
return;
}

jsonData = JSON.parse(jsonData);

$content.html(getParkedMFHtml());

Coral.commons.ready($content.find(FIELD_TYPE_SELECTOR)[0], (fieldTypeSelect) => {
fieldTypeSelect.value = jsonData[fieldTypeSelect.name];

doVisibility(fieldTypeSelect, jsonData);
});

fillMultiFieldItem(item, jsonData);
});

addCollapsers();
}

function fillMultiFieldItem(mfItem, jsonData){
_.each(jsonData, function(fValue, fKey){
const field = mfItem.querySelector("[name='" + fKey + "']");

if(field == null){
return;
}

if(field.tagName === 'CORAL-DATEPICKER'){
field.valueAsDate = new Date(fValue);
}else{
field.value = fValue;
}
});
}

function createTemplateFromLastParkedTab(){
const $kvMulti = $(KV_MF_SELECTOR);

$kvMulti.find("template").remove();

let template = '<template coral-multifield-template=""><div>' + getParkedMFHtml() + '</div></template>';

$kvMulti.append(template);
}

function getParkedMFHtml(){
const $parkedMFTab = $("coral-panel").last();
return $parkedMFTab.html() || $parkedMFTab.find("coral-panel-content").html();
}

function getKeyValueData(){
const $kvMulti = $(KV_MF_SELECTOR),
kvMFName = $kvMulti.attr("data-granite-coral-multifield-name");
let kevValueData = [];

_.each($kvMulti[0].items.getAll(), function(item) {
const $fields = $(item.content).find("[name]"),
data = {};

_.each($fields, function(field){
if(canBeSkipped(field)){
return;
}

data[field.getAttribute("name")] = field.value;
});

kevValueData.push(JSON.stringify(data));
});

return { [ kvMFName] : kevValueData} ;
}

function canBeSkipped(field){
return (($(field).attr("type") == "hidden") || $(field).closest(CFFW).is(":hidden") ||!field.value);
}

function addFieldGrouping(mfItem){
Coral.commons.ready($(mfItem).find(FIELD_TYPE_SELECTOR)[0], doVisibility);
}

function doVisibility(fieldTypeSelect, jsonData){
if(!fieldTypeSelect){
return;
}

const widgetItems = fieldTypeSelect.items.getAll();

hideAllButThis(fieldTypeSelect.selectedItem.value);

fieldTypeSelect.on("change", function() {
hideAllButThis(this.value);
});

function hideAllButThis(doNotHide){
_.each(widgetItems, (item) => {
let $widget = $(fieldTypeSelect).closest("coral-multifield-item").find("[name^='" + item.value + "_']");

if(jsonData && jsonData[$widget.attr("name")]){
$widget.val(jsonData[$widget.attr("name")]);
}

const $cffw = $widget.closest(CFFW);
$cffw.css("display", ( doNotHide == item.value ) ? "block" : "none");

if( (doNotHide === FIELD_MULTI_STRING) && ( doNotHide == item.value )){
addRTEContainer($cffw, $widget);
}
})
}
}

function addRTEContainer($cffw, $widget){
$widget.hide();

const rteName = MF_RTE_NAME + "-" + (Math.random() + 1).toString(36).substring(7)

if($widget.attr(MF_RTE_NAME)){
return;
}

$widget.attr(MF_RTE_NAME, rteName);

$cffw.append(getRTEBlock(rteName, $widget.val()));
}

function addCollapsers(){
const $kvMulti = $(KV_MF_SELECTOR).css("padding-right", "2.5rem");

if(_.isEmpty($kvMulti)){
return;
}

$kvMulti.find(CORAL_MULTIFIELD_ITEM).each(handler);

$kvMulti.on('change', function(){
$kvMulti.find(CORAL_MULTIFIELD_ITEM).each(handler);
});

addExpandCollapseAll($kvMulti);

function handler(){
const $mfItem = $(this);

if(!_.isEmpty($mfItem.find("[icon=accordionUp]"))){
return;
}

addAccordionIcons($mfItem);

addSummarySection($mfItem);
}
}

function addAccordionIcons($mfItem){
const up = new Coral.Button().set({
variant: "quiet",
icon: "accordionUp",
title: "Collapse"
});

up.setAttribute('style', 'position:absolute; top: 0; right: -2.175rem');
up.on('click', handler);

$mfItem.append(up);

const down = new Coral.Button().set({
variant: "quiet",
icon: "accordionDown",
title: "Expand"
});

down.setAttribute('style', 'position:absolute; top: 0; right: -2.175rem');
down.on('click', handler).hide();

$mfItem.append(down);

function handler(event){
event.preventDefault();

const mfName = $(this).closest(CORAL_MULTIFIELD).attr("data-granite-coral-multifield-name"),
$mfItem = $(this).closest(CORAL_MULTIFIELD_ITEM),
$summarySection = $mfItem.children("." + EAEM_SUMMARY);

$summarySection.html(getSummary($mfItem, mfName));

adjustUI.call(this, $summarySection);
}

function adjustUI($summarySection){
const icon = $(this).find("coral-icon").attr("icon"),
$content = $mfItem.find(CORAL_MULTIFIELD_ITEM_CONTENT);

if(icon == "accordionUp"){
if($summarySection.css("display") !== "none"){
return;
}

$summarySection.show();

$content.slideToggle( "fast", function() {
$content.hide();
});

up.hide();
down.show();
}else{
if($summarySection.css("display") === "none"){
return;
}

$summarySection.hide();

$content.slideToggle( "fast", function() {
$content.show();
});

up.show();
down.hide();
}
}

function getSummary($mfItem){
const fieldTypeSelect = $mfItem.find(FIELD_TYPE_SELECTOR);
let summary = $mfItem.find("[name^='" + fieldTypeSelect[0].value + "_']").val();

if(!summary){
summary = "Click to expand";
}

return summary;
}
}

function addExpandCollapseAll($kvMulti){
let $mfAdd, expandAll, collapseAll;

$kvMulti.find("[coral-multifield-add]").each(handler);

function handler(){
$mfAdd = $(this);

expandAll = new Coral.Button().set({
variant: 'secondary',
innerText: "Expand All"
});

$(expandAll).css("margin-left", "10px").click((event) => {
event.preventDefault();
$(this).closest(CORAL_MULTIFIELD).find("[icon='accordionDown']").click();
});

collapseAll = new Coral.Button().set({
variant: 'secondary',
innerText: "Collapse All"
});

$(collapseAll).css("margin-left", "10px").click((event) => {
event.preventDefault();
$(this).closest(CORAL_MULTIFIELD).find("[icon='accordionUp']").click();
});

$mfAdd.after(expandAll).after(collapseAll);
}
}

function addSummarySection($mfItem){
const $summarySection = $("<div/>").insertAfter($mfItem.find(CORAL_MULTIFIELD_ITEM_CONTENT))
.addClass("coral-Well " + EAEM_SUMMARY).css("cursor", "pointer").hide();

$summarySection.click(function(){
$mfItem.find("[icon='accordionDown']").click();
});
}

function extendRequestSave(){
const CFM = window.Dam.CFM,
orignFn = CFM.editor.Page.requestSave;

CFM.editor.Page.requestSave = requestSave;

function requestSave(callback, options) {
orignFn.call(this, callback, options);

const kvData = getKeyValueData();

if(_.isEmpty(kvData)){
return;
}

const url = CFM.EditSession.fragment.urlBase + ".cfm.content.json",
variation = getVariation(),
createNewVersion = (options && !!options.newVersion) || false;

let data = {
":type": "multiple",
":newVersion": createNewVersion,
"_charset_": "utf-8"
};

if(variation !== MASTER){
data[":variation"] = variation;
}

const request = {
url: url,
method: "post",
dataType: "json",
data: _.merge(data, kvData),
cache: false
};

CFM.RequestManager.schedule({
request: request,
type: CFM.RequestManager.REQ_BLOCKING,
condition: CFM.RequestManager.COND_EDITSESSION,
ui: (options && options.ui)
})
}
}

function getRTEBlock(rteName, value){
const iframeHTML = "<iframe width='1050px' height='150px' frameBorder='0' " +
"src='" + RTE_PAGE_URL + "?rteName=" + rteName + "&value=" + encodeURIComponent(value)+ "'>" +
"</iframe>";

return "<div>" + iframeHTML + "</div>";
}

function getVariation(){
var variation = $(CFM_EDITOR_SEL).data('variation');

variation = variation || "master";

return variation;
}

function isCFEditor(){
return ((URL.indexOf("/editor.html") == 0)
|| (URL.indexOf("/mnt/overlay/dam/cfm/admin/content/v2/fragment-editor.html") == 0) )
}
}(jQuery));

2) To avoid UI issues, the RTEs in MF are shown in an iframe, create a page /apps/eaem-cf-mf-rte/rte-page with RTE config for showing it in iframe...

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="cq:Page">
<jcr:content
jcr:mixinTypes="[sling:VanityPath]"
jcr:primaryType="nt:unstructured"
jcr:title="RTE"
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, cq.authoring.dialog,cq.authoring.dialog.rte.coralui3, eaem.rte.frame]"/>
</head>
<body
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/page/body">
<items jcr:primaryType="nt:unstructured">
<properties
jcr:primaryType="nt:unstructured"
jcr:title="Properties"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<text
jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/authoring/dialog/richtext"
name="./text"
useFixedInlineToolbar="{Boolean}true">
<rtePlugins jcr:primaryType="nt:unstructured">
<format
jcr:primaryType="nt:unstructured"
features="bold,italic"/>
<justify
jcr:primaryType="nt:unstructured"
features="-"/>
<links
jcr:primaryType="nt:unstructured"
features="modifylink,unlink"/>
<lists
jcr:primaryType="nt:unstructured"
features="*"/>
<misctools jcr:primaryType="nt:unstructured">
<specialCharsConfig jcr:primaryType="nt:unstructured">
<chars jcr:primaryType="nt:unstructured">
<default_copyright
jcr:primaryType="nt:unstructured"
entity="&amp;copy;"
name="copyright"/>
<default_euro
jcr:primaryType="nt:unstructured"
entity="&amp;euro;"
name="euro"/>
<default_registered
jcr:primaryType="nt:unstructured"
entity="&amp;reg;"
name="registered"/>
<default_trademark
jcr:primaryType="nt:unstructured"
entity="&amp;trade;"
name="trademark"/>
</chars>
</specialCharsConfig>
</misctools>
<paraformat
jcr:primaryType="nt:unstructured"
features="*">
<formats jcr:primaryType="nt:unstructured">
<default_p
jcr:primaryType="nt:unstructured"
description="Paragraph"
tag="p"/>
<default_h1
jcr:primaryType="nt:unstructured"
description="Heading 1"
tag="h1"/>
<default_h2
jcr:primaryType="nt:unstructured"
description="Heading 2"
tag="h2"/>
<default_h3
jcr:primaryType="nt:unstructured"
description="Heading 3"
tag="h3"/>
<default_h4
jcr:primaryType="nt:unstructured"
description="Heading 4"
tag="h4"/>
<default_h5
jcr:primaryType="nt:unstructured"
description="Heading 5"
tag="h5"/>
<default_h6
jcr:primaryType="nt:unstructured"
description="Heading 6"
tag="h6"/>
<default_blockquote
jcr:primaryType="nt:unstructured"
description="Quote"
tag="blockquote"/>
<default_pre
jcr:primaryType="nt:unstructured"
description="Preformatted"
tag="pre"/>
</formats>
</paraformat>
<table
jcr:primaryType="nt:unstructured"
features="-">
<hiddenHeaderConfig
jcr:primaryType="nt:unstructured"
hiddenHeaderClassName="cq-wcm-foundation-aria-visuallyhidden"
hiddenHeaderEditingCSS="cq-RichText-hiddenHeader--editing"/>
</table>
<tracklinks
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,#justify,#lists,links#modifylink,links#unlink,#paraformat]">
<popovers jcr:primaryType="nt:unstructured">
<justify
jcr:primaryType="nt:unstructured"
items="[justify#justifyleft,justify#justifycenter,justify#justifyright,justify#justifyjustify]"
ref="justify"/>
<lists
jcr:primaryType="nt:unstructured"
items="[lists#unordered,lists#ordered,lists#outdent,lists#indent]"
ref="lists"/>
<paraformat
jcr:primaryType="nt:unstructured"
items="paraformat:getFormats:paraformat-pulldown"
ref="paraformat"/>
</popovers>
</inline>
<tableEditOptions
jcr:primaryType="nt:unstructured"
toolbar="[table#insertcolumn-before,table#insertcolumn-after,table#removecolumn,-,table#insertrow-before,table#insertrow-after,table#removerow,-,table#mergecells-right,table#mergecells-down,table#mergecells,table#splitcell-horizontal,table#splitcell-vertical,-,table#selectrow,table#selectcolumn,-,table#ensureparagraph,-,table#modifytableandcell,table#removetable,-,undo#undo,undo#redo,-,table#exitTableEditing,-]"/>
</cui>
</uiSettings>
</text>
</items>
</properties>
</items>
</body>
</jcr:content>
</jcr:root>


3) Create a clientlib /apps/eaem-cf-mf-rte/clientlib-rte-frame with categories=eaem.rte.frame and dependencies=[lodash.compat], for posting the RTE content to parent window with the MF items...

(function ($, $document) {
const RTE_CONTAINER_CLASS = ".richtext-container",
DATA_RTE_INSTANCE = "rteinstance",
RTE_VALUE_SEL = "[name='./text']";

$document.one("foundation-contentloaded", initRTE);

function initRTE(){
const queryParams = queryParameters();

$(RTE_CONTAINER_CLASS).css("width", "95%").css("padding-left", "20px");

const INIT_INTERVAL = setInterval(() =>{
const rteInstance = $(".cq-RichText-editable").data(DATA_RTE_INSTANCE);

if(rteInstance && rteInstance.editorKernel){
rteInstance.setContent(decodeURIComponent(queryParams.value));
clearInterval(INIT_INTERVAL);
}
}, 500);

setInterval( () => {
const rteInstance = $(".cq-RichText-editable").data(DATA_RTE_INSTANCE);

let message = {
rteName: queryParams.rteName,
content: rteInstance ? rteInstance.getContent() : $(RTE_VALUE_SEL).val()
};

getParent().postMessage(JSON.stringify(message), "*");
}, 1000);
}

function getParent() {
if (window.opener) {
return window.opener;
}

return parent;
}

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;
}
}(jQuery, jQuery(document)));

2 comments:

  1. Could you please show the solution for a composite multifield with AEM 6.5 . The earlier solution you have given http://experience-aem.blogspot.com/2018/10/aem-6420-assets-content-fragments-coral-3-composite-multifield.html no longer works. Please provide the solution with composite=true way. Thanks.

    ReplyDelete
    Replies
    1. not sure the composite=true MF works in content fragment editor, we did one for cloud services you can try adjusting for 65 https://experience-aem.blogspot.com/2022/07/aem-cloud-service-composite-multifield-content-fragments.html

      Delete