Favorites (Bookmarks) Widget Component

Goal


This blog post is on creating a Favorites (Bookmarks) Component. For dashboard use-cases where an author has to group a set of page links and put it on a page for quick access, the following approach could be useful. It also explains

             1) Creating custom widget in CQ

             2) Using CQ.form.MultiField

             3) Extending CQ.form.CompositeField

             4) Sample Ajax Sling Servlet

             5) Customizing CQ.form.PathField search window with new buttons and tree loader




In the demo video

             1) User admin logs in and shows the permissions for user John. John has read permissions on all /content nodes but modify on sites first-app-demo-site and geometrixx only

             2)  John logs in, creates the favorites component, adds some links, rearranges them, deletes etc.

Source code and Demo

Thanks to this blogger for giving a start http://cq.shishank.info/2011/12/19/multifield-with-custom-xtype/

Prerequisites


If you are new to CQ

1) Read this post on how to create a sample page component

2) Read this post on how to setup your IDE and create an OSGI component

Create Servlet


1) The first step is creating and deploying a servlet to read content page nodes from CRX. Here the servlet PageNodesServlet ( deployed as OSGI component ) accepts content path and returns the nodes in a json response. The pathfield widget treeloader requests this servlet for nodes and based on the input parameters ( path and type) returns all child nodes of a path or just the nodes for which user has modify permission

package apps.mycomponents.favorites;

import com.day.cq.commons.LabeledResource;
import com.day.text.Text;
import org.apache.commons.collections.Predicate;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

@SlingServlet (
    paths="/bin/mycomponents/favorites/pagenodes",
    methods = "GET",
    metatype = true,
    label = "Page Nodes Servlet"
)
public class PageNodesServlet extends SlingAllMethodsServlet {
    private static final long serialVersionUID = 1L;

    private static final Logger LOG = LoggerFactory.getLogger(PageNodesServlet.class);

    class FolderOrPagePredicate implements Predicate {
        @Override
        public boolean evaluate(Object o) {
            Resource resource = (Resource)o;
            return resource.getResourceType().equals("sling:Folder") || resource.getResourceType().equals("cq:Page");
        }
    }

    private final Predicate FOLDER_PAGE_PREDICATE = new FolderOrPagePredicate();

    @Override
    protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
                                throws ServletException, IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

        String path = request.getParameter("path");
        String type = request.getParameter("type");

        if( ( type == null ) || type.trim().equals("")){
            type = "all";
        }

        ResourceResolver resolver = request.getResourceResolver();
        Resource res = resolver.getResource(path);
        Session userSession = resolver.adaptTo(Session.class);

        List<Resource> children = new LinkedList<Resource>();
        JSONWriter jw = new JSONWriter(response.getWriter());

        try{
            for (Iterator iter = resolver.listChildren(res); iter.hasNext(); ) {
                Resource child = (Resource)iter.next();

                if(FOLDER_PAGE_PREDICATE.evaluate(child) && hasPermission(type, userSession, child.getPath())){
                    children.add(child);
                }
            }

            write(request, jw, children, type);
        }catch(Exception e){
            LOG.error("Error getting nodes",e);
            throw new ServletException(e);
        }
    }

    private boolean hasPermission(String type, Session userSession, String resourcePath) throws Exception{
        return "all".equals(type) || userSession.hasPermission(resourcePath, "set_property");
    }

    private boolean hasChildren(Resource res, ResourceResolver resolver, String type ) throws Exception{
        Session userSession = resolver.adaptTo(Session.class);
        Iterator<Resource> iter = resolver.listChildren(res);

        while (iter.hasNext()) {
            Resource child = iter.next();
            if (FOLDER_PAGE_PREDICATE.evaluate(child) && hasPermission(type, userSession, child.getPath())) {
                return true;
            }
        }

        return false;
    }

    public void write(SlingHttpServletRequest request, JSONWriter out, List<Resource> list, String type) throws Exception{
        ResourceResolver resolver = request.getResourceResolver();
        out.array();

        for (Resource resource : list) {
            out.object();

            LabeledResource lr = resource.adaptTo(LabeledResource.class);
            String name = Text.getName(resource.getPath());

            out.key("name").value(name);
            out.key("type").value(resource.getResourceType());

            boolean hasChildren = hasChildren(resource, resolver, type);

            out.key("cls").value(hasChildren ? "folder" : "file");

            if (!hasChildren) {
                out.key("leaf").value(true);
            }

            String text;

            if (lr == null) {
                text = name;
            } else {
                text = lr.getTitle() == null ? name : lr.getTitle();
            }

            out.key("text").value(text);
            out.endObject();
        }

        out.endArray();
    }
}

2) Install ( this post explains how-to ) the servlet; you should see folder /apps/favorites/install created in CRXDE Lite (http://localhost:4502/crx/de) with bundle jar

3) Access the servlet in browser with url http://localhost:4502/bin/mycomponents/favorites/pagenodes?path=/content and nodes under /content are returned in a json response

Create Component


1) Create the component (of type cq:Component) /apps/favorites/favorites



2) Create dialog /apps/favorites/favorites/dialog (of type cq:Dialog)


3) Create node /apps/favorites/favorites/clientlib ( of type cq:ClientLibraryFolder ) with following properties





4) Create file node /apps/favorites/favorites/clientlib/favorites.js and add the following code. Here we are creating a new ExtJS xtype for favorites field widget and registering it for use in multifield widget of dialog, which we are going to configure in the next steps..

var MyClientLib = {
    dataUrl: ''
};

MyClientLib.FavoritesField = CQ.Ext.extend(CQ.form.CompositeField, {
    favText: null,
    favPath: null,
    fav: null,

    constructor: function(config){
        config = config || {};
        var defaults = { "labelWidth" : 150, "layout" : "form", border: true,
            padding: "10px", width: 500, boxMaxWidth : 520 };
        config = CQ.Util.applyDefaults(config, defaults);
        MyClientLib.FavoritesField.superclass.constructor.call(this, config);
    },

    initComponent: function () {
        MyClientLib.FavoritesField.superclass.initComponent.call(this);

        this.fav = new CQ.Ext.form.Hidden({
            name: this.name
        });

        this.add(this.fav);

        this.add(new CQ.Ext.form.Label({
            text: "Display Text"
        }));

        this.favText = new CQ.Ext.form.TextField({
            width: 300,
            allowBlank: true
        });

        this.add(this.favText);

        this.add(new CQ.Ext.form.Label({
            text: "Select Path"
        }));

        var handlerFn = function(thisObj, type){
            var treePanel = thisObj.treePanel;
            var path = thisObj.path;
            thisObj.treeLoader.dataUrl = MyClientLib.dataUrl + "?type=" + type;
            thisObj.treeLoader.load(thisObj.treePanel.root, function(){
                treePanel.selectPath(path);
            });
        }

        var buttons = [ new CQ.Ext.Button( { text: "All", width: 68, tooltip: 'Show all tree nodes', handler: function(){ handlerFn(this,'all'); }} ),
            new CQ.Ext.Button( { text: "Modify", width: 95, tooltip: 'Show nodes with modify permission only', handler: function(){ handlerFn(this,'write'); } } ),
            CQ.Dialog.OK, CQ.Dialog.CANCEL
        ];

        this.favPath = new CQ.form.PathField({
            treeLoader: new CQ.Ext.tree.TreeLoader({
                dataUrl:MyClientLib.dataUrl,
                requestMethod: "GET"
            }),
            browseDialogCfg: { "buttons" : buttons},
            allowBlank: false,
            width: 300,
            listeners: {
                dialogclose: {
                    fn: function(f){
                        var selNode = this.browseDialog.treePanel.getSelectionModel().getSelectedNode();
                        this.ownerCt.favText.setValue(selNode.text);
                    }
                }
            }
        });

        this.add(this.favPath);

        var dialog = this.findParentByType('dialog');

        dialog.on('beforesubmit', function(){
            var value = this.getValue();

            if(value){
                this.fav.setValue(value);
            }
        },this)
    },

    getValue: function () {
        if(this.favPath.el && this.favPath.el.dom){
            var link = {
                "url" : this.favPath.getValue(),
                "text" : this.favText.getValue()
            };

            return JSON.stringify(link);
        }

        return null;
    },

    setValue: function (value) {
        var link = JSON.parse(value);
        this.favText.setValue(link.text);
        this.favPath.setValue(link.url);
        this.fav.setValue(value);
    }
});

CQ.Ext.reg("myfavoritesfield", MyClientLib.FavoritesField);

5) Create file node /apps/favorites/favorites/clientlib/js.txt and add

                  favorites.js

6) Set the title property of /apps/favorites/favorites/dialog/items/items/tab1 to Add

7) Create node /apps/favorites/favorites/dialog/items/items/tab1/items of type cq:WidgetCollection

8) Create node /apps/favorites/favorites/dialog/items/items/tab1/items/favfield of type cq:Widget



9) Create node /apps/favorites/favorites/dialog/items/items/tab1/items/favfield/fieldConfig of type cq:Widget with xtype myfavoritesfield

10) Add the following code in /apps/favorites/favorites/favorites.jsp

<%@ page import="com.day.cq.wcm.api.WCMMode" %>
<%@ page import="org.osgi.framework.FrameworkUtil" %>
<%@ page import="apps.mycomponents.favorites.PageNodesServlet" %>
<%@ page import="org.osgi.framework.Bundle" %>
<%@ page import="org.osgi.framework.ServiceReference" %>
<%@ page import="org.apache.sling.commons.json.JSONObject" %>
<%@ page import="java.io.PrintWriter" %>
<%@include file="/libs/foundation/global.jsp" %>
<%@page session="false" %>

<cq:includeClientLib categories="mycomponents.favorites"/>

<div style="display: block; border-style: solid; border-width: 1px; margin: 10px; padding: 10px">
    <b>Favorites Component</b>

    <%
        try {
            Property property = null;

            if(currentNode.hasProperty("favorites")){
                property = currentNode.getProperty("favorites");
            }

            if (property != null) {
                JSONObject obj = null;
                String resourcePath = null;
                Value[] favorites = null;

                if(property.isMultiple()){
                    favorites = property.getValues();
                }else{
                    favorites = new Value[1];
                    favorites[0] = property.getValue();
                }

                for (Value val : favorites) {
                    obj = new JSONObject(val.getString());
                    resourcePath = xssAPI.getValidHref(String.valueOf(obj.get("url")) + ".html");
    %>
                    <a href="<%=resourcePath%>"><%=obj.get("text")%></a>
                    <br><br>
    <%
                }
            } else {
    %>
                Configure the favorite pages in dialog
                <br><br>
    <%
            }
    %>

</div>

<%
        if (WCMMode.fromRequest(request) != WCMMode.DISABLED) {
            Bundle bundle = FrameworkUtil.getBundle(PageNodesServlet.class);
            ServiceReference[] services = bundle.getRegisteredServices();

            //assuming we have only one servlet as osgi service
            String sPath = String.valueOf(services[0].getProperty("sling.servlet.paths"));

%>
            <script type="text/javascript">
                CQ.Ext.onReady(function () {
                    MyClientLib.dataUrl = '<%=sPath%>';
                })
            </script>
<%
        }
    } catch (Exception e) {
        e.printStackTrace(new PrintWriter(out));
    }
%>


11) Save All and Done; add the component on a page and you should be able to bookmark site pages



2 comments:

  1. i pasted the same code in aem 6.0 , but it is not working

    ReplyDelete
    Replies
    1. In favorites.js, closing brace is duplicated as shown below.
      Just remove one closing brace there.

      this.add(new CQ.Ext.form.Label({
      text: "Display Text"
      }));

      Delete