AEM 6 SP2 - Adding Dynamic Request Headers to Replication Requests

Goal


Replication Agent for Publish adds default headers Action, Path, Handle to request. Any custom headers can be added to requests by configuring Extended tab -> HTTP Headers of Publish Agent http://localhost:4502/etc/replication/agents.author/publish.html

This post is on adding dynamic headers to publish replication requests carried out using workflows. A process step introduced into workflow adds timestamp header eaem-unique-key to agent configuration before activation

Demo | Package Install | Source Code


Sample Request with Header eaem-unique-key




Solution


1) Create a Workflow Process OSGI Service apps.experienceaem.replication.SetUniqueKeyReplicationHeader, add the following code

package apps.experienceaem.replication;

import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import java.util.*;

@Component(metatype = false)
@Service
@Property(name = "process.label", value = "Experience AEM Unique Key Replication Header")
public class SetUniqueKeyReplicationHeader implements WorkflowProcess {
    private static final Logger log = LoggerFactory.getLogger(SetUniqueKeyReplicationHeader.class);

    private static String PUBLISH_AGENT_CONFIG = "/etc/replication/agents.author/publish/jcr:content";
    private static String PROTOCOL_HTTP_HEADERS = "protocolHTTPHeaders";
    private static String EAEM_UNIQUE_KEY = "eaem-unique-key: ";

    @Reference
    private ResourceResolverFactory rrFactory;

    public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args)
                            throws WorkflowException {
        try {
            Session session = workflowSession.getSession();

            Map<String, Object> authInfo = new HashMap<String, Object>();
            authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session);

            Resource res = rrFactory.getResourceResolver(authInfo).getResource(PUBLISH_AGENT_CONFIG);

            if(res == null){
                log.warn("Resource - " + PUBLISH_AGENT_CONFIG + ", not available");
                return;
            }

            ValueMap vm = res.adaptTo(ValueMap.class);

            String[] headers = vm.get(PROTOCOL_HTTP_HEADERS, String[].class);

            headers = addUniqueKeyHeader(headers);

            res.adaptTo(Node.class).setProperty(PROTOCOL_HTTP_HEADERS, headers);

            session.save();
        } catch (Exception e) {
            throw new WorkflowException(e);
        }
    }

    private String[] addUniqueKeyHeader(String[] headers){
        if(ArrayUtils.isEmpty(headers)){
            headers = new String[]{
                    "Action: {action}",
                    "Path: {path}",
                    "Handle: {path}",
                    EAEM_UNIQUE_KEY + new Date().getTime()
            };

            return headers;
        }

        for(int i = 0; i < headers.length; i++){
            if(headers[i].startsWith(EAEM_UNIQUE_KEY)){
                headers[i] = EAEM_UNIQUE_KEY + new Date().getTime();
            }
        }

        return headers;
    }
}

2) Add a Process Step in Request for Activation workflow (http://localhost:4502/cf#/etc/workflow/models/request_for_activation.html), configure it with apps.experienceaem.replication.SetUniqueKeyReplicationHeader (donot forget to save workflow)





3) Any activation requested through Request for Activation workflow now adds a eaem-unique-key header to the publish agent configuration (/etc/replication/agents.author/publish/jcr:content) just before activation (there might be synchronization issues when there is heavy replication using other workflows or direct activations )


4) A sample publish request with header eaem-unique-key

(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish Sending POST request to http://localhost:4503/bin/receive?sling:authRequestLogin=1
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish sent. Response: 200 OK
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish ------------------------------------------------
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish Sending message to localhost:4503
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> POST /bin/receive HTTP/1.0
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> Action: Activate
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> Path: /content/geometrixx/en
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> Handle: /content/geometrixx/en
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> eaem-unique-key: 1430403556120
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> Referer: about:blank
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> ...spooling 56653 bytes...
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish --
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << HTTP/1.1 200 OK
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << Date: Thu, 30 Apr 2015 14:19:16 GMT
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << Content-Type: text/plain;charset=UTF-8
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << Content-Length: 30
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << Server: Jetty(8.1.14.v20131031)
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << 
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << ReplicationAction ACTIVATE ok.
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish Message sent.

5) Publish agent settings after the SetUniqueKeyReplicationHeader process step executes. The key eaem-unique-key changes with every activation


AEM 6 SP2 - Touch UI Open Component Dialog Programmatically

Goal


Open a Touch UI component dialog using sample action config - cq:actionConfigs. For the same in Classic UI follow this post

For similar extension on AEM 63 check this post

Demo | Package Install

Thank you Kaushal Mall for the solution - 2 below

A sample toolbar action for opening component dialog




Solution - 1


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-open-component-dialog

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-open-component-dialog/clientlib, set a property categories of String type to cq.authoring.dialog

3) Create file ( type nt:file ) /apps/touchui-open-component-dialog/clientlib/js.txt, add the following

                         open.js

4) Create file ( type nt:file ) /apps/touchui-open-component-dialog/clientlib/open.js, add the following code

(function ($, author) {
    "use strict";

    if (typeof window.ExperienceAEM == "undefined") {
        window.ExperienceAEM = {};
    }

    ExperienceAEM.open = open;

    function open(editable, param, target){
        //Granite.author.store contains editables added on page
        author.DialogFrame.openDialog(editable);
    }
})($, Granite.author);

5) # 11 is for opening the component editable's dialog

6) Add a sample action config cq:actionConfigs, open action in the component's edit config cq:editConfig



7) Sample component's open dialog action config as xml - /apps/touchui-open-component-dialog/touchui-open-component-dialog/cq:editConfig

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root 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:EditConfig">
    <cq:actionConfigs jcr:primaryType="nt:unstructured">
        <open
            jcr:primaryType="nt:unstructured"
            handler="ExperienceAEM.open"
            icon="coral-Icon--game"
            text="Open Dialog"/>
    </cq:actionConfigs>
</jcr:root>

8) #7 ExperienceAEM.open handler (added in step 4) is executed on clicking the open toolbar action


Solution - 2


To show ootb component actions and custom action  - open dialog, check this adobe doc for more details

Package Install


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/touchui-open-component-dialog-register-action

2) Create node /apps/touchui-open-component-dialog-register-action/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.authoring.editor.hook

3) Create file (nt:file) /apps/touchui-open-component-dialog-register-action/clientlib/js.txt and add

                       open.js

4) Create file (nt:file) /apps/touchui-open-component-dialog-register-action/clientlib/open.js and add the following code

(function ($document, author) {
    var openDialog = {
        icon: 'coral-Icon--game',
        text: 'Open Dialog',
        handler: function (editable, param, target) {
            author.DialogFrame.openDialog(editable);
        },
        condition: function (editable) {
            return editable.type === "touchui-open-component-dialog-register-action/touchui-open-component-dialog";
        },
        isNonMulti: true
    };

    $document.on('cq-layer-activated', function (ev) {
        if (ev.layer === 'Edit') {
            author.EditorFrame.editableToolbar.registerAction('EAEM_OPEN_DIALOG', openDialog);
        }
    });
})($(document), Granite.author);



AEM 6 SP2 - Classic UI Show Html5 Smart Image Mouse Coordinates

Goal


Show the (X,Y) coordinates of Mouse on Html5 Smart Image (CQ.html5.form.SmartImage) added in Image Component. Mousemove on the image to add coordinates in a textfield added to map tool. Note, this is plain vanilla, may not work as expected if image is processed using crop, zoom, rotate etc.

Demo | Package Install




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classic-ui-smart-image-coordinates

2) Create clientlib (type cq:ClientLibraryFolder/apps/classic-ui-smart-image-coordinates/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classic-ui-smart-image-coordinates/clientlib/js.txt, add the following

                         coords.js

4) Create file ( type nt:file ) /apps/classic-ui-smart-image-coordinates/clientlib/coords.js, add the following code

(function(){
    if (typeof window.ExperienceAEM == "undefined") {
        window.ExperienceAEM = {};
    }

    ExperienceAEM.showMouseCoordinates = function(image){
        if(!image || !image.imagePanel || !image.imageToolDefs){
            return;
        }

        var imgTools = image.imageToolDefs,
            mapTool, imageOffsets = image.imagePanel.imageOffsets;

        for(var x = 0; x < imgTools.length; x++){
            if(imgTools[x].toolId == 'smartimageMap'){
                mapTool = imgTools[x];
                break;
            }
        }

        var mapCoords = mapTool.userInterface.findBy(function(comp){
            return comp["itemId"] == "areaDefCoords";
        })[0];

        var coords = new CQ.Ext.form.TextField({
            fieldLabel: "Mouse"
        });

        mapCoords.ownerCt.add(coords);
        mapCoords.ownerCt.doLayout();

        var $img = $(image.imagePanel.el.dom).find("img");

        $img.mousemove(function(event) {
            var offset = $(this).offset(),
                relX = (event.pageX - offset.left),
                relY = (event.pageY - offset.top);

            relX = relX - imageOffsets.x;
            relY = relY - imageOffsets.y;

            coords.setValue("(" + relX + "/" + relY + ")");
        });
    }
}());

5) To test the above logic, add a listener on image. In the demo loadimage listener was added on foundation image component widget /libs/foundation/components/image/dialog/items/image, which is bad; Ideally the foundation components should never be modified; did it for demonstration purposes only

6) Create node listeners (nt:unstructured) /libs/foundation/components/image/dialog/items/image/listeners, add a property loadimage with the following value (in other words, add this listener on html5 smart images needing the image mouse coordinates functionality)

function(image) { 
      ExperienceAEM.showMouseCoordinates(image) ;
}







AEM 6 SP2 - Date Time Auto Advancer

Goal


Create a Date Time Auto Advancer extending ootb Absolute Time Auto Advancer to provide the times of day or exact date at which the participant step using this handler should time out.

Demo | Package Install | Source Code


The following screen grab shows Request for Activation workflow's Waiting for activation step using apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer





Selecting Date Time for Timeout shows a dialog with multifields for setting the Times in a day and exact Date & Times




In the above example, for workflow Request for Activation, Waiting for Activation step times out at the earliest time available after step Approve Content is completed. So lets say Approve Content was completed for an asset at 6:30 PM, the handler times out  at 10:30 PM and flow moves to next step Activate Content; asset gets activated at may be 10:31 PM, depending on how big the replication queue is....


Timeout values added in multifield, set in CRX





A sample sling job scheduled for Waiting for Activation




Solution


1) Create a OSGI service apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer extending com.day.cq.workflow.timeout.autoadvance.AbsoluteTimeAutoAdvancer. This is the handler selected for handling date time timeouts. If no time or date is selected for timeout, handler falls back to its super AbsoluteTimeAutoAdvancer for handling timeouts

package apps.experienceaem.autoadvancer.datetime;

import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.job.AbsoluteTimeoutHandler;
import com.day.cq.workflow.timeout.autoadvance.AbsoluteTimeAutoAdvancer;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.SimpleDateFormat;
import java.util.*;

@Component(metatype = false)
@Service(value={WorkflowProcess.class, AbsoluteTimeoutHandler.class})
@Properties({
        @Property(name="service.description", value="Experience AEM Date Time Auto Advancer Process"),
        @Property(name="process.label", value = "Experience AEM Date Time Auto Advancer")
})
public class DateTimeAutoAdvancer extends AbsoluteTimeAutoAdvancer {
    protected final Logger log = LoggerFactory.getLogger(DateTimeAutoAdvancer.class);

    private static SimpleDateFormat JS_JAVA_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
    private static String DATE_TIMES_PROP = "eaemTimeoutDateTimes";

    static {
        JS_JAVA_FORMATTER.setTimeZone(TimeZone.getTimeZone("GMT"));
    }

    private void addTimes(Date currentDate, JSONArray times, List<Long> dtNumbers) throws Exception{
        if( (times == null) || (times.length() == 0)){
            return;
        }

        Calendar cal;

        String time[], hhmm[], ampm;
        int hour, min, nextDayHour = -1, nextDayMin = -1;

        for(int i = 0, len = times.length(); i < len; i++){
            time = ((String)times.get(i)).split(" ");

            hhmm = time[0].split(":");
            ampm = time[1];

            cal = Calendar.getInstance();

            hour = NumberUtils.createInteger(hhmm[0]);
            hour = (ampm.equalsIgnoreCase("AM")) ? hour : hour + 12;
            min = NumberUtils.createInteger(hhmm[1]);

            cal.set(Calendar.HOUR_OF_DAY, hour);
            cal.set(Calendar.MINUTE, min);

            //get the earliest time of next day, if a page/asset was approved after the timeout times set
            //in a day, are passed
            if(i == 0){
                nextDayHour = hour;
                nextDayMin = min;
            }else{
                if(hour < nextDayHour){
                    nextDayHour = hour;
                    nextDayMin = min;
                }else if(hour == nextDayHour){
                    if( min < nextDayMin ){
                        nextDayMin = min;
                    }
                }
            }

            //skip past dates
            if(currentDate.getTime() > cal.getTimeInMillis()){
                continue;
            }

            dtNumbers.add(cal.getTimeInMillis());
        }

        if(dtNumbers.isEmpty()){
            cal = Calendar.getInstance();

            cal.add(Calendar.DATE, 1);
            cal.set(Calendar.HOUR_OF_DAY, nextDayHour);
            cal.set(Calendar.MINUTE, nextDayMin);

            dtNumbers.add(cal.getTimeInMillis());
        }
    }

    private void addDateTimes(Date currentDate, JSONArray dateTimes, List<Long> dtNumbers)
                                    throws Exception{
        if( (dateTimes == null) || (dateTimes.length() == 0)){
            return;
        }

        String dateStr = null; Date date = null;

        for(int i = 0, len = dateTimes.length(); i < len; i++){
            dateStr = (String)dateTimes.get(i);

            date = JS_JAVA_FORMATTER.parse(dateStr);

            //skip past dates
            if(currentDate.getTime() > date.getTime()){
                continue;
            }

            dtNumbers.add(date.getTime());
        }
    }

    public long getTimeoutDate(WorkItem workItem) {
        List<Long> dtNumbers = new ArrayList<Long>();
        Date currentDate = new Date();

        try{
            String dateTimesVal = workItem.getNode().getMetaDataMap().get(DATE_TIMES_PROP, String.class);

            if(StringUtils.isEmpty(dateTimesVal)){
                return super.getTimeoutDate(workItem);
            }

            JSONObject dt = new JSONObject(dateTimesVal);

            JSONArray times = (JSONArray)dt.get("times");
            JSONArray dateTimes = (JSONArray)dt.get("datetimes");

            addTimes(currentDate, times, dtNumbers);
            addDateTimes(currentDate, dateTimes, dtNumbers);

            Collections.sort(dtNumbers);
        }catch(Exception e){
            log.error("Could not calculate timeout", e);
        }

        // get the most recent date&time in future, at which the auto advancer should timeout
        return dtNumbers.isEmpty() ? super.getTimeoutDate(workItem) : dtNumbers.get(0);
    }
}


2) If any time of day or exact date, is in the past, handler simply ignores it. Handler adds all the next timeout dates in a collection, sorts them and gets the lowest long which is the most recent time in future the handler should time out

3) #62, #86, if times are set in dialog, but time of day is already in the past (say user has approved page/asset very late in the night) and no more timeouts are available for the day, handler tries to find earliest time of next day, it should timeout

4) Install the OSGI service Experience AEM Date Time Auto Advancer in CRX folder /apps/classicui-auto-advancer-date-time-selector

5) Login to CRXDE Lite, create clientlib (type cq:ClientLibraryFolder/apps/classicui-auto-advancer-date-time-selector/clientlib and set a property categories of String type to cq.widgets and dependencies type String[] to underscore

6) Create file ( type nt:file ) /apps/classicui-auto-advancer-date-time-selector/clientlib/js.txt, add the following

                         add-date-time.js

7) Create file ( type nt:file ) /apps/classicui-auto-advancer-date-time-selector/clientlib/add-date-time.js, add the following code

(function(){
    var DATE_TIME_AA_CLASS = "apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer";
    var DATE_TIME_SEL_VALUE = -1;
    var DATE_TIMES_PROP = "./metaData/eaemTimeoutDateTimes";

    var pathName = window.location.pathname;

    if( pathName.indexOf("/etc/workflow") != 0 ){
        return;
    }

    function getDialog(dateTimesHidden){
        var datetimes = new  CQ.form.MultiField({
            border: false,
            fieldLabel: "Date and Time",
            fieldConfig: {
                "xtype": "datetime"
            }
        });

        var times = new CQ.form.MultiField({
            border: false,
            fieldLabel: "Time",
            fieldConfig: {
                "xtype": "timefield"
            }
        });

        var text = "Time: The times in a day, handler should timeout - Date and Time: The exact date&time, handler should timeout" +
                   "Example: Workflow - Request for Activation, Handler Step: Waiting for Activation, Previous Step - Approve Content" +
                   "Scenario 1: 'Time' is set to '4:00 PM', '5:00 PM', '6:00 PM' and 'Approve Content' step is completed at '4:30 PM', " +
                   "'Waiting for Activation' step is guaranteed to timeout at '5:00 PM'" +
                   "Scenario 2: 'Time' is set to '4:00 PM', '5:00 PM', '6:00 PM' and " +
                   "'Date and Time' is set to 'May 04, 2015, 5:10 PM', 'June 13, 2015, 4:30 PM'; " +
                   "If 'Approve Content' step completes on 'May 04, 2015, 4:20 PM', the 'Waiting for Activation' timesout at " +
                   "'5:00 PM' and not '5:10 PM'. 'Approve Content' step completed on 'June 13, 2015, 4:20 PM' will timeout " +
                   "'Waiting for Activation' at '4:30 PM'";

        var config = {
            "jcr:primaryType": "cq:Dialog",
            width: 600,
            height: 400,
            title: "Date Time",
            items: {
                "jcr:primaryType": "cq:Panel",
                bodyStyle: "padding: 10px",
                html: text,
                items: {
                    "jcr:primaryType": "cq:WidgetCollection",
                    times: times,
                    datetimes: datetimes
                }
            },
            ok: function(){
                var value = {
                    times: times.getValue(),
                    datetimes: datetimes.getValue()
                };

                dateTimesHidden.setValue(JSON.stringify(value));

                this.close();
            },
            cancel: function(){
                this.close();
            }
        };

        var dateTimeValues = dateTimesHidden.getValue();

        if(!_.isEmpty(dateTimeValues)){
            dateTimeValues = JSON.parse(dateTimeValues);

            datetimes.setValue(dateTimeValues.datetimes);
            times.setValue(dateTimeValues.times);
        }

        return CQ.WCM.getDialog(config);
    }

    function initTimeoutSelection(dialog, timeoutType){
        var dateTimeOption = {
            text: "Date Time",
            value: DATE_TIME_SEL_VALUE
        };

        var dateTimesHidden = dialog.addHidden({ "./metaData/eaemTimeoutDateTimes" : ""})[DATE_TIMES_PROP];

        $.getJSON(dialog.form.url + ".infinity.json").done(function(data){
            if(_.isEmpty(data.metaData) || _.isEmpty(data.metaData.eaemTimeoutDateTimes)){
                return;
            }
            dateTimesHidden.setValue(data.metaData.eaemTimeoutDateTimes);
        });

        timeoutType.options.push(dateTimeOption);

        timeoutType.setOptions(timeoutType.options);

        timeoutType.reset();

        return dateTimesHidden;
    }

    function handleTimeoutChange(timesDialog, handlerType, dateTimesHidden, timeoutValue){
        if(timeoutValue != DATE_TIME_SEL_VALUE){
            return timesDialog;
        }

        if(handlerType.getValue() != DATE_TIME_AA_CLASS){
            CQ.Ext.Msg.alert("Invalid", "Handler selected cannot handle datetime");

            this.setValue("Off");

            return timesDialog;
        }

        //to handle the change event fired by combo in timeout selection
        if(timesDialog && timesDialog.isVisible()){
            return timesDialog;
        }

        timesDialog = getDialog(dateTimesHidden);

        timesDialog.show();

        return timesDialog;
    }

    function handleTimeoutHandlerChange(timeoutType, handlerValue){
        if(handlerValue != DATE_TIME_AA_CLASS){
            timeoutType.setValue("Off");
        }
    }

    function registerDateTime(dialog, handlerType, timeoutType){
        if(dialog.eaemListenersAdded){
            return;
        }

        dialog.eaemListenersAdded = true;

        var dateTimesHidden = initTimeoutSelection(dialog, timeoutType);

        var timesDialog;

        handlerType.on("selectionchanged", function(t, value){
            handleTimeoutHandlerChange(timeoutType, value);
        });

        timeoutType.on("selectionchanged", function(t, value){
            timesDialog = handleTimeoutChange.call(this, timesDialog, handlerType, dateTimesHidden, value);
        });
    }

    function findTimeoutHandler(editable){
        function handler(){
            var dialog = editable.dialogs[CQ.wcm.EditBase.EDIT];

            var selTypes = dialog.findByType("selection");

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

            var handlerType, timeoutType;

            _.each(selTypes, function(selType){
                if(selType.name == "./metaData/timeoutHandler"){
                    handlerType = selType;
                }else if(selType.name == "./metaData/timeoutMillis"){
                    timeoutType = selType;
                }
            });

            //wait until the dialog gets opened by ootb handlers & initialized
            if(!handlerType || !timeoutType){
                return;
            }

            clearInterval(INTERVAL);

            registerDateTime(dialog, handlerType, timeoutType);
        }

        var INTERVAL = setInterval(handler, 250);
    }

    var INTERVAL = setInterval(function(){
        var editables = CQ.WCM.getEditables();

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

        clearInterval(INTERVAL);

        _.each(editables, function(editable){
            editable.el.on('dblclick', function(e){
                findTimeoutHandler(editable);
            }, this);
        });
    }, 250);
})();


8) The JS extension waits until Handler and Timeout select widgets are available, adds the Date Time option and multifields dialog




AEM 6 SP2 - TouchUI Adding Dynamic Select Options

Goal


Add two Granite Select widgets - /libs/granite/ui/components/foundation/form/select with second select being dynamic. In this example, selecting language fills the country select widget with available countries for that language in CRX /libs/wcm/core/resources/languages

Demo | Package Install




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-fill-second-select

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-fill-second-select/clientlib, set a property categories of String type to cq.authoring.dialog and dependencies of type String[] to underscore

3) Create file ( type nt:file ) /apps/touchui-fill-second-select/clientlib/js.txt, add the following

                         listener.js

4) Create file ( type nt:file ) /apps/touchui-fill-second-select/clientlib/listener.js, add the following code

(function ($, $document) {
    "use strict";

    var LANGUAGE = "./language", COUNTRY = "./country";

    function adjustLayoutHeight(){
        //with only two selects, the second select drop down is not visible when expanded, so adjust the layout height
        //fixedcolumns i guess doesn't support css property height, so fallback to jquery
        //http://docs.adobe.com/docs/en/aem/6-0/develop/ref/granite-ui/api/jcr_root/libs/granite/ui/components/foundation/layouts/fixedcolumns/index.html
        $(".coral-FixedColumn-column").css("height", "20rem");
    }

    $document.on("dialog-ready", function() {
        adjustLayoutHeight();

        //http://docs.adobe.com/docs/en/aem/6-0/develop/ref/granite-ui/api/jcr_root/libs/granite/ui/components/foundation/form/select/index.html
        var language = new CUI.Select({
            element: $("[name='" + LANGUAGE +"']").closest(".coral-Select")
        });

        var country = new CUI.Select({
            element: $("[name='" + COUNTRY +"']").closest(".coral-Select")
        });

        if(_.isEmpty(country) || _.isEmpty(language)){
            return;
        }

        var langCountries = {};

        //workaround to remove the options getting added twice, using CUI.Select()
        language._selectList.children().not("[role='option']").remove();

        function fillCountries(selectedLang, selectedCountry){
            country._select.children().remove();
            country._selectList.children().remove();

            _.each(langCountries, function(value, lang){
                if( (lang.indexOf(selectedLang) !== 0) || (value.country == "*") ){
                    return;
                }

                $("<option>").appendTo(country._select)
                                .val(lang).html(value.country);
            });

            country = new CUI.Select({
                element: $("[name='" + COUNTRY +"']").closest(".coral-Select")
            });

            if(!_.isEmpty(selectedCountry)){
                country._select.val(selectedCountry).trigger('change');
            }
        }

        //listener on language select for dynamically filling the countries on language select
        language._selectList.on('selected.select', function(event){
            fillCountries(event.selectedValue);
        });

        //get the langs list
        $.getJSON("/libs/wcm/core/resources/languages.2.json").done(function(data){
            langCountries = data;

            var $form = country.$element.closest("form");

            //get the second select box (country) saved value
            $.getJSON($form.attr("action") + ".json").done(function(data){
                if(_.isEmpty(data)){
                    return;
                }

                fillCountries(language.getValue(), data.country);
            })
        });
    });
})($, $(document));

5) Create a simple datasource for languages - /apps/touchui-fill-second-select/datasource/language/language.jsp


<%@include file="/libs/granite/ui/global.jsp"%>

<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.sling.api.wrappers.ValueMapDecorator" %>
<%@ page import="com.adobe.granite.ui.components.ds.SimpleDataSource" %>
<%@ page import="org.apache.commons.collections.iterators.TransformIterator" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.LinkedHashMap" %>
<%@ page import="org.apache.commons.collections.Transformer" %>
<%@ page import="org.apache.sling.api.resource.*" %>

<%
    final Map<String, String> languages = new LinkedHashMap<String, String>();

    languages.put("ar", "Arabic");
    languages.put("en", "English");
    languages.put("de", "German");

    final ResourceResolver resolver = resourceResolver;

    DataSource ds = new SimpleDataSource(new TransformIterator(languages.keySet().iterator(), new Transformer() {
        public Object transform(Object o) {
            String language = (String) o;
            ValueMap vm = new ValueMapDecorator(new HashMap<String, Object>());

            vm.put("value", language);
            vm.put("text", languages.get(language));

            return new ValueMapResource(resolver, new ResourceMetadata(), "nt:unstructured", vm);
        }
    }));

    request.setAttribute(DataSource.class.getName(), ds);
%>

7) The Touch UI dialog xml - /apps/touchui-fill-second-select/fill-second-select/cq:dialog

<?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"
    jcr:primaryType="nt:unstructured"
    jcr:title="Select TouchUI Component"
    sling:resourceType="cq/gui/components/authoring/dialog"
    helpPath="en/cq/current/wcm/default_components.html#Text">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
        <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
        <items jcr:primaryType="nt:unstructured">
            <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/foundation/container">
                <items jcr:primaryType="nt:unstructured">
                    <fieldset
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Sample Select"
                        sling:resourceType="granite/ui/components/foundation/form/fieldset">
                        <layout
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                        <items jcr:primaryType="nt:unstructured">
                            <column
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/foundation/container">
                                <items jcr:primaryType="nt:unstructured">
                                    <language
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/select"
                                        fieldLabel="Language"
                                        name="./language">
                                        <datasource
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="/apps/touchui-fill-second-select/datasource/language"
                                            addNone="{Boolean}true"/>
                                    </language>
                                    <country
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/select"
                                        fieldLabel="Country"
                                        name="./country"/>
                                </items>
                            </column>
                        </items>
                    </fieldset>
                </items>
            </column>
        </items>
    </content>
</jcr:root>


AEM 6 SP2 - Sample Datasource Touch UI Select Listener

Goal


Add two Granite Select widgets - /libs/granite/ui/components/foundation/form/select with listeners, each listening to the select event of other. In this sample, Country Select widget and Capital Select widget are fed with data sources, countries and capitals respectively for options, selecting country changes capital, vice versa

Demo | Package Install



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-select-listener

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-select-listener/clientlib, set a property categories of String type to cq.authoring.dialog and dependencies of type String[] to underscore

3) Create file ( type nt:file ) /apps/touchui-select-listener/clientlib/js.txt, add the following

                         listener.js

4) Create file ( type nt:file ) /apps/touchui-select-listener/clientlib/listener.js, add the following code

(function ($, $document) {
    "use strict";

    var COUNTRY = "./country", CAPITAL = "./capital";

    function adjustLayoutHeight(){
        //with only two selects, the second select drop down is not visible when expanded, so adjust the layout height
        //fixedcolumns i guess doesn't support property height, so fallback to jquery
        //http://docs.adobe.com/docs/en/aem/6-0/develop/ref/granite-ui/api/jcr_root/libs/granite/ui/components/foundation/layouts/fixedcolumns/index.html
        $(".coral-FixedColumn-column").css("height", "18rem");
    }

    $document.on("dialog-ready", function() {
        adjustLayoutHeight();

        //get the country widget
        var country = new CUI.Select({
            element: $("[name='" + COUNTRY +"']").closest(".coral-Select")
        });

        //get the capital widget
        var capital = new CUI.Select({
            element: $("[name='" + CAPITAL +"']").closest(".coral-Select")
        });

        if(_.isEmpty(country) || _.isEmpty(capital)){
            return;
        }

        //workaround to remove the options getting added twice, using CUI.Select()
        country._selectList.children().not("[role='option']").remove();
        capital._selectList.children().not("[role='option']").remove();

        //listener on country select
        country._selectList.on('selected.select', function(event){
            //select country's capital and throw change event for touchui to update ui
            capital._select.val(event.selectedValue).trigger('change');
        });

        //listener on capital select
        capital._selectList.on('selected.select', function(event){
            //select capital's country and throw change event for touchui to update ui
            country._select.val(event.selectedValue).trigger('change');
        });
    });
})($, $(document));


5) Create a simple datasource for countries - /apps/touchui-select-listener/datasource/country/country.jsp

<%@include file="/libs/granite/ui/global.jsp"%>

<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.sling.api.wrappers.ValueMapDecorator" %>
<%@ page import="com.adobe.granite.ui.components.ds.SimpleDataSource" %>
<%@ page import="org.apache.commons.collections.iterators.TransformIterator" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.LinkedHashMap" %>
<%@ page import="org.apache.commons.collections.Transformer" %>
<%@ page import="org.apache.sling.api.resource.*" %>

<%
    final Map<String, String> countries = new LinkedHashMap<String, String>();

    countries.put("INDIA", "India");
    countries.put("USA", "United States");
    countries.put("CHINA", "China");

    final ResourceResolver resolver = resourceResolver;

    DataSource ds = new SimpleDataSource(new TransformIterator(countries.keySet().iterator(), new Transformer() {
        public Object transform(Object o) {
            String country = (String) o;
            ValueMap vm = new ValueMapDecorator(new HashMap<String, Object>());

            vm.put("value", country);
            vm.put("text", countries.get(country));

            return new ValueMapResource(resolver, new ResourceMetadata(), "nt:unstructured", vm);
        }
    }));

    request.setAttribute(DataSource.class.getName(), ds);
%>


6) Create datasource for capitals - /apps/touchui-select-listener/datasource/capital/capital.jsp

<%@include file="/libs/granite/ui/global.jsp"%>

<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.sling.api.wrappers.ValueMapDecorator" %>
<%@ page import="com.adobe.granite.ui.components.ds.SimpleDataSource" %>
<%@ page import="org.apache.commons.collections.iterators.TransformIterator" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.LinkedHashMap" %>
<%@ page import="org.apache.commons.collections.Transformer" %>
<%@ page import="org.apache.sling.api.resource.*" %>

<%
    final Map<String, String> capitals = new LinkedHashMap<String, String>();

    capitals.put("INDIA", "New Delhi");
    capitals.put("USA", "Washington DC");
    capitals.put("CHINA", "Beijing");

    final ResourceResolver resolver = resourceResolver;

    DataSource ds = new SimpleDataSource(new TransformIterator(capitals.keySet().iterator(), new Transformer() {
        public Object transform(Object o) {
            String capital = (String) o;
            ValueMap vm = new ValueMapDecorator(new HashMap<String, Object>());

            vm.put("value", capital);
            vm.put("text", capitals.get(capital));

            return new ValueMapResource(resolver, new ResourceMetadata(), "nt:unstructured", vm);
        }
    }));

    request.setAttribute(DataSource.class.getName(), ds);
%>


7) The Touch UI dialog xml - /apps/touchui-select-listener/sample-select-listener/cq:dialog

<?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"
    jcr:primaryType="nt:unstructured"
    jcr:title="Multifield TouchUI Component"
    sling:resourceType="cq/gui/components/authoring/dialog"
    helpPath="en/cq/current/wcm/default_components.html#Text">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
        <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
        <items jcr:primaryType="nt:unstructured">
            <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/foundation/container">
                <items jcr:primaryType="nt:unstructured">
                    <fieldset
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Sample Select"
                        sling:resourceType="granite/ui/components/foundation/form/fieldset">
                        <layout
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                        <items jcr:primaryType="nt:unstructured">
                            <column
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/foundation/container">
                                <items jcr:primaryType="nt:unstructured">
                                    <country
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/select"
                                        fieldLabel="Country"
                                        name="./country">
                                        <datasource
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="/apps/touchui-select-listener/datasource/country"
                                            addNone="{Boolean}true"/>
                                    </country>
                                    <capital
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/select"
                                        disabled="{Boolean}false"
                                        fieldLabel="Capital"
                                        name="./capital">
                                        <datasource
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="/apps/touchui-select-listener/datasource/capital"
                                            addNone="{Boolean}true"/>
                                    </capital>
                                </items>
                            </column>
                        </items>
                    </fieldset>
                </items>
            </column>
        </items>
    </content>
</jcr:root>


8) The component structure in CRXDE Lite