Saturday, August 13, 2016

AEM 6.2 sort pages in sites admin console

Disclaimer : Ideas/Implementions provided in this blog express my personal view. Adobe or Me will not be held responsible for damage caused on your system because of information. Please don't try any suggestion in production system without proper testing. This is not an Adobe supported blog


AEM 6.2 sites console provides support for sorting but we need to customize the sort parameter so that the sites console displays the sorted pages.

Objective:

Sort pages in sites admin console by last modified date, with latest being on the top of the list, in all the views (card, list and column)

PackageVideo

The sorted list looks like :


Follow the below steps:


  1. go to /libs/wcm/core/content/sites/jcr:content/views/column/datasource in crx/de
  2. Right click on the 'datasource' node and click on the "Overlay Node" and in the modal shown, check the 'Match Node Types' checkbox  and click on 'OK'
  3. This will overlay this node in "/apps/wcm/core/content/sites/jcr:content/views/column/datasource". Go to "/apps/wcm/core/content/sites/jcr:content/views/column/datasource" in crx/de
  4. add the following two properties on that node
         sortName = modified (Type is String)
         sortDir = asc (Type is String)
   5. save the changes.
   6. Now we've added the configuration for sorting on last modified for column view. 
   7. Similarly, overlay "/libs/wcm/core/content/sites/jcr:content/views/card/datasource" and           
      "/libs/wcm/core/content/sites/jcr:content/views/list/datasource" and add the above two 
       properties in the overlaid location.
       
Configurations:
  • valid values for sortDir
    • asc - to sort in ascending order
    • desc - to sort in descending order
  • valid values for sortName
    • modified - to sort on the page last modified date
    • main - to sort on the title of the page
    • published - to sort on the page published date


Sunday, August 7, 2016

AEM Touch UI Path Browser With Preview

Disclaimer : Ideas/Implementions provided in this blog express my personal view. Adobe or Me will not be held responsible for damage caused on your system because of information. Please don't try any suggestion in production system without proper testing. This is not an Adobe supported blog


AEM OOTB provides a path browser widget at /libs/granite/ui/components/coral/foundation/form/pathbrowser (in 6.2) or /libs/granite/ui/components/foundation/form/pathbrowser (in 6.1)

Objective:

Add a preview capability to the OOTB touch ui path browser widget, which will show the preview while we navigate the path browser. If authors create/upload the thumbnail of the pages while they author, they can see the updated thumbnail while using this widget.

Package   Github - Widget | Sample Component  Video

The component looks like:



How It Works:


  1. We'll create a clientlibs with the "cq.authoring.dialog" so that those get loaded automatically in the authoring
  2. when pathbrowser button () is clicked, we'll listen for the click event and adjust the widths of modal display to accommodate preview as well. We'll add corresponding divs and data attributes to the pathbrowser picker modal.
  3. When author clicks on any column item, we'll update the thumbnail using the javascript.

Follow the below steps:

1. Create "cq.authoring.dialog" clientlibs at /apps/aemdevcustomization/widgets/path-browser-preview
  • create js.txt in the clientlibs folder with the following content
#base=js
path-browser-preview.js
  • create path-browser-preview.js file at /apps/aemdevcustomization/widgets/path-browser-preview/js/path-browser-preview.js with the following content
(function (document, $) {
    var rel_preview_enabled_path_browser = "[data-enable-path-browser-preview='true'] .js-coral-pathbrowser-button";
    var rel_path_browser_item = ".coral-Pathbrowser-picker[data-enable-path-browser-preview=\"true\"] .coral-ColumnView-item";
    var rel_path_browser_preview = ".path-browser-preview";
    var rel_path_browser_preview_image = rel_path_browser_preview + " .path-browser-preview-image";
    var rel_active_item = ".coral-ColumnView-column.is-active .coral-ColumnView-item.is-active";
    var previewUrl;
    $(document).on("click", rel_preview_enabled_path_browser, function (e) {
        var target = $(e.target);
        var pathBrowser = target.closest("[data-enable-path-browser-preview='true']");
        previewUrl = pathBrowser.data("previewUrl") || "$path.thumb.319.319.png";
        if (pathBrowser.length) {
            var $pathBrowser = pathBrowser.data("pathBrowser");
            var $pathBrowserPicker = $pathBrowser.$picker;
            $pathBrowserPicker.attr("data-enable-path-browser-preview", "true");
            var $pickerPanel = $pathBrowserPicker.find(".coral-Pathbrowser-pickerPanel");
            var $pickerColumnView = $pickerPanel.find(".coral-ColumnView");
            $pickerColumnView.css({
                width: "75%"
            });
            var $preview = $("<div class='" + rel_path_browser_preview.substr(1) + "'><div class='path-browser-preview-text'>Preview</div><div class='path-browser-preview-image'></div></div>");
            $preview.css({
                width: "25%",
                height: "100%",
                "border-left": "1px solid black",
                float: "right"
            });
            $pickerPanel.append($preview);
        }
    });
    $(document).on("click", rel_path_browser_item, function (e) {
        var activeItem = $(rel_active_item);
        var path = activeItem.data('value');
        var previewImage = $(rel_path_browser_preview_image);
        previewImage.find("img").remove();
        if (path) {
            var previewThumbnail = previewUrl.replace("$path", path);
            previewImage.append("<img src='" + previewThumbnail + "'>");
        }
    });
})(document, Granite.$);

  • create css.txt in the clientlibs folder with the following content
#base=css
path-browser-preview.css
  • create path-browser-preview.css file at /apps/aemdevcustomization/widgets/path-browser-preview/css/path-browser-preview.css with the following content
.path-browser-preview-text {
    padding: 0 8rem;
}
Now you can enable preview for the OOTB path browser widget in authoring dialog, with the following example structure:

For AEM 6.2
<pathbrowser
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
    fieldLabel="Path Browser"
    name="./path"
    rootPath="/content">
    <granite:data
        jcr:primaryType="nt:unstructured"
        enable-path-browser-preview="{Boolean}true"
        preview-url="$path.thumb.319.319.png"/>
</pathbrowser>
For AEM 6.1
<pathbrowser
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
    fieldLabel="Path Browser"
    name="./path"
    rootPath="/content">
    <granite:data
        jcr:primaryType="nt:unstructured"
        enable-path-browser-preview="{Boolean}true"
        preview-url="$path.thumb.319.319.png"/>
</pathbrowser>

Widget Configurations:

enable-path-browser-preview="{Boolean}true" - to enable preview on OOTB AEM Path browser widget

preview-url="$path.thumb.319.319.png" - you can give any url where you might have your own servlet. 
 $path will be replaced with the current selection

Tuesday, May 3, 2016

AEM 6.1 Tab(ular) Multi Field for touch ui authoring dialog


Disclaimer : Ideas/Implementions provided in this blog express my personal view. Adobe or Me will not be held responsible for damage caused on your system because of information. Please don't try any suggestion in production system without proper testing. This is not an Adobe supported blog


AEM OOTB provides a simple multi field widget at /libs/granite/ui/components/foundation/form/multifield. But this widget can't handle the multiple fields.

Objective:

Create new composite multi field widget which displays each field(set) in a separate tab. The tabular separation of field sets helps to organize the fields easily rather than showing each field set one after the other vertically

You can download the package here

The component looks like:



The dialog structure looks like :



The content after authoring looks like:



  1. Create the path /apps/authoring/touchui/widgets/tabular-multifield, with 'tabular-multifield' being sling:Folder node type
    • On tabular-multifild, set sling:resourceSuperType to 'granite/ui/components/foundation/form/field'
    • Create 'render.jsp' in 'tabular-mutlifield' with the following content
<%@include file="/libs/granite/ui/global.jsp" %><%
%><%@page session="false"
          import="java.util.HashMap,
                  java.util.Iterator,
      java.util.List,
         java.util.ArrayList,
      org.apache.sling.api.resource.ResourceResolver,
                  org.apache.sling.api.wrappers.ValueMapDecorator,
                  com.adobe.granite.ui.components.AttrBuilder,
                  com.adobe.granite.ui.components.ComponentHelper,
                  com.adobe.granite.ui.components.ComponentHelper.Options,
                  com.adobe.granite.ui.components.Config,
                  com.adobe.granite.ui.components.Field,
                  com.adobe.granite.ui.components.Tag,
                  com.adobe.granite.ui.components.Value" %><%--###
Example::
   
      + myinput
        - name = "myTabularMultiField"
        - sling:resourceType = "/apps/authoring/touchui/widgets/tabular-multifield"
        + field
          - sling:resourceType = "granite/ui/components/foundation/container"
          + items
              + pathfield
                 - sling:resourceType = "granite/ui/components/foundation/form/pathbrowser"
                 - name = "./one"
              + textfield
                 - sling:resourceType = "granite/ui/components/foundation/form/textfield"
                 - name = "./two"

###--%>
<%
Config cfg = cmp.getConfig();
ValueMap vm = (ValueMap) request.getAttribute(Field.class.getName());

Tag tag = cmp.consumeTag();
AttrBuilder attrs = tag.getAttrs();

attrs.add("id", cfg.get("id", String.class));
attrs.addRel(cfg.get("rel", String.class));
attrs.addClass(cfg.get("class", String.class));
attrs.add("title", i18n.getVar(cfg.get("title", String.class)));
String name = cfg.get("name", String.class);
attrs.add("name", name);
attrs.addClass("tabular-Multifield");
attrs.add("data-init", "tabularmultifield");

attrs.addOthers(cfg.getProperties(), "id", "rel", "name", "class", "title", "fieldLabel", "fieldDescription", "renderReadOnly");

Resource field = cfg.getChild("field");

List<TabInfo> tabsInfo = getTabsInfo(request, resourceResolver, name);
%><div <%= attrs.build() %>><%
    if (cfg.get("deleteHint", true)) {
     for (TabInfo tabInfo : tabsInfo) {
            %><input type="hidden" name="./<%= name != null ? name + "/" : "" + tabInfo.getNodeName() %>@Delete"><%
     }
 }
    %>    
    <div class="coral-TabPanel" data-init="tabs">
        <nav class="coral-TabPanel-navigation">
            <%for (TabInfo tabInfo : tabsInfo) {%>            
            <a class="coral-TabPanel-tab" href="#"  data-toggle="tab">
                <span><%=tabInfo.getName()%><span>
                    <i class="close-tab-button js-tabular-Multifield-remove coral-Icon coral-Icon--close coral-Icon--sizeXS"></i>
                    </span></span>
            </a>
            <%}%>
            <a class="coral-TabPanel-tab js-tabular-Multifield-add" href="#" data-target="#field-add" data-toggle="tab">
                <i class="coral-Icon coral-Icon--add coral-Icon--sizeXS"></i>
            </a>
        </nav>
        <div class="coral-TabPanel-content">
            <%for (TabInfo tabInfo : tabsInfo) {%>            
            <section class="coral-TabPanel-pane" role="tabpanel">
                <% include(field, tabInfo.getValues(), cmp, request); %>
            </section>
            <%}%>
            <section class="dummy-section coral-TabPanel-pane" style="display:none">
            </section>
        </div>
    </div>
    <script class="js-tabular-Multifield-input-template" type="text/html"><% include(field, cmp, request); %></script>
</div><%!

private void include(Resource field, ComponentHelper cmp, HttpServletRequest request) throws Exception {
    // include the field with no value set at all
    
    ValueMap existingVM = (ValueMap) request.getAttribute(Value.FORM_VALUESS_ATTRIBUTE);
    String existingPath = (String) request.getAttribute(Value.CONTENTPATH_ATTRIBUTE);
    
    request.removeAttribute(Value.FORM_VALUESS_ATTRIBUTE);
    request.removeAttribute(Value.CONTENTPATH_ATTRIBUTE);
    
    cmp.include(field, new Options().rootField(false));

    request.setAttribute(Value.FORM_VALUESS_ATTRIBUTE, existingVM);
    request.setAttribute(Value.CONTENTPATH_ATTRIBUTE, existingPath);
}

private void include(Resource field, ValueMap map, ComponentHelper cmp, HttpServletRequest request) throws Exception {    
    ValueMap existing = (ValueMap) request.getAttribute(Value.FORM_VALUESS_ATTRIBUTE);
    request.setAttribute(Value.FORM_VALUESS_ATTRIBUTE, map);

    cmp.include(field, new Options().rootField(false));

    request.setAttribute(Value.FORM_VALUESS_ATTRIBUTE, existing);
}
private List<TabInfo> getTabsInfo(HttpServletRequest request, ResourceResolver resolver, String name) throws Exception {
    List<TabInfo> tabsInfo = new ArrayList<TabInfo>();
    String contentPath = (String) request.getAttribute(Value.CONTENTPATH_ATTRIBUTE);
    if (name != null) {
        contentPath = contentPath + "/" + name;
    }
    Resource contentRes = resolver.getResource(contentPath);
    if (contentRes != null) {
        for (Iterator<Resource> it = contentRes.listChildren(); it.hasNext(); ) {
            Resource r = it.next();
            String rName = r.getName();
            if (rName.startsWith("tab")) {
                String tabIndex = rName.replace("tab", "");
                String tabName = "Tab " + tabIndex;
                ValueMap vm = r.adaptTo(ValueMap.class);
                TabInfo tabInfo = new TabInfo(tabName, rName, vm);
                tabsInfo.add(tabInfo);
            }
        }
    }
    return tabsInfo;
}

class TabInfo {
    private String name;
    private String nodeName;
    private ValueMap values;
    TabInfo(String name, String nodeName, ValueMap values) {
        this.name = name;
        this.nodeName = nodeName;
        this.values = values;
    }
    String getName() {
        return name;
    }
    String getNodeName() {
        return nodeName;
    }
    ValueMap getValues() {
        return values;
    }
}
%>
2. Create "cq.authoring.dialog" clientlibs in /apps/authoring/touchui/widgets/tabular-multifield/clientlibs
  • create js.txt in the clientlibs folder with the following content
#base=js
tabular-multi-field.js
  • create css.txt in the clientlibs folder with the following content
#base=css
tabular-multi-field.css
  • create tabular-multi-field.js file at /apps/authoring/touchui/widgets/tabular-multifield/clientlibs/js/tabular-multi-field.js with the following content
(function ($, console) {
  "use strict";

  var listTemplate =
    "<div class=\"coral-TabPanel\" data-init=\"tabs\"><nav class=\"coral-TabPanel-navigation\"></nav><div class=\"coral-TabPanel-content\"></div></div>";




  CUI.TabularMultifield = new Class({
    toString: "TabularMultifield",
    extend: CUI.Widget,

    /**
     @extends CUI.Widget

     @classdesc A composite field that allows you to add/remove multiple instances of a component.
     The component is added based on a template defined in a <code>&lt;script type=&quot;text/html&quot;&gt;</code> tag.
     The current added components are managed inside a <code>div.tabular-Multifield</code> element.

     @desc Creates a TabularMultifield component.
     @constructs
     */
    construct: function (options) {
      this.script = this.$element.find(".js-tabular-Multifield-input-template");
      this.tabsContainer = this.$element.find(".coral-TabPanel").data("tabs");
      var prevTabIndex = 0;
      var tabs = this.tabsContainer._getTabs();
      tabs.each(function() {
          var $this = $(this);
          var i = $this.text().trim().replace("Tab ", "");
          if (!isNaN(i)) {
              var index = parseInt(i);
              if (index > prevTabIndex) {
                  prevTabIndex = index;
              }
          }
      });
      this.tabIndex = prevTabIndex === 0 ? 1: prevTabIndex + 1;

      if (this.tabsContainer.length === 0) {
        this.tabsContainer = $(listTemplate).prependTo(this.$element).find(".coral-TabPanel").data("tabs");
      }
      var tabularCompositeFieldName = this.$element.attr("name") || "";
      if (tabularCompositeFieldName) {
          tabularCompositeFieldName+= "/";
      }

      var panels = this.tabsContainer._getPanels();
      var self = this;
      panels.each(function(){
        var $this = $(this);
        var panelIndex = $this.index();
        var tabNumber = $(tabs[panelIndex]).text().trim().replace("Tab ", "");
        self.adjustInputNames($this, tabularCompositeFieldName + "tab" + tabNumber);
        self._handleRadioButtons($this);
      });

      this._adjustMarkup();
      this._addListeners();
    },

    _handleRadioButtons: function(item) {
        item.find("input[type='radio']").each(function(){
           var $this = $(this);
           if ($this.attr("checked") == "checked") {
               $this.attr("aria-selected",  true);
               $this.prop("checked", "checked");
           }
        });
    },

    /**
     * Enhances the markup required for multifield.
     * @private
     */
    _adjustMarkup: function () {
      this.$element.addClass("coral-Multifield");
    },

    /**
     * Initializes listeners.
     * @private
     */
    _addListeners: function () {
      var self = this;
      var tabularCompositeFieldName = self.$element.attr("name") || "";
      if (tabularCompositeFieldName) {
          tabularCompositeFieldName+= "/";
      }

      //add
      this.$element.on("click", ".js-tabular-Multifield-add", function (e) {
          var item = $(self.script.html().trim());
          self.adjustInputNames(item, tabularCompositeFieldName + "tab" + self.tabIndex);
          self.handleCheckBoxesDefaultValue(item);
          var tabCount = self.tabsContainer._getTabs().length;
          var tabContent = "<span>Tab " + self.tabIndex + "<span> <i class=\"close-tab-button js-tabular-Multifield-remove coral-Icon coral-Icon--close coral-Icon--sizeXS\"></i>"
          self.tabsContainer.addItem({tabContent:tabContent, panelContent:item, index:tabCount-1});
          self.tabIndex++;
          $(self.$element).trigger("cui-contentloaded");

      });
      //remove
      this.$element.on("click", ".js-tabular-Multifield-remove", function (e) {
          var $this = $(this);
          var tabIndex = $(this).closest(".coral-TabPanel-tab.is-active").index();
          self.tabsContainer.removeItem(tabIndex);
          var tabCount = self.tabsContainer._getTabs().length;
          if (tabCount === 1) {
              self.tabIndex = 1;
          }
      });
    },
    handleCheckBoxesDefaultValue: function(item) {
        item.find("input[type='checkbox'][data-check-in-multifield='true']").each(function(){
            var $this = $(this);
            $this.prop("checked", true);
        });
    },
    adjustInputNames: function (item, prefix) {
        item.find("[name]").each(function(){
            var $this = $(this);
            var name = $this.attr("name");
            $this.attr("name", "./" + prefix + name.substr(1));
        });
    }
  });

  CUI.Widget.registry.register("tabularmultifield", CUI.TabularMultifield);

  if (CUI.options.dataAPI) {
    $(document).on("cui-contentloaded.data-api", function (e) {
      CUI.TabularMultifield.init($("[data-init~=tabularmultifield]", e.target));
    });
  }
})(jQuery, window.console);
  • create tabular-multi-field.css at /apps/authoring/touchui/widgets/tabular-multifield/clientlibs/css/tabular-multi-field.css with the following content
.tabular-Multifield a[data-toggle="tab"] i {
    font-size: 0.5rem;
    padding-right: 1rem;
    padding-left: 0.5rem;
}

.tabular-Multifield a[data-toggle="tab"] {
    text-transform: none;
}
Now you can use this multi field widget in authoring dialog, with the following example structure:

                          <tabular_multifield

                                jcr:primaryType="nt:unstructured"

                                sling:resourceType="/apps/authoring/touchui/widgets/tabular-multifield"

                                fieldLabel="My Tabular Multi Field"

                                name="tabularFields">

                                <field

                                    jcr:primaryType="nt:unstructured"

                                    sling:resourceType="granite/ui/components/foundation/container">

                                    <items jcr:primaryType="nt:unstructured">

                                        <one

                                            jcr:primaryType="nt:unstructured"

                                            sling:resourceType="granite/ui/components/foundation/form/pathbrowser"

                                            fieldLabel="path browser"

                                            name="./path"

                                            rootPath="/content"/>

                                        <two

                                            jcr:primaryType="nt:unstructured"

                                            sling:resourceType="granite/ui/components/foundation/form/textfield"

                                            fieldLabel="text field"

                                            name="./text"/>

                                        <position

                                            jcr:primaryType="nt:unstructured"

                                            sling:resourceType="granite/ui/components/foundation/form/radiogroup"

                                            fieldDescription="alignment"

                                            fieldLabel="Choose"

                                            text="Alignment">

                                            <items jcr:primaryType="nt:unstructured">

                                                <left

                                                    jcr:primaryType="nt:unstructured"

                                                    sling:resourceType="granite/ui/components/foundation/form/radio"

                                                    class="slide-item"

                                                    name="./overlayPosition"

                                                    text="Left"

                                                    value="left"/>

                                                <center

                                                    jcr:primaryType="nt:unstructured"

                                                    sling:resourceType="granite/ui/components/foundation/form/radio"

                                                    checked="true"

                                                    class="slide-item"

                                                    name="./overlayPosition"

                                                    text="Center"

                                                    value="center"/>

                                                <right

                                                    jcr:primaryType="nt:unstructured"

                                                    sling:resourceType="granite/ui/components/foundation/form/radio"

                                                    class="slide-item"

                                                    name="./overlayPosition"

                                                    text="Right"

                                                    value="right"/>

                                            </items>

                                        </position>

                                        <checkbox

                                            jcr:primaryType="nt:unstructured"

                                            sling:resourceType="granite/ui/components/foundation/form/checkbox"

                                            check-in-multifield="{Boolean}true"

                                            fieldDescription="My Checkbox"

                                            fieldLabel="checkbox field"

                                            name="./mycheckBox"

                                            text="My Checkbox"

                                            value="checked"/>

                                    </items>

                                </field>

                            </tabular_multifield>