AEM 61 - Touch UI Rich Text Editor (RTE) Browse and Insert Image

Goal


Touch UI Rich Text Editor (RTE) Plugin to open a DialogSelect 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));

AEM 61 - Hide Specific Folders in Touch UI

Goal


In latest versions of AEM set "hidden=true" property on the folder to hide it in card/list/column views...

This sample extension is for hiding specific paths (folders or assets) in Touch UI Assets console Card View, List View and Column View. Setting up ACLs is a better way of providing necessary read/write access on folders....

Demo | Package Install


Product - /content/dam/geometrixx/portraits visible



Extension - /content/dam/geometrixx/portraits hidden


Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touch-ui-hide-specific-folders

2) Create clientlib (type cq:ClientLibraryFolder/apps/touch-ui-hide-specific-folders/clientlib, set a property categories of String type to granite.ui.foundation.admin and dependencies of type String[] to underscore

3) Create file ( type nt:file ) /apps/touch-ui-hide-specific-folders/clientlib/js.txt, add the following

                         hide.js

4) Create file ( type nt:file ) /apps/touch-ui-hide-specific-folders/clientlib/hide.js, add the following code. The array PATHS_TO_HIDE has paths that should be hidden while rendering Assets console

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

    var PATHS_TO_HIDE = ["/content/dam/catalogs", "/content/dam/projects", "/content/dam/geometrixx/portraits" ];

    var DAM_ADMIN_CHILD_PAGES = "cq-damadmin-admin-childpages",
        LOAD_EVENT = "coral-columnview-load",
        FOUNDATION_LAYOUT_CARD = ".foundation-layout-card",
        NS_COLUMNS = ".foundation-layout-columns";

    $(document).on("foundation-mode-change", function(e, mode, group){
        //not assets console, return
        if(group != DAM_ADMIN_CHILD_PAGES){
            return;
        }

        hide();

        var $collection = $(".foundation-collection[data-foundation-mode-group=" + group + "]");

        //for column view
        $collection.on(LOAD_EVENT, function(){
            setTimeout( hide ,200);
        });

        //for column view select
        $collection.on("coral-columnview-item-select" + NS_COLUMNS, hide);

        if (!$collection.is(FOUNDATION_LAYOUT_CARD)) {
            return;
        }

        var $scrollContainer = $collection.parent();

        //for card view scroll
        $scrollContainer.on("scroll" + FOUNDATION_LAYOUT_CARD, _.throttle(function(){
            var paging = $collection.data("foundation-layout-card.internal.paging");

            if(!paging.isLoading){
                return;
            }

            var INTERVAL = setInterval(function(){
                if(paging.isLoading){
                    return;
                }

                clearInterval(INTERVAL);

                hide();
            }, 250);
        }, 100));
    });

    function hide(){
        var $articles = $("article"), $article, path;

        $articles.each(function(index, article){
            $article = $(article);

            path = $article.data("path");

            if(PATHS_TO_HIDE.indexOf(path) < 0){
                return;
            }

            $article.hide();
        });
    }
})(document, jQuery);

AEM 61 - Classic UI Show Site Specific Workflows

Goal


Show Site specific Workflows in the Start Workflow Dialog of Classic UI SiteAdmin console

Demo | Package Install


Set Site Path on Workflow - eaemSitePath

              Set site root path - eaemSitePath for which the specific workflow should be available. Here Request for Activation workflow is shown for pages of Geometrixx Demo 
              Site /content/geometrixx only



Request for Activation available for pages of /content/geometrixx



Request for Activation NOT available for pages of  /content/geometrixx-media



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classicui-site-specific-workflows

2) Create clientlib (type cq:ClientLibraryFolder/apps/classicui-site-specific-workflows/clientlib and set a property categories of String type to cq.widgets and dependencies value underscore

3) Create file ( type nt:file ) /apps/classicui-site-specific-workflows/clientlib/js.txt, add the following

                         site-workflows.js

4) Create file (type nt:file) /apps/classicui-site-specific-workflows/clientlib/site-workflows.js, add the following code

(function($){
    if(window.location.pathname != "/siteadmin"){
        return;
    }

    var SA_GRID = "cq-siteadmin-grid",
        WORKFLOW_BUT_TEXT = "Workflow...",
        START_WF_DIALOG_ID = "cq-workflowdialog",
        QUERY = "SELECT * FROM [cq:Page] WHERE ISDESCENDANTNODE([/etc/workflow/models]) AND " +
                "([jcr:content/eaemSitePath] = 'PLACEHOLDER' OR [jcr:content/eaemSitePath] IS NULL)";

    var SA_INTERVAL = setInterval(function(){
        var grid = CQ.Ext.getCmp(SA_GRID);

        if(!grid || ( grid.rendered != true)){
            return;
        }

        clearInterval(SA_INTERVAL);

        var toolBar = grid.getTopToolbar();

        try{
            var wButton = toolBar.find("text", WORKFLOW_BUT_TEXT)[0];

            wButton.on('click', filterWorkflows);
        }catch(err){
            console.log("Error adding workflow button listener");
        }
    }, 250);

    function filterWorkflows(){
        var wMgr = CQ.Ext.WindowMgr, winId;

        var W_INTERVAL = setInterval(function () {
            wMgr.each(function (win) {
                winId = win.id;

                if (winId && (winId.indexOf(START_WF_DIALOG_ID) < 0)) {
                    return;
                }

                var modelCombo = CQ.Ext.getCmp(winId + "-model");

                if(modelCombo.eaemInit){
                    return;
                }

                clearInterval(W_INTERVAL);

                modelCombo.eaemInit = true;

                var contentPath = window.location.hash.split("#")[1],
                    //you may want to replace the following query with a servlet returning results
                    query = "/crx/de/query.jsp?type=JCR-SQL2&showResults=true&stmt=" + QUERY.replace('PLACEHOLDER', contentPath);

                $.ajax( { url: query, context: modelCombo } ).done(filter);
            });
        }, 250);

        function filter(data){
            function handler(store, recs){
                var paths = _.pluck(data.results, "path"), modelId;

                _.each(recs, function (rec){
                    modelId = rec.data.wid;
                    modelId = modelId.substring(0, modelId.indexOf("/jcr:content/model"));

                    if(paths.indexOf(modelId) != -1){
                        return;
                    }

                    store.remove(rec);
                })
            }

            this.store.on('load', handler);
        }
    }
}(jQuery));

AEM 61 - Touch UI Add New Fields to User Editor (Save to Profile)

Goal


Extend User Editor - http://localhost:4502/libs/granite/security/content/userEditor.html to add new fields to the form, saved to user profile node in CRX eg. Textfield for Alternate Email

For Classic UI, check this post

Demo | Package Install


Field Alternate Email



Alternate Email TextField in User Editor



Alternate Email saved to User Profile



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-usereditor-new-field

2) Create /apps/touchui-usereditor-new-field/content of type sling:Folder

3) Add new fields (one or more) in a node structure (similar to authoring dialogs) in /apps/touchui-usereditor-new-field/content/addn-details eg. the nodes as xml for new textfield Alternate Email

<?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"
    class="well user-details-sections-margin user-editor-container">
    <layout
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/layouts/well"/>
    <items jcr:primaryType="nt:unstructured">
        <alt-email
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/form/textfield"
            class="save-button-enabler"
            fieldLabel="Alternate Email"
            name="./profile/alt-email"/>
    </items>
</jcr:root>

4) Fields in content node /apps/touchui-usereditor-new-field/content/addn-details are added to the user editor by clientlib logic explained in next steps...

5) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-usereditor-new-field/clientlib and set a property categories of String type to granite.securityCUI and dependencies String[] underscore

6) Create file ( type nt:file ) /apps/touchui-usereditor-new-field/clientlib/js.txt, add the following

                         fields.js

7) Create file ( type nt:file ) /apps/touchui-usereditor-new-field/clientlib/fields.js, add the following code

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

    var USER_EDITOR_CONTAINER = ".user-editor-container",
        USER_ADMIN_CLEAR = ".user-admin-clear",
        USER_EDITOR_URL = "/libs/granite/security/content/userEditor.html",
        ADD_DETAILS_CONTENT_URL = "/apps/touchui-usereditor-new-field/content/addn-details";

    getAdditionalFields();

    function getAdditionalFields(){
        $.ajax( { url: ADD_DETAILS_CONTENT_URL + ".html", dataType: 'html'}).done(handler);

        function handler(data){
            if(_.isEmpty(data)){
                return;
            }

            var $fields = ($(data)).children();

            $fields.insertBefore($(USER_EDITOR_CONTAINER).find(USER_ADMIN_CLEAR));

            fillAdditionalFields($fields);
        }
    }

    function fillAdditionalFields($fields){
        if(_.isEmpty($fields)){
            return;
        }

        var url = document.location.pathname;

        url = url.substring(USER_EDITOR_URL.length);

        $.ajax(url + "/profile.json").done(handler);

        function handler(data){
            if(_.isEmpty(data)){
                return;
            }

            var $input, name;

            //handles input types only, add additional logic for other types like checkbox...
            $fields.each(function(i, field){
                $input = $(field).find("input");

                name = $input.attr("name");

                name = getStringAfterLastSlash(name);

                $input.val(data[name]);
            });
        }
    }

    function getStringAfterLastSlash(str){
        if(!str || (str.indexOf("/") == -1)){
            return str;
        }

        return str.substr(str.lastIndexOf("/") + 1);
    }
})(jQuery, document);



AEM 61 - Classic UI Implementing a Simple Password Policy

Goal


Extend User Properties of Classic UI to enforce a Simple Password Policy. The validation checks if user has entered atleast one number in password, while creating a new user or changing password of existing user

For Touch UI check this post

For product password validation provider, check this post

Demo | Package Install


Create User



Change Password



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classic-ui-password-policy

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

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

                         policy.js

4) Create file (type nt:file) /apps/classic-ui-password-policy/clientlib/policy.js, add the following code

(function(){
    if(window.location.pathname != "/useradmin"){
        return;
    }

    //password validation text
    var POLICY_TEXT = "New Password must contain atleast one number";

    //add your policy implementation logic in the below function returning true/false
    function isValidPassword(text){
        if(!text){
            return false;
        }

        //check for number in text
        return /\d/.test(text);
    }

    var UA_INTERVAL = setInterval(function(){
        var userAdmin = CQ.Ext.getCmp("cq-useradmin");

        if(userAdmin && userAdmin.userProperties){
            clearInterval(UA_INTERVAL);

            var pwdButton = userAdmin.userProperties.pwdButtons.get(0);

            pwdButton.on("click", function(){
                findDialog("Set Password");
            });

            addPolicyToCreateUser(userAdmin);
        }
    }, 250);

    function addPolicyToCreateUser(userAdmin){
        var menu = null;

        try{
            menu = userAdmin.list.actions.edit.menu;
        }catch(err){
            console.log("Error reading menu");
        }

        var createMenu = menu.findBy(function(comp){
            return comp.text === "Create";
        })[0];

        var createUserItem = createMenu.menu.findBy(function(comp){
            return comp.text === "Create User";
        })[0];

        createUserItem.on("click", function(){
            findDialog("Create User");
        });
    }

    function findDialog(title){
        var wMgr = CQ.Ext.WindowMgr;

        //get the set password dialog from window manager; could not find dialog reference in userAdmin.userProperties
        var W_INTERVAL = setInterval(function () {
            wMgr.each(function (win) {
                if (win.title !== title) {
                    return;
                }

                clearInterval(W_INTERVAL);

                addPolicyText(win);

                addValidationHandler(win);
            });
        }, 250);
    }

    function addPolicyText(passwordWin){
        if(!passwordWin){
            return;
        }

        var panel = passwordWin.items.get(0);

        panel.body.insertHtml("afterBegin", "<div style='text-align:center; font-style: italic'>"
                                    + POLICY_TEXT + "</div>");
    }

    function addValidationHandler(passwordWin){
        passwordWin.on("beforesubmit", function(){
            var passField = this.findBy(function(comp){
                return comp.name == "rep:password";
            })[0];

            if(!isValidPassword(passField.getValue())){
                CQ.Ext.Msg.show({ title: "Error", msg: POLICY_TEXT,
                                buttons: CQ.Ext.Msg.OK, icon: CQ.Ext.Msg.ERROR});
                return false;
            }

            return true;
        })
    }
}());

AEM 61 - Show References (Page, Assets) while Deleting Tags in Touch UI

Goal


Show Dialog with Paths (Pages, Assets) Referencing Tag(s), before user hits yes and Tag(s) removed, in Touch UI Tagging Console - http://localhost:4502/aem/tags.html

For Classic UI check this post

Demo | Package Install


Product



Extension



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-show-tag-references

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-show-tag-references/clientlib and set a property categories of String type to cq.tagging.touch.deletetag and dependencies String[] underscore

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

                         tag-references.js

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

(function ($, $document) {
    //the query to find tag references (pages and assets)
    var CHECK_TAGS_SQL_2_QUERY = "SELECT * from [nt:base] AS t WHERE ISDESCENDANTNODE([/content]) AND ( ",
        DELETE_TAG_ACTIVATOR = ".cq-tagging-touch-actions-deletetag-activator";

    $document.on('foundation-contentloaded', function() {
        registerShowRefsAlert();
    });

    function registerShowRefsAlert(){
        $(DELETE_TAG_ACTIVATOR).click(clickHandler);
    }

    function clickHandler(event){
        var $selectedItems = $(".foundation-selections-item"),
            query = CHECK_TAGS_SQL_2_QUERY, tagId,
            path, paths = [];

        $selectedItems.each(function(index, item) {
            path = $(item).data("foundation-collection-item-id");

            paths.push(path);

            tagId = getTagIDFromPath(path);

            query = query + "t.[cq:tags] = '" + tagId + "'";

            if(index < ($selectedItems.length - 1)){
                query = query + " OR ";
            }
        });

        query = query + " )";

        //you may want to replace this crxde lite call with a servlet returning query results
        query = "/crx/de/query.jsp?type=JCR-SQL2&showResults=true&stmt=" + query;

        $.ajax( { url: query, async: false } ).done(function(data){
            showAlert(data, paths, event);
        });
    }

    function showAlert(data, paths, event){
        if(_.isEmpty(data) || _.isEmpty(data.results)){
            return;
        }

        event.stopPropagation();

        var fui = $(window).adaptTo("foundation-ui"),
            options = [{
                text: "NO"
            },{
                id: "YES",
                text: "YES",
                primary: true
            }];

        function callback(actionId){
            if (actionId != "YES") {
                return;
            }

            deleteTags(paths).done(handler);

            function handler(){
                $(".foundation-content").adaptTo("foundation-content").refresh();

                var fui = $(window).adaptTo("foundation-ui"),
                    options = [{
                        text: "OK"
                    }];

                fui.prompt("Success", "Tag(s) deleted successfully", "default", options);
            }
        }

        var message = "Selected tag(s) are referenced. Click 'yes' to proceed deleting, 'no' to cancel the operation.";

        _.each(data.results, function(result){
            message = message + result.path;
        });

        fui.prompt("Delete Tag", message, "default", options, callback);
    }

    function getTagIDFromPath(tagPath){
        return tagPath.substring("/etc/tags".length + 1).replace("/", ":");
    }

    function deleteTags(paths) {
        return $.ajax({
            url: "/bin/tagcommand",
            type: "post",
            data: {
                cmd: "deleteTag",
                path: paths,
                "_charset_": "utf-8"
            }
        });
    }
}(jQuery, jQuery(document)));

AEM 61 - ClassicUI Show Alert with References while Removing a Tag

Goal


Show Alert with Paths Referencing the Tag, before user hits yes and Tag removed in Tagging console - http://localhost:4502/tagging

For Touch UI check this post

Demo | Package Install


Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classicui-show-tag-references

2) Create clientlib (type cq:ClientLibraryFolder/apps/classicui-show-tag-references/clientlib and set a property categories of String type to cq.tagging and dependencies value underscore

3) Create file ( type nt:file ) /apps/classicui-show-tag-references/clientlib/js.txt, add the following

                         tag-references.js

4) Create file (type nt:file) /apps/classicui-show-tag-references/clientlib/tag-references.js, add the following code

(function () {
    if (window.location.pathname !== "/tagging") {
        return;
    }

    registerShowRefsAlert();

    //the query to find tag references (pages and assets)
    //Exact match: SELECT * from [nt:base] AS t WHERE NAME(t) = 'jcr:content' AND CONTAINS(t.[cq:tags], 'experience-aem:english/us')
    var CHECK_TAGS_SQL_2_QUERY = "SELECT * from [nt:base] AS t WHERE NAME(t) = 'jcr:content' " +
                                    "AND CONTAINS(t.*, 'PLACEHOLDER')";

    function registerShowRefsAlert(){
        var tagAdmin = CQ.tagging.TagAdmin,
            deleteTagFn = tagAdmin.deleteTag;

        //override ootb function to inject the logic showing references alert
        tagAdmin.deleteTag = function(){
            var tagPath = tagAdmin.getSelectedTag();

            if (tagPath == null) {
                return;
            }

            tagPath = tagPath.substring( this.tagsBasePath.length + 1);

            var tagInfo = CQ.tagging.parseTag(tagPath, true),
                query = encodeURIComponent(CHECK_TAGS_SQL_2_QUERY.replace("PLACEHOLDER", tagInfo.getTagID()));

            //you may want to replace this crxde lite call with a servlet returning query results
            query = "/crx/de/query.jsp?type=JCR-SQL2&showResults=true&stmt=" + query;

            //"this" here is tagadmin object, passed as context
            $.ajax( { url: query, context: this } ).done(showAlert);
        };

        function showAlert(data){
            if(_.isEmpty(data) || _.isEmpty(data.results)){
                deleteTagFn.call(this);
                return;
            }

            var message = "Selected tag is referenced. Click 'yes' to proceed deleting, 'no' to cancel the operation.";

            _.each(data.results, function(result){
                message = message + result.path + "";
            });

            CQ.Ext.Msg.show({
                "title": "Delete Tag",
                "msg": message,
                "buttons": CQ.Ext.Msg.YESNO,
                "icon": CQ.Ext.MessageBox.QUESTION,
                "fn": function (btnId) {
                    if (btnId == "yes") {
                        this.postTagCommand("deleteTag", tagAdmin.getSelectedTag());
                    }
                },
                "scope": this
            });
        }
    }
}());

AEM 61 - Touch UI Add Simple Password Policy

Goal


Extend User Properties form of Touch UI to enforce a simple password policy. The validation checks if user has entered atleast one number in password, while creating a new user or changing the password for existing user

For Classic UI, check this post

For product password validation provider, check this post

Demo | Package Install


New User



Change Password



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-enforce-password-policy

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-enforce-password-policy/clientlib and set a property categories of String type to granite.securityCUI

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

                         policy.js

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

(function ($, $document) {
    //id added in /libs/granite/security/content/userEditor/items/page/items/contentWrapper/items/contentContainer/items/content/items/content/items/photoSettings/items/col1/items/accountSettings/items/changePassword
    var ADMIN_PASSWORD_DIV = "#admin_password",
        FIELD_CHANGE_PASSWORD = ".change-user-password", // change password form
        FIELD_NEW_USER_PASSWORD = ".user-password-fields", //new user form
        BUTTON_OK = ".user-admin-change-password",  // change password form
        BUTTON_SAVE = ".user-admin-save-user", //new user form
        RE_TYPE_PASSWORD_FIELD = "[name='rep:re-password']";

    var $policyText = $("<div/>").css('padding-bottom', '10px')
                                .css('font-style', 'italic')
                                .css('text-align', 'center')
                                .html('New Password must contain a number');

    $(ADMIN_PASSWORD_DIV).find(".coral-Modal-body")
                        .prepend($policyText);

    $(ADMIN_PASSWORD_DIV).find(RE_TYPE_PASSWORD_FIELD).focusout(focusHandler);

    $(FIELD_NEW_USER_PASSWORD).find(RE_TYPE_PASSWORD_FIELD).focusout(focusHandler);

    //change password form
    $document.on("keyup", FIELD_CHANGE_PASSWORD, function(){
        keyHandler($(FIELD_CHANGE_PASSWORD));
    });

    //new user form
    $document.on("keyup.user-admin change.user-admin selected.user-admin", ".save-button-enabler", function(){
        keyHandler( $(FIELD_NEW_USER_PASSWORD).find("[type=password]"));
    });

    function keyHandler($fields){
        if(!$fields || $fields.length != 2){
            return;
        }

        var one = $($fields[0]).val(), two = $($fields[1]).val();

        if(isValidPassword(one) && isValidPassword(two) && (one == two)){
            return;
        }

        $(BUTTON_OK).attr("disabled", "disabled"); // change password form
        $(BUTTON_SAVE).attr("disabled", "disabled"); //new user form
    }

    function focusHandler(event){
        clearErrors();

        var val = $(event.currentTarget).val();

        if(isValidPassword(val)){
            return;
        }

        var message = "Password must contain a number";

        showErrorAlert(message);
    }

    function clearErrors(){
        $(BUTTON_OK).removeAttr("disabled");   // change password form
        $(BUTTON_SAVE).removeAttr("disabled");  //new user form
    }

    function isValidPassword(text){
        if(!text){
            return true;
        }

        //check for number in text
        return /\d/.test(text);
    }

    function showErrorAlert(message, title){
        var fui = $(window).adaptTo("foundation-ui"),
            options = [{
                text: "OK",
                warning: true
            }];

        title = title || "Error";

        fui.prompt(title, message, "notice", options);

        $(BUTTON_OK).attr("disabled", "disabled"); // change password form
        $(BUTTON_SAVE).attr("disabled", "disabled"); //new user form
    }
})(jQuery, jQuery(document));



AEM 61 - Touch UI Add New Column to Assets Console List View

Goal


Add a New Column to List View of Assets Console http://localhost:4502/assets.html/content/dam

Here we add two columns, the following are available in Experience AEM section of Configure Columns flyout menu

                   Folder Assets Count - Count of DAM Assets in the folder (for deep nested count, use /bin/querybuilder.json )
                   Created By - The folder creator id

For adding columns to list view of AEM 6 SP2 check this post, for Classic UI check this post

For 62 Touch UI check this post

Demo | Package Install




Solution


1) Overlay /libs/dam/gui/content/commons/listview/assetsavailablecolumns to /apps/dam/gui/content/commons/listview/assetsavailablecolumns to add the necessary columns. Sling Resource Merger takes care of merging the columns in /apps... with the ones in /libs... while rendering. Use a utility servlet to create the overlay, check this post; After installing servlet package access the servlet with path to overlay - http://localhost:4502/bin/experience-aem/create/overlay?path=/libs/dam/gui/content/commons/listview/assetsavailablecolumns/type and rename the overlayed type column to assetsCount and change necessary properties

2) The new columns assetsCountcreatedBy in /apps/dam/gui/content/commons/listview/assetsavailablecolumns



3) XML representation of columns added - /apps/dam/gui/content/commons/listview/assetsavailablecolumns

<?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">
    <assetsavailablecolumns jcr:primaryType="nt:unstructured">
        <assetsCount
            jcr:primaryType="nt:unstructured"
            sling:resourceType="cq/gui/components/siteadmin/admin/pages/headers/deflt"
            class="assetsCount"
            columnGroup="Experience AEM"
            default="{Boolean}true"
            ignorecase="{Boolean}true"
            show-selector=".label .assetsCount"
            sort-selector=".label .assetsCount"
            sort-type="numeric"
            title="Folder Assets Count"/>
        <createdBy
            jcr:primaryType="nt:unstructured"
            sling:resourceType="cq/gui/components/siteadmin/admin/pages/headers/deflt"
            class="createdBy"
            columnGroup="Experience AEM"
            default="{Boolean}true"
            ignorecase="{Boolean}true"
            show-selector=".label .createdBy"
            sort-selector=".label .createdBy"
            title="Created By"/>
    </assetsavailablecolumns>
</jcr:root>

4) The above steps add static columns to list view; in the next steps we add some dynamic behavior to columns like count of assets in each folder

5) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-assets-add-column

6) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-assets-add-column/clientlib and set a property categories of String type to cq.gui.damadmin.admindependencies of type String[] with value underscore

7) Create file ( type nt:file ) /apps/touchui-assets-add-column/clientlib/css.txt, add the following

                         asset-count.less

8) Create file ( type nt:file ) /apps/touchui-assets-add-column/clientlib/asset-count.less, add the following code. If there is a better way with little code, to generate the .list-columns-xx.list css, please leave a comment; List view shows ten columns max..

.list-columns-1.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy  {
      width: 48%;
      float: left;
    }
  }
}

.list-columns-2.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 31%;
      float: left;
    }
  }
}

.list-columns-3.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 21%;
      float: left;
    }
  }
}

.list-columns-4.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 16%;
      float: left;
    }
  }
}

.list-columns-5.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 13%;
      float: left;
    }
  }
}

.list-columns-6.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 11.5%;
      float: left;
    }
  }
}

.list-columns-7.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 10%;
      float: left;
    }
  }
}

.list-columns-8.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 9.5%;
      float: left;
    }
  }
}

.list-columns-9.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 9%;
      float: left;
    }
  }
}

.list-columns-10.list {
  .card-asset, .card-directory, .card-collection {
    .assetsCount, .createdBy {
      width: 8.5%;
      float: left;
    }
  }
}

9) Create file ( type nt:file ) /apps/touchui-assets-add-column/clientlib/js.txt, add the following

                         asset-count.js

10) Create file ( type nt:file ) /apps/touchui-assets-add-column/clientlib/asset-count.js, add the following code

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

    var FOUNDATION_LAYOUT_LIST = ".foundation-layout-list",
        DAM_ADMIN_CHILD_PAGES = "cq-damadmin-admin-childpages",
        ATTR_DATA_FOUNDATION_COLLECTION_ID = "data-foundation-collection-id",
        ASSETS_COUNT_CLASS = "assetsCount",
        CREATED_BY_CLASS = "createdBy",
        OOTB_DEFAULT_CSS = "shared";

    $(document).on("foundation-mode-change", function(e, mode, group){
        //not on assets list, return
        if((group != DAM_ADMIN_CHILD_PAGES) || (mode == "selection") ){
            return;
        }

        //group is cq-damadmin-admin-childpages for assets
        var $collection = $(".foundation-collection[data-foundation-mode-group=" + group + "]");

        if (!$collection.is(FOUNDATION_LAYOUT_LIST)) {
            return;
        }

        var $articles = $("article");

        //css class shared added in /libs/dam/gui/components/admin/childasset/childasset.jsp,
        //rename it to the first extension column css class - assetsCount for "Assets Count"
        $articles.find("." + OOTB_DEFAULT_CSS)
                    .removeClass(OOTB_DEFAULT_CSS)
                    .addClass(ASSETS_COUNT_CLASS);

        //add new dom for second extension column "Created By"
        $("<p/>").attr("class", CREATED_BY_CLASS).insertAfter($articles.find("." + ASSETS_COUNT_CLASS));

        var path = $("." + DAM_ADMIN_CHILD_PAGES).attr(ATTR_DATA_FOUNDATION_COLLECTION_ID);

        $.ajax(path + ".2.json").done(handler);

        function handler(data){
            var articleName, assets;

            $articles.each(function(index, article){
                var $article = $(article);

                articleName = getStringAfterLastSlash($article.data("path"));

                //reject non assets nodes
                assets = _.reject(data[articleName], function(value, key){
                    return (key.indexOf("jcr:") == 0) || value["jcr:primaryType"] == "sling:OrderedFolder";
                });

                $article.find("." + CREATED_BY_CLASS).html(data[articleName]["jcr:createdBy"]);
                $article.find("." + ASSETS_COUNT_CLASS).html(Object.keys(assets).length);
            });
        }

        function getStringAfterLastSlash(str){
            if(!str || (str.indexOf("/") == -1)){
                return "";
            }

            return str.substr(str.lastIndexOf("/") + 1);
        }
    });
})(document, jQuery);